Taxi cars dispatcher

Intro

This tutorial shows you how to create a web application for a taxi company using version 6 of the TomTom Maps SDK for Web.

It's not easy to identify which taxi can get the fastest to the customer by just looking at the map. Using the Calculate Route service you can obtain the travel time of each individual taxi to a particular point, in this case, a passenger. In this application you will use this service so that the dispatcher will pick the taxi that reaches the customer the fastest way.

In this tutorial you will create a map with markers pointing at taxis and passenger's locations. The application calculates each route and chooses the fastest one.

Prerequisites:

To start using the TomTom Maps SDK for Web, you need an API Key. If you don't have it, please visit a How to get a TomTom API key site and create one.

Map initialization

To display a TomTom map use the following code. You can just copy and paste it to your favorite code editor.

1<!DOCTYPE html>
2<html>
3 <head>
4 <title>Taxi cars dispatcher</title>
5 <meta charset="UTF-8" />
6 <!-- Replace version in the URL with desired library version -->
7 <link
8 rel="stylesheet"
9 type="text/css"
10 href="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/<version>/maps/maps.css"
11 />
12 <script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/<version>/maps/maps-web.min.js"></script>
13 <script src="https://api.tomtom.com/maps-sdk-for-web/cdn/6.x/<version>/services/services-web.min.js"></script>
14 <style>
15 body {
16 margin: 0
17 }
18 #map {
19 height: 100vh;
20 width: 100vw;
21 }
22 </style>
23 </head>
24
25 <body>
26 <div id="map"></div>
27 <script>
28 const apiKey = "<your-api-key>"
29 const map = tt.map({
30 key: apiKey,
31 container: "map",
32 center: [4.876935, 52.360306],
33 zoom: 13,
34 })
35 </script>
36 </body>
37</html>

Note that the container parameter of tt.map constructor requires the same value as the id of a HTML <div> element where the map will be embedded. Replace <your-api-key> placeholder with your API Key.

You can find the custom markers used for this tutorial on GitHub.

Passenger

Add a passenger marker to the map.

To add the passenger marker into the map:

  • First: You need to add variables to the JavaScript section: passengerInitCoordinates , passengerMarker, and a createPassengerMarker(markerCoordinates, popup) function.
  • Second: The passengerInitCoordinates variable needs to be declared before the map initialization.
1const passengerInitCoordinates = [4.876935, 52.360306]
2let passengerMarker
3
4function createPassengerMarker(markerCoordinates, popup) {
5 const passengerMarkerElement = document.createElement("div")
6 passengerMarkerElement.innerHTML =
7 "<img src='img/man-waving-arm_32.png' style='width: 30px; height: 30px';>"
8 return new tt.Marker({ element: passengerMarkerElement })
9 .setLngLat(markerCoordinates)
10 .setPopup(popup)
11 .addTo(map)
12}

To center the map on the passenger, replace the map definition like the one in the following example:

1const map = tt.map({
2 key: apiKey,
3 container: "map",
4 center: passengerInitCoordinates,
5 zoom: 13,
6})

Ensure that the placeholder has been replaced by your API Key.

The following code puts the passenger marker on the map and opens a popup.

1passengerMarker = createPassengerMarker(
2 passengerInitCoordinates,
3 new tt.Popup({ offset: 35 }).setHTML(
4 "Click anywhere on the map to change passenger location."
5 )
6)
7
8passengerMarker.togglePopup()

Change the passenger marker position.

Now you can add a feature that allows users to click on the map to move a passenger. To do so, add an event listener on map click.

  • In the handler function, call the reverseGeocode method with a position parameter from the event’s property geoResponse.
  • In the then method define a callback that executes a drawPassengerMarkerOnMap function.

Add a conditional statement in the drawPassengerMarkerOnMap function: if the Reverse Geocoding Response contains an address, then a marker with the previous position is removed and a new one is created at the indicated location.

1function drawPassengerMarkerOnMap(geoResponse) {
2 if (
3 geoResponse &&
4 geoResponse.addresses &&
5 geoResponse.addresses[0].address.freeformAddress
6 ) {
7 passengerMarker.remove()
8 passengerMarker = createPassengerMarker(
9 geoResponse.addresses[0].position,
10 new tt.Popup({ offset: 35 }).setHTML(
11 geoResponse.addresses[0].address.freeformAddress
12 )
13 )
14 passengerMarker.togglePopup()
15 }
16}

Now you can add an event that draws the passenger marker each time you click on the map area.

1map.on("click", function (event) {
2 const position = event.lngLat
3 tt.services
4 .reverseGeocode({
5 key: apiKey,
6 position: position,
7 })
8 .then(function (results) {
9 drawPassengerMarkerOnMap(results)
10 })
11})

Taxi cabs

Add the taxi markers definition, where the coordinates for taxi cabs are fixed for simplicity.

1let taxiConfig
2function setDefaultTaxiConfig() {
3 taxiConfig = [
4 createTaxi("CAR #1", "#006967", [4.902642, 52.373627], "img/cab1.png"),
5 createTaxi("CAR #2", "#EC619F", [4.927198, 52.365927], "img/cab2.png"),
6 createTaxi("CAR #3", "#002C5E", [4.893488, 52.347878], "img/cab3.png"),
7 createTaxi("CAR #4", "#F9B023", [4.858433, 52.349447], "img/cab4.png"),
8 ]
9}
10
11function createTaxi(
12 name,
13 color,
14 coordinates,
15 iconFilePath,
16 iconWidth = 55,
17 iconHeight = 55
18) {
19 return {
20 name: name,
21 color: color,
22 icon:
23 "<img src=" +
24 iconFilePath +
25 " style='width: " +
26 iconWidth +
27 "px; height: " +
28 iconHeight +
29 "px;'>",
30 coordinates: coordinates,
31 }
32}

Add the taxi cabs into the map and an initialization of the setDefaultTaxiConfig function.

1setDefaultTaxiConfig()
2
3taxiConfig.forEach(function (taxi) {
4 const carMarkerElement = document.createElement("div")
5 carMarkerElement.innerHTML = taxi.icon
6 new tt.Marker({ element: carMarkerElement, offset: [0, 27] })
7 .setLngLat(taxi.coordinates)
8 .addTo(map)
9})
taxi-dispatcher

Submit button

To add the Submit button, the map <div> tag needs to be updated with the following code:

1<div id="map"></div>
2<div id="labels-container">
3 <label>Find the taxi that will arrive fastest</label>
4 <div id="route-labels"></div>
5 <input type="button" id="submit-button" value="Submit" />
6</div>

To see the button on the map, you need to add CSS styles.

1#submit-button {
2 background: #df1b12;
3 padding: 10px;
4 margin-top: 10px;
5 width: 100%;
6 color: white;
7 font-weight: bold;
8 transition: background-color 0.15s ease-in-out;
9 text-transform: uppercase;
10 border: none;
11 outline: none;
12}
13
14#submit-button:hover {
15 cursor: pointer;
16 background: #b1110e;
17}
18
19#labels-container {
20 font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
21 position: fixed;
22 top: 10px;
23 right: 10px;
24 width: 400px;
25 padding: 10px;
26 margin: 10px;
27 background-color: white;
28 box-shadow: rgba(0, 0, 0, 0.45) 2px 2px 2px 0px;
29}
30
31#labels-container label {
32 line-height: 2;
33 font-size: 1.2em;
34 font-weight: bold;
35}
36
37#labels-container #route-labels div {
38 border-left: 6px solid;
39 padding-left: 5px;
40 margin-top: 3px;
41}
42
43#route-labels div:hover {
44 cursor: pointer;
45 box-shadow: 0px 2px #888888;
46}
submit button

Clear routes

To clear routes we need to first declare a variable.

let routes = []

The clear function:

  • Removes routes from the map.
  • Clears the routes array.
  • Calls setDefaultTaxiConfig.
1function clear() {
2 routes.forEach(function (child) {
3 map.removeLayer(child[0])
4 map.removeLayer(child[1])
5 map.removeSource(child[0])
6 map.removeSource(child[1])
7 })
8 routes = []
9 setDefaultTaxiConfig()
10 passengerMarker.togglePopup()
11}

Next, the clear function can be embedded into submitButtonHandler.

1function submitButtonHandler() {
2 clear()
3}

Don’t forget to assign the submitButtonHandler listener to the button in the HTML document.

1document
2 .getElementById("submit-button")
3 .addEventListener("click", submitButtonHandler)

Now you can go to the next chapter where the clear function can be used.

Draw routes

The variables: routeWeight and routeBackgroundWeight need to be initialized as in the following example.

const routeWeight = 9
const routeBackgroundWeight = 12

For convenience, let's also create a taxiPassengerBatchCoordinates array and a updateTaxiBatchLocations(passengerCoordinates) function which will be helpful with preparing data for a Batch Routing call later.

