Search for content

Toll Routing

screenshot

In this tutorial you will learn how a Toll Routing App is implemented. It will allow to display a map, select and calculate a route with toll between waypoints. The aim of the code examples is to illustrate the use of the toll usecase of PTV Developer Routing API

Try it! Download from GitHub

Prerequisites

  • Basic JavaScript knowledge.
  • Basic knowledge of JavaScript asynchronous programming using Promises.
  • Helpful: Basic knowledge of jQuery. A page like the W3 Schools jQuery tutorial is sufficient.

How to use this tutorial

The tutorial will start similar to a cooking recipe. After the basic steps how to create the app are explained, the tutorial will focus on the major building blocks.

Getting started

Create a HTML page to display a map

Create a new HTML file called index.html and initialize a Leaflet map using the code below.

<html>
  <head>
    <title>Toll Routing</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" />
    <link rel="stylesheet" href="style.css" />
    <script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    <script src="./tollRouting.js"></script>
    </head>
      <body>
          <id="map"></div>
      </body>
</html>

 

Create JavaScript File

Create a new JavaScript file called tollRouting.js. Add the code below and replace the string "YOUR_API_KEY" with your PTV Developer API key. After loading the index.html file into a web browser, you should see a panable and zoomable map.

$(document).ready(function() {
    const api_key = "YOUR_API_KEY";
    const tileURL = `https://api.myptv.com/rastermaps/v1/image-tiles/{z}/{x}/{y}?apiKey=${api_key}`;
    const routingURL = "https://api.myptv.com/routing/v1/routes";
    const coordinates = L.latLng(49.0, 8.4);
    var startPosition = null;
    var destinationPosition = null;
    var routingURLObj = new URL(routingURL);
    var geoURLObj = new URL(geoURL);
    var map = new L.Map('map', {
        center: coordinates,
        zoom: 13,
        zoomControl: false
    });
    L.control.zoom({
        position: 'bottomright'
    }).addTo(map);
    var tileLayer = new L.tileLayer(tileURL, {
        attribution: '©2023, PTV Group, HERE'
    }).addTo(map);
});

 

Explanation of the important code lines

This jQuery expression indicates that the code inside the curly brackets is executed only if the root index.html file is completely loaded.

$(document).ready(function() {
    ...
});

The first argument given to the L.Map constructor references the DOM element with the id map. The following lines show how a leaflet map control like zooming is created.

var map = new L.Map('map', {
    center: coordinates,
    zoom: 13,
    zoomControl: false
});
L.control.zoom({
    position: 'bottomright'
}).addTo(map);

The expressions {z}/{x}/{y} in the tileURL are placeholders which are replaced by default.

const tileURL = `https://api.myptv.com/rastermaps/v1/image-tiles/{z}/{x}/{y}?apiKey=${api_key}`;

More information on this can be found in the Raster Maps API Documentation

All other replacements have to be defined in the constructor parameters of L.tileLayer.

var tileLayer = new L.tileLayer(tileURL, {
    attribution: '©2023, PTV Group, HERE'
}).addTo(map);

The rendered HTML page constitutes already an interactive zoom-able and pan-able map. The next section shows how to create input controls for start and destination.

Adding Waypoints to the Map

To set waypoints on the map a event handler has to be registered:

$(document).ready(function () {
    ...
    map.on('click', onMapClick);

Then implement the event handler. Additionally an event handler for the waypoint is implemented. It triggers when right clicking a waypoint and removes it.

function onMapClick(e) {
    var marker = L.marker(e.latlng).addTo(map);
    marker.on('contextmenu', removeMarker)
}
function removeMarker(e) {
    map.eachLayer((layer) => {
        if (layer instanceof L.Marker && layer._latlng === e.latlng) {
            layer.remove();
        }
    });
}

 

 

Adding controls to manipulate the Toll Routing

Beginning from this step, we will focus on the most important chunks of the source code.

The following function adds controls for the vehicle Profile and for the toll results.

function addControls() {
    const routingControl = L.control({ position: 'topleft' });
    routingControl.onAdd = function (map) {
        const div = L.DomUtil.create('div', 'routing-control');
		const html = `
				<h2>Toll Routing</h2>
		<div>
		  <label for="vehicleProfile" style="display: block;">Vehicle Profile</label>
		  <select name="vehicleProfile" id="vehicleProfile" style="display: block;">
		    <option value="">No Value</option>
		    <option value="EUR_TRAILER_TRUCK">EUR_TRAILER_TRUCK</option>
		    <option value="EUR_TRUCK_40T">EUR_TRUCK_40T</option>
		    <option value="EUR_TRUCK_11_99T">EUR_TRUCK_11_99T</option>
		    <option value="EUR_TRUCK_7_49T">EUR_TRUCK_7_49T</option>
		    <option value="EUR_VAN">EUR_VAN</option>
		    <option value="EUR_CAR">EUR_CAR</option>
		    <option value="USA_8_SEMITRAILER_5AXLE">USA_8_SEMITRAILER_5AXLE</option>
		    <option value="USA_5_DELIVERY">USA_5_DELIVERY</option>
		    <option value="USA_1_PICKUP">USA_1_PICKUP</option>
		    <option value="AUS_LCV_LIGHT_COMMERCIAL">AUS_LCV_LIGHT_COMMERCIAL</option>
		    <option value="AUS_MR_MEDIUM_RIGID">AUS_MR_MEDIUM_RIGID</option>
		    <option value="AUS_HR_HEAVY_RIGID">AUS_HR_HEAVY_RIGID</option>
		    <option value="IMEA_TRUCK_40T">IMEA_TRUCK_40T</option>
		    <option value="IMEA_TRUCK_7_49T">IMEA_TRUCK_7_49T</option>
		    <option value="IMEA_VAN">IMEA_VAN</option>
		    <option value="IMEA_CAR">IMEA_CAR</option>
		  </select>
		</div>
		<div>
		  <label for="co2EmissionClass" style="display: block;">Co2 Emission Class</label>
		  <select name="co2EmissionClass" id="co2EmissionClass" style="display: block;">
		    <option value="">No Value</option>
		    <option value="1">1</option>
		    <option value="2">2</option>
		    <option value="3">3</option>
		    <option value="4">4</option>
		    <option value="5">5</option>
		  </select>
		</div>
		<div>
		  <label for="engineType" style="display: block;">Engine Type</label>
		  <select name="engineType" id="engineType" style="display: block;">
		    <option value="">No Value</option>
		    <option value="COMBUSTION">COMBUSTION</option>
		    <option value="ELECTRIC">ELECTRIC</option>
		    <option value="HYBRID">HYBRID</option>
		  </select>
		</div>
		<div>
		  <label for="emissionStandard" style="display: block;">Emission Standard</label>
		  <select name="emissionStandard" id="emissionStandard" style="display: block;">
		    <option value="">No Value</option>
		    <option value="NONE">NONE</option>
		    <option value="EURO_0">EURO_0</option>
		    <option value="EURO_1">EURO_1</option>
		    <option value="EURO_2">EURO_2</option>
		    <option value="EURO_3">EURO_3</option>
		    <option value="EURO_4">EURO_4</option>
		    <option value="EURO_5">EURO_5</option>
		    <option value="EURO_EEV">EURO_EEV</option>
		    <option value="EURO_6">EURO_6</option>
		    <option value="EURO_6C">EURO_6C</option>
		    <option value="EURO_6D_TEMP">EURO_6D_TEMP</option>
		    <option value="EURO_6D">EURO_6D</option>
		    <option value="EURO_6E">EURO_6E</option>
		    <option value="EURO_7">EURO_7</option>
		  </select>
		</div>
		<div>
		  <span>Reference time</span>
		  <table style="border: none;">
		    <tr>
		      <td height="20px" style="text-align:right; padding: 0px 5px 0px 0px; border: none;">
		        <input type="date" id="referenceDate" value="2023-08-29" />
		      </td>
		      <td height="20px" style="text-align:right; border: none;">
		        <input type="time" id="referenceTime" value="12:00" />
		      </td>
		    </tr>
		  </table>
		</div>
		<div>
		  <label for="currency" style="display: block;">Currency</label>
		  <select name="currency" id="currency" style="display: block;">
		    <option value="">No Value</option>
		    <option value="USD">USD</option>
		    <option value="EUR">EUR</option>
		    <option value="CHF">CHF</option>
		    <option value="PLN">PLN</option>
		    <option value="GBP">GBP</option>
		  </select>
		</div>
		<div>
		  <input type="checkbox" id="TOLL_COSTS" checked>
		  <label for="TOLL_COSTS">TOLL_COSTS</label>
		</div>
		<div>
		  <input type="checkbox" id="TOLL_SYSTEMS">
		  <label for="TOLL_SYSTEMS">TOLL_SYSTEMS</label>
		</div>
		<div>
		  <input type="checkbox" id="TOLL_SECTIONS">
		  <label for="TOLL_SECTIONS">TOLL_SECTIONS</label>
		</div>
		<div>
		  <input type="checkbox" id="TOLL_EVENTS">
		  <label for="TOLL_EVENTS">TOLL_EVENTS</label>
		</div>
`	;
    routingControl.addTo(map);
    document.getElementById("TOLL_COSTS").addEventListener("click", fetchRoute);
    document.getElementById("TOLL_SYSTEMS").addEventListener("click", fetchRoute);
    document.getElementById("TOLL_SECTIONS").addEventListener("click", fetchRoute);
    document.getElementById("TOLL_EVENTS").addEventListener("click", fetchRoute);
    document.getElementById("vehicleProfile").addEventListener("change", fetchRoute);
}

 

Call the PTV Routing API

With fetchRoute(), a call to the PTV Routing API is made.

function fetchRoute() {
    const waypoints = [];
    map.eachLayer((layer)  => {
        if (layer instanceof L.Marker) {
            waypoints.push(layer._latlng);
        }
    });
    if (waypoints.length  > 1) {
        fetch(
            routingURL + getTollQuery(waypoints)),
            {
                method: "GET",
                headers: {
                    "Content-Type": "application/json",
                    "apiKey": api_key
                }
            })
            .then(response  => response.json()
                .then(result  &> {
                    displayPolyline(JSON.parse(result.polyline));
                    console.log(result);
                }));
    }
}

 

Explanation of the important code lines

First all Waypoints are found by retrieving its coordinates from leaflets map layers. Then, if there are more two or more waypoints the Routing API is called.

The function getTollQuery() returns the query for the request.

function getTollQuery(waypoints) {
    var tollQuery = "?results=POLYLINE";
    var tollResults = [];

    if (document.getElementById("TOLL_COSTS").checked) {
        tollResults.push("TOLL_COSTS");
    }
    if (document.getElementById("TOLL_SYSTEMS").checked) {
        tollResults.push("TOLL_SYSTEMS");
    }
    if (document.getElementById("TOLL_SECTIONS").checked) {
        tollResults.push("TOLL_SECTIONS");
    }
    if (document.getElementById("TOLL_EVENTS").checked) {
        tollResults.push("TOLL_EVENTS");
    }

    if (tollResults.length > 0) {
        tollQuery += "," + tollResults.join();
    }
    if (document.getElementById("vehicleProfile").value !== "") {
        tollQuery += "&profile=" + document.getElementById("vehicleProfile").value;
    }
    if (document.getElementById("co2EmissionClass").value !== "") {
        tollQuery += "&vehicle[co2EmissionClass]=" + parseInt(document.getElementById("co2EmissionClass").value);
    }
    if (document.getElementById("currency").value !== "") {
        tollQuery += "&options[currency]=" + document.getElementById("currency").value;
    }
    if (document.getElementById("emissionStandard").value !== "") {
         tollQuery += "&vehicle[emissionStandard]=" + document.getElementById("emissionStandard").value;
    }
    if (document.getElementById("referenceTime").value !== "" && document.getElementById("referenceDate").value) {
        const date = new Date(document.getElementById('referenceDate').value + 'T' + document.getElementById('referenceTime').value).toISOString()
        tollQuery += "&options[startTime]=" + date
    }

    waypoints.forEach(waypoint => {
        tollQuery += "&waypoints=" + waypoint.lat + "," + waypoint.lng;
    });

    // e.g. ?results=POLYLINE&results=TOLL_COSTS&profile=...
    //          &options[currency]=EUR&waypoints=...&waypoints=...
    return tollQuery;
}

After the fetch method is finished, the response will then be converted to JSON. In the next line, the polyline will be displayed on the map.

.then(response => response.json()
    .then(result => {
        displayPolyline(map, JSON.parse(result.polyline));
    }));

The function displayPolyline contains the style of the polyline. We use the polyline in the geoJSON format as provided by the response of the routing API call.

var polylineLayer = null;
function displayPolyline(polyline) {
    if (polylineLayer !== null) {
        map.removeLayer(polylineLayer);
    }

    var myStyle = {
        "color": '#2882C8',
        "weight": 5,
        "opacity": 0.65
    };

    polylineLayer = L.geoJSON(polyline, {
        style: myStyle
    }).addTo(map);

    map.fitBounds(polylineLayer.getBounds());
}

Add Stylesheet

Create a new file called style.css. and put the following code in it.

#map {
    width: 100%;
    height: 100%;
}

.routing-control {
    background-color: #ffffff;
    width: 270px;
    height: 220px;
    padding-left: 30px;
    border-radius: 15px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}

How to display the result

To display the result a control can be added to the map as previously done in the step Adding controls to manipulate the Toll Routing. As example the toll sections will be displayed.

function addSectionResultControl() {
    const resultControl = L.control({ position: 'topright' });
    resultControl.onAdd = function (map) {
        const div = L.DomUtil.create('div', 'result-control');
        const html = `
        <h2>Toll Sections</h2>
        <div id="tollReportTableWrapper">
            <table id="tollSectionsTable"></table>
        </div>
    `;
        div.innerHTML = html;

        L.DomEvent.disableScrollPropagation(div);
        L.DomEvent.disableClickPropagation(div);

        return div;
    };
    resultControl.addTo(map);
}

The code below will add the toll sections and their price to the container. If a currency option was used in the request the selected currency will be displayed instead of the toll sections local currency. For the sake of this tutorial the display of the result is simplyfied.

Please note that for the sake of this tutorial the display of the result is simplyfied. When routing through multiple countries with different currency without using a currency option this function might not display the result correctly.

function displayTollSections(sections) {
    $("#tollSectionsTable").append($.parseHTML(getRow(["Section", "Price"])));
    sections.forEach((section) => {
        let price = 0;
        let currency = "";
        if (section.costs[0].convertedPrice) {
            section.costs.forEach((c) => {
                price += c.convertedPrice.price;
            });
            currency = section.costs[0].convertedPrice.currency;
        } else {
            section.costs.forEach((c) => {
                price += c.price;
            });
            currency = section.costs[0].currency;
        }
        $("#tollSectionsTable").append($.parseHTML(getRow([section.displayName, price + " " +  currency])));
    });
}

function getRow(columns) {
    var row = '';
    columns.forEach(col => {
        row += '<td>' + col + '</td>'
    });
    return '<tr>' + row + '</tr>'
}

Add the styling for the result control to style.css.

#tollReportTableWrapper {
    overflow: auto;
    max-height: 200px;
}

.result-control {
    background-color: #ffffff;
    width: 300px;
    min-height: 60px;
    max-height: 260px;
    padding-left: 30px;
    padding-right: 30px;
    padding-bottom: 10px;
    border-radius: 15px;
    box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}