OSRM Route API: Free directions API with turn by turn directions and polylines

In my last two posts in this series on Open Street Maps Routing Machine (OSRM), I explained how to set up osrm-backend on Docker and host the OSRM API as a web service on EC2. These posts were mainly about infrastructure - installing OSRM and deploying it locally or on the web.

This write up will instead focus on functionality - specifically, OSRM's Route API, a service that quickly calculates the fastest path between given coordinates in the order you provide. It's a free alternative to Google's Directions API and enables your application to offer detailed turn-by-turn navigation and visually represent routes with polylines on a map. At the end of this tutorial we'll put everything together to create a simple web based navigation app, guiding users from a given start location to their chosen destination.

Part 1: Introduction to OSRM: Setting up osrm-backend using Docker
Part 2: Hosting the OSRM API on EC2: Running osrm-backend as a web service
Part 3: OSRM Route API: Free turn by turn directions and polylines (this article)
Part 4: OSRM Table API: Free distance matrix API

As with the other tutorials on this blog, full source code for this project, osrm_nav_app, can be found on GitHub.

We are going to build this turn by turn navigation app powered by OSRM

What is the OSRM Route API?

The OSRM Route API is a service that accepts an HTTP request and returns JSON formatted directions between two points. Let's suppose you just arrived at Vancouver Airport and need to get the downtown cruise terminal at Canada Place as quickly as possible to make it for your sailing. If OSRM were a person, you would ask him, "Hey OSRM! Tell me the fastest way to get from Vancouver Airport (49.195,-123.181) to Canada Place (49.290,-123.105)." OSRM would think about it for a few milliseconds and reply, "So from where you are, go straight, then after 300 m make the next right, ... and after 35 minutes you'll reach your destination. Oh and by the way, here's an encoded polyline string you can use to plot the route on a map so you know where you are going.

In the next section, we'll be using the OSRM server loaded with the map of British Columbia, Canada that we set up in last post. Take note of its URL, {osrm_server_url} because we'll need it to make API calls in the next section.

💡
If you aren't ready to set up your own OSRM server, you can use the public test server https://routing.openstreetmap.de/routed-car instead. OSRM uses this same endpoint for its map demo.

OSRM Route API example

Endpoint GET 
http://{osrm_server_url}:5000/route/v1/driving/
-123.1788482,49.195113;-123.1111209,49.2888248?
overview=full&geometries=polyline&steps=true&generate_hints=false

The general structure of a Route API call is as follows:

/{service}/{version}/{profile}/{coordinates}?option=value&option=value

:5000 is the default port OSRM comes installed with.

/route/v1/driving/ instructs OSRM to call the Route Service API (ver 1) with the driving profile.

-123.1788482,49.195113;-123.1111209,49.2888248 are the GPS coordinates (in longitude, latitude format) of the stops you want to generate a route for. If you need to visit an intermediate stop e.g. if I wanted to grab dim sum in Empire Seafood in Richmond (49.178,-123.142) before heading to the cruise, I could insert the restaurant's coordinates as an additional waypoint like so: -123.1788482,49.195113;-123.1424345,49.1789707;-123.1111209,49.2888248.

overview=full lets us add overview geometry (either full, simplified) to the polyline. full gives us a polyline that tracks roads very closely, even at the highest zoom levels.

geometries=polyline returns the encoded polyline in the geometries object of the response (more on this later).

steps=true includes route steps for each leg in the route. We'll use this in the following sections to generate turn by turn directions.

Output

