Search for content

Block Intersecting Roads

Block Intersecting Roads

In this tutorial you will learn how to implement a Block Intersecting Roads Routing App. It will allow to display a map, add polylines to block the intersecting roads, and calculate a route taking into account the blocked roads or not. The aim of the code examples is to illustrate the use of the block intersecting roads 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 an 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>Block Intersecting Roads 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="./index.js"></script>
    </head>
      <body>
          <div id="map"></div>
          <script type="text/javascript" src="https://unpkg.com/maplibre-gl@2.1.7/dist/maplibre-gl.js"></script>
          <script type="text/javascript" src="https://unpkg.com/@maplibre/maplibre-gl-leaflet@0.0.15/leaflet-maplibre-gl.js"></script>
      </body>
</html>

Create JavaScript File

Create a new JavaScript file called index.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.

const api_key = "YOUR_API_KEY";

// global variables for map rendering
var map;
...

$(document).ready(function() {
    //Lazy load the plugin to support right-to-left languages such as Arabic and Hebrew.
    maplibregl.setRTLTextPlugin(
        'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js',
        null,
        true
    );

    map = new L.Map('map', {
        center: L.latLng(49, 8.4),
        zoom: 13,
        maxZoom: 18,
        zoomControl: false
    });

    L.control.zoom({
        position: 'bottomright'
    }).addTo(map);

    var vectorStyleUrl = 'https://vectormaps-resources.myptv.com/styles/latest/standard.json';
    const tileLayer = new L.maplibreGL({
        attribution: '&copy; ' + new Date().getFullYear() + ' PTV Group, HERE',
        interactive:false,
        maxZoom: 18,
        style: vectorStyleUrl,
        transformRequest: (url) => {
          let transformedUrl = url;
          let mapsPathIndex = url.indexOf('/maps/');

          if (mapsPathIndex > 0) {
            transformedUrl = 'https://developer.myptv.com' + url.substring(mapsPathIndex);
            return {
              url: `${transformedUrl}`
            };
          }
          return null;
        }
    }).addTo(map);

    ...
});

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.

map = new L.Map('map', {
    center: L.latLng(49, 8.4),
    zoom: 13,
    zoomControl: false
});

L.control.zoom({
    position: 'bottomright'
}).addTo(map);

Define the vector tiles with MapLibre.

var vectorStyleUrl = "https://vectormaps-resources.myptv.com/styles/latest/standard.json";

const tileLayer = new L.maplibreGL({
    attribution: '&copy; ' + new Date().getFullYear() + ' PTV Group, HERE',
    interactive:false,
    maxZoom: 18,
    style: vectorStyleUrl,
    transformRequest: (url) => {
      let transformedUrl = url;
      let mapsPathIndex = url.indexOf('/maps/');

      if (mapsPathIndex > 0) {
        transformedUrl = 'https://api.myptv.com/' + url.substring(mapsPathIndex) + '?apiKey=' + api_key;
        return {
          url: `${transformedUrl}`
        };
      }
      return null;
    }
}).addTo(map);

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

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);
    ...
    waypointsLayer.addTo(map);

The layer with the waypoints is also added to the map.
Then implement the event handler. Additionaly an event handler for the waypoint is implemented. It triggers when right clicking a waypoint and removes it.

function onMapClick(e) {
    const marker = L.marker(e.latlng).addTo(waypointsLayer);
    marker.on('contextmenu', removeWaypoint);
}
function removeMarker(e) {
    if (routeCalculationInProgress)
        return;

    waypointsLayer.eachLayer((layer) => {
        if (layer instanceof L.Marker && layer._latlng === e.latlng) {
            waypointsLayer.removeLayer(layer);
        }
    });
    calculateRoute();
}

 

Call the PTV Routing API

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

async function calculateRoute() {
    if (routeCalculationInProgress)
        return;

    document.getElementById('map').style.cursor = 'wait';

    routeCalculationInProgress = true;

    const waypoints = [];
    waypointsLayer.eachLayer((layer) => {
        if (layer instanceof L.Marker) {
            waypoints.push(layer._latlng);
        }
    });
    if (waypoints.length > 1) {
        clearPolyline();

        // normal route
        await fetch(
            "https://api.myptv.com/routing/v1/routes" + getQuery(waypoints, false),
            {
              method: 'GET',
              headers: {
                'Content-Type': 'application/json',
                'apiKey': api_key
              }
            }
        )
        .then((response) => response.json()
            .then((result) => {
                addPolyline(JSON.parse(result.polyline), false);
                addToResult(result, false);
            })
        );

        // finally display the results
        displayResults();
        map.fitBounds(routesLayer.getBounds(), {padding: [50, 50]});
    } else {
        clearResults();
    }
    document.getElementById('map').style.cursor = '';
    routeCalculationInProgress = false;
}

