Route finding with the Directions API and Turf.js

advanced
JavaScript
Prerequisite

Familiarity with front-end development concepts.

In this tutorial, you will create a navigation app that finds a route between two user-selected locations and detects whether that route intersects with a low clearance area like a bridge or overpass. If so, the app will automatically look for a new route that avoids the low clearance area. A navigation app that does this could be useful for a company that uses large vehicles, like a moving or trucking company.

To create this app, you will use the Mapbox Directions API to calculate potential routes. The app will also use Turf.js to detect whether the route provided by the Directions API collides with any obstacles. You will display the routes on the Mapbox GL JS map, and display detailed notes about them in the sidebar.

Getting started

Here are the resources you need to get started:

  • A Mapbox access token. Your Mapbox access token is used to associate a map with your account. Your access tokens on your Account page.
  • Mapbox GL JS. Mapbox GL JS is 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.js is a JavaScript library used for geospatial analysis.
  • Polyline.js. Polyline.js is a 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.

Set up the page structure

This app has several foundational components: the map, which takes up a large part of the page, the sidebar, which will be used to display route information, and the Directions GL plugin widget.

tutorial
Related tutorial: Route avoidance with the Directions API and Turf.js

The route avoidance tutorial teaches you how to build an app that determines whether there is an obstacle in a given route. This tutorial for a route finding app also determines whether there is an obstacle in the way of a returned route, and then goes a step further by looking for alternative routes.

Create a map

To get started, you will create a map using Mapbox GL JS. Open your text editor and create a new file named index.html. Set up this new HTML file by pasting the following code into your text editor:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>Route finder</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
  <!-- Import 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" />
  <style>
    body {
      margin: 0;
      padding: 0;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
  </style>
</head>
<body>

  <div id="map"></div>

  <script>
    mapboxgl.accessToken = "YOUR_MAPBOX_ACCESS_TOKEN";
    var map = new mapboxgl.Map({
      container: "map", // Specify the container ID
      style: "mapbox://styles/mapbox/light-v10", // Specify which map style to use
      center: [-84.5, 38.05], // Specify the starting position [lng, lat]
      zoom: 11 // Specify the starting zoom
    });
  </script>

</body>
</html>    

This code creates the structure of the page. It imports Mapbox GL JS in the <head> of the page, which allows you to use Mapbox GL JS functionality and style in your web app.

This code contains a <div> element with the ID map in the <body> of the page. This <div> is the container in which the map will be displayed on the page.

Save your changes. Open the HTML file in your browser to see the rendered map, which is centered on the city of Lexington, Kentucky.

Import dependencies

This tutorial uses the latest versions of Turf.js and Polyline.

  • Turf is a JavaScript library for adding spatial and statistical analysis to your web maps. You will use Turf to expand the size of the obstacles, and to detect whether the returned 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.

Add these libraries to your HTML file by adding the following code to the head of your page:

<!-- Import Turf and 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>

Add the sidebar

Along with the map, this app will have a sidebar in which to display reports about each potential route. In the body of your HTML, create a div for your sidebar. In the sidebar, add a header with the text "Reports". Update the code in the <body> of your HTML:

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

Next, you will add some new CSS to style the sidebar:

.sidebar {
  position: absolute;
  margin: 20px 20px 30px 20px;
  width: 25%;
  top: 0;
  bottom: 0;
  padding: 20px;
  background-color: #FFFFFF;
  overflow-y: scroll;
  font-family: Open Sans, sans-serif;
}

Save your changes and refresh the page. The new sidebar will be on the left side of the page.

Add the Directions plugin

Now you will add the Mapbox GL 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.

Import the Directions plugin in the <head> of your HTML:

<!-- Import 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" />

Add the following code to the script tag. This will allow users to get directions from one place to another. The Mapbox GL Directions plugin allows you to use different parameters to customize the results returned from a query. In this case, you will set the value of profile to "mapbox/driving" and the value of alternatives to false. If this parameter is set to true, it will return alternative routes if they're available. For this app, you will also set the value of controls.instructions to false. If this parameter is set to true, the plugin will return and display turn-by-turn instructions from the starting point to the ending point. To learn more about how to customize a query using this plugin, see the Mapbox GL Directions documentation.

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

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

Save your changes and refresh your browser. You will see the Directions plugin in the top right corner of the map. Click on the map to add starting and ending points, and the new Directions plugin will generate a route between the points and render it on the map.

Add obstacle data to the map

Next, you will add the obstacle data as a new layer on the map.

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",
      },
    }
  ]
};

Convert the obstacles to Turf objects

Since you are using Turf to check if the routes collide with the obstacles, you need to convert the obstacles to Turf objects. To convert them, use the Turf buffer command, which 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 0.25 kilometers. These are the objects you will use for the collision detection. Add the following variables in the <script> tag:

var obstacle = turf.buffer(clearances, 0.25, { units: "kilometers" });
var bbox = [0, 0, 0, 0];
var polygon = turf.bboxPolygon(bbox);

Display the obstacles on the map

Next, you will create a new layer with the obstacle data using the Mapbox GL JS addLayer method. This step uses addLayer to add a new layer to the map using the obstacle data that you added in the last step, and styles the layer using fill-specific paint properties. Add the clearances layer after the map loads by wrapping it in a 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",
    },
  });

});

Save your work and refresh your browser page. You will see the new clearances layer loaded on the map, with the location of each obstacle painted as a semi-opaque red circle.

Prepare to add route reports to the sidebar

Next, you will create the route reports that will populate the sidebar. These reports show whether a route that the app has found intersects with any of the obstacles or not.

Populate the route report

Add the reporting functions to provide the user with some detailed information on the routes that the Directions API returned. First, add the following variables, which will help populate the reports:

var counter = 0;
var maxAttempts = 50;
var emoji = '';
var collision = '';

If there is a route to report on, the addCard function is called. It creates a new "card" element for each route that the app looks at. Add this function to your <script> tag:

var addCard = function(id, element, clear, detail) {
  var card = document.createElement("div");
  card.className = "card";
  // Add the response to the individual report created above
  var heading = document.createElement("div");
  // Set the class type based on clear value
  if (clear == true) {
  heading.className = "card-header route-found";
} else {
  heading.className = "card-header obstacle-found";
  }

  if (id == 0) {
    heading.innerHTML = emoji + " The route " + collision;
  } else {
    heading.innerHTML = emoji + " Route " + id + ' ' + collision;
  }

  var details = document.createElement("div");
  details.className = "card-details";
  details.innerHTML = "This " + detail + " obstacles.";

  card.appendChild(heading);
  card.appendChild(details);
  element.insertBefore(card, element.firstChild);
};

If there are no routes to display, the noRoutes function is called, which will tell the user that the app has exhausted the number of attempts allowed. Add this function to your script tag:

var noRoutes = function(element) {
  var card = document.createElement("div");
  card.className = "card";
  // Add the response to the individual report created above
  var heading = document.createElement("div");
  heading.className = "card-header no-route";
  emoji = '🛑';
  heading.innerHTML = emoji + " Ending search.";

  // Add details to the individual report
  var details = document.createElement("div");
  details.className = "card-details";
  details.innerHTML = "No clear route found in " + counter + " tries.";

  card.appendChild(heading);
  card.appendChild(details);
  element.insertBefore(card, element.firstChild);
};

Together, the addCard and noRoute functions will be used to add information about the routes to the sidebar.

To style these new card elements and their contents, add the following CSS to your <style> tag:

.card {
  display: inline-block;
  font-size: small;
  border-bottom: solid #D3D3D3 2px;
  margin-bottom: 6px;
  width: 100%;
}

.card-header {
  font-weight: bold;
  padding: 6px;
  font-weight: bold;
}

