Electronic Horizon
Electronic Horizon uses the map as a sensor to provide a continuous forecast of the upcoming road network by ingesting the map topography that is currently out of the driver’s sight. This functionality can be valuable for advanced navigation, driver-assistance, or ADAS-adjacent features.
NoteNote that this is a beta release of this feature, so there could be a few bugs and unexpected behaviors. Related APIs may change for new releases without a deprecation process.
Main benefits of Electronic Horizon:
- Predictive path awareness: Get early insight into upcoming road segments before a vehicle reaches them. The most preferred path is predicted based on probability.
- Contextual map attributes: Query road signs, speed limits, and road attributes along the horizon.
- Dynamic updates: Automatically load the required map data as the horizon evolves.
- Route and tracking support: Works with a predefined route (map-matched mode) or without a route (tracking mode).
Types of data include:
- Most preferred path (MPP) — the predicted roads most likely to be followed.
- Alternative side paths — possible deviations or branching roads from the MPP.
- Speed limits — speed information along the horizon.
- Road geometry — the polyline representing each upcoming road segment.
- Road classes — classification of roads.
- Road types — specific road characteristics.
- Toll points - upcoming toll structures with payment information.
- Traffic lights - information on upcoming traffic signals.
- ...and more — depending on the configured data loading options. Take a look at the
SegmentDataclass for an overview.
The ElectronicHorizonEngine component fully supports offline use cases when the map data is cached, prefetched, or installed on a device. It works in hybrid mode so that it automatically fetches data online when a device contains no data.
NoteWhile the HERE SDK provides Electronic Horizon functionality, it currently does not offer direct support for the ADASISv2 and ADASISv3 protocols.
Key Concepts
| Concept | Description |
|---|---|
| Electronic Horizon | A continuously updated model of the road network ahead of the vehicle. |
| Most preferred path (MPP) | The route segment most likely to be followed based on previous location updates or an active route. |
| ElectronicHorizonDataLoader | A utility that automatically loads required map data for segments along the horizon. |
| ElectronicHorizonUpdate | A data structure representing new or changed road segments in the current horizon. |
Similar to the VisualNavigator, you need to update the ElectronicHorizonEngine with a location. However, unlike the VisualNavigator, this location must be map-matched. Therefore, you can use the MapMatchedLocation from the VisualNavigator or Navigator.
The ElectronicHorizonEngine class accepts an ElectronicHorizonListener to notify about the MPP ahead. This listener asynchronously delivers an ElectronicHorizonUpdate, which initially contains only empty lists.
The listener updates the ElectronicHorizonUpdate instance with each call, so clients need to maintain only one instance. At any given time, it represents the current known state of the electronic horizon through the following lists:
electronicHorizonPaths: Holds the complete electronic horizon path tree based onElectronicHorizonOptionsand current vehicle position.ElectronicHorizonDataLoadedStatusindicates when the tree is fully loaded. With the next update, a new path tree is generated that will be partially loaded - and so on.addedSegments: Holds segments added since the previous update. Based on these paths, the data loader will request the desired map data.removedSegmentIds: Holds removed segment IDs since the previous update. TheElectronicHorizonEngineremoves segments based on thetrailingDistanceInMeterswhen they fall behind the vehicle. Segments are also removed when they pass the decision point for side paths.
Use the ElectronicHorizonDataLoaderStatusListener to get notified when new data is loaded.
A typical sequence flow may look like this:
ElectronicHorizonListenerprovides the firstElectronicHorizonUpdate.- Load that update via
ElectronicHorizonDataLoader. ElectronicHorizonDataLoaderStatusListenernotifies when the update contains new or changed data.ElectronicHorizonListenerprovides an updatedElectronicHorizonUpdatebased on the previous one.- Repeat step 2 — load that update, and so on.
Note that there is no guarantee that each ElectronicHorizonUpdate is immediately followed by an ElectronicHorizonDataLoadedStatus. For example, you may receive two consecutive ElectronicHorizonUpdate events and only afterwards receive two ElectronicHorizonDataLoadedStatus events.
Integrate the Electronic Horizon API
Before integrating the Electronic Horizon API, make sure you have the following prerequisites:
- Integrate the HERE SDK into your project.
- Initialize the
SDKNativeEngineor reuse an existing instance. - Ensure you already have a
VisualNavigatororNavigatorsupplying map-matched locations (for accurate horizon updates). - Optionally, use a
RoutePrefetcherto fetch map data for offline use, or use theMapDownloaderto install the region data needed for your trip. If no map data is found on the device, the Electronic Horizon API will fetch the required map data online.
Now, let's start the integration — from initialization to updates and data retrieval.
Create and configure the ElectronicHorizonEngine instance
Use the ElectronicHorizonEngine class to open the horizon model. You can choose whether to pass an active Route or null for tracking mode without following a route.
In this example, we enable the electronic horizon in car mode, but other transport modes are also supported.
List<Double> lookAheadDistancesInMeters = List.of(1000.0, 500.0, 250.0);
double trailingDistanceInMeters = 500;
ElectronicHorizonOptions electronicHorizonOptions = new ElectronicHorizonOptions(lookAheadDistancesInMeters, trailingDistanceInMeters);
TransportMode transportMode = TransportMode.CAR;
try {
electronicHorizonEngine = new ElectronicHorizonEngine(getSDKNativeEngine(), electronicHorizonOptions, transportMode, route);
} catch (InstantiationErrorException e) {
throw new RuntimeException("ElectronicHorizonEngine is not initialized: " + e.error.name());
}
val lookAheadDistancesInMeters = listOf(1000.0, 500.0, 250.0)
val trailingDistanceInMeters = 500.0
val electronicHorizonOptions =
ElectronicHorizonOptions(lookAheadDistancesInMeters,
val transportMode: TransportMode = TransportMode.CAR
try {
electronicHorizonEngine = ElectronicHorizonEngine(
this.sDKNativeEngine,
electronicHorizonOptions,
transportMode,
route
)
} catch (e: InstantiationErrorException) {
throw RuntimeException("ElectronicHorizonEngine is not initialized: " + e.error.name)
}
The ElectronicHorizonEngine uses ElectronicHorizonOptions to define:
lookAheadDistancesInMetersfor the main and side paths: The first entry of the list is for the most preferred path, the second is for the side paths of the first level, the third is for the side paths of the second level, and so on. Each entry defines how far ahead the path should be provided.- A
trailingDistanceInMetersdefining when past segments are removed: Segments will be removed by the HERE SDK once passed and the distance to them exceedstrailingDistanceInMeters.
The HERE SDK automatically maintains a list of segments ahead. Each time the horizon changes, the ElectronicHorizonListener is notified.
Update with map-matched location
You must continuously update the Electronic Horizon with a MapMatchedLocation from the VisualNavigator or Navigator.
electronicHorizonEngine.update(mapMatchedLocation);
electronicHorizonEngine?.update(mapMatchedLocation)
Each update recalculates the preferred paths ahead based on the current position and direction.
Ge notified on added paths ahead
Before you can load data, you need to know what roads are ahead. For this, create a listener to get notified asynchronously about electronic horizon updates as a user moves forward. This informs about the available segment IDs and indexes so that the actual data can be requested by the ElectronicHorizonDataLoader later on:
private ElectronicHorizonListener createElectronicHorizonListener() {
return new ElectronicHorizonListener() {
@Override
public void onElectronicHorizonUpdated(@Nullable ElectronicHorizonErrorCode errorCode,
@NonNull ElectronicHorizonUpdate electronicHorizonUpdate) {
if (errorCode != null) {
Log.e(LOG_TAG, "ElectronicHorizonUpdate error: " + errorCode.name());
return;
}
Log.d(LOG_TAG, "ElectronicHorizonUpdate received.");
// Asynchronously start to load required data for the new segments.
// Use the ElectronicHorizonDataLoaderStatusListener to get notified when new data is arriving.
if (electronicHorizonUpdate.electronicHorizon != null) {
lastRequestedElectronicHorizon = electronicHorizonUpdate.electronicHorizon;
electronicHorizonDataLoader.loadData(electronicHorizonUpdate);
}
}
};
}
private fun createElectronicHorizonListener(): ElectronicHorizonListener {
return object : ElectronicHorizonListener {
override fun onElectronicHorizonUpdated(
errorCode: ElectronicHorizonErrorCode?,
electronicHorizonUpdate: ElectronicHorizonUpdate?
) {
if (errorCode != null) {
Log.e(LOG_TAG, "ElectronicHorizonUpdate error: " + errorCode.name)
return
}
Log.d(LOG_TAG, "ElectronicHorizonUpdate received.")
// Asynchronously start to load required data for the new segments.
// Use the ElectronicHorizonDataLoaderStatusListener to get notified when new data is arriving.
if (electronicHorizonUpdate?.electronicHorizon != null) {
lastRequestedElectronicHorizon = electronicHorizonUpdate.electronicHorizon
electronicHorizonDataLoader.loadData(electronicHorizonUpdate)
}
}
}
}
Make sure to add the listener to the ElectronicHorizion instance.
Create and configure a data loader
The ElectronicHorizonListener provides updates that can then be loaded. For this, you need to instantiate an ElectronicHorizonDataLoader to load asynchronously detailed map data for horizon segments. It accepts SegmentDataLoaderOptions, which let you define which data to include.
// Many more options are available; see SegmentDataLoaderOptions in the API Reference.
SegmentDataLoaderOptions segmentDataLoaderOptions = new SegmentDataLoaderOptions();
segmentDataLoaderOptions.loadRoadSigns = true;
segmentDataLoaderOptions.loadSpeedLimits = true;
segmentDataLoaderOptions.loadRoadAttributes = true;
// The cache size defines how many road segments are cached locally. A larger cache size
// can reduce data usage but requires more storage memory in the cache.
int segmentDataCacheSize = 10;
try {
electronicHorizonDataLoader = new ElectronicHorizonDataLoader(getSDKNativeEngine(), segmentDataLoaderOptions, segmentDataCacheSize);
} catch (InstantiationErrorException e) {
throw new RuntimeException("ElectronicHorizonDataLoader is not initialized: " + e.error.name());
}
// Many more options are available, see SegmentDataLoaderOptions in the API Reference.
val segmentDataLoaderOptions = SegmentDataLoaderOptions()
segmentDataLoaderOptions.loadRoadSigns = true
segmentDataLoaderOptions.loadSpeedLimits = true
segmentDataLoaderOptions.loadRoadAttributes = true
// The cache size defines how many road segments are cached locally. A larger cache size
// can reduce data usage, but requires more storage memory in the cache.
val segmentDataCacheSize = 10
try {
electronicHorizonDataLoader = ElectronicHorizonDataLoader(
this.sDKNativeEngine,
segmentDataLoaderOptions,
segmentDataCacheSize
)
} catch (e: InstantiationErrorException) {
throw RuntimeException("ElectronicHorizonDataLoader is not initialized: " + e.error.name)
}
For convenience, the ElectronicHorizonDataLoader wraps a SegmentDataLoader that continuously loads the required map data segments based on the most preferred path(s) of the ElectronicHorizonEngine.
As a second step, you probably also want to handle newly arriving map data segments provided by the ElectronicHorizonDataLoader. For this, you can create an ElectronicHorizonDataLoaderStatusListener. The HERE SDK calls this listener when the data loader’s status updates and new segments are loaded.
private ElectronicHorizonDataLoaderStatusListener createElectronicHorizonDataLoaderStatusListener() {
return new ElectronicHorizonDataLoaderStatusListener() {
@Override
public void onElectronicHorizonDataLoaderStatusUpdated(@NonNull Map<Integer, ElectronicHorizonDataLoadedStatus> statusMap) {
Log.d(LOG_TAG, "ElectronicHorizonDataLoaderStatus updated.");
// Access the loaded segments here.
}
};
}
private fun createElectronicHorizonDataLoaderStatusListener(): ElectronicHorizonDataLoaderStatusListener {
return object : ElectronicHorizonDataLoaderStatusListener {
override fun onElectronicHorizonDataLoaderStatusUpdated(electronicHorizonDataLoaderStatuses: Map<Int, ElectronicHorizonDataLoadedStatus>) {
Log.d(LOG_TAG, "ElectronicHorizonDataLoaderStatus updated.")
// Access the loaded segments here.
}
}
}
Make sure to add the listener to the ElectronicHorizonDataLoader instance.
Accessing segment data
After the ElectronicHorizonDataLoaderStatusListener signals via onElectronicHorizonDataLoaderStatusUpdated(...) that segments are fully loaded, you can query individual segments for their attributes.
The statusMap parameter of type Map<Integer, ElectronicHorizonDataLoadedStatus> contains the following information:
- The integer key represents the level of the most preferred path (0) and side paths (1, 2, ...).
- The status contains information on whether the segment has been fully loaded and is ready to be used, for example, by checking for
ElectronicHorizonDataLoadedStatus.FULLY_LOADED.
Now, access the segments that were part of a previously requested electronic horizon update. The app requested these segments for loading in the call to electronicHorizonDataLoader.loadData(...). The data loader provides a method the get access to the actual data via electronicHorizonDataLoader.getSegment(directedOCMSegmentId.id).
Internally, the data loader tracks which segments the app requested and continuously updates the provided ElectronicHorizonUpdate instance.
The following example waits until the MPP is fully loaded and then iterates over the entire tree:
for (Map.Entry<Integer, ElectronicHorizonDataLoadedStatus> entry : statusMap.entrySet()) {
ElectronicHorizonDataLoadedStatus status = entry.getValue();
// The integer key represents the level of the most preferred path (0) and side paths (1, 2, ...).
int level = entry.getKey();
// This example shows only how to look at the fully loaded segments of the most preferred path (level 0).
if (level == 0 && status == ElectronicHorizonDataLoadedStatus.FULLY_LOADED) {
// Now, level 0 segments have been fully loaded and you can access their data.
// The electronicHorizonPaths list contains segments from all levels, so you need to filter for level 0 below.
List<ElectronicHorizonPath> electronicHorizonPaths = lastRequestedElectronicHorizon.paths;
for (ElectronicHorizonPath electronicHorizonPath : electronicHorizonPaths) {
List<ElectronicHorizonPathSegment> electronicHorizonPathSegment = electronicHorizonPath.segments;
for (ElectronicHorizonPathSegment segment : electronicHorizonPathSegment) {
// For any segment you can check the parentPathIndex to determine
// if it is part of the most preferred path (MPP) or a side path.
if (segment.parentPathIndex != 0) {
// Skip side path segments as we only want to log MPP segment data in this example.
// And this example only logs fully loaded segments.
continue;
}
DirectedOCMSegmentId directedOCMSegmentId = segment.segmentId.ocmSegmentId;
if (directedOCMSegmentId == null) {
continue;
}
// Retrieving segment data from the loader is executed synchrounous. However, since the data has been
// already loaded, this is a fast operation.
ElectronicHorizonDataLoaderResult result = electronicHorizonDataLoader.getSegment(directedOCMSegmentId.id);
if (result.errorCode == null) {
// When errorCode is null, segmentData is guaranteed to be non-null.
SegmentData segmentData = result.segmentData;
assert segmentData != null;
// Access the data that was requested to be loaded in SegmentDataLoaderOptions.
// For this example, we just log road signs.
List<RoadSign> roadSigns = segmentData.getRoadSigns();
if (roadSigns == null || roadSigns.isEmpty()) {
continue;
}
for (RoadSign roadSign : roadSigns) {
GeoCoordinates roadSignCoordinates = getGeoCoordinatesFromOffsetInMeters(segmentData.getPolyline(), roadSign.offsetInMeters);
Log.d(LOG_TAG, "RoadSign: type = "
+ roadSign.roadSignType.name()
+ ", offsetInMeters = " + roadSign.offsetInMeters
+ ", lat/lon: " + roadSignCoordinates.latitude + "/" + roadSignCoordinates.longitude
+ ", segmentId = " + directedOCMSegmentId.id.localId);
}
}
}
}
}
}
for (entry in electronicHorizonDataLoaderStatuses.entries) {
val status: ElectronicHorizonDataLoadedStatus = entry.value
// The integer key represents the level of the most preferred path (0) and side paths (1, 2, ...).
val level: Int = entry.key
// This example shows only how to look at the fully loaded segments of the most preferred path (level 0).
if (level == 0 && status == ElectronicHorizonDataLoadedStatus.FULLY_LOADED) {
// Now, level 0 segments have been fully loaded and you can access their data.
// The electronicHorizonPaths list contains segments from all levels, so you need to filter for level 0 below.
val electronicHorizonPaths = lastUpdate.electronicHorizon.paths
for (electronicHorizonPath in electronicHorizonPaths) {
val electronicHorizonPathSegments = electronicHorizonPath.segments
for (segment in electronicHorizonPathSegments) {
// For any segment you can check the parentPathIndex to determine
// if it is part of the most preferred path (MPP) or a side path.
if (segment.parentPathIndex != 0) {
// Skip side path segments as we only want to log MPP segment data in this example.
// And we only want to log fully loaded segments.
continue
}
val directedOCMSegmentId: DirectedOCMSegmentId = segment.segmentId.ocmSegmentId ?: continue
// Retrieving segment data from the loader is executed synchronous. However, since the data has been
// already loaded, this is a fast operation.
val result: ElectronicHorizonDataLoaderResult =
electronicHorizonDataLoader.getSegment(directedOCMSegmentId.id)
if (result.errorCode == null) {
// When errorCode is null, segmentData is guaranteed to be non-null.
val segmentData: SegmentData = checkNotNull(result.segmentData)
// Access the data that was requested to be loaded in SegmentDataLoaderOptions.
// For this example, we just log road signs.
val roadSigns: List<RoadSign>? = segmentData.roadSigns
if (roadSigns == null || roadSigns.isEmpty()) {
continue
}
for (roadSign in roadSigns) {
val roadSignCoordinates: GeoCoordinates =
getGeoCoordinatesFromOffsetInMeters(
segmentData.polyline,
roadSign.offsetInMeters.toDouble()
)
Log.d(
LOG_TAG, ("RoadSign: type = "
+ roadSign.roadSignType.name
+ ", offsetInMeters = " + roadSign.offsetInMeters
+ ", lat/lon: " + roadSignCoordinates.latitude + "/" + roadSignCoordinates.longitude
+ ", segmentId = " + directedOCMSegmentId.id.localId)
)
}
}
}
}
}
}
To fetch the geographic coordinates from a segment, you can use the provided offset in meters and the helper below:
// Convert an offset in meters along a GeoPolyline to GeoCoordinates using the HERE SDK's coordinatesAtOffsetInMeters.
private GeoCoordinates getGeoCoordinatesFromOffsetInMeters(GeoPolyline geoPolyline, int offsetInMeters) {
return geoPolyline.coordinatesAtOffsetInMeters(offsetInMeters, GeoPolylineDirection.FROM_BEGINNING);
}
// Convert an offset in meters along a GeoPolyline to GeoCoordinates using the HERE SDK's coordinatesAtOffsetInMeters.
private fun getGeoCoordinatesFromOffsetInMeters(
geoPolyline: GeoPolyline,
offsetInMeters: Double
): GeoCoordinates {
return geoPolyline.coordinatesAtOffsetInMeters(
offsetInMeters,
GeoPolylineDirection.FROM_BEGINNING
)
}
Note that segment.parentPathIndex is 0 when it's the MPP. Optionally, you can iterate further down the tree to retrieve information for side paths.
Stop and clean up
When navigation ends or tracking mode stops, remove listeners and release resources.
electronicHorizonEngine.removeElectronicHorizonListener(listener);
electronicHorizonDataLoader.removeElectronicHorizonDataLoaderStatusListener(statusListener);
electronicHorizonEngine?.removeElectronicHorizonListener(electronicHorizonListener)
electronicHorizonDataLoader.removeElectronicHorizonDataLoaderStatusListener(
electronicHorizonDataLoaderStatusListener
)
Best practices
- Use look-ahead distances and trailing distances that match your use case (e.g., highway vs. urban).
- Process only level 0 (most preferred path) segments unless you actively want side-path data.
- Prefetch map data ahead of time if your app will run offline or in low-connectivity scenarios.
- Choose a reasonable cache size for the data loader to balance memory and performance.
- Remove listeners and free resources when no longer needed to avoid memory leaks or unnecessary background work.
Try the example app
In the "Navigation" example app on GitHub, the ElectronicHorizonHandler class demonstrates how to:
- Initialize and start the Electronic Horizon.
- Update it with map-matched locations during navigation.
- Retrieve and log road-level data (for example, road signs) as the vehicle moves along a route.
For a full implementation in Java, see
ElectronicHorizonHandler.java. For a full implementation in Kotlin, see
ElectronicHorizonHandler.kt.
Updated yesterday