Skip to content

Interactivity using GeoJSON vs. UTFGrid (vs. MapBox)

5. March 2013

During the last days I was playing a little bit with interactivity within tile based maps. I began to learn programming in JavaScript and tried different modestmaps.js, mapbox.js, leaflet.js and some offers I do not remember at this moment. My first statement that I have to make on interactive web maps and its possibilities: Oh my god…the possibilities seem to be endless!!! There are so many different scripts developed by so many different projects that it seems to be impossible to stay tuned on the latest developments.

To get at least a small overview/insight, I will begin to test some technical possibilities and describe them here. Lets start with the interactive visualisation of additional data attributes.

The data:

I want to visualise the world wide press freedom. A corresponding ranking was published last week by “Reporter ohne Grenzen“. To visualise this list I used the “ne_50m_admin_0_countries” shapefile from Natural Earth, defined a new attribute ‘rank’ within that shapefile and defined the ranking position for each object.

Therefore I wrote I simple python script, which helped me to define the new attribute in a semi-automatic manner…readList

The easiest way…using MapBox

The easiest way for applying interactive functionalities to tile-based maps is to define a map with TileMill and upload this as *.mbtiles to the MapBox upload portal. The big advantage of that is that TileMill provides a good GUI for defining the interactivity and you don’t have to care for an server administration. How did I do it:

1. Styling the map with TileMill – I decided for a very simple styling of the map by this CartoCSS definition:

Map { background-color: #b3c5c9; }

#ranking { 
  ::polygon_good[rank>0][rank<=21]{
    polygon-fill: #4bcf15; } 
  ::polygon_goodmid[rank>21][rank<=41]{
    polygon-fill: #ffcc00; } 
  ::polygon_mid[rank>41][rank<=120]{
    polygon-fill: #f0681a; }
  ::polygon_midbad[rank>120][rank<=165]{
    polygon-fill: #ff0000; }
  ::polygon_bad[rank>165][rank<=200]{
    polygon-fill: #410404; }
  ::polygon_nothing[rank=-1]{
    polygon-fill: #ffffff; }

    line-color: #434346;
    line-width: 1;
    line-join: round;
}

I kept the categorisation of the original source, where they define the countries between:1-21 = good…22-41 = satisfactory…42-120 = noticeable…121-165 = difficult…166+ = very serious……situation

Additionally, I made an overlay with a kind of paper structure. This is very similar to the structure in the ‘geography class’ example:

#paper { 
  polygon-pattern-file:url(textures/paper_folded_512.png);}

After these basics…you can now define the interactive part within the map, which is called “Teaser” in TileMill. Very easy…choose a layer which should provide the data for interactivity and copy-paste the attribute field of which the information will be provided to the user. In my case it looks this way:

<b>{{{name}}}</b>
<hr>
Ranking position: 
<strong>{{rank}}</strong>

As you can see, you can improve the visualisation of this information with basic html-coding. In my case, I show the name of the land (by the field ‘name’) and the corresponding ranking position (by the field ‘rank’).

That’s all for interacitvity! You can now export the map as *.mbtiles and upload it to MapBox. The resulting map can now be viewed online, like my example.

That was really easy, right? But for me it is not satisfying to not know the backend and have no idea of the corresponding processes that run in background. Additionally, you are limitted to 50MB at Mapbox when having just a free account,…

So I would like to do this on my own…but how.

The most interactive way…using Leaflet and GeoJSON

So how can I implement this by just using leaflet? On the main page you can find an examplary implementation of choropleth maps that seems to have the same interactive functionalities like my TileMill map.

Following the given instructions, you define a tile-based map with a background layer, using tiles from cloudemade, and an GeoJSON-overlay providing the statistical informations for the choropleth map.

How does it look for my map of world wide press freedom?

Necessary scripts and stylings:

<link rel="stylesheet" href="scripts/leaflet.css" />
<script src="scripts/leaflet.js"></script>
<script type="text/javascript" src="scripts/press.json"></script>
<style>
    #map { height: 400px; width:600px; }
    .info {
        padding: 6px 8px;
        font: 14px/16px Arial, Helvetica, sans-serif;
        background: white;
        background: rgba(255,255,255,0.8);
        box-shadow: 0 0 15px rgba(0,0,0,0.7);
        border-radius: 10px;
    }
    .info h4 {
        margin: 0 0 5px;
        color: #777;
    }
</style>

That is all for the head. Now let’s go to the body:

The map element.

<div id="map"></div>

And the content of the script, that contains all processing:

Load map and set the view

var map = L.map('map').setView([0, 0], 2)

Load the background layer

var cloudmade = L.tileLayer('http://{s}.tile.cloudmade.com/{key}/{styleId}/256/{z}/{x}/{y}.png', {
 attribution: 'Map data &copy; 2011 OpenStreetMap contributors, Imagery &copy; 2011 CloudMade, Data &copy; 2013 <a href="http://www.reporter-ohne-grenzen.de/ranglisten/rangliste-2013/">ROG/RSF</a>',
 key: 'BC9A493B41014CAABB98F0471D759707',
 styleId: 22677
 }).addTo(map);

Load the GeoJSON file which I made using gdal by converting the shapefile containing the rank-attribute (using this command: ogr2ogr -f geoJSON press.json pf_rank.shp):

var geojson = L.geoJson(statesData, {
 style: style,
 onEachFeature: onEachFeature
 }).addTo(map)

There is already the request for styling (‘style’) and interactivity (‘onEachFeature’) included. Delete that for running before adding the functions!

!The geoJSON file is loaded by the command “statesDate”. So xou have to define the corresponding file as script (e.g. press.json) and define within this file the variable name…originary it has no…so set a new one. For this example: just add “var statesData = ” to the the beginning of the file ‘press.json’!

Define the functions that make a choropleth map from the GeoJSON data:

function style(feature) {
        return {
        fillColor: getColor(feature.properties.rank),
        weight: 1,
        opacity: 1,
        color: 'white',
        dashArray: '3',
        fillOpacity: 0.7
        };
    }
function getColor(d) {
        var grades = [0, 21, 41, 120, 165];
        return d > grades[4]  ? ' #410404' :
           d > grades[3]  ? '#ff0000' :
           d > grades[2]   ? '#f0681a' :
           d > grades[1]   ? '#ffcc00' :
                      '#4bcf15';
    }

Now you just have to add the functions that provide interactivity:

    function highlightFeature(e) {
        var layer = e.target;

        layer.setStyle({
        weight: 2,
        color: '#666',
        dashArray: '',
        fillOpacity: 0.7
        });

        if (!L.Browser.ie && !L.Browser.opera) {
        layer.bringToFront();
        }
        info.update(layer.feature.properties);
    }
    function resetHighlight(e) {
        geojson.resetStyle(e.target);
        info.update();
    }
    function zoomToFeature(e) {
        map.fitBounds(e.target.getBounds());
    }
    function onEachFeature(feature, layer) {
        layer.on({
        mouseover: highlightFeature,
        mouseout: resetHighlight,
        click: zoomToFeature
        });
    }

    var info = L.control();

    info.onAdd = function (map) {
        this._div = L.DomUtil.create('div', 'info'); // create a div with a class "info"
        this.update();
        return this._div;
    };

    // method that we will use to update the control based on feature properties passed
    info.update = function (props) {
        this._div.innerHTML = '<h4>World wide press freedom</h4>' +  (props ?
        '<b>' + props.name + '</b><br />Ranking position: ' + props.rank 
        : 'Hover over a state');
    };

    info.addTo(map);

In result we have an interactive map of the press freedom, which should look like this!

The advantages this way of visualising interactively informations is:

– the direct access to geometries

– the possibility for defining styling of geometries

The disadvantage are:

– the huge file size

– and the corresponding processing time of the map, especially processing is done in the browser –> my Netbook has problems while displaying the map while my PC displays it without any delay!

But how is this implemented for the MapBox map?

An individual and fast way…using the UTFGrid independently in a Leaflet script

When exporting a map from TileMill to *.mbtiles it compresses not only all png-images (tiles) to a SQLite database, it also (when you have defined something for the Teaser) exports an UTFGrid to the database.

When the TileMill map is uploaded to a MapBox account, is also the corresponding UTFGrid accessable: see an example

This is the original source that helped me a lot: https://github.com/danzel/Leaflet.utfgrid

What has to be done?

Download the leaflet – UTFGrid extension and import it to your script:

<script src="static/leaflet.utfgrid.js"></script>

We need a new UTFGrid-object:

var utfGrid = new L.UtfGrid('http://{s}.tiles.mapbox.com/v3/milkator.press_freedom/{z}/{x}/{y}.grid.json?callback={cb}', {
            resolution: 4
        });

and have to add this to the map:

map.addLayer(utfGrid)

The rest is pretty simple and is very similar to the processing of the GeoJSON data.

Define which actions should be done and which functions have to be called on these actions:

utfGrid.on('click', clicking)
 .on('mouseover', hovering)
 .on('mouseout', function (e) {console.log("Nothing implemented!");}
);

This is equivalent to the “onEachFeature” function of the GeoJSON layer in the example above!

The corresponding functions look as follows:

function hovering(e){
    if (e.data) {
        document.getElementById('hover').innerHTML = "<h4>World wide press freedom</h4>" +  (e ?
    "<b>" + e.data.name + "</b><br />Ranking position: " + e.data.rank 
    : 'Hover over a state');} 
    else {
        document.getElementById('hover').innerHTML = 'hover: nothing';}
    var layer = e.target;
        info.update(e);    }

function clicking(e){
    if (e.data) {
        document.getElementById('click2').innerHTML = 'click: ' + e.data.rank;} 
    else {
        document.getElementById('click2').innerHTML = 'click: nothing'; }

Find an example and the corresponding source code here!

The UTFGrid can also be calculated on-the-fly served as tiles by using TileStache, but that will be described in another post!

Extension: I’ve made a new Gist-repository version! So you can directly clone the code from there: http://bl.ocks.org/milkbread/6449317!
Have much fun with it!

Advertisements
4 Comments
  1. Your UTFGrid example has what appears to be a coordinate issue

  2. Hello,
    Nice article, well detailed thanks. I believe there is something wrong with the last example, the data seems to be offset by on tile on the X axis, Try hovering France for instance and you will get Turkey, Bulgaria, etc.

    On the GeoJSON example, gzip could be activated to reduce the download size, but it won’t make the processing faster tho

    Fabien

    • Hi,

      thank you for the advice…I’ve fixed it!

      Strange thing…it was because I’ve styled the background with: “position:absolute;”
      Removing that removed also the ‘offset’!

      Ralf

  3. Chris Calip permalink

    For people encountering this need with a big feature set 100k + , using mbtiles takes forever on a large feature set, best use the geojson alternative

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: