車両前方の地図情報
Electronic Horizonは地図をセンサーとして使用し、ドライバーの視界外にある地形情報を取り込むことにより、これから先の道路ネットワークを継続的に予測します。この機能は高度なナビゲーション、ドライバー支援、ADASに関連する機能において有用です。
注これは本機能のベータリリースであるため、いくつかのバグや予期しない動作が発生する可能性があります。関連するAPIは、廃止のプロセスを経ずに、新しいリリースに変更される可能性があります。
Electronic Horizonの主なメリット:
- 予測経路認識:車両が到達する前に、前方の道路セグメントに関する情報を早期に把握できます。最も優先される経路は確率に基づいて予測されます。
- コンテキスト地図属性:道路標識、制限速度、その他の道路属性を前方情報に沿って照会します。
- 動的な更新:前方情報の変化に応じて、必要なマップデータを自動的に読み込みます。
- ルートおよびトラッキングのサポート:あらかじめ定義されたルート (マップマッチングモード) でも、ルートなし (トラッキングモード) でも動作します。
データの種類は次のとおりです。
- Most Preferred Path (MPP、最も優先される経路) - 最も通行される可能性が高いと予測される道路です。
- Alternative side paths (代替の側道) - MPPから分岐または逸脱する可能性のある道路です。
- 制限速度 - 前方情報に沿った速度情報です。
- 道路ジオメトリー - それぞれの前方道路セグメントを表すポリラインです。
- 道路クラス - 道路の分類です。
- 道路タイプ - 道路の特性です。
- Toll points (料金所) - 今後通過する料金構造とその支払い情報です。
- 信号機 - 今後出現する交通信号に関する情報です。
- ...その他 - 設定されているデータ読み込みオプションに応じて異なります。
SegmentDataクラスの概要を確認してください。
ElectronicHorizonEngineコンポーネントは、マップデータがキャッシュ、プリフェッチ、または端末にインストールされている場合、オフラインユースケースを完全にサポートします。ハイブリッドモードで動作するため、端末にデータが存在しない場合は自動的にオンラインから取得します。
注HERE SDKはElectronic Horizon機能を提供しますが、現時点ではADASISv2プロトコルとADASISv3プロトコルの直接的なサポートは提供していません。
主な概念
| コンセプト | 説明 |
|---|---|
| 車両前方の地図情報 | 車両の前方にある道路ネットワークを継続的に更新するモデルです。 |
| 最も優先される経路 (MPP) | 過去の位置情報の更新またはアクティブなルートに基づいて、最も通行される可能性が高いルートセグメントです。 |
| ElectronicHorizonDataLoader | 前方情報に沿ったセグメントに対して必要なマップデータを自動的に読み込むユーティリティです。 |
| ElectronicHorizonUpdate | 現在の前方情報で新たに追加または変更された道路セグメントを表すデータ構造です。 |
VisualNavigatorと同様に、ElectronicHorizonEngineを位置情報で更新する必要があります。ただし、VisualNavigatorと異なり、この位置情報はマップマッチングされている必要があります。そのため、VisualNavigatorまたはNavigatorからのMapMatchedLocationを使用できます。
ElectronicHorizonEngineクラスは、MPPを通知するためにElectronicHorizonListenerを受け取ります。このリスナーは非同期でElectronicHorizonUpdateを配信しますが、初期状態では空のリストのみを含みます。
リスナーは呼び出しのたびにElectronicHorizonUpdateインスタンスを更新するため、クライアント側では1つのインスタンスのみを保持すれば十分です。任意の時点で、以下のリストを通じて車両前方の地図情報の現在の状態を表します。
electronicHorizonPaths:ElectronicHorizonOptionsと現在の車両位置に基づいて、車両前方の地図情報の経路ツリー全体を保持します。ElectronicHorizonDataLoadedStatusは、ツリーが完全に読み込まれたことを示します。次の更新により、新しい経路ツリーが生成され、部分的に読み込まれます。以降も同様に繰り返されます。addedSegments:前回の更新以降に追加されたセグメントを保持します。これらの経路に基づき、必要なマップデータをデータローダーが要求します。removedSegmentIds:前回の更新以降に削除されたセグメントIDを保持します。ElectronicHorizonEngineはtrailingDistanceInMetersに基づいて車両の後方にあるセグメントを削除します。また、側道の分岐ポイントを通過した場合にもセグメントが削除されます。
新しいデータが読み込まれた際の通知を受け取るには、ElectronicHorizonDataLoaderStatusListenerを使用します。
典型的なシーケンスフローは以下のようになります。
ElectronicHorizonListenerが最初のElectronicHorizonUpdateを提供します。- その更新を
ElectronicHorizonDataLoaderを使って読み込みます。 - 更新に新規または変更されたデータが含まれている場合、
ElectronicHorizonDataLoaderStatusListenerが通知します。 ElectronicHorizonListenerが前回のデータに基づいて更新されたElectronicHorizonUpdateを提供します。- 手順2を繰り返して、その更新を読み込みます。
なお、すべてのElectronicHorizonUpdateにElectronicHorizonDataLoadedStatusが直ちに続くとは限りません。たとえば、連続して2回のElectronicHorizonUpdateイベントを受け取った後に、2回のElectronicHorizonDataLoadedStatusイベントを受信する可能性があります。
Electronic Horizon APIを統合する
Electronic Horizon APIを統合する前に、以下の前提条件を確認してください。
- プロジェクトにHERE SDKを統合していること。
SDKNativeEngineを初期化するか、既存のインスタンスを再利用すること。- 正確なホライズン更新のために、マップマッチングされた位置情報を提供する
VisualNavigatorまたはNavigatorがあること。 - オプションとして、
RoutePrefetcherを使用してオフライン用の地図データを取得するか、MapDownloaderを使用して移動に必要な地域データをインストールします。デバイスにマップデータが存在しない場合、Electronic Horizon APIは必要なマップデータをオンラインで取得します。
それでは、初期化から更新、データの取得まで、統合を始めましょう。
ElectronicHorizonEngineインスタンスを作成して設定する
ElectronicHorizonEngineクラスを使用して前方情報モデルを開きます。ルートを使用しないトラッキングモードの場合は、アクティブなRouteまたはnullを渡すかどうかを選択できます。
この例では、乗用車モードで車両前方の地図情報を有効化しますが、他の移動モードもサポートされています。
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)
}
ElectronicHorizonEngineでは、以下の内容をElectronicHorizonOptionsを使用して定義します。
- メイン経路および側道に関する
lookAheadDistancesInMeters:リストの最初のエントリーは最も優先される経路、2番目は第1レベルの側道、3番目は第2レベルの側道、というように定義されます。各エントリーでは、どの程度前方まで経路を提供するかを定義します。 - 過去のセグメントが削除されるタイミングを定義する
trailingDistanceInMeters:HERE SDKは通過済みでtrailingDistanceInMetersを超える距離があるセグメントを削除します。
HERE SDKは、前方のセグメントを自動的にリスト管理します。前方情報が更新されるたびに、ElectronicHorizonListenerに通知されます。
マップマッチングした場所で更新する
VisualNavigatorまたはNavigatorからのMapMatchedLocationを使用して、Electronic Horizonを継続的に更新する必要があります。
electronicHorizonEngine.update(mapMatchedLocation);
electronicHorizonEngine?.update(mapMatchedLocation)
更新のたびに、現在位置と進行方向に基づいて、優先される経路が再計算されます。
前方に追加された経路に関する通知を受け取る
データを読み込む前に、前方にある道路を知る必要があります。これには、ユーザーが移動するにつれて、車両前方の地図情報の更新を非同期で通知するリスナーを作成します。そうすることにより、使用可能なセグメントIDとインデックスに関する情報が通知され、ElectronicHorizonDataLoaderによって後で実際のデータを要求できます。
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)
}
}
}
}
必ずこのリスナーをElectronicHorizionインスタンスに追加してください。
データローダーを作成して設定する
ElectronicHorizonListenerは読み込み可能な更新を提供します。これには、前方情報セグメントの詳細なマップデータを非同期に読み込むためのElectronicHorizonDataLoaderをインスタンス化する必要があります。SegmentDataLoaderOptionsを受け取り、含めるデータを定義できます。
// 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)
}
利便性のため、ElectronicHorizonDataLoaderはElectronicHorizonEngineの最も優先される経路に基づいて必要なマップデータセグメントを継続的に読み込むSegmentDataLoaderをラップします。
第2ステップとして、ElectronicHorizonDataLoaderによって提供される新たに到着したマップデータセグメントの処理が必要になる場合があります。それには、ElectronicHorizonDataLoaderStatusListenerを作成します。HERE SDKはデータローダーのステータスが更新され新しいセグメントが読み込まれたときに、このリスナーを呼び出します。
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.
}
}
}
必ずこのリスナーをElectronicHorizonDataLoaderインスタンスに追加してください。
セグメントデータにアクセスする
onElectronicHorizonDataLoaderStatusUpdated(...)を通じてElectronicHorizonDataLoaderStatusListenerがセグメントが完全に読み込まれたことを通知したら、個々のセグメントの属性を照会できます。
Map<Integer, ElectronicHorizonDataLoadedStatus>型のstatusMapパラメーターには以下の情報が含まれます。
- 整数のキーは、最も優先される経路のレベル (0) および側道 (1、2、...) を表します。
- ステータスには、そのセグメントが完全に読み込まれて使用可能かどうかの情報が含まれます。たとえば
ElectronicHorizonDataLoadedStatus.FULLY_LOADEDをチェックして確認できます。
これで、以前に要求された車両前方の地図情報の更新の一部であったセグメントにアクセスできます。アプリはelectronicHorizonDataLoader.loadData(...)の呼び出し内で、これらのセグメントを読み込むように要求しました。データローダーはelectronicHorizonDataLoader.getSegment(directedOCMSegmentId.id)を通じて実際のデータにアクセスするメソッドを提供します。
内部的には、データローダーはアプリが要求したセグメントを追跡し、提供されたElectronicHorizonUpdateインスタンスを継続的に更新します。
以下の例では、MPPが完全に読み込まれるのを待ってから、ツリー全体を繰り返し処理します。
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)
)
}
}
}
}
}
}
セグメントから地理座標を取得するには、提供されたメートル単位のオフセットと、以下のヘルパーを使用できます。
// 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
)
}
MPPの場合、segment.parentPathIndexは0になります。必要に応じて、ツリーのさらに下位へ反復処理を行い、側道に関する情報を取得することもできます。
停止してクリーンアップする
ナビゲーションが終了するかトラッキングモードが停止したときは、リスナーを削除しリソースを解放します。
electronicHorizonEngine.removeElectronicHorizonListener(listener);
electronicHorizonDataLoader.removeElectronicHorizonDataLoaderStatusListener(statusListener);
electronicHorizonEngine?.removeElectronicHorizonListener(electronicHorizonListener)
electronicHorizonDataLoader.removeElectronicHorizonDataLoaderStatusListener(
electronicHorizonDataLoaderStatusListener
)
ベストプラクティス
- ユースケースに合った先読み距離や追跡距離を使用します (例:高速道路対都市部)。
- 側道データを積極的に必要としない限り、レベル0 (最も優先される経路) セグメントのみを処理します。
- アプリがオフラインまたは低接続環境で実行される場合は、地図データをプリフェッチします。
- メモリとパフォーマンスのバランスをとるために、データローダーには合理的なキャッシュサイズを選択します。
- 不要になったらリスナーを削除しリソースを解放して、メモリリークや不要なバックグラウンド処理を回避します。
サンプルアプリを試す
GitHubの「Navigation」サンプルアプリでは、ElectronicHorizonHandlerクラスが以下の方法を示しています。
- Electronic Horizonを初期化して開始する。
- ナビゲーション中にマップマッチングした場所を使用して更新する。
- 車両がルートに沿って移動するにつれて、道路レベルのデータ (例:道路標識) を取得しログに記録する。
Javaでの完全な実装については、次を参照してください:
ElectronicHorizonHandler.java。Kotlinでの完全な実装については、次を参照してください:
ElectronicHorizonHandler.kt。
7 日前の更新










