Thursday, June 21, 2012

This is gonna be three in a row where I complain about Leaflet. Honestly, I'm not being a jerk: my biggest project right now is based on Leaflet, so it's their turn to host the latest raves and rants. Don't get too annoyed: it's constructive criticism, and by September it'll probably be some other framework that gets the "how I figured this out despite the docs" and "what I had to fix today" posts.

SCALEBAR

Leaflet 3 lacks a scalebar control. This is really too bad: we're showing a map of parks, and people need to know whether that pavilion is 500 feet or 1.5 miles.

UPDATE: This is now posted to Leaflet https://github.com/CloudMade/Leaflet/issues/823
I would be interested to hear whether this is something worth improving; the current customer is happy with it as-is.

UPDATED UPDATE: Apparently this is now in the Master version of Leaflet 4. Hooray for the late adopters, too bad for us early adopters, eh?

WKT PARSER

Leaflet also lacks a parser to convert OCG Well Known Text (WKT) into a Marker, a Polyline, et cetera. I have written the beginnings of one. Again, being centric to this project its development is 100% driven by the needs of this project. At this time it only supports LINESTRING and MULTILINESTRING geometries, converting them to L.Polyline and L.MultiPolyline

Here's the program code thus far. As usual, use as your own risk, no warranty of your computer not catching fire, and so forth.

/*
 * parse Well Known Text (WKT) and return a PolyLine, MultiPolyline, et cetera
 * params:
 * - wkt, String WKT to be parsed
 * - options, object to be passed to the new feature as it is constructed, e.g. fillColor, strokeWidth
 * returns:
 * - an instance of a L.Path subclass, e.g. L.Polyline
 *
 * The supported options depends on the type of feature found   http://leaflet.cloudmade.com/reference.html#path
 */
L.WKTtoFeature = function (wkt,options) {
    // really, this is a wrapper to the WKTtoFeature.parse* functions
    wkt = wkt.replace(/^\s*/g,'').replace(/\s*$/,'');
    if (wkt.indexOf('LINESTRING') == 0)      return L.WKTtoFeature.parseLinestring(wkt,options);
    if (wkt.indexOf('MULTILINESTRING') == 0) return L.WKTtoFeature.parseMultiLinestring(wkt,options);
};


L.WKTtoFeature.parseLinestring = function (wkt,options) {
    // split on , to get vertices. handle possible spaces after commas
    var verts = wkt.replace(/^LINESTRING\s*\(/, '').replace(/\)$/, '').split(/,\s*/);

    // collect vertices into a line
    var line = [];
    for (var vi=0, vl=verts.length; vi<vl; vi++) {
        var lng = parseFloat( verts[vi].split(" ")[0] );
        var lat = parseFloat( verts[vi].split(" ")[1] );
        line[line.length] = new L.LatLng(lat,lng);
    }

    // all set, return the Polyline with the user-supplied options/style
    var feature = new L.Polyline(line, options);
    return feature;
}


L.WKTtoFeature.parseMultiLinestring = function (wkt,options) {
    // some text fixes
    wkt = wkt.replace(/^MULTILINESTRING\s*\(\(/, '').replace(/\)\)$/, '');

    // split by () content to get linestrings, split linestrings by commas to get vertices
    var multiline = [];
    var getLineStrings = /\((.+?)\)/g;
    var getVerts = /,\s*/g;
    while (lsmatch = getLineStrings.exec(wkt)) {
        var line = [];
        var verts = lsmatch[1].split(getVerts);
        for (var i=0; i<verts.length; i++) {
            var lng = parseFloat( verts[i].split(" ")[0] );
            var lat = parseFloat( verts[i].split(" ")[1] );
            line[line.length] = new L.LatLng(lat,lng);
        }
        multiline[multiline.length] = line;
    }

    // all set, return the MultiPolyline with the user-supplied options/style
    var feature = new L.MultiPolyline(multiline, options);
    return feature;
}

Wednesday, June 6, 2012

Over the last few days I've been working with Leaflet. It's a lightweight framework for making web-based maps, it's a quick download, and it has the minimum features for making minimal maps. My first issues came almost immediately, when a client wanted the (apparently) unusual and bizarre functionality of being able to turn off layers and turn them on again, without the new layer lying on top of the stacking order and obscuring the stuff that's now beneath it.

Today's gripes about Leaflet, are about L.TileLayer.WMS This class allows you to load tiles from a WMS, as opposed to the WMS-C or TMS supported by L.TileLayer. But it's lacking more functionality once you move beyond the "It loads, send my my check. Next!" degree of complexity.

In this case, the bug reports and my patches at github, speak for themselves. These add missing methods and functionality to WMS layers: the equivalent of mergeNewParams(), a stacking order fix (such as stacking order goes in Leaflet), and the use of {s} subdomain substitutions.

https://github.com/CloudMade/Leaflet/issues/718

https://github.com/CloudMade/Leaflet/issues/735

https://github.com/CloudMade/Leaflet/issues/719

A "more mature" framework such as OpenLayers already has these bugs and issues corrected, but I'm sure that at one time it was this buggy too. Leaflet is working out well in general, though: despite these missing components, it's easier to use than OL for the more minimal stuff such as panning and centering, drawing markers, etc.

Monday, June 4, 2012

Leaflet is a framework for making online maps. Its design follows that of Google Maps API, which is sometimes a good thing (all coordinates are LatLng, reprojection is automatic) and sometimes not so great (many advanced features are lacking).

Leaflet is meant to be lightweight: the library is under 30 KB in size, which is great when it's only one of 200 files being downloaded as your site loads. But it also has a cost: features are lacking and some missing features are effectively bugs.

I'm working on our first significantly-complex program based on Leaflet. It's significantly more complex than "center an OSM map on my city, allow pan and zoom" So I'll focus on the individual bugs and the fixes I had to produce for them. These are for Leaflet 3, since at the time the project started Leaflet 4 was not yet considered in a stable condition, and a lot of them are The Way Google Does It so it's natural that they'd fall into some of the same issues as I have with Google Maps API.

NO GOOGLE MAPS

Leaflet doesn't support Google Maps, Bing Maps, nor any other commercial tile provider. There are community plugins to add L.TileLayer.Google and L.TileLayer.Bing However, the Google one simply doesn't work in IE at all. The Bing one works, though.

https://gist.github.com/1286658

LAYER STACKING / TOGGLING VISIBILITY / LAYER IDS

Layers do not support an explicit stacking order. The order in which they are added to the map, is the order in which they will be drawn, so be sure to add your basemap first, then the parks, then the trails in the parks, then the buildings which will block the parks.

However, layers also don't support having their visibility toggled. To turn off a layer, remove it from the map with map.removeLayer() But, turning the layer on again with map.addLayer() messes with your layer stacking: the park boundaries now appear over top of the buildings and trails, obscuring them completely.

The obvious "easy way out" is to not turn the layer off, but instead set its opacity to 0. This has two issues, though: a) the tiles are still being downloaded and sucking up bandwidth and time, and b) I had all sorts of strange bugs with the layer still appearing partially, pieces of tiles appearing over top of the other layers.

So, in the end, the only realistic way to go on, is to write your own function to toggle the visibility of a single layer. But wait, there's more: Leaflet doesn't give layers a unique ID that you can use to identify a layer, nor a getLayerByName() sort of function, nor does it have a method for getting the current visibility status, which isn't surprising since it never occured to them that someone may want to turn off a layer. So before you can toggle a layer's visibility (e.g. from a checkbox that has the layer's name as its value), you need to be able to identify the layer and also store its visibility state.

The finished code looks like this. The overlay maps are stored in a list (not a dictionary / assocarray, that doesn't retain its order) so we can add them in a reliable stacking sequence. I add the extra options "id" so we can uniquely identify a layer, and "visibility" so the visibility can be fetched and saved.

var PHOTOBASE = new L.TileLayer.WMS("http://server.com/wms", { layers:'aerial_2011', format:'image/jpeg', reuseTiles:true, subdomains:SUBDOMAINS });
var MAPBASE   = new L.TileLayer.WMS("http://server.com/wms", { layers:'topomap', format:'image/jpeg', reuseTiles:true, subdomains:SUBDOMAINS });
var OVERLAYS  = [];
OVERLAYS[OVERLAYS.length] = new L.TileLayer.WMS("http://server.com/wms", { id:'parks', visibility:true, layers:'parkbounds2012', format:'image/png', transparent:'TRUE', reuseTiles:true  });
OVERLAYS[OVERLAYS.length] = new L.TileLayer.WMS("http://server.com/wms", { id:'trails', visibility:true, layers:'trails_hike,trails_equestrian', format:'image/png', transparent:'TRUE', reuseTiles:true  });
OVERLAYS[OVERLAYS.length] = new L.TileLayer.WMS("http://server.com/wms", { id:'buildings', visibility:true, layers:'buildings_solid', format:'image/png', transparent:'TRUE', reuseTiles:true  });
function toggleLayer(which,status) {
    // go over the layers...
    for (var i=0, l=OVERLAYS.length; i<l; i++) {
        var layer = OVERLAYS[i];
        // if this is the matching layer, toggle its visiblity
        if (layer.options.id == which) {
            layer.options.visibility = status;
        }

        // regardless of whether this is the desired layer, remove it and re-add it so we can maintain the stacking order
        MAP.removeLayer(layer);
        if (layer.options.visibility) MAP.addLayer(layer);
    }
} 
 
function selectBasemap(which) {
    switch (which) {
        case 'photo':
            MAP.removeLayer(MAPBASE);
            MAP.removeLayer(PHOTOBASE);
            MAP.addLayer(PHOTOBASE,true);
            break;
        case 'map':
            MAP.removeLayer(MAPBASE);
            MAP.removeLayer(PHOTOBASE);
            MAP.addLayer(MAPBASE,true);
            break;
    }
}

Now that we have basic layer toggling, time to get cracking on programming my map app!