Search for content

Position Matching


 In this tutorial you will learn how a basic Position Matching App is implemented. We will show an interactive map, use a click in the map to specify the position, invoke the endpoint, and display the result returned by the position matching endpoint. To concentrate on the basic principles, most parameters are hard-coded and there is only very basic error handling. The aim of the code examples is to illustrate the use of the PTV Developer Map Matching 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 guide you like a cooking recipe. After creating an html page with a vector map, the tutorial will focus on how to call the position matching endpoint.

Getting started

Create an HTML page to display a map

Create a new HTML file called index.html and prepare it to contain a MapLibre map using the code below. Make sure you also define a size to the map div in the style section of your HTML file. Otherwise, you will not be able to see the map.

<html>
<head>
  <title>Position Matching</title>
  <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@1.15.2/dist/maplibre-gl.css" />
  <script src="https://unpkg.com/maplibre-gl@2.1.9/dist/maplibre-gl.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <style>
    #map {
      width: 100%;
      height: 80vh;
    }
  </style>
</head>
<body>
  <h2>Position Matching Tutorial</h2>
  <div id='map'></div>
  <script src="PositionMatching.js"></script>
</body>
</html>

 

Create the JavaScript code to show the map

Create a new JavaScript file called PositionMatching.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 pannable and zoomable map.

$(document).ready(() => {
    const api_key = "YOUR_API_KEY";
    const styleUrl = "https://vectormaps-resources.myptv.com/styles/latest/standard.json";
    const mapLocation = [8.4055677, 49.0070036];

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

    const map = new maplibregl.Map({
        container: 'map',
        zoom: 11,
        pitch: 0,
        minZoom: 2,
        center: mapLocation,
        antialias: true,
        hash: true,
        style: styleUrl,
        transformRequest: (url, resourceType) => {
            if (resourceType === 'Tile') {
                return {
                    url: url,
                    headers: {'apiKey': api_key}
                }
            }
            return null;
        },
    });

    // Add controls to the map.
    map.addControl(new maplibregl.NavigationControl());

    //
    // add position matching code here
    //
});

 

Explanation of the code

The jQuery expression

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

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

The code inside this statement is very similar to the PTV Developer Vector Maps API sample, which also explains some details that cannot be covered here. After initializing some variables (like the API Key), the code continues with initializing the map and some controls in it. At the end, a comment marks the place where we are going to insert the code calling the position matching endpoint.

If you start the code at this point, you will see an interactive map that can be zoomed and panned.

Get the input position by listening to click events in the map

Replace the comment // add position matching code here with the following code:

let mapMarker = undefined; // we keep track of the marker here
function addMarker(lngLat, textAsHtml) {
  const popup = new maplibregl.Popup({closeButton: false})
    .setHTML(textAsHtml);
  const marker = new maplibregl.Marker()
    .setLngLat(lngLat)
    .setPopup(popup)
    .addTo(map);
  mapMarker = marker;
  marker.togglePopup();
}
function clearMarker() {
  if (mapMarker) mapMarker.remove();
  mapMarker = undefined;
}
map.on('click', (event) => {
  clearMarker();
  addMarker(event.lngLat, `You clicked at ${event.lngLat}`);
});

 

Explanation of the code

The code defines two helper methods to add and clear a marker and registers a listener for click events, which adds a marker with a popup showing some information about the clicked position.

If you start the code at this point and click into the map, you will get a popup displaying the clicked input coordinates in WGS84 format (latitude, longitude).

Call the position matching endpoint

Replace the complete map.on('click' ...); block with the following code:

function callPositionMatching(lngLat, action) {
  fetch(
    getMapMatchRequest(lngLat),
    {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'apiKey': api_key
      },
    },
  )
    .then((response) => response.json())
    .then((result) => {
      showMapMatchResult(lngLat, result, action);
    });
}
map.on('click', (e) => {
  clearMarker();
  callPositionMatching(e.lngLat, (lngLat, result) => {
    addMarker(lngLat, `${result}`);
  });
});

 

Explanation of the code

The code defines a helper method callPositionMatching(), which calls the position matching endpoint as follows: After calling getMapMatchRequest(lngLat) to get the URL, it invokes the endpoint using fetch(). In the then() statements, the response body is converted to a json object and handed over to showMapMatchResult(lngLat, result, action) which displays the result.

The code will not yet run, because we first need to define the getMapMatchRequest() and showMapMatchResult() methods.

Construct the request URL

Insert the following code snippet before function callPositionMatching(){...};:

function getMapMatchRequest(lngLat) {
    let url = "https://api.myptv.com/mapmatch/v1/positions/";
    url = `${url + lngLat.lat}/${lngLat.lng}`;
    url = `${url}?results=SEGMENT_LOCATION_DESCRIPTORS`;
    return url;
}

 

Explanation of the code

The code defines the the function getMapMatchRequest(lngLat), which is responsible to construct the URL for the position matching request. The URL consists of

  • the protocol "https",
  • the host name "api.myptv.com",
  • the path to the endpoint "mapmatch/v1/positions",
  • latitude and longitude of the requested position as path elements,
  • any parameters as request parameters (here only the results parameter), and
  • the ApiKey, also as a request parameter.

 

To be able to run the code successfully, we are still missing another helper method.

Display the result

Insert the following code before function callPositionMatching(){...};:

function showMapMatchResult(lngLat, result, action) {
  const inputLatitude = normalize(lngLat.lat);
  const inputLongitude = normalize(lngLat.lng);
  if (Object.keys(result).length === 0) {
    action(lngLat, `(${inputLatitude}, ${inputLongitude}) --> <br> no result`);
  } else {
    if (result.longitude && result.latitude) {
      const resultLatitude = normalize(result.latitude);
      const resultLongitude = normalize(result.longitude);
      action({ lat: result.latitude, lng: result.longitude },
        `(${inputLatitude}, ${inputLongitude}) --> <br> (${resultLatitude}, ${resultLongitude}) : <br> ` +
        `${result.segmentLocationDescriptors.street} (${result.segmentLocationDescriptors.city})`);
    } else {
      action(lngLat, JSON.stringify(result, null, 4));
    }
  }
}
function normalize(value) {
  return value.toFixed(6);
}

 

Explanation of the code

The code defines a method showMapMatchResult(lngLat, result, action) that reads the result:

showMapMatchResult(lngLat, result, action) receives the original click point, the result object of the API call and an action object that shall be used to display the results. The code distinguishes three cases:

  • If the result object is empty, no result was returned by the endpoint. In this case, an appropriate message is displayed at the input coordinates.
  • If the result object is not empty and contains latitude and longitude, the position matching was successful. In this case, a message is constructed from the result object's fields and displayed at the result coordinates.
  • If the result object is not empty but contains no coordinates, an error is assumed. In this case, the result object is converted to a string and displayed as-is.

 

To improve the readability of the text shown in the marker's popup, we normalize the latitude and longitude by calling another helper function normalize(value) that cuts the value to six decimal digits.

If you start the code at this point and click into the map, you will get a popup displaying a message depending on which of the three cases occurred.

Next steps to try

As a next step you might want to display more properties of the result (like e.g. the country), or extend the request parameter results to request additional result fields, like segment attributes or geometry. In order to learn more about that, please refer to the PTV Developer Map Matching API documentation and the PTV Developer Map Matching API code samples.