last updated June 9, 2016 12:00 AM
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.
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.
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 ;).
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 (
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 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...)
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 });
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:
Now that we know how to get our start and end points, we can explore the routing APIs.
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:
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...)