Multi-Vehicle Delivery Route Optimizer

Solve the Vehicle Routing Problem with a real optimization API and map.

Introduction

30 Second Summary

Every time you order something online, a logistics team has to figure out how to get dozens of packages to dozens of addresses using a limited number of trucks. Getting the order and assignments right saves hours of driving and thousands in fuel costs.

In this project, you will build a browser-based fleet route optimizer that solves this exact problem. You will drop delivery pins on an interactive map, define a vehicle fleet, and watch a real optimization engine split and sequence stops across multiple drivers with color-coded routes drawn on actual roads.

What You'll Build

You will have a live map where you click to place delivery stops, hit Optimize, and watch a fleet of trucks fan out across the city on efficient, road-following routes you can show anyone in your browser.

By the end of this project, you'll have:

  • An interactive delivery map where you right-click to place a warehouse depot, left-click to add numbered stops, and see a naive route connecting them in random order.
  • A working connection to the OpenRouteService Optimization API (powered by VROOM) that solves the Vehicle Routing Problem and draws optimized routes on real roads.
  • A multi-vehicle fleet view with color-coded routes, per-driver stats, and visible proof that capacity constraints force fleet expansion.
  • Secret Mission: Add delivery time windows so the solver respects scheduling constraints like "deliver between 9-11am."

Are there any prerequisites?

You should be comfortable with JavaScript (async/await, the Fetch API, and DOM manipulation) and know the basics of HTML and CSS. No prior mapping or optimization experience is needed.

Before We Start

Before We Start...

Before we dive in, let's lock in what you're about to build and why it matters.

Set Up Your Project

To optimize delivery routes, you need two things: an optimization engine that solves the routing math, and a map to visualize the results.

In this step, you will get a free API key from OpenRouteService (the optimization engine) and create the project skeleton that renders an interactive map in your browser.

In this step, get ready to:
  • Sign up for a free OpenRouteService API key.
  • Create a project folder with three files for your route optimizer.
  • Open the project in your browser and confirm the map renders.
Get your OpenRouteService API key

OpenRouteService provides a free optimization API powered by the VROOM engine. You need an API key to authenticate your requests.

  • Open the OpenRouteService login page in your browser.
  • Click Sign in with GitHub to create an account (or sign in with an existing account).
  • After signing in, you land on the API Key tab. You will see your Basic Key displayed with a Copy button next to it.
  • Click Copy to copy your API key to your clipboard. Keep it handy for later.

What is OpenRouteService?

OpenRouteService (ORS) is a free routing and optimization API hosted by HeiGIT. Its optimization endpoint runs the VROOM engine, which solves Vehicle Routing Problems. The free Standard plan gives you 500 optimization requests per day, far more than you need for this project.

Can't see your API key?

If the dashboard stalls or shows an error, try refreshing the page. The key is generated automatically on account creation. If issues persist, contact support@smartmobility.heigit.org.

help me get my OpenRouteService API key

Create your project files

Your route optimizer is a browser-based app with three files: an HTML page for structure, a CSS file for styling, and a JavaScript file for map logic.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir fleet-route-optimizer
cd fleet-route-optimizer
  • Create the three project files by running:
touch index.html style.css app.js
  • Confirm the files exist by running:
ls

You should see index.html, style.css, and app.js listed in the output.

  • Open the fleet-route-optimizer folder in your code editor. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the fleet-route-optimizer folder, and click Open.

Now you will add content to each file. Start with the HTML.

  • Open index.html in the editor and paste the first section of the page structure:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fleet Route Optimizer</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="app">
        <div id="sidebar">
            <h1>Fleet Route Optimizer</h1>
            <div id="controls">
                <div class="control-group">
                    <h3>Depot</h3>
                    <p id="depot-info">Right-click map to set depot</p>
                </div>
                <div class="control-group">
                    <h3>Deliveries (<span id="delivery-count">0</span>)</h3>
                    <p>Left-click map to add delivery stops</p>
                    <div id="delivery-list"></div>
                </div>

What does this code do?

  • The <head> loads Leaflet 1.9.4 from a CDN. Leaflet is a lightweight JavaScript library for interactive maps.
  • The <body> creates a two-panel layout: a sidebar on the left for controls, and a map container on the right.
  • The sidebar sections define the depot info display, delivery list, and fleet configuration areas you will use later.
  • Directly below the code you just pasted (still inside index.html), add the rest of the page structure:
                <div class="control-group">
                    <h3>Fleet Configuration</h3>
                    <label>Number of vehicles:
                        <input type="number" id="vehicle-count" min="1" max="5" value="3">
                    </label>
                    <label>Capacity per vehicle:
                        <input type="number" id="vehicle-capacity" min="1" max="20" value="4">
                    </label>
                </div>
                <div class="control-group">
                    <button id="optimize-btn" disabled>Optimize Routes</button>
                    <button id="clear-btn">Clear All</button>
                </div>
            </div>
            <div id="results" class="hidden">
                <h3>Optimization Results</h3>
                <div id="summary"></div>
                <div id="unassigned-info"></div>
                <div id="route-details"></div>
            </div>
        </div>
        <div id="map"></div>
    </div>
    <script src="app.js"></script>
</body>
</html>

What does this section add?

  • Fleet configuration inputs let you set the number of vehicles (1-5) and capacity per vehicle (1-20).
  • The Optimize Routes button starts disabled and only activates once you have placed a depot and deliveries.
  • The results div is hidden by default and displays optimization output after the API responds.
  • The <div id="map"> is where Leaflet renders the interactive map.
  • Save index.html.
  • Open index.html in your browser by double-clicking the file in Finder (macOS) or File Explorer (Windows), or by right-clicking the file in VS Code and selecting Open with Live Server if you have that extension.

You should see unstyled text showing "Fleet Route Optimizer" and the form labels. The page will look plain because you have not added CSS yet.

  • Now open app.js in the editor and paste the following map initialization code:
// API configuration
const ORS_API_KEY = 'your-api-key-here';
const ORS_OPTIMIZATION_URL = 'https://api.openrouteservice.org/optimization';

