In this tutorial you will learn how an Emissions Routing App is implemented. PTV Developer Routing API.
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
- Request an API key:
- Register and login at myptv.com
- Activate PTV Developer
- Create your API key
- Download an editor like Visual Studio Code or Atom to work on HTML and JavaScript.
- Look up more details of requests and responses in the respective reference manual.
- Read the Leaflet reference documentation for further information.
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>Emissions 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="./emissionsRouting.js"></script>
</head>
<body>
<div id="map" style="width:100%;height:100%;"></div>
</body>
</html>
Create JavaScript File
Create a new JavaScript file called emissionsRouting.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 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 Emissions Routing
Beginning from this step, we will focus on the most important chunks of the source code.
The following function adds controls for the emission method and for the vehicle Profile with custom fuelType and averageFuelConsumption.
function addControls() {
const routingControl = L.control({position: 'topleft'});
routingControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'routing-control');
const html = `
<h2>Emission Routing</h2>
<div>
<div>
<label for="emissionProfile" style="display: block;">Emission Profile</label>
<select name="emissionProfile" id="emissionProfile" style="display: block; width: 100%;">
<option value="EMISSIONS_EN16258_2012">EN16258_2012</option>
<option value="EMISSIONS_EN16258_2012_HBEFA">EN16258_2012_HBEFA</option>
<option value="EMISSIONS_FRENCH_CO2E_DECREE_2017_639">FRENCH_CO2E_DECREE_2017_639</option>
<option value="EMISSIONS_ISO14083_2022_HBEFA_EUROPE">EMISSIONS_ISO14083_2022_HBEFA_EUROPE</option>
<option value="EMISSIONS_ISO14083_2022_HBEFA_NORTH_AMERICA">EMISSIONS_ISO14083_2022_HBEFA_NORTH_AMERICA</option>
</select>
</div>
<div>
<label for="vehicleProfile" style="display: block;">Vehicle Profile</label>
<select name="vehicleProfile" id="vehicleProfile" style="display: block; width: 100%;">
<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_1_PICKUP">USA_1_PICKUP</option>
<option value="USA_5_DELIVERY">USA_5_DELIVERY</option>
<option value="USA_8_SEMITRAILER_5AXLE">USA_8_SEMITRAILER_5AXLE</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>
</select>
</div>
<div>
<label for="fuelType" id="fuelTypeLabel" style="display: block;">Fuel Type</label>
<select name="fuelType" id="fuelType" style="display: block; width: 100%;">
<option value="GASOLINE">GASOLINE</option>
<option value="DIESEL">DIESEL</option>
<option value="COMPRESSED_NATURAL_GAS">COMPRESSED_NATURAL_GAS</option>
<option value="LIQUEFIED_PETROLEUM_GAS">LIQUEFIED_PETROLEUM_GAS</option>
</select>
</div>
<div>
<label for="averageFuelConsumption" id="averageFuelConsumptionLabel" style="display: block;">Fuel Consumption (l / 100km)</label>
<input type="number" id="averageFuelConsumption" name="averageFuelConsumption" style= "width: 100%;"
min="1" step=".5" value="35" max="100"/>
</div>
</div>
`;
div.innerHTML = html;
L.DomEvent.disableScrollPropagation(div);
L.DomEvent.disableClickPropagation(div);
return div;
};
routingControl.addTo(map);
document.getElementById('emissionProfile').addEventListener('change', fetchRoute);
document.getElementById('vehicleProfile').addEventListener('change', fetchRoute);
document.getElementById('fuelType').addEventListener('change', calculateRoute);
document.getElementById('averageFuelConsumption').addEventListener('change', calculateRoute);
}
Call the PTV Routing API
With calculateRoute(), a call to the PTV Routing API is made. With fetchRoute(), we handle the emissions logic.
function fetchRoute() {
if (emissionProfileHBEFA[document.getElementById('emissionProfile').value]) {
document.getElementById('averageFuelConsumption').disabled = true;
document.getElementById('fuelType').disabled = true;
document.getElementById('fuelTypeLabel').textContent = 'Fuel Type, not supported with HBEFA';
document.getElementById('averageFuelConsumptionLabel').textContent = 'Fuel Consumption (l / 100km), taken from HBEFA';
calculateRoute();
}
else {
document.getElementById('averageFuelConsumption').disabled = false;
document.getElementById('fuelType').disabled = false;
document.getElementById('fuelTypeLabel').textContent = 'Fuel Type';
document.getElementById('averageFuelConsumptionLabel').textContent = 'Fuel Consumption (l / 100km)';
document.getElementById('averageFuelConsumption').value = averageFuelConsumption[document.getElementById('vehicleProfile').value];
calculateRoute();
}
}
function calculateRoute() {
const waypoints = [];
map.eachLayer((layer) => {
if (layer instanceof L.Marker) {
waypoints.push(layer._latlng);
}
});
if (waypoints.length > 1) {
fetch(routingURL + getEmissionsQuery(waypoints),
{
method: "GET",
headers: {
"Content-Type": "application/json",
"apiKey": api_key
}
}
)
.then((response) => response.json()
.then((result) => {
clearResults();
displayPolyline(JSON.parse(result.polyline));
displayResults(result);
})
);
}
}
Explaination of the important code lines
First all Waypoints are found by retrieving their coordinates from leaflet's map layers. Then, if there are two or more waypoints the Routing API is called.
In case of a HBEFA-based consumption like EMISSIONS_EN16258_2012_HBEFA
, custom fuelType
is not supported and averageFuelConsumption
is taken from HBEFA data. Therefore, both fuelType
and averageFuelConsumption
are disabled, with a little text explanation.
Otherwise, we set the default value of averageFuelComsumption
to the value defined in the predefined profiles. The user can still define a custom value by tuning the HTML control.
The function getEmissionsQuery() returns the query for the request.
function getEmissionsQuery(waypoints) {
let query = '?results=POLYLINE';
const results = [];
results.push(document.getElementById('emissionProfile').value);
if (results.length > 0) {
query += ',' + results.join();
}
query += '&profile=' + document.getElementById('vehicleProfile').value;
if(!emissionProfileHBEFA[document.getElementById('emissionProfile').value]){
query += '&vehicle[averageFuelConsumption]=' + document.getElementById('averageFuelConsumption').value;
query += '&vehicle[fuelType]=' + document.getElementById('fuelType').value;
}
waypoints.forEach((waypoint) => {
query += '&waypoints=' + waypoint.lat + ',' + waypoint.lng;
});
return query;
}
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.
.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 Emissions Routing. As example, the emissions metrics will be displayed.
function addEmissionsResultControl() {
const resultControl = L.control({position: 'topleft'});
resultControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'result-control-left');
const html = `
<h2>Emissions</h2>
<div id="emissionsReportTableWrapper">
<table id="emissionsCostsTable"></table>
</div>
`;
div.innerHTML = html;
L.DomEvent.disableScrollPropagation(div);
L.DomEvent.disableClickPropagation(div);
return div;
};
resultControl.addTo(map);
}
The code below will add the different emissions metrics in a container. For the sake of this tutorial the display of the result is simplified.
function displayEmissions(emissions) {
const table = document.createElement('table');
table.id = 'emissionsReportTableWrapper';
const thead = document.createElement('thead');
thead.innerHTML = `
<tr>
<td>Type</td>
<td>Amount</td>
</tr>`;
table.appendChild(thead);
const tbody = document.createElement('tbody');
for (const key in emissions) {
if (emissions.hasOwnProperty(key)) {
const tr = document.createElement('tr');
const th = document.createElement('td');
th.textContent = key;
const td = document.createElement('td');
td.textContent = `${emissions[key].toFixed(4)} ${units[key]}`;
tr.appendChild(th);
tr.appendChild(td);
tbody.appendChild(tr);
}
}
table.appendChild(tbody);
document.getElementById('emissionsReportTableWrapper').appendChild(table);
}
Next steps to try
In order to learn more about emissions, please refer to the PTV Developer Emissions API concept page and the PTV Developer Emissions API code samples.