Visualize and update traffic on routes

Enhance your map applications with real-time traffic data using HERE SDK's advanced traffic features. Display live traffic flow and incidents on the map to keep users informed and help them navigate efficiently. Additionally, you can pick and highlight specific traffic incidents for a detailed view.

For the HERE SDK (Navigate) you can also incorporate traffic information from radio stations.

Show real-time traffic flow and incidents on the map

You can easily visualize traffic incidents on the map by enabling the map layer state TRAFFIC_INCIDENTS. The HERE SDK also supports a separate layer to see the current traffic situation. See the example below for how to show or hide a layer on the map.

Screenshot: Traffic incidents visualized on the map.

After a layer is set, the visible area of the map is automatically updated. So you can freely pan the map in all directions to see the latest traffic incidents.

In many situations, drivers are interested in finding the fastest route based on the current traffic jams in a city - or outside a city. The HERE SDK allows you to show a layer holding all the current traffic jams, visualized by lines in different colors to indicate the severity of the jam - always updated in real-time. This feature requires an online connection and consumes slightly more data. However, the traffic lines are shown as part of the map tiles and are therefore highly performant.

Together - or independently - you can visualize such traffic information on the map with just a few lines of code:

private void enableTrafficVisualization() {
    // Try to refresh the TRAFFIC_FLOW vector tiles 5 minutes.
    // If MapFeatures.TRAFFIC_FLOW is disabled, no requests are made.
    try {
        MapContentSettings.setTrafficRefreshPeriod(Duration.ofMinutes(5));
    } catch (MapContentSettings.TrafficRefreshPeriodException e) {
        throw new RuntimeException("TrafficRefreshPeriodException: " + e.error.name());
    }

    Map<String, String> mapFeatures = new HashMap<>();
    // Once these traffic layers are added to the map, they will be automatically updated while panning the map.
    mapFeatures.put(MapFeatures.TRAFFIC_FLOW, MapFeatureModes.TRAFFIC_FLOW_WITH_FREE_FLOW);
    // MapFeatures.TRAFFIC_INCIDENTS renders traffic icons and lines to indicate the location of incidents.
    mapFeatures.put(MapFeatures.TRAFFIC_INCIDENTS, MapFeatureModes.DEFAULT);
    mapView.getMapScene().enableFeatures(mapFeatures);
}
private fun enableTrafficVisualization() {
    // Try to refresh the TRAFFIC_FLOW vector tiles 5 minutes.
    // If MapFeatures.TRAFFIC_FLOW is disabled, no requests are made.
    //
    // Note: This code initiates periodic calls to the HERE Traffic backend. Depending on your contract,
    // each call may be charged separately. It is the application's responsibility to decide how
    // often this code should be executed.
    try {
        MapContentSettings.setTrafficRefreshPeriod(Duration.ofMinutes(5))
    } catch (e: TrafficRefreshPeriodException) {
        throw RuntimeException("TrafficRefreshPeriodException: " + e.error.name)
    }

    val mapFeatures: MutableMap<String, String> = HashMap()
    // Once these traffic layers are added to the map, they will be automatically updated while panning the map.
    mapFeatures[MapFeatures.TRAFFIC_FLOW] = MapFeatureModes.TRAFFIC_FLOW_WITH_FREE_FLOW
    // MapFeatures.TRAFFIC_INCIDENTS renders traffic icons and lines to indicate the location of incidents.
    mapFeatures[MapFeatures.TRAFFIC_INCIDENTS] = MapFeatureModes.DEFAULT
    mapView.mapScene.enableFeatures(mapFeatures)
}

Optionally, for MapFeatures.TRAFFIC_FLOW and MapFeatures.TRAFFIC_INCIDENTS you can also specify how often a request should be initiated in order to fetch the latest traffic data from the backend. Setting this via MapContentSettings.setTrafficRefreshPeriod(Duration.ofMinutes(5)) is most useful in a scenario where the map view remains static.

Regardless of the traffic refresh period setting, when a viewport change occurs, the HERE SDK may need to fetch new traffic data to render the updated viewport correctly.

📘

Note

During turn-by-turn navigation, viewport changes can happen multiple times per second and may result in a high number of Traffic Vector Tile requests when these features are enabled. As an alternative, consider using TrafficOnRoute to update only the traffic visualization along the route itself.

For disabling the traffic layers, you can call:

private void disableTrafficVisualization() {
    List<String> mapFeatures = new ArrayList<>();
    mapFeatures.add(MapFeatures.TRAFFIC_FLOW);
    mapFeatures.add(MapFeatures.TRAFFIC_INCIDENTS);
    mapView.getMapScene().disableFeatures(mapFeatures);
}
private fun disableTrafficVisualization() {
    val mapFeatures: MutableList<String> = ArrayList()
    mapFeatures.add(MapFeatures.TRAFFIC_FLOW)
    mapFeatures.add(MapFeatures.TRAFFIC_INCIDENTS)
    mapView.mapScene.disableFeatures(mapFeatures)
}

The traffic flow lines are color coded as follows:

  • Green: Normal traffic
  • Amber/Yellow: High traffic
  • Red: Very high traffic
  • Black: Blocking traffic
Screenshot: Traffic flow visualized on the map together with incidents.

Pick traffic incidents

When the TRAFFIC_INCIDENTS is shown on the MapView, you can set up a tap handler and pick the traffic incidents to get more information.

private void setTapGestureHandler() {
    mapView.getGestures().setTapListener(touchPoint -> {
        GeoCoordinates touchGeoCoords = mapView.viewToGeoCoordinates(touchPoint);
        // Can be null when the map was tilted and the sky was tapped.
        if (touchGeoCoords != null) {
            // Pick incidents that are shown in TRAFFIC_INCIDENTS.
            pickTrafficIncident(touchPoint);
        }
    });
}

// Traffic incidents can only be picked, when TRAFFIC_INCIDENTS is visible.
private void pickTrafficIncident(Point2D touchPointInPixels) {
    Point2D originInPixels = new Point2D(touchPointInPixels.x, touchPointInPixels.y);
    Size2D sizeInPixels = new Size2D(1, 1);
    Rectangle2D rectangle = new Rectangle2D(originInPixels, sizeInPixels);

    // Creates a list of map content type from which the results will be picked.
    // The content type values can be MAP_CONTENT, MAP_ITEMS and CUSTOM_LAYER_DATA.
    ArrayList<MapScene.MapPickFilter.ContentType> contentTypesToPickFrom = new ArrayList<>();

    // MAP_CONTENT is used when picking embedded Carto POIs, traffic incidents, vehicle restriction etc.
    // MAP_ITEMS is used when picking map items such as MapMarker, MapPolyline, MapPolygon etc.
    // Currently we need traffic incidents so adding the MAP_CONTENT filter.
    contentTypesToPickFrom.add(MapScene.MapPickFilter.ContentType.MAP_CONTENT);
    MapScene.MapPickFilter filter = new MapScene.MapPickFilter(contentTypesToPickFrom);

    // If you do not want to specify any filter you can pass filter as NULL and all of the pickable contents will be picked.
    mapView.pick(filter, rectangle, new MapViewBase.MapPickCallback() {
        @Override
        public void onPickMap(@Nullable MapPickResult mapPickResult) {
            if (mapPickResult == null) {
                // An error occurred while performing the pick operation.
                return;
            }

            List<PickMapContentResult.TrafficIncidentResult> trafficIncidents =
                    mapPickResult.getMapContent().getTrafficIncidents();
            if (trafficIncidents.isEmpty()) {
                Log.d(TAG, "No traffic incident found at picked location");
            } else {
                Log.d(TAG, "Picked at least one incident.");
                PickMapContentResult.TrafficIncidentResult firstIncident = trafficIncidents.get(0);
                showDialog("Traffic incident picked:", "Type: " +
                        firstIncident.getType().name());

                // Find more details by looking up the ID via TrafficEngine.
                findIncidentByID(firstIncident.getOriginalId());
            }

            // Optionally, look for more map content like embedded POIs.
        }
    });
}
private fun setTapGestureHandler() {
    mapView.gestures.tapListener =
        TapListener { touchPoint: Point2D ->
            val touchGeoCoords = mapView.viewToGeoCoordinates(touchPoint)
            // Can be null when the map was tilted and the sky was tapped.
            if (touchGeoCoords != null) {
                // Pick incidents that are shown in TRAFFIC_INCIDENTS.
                pickTrafficIncident(touchPoint)
            }
        }
}

