Traffic Tutorial

Purpose

This tutorial application shows how to use the TomTom Maps SDK for Web and its traffic capabilities like the Traffic Flow Tiles Tier, the Traffic Incident Tiers, and the Traffic Incident Details service. The main screen of the application looks like this:

main

The application UI is based on the Bootstrap library. It divides the view into two main columns: the map and a sidebar. These contain options like:

  • A search box field that is implemented by using a convenient SearchBox Plugin.
  • Checkboxes for showing traffic incidents and flow by using the TrafficIncidentTier and TrafficFlowTilesTier
  • And the most important feature that enables you to draw a bounding box and directly call the Incident Details service.

Prerequisites

  1. Clone a GitHub repository with the source code of the Traffic Tutorial application
  2. If you don't have an API key visit a How to get a TomTom API key site and create one.
  3. Copy and paste the API key in the “YOUR_API_KEY” placeholder in the first line in traffic.js file.

SearchBox Plugin

The SearchBox Plugin is a convenient way to implement searching functionality inside your application. In this Traffic Tutorial application, the HTML part for this section looks like this:

1<div class="row row-border">
2 <div class="col pt-3 label">
3 <span>Choose your location</span>
4 <div id="search-panel-container" class="row">
5 <div id="search-panel" class="container-fluid pb-4"></div>
6 </div>
7 </div>
8</div>

As the tutorial UI is based on Bootstrap, the search option (like the other options) is implemented as a combination of rows and columns. The search-panel ID will be used inside JavaScript code to create the HTML content of the search box after initializing and configuring its instance.

1var apiKey = "YOUR_API_KEY"
2var searchBoxInstance
3
4var commonSearchBoxOptions = {
5 key: apiKey,
6 center: map.getCenter(),
7}
8
9function initApplication() {
10 searchBoxInstance = new tt.plugins.SearchBox(tt.services, {
11 minNumberOfCharacters: 0,
12 labels: {
13 placeholder: "Search",
14 },
15 noResultsMessage: "No results found.",
16 searchOptions: commonSearchBoxOptions,
17 autocompleteOptions: commonSearchBoxOptions,
18 })
19
20 searchBoxInstance.on("tomtom.searchbox.resultselected", onSearchBoxResult)
21 document
22 .getElementById("search-panel")
23 .append(searchBoxInstance.getSearchBoxHTML())
24 map.on("moveend", updateSearchBoxOptions)
25}
26
27function updateSearchBoxOptions() {
28 var updatedOptions = Object.assign(commonSearchBoxOptions, {
29 center: map.getCenter(),
30 })
31 searchBoxInstance.updateOptions({
32 minNumberOfCharacters: 0,
33 searchOptions: updatedOptions,
34 autocompleteOptions: updatedOptions,
35 })
36}
37
38function onSearchBoxResult(result) {
39 map.flyTo({
40 center: result.data.result.position,
41 speed: 3,
42 })
43}
44
45initApplication()

After initializing the searchBoxInstance variable with the tt.plugins.SearchBox method, the search box inner-html can be appended to the search-panel element. Because the applications allow the user to move the map, there is a need to react on the moveend map event and update the search area center each time when the map stops moving. By doing that, the user can get more precise results. When the search result is selected the onSearchBoxResult method is called, and the map changes its position to the center point of the new result.

Traffic Flow Tiles Tier

The TrafficFlowTilesTier class is a convenient class used to show traffic flow information with a few lines of code. The usage is simple and boils down to creating an instance of the TrafficFlowTilesTier class with proper options described in the documentation ( like the style or a traffic flow refresh time) and adding it to the map as shown in the following JavaScript code:

1var styleBase = "tomtom://vector/1/"
2var styleS1 = "s1"
3var styleRelative = "relative"
4var refreshTimeInMillis = 30000
5
6var trafficFlowTilesTier = new tt.TrafficFlowTilesTier({
7 key: apiKey,
8 style: styleBase + styleRelative,
9 refresh: refreshTimeInMillis,
10})
11
12function toggleTrafficFlowTilesTier() {
13 if (document.getElementById("flow-toggle").checked) {
14 map.addTier(trafficFlowTilesTier)
15 } else {
16 map.removeTier(trafficFlowTilesTier.getId())
17 }
18}
19document
20 .getElementById("flow-toggle")
21 .addEventListener("change", toggleTrafficFlowTilesTier)

Here is the HTML for this section:

