Sorry, you need to enable JavaScript to visit this website.

Pizza delivery

Overview

This tutorial shows you how to use the TomTom Maps SDK for Web to create a web application that helps a pizza chain select from which restaurant a pizza should be delivered to the customer.

It shows how to use:

  • The Routing API:
    • Its Reachable Range method to calculate and display a vehicle-reachable range in a specified time.
    • Its Calculate Route method to calculate travel time between 2 points.
  • The Batch Routing API to execute multiple Calculate Route and Reachable Range queries in a single request.
  • An autocomplete search input box map control to find addresses and points of interest.

A user can start interacting with the application by typing the address of a place inside the search box where the pizza should be delivered.

When the user clicks on a Calculate Route button:

  1. Polygons with a reachable range from each pizza restaurant are shown on the map.
  2. A travel time from each restaurant is calculated and displayed.

Prerequisites

To start using the TomTom Maps SDK for Web, you need the following:

  1. An application template that can be downloaded from here.
  2. An SDK that can be downloaded from here: Downloads.
    • Unzip it and save it in a folder.
    • This tutorial assumes your folder is named sdk.
    • If you choose another name, remember to change the provided paths to match.

      Place the sdk directory inside the folder with the application template.

  3. API key

    Create your API key now

    To create a new API key you need to first register on the portal. Once you are logged in there are only two steps remaining:

    Create a new application in your Dashboard:

    Choose all the APIs:

    All set up. You can now click on your newly created app and copy your API key:

Inside the directory with the application template (which you just downloaded) you can see multiple files:

  • pizza-delivery.html with an application HTML code.
  • points.js where you can find a GeoJSON definition with coordinates to pizza restaurants are on the map.
  • The points.js file contains a collection of GeoJSON features assigned to a variable named geojson.
  • These features represent points where pizza restaurants are placed on the map. For each point additional properties are defined:
    • a path to a marker image named iconUrl and
    • a color of a range polygon named polygonColor.
  • See the following code example.

    var geojson = {
        "type": "FeatureCollection",
        "features": [
            {
                "type": "Feature",
                "geometry": {
                    "type": "Point",
                    "coordinates": [
                        4.87408,
                        52.37617
                    ]
                },
                "properties": {
                    "iconUrl": "img/ic_restaurant_1_map.png",
                    "polygonColor": "#abc3f2"
                }
            },
    …other geojson features
    
        ]
    };
    
  • The geojson.js script file is already included as a <script> html element in the pizza-delivery.html file. This means that you can use the geojson variable inside other scripts included after the geojson.js file.
  • styles.css where CSS styles for the map and other html elements are defined.
  • pizza-delivery.js with the application JS code. This is the main file on which you will be working with in next parts of this tutorial.
  • img directory with all images needed in the tutorial.
  • mono.json vector map style used to display a map with custom colors. Put this file inside the sdk/styles/ directory.

The Pizza delivery application uses CSS classes defined inside a Bootstrap 4 library. Additionally, it uses the Bootstrap Slider component to allow users to choose a time

they would like to have the pizza delivered.

The directory structure of your project should look like this:

Vector map initialization (with UI elements)

To display a TomTom map with a custom map style you need to use a vector map. You can initialize the map by adding the following code inside the pizza-delivery.js file. See the following code example.

 

var map = tomtom.L.map('map', {
    key: '<your-api-key>',
    center: [52.37187, 4.89218],
    basePath: '/sdk',
    source: 'vector',
    styleUrlMapping: {
        main: {
            basic: '/sdk/styles/mono.json',
            labels: '/sdk/styles/labels_main.json'
        }
    },
    zoom: 12
});

The first parameter of the tomtom.L.map constructor requires the same value as the id of a HTML div element where the map will be embedded. Make sure that you replace the <your-api-key> placeholder with your API key.

Inside the above code snippet, a custom map style is used. This adds relative paths to files where map styles are defined inside the styleUrlMapping section. See the following code example.

styleUrlMapping: {
    main: {
        basic: '/sdk/styles/mono.json',
        labels: '/sdk/styles/labels_main.json'
    }
},

To run your application with a vector map you need to start a HTTP server.

  • The easiest way to get a server running is to use an existing http-server package from a npm repository.
  • To install this package. type the command npm install http-server in your terminal, inside the directory with the pizza-delivery.js file.
  • Now you can start the server by typing a http-server command in the terminal.
  • To see your application with the map displayed go to http://127.0.0.1:8080/pizza-delivery.html url.

 

Note: You might may need to clear your browser cache after each change inside the pizza-delivery.js file

Initializing the side menu

1. Define the variables used in the next sections of this tutorial:

  • MILLIS_IN_SECOND: A constant with the number of milliseconds in one minute.
  • DELIVERY_TIME_IN_MINUTES: The amount of the time in minutes necessary to calculate delivery range polygons.
  • MIN_SLIDER_VALUE: The start of a range for a delivery time slider with time in minutes, equal to 480 (8:00).
  • MAX_SLIDER_VALUE: The end of a range for a delivery time slider with time in minutes, equal to 1320(22:00).
  • reachableRangeBudgetTimeInSeconds: The amount of time in seconds needed to calculate range polygons.
  • pizzaPrefixId: A prefix used in identifiers of the elements from the side menu.
  • polygonLayers: An array where polygons displayed on the map are stored.
  • pizzaMarkers: An array where pizza restaurant map markers are stored.
  • clientMarker: A variable where a pizza delivery destination marker is stored.
  • deliveryTimeSlider: A variable with the delivery time slider interface control.

2. Add all variables to the pizza-delivery.js file. See the following code example.

var MILLIS_IN_SECOND = 1000;
var DELIVERY_TIME_IN_MINUTES = 15;
var MIN_SLIDER_RANGE = 480;
var MAX_SLIDER_RANGE = 1320;
var reachableRangeBudgetTimeInSeconds = 60 * DELIVERY_TIME_IN_MINUTES;
var pizzaPrefixId = 'pizza-';
var polygonLayers = [];
var pizzaMarkers = [];
var clientMarker;
var deliveryTimeSlider;

3. Inside the pizza-delivery.html file you can find an already-defined structure of the side menu. You need to initialize:

  • A delivery time slider.
  • An autocomplete search box.

4. To do that, add a method initControlMenu to the pizza-delivery.js file and execute it. See the following code example.

function initControlMenu() {
    var searchBoxInstance = tomtom.searchBox({
        collapsible: false,
        searchOnDragEnd: 'never'
    }).addTo(map);
    document.getElementById('search-panel').appendChild(searchBoxInstance.getContainer());
    deliveryTimeSlider = new Slider('#slider-input', {
        min: MIN_SLIDER_RANGE,
        max: MAX_SLIDER_RANGE,
        value: MIN_SLIDER_RANGE,
        step: 15,
        tooltip: 'hide',
        enabled: false,
        rangeHighlights: [
            {start: 510, end: 810, class: 'medium-traffic'},
            {start: 540, end: 705, class: 'high-traffic'}
        ]
    });
    deliveryTimeSlider.on('change', function (event) {
        document.getElementById('delivery-time').innerText = convertSliderValueToTimeString(event.newValue);
    }, false);
    deliveryTimeSlider.on('slideStop', function () {
        setDeliveryTimeSliderValue();
        setDeliveryTimeSpanValue();
    });
}

initControlMenu();
  • Inside this function the search box with an autocomplete functionality is initialized and assigned to a variable searchBoxInstance.
  • This search box is a map control, which means that it can only be initialized when the map is present.
  • In the next step:
    • The search box is moved under a html element with id search-panel in the side menu.
    • The delivery time slider is initialized and assigned to the variable deliveryTimeSlider. This slider’s ‘change’ event updates the currently displayed delivery time. A ‘slideStop’ event makes sure that the slider’s selected value is later than the current time.

5. You also need helper methods:

  • convertSliderValueToTimeString: Used to convert the delivery time slider value (current time in minutes) to hour:minute(hh:mm) format.
  • setDeliveryTimeSpanValue: Where the delivery time is displayed inside a HTML element with an id of delivery-time.
  • setDeliveryTimeSliderValue: Where the delivery time slider allowed values are restricted to the time which is later than the current time.