// Traffic incidents can only be picked, when TRAFFIC_INCIDENTS is visible.
private fun pickTrafficIncident(touchPointInPixels: Point2D) {
    val originInPixels = Point2D(touchPointInPixels.x, touchPointInPixels.y)
    val sizeInPixels = Size2D(1.0, 1.0)
    val rectangle = Rectangle2D(originInPixels, sizeInPixels)

    // Creates a list of map content type from which the results will be picked.
    // The content type values can be MAP_CONTENT, MAP_ITEMS and CUSTOM_LAYER_DATA.
    val contentTypesToPickFrom = ArrayList<MapPickFilter.ContentType>()

    // MAP_CONTENT is used when picking embedded Carto POIs, traffic incidents, vehicle restriction etc.
    // MAP_ITEMS is used when picking map items such as MapMarker, MapPolyline, MapPolygon etc.
    // Currently we need traffic incidents so adding the MAP_CONTENT filter.
    contentTypesToPickFrom.add(MapPickFilter.ContentType.MAP_CONTENT)
    val filter = MapPickFilter(contentTypesToPickFrom)

    // If you do not want to specify any filter you can pass filter as NULL and all of the pickable contents will be picked.
    mapView.pick(filter, rectangle, MapPickCallback { mapPickResult ->
        if (mapPickResult == null) {
            // An error occurred while performing the pick operation.
            return@MapPickCallback
        }
        val trafficIncidents =
            mapPickResult.mapContent!!.trafficIncidents
        if (trafficIncidents.isEmpty()) {
            Log.d(TAG, "No traffic incident found at picked location")
        } else {
            Log.d(TAG, "Picked at least one incident.")
            val firstIncident = trafficIncidents[0]
            showDialog(
                "Traffic incident picked:", "Type: " +
                        firstIncident.type.name
            )

            // Find more details by looking up the ID via TrafficEngine.
            findIncidentByID(firstIncident.originalId)
        }
        // Optionally, look for more map content like embedded POIs.
    })
}

With the tap handler, we get the touched location in view coordinates that can be passed to mapView.pickMapContent(). Here, we just use a point-sized rectangle, but you can also enlarge the pick area to include more content at once.

The callback provides a PickMapContentResult which can contain a TrafficIncidentResult, but also other embedded types like default POI markers, that are always visible on the map. The TrafficIncidentResult type already provides most information about the incident, but to get all available information, we can use the TrafficEngine (see also below) to search for the picked incident by ID:

private void findIncidentByID(String originalId) {
    TrafficIncidentLookupOptions trafficIncidentsQueryOptions = new TrafficIncidentLookupOptions();
    // Optionally, specify a language:
    // the language of the country where the incident occurs is used.
    // trafficIncidentsQueryOptions.languageCode = LanguageCode.EN_US;
    trafficEngine.lookupIncident(originalId, trafficIncidentsQueryOptions, new TrafficIncidentLookupCallback() {
        @Override
        public void onTrafficIncidentFetched(@Nullable TrafficQueryError trafficQueryError, @Nullable TrafficIncident trafficIncident) {
            if (trafficQueryError == null) {
                Log.d(TAG, "Fetched TrafficIncident from lookup request." +
                        " Description: " + trafficIncident.getDescription().text);
            } else {
                showDialog("TrafficLookupError:", trafficQueryError.toString());
            }
        }
    });
}
private fun findIncidentByID(originalId: String) {
    val trafficIncidentsQueryOptions = TrafficIncidentLookupOptions()
    // Optionally, specify a language:
    // the language of the country where the incident occurs is used.
    // trafficIncidentsQueryOptions.languageCode = LanguageCode.EN_US;
    trafficEngine.lookupIncident(originalId, trafficIncidentsQueryOptions,
        TrafficIncidentLookupCallback { trafficQueryError, trafficIncident ->
            if (trafficQueryError == null) {
                Log.d(
                    TAG, "Fetched TrafficIncident from lookup request." +
                            " Description: " + trafficIncident!!.description.text
                )
            } else {
                showDialog("TrafficLookupError:", trafficQueryError.toString())
            }
        })
}

A usage example is available on GitHub as part of the "Traffic" example app, provided in both Java and Kotlin.

Get traffic broadcasts from radio stations (only available for Navigate)

With the TrafficDataProvider interface the HERE SDK allows to integrate radio station signals providing traffic broadcasts.

A TrafficBroadcast expects traffic data in the RDS-TMC format and can be used without an internet connection. In such cases, the OfflineRoutingEngine can utilize traffic data received over the radio channel, provided the RDS_TRAFFIC layer is enabled using LayerConfiguration. For more information on how to use LayerConfiguration, see here.

The trafficBroadcast.activate() method needs to be called to receive traffic data events.

A TrafficBroadcast continuously reacts to new locations provided from a location source and acts as a LocationListener. The location must be updated regardless of calling activate().

📘

Note

In order to adopt the interface special hardware is required. Talk to your HERE representative for more details. Note that this feature is released as a beta feature.

Update and visualize traffic on routes

