Integrating Real-Time Traffic Updates with the HERE SDK Navigate
Introduction
As navigation applications evolve, users increasingly expect real-time, context-sensitive updates that enhance their journey. One such critical enhancement is integrating dynamic traffic conditions directly into navigation experiences. By leveraging HERE SDK’s calculateTrafficOnRoute() method combined with visualNavigator.setTrafficOnRoute(), developers can deliver up-to-date traffic flow information, thus refining ETAs and guidance instructions as conditions change—without the need to recalculate or re-set the route from scratch.
In this guide, we’ll explore how to integrate the HERE SDK traffic update functionalities. You’ll learn how to:
- Use calculateTrafficOnRoute() to periodically refresh traffic data along a currently navigated route.
- Integrate real-time traffic conditions seamlessly into ongoing navigation via visualNavigator.setTrafficOnRoute() without altering the route geometry.
- Dynamically update visual elements (traffic-induced color changes on the route polyline) and receive updated ETAs and instructions automatically.
This approach differs from older strategies that recalculated the route from scratch or manually re-drew traffic polylines on every update—leading to confusion, performance overhead, and potential user experience degradation. With the improved method described here, you maintain a consistent route geometry while continuously refreshing traffic data, ensuring smooth, lag-free navigation experiences.
Overview Of Key Steps
- Initialize and set up the HERE SDK for navigation and rendering.
- Calculate an initial route and start simulated navigation.
- Periodically invoke calculateTrafficOnRoute() to fetch updated traffic conditions for the current route.
- Integrate the updated traffic data into VisualNavigator using visualNavigator.setTrafficOnRoute(trafficOnRoute)—updating ETAs and instructions internally.
- Update map polylines to reflect changing traffic conditions smoothly, without re-setting the route or recalculating geometry.
Why This Improved Approach?
Traditional Approach:
- Developers often recalculated the entire route using calculateRoute() during navigation when traffic changed.
- This caused sudden map shifts, frequent route resets, or confusing changes in route geometry.
- ETAs and instructions might not sync seamlessly with the new route, causing user confusion.
Improved Approach Using calculateTrafficOnRoute():
- The route geometry remains fixed—VisualNavigator continues along the same route.
- New traffic conditions are injected into the existing route via visualNavigator.setTrafficOnRoute().
- ETAs, instructions, and route progress are updated internally, ensuring smooth transitions and accurate guidance.
- Performance improves as there’s no need for full route recalculation or complex polyline re-drawing logic. Only traffic colors and ETAs adapt.
Understanding TrafficOnRoute
The TrafficOnRoute class is central to this solution. When you call calculateTrafficOnRoute(), it returns a TrafficOnRoute object that encapsulates all current traffic conditions along the already navigated route. Key benefits include:
- No Geometry Changes: TrafficOnRoute provides updated traffic data for the existing route, ensuring the original route shape and waypoints remain intact.
- Seamless ETA & Instruction Updates: By passing trafficOnRoute to visualNavigator.setTrafficOnRoute(trafficOnRoute), the VisualNavigator internally recalculates ETAs, instructions, and maneuvers to reflect the new conditions—without requiring you to reset or recalculate the route.
- Optimized Performance & User Experience: Since only traffic data is updated, rather than recalculating the route’s geometry, the transition to new traffic conditions is smooth and unobtrusive. Users experience minimal lag and no confusing reroutes, while still benefiting from the most current traffic information.
In essence, TrafficOnRoute acts like a real-time data layer superimposed on your static route geometry, continuously adapting guidance and visuals to current road conditions.
Steps for Integration
1. Setting Up HERE SDK
As a prerequisite, ensure you have initialized the HERE SDK similarly to other examples. Refer to the official HERE SDK documentation for initialization details.
2. Calculating the Initial Route
First, calculate the route from a start to a destination waypoint using:
routingEngine.calculateRoute().
Waypoint startWaypoint = new Waypoint(routeStartGeoCoordinates);
Waypoint destinationWaypoint = new Waypoint(new GeoCoordinates(52.46093, 13.32293));
routingEngine.calculateRoute(
Arrays.asList(startWaypoint, destinationWaypoint),
carOptions,
(routingError, routes) -> {
if (routingError == null && routes != null && !routes.isEmpty()) {
mCurrentRoute = routes.get(0);
// Draw initial traffic lines from mCurrentRoute's sections:
new DrawTrafficLines().execute(new ArrayList<>(mCurrentRoute.getSections()));
animateToRouteStart(mCurrentRoute);
} else {
Log.e("Route calculation error", routingError.toString());
}
}
);
Once the initial route is drawn and guidance starts, the VisualNavigator handles ETAs and instructions based on current conditions.
3. Starting Navigation and Simulation
Use visualNavigator.setRoute() and start a LocationSimulator to simulate movement along the route:
visualNavigator.setRoute(mCurrentRoute);
visualNavigator.startRendering(mapView);
startRouteSimulation(mCurrentRoute);
As navigation proceeds, VisualNavigator provides ETAs and instructions for the existing route.
4. Periodically Updating Traffic Conditions
Instead of recalculating a new route, call calculateTrafficOnRoute() at regular intervals. This fetches updated traffic conditions for the current route handle—no geometry changes:
private void requestRealtimeTrafficOnRoute() {
Log.d(TAG, "Requesting real-time traffic update.");
if (mCurrentRoute == null) return;
routingEngine.calculateTrafficOnRoute(
mCurrentRoute,
0, 0, // Indicate last traveled section and distance if available
(routingError, trafficOnRoute) -> {
if (routingError == null && trafficOnRoute != null) {
runOnUiThread(() -> {
clearMapTrafficPolyline();
// Update VisualNavigator with the new traffic data
visualNavigator.setTrafficOnRoute(trafficOnRoute);
// Redraw traffic lines for the updated conditions
new DrawTrafficLinesFromTrafficOnRoute(lastKnownLocation != null ? lastKnownLocation.coordinates : mCurrentRoute.getGeometry().vertices.get(0))
.execute(trafficOnRoute);
});
} else {
Log.e(TAG, "Error updating traffic on route:" + routingError);
}
}
);
}
Key Point:
We call visualNavigator.setTrafficOnRoute(trafficOnRoute) here. This ensures that ETAs and instructions now reflect the latest traffic conditions—without requiring a full route recalculation.
5. Reflecting Traffic Changes on the Map
The DrawTrafficLines and DrawTrafficLinesFromTrafficOnRoute tasks render polylines on the map. Initially, DrawTrafficLines uses the spans from the current route's sections. After traffic updates, DrawTrafficLinesFromTrafficOnRoute re-colors polylines according to the new jam factors, maintaining a smooth and visually appealing update process:
// Example from DrawTrafficLinesFromTrafficOnRoute
mapView.getMapScene().addMapPolylines(trafficPolylines);
Log.d(TAG, "Traffic polylines updated from TrafficOnRoute: " + trafficPolylines.size());
No route re-setting is needed, no forced camera changes, and no confusing geometry shifts. The route remains consistent, and the user sees updated traffic colors and ETAs seamlessly.
Completed Classes
- MainActivityNew.java: Manages route calculation, guidance start, periodic traffic updates, and visualization.
Here is the full class snippet:
//Using calculateTrafficOnRoute
/**
* date:2024/12/4
* time:15:42
* author:sachin jonda
* desc:
*/
package com.here.navigationcustom;
import static com.here.navigationcustom.TrafficConditionUtils.GUIDANCE_CONDITION_CLOSED_COLOR;
import static com.here.navigationcustom.TrafficConditionUtils.GUIDANCE_CONDITION_OBSTRUCTION_COLOR;
import static com.here.navigationcustom.TrafficConditionUtils.GUIDANCE_CONDITION_SLOW_COLOR;
import static com.here.navigationcustom.TrafficConditionUtils.GUIDANCE_CONDITION_STRAIGHTWAY_COLOR;
import static com.here.sdk.mapview.LocationIndicator.IndicatorStyle.PEDESTRIAN;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.here.navigationcustom.PermissionsRequestor.ResultListener;
import com.here.sdk.animation.AnimationListener;
import com.here.sdk.animation.AnimationState;
import com.here.sdk.core.Color;
import com.here.sdk.core.GeoCoordinates;
import com.here.sdk.core.GeoCoordinatesUpdate;
import com.here.sdk.core.GeoOrientationUpdate;
import com.here.sdk.core.GeoPolyline;
import com.here.sdk.core.Location;
import com.here.sdk.core.LocationListener;
import com.here.sdk.core.engine.SDKNativeEngine;
import com.here.sdk.core.engine.SDKOptions;
import com.here.sdk.core.errors.InstantiationErrorException;
import com.here.sdk.mapview.LineCap;
import com.here.sdk.mapview.LocationIndicator;
import com.here.sdk.mapview.MapCameraAnimation;
import com.here.sdk.mapview.MapCameraAnimationFactory;
import com.here.sdk.mapview.MapContext;
import com.here.sdk.mapview.MapError;
import com.here.sdk.mapview.MapFeatureModes;
import com.here.sdk.mapview.MapFeatures;
import com.here.sdk.mapview.MapMeasure;
import com.here.sdk.mapview.MapMeasureDependentRenderSize;
import com.here.sdk.mapview.MapPolyline;
import com.here.sdk.mapview.MapScene;
import com.here.sdk.mapview.MapScheme;
import com.here.sdk.mapview.MapView;
import com.here.sdk.mapview.RenderSize;
import com.here.sdk.navigation.DestinationReachedListener;
import com.here.sdk.navigation.DynamicCameraBehavior;
import com.here.sdk.navigation.InterpolatedLocationListener;
import com.here.sdk.navigation.LocationSimulator;
import com.here.sdk.navigation.LocationSimulatorOptions;
import com.here.sdk.navigation.MapMatchedLocation;
import com.here.sdk.navigation.NavigableLocation;
import com.here.sdk.navigation.NavigableLocationListener;
import com.here.sdk.navigation.RouteProgress;
import com.here.sdk.navigation.RouteProgressColors;
import com.here.sdk.navigation.RouteProgressListener;
import com.here.sdk.navigation.SectionProgress;
import com.here.sdk.navigation.VisualNavigator;
import com.here.sdk.navigation.VisualNavigatorColors;
import com.here.sdk.routing.CalculateRouteCallback;
import com.here.sdk.routing.CarOptions;
import com.here.sdk.routing.Route;
import com.here.sdk.routing.RouteOptions;
import com.here.sdk.routing.RoutingError;
import com.here.sdk.routing.RoutingEngine;
import com.here.sdk.routing.Section;
import com.here.sdk.routing.SectionTransportMode;
import com.here.sdk.routing.TrafficOnRoute;
import com.here.sdk.routing.TrafficOnSection;
import com.here.sdk.routing.TrafficOnSpan;
import com.here.sdk.routing.Waypoint;
import com.here.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MainActivityNew extends AppCompatActivity {
private static final String TAG = MainActivityNew.class.getSimpleName();
private static final double DISTANCE_IN_METERS = 5000;
private PermissionsRequestor permissionsRequestor;
private MapView mapView;
private RoutingEngine routingEngine;
private VisualNavigator visualNavigator;
private LocationSimulator locationSimulator;
private LocationIndicator mTriangleIndicator;
private Location lastKnownLocation = null;
private GeoCoordinates routeStartGeoCoordinates;
private TextView msgTextView;
private boolean isNavigating = false;
private Route mCurrentRoute;
// Refresh interval for traffic updates: 2 seconds (as per your snippet)
private static final int REFRESH_ROUTE_PERIOD_IN_Millis = 1000 * 2;
private Runnable trafficUpdateRunnable;
private boolean isTrafficEnabled = true;
private List<MapPolyline> trafficPolylines = new ArrayList<>();
private List<MapPolyline> mapTravelledPolylines = new ArrayList<>();
private Handler trafficOnRouteUpdateHandler;
private List<Route> mapRoutes = new ArrayList<>();
private List<MapPolyline> mapPolylines = new ArrayList<>();
private int selectedRouteIndex = 0; // Index of the selected route
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initializeHERESDK();
setContentView(R.layout.activity_main);
mapView = findViewById(R.id.map_view);
mapView.onCreate(savedInstanceState);
MapAdapter.getInstance().setMapView(mapView);
msgTextView = findViewById(R.id.label_msg);
handleAndroidPermissions();
initTriangleIndicator();
}
private void initializeHERESDK() {
String accessKeyID = "youraccessKeyID ";
String accessKeySecret = "your accessKeySecret ";
SDKOptions options = new SDKOptions(accessKeyID, accessKeySecret);
try {
SDKNativeEngine.makeSharedInstance(this, options);
} catch (InstantiationErrorException e) {
throw new RuntimeException("HERE SDK init failed: " + e.error.name());
}
}
private void initTriangleIndicator() {
if (mTriangleIndicator == null) {
mTriangleIndicator = new LocationIndicator();
}
mTriangleIndicator.setAccuracyVisualized(false);
mTriangleIndicator.setLocationIndicatorStyle(PEDESTRIAN);
}
private void resetLocationIndicator(boolean isNavigating) {
if (isNavigating) {
mTriangleIndicator.disable();
} else {
mTriangleIndicator.setLocationIndicatorStyle(PEDESTRIAN);
mTriangleIndicator.enable(mapView);
}
mTriangleIndicator.updateLocation(getLastKnownLocationLocation());
}
private void handleAndroidPermissions() {
permissionsRequestor = new PermissionsRequestor(this);
permissionsRequestor.request(new PermissionsRequestor.ResultListener() {
@Override
public void permissionsGranted() {
loadMapScene();
}
@Override
public void permissionsDenied() {
Log.e(TAG, "Permissions denied.");
}
});
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
permissionsRequestor.onRequestPermissionsResult(requestCode, grantResults);
}
private void loadMapScene() {
routeStartGeoCoordinates = new GeoCoordinates(52.51746, 13.41629);
mapView.getMapScene().loadScene(MapScheme.NORMAL_DAY, mapError -> {
if (mapError == null) {
Map<String, String> mapFeatures = new HashMap<>();
mapFeatures.put(MapFeatures.LANDMARKS, MapFeatureModes.LANDMARKS_TEXTURED);
mapView.getMapScene().enableFeatures(mapFeatures);
mapView.getCamera().lookAt(routeStartGeoCoordinates, new MapMeasure(MapMeasure.Kind.DISTANCE, DISTANCE_IN_METERS));
startAppLogic();
} else {
Log.d(TAG, "Loading map failed: " + mapError.name());
}
});
}
public void startAppLogic() {
try { routingEngine = new RoutingEngine(); }
catch (InstantiationErrorException e) { throw new RuntimeException(e); }
try { visualNavigator = new VisualNavigator(); }
catch (InstantiationErrorException e) { throw new RuntimeException(e); }
Map<String, String> mapFeatures = new HashMap<>();
mapFeatures.put(MapFeatures.TRAFFIC_FLOW, MapFeatureModes.TRAFFIC_FLOW_WITH_FREE_FLOW);
mapFeatures.put(MapFeatures.TRAFFIC_INCIDENTS, MapFeatureModes.DEFAULT);
mapView.getMapScene().enableFeatures(mapFeatures);
resetLocationIndicator(false);
}
public void startButtonClicked(View view) {
visualNavigator.stopRendering();
if (visualNavigator.isRendering()) {
return;
}
Waypoint startWaypoint = new Waypoint(routeStartGeoCoordinates);
RouteOptions routeOptions = new RouteOptions();
routeOptions.enableRouteHandle = true; // Ensure route handle for TrafficOnRoute
CarOptions carOptions = new CarOptions();
carOptions.routeOptions = routeOptions;
Waypoint destinationWaypoint = new Waypoint(new GeoCoordinates(52.46093, 13.32293));
routingEngine.calculateRoute(
Arrays.asList(startWaypoint, destinationWaypoint),
carOptions,
(routingError, routes) -> {
if (routingError == null && routes != null && !routes.isEmpty()) {
mCurrentRoute = routes.get(0);
// We don't show multiple routes scenario here
// Invoke DrawTrafficLines here:
new DrawTrafficLines().execute(new ArrayList<>(mCurrentRoute.getSections()));
animateToRouteStart(mCurrentRoute);
} else {
Log.e(TAG, "Route calc error: " + routingError);
}
}
);
}
public void stopButtonClicked(View view) {
stopGuidance();
}
private Location getLastKnownLocationLocation() {
if (lastKnownLocation == null) {
Location location = new Location(routeStartGeoCoordinates);
location.time = new Date();
location.bearingInDegrees = 0.0;
return location;
}
return lastKnownLocation;
}
private void animateToRouteStart(Route route) {
GeoCoordinates startOfRoute = route.getGeometry().vertices.get(0);
MapCameraAnimation animation = MapCameraAnimationFactory.flyTo(
new GeoCoordinatesUpdate(startOfRoute),
new GeoOrientationUpdate(null, 70.0),
new MapMeasure(MapMeasure.Kind.DISTANCE, 50), 1, Duration.ofSeconds(3));
mapView.getCamera().startAnimation(animation, animationState -> {
if (animationState == AnimationState.COMPLETED || animationState == AnimationState.CANCELLED) {
startGuidance(route);
}
});
}
private void animateToDefaultMapPerspective() {
GeoCoordinates target = mapView.getCamera().getState().targetCoordinates;
MapCameraAnimation animation = MapCameraAnimationFactory.flyTo(
new GeoCoordinatesUpdate(target),
new GeoOrientationUpdate(null, (double)0),
new MapMeasure(MapMeasure.Kind.DISTANCE, DISTANCE_IN_METERS), 1, Duration.ofSeconds(3)
);
mapView.getCamera().startAnimation(animation);
}
private void startGuidance(Route route) {
if (visualNavigator.isRendering()) return;
isNavigating = true;
customizeVisualNavigatorColors();
customizeGuidanceView();
visualNavigator.startRendering(mapView);
resetLocationIndicator(true);
visualNavigator.setRoute(route);
visualNavigator.setRouteProgressListener(routeProgress -> {
if (visualNavigator.getRoute() != null) {
mCurrentRoute = visualNavigator.getRoute();
}
List<SectionProgress> sectionProgressList = routeProgress.sectionProgress;
SectionProgress lastSectionProgress = sectionProgressList.get(sectionProgressList.size()-1);
String msg = "Distance: " + lastSectionProgress.remainingDistanceInMeters + "m in " + lastSectionProgress.remainingDuration.getSeconds() + "s";
msgTextView.setText(msg);
});
visualNavigator.setNavigableLocationListener(navigableLocation -> {
if (navigableLocation.mapMatchedLocation == null) return;
MapMatchedLocation mapLoc = navigableLocation.mapMatchedLocation;
Log.d(TAG, "Lat: " + mapLoc.coordinates.latitude + " Lon: " + mapLoc.coordinates.longitude);
});
visualNavigator.setDestinationReachedListener(() -> {
Log.d(TAG, "Destination reached.");
stopGuidance();
});
setInterpolatedLocationListener();
startPeriodicTrafficUpdateOnRoute();
startRouteSimulation(route);
}
private void setInterpolatedLocationListener() {
visualNavigator.setInterpolatedLocationListener(location -> {
Log.d(TAG, "Interpolated location: " + location.coordinates);
showRouteOnMapRefresh(location.coordinates);
});
}
private void showRouteOnMapRefresh(GeoCoordinates currentCoordinates) {
if (visualNavigator.getRoute() != null) {
new Handler(Looper.getMainLooper()).postDelayed(
() -> {
if (mapTravelledPolylines.isEmpty()) {
List<GeoCoordinates> coords = new ArrayList<>();
try {
coords.add(visualNavigator.getRoute() != null ? visualNavigator.getRoute().getGeometry().vertices.get(0) : currentCoordinates);
coords.add(currentCoordinates);
float widthInPixels = 25;
MapPolyline routeTravelledMapPolyline = new MapPolyline(new GeoPolyline(coords),
new MapPolyline.SolidRepresentation(new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels), Color.valueOf(android.graphics.Color.LTGRAY), new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, 5), Color.valueOf(android.graphics.Color.LTGRAY), LineCap.ROUND));
routeTravelledMapPolyline.setDrawOrder(2);
mapView.getMapScene().addMapPolyline(routeTravelledMapPolyline);
mapTravelledPolylines.add(routeTravelledMapPolyline);
} catch (Exception e) {
e.printStackTrace();
}
} else {
GeoPolyline geoPolyline = mapTravelledPolylines.get(0).getGeometry();
geoPolyline.vertices.add(currentCoordinates);
mapTravelledPolylines.get(0).setGeometry(geoPolyline);
}
}, 0
);
}
}
private void startPeriodicTrafficUpdateOnRoute() {
trafficOnRouteUpdateHandler = new Handler();
trafficUpdateRunnable = new Runnable() {
@Override
public void run() {
requestRealtimeTrafficOnRoute();
trafficOnRouteUpdateHandler.postDelayed(this, REFRESH_ROUTE_PERIOD_IN_Millis);
}
};
trafficOnRouteUpdateHandler.post(trafficUpdateRunnable);
}
private void requestRealtimeTrafficOnRoute() {
Log.d(TAG, "Requesting real-time traffic update.");
if (mCurrentRoute == null) return;
// Use calculateTrafficOnRoute to update traffic on the current route without changing its geometry
// This ensures consistent route geometry while still updating traffic conditions
routingEngine.calculateTrafficOnRoute(
mCurrentRoute,
0, 0,
(routingError, trafficOnRoute) -> {
if (routingError == null && trafficOnRoute != null) {
runOnUiThread(() -> {
clearMapTrafficPolyline();
GeoCoordinates navPos = (lastKnownLocation != null) ? lastKnownLocation.coordinates : mCurrentRoute.getGeometry().vertices.get(0);
visualNavigator.setTrafficOnRoute(trafficOnRoute);
new DrawTrafficLinesFromTrafficOnRoute(navPos).execute(trafficOnRoute);
});
} else {
Log.e(TAG, "Error updating traffic on route:" + routingError);
}
}
);
}
private void stopPeriodicTrafficUpdateOnRoute() {
if (trafficOnRouteUpdateHandler != null) {
trafficOnRouteUpdateHandler.removeCallbacks(trafficUpdateRunnable);
}
}
private void stopGuidance() {
visualNavigator.stopRendering();
isNavigating = false;
if (locationSimulator != null) {
locationSimulator.stop();
}
visualNavigator.setCameraBehavior(null);
clearMap();
stopPeriodicTrafficUpdateOnRoute();
Log.d(TAG, "stopGuidance");
animateToDefaultMapPerspective();
}
private void clearMap() {
clearMapTrafficPolyline();
clearMapTravelledPolylines();
}
private void clearMapTrafficPolyline() {
if (trafficPolylines != null) {
for (MapPolyline polyline : trafficPolylines) {
mapView.getMapScene().removeMapPolyline(polyline);
}
trafficPolylines.clear();
trafficPolylines = null;
}
}
private void clearMapTravelledPolylines() {
if (mapTravelledPolylines != null) {
for (MapPolyline polyline : mapTravelledPolylines) {
mapView.getMapScene().removeMapPolyline(polyline);
}
mapTravelledPolylines.clear();
}
}
private void customizeVisualNavigatorColors() {
Color routeAheadColor = Color.valueOf(android.graphics.Color.BLUE);
Color routeAheadOutlineColor = Color.valueOf(android.graphics.Color.GRAY);
Color routeBehindColor = Color.valueOf(android.graphics.Color.LTGRAY);
Color routeBehindOutlineColor = Color.valueOf(android.graphics.Color.GRAY);
VisualNavigatorColors visualNavigatorColors = VisualNavigatorColors.dayColors();
RouteProgressColors routeProgressColors = new RouteProgressColors(
routeAheadColor,
routeBehindColor,
routeAheadOutlineColor,
routeBehindOutlineColor);
visualNavigatorColors.setRouteProgressColors(SectionTransportMode.CAR, routeProgressColors);
visualNavigator.setColors(visualNavigatorColors);
}
private void customizeGuidanceView() {
visualNavigator.setCameraBehavior(new DynamicCameraBehavior());
}
private final LocationListener myLocationListener = location -> {
visualNavigator.onLocationUpdated(location);
lastKnownLocation = location;
};
private void startRouteSimulation(Route route) {
if (locationSimulator != null) {
locationSimulator.stop();
}
try {
LocationSimulatorOptions locationSimulatorOptions = new LocationSimulatorOptions();
locationSimulatorOptions.speedFactor = 2.0;
locationSimulatorOptions.notificationInterval = Duration.ofMillis(500);
locationSimulator = new LocationSimulator(route, locationSimulatorOptions);
} catch (InstantiationErrorException e) {
throw new RuntimeException("Initialization of LocationSimulator failed: " + e.error.name());
}
locationSimulator.setListener(myLocationListener);
locationSimulator.start();
}
// Use the unified getTrafficLineColor logic from TrafficConditionUtils for consistency
@Nullable
public static Color getTrafficLineColor(Double jamFactor) {
if (jamFactor == null) {
return GUIDANCE_CONDITION_STRAIGHTWAY_COLOR; // Default to green if jamFactor is null
} else if (jamFactor < 4) {
return GUIDANCE_CONDITION_STRAIGHTWAY_COLOR; // Green for low traffic
} else if (jamFactor >= 4 && jamFactor < 8) {
return GUIDANCE_CONDITION_SLOW_COLOR; // Yellow
} else if (jamFactor >= 8 && jamFactor < 10) {
return GUIDANCE_CONDITION_OBSTRUCTION_COLOR; // Red
}
return GUIDANCE_CONDITION_CLOSED_COLOR; // Blue for jamFactor >= 10
}
private GeoPolyline buildPolylineForTrafficSpan(List<GeoCoordinates> sectionGeometry, TrafficOnSpan trafficSpan) {
int startIndex = trafficSpan.trafficSectionPolylineOffset;
if (startIndex < 0 || startIndex >= sectionGeometry.size()) return null;
double lengthNeeded = trafficSpan.lengthInMeters;
List<GeoCoordinates> spanVertices = new ArrayList<>();
spanVertices.add(sectionGeometry.get(startIndex));
double accumulatedDistance = 0.0;
for (int i = startIndex; i < sectionGeometry.size() - 1; i++) {
GeoCoordinates current = sectionGeometry.get(i);
GeoCoordinates next = sectionGeometry.get(i + 1);
double segmentDist = current.distanceTo(next);
if (accumulatedDistance + segmentDist > lengthNeeded) {
double ratio = (lengthNeeded - accumulatedDistance) / segmentDist;
double lat = current.latitude + ratio * (next.latitude - current.latitude);
double lon = current.longitude + ratio * (next.longitude - current.longitude);
spanVertices.add(new GeoCoordinates(lat, lon));
break;
} else {
spanVertices.add(next);
accumulatedDistance += segmentDist;
if (Math.abs(accumulatedDistance - lengthNeeded) < 0.1) {
break;
}
}
}
if (spanVertices.size() < 2) return null;
try {
return new GeoPolyline(spanVertices);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
// Draw initial traffic lines from the original route calculation using calculateRoute()
class DrawTrafficLines extends AsyncTask<ArrayList<Section>, Void, Void> {
@SafeVarargs
@Override
protected final Void doInBackground(ArrayList<Section>... arrayLists) {
System.runFinalization();
Runtime.getRuntime().runFinalization();
Runtime.getRuntime().gc();
System.gc();
clearMapTrafficPolyline();
trafficPolylines = null;
if (isTrafficEnabled) {
if (!arrayLists[0].isEmpty()) {
List<TrafficLineColor> trafficLineColors = new ArrayList<>();
// Using TrafficConditionUtils to get line colors
for (Section section : arrayLists[0]) {
trafficLineColors.addAll(TrafficConditionUtils.calculateSpansv2(MainActivityNew.this, section.getSpans()));
}
try {
trafficPolylines = new ArrayList<>();
float widthInPixels = 15; // unify thickness
for (TrafficLineColor trafficLine : trafficLineColors) {
GeoPolyline trafficGeoPolyline = new GeoPolyline(trafficLine.getCoordinatesList());
MapPolyline trafficRouteMapPolyline = new MapPolyline(
trafficGeoPolyline,
new MapPolyline.SolidRepresentation(
new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels),
trafficLine.getLineColor(),
new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, 1),
trafficLine.getLineColor(),
LineCap.ROUND
)
);
trafficRouteMapPolyline.setDrawOrder(1);
trafficPolylines.add(trafficRouteMapPolyline);
}
mapView.getMapScene().addMapPolylines(trafficPolylines);
Log.d(TAG, "Number of traffic polylines added: " + trafficPolylines.size());
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
}
// Draw updated traffic lines from the current route using calculateTrafficOnRoute()
class DrawTrafficLinesFromTrafficOnRoute extends AsyncTask<TrafficOnRoute, Void, Void> {
private GeoCoordinates navigatorPos;
DrawTrafficLinesFromTrafficOnRoute(GeoCoordinates navigatorPos) {
this.navigatorPos = navigatorPos;
}
@Override
protected final Void doInBackground(TrafficOnRoute... trafficOnRoutes) {
TrafficOnRoute trafficOnRoute = trafficOnRoutes[0];
System.runFinalization();
Runtime.getRuntime().runFinalization();
Runtime.getRuntime().gc();
System.gc();
clearMapTrafficPolyline();
trafficPolylines = new ArrayList<>();
if (!isTrafficEnabled || trafficOnRoute.trafficSections.isEmpty()) return null;
float widthInPixels = 15; // unify thickness here as well
for (TrafficOnSection trafficSection : trafficOnRoute.trafficSections) {
List<GeoCoordinates> sectionGeometry = trafficSection.geometry;
for (TrafficOnSpan trafficSpan : trafficSection.trafficSpans) {
Double jamFactor = trafficSpan.jamFactor;
Color lineColor = getTrafficLineColor(jamFactor);
if (lineColor == null) continue;
GeoPolyline spanPolyline = buildPolylineForTrafficSpan(sectionGeometry, trafficSpan);
if (spanPolyline != null) {
MapPolyline trafficSpanMapPolyline;
try {
trafficSpanMapPolyline = new MapPolyline(
spanPolyline,
new MapPolyline.SolidRepresentation(
new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels),
lineColor,
LineCap.ROUND
)
);
} catch (MapPolyline.Representation.InstantiationException |
MapMeasureDependentRenderSize.InstantiationException e) {
throw new RuntimeException(e);
}
trafficSpanMapPolyline.setDrawOrder(1);
trafficPolylines.add(trafficSpanMapPolyline);
}
}
}
if (!trafficPolylines.isEmpty()) {
runOnUiThread(() -> {
mapView.getMapScene().addMapPolylines(trafficPolylines);
Log.d(TAG, "Traffic polylines updated from TrafficOnRoute: " + trafficPolylines.size());
});
}
return null;
}
}
@Override
protected void onPause() {
mapView.onPause();
if (isNavigating && visualNavigator != null) {
visualNavigator.stopRendering();
} else {
mTriangleIndicator.setLocationIndicatorStyle(PEDESTRIAN);
mTriangleIndicator.enable(mapView);
}
// Free resources when the app goes into the background
mapView.getMapContext().freeResource(MapContext.ResourceType.MEMORY, MapContext.FreeResourceSeverity.CRITICAL);
super.onPause();
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
if (visualNavigator != null) {
if (isNavigating) {
visualNavigator.startRendering(mapView);
mTriangleIndicator.disable();
} else {
mTriangleIndicator.setLocationIndicatorStyle(PEDESTRIAN);
mTriangleIndicator.enable(mapView);
mTriangleIndicator.updateLocation(getLastKnownLocationLocation());
}
}
}
@Override
public void onLowMemory() {
super.onLowMemory();
mapView.getMapContext().freeResource(MapContext.ResourceType.MEMORY, MapContext.FreeResourceSeverity.CRITICAL);
}
@Override
protected void onDestroy() {
mapView.onDestroy();
disposeHERESDK();
super.onDestroy();
}
private void disposeHERESDK() {
SDKNativeEngine sdkNativeEngine = SDKNativeEngine.getSharedInstance();
if (sdkNativeEngine != null) {
sdkNativeEngine.dispose();
SDKNativeEngine.setSharedInstance(null);
}
}
}
- MapAdapter.java: Provides a singleton pattern for accessing MapView.
Here is the full class snippet:
package com.here.navigationcustom;
import com.here.sdk.mapview.MapView;
/**
* date:2024/12/4
* time:15:42
* author:sachin jonda
* desc:
*/
public class MapAdapter {
private volatile static MapAdapter INSTANCE;
private MapView mapView;
private MapAdapter() {
}
public static MapAdapter getInstance() {
if (INSTANCE == null) {
synchronized (MapAdapter.class) {
if (INSTANCE == null) {
INSTANCE = new MapAdapter();
}
}
}
return INSTANCE;
}
public void setMapView(MapView mapView) {
this.mapView = mapView;
}
public MapView getMapView() {
return mapView;
}
}
TrafficConditionUtils.java: Contains logic for determining polyline colors based on jam factors.
Here is the full class snippet:
package com.here.navigationcustom;
/**
* date:2024/12/4
* time:15:42
* author:sachin jonda
* desc:
*/
import android.content.Context;
import androidx.annotation.Nullable;
import com.blankj.utilcode.util.Utils;
import com.here.odnp.util.Log;
import com.here.sdk.core.Color;
import com.here.sdk.core.GeoCoordinates;
import com.here.sdk.routing.DynamicSpeedInfo;
import com.here.sdk.routing.Span;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for handling traffic conditions and drawing traffic lines.
*/
public class TrafficConditionUtils {
public final static Color GUIDANCE_CONDITION_STRAIGHTWAY_COLOR = Color.valueOf(Utils.getApp().getColor(R.color.route_line_color));
public final static Color GUIDANCE_CONDITION_SLOW_COLOR = Color.valueOf(Utils.getApp().getColor(R.color.guidance_condition_slow));
public final static Color GUIDANCE_CONDITION_OBSTRUCTION_COLOR = Color.valueOf(Utils.getApp().getColor(R.color.guidance_condition_obstruction));
public final static Color GUIDANCE_CONDITION_CLOSED_COLOR = Color.valueOf(Utils.getApp().getColor(R.color.guidance_condition_closed));
@Nullable
public static Color getTrafficLineColor(Double jamFactor) {
if (jamFactor == null) {
return GUIDANCE_CONDITION_STRAIGHTWAY_COLOR; // Default to green if jamFactor is null
} else if (jamFactor < 4) {
return GUIDANCE_CONDITION_STRAIGHTWAY_COLOR; // Green for low traffic
} else if (jamFactor >= 4 && jamFactor < 8) {
return GUIDANCE_CONDITION_SLOW_COLOR; // Yellow
} else if (jamFactor >= 8 && jamFactor < 10) {
return GUIDANCE_CONDITION_OBSTRUCTION_COLOR; // Red
}
return GUIDANCE_CONDITION_CLOSED_COLOR; // Blue for jamFactor >= 10
}
public static List<TrafficLineColor> calculateSpansv2(Context context, List<Span> spanList) {
List<TrafficLineColor> trafficLineColors = new ArrayList<>();
List<GeoCoordinates> spanPolyline = new ArrayList<>();
Color currentLineColor = null; // Start with no color
for (Span span : spanList) {
DynamicSpeedInfo dynamicSpeed = span.getDynamicSpeedInfo();
if (dynamicSpeed == null) {
continue;
}
double jamFactor = dynamicSpeed.calculateJamFactor();
Log.d("TrafficConditionUtils", "Jam Factor: " + jamFactor);
// Determine line color based on jam factor using the updated method
Color lineColor = getTrafficLineColor(jamFactor);
if (lineColor == null) {
continue;
}
// If the current color matches the previous, continue adding to the polyline
if (currentLineColor != null && currentLineColor.toArgb() == lineColor.toArgb()) {
spanPolyline.addAll(span.getGeometry().vertices);
} else {
// Add the completed polyline with its color to the list
if (!spanPolyline.isEmpty()) {
trafficLineColors.add(new TrafficLineColor(new ArrayList<>(spanPolyline), currentLineColor));
spanPolyline.clear();
}
// Start a new polyline with the new color
currentLineColor = lineColor;
spanPolyline.addAll(span.getGeometry().vertices);
}
}
// Add the last segment if it exists
if (!spanPolyline.isEmpty() && currentLineColor != null) {
trafficLineColors.add(new TrafficLineColor(new ArrayList<>(spanPolyline), currentLineColor));
}
Log.d("TrafficConditionUtils", "Total traffic line segments: " + trafficLineColors.size());
return trafficLineColors;
}
}
- TrafficLineColor.java: A helper class to store polyline segments and colors.
Here is the full class snippet:
package com.here.navigationcustom;
/**
* date:2024/12/4
* time:15:42
* author:sachin jonda
* desc:
*/
import com.here.sdk.core.Color;
import com.here.sdk.core.GeoCoordinates;
import java.util.List;
/**
* Helper class to store polyline coordinates with their associated color.
*/
public class TrafficLineColor {
private List<GeoCoordinates> coordinatesList;
private Color lineColor;
public TrafficLineColor(List<GeoCoordinates> coordinatesList, Color lineColor) {
this.coordinatesList = coordinatesList;
this.lineColor = lineColor;
}
public List<GeoCoordinates> getCoordinatesList() {
return coordinatesList;
}
public Color getLineColor() {
return lineColor;
}
}
Adding it All Up
- No Forced Recalculation: Rather than calling calculateRoute() repeatedly, you now call calculateTrafficOnRoute()—improving performance and consistency.
- Seamless ETA Updates: visualNavigator.setTrafficOnRoute() updates ETAs, maneuvers, and instructions internally. The user experience is smoother because VisualNavigator continues along the same route.
- Minimal Code Changes: The approach leverages existing HERE SDK navigation mechanisms, so the integration requires fewer code modifications and leads to more maintainable and robust applications.
Conclusion
By using calculateTrafficOnRoute() and visualNavigator.setTrafficOnRoute() together, you maintain a consistent route while seamlessly integrating updated traffic conditions. This ensures ETAs, instructions, and route colors remain in sync with actual road conditions, significantly enhancing user experience.
Compared to older methods that recalculated routes or redrew polylines entirely on each update, this approach is both elegant and efficient. As real-time navigation needs grow, employing such refined integrations keeps you ahead—providing navigation experiences that are not only accurate but also context-aware, intuitive, and performance-optimized.
Use these insights to provide navigation experiences that satisfy users, accurately mirror real-world conditions, and meet the high standards expected in modern map-based applications.
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