blog: Toronto's Bike Lanes on on a user friendly map

Posted on 30 Mar 2012 under code

Toronto is a wonderful city that has a strong bike culture. The community is caring, fun, and hard working. One thing I always enjoyed from the official side of this community is the well drawn bike map the city provides. However, it’s a large file and can be tough to navigate on a slower computer, or mobile device.

With that, I decided to spend part of my weekend learning some new things. There has been plenty of talk about mapping lately, it seems, so I figured why not plot the provided open data of all the bike lanes onto a map that my browser can easily open? Sure!

First question is, what will I need/want to use to accomplish this?

Getting the data into a postgres database.

This part, for being a complete newbie to PostgresSQL was surprisingly simple. After installing postgres and postgis, I followed the great instructions of creating a spatially-enabled database, then converted the shapefile from the city into SQL using the shp2pgsql tool. Once that was produced, I simply imported it into the database and, done!

Since writing this, I also discovered the most excellent Postgres.app which makes setting up Postgres on OSX very sane. The instructions if you install Postgres.app are quite similar, less the postgis paths below. You will be able to find the PostGis contrib files under /Applications/Postgres.app/Contents/MacOS/share/contrib/postgis-2.0. Enjoy!

shp2pgsql shapefile.shp > bikeways.sql

createdb bikeways
createlang plpgsql bikeways
psql -d bikeways -f /usr/share/postgresql/9.0/contrib/postgis-1.5/postgis.sql
psql -d bikeways -f /usr/share/postgresql/9.0/contrib/postgis-1.5/spatial_ref_sys.sql
psql -d bikeways -f /usr/share/postgresql/9.0/contrib/postgis_comments.sql
psql -d bikeways -f bikeways.sql

After you’ve filled your database with all that delicious data, you may want to make sure it’s in there correctly. For this, I found a tool named QuantumGIS

To add your new PostGIS layer, it was as simple as hitting the “Add PostGIS layer” button in the toolbar, adding a connection to the database, picking the right format (WGS84) and giving it a couple of minutes to parse all the data.

When loaded, you get a pretty cool object that looks something like this.

Bike Map in QunatumGIS

Now this is odd, because we sure don’t have that many bike lanes in the city. The reason seems to be that the open data plots all roads/paths, so I had to find an attribute that might allow me to parse out only the data I need.

It was the cp_type column within the table. For any bike-related path, it had an attribute, where-as all other unrelated ones were null. You can use the query builder (Layer > Query) within the app to add a WHERE clause. For example, try only looking for bike lanes:

    cp_type = 'Bike Lanes'

And you’ll see you have much less data on your screen. Of course, now you’re only seeing officially marked bike lanes, but there are many more styles, connections, and suggested routes. I’ll dive more into those later.

Data side looks good, so it’s time to build an app that will serve this.

Building the node.js application to serve our gis data

Now I want to show my data on a pretty map. I discovered the wonderful maps at http://maps.stamen.com and liked the look of both the “Toner” and “Terrain” maps for a project where-in there are plenty of overlays on streets (like a bike map!).

I wanted to use node.js, for no reason other than I felt like it, and discovered the Geddy framework. This is a simple, structured framework and it delivered on that premise. It was pretty easy to get started following their tutorial

    npm install -g jake geddy
    geddy app bikemap

The default views and routing would be enough to begin adding some front-end code to the app. Specifically the map itself.

This is where LeafletJS came into play. It’s a modern framework that provides a consistent interface to load and manipulate open street maps. Thus, switching the base look and feel of your map is insanely simple, and the API to add layers and other controls is wonderfully easy as well.

The initial setup for a map from the folks at Stamen is this simple:

$(function() {
  var lanesLayer;
  var center = new L.LatLng(43.6481, -79.4042);
  var map = new L.Map('map', {
    center: center,
    zoom: 13,
    layers: [new L.StamenTileLayer("terrain")]
  });

Then, your html simply needs to reference leaflet, the stamen map in this case, and have a container for the map. jQuery is provided when you create a Geddy app, as is an initial bootstrap setup.

    ...
    <script src="http://code.leafletjs.com/leaflet-0.3.1/leaflet.js"></script>
    <script type="text/javascript" src="http://maps.stamen.com/js/tile.stamen.js"></script>
    ...
    <div id='map' style='height: 500px;'></div>

When you load this up, Leaflet will load the Stamen map as a new layer and center it around the middle of Toronto.

Now, we need this to actually do something. Because pretty maps that you can move around are only so entertaining. Leaflet comes with a powerful and easy event API. In this case, I want to watch for whenever the map is done being moved around.

  var requestMap = function(bounds) {
    $.ajax({
      type: 'POST',
      url: '/fetch',
      dataType: 'json',
      data: {bounds: bounds.toBBoxString()},
      contentType: 'application/json charset=utf-8',
      success: fillMap,
      error: function(req, status, error) {
        console.error("Unable to fetch map data", error);
      }
    });
  };

  map.on('moveend', function(e) {
    requestMap(e.target.getBounds());
  });

A bit to take in here but if you’ve ever used Javascript it should make sense. You essentially watch for the moveend event, when it triggers, get the current bounds of the map (this is provided by Leaflet), and send that to a function which takes that bounding data and passes it to our server for some actual querying of the data!

You will notice there is a (currently) undefined fillMap function, but we will get to that shortly. Now to dive into some backend code. You will see I called the /fetch url, so I added a route in config/router.js to a new controller named Map with a fetch function. This might go against how Geddy wants you to build an app, but I didn’t really care at this point

So how does the fetch function look like in the node.js app?

var pg = require('pg');

var Map = function () {
  this.respondsWith = ['json'];
  this.fetch = function(req, resp, params) {
    var self = this;

    pg.connect(geddy.config.database.connection, function(err, client) {
      var bounds = params.bounds.split(",");
      var bounds = {
        southWest: {
          lng: bounds[0],
          lat: bounds[1],
        },
        northEast: {
          lng: bounds[2],
          lat: bounds[3]
        }
      };

      var bounding_box = [
        bounds.southWest.lng, bounds.southWest.lat,
        bounds.northEast.lng, bounds.northEast.lat
      ].join(",");

      var sql = [
        "SELECT ST_AsGeoJSON(the_geom) as shape, cp_type",
        "FROM public.centreline_od_bikeways_dec2011_wgs84",
        "WHERE cp_type != 'null'",
        "AND ST_Intersects(the_geom, ST_MakeEnvelope(" + bounding_box + ", -1))"
      ].join(' ');

      client.query(sql, function(err, result) {
        if (err) {
          self.respond({});
        }

        var featureCollection = new FeatureCollection();
        for (i = 0; i < result.rows.length; i++) {
          featureCollection.features[i] = JSON.parse(result.rows[i].shape);
          featureCollection.cp_type[i] = result.rows[i].cp_type;
        }
        self.respond(featureCollection);
      });
    });
  };

  function FeatureCollection() {
    this.type = 'FeatureCollection';
    this.features = new Array();
    this.cp_type = new Array();
  };
};

exports.Map = Map;

Oh dear that’s a lot of code! In essence it’s pretty simple, just some initial cruft of creating the query. First, I fetch the bounds from the POST parameters and build them into an object. This part is kind of ugly, but I could not in my short time find the right way to do this (please chime in if you know!)

Then, I create a polygon in which I will use for the query. This may not be the most effcient way?

Finaly, I run the postgres query and wait for a result. Once I have it, I push the data into a FeatureCollection object which is part of the GeoJSON spec. I add the cp_type into my collection as an extra attribute because I want to identify each type of lane on the front-end.

When everything has been parsed, the server responds in JSON format with all the data. If you go back and look at our front-end code, you’ll see we had success/error handlers on the $.ajax call, and fillMap was the success handler. So how does this look? Jump back to the front-end code!

var lanesLayer;
var fillMap = function(result) {
    var resultCounter = 0;
    if (lanesLayer != undefined) {
      map.removeLayer(lanesLayer);
    }

    lanesLayer = new L.GeoJSON();
    // Map the types of lanes to different colours
    lanesLayer.on('featureparse', function(e) {
      var _type = result.cp_type[resultCounter];
      var _style = (typesMapping[_type] != undefined ?  typesMapping[_type] 
                      : typesMapping['Bike Lanes']); 
      e.layer.setStyle(_style);
      resultCounter ++;
    });
    lanesLayer.addGeoJSON(result);
    map.addLayer(lanesLayer);
};

There is nothing fancy going on here. I simply add the entire result object as a GeoJSON layer and Leaflet handles the rest! The only thing I do is watch for the featureparse event, which happens on the addition of any feature object, and I add some styling dependant on the bike path. The associated typesMapping variable is nothing special, and actually violates DRY a bit with the key names, but alas.

  var typesMapping = {
    'Bike Lanes': {color: '#dd2c24', weight: 7},
    'Sharrows': {color: '#FDAD3F', weight: 5, stroke: true},
    'Contra-Flow Bike Lanes': {color: '#f6c8c6', weight: 5},
    'Major Multi-use Pathway': {color: '#663E93', weight: 5},
    'Minor Multi-use Pathway': {color: '#663E93', weight: 2},
    'Signed Routes': {color: '#266df1', weight: 5},
    'Park Roads': {color: '#1de525', weight: 6},
    'Suggested On-Street Connections': {color: '#fdd29f', weight: 6},
    'Suggested On-Street Routes': {color: '#FEE168', weight: 6}
  };

It is pretty impressive the amount of swagger LeafletJS deploys as it parses through all this data, but it does it with lightning speed and efficiency.

At this point, the data is being requested and filled onto the map. You should have a functioning bike map filled with all the data!

Futher fine tuning, references and full code

I noticed the queries were kind of slow, especially when the map was zoomed out and it had to contain plenty of objects. For this, I created a spatial index which was fairly simple.

CREATE INDEX bikeways_idx1
   ON centreline_od_bikeways_dec2011_wgs84 USING GIST (the_geom);

I’m not much sure at this moment yet what I can optimize next, but I hope to dive deeper into this later. Being a weekend project, I was fairly happy with how much I learned in a day and a half of hacking.

References:

Finally, the entire code base for this is available on github

Thanks for reading. If you have any advice or questions, please leave them in the comments.

Others Posts You May Enjoy

Thanks for reading. How about leaving a comment?

blog comments powered by Disqus
Fork me on GitHub