Search for content

Emissions Routing

In this tutorial you will learn how an Emissions Routing App is implemented. It will allow to display a map, select a vehicle and an emission method, and calculate a route with emissions between waypoints. The aim of the code examples is to illustrate the use of the emissions use case 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>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_2023_HBEFA_EUROPE">EMISSIONS_ISO14083_2023_HBEFA_EUROPE</option>
                      <option value="EMISSIONS_ISO14083_2023_HBEFA_NORTH_AMERICA">EMISSIONS_ISO14083_2023_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.