Route finding with the Directions API and Turf.js
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.
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.14.1/mapbox-gl.js></script>
<link href=https://api.tiles.mapbox.com/mapbox-gl-js/v2.14.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-v11', // 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.14.1/mapbox-gl.js"></script><linkhref="https://api.tiles.mapbox.com/mapbox-gl-js/v2.14.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><linkrel="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 IDstyle: 'mapbox://styles/mapbox/light-v11', // Specify which map style to usecenter: [-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 boxmap.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 aboveconst heading = document.createElement('div');// Set the class type based on clear valueheading.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 aboveconst heading = document.createElement('div');heading.className = 'card-header no-route';emoji = '🛑';heading.innerHTML = `${emoji} Ending search.`; // Add details to the individual reportconst 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 zeromap.setLayoutProperty('theRoute', 'visibility', 'none');map.setLayoutProperty('theBox', 'visibility', 'none'); if (counter >= maxAttempts) {noRoutes(reports);} else {// Make each route visiblefor (const route of event.route) {// Make each route visiblemap.setLayoutProperty('theRoute', 'visibility', 'visible');map.setLayoutProperty('theBox', 'visibility', 'visible'); // Get GeoJSON LineString feature of routeconst routeLine = polyline.toGeoJSON(route.geometry); // Create a bounding box around this route// The app will find a random point in the new bboxbbox = turf.bbox(routeLine);polygon = turf.bboxPolygon(bbox); // Update the data for the route// This will update the route line on the mapmap.getSource('theRoute').setData(routeLine); // Update the boxmap.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 boxmap.setLayoutProperty('theBox', 'visibility', 'none');// Reset the countercounter = 0;} else {// Collision occurred, so increment the countercounter = counter + 1;// As the attempts increase, expand the search area// by a factor of the attempt countpolygon = 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 APIconst randomWaypoint = turf.randomPoint(1, { bbox: bbox });directions.setWaypoint(0,randomWaypoint['features'][0].geometry.coordinates);}// Add a new report section to the sidebaraddCard(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
totrue
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.