www.trailforks.com is a community-powered mountain biking trail database currently focused on BC Canada. The main focus is trail gps data and Google maps/earth views. Trailforks has a high focus on promoting local riding associations and groups and aims to be a tool for them to use.

Background

Back in 2009 I created a simple Facebook app called "Ride Log" for tracking days ridden in the season. From that I got the idea of creating a database of trails and looked around and didn't find an existing website that I thought did a good job specific for mountain biking, especial in my region. I started working on the site in September 2011 in my spare time and finally launched it in Feb 2012.

I chose to build the site using Drupal mainly because the power of "Views". I had also been developing several other sites with Drupal in the past 2 years so I have become very familiarly with it. I chose Drupal 7 which seems pretty mature now with community module support. The last site I made in early 2011 I stuck with D6 because D7 still didn't have many modules I wanted to use, but there now is pretty much everything I need for D7.

Building the site

Drupal views is so powerful and easy to create so much of a website, especially lists of data. I love the power of views exposed filters, its something that I use a lot, example: http://www.trailforks.com/trails
I used a lot of other community modules to add features to the site. The core gps trail feature was built using a combination of my own custom code and hacking apart 2 existing Drupal6 modules "kml" & "trackfield".

GPS data is extracted from uploaded GPX or KML files and stored in the database as raw data that can then be used to derive stats from, create height graphs and create tracks for KML output.

The KML module is super useful for using the power of views to generate google kml files. I modified the module to add additional features like tracks, coloured tracks, different icons and styles and polygon shapes. On some pages I use gmap for simple pushpin maps, but most of the maps i create in the theme template files using google maps api and the kml files generated by views. Example: http://www.trailforks.com/riding_areas/mount-fromme

I have several of my own custom modules for custom theming, modifications to forms, usability mods, menus and such. One thing I always like to do with a custom module is override taxonomy term output to use views.

function central_term_page($tid) {
  $vocabulary = taxonomy_vocabulary_load($tid->vid);
  switch($vocabulary->machine_name) {
    case 'location':
      return custom_taxonomy_page($tid);
    case 'directory_type':
      return views_embed_view('directory', 'page_4', $tid->tid);
    case 'product_category':
      return views_embed_view('products', 'page_1', $tid->tid);
    case 'event_types':
      return views_embed_view('events', 'page_2', $tid->tid);
    case 'trail_type':
    case 'difficulty':
    case 'recommended_bike_type':
    case 'ttf':
    case 'physical_rating':
      return views_embed_view('trails', 'block_4', $tid->tid);
    default:
      // Returns the default term page provided by taxonomy module.
      module_load_include('inc', 'taxonomy', 'taxonomy.pages');
      return taxonomy_term_page($tid);
  }
}

The "trail reports" feature updates the status field on trails whenever a new trail report is added. Example overview of a regions trail status: http://www.trailforks.com/canada/north-shore/status I utilized association's a lot to minimize the amount of data a user has to input on forms.

The site has a hierarchy of content, locations/regions are taxonomy then a "riding area" node is at the bottom level of the taxonomy. Trails are then associated with a riding area. Photos, videos, trail reports and so on are then associated with a trail. Using this string of associations I can show any of the content at any level of the hierarchy. Riding areas show the total number of trails associated with it separated by trail difficulty, i use the "computed field" module for this. http://www.trailforks.com/riding_areas

db_query("DELETE FROM {field_data_field_count_difficulty} WHERE entity_id=':d'", array(':d' => $entity->nid));
$terms_array = array();
$terms = db_query("SELECT t.tid FROM {field_data_field_riding_area} f LEFT JOIN {taxonomy_index} t ON f.entity_id=t.nid LEFT JOIN {taxonomy_term_data} tt ON t.tid=tt.tid WHERE f.field_riding_area_nid=:d AND f.bundle='trail' AND tt.vid='5' GROUP BY f.entity_id", array(':d' => $entity->nid))->fetchAll();
foreach ($terms as $item) {
	$terms_array[] = $item->tid;
}
$count = array_count_values($terms_array);
ksort($count);

$c = 0;
foreach ($count as $key => $value) {
	switch ($key) {
		case '10': $image = 'double_black.gif'; break;
		case '11': $image = 'black.gif'; break;
		case '12': $image = 'blue.gif'; break;
		case '13': $image = 'green.gif'; break;
                case '93': $image = ''; break;
	}
	if (!empty($image)) $entity_field[$c]['value'] = '<span class="diffcount"><img src="'.url('sites/default/files/'.$image).'" />'.$value.'</span>';
	$c++;
}

