Saturday, July 21, 2012

MapFish Print and Leaflet, Day 2

Today I expand on yesterday's post about MapFish Print and Leaflet, to include vector layers. MFP's documentation doesn't even mention that this exists; there is a brief mention in a MFP commit note, but zero documentation. Fortunately, the MapFish Print demo app uses OpenLayers, and I have Firebug, so analyzing the output was possible. Took me all day to get it right, but possible.

To add a vector feature, you expand on the "layers" parameter in the client spec. You saw yesterday that I constructed "layers" as a list, adding items one by one depending on whether the layer is enabled. Same idea here, except that instead of sending a WMS layer spec we're sending a spec for a Vector layer and then for a GeoJSON feature within that Vector layer.

The demo generates output like this:

    {"type":"Vector",
        "styles":{
            "1":{"externalGraphic":"http://openlayers.org/dev/img/marker-blue.png","strokeColor":"red","fillColor":"red","fillOpacity":0.7,"strokeWidth":2,"pointRadius":12}
        },
        "styleProperty":"_gx_style",
        "geoJson":{"type":"FeatureCollection",
        "features":[
            {"type":"Feature","id":"OpenLayers.Feature.Vector_52","properties":{"_gx_style":1},"geometry":{"type":"Polygon","coordinates":[[[15,47],[16,48],[14,49],[15,47]]]}},
            {"type":"Feature","id":"OpenLayers.Feature.Vector_61","properties":{"_gx_style":1},"geometry":{"type":"LineString","coordinates":[[15,48],[16,47],[17,46]]}},
            {"type":"Feature","id":"OpenLayers.Feature.Vector_64","properties":{"_gx_style":1},"geometry":{"type":"Point","coordinates":[16,46]}}]}
        ],
        "name":"vector","opacity":1}
    }
 I'm going to go slightly differently, though, and have each vector feature in its own Layer,since we're not sure that each feature exists. Trust me, it'll be fine.

POINT MARKER

In our case, it's a single marker that may exist. For your map, you may have multiple markers so you may need to write this as a loop rather than a single "if" But still, this is the real meat of how to get a marker into your map.
    // MARKER_TARGET is a L.Marker, ICON_TARGET is a L.Icon subclass used by the marker 
    // wgsToGoogle() was posted yesterday; it converts a LatLng in WGS84 to a [x,y] in Web Mercator

    if (MARKER_TARGET && MAP.hasLayer(MARKER_TARGET) ) {
        var iconurl  = ICON_TARGET.prototype.iconUrl;
        var projdot  = wgsToGoogle(MARKER_TARGET.getLatLng());
        var iconxoff = -10; // offset to place the marker; MFP drifts it for some reason
        var iconyoff = 0; // offset to place the marker; MFP drifts it for some reason
        var iconsize = 15; // the scaling factor for the icon; determined to taste through trial and error

        // all of this is required: styleProperty and properties form the link to a style index, fillOpacity really works
        layers[layers.length] = {
            type:"Vector", name:"Target Marker", opacity:1.0,
            styleProperty: "style_index",
            styles:{
                "default":{ externalGraphic:iconurl, fillOpacity:1.0, pointRadius:iconsize, graphicXOffset: iconxoff, graphicYOffset: iconyoff }
            },
            geoJson:{
                type:"FeatureCollection",
                features:[
                    { type:"Feature", properties:{ style_index:"default" }, geometry:{ type:"Point", coordinates:projdot } }
                ]
            }
        };
    }
Some of this stuff is familiar to OpenLayers folks.
 
You'll see that the "styles" supplies a single style ("default"), that this is referenced by the "properties.style_index", and that "style_index" is made special by it being the styleProperty. Note that the iconurl must be a full path including http://server.com/ since MFP won't know the relative path to the icon.
 
The Point geometry is defined within the Feature; the format is a single coordinate pair [x,y] and recall that we're using Web Mercator. As such, the returned array from wgsToGoogle() is perfect for this situation.


LINESTRING AND MULTILINESTRING

    // DIRECTIONS_LINE is either a L.Polyline or a L.MultiPolyline, so we support both
    // DIRECTIONS_LINE_STYLE is an {object} of L.Path options, e.g. color and opacity
    // we use wgsToGoogle() on each vertex so the resulting geometry is in the right SRS
    if (DIRECTIONS_LINE && MAP.hasLayer(DIRECTIONS_LINE) ) {
        // the directions line, using either a single Polyline (not Google driving directions) or a MultiPolyline (Google)
        // Construct a list-of-lists multilinestring. Remember that OpenLayers and MFP do lng,lat instead of lat,lng
        var vertices = [];
        if (DIRECTIONS_LINE.getLatLngs) {
            // a single Polyline
            // collect the coordinates into a list, then make that list the only list within "vertices" (a multilinestring with 1 linestring component)
            var vx = DIRECTIONS_LINE.getLatLngs();
            for (var i=0, l=vx.length; i<l; i++) {
                vertices[vertices.length] = wgsToGoogle([ vx[i].lng, vx[i].lat ]);
            }
            vertices = [ vertices ];
        } else {
            // a MultiPolyline
            // use non-API methods to iterate over the line components, collecting them into "vertices" to form a list of lists
            for (var li in DIRECTIONS_LINE._layers) {
                var subline = DIRECTIONS_LINE._layers[li];
                var subverts = [];
                for (var i=0, l=subline._latlngs.length; i<l; i++) {
                    subverts[subverts.length] = wgsToGoogle([ subline._latlngs[i].lng, subline._latlngs[i].lat ]);
                }
                vertices[vertices.length] = subverts;
            }
        }

        // the styling is simply pulled from the styling constant
        var opacity = DIRECTIONS_LINE_STYLE.opacity;
        var color   = DIRECTIONS_LINE_STYLE.color;
        var weight  = 3;

        layers[layers.length] = {
            type:"Vector", name:"Directions Line", opacity:opacity,
            styles:{
                "default":{ strokeColor:color, strokeWidth:weight, strokeLinecap:"round" }
            },
            styleProperty:"style_index",
            geoJson:{
                type: "FeatureCollection",
                features:[
                    { type:"Feature", properties:{"style_index":"default"}, geometry:{ type:"MultiLineString", coordinates:vertices } }
                ]
            }
        };
    }
Not a lot new to say about this one that wasn't said about the point. Again, similar to OpenLayers styles. The vertices are in [x,y] per OGC spec, and are in Web Mercator.


POLYGONS

This project doesn't have any vector polygons to print, so it didn't come up.


CONCLUSION

All told, this finally got printing working! We have base maps, overlays, markers, and lines all drawing into the PDF, and the map isn't unduly distorted. Success!


No comments:

Post a Comment

Note: Only a member of this blog may post a comment.