Michal ZimmermannPieces of knowledge from the world of GIS.

Articles in the web maps category

Introducing Blind Maps Project

I’d like to introduce you to my little pet project, which might just as well be awarded the first pet project I’ve ever completed, called Blind maps.

It’s a very simple, yet useful web application built on top of the great Leaflet library meant to help you get to know our world a bit better. As the name suggests, the app shows you, well… a blind map, and you try to fill as many features as you can.

The app is ready and can be used:

What I find great about this project is the ease of adding new dataset. For starters, I filled it with data coming from Natural Earth:

If you wish, feel free to send me a pull request with GeoJSON data, I’ll be happy to have more datasets available! The process is described at the project homepage.

As you notice at the project homepage, there are two versions of the game available:

Have fun!

PostGIS Case Study: Vozejkmap Open Data (Part III)

After a while I got back to my PostGIS open data case study. Last time I left it with clustering implemented, looking forward to incorporate Turf.js in the future. And the future is now. The code is still available on GitHub.

Subgroup clustering

Vozejkmap data is categorized based on the place type (banks, parking lots, pubs, …). One of the core features of map showing such data should be the easy way to turn these categories on and off.

As far as I know, it’s not trivial to do this with the standard Leaflet library. Extending L.control.layers and implement its addOverlay, removeOverlay methods on your own might be the way to add needed behavior. Fortunately, there’s an easier option thanks to Leaflet.FeatureGroup.SubGroup that can handle such use case and is really straightforward. See the code below.

cluster = L.markerClusterGroup({
    chunkedLoading: true,
    chunkInterval: 500
});

cluster.addTo(map);

...

for (var category in categories) {
    // just use L.featureGroup.subGroup instead of L.layerGroup or L.featureGroup
    overlays[my.Style.set(category).type] = L.featureGroup.subGroup(cluster, categories[category]);
}

mapkey = L.control.layers(null, overlays).addTo(map);

With this piece of code you get a map key with checkboxes for all the categories, yet they’re still kept in the single cluster on the map. Brilliant!

Using Turf.js for analysis

Turf is one of those libraries I get amazed easily with, spending a week trying to find a use case, finally putting it aside with “I’ll get back to it later”. I usually don’t. This time it’s different.

I use Turf to get the nearest neighbor for any marker on click. My first try ended up with the same marker being the result as it was a member of a feature collection passed to turf.nearest() method. After snooping around the docs I found turf.remove() method that can filter GeoJSON based on key-value pair.

Another handy function is turf.distance() that gives you distance between two points. The code below adds an information about the nearest point and its distance into the popup.

