All docsHelpTutorialsGenerate an optimized route with the Optimization API

Generate an optimized route with the Optimization API

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.

Getting started

Here are the resources that you'll use throughout this guide:

  • Mapbox account and access token. Sign up for an account at mapbox.com/signup. You can find your access tokens on your Account page.
  • Mapbox GL JS. Mapbox GL JS is our JavaScript library that uses WebGL to render interactive web maps from Mapbox GL styles.
  • Turf.js. Turf is a JavaScript library that allows you to add geospatial analysis to your map.
  • Mapbox Optimization API. Our Optimization API will help you generate optimal delivery routes for multiple stops across an entire fleet.

Initialize the map

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/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;
      }
      #map {
        position: absolute;
        top: 0;
        bottom: 0;
        right: 0;
        left: 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-v10', // 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.

Set up the map

Next, add the location of a delivery truck and the pick up location.

Add truck 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.

What is a callback?

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.

Note

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

Add warehouse location

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-15',
    'icon-size': 1
  },
  paint: {
    'text-color': '#3887be'
  }
});

Refresh your application and you'll see a map displaying the location of the delivery truck and the warehouse.

Add drop-off locations on click

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:

  • You'll use the default behavior of the Optimization API, which is to return round trip routes. This means that the first coordinate is both the start and end point of the trip. (This is helpful if the delivery truck needs to return to a depot.) Both the start and the end point will be counted toward the 12 coordinate limit.
  • The pick up location will also be counted toward the 12 coordinate limit.
  • As a result, the user can select up to 9 points — the 12 coordinate limit less the truck location as both the start and finish (2 coordinates) and the pick up location (1 coordinate).

Add a new layer

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'
  }
});

Add click listener

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.

Generate and add a route

Next, you'll build a request for the Optimization API, add the route from the response to the map, and style the route.

Create a layer with an empty source

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

Optimization API request format

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 or not 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.

Build the request

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
  }`;
}

Make the request

Use fetch to make the request. There are several things going on in the code below:

  • Make the request. Make a request to the Optimization API using the URL that you assembled in the assembleQueryURL() function in the previous step.
  • Create a GeoJSON feature collection. The API response will include an array of trips. Use the first trip in this array (trips[0]) to create a GeoJSON feature collection that contains the route.
  • Update the route data. Before the user interacts with the map, the 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:
    • If there is no trip available, the appropriate error will be displayed.
    • If there is a trip, get the route source and set the data equal to routeGeoJSON.
  • Display a warning message when reaching coordinate limit. The user can select up to nine points on the map. When the user reaches that limit, display a warning message that they cannot add any more points to the route.

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.

Add directionality to route line

With the current style, it's a bit 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.

Final product

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/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;
}
#map {
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 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 = '<your access token here>';
// Initialize a map
const map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v10', // 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-15',
'icon-size': 1
},
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>

Next steps

Read the Mapbox Optimization API documentation for more information or learn how to build a driver app for iOS and Android with our step-by-step tutorials: