In this tutorial, you will use the Mapbox Optimization API to build an application that creates an optimized delivery route from a warehouse to multiple locations. You will create a custom marker to represent a delivery vehicle, add a warehouse location to the map, add a marker icon when a user clicks on the map, and build an optimized route between those points in real time.
Here are the resources that you'll use throughout this guide:
Start by initializing a map with Mapbox GL JS. Create a new HTML file and use the code below to initialize a map centered on Detroit, Michigan. This code also references a couple scripts in the head
and declares a few JavaScript constants that you will need in later steps.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Delivery App</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
inset: 0;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const truckLocation = [-83.093, 42.376];
const warehouseLocation = [-83.083, 42.363];
const lastAtRestaurant = 0;
const keepTrack = [];
const pointHopper = {};
// Add your access token
mapboxgl.accessToken = 'YOUR_MAPBOX_ACCESS_TOKEN ';
// Initialize a map
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: truckLocation, // starting position
zoom: 12 // starting zoom
});
</script>
</body>
</html>
Open the file in your browser, and you will see a map centered on Detroit.
Next, add the location of a delivery truck and the pick up location.
There are several ways to add point data using Mapbox GL JS. In this guide you'll create an HTML DOM element for each marker and bind it to a coordinate using the Mapbox GL JS Marker
method. When you add a marker using the Marker method, you are attaching an empty div
to the point. You'll need to specify the style of the marker before adding it to the map.
Start by adding the truck
class between the style
tags.
.truck {
width: 20px;
height: 20px;
border: 2px solid #fff;
border-radius: 50%;
background: #3887be;
pointer-events: none;
}
Next, add a marker to the map using mapboxgl.Marker
. To make sure the rest of the code can execute, it needs to live in a callback function that is executed when the map is finished loading.
Initializing the map on the page does more than create a container in the
map
div: it also tells the browser to request the Mapbox Studio style you
created in part 1. This can take variable amounts of time depending on how
quickly the Mapbox server can respond to that request, and everything else
you're going to add in the code relies on that style being loaded onto the
map. As such, it's important to make sure the style is loaded before any more
code is executed.Fortunately, the map object can tell your browser about
certain events that occur when the map's state changes. One of these events is
load
, which is emitted when the style has been loaded onto the map. When you
use the map.on
method, you can make sure that the rest of your code is
executed only after the map has finished loading by placing it in a
callback function that is
called when the load
event occurs.
In the previous step, you declared a constant called truckLocation
that contains an array of two numbers (longitude and latitude). Use truckLocation
as the coordinate for the truck marker using setLngLat
.
In this guide you are using a static point as the location of the delivery truck. In your application, you could set the longitude and latitude using the location of physical devices such as the location of a phone or tablet, collecting pings from IoT devices, or GPS signals from vehicle locations.
Use the code below to add a marker once the map has loaded.
map.on('load', async () => {
const marker = document.createElement('div');
marker.classList = 'truck';
// Create a new marker
new mapboxgl.Marker(marker).setLngLat(truckLocation).addTo(map);
});
Now add the warehouse location to the map. This will be where the delivery truck has to go to pick up items to deliver. Start by creating a GeoJSON feature collection that includes a single point feature that describes where the warehouse is located. The Turf.js featureCollection
method is a convenient way to turn a coordinate, or series of coordinates, into a GeoJSON feature collection. Add the code below before your map.on('load', () => {});
callback. Note that the warehouseLocation
constant is a latitude, longitude pair that was declared in the first code block you copied when initializing your map.
// Create a GeoJSON feature collection for the warehouse
const warehouse = turf.featureCollection([turf.point(warehouseLocation)]);
Then, within the map.on('load', () => {});
callback, create two new layers — a circle layer and a symbol layer. Both layers will use the warehouse
GeoJSON feature collection as the data source.
// Create a circle layer
map.addLayer({
id: 'warehouse',
type: 'circle',
source: {
data: warehouse,
type: 'geojson'
},
paint: {
'circle-radius': 20,
'circle-color': 'white',
'circle-stroke-color': '#3887be',
'circle-stroke-width': 3
}
});
// Create a symbol layer on top of circle layer
map.addLayer({
id: 'warehouse-symbol',
type: 'symbol',
source: {
data: warehouse,
type: 'geojson'
},
layout: {
'icon-image': 'grocery',
'icon-size': 1.5
},
paint: {
'text-color': '#3887be'
}
});
Refresh your application and you'll see a map displaying the location of the delivery truck and the warehouse.
In an application that generates optimized routes between several points, there are many different ways you could generate the points: a user could input addresses, a user could select coordinates on the map, or you could pull data in from an external source. In this guide, you'll allow the user to select points by clicking on the map.
A request to the Optimization API must contain 2-12 coordinates. In this guide:
Start by creating a new layer that contains all the drop-off locations.
Create an empty GeoJSON feature collection. Later you'll store drop-off locations in this feature collection when the map is clicked. Add this directly after the warehouse
constant you added above.
// Create an empty GeoJSON feature collection for drop-off locations
const dropoffs = turf.featureCollection([]);
Then, within map.on('load', () => {});
, add a new layer called dropoffs-symbol
, which will display all the drop-off locations. Layers in Mapbox GL JS require a source
, but the user hasn't selected any drop-off locations yet, so you will need to provide an empty GeoJSON feature collection as a starting point. When the map is first initialized, the data source will be the empty dropoffs
feature collection you defined above. Later, after the user clicks on the map, the points that are clicked will be added to the dropoffs
feature collection and the data source for the layer will update.
map.addLayer({
id: 'dropoffs-symbol',
type: 'symbol',
source: {
data: dropoffs,
type: 'geojson'
},
layout: {
'icon-allow-overlap': true,
'icon-ignore-placement': true,
'icon-image': 'marker-15'
}
});
You'll need to listen for when a user clicks on the map. Add a click listener inside the map.on('load', () => {});
callback.
map.on('load', async () => {
// Code from previous steps
// Listen for a click on the map
await map.on('click', addWaypoints);
});
After map.on('load', () => {});
, create an addWaypoints
function with two new functions newDropoff
and updateDropoffs
, which will be called every time the map is clicked.
async function addWaypoints(event) {
// When the map is clicked, add a new drop off point
// and update the `dropoffs-symbol` layer
await newDropoff(map.unproject(event.point));
updateDropoffs(dropoffs);
}
Build out the newDropoff
and updateDropoffs
functions. When a user clicks on the map, you will make a new request for delivery. Two things will happen when a user makes a new request: you'll create a new drop-off up location and you'll update the data source for the dropoffs-symbol
layer.
Add the following code outside of your map.on('load', () => {});
callback.
async function newDropoff(coordinates) {
// Store the clicked point as a new GeoJSON feature with
// two properties: `orderTime` and `key`
const pt = turf.point([coordinates.lng, coordinates.lat], {
orderTime: Date.now(),
key: Math.random()
});
dropoffs.features.push(pt);
}
function updateDropoffs(geojson) {
map.getSource('dropoffs-symbol').setData(geojson);
}
Refresh your application. When you click on the map, a new marker symbol that is a drop-off location will be added to the map.
Next, you'll build a request for the Optimization API, add the route from the response to the map, and style the route.
Like in the previous step, create an empty GeoJSON feature collection, which will later contain the route after the map is clicked and a route is generated. Add this line of code immediately after you declare the dropoffs
constant.
// Create an empty GeoJSON feature collection, which will be used as the data source for the route before users add any new data
const nothing = turf.featureCollection([]);
Within map.on('load', () => {});
, directly after the code you wrote to add the location of the warehouse as a layer, add a new layer with an empty source. You will use this to display the route after you make the API request.
map.addSource('route', {
type: 'geojson',
data: nothing
});
map.addLayer(
{
id: 'routeline-active',
type: 'line',
source: 'route',
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#3887be',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 3, 22, 12]
}
},
'waterway-label'
);
The Optimization API returns a duration-optimized route between two and 12 input coordinates.
An Optimization API request has two required parameters:
profile
: This is the mode of transportation used by the request. Choose from one of the Mapbox Directions routing profile IDs (mapbox/driving
, mapbox/walking
, mapbox/cycling
, and mapbox/driving-traffic
). In this tutorial, you will use the mapbox/driving
profile.coordinates
: This is a semicolon-separated list of {longitude},{latitude}
coordinates. There must be between 2 and 12 coordinates, and the first coordinate is the start and end point of the trip by default. In this guide, the list of coordinates includes the starting location of the truck, the location of the warehouse, and up to nine drop-off locations.An Optimization API request that uses these two required parameters looks like this:
https://api.mapbox.com/optimized-trips/v1/mapbox/driving/-122.42,37.78;-122.45,37.91;-122.48,37.73?access_token=YOUR_MAPBOX_ACCESS_TOKEN
The Optimization API also accepts optional parameters that can be used to customize the query. For this app, you will be using several optional parameters:
distributions
: This is a semicolon-separated list of number pairs that correspond with the coordinates list. The first number indicates the index to the coordinate of the pick-up location in the coordinates list, and the second number indicates the index to the coordinate of the drop-off location in the coordinates list. Each pair must contain exactly two numbers. Pick-up and drop-off locations in one pair cannot be the same. The returned solution will visit pick-up locations before visiting drop-off locations.overview
: This parameter tells the Optimization API what type of overview geometry to return. For this app, you will specify overview=full
to get back the most detailed geometry available.steps
: This parameter tells the Optimization API whether it should return turn-by-turn instructions. Since this app should provide users with turn-by-turn instructions, you will use steps=true
.geometries
: This parameter tells the Optimization API what format the returned geometry should be. For this app, you will request geometries=geojson
to get GeoJSON data.source
: This parameter tells the Optimization API which coordinates to start the route at. For this app, you want the route to start at the truck's location, which will be the first item in the coordinates list, so you will specify source=first
in the API request.To learn more about the Optimization API and its other optional parameters, explore the Optimization API documentation.
Next, add the following after the updateDropoffs
function you added in the previous section. This function will build the request to the Optimization API to generate a route.
// Here you'll specify all the parameters necessary for requesting a response from the Optimization API
function assembleQueryURL() {
// Store the location of the truck in a constant called coordinates
const coordinates = [truckLocation];
const distributions = [];
keepTrack = [truckLocation];
// Create an array of GeoJSON feature collections for each point
const restJobs = Object.keys(pointHopper).map((key) => pointHopper[key]);
// If there are any orders from this restaurant
if (restJobs.length > 0) {
// Check to see if the request was made after visiting the restaurant
const needToPickUp =
restJobs.filter((d) => {
return d.properties.orderTime > lastAtRestaurant;
}).length > 0;
// If the request was made after picking up from the restaurant,
// Add the restaurant as an additional stop
if (needToPickUp) {
const restaurantIndex = coordinates.length;
// Add the restaurant as a coordinate
coordinates.push(warehouseLocation);
// push the restaurant itself into the array
keepTrack.push(pointHopper.warehouse);
}
for (const job of restJobs) {
// Add dropoff to list
keepTrack.push(job);
coordinates.push(job.geometry.coordinates);
// if order not yet picked up, add a reroute
if (needToPickUp && job.properties.orderTime > lastAtRestaurant) {
distributions.push(`${restaurantIndex},${coordinates.length - 1}`);
}
}
}
// Set the profile to `driving`
// Coordinates will include the current location of the truck,
return `https://api.mapbox.com/optimized-trips/v1/mapbox/driving/${coordinates.join(
';'
)}?distributions=${distributions.join(
';'
)}&overview=full&steps=true&geometries=geojson&source=first&access_token=${
mapboxgl.accessToken
}`;
}
Use fetch to make the request. There are several things going on in the code below:
assembleQueryURL()
function in the previous step.trips
. Use the first trip in this array (trips[0]
) to create a GeoJSON feature collection that contains the route.route
source is an empty GeoJSON feature collection. Update the route
source from an empty feature collection to the trip
returned in the API response. Then check if a trip exists:
route
source and set the data equal to routeGeoJSON
.All this will happen within the existing newDropoff
function, meaning there will be a request to the Optimization API every time there is a new dropoff location specified — whenever a user clicks on the map.
Update the existing newDropoff
function and add the API request using the code below.
async function newDropoff(coordinates) {
// Store the clicked point as a new GeoJSON feature with
// two properties: `orderTime` and `key`
const pt = turf.point([coordinates.lng, coordinates.lat], {
orderTime: Date.now(),
key: Math.random()
});
dropoffs.features.push(pt);
pointHopper[pt.properties.key] = pt;
// Make a request to the Optimization API
const query = await fetch(assembleQueryURL(), { method: 'GET' });
const response = await query.json();
// Create an alert for any requests that return an error
if (response.code !== 'Ok') {
const handleMessage =
response.code === 'InvalidInput'
? 'Refresh to start a new route. For more information: https://docs.mapbox.com/api/navigation/optimization/#optimization-api-errors'
: 'Try a different point.';
alert(`${response.code} - ${response.message}\n\n${handleMessage}`);
// Remove invalid point
dropoffs.features.pop();
delete pointHopper[pt.properties.key];
return;
}
// Create a GeoJSON feature collection
const routeGeoJSON = turf.featureCollection([
turf.feature(response.trips[0].geometry)
]);
// Update the `route` source by getting the route source
// and setting the data equal to routeGeoJSON
map.getSource('route').setData(routeGeoJSON);
}
Refresh your app, select points on the map, and you will see a route drawn between all points.
With the current style, it's difficult to understand the directionality of the route line. Add another layer called routearrows
to help the map viewer understand which way the truck will be traveling along the route. The routearrows
layer will use the same data source as the routeline-active
layer. Instead of a line layer, use a symbol layer with arrows indicating the directionality of routes along the line.
Add the following code inside the map.on('load', () => {});
callback function.
map.addLayer(
{
id: 'routearrows',
type: 'symbol',
source: 'route',
layout: {
'symbol-placement': 'line',
'text-field': '▶',
'text-size': ['interpolate', ['linear'], ['zoom'], 12, 24, 22, 60],
'symbol-spacing': ['interpolate', ['linear'], ['zoom'], 12, 30, 22, 160],
'text-keep-upright': false
},
paint: {
'text-color': '#3887be',
'text-halo-color': 'hsl(55, 11%, 96%)',
'text-halo-width': 3
}
},
'waterway-label'
);
Refresh your app, select points on the map, and you will see a route with arrows drawn between all points.
You used the Optimization API to build a web application that allows a user to choose multiple points on a map, makes an API request, and displays an optimized route in real-time.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Demo: Generate an optimized route with the Optimization API</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://npmcdn.com/@turf/turf/turf.min.js"></script>
<script src="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.js"></script>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v3.3.0/mapbox-gl.css"
rel="stylesheet"
/>
<style>
body {
margin: 0;
padding: 0;
}
#map {
position: absolute;
inset: 0;
}
.truck {
width: 20px;
height: 20px;
border: 2px solid #fff;
border-radius: 50%;
background: #3887be;
pointer-events: none;
}
</style>
</head>
<body>
<div id="map"></div>
<script>
const truckLocation = [-83.093, 42.376];
const warehouseLocation = [-83.083, 42.363];
const lastAtRestaurant = 0;
let keepTrack = [];
const pointHopper = {};
// Add your access token
mapboxgl.accessToken = '{{MAPBOX_ACCESS_TOKEN}}';
// Initialize a map
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v11', // stylesheet location
center: truckLocation, // starting position
zoom: 12 // starting zoom
});
const warehouse = turf.featureCollection([turf.point(warehouseLocation)]);
// Create an empty GeoJSON feature collection for drop off locations
const dropoffs = turf.featureCollection([]);
// Create an empty GeoJSON feature collection, which will be used as the data source for the route before users add any new data
const nothing = turf.featureCollection([]);
map.on('load', async () => {
const marker = document.createElement('div');
marker.classList = 'truck';
// Create a new marker
new mapboxgl.Marker(marker).setLngLat(truckLocation).addTo(map);
// Create a circle layer
map.addLayer({
id: 'warehouse',
type: 'circle',
source: {
data: warehouse,
type: 'geojson'
},
paint: {
'circle-radius': 20,
'circle-color': 'white',
'circle-stroke-color': '#3887be',
'circle-stroke-width': 3
}
});
// Create a symbol layer on top of circle layer
map.addLayer({
id: 'warehouse-symbol',
type: 'symbol',
source: {
data: warehouse,
type: 'geojson'
},
layout: {
'icon-image': 'grocery',
'icon-size': 1.5
},
paint: {
'text-color': '#3887be'
}
});
map.addLayer({
id: 'dropoffs-symbol',
type: 'symbol',
source: {
data: dropoffs,
type: 'geojson'
},
layout: {
'icon-allow-overlap': true,
'icon-ignore-placement': true,
'icon-image': 'marker-15'
}
});
map.addSource('route', {
type: 'geojson',
data: nothing
});
map.addLayer(
{
id: 'routeline-active',
type: 'line',
source: 'route',
layout: {
'line-join': 'round',
'line-cap': 'round'
},
paint: {
'line-color': '#3887be',
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 3, 22, 12]
}
},
'waterway-label'
);
map.addLayer(
{
id: 'routearrows',
type: 'symbol',
source: 'route',
layout: {
'symbol-placement': 'line',
'text-field': '▶',
'text-size': [
'interpolate',
['linear'],
['zoom'],
12,
24,
22,
60
],
'symbol-spacing': [
'interpolate',
['linear'],
['zoom'],
12,
30,
22,
160
],
'text-keep-upright': false
},
paint: {
'text-color': '#3887be',
'text-halo-color': 'hsl(55, 11%, 96%)',
'text-halo-width': 3
}
},
'waterway-label'
);
// Listen for a click on the map
await map.on('click', addWaypoints);
});
async function addWaypoints(event) {
// When the map is clicked, add a new drop off point
// and update the `dropoffs-symbol` layer
await newDropoff(map.unproject(event.point));
updateDropoffs(dropoffs);
}
async function newDropoff(coordinates) {
// Store the clicked point as a new GeoJSON feature with
// two properties: `orderTime` and `key`
const pt = turf.point([coordinates.lng, coordinates.lat], {
orderTime: Date.now(),
key: Math.random()
});
dropoffs.features.push(pt);
pointHopper[pt.properties.key] = pt;
// Make a request to the Optimization API
const query = await fetch(assembleQueryURL(), { method: 'GET' });
const response = await query.json();
// Create an alert for any requests that return an error
if (response.code !== 'Ok') {
const handleMessage =
response.code === 'InvalidInput'
? 'Refresh to start a new route. For more information: https://docs.mapbox.com/api/navigation/optimization/#optimization-api-errors'
: 'Try a different point.';
alert(`${response.code} - ${response.message}\n\n${handleMessage}`);
// Remove invalid point
dropoffs.features.pop();
delete pointHopper[pt.properties.key];
return;
}
// Create a GeoJSON feature collection
const routeGeoJSON = turf.featureCollection([
turf.feature(response.trips[0].geometry)
]);
// Update the `route` source by getting the route source
// and setting the data equal to routeGeoJSON
map.getSource('route').setData(routeGeoJSON);
}
function updateDropoffs(geojson) {
map.getSource('dropoffs-symbol').setData(geojson);
}
// Here you'll specify all the parameters necessary for requesting a response from the Optimization API
function assembleQueryURL() {
// Store the location of the truck in a variable called coordinates
const coordinates = [truckLocation];
const distributions = [];
let restaurantIndex;
keepTrack = [truckLocation];
// Create an array of GeoJSON feature collections for each point
const restJobs = Object.keys(pointHopper).map(
(key) => pointHopper[key]
);
// If there are actually orders from this restaurant
if (restJobs.length > 0) {
// Check to see if the request was made after visiting the restaurant
const needToPickUp =
restJobs.filter((d) => d.properties.orderTime > lastAtRestaurant)
.length > 0;
// If the request was made after picking up from the restaurant,
// Add the restaurant as an additional stop
if (needToPickUp) {
restaurantIndex = coordinates.length;
// Add the restaurant as a coordinate
coordinates.push(warehouseLocation);
// push the restaurant itself into the array
keepTrack.push(pointHopper.warehouse);
}
for (const job of restJobs) {
// Add dropoff to list
keepTrack.push(job);
coordinates.push(job.geometry.coordinates);
// if order not yet picked up, add a reroute
if (needToPickUp && job.properties.orderTime > lastAtRestaurant) {
distributions.push(
`${restaurantIndex},${coordinates.length - 1}`
);
}
}
}
// Set the profile to `driving`
// Coordinates will include the current location of the truck,
return `https://api.mapbox.com/optimized-trips/v1/mapbox/driving/${coordinates.join(
';'
)}?distributions=${distributions.join(
';'
)}&overview=full&steps=true&geometries=geojson&source=first&access_token=${
mapboxgl.accessToken
}`;
}
</script>
</body>
</html>
Congratulations! You've built a web application that creates an optimized delivery route from a warehouse to multiple locations and shows the routes on a map.