.no-route {
  background-color: #D3D3D3;
  color: #ff0000;
  
}

.obstacle-found {
  background-color: #D3D3D3;
  color: #FFFFFF;
}

.route-found {
  background-color: #33A532;
  color: #FFFFFF;
}

.card-details {
  padding: 3px 6px;
}

Prepare for routes

You will add sources and layers for the route and associated bounding box returned from the Directions API. The sources and layers will be used to draw the routes and the extent of the bounding boxes on the map. The line-color in 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 intersects with an obstacle.

Add the following code inside the map.on("load") function:

// Source and layer for the route
map.addSource("theRoute", {
  type: "geojson",
  data: {
    type: "Feature",
  },
});

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

// Source and layer for the bounding box
map.addSource("theBox", {
  type: "geojson",
  data: {
    type: "Feature"
  }
});
map.addLayer({
  id: "theBox",
  type: "fill",
  source: "theBox",
  layout: {},
  paint: {
    "fill-color": "#FFC300",
    "fill-opacity": 0.5,
    "fill-outline-color": "#FFC300"
  }
});

Hide the routes and boxes

Add functionality to hide the routes and boxes when the user removes entries from the Directions origin and destination text fields. Add code to change the visibility of these layers in the directions.on("clear") function:

directions.on("clear", function () {

  map.setLayoutProperty("theRoute", "visibility", "none");
  map.setLayoutProperty("theBox", "visibility", "none");

  counter = 0;
  reports.innerHTML = "";
});

Create the route reports

Next, you will create the reports that show whether a route intersects with any obstacles, and display them in the app's sidebar.

Check the routes for collisions

Now that the obstacles are Turf objects, you need to turn the routes into Turf objects too. Each code snippet in this section goes through this function step-by-step, and the complete function is at the end of this section.

First, set up the routes and reports variables:

directions.on("route", function(e) {
  var reports = document.getElementById("reports");
  var routes = e.route;

  // Hide the route and box by setting the opacity to zero
  map.setLayoutProperty("theRoute", "visibility", "none");
  map.setLayoutProperty("theBox", "visibility", "none");

  // Placeholder for upcoming code here
});

Track the search attempts

To keep the function from running infinitely, add a check to see if the number of attempts, which is tracked in counter, has exceeded the maxAttempts variable. You can adjust the number of attempts that the function will try by changing maxAttempts. Start your new function, in the directions.on("route") block:

if (counter >= maxAttempts) {
  noRoutes(reports);
} else {
  // Make each route visible
  routes.forEach(e => {
    // Placeholder for upcoming code here
  })
}

For every route you receive, you'll make the route and the bounding box visible. You'll take a polyline of the route's geometry and see where it intersects with any obstacles by passing them to the turf.lineIntersect method. Paste the following code inside the routes.forEach() function:

//Make each route visible
map.setLayoutProperty("theRoute", "visibility", "visible");
map.setLayoutProperty("theBox", "visibility", "visible");

//Get GeoJson LineString feature of route
var routeLine = polyline.toGeoJSON(e.geometry);
var intersects = turf.lineIntersect(obstacle, routeLine);

//Placeholder for upcoming code here

You'll find the bounding box of the routeLine by passing the routeLine into turf.bbox. The turf.bbox function returns the smallest enclosing box of the objects you pass to it. You'll restrict your search for a random point to the routeLine's bounding box. This helps keep your random waypoints near the route. The app will also draw the box on the map, so convert the resulting bounding box with turf.bboxPolygon.

// Create a bounding box around this route
// The app will find a random point in the new bbox
bbox = turf.bbox(routeLine);
polygon = turf.bboxPolygon(bbox);

Next, update the data for the route. This will update the route line on the map.

// Update the data for the route
// This will update the route line on the map
map.getSource("theRoute").setData(routeLine);

Apply the polygon to the box layer:

// Update the bounding box
map.getSource("theBox").setData(polygon);

Take the obstacle and the route, and see whether 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.

if (clear == true) {
  collision = "does not intersect any obstacles!";
  detail = "takes " + (e.duration / 60).toFixed(0) + " minutes and avoids";
  emoji =  "✔️";
  map.setPaintProperty("theRoute", "line-color", "#74c476");
  // Hide the box
  map.setLayoutProperty("theBox", "visibility", "none");
  // Reset the counter
  counter = 0;
} else {
  // Collision occurred, so increment the counter
  counter = counter + 1;
  // As the attempts increase, expand the search area
  // by a factor of the attempt count
  polygon = turf.transformScale(polygon, counter * 0.01);
  bbox = turf.bbox(polygon);
  collision = "is bad.";
  detail =
    "takes " + (e.duration / 60).toFixed(0) + " minutes and hits";
  emoji = "⚠️";
  map.setPaintProperty("theRoute", "line-color", "#de2d26");
};

Find a new route

When the function detects a collision, it will request a random point within the route's bounding box and add it as a new waypoint to the route. Doing so will return a new route from the Directions API. This is the mechanism the app will use to find a new route that doesn't collide with an obstacle.

// Add a randomly selected waypoint to get a new route from the Directions API
var randomWaypoint = turf.randomPoint(1, { bbox: bbox });
directions.setWaypoint(0, randomWaypoint["features"][0].geometry.coordinates);

And now you call addCard to update the sidebar with information on the route that you received. Add the following code after the closing bracket of the else statement:

// Add a new report section to the sidebar
addCard(counter, reports, clear, detail);

The complete route report function

The complete function should look as follows. Add it to the <script> tag, and reload the page. 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");
  let routes = e.route;
  
  // Hide all routes by setting the visibility to none.
  map.setLayoutProperty("theRoute", "visibility", "none");
  map.setLayoutProperty("theBox", "visibility", "none");

  if (counter >= maxAttempts) {
    noRoutes(reports);
  } else {
    routes.forEach(e => {
      // Make each route visible, by setting the opacity to 50%.
      map.setLayoutProperty("theRoute", "visibility", "visible");
      map.setLayoutProperty("theBox", "visibility", "visible");

      // Get GeoJson LineString feature of routes, intersections and bounding boxes
      var routeLine = polyline.toGeoJSON(e.geometry);
      var intersects = turf.lineIntersect(obstacle, routeLine);
      // Create a bounding box around this route, which we'll use to find a random point in.
      bbox = turf.bbox(routeLine);
      polygon = turf.bboxPolygon(bbox);

      // Update the data for the route, updating the visual.
      map.getSource("theRoute").setData(routeLine);
      // Update the bbox
      map.getSource("theBox").setData(polygon);

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

      if (clear == true) {
        // The route is clear
        collision = "does not intersect any obstacles!";
        detail =
          "takes " + (e.duration / 60).toFixed(0) + " minutes and avoids";
        emoji = '✔️';
        map.setPaintProperty("theRoute", "line-color", "#74c476");
        // Hide the box
        map.setLayoutProperty("theBox", "visibility", "none");

        // Reset the counter
        counter = 0;
        } else {
          // A Collision occurred, so increment the counter
          counter = counter + 1;

          // As the attempts increase, expand the search area
          // by a factor of the attempt count
          polygon = turf.transformScale(polygon, counter * 0.01);
          bbox = turf.bbox(polygon);
          collision = "is bad.";
          detail =
            "takes " + (e.duration / 60).toFixed(0) + " minutes and hits";
          emoji = "⚠️";
          map.setPaintProperty("theRoute", "line-color", "#de2d26");

          // Add a randomly selected waypoint to get a new route from the Directions API
          var randomWaypoint = turf.randomPoint(1, { bbox: bbox });
          directions.setWaypoint(
            0,
            randomWaypoint["features"][0].geometry.coordinates
          );
        }
        // Add a new report section to the sidebar
        addCard(counter, reports, clear, detail);
    });
  }
});

Finished product

Well done! You have used the Mapbox Directions API, Mapbox GL JS, Turf, and Polyline to create a directions-enabled map that checks the routes from the Directions API for collisions with obstacles. If the route hits an obstacle, a new route is created by adding a random waypoint to the directions request. This process repeats until a clean route is returned. You've also added a fail-safe to keep from indefinitely searching for a new route. The app does this by monitoring the number of attempts and stopping when it reaches the limit.

Your finished HTML file should look like this:

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <title>Route finder</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
  <!-- Import 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" />
  
  <!-- Import Turf and 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>
  
  <!-- Import 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" />

  <style>
    body {
      margin: 0;
      padding: 0;
    }

    #map {
      position: absolute;
      top: 0;
      bottom: 0;
      width: 100%;
    }
    
    .sidebar {
      position: absolute;
      margin: 20px 20px 30px 20px;
      width: 25%;
      top: 0;
      bottom: 0;
      padding: 20px;
      background-color: #FFFFFF;
      overflow-y: scroll;
      font-family: Open Sans, sans-serif;
    }
    
    .card {
      display: inline-block;
      font-size: small;
      border-bottom: solid #D3D3D3 2px;
      margin-bottom: 6px;
      width: 100%;
    }
    
    .card-header {
      font-weight: bold;
      padding: 6px;
      font-weight: bold;
    }

    .no-route {
      background-color: #D3D3D3;
      color: #ff0000;
      
    }

    .obstacle-found {
      background-color: #D3D3D3;
      color: #FFFFFF;
    }

    .route-found {
      background-color: #33A532;
      color: #FFFFFF;
    }
    
    .card-details {
      padding: 3px 6px;
    }
  </style>