// Color palette for multi-vehicle routes
const VEHICLE_COLORS = ['#e63946', '#457b9d', '#2a9d8f', '#e9c46a', '#f4a261'];

// Application state variables
let map;
let depot = null;
let deliveries = [];
let depotMarker = null;
let deliveryMarkers = [];
let routeLayers = [];
let naiveRouteLayer = null;

// Initialize the Leaflet map centered on London
function initMap() {
    map = L.map('map').setView([51.505, -0.09], 13);
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);
}

initMap();

What does this code do?

  • ORS_API_KEY stores your API key (you will paste your real key shortly).
  • ORS_OPTIMIZATION_URL points to the OpenRouteService optimization endpoint.
  • The state variables (depot, deliveries, routeLayers) track map objects you will add in later steps.
  • initMap() creates a Leaflet map using OpenStreetMap tiles, centered on London at zoom level 13.
  • Save app.js and refresh your browser.

You should now see an interactive map on the right side of the page. You can zoom in/out and pan around London.

Map not showing up?

  • Check that the <script src="app.js"></script> line is at the bottom of your HTML body, after the <div id="map">.
  • Open your browser's Developer Console (Cmd+Option+J on macOS, Ctrl+Shift+J on Windows) and look for red errors. A common issue is a typo in the Leaflet CDN URL.
  • Make sure you saved both files before refreshing.

my Leaflet map is not rendering in the browser

Now add the styling. The CSS creates a dark sidebar on the left and a full-height map on the right.

  • Open style.css and paste the layout foundation:
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    height: 100vh;
    overflow: hidden;
}

#app {
    display: flex;
    height: 100vh;
}

#sidebar {
    width: 350px;
    padding: 20px;
    overflow-y: auto;
    background: #1a1a2e;
    color: #eee;
}

#sidebar h1 {
    font-size: 1.4rem;
    margin-bottom: 20px;
    color: #00d4aa;
}

What does this code do?

  • The CSS reset removes default browser margins so the layout fills the viewport exactly.
  • display: flex on #app places the sidebar and map side by side.
  • The sidebar gets a fixed 350px width with a dark navy background (#1a1a2e).
  • Below the code you just pasted in style.css, add the control group and input styles:
#sidebar h3 {
    font-size: 0.95rem;
    margin-bottom: 8px;
    color: #ccc;
}

.control-group {
    margin-bottom: 20px;
    padding-bottom: 15px;
    border-bottom: 1px solid #2a2a4a;
}

.control-group label {
    display: block;
    margin-bottom: 8px;
    font-size: 0.85rem;
}

.control-group input[type="number"] {
    width: 60px;
    padding: 4px 8px;
    border: 1px solid #444;
    border-radius: 4px;
    background: #2a2a4a;
    color: #eee;
    margin-left: 8px;
}

.control-group p {
    font-size: 0.8rem;
    color: #888;
}
  • Save style.css and refresh your browser.

You should see the dark sidebar on the left with white text, and the map filling the remaining space on the right. The control sections have subtle border separators between them.

  • Continue in style.css. Add the delivery list and button styles below your existing code:
#delivery-list {
    max-height: 150px;
    overflow-y: auto;
    margin-top: 8px;
    font-size: 0.8rem;
}

.delivery-item {
    padding: 4px 0;
    color: #aaa;
}

button {
    width: 100%;
    padding: 10px;
    margin-bottom: 8px;
    border: none;
    border-radius: 6px;
    font-size: 0.9rem;
    cursor: pointer;
    font-weight: 600;
}

#optimize-btn {
    background: #00d4aa;
    color: #1a1a2e;
}

#optimize-btn:disabled {
    background: #444;
    color: #888;
    cursor: not-allowed;
}

What does this code do?

  • The delivery list scrolls when you have many stops so it does not overflow the sidebar.
  • Buttons get full-width styling with rounded corners. The optimize button is teal (#00d4aa) and becomes grey when disabled.
  • Add the remaining button states and results panel styles below your existing code in style.css:
#optimize-btn:not(:disabled):hover {
    background: #00f0c0;
}

#clear-btn {
    background: transparent;
    color: #888;
    border: 1px solid #444;
}

#clear-btn:hover {
    border-color: #888;
    color: #ccc;
}

#results {
    margin-top: 10px;
}

#results.hidden {
    display: none;
}

#summary {
    background: #2a2a4a;
    padding: 12px;
    border-radius: 6px;
    margin-bottom: 12px;
    font-size: 0.85rem;
    line-height: 1.6;
}
  • Save style.css and refresh your browser.

You should see the Optimize Routes button styled in grey (disabled) and the Clear All button with a subtle border. The layout is taking shape.

  • Add the final styles for route cards, warnings, the map container, and animations at the bottom of style.css:
.route-card {
    background: #2a2a4a;
    padding: 10px 12px;
    border-radius: 6px;
    margin-bottom: 8px;
    border-left: 4px solid;
    font-size: 0.8rem;
    line-height: 1.5;
}

.unassigned-warning {
    background: #4a2a2a;
    padding: 10px 12px;
    border-radius: 6px;
    color: #ff6b6b;
    font-size: 0.85rem;
    margin-bottom: 12px;
}

#map {
    flex: 1;
    height: 100vh;
}

@keyframes pulse-unassigned {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.4; }
}

.marker-unassigned {
    animation: pulse-unassigned 1.5s infinite;
}

What do these styles do?

  • route-card styles the per-vehicle result cards with a colored left border (the color is set dynamically per vehicle).
  • unassigned-warning shows a red-tinted banner when deliveries cannot fit into the fleet.
  • The pulse-unassigned animation makes unserved delivery markers blink on the map.
  • flex: 1 on #map ensures the map expands to fill all remaining space after the sidebar.
  • Save style.css and refresh your browser.

Your page should now show a dark sidebar on the left with the teal "Fleet Route Optimizer" heading, and a full interactive map on the right with zoom controls.

✔️ Awesome, I've got everything!

Great. Make sure you have saved all three files before moving on.

ⓧ I'd like to double check the full code

