Route finding for bikes

Using routing APIs in Isère, France

This article is severely out of date! Not only does the Métromobilité routing API no longer function, but the coding approach is not current either. Update in progress!

In this tutorial, we are going to explore the components of route-finding APIs, and build a simple application to find a bike route between two points. We are going to use html5 geolocation to pin-point our location, the BAN database of french addresses to find our destination, and métromobilité to find the actual routes.

House keeping

To successfully follow this tutorial you need a text editor (such as Notepad++ or Atom). Go ahead and use your personal favorite, or if you don't have one installed, download Atom Editor.

You will also need to be able to run a local webserver in order to access remote resources, such as APIs. The easiest way to do this if you have python installed is to navigate in the console to the root of the project, and run python -m SimpleHTTPServer. This will enable a webpage at http://localhost:8000.

Getting Started

Websites

Since we will be building a simple web application in the workshop, we should touch on some of the components of a website first.

A basic website is comprised of HTML, css, and javascript files. The HTML (Hyper Text Markup Language) is the structure of the page - it indicates what goes where, as well as referencing the other components of the page. CSS (Cascading Style Sheets) are the style rules we put in place to dictate how we want the HTML elements to appear. Javascript is a programming language used by websites to do things - get data, manage interactions, format data, etc. jQuery is a javascript library that can make document manipulation, event handling and data fetching much simpler. Bootstrap is a framework that includes css and javascript helpers to make styling a website much easier as well (without developing the styles ourselves.) This can be great for prototyping sites, or making a decent looking tutorial in less time ;).

The basic webmap

We are also going to use MapboxJS (which extends LeafletJS) to get the map part of the application in place in very few lines of code.

In our project root, we will create a file called 'index.html', a file called 'style.css' and a file called 'script.js'.

In index.html we put this content:

1<!DOCTYPE html>
2<html>
3
4<head>
5  <meta charset=utf-8 />
6  <title>VéloIsère</title>
7  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
8  <link href='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css' rel='stylesheet' />
9  <link href='style.css' rel='stylesheet' />
10  <script src='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js'></script>
11</head>
12
13<body>
14  <div id="map"></div>
15  <script src="script.js"></script>
16</body>
17
18</html>

You will notice in the section that we have included references to our style.css, as well as the mapbox.js. Below the map container (

) we put our script.js reference, so the mapping library isn't called before the map container is rendered.

In style.css, put this content:

1body {
2  margin: 0;
3  padding: 0;
4}
5#map {
6  position: absolute;
7  top: 0;
8  bottom: 0;
9  width: 100%;
10}

Here we tell the page and map container to be full width and height.

And in script.js, put this content:

1// define mapbox token for map creation - get your own, yeah? It's free!
2L.mapbox.accessToken = 'pk.eyJ1IjoiYWJlbnJvYiIsImEiOiJEYmh3WWNJIn0.fus8CLBKPBHDvSxiayhJyg';
3
4// set up map
5var map = L.mapbox.map('map')
6    .addLayer(L.mapbox.styleLayer("mapbox://styles/mapbox/streets-v11"))
7    .setView([45.186502, 5.736339], 13);

Here we tell the mapbox library to use our token (mapbox is a subscription service, with a free tier) and to create a map using the 'streets' style, centered on Grenoble, and assigned it to the HTML element with the id of 'map'.

Fire up your webserver, and check out the page we created:

Geolocation

Geolocation allows us to get the user's location (in geographic coordinates) and is part of the HTML5 spec. Since we need a starting point for our route, we are going to play with geolocation in the process.

The geolocation object is a part of the browser's navigator object. Most modern browsers support geolocation, but Chrome 50+ (and probably all the others as well) no longer support it from unsecure origins. That means a public website without SSL (HTTP not HTTPS) can not use geolocation in chrome 50+. Luckily for us, Firefox still supports it, and localhost is a secure origin, so the demo should work locally, even in chrome.

Add to script.js:

1// define global variables
2var pos = [];
3var posMarker;
4var addressPos = [];
5var addressMarker;
6var bikeRouteMM;
7var bikeRouteII;
8
9// function for successful geolocation
10function geoSuccess(position) {
11  pos = [position.coords.latitude, position.coords.longitude];
12
13  // tell map to go to the new position
14  map.panTo(pos);
15
16  // remove the marker if it is already on the map
17  if (posMarker) {map.removeLayer(posMarker);};
18
19  // create a new mapbox marker for our position, add it to the map
20  posMarker = L.marker(pos, {
21    icon: L.mapbox.marker.icon({
22      'marker-size': 'large',
23      'marker-symbol': 'bicycle',
24      'marker-color': '#fa0',
25    }),
26  }).addTo(map);
27}
28
29// function for geolocation error
30function geoError(err) {
31  // tell user of issue
32  alert('Sorry, no position available.');
33}
34
35// use html5 geolocation
36function getUserLocation() {
37  if ('geolocation' in navigator) {
38    navigator.geolocation.getCurrentPosition(geoSuccess, geoError);
39  } else {
40    console.log('No geolocation available');
41  }
42}
43
44// call the actual function now
45getUserLocation();