function getQuery(waypoints, addBlockIntersectingRoads) {
    let query = '?results=POLYLINE&options[polylineMapType]=VECTOR';
    waypoints.forEach((waypoint) => {
        query += '&waypoints=' + waypoint.lat + ',' + waypoint.lng;
    });
    return query;
}

 

Explanation of the important code lines

First of 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.

After the fetch method is finished, the response will then be converted to JSON. In the next line, the result is added to the result object, and the polyline is stored on the layer.

.then(response => response.json()
    .then(result => {
        addPolyline(JSON.parse(result.polyline), false);
        addToResult(result, false);
    }));

The function addPolyline contains the style of the polyline. The polyline is in the geoJSON format as provided by the response of the routing API call.
The function addToResult contains the final result of the route.
Finally the distance, travel time (etc...) are displayed in the summaryControl.

var routesLayer = L.geoJSON();
var routeResults = { 'stdRoute': {}, 'birRoute': {} };
var summaryControl;
var routeCalculationInProgress = false;

$(document).ready(function () {
    ...
    routesLayer.addTo(map);
    addSummaryControl();
}

function addSummaryControl() {
    summaryControl = L.control({position: 'topright'});
    summaryControl.onAdd = function(map) {
        const div = L.DomUtil.create('div', 'summary-control');
        const html = `
            <h2>Summary</h2>
            <div id="summaryTableWrapper">
                <h3>Standard route</h3>
                <div class="key-value"><span>Distance: </span><span id="stdRoute-distance">-</span></div>
                <div class="key-value"><span>Travel time: </span><span id="stdRoute-traveltime">-</span></div>
                <div class="key-value"><span>Route violated: </span><span id="stdRoute-violated">-</span></div>
                <h3>Route with blocked roads</h3>
                <div class="key-value"><span>Distance: </span><span id="birRoute-distance">-</span></div>
                <div class="key-value"><span>Travel time: </span><span id="birRoute-traveltime">-</span></div>
                <div class="key-value"><span>Route violated: </span><span id="birRoute-violated">-</span></div>
            </div>
        `;
        div.innerHTML = html;

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

        return div;
    };
}

function clearPolyline() {
    routesLayer.clearLayers();
}

function clearResults() {
    summaryControl.remove();
    clearPolyline();
}

function addPolyline(polyline, addBlockIntersectingRoads) {
    const myStyle = {
        'color': '#2882C8',
        'weight': 5,
        'opacity': 0.65
    };
    const polylineLayer = L.geoJSON(polyline, { style: myStyle }).addTo(routesLayer);
}

function addToResult(result, addBlockIntersectingRoads) {
    routeResults.stdRoute = { 'distance': convertDistance(result.distance), 'traveltime': convertTime(result.travelTime), 'violated': result.violated };
}

function displayResults() {
    summaryControl.addTo(map);
    document.getElementById('stdRoute-distance').innerText = routeResults.stdRoute.distance;
    document.getElementById('stdRoute-traveltime').innerText = routeResults.stdRoute.traveltime;
    document.getElementById('stdRoute-violated').innerText = routeResults.stdRoute.violated;
}

Implement blockIntersectionRoads feature

To implement the complete feature, some layers are needed: one layer will contains the final polyline ; some other layers are used when drawing the polylines.

var blockIntersectionRoadsLayer = L.featureGroup();

var curIntersectingPolylineLayer = null;
var curIntersectingMarkerLayer = null;
var curIntersectingMouselineLayer = L.featureGroup();

var editionMode = false;

The layers are added to the map. We defined also a control to allow the edition of polylines.

$(document).ready(function() {
    ...
    blockIntersectionRoadsLayer.addTo(map);
    curIntersectingMouselineLayer.addTo(map);
    addRoutingControl();
}

The routingControl displays the number of created polylines. It is also possible to add a new polyline, and another button can delete al polylines.

function addRoutingControl() {
    const routingControl = L.control({position: 'topleft'});
    routingControl.onAdd = function(map) {
        const div = L.DomUtil.create('div', 'routing-control');
        const html = `
            <h2>Intersecting polylines</h2>
            <span id="blockIntersectionPolylineCount">0</span><span> polyline(s) have been created. (max. 10)</span>
            <div class="group space-between">
                <button type="button" id="btn-add-polyline">Add polyline</button>
                <button type="button" id="btn-delete-polylines">Delete all polylines</button>
            </div>
        `;
        div.innerHTML = html;

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

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

    document.getElementById('btn-add-polyline').addEventListener("click", addIntersectingPolyline);
    document.getElementById('btn-delete-polylines').addEventListener("click", resetAllIntersectingPolylines);
}

To add a new polyline, several functions should be implemented or modified.
When moving the mouse, a line is drawn between the latest point of the polyline and the mouse's cursor. When clicking on the map, an intermediate point of the polyline is added. Using the right-click finishes the polyline.
When a polyline is created, a route calculation is automatically requested.

function addIntersectingPolyline() {
    if (routeCalculationInProgress)
        return;

    editionMode = true;
    document.getElementById('btn-add-polyline').disabled = true;
    document.getElementById('btn-delete-polylines').disabled = true;
    switchDescriptionBannerText();

    curIntersectingPolylineLayer = L.polyline([], {color: 'red'});
    curIntersectingPolylineLayer.addTo(map);
    curIntersectingMarkerLayer = L.featureGroup();
    curIntersectingMarkerLayer.addTo(map);
}

$(document).ready(function() {
    ...
    map.on('contextmenu', OnMapContextMenu);
    map.on('mousemove', onMouseMove);
}

function onMapClick(e) {
    if (routeCalculationInProgress)
        return;

    if (editionMode) {
        curIntersectingPolylineLayer.addLatLng(e.latlng);
        const marker = L.circleMarker(e.latlng, {color: '#AA0000'}).addTo(curIntersectingMarkerLayer);
    } else {
        if (waypointsLayer.getLayers().length == 5) {
            alert('The maximum number of waypoints is reached');
        } else {
            const marker = L.marker(e.latlng).addTo(waypointsLayer);
            marker.on('contextmenu', removeWaypoint);
            calculateRoute();
        }
    }
}

function onMouseMove(e) {
    if (!editionMode) return;
    let points = curIntersectingPolylineLayer.getLatLngs();
    curIntersectingMouselineLayer.clearLayers();
    const marker = L.circleMarker(e.latlng, {color: '#FF7F7F'}).addTo(curIntersectingMouselineLayer);
    if (points.length > 0) {
        const line = L.polyline([points[points.length - 1], e.latlng], {color: '#FF7F7F', dashArray: '20, 20', dashOffset: '0'}).addTo(curIntersectingMouselineLayer);
    }
}

function OnMapContextMenu(e) {
    if (routeCalculationInProgress)
        return;

    if (editionMode) {
        let fireCalculateRoute = false;
        // finalize polyline
        let points = curIntersectingPolylineLayer.getLatLngs();
        if (points.length > 1) {
            const newPolyline = L.polyline(points, {color: '#AA0000'}).addTo(blockIntersectionRoadsLayer);
            fireCalculateRoute = true;
        }
        curIntersectingMouselineLayer.clearLayers();
        curIntersectingPolylineLayer.remove();
        curIntersectingMarkerLayer.remove();
        editionMode = false;
        document.getElementById('blockIntersectionPolylineCount').innerText = blockIntersectionRoadsLayer.getLayers().length;
        document.getElementById('btn-delete-polylines').disabled = false;
        document.getElementById('btn-add-polyline').disabled = (blockIntersectionRoadsLayer.getLayers().length >= 10); // impossible to create more than 10 !
        switchDescriptionBannerText();
        if (fireCalculateRoute) {
            calculateRoute();
        }
    }
}

To reset all intersecting polylines, the following function is implemented. A route calculation is triggered also.

function resetAllIntersectingPolylines() {
    if (routeCalculationInProgress)
        return;

    blockIntersectionRoadsLayer.clearLayers();
    document.getElementById('blockIntersectionPolylineCount').innerText = blockIntersectionRoadsLayer.getLayers().length;
    document.getElementById('btn-add-polyline').disabled = false;
    calculateRoute();
}

Then, the calculation function should be adapted in order to call PTV Developer API with the blockIntersectingRoads option. A new fetch method is called beside the already existing one for the standard route calculation.

async function calculateRoute() {
    ...
    if (waypoints.length > 1) {
        clearPolyline();
        ...
        // route considering the blockings
        await fetch(
            "https://api.myptv.com/routing/v1/routes" + getQuery(waypoints, true),
            {
              method: 'GET',
              headers: {
                'Content-Type': 'application/json',
                'apiKey': api_key
              }
            }
        )
        .then((response) => response.json()
            .then((result) => {
                addPolyline(JSON.parse(result.polyline), true);
                addToResult(result, true);
            })
        );
        ...
    }
}

function getQuery(waypoints, addBlockIntersectingRoads) {
    let query = '?results=POLYLINE&options[polylineMapType]=VECTOR';
    waypoints.forEach((waypoint) => {
        query += '&waypoints=' + waypoint.lat + ',' + waypoint.lng;
    });
    if (addBlockIntersectingRoads) {
        queryOption = '';
        blockIntersectionRoadsLayer.eachLayer((layer) => {
            if (layer instanceof L.Polyline) {
                let line = '';
                layer.getLatLngs().forEach((latlng) => {
                    line += latlng.lat + ',' + latlng.lng + ',';
                });
                queryOption += line.slice(0, -1) + '|';
            }
        });
        if (queryOption.length > 1) {
            query += '&options[blockIntersectingRoads]=' + queryOption.slice(0,-1);
        }
    }
    return query;
}

Finally, some functions are adapted to display the result of the route calculation with blockIntersectingRoads.
Both polylines are added with different styles. The distance and travel time of both routes are displayed in the summaryControl.

function addPolyline(polyline, addBlockIntersectingRoads) {
    const myStyle = {
        'color': addBlockIntersectingRoads ? '#FF6A00': '#2882C8',
        'weight': addBlockIntersectingRoads ? 8 : 5,
        'opacity': addBlockIntersectingRoads? 0.85 : 0.65
    };
    const polylineLayer = L.geoJSON(polyline, { style: myStyle }).addTo(routesLayer);
    if (addBlockIntersectingRoads) {
        polylineLayer.bringToBack();
    }
}

function addToResult(result, addBlockIntersectingRoads) {
    if (addBlockIntersectingRoads) {
        routeResults.birRoute = { 'distance': convertDistance(result.distance), 'traveltime': convertTime(result.travelTime), 'violated': result.violated };
    } else {
        routeResults.stdRoute = { 'distance': convertDistance(result.distance), 'traveltime': convertTime(result.travelTime), 'violated': result.violated };
    }
}

function displayResults() {
    summaryControl.addTo(map);
    document.getElementById('stdRoute-distance').innerText = routeResults.stdRoute.distance;
    document.getElementById('stdRoute-traveltime').innerText = routeResults.stdRoute.traveltime;
    document.getElementById('stdRoute-violated').innerText = routeResults.stdRoute.violated;
    document.getElementById('birRoute-distance').innerText = routeResults.birRoute.distance;
    document.getElementById('birRoute-traveltime').innerText = routeResults.birRoute.traveltime;
    document.getElementById('birRoute-violated').innerText = routeResults.birRoute.violated;
}

Add an information banner

A banner is finally added to the showcase in order to help the user. The banner displays two messages depending on the editionMode.

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

function addDescriptionBanner() {
    const banner = L.control({position: 'bottomleft'});
    banner.onAdd = function(map) {
        const div = L.DomUtil.create('div', 'banner');
        const html = `<p><span class="" id="bannerDescriptionText"/>-</span></p>`;
        div.innerHTML = html;

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

        return div;
    };
    banner.addTo(map);
    switchDescriptionBannerText(false);
}

function switchDescriptionBannerText() {
    if (editionMode) {
        document.getElementById('bannerDescriptionText').innerText =
        `Left click to add a point to the intersecting polyline.
        Right click to validate the polyline and exit the edition mode.`;
    } else {
        document.getElementById('bannerDescriptionText').innerText =
        `Left click to add a waypoint and right click to remove one. (max. 5)
        The waypoint order is determined by the order of their creation.`;
    }
}

Next steps to try

In order to learn more about this feature, please refer to the PTV Developer API concept page and the PTV Developer API code samples.