Your style.css should contain all five chunks in order: the layout foundation (reset, body, #app, #sidebar, #sidebar h1), then control group styles, then delivery list and button styles, then button states and results/summary, then route cards, warnings, map, and animations. Scroll up and confirm each section is present.

Your index.html should contain the full head section with Leaflet CDN links, then the body with sidebar controls (depot, deliveries, fleet config, buttons), a hidden results div, the map div, and the app.js script tag. Scroll up and confirm both chunks are present.

Your app.js should match this exactly:

const ORS_API_KEY = 'your-api-key-here';
const ORS_OPTIMIZATION_URL = 'https://api.openrouteservice.org/optimization';

const VEHICLE_COLORS = ['#e63946', '#457b9d', '#2a9d8f', '#e9c46a', '#f4a261'];

let map;
let depot = null;
let deliveries = [];
let depotMarker = null;
let deliveryMarkers = [];
let routeLayers = [];
let naiveRouteLayer = null;

function initMap() {
    map = L.map('map').setView([51.505, -0.09], 13);
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);
}

initMap();
Connect your API key and verify

The map renders with the placeholder key, but you need your real key for the optimization API to work in later steps.

  • In app.js, find this line at the top of the file:
const ORS_API_KEY = 'your-api-key-here';
  • Replace your-api-key-here with the API key you copied from the OpenRouteService dashboard earlier. Keep the surrounding quotes.
  • Save app.js.

Before you check the final result, what do you expect to see when you refresh the browser?

  • Refresh your browser one final time.

You should see the same interactive OpenStreetMap filling the right side of the screen, with the dark sidebar on the left showing all the control sections. The map responds to zoom (scroll wheel) and pan (click and drag). Your API key is now saved and ready for optimization requests.

Sidebar and map overlapping or misaligned?

  • Make sure your style.css has display: flex on #app and flex: 1 on #map. These two rules create the side-by-side layout.
  • Check that body has height: 100vh and overflow: hidden. Without these, the page may scroll instead of filling the viewport.

my sidebar and map layout is broken

Your project skeleton is ready and the map is live. Next up, you will make the map interactive by adding click handlers that let you place delivery stops and a depot.

Build the Interactive Delivery Map

You have a map on screen, but no way to tell it where deliveries need to go. Before you can optimize anything, you need an input mechanism for placing stops on the map.

In this step, you will make the map interactive so clicking drops delivery pins and a depot marker. Seeing the stops connected in random click-order will make it obvious why optimization matters later.

In this step, get ready to:
  • Place a depot and numbered delivery markers by clicking the map.
  • Draw a naive straight-line route connecting stops in click-order.
  • Wire up the Clear All button to reset everything.
Register click handlers for the depot and deliveries

The Leaflet map fires events when users click it. You will listen for left-clicks to add delivery stops and right-clicks to place the warehouse depot.

  • In app.js, find the closing } of your initMap() function.
  • Find this line:
    }).addTo(map);
}
  • Add two event listeners between the .addTo(map); line and the closing }, so initMap now looks like this:
function initMap() {
    map = L.map('map').setView([51.505, -0.09], 13);
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 19,
        attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    }).addTo(map);

    map.on('click', addDelivery);
    map.on('contextmenu', setDepot);
}

What do these listeners do?

map.on('click', addDelivery) fires addDelivery every time the learner left-clicks the map.

map.on('contextmenu', setDepot) fires setDepot on right-click. The contextmenu event is the browser's name for a right-click action.

  • Now add the setDepot function and its helper updateOptimizeButton below initMap() by pasting this code:
function setDepot(e) {
    // Prevent the browser's default right-click menu
    e.originalEvent.preventDefault();
    // Store depot as [lng, lat] for the ORS API
    depot = [e.latlng.lng, e.latlng.lat];

    // Remove old depot marker if one exists
    if (depotMarker) {
        map.removeLayer(depotMarker);
    }

    // Place a green circle marker at the clicked location
    depotMarker = L.marker(e.latlng, {
        icon: L.divIcon({
            className: 'depot-icon',
            html: '<div style="background:#00d4aa;width:20px;height:20px;border-radius:50%;border:3px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.4);"></div>',
            iconSize: [20, 20],
            iconAnchor: [10, 10]
        })
    }).addTo(map).bindPopup('Depot (start/end)');

    // Update the sidebar with the depot coordinates
    document.getElementById('depot-info').textContent =
        'Set at [' + e.latlng.lat.toFixed(4) + ', ' + e.latlng.lng.toFixed(4) + ']';

    updateOptimizeButton();
    drawNaiveRoute();
}

function updateOptimizeButton() {
    // Enable the optimize button only when depot + 2 deliveries exist
    var btn = document.getElementById('optimize-btn');
    btn.disabled = !(depot && deliveries.length >= 2);
}

What does this code do?

setDepot stores the clicked coordinates in [longitude, latitude] order. The OpenRouteService API expects coordinates in [lng, lat] order, while Leaflet uses [lat, lng]. The function handles both formats.

updateOptimizeButton keeps the Optimize button disabled until the learner has placed at least a depot and two delivery stops.

Why does setDepot call drawNaiveRoute?

You have not written drawNaiveRoute yet. If you right-click now, the depot marker will appear but you will see a ReferenceError in the console. That is expected and temporary. You will define drawNaiveRoute in the next substep.

  • Save app.js and refresh your browser.
  • Right-click anywhere on the map.

You should see a green circle marker appear at the clicked spot and the sidebar text update to show the coordinates. Ignore any console error about drawNaiveRoute for now.

Don't see the green marker?

  • Make sure you saved app.js and refreshed the browser page.
  • Confirm the two map.on(...) lines are inside initMap() before its closing brace, not after it.
  • On some trackpads, right-click requires a two-finger tap or Ctrl+click (macOS).

Help me debug why my right-click depot marker is not appearing.

  • Now add the addDelivery function and its helper updateDeliveryList below updateOptimizeButton by pasting this code:
function addDelivery(e) {
    // Assign an incrementing ID to each delivery
    const id = deliveries.length + 1;
    const delivery = {
        id: id,
        location: [e.latlng.lng, e.latlng.lat],
        latlng: e.latlng
    };
    deliveries.push(delivery);

    // Place a red numbered circle marker
    const marker = L.marker(e.latlng, {
        icon: L.divIcon({
            className: 'delivery-icon',
            html: '<div style="background:#e63946;color:#fff;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:bold;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,0.3);">' + id + '</div>',
            iconSize: [24, 24],
            iconAnchor: [12, 12]
        })
    }).addTo(map).bindPopup('Delivery #' + id);

    deliveryMarkers.push(marker);
    updateDeliveryList();
    updateOptimizeButton();
    drawNaiveRoute();
}

function updateDeliveryList() {
    // Render the sidebar list of all delivery coordinates
    const list = document.getElementById('delivery-list');
    list.innerHTML = deliveries.map(function(d) {
        return '<div class="delivery-item">#' + d.id + ' [' + d.latlng.lat.toFixed(4) + ', ' + d.latlng.lng.toFixed(4) + ']</div>';
    }).join('');
    document.getElementById('delivery-count').textContent = deliveries.length;
}

What does this code do?

addDelivery creates a delivery object with an auto-incrementing ID and both coordinate formats ([lng, lat] for the API, latlng for Leaflet). It places a red numbered marker on the map.

updateDeliveryList renders every delivery's coordinates into the sidebar and updates the count badge.

  • Save app.js and refresh your browser.
  • Left-click three different spots on the map.

You should see three red circle markers labeled 1, 2, and 3. The sidebar should show three entries with coordinates and the delivery count should read 3.

Markers not appearing on left-click?

  • Check the browser console for errors. A ReferenceError about updateDeliveryList means the function was not pasted below addDelivery.
  • Make sure map.on('click', addDelivery) is inside initMap() and spelled exactly as shown.

Help me debug why left-clicking the map does not add delivery markers.

Draw the naive unoptimized route

Clicking the map now places markers, but there is no visual connection between them. A line drawn in raw click-order shows how inefficient an unoptimized delivery sequence looks.

  • Add the drawNaiveRoute function below updateDeliveryList by pasting this code:
function drawNaiveRoute() {
    // Remove any existing naive route line
    if (naiveRouteLayer) {
        map.removeLayer(naiveRouteLayer);
        naiveRouteLayer = null;
    }

    // Only draw when depot exists and at least 2 deliveries are placed
    if (!depot || deliveries.length < 2) return;

    // Build the path: depot -> all deliveries in click order -> back to depot
    const depotLatLng = [depot[1], depot[0]];
    const points = [depotLatLng];
    deliveries.forEach(d => points.push([d.latlng.lat, d.latlng.lng]));
    points.push(depotLatLng);

    // Draw a red dashed polyline (straight lines, not roads)
    naiveRouteLayer = L.polyline(points, {
        color: '#ff0000',
        weight: 2,
        dashArray: '8, 8',
        opacity: 0.5
    }).addTo(map);
}

What does this code do?

drawNaiveRoute connects all stops in the exact order you clicked them, using straight lines (not actual roads). The path starts at the depot, visits every delivery in sequence, and returns to the depot.

This is deliberately inefficient. When you optimize later, the contrast between this random zigzag and the optimized route will be dramatic.

  • Save app.js and refresh your browser.
  • Right-click to place a depot, then left-click at least 5 different spots to add deliveries.

Before you look at the result, predict what you will see. Do you expect the connecting lines to follow roads or cut straight across buildings?

You should see a red dashed line cutting straight through buildings and parks, connecting your stops in the random order you clicked them. The line starts at the green depot, zigzags through every delivery, and returns to the depot.

Don't see the red dashed line?

  • You need at least a depot (right-click) AND two deliveries (left-click twice) before the line appears.
  • Check the console for any error mentioning drawNaiveRoute. If it says the function is not defined, confirm it was pasted below updateDeliveryList and not inside another function.
  • Try zooming out. If your clicks were very close together, the line might be tiny.

Help me figure out why the naive route polyline is not showing up.

Wire up the Clear All button

You can add stops, but there is no way to start over. The Clear All button needs to remove every marker, line, and sidebar entry so the learner can try a new set of stops.

  • Add the clearRoutes and clearUnassignedMarkers helper functions below drawNaiveRoute by pasting this code:
function clearRoutes() {
    // Remove all optimized route polylines from the map
    routeLayers.forEach(function(layer) { map.removeLayer(layer); });
    routeLayers = [];
    document.getElementById('results').classList.add('hidden');
    clearUnassignedMarkers();
}

function clearUnassignedMarkers() {
    // Remove pulsing animation from any unassigned delivery markers
    deliveryMarkers.forEach(function(marker) {
        var el = marker.getElement();
        if (el) el.classList.remove('marker-unassigned');
    });
}

What does this code do?

clearRoutes removes optimized route lines from the map and hides the results panel. You will use this in later steps when re-running the optimizer.

clearUnassignedMarkers removes a pulsing animation that you will add to unassigned deliveries in a future step. Defining both now keeps the clearAll function complete from the start.

  • Now add the clearAll function and the button event listeners below clearUnassignedMarkers by pasting this code:
function clearAll() {
    clearRoutes();
    // Remove the naive route line
    if (naiveRouteLayer) {
        map.removeLayer(naiveRouteLayer);
        naiveRouteLayer = null;
    }
    // Remove all markers from the map
    if (depotMarker) map.removeLayer(depotMarker);
    deliveryMarkers.forEach(function(m) { map.removeLayer(m); });

    // Reset all state variables
    depot = null;
    deliveries = [];
    depotMarker = null;
    deliveryMarkers = [];

    // Reset sidebar text
    document.getElementById('depot-info').textContent = 'Right-click map to set depot';
    document.getElementById('delivery-list').innerHTML = '';
    document.getElementById('delivery-count').textContent = '0';
    updateOptimizeButton();
}

// Wire up the sidebar buttons
document.getElementById('optimize-btn').addEventListener('click', optimizeRoutes);
document.getElementById('clear-btn').addEventListener('click', clearAll);

What does this code do?

clearAll removes every marker and polyline from the map, resets all state arrays to empty, and restores the sidebar to its initial text.

The two addEventListener lines connect the sidebar buttons to their functions. The optimize button calls optimizeRoutes which you will write in the next step. Clicking it now would produce a console error, but the button stays disabled until you have a depot and two deliveries.

  • Save app.js and refresh your browser.
  • Right-click to place a depot, then left-click to add a few deliveries.
  • Click the Clear All button in the sidebar.

All markers, the red dashed line, and the sidebar entries should disappear. The delivery count resets to 0 and the depot info reverts to its placeholder text.

Clear All not removing everything?

  • Check the console for errors. If clearRoutes is not defined, make sure it was pasted above clearAll in the file.
  • Confirm the addEventListener lines are placed BEFORE the initMap(); call at the bottom of the file, not after it.

Help me debug why Clear All is not resetting the map properly.

Make sure initMap() is last

The initMap(); call must remain the very last line in app.js. All function definitions and addEventListener calls go above it.

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js and your map responds to right-clicks (depot), left-clicks (deliveries), and the Clear All button.

ⓧ I'd like to double check the full code

Scroll through your app.js and confirm these functions appear in this order:

  • initMap() with map.on('click', addDelivery) and map.on('contextmenu', setDepot) inside it.
  • setDepot(e) ending with updateOptimizeButton() and drawNaiveRoute().
  • updateOptimizeButton() checking depot && deliveries.length >= 2.
  • addDelivery(e) ending with updateDeliveryList(), updateOptimizeButton(), drawNaiveRoute().
  • updateDeliveryList() rendering delivery-list and delivery-count.
  • drawNaiveRoute() building the polyline from depot through deliveries and back.
  • clearRoutes() and clearUnassignedMarkers().
  • clearAll() resetting all state and DOM elements.
  • Two addEventListener calls for optimize-btn and clear-btn.
  • initMap(); as the very last line.

Your map is now fully interactive. Next up, you will call the optimization API to replace that inefficient zigzag with a route that actually follows roads in the best possible order.

Optimize a Single-Vehicle Route

The naive route from the previous step draws straight lines between stops in the order you clicked them. It ignores roads entirely and makes no effort to find an efficient sequence.

In this step, you will call the OpenRouteService Optimization API to replace that naive line with an efficient, road-following route. The API uses the VROOM engine under the hood to solve the Vehicle Routing Problem and reorder your stops.

In this step, get ready to:
  • Build and send an optimization request to the VROOM API.
  • Decode the encoded polyline geometry from the response and draw it on the map.
  • Display route statistics in the sidebar.
Build the optimization request

The optimization API needs two things: a list of jobs (your delivery stops) and a list of vehicles (your fleet). Each job carries a capacity demand, and each vehicle starts and ends at the depot.

  • In app.js, find the line function clearRoutes() and add the following optimizeRoutes function above it:
async function optimizeRoutes() {
    var btn = document.getElementById('optimize-btn');
    btn.disabled = true;
    btn.textContent = 'Optimizing...';
    clearRoutes();
    var vehicleCount = parseInt(document.getElementById('vehicle-count').value);
    var vehicleCapacity = parseInt(document.getElementById('vehicle-capacity').value);
    // Each delivery becomes a job with a capacity demand of 1
    var jobs = deliveries.map(function(d) {
        return {
            id: d.id,
            location: d.location,
            service: 300,
            amount: [1]
        };
    });
    // Each vehicle starts and ends at the depot
    var vehicles = [];
    for (var i = 0; i < vehicleCount; i++) {
        vehicles.push({
            id: i + 1,
            profile: 'driving-car',
            start: depot,
            end: depot,
            capacity: [vehicleCapacity]
        });
    }

What does this code do?

This is the first half of optimizeRoutes. It disables the button to prevent double-clicks, then reads the fleet configuration from the sidebar inputs.

  • Each delivery becomes a job with service: 300 (5 minutes at each stop) and amount: [1] (takes 1 unit of vehicle capacity).
  • Each vehicle uses the driving-car routing profile and loops back to the depot after completing its deliveries.
  • Immediately below the code you just added (still inside the function), add the API call and error handling to complete the function:
    // g: true tells the API to return route geometry
    var requestBody = {
        jobs: jobs,
        vehicles: vehicles,
        options: { g: true }
    };
    try {
        var response = await fetch(ORS_OPTIMIZATION_URL, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': ORS_API_KEY
            },
            body: JSON.stringify(requestBody)
        });
        if (!response.ok) {
            var errorText = await response.text();
            throw new Error('API error ' + response.status + ': ' + errorText);
        }
        var result = await response.json();
        displayResults(result, vehicleCount);
    } catch (error) {
        alert('Optimization failed: ' + error.message);
        console.error(error);
    } finally {
        btn.disabled = false;
        btn.textContent = 'Optimize Routes';
        updateOptimizeButton();
    }
}

What does this code do?

This completes the optimizeRoutes function. It POSTs your jobs and vehicles to the OpenRouteService Optimization API with your key in the Authorization header.

  • options: { g: true } tells the API to include encoded route geometry in the response. Without this flag, you get stats but no drawable path.
  • The finally block re-enables the button regardless of success or failure.
  • Save app.js.
  • Open your browser's DevTools by pressing Cmd+Option+I (macOS) or Ctrl+Shift+I (Windows).
  • Switch to the Network tab.
  • In the app, right-click to set a depot, then left-click to add 4 delivery stops.
  • Set Number of vehicles to 1 in the sidebar.
  • Click Optimize Routes.

In the Network tab, you should see a POST request to optimization with a 200 status. You will also see an alert saying the optimization failed because displayResults is not defined yet. Close the alert.

Seeing a 401 or 403 error?

  • Double-check that ORS_API_KEY at the top of app.js contains your actual API key, not the placeholder text.
  • Make sure the key is passed in the Authorization header, not as a query parameter.

Having trouble?

Decode the response geometry and add helper functions

The API returns route geometry as an encoded polyline string. You need a decoder to convert it into coordinates that Leaflet can draw.

  • Below your clearAll function in app.js, add the polyline decoder:
function decodePolyline(encoded) {
    var points = [];
    var index = 0;
    var lat = 0;
    var lng = 0;
    while (index < encoded.length) {
        var b;
        var shift = 0;
        var result = 0;
        do {
            b = encoded.charCodeAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);
        var dlat = (result & 1) ? ~(result >> 1) : (result >> 1);
        lat += dlat;
        shift = 0;
        result = 0;
        do {
            b = encoded.charCodeAt(index++) - 63;
            result |= (b & 0x1f) << shift;
            shift += 5;
        } while (b >= 0x20);
        var dlng = (result & 1) ? ~(result >> 1) : (result >> 1);
        lng += dlng;
        points.push([lat / 1e5, lng / 1e5]);
    }
    return points;
}

What does this code do?

This implements the standard Google polyline decoding algorithm. Encoded polylines compress a series of coordinates into a compact string. The decoder reads 5-bit chunks, reassembles latitude and longitude deltas, and returns an array of [lat, lng] pairs that Leaflet can plot directly.

  • Below decodePolyline, add the formatting and marker helper functions:
function formatDuration(seconds) {
    var mins = Math.round(seconds / 60);
    if (mins < 60) return mins + ' min';
    var hours = Math.floor(mins / 60);
    var remainMins = mins % 60;
    return hours + 'h ' + remainMins + 'm';
}

function formatDistance(meters) {
    if (meters < 1000) return meters + ' m';
    return (meters / 1000).toFixed(1) + ' km';
}

// Mark deliveries that the solver could not assign
function markUnassigned(unassignedJobs) {
    var unassignedIds = unassignedJobs.map(function(u) { return u.id; });
    deliveryMarkers.forEach(function(marker, index) {
        if (unassignedIds.indexOf(index + 1) !== -1) {
            var el = marker.getElement();
            if (el) el.classList.add('marker-unassigned');
        }
    });
}

What do these helpers do?

  • formatDuration converts raw seconds into a readable string like 12 min or 1h 15m.
  • formatDistance converts meters into m or km as appropriate.
  • markUnassigned adds a pulsing CSS animation to any delivery markers the solver could not fit into a vehicle. You will use this in the next step.
  • Save app.js and refresh your browser.
  • Open the browser console (Console tab in DevTools) and run:
formatDuration(3661)

You should see 1h 1m printed in the console. This confirms your helper functions loaded correctly.

Seeing an error in the console?

  • If you see a syntax error, check that you closed the decodePolyline function with a matching } before starting formatDuration.
  • Make sure all three functions (decodePolyline, formatDuration, formatDistance) are placed after clearAll and before the event listener lines at the bottom of the file.

Still stuck?

Display optimization results in the sidebar

Now you need the displayResults function that optimizeRoutes calls on success. It will remove the naive route, show stats, and draw the optimized path.

  • Above markUnassigned in app.js, add the first half of displayResults which handles the summary and unassigned warnings:
function displayResults(result, vehicleCount) {
    // Remove the naive route now that we have the optimized one
    if (naiveRouteLayer) {
        map.removeLayer(naiveRouteLayer);
        naiveRouteLayer = null;
    }
    var resultsDiv = document.getElementById('results');
    resultsDiv.classList.remove('hidden');
    // Populate summary stats in the sidebar
    var summary = result.summary;
    document.getElementById('summary').innerHTML =
        '<div>Total duration: ' + formatDuration(summary.duration) + '</div>' +
        '<div>Total distance: ' + formatDistance(summary.distance) + '</div>' +
        '<div>Routes used: ' + result.routes.length + ' / ' + vehicleCount + '</div>' +
        '<div>Unassigned jobs: ' + result.unassigned.length + '</div>';
    // Show warning if any deliveries could not be assigned
    var unassignedDiv = document.getElementById('unassigned-info');
    if (result.unassigned.length > 0) {
        var ids = result.unassigned.map(function(u) { return '#' + u.id; }).join(', ');
        unassignedDiv.innerHTML = '<div class="unassigned-warning">Unassigned deliveries: ' + ids + '. Try adding more vehicles or increasing capacity.</div>';
        markUnassigned(result.unassigned);
    } else {
        unassignedDiv.innerHTML = '';
        clearUnassignedMarkers();
    }

What does this code do?

  • First it removes the naive red dashed polyline so only the real road route remains.
  • It unhides the results panel and fills in total duration, distance, routes used, and unassigned job count.
  • If any jobs are unassigned (capacity exceeded), it shows a red warning listing those delivery IDs.
  • Immediately below (still inside the function), add the route drawing logic to complete displayResults:
    // Draw each vehicle's route with a unique color
    var routeDetailsDiv = document.getElementById('route-details');
    routeDetailsDiv.innerHTML = '';
    result.routes.forEach(function(route, index) {
        var color = VEHICLE_COLORS[index % VEHICLE_COLORS.length];
        var stops = route.steps.filter(function(s) { return s.type === 'job'; });
        var card = document.createElement('div');
        card.className = 'route-card';
        card.style.borderColor = color;
        card.innerHTML =
            '<strong style="color:' + color + '">Vehicle ' + route.vehicle + '</strong><br>' +
            'Stops: ' + stops.map(function(s) { return '#' + s.id; }).join(' &rarr; ') + '<br>' +
            'Duration: ' + formatDuration(route.duration) + '<br>' +
            'Distance: ' + formatDistance(route.distance);
        routeDetailsDiv.appendChild(card);
        if (route.geometry) {
            var decoded = decodePolyline(route.geometry);
            var polyline = L.polyline(decoded, {
                color: color,
                weight: 5,
                opacity: 0.8
            }).addTo(map);
            routeLayers.push(polyline);
        }
    });
    // Zoom map to fit all route polylines
    if (routeLayers.length > 0) {
        var group = L.featureGroup(routeLayers);
        map.fitBounds(group.getBounds().pad(0.1));
    }
}

What does this code do?

  • For each route in the response, it creates a sidebar card showing the vehicle color, stop sequence, duration, and distance.
  • It decodes the encoded polyline geometry into coordinate pairs and draws a colored Leaflet polyline on actual roads.
  • Finally, it zooms the map to fit all drawn routes with a small padding.
  • Save app.js and refresh your browser.
  • Right-click the map to set a depot.
  • Left-click to add 4 delivery stops around the depot.
  • Set Number of vehicles to 1.

Before you click Optimize, what do you expect to happen to the red dashed naive route?

  • Click Optimize Routes.

The red dashed naive line disappears and a smooth colored route appears following actual roads. The sidebar shows total duration, total distance, and the reordered stop sequence.

Route not appearing on the map?

  • Check the console for errors. If you see displayResults is not a function, make sure the function is defined above the event listener lines at the bottom of the file.
  • If you see the summary stats but no line on the map, verify that decodePolyline is defined and that the options: { g: true } line is present in your requestBody.
  • If the button stays on "Optimizing..." indefinitely, check the Network tab for a failed request. A 403 means your API key is incorrect.

Still stuck?

✔️ Awesome, I've got everything!

Great. Double check you have saved app.js and your optimized route is drawing on actual roads.

ⓧ I'd like to double check the full code

Your app.js now contains these functions in this order. Scroll through your file and confirm each is present:

  • initMap (unchanged from before).
  • setDepot (unchanged from before).
  • addDelivery (unchanged from before).
  • drawNaiveRoute (unchanged from before).
  • updateDeliveryList (unchanged from before).
  • updateOptimizeButton (unchanged from before).
  • optimizeRoutes (new: builds request, calls API, calls displayResults).
  • displayResults (new: removes naive route, shows summary, draws colored polylines).
  • markUnassigned (new: adds pulsing animation to unassigned markers).
  • clearUnassignedMarkers (unchanged from before).
  • clearRoutes (unchanged from before).
  • clearAll (unchanged from before).
  • decodePolyline (new: standard Google polyline decoder).
  • formatDuration (new: seconds to readable string).
  • formatDistance (new: meters to readable string).
  • Event listeners for optimize-btn and clear-btn (unchanged).
  • initMap() call at the bottom (unchanged).

Your optimizer is working. You have a single vehicle following an efficient road route. Next up, you will discover what happens when you give it more deliveries than the truck can carry.

Hit the Capacity Wall

Your optimizer routes deliveries beautifully when everything fits on one truck. But real logistics has physical limits.

What happens when you have more packages than a single vehicle can carry? In this step, you will deliberately overload your fleet and watch the solver refuse to assign deliveries it cannot serve.

In this step, get ready to:
  • Overload a single vehicle's capacity with more deliveries than it can handle.
  • Observe how the solver leaves excess jobs unassigned and warns you in the sidebar.
  • Spot the pulsing visual markers that distinguish unassigned deliveries on the map.
Configure a single-vehicle scenario

Your sidebar already has fleet configuration controls that feed directly into the optimization request. Time to push them to their limits.

  • Switch back to your browser with the Fleet Route Optimizer open.
  • Click Clear All to start fresh.
  • In the Fleet Configuration section of the sidebar, change Number of vehicles to 1.
  • Keep Capacity per vehicle at 4.

What do these numbers mean?

Each delivery job in your request has amount: [1], meaning it takes up 1 unit of truck space. A vehicle with capacity: [4] can carry exactly 4 deliveries.

So with 1 vehicle and capacity 4, the maximum number of deliveries the solver can assign is 4. Anything beyond that must be left behind.

Now flood the map with more deliveries than the truck can handle.

  • Right-click on the map to place your green depot marker.
  • Left-click 8 different locations on the map to drop 8 numbered delivery markers.

You should see 8 red delivery markers on the map, your depot in green, and the red dashed naive route connecting everything in click-order. The sidebar shows the delivery list and the count reads 8.

Trigger the capacity wall

You have 8 deliveries but your single truck can only carry 4. Before you click Optimize, think about what should happen. Will the solver route all 8 stops, or will it refuse some?

  • Click Optimize Routes.

The solver returns, and the sidebar reveals the capacity wall. You see a red warning listing the delivery IDs that could not be served. The route drawn on the map only covers 4 of your 8 stops.

Why does the solver leave jobs unassigned?

The VROOM optimization engine respects hard capacity constraints. It will never overload a vehicle past its declared capacity. When total demand exceeds total fleet capacity, the solver leaves excess jobs in the unassigned array of the response rather than breaking the constraint.

This is exactly how real logistics software works. A dispatcher sees unassigned packages and knows they need to add another truck or increase capacity.

Spot the pulsing unassigned markers

The sidebar warning tells you which deliveries were dropped. Look at the map to see the visual difference.

  • Look at the delivery markers on the map. The ones listed in the red warning are now pulsing with a fading animation.
  • Compare: the 4 assigned stops sit along the drawn route. The remaining 4 pulse to signal they were left behind.

How does the pulsing animation work?

When the solver reports unassigned jobs, the markUnassigned function adds the CSS class marker-unassigned to those markers. Your style.css includes a pulse-unassigned keyframe animation that fades the marker opacity between 1 and 0.4 every 1.5 seconds.

Not seeing the pulsing animation?

Make sure your style.css includes the @keyframes pulse-unassigned rule and the .marker-unassigned class. Also confirm you placed at least 5 delivery stops so some exceed the capacity of 4.

help me debug why my unassigned delivery markers are not pulsing on the map

You have just hit the fundamental problem that the Vehicle Routing Problem exists to solve. A single truck with limited capacity cannot serve all demand. The answer is a fleet. In the next step, you will expand to multiple vehicles and watch the solver split deliveries across color-coded routes.

Expand to Multi-Vehicle Fleet

In the last step, you proved that a single vehicle with limited capacity cannot serve all your deliveries. The solver left jobs unassigned because one truck simply cannot carry everything.

The solution is what real logistics companies use: a fleet. By expanding to multiple vehicles, the VROOM solver can split deliveries across drivers, giving each one an efficient route that respects capacity. In this step, you will use the fleet controls you already built to watch the solver distribute work across a multi-vehicle fleet.

In this step, get ready to:
  • Run a multi-vehicle optimization and see color-coded routes split across drivers.
  • Read per-vehicle stats cards showing each driver's stop sequence, duration, and distance.
  • Confirm the fleet summary shows zero unassigned jobs when capacity is sufficient.
Set up a multi-vehicle fleet test

Your code already handles multiple vehicles. The displayResults function assigns a unique color from the VEHICLE_COLORS palette to each route, draws separate polylines, and builds individual stats cards. Now you will exercise that logic with a proper fleet.

  • Switch back to your browser with the Fleet Route Optimizer open.
  • Click Clear All to reset the map from your previous capacity-wall test.
  • In the Fleet Configuration section of the sidebar, set Number of vehicles to 3.
  • Set Capacity per vehicle to 4.
  • Right-click the map to place your depot (the green marker).
  • Left-click the map 10 times to add 10 delivery stops, spread across different areas of the map.

You should see 10 numbered red markers and the red dashed naive route connecting them in click-order.

Why spread the stops apart?

When delivery stops are geographically clustered, the solver might assign them all to one vehicle. Spreading stops across the map forces the solver to split them between drivers based on proximity, giving you visually distinct routes.

Run the fleet optimization

With 3 vehicles each holding 4 deliveries, your fleet has a total capacity of 12. That is more than enough for 10 stops. Before you click Optimize, think: do you expect all 3 vehicles to be used, or might the solver leave one idle?

  • Click Optimize Routes.

The red dashed naive route disappears and is replaced by multiple color-coded polylines following actual roads. Each polyline represents one vehicle's optimized route, starting and ending at the green depot marker. The map auto-zooms to fit all routes in view.

What do the route colors mean?

Each vehicle gets a color from the VEHICLE_COLORS palette: red, blue, teal, gold, and orange. The solver may use 2 or 3 vehicles depending on how it splits the work. You will see 2-3 distinct colored routes on the map.

Read the per-vehicle stats and fleet summary

The sidebar now shows two key sections: a summary at the top and individual route cards below it.

  • Look at the summary section at the top of the results panel.

You should see four stats: total duration across all vehicles, total distance, routes used vs available (e.g. "2 / 3" or "3 / 3"), and unassigned jobs showing 0.

  • Scroll down to the route cards below the summary.

Each card has a colored left border matching its route on the map. Inside, you see the vehicle number, the stop sequence (e.g. #2, #5, #1), the route duration, and the route distance. Each vehicle's stops are different because the solver assigned them based on geographic efficiency.

Why does the solver sometimes use fewer vehicles than available?

The Vehicle Routing Problem solver minimizes total travel cost. If 2 vehicles can handle all 10 deliveries within their capacity and produce a shorter combined route than 3 vehicles would, the solver leaves the third idle. This is optimal fleet management.

  • Confirm that the "Unassigned jobs" line in the summary reads 0. This proves your fleet has enough capacity for all 10 deliveries.

Still seeing unassigned deliveries?

Make sure your Number of vehicles is set to 3 and Capacity per vehicle is 4. With a total capacity of 12, 10 deliveries should all be assigned. If you added more than 12 stops, some will be unassigned.

Help me figure out why my fleet optimization still shows unassigned jobs

You now have a fully working multi-vehicle fleet optimizer. You can place any number of delivery stops, configure your fleet size and capacity, and watch the VROOM solver split work efficiently across drivers with road-following routes. Next up: the Secret Mission, where you will add time window scheduling constraints to simulate real delivery windows.

Secret mission

Add Delivery Time Windows

Real delivery drivers don't just show up whenever they want. Customers expect deliveries within specific time slots. Can you extend your optimizer to respect scheduling constraints and show when each delivery actually arrives?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with a free API key, so there are no ongoing costs. Decide whether to keep your project files to continue experimenting or delete them entirely.

Resources you used:

  • Local project files (index.html, style.css, app.js).
  • OpenRouteService API key (free tier, no charges).

Keep everything running

No action needed. Choose this if you want to keep experimenting with your route optimizer.

  • Your project files stay on your Desktop, ready to open anytime.
  • Your OpenRouteService API key remains active with 2,500 optimization requests per day at no cost.
  • You can continue adding features, changing vehicle counts, or testing time windows whenever you like.

Pause - I'll come back to this later

There is nothing to pause for this project. Your files sit on disk and your API key stays active with no charges. Come back whenever you are ready.

  • Your local files use negligible disk space and cost nothing to keep.
  • Your OpenRouteService key does not expire and incurs no charges while idle.

Delete - I don't want to use this again

Remove all project files from your machine. Your API key has no documented revocation path in the current ORS dashboard, but it incurs zero cost while unused.

  • Delete your project folder from the Desktop by opening Finder (macOS) or File Explorer (Windows), navigating to your Desktop, and dragging the folder containing index.html, style.css, and app.js to the Trash (macOS) or Recycle Bin (Windows).
  • Empty the Trash or Recycle Bin to permanently remove the files.
  • Your OpenRouteService API key will remain in your account but costs nothing. If you want to fully close your account in the future, contact ORS support at the email listed on their contact page.

Nice Work!

Nice Work!

You just built a full fleet route optimizer that solves the Vehicle Routing Problem visually in your browser. From dropping pins on a map to watching an optimization engine split deliveries across color-coded drivers, you now have hands-on experience with a problem that powers real-world logistics.

You've learned how to:

  • Build an interactive map with Leaflet.js where click events place a depot and numbered delivery stops, connected by a naive route that reveals why optimization matters.
  • Connect to the OpenRouteService Optimization API (powered by VROOM) to solve the Vehicle Routing Problem, decoding polyline geometry to draw efficient road-following routes on the map.
  • Discover capacity constraints by overloading a single vehicle and watching jobs go unassigned, then expand to a multi-vehicle fleet with per-driver color-coded routes and statistics.
  • Secret Mission: Added delivery time windows so the solver respects 2-hour scheduling slots, with formatted arrival times displayed per stop and unassigned warnings for scheduling conflicts.

Ready to quiz yourself?