Integrating Turn-by-Turn Navigation with HERE Routing v8 and the HERE Maps API for JavaScript
Introduction
Integrating turn-by-turn navigation into your web application can significantly enhance the user experience by providing real-time route visualization and guidance. The HERE Maps JavaScript API, combined with the HERE Routing v8 service, allows developers to fetch routes, display them on the map, and dynamically animate movement along that route.
In this tutorial, we will:
- Initialize the HERE Maps JavaScript API on a webpage.
- Use HERE Routing v8 to calculate a driving route between two points.
- Display the route incrementally on the map.
- Animate a car icon along the route to simulate movement.
- Adjust bearing (direction) as the car moves for a more realistic feel.
Note: You need a valid HERE API key to run this example. You should be registered on HERE Platform with a Base Plan.
HTML and Script Includes
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, width=device-width, height=device-height" />
<link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-core.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-service.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-ui.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-harp.js"></script>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#map {
height: 100%;
width: 100%;
}
#carIcon {
position: absolute;
width: 35px;
height: 80px;
display: none;
transition: transform 0.5s linear, left 0.5s linear, top 0.5s linear;
}
</style>
</head>
<body>
<div id="map"></div>
<img id="carIcon" src="Car.png" alt="Car Icon">
Explanation:
- HERE Maps API for JS: We include multiple HERE Maps API for JavaScript files (core, service, UI, events, harp) which provide the base map, routing services, UI controls, map interactivity, and rendering engine.
- CSS and Layout: We ensure the map occupies the full screen. We also define a #carIcon element that will be the vehicle marker. This icon is absolutely positioned and initially hidden.
- Responsive Viewport: The meta tag ensures the map scales well on mobile devices.
Initializing the Map and Platform
<script>
var platform = new H.service.Platform({
'apikey': 'yourAPIKey'
});
const engineType = H.Map.EngineType['HARP'];
var maptypes = platform.createDefaultLayers({ engineType });
var map = new H.Map(
document.getElementById('map'),
maptypes.vector.normal.map,
{
engineType,
zoom: 10,
center: { lat: 52.5159, lng: 13.3777 }
}
);
Explanation:
- Platform Initialization: The H.service.Platform object is created using your HERE API key. This grants access to various HERE services such as Maps and Routing.
- engineType 'HARP': We specify the HARP engine for vector rendering for high-quality 3D map rendering.
- Map Creation: We create a H.Map instance, attaching it to the #map div. We set an initial zoom and center location (Berlin coordinates).
Enabling Map Interactions and UI Controls
var mapEvents = new H.mapevents.MapEvents(map);
var behavior = new H.mapevents.Behavior(mapEvents);
var ui = H.ui.UI.createDefault(map, maptypes);
Explanation:
- MapEvents & Behavior: H.mapevents.MapEvents enables handling of map events like pan, zoom, and tilt. H.mapevents.Behavior activates default map interactions (dragging, pinch zooming).
- UI Controls: H.ui.UI.createDefault adds default UI elements such as zoom buttons and scalebars to the map interface.
Defining Routing Parameters and Calling the Routing Service
var routingParameters = {
'routingMode': 'fast',
'transportMode': 'car',
'origin': '52.54181,13.2391',
'destination': '52.42054,13.49164',
'return': 'polyline'
};
var router = platform.getRoutingService(null, 8);
var routeLine;
var routePoints = [];
var routeStrip = new H.geo.LineString();
var carIcon = document.getElementById('carIcon');
Explanation:
- Routing Parameters: We set the routing mode (fast), transport mode (car), origin/destination coordinates, and the return type (polyline) so we can get a detailed route geometry.
- Routing Service: platform.getRoutingService(null, 8) accesses the HERE Routing v8 API.
- Polyline Management: routePoints will store the route coordinates, routeStrip is a H.geo.LineString that will eventually be turned into a polyline on the map.
- Car Icon Reference: We store the #carIcon DOM element to move it along the route later.
Handling the Routing Response
function onResult(result) {
if (result.routes.length) {
routePoints = [];
result.routes[0].sections.forEach((section) => {
const linestring = H.geo.LineString.fromFlexiblePolyline(section.polyline);
linestring.eachLatLngAlt((lat, lng, alt) => {
routePoints.push({ lat: lat, lng: lng });
});
});
map.getViewModel().setLookAtData({
bounds: new H.geo.Rect(
routePoints[0].lat, routePoints[0].lng,
routePoints[routePoints.length - 1].lat, routePoints[routePoints.length - 1].lng
)
});
drawRouteGradually();
}
}
Explanation:
- Processing the Route: The routing response contains sections, each with a polyline. We convert the polyline to a list of latitude/longitude points.
- Storing Route Points: We push these coordinates into routePoints.
- Fit Map Bounds: We adjust the map viewport to show the entire route.
- Draw Route Gradually: Instead of showing the entire route at once, we call drawRouteGradually() to animate the polyline drawing and car movement.
Gradually Drawing the Route
function drawRouteGradually() {
let currentIndex = 0;
function drawNextSegment() {
if (currentIndex < routePoints.length) {
const point = routePoints[currentIndex];
routeStrip.pushLatLngAlt(point.lat, point.lng, 0);
if (routeLine) {
map.removeObject(routeLine);
}
if (routeStrip.getPointCount() > 1) {
routeLine = new H.map.Polyline(routeStrip, {
style: { strokeColor: 'blue', lineWidth: 3 }
});
map.addObject(routeLine);
}
moveCar(point, currentIndex);
currentIndex++;
setTimeout(drawNextSegment, 100);
}
}
drawNextSegment();
}
Explanation:
Incremental Drawing: We iterate through each point in routePoints. For each point:
- Add it to the routeStrip.
- Remove the old routeLine and create a new polyline with the updated routeStrip.
- Move the car icon to the current point via moveCar().
Timing: setTimeout(drawNextSegment, 100) sets a short delay between adding points, creating a smooth "drawing" animation.
Animating the Car Icon
function moveCar(point, currentIndex) {
carIcon.style.display = 'block';
const screenCoords = map.geoToScreen(point);
carIcon.style.left = `${screenCoords.x - carIcon.width / 2}px`;
carIcon.style.top = `${screenCoords.y - carIcon.height / 2}px`;
if (routePoints.length > 1) {
const nextPoint = routePoints[currentIndex + 1] || point;
const bearing = getBearing(point, nextPoint);
carIcon.style.transform = `rotate(${bearing}deg)`;
}
}
Explanation:
- Positioning the Car: We convert the geo-coordinates of the current route point to screen coordinates using map.geoToScreen().
- Centering the Icon: We offset by half the icon’s width and height to position it correctly.
- Adjusting Bearing: We calculate the direction the car should face. By comparing the current point to the next point, we compute a bearing angle and rotate the car icon accordingly, giving a realistic navigation feel.
Bearing Calculation
function getBearing(start, end) {
const startLat = start.lat * Math.PI / 180;
const startLng = start.lng * Math.PI / 180;
const endLat = end.lat * Math.PI / 180;
const endLng = end.lng * Math.PI / 180;
const y = Math.sin(endLng - startLng) * Math.cos(endLat);
const x = Math.cos(startLat)*Math.sin(endLat) - Math.sin(startLat)*Math.cos(endLat)*Math.cos(endLng - startLng);
const brng = Math.atan2(y, x) * 180 / Math.PI;
return (brng + 360) % 360;
}
Explanation:
- Calculate Bearing: Using the haversine formula and trigonometry, we determine the direction (bearing) in degrees from the start point to the next route point. This angle is used to rotate the car icon appropriately.
Handling Errors and Final Setup
router.calculateRoute(routingParameters, onResult, function(error) {
alert(error.message);
});
map.addEventListener('pointermove', function () {
behavior.enable();
});
Explanation:
- Routing Call: We call router.calculateRoute() with our parameters and handle success via onResult() or failure via an alert.
- Interactions: We add an event listener on pointermove to ensure map interactions stay enabled.
Full working snippet:
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, width=device-width, height=device-height" />
<link rel="stylesheet" type="text/css" href="https://js.api.here.com/v3/3.1/mapsjs-ui.css" />
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-core.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-service.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-ui.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-mapevents.js"></script>
<script type="text/javascript" src="https://js.api.here.com/v3/3.1/mapsjs-harp.js"></script>
<style>
html,
body {
height: 100%;
margin: 0;
padding: 0;
}
#map {
height: 100%;
width: 100%;
}
#carIcon {
position: absolute;
width: 35px;
height: 80px;
display: none;
transition: transform 0.5s linear, left 0.5s linear, top 0.5s linear;
}
</style>
</head>
<body>
<div id="map"></div>
<img id="carIcon" src="Car.png" alt="Car Icon">
<script>
var platform = new H.service.Platform({
'apikey': 'yourApiKey'
});
const engineType = H.Map.EngineType['HARP'];
var maptypes = platform.createDefaultLayers({ engineType });
var map = new H.Map(
document.getElementById('map'),
maptypes.vector.normal.map,
{
engineType,
zoom: 10,
center: { lat: 52.5159, lng: 13.3777 }
});
var mapEvents = new H.mapevents.MapEvents(map);
var behavior = new H.mapevents.Behavior(mapEvents);
var ui = H.ui.UI.createDefault(map, maptypes);
var routingParameters = {
'routingMode': 'fast',
'transportMode': 'car',
'origin': '52.54181,13.2391',
'destination': '52.42054,13.49164',
'return': 'polyline'
};
var router = platform.getRoutingService(null, 8);
var routeLine;
var routePoints = [];
var routeStrip = new H.geo.LineString();
var carIcon = document.getElementById('carIcon');
function onResult(result) {
if (result.routes.length) {
routePoints = [];
result.routes[0].sections.forEach((section) => {
const linestring = H.geo.LineString.fromFlexiblePolyline(section.polyline);
linestring.eachLatLngAlt((lat, lng, alt) => {
routePoints.push({ lat: lat, lng: lng });
});
});
map.getViewModel().setLookAtData({
bounds: new H.geo.Rect(routePoints[0].lat, routePoints[0].lng, routePoints[routePoints.length - 1].lat, routePoints[routePoints.length - 1].lng)
});
drawRouteGradually();
}
}
function drawRouteGradually() {
let currentIndex = 0;
function drawNextSegment() {
if (currentIndex < routePoints.length) {
const point = routePoints[currentIndex];
routeStrip.pushLatLngAlt(point.lat, point.lng, 0); // Ensure we add a valid point to the routeStrip
if (routeLine) {
map.removeObject(routeLine);
}
// Validate if routeStrip has valid points before creating polyline
if (routeStrip.getPointCount() > 1) {
routeLine = new H.map.Polyline(routeStrip, {
style: { strokeColor: 'blue', lineWidth: 3 }
});
map.addObject(routeLine);
}
moveCar(point, currentIndex);
currentIndex++;
setTimeout(drawNextSegment, 100); // Adjust the interval time for smoother drawing
}
}
drawNextSegment();
}
function moveCar(point, currentIndex) {
carIcon.style.display = 'block';
const screenCoords = map.geoToScreen(point);
carIcon.style.left = `${screenCoords.x - carIcon.width / 2}px`;
carIcon.style.top = `${screenCoords.y - carIcon.height / 2}px`;
if (routePoints.length > 1) {
const nextPoint = routePoints[currentIndex + 1] || point;
const bearing = getBearing(point, nextPoint);
carIcon.style.transform = `rotate(${bearing}deg)`;
}
}
function getBearing(start, end) {
const startLat = start.lat * Math.PI / 180;
const startLng = start.lng * Math.PI / 180;
const endLat = end.lat * Math.PI / 180;
const endLng = end.lng * Math.PI / 180;
const y = Math.sin(endLng - startLng) * Math.cos(endLat);
const x = Math.cos(startLat) * Math.sin(endLat) - Math.sin(startLat) * Math.cos(endLat) * Math.cos(endLng - startLng);
const brng = Math.atan2(y, x) * 180 / Math.PI;
return (brng + 360) % 360;
}
router.calculateRoute(routingParameters, onResult, function (error) {
alert(error.message);
});
// Ensuring map is not blocked for interactions
map.addEventListener('pointermove', function () {
behavior.enable();
});
</script>
</body>
</html>
Conclusion
With these steps:
- Map Initialization: We set up the HERE Maps API for JavaScript with vector tiles and UI controls.
- Route Calculation: We used HERE Routing v8 to get a car route between two points.
- Smooth Animation: By incrementally adding segments to a polyline, we create a dynamic effect of "drawing" the route over time.
- Car Icon Movement and Orientation: We placed an icon on the route and updated its position and bearing at each step, simulating turn-by-turn navigation visually.
This approach provides a foundation for integrating a more comprehensive navigation system, including turn instructions, traffic conditions, and other real-time elements.
Have your say
Sign up for our newsletter
Why sign up:
- Latest offers and discounts
- Tailored content delivered weekly
- Exclusive events
- One click to unsubscribe