How to Display Feature Collection GeoJson with Leaflet’s Marker Clusters

I enjoyed playing with Leaflet’s marker cluster plugin this weekend while writing a new demo, Demo 5, that shows how to load feature collection GeoJson into a Leaflet map layer.

Leaflet Marker ClusterIt was surprisingly easy to setup. First, I updated my GeoJson utility project to create feature collection GeoJson loaded with 768 of my runs from Garmin Connect. The features in the feature collection are summaries. They contain the same properties as the activity detail GeoJson from Demo 4, except that they use starting latitude/longitude instead of a polyline.   Next, I generated individual feature GeoJson files for each run with the full polyline. Finally, I added the new GeoJson files to my website at http://www.exploringspatial.com/activity/.

A Backbone Collection, Activities.js, uses AJAX to fetch the activity summaries. The collection is passed into the ActivitiesMapLayerView.js. The code in its render function is simple:

render: function() {
    var _self = this;
    geoJsonLayer = L.geoJson(this.collection.toJSON(),{
        onEachFeature: _self.onEachFeature
    });
    this.map.fitBounds(geojson.getBounds());
    this.activitiesLayer = L.markerClusterGroup();
    this.activitiesLayer.addLayer(geoJsonLayer);
    this.map.addLayer(this.activitiesLayer);
    this.map.on('popupopen', function(event) {_self.onPopupOpen(event);});
    $('.returnToSearch').on('click', '.returnTrigger', function(event){
        _self.onReturnToSearch(event)
      });
}
  1. L.geoJson – Create a map layer by passing in the activity collection JSON along with a onEachFeature function (see below).
  2. L.markerClusterGroup – Create a marker cluster map layer.
  3. addLayer – Add the GeoJson layer to the marker cluster group.
  4. addLayer – Add the marker cluster group to the map.

The last two lines of code bind a listener to the popupopen event, and add a listener for the activity detail page event that returns to the main page.

The onEachFeature function is used to bind a popup to each feature marker on the map. It does some date and number formatting and then passes HTML into layer.bindPopup.

onEachFeature: function(feature, layer) {
    var date = new Date(feature.properties.startTime);
    var triggerId = feature.properties.activityId;
    var msg = [];
    msg.push(feature.properties.name);
    msg.push('Start: ' + date.toLocaleDateString() + ' ' + date.toLocaleTimeString() + '');
    var dist = Math.round((feature.properties.totalMeters * 0.000621371)*100)/100;
    msg.push('Dist: ' + dist + ' mi');
    msg.push('Go to Activity');
    layer.bindPopup(msg.join(''), {maxWidth: 200});
},

The interesting bit happens in the onOpenActivity and renderActivity functions when a user clicks the “Go to Activity” anchor in the popup. The onOpenActivity function instantiates an Activity Backbone model and invokes its fetch function to go get the polyline for the selected activity id. The renderActivity function is passed in the success property of the AJAX call.

onOpenActivity: function(event, popup) {
    var location = popup._latlng;
    this.map.closePopup(popup);
    // Capture the current center and zoom to restore map later
    this.originalCenter = this.map.getCenter();
    this.originalZoom = this.map.getZoom();
    this.activity = new Activity({activityId: event.target.id});
    var _this = this;
    this.activity.fetch({
        success: function () {
        _this.renderActivity();
    }
});
        },

When the polyline is successfully returned from the server the renderActivity function creates a new activity map layer to swap with the marker cluster map layer. A feature GeoJson map layer is added to the map, along with the start and end markers.

renderActivity: function() {
    $('.returnToSearch').show();
    if (this.map.hasLayer(this.activitiesLayer)) {
        this.map.removeLayer(this.activitiesLayer);
    }
    var props = this.activity.get('properties');
    this.map.fitBounds([
        [props.get('minLat'), props.get('minLon')],
        [props.get('maxLat'), props.get('maxLon')]
    ]);
    var style = {
        color: '#FF0000',
        weight: 3,
        opacity: 0.6
    };
    this.activityLayer = L.geoJson(this.activity.toJSON(), 
        {style: style}).addTo(this.map);
    var polyline = this.activity.get('geometry').get('coordinates');
    var startPoint = polyline[0];
    var endPoint = polyline[polyline.length - 1];
    this.activityStart = L.marker([startPoint[1], startPoint[0]], 
        {icon: this.startIcon}).addTo(this.map);
    this.activityEnd = L.marker([endPoint[1], endPoint[0]], 
        {icon: this.endIcon}).addTo(this.map);
},

Once the user clicks Back to Search, this is the block of code that restores the marker cluster layer. The activity feature layer, start, and end markers are removed (not shown), and then the marker cluster layer is added back to the map and the original center and zoom are restored.

 this.map.addLayer(this.activitiesLayer);
 if (this.originalCenter != null && this.originalZoom != null) {
    this.map.setView(this.originalCenter, this.originalZoom, {animate: true});
    this.originalCenter = null;
    this.originalZoom = null;
 }

That’s it! Be sure to go to the demo, http://www.exploringspatial.com/#demo/5, and click around.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s