Introduction

When we set out to revamp the tracking system for Cityflo, we quickly realized we needed a way to align the vehicle locations we receive with actual roads. While Google Maps offers a Snap to Roads API that accomplishes this, it can get quite expensive when you’re processing locations in real-time.

Enter osrm-backend, a powerful routing engine coded in C++ that uses OpenStreetMap data. What piqued our interest was its match service, which is excellent for correcting noisy GPS data by snapping it to the closest road network.

Technologies

For deploying osrm-backend, we’ll be leveraging the following technologies:

  • Docker: For containerizing our application
  • Node (ExpressJS): To build our OSRM API backend
  • Osmium: A helpful tool for handling OpenStreetMap data

Preparing Data

At the time of penning this article, Cityflo is serving just two Indian cities—Mumbai and Hyderabad. OpenStreetMap has segmented India into various zones, each covering a specific geographical area. To construct our network graph for Mumbai and Hyderabad, we’ll need to focus on obtaining data for both the Western and Southern zones of India.

The code snippets provided in this article presume that you’ve already installed osrm-backend, docker, osmium, and wget on your development environment.

wget -nc -O /tmp/india-western.osm.pbf https://download.geofabrik.de/asia/india/western-zone-latest.osm.pbf
wget -nc -O /tmp/india-southern.osm.pbf https://download.geofabrik.de/asia/india/southern-zone-latest.osm.pbf

osmium merge /tmp/india-western.osm.pbf tmp/india-southern.osm.pbf -o data/india-western-southern.osm.pbf

Next, let’s get the merged data file ready for OSRM with the commands below. We’ll stick to using the car mode to construct our network.

osrm-extract -p /opt/car.lua data/india-western-southern.osm.pbf

# The order of the following commands needs to be this way if you want OSRM
# to use MLD(Multi level Dijkstra) algorithm for graph traversal
osrm-partition data/india-western-southern.osrm
osrm-customize data/india-western-southern.osrm

Note that the commands mentioned are resource-intensive, both in terms of CPU and memory. The system requirements will depend on the volume of the data you’re dealing with. For the context of this tutorial, you should have a minimum of 8GB of RAM on your development machine.

Development Server

The osrm-backend ships with a built in http server which is not fit for production deployment. You can use it to test in the development mode.

The development server can be started with

# Start development server
osrm-routed -algorithm mld data/india-western-southern.osm.pbf

The osrm-backend repository provides bindings for nodejs which can be used to build a node based server in any framework. We’ll be using ExpressJS as our nodejs framework.

With the map data now prepped, we can proceed to setting up the osrm-backend server.

Docker Image

Let’s create two files package.json and index.js with the following contents respectively:

// package.json
{
  "name": "osrm-backend",
  "version": "1.0.0",
  "description": "osrm-backend docker image builder with western and southern India map data",
  "main": "index.js",
  "dependencies": {
    "express": "^4.18.2",
    "@project-osrm/osrm": "^5.27.1"
  }
}
// index.js
const express = require('express')
const OSRM = require("@project-osrm/osrm");
port = process.env.PORT || 5000;
var app = express();
var osrm = new OSRM({algorithm: 'MLD', path: "./data/india-western-southern.osrm"});

function match(req, res) {
    radiuses = req.query.radiuses ? req.query.radiuses.split(';').map(Number) : null;
    timestamps = req.query.timestamps ? req.query.timestamps.split(';').map(Number) : null;
    coordinates = req.params.coordinates.split(';');
    for (var i = 0; i < coordinates.length; i++) {
        coordinates[i] = coordinates[i].split(',').map(Number);
    }
    var options = {
        coordinates: coordinates,
        timestamps: timestamps,
        radiuses: radiuses,
    };
    osrm.match(options, function(err, result) {
        if (err)  {
            console.log(err);
            return res.json({
                error: err.message
            });
        } else {
            return res.json(result);
        }
    });
}

app.get('/match/v1/driving/:coordinates', match);
app.listen(port);

This is our express server which internally calls the node bindings for osrm-backend. Now we need to create the Dockerfile for this application.

# Dockerfile
FROM node:18-bullseye

RUN apt-get update && \
    apt-get install -y build-essential git pkg-config cmake gcc libbz2-dev \
    libxml2-dev libzip-dev libboost-all-dev lua5.2 liblua5.2-dev libtbb-dev

WORKDIR /service

ADD data ./

COPY ["package.json", "index.js", "./"]

EXPOSE 5000

RUN npm install

CMD [ "node", "index.js" ]

And that’s a wrap! You can build the Docker image using the command docker build -t osrm-backend . and then deploy it using your cloud provider of choice. While this method results in a larger image size, it is more resource-efficient at runtime compared to running osrm-routed.

We’ve successfully deployed this on AWS ECS, allocating just 0.25 CPU and 0.5GB of RAM. Below are the runtime metrics to give you an idea of the resource usage.

CPU Usage Memory Usage
CPU Usage memory Usage

Conclusion

We’ve walked through the entire process of deploying osrm-backend to production, from data preparation to server setup and cloud deployment. While our approach may lead to a larger initial image size, it offers notable benefits in terms of runtime resource efficiency. Our experience with AWS ECS has been smooth, and the runtime metrics are quite promising.

Deploying such a solution not only saves costs but also provides an efficient and reliable service for snapping GPS locations to road networks. Whether you’re working in a small startup or a large enterprise, optimizing resource usage while delivering high-quality services is key to long-term success.

Thanks for reading! If you have any questions or thoughts, feel free to drop them in the comments section below. Happy coding!