1<div class="row align-items-center pt-2">
2 <div class="col-sm-2">
3 <img class="traffic-icon" src="img/traffic-flow.png" alt="" />
4 </div>
5 <div class="col pt-2">
6 <label for="flow-toggle" class="traffic-text">Traffic flow</label>
7 </div>
8 <div class="col-sm-3 pt-2 text-right">
9 <label class="switch">
10 <input id="flow-toggle" type="checkbox" />
11 <span class="toggle round"></span>
12 </label>
13 </div>
14</div>

When the user selects the ‘flow-toggle’ checkbox, traffic flow tiles should be visible on the map as shown on the following screen:

main

Traffic Incident Tier

The TrafficIncidentTier class is a convenient class used to display real time traffic incidents on the map without forcing the developer to directly access the IncidentDetails service. The usage of this class is very similar to the TrafficFlowTilesTier class. The developer needs to create a new instance of the TrafficIncidentTier class with proper options (like a style or traffic refresh time) as described in the documentation. The traffic incidents will become visible by adding it to the map as shown in the following JavaScript code example:

1var trafficIncidentCheckbox = document.getElementById("incidents-toggle")
2var styleBase = "tomtom://vector/1/"
3var styleS1 = "s1"
4var styleRelative = "relative"
5var refreshTimeInMillis = 30000
6
7var trafficIncidentsTier = new tt.TrafficIncidentTier({
8 key: apiKey,
9 incidentDetails: {
10 style: styleS1,
11 },
12 incidentTiles: {
13 style: styleBase + styleS1,
14 },
15 refresh: refreshTimeInMillis,
16})
17
18function showTrafficIncidentsTier() {
19 trafficIncidentCheckbox.checked = true
20 map.addTier(trafficIncidentsTier)
21}
22
23function hideTrafficIncidentsTier() {
24 trafficIncidentCheckbox.checked = false
25 map.removeTier(trafficIncidentsTier.getId())
26 clearIncidentList()
27 removeBoundingBox()
28}
29
30function toggleTrafficIncidentsTier() {
31 if (trafficIncidentCheckbox.checked) {
32 showTrafficIncidentsTier()
33 } else {
34 hideTrafficIncidentsTier()
35 }
36}
37
38trafficIncidentCheckbox.addEventListener("change", toggleTrafficIncidentsTier)

Here is the HTML for this section:

1<div class="row align-items-center pt-2">
2 <div class="col-sm-2">
3 <img class="traffic-icon" src="img/traffic_lights.png" alt="" />
4 </div>
5 <div class="col pt-2">
6 <label for="incidents-toggle" class="traffic-text">Traffic incidents</label>
7 </div>
8 <div class="col-sm-3 pt-2 text-right">
9 <label class="switch">
10 <input id="incidents-toggle" type="checkbox" />
11 <span class="toggle round"></span>
12 </label>
13 </div>
14</div>

When the user will select the ‘incidents-toggle’ checkbox, traffic incidents should be visible on the map as shown on the following screen:

main

This tutorial only shows the basic usage of the TrafficIncidentTier class. If you are interested in the more advanced features of this class, see one of the functional examples.

Incident Details Service

The tt.services.incidentDetails service provides current traffic incidents in a given region ( bounding box), on a given zoom with a given style by using the Traffic Incidents Details API.

In this section we’ll learn how to display a traffic incidents by using the Incident Details service and the bounding box parameter. Let’s take a look at the HTML part of the Bounding Box button:

1<div class="py-3 row row-border">
2 <div class="col">
3 <span class="show-traffic-layers">Bounding box for traffic incidents</span>
4 <button
5 id="bounding-box-button"
6 type="button"
7 class="btn btn-primary btn-block my-2"
8 >
9 DRAW BOUNDING BOX
10 </button>
11 </div>
12</div>
13<div id="incident-list-wrapper" class="row pt-0">
14 <div class="col">
15 <div id="incident-list-container" class="shadow p-0">
16 <div id="incident-list" class="list-group"></div>
17 </div>
18 </div>
19</div>
20(…)
21<div id="popup-wrapper"></div>

A few IDs from the preceding HTML example are used in the JavaScript code: incident-list , bounding-box-button and the popup-wrapper. The incident-list ID represents a list group which will contain traffic incidents after running the API which will be described later. The popup-wrapper ID is only used to display messages to the user. Let’s check what happens when the user clicks the “Draw bounding box” button:

1document
2 .getElementById("bounding-box-button").addEventListener("click", enableBoundingBoxDraw)
3
4function enableBoundingBoxDraw() {
5 removeBoundingBox()
6 clearIncidentList()
7 drawBoundingBoxButtonPressed = true
8 showInfoPopup("Click and drag to draw a bounding box")
9}
10
11function getPopupWrapper() {
12 return document.getElementById("popup-wrapper")
13}
14
15function showPopup(element) {
16 element.style.opacity = "0.9"
17}
18
19function showInfoPopup(message) {
20 var popupElementDiv = getPopupWrapper()
21 popupElementDiv.innerHTML = getPopupInnerHTML("popup-info", message)
22 showPopup(popupElementDiv)
23}
24
25function showErrorPopup(message) {
26 var popupElementDiv = getPopupWrapper()
27 popupElementDiv.innerHTML = getPopupInnerHTML("popup-error", message)
28 showPopup(popupElementDiv)
29}
30
31function hidePopup(delayInMilis) {
32 var element = getPopupWrapper()
33 if (delayInMilis == 0) {
34 element.style.opacity = "0"
35 } else {
36 setTimeout(function () {
37 element.style.opacity = "0"
38 }, delayInMilis)
39 }
40}
41
42function getPopupInnerHTML(popupClass, popupMessage) {
43 return `<div class="container ${popupClass} popup"> <div class="row"> <div class="col py-2"> <div class="row align-items-center pt-1"> <div class="col-sm-1"> <img src="img/error-symbol.png" alt=""/> </div><div id="popup-message" class="col"> ${popupMessage} </div></div></div></div></div>`
44}
45
46function removeBoundingBox() {
47 if (map.getSource(sourceID)) {
48 map.removeLayer(layerFillID)
49 map.removeLayer(layerOutlineID)
50 map.removeSource(sourceID)
51 }
52}
53
54function clearIncidentList() {
55 incidentListContainer.innerHTML = ""
56}

The enableBoundingBoxDraw event listener is added to the bounding box button. The event function:

  • Sets the drawBoundingBoxButtonPressed variable.
  • Removes an existing GeoJSON Layer (the fill and border of the bounding box) together with its GeoJSON source.
  • Clears the traffic incident.
  • Shows a message to the user that they can start drawing a bounding box, just like on the following screen:
main

Now the user can select an area of the map and draw a rectangle that represents the bounding box parameter passed to the Incident Details service. In order to draw the rectangle, a mouse event needs to be overwritten as shown in the following code example:

