Difference between revisions of "Google Maps"

From Organic Design wiki
m (Marker clustering: pics)
m (Marker clustering)
Line 130: Line 130:
 
this.clusterer = new MarkerClusterer( this.map, markers, { gridSize: 30, maxZoom: 12 });
 
this.clusterer = new MarkerClusterer( this.map, markers, { gridSize: 30, maxZoom: 12 });
 
}</js>}}
 
}</js>}}
 +
  
 
{|
 
{|

Revision as of 17:14, 24 February 2012

This was job which I did at the beginning of 2012 for Trail WIKI, I'm documenting it here because it involved many different interesting aspects and also included some useful information which will help with later jobs.

The wiki contains many pages which represent hiking trails. It uses an infobox template so that various information about each trail such as its distance, elevation and other attributes can be contained in a structured way in the article.

On the main page is a Google map which has markers for all the trails, and when one is clicked a popup is revealed containing a link to the associated trail article.

The client wanted to have this map functionality extended such that the infobox that pops up is customised to the style of the site and contains some of the key information from the trail article's infobox. Also he wanted to use the Semantic Maps extension so that he could have other maps throughout the wiki which shows only very specific sets of markers such as trails with a certain region or above a certain elevation.

Another issue is that the wiki will eventually contain thousands of trails, so the maps need to load the trail location data after the page has loaded, and the popup boxes in the markers need to load their content on-demand too.

Semantically annotating the infobox template

AjaxMap - Pages using coordinates property.jpg     AjaxMap - semantic markers.jpg

Retrieving the location data via AJAX

AjaxMap - ajax result.jpg


<js>$.ajax({

type: 'GET', url: mw.util.wikiScript(), data: { action: 'traillocations' }, dataType: 'json', success: function( data ) { for( i in data ) { var pos = i.split(','); var marker = new google.maps.Marker({ position: new google.maps.LatLng(pos[0], pos[1]), icon: icon, map: map, titles: data[i] }); google.maps.event.addListener( marker, 'click', function() { new InfoBox(this); }); } } });</js>


{{{1}}}

Creating a custom popup box using the overlay method

I started with this script from the exampled in the Google documentation.

AjaxMap - custom-overlay.jpg

Populating the popup box with data via AJAX

AjaxMap - multiple-results-one-location.jpg

{{{1}}}


AjaxMap - icons and data.jpg

Selecting markers with SMW queries

AjaxMaps - query.jpg

This was achieved by allowing a query parameter to be added to the #ajaxmap parser-function that contains a SMW #ask query (or in fact any other kind of query that produces a list of title links as a result such as DPL). These article titles are then extracted out of the resulting list and included in the ajaxmap_opt array defined in the JavaScript for that map. The JavaScript Ajax request then includes these titles in its query parameter when it asks for the location information.

This is not perfect because the list of titles should not have to be sent to and from the client, it would be better for the Ajax request to send just an identifier that the PHP can use to perform the #ask query then rather than performing it when the parser-function was first expanded.

Finishing up phase one

AjaxMap - final result.jpg

Marker clustering

Many of the markers are too close together to be able to be separately clicked or moused-over, so these very dense areas of markers can now have named clusters defined for them. Each cluster is centred at a specific location, covers a specified radius and works of a specified zoom-level range. If there are few enough trails in a cluster, then clicking on it will bring up the trail information as usual, but if there are too many, then just the headings will be rendered in the infobox.

I looked at the Google Marker Clusterer, but found it to be not quite configurable enough for our needs. We want to be able to cluster just specific groups of markers and don't want general grid-based clustering.

The clustering has been added into the updateMarkers method which is called when filtering is changed, and is now also called in response to a zoom even by adding the following listener to the map:

<js>google.maps.event.addListener(opt.map, 'zoom_changed', function() {

window.updateMarkers.call(window.ajaxmap_opt[this.id],{}); });</js>


The cluster marker icons require a textual label that shows how many markers the cluster represents. I used the MarkerWithLabel class because it integrates with existing code nicely by sub-classing the standard Marker class. Here's how I used it in the clustering code:

{{{1}}}


Here's some images showing a test cluster at a few different zoom-levels:

Ajaxmap-cluster-2.jpg     Ajaxmap-cluster-1.jpg     Ajaxmap-cluster-3.jpg


Unfortunately I ran out of time to implement a custom clusterer and had to use the Google one:

{{{1}}}


Ajaxmap-google-clusterer-1.jpg     Ajaxmap-google-clusterer-2.jpg     Ajaxmap-google-clusterer-3.jpg

Filter form

A filter form has been added so that, if enabled, a form is available that allows users to tick or un-tick options such as "Dog friendly", or specify parameter ranges such distance or elevation, and the markers will be added/removed dynamically to match the query.

Ajaxmap-filter-form.jpg    
Ajaxmaps-filter-form-css.jpg
    Ajaxmap-filter-3.jpg

Location caching

Obtaining the location data for many hundreds of trails takes 10-15 seconds to return from the server. This is because the data for each trail needs to be extracted from the wikitext content of each trail article. So to speed this up, an SQL table was created to cache the location data. The getLocation method first checks if the requested trail has its location stored in the cache, and returns the data straight away if so. If not, it then calls the more expensive getTrailInfo method which reads the article and extracts the data from the wikitext content.

+------------+------------------+------+-----+---------+----------------+
| Field      | Type             | Null | Key | Default | Extra          |
+------------+------------------+------+-----+---------+----------------+
| twlc_id    | int(11) unsigned | NO   | PRI | NULL    | auto_increment |
| twlc_trail | int(11) unsigned | NO   |     | NULL    |                |
| twlc_lat   | double           | YES  |     | NULL    |                |
| twlc_lon   | double           | YES  |     | NULL    |                |
+------------+------------------+------+-----+---------+----------------+

The cache system has to tie into a couple of hooks so that the cache can be updated when trail information is created, deleted, or modified. The RevisionInsertComplete hook is used to detect if newly saved revisions have create a new trail or made any changes to the location of an existing trail. Both this hook and the ArticleDeleteComplete hook also do a general check to remove any location data from the cache that no longer has a corresponding trail article in the wiki. This could occur from articles being deleted or trail infobox templates being deleted or overwritten.

The trail location list now returns the data in a fraction of a second even for many hundreds of trails. It could still be improved more by making use of the modified HTTP headers to tell the client that it can locally cache the data and check the modified header to check if the entire data-set needs to be transferred.

Current TODO & Issues

  • Change the markers for clusters to custom markers having the number of trails shown
  • Disable clustering and zoom > 8
  • The filtering should be applied to the total trails before the clustering is applied because its less expensive
  • The filtering should be stored in the map-opts as current_filter rather than as a parameter in updateMarkers
  • Cluster should not be a cluster if all trails within are in exactly the same location

Later (maybe)

  • Store article lists from ask queries in DB so the lists aren't sent to and from the client
  • Store the icon paths in the JS so they're not sent to client
  • Image links can be made more effciient since they double-up the name and include a constant "140px"
  • Cache all trail data, not just location data
  • Use HTTP "modified" headers to reduce data-transfer of trail info
  • Another useful addition would be to allow this transform to separate out the trails that start at identical locations into a circle so they can be seen separately at a small enough scale.
  • The check for unused location data in the cache is inefficient - it should be done with one SQL query using a left join