// data is a geojson feature collection
json = L.geoJson(data, {
    onEachFeature: function(feature, layer) {
        layer.on("click", function(e) {
            var nearest = turf.nearest(layer.toGeoJSON(), turf.remove(data, "title", feature.properties.title)),
                distance = turf.distance(layer.toGeoJSON(), nearest, "kilometers").toPrecision(2),
                popup = L.popup({offset: [0, -35]}).setLatLng(e.latlng),
                content = L.Util.template(
                    "<h1>{title}</h1><p>{description}</p> \
                    <p>Nejbližší bod: {nearest} je {distance} km daleko.</p>", {
                    title: feature.properties.title,
                    description: feature.properties.description,
                    nearest: nearest.properties.title,
                    distance: distance
                });

            popup.setContent(content);
            popup.openOn(map);

            ...

From what I’ve tried so far, Turf seems to be incredibly fast and easy to use. I’ll try to find the nearest point for any of the categories, that could take Turf some time.

Update

Turf is blazing fast! I’ve implemented nearest point for each of the categories and it gets done in a blink of an eye. Some screenshots below. Geolocation implemented as well.

You can locate the point easily.

You can hide the infobox.

You can jump to any of the nearest places.

Animating SVG Maps With SMIL

Using SVG to build web maps have both pros and cons and to be honest I don’t know any serious map/GIS project built on top of SVG. However, as a part of my job at university, I was forced to use both SVG and SMIL to produce animated web map (see the small version below or the big one at GitHub) and I’d like to share my findings.

Data preprocessing

I chose Natural Earth dataset both for basemap and thematic layer:

I decided that animation should go like this:

  1. Load basemap and Vaclav Havel airport (PRG).
  2. Animate destinations one by one. They are revealed in order of their distance from PRG.
  3. Animate airways.
  4. Once airways are animated, animate airplanes along their path from PRG to their destination in order of their time of departure.
  5. Profit.

My goal was to create an animation of all departures from Vaclav Havel airport during one day. These data can be obtained at FlightStats, I didn’t find a way make this process automatic though. OpenFlights might be better source then.

SVG creation

Kartograph is a great tool both for SVG generation and scripting. What a pity it’s probably a dead project according to the last commit date. After installing Python part of library used to create SVG files out of vector geometries, it can be run with something like this:

kartograph --output map.svg --pretty-print --style style.css config.json

Pretty self-explanatory, let’s have a look at config file:

{
    "layers": {
        "countries": {
            "src": "ne_50m_admin_0_countries/ne_50m_admin_0_countries.shp",
            "attributes": ["name"]
        },
        "airports": {
            "src": "ne_10m_airports/ne_10m_airports_prg.shp",
            "attributes": ["name", "abbrev"]
        },
        "travels": {
            "src": "ne_10m_airports/travels.shp",
            "attributes": ["time", "distance"]
        },
        "grid": {
            "special": "graticule",
            "latitudes": 10,
            "longitudes": 10
        }
    },
    "proj": {
        "id": "satellite",
        "lon0": 0.0,
        "lat0": 48.0,
        "dist": 45,
        "up": 15
    },
    "bounds": {
        "mode": "bbox",
        "data": [-180, -90, 180, 90],
        "padding": 1
    },
    "export": {
        "round": 1,
        "width": 1000,
        "ratio": 1
    }
}

It is possible to adjust map settings in many different ways. The most important/interesting:

You can change SVG look with simple CSS, just be sure to use layer names as CSS ids:

#airports {
    fill: #CC0000;
    fill-opacity: 0;
    stroke: #660000;
    stroke-opacity: 0;
}

#countries {
    fill: #e6deb4;
    stroke: #a59f81;
}

#grid {
    stroke: #d0d0d0;
    stroke-width: .3px;
}

#travels {
    stroke: #1f78b4;
    stroke-opacity: 0;
    stroke-dasharray: 5,5;
}

Data adjustment & animation

SMIL is a XML based language for multimedia representation. It comes ready for timing, animation, visual transitions etc. I guess it might be considered easier to read for a web development beginner. Once you start using it, you immediately realize it suffers from the same disease like XML does: it is so wordy!

Let’s get back to my example. To animate airports one by one, let’s give them unique ids, so they look something like:

<circle id="brs" stroke-opacity="0" fill-opacity="0" cx="476.597304864" cy="539.487783171" data-abbrev="BRS" data-name="Bristol Int'l" r="3"/>

That’s something you do by hand as kartograph doesn’t give ids to SVG elements. Once you’re done with that, you can run SMIL animation. If you look closer at the final map, you’ll notice there are three properties animated for each airport: fill opacity, stroke opacity and radius. Each property needs to use separate SMIL <animate />, which might look like the one below:

<animate attributeName="fill-opacity"
    id="kos_ani_fo"
    from="0"
    to="1"
    begin="osr_ani.end"
    dur="0.25s"
    fill="freeze"
    xlink:href="#kos"
/>
<animate attributeName="stroke-opacity"
    id="kos_ani_so"
    from="0"
    to="1"
    begin="osr_ani.end"
    dur="0.25s"
    fill="freeze"
    xlink:href="#kos"
/>
<animate attributeName="r"
    id="kos_ani_r"
    from="10px"
    to="3px"
    begin="osr_ani.end"
    dur="0.25s"
    xlink:href="#kos"
/>

I guess you get the idea how long this would take for more airports. Make sure to notice that SMIL can start animation based on another animation’s end (osr_ani.end) - that’s pretty neat.

Airways animation works almost the same. First, add unique id to each airway:

<path d="M550.9,562.9L568.0,495.0 " id="travel-arn"/>

Second, start animation after all the airports are visible on the map. Notice the initial definition of d attribute - it’s a line with zero length.

<animate attributeName="d"
    id="path_ani"
    from="M550.9,562.9L550.9,562.9"
    to="M550.9,562.9L568.0,495.0"
    begin="icn_ani_r.end"
    dur="3s"
    xlink:href="#travel-arn"
/>

Once airways animation has finished, let airplanes fly around the globe with a simple JavaScript function:

/**
 * @param  number coef  scale radius by number of flights to the given destination
 * @param  string flight_id
 */
var circle = function(coef, flight_id, timeshift) {
    var svgns = "http://www.w3.org/2000/svg";
    var svgDocument =document;
    var motion = svgDocument.createElementNS(svgns,"animateMotion");
    var animation = svgDocument.createElementNS(svgns,"animate");
    var shape  = svgDocument.createElementNS(svgns, "circle");
    var time = 15 + timeshift;
    var dur = document.getElementById(flight_id).getAttributeNS(null, "data-dist")/100;
    motion.setAttribute("begin", time + "s");
    motion.setAttribute("dur", dur + "s");
    motion.setAttribute("path", document.getElementById(flight_id).getAttributeNS(null, "d"));
    motion.setAttribute("xlink:href", "#" + flight_id);
    motion.setAttribute("id", flight_id + "_motion");

    animation.setAttribute("attributeName", "opacity");
    animation.setAttribute("from", "1");
    animation.setAttribute("to", "0");
    animation.setAttribute("begin", time + dur + "s");
    animation.setAttribute("dur", "0.1s");
    animation.setAttribute("fill", "freeze");


    shape.setAttributeNS(null, "r",  1*coef);
    shape.setAttributeNS(null, "fill", "1f78b4");
    shape.setAttributeNS(null, "stroke", "1f78b4");
    shape.setAttribute("id", "airplane-" + flight_id);
    shape.appendChild(motion);
    shape.appendChild(animation);

    document.getElementById("airplanes").appendChild(shape);
}

SMIL with SVG seems to be interesting option for web map animation, a bit lengthy though. Syncing animations can easily become pain in the ass (see StackOverflow thread). Never call your function animate - there is namesake function defined in Web Animations API that makes animation crash in Chrome. <animateMotion /> is a great tool to animate elements along path.

Connecting To Secured ArcGIS Server Layer With OpenLayers 3

I was made to use ArcGIS Server with Openlayers 3 just recently as one of the projects I’ve been working on demands such different tools to work together.

tl;dr: I hate Esri.

I found myself in need to access secured layers published via WMS on ArcGIS Server using username and password I was given, so here’s a little how-to for anyone who would have to do the same.

Let’s start with a simple ol.layer.Image and pretend this is the secured layer we’re looking for:

var layer = new ol.layer.Image({
    extent: extent,
    source: new ol.source.ImageWMS(/** @type {olx.source.ImageWMSOptions} */ ({
        url: url,
        params: {
            'LAYERS': 'layer',
            'CRS': 'EPSG:3857',
        }
    }))
});

We need to retrieve the token, so we define a function:

function retrieveToken(callback) {
    var req = new XMLHttpRequest;

    req.onload = function() {
        if (req.status == "200") {
            var response = JSON.parse(req.responseText);
            if (response.contents) {
                callback(response.contents); // response contents is where the token is stored
        }
    };
    req.open("get", "http://server.address/arcgis/tokens/?request=getToken&username=username&password=password&expiration=60", true);
    req.send()
}

I pass a parameter called callback - that’s a very important step, otherwise you would not be able to retrieve the token when you actually need it (AJAX stands for asynchronous). Now you just pass the token to the layer params like this:

retrieveToken(function(token) {
    layer.getSource().updateParams({
        token: token
    })
}

When you open Firebug and inspect Network tab, you should find token URL parameter passed along with WMS GetMap request.

Few sidenotes:

  1. Although you might be logged in ArcGIS Server via web interface, you might need to pass the token URL param when trying to access Capabilities document. Don’t know why though.
  2. You should probably take care of calling the retrieveToken() in shorter interval than the token expiration is set to. Otherwise you might end up with invalid token.
  3. You need to hide the username and password from anonymous users (I guess that’s only possible with server side implementation of selective JavaScript loading).

WMTS: Few Things I Want To Remember

Total count of tile matrices

nTileMatrices × nTiledStyles × nTiledFormats (if no dimensions are defined)

Total count of tiles in a tile matrix

matrixWidth × matrixHeight

Other equations