Nautilytics was recently approached by WalkJogRun, one of the leaders in iOS GPS running applications, to create a visualization of the most popular public running routes in their global network. For initial inspiration on this project we revisited our 2013 running route density collaboration with WalkJogRun, which had the amazing effect of providing an accurate outline of the US and Europe with only the starting GPS point of each route.

WalkJogRun_Route_Density_2013

This visualization was great as a starting point, but we wanted to make it more useful and interactive for the entire WalkJogRun community. While we were brainstorming possible approaches to this project, we ran across a great post by Nathan Yau over at Flowing Data showing static images of running routes. Given the three plus years (125,000+ GPS tracked routes and counting) of public running routes, we knew for the visualization to give back to the running community it had to be dynamic and available to runners worldwide. The result of this project gives users of the application a better understanding of where people run and the most popular routes in a new city. In theory, if many runners are running the same route then it may be safer, more scenic, and less polluted.

Need some help with a project like this? Check out our source code or contact us for mapping consulting.

Examples

WalkJogRun App
Nautilytics Sample Web Application

Live Web Application for Interacting with over 125,000 Public GPS routes from WalkJogRun:

Tile Server

The approach we took to creating the tile server was to spin up a new small Ubuntu LTS virtual machine using Windows Azure – we chose Azure because of their great BizSpark program for startups.

After some deliberation and googling, we decided our best technology stack for this project was the PostGIS extension of PostgreSQL, node.js, and Mapnik. node.js and PostGIS were easy to get up and running correctly; Mapnik on the other hand was a bit trickier, so we’ll outline the steps.

Mapnik Installation

sudo apt-get install -y python-software-properties
sudo add-apt-repository ppa:mapnik/nightly-2.3
sudo apt-get update
sudo apt-get install libmapnik libmapnik-dev mapnik-utils python-mapnik
sudo apt-get install mapnik-input-plugin-postgis

To ensure Mapnik installed correctly:

mapnik-config

Setting up the Back End

Once we’ve confirmed Mapnik and node.js are up and running, we created a PostgreSQL database that has a Spherical Mercator – SRID:3857 geometry column with the running routes saved as LineString objects.

Note: Add an index on this geometry column (‘the_web_geom’) for faster tile image retrieval:

CREATE INDEX the_web_geom_gix ON table_name USING GIST (the_web_geom);

Now that the database, node.js, and Mapnik are installed, it’s time to create a node script for handling client requests for tiles. Before we can run the node script, we had to make sure that we had the necessary node modules installed. Again the only module that gave us any trouble was node-mapnik – make sure the dependencies are installed before installing mapnik via npm:

sudo apt-get install automake libtool g++ protobuf-compiler libprotobuf-dev libboost-dev libutempter-dev libncurses5-dev zlib1g-dev libio-pty-perl libssl-dev pkg-config

tile_server.js

var mapnik = require('mapnik');
var mercator = require('./sphericalmercator');
var url = require('url');
var fs = require('fs');
var http = require('http');
var parseQueryParams = require('./tile.js').parseQueryParams;
var path = require('path');
var TMS_SCHEME = false;

// the db connection info
var postgis_settings = {
    'host': 'localhost',
    'dbname': <database_name>,
    'table': <table_with_geometry_column>,
    'user': <user_name>,
    'password': <password>,
    'type': 'postgis',
    'initial_size': '10',
    'geometry_field': 'the_web_geom',
    'srid': 3857,
};

function createStyles() {
    var s = '<?xml version="1.0" encoding="utf-8"?>';
    s += '<!DOCTYPE Map [';
    s += '    ]>';
    s += '<Map minimum-version="2.0.0">';
    s += '<Style name="line">';
    s += '    <Rule>';
    s += '        <LineSymbolizer stroke="red" stroke-width="1.5" stroke-opacity=".5"/>';
    s += '    </Rule>';
    s += '</Style>';
    s += '</Map>';
    return s;
}

http.createServer(function (req, res) {

    parseQueryParams(req, TMS_SCHEME, function (err, params) {
        if (err) {
            res.writeHead(500, {'Content-Type': 'text/plain'});
            res.end(err.message);
        } else {
            try {
                var map = new mapnik.Map(256, 256, mercator.proj4);
                var bbox = mercator.xyz_to_envelope(parseInt(params.x),
                    parseInt(params.y),
                    parseInt(params.z), false);

                // Create a new layer with the running routes
                var layer = new mapnik.Layer('tile', mercator.proj4);
                layer.datasource = new mapnik.Datasource(postgis_settings);
                layer.styles = ['line'];

                // Create style filters
                map.fromStringSync(createStyles());

                // Draw the map as a PNG image
                map.bufferSize = 64; // how much edging is provided for each tile rendered
                map.add_layer(layer);
                map.extent = bbox;
                var im = new mapnik.Image(map.width, map.height);
                map.render(im, function (err, im) {
                    if (err) {
                        throw err;
                    } else {
                        res.writeHead(200, {'Content-Type': 'image/png'});
                        res.end(im.encodeSync('png'));
                    }
                });
            }
            catch (err) {
                res.writeHead(500, {'Content-Type': 'text/plain'});
                res.end(err.message);
            }
        }
    });
}).listen(8000);

Finally, in the same directory as tile_server.js, we needed sphericalmercator.js and tile.js.

Setting up the Front End

This is the easy part! We created a simple index.html file to request map tiles from the node.js script on our server.

index.html

<html>
<head>
    <script src='http://maps.google.com/maps/api/js?sensor=false' type='text/javascript'>
    </script>
    <script src='wax/dist/wax.g.min.js' type='text/javascript'></script>
    <style type="text/css">
        html, body {
            height: 100%;
            overflow: hidden;
        }
        #map {
            height: 100%;
        }
    </style>
</head>
<body>
<div id="map"></div>
<script>
    var runningTiles = {
        tilejson: '2.0.0',
        tiles: ['localhost:8000/{z}/{x}/{y}.png'] // make sure port lines up with port in node
    };
    var map = new google.maps.Map(document.getElementById('map'), {
        center: new google.maps.LatLng(51.5072, -0.1275), // London
        zoom: 10,
        zoomControlOptions: {
            style: google.maps.ZoomControlStyle.SMALL
        }
    });
    map.overlayMapTypes.insertAt(0, new wax.g.connector(runningTiles));
</script>
</body>
</html>

iOS Map Tiles

WJR_pop_running_routes

We’re not going to walk through the intricacies of getting the map tiles working with Google Maps on iOS, but if you’re at that step either contact us or check out this article on iOS overlay tiles.

Written by Christopher Lanoue.