</head>

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

  <script>
    mapboxgl.accessToken = "YOUR_MAPBOX_ACCESS_TOKEN";
    var map = new mapboxgl.Map({
      container: "map", // Specify the container ID
      style: "mapbox://styles/mapbox/light-v10", // Specify which map style to use
      center: [-84.5, 38.05], // Specify the starting position [lng, lat]
      zoom: 11 // Specify the starting zoom
    });
    
    var directions = new MapboxDirections({
      accessToken: mapboxgl.accessToken,
      unit: "metric",
      profile: "mapbox/driving",
      alternatives: false,
      geometries: "geojson",
      controls: { instructions: false },
      flyTo: false
    });

    map.addControl(directions, "top-right");
    map.scrollZoom.enable();
    
    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" });
    var bbox = [0, 0, 0, 0];
    var polygon = turf.bboxPolygon(bbox);
    
    map.on("load", function () {

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

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

     // Source and layer for the bounding box
     map.addSource("theBox", {
       type: "geojson",
       data: {
         type: "Feature"
       }
     });
     map.addLayer({
       id: "theBox",
       type: "fill",
       source: "theBox",
       layout: {},
       paint: {
         "fill-color": "#FFC300",
         "fill-opacity": 0.5,
         "fill-outline-color": "#FFC300"
       }
     });
    });
    
    var counter = 0;
    var maxAttempts = 50;
    var emoji = '';
    var collision = '';
    
    var addCard = function(id, element, clear, detail) {
      var card = document.createElement("div");
      card.className = "card";
      // Add the response to the individual report created above
      var heading = document.createElement("div");
      // Set the class type based on clear value
      if (clear == true) {
      heading.className = "card-header route-found";
    } else {
      heading.className = "card-header obstacle-found";
      }

      if (id == 0) {
        heading.innerHTML = emoji + " The route " + collision;
      } else {
        heading.innerHTML = emoji + " Route " + id + ' ' + collision;
      }

      var details = document.createElement("div");
      details.className = "card-details";
      details.innerHTML = "This " + detail + " obstacles.";

      card.appendChild(heading);
      card.appendChild(details);
      element.insertBefore(card, element.firstChild);
    };
    
    var noRoutes = function(element) {
      var card = document.createElement("div");
      card.className = "card";
      // Add the response to the individual report created above
      var heading = document.createElement("div");
      heading.className = "card-header no-route";
      emoji = '🛑';
      heading.innerHTML = emoji + " Ending search.";

      // Add details to the individual report
      var details = document.createElement("div");
      details.className = "card-details";
      details.innerHTML = "No clear route found in " + counter + " tries.";

      card.appendChild(heading);
      card.appendChild(details);
      element.insertBefore(card, element.firstChild);
    };
    
    directions.on("clear", function() {

      map.setLayoutProperty("theRoute", "visibility", "none");
      map.setLayoutProperty("theBox", "visibility", "none");

      counter = 0;
      reports.innerHTML = "";

    });
    
    directions.on("route", function(e) {
      var reports = document.getElementById("reports");
      var routes = e.route;

      // Hide the route and box by setting the opacity to zero
      map.setLayoutProperty("theRoute", "visibility", "none");
      map.setLayoutProperty("theBox", "visibility", "none");

      if (counter >= maxAttempts) {
        noRoutes(reports);
      } else {
        // Make each route visible
        routes.forEach(e => {
          // Make each route visible
          map.setLayoutProperty("theRoute", "visibility", "visible");
          map.setLayoutProperty("theBox", "visibility", "visible");

          // Get GeoJson LineString feature of route
          var routeLine = polyline.toGeoJSON(e.geometry);
          var intersects = turf.lineIntersect(obstacle, routeLine);

          // Create a bounding box around this route
          // The app will find a random point in the new bbox
          bbox = turf.bbox(routeLine);
          polygon = turf.bboxPolygon(bbox);
          
          // Update the data for the route
          // This will update the route line on the map
          map.getSource("theRoute").setData(routeLine);
          
          // Update the box
          map.getSource("theBox").setData(polygon);
          
          var clear = turf.booleanDisjoint(obstacle, routeLine);
          
          if (clear == true) {
            collision = "does not intersect any obstacles!";
            detail = "takes " + (e.duration / 60).toFixed(0) + " minutes and avoids";
            emoji =  "✔️";
            map.setPaintProperty("theRoute", "line-color", "#74c476");
            // Hide the box
            map.setLayoutProperty("theBox", "visibility", "none");
            // Reset the counter
            counter = 0;
          } else {
            // Collision occurred, so increment the counter
            counter = counter + 1;
            // As the attempts increase, expand the search area
            // by a factor of the attempt count
            polygon = turf.transformScale(polygon, counter * 0.01);
            bbox = turf.bbox(polygon);
            collision = "is bad.";
            detail =
              "takes " + (e.duration / 60).toFixed(0) + " minutes and hits";
            emoji = "⚠️";
            map.setPaintProperty("theRoute", "line-color", "#de2d26");
            
            // Add a randomly selected waypoint to get a new route from the Directions API
            var randomWaypoint = turf.randomPoint(1, { bbox: bbox });
            directions.setWaypoint(0, randomWaypoint["features"][0].geometry.coordinates);
          };
          // Add a new report section to the sidebar
          addCard(counter, reports, clear, detail);
        })
      }
    });
  </script>

</body>
</html>

Next steps

This example has several parameters that can change its behavior:

  • Update any values in the Directions plugin to change its behavior. For instance, you could set controls.instructions to true and display turn-by-turn directions for obstacle-free routes. For more information on these options, see the documentation for the Mapbox GL Directions plugin.
  • Change the value of the app's maxAttempts variable to raise or lower the maximum number of times the search can run.
  • Change the values in the Turf transformScale function to update the size of the waypoint search area.

Experiment with the app to get the behavior you want, or to expose controls to let users adjust them directly.

Was this page helpful?