For the Google earth maps I had a hardtime centering the camera on the area and having it tilt by default. So riding areas have a field called "polygon" where I input the coordinates of a polygon shape outlining the entire riding area that I create using Google Earth program. Then in my custom module I use a bit of code to find the centre of the polygon and use that point to centre the camera on and tilt from.
Here is my code for finding centre of a polygon, its not perfect, but its approximate enough for this need.

function polyCenter($polygon) {
  $p = array_chunk($polygon,2);
  $n = count($p);
  for($i=0;$i<$n;$i++) {
    $x+=$p[$i][0];
    $y+=$p[$i][1];
  }
  return array($x/$n,$y/$n);
}

I used the "framework" theme as the base for the site and have template override pages for every node type and much more. user profile page screenshot: https://fbcdn-sphotos-a.akamaihd.net/hphotos-ak-snc7/409225_298875186837...

I originally was using megamenu module for the sites main navigation, but I found it very slow and hard to customize. So I opted to not use a module and instead hard coded the menu in the template file and used my own jquery & css. This is not ideal for others to easily manage the menu using the Drupal admin GUI, but its way faster to render and I don't mind having to edit a template file to update the menu. I think this is a practice I will use on other sites that arn't being handed off to a client.

I use the "userpoints" module to give users credit for contributing content and I created a bunch of custom achievements using the achievements module API. The activity module is used to monitor users on the site. I also created a bunch of other views to further monitor site activity, Example the vote log: http://www.trailforks.com/tracker/votes/log

I use the "services" module to create an API for Trailforks. I have used this API to integrate trail reports into my RideLog Facebook app. I also created some standalone embeddable widgets that don't call Drupal bootstrap but instead use the API http://www.trailforks.com/widget

I use the calendar, date & signup modules for events & group rides systems. I want to be able to import ical feeds to auto create events, but the current Drupal7 version of the "Feeds" ical parser is broken.

I run the site on Windows Server 2008 with IIS7, so I use the PHP extension called wincache to help performance. I also use a memcache server and Boost with IIS web.config rules.

Community Modules Used

  • achievements
  • acl
  • activity
  • addanother
  • addthis
  • adsense
  • advanced_forum
  • auto_nodetitle
  • better_exposed_filters
  • better_formats
  • block_class
  • boost
  • calendar
  • calendar_ical
  • colorbox
  • computed_field
  • ctools
  • custom_breadcrumbs
  • date
  • diff
  • entitycache
  • exif
  • fast_404
  • feeds
  • filefield_paths
  • fivestar
  • flag
  • gmap
  • googleanalytics
  • hierarchical_select
  • inactive_user
  • insert
  • location
  • mailchimp
  • memcache
  • metatag
  • mollom
  • nodereference_count
  • nodereference_url
  • node_reference
  • opengraph_meta
  • openx
  • page_title
  • parser_ical
  • pathauto
  • performance_hacks
  • prepopulate
  • privatemsg
  • rest_server
  • revisioning
  • rpx
  • rules
  • services
  • services_oauth
  • signup
  • subscriptions
  • taxonomy_breadcrumb
  • taxonomy_menu
  • tipsy
  • token
  • token_tweaks
  • uniqueness
  • userpoints
  • userpoints_rules
  • user_dashboard
  • user_reference
  • video_embed_field
  • views
  • views_slideshow
  • views_slideshow_cycle
  • views_tooltip
  • votingapi
  • votingpoints
  • voting_rules
  • weather_google
  • xcal_format
  • xmlrpc_server
  • xmlsitemap

Please feel free to post any questions, critiques or suggestions!

Comments

hostmaker’s picture

Nice website.

shamio’s picture

This website looks very nice. I really liked the theme of this website and also the menus are so nice. The images near menus made it very nice. I like to have such theme for my Drupal site.

memmons’s picture

I saw in a different post that you had experimented with Output caching as an alternative to Boost. I notice Boost is in your list of Modules so I am assuming you prefer that. Were you unable to get Output caching to work the way you wanted to? If you did but just prefer Boost, would you mind sharing your configurations and Keys that you used for caching. This is a fantastic site by the way, it is super fast and is well designed.

-Mike

Canadaka’s picture

thanks for your comments.

