Tutorials
advanced
JavaScript

Route avoidance with the Directions API and Turf.js

Prerequisite

Familiarity with front-end development concepts.

The Mapbox Directions API calculates turn-by-turn directions and calculates optimal driving, walking, and cycling routes using traffic- and incident-aware routing. But sometimes you need to consider how to avoid routes in those directions! This tutorial shows you how to bring avoidance areas into your map, and to detect if directions returned from the Mapbox Directions API hit them.

You will make a map that loads in the low clearance obstacle data, and uses a Turf.js function to detect whether the route generated by the Directions API collides with the obstacles. You will display and style the results on the Mapbox GL JS map, and display detailed notes in the sidebar.

Getting started

Here are the resources you need to get started:

  • An access token. The token is used to associate a map with your account. You can find your access tokens on your Access Tokens page and below, if you are logged in to Mapbox:
mapboxgl.accessToken = "YOUR_MAPBOX_ACCESS_TOKEN";
  • Mapbox GL JS. A Mapbox JavaScript API for building maps.

  • Mapbox GL Directions. The Mapbox GL Directions plugin uses the Mapbox Directions API to add directions functionality to your Mapbox GL JS map.

  • Turf.js. Turf is the JavaScript library you'll be using today to add analysis to your map.

  • Polyline.js. Polyline is the JavaScript library that converts direction routes into line routes.

  • Data. This example uses data that identifies low clearance areas in Lexington, Kentucky. This data is provided in the Load obstacle data section of the tutorial.

  • A text editor. You'll be writing HTML, CSS, and JavaScript.

Add Turf and Polyline

This tutorial uses the latest versions of Mapbox GL JS, Mapbox GL Directions, Turf.js, and Polyline. Add these libraries to your HTML file by adding this to the head of your page.

Turf is a JavaScript library for adding spatial and statistical analysis to your web maps. You'll be using Turf to expand the size of the obstacles, and to detect if the routes overlap with any obstacles.

Polyline is a polyline implementation in JavaScript, which lets you encode from and decode into [lat, lng] coordinate pairs. You'll use Polyline to convert the routes provided from the Mapbox Directions API into GeoJSON. Once the routes have been converted, you'll be able to use them in Turf.

<!-- Mapbox GL JS -->
<script src=https://api.tiles.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js></script>
<link href=https://api.tiles.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css rel="stylesheet" />

<!-- Mapbox GL Directions -->
<script src=https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.0.2/mapbox-gl-directions.js></script>
<link rel="stylesheet" href=https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.0.2/mapbox-gl-directions.css type="text/css" />

<!-- Turf & Polyline -->
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
<script src=https://cdnjs.cloudflare.com/ajax/libs/mapbox-polyline/1.1.1/polyline.js></script>

Set up the structure

This app will have a map that takes up a large part of the page, as well as a sidebar that will display route details. First, in the body, create an empty div for your map and a div for your sidebar, as shown:

<div id="map"></div>
<div class="sidebar">
  <div class="heading">
    <h1>Routes</h1>
  </div>
  <div id="reports" class="reports"></div>
</div>

Next, add some CSS with this style element in the head to properly position the map and format the contents of the sidebar.