See the following code example.

function convertSliderValueToTimeString(sliderValue) {
    var hours = Math.floor(sliderValue / 60);
    var minutes = sliderValue % 60;
    if (hours < 10) {
        hours = '0' + hours;
    }
    if (minutes < 10) {
        minutes = '0' + minutes;
    }
    return hours + ':' + minutes;
}

function setDeliveryTimeSliderValue() {
    var currentDate = new Date();
    var currentTimeInMinutesWithDeliveryTime = (currentDate.getHours() * 60) + currentDate.getMinutes() + DELIVERY_TIME_IN_MINUTES;
    if (deliveryTimeSlider.getValue() < currentTimeInMinutesWithDeliveryTime) {
        if (currentTimeInMinutesWithDeliveryTime < MIN_SLIDER_RANGE) {
            deliveryTimeSlider.setValue(MIN_SLIDER_RANGE);
        }
        else if (currentTimeInMinutesWithDeliveryTime > MAX_SLIDER_RANGE) {
            deliveryTimeSlider.setValue(MAX_SLIDER_RANGE);
        } else {
            var roundedCurrentTime = currentTimeInMinutesWithDeliveryTime % 15 === 0 ? currentTimeInMinutesWithDeliveryTime : Math.ceil(currentTimeInMinutesWithDeliveryTime / 15) * 15;
            deliveryTimeSlider.setValue(roundedCurrentTime);
        }
    }
}

function setDeliveryTimeSpanValue() {
    var deliveryTimeSpan = document.getElementById('delivery-time');
    if (deliveryTimeSlider.isEnabled()) {
        deliveryTimeSpan.innerText = convertSliderValueToTimeString(deliveryTimeSlider.getValue());
    } else {
        deliveryTimeSpan.innerText = '--:--';
    }
}

6. Now go to the http://127.0.0.1:8080/pizza-delivery.html url and refresh the page to see the map with the side menu (you may need to clear your browser cache before you refresh the page).

Displaying markers on the map

As the map is now visible on the page, you can add pizza restaurant markers with their custom icons.

1. Add the method “displayPizzaMarkers” where pizza restaurant marker icons are added to the map. See the following code example.

function displayPizzaMarkers() {
    tomtom.L.geoJSON(geojson, {
        pointToLayer: createMarker
    }).addTo(map);
}

function createMarker(geoJsonPoint) {
    var coordinates = geoJsonPoint.geometry.coordinates.reverse();
    var marker = tomtom.L.marker(coordinates, {
        icon: tomtom.L.icon({
            iconUrl: geoJsonPoint.properties.iconUrl,
            iconSize: [60, 60],
            iconAnchor: [30, 30],
            popupAnchor: [0, -30]
        }),
        draggable: true
    });
    marker.on('dragend', function () {
        if (polygonLayers.length > 0) {
            displayReachableRangePolygons();
        }
    });
    pizzaMarkers.push(marker);
    return marker;
}

function displayReachableRangePolygons() {
}

2. Inside the displayPizzaMarkers method, the function passed in the pointToLayer option is executed for each point from the geojson variable (defined in the geojson.js file).

See the following code example.

tomtom.L.geoJSON(geojson, {
        pointToLayer: createMarker
    }).addTo(map);

3. For each point, a map marker is created in the createMarker method.

  • The first line of this method is used to reverse a latitude and longitude coordinates pair from the format used in GeoJSON (lon/lat) to the format used in tomtom map markers (lat/lon).

See the following code example.

var coordinates = geoJsonPoint.geometry.coordinates.reverse()

4. The createMarker method constructs tomtom.L.marker objects, with icons loaded from urls defined as GeoJSON properties.

  • Those icons are displayed on the map for each pizza restaurant marker.
  • Optionally you can implement a ‘dragend’ marker event, where range polygons (added in a later part of this tutorial) are recalculated each time when a user moves any pizza restaurant marker to another position on the map.

5. Next push all markers to pizzaMarkers array.

  • By adding the placeholder method displayReachableRangePolygons, you will implement this method in next section of the tutorial.

6. Execute the displayPizzaMarkers method after the initControlMenu method. See the following code example.

initControlMenu();
displayPizzaMarkers();

7. Now you also need a marker representing the customer of the pizza restaurant.

  • This marker is added whenever a user of the application types a customer address inside the autocomplete search box.

 

8. Add a ResultClicked event on the searchBoxInstance. See the following code example.

searchBoxInstance.on(searchBoxInstance.Events.ResultClicked, showClientMarkerOnTheMap);

and implement this event handling with the following code:

function showClientMarkerOnTheMap(result) {
    document.getElementById('calculate-range').disabled = false;
    if (clientMarker) {
        map.removeLayer(clientMarker);
    }
    clientMarker = tomtom.L.marker(result.data.position, {
        icon: tomtom.L.icon({
            iconUrl: 'img/pizza_marker-1.png',
            iconSize: [50, 50],
            iconAnchor: [25, 25]
        })
    }).addTo(map);
    if(polygonLayers.length > 0) {
        displayReachableRangePolygons();
    }
}

9. In the method showClientMarkerOnTheMap, the marker representing the customer:

  • Is removed from the map (if it exists).
  • A new customer marker is created in another position and added to the map.

10. Let’s shortly summarize what you implemented so far.

  • At this point you should see on the map markers representing pizza restaurants.
  • Search for an address by typing it inside the search box on the side menu.
  • Clicking a found address will show it on the map as the customer marker.

Display reachable range polygons

Now you can implement a displayReachableRangePolygons method. See the following code example.

function displayReachableRangePolygons() {
    clearPolygonLayers();
    tomtom.reachableRange(constructRangeBatchRequest())
        .go()
        .then(displayMarkerPolygons());
}

function constructRangeBatchRequest() {
    var queries = [];

    pizzaMarkers.forEach(function (marker) {
        var query = {
            origin: [marker.getLatLng().lat, marker.getLatLng().lng],
            timeBudgetInSec: reachableRangeBudgetTimeInSeconds
        };
        queries.push(query);
    });
    return queries;
}

function clearPolygonLayers() {
    polygonLayers.forEach(function (layer) {
        map.removeLayer(layer);
    })
}

1. The first line inside the displayReachableRangePolygons method executes a clearPolygonLayers function.

  • In this function, all polygons which are in the polygonLayers array are removed from the map.

2. In this tutorial you are using batch services to send multiple requests to the TomTom Routing API at the same time.

  • The constructRangeBatchRequest method returns an array of queries which are sent to the batch routing service.
  • Each query is defined by specifying an origin and a timeBudgetInSec properties. See the following code example.

var query = {
    origin: [marker.getLatLng().lat, marker.getLatLng().lng],
    timeBudgetInSec: reachableRangeBudgetTimeInSeconds
};

3. When the batch calculate route service returns a response, a displayMarkerPolygons method is executed.

  • You can define a displayMarkerPolygons method with the following code:

    function displayMarkerPolygons() {
        return function (polygons) {
            polygons.forEach(function (rangeData, index) {
                if (pizzaMarkers[index]) {
                    addPolygonToMap(rangeData, pizzaMarkers[index].feature.properties.polygonColor)
                }
            });
        };
    }
    
    function addPolygonToMap(rangeData, polygonColor) {
        var polygon = L.geoJson(rangeData, {
            style: createMarkerPolygonStyle(polygonColor)
        }).addTo(map);
        polygonLayers.push(polygon);
    }
    
    function createMarkerPolygonStyle(color) {
        return {
            color: color,
            opacity: 0,
            fillOpacity: 0.68
        };
    }
  • Inside this method, geojson polygons received from the batch calculate reachable range service are added to the map.
  • Each range polygon uses a style with a different color.
  • Polygon colors are separately defined inside the geojson.js file for each point from which ranges are calculated.

4. Make sure to add an on ‘click’ button event for a button with a calculate-range id inside the initControlMenu method. See the following code example.

document.getElementById('calculate-range').addEventListener('click', displayReachableRangePolygons);

5. Now each time when the Calculate route button is clicked:

  • Polygons with the reachable range from each pizza restaurant are shown on the map.
  • All polygons have display styles with different colors.

 