Keep your routes up-to-date with the latest traffic conditions and visualize them on the map. This section explains how to refresh traffic data on existing routes, extract updated ETAs, visualize traffic along routes, and leverage dynamic routing during navigation to find better alternatives when traffic changes.

Update traffic information on routes

Use the option that best matches your scenario:

  • When your use case is route preview and you only need to refresh traffic and ETA on the current route, then choose calculateTrafficOnRoute(). It returns a TrafficOnRoute object with traffic details per TrafficOnSection and TrafficOnSpan (see Update traffic during route preview).
  • When your use case is updating an existing route instance with refreshed route data such as metadata or origin-related updates, then choose refreshRoute() (see Update traffic during route preview).
  • When your use case is turn-by-turn navigation and you want to keep ETA updates on the current guided route, then choose calculateTrafficOnRoute() together with VisualNavigator.setTrafficOnRoute() (see Update traffic during turn-by-turn navigation and Update ETA and traffic during navigation).
  • When your use case is finding a potentially better route while driving, including possible geometry changes due to current traffic, then choose DynamicRoutingEngine (see Navigation).
  • When your use case is showing route traffic next to live traffic flow, then choose the adjacent polyline approach (see Render a polyline adjacent to traffic flow).
  • When your use case is turn-by-turn route rendering with Navigate defaults, then choose VisualNavigator polyline widths for consistent guidance visuals (see Render a polyline with the VisualNavigator (Navigate only)).
  • When your use case is custom on-route traffic visualization from predictive route data (for example by jam factor), then choose custom span rendering (see Render custom traffic along a route).

Note that calculateTrafficOnRoute() and refreshRoute() do not change the route distance and geometry.

Note (only for Navigate)

Alternatively, you can use the DynamicRoutingEngine to find improved routes during navigation. These routes are optimized based on the current traffic flow. As a result, they may also include new route geometry to bypass heavy traffic. For more details about the DynamicRoutingEngine, refer to the Navigation section.

Update traffic during route preview

The routingEngine.calculateTrafficOnRoute(..) method updates traffic on a specific section or an entire route after it has been created. It allows to calculated this during navigation or in a route preview context.

It does this by recalculating traffic from the last traveled route section, using its index and an offset (in meters) from the last visited position. In a route preview context you can set lastTraveledSectionIndex and traveledDistanceOnLastSectionInMeters to 0 in order to update traffic for the entire route.

Here's how the method can be utilized in screens primarily designed for route previews:

public void updateTrafficOnRoute(Route route) {
    // Since traffic is being calculated for the entire route, lastTraveledSectionIndex and traveledDistanceOnLastSectionInMeters are set to 0.
    int lastTraveledSectionIndex = 0;
    int traveledDistanceOnLastSectionInMeters = 0;

    routingEngine.calculateTrafficOnRoute(route, lastTraveledSectionIndex, traveledDistanceOnLastSectionInMeters, new CalculateTrafficOnRouteCallback() {
        @Override
        public void onTrafficOnRouteCalculated(@Nullable RoutingError routingError, @Nullable TrafficOnRoute trafficOnRoute) {
            if (routingError != null) {
                Log.d(TAG, "CalculateTrafficOnRoute error: " + routingError.name());
            } else {
                showUpdatedETA(trafficOnRoute);
            }
        }
    });
}
private fun updateTrafficOnRoute(route: Route) {
    // Since traffic is being calculated for the entire route, lastTraveledSectionIndex and traveledDistanceOnLastSectionInMeters are set to 0.
    val lastTraveledSectionIndex = 0
    val traveledDistanceOnLastSectionInMeters = 0

    routingEngine?.calculateTrafficOnRoute(
        route,
        lastTraveledSectionIndex,
        traveledDistanceOnLastSectionInMeters,
        object : CalculateTrafficOnRouteCallback {
            override fun onTrafficOnRouteCalculated(
                routingError: RoutingError?,
                trafficOnRoute: TrafficOnRoute?
            ) {
                if (routingError != null) {
                    Log.d(TAG, "CalculateTrafficOnRoute error: " + routingError.name)
                } else {
                    showUpdatedETA(trafficOnRoute)
                }
            }
        }
    )
}

The updated ETA from TrafficOnRoute is available through TrafficOnSpan. Each TrafficOnSpan provides traffic information along the route. The following method can be used to extract the updated ETA:

private void showUpdatedETA(TrafficOnRoute trafficOnRoute) {
    for (TrafficOnSection section : trafficOnRoute.trafficSections) {
        List<TrafficOnSpan> spans = section.trafficSpans;

        long updatedETAInSeconds = spans.stream()
                .mapToLong(span -> span.duration.getSeconds())
                .sum();

        long updatedTrafficDelayInSeconds = spans.stream()
                .mapToLong(span -> span.trafficDelay.getSeconds())
                .sum();

        String updatedETAString = String.format("Updated ETA %s\nUpdated traffic delay %s",
                timeUtils.formatTime(updatedETAInSeconds),
                timeUtils.formatTime(updatedTrafficDelayInSeconds));
        showDialog("Updated traffic", updatedETAString);
    }
}
private fun showUpdatedETA(trafficOnRoute: TrafficOnRoute?) {
    for (section in trafficOnRoute?.trafficSections!!) {
        val spans = section.trafficSpans

        val updatedETAInSeconds = spans.sumOf { it.duration.seconds }
        val updatedTrafficDelayInSeconds = spans.sumOf { it.trafficDelay.seconds }

        val updatedETAString = String.format(
            "Updated ETA %s\nUpdated traffic delay %s",
            timeUtils.formatTime(updatedETAInSeconds),
            timeUtils.formatTime(updatedTrafficDelayInSeconds)
        )
        showDialog("Updated traffic", updatedETAString)
    }
}
📘

Note

Traffic updates use predictive models to forecast conditions along your route, which may differ from real-time traffic shown on map layers. For details on these differences, see Real-time versus predictive traffic data sources.

Update traffic during turn-by-turn navigation (only for HERE SDK for Navigate)

For guidance-specific traffic updates with VisualNavigator.setTrafficOnRoute(), including a periodic calculateTrafficOnRoute() example, see Update ETA and traffic during navigation.

Visualize traffic on routes

The HERE SDK enables you to visualize traffic conditions directly on route polylines. You can choose between two approaches: rendering routes adjacent to the real-time traffic flow layer on the map, or drawing custom traffic overlays using predictive traffic data from the route itself.

To understand the difference between real-time and predictive traffic data sources and when to use each approach, see Real-time versus predictive traffic data sources.

Render a polyline adjacent to traffic flow

By default, the HERE SDK provides the color encoding used for the traffic flow layer which indicates traffic conditions. Using MapMeasureDependentRenderSize, you can define the rendering width of your polyline based on the MapMeasure.Kind so that it renders adjacent to these traffic polylines.

Screenshot: Polyline adjacent to traffic flow.

Here's an example to implement it:

GeoPolyline routeGeoPolyline = route.getGeometry();
double outlineWidthInPixel;
Color polylineColor = Color.valueOf(0.051f, 0.380f, 0.871f, 1.0f);
Color outlineColor = Color.valueOf(0.043f, 0.325f, 0.749f, 1.0f);
MapPolyline routeMapPolyline = null;
try {
    // Below, we're creating an instance of MapMeasureDependentRenderSize. This instance will use the scaled width values to render the route polyline.
    // We can also apply the same values to MapArrow.setMeasureDependentTailWidth().
    // The parameters for the constructor are: the kind of MapMeasure (in this case, ZOOM_LEVEL), the unit of measurement for the render size (PIXELS), and the scaled width values.
    MapMeasureDependentRenderSize mapMeasureDependentLineWidth = new MapMeasureDependentRenderSize(MapMeasure.Kind.ZOOM_LEVEL, RenderSize.Unit.PIXELS, getDefaultLineWidthValues());

    // We can also use MapMeasureDependentRenderSize to specify the outline width of the polyline.
    outlineWidthInPixel = 1.23 * mapView.getPixelScale();
    MapMeasureDependentRenderSize mapMeasureDependentOutlineWidth = new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, outlineWidthInPixel);
    routeMapPolyline = new MapPolyline(routeGeoPolyline, new MapPolyline.SolidRepresentation(
            mapMeasureDependentLineWidth,
            polylineColor,
            mapMeasureDependentOutlineWidth,
            outlineColor,
            LineCap.ROUND));

} catch (MapPolyline.Representation.InstantiationException e) {
    Log.e("MapPolyline Representation Exception:", e.error.name());
} catch (MapMeasureDependentRenderSize.InstantiationException e) {
    Log.e("MapMeasureDependentRenderSize Exception:", e.error.name());
}
mapView.getMapScene().addMapPolyline(routeMapPolyline);
val routeGeoPolyline = route.geometry
var outlineWidthInPixel: Double
val polylineColor = Color.valueOf(0.051f, 0.380f, 0.871f, 1.0f)
val outlineColor = Color.valueOf(0.043f, 0.325f, 0.749f, 1.0f)
var routeMapPolyline: MapPolyline? = null
try {
    // Below, we're creating an instance of MapMeasureDependentRenderSize. This instance will use the scaled width values to render the route polyline.
    // We can also apply the same values to MapArrow.setMeasureDependentTailWidth().
    // The parameters for the constructor are: the kind of MapMeasure (in this case, ZOOM_LEVEL), the unit of measurement for the render size (PIXELS), and the scaled width values.
    val mapMeasureDependentLineWidth = MapMeasureDependentRenderSize(
        MapMeasure.Kind.ZOOM_LEVEL, RenderSize.Unit.PIXELS, getDefaultLineWidthValues()
    )

    // We can also use MapMeasureDependentRenderSize to specify the outline width of the polyline.
    outlineWidthInPixel = 1.23 * mapView.pixelScale
    val mapMeasureDependentOutlineWidth = MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, outlineWidthInPixel)
    routeMapPolyline = MapPolyline(
        routeGeoPolyline, MapPolyline.SolidRepresentation(
            mapMeasureDependentLineWidth,
            polylineColor,
            mapMeasureDependentOutlineWidth,
            outlineColor,
            LineCap.ROUND
        )
    )
} catch (e: MapPolyline.Representation.InstantiationException) {
    Log.e("MapPolyline Representation Exception:", e.error.name)
} catch (e: MapMeasureDependentRenderSize.InstantiationException) {
    Log.e("MapMeasureDependentRenderSize Exception:", e.error.name)
}
mapView.mapScene.addMapPolyline(routeMapPolyline)

The width values specified for MapMeasureDependentRenderSize are linearly interpolated between each successive pair of data points. It can be generated like this (code only applicable for Explore):

// Returns a hashMap where the key is the zoom level and the value is the corresponding scaled width.
private HashMap<Double, Double> getDefaultLineWidthValues() {
    HashMap<Double, Double> widthsPerZoomLevel = new HashMap<>();
    // Here width value will remain 2.18 from 0.0 zoom level to 6.0.
    // From 6.0 the updated value 2.48 will be used.
    widthsPerZoomLevel.put(0.0, 2.18);
    widthsPerZoomLevel.put(6.0, 2.48);
    widthsPerZoomLevel.put(7.0, 2.78);
    widthsPerZoomLevel.put(11.0, 3.1);
    widthsPerZoomLevel.put(16.0, 3.15);
    widthsPerZoomLevel.put(17.0, 3.35);
    widthsPerZoomLevel.put(18.0, 3.78);
    widthsPerZoomLevel.put(19.0, 6.5);
    widthsPerZoomLevel.put(20.0, 10.2);
    widthsPerZoomLevel.put(24.0, 77.5);
    return scaleCurrentHashMap(widthsPerZoomLevel);
}

private HashMap<Double, Double> scaleCurrentHashMap(HashMap<Double, Double> currentValues) {
    for (Map.Entry<Double, Double> entry : currentValues.entrySet()) {
        entry.setValue(entry.getValue() * mapView.getPixelScale());
    }
    return currentValues;
}
// Returns a hashMap where the key is the zoom level and the value is the corresponding scaled width.
private fun getDefaultLineWidthValues(): HashMap<Double, Double> {
    val widthsPerZoomLevel = HashMap<Double, Double>()
    // Here width value will remain 2.18 from 0.0 zoom level to 6.0.
    // From 6.0 the updated value 2.48 will be used.
    widthsPerZoomLevel[0.0] = 2.18
    widthsPerZoomLevel[6.0] = 2.48
    widthsPerZoomLevel[7.0] = 2.78
    widthsPerZoomLevel[11.0] = 3.1
    widthsPerZoomLevel[16.0] = 3.15
    widthsPerZoomLevel[17.0] = 3.35
    widthsPerZoomLevel[18.0] = 3.78
    widthsPerZoomLevel[19.0] = 6.5
    widthsPerZoomLevel[20.0] = 10.2
    widthsPerZoomLevel[24.0] = 77.5
    return scaleCurrentHashMap(widthsPerZoomLevel)
}

private fun scaleCurrentHashMap(currentValues: HashMap<Double, Double>): HashMap<Double, Double> {
    for ((_, value) in currentValues) {
        currentValues.put(currentValues.keys.first { currentValues[it] == value }, value * mapView.pixelScale)
    }
    return currentValues
}

Render a polyline with the VisualNavigator (Navigate only)

During turn-by-turn guidance you can use the default values from VisualNavigator which can be retrieved using defaultRouteManeuverArrowMeasureDependentWidths. Further scaling the width values will enable a clean and persistent look across various devices.

// Retrieves the default widths of a route polyline and maneuver arrows from VisualNavigator,
// scaling them based on the screen's pixel density.
private HashMap<Double, Double> getDefaultLineWidthValues() {
    HashMap<Double, Double> widthsPerZoomLevel = new HashMap<>();
    for (Map.Entry<MapMeasure, Double> defaultValues : VisualNavigator.defaultRouteManeuverArrowMeasureDependentWidths().entrySet()) {
        Double key = defaultValues.getKey().value;
        Double value = defaultValues.getValue() * mapView.getPixelScale();
        widthsPerZoomLevel.put(key, value);
    }
    return widthsPerZoomLevel;
}
// Retrieves the default widths of a route polyline and maneuver arrows from VisualNavigator,
// scaling them based on the screen's pixel density.
private fun getDefaultLineWidthValues(): HashMap<Double, Double> {
    val widthsPerZoomLevel = HashMap<Double, Double>()
    for ((key, value) in VisualNavigator.defaultRouteManeuverArrowMeasureDependentWidths()) {
        widthsPerZoomLevel[key.value] = value * mapView.pixelScale
    }
    return widthsPerZoomLevel
}

Render custom traffic along a route

As an alternative to rendering the route's polyline adjacent to traffic flow, you can also render the traffic yourself utilizing the traffic data available as part of the Route object. This can be useful during route preview use cases, before starting actual guidance.

Note that this approach may be perceived as less accurate because the traffic information includes predictive data from the route instead of the real-time information provided by the traffic flow schemes. However, similar to the color encoding used for the traffic flow layer, you can indicate the traffic along a route using dynamicSpeedInfo.calculateJamFactor() that ranges from 0 (no traffic) to 10 (road is blocked). Each Section of a Route can contain various DynamicSpeedInfo instances. These are valid along the Span until the next Span. Each Span geometry is represented by a polyline that is part of the full route's polyline shape.

The following code snippet shows how to get a DynamicSpeedInfo element of the first Span of a Section:

Section firstSection = route.getSections().get(0);
DynamicSpeedInfo dynamicSpeed = firstSection.getSpans().get(0).getDynamicSpeedInfo();
val firstSection = route.sections[0]
val dynamicSpeed = firstSection.spans[0].dynamicSpeedInfo

DynamicSpeedInfo contains the baseSpeedInMetersPerSecond, which is the expected default travel speed. Note that this may not be the same as the current speed limit on a road - as a bad road condition may justify a slower travel speed. In addition, you can get the estimated actual travel speed based on the current traffic conditions with trafficSpeedInMetersPerSecond.

An example how this value can be mapped to a suitable color is shown below:

Illustration: Traffic jam factors.

Usually, the jam factor can be interpreted like this:

  • 0 <= jam factor < 4: No or light traffic.
  • 4 <= jam factor < 8: Moderate or slow traffic.
  • 8 <= jam factor < 10: Severe traffic.
  • jam factor = 10: No traffic, that is the road is blocked.
📘

Note

Note that the jam factor you can get from DynamicSpeedInfo is calculated piecewise-linear from the ratio of trafficSpeedInMetersPerSecond / baseSpeedInMetersPerSecond - without taking road types and other parameters into account. Therefore, the provided jam factor does not necessarily match exactly the traffic flow visualization on the map view (if enabled). On top, the RoutingEngine uses predictive location services to predict upcoming traffic while traversing the route - whereas the traffic flow visualization represents only the current point in time (real time). Also, worth to mention, the prerendered traffic flow may use a different color representation based on the current zoom level.

It is an app decision whether to prefer predicted traffic along a route that matches the given ETA - or to show instead the real-time traffic situation, which might not be accurate any more when reaching a destination in the future.

The recommended approach for the latter is to show the route and the traffic-flow side-by-side by using a map-measure dependent width for the route's polyline - instead of rendering the traffic additionally on the route itself.

Screenshot: Traffic visualized on a route using jam factor.

If you want to visualize the traffic along a route, consider rendering multiple colored MapPolyline objects for each span of a section:

// This renders the traffic jam factor on top of the route as multiple MapPolylines per span.
private void showTrafficOnRoute(Route route) {
    if (route.getLengthInMeters() / 1000 > 5000) {
        Log.d(TAG, "Skip showing traffic-on-route for longer routes.");
        return;
    }

    for (Section section : route.getSections()) {
        for (Span span : section.getSpans()) {
            DynamicSpeedInfo dynamicSpeed = span.getDynamicSpeedInfo();
            Color lineColor = getTrafficColor(dynamicSpeed.calculateJamFactor());
            if (lineColor == null) {
                // We skip rendering low traffic.
                continue;
            }
            float widthInPixels = 10;
            MapPolyline trafficSpanMapPolyline = null;
            try {
                trafficSpanMapPolyline = new MapPolyline(span.getGeometry(), new MapPolyline.SolidRepresentation(
                                new MapMeasureDependentRenderSize(RenderSize.Unit.PIXELS, widthInPixels),
                                lineColor,
                                LineCap.ROUND));
            } catch (MapPolyline.Representation.InstantiationException e) {
                Log.e("MapPolyline Representation Exception:", e.error.name());
            } catch (MapMeasureDependentRenderSize.InstantiationException e) {
                Log.e("MapMeasureDependentRenderSize Exception:", e.error.name());
            }

            mapView.getMapScene().addMapPolyline(trafficSpanMapPolyline);
            mapPolylines.add(trafficSpanMapPolyline);
        }
    }
}

// Define a traffic color scheme based on the route's jam factor.
// 0 <= jamFactor < 4: No or light traffic.
// 4 <= jamFactor < 8: Moderate or slow traffic.
// 8 <= jamFactor < 10: Severe traffic.
// jamFactor = 10: No traffic, that is the road is blocked.
// Returns null in case of no or light traffic.
@Nullable
private Color getTrafficColor(Double jamFactor) {
    if (jamFactor == null || jamFactor < 4) {
        return null;
    } else if (jamFactor >= 4 && jamFactor < 8) {
        return Color.valueOf(1, 1, 0, 0.63f); // Yellow
    } else if (jamFactor >= 8 && jamFactor < 10) {
        return Color.valueOf(1, 0, 0, 0.63f); // Red
    }
    return Color.valueOf(0, 0, 0, 0.63f); // Black
}
// This renders the traffic jam factor on top of the route as multiple MapPolylines per span.
private fun showTrafficOnRoute(route: Route) {
    if (route.lengthInMeters / 1000 > 5000) {
        Log.d(TAG, "Skip showing traffic-on-route for longer routes.")
        return
    }

    for (section in route.sections) {
        for (span in section.spans) {
            val dynamicSpeed: DynamicSpeedInfo? = span.dynamicSpeedInfo
            val lineColor = getTrafficColor(dynamicSpeed?.calculateJamFactor())
                ?: // We skip rendering low traffic.
                continue
            val widthInPixels = 10f
            var trafficSpanMapPolyline: MapPolyline? = null
            try {
                trafficSpanMapPolyline = MapPolyline(
                    span.geometry, MapPolyline.SolidRepresentation(
                        MapMeasureDependentRenderSize(
                            RenderSize.Unit.PIXELS,
                            widthInPixels.toDouble()
                        ),
                        lineColor,
                        LineCap.ROUND
                    )
                )
            } catch (e: MapPolyline.Representation.InstantiationException) {
                Log.e("MapPolyline Representation Exception:", e.error.name)
            } catch (e: MapMeasureDependentRenderSize.InstantiationException) {
                Log.e("MapMeasureDependentRenderSize Exception:", e.error.name)
            }

            if (trafficSpanMapPolyline != null) {
                mapView.mapScene.addMapPolyline(trafficSpanMapPolyline)
                mapPolylines.add(trafficSpanMapPolyline)
            }
        }
    }
}

// Define a traffic color scheme based on the route's jam factor.
// 0 <= jamFactor < 4: No or light traffic.
// 4 <= jamFactor < 8: Moderate or slow traffic.
// 8 <= jamFactor < 10: Severe traffic.
// jamFactor = 10: No traffic, ie. the road is blocked.
// Returns null in case of no or light traffic.
private fun getTrafficColor(jamFactor: Double?): Color? {
    if (jamFactor == null || jamFactor < 4) {
        return null
    } else if (jamFactor >= 4 && jamFactor < 8) {
        return Color.valueOf(1f, 1f, 0f, 0.63f) // Yellow
    } else if (jamFactor >= 8 && jamFactor < 10) {
        return Color.valueOf(1f, 0f, 0f, 0.63f) // Red
    }
    return Color.valueOf(0f, 0f, 0f, 0.63f) // Black
}

Note that we skip traffic rendering for longer routes due to performance reasons - since in this example each span with traffic is rendered as a separate polyline.


Did this page help you?