<style>
  body {
    color: #404040;
    font: 400 15px/22px 'Source Sans Pro', 'Helvetica Neue', Sans-serif;
    margin: 0;
    padding: 0;
    -webkit-font-smoothing: antialiased;
  }

  * {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
  }

  #map {
    position: absolute;
    left: 15%;
    top: 0;
    bottom: 0;
    width: 85%;
  }

  .sidebar {
    position: absolute;
    width: 15%;
    height: 100%;
    top: 0;
    left: 0;
    overflow: hidden;
    border-right: 1px solid rgba(0, 0, 0, 0.25);
  }

  h1 {
    font-size: 22px;
    margin: 0;
    font-weight: 400;
  }

  a {
    color: #404040;
    text-decoration: none;
  }

  a:hover {
    color: #101010;
  }

  .heading {
    background: #fff;
    border-bottom: 1px solid #eee;
    min-height: 60px;
    line-height: 60px;
    padding: 0 10px;
  }

  .reports {
    height: 100%;
    overflow: auto;
    padding-bottom: 60px;
  }

  .reports .item {
    display: block;
    border-bottom: 1px solid #eee;
    padding: 10px;
    text-decoration: none;
  }

  .reports .item:last-child {
    border-bottom: none;
  }

  .reports .item .title {
    display: block;
    color: #00853e;
    font-weight: 700;
  }

  .reports .item .warning {
    display: block;
    color: red;
    font-weight: 700;
  }

  .reports .item .title small {
    font-weight: 400;
  }

  .reports .item.active .title,
  .reports .item .title:hover {
    color: #8cc63f;
  }

  .reports .item.active {
    background-color: #f8f8f8;
  }

  ::-webkit-scrollbar {
    width: 3px;
    height: 3px;
    border-left: 0;
    background: rgba(0, 0, 0, 0.1);
  }

  ::-webkit-scrollbar-track {
    background: none;
  }

  ::-webkit-scrollbar-thumb {
    background: #00853e;
    border-radius: 0;
  }
</style>

Initialize the map

Now that your page has a good structure and style, add a map to it using Mapbox GL JS. This is where you'll need to use your access token. Create a script tag in the body with a new mapboxgl.Map object called map, using center and zoom to set the view on Lexington, Kentucky.

<script>
  mapboxgl.accessToken = "YOUR_MAPBOX_ACCESS_TOKEN";
  var map = new mapboxgl.Map({
    container: "map", // container id
    style: "mapbox://styles/mapbox/light-v10", // stylesheet location
    center: [-84.5, 38.05], // starting position
    zoom: 12, // starting zoom
  });
</script>

Now your page has a sidebar on the left and a map next to it centered on Lexington, Kentucky.

Load obstacle data

Add the low clearance areas in Lexington, Kentucky as a GeoJSON FeatureCollection. Place the following code in the script tag.

var clearances = {
type: "FeatureCollection",
features: [
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.47426, 38.06673],
    },
    properties: {
      clearance: "13' 2",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.47208, 38.06694],
    },
    properties: {
      clearance: "13' 7",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.60485, 38.12184],
    },
    properties: {
      clearance: "13' 7",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.61905, 37.87504],
    },
    properties: {
      clearance: "12' 0",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.55946, 38.30213],
    },
    properties: {
      clearance: "13' 6",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.27235, 38.04954],
    },
    properties: {
      clearance: "13' 6",
    },
  },
  {
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [-84.27264, 37.82917],
    },
    properties: {
      clearance: "11' 6",
    },
  }
],
};

Prepare the obstacles

Since you are using Turf to check if the routes collide with the obstacles, you need to convert them to Turf objects.

Start with the obstacles by running them through the buffer command. Buffer lets you set the size of a point by specifying the desired radius and the units you want to use. For the low clearance obstacles, set the obstacles to a quarter of a kilometer. These are the objects you will use for the collision detection.

var obstacle = turf.buffer(clearances, 0.25, { units: "kilometers" });

Display the obstacles

Add the clearances layer after the map loads by wrapping it in map.on('load', function(){});.

map.on("load", function (e) {

  map.addLayer({
    id: "clearances",
    type: "fill",
    source: {
      type: "geojson",
      data: obstacle,
    },
    layout: {},
    paint: {
      "fill-color": "#f03b20",
      "fill-opacity": 0.5,
      "fill-outline-color": "#f03b20",
    },
  });

});

Preparing for routes

You will add sources and layers for the routes that are returned from the Directions API. The sources and layers will be used to draw the routes on the map. Give them the IDs 'route1', 'route2' and 'route3' using a for loop, in the on.load function. The line-colorin the paint property is being set to a light shade of gray here. Later, when you check the routes for collisions, you'll change the line-color to red or green based on whether the route goes through an obstacle or not.