6. You can drag-and-drop any of the pizza marker icons to a new position on the map.

  • When you drop the marker, reachable range polygon coordinates are re-calculated and new polygons are displayed on the map.

Calculate travel time

Now that you can see polygons with the reachable range on the map, you need to calculate travel times from each pizza restaurant to the customer.

1. Add a calculateTravelTime function to the pizza-delivery.js file. See the following code example.

function calculateTravelTime() {
    if (clientMarker && pizzaMarkers.length > 0) {
        tomtom.routing((constructBatchRequest()))
            .go()
            .then(displayBatchRoutingResults)
    }
}

2. Inside this function, routing queries are sent to the batch routing service.

  • When a response is received from the service, results with travel times from each pizza restaurant to the customer are displayed on the side menu.

3. Execute the calculateTravelTime method from inside the displayReachableRange function.

  • Insert following line as the last line of the displayReachableRange function:

calculateTravelTime();

4. You also need a method to construct multiple routing queries which are sent to the batch service.

  • To construct queries, add a constructBatchRequest method. See the following code example.

function constructBatchRequest() {
    var queries = [];

    pizzaMarkers.forEach(function (marker) {
        var query = {
            locations: [marker.getLatLng(), clientMarker.getLatLng()],
            computeTravelTimeFor: 'all'
        };
        queries.push(query)
    });
    return queries;
}
  • Inside this method for each pizza restaurant, you create a routing API query with the following parameters:
    • Locations: Each query has a different origin (pizza restaurant) and a common destination location (the customer),
    • computeTravelTimeFor: ‘all’ The query result contains an additional information regarding travel times.
  • The constructBatchRequest method returns an array of routing service queries, which can be used in a single batch routing service request.

 

5. In the final step, add a displayBatchRoutingResults method to display the information about travel times from each pizza restaurant to the customer inside the side menu. See the following code example.

function displayBatchRoutingResults(resultData) {
    var indexShortestTime;
    var shortestTime;
    resultData.forEach(function (routeData, index) {
        var pizzaElement = document.getElementById(pizzaPrefixId + (index + 1));
        pizzaElement.classList.remove('active');
        var travelTimesElements = pizzaElement.getElementsByClassName('travel-time-minutes');
        if (travelTimesElements.length > 0) {
            pizzaElement.removeChild(travelTimesElements[0]);
        }

        if (!routeData.error) {
            var travelTime = routeData.features[0].properties.summary.travelTimeInSeconds;
            if (!shortestTime || shortestTime > travelTime) {
                indexShortestTime = index;
                shortestTime = travelTime;
            }
            var travelTimeSpan = document.createElement('span');
            travelTimeSpan.innerHTML = Math.ceil(travelTime / 60).toString() + ' mins';
            travelTimeSpan.classList.add('travel-time-minutes');
            pizzaElement.appendChild(travelTimeSpan);
        }
    });
    if (typeof indexShortestTime !== 'undefined' || indexShortestTime !== null) {
        document.getElementById(pizzaPrefixId + (indexShortestTime + 1)).classList.add('active');
    }
    map.closePopup();
    createAndBindPopups();
}
  • This method for each result received from the batch routing service, which did not end with an error, takes information about the travel time in seconds and displays this information at the bottom of the side menu.
  • Travel times are inserted inside HTML elements with ids equal to:

    pizzaPrefixId + (index + 1);
    

    Where the pizzaPrefixId variable is equal to “pizza-” and index is an index of the batch routing query results array.

6. Additionally, the CSS class active is assigned to the pizza restaurant HTML element on the side menu.

  • This adds CSS styling with an additional border to this element.
  • The last two lines from this method are used to:
    • Close any open marker popup.
    • Recreate popups to include travel time information.

Choosing delivery time

The prototype which you downloaded as part of the prerequisites includes a functionality to allow a user to select the time when the customer requested to have the pizza delivered.

1. First, you need methods to get information about the delivery time that the user chooses on the time slider from the side menu and if the delayed delivery is currently enabled.

2. You can find out if the delivery is delayed by checking if deliveryTimeSlider is enabled inside the isDeliveryDelayed method. See the following code example.