Something to notice is the use of callback functions (geoSuccess and geoError in this case). Functions in javascript can be either synchronous or asynchronous. Synchronous functions finish running before allowing the script to move on to the next step. That is fine for many things, as the time to run many functions is unnoticeable. For longer functions (especially those that need to go get data), we don't want the application to finish getting all the data before moving on. We want to tell the app to get the data, and tell it what to do when the data are successfully received, but to continue running the rest of the script without waiting for the data. This pattern is called asynchronous.

And the results again (try firefox, chrome 50+ will no longer function...)

APIs

An API (Application Programming Interface) is a set of instructions that can be used to tell an application what to do. Recently, API is frequently used to describe a set of routes that can be accessed to get, update or put data from remote sources. In our case, we are only interested in getting data (Addresses and Routes) and in JSON (javascript object notation) format, which is an human-readable data structure which is easy to use and parse in javascript applications.

To access these remote data sources, we are going to use jQuery, a javascript helper library which is useful for document traversal, user event handling, and data access via Ajax (asynchronous javascript and xml, although we use it for JSON as well). jQuery has a json-specific helper-class called getJSON which we will use to get our data. A basic getJSON pattern is written like this:

1$.getJSON( 'url/to/my/data/', function( json ) {
2  // this part is the asynchronous callback
3  console.log('Look, I got this new data: ' + json.dataname);
4 });

Addresses

In order to create a route, we need two points: start and end. We have the start (our geolocation), and for the end, we are going to use BAN (la Base Addresse National française).

We are going to use the getJSON function, and log the results to the console so we can explore them. (Feel free to delete the "getUserLocation();" line in script.js - we'll use it later.)

Add jQuery to index.html, right after the style.css reference

1<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js'></script>

Reload your page, then open the console see this SO question for help.

Paste this in the console, and hit enter.

1$.getJSON('https://api-adresse.data.gouv.fr/search/?q=31 rue gustave eiffel', function (data) {
2  console.log(data);
3});

The BAN API allows us to provide locational precision to our results, so we are returned the nearest matches first. Try the same console code from before (press the up arrow when in the console to access previous commands), but add latitude,longitude to the end of the request url like so: &lat=45.186502&lon=5.736339. Notice how the Grenoble result moves to first place?

Let's build a function to put a point on the map for a found address.

Add to script.js:

1// get address from BAN database https://adresse.data.gouv.fr/api/
2function getBanAddressCoords(q, lat, lon, callback) {
3  // build uri for ban data
4  var uri = 'https://api-adresse.data.gouv.fr/search/?q=' + q;
5  if (lat && lon) { uri = uri + '&lat=' + lat + '&lon=' + lon; };
6
7  $.getJSON(uri, function (data) {
8    // grab the first address (feature);
9    var coords = data.features[0].geometry.coordinates;
10
11    // remove marker from map if it exists
12    if (addressMarker) {map.removeLayer(addressMarker);};
13
14    addressPos = [coords[1], coords[0]];
15
16    // create a new mapbox marker for our position, add it to the map
17    addressMarker = L.marker(addressPos, {
18      icon: L.mapbox.marker.icon({
19        'marker-size': 'large',
20        'marker-symbol': 'rocket',
21        'marker-color': '#66ccff',
22      }),
23    }).addTo(map);
24
25    if (pos.length > 0) {
26      // we have a position, use it to zoom to both points
27      var group = new L.featureGroup([posMarker, addressMarker]);
28      map.fitBounds(group.getBounds().pad(0.5));
29    } else {
30      // no position, just pan to new point
31      map.panTo(addressPos);
32    }
33
34    callback();
35  });
36};
37
38getBanAddressCoords('31 rue gustave eiffel', 45.186502, 5.736339, function () {
39  console.log('point added');
40});

And the results again:

Routes

Now that we know how to get our start and end points, we can explore the routing APIs.

Métromobilité

The Métromobilité API has a resource called Horaires OTP, which uses OpenTripPlanner to provide multimodal route-finding based on their data. To get a bike route, we need only provide start coordinates, end coordinates and mode, which in our case is 'BICYCLE'.

Let's try it out. Paste this in the console, and hit enter.

1var results;
2$.getJSON('http://data.metromobilite.fr/otp/routers/default/plan?mode=BICYCLE&fromPlace=45.1836656,5.703573&toPlace=45.195926,5.735935', function (data) {
3  console.log(data);results = data;
4});

For the mapping exercise, we want the geometry of the trip. Since we assigned the returned data to the results variable, we can play with the data in the console. To see the route geometry, type results.plan.itineraries[0].legs[0].legGeometry.points in the console. We see an encoded string, which contains the geometry of the entire route. Also check out results.plan.itineraries[0].legs[0].steps. This provides coordinates and directions. Think how we could use this to message each part of the route to the user.

Hold on, that geometry is nonsense! To make the route geometry useful, we'll need to decode it. Mapbox created a helper library to do just that, called polyline.

Add polyline to index.html, right after the jQuery reference

1<script src='https://rawgithub.com/mapbox/polyline/master/src/polyline.js'></script>

In script.js, you can remove the call to the address function for now. Add these functions to the bottom of the file now (Function to clear any routes from the map, function to create métromobilité route, call to route creator):

1// function to clear routes from map
2function clearRoutes() {
3  if (bikeRouteMM) {map.removeLayer(bikeRouteMM);};
4
5  if (bikeRouteII) {map.removeLayer(bikeRouteII);};
6};
7
8function getMetromobiliteRoute(fromPos, toPos, callback) {
9  clearRoutes();
10
11  // build uri for API
12  var uri = 'http://data.metromobilite.fr/otp/routers/default/plan' +
13            '?mode=BICYCLE&fromPlace=' + fromPos + '&toPlace=' + toPos;
14
15  $.getJSON(uri, function (data) {
16    // get all node points from first returned trip
17    var pts = data.plan.itineraries[0].legs[0].legGeometry.points;
18
19    //use mapbox.polyline to decode encoded polyline string
20    var decoded = polyline.decode(pts);
21
22    // create polyline, assign color, bind popup, add to map
23    bikeRouteMM = L.polyline(decoded, { color: '#21881c' }).bindPopup('Metromobilité').addTo(map);
24
25    callback();
26  });
27};
28
29getMetromobiliteRoute([45.1836656,5.703573], [45.195926,5.735935], function(){
30  console.log('route completed')
31});

And the results again:

App Build

All those parts are great, but we need to pull them together now... We will need to create buttons and inputs to trigger each of the steps: get location, get address, get métromobilité route, get itinisère route.

We are going to use bootstrap to create a nice navbar, and put in buttons for each of the actions.

Let's reference the bootstrap libraries (we are going to use a pre-styled build of bootstrap, provided by bootswatch).

Your of index.html should look like this:

1<head>
2  <meta charset=utf-8 />
3  <title>VéloIsère</title>
4  <meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' />
5  <link href='https://maxcdn.bootstrapcdn.com/bootswatch/3.3.6/superhero/bootstrap.min.css' rel='stylesheet' />
6  <link href='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.css' rel='stylesheet' />
7  <link href='style.css' rel='stylesheet' />
8  <script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js'></script>
9  <script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js'></script>
10  <script src='https://rawgithub.com/mapbox/polyline/master/src/polyline.js'></script>
11  <script src='https://api.mapbox.com/mapbox.js/v2.4.0/mapbox.js'></script>
12</head>

To add in the navbar and action buttons, replace your block with this:

1<body>
2  <div class="navbar navbar-default navbar-static-top">
3    <div class="container">
4      <div class="navbar-header">
5        <div class="navbar-brand">
6          VéloIsère
7        </div>
8
9        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navHeaderCollapse">
10          <span class="icon-bar"></span>
11          <span class="icon-bar"></span>
12          <span class="icon-bar"></span>
13        </button>
14      </div>
15      <div class="collapse navbar-collapse navHeaderCollapse">
16        <ul class="nav navbar-nav navbar-right">
17          <li><button id="get_position" type="button" class="btn btn-primary">Get position</button></li>
18          <li><form class="navbar-form" role="search">
19            <div class="form-group">
20              <input id="address_search" type="text" class="form-control" placeholder="Search">
21            </div>
22            <button id="get_address" class="btn btn-success" onclick="return false;">Get address</button>
23          </form></li>
24          <li><button id="get_route" class="btn btn-info" onclick="return false;">Get address</button></li>
25        </ul>
26      </div>
27    </div>
28  </div>
29
30  <div id="map"></div>
31  <script src="script.js"></script>
32</body>

and your style.css file should now look like this:

1body {
2  margin: 0;
3  padding: 0;
4}
5.navbar {
6  z-index: 2000;
7}
8.menu-btn {
9  width: 100%;
10}
11#map {
12  position: absolute;
13  top: 41px;
14  bottom: 0;
15  width: 100%;
16}

To trigger actions, we are going to use jQuery's 'click' binding. We tell the application that on click of a certain element, do something.

1$('#element_id').click(function () {
2  // do something now!
3}

Add this to the bottom of your script.js file:

1// function to close mobile nav menu
2function closeNav() {
3  $('.navHeaderCollapse').collapse('hide');
4};
5
6// jQuery click actions
7// on get address click
8$('#get_address').click(function () {
9  // remove any routes
10  clearRoutes();
11
12  // get value of search box
13  var search = $('#address_search').val();
14
15  // call address function
16  getBanAddressCoords(search, pos[0] || 45.186502, pos[1] || 5.736339, function () {
17    $('#route-menu').prop('disabled', false);
18  });
19});
20
21// on get location button click
22$('#get_position').click(function () {
23  // remove any routes
24  clearRoutes();
25  getUserLocation();
26});
27
28// on dropdown select, get the route
29$('get_route').click(function () {
30  getMetromobiliteRoute(pos, addressPos, function () {
31    closeNav();
32  });
33});

Now we have an app! (Try firefox to use the geolocation...)

Fin !