for (i = 0; i <= 2; i++) {
  map.addSource("route" + i, {
    type: "geojson",
    data: {
      type: "Feature",
    },
  });

  map.addLayer({
    id: "route" + i,
    type: "line",
    source: "route" + i,
    layout: {
      "line-join": "round",
      "line-cap": "round",
    },
    paint: {
      "line-color": "#cccccc",
      "line-opacity": 0.5,
      "line-width": 13,
      "line-blur": 0.5,
    },
  });
}

Add directions

Now you will add the Mapbox Directions plugin, which is a full feature directions plugin for Mapbox GL JS that uses the Mapbox Directions API. It lets you quickly add a user interface to display driving, cycling, or walking directions on the map.

Add the following code to the script tag. This will allow users to get directions from one place to another. Set it up to use the driving profile and to return alternative routes, if they're available.

var nav = new mapboxgl.NavigationControl();

var directions = new MapboxDirections({
  accessToken: mapboxgl.accessToken,
  unit: "metric",
  profile: "mapbox/driving",
  alternatives: "true",
  geometries: "geojson",
});

map.scrollZoom.enable();
map.addControl(directions, "top-right");

Check the routes for collisions

Now that the obstacles are Turf objects, you need to turn the routes into Turf objects too. Let's walk through the function step-by-step; the complete function is at the end of this section.

Set up the report, detail, and routes variables:

directions.on("route", (e) => {
  var report = document.getElementById("report");
  report.innerHTML = "";
  var detail = report.appendChild(document.createElement("div"));

  let routes = e.route;

//Hide all routes by setting the opacity to zero.
for(i=0; i<3; i++){
  map.setLayoutProperty("route" + i, 'visibility', 'none');
}

Add IDs to the routes. This allows you reference each route individually using the ID.

//Add IDs to the routes
routes.forEach(function (route, i) {
  route.id = i;
});

For every route you receive, you'll make the route visible. You'll take a polyline of the route's geometry.

//For each route...
routes.forEach((e) => {

//Make each route visible
map.setLayoutProperty("route" + e.id, 'visibility', 'visible');

//Get GeoJson LineString feature of route
var routeLine = polyline.toGeoJSON(e.geometry);

Then you'll update the data for the route. This will update the route line on the map.

//Update the data for the route which will update the route line on the map
map.getSource("route" + e.id).setData(routeLine);

Now, take the obstacle and the route, and see if they collide. You do this by passing them through the boolean-disjoint function in Turf. If they don't intersect, the result is true. If they do intersect, the result is false.

var clear = turf.booleanDisjoint(obstacle, routeLine);

Use the results to appropriately populate the variables that will be used to style the route, and to assemble the results display as text in the sidebar.

//Populate the variables with the results, which we'll use in the reports
var collision = "";
var emoji = "";

if (clear == true) {
  collision = "is good!";
  detail = "does not go"
  emoji =  "✔️";
  report.className = "item";
  map.setPaintProperty("route" + e.id, "line-color", "#74c476");
} else {
  collision = "is bad.";
  detail = "goes"
  emoji =  "⚠️";
  report.className = "item warning";
  map.setPaintProperty("route" + e.id, "line-color", "#de2d26");
}

Add a new report section to the sidebar.

//Add a new report section to the sidebar.
//Assign a unique `id` to the report.
report.id = "report-" + e.id;

//Add the response to the individual report created above.
var heading = report.appendChild(document.createElement("h3"));

// Set the class type based on clear value.
if(clear == true){
  heading.className = "title";
} else {
  heading.className = "warning";
}

//Update the reports
heading.innerHTML = emoji + " Route " + (e.id + 1) + " " + collision;

//Add more detail to the individual report.
var details = report.appendChild(document.createElement("div"));
details.innerHTML = "This route " + detail + " through an avoidance area.";
report.appendChild(document.createElement("hr"));

  });
});

The complete function should look as follows. Add it to the script tag, and reload your web app. When you select an origin and destination by clicking on the map, route directions will be drawn on the map, and the sidebar will display the route report.