1let taxiPassengerBatchCoordinates = []
2
3function updateTaxiBatchLocations(passengerCoordinates) {
4 taxiPassengerBatchCoordinates = []
5 taxiConfig.forEach((taxi) => {
6 taxiPassengerBatchCoordinates.push(
7 taxi.coordinates + ":" + passengerCoordinates
8 )
9 })
10}

Call the updateTaxiBatchLocations(passengerCoordinates) function right after the setDefaultTaxiConfig();.

setDefaultTaxiConfig()
updateTaxiBatchLocations(passengerInitCoordinates)

The drawAllRoutes function uses the calculateRoute method to get JSON objects with that route’s geometry coordinates for each four taxi locations inside the taxiPassengerBatchCoordinates array and a current passenger location.

  • The JSON is used to create GeoJSON route layers by calling a buildStyle function.
  • Created layers are then added to the map.
1let bestRouteIndex
2
3function drawAllRoutes() {
4 tt.services
5 .calculateRoute({
6 batchMode: "sync",
7 key: apiKey,
8 batchItems: [
9 { locations: taxiPassengerBatchCoordinates[0] },
10 { locations: taxiPassengerBatchCoordinates[1] },
11 { locations: taxiPassengerBatchCoordinates[2] },
12 { locations: taxiPassengerBatchCoordinates[3] },
13 ],
14 })
15 .then(function (results) {
16 results.batchItems.forEach(function (singleRoute, index) {
17 const routeGeoJson = singleRoute.toGeoJson()
18 const route = []
19 const route_background_layer_id = "route_background_" + index
20 const route_layer_id = "route_" + index
21
22 map
23 .addLayer(
24 buildStyle(
25 route_background_layer_id,
26 routeGeoJson,
27 "black",
28 routeBackgroundWeight
29 )
30 )
31 .addLayer(
32 buildStyle(
33 route_layer_id,
34 routeGeoJson,
35 taxiConfig[index].color,
36 routeWeight
37 )
38 )
39
40 route[0] = route_background_layer_id
41 route[1] = route_layer_id
42 routes[index] = route
43
44 if (index === bestRouteIndex) {
45 const bounds = new tt.LngLatBounds()
46 routeGeoJson.features[0].geometry.coordinates.forEach(function (
47 point
48 ) {
49 bounds.extend(tt.LngLat.convert(point))
50 })
51 map.fitBounds(bounds, { padding: 150 })
52 }
53
54 map.on("mouseenter", route_layer_id, function () {
55 map.moveLayer(route_background_layer_id)
56 map.moveLayer(route_layer_id)
57 })
58
59 map.on("mouseleave", route_layer_id, function () {
60 bringBestRouteToFront()
61 })
62 })
63 bringBestRouteToFront()
64 })
65}
66
67function bringBestRouteToFront() {
68 map.moveLayer(routes[bestRouteIndex][0])
69 map.moveLayer(routes[bestRouteIndex][1])
70}
71
72function buildStyle(id, data, color, width) {
73 return {
74 id: id,
75 type: "line",
76 source: {
77 type: "geojson",
78 data: data,
79 },
80 paint: {
81 "line-color": color,
82 "line-width": width,
83 },
84 layout: {
85 "line-cap": "round",
86 "line-join": "round",
87 },
88 }
89}

The route layer is drawn twice to create an outline:

  1. route[0] (route_background_layer_id) in black, with a thick stroke.
  2. route[1] (route_layer_id) using the color from config and a normal stroke.

Add a processMatrixResponse function. It extracts 3 elements from each Response and uses them to populate 3 separate arrays:

  • travel times
  • route lengths
  • traffic delays
1function processMatrixResponse(result) {
2 const travelTimeInSecondsArray = []
3 const lengthInMetersArray = []
4 const trafficDelayInSecondsArray = []
5 result.matrix.forEach(function (child) {
6 travelTimeInSecondsArray.push(
7 child[0].response.routeSummary.travelTimeInSeconds
8 )
9 lengthInMetersArray.push(child[0].response.routeSummary.lengthInMeters)
10 trafficDelayInSecondsArray.push(
11 child[0].response.routeSummary.trafficDelayInSeconds
12 )
13 })
14 drawAllRoutes()
15}

Next, add the functions: convertToPoint, buildDestinationsParameter, and buildOriginsParameter.

1function convertToPoint(lat, long) {
2 return {
3 point: {
4 latitude: lat,
5 longitude: long,
6 },
7 }
8}
9
10function buildOriginsParameter() {
11 const origins = []
12 taxiConfig.forEach(function (taxi) {
13 origins.push(convertToPoint(taxi.coordinates[1], taxi.coordinates[0]))
14 })
15 return origins
16}
17
18function buildDestinationsParameter() {
19 return [
20 convertToPoint(
21 passengerMarker.getLngLat().lat,
22 passengerMarker.getLngLat().lng
23 ),
24 ]
25}

