Michal Zimmermann Pieces of knowledge from the world of GIS.

Articles in the web maps category

Introducing Blind Maps Project

Written on Nov 2, 2016 and marked as javascript, leaflet | web maps

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:

  • online at Blind maps with the map of your choice (if available)
  • offline, downloaded to your computer and filled with whatever data you want

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:

  • CONUS states
  • European states
  • World capitals

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:

  • one lets you find map features by their names
  • the other one lets you type name highlighted feature (much tougher)

Have fun!

PostGIS Case Study: Vozejkmap Open Data (Part III)

Written on Nov 14, 2015 and marked as postgresql, postgis, leaflet, javascript | web maps

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

Written on Apr 29, 2015 and marked as svg, smil | web maps

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:

  • countries polygon layer for basemap
  • airports point layer for 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:

  • Choose what attributes you want to have exported from source file with attributes key for every layer. They’ll be available as data- attribute of SVG elements.
  • It comes with Grid generation packed in! Really great. Sea generation works for some projections only.
  • Set the projections you want to use with additional settings.
  • bounds settings should - according to the docs - use layer extent as well, I couldn’t make it work though. Use [-180, -90, 180, 90] as a workaround to get the whole world. Don’t forget to set padding, so your map doesn’t get clipped on edges.
  • exporting coordinates rounded to one decimal place makes your SVG a lot smaller.

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

Written on Sep 12, 2014 and marked as javascript, openlayers, ogc | web maps

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

Written on Sep 10, 2014 and marked as ogc, wmts | web maps
  • Used to serve prepared rectangular tiles; this means you are limited by web server speed rather than map server speed
  • Several ways to retrieve tiles are defined: KVP and REST are mandatory, SOAP is optional
  • Does not allow layer combination; additional tile matrix would have to be created
  • GetCapabilities, GetTile and GetFeatureInfo requests are defined
  • tile
  • rectangular representation of space
  • defined by tile and row indices
  • tile matrix
  • set of tiles for a given scale
  • defined with:
    • tile size derived from standardized pixel size (0.28 × 0.28 mm)
    • tile width and tile height (px)
    • left upper corner coordinates
    • matrix width and height as number of tiles
  • tile matrix set
  • set of tile matrices for different scales

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

  • pixelSpan = scaleDenominator × 0.28 103 / metersPerUnit(crs);
  • tileSpanX = tileWidth × pixelSpan;
  • tileSpanY = tileHeight × pixelSpan;
  • tileMatrixMaxX = tileMatrixMinX + tileSpanX × matrixWidth;
  • tileMatrixMinY = tileMatrixMaxY - tileSpanY × matrixHeight;