directions.on("route", (e) => {
  var reports = document.getElementById("reports");
  reports.innerHTML = "";
  var report = reports.appendChild(document.createElement("div"));
  let routes = e.route;

  //Hide all routes by setting the opacity to zero.
  for (i = 0; i < 3; i++) {
    map.setLayoutProperty("route" + i, "visibility", "none");
  }

  routes.forEach(function (route, i) {
    route.id = i;
  });

  routes.forEach((e) => {
    //Make each route visible, by setting the opacity to 50%.
    map.setLayoutProperty("route" + e.id, "visibility", "visible");

    //Get GeoJson LineString feature of route
    var routeLine = polyline.toGeoJSON(e.geometry);

    //Update the data for the route, updating the visual.
    map.getSource("route" + e.id).setData(routeLine);

    var collision = "";
    var emoji = "";
    var clear = turf.booleanDisjoint(obstacle, routeLine);

    if (clear == true) {
      collision = "is good!";
      detail = "does not go";
      emoji = "✔️";
      report.className = "item";
      map.setPaintProperty("route" + e.id, "line-color", "#74c476");
    } else {
      collision = "is bad.";
      detail = "goes";
      emoji = "⚠️";
      report.className = "item warning";
      map.setPaintProperty("route" + e.id, "line-color", "#de2d26");
    }

    //Add a new report section to the sidebar.
    // Assign a unique `id` to the report.
    report.id = "report-" + e.id;

    // Add the response to the individual report created above.
    var heading = report.appendChild(document.createElement("h3"));

    // Set the class type based on clear value.
    if (clear == true) {
      heading.className = "title";
    } else {
      heading.className = "warning";
    }

    heading.innerHTML = emoji + " Route " + (e.id + 1) + " " + collision;

    // Add details to the individual report.
    var details = report.appendChild(document.createElement("div"));
    details.innerHTML = "This route " + detail + " through an avoidance area.";
    report.appendChild(document.createElement("hr"));
  });
});

Finished product

Well done! You have used the Mapbox Directions API, Mapbox GL JS, Turf, and Polyline to create a directions-enabled map that tells you if the routes from the Directions API will collide with obstacles you've added to the map.

Your finished HTML file should look like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Final</title>
    <meta
      name="viewport"
      content="initial-scale=1,maximum-scale=1,user-scalable=no"
    />

    <!-- Mapbox GL JS -->
    <script src=https://api.tiles.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js></script>
    <link
      href=https://api.tiles.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css
      rel="stylesheet"
    />

    <!-- Turf & Polyline -->
    <script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
    <script
      src=https://cdnjs.cloudflare.com/ajax/libs/mapbox-polyline/1.1.1/polyline.js
    ></script>

    <!-- Mapbox GL Directions -->
    <script src=https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.0.2/mapbox-gl-directions.js></script>
    <link
      rel="stylesheet"
      href=https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-directions/v4.0.2/mapbox-gl-directions.css
      type="text/css"
    />

  </head>

  <body>
    <div id="map"></div>
    <div class="sidebar">
      <div class="heading">
        <h1>Routes</h1>
      </div>
      <div id="reports" class="reports"></div>
    </div>

    <script>

      mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
      
      var map = new mapboxgl.Map({
        container: 'map', // container id
        style: 'mapbox://styles/mapbox/light-v10',
        center: [-84.5, 38.05], // starting position
        zoom: 11 // starting zoom
      });

      var nav = new mapboxgl.NavigationControl();

      var directions = new MapboxDirections({
        accessToken: mapboxgl.accessToken,
        unit: 'metric',
        profile: 'mapbox/driving',
        alternatives: 'false',
        geometries: 'geojson'
      });

      map.scrollZoom.enable();
      map.addControl(directions, 'top-right');

      var clearances = {
          type: "FeatureCollection",
          features: [
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.47426, 38.06673],
              },
              properties: {
                clearance: "13' 2",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.47208, 38.06694],
              },
              properties: {
                clearance: "13' 7",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.60485, 38.12184],
              },
              properties: {
                clearance: "13' 7",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.61905, 37.87504],
              },
              properties: {
                clearance: "12' 0",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.55946, 38.30213],
              },
              properties: {
                clearance: "13' 6",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.27235, 38.04954],
              },
              properties: {
                clearance: "13' 6",
              },
            },
            {
              type: "Feature",
              geometry: {
                type: "Point",
                coordinates: [-84.27264, 37.82917],
              },
              properties: {
                clearance: "11' 6",
              },
            }
          ],
        };

      var obstacle = turf.buffer(clearances, 0.25, { units: 'kilometers' });

      map.on('load', function(e) {
        map.addLayer({
          id: 'clearances',
          type: 'fill',
          source: {
            type: 'geojson',
            data: obstacle
          },
          layout: {},
          paint: {
            'fill-color': '#f03b20',
            'fill-opacity': 0.5,
            'fill-outline-color': '#f03b20'
          }
        });


        //Create sources and layers for the returned routes.
        //There will be a maximum of 3 results from the Directions API.
        //We use a loop to create the sources and layers.
        for (i = 0; i <= 2; i++) {
          map.addSource('route' + i, {
            type: 'geojson',
            data: {
              type: 'Feature'
            }
          });

          map.addLayer({
            id: 'route' + i,
            type: 'line',
            source: 'route' + i,
            layout: {
              'line-join': 'round',
              'line-cap': 'round'
            },
            paint: {
              'line-color': '#cccccc',
              'line-opacity': 0.5,
              'line-width': 13,
              'line-blur': 0.5
            }
          });
        }

      });

      directions.on('route', e => {
        var reports = document.getElementById('reports');
        reports.innerHTML = '';
        var report = reports.appendChild(document.createElement('div'));
        let routes = e.route;

        //Hide all routes by setting the opacity to zero.
        for (i = 0; i < 3; i++) {
          map.setLayoutProperty('route' + i, 'visibility', 'none');
        }

        routes.forEach(function(route, i) {
          route.id = i;
        });

        routes.forEach(e => {
          //Make each route visible, by setting the opacity to 50%.
          map.setLayoutProperty('route' + e.id, 'visibility', 'visible');

          //Get GeoJson LineString feature of route
          var routeLine = polyline.toGeoJSON(e.geometry);

          //Update the data for the route, updating the visual.
          map.getSource('route' + e.id).setData(routeLine);

          var collision = '';
          var emoji = '';
          var clear = turf.booleanDisjoint(obstacle, routeLine);

          if (clear == true) {
            collision = 'is good!';
            detail = 'does not go';
            emoji = '✔️';
            report.className = 'item';
            map.setPaintProperty('route' + e.id, 'line-color', '#74c476');
          } else {
            collision = 'is bad.';
            detail = 'goes';
            emoji = '⚠️';
            report.className = 'item warning';
            map.setPaintProperty('route' + e.id, 'line-color', '#de2d26');
          }

          //Add a new report section to the sidebar.
          // Assign a unique `id` to the report.
          report.id = 'report-' + e.id;

          // Add the response to the individual report created above.
          var heading = report.appendChild(document.createElement('h3'));

          // Set the class type based on clear value.
          if (clear == true) {
            heading.className = 'title';
          } else {
            heading.className = 'warning';
          }

          heading.innerHTML = emoji + ' Route ' + (e.id + 1) + ' ' + collision;

          // Add details to the individual report.
          var details = report.appendChild(document.createElement('div'));
          details.innerHTML =
            'This route ' + detail + ' through an avoidance area.';
          report.appendChild(document.createElement('hr'));
        });
      });



    </script>
  </body>
</html>

Next steps

This is but one example that benefits from this functionality. Others include challenging speed limits, steep curvy gradients or gas station deserts. Turf has many tools that you can use to extend this example. For example, you could locate where the obstacle collides with the route and use it to add a waypoint, then generate a new set of directions.

Was this page helpful?