I could never get IIS Output Caching working with authenticated users, so I mostly use Boost. But I also can't get full boost support working with IIS URL Rewrite, so I only boost HTML files. I figure IIS is already doing static cache on js and css files anyway.

Here is the IIS Rewrite rule I use for Boost.

<rule name="Boost html" enabled="true" stopProcessing="true">
    <match url=".*" ignoreCase="false" />
    <conditions logicalGrouping="MatchAll" trackAllCaptures="false">
        <add input="{REQUEST_METHOD}" pattern="^GET$" ignoreCase="false" />
        <add input="{URL}" pattern="(^(admin|cache|misc|modules|sites|system|themes|node/add))|(/(comment/reply|edit|user|user/(login|password|register))$)" ignoreCase="false" negate="true" />
        <add input="{HTTP_COOKIE}" pattern="DRUPAL_UID" ignoreCase="false" negate="true" />
        <add input="{HTTPS}" pattern="on" ignoreCase="false" negate="true" />
        <add input="{DOCUMENT_ROOT}/cache/normal/{SERVER_NAME}{URL}_{QUERY_STRING}.html" matchType="IsFile" />
    </conditions>
    <serverVariables>
        <set name="CONTENT_TYPE" value="text/html" />
    </serverVariables>
    <action type="Rewrite" url="cache/normal/{SERVER_NAME}{URL}_{QUERY_STRING}.html" />
</rule>

I also now use Authcache for authenticated users and I use memcache instead of the Drupal db cache. I also run "Wincache" for PHP on Windows, which offers a big performance boost, like all opcode caches do. I use built 1.2.1209.0

memmons’s picture

Thanks, I was having the same issue, hope this feature can be used someday. Thanks for sending me your rewrite rule configuration. I just got memcache working today and there is a noticeable improvement but the first GET, the html file is still taking around 1 second. Did you have to do any additional configuration for Wincache or are you just including the extension in your ini file?

Good to see others are using Windows and IIS. Are you using MS SQL as well?

Canadaka’s picture

No I use MySQL. I also use Views caching a lot along with the 'Entity cache' module and a CDN for images, js & css. Wincache defiantly has the biggest impact. Memcache doesn't actually have a huge impact unless the site is busy. I have 2 fairly fast dedicated servers and the database is run on an SSD.

memmons’s picture

Entity cache gives a huge improvement and I did notice a significant improvement when adding memcache. CDN has been working well for us and we have all images and CSS files on 4 parallel servers hosted by Akamai. Aside from tweaking the far future expiration tags I think we are optimizing the front end as much as possible. We have 3 dedicated servers and we are throwing in a fourth. Our MSSQL database is also on a dedicated machine and only hits about 20-30% during our tests. Originally we were using APC and Winchache, but after doing more research I have pulled APC from all our other environments. Do you know if APC would add any additional performance boost or would it just conflict with Wincache? I did not pull APC out of production yet but it does not seem to be working anyway. I am wondering if there is some kind of file permission issue and is causing a conflict with Wincache.

I wish we could use some kind of reverse proxy or something like boost but all our data has to be real time and ESI is not something we want to pay for with Akamia.

Do you have any tips regarding tuning PHP on windows. Does the amount of memory make a difference and things like that?

memmons’s picture

Finally got our site up, you can see where some performance improvements can be made. I am going to make my own post with all the specifics sometime next week:

http://www.elearners.com/

Canadaka’s picture

APC & Wincache essentially do the same thing, so running both is counter productive. APC doens't work with PHP-NTS using FastCGI on IIS anyway. I think Wincache offers some additional things APC doesn't as well.

Generally you want to set the PHP memory_limit as low as your site can handle, that way if you get lots of traffic you arn't using a ton of memory for each php process. But Drupal requires a fair bit of memory, especially if your clearing caches and use image styles. So you want to make sure you have enough for that. I use 96MB on my setup.

If you're using IIS and fastCGi you will want these settings in your php.ini
cgi.force_redirect=0
fastcgi.impersonate=1
fastcgi.logging = 0
cgi.fix_pathinfo = 1

If you are using IIS you can play around with changing the amount of "max instances" for fastCGI along with the number of "web gardens" for the sites application pool. Depending on your traffic levels changing these can greating increase your pages/sec if you have the resources. For a low traffic site changing these values can sometimes lower your performance, so best to do some testing and research. IIS 7.5 in Win 2008 R2 has a new setting for fastcgi "max instances" that allows you to set the value to "0" in which IIS will auto-tune the number of php processes it thinks is optimal.