All docsHelpTutorialsRoute finding with the Directions API and Turf.js

Route finding with the Directions API and Turf.js

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 lang='en'>

<head>
  <meta charset='utf-8' />
  <title>Route finder</title>
  <meta name='viewport' content='width=device-width, initial-scale=1' />
  <!-- Import Mapbox GL JS -->
  <script src=https://api.tiles.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.js></script>
  <link href=https://api.tiles.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.css rel="stylesheet" />
  <style>
    body {
      margin: 0;
      padding: 0;
      font-family: 'Open Sans', sans-serif;
    }

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

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

  <script>
    mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN';
    const 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: #fff;
  overflow-y: scroll;
}

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.

const 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:

const 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 constants in the <script> tag:

const obstacle = turf.buffer(clearances, 0.25, { units: 'kilometers' });
const bbox = [0, 0, 0, 0];
const 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', () => {
  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 constants, which will help populate the reports:

const counter = 0;
const maxAttempts = 50;
const emoji = '';
const collision = '';
const detail = '';
const reports = document.getElementById('reports');

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:

function addCard(id, element, clear, detail) {
  const card = document.createElement('div');
  card.className = 'card';
  // Add the response to the individual report created above
  const heading = document.createElement('div');
  // Set the class type based on clear value
  heading.className =
    clear === true ? 'card-header route-found' : 'card-header obstacle-found';
  heading.innerHTML =
    id === 0
      ? `${emoji} The route ${collision}`
      : `${emoji} Route ${id} ${collision}`;

  const 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:

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

  // Add details to the individual report
  const 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 {
  font-size: small;
  border-bottom: solid #d3d3d3 2px;
  margin-bottom: 6px;
}

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

.no-route {
  background-color: #d3d3d3;
  color: #f00;
}

.obstacle-found {
  background-color: #d3d3d3;
  color: #fff;
}

.route-found {
  background-color: #33a532;
  color: #fff;
}

.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', () => {
  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 constants:

directions.on('route', (event) => {
  // 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 constant. 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
  for (const route of event.route) {
    // 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 for...of statement:

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

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

// 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 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.

const 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
const 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', (event) => {
  // 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
    for (const route of event.route) {
      // Make each route visible
      map.setLayoutProperty('theRoute', 'visibility', 'visible');
      map.setLayoutProperty('theBox', 'visibility', 'visible');

      // Get GeoJSON LineString feature of route
      const routeLine = polyline.toGeoJSON(properties.geometry);

      // 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);

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

      if (clear === true) {
        collision = 'does not intersect any obstacles!';
        detail = `takes ${(properties.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 ${(properties.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
        const 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.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Route finding with the Directions API and Turf.js</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Import Mapbox GL JS -->
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v2.5.1/mapbox-gl.css"
rel="stylesheet"
/>
<!-- 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"
/>
<!-- Import 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>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Open Sans', sans-serif;
}
#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: #fff;
overflow-y: scroll;
}
.card {
font-size: small;
border-bottom: solid #d3d3d3 2px;
margin-bottom: 6px;
}
.card-header {
font-weight: bold;
padding: 6px;
}
.no-route {
background-color: #d3d3d3;
color: #f00;
}
.obstacle-found {
background-color: #d3d3d3;
color: #fff;
}
.route-found {
background-color: #33a532;
color: #fff;
}
.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 access token here>';
const 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
});
const 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();
const 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"
}
}
]
};
const obstacle = turf.buffer(clearances, 0.25, { units: 'kilometers' });
let bbox = [0, 0, 0, 0];
let polygon = turf.bboxPolygon(bbox);
map.on('load', () => {
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'
}
});
});
let counter = 0;
const maxAttempts = 50;
let emoji = '';
let collision = '';
let detail = '';
const reports = document.getElementById('reports');
function addCard(id, element, clear, detail) {
const card = document.createElement('div');
card.className = 'card';
// Add the response to the individual report created above
const heading = document.createElement('div');
// Set the class type based on clear value
heading.className =
clear === true
? 'card-header route-found'
: 'card-header obstacle-found';
heading.innerHTML =
id === 0
? `${emoji} The route ${collision}`
: `${emoji} Route ${id} ${collision}`;
const details = document.createElement('div');
details.className = 'card-details';
details.innerHTML = `This ${detail} obstacles.`;
card.appendChild(heading);
card.appendChild(details);
element.insertBefore(card, element.firstChild);
}
function noRoutes(element) {
const card = document.createElement('div');
card.className = 'card';
// Add the response to the individual report created above
const heading = document.createElement('div');
heading.className = 'card-header no-route';
emoji = '🛑';
heading.innerHTML = `${emoji} Ending search.`;
// Add details to the individual report
const 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', () => {
map.setLayoutProperty('theRoute', 'visibility', 'none');
map.setLayoutProperty('theBox', 'visibility', 'none');
counter = 0;
reports.innerHTML = '';
});
directions.on('route', (event) => {
// 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
for (const route of event.route) {
// Make each route visible
map.setLayoutProperty('theRoute', 'visibility', 'visible');
map.setLayoutProperty('theBox', 'visibility', 'visible');
// Get GeoJSON LineString feature of route
const routeLine = polyline.toGeoJSON(route.geometry);
// 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);
const clear = turf.booleanDisjoint(obstacle, routeLine);
if (clear === true) {
collision = 'does not intersect any obstacles!';
detail = `takes ${(route.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 ${(route.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
const 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 constant 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.