[GIS] Leaflet – GPX – Clustering

clusteringgpxleaflet

I'm using Leaflet maps together with "Plugins by Pavel Shramov", which allows google usage and GPX file use.

This bit was easy enough, but trying now to use markerClusterer. I've spent a few hours playing around with things but complete stabs in the dark, and no joy yet.

Most of the examples I'm seeing are JSON, not GPX, and I don't think are really relevant?

My test page is up here: http://www.talesfromthesaddle.com/mapproject/map.html

If anyone knows of similar usage examples, please let me know so I can take a look. Or, if you want to head over to the test page and give me a few pointers, also welcome!

This is the GPX parser, which might provide some insight:

L.GPX = L.FeatureGroup.extend({
initialize: function(gpx, options) {
    L.Util.setOptions(this, options);
    this._gpx = gpx;
    this._layers = {};

    if (gpx) {
        this.addGPX(gpx, options, this.options.async);
    }
},

loadXML: function(url, cb, options, async) {
    if (async === undefined) async = this.options.async;
    if (options === undefined) options = this.options;

    var req = new window.XMLHttpRequest();
    req.open('GET', url, async);
    try {
        req.overrideMimeType('text/xml'); // unsupported by IE
    } catch(e) {}
    req.onreadystatechange = function() {
        if (req.readyState !== 4) return;
        if(req.status === 200) cb(req.responseXML, options);
    };
    req.send(null);
},

_humanLen: function(l) {
    if (l < 2000)
        return l.toFixed(0) + ' m';
    else
        return (l/1000).toFixed(1) + ' km';
},

_polylineLen: function(line)//line is a L.Polyline()
{
    var ll = line._latlngs;
    var d = 0, p = null;
    for (var i = 0; i < ll.length; i++)
    {
        if(i && p)
            d += p.distanceTo(ll[i]);
        p = ll[i];
    }
    return d;
},

addGPX: function(url, options, async) {
    var _this = this;
    var cb = function(gpx, options) { _this._addGPX(gpx, options); };
    this.loadXML(url, cb, options, async);
},

_addGPX: function(gpx, options) {
    var layers = this.parseGPX(gpx, options);
    if (!layers) return;
    this.addLayer(layers);
    this.fire('loaded');
},  

parseGPX: function(xml, options) {
    var j, i, el, layers = [];
    var named = false, tags = [['rte','rtept'], ['trkseg','trkpt']];

    for (j = 0; j < tags.length; j++) {
        el = xml.getElementsByTagName(tags[j][0]);
        for (i = 0; i < el.length; i++) {
            var l = this.parse_trkseg(el[i], xml, options, tags[j][1]);
            for (var k = 0; k < l.length; k++) {
                if (this.parse_name(el[i], l[k])) named = true;
                layers.push(l[k]);
            }
        }
    }

    el = xml.getElementsByTagName('wpt');
    if (options.display_wpt !== false) {
        for (i = 0; i < el.length; i++) {
            var marker = this.parse_wpt(el[i], xml, options);
            if (!marker) continue;
            if (this.parse_name(el[i], marker)) named = true;
            layers.push(marker);
        }
    }

    if (!layers.length) return;
    var layer = layers[0];
    if (layers.length > 1) 
        layer = new L.FeatureGroup(layers);
    if (!named) this.parse_name(xml, layer);
    return layer;
},

parse_name: function(xml, layer) {
    var i, el, txt='', name, descr='', len=0;
    el = xml.getElementsByTagName('name');
    if (el.length)
        name = el[0].childNodes[0].nodeValue;
    el = xml.getElementsByTagName('desc');
    for (i = 0; i < el.length; i++) {
        for (var j = 0; j < el[i].childNodes.length; j++)
            descr = descr + el[i].childNodes[j].nodeValue;
    }

    if(layer instanceof L.Path)
        len = this._polylineLen(layer);

    if (name) txt += '<h2>' + name + '</h2>' + descr;
    if (len) txt += '<p>' + this._humanLen(len) + '</p>';

    if (layer && layer._popup === undefined) layer.bindPopup(txt);
    return txt;
},

parse_trkseg: function(line, xml, options, tag) {
    var el = line.getElementsByTagName(tag);
    if (!el.length) return [];
    var coords = [];
    for (var i = 0; i < el.length; i++) {
        var ll = new L.LatLng(el[i].getAttribute('lat'),
                    el[i].getAttribute('lon'));
        ll.meta = {};
        for (var j in el[i].childNodes) {
            var e = el[i].childNodes[j];
            if (!e.tagName) continue;
            ll.meta[e.tagName] = e.textContent;
        }
        coords.push(ll);
    }
    var l = [new L.Polyline(coords, options)];
    this.fire('addline', {line:l});
    return l;
},

parse_wpt: function(e, xml, options) {
    var m = new L.Marker(new L.LatLng(e.getAttribute('lat'),
                    e.getAttribute('lon')), options);
    this.fire('addpoint', {point:m});
    return m;
}  });

Best Answer

GeoJSON or GPX doesn't really matter, you just need a way to construct a marker from each coordinate pair, and add it to a MarkerCluster group.

Here is the start of a solution; ugly as it is, it works (for clustering the hundreds of gpx points), and it should point you in the right direction to a cleaner solution.

  1. Add a max zoom to the map. Not sure why, but L.MarkerCluster complains if you don't

    var map = new L.Map('map', {center: new L.LatLng(45, 0), zoom: 2, maxZoom:12});

  2. Create a marker cluster group before you start parsing the gpx

    var markerClusters = new L.MarkerClusterGroup();

  3. Drop the on.('loaded') function from this line

    var track = new L.GPX("./map.gpx", {async: true});

  4. Don't add the 'track' to the map. Instead, get GPX.js to add the markers to the marker cluster group, by changing the parseWPT() function as shown below. (Basically, use the existing methods in the GPX.js plugin to construct markers from the lat and log attributes, and add them to the clusters)

    parse_wpt: function(e, xml, options) { var m = new L.Marker(new L.LatLng(e.getAttribute('lat'), e.getAttribute('lon')), options); markerClusters.addLayer(m); }

  5. Finally, add the markerCluster to the map

    map.addLayer(markerClusters)

Note, this is not adding your tracks to the map. If you need these, I suggest separating the points from the tracks in your gpx file, and working from there, with a second call to GPX.js.