The callMatrix function uses a matrixRouting method to create a Request for the Matrix Routing API.

  • The go method then calls the API asynchronously, to allow for service processing time.
  • Use the then function to define a callback that processes the Response, including travelTime in seconds and distance in meters.

To deal with the volume of route plans that may be required, this implementation uses the Routing API.

  • In a simple implementation with one passenger and four taxis, the service would calculate a small matrix of 4 x 1 routes.
  • But in a real life application, there may be many passengers requesting a taxi at the same time and many available taxis.
  • A scenario with 3 passengers and 20 taxis creates a matrix of 20 x 3 = 60 routes.

For more information on the API, see Matrix Routing.

1function callMatrix() {
2 const origins = buildOriginsParameter()
3 const destinations = buildDestinationsParameter()
4 tt.services
5 .matrixRouting({
6 key: apiKey,
7 origins: origins,
8 destinations: destinations,
9 traffic: true,
10 })
11 .then(processMatrixResponse)
12}

Finally, you can add the callMatrix function call and update submitButtonHandler.

1function submitButtonHandler() {
2 clear()
3 callMatrix()
4}

And the winner is …

Create a modal to inform the user which route is the fastest one.

  • The modal appears when all calculations are done.
  • Put a <div> with a modal id in the <body> tag, following the map <div> tag.
1<div id="modal">
2 <div id="modal-content"></div>
3</div>

Add the CSS for the modal to your style section:

1#modal {
2 display: none;
3 position: fixed;
4 z-index: 1100;
5 left: 0;
6 top: 0;
7 width: 100%;
8 height: 100%;
9 overflow: auto;
10 background-color: rgba(0, 0, 0, 0.5);
11}
12
13#modal-content {
14 background-color: lightgray;
15 color: #555;
16 font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
17 font-weight: bold;
18 text-align: center;
19 margin: 15% auto;
20 padding: 20px;
21 border: 1px solid #888;
22 width: 20%;
23}

Next, add the necessary variables.

1const modal = document.getElementById("modal")
2const modalContent = document.getElementById("modal-content")
3const fastestRouteColor = "#65A7A9"

To display and remove the modal from the screen, add the following code:

1modal.addEventListener("click", function () {
2 modal.style.display = "none"
3})
4
5function displayModal() {
6 modalContent.innerText = "Dispatch car number " + String(bestRouteIndex + 1)
7 modal.style.display = "block"
8}

The user can click anywhere to hide the modal.

Now is a time to choose the winner in a modifyFastestRouteColor function.

  • If it is the fastest route, it is displayed above all other routes.
  • The function copies the array travelTimeInSecondsArray to a new sortedTab array and sorts it.

Because the default comparison function is alphabetic (meaning that '11' is less than '2'), the function uses a custom comparison function.

  • The following one takes two values (such as 7 and 9) and subtracts one from the other.
  • If the result is negative (-2), the function sorts 7 as lower than 9.
1function modifyFastestRouteColor(travelTimeInSecondsArray) {
2 const sortedTab = travelTimeInSecondsArray.slice()
3 sortedTab.sort(function (a, b) {
4 return a - b
5 })
6 bestRouteIndex = travelTimeInSecondsArray.indexOf(sortedTab[0])
7 taxiConfig[bestRouteIndex].color = fastestRouteColor
8}

What's left is updating the processMatrixResponse, as shown in the following code:

1function processMatrixResponse(result) {
2 const travelTimeInSecondsArray = []
3 const lengthInMetersArray = []
4 const trafficDelayInSecondsArray = []
5 result.matrix.forEach(function (child) {
6 travelTimeInSecondsArray.push(
7 child[0].response.routeSummary.travelTimeInSeconds
8 )
9 lengthInMetersArray.push(child[0].response.routeSummary.lengthInMeters)
10 trafficDelayInSecondsArray.push(
11 child[0].response.routeSummary.trafficDelayInSeconds
12 )
13 })
14 modifyFastestRouteColor(travelTimeInSecondsArray)
15 drawAllRoutes()
16 displayModal()
17}
taxi dispatcher v5 modal

Summary

This tutorial has demonstrated:

  1. How to do a Matrix Routing API query.
  2. How to draw a route on the map.
  3. How to work with a route by setting event listeners , setting a style , and bringing the route to the front or back.

All the source code of that application can be found on GitHub.