{
    "code": "Ok",
    "routes": [
        {
            "geometry": "kjgkHxiynVPHx@Vf@hDDNFJFJHFHDJB~AZL@dAR`@CRYt@yAXaAHo@RuBD_AT{CFkAHoA@WNgCBWDy@Bm@B_@Bc@?c@?a@Ai@Ee@WaDCYAEEi@KmAa@qE]eE[oDGs@Gs@E}@Eu@Cw@Cw@E}A?mA?IA[@kBD{g@?aF@qG?{@?wA?sD?kAAgAAiACiACkACgACeAEeAEaAGcAG_AGaAKwAAUEi@Ee@IgAe@cE_AoIsAoL]gDmB}P]iDGq@O}AGe@Gc@Ga@Ia@I_@I_@Mc@I]M]M]M]O[OYQYQYKOcAsA}@q@s@c@m@Wi@OkBk@eD_AuIkCKSiBy@u@[yBy@SEUAS?S@UDSFSHQJSNSPQTOTOXMZKZeAxD{AxFs@vCO`@{@jDSr@Wt@Wn@QVORSLSHID]DQLaFEO?MAOM_BI_DEW?SAeCCaB@MLG?iBCM?M?{@C}AAsDEqDEuBCu@AyDKQ?kDCuDIoB?SAwDGU?kDEmBEaDEQ?uDEsDGuDGoBC_CEcLO[?yDImDC{DGuDEsJKaG?yAAW?W?yDKsDGeDC}DG{DE_EG{DIcEE{DEcDEoHGu@AsEGwDGwDCgEEa@AgCCaCEeCCqACsFGyGIY?oIMY?U?_BC{DEuDEwDGc@A_CCY?S?OAuCEwDGmCCk@?MAmDGgDGoACwAA_@MeACg@AsCEsAC_@Ce@C[E[K_@O]OWQUO]YGUQQKMMQKMy@uAmUia@ESg@{@Yg@M]EOGYG[o@uDk@_EKg@Ok@MYw@sA_AcBCEGMMQwD_HMUIMcAiBoAwBi@y@IOIOaAeByB{DIOIO}AiCaBqCIKKSAAyD{GIMMSyBqDkAwBQWOY}B_EQ[u@qAIMGKCEgCgE{@yAq@mAKOcAiBaA_BcAgBQZS\\wAfCGJU`@cBzCeA}Ae@s@??AA?????AA????AA???A??A??A????AA?????A??AA?????A?????AA??A?????A???A?????A?????A???A?????A???A??@??A?????A???A@????C[GMCC{@g@",
            "legs": [
                {
                    "steps": [
                        {
                            "geometry": "kjgkHxiynVPHx@V",
                            "maneuver": {
                                "bearing_after": 199,
                                "bearing_before": 0,
                                "location": [
                                    -123.17869,
                                    49.194776
                                ],
                                "modifier": "straight",
                                "type": "depart"
                            },
                            "mode": "driving",
                            "driving_side": "right",
                            "name": "",
                            "intersections": [
                                {
                                    "classes": [
                                        "tunnel"
                                    ],
                                    "out": 0,
                                    "entry": [
                                        true
                                    ],
                                    "bearings": [
                                        199
                                    ],
                                    "location": [
                                        -123.17869,
                                        49.194776
                                    ]
                                },
                                {
                                    "out": 1,
                                    "in": 0,
                                    "entry": [
                                        false,
                                        true
                                    ],
                                    "bearings": [
                                        15,
                                        195
                                    ],
                                    "location": [
                                        -123.178737,
                                        49.194688
                                    ]
                                }
                            ],
                            "weight": 10.6,
                            "duration": 10.6,
                            "distance": 43.3
                        },
                        ... 11 more step objects
                    ],
                    "summary": "Grant McConachie Way, Granville Street",
                    "weight": 1456.1,
                    "duration": 1456.1,
                    "distance": 14899.7
                }
            ],
            "weight_name": "routability",
            "weight": 1456.1,
            "duration": 1456.1,
            "distance": 14899.7
        }
    ],
    "waypoints": [
        {
            "distance": 39.209515718,
            "name": "",
            "location": [
                -123.17869,
                49.194776
            ]
        },
        {
            "distance": 163.535250862,
            "name": "",
            "location": [
                -123.112656,
                49.287751
            ]
        }
    ]
}

Drawing the OSRM route polyline on a map

In the geometry object of the routes array in the response, we can find the route polyline:

kjgkHxiynVPHx@Vf@hDDNFJFJHFHDJB~AZL@dAR`@CRYt@yAXaAHo@RuBD_AT{CFkAHoA@WNgCBWDy@Bm@B_@Bc@?c@?a@Ai@Ee@WaDCYAEEi@KmAa@qE]eE[oDGs@Gs@E}@Eu@Cw@Cw@E}A?mA?IA[@kBD{g@?aF@qG?{@?wA?sD?kAAgAAiACiACkACgACeAEeAEaAGcAG_AGaAKwAAUEi@Ee@IgAe@cE_AoIsAoL]gDmB}P]iDGq@O}AGe@Gc@Ga@Ia@I_@I_@Mc@I]M]M]M]O[OYQYQYKOcAsA}@q@s@c@m@Wi@OkBk@eD_AuIkCKSiBy@u@[yBy@SEUAS?S@UDSFSHQJSNSPQTOTOXMZKZeAxD{AxFs@vCO`@{@jDSr@Wt@Wn@QVORSLSHID]DQLaFEO?MAOM_BI_DEW?SAeCCaB@MLG?iBCM?M?{@C}AAsDEqDEuBCu@AyDKQ?kDCuDIoB?SAwDGU?kDEmBEaDEQ?uDEsDGuDGoBC_CEcLO[?yDImDC{DGuDEsJKaG?yAAW?W?yDKsDGeDC}DG{DE_EG{DIcEE{DEcDEoHGu@AsEGwDGwDCgEEa@AgCCaCEeCCqACsFGyGIY?oIMY?U?_BC{DEuDEwDGc@A_CCY?S?OAuCEwDGmCCk@?MAmDGgDGoACwAA_@MeACg@AsCEsAC_@Ce@C[E[K_@O]OWQUO]YGUQQKMMQKMy@uAmUia@ESg@{@Yg@M]EOGYG[o@uDk@_EKg@Ok@MYw@sA_AcBCEGMMQwD_HMUIMcAiBoAwBi@y@IOIOaAeByB{DIOIO}AiCaBqCIKKSAAyD{GIMMSyBqDkAwBQWOY}B_EQ[u@qAIMGKCEgCgE{@yAq@mAKOcAiBaA_BcAgBQZS\\wAfCGJU`@cBzCeA}Ae@s@??AA?????AA????AA???A??A??A????AA?????A??AA?????A?????AA??A?????A???A?????A?????A???A?????A???A??@??A?????A???A@????C[GMCC{@g@

Luckily for us, OSRM uses a well known polyline encoding/decoding scheme used by both Google Maps and Mapbox, so you can use the @mapbox/polyline library with precision level 5 to decode it.

var polyline = require('@mapbox/polyline');

// returns an array of lat, lon pairs
polyline.decode('kjgkHxiynVPHx@Vf... rest of str', 5);

// -> '[[ 49.19478, -123.17869 ], [ 49.19469, -123.17874 ] ... 379 more items]'

You can even use the oh so convenient Polyline Decoder Utility from Google to draw the decoded route string on a map.

Google Maps polyline decoder utility

Turn by turn navigation with OSRM

Turn by turn navigation is available in the response of the Route API if you include the steps=true parameter in your API call. Let's give it a try using the public OSRM Route API endpoint:

Turn by turn directions returned by the Route API

To make our turn by turn directions absolutely clear, we are going to use this format: ${maneuver} ${road_name} ${distance} so something like STRAIGHT Arthur Laing Bridge 1094.3m means that you go straight on Arthur Laing Bridge and continue for 1094.3 meters.

⚠️
The public OSRM API at https://routing.openstreetmap.de/routed-car is rate limited and available for light testing only. If you need to use OSRM in production please host osrm-backend on your own server.

Endpoint GET

https://routing.openstreetmap.de/routed-car/route/v1/driving-123.1788482,49.195113;-123.1111209,49.2888248?steps=true

Output (partial)

Zooming in on the steps object, we see:

{
    "geometry": "irgkH|hqnVcAsA}@q@s@c@m@Wi@OkBk@eD_AuIkCmDeAgDgA[K[QYQWUWWW[U[S_@S_@Qc@mAoDkAuDe@}AEc@",
    "maneuver": {
        "bearing_after": 37,
        "bearing_before": 40,
        "location": [
            -123.137593,
            49.196046
        ],
        "modifier": "straight",
        "type": "new name"
    },
    "mode": "driving",
    "driving_side": "right",
    "name": "Arthur Laing Bridge",
    "intersections": [
        {
            "out": 0,
            "in": 1,
            "entry": [
                true,
                false
            ],
            "bearings": [
                45,
                225
            ],
            "location": [
                -123.137593,
                49.196046
            ]
        },
        {
            "out": 0,
            "in": 2,
            "entry": [
                true,
                true,
                false
            ],
            "bearings": [
                15,
                45,
                195
            ],
            "location": [
                -123.135298,
                49.200476
            ]
        }
    ],
    "weight": 83,
    "duration": 83,
    "distance": 1094.3
}

The best way to make sense of the API response is to copy the output and paste it into a JSON prettifier like JSONLint.

maneuver.modifier is your maneuver, one of STRAIGHT, LEFT, RIGHT, SLIGHT LEFT and SLIGHT RIGHT.

name is the name of the road you are executing the maneuver on e.g. LEFT Grant McConachie Way means "turn left on Grant McConachie Way".

distance is how the driving distance until the next maneuver e.g. LEFT Grant McConachie Way 2216m means "after you turn left on Grant McConachie Way, continue driving for 2216 meters".

Building a turn by turn navigation app with OSRM

Now that we know how to draw polylines and retrieve turn by turn directions from the Route API, the next step is to develop a web-based navigation app that provides turn-by-turn guidance (screenshot below).

What we'll build: an OSRM powered turn by turn navigation app with route polylines

osrm-nav-app is a simple React app scaffolded with the Create React App build kit. It takes inspiration from OSRM's official demo app. To run it locally, clone the repo and run npm install followed by npm start. You'll also need to create a .env file with the line REACT_APP_MAPBOX_ACCESS_TOKEN={MAPBOX_ACCESS_TOKEN} added, where {MAPBOX_ACCESS_TOKEN} is the Mapbox API key provisioned with your Mapbox account. We'll be using Mapbox for the address autocomplete and map tiles.

The navigation app consists of three primary components:

  1. Address autocomplete and geocoding: Located in the top left corner, this feature allows for the input of start and end locations. It uses the Mapbox Places API to suggest addresses as you type and to convert these addresses into geographic coordinates.
  2. Interactive map interface: This central feature, also powered by Mapbox, displays the map. It shows the starting location marked with a green truck icon, the destination indicated by a red flag icon, and the planned route represented as a polyline.
  3. Directions table: On the right-hand side, there is a table that provides detailed turn-by-turn directions. This section also includes the total distance of the trip and the estimated travel time, all of which are sourced from the Route API.

Address autocomplete and geocoding

The code for the origin and destination text input boxes can be found in App.js in the render() method (or more accurately, in the return statement of App.js).

React components used in the address autocomplete / geocoding feature

Near the top of the App.js's return statement you can find the address-box div:

<div className="address-box">{renderAddressBox()}</div>

which in turn contains the renderAddressBox() method. This displays the address input boxes like so:

/* App.jsx */
const renderAddressBox = () => (
  <>
    <div className="address-controls">
      <div className="address-control">
        <span className="origin-circle"></span>
        <AutoCompleteInput
          setAddress={setOriginAddress}
          onManualInputChange={handleManualOriginAddressInputChange}
          placeName={originAddress.placeName}
          placeholder="Origin address"
        />
      </div>
      <div className="address-control">
        <span className="destination-circle"></span>
        <AutoCompleteInput
          setAddress={setDesitinationAddress}
          onManualInputChange={handleManualDestinationAddressInputChange}
          placeName={destinationAddress.placeName}
          placeholder="Destination address"
        />
      </div>
    </div>
  </>
);

All of the query logic resides in the <AutoCompleteInput/> component itself, which fires off the handleChange() method when the user starts typing.

/* /components/AddressInput/index.jsx */
return (
    <div className="address-input-container">
      {/* Input field for the address with provided props */}
      <input
        type="text"
        value={placeName}
        onChange={handleChange}
        {...others}
      />

      {/* List to display address suggestions */}
      <ul className="address-suggestions">
        {/* Map through suggestions and render each as a list item */}
        {suggestions?.map((suggestion, index) => (
          <li key={index} onClick={() => handleSelectSuggestion(suggestion)}>
            {/* Display the place name from the suggestion */}
            {suggestion.place_name}
          </li>
        ))}
      </ul>
    </div>
  );

When a user starts typing in his start address in the <AutoCompleteInput/> text box, the handleChange() event handler is called.

/* /components/AddressInput/index.jsx */
const handleChange = (event) => {
  onManualInputChange(event, "placeName");
  queryPlaces(event.target.value);
};

Inside the handleChange() method, queryPlaces() sends the event.target.value string to the Mapbox Places API.

queryPlace() uses the well known async/await design pattern to make an asynchronous request to Mapbox Places (read the full docs here).

/* /api/getPlaces.js */
export default async function getPlaces(query) {
  try {
    const response = await fetch(
      `https://api.mapbox.com/geocoding/v5/mapbox.places/${query}.json?access_token=${process.env.REACT_APP_MAPBOX_TOKEN}`,
    );

    return response.json();
  } catch (error) {
    console.error("There was an error while fetching places:", error);
  }
}

For example, the string "Canada Place" used in the examples above is used to retrieve matching addresses with this GET call to Mapbox Places (replace {MAPBOX_ACCES_TOKEN} with your Mapbox API key):

https://api.mapbox.com/geocoding/v5/mapbox.places/Canada%20Place.json?access_token={MAPBOX_ACCESS_TOKEN}
Mapbox Places API address suggestions and geocoding

The Places API provides an array of potential addresses, known as place_names, which are then shown in the address-suggestions div. If a user selects one of these addresses, the chosen place_name along with its coordinates are stored using the setAddress() method. These coordinates will be utilized later to fetch the route solution from OSRM's Route API.

Interactive map interface

I've decided to go with Mapbox for the map layer used in this project but rather than go with its standard Javascript API, Mapbox GL JS, I've chosen to use the third party react-map-gl wrapper so that I can easily inject data and style our map using Marker,  Source and Layer components like this:

<Map>
  <Marker/><Marker/>
  <Source>
    <Layer/>
  </Source>
</Map>

Displaying the start (green truck icon) and end (red flag icon) markers is easy with react-map-gl.

Start and end markers displayed on the Mapbox map

First, we check if either originAddress.latitude or destinationAddress.latitude variables are not null. This is a lazy (but effective!) way of checking if either an origin or destination address has been saved to the app.

/* App.js */
{
  originAddress.latitude && (
    <Marker
      longitude={originAddress.longitude}
      latitude={originAddress.latitude}
    >
      <img alt="Origin Marker" src={OriginMarker} style={{ width: 48 }} />
    </Marker>
  );
}
{
  destinationAddress.latitude && (
    <Marker
      longitude={destinationAddress.longitude}
      latitude={destinationAddress.latitude}
    >
      <img
        alt="Destination Marker"
        src={DestinationMarker}
        style={{ width: 48 }}
      />
    </Marker>
  );
}

If so, we inject a <Marker/> component into <Map/> and place it at the latitude and longitude returned from the Mapbox Places API in the previous section. The marker is styled with width:48 and represented by the origin_marker.png and destination_marker.png files stored in the /assets/images folder.

Similarly, the route polyline is added to the map with the line:

{polylineCoodinates && <Polyline coordinates={polylineCoodinates} />}

Within the <Polyline/> component, we make use of react-map-gl's <Source><Layer/></Source> structure to add a <Layer/> of type: "line" and a type="geojson" <Source/> injected with data from the coordinates field via coordinates={polylineCoodinates} above.

/* /components/Polyline.jsx */
const sourceData = {
  type: "Feature",
  properties: {},
  geometry: {
    type: "LineString",
    coordinates,
  },
};

// ... Additional code not related to polyline data source
  
return (
  <Source type="geojson" data={sourceData}>
    <Layer {...layerProps} />
  </Source>
);

One last thing - how did we receive the polyLineCoordinates? This came from the useEffect() hook whenever the origin or destination address changes.

/* App.js */
useEffect(() => {
  const getDirectionFunc = async () => {
    const res = await getDirection(originAddress, destinationAddress);
    const firstRoute = res.routes[0];
    const polylineCoodinatesTmp = _flatten(
      firstRoute?.legs[0]?.steps.map((step) => step.geometry.coordinates),
    );
    setRoute(firstRoute);
    setPolylineCoordinates(polylineCoodinatesTmp);
  };

  if (!originAddress.latitude || !destinationAddress.latitude) return;

  getDirectionFunc();

  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [originAddress.latitude, destinationAddress.latitude]);

Directions table

The final element of our navigation application is the directions table. This table provides a detailed list of turn-by-turn directions, guiding users from the beginning to the end of the route.

The first thing we do is extract the steps array from the Route API response.

const steps = route?.legs[0]?.steps || [];

This gives us a Javascript object that looks something like this:

Together with the maneuver object within each step object in the steps array, we can extract everything needed to build the directions table - step.name, step.maneuver.modifier and step.distance.

The code that does this is located in the renderDirectionDetail() method of App.js.

/* App.js */
{
  steps?.map((step, idx) => {
    if (idx === 0 || idx === steps.length - 1) return null;
    return (
      <div
        className="item"
        onClick={() => setMapCenter(step.maneuver.location)}
      >
        <div className="step-name">
          <img
            width={24}
            alt="Step icon"
            src={DIRECTION_ARROWS[step.maneuver.modifier]}
          />
          <span>{step.name}</span>
        </div>
        <span>{step.distance} m</span>
      </div>
    );
  });
}

Wrap up and next steps

This concludes our tutorial on the OSRM Route API. We covered the basics of making API requests to it and used waypoints and polyline data included in its response to build a simple navigation app with turn by turn directions. In an upcoming post, we will explore the OSRM Table API, a useful free alternative to Google Map's Directions Service.

👋 As always, if you have any questions or suggestions for me, please reach out or say hello on LinkedIn.

Next: Part 4: OSRM Table API: Free distance matrix API