ガイド変更履歴HERE SDK API references
ガイド

GPXレコーディングアプリを作成する

ポジショニングはNavigateライセンスでのみ使用できます。

HERE Positioningを使用して、あなたの冒険を記録するデジタル日記を作成しましょう。

冒険愛好家が、ハイキングやトレッキングの旅の記録を家族や友人に見せるときのワクワク感を、誰もが知っています。このような体験は、カメラなどの従来の方法で画像としては記録できますが、たどった正確なルートをキャプチャーすることはできません。このセクションでは、HERE SDK for Android (Navigate) を使用して、大胆な冒険の旅を記録し、可視化します。

作成したアプリを出発点として、HERE SDKの多くの機能を利用してカスタマイズされた独自のアプリケーションを作成できます。さらに、HERE SDK には既製のコードが添付されています。これは GitHub でも入手できます。

ハイキング ルートは HERE マップで確認します。そのため、歩数のカウントではなく、GPS 信号の記録にフォーカスします。

それでは、HERE SDK を使用して HikingDiary アプリを構築する方法を詳しく見ていきましょう。

位置情報を取得して表示する

この GitHub リポジトリにある HERE SDK のコード スニペットを使用すると、アプリに HEREBackgroundPositioningServiceProvider を統合して、位置を特定するコードを簡単にコピー & ペーストできます。HEREBackgroundPositioningServiceProviderHEREBackgroundPositioningService をバックグラウンド サービス リスナーとして使用し、バックグラウンドでもポジショニングの更新をリッスンします。

通常、ハイキング中はデバイスをポケットにしまって、そこに入れたままにします。このため、ハイキングの追跡を続けるために、デフォルトでアプリのバックグラウンド更新を有効にしています。

HEREBackgroundPositioningService は、Android プラットフォーム ポジショニングを使用する HERE SDK によって提供される LocationEngine を利用して、GPS、ネットワーク ロケーション プロバイダー、その他の全地球衛星測位システム (GNSS) レシーバーなどのさまざまな情報ソースから高精度の位置データを提供します。

次のステップで、メイン クラス (HikingApp.java と命名) で LocationListener を継承することで、コピーした HEREBackgroundPositioningServiceProvider を統合、そのメイン クラスで HEREBackgroundPositioningServiceProvider インスタンスを作成して開始できます。

// A class to receive location events from the device.
public HEREBackgroundPositioningServiceProvider hereBackgroundPositioningServiceProvider;

// Sets listener to receive locations from HERE Positioning.
hereBackgroundPositioningServiceProvider = new HEREBackgroundPositioningServiceProvider(activity, locationListener);

...

// Receives location updates from LocationListener.
func onLocationUpdated(_ location: heresdk.Location) {
  ...
}

注:LocationListener はメイン スレッドで呼び出され、新しい Location を取得するたびに呼び出されます。

AndroidManifest.xml ファイルにサービス クラスHEREBackgroundPositioningService を忘れずに追加してください。

  <service
    android:name="com.here.hikingdiary.backgroundpositioning.HEREBackgroundPositioningService"
    android:exported="false"/>

精度パラメーターを使用して、位置更新の精度を設定できます。navigation 設定が最も高精度です。

GitHubHikingApp クラスの最終的な実装を確認できます。

必要な権限を追加する

位置追跡では、ユーザーの居場所や活動に関する秘密情報が漏れる可能性があるため、プライバシーに関する懸念が生じます。これらの懸念に対処するために、多くのモバイル プラットフォームでは、位置データにアクセスする前にユーザーの明示的な同意を取得すること、位置追跡の頻度と粒度の制御オプションを提供することをアプリに義務付けています。アプリ開発者はプライバシー規制とベスト プラクティスに従い、責任を持って位置情報データを扱うことが重要です。そのため、アプリで LocationEngine を使用する前に、アプリの AndroidManifest.xml ファイルに次のように必要な権限を追加する必要があります。

    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.INTERNET"/>

    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>

権限が追加された AndroidManifest.xml ファイルは GitHub にあります。

位置情報の精度に対応する

これで位置情報を受信できるようになりました。次は位置情報の精度を確認する必要があります。

onLocationUpdated から受け取る Location オブジェクトは、特定の時刻におけるユーザーの世界の位置を記述しており、これは単なる実際の地理座標の推定にすぎません。ユーザー自身は静止していても、GNSS 衛星は静止しておらず、宇宙空間を猛スピードで移動しているため、推定位置が移動する可能性があります。たとえば、トレッキング中に瞑想したり食事をしたりする場合、推定位置は実際の位置の周囲で変動します。このようなユースケースでは、アプリは位置を更新すべきではありません。なぜなら、家にずっといたのに何マイルも移動したようになってしまうからです。

次の図は、受信した GPS 信号の変動に関する問題を図示したものです。

図:GPS 信号の図示。

ハイキング中の人物は、ユーザーの位置 (移動中または非移動中) を示します。この人物を囲む円は精度円であり、その半径は任意の時刻 t1 での horizontalAccuracyInMeters によって定義されます。青い円は、任意の時刻 t1 での推定される位置の例であり、水平精度円の内側にいる確率は 68%、精度円の外側にいる確率は 32% です。たとえば、horizontalAccuracyInMeters が 10 メートルの場合、実際の地理座標が半径 10 メートル以内に存在する可能性は 68% です。ハイキングの人物が時刻 t1 から t2 に移動するにつれて horizontalAccuracyInMeters 値が減少し、その結果、精度円が縮小します。ただし、精度円内で真の地理座標を見つける確率は高くなります。したがって、小さい精度円は、大きい精度円よりも精度が高いと結論付けることができます。

単純な実装では、特定の精度しきい値を下回る位置情報をそのまま受け入れることができます。

これにより、距離計算は次に示すように不正確になる可能性があります。

図:距離計算に問題がある場合。

黒い線は GPS 信号に基づいて計算された距離で、青い線は実際の距離ですが、後ろにロープを伸ばして実際のルートを追跡しない限り、その距離を確定することは不可能です。

この問題を解決するにはどうすればよいでしょうか?

位置情報フィルタリング アルゴリズムを作成する

上の問題を解決するためには、移動中にのみ位置情報を受け付ける位置情報フィルタリング アルゴリズムを作成する必要があります。そのために、新しい DistanceAccuracyLocationFilter クラスを作成し、推定位置の水平精度および前回の推定位置との距離に基づいたフィルタリング方法を提供します。

これには、次の 2 つのタイプのフィルターが必要です。

  1. DistanceFilter:距離のしきい値に基づいて位置情報をフィルタリングします。
  2. AccuracyFilter:推定位置の水平精度に基づいて位置をフィルタリングし、一定レベルの精度を持つ読み取り値のみを含めます。このフィルターを作成するには、HERE SDK が提供する Location オブジェクトにある horizontalAccuracyInMeters プロパティを使用します。これは推定水平精度をメートル単位で示し、実際の地理座標が 68% の確率でこの不確実な範囲内に収まることを示します (上の最初の図を参照)。

このような距離フィルターを実装する方法を見てみましょう。まず、distanceThresholdInMeters プロパティを定義する必要があります。ここでは 15 メートルとしましょう。推定位置が最後に受け取った位置から distanceThresholdInMeters を超える距離にある場合にのみ、GPS 信号を受け入れます。以下の図は、作成するメカニズムのアイデアを図示したものです。

図:距離しきい値の図示。

それぞれの円は、さまざまな精度円を使用した 1 か所の推定位置を表します。バツ印の円は距離のしきい値を下回っているため、破棄されます。

距離フィルターに加えて、各 GPS 信号の精度も考慮する必要があります (上を参照)。このために、accuracyRadiusThresholdInMeters プロパティを定義します。ここでは 10 メートルとしましょう。したがって、accuracyRadiusThresholdInMeters よりも高い値を持つ各 GPS 信号はフィルターで除外されます。上の最初の図では、これは青色の GPS 信号がすべて受け入れられ、赤色の GPS 信号が破棄されることを意味します。

これで、2 つのフィルター メカニズムを使用してアルゴリズムを実装する準備が整いました。

public interface LocationFilterInterface {
  boolean checkIfLocationCanBeUsed(Location location);
}

public class DistanceAccuracyLocationFilter implements LocationFilterInterface {
  // These two parameters define if incoming location updates are considered to be good enough.
  // In the field, the GPS signal can be very unreliable, so we need to filter out inaccurate signals.
  private static final double ACCURACY_RADIUS_THRESHOLD_IN_METERS = 10.0;
  private static final double DISTANCE_THRESHOLD_IN_METERS = 15.0;
  private GeoCoordinates lastAcceptedGeoCoordinates;

  @Override
  public boolean checkIfLocationCanBeUsed(Location location) {
      if (isAccuracyGoodEnough(location) && isDistanceFarEnough(location)) {
          lastAcceptedGeoCoordinates = location.coordinates;
          return true;
      }
      return false;
  }

  // Checks if the accuracy of the received GPS signal is good enough.
  private boolean isAccuracyGoodEnough(Location location) {
      Double horizontalAccuracyInMeters = location.horizontalAccuracyInMeters;
      if (horizontalAccuracyInMeters == null) {
          return false;
      }

      // If the location lies within the radius of ACCURACY_RADIUS_THRESHOLD_IN_METERS then we accept it.
      if (horizontalAccuracyInMeters <= ACCURACY_RADIUS_THRESHOLD_IN_METERS) {
          return true;
      }
      return false;
  }

  // Checks if last accepted location is farther away than xx meters.
  // If it is, the new location will be accepted.
  // This way we can filter out signals that are caused by a non-moving user.
  private boolean isDistanceFarEnough(Location location) {
      if (lastAcceptedGeoCoordinates == null) {
          // We always accept the first location.
          lastAcceptedGeoCoordinates = location.coordinates;
          return true;
      }

      double distance = location.coordinates.distanceTo(lastAcceptedGeoCoordinates);
      if (distance >= DISTANCE_THRESHOLD_IN_METERS) {
          return true;
      }
      return false;
  }
}

DistanceAccuracyLocationFilter アルゴリズムは HERE SDK の GitHub リポジトリにもあります。

これで、DistanceAccuracyLocationFilter のインスタンスを作成し、受信する位置信号をフィルターで除外し、地図の位置情報を更新できます。

private LocationFilterInterface locationFilter;

...

// Filter out undesired location signals.
locationFilter = new DistanceAccuracyLocationFilter();

...

public void onLocationUpdated(@NonNull Location location) {
    if (locationFilter.checkIfLocationCanBeUsed(location)) {
      // Use the location.
    }
}

LocationFilterInterface プロトコルを採用して、独自の位置情報フィルター アルゴリズムを作成することもできます。たとえば、次のようなバイパス フィルターを実装できます。

// The DefaultLocationFilter class implements the LocationFilterInterface and
// allows every location signal to pass in order to visualize the raw GPS signals on the map.
public class DefaultLocationFilter implements LocationFilterInterface {
    @Override
    public boolean checkIfLocationCanBeUsed(Location location) {
        return true;
    }
}

LocationFilterInterface を使用してアプリをカスタマイズすることで、アプリのコア機能を変更せずに、位置情報のフィルタリングにさまざまなアルゴリズムを使用できるようになります。上のフィルタリング アルゴリズムは可能な限りシンプルに保たれており、必要に応じて改良できます。

位置情報の更新を記録する

位置情報が更新されたら、次のステップはそれを記録することです。このために、HERE SDK GitHub の GPXTrackWriter コード スニペットを使用して、GPXTrack を作成します。この GPXTrack は、GPXDocument クラスを使用して保存および読み込みができます。すべての GPX トラックは GPXDocument に含まれており、GPX ファイル形式で保存されます。したがって、一度保存すると、GPX ファイル形式に対応する他のアプリケーションと簡単に共有できます。

GPX は、経由地、トラック、ルートを含む GPS データ交換の事実上の標準データ形式です。GPX ファイルには XML 形式の地理情報が含まれており、さまざまなソフトウェア プログラムや GPS デバイスで簡単に処理できます。

HERE SDK は、さまざまな方法で GPX データを読み取り、表示、操作するツールを提供することで、GPX データの処理をサポートします。

位置情報の更新を記録するには、次のように、GPX 操作を管理する GPXManager インスタンスを作成します。

private GPXTrackWriter gpxTrackWriter = new GPXTrackWriter();
private GPXManager gpxManager;

// Create a GPXDocument file with named as myGPXDocument.
gpxManager = new GPXManager("myGPXDocument.gpx", context);

これで、onLocationUpdated() メソッド内の GPXTrack に位置情報の更新を書き込むことができるようになりました。

// Add location updates to a GPX track.
gpxTrackWriter.onLocationUpdated(location)

最後に、ハイキングの道程を保存するには、このワンライナー コードを使用して GPX トラックを GPXDocument に保存します。

// Permanently store the trip on the device.
gpxManager.saveGPXTrack(gpxTrackWriter.getTrack());

道程を読み込むには、次のように呼び出します。

GPXTrack gpxTrack = gpxManager.getGPXTrack(index);
if (gpxTrack == null) {
    return;
}

複数のハイキングの道程を 1 つの GPXDocument 内の複数のGPXTrack として記録および保存できます。

アプリを完成させる

それでは、アプリを完成させるために、移動したルートを地図に MapPolyline で表示します。MapPolyline の実装は、HERE SDK の開発者ガイドにあるマップ アイテム ガイドに従います。これは、HikingApp.java クラスでも確認できます。

ハイキング中にポリラインを拡張するために、gpxManager インスタンスから最新の座標リストを取得します。これには最新の位置更新がすでに含まれています。このリストを使用して、既存の mapPolyline インスタンスに設定する新しい geoPolyline インスタンスを作成できます。

// Update the polyline shape that shows the travelled path of the user.
mapPolyline.setGeometry(geoPolyline);

これにより、マップ ビューにすでに表示されているポリラインが即座に拡張されます。

このチュートリアルでは、アプリ開発プロセスのすべてのステップを取り上げているわけではありません。全体像を詳細に把握するには、こちらの「利用開始」のガイドを参照してください。

完全な Android プロジェクトは GitHub にあります。

以下に、完成したアプリを示します。

スクリーンショット:衛生マップ スキームで表示された HikingDiary アプリ。
スクリーンショット:屋外ラスター レイヤーで表示された HikingDiary アプリ。

HERE SDK にはさまざまなマップ スキームが用意されています。デフォルトでは、アプリではSATELLITEマップスキームが表示されます。ただし、MapScheme を変更して別のマップ スキームに切り替えることができます。利用可能なMapSchemeオプションの詳細については、HERE SDKの「APIリファレンス」を参照してください。

また、このアプリでは、メイン ビューの右上隅にあるスイッチをスライドさせることで、マップ ビューの上に屋外ラスター レイヤーの表示を有効にできます。なお、これはサードパーティのラスター レイヤーです。このラスター レイヤーは等高線とより詳細な道筋を表示しており、ハイキングに適したものです。ここでは、thunderforest.com の屋外地図を使用しています。カスタム ラスター レイヤーの詳細については、HERE SDK の開発者ガイドを参照してください。

次のステップ

複数の方法を使って、この小さな HikingDiary アプリを強化できます。以下にいくつかのアイデアを示します。

  • GeoCoordinates オブジェクトの altitude プロパティを使用して、道程の高度プロフィールを含めます。
  • より多くのマップ スキームを統合します。たとえば、HERE SDK は傾斜を濃淡で示す TERRAIN スキームを備えています。
  • 道程全体の所要時間および休憩時間のタイムスタンプを保存します。
  • 保存された GPX ファイルのエクスポート オプションを組み込んで、他のアプリケーションと共有します。

このアプリは GitHub でオープン ソース プロジェクトとして公開されています。貢献をお待ちしています。バグ修正、新機能、全体的な改善点について、お気軽にプル リクエストを送信してください。

楽しいハイキングを!


EN 日本語

HERE documentation

Find answers to your product and technical questions

Documentation

What's new

Videos

EN 日本語

HERE ドキュメント

製品や技術に関する質問の答えを見つけましょう。より多くの内容と最新の情報については、英語版をご覧ください。

ドキュメント

ダイナミックマップ

動的コンテンツ関連のAPIをアプリやサービスに活用して、ドライバーが安全・快適かつ予定どおりに目的地へ到着できるよう支援します。

地図とデータ

世界中を走行する多数のマッピング車両から得られる最新の位置情報データを活用し、精度の高い地図やカスタムレイヤーを構築できます。

最新情報

動画

(function () { const input = document.querySelector('input[data-typeahead]'); if (!input) return; // Prevent the form from submitting/navigating input.closest('form')?.addEventListener('submit', e => e.preventDefault()); input.addEventListener('input', function () { const q = this.value.trim().toLowerCase(); document.querySelectorAll('.nav-group-name').forEach(group => { let anyVisible = false; group.querySelectorAll('.nav-group-task').forEach(task => { const text = task.textContent.trim().toLowerCase(); const show = !q || text.includes(q); task.style.display = show ? '' : 'none'; if (show) anyVisible = true; }); // Hide the whole group header if nothing matches group.style.display = anyVisible || !q ? '' : 'none'; }); }); })(); (function () { function onTokenClick(event) { var link = event.target.closest('.sdk-for-ios .item .token'); if (!link) return; event.preventDefault(); console.log('token clicked', link.textContent.trim()); var item = link.closest('.item'); if (!item) return; var content = item.querySelector('.height-container'); if (!content) { console.log('no .height-container found for item', item); return; } var isHidden = window.getComputedStyle(content).display === 'none'; content.style.display = isHidden ? 'block' : 'none'; link.classList.toggle('token-open', isHidden); var href = link.getAttribute('href'); if (href) { if (history.pushState) history.pushState({}, '', href); else location.hash = href; } } function openHashTarget() { var hash = window.location.hash.slice(1); if (!hash) return; var anchor = document.querySelector('.sdk-for-ios a[name="' + hash + '"]'); if (!anchor) return; var item = anchor.closest('.item'); if (!item) return; var link = item.querySelector('.token'); var content = item.querySelector('.height-container'); if (!link || !content) return; content.style.display = 'block'; link.classList.add('token-open'); } function init() { console.log('HERE SDK accordion init'); openHashTarget(); } document.removeEventListener('click', onTokenClick); document.addEventListener('click', onTokenClick); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } window.addEventListener('hashchange', openHashTarget); window.addEventListener('pageLoad', init); })();