1var startCornerLngLat
2var endCornerLngLat
3var mousePressed
4var drawBoundingBoxButtonPressed
5var layerFillID = "layerFillID"
6var layerOutlineID = "layerOutlineID"
7var sourceID = "sourceID"
8var popupHideDelayInMilis = 4000
9
10map.on("mousedown", onMouseDown)
11map.on("mousemove", onMouseMove)
12map.on("mouseup", onMouseUp)
13
14function onMouseDown(eventDetails) {
15 if (drawBoundingBoxButtonPressed) {
16 eventDetails.preventDefault()
17 mousePressed = true
18 startCornerLngLat = eventDetails.lngLat
19 removeBoundingBox()
20 map.addSource(
21 sourceID,
22 getPolygonSource(startCornerLngLat, startCornerLngLat)
23 )
24 map.addLayer({
25 id: layerFillID,
26 type: "fill",
27 source: sourceID,
28 layout: {},
29 paint: {
30 "fill-color": "#666",
31 "fill-opacity": 0.1,
32 },
33 })
34 map.addLayer({
35 id: layerOutlineID,
36 type: "line",
37 source: sourceID,
38 layout: {},
39 paint: {
40 "line-width": 4,
41 "line-color": "#424242",
42 "line-dasharray": [2, 1],
43 "line-blur": 0.5,
44 },
45 })
46 }
47}
48
49function onMouseMove(eventDetails) {
50 if (mousePressed) {
51 endCornerLngLat = eventDetails.lngLat
52 updateRectangleData(startCornerLngLat, endCornerLngLat)
53 }
54}
55
56function onMouseUp(eventDetails) {
57 mousePressed = false
58 hidePopup(0)
59 if (drawBoundingBoxButtonPressed) {
60 endCornerLngLat = eventDetails.lngLat
61 if (bothLngLatAreDifferent(startCornerLngLat, endCornerLngLat)) {
62 updateRectangleData(startCornerLngLat, endCornerLngLat)
63 clearIncidentList()
64 displayTrafficIncidents(
65 getLngLatBoundsForIncidentDetailsCall(
66 startCornerLngLat,
67 endCornerLngLat
68 )
69 )
70 showTrafficIncidentsTier()
71 } else {
72 showErrorPopup("Try to select bigger bounding box.")
73 hidePopup(popupHideDelayInMilis)
74 }
75 }
76 drawBoundingBoxButtonPressed = false
77}
78function getPolygonSourceData(startCornerLngLat, endCornerLngLat) {
79 return {
80 type: "Feature",
81 geometry: {
82 type: "Polygon",
83 coordinates: [
84 [
85 [startCornerLngLat.lng, startCornerLngLat.lat],
86 [startCornerLngLat.lng, endCornerLngLat.lat],
87 [endCornerLngLat.lng, endCornerLngLat.lat],
88 [endCornerLngLat.lng, startCornerLngLat.lat],
89 [startCornerLngLat.lng, startCornerLngLat.lat],
90 ],
91 ],
92 },
93 }
94}
95
96function getPolygonSource(startCornerLngLat, endCornerLngLat) {
97 return {
98 type: "geojson",
99 data: getPolygonSourceData(startCornerLngLat, endCornerLngLat),
100 }
101}
102
103function updateRectangleData(startCornerLngLat, endCornerLngLat) {
104 map
105 .getSource(sourceID)
106 .setData(getPolygonSourceData(startCornerLngLat, endCornerLngLat))
107}
108
109function bothLngLatAreDifferent(lngLat1, lngLat2) {
110 return lngLat1.lat !== lngLat2.lat && lngLat1.lng !== lngLat2.lng
111}
112
113function getLngLatBoundsForIncidentDetailsCall(
114 startCornerLngLat,
115 endCornerLngLat
116) {
117 var bottomLeftCorner = new tt.LngLat(
118 startCornerLngLat.lng < endCornerLngLat.lng
119 ? startCornerLngLat.lng
120 : endCornerLngLat.lng,
121 startCornerLngLat.lat < endCornerLngLat.lat
122 ? startCornerLngLat.lat
123 : endCornerLngLat.lat
124 )
125 var topRightCorner = new tt.LngLat(
126 startCornerLngLat.lng > endCornerLngLat.lng
127 ? startCornerLngLat.lng
128 : endCornerLngLat.lng,
129 startCornerLngLat.lat > endCornerLngLat.lat
130 ? startCornerLngLat.lat
131 : endCornerLngLat.lat
132 )
133 return tt.LngLatBounds.convert([
134 bottomLeftCorner.toArray(),
135 topRightCorner.toArray(),
136 ])
137}

Inside the onMouseDown event (only when “Draw bounding box” button has been pressed) the GeoJSON source and layers are added to the map.

The developer assigns the first mouse press position to the startCornerLngLat variable. The mousePressed variable is set to true so that during the onMouseMove event the developer can check whether the user is still holding the mouse button. If yes, it updates the bounding box rectangle data using the new coordinate taken from eventDetails.lngLat variable.

When the user releases the mouse button, all necessary data (like the bounding box size) is gathered. One last thing needs to be done. The Incident Details API endpoint describes the bounding box requirements here.

One of the requirements is: “The first coordinate pair is for the lower-left corner and the second pair for the upper-right corner.”. Therefore, the developer needs to adjust the bounding box so that it meets that requirement by calling the getLngLatBoundsForIncidentDetailsCall function.

Now the developer can perform the tt.services.incidentDetails call by calling the displayTrafficIncidents function. See the following code example:

1var incidentListContainer = document.getElementById("incident-list")
2
3function displayTrafficIncidents(boundingBox) {
4 var iconsMapping = [
5 "danger",
6 "accident",
7 "fog",
8 "danger",
9 "rain",
10 "ice",
11 "incident",
12 "laneclosed",
13 "roadclosed",
14 "roadworks",
15 "wind",
16 "flooding",
17 "detour",
18 "",
19 ]
20 var delayMagnitudeMapping = [
21 "unknown",
22 "minor",
23 "moderate",
24 "major",
25 "undefined",
26 ]
27
28 tt.services
29 .incidentDetails({
30 key: apiKey,
31 boundingBox: boundingBox,
32 style: styleS1,
33 zoomLevel: parseInt(map.getZoom()),
34 }).go().then(function (results) {
35 if (results.tm.poi.length === 0) {
36 showErrorPopup("There are no traffic incidents in this area.")
37 hidePopup(popupHideDelayInMilis)
38 } else {
39 results.tm.poi.forEach(function (incident) {
40 var buttonListItem = createButtonItem(incident.p)
41
42 if (isCluster(incident)) {
43 buttonListItem.innerHTML = getButtonClusterContent(
44 incident.id,
45 incident.cs,
46 delayMagnitudeMapping[incident.ty]
47 )
48 incidentListContainer.appendChild(buttonListItem)
49 } else {
50 buttonListItem.innerHTML = getButtonIncidentContent(
51 incident.d.toUpperCase(),
52 iconsMapping[incident.ic],
53 delayMagnitudeMapping[incident.ty],
54 incident.f,
55 incident.t
56 )
57 incidentListContainer.appendChild(buttonListItem)
58 }
59 })
60 }
61 })
62}
63
64function createButtonItem(incidentPosition) {
65 var incidentBtn = document.createElement("button")
66 incidentBtn.setAttribute("type", "button")
67 incidentBtn.classList.add(
68 "list-group-item",
69 "list-group-item-action",
70 "incidendDetailsListItemButton"
71 )
72 incidentBtn.addEventListener(
73 "click",
74 function () {
75 map.flyTo({
76 center: incidentPosition,
77 })
78 },
79 false
80 )
81
82 return incidentBtn
83}
84
85function getButtonIncidentContent(
86 description,
87 iconCategory,
88 delayMagnitude,
89 fromAddress,
90 toAddress
91) {
92 return `<div class="row align-items-center pb-2"> <div class="col-sm-2"> <div class="tt-traffic-icon"> <div class="tt-icon-circle-${delayMagnitude} traffic-icon"> <div class="tt-icon-${iconCategory}"></div></div></div></div><div class="col label pl-0"> ${description} </div></div><div class="row"> <div class="col-sm-2"><label class="label">From: </label></div><div class="col"><label class="incident-details-list-normal-text">${fromAddress}</label> </div></div><div class="row"> <div class="col-sm-2"><label class="label">To: </label></div><div class="col"><label class="incident-details-list-normal-text">${toAddress}</label></div></div>`
93}
94
95function getButtonClusterContent(
96 description,
97 numberOfIncidents,
98 delayMagnitude
99) {
100 return `<div class="row align-items-center pb-2"> <div class="col-sm-2"> <div class="tt-traffic-icon"> <div class="tt-icon-circle-${delayMagnitude} traffic-icon"> <div id="cluster-icon" class="tt-icon-number">${numberOfIncidents}</div></div></div></div><div class="col label pl-0"> ${description} </div></div>`
101}
102
103function isCluster(incident) {
104 return incident.id.includes("CLUSTER")
105}

As the traffic-incidents.css is used inside the tutorial, it’s very convenient to introduce icon mapping variables: iconsMapping and delayMagnitudeMapping so that using the traffic icons will be much easier.

By having the bounding box parameter in place, the developer can perform a call to the tt.services.incidentDetails service.

  • When there are no traffic incidents inside the given bounding box, the error popup message is displayed.
  • When there are traffic incidents, depending on the map zoom, the incident may be a cluster containing multiple incidents or a single incident.

Cluster items are created by calling the getButtonClusterContent method and single incident items are created by calling the getButtonIncidentContent method.

Incidents items are created for each particular result and are added to the incidentListContainer div element. See the following application screen:

main

One of the interesting examples to observe is when the user selects the bounding box which seems to be empty, but there are traffic incidents returned in the list like on the following screen:

main

The Incident Details API endpoint returned a cluster of incidents even if the bounding box seems to be outside of the cluster. This is a typical and correct scenario when one of the single incidents which is inside the cluster belongs to the bounding box. After slightly zooming in the map to force the cluster to expand, the user can see that a few single incidents are inside the bounding box, as shown in the following screen:

main

Another interesting scenario is how the single traffic incident is structured. A traffic incident consists of the POI icon which is a starting point of the incident, and the incident geometry (tube) which might be quite long depending on the incident. As you can see on the following screen, the Incident Details API endpoint returned a single traffic incident although it seems to be outside of the bounding box:

main

After zooming in the map you can spot that even if the beginning of the traffic incident is ** outside** of the bounding box, the rest of the incident geometry is inside of the bounding box so that it is returned by the incident details service. See the following screen:

main

The full source code of this tutorial is available on the TomTom GitHub account: https://github.com/tomtom-international/traffic-tutorial-web-sdk. Feel free to fork it, modify it, and learn from it.

Happy coding!