function getDeliveryDateTime() {
    var timeParts = document.getElementById('delivery-time').innerText.split(':');
    var chosenDeliveryDate = new Date();
    chosenDeliveryDate.setHours(parseInt(timeParts[0]));
    chosenDeliveryDate.setMinutes(parseInt(timeParts[1]));
    return chosenDeliveryDate;
}

function getDepartureDeliveryDate() {
    return new Date(getDeliveryDateTime().getTime() - reachableRangeBudgetTimeInSeconds * MILLIS_IN_SECOND);
}

function isDeliveryDelayed() {
    return deliveryTimeSlider.isEnabled();
}

3. In next step add the following lines:

if (isDeliveryDelayed()) {
    var departureDeliveryDate = getDepartureDeliveryDate();
    if (departureDeliveryDate > new Date()) {
        query.departAt = departureDeliveryDate;
    }
}

inside the constructRangeBatchRequest and constructBatchRequest methods where you create reachable range and calculate route queries.

Make sure that those lines are inserted just before any routing queries are pushed to the array, with all queries to be sent in a batch request.

4. In the final step, add an ‘on-change’ event listener to the HTML element with a delivery-toggle id at the end of the initControlMenu method. See the following code example.

document.getElementById('delivery-toggle').addEventListener('change', toggleDelayedDelivery);

With the implementation:

function toggleDelayedDelivery() {
    deliveryTimeSlider.toggle();
    setDeliveryTimeSliderValue();
    setDeliveryTimeSpanValue();
}

Now you should be able to choose the delivery time using the slider from the side menu to configure the time when the customer wants to have his pizza delivered.

Marker popups with custom icons

To display a popup with information about the specific pizza restaurant after you click on its marker:

1. Add a createAndBindPopups method. See the following code example.

function createAndBindPopups() {
    pizzaMarkers.forEach(function (marker, index) {
        var pizzaMenuDiv = document.getElementById(pizzaPrefixId + (index + 1));
        var pizzaSpans = pizzaMenuDiv.getElementsByTagName('span');
        var pizzaString = '<span><b>' + pizzaSpans[0].textContent + '</b>';
        if (pizzaSpans.length > 1) {
            pizzaString += '<br>' + pizzaSpans[1].textContent;
        }
        pizzaString += '</span>';

        var customPopup = '<div class="pizza-balloon">' + pizzaString +
            '<img src="img/pizza_oven_illustration.png" alt="pizza oven"/></div>';
        marker.bindPopup(customPopup).addTo(map);
    });
}
  • Inside this method you need to create and bind the popup for each pizza marker from the pizzaMarkers array.
  • Popups are created by using HTML markup and values from HTML elements with travel times.
  • Created popups are later bound to pizza restaurant markers and added to the map.

2. To initialize popups, execute the createAndBindPopups method after the displayPizzaMarkers method. See the following code example.

initControlMenu();
displayPizzaMarkers();
createAndBindPopups();

Traffic layer

To display a traffic flow information on the map:

1. Add a toggleTrafficFlowLayer method. See the following code example.

function toggleTrafficFlowLayer() {
    var flowLayer = tomtom.L.MapUtils.findLayersByName('vectorTrafficFlow', map)[0];
    if (!flowLayer) {
        map.addLayer(new L.TomTomVectorTrafficFlowLayer());
    } else {
        map.removeLayer(flowLayer);
    }
}

2. To make sure that the traffic layer is shown (or hidden) whenever the Show traffic toggle switch is clicked, add an ‘on-change’ event listener to the traffic-toggle HTML element inside the initControlMenu method. See the following code example.

document.getElementById('traffic-toggle').addEventListener('change', toggleTrafficFlowLayer);

You can see full application here:

Pizza delivery application

Summary

This tutorial explained:

  • How to create an application which uses the batch routing service to send multiple queries to the Routing API in a single request.
  • The Routing API reachable range endpoint was used to calculate and display range polygons on the map and the routing calculate route endpoint was used to calculate a travel time from an origin to the destination.
  • An autocomplete search box map control was used to find addresses and points of interest based on user’s input. The Full application is visible below.

All the source code of the application can be found on Github.

You are here