Journey Speed Limit Review with the TomTom Snap to Roads API
TomTom’s Snap to Roads API works by taking two coordinates to create the optimal route, factoring in speed limit, types of roads, vehicle type, and elevation. It can also take a GPS trace and recreate a driver’s journey. The goal of this API is to understand the driver’s performance on the journey.
TomTom’s Snap to Roads API is a REST API that creates routes on a map based on GPS coordinates. It takes a GPS trace with coordinates and timestamps and recreate a driver’s journey. The goal of this API is to enable you to understand the driver’s performance on the journey. Some benefits you can derive from it include speed limit violations, route selection ability, and the ability to drive at optimal speed.
In this article, we’ll start by creating an optimal route for a journey. We’ll then recreate a driver’s route taken to make the same journey. Last, we’ll compare the routes to see if there were any speed violations.
Speed Comparison
To follow this step-by-step guide, ensure you have the following:
A working Node.js installation. If you don’t have one, download the latest version.
A TomTom account and a TomTom API key. You can follow these steps to get the key.
Getting the Data
In this demonstration, we use 100 data points (longitude and latitude, timestamps, and speed) generated by a GPS tracker on a vehicle in New York City.
Below is a snapshot of our data:
Creating a React Application
Let’s start by creating a simple React app using the Create React App (CRA) library. To do this, open a terminal and run this command:
$ npx create-react-app snap-to-road
Navigate to the project folder:
$ cd snap-to-road
Start the project using this command:
$ npm start
Install the tomtom package in your project.
$ npm install @tomtom-international/web-sdk-maps -save
Install Axios to handle the API request.
$ npm I axios
Open the app.js file in the src folder and replace the code with the code below. Ensure that you replace {{API_KEY}} with a valid TomTom API key.
import './App.css'
import "@tomtom-international/web-sdk-maps/dist/maps.css";
import * as ttmaps from "@tomtom-international/web-sdk-maps";
import { useState, useEffect, useRef } from "react";
import axios from "axios"
export default function App() {
const mapElement = useRef();
const [mapZoom, setMapZoom] = useState(14);
const [map, setMap] = useState({});
useEffect(() => {
let map = ttmaps.map({
key: "<ADD YOUR API KEY HERE>",
container: mapElement.current,
zoom: mapZoom
});
setMap(map);
return () => map.remove();
}, []);
const getSnapFunction = () => {
axios.get("https://api.tomtom.com/snap-to-roads/1/snap-to-roads?points=-73.9856644,40.7484405;-74.00029,40.76254&fields={projectedPoints{type,geometry{type,coordinates},properties{routeIndex}},route{type,geometry{type,coordinates},properties{id,speedRestrictions{maximumSpeed{value,unit}}}}}&key={{API _KEY}}").then((res) =>
{
console.log(res.data)
res.data.route.forEach(
(item) => {
map.addLayer({
id: Math.random().toString(),
type: "line",
source: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "LineString",
properties: {},
coordinates: item.geometry.coordinates
}
}
]
}
},
layout: {
"line-cap": "round",
"line-join": "round"
},
paint: {
"line-color": "#ff0000",
"line-width": 4
}
});
}
)
map.setCenter([parseFloat(-73.9856644), parseFloat(40.7484405)]);
}).catch((err) => console.log(err))
}
getSnapFunction();
return (
<div className="App">
<div ref={mapElement} className="mapDiv"></div>
</div>
);
}
Open the App.css file and replace the content in that file with the code below:
.mapDiv {
height: 100vh;
}
This imports the TomTom Library and displays the map using useRef. Also, note that we have set the zoom level to 14.
Each layer that we add must have a unique ID. In the code above, the layer added by the function map.addLayer() is assigned an id generated by Math.random().toString().
Our map should look like this:
Recreating a Route
Let’s now recreate the path a truck driver uses from the same origin to the same destination. We’ll need the GPS trace containing the coordinates and the timestamps to do that. The data must be in a format that the TomTom API can consume.
Below are the formatted 100 GPS coordinates:
-73.9856644,40.7484405;-73.9859004,40.7481246;-73.98611,40.74821;-73.98806,40.74905;-73.98823,40.74912;-73.98826,40.74914;-73.98842,40.7492;-73.98924,40.74954;-73.98945,40.74963;-73.9895,40.74965;-73.98966,40.74972;-73.98986,40.7498;-73.99027,40.74998;-73.99032,40.74999;-73.99048,40.75006;-73.99068,40.75015;-73.99069,40.75015;-73.9911,40.75033;-73.9911014,40.7503293;-73.99121,40.75018;-73.99122,40.75017;-73.9913,40.75006;-73.99141,40.74991;-73.99156,40.74971;-73.99169,40.74953;-73.9918,40.74938;-73.99201,40.7491;-73.99211,40.74896;-73.99218,40.74887;-73.99224,40.74878;-73.99238,40.74859;-73.99247,40.74848;-73.99292,40.74786;-73.9929174,40.7478614;-73.99331,40.74803;-73.99391,40.74828;-73.99499,40.74874;-73.99555,40.74897;-73.99575,40.74905;-73.99601,40.74916;-73.99649,40.74936;-73.99718,40.74965;-73.99846,40.75019;-73.99863,40.75026;-73.99918,40.75049;-73.99982,40.75076;-74.00025,40.75095;-74.00047,40.75104;-74.00112,40.75132;-74.00124,40.75137;-74.00144,40.75146;-74.00185,40.75163;-74.00206,40.75172;-74.00227,40.75181;-74.00247,40.7519;-74.00268,40.75199;-74.00331,40.75225;-74.00385,40.75248;-74.00408,40.75257;-74.00427,40.75265;-74.00595,40.75335;-74.00649,40.75358;-74.00686,40.75373;-74.00691,40.75375;-74.00697,40.75377;-74.00704,40.75379;-74.0070399,40.753792;-74.007,40.75387;-74.00696,40.75395;-74.00691,40.75402;-74.00687,40.75409;-74.00682,40.75416;-74.00677,40.75424;-74.0067,40.75435;-74.00662,40.75446;-74.00655,40.75456;-74.00527,40.75631;-74.00508,40.75659;-74.00497,40.75675;-74.00495,40.75678;-74.0048,40.75697;-74.00467,40.75717;-74.00382,40.75833;-74.00372,40.75847;-74.00362,40.75862;-74.00315,40.75926;-74.00293,40.75956;-74.00262,40.76;-74.00254,40.76011;-74.00248,40.76019;-74.00236,40.76035;-74.00224,40.7605;-74.00211,40.76068;-74.00207,40.76073;-74.00186,40.76101;-74.00174,40.76117;-74.00163,40.76133;-74.00151,40.76151;-74.00147,40.76156;-74.00143,40.76161
As the Snap-to-Roads feature only requires the first timestamp, we’ll include that and let the API handle the rest of the route. As such, we’ll include the following string for our timestamps
2022-03-04T0:00:00Z;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
If you have timestamps for each coordinate you are using, feel free to include those for more accurate results.
Replace the code in App.js with the code below. Also, replace the {{GPS_COORDINATES}}, {{TIMESTAMPS}} and the {{API_KEY}} fields on the API request link. The fields parameter in the URL specifies what we want to be included in the response body. Below is a snapshot of how it appears in the response body. You can also learn more from the documentation.
import './App.css'
import "@tomtom-international/web-sdk-maps/dist/maps.css";
import * as ttmaps from "@tomtom-international/web-sdk-maps";
import { useState, useEffect, useRef } from "react";
import axios from "axios"
export default function App() {
const mapElement = useRef();
const [mapZoom, setMapZoom] = useState(14);
const [map, setMap] = useState({});
useEffect(() => {
let map = ttmaps.map({
key: "LpOAGDnbLFs8TAAre5yzdE44mGYjlL1W",
container: mapElement.current,
center: [-74.814649, 40.646887],
zoom: mapZoom
});
setMap(map);
return () => map.remove();
}, []);
const getSnapFunction = () => {
//The violations array will be used later on.
let violations = [];
axios.get("https://api.tomtom.com/snap-to-roads/1/snap-to-roads?points={{GPS_COORDINATES}}×tamps={{TIMESTAMPS}}&vehicleType=Truck&fields=%7BprojectedPoints%7Btype%2Cgeometry%7Btype%2Ccoordinates%7D%2Cproperties%7BrouteIndex%2CsnapResult%7D%7D%2Croute%7Btype%2Cgeometry%7Btype%2Ccoordinates%7D%2Cproperties%7Bid%2ClinearReference%2CspeedLimits%7Bvalue%2Cunit%2Ctype%7D%2Caddress%7BroadName%2CroadNumbers%7D%2Cfrc%2CformOfWay%2CroadUse%2ClaneInfo%7BnumberOfLanes%7D%2CheightInfo%7Bheight%2Cchainage%7D%2CtrafficSigns%7BsignType%2Cchainage%7D%2CtrafficLight%7D%7D%2Cdistances%7Btotal%2Croad%2CoffRoad%7D%7D&key= {{API_KEY}}").then((res) =>
{
console.log(res.data)
res.data.route.forEach(
(item) => {
map.addLayer({
id: Math.random().toString(),
type: "line",
source: {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: {
type: "LineString",
properties: {},
coordinates: item.geometry.coordinates
}
}
]
}
},
layout: {
"line-cap": "round",
"line-join": "round"
},
paint: {
"line-color": "#ff0000",
"line-width": 4
}
});
}
)
map.setCenter([parseFloat(-73.9856644), parseFloat(40.7484405)]);
}).catch((err) => console.log(err))
}
getSnapFunction();
return (
<div className="App">
<div ref={mapElement} className="mapDiv"></div>
</div>
);
}
Our map should now appear as shown below:
Checking for Speed Limit Violations
To check for speeding violations, we will need to have the recorded speed to go along with our coordinates. We have simulated that data and will use the following speeds to match with the timestamps above.
Speed (MPH): 25,27,26,21,15,24,22,24,15,24,24,22,25,28,23,22,16,20,19,19,15,18,27,23,25,24,24,27,17,18,24,28,24,17,26,19,27,19,28,17,24,19,18,28,19,15,15,26,17,21,18,16,16,18,15,25,15,22,26,18,26,28,25,22,22,17,17,22,16,25,17,21,21,23,28,27,22,19,24,18,26,25,28,18,25,15,25,25,15,25,15,15,23,22,27,17,15,26,20,26
These speeds will need to be added to your App.js, at the top of your getSnapFunction() as follows:
let speeds = [25,27,26,21,15,24,22,24,15,24,24,22,25,28,23,22,16,20,19,19,15,18,27,23,25,24,24,27,17,18,24,28,24,17,26,19,27,19,28,17,24,19,18,28,19,15,15,26,17,21,18,16,16,18,15,25,15,22,26,18,26,28,25,22,22,17,17,22,16,25,17,21,21,23,28,27,22,19,24,18,26,25,28,18,25,15,25,25,15,25,15,15,23,22,27,17,15,26,20,26];
We then modify our App.js to check the posted speed limits for each road segment against the coordinates that fall within those segments. We will record and display these violations.
To start, modify the top of your App.js file to include the following:
import './App.css'
import "@tomtom-international/web-sdk-maps/dist/maps.css";
import * as ttmaps from "@tomtom-international/web-sdk-maps";
import { useState, useEffect, useRef } from "react";
import axios from "axios"
export default function App() {
const mapElement = useRef();
const [mapZoom, setMapZoom] = useState(14);
const [map, setMap] = useState({});
const [violations, setViolations] = useState([]);
...
This will hold your discovered violations. Then, at the end of your res.data.route.forEach() loop, add the following code:
let speedLimit = item.properties.speedLimits.value;
let coordArray = points.split(";");
for(let i=0;i<coordArray.length;i++){
let coord = coordArray[i];
let lat = Math.abs(coord.split(",")[0]);
let lon = Math.abs(coord.split(",")[1]);
if(lat > Math.abs(item.geometry.coordinates[0][0]) && lat < Math.abs(item.geometry.coordinates[1][0]) && lon > Math.abs(item.geometry.coordinates[0][1]) && lon < Math.abs(item.geometry.coordinates[1][0])){
//use this limit and check speed
let speed = speeds[i];
if(speed > speedLimit){
//violation has occured, record it
let violation = {};
violation.location = coord;
violation.speed = speed;
violation.speedLimit = speedLimit;
violation.street = item.properties.address.roadName;
violations.push(violation);
}
}
setViolations(violations);
This code checks which points are on the line segments, then compares the speeds at those points to the returned speed limit. If a speed is over the limit, the violation is recorded.
To display the violations, we’ll create a simple list to display below the map. Paste the following function above the getSnapFunction() in App.js:
function RenderViolations(){
const listItems = violations.map(
(element) => {
return(
<ul>
<li>Lat/Lon: {element.location}</li>
<li>Street: {element.street}</li>
<li>Travelling Speed (MPH): {element.speed}</li>
<li>Speed Limit (MPH): {element.speedLimit}</li>
</ul>
)
}
)
return (
<div>
<h2> Speeding Violations Recorded</h2>
{listItems}
</div>
)
}
Running this code displays the violations as such below the map:
As you can see, speed limit violations are clearly shown.
Conclusion
We can use the Snap to Roads API to create an optimal route or to recreate a route taken by a driver. When used to recreate a route, the API also returns the speed limit of each section along the route taken. This makes it easy to capture any speed violations along the journey.
Learn more about TomTom and try Snap-to-Roads for yourself.