【Esri Community Blog】
はじめに
この記事では、ArcGIS のネイティブ アプリ開発用 SDK である ArcGIS Maps SDK for .NET(以下、.NET SDK)を使用したリアルタイム データを処理する地図アプリの作成プロセスを説明します。リアルタイム アプリの例として、シェアサイクルの使用状況をマップ上でモニタリングする .NET MAUI (C#) アプリを開発します。
DynamicEntity04.mp4Play Video
リアルタイムとは
データ駆動型の意思決定では、急速に変化する現場の最新状況の情報を必要とします。リアルタイム アプリは、現在の状況を示すために頻繁に更新されるデータを使用します。このデータは通常、GPS やセンサーなどから取得され、位置や属性の更新情報を含んでいます。アプリでは、気象観測所に設置された固定センサー、車両のような移動体、犯罪や事故のような特定時点の事象など、さまざまなデータストリームからデータを受信し、処理します。
下記はリアルタイム アプリが活用される分野の一例です。
- 電力:電力消費を監視し、需要の変化や停電に迅速に対応する
- 公共安全:犯罪や事故などの緊急事態が発生した際に、そのエリアに最も早く駆けつけられる対応者を特定する
- 環境:洪水リスクを評価するために、水位計と現在の気象条件を監視する
- 交通:公共交通機関全体で車両を追跡し、利用者が遅延を予測できるようにする
リアルタイム アプリ開発用の API
.NET SDK では、フィードからのデータを処理および地図上に表示するのに本来必要となる複雑なコードの多くを、簡単に実装できるように API として提供しています。数行のコードで、ストリーム データソースに接続できます。ストリームからのオブジェクト(.NET SDK では Dynamic Entity(動的エンティティ)と呼びます)を、リアルタイム表示用に設計されたレイヤーに表示します。一度確立された接続は、接続が閉じられるまで更新され続けます。ストリーム データの読み込みと表示にはそれ以上の作業は必要ありませんが、加えてデータソースのイベントを処理することもできます。これらのイベントでは、データの更新や接続状態などをより細かく制御できます。
- DynamicEntityDataSource:ストリーム データソースに接続し、データの更新と接続ステータスの変更の通知を提供します。接続は維持され(必要に応じて自動的に再接続されるなど)、データが継続して流れるようにします。
- DynamicEntity:データソースによって記述される実世界のオブジェクトの 1 つを表します。
- DynamicEntityObservation:DynamicEntity の状態(場所と属性)のスナップショットを表します。
- DynamicEntityLayer:DynamicEntity と DynamicEntityObservation を表示し、データが受信されるとそれらを更新します。
.NET SDK を使用すると、データストリームに接続して更新データをアプリケーションに取り込み、リアルタイムに処理できます。一度確立されたデータストリームへの接続は、接続が閉じられるまで更新を配信し続けます。
データストリームに接続する
通常、DynamicEntityDataSource は、ArcGIS ストリーム サービスを使用します。ArcGIS ストリーム サービスは、ArcGIS Velocity または ArcGIS GeoEvent Server を使用して作成します。それらの製品を使用すると、さまざまなフィード形式を使用してデータソースを構成し、ArcGIS ストリーム サービスとして公開できます。サポートされているフィード形式には、AWS、Azure、HTTP ポーリングなど様々な形式が含まれます。ArcGIS Velocity または ArcGIS GeoEvent Server を使用するとノーコードでフィードに接続し、データソースを独自の形式に加工できます。その他にも空間的な解析や監視、イベント検知時のメール通知などの多くの機能が標準で備わっています。
カスタム データソースを作成する
.NET SDK では、ArcGIS Velocity または ArcGIS GeoEvent Server を使用してデータストリームを配信する方法が通常ではありますが、ArcGIS GeoEvent Server または ArcGIS Velocity で標準サポートされていない形式のデータフィードの場合や、システム構成の都合上使用することが難しい場合に、カスタム データソース(カスタム DynamicEntityDataSource)が優れたオプションになります。カスタム データソースは、ほぼすべてのフィードを DynamicEntityDataSource としてラップして、アプリで使用できるようにします。 カスタム データソースの作成は簡単ではないかもしれません。もちろん、フィードの性質と要件によって実装の複雑さは異なります。しかし、カスタム DynamicEntityDataSource を一度作成すれば、通常の DynamicEntityDataSource と同様に多くの便利な機能を使用できます。たとえば、DynamicEntityLayer を使用してマップ上に表示したり、データ更新の通知を取得したり、ローカル データ キャッシュを管理したりできます。車両などの移動する動的エンティティの場合、DynamicEntityLayer に軌跡を表示するなどの表示オプションも使用できます。
カスタム データソースを使用したリアルタイム アプリ(シェアサイクル ステーションのモニタリング)の作成
このアプリは、カスタム DynamicEntityDataSource の実装方法を示す .NET MAUI で開発されたサンプルです。このアプリは、OpenStreet 社が運営する HELLO CYCLING のシェアサイクル ステーションを表示します。ステーションの位置は固定なので、地図上では何も移動しません。しかし、自転車の空き状況などの属性は頻繁に更新されるため、それらの属性値は動的です。ステーションの色は貸出可能な自転車の数を示しています。台数が多いほど濃い色で表示されています。ステーションの属性には、貸出可能台数と駐車可能台数が格納されています。お気に入りページには、ステーション情報の最終更新時刻と、前回の更新以降の貸出可能台数の変化(+/-)が表示されます。自転車が貸出される(貸出可能な台数が減る)とステーションが赤色に点滅し、自転車が返却される(貸出可能な台数が増える)と青色に点滅します。
シェアサイクル ステーションの自転車の空き状況を表示するマルチプラットフォームの .NET MAUI アプリ
HELLO CYCLING のシェアサイクル ステーションの情報は公共交通オープンデータで公開されている 「OpenStreet(ハローサイクリング)のバイクシェア関連情報(GBFS形式 / station_information)」のデータを使用します。このデータは、JSON で情報を取得できます。アプリ側ではタイマーを使用して、公開されているデータから定期的に情報をリクエストします。次に、そのレスポンスからステーション(ポイントの位置と関連する属性)を表すクラスに逆シリアル化します。
ここからは、.NET SDK のカスタム DynamicEntityDataSource の実装方法を解説していきます。.NET MAUI (C#) でのコードを示します。ArcGIS で提供される Kotlin (Android)、Swift (iOS) 用の SDK でも、言語の違いはありますが実装方法は共通です。
カスタム DynamicEntityDataSource を作成する5つのステップ
カスタム DynamicEntityDataSource の実装を作成するには、次の基本手順に従います。
1. DynamicEntityDataSource ベースクラスから派生するカスタム データソースのクラスを作成する
2. フィードに接続する
3. データソースに関するメタデータを定義して返す
4. フィードからデータを読み取り、観測データを追加するロジックを実装する
5. データソースが切断されたときにクリーンアップする
その後、アプリでカスタム DynamicEntityDataSource を使用できるようになります。
6. データソースのインスタンスを作成する
7. 新しい DynamicEntityLayer を作成してデータソースを表示する
8. データソースのイベントを処理して更新通知を取得する
1. データソースのクラスを作成する
まず、DynamicEntityDataSource に相当する新しいクラスを作成します。このクラスは、DynamicEntityDataSource ベースクラスから継承し、いくつかのメソッドをオーバーライドする必要があります。これらのオーバーライドの詳細については後ほど説明します。今回作成するクラス(CityBikesDataSource)は、シェアサイクル ステーションで利用可能な自転車を表示するフィードを使用します。
更新を取得するタイマー(IDispatcherTimer)を使用して、指定された間隔で更新をリクエストします。また、最後に受信した観測データも保存します。これにより、更新間の貸出可能な自転車台数の変化を確認できます。これらはすべてこのアプリ固有の実装であり、フィードとユースケースによって大きく異なる可能性があります。
internal class CityBikesDataSource : DynamicEntityDataSource
{
// 指定された間隔で更新をリクエストするタイマー
private readonly IDispatcherTimer _getBikeUpdatesTimer = Application.Current.Dispatcher.CreateTimer();
// HELLO CYCLING のシェアサイクル ステーションのオープンデータのエンドポイント
// https://api-public.odpt.org/api/v4/gbfs/hellocycling/station_information.json
private readonly string _cityBikesUrl;
// シェアサイクル ステーションの以前の観測データ(台数の変化を確認するため)
private readonly Dictionary<string, Dictionary<string, object>> _previousObservations = new();
コンストラクターでは、データ取得用のエンドポイントの URL(文字列)と秒単位の更新間隔(数値)の引数を受け入れます。これらの値を適切な変数に保存し、タイマー間隔で実行する関数を定義します。PullBikeUpdates() 関数には、シェアサイクル ステーションのオープンデータのエンドポイントにリクエストを送信し、結果を逆シリアル化し、データソースに観測データを追加するロジックが含まれています。
public CityBikesDataSource(string cityBikesUrl,
int updateIntervalSeconds)
{
// タイマー間隔(URL から更新をリクエストする頻度)を保存
_getBikeUpdatesTimer.Interval = TimeSpan.FromSeconds(updateIntervalSeconds);
// シェアサイクル ステーションの URL
_cityBikesUrl = cityBikesUrl;
// タイマー間隔ごとに実行する関数を設定
_getBikeUpdatesTimer.Tick += (s, e) => _ = PullBikeUpdates();
}
2. フィードに接続する
カスタム DynamicEntityDataSource は OnConnectAsync メソッドをオーバーライドする必要があります。ここには、フィードに接続するために必要なロジックを配置します。CityBikesDataSource クラスの場合は、タイマーを開始するだけで、指定された間隔で更新が受信され始めるようになります。
protected override Task OnConnectAsync(CancellationToken cancellationToken)
{
// タイマーを開始して、定期的に更新を取得します。
_getBikesTimer.Start();
return Task.CompletedTask;
}
ここで PullBikeUpdates() を呼び出して、最初の観測データのセットを取得したくなるかもしれません。そうしないと、ユーザーは地図上に何かが表示される次のタイマー間隔まで待たなければなりません。残念ながら、データソースが接続されるまで(このオーバーライドが完了するまで)観測データを追加することはできません。これに対処するために、動的エンティティの初期セットを設定するパブリック メソッドを追加しています。この記事では詳細は割愛しますが、興味がある場合は、GitHub のコードを参照してください。
public async Task GetInitialBikeStations()
{
// データソースが接続されていない場合は終了する
if (this.ConnectionStatus != ConnectionStatus.Connected) { return; }
// <動的エンティティの観測データを追加するためのロジックをここに実装する>
}
そのコードは、接続が確立された後にこのメソッドを呼び出します(このコードについては後ほど説明します)。
3. データソースに関するメタデータを定義する
データソースのいくつかの重要な側面に関するメタデータを提供する必要があります。具体的には、観測データに含めるスキーマ(フィールド)、動的エンティティを一意に識別するフィールド名、およびそのジオメトリに使用される空間参照です。ジオメトリはスキーマ内のフィールドとして定義されていないことに注意してください。代わりに、緯度と経度の値からポイントを作成します。次に、観測データを追加し、場所と属性を指定します。そのプロセスについては記事の後半で説明します。
DynamicEntityDataSourceInfo オブジェクトを使用してデータソースのメタデータを定義します。 OnLoadAsync() メソッドのオーバーライドから返します。
protected override Task<DynamicEntityDataSourceInfo> OnLoadAsync()
{
// データソースがロードされたら、以下を定義するメタデータを作成する
// - 観測データ(シェアサイクル ステーション)のスキーマ(フィールド)
// - エンティティを一意に識別するフィールド(StationID)
// - ステーションの位置の空間参照(WGS84)
var fields = new List<Field>
{
new Field(FieldType.Text, "StationID", "", 50), //一意の ID(オリジナルの JSON のキー名は "station_id")
new Field(FieldType.Text, "StationName", "", 125), //ステーション名(オリジナルの JSON のキー名は "name")
new Field(FieldType.Text, "Address", "", 125), //ステーションの住所(オリジナルの JSON のキー名は "address")
new Field(FieldType.Float32, "Longitude", "", 0), //ステーションの経度(オリジナルの JSON のキー名は "lon")
new Field(FieldType.Float32, "Latitude", "", 0), //ステーションの緯度(オリジナルの JSON のキー名は "lat")
new Field(FieldType.Int32, "BikesAvailable", "", 0), //貸出可能な台数(オリジナルの JSON のキー名は "num_bikes_rentalable")
new Field(FieldType.Int32, "EmptySlots", "", 0), //駐車可能な台数(オリジナルの JSON のキー名は "num_bikes_parkable")
new Field(FieldType.Int32, "InventoryChange", "", 0), //貸出可能台数の変化数
new Field(FieldType.Text, "ImageUrl", "", 255)
};
var info = new DynamicEntityDataSourceInfo("StationID", fields)
{
SpatialReference = SpatialReferences.Wgs84
};
return Task.FromResult(info);
}
観測データはフィードから直接取得される場合があります(StationName、Address、BikesAvailable 属性など)。また、ユーザーが指定した値または自分で計算した値(ImageUrl や InventoryChange など)から取得される場合もあります。
4. フィードからデータを読み取る
フィードからの更新がどのように読み取られて処理されるかについての詳細は、実装によって異なる場合があります。CityBikesDataSource クラスの詳細については詳しく説明しませんが、興味がある場合は、GitHub のコードを確認してください。
まず、潜在的な例外を回避するために、更新をリクエストする前に接続ステータスをチェックします。データソースが接続状態にない場合、データソースに観測値を追加することはできません。
if (this.ConnectionStatus != ConnectionStatus.Connected) { return; }
フィードから読み取るときは、各観測データの属性のディクショナリ(Dictionary<string, object>)と位置(MapPoint)を作成します。次に、AddObservation() を呼び出して属性と場所を渡すことにより、観測データを保存します。
// --フィードから一連の更新を取得するには、ここにコードを記述します--
// 各更新を処理する
foreach (var update in bikeUpdates)
{
var attributes = new Dictionary<string, object>
{
{ "StationID", update.StationID },
{ "StationName", update.StationName },
{ "Address", update.Address },
{ "Longitude", update.Longitude },
{ "Latitude", update.Latitude },
{ "BikesAvailable", update.StationCapacity.BikesAvailable },
{ "EmptySlots", update.StationCapacity.EmptySlots },
{ "InventoryChange", 0 },
{ "ImageUrl", "" }
};
// 緯度/経度の値から位置の MapPoint を作成する
var location = new MapPoint(update.Longitude, update.Latitude, SpatialReferences.Wgs84);
// 観測データをデータソースに追加する
AddObservation(location, attributes);
}
自転車台数の更新処理で、リクエストごとに各ステーションの観測データを追加すると、ステーションに変化がない場合でも観測データを追加することになります。必要以上に多くの「更新」を追加するため、これはかなり非効率です。また、これにより通知イベントが本質的に無意味になってしまいます。その通知は、そのステーションで貸出可能な自転車台数に変化が生じたかどうかに関係なく発生します。
その解決策は、ディクショナリを使用して以前の値のセットを保存することです。その後、各ステーションの以前の貸出可能台数を更新された値と比較できます。貸出可能な自転車台数の値が変化した場合は、台数の変化を記録し、観測データを追加します。貸出可能な自転車台数の値が同じ場合は、更新をスキップします。データソースの利用者にとって、これは、ステーションの値が変更された場合にのみ通知が届くことを意味します。
// 更新で貸出可能な台数(BikesAvailable)の値が異なるかどうかを確認する
if ((int)attributes["BikesAvailable"] != (int)lastObservation["BikesAvailable"])
{
// 貸出可能な自転車台数の変化を計算する
var stationInventoryChange = (int)attributes["BikesAvailable"] –
(int)lastObservation["BikesAvailable"];
attributes["InventoryChange"] = stationInventoryChange;
totalInventoryChange += stationInventoryChange;
// 更新をデータソースに追加する
AddObservation(location, attributes);
}
5. データソースが切断されたときにクリーンアップする
データソースが切断されたときに処理するには、データソースの OnDisconnectAsync メソッドをオーバーライドする必要があります。ここに追加するコードは実装によって異なる場合があります。今回のアプリの場合は、タイマーを停止して、以前の観測データのディクショナリをクリアするだけです。
protected override Task OnDisconnectAsync()
{
_getBikeUpdatesTimer.Stop();
_previousObservations.Clear();
return Task.CompletedTask;
}
この時点で、ArcGIS ストリーム サービスを使用する場合と同じ方法で、アプリでカスタム DynamicEntityDataSource を使用できます。
6. データソースのインスタンスを作成する
DynamicEntityDataSource クラスの新しいインスタンスを作成し、必要な引数をすべて渡します。このアプリの場合は、データ取得用のエンドポイントの URL と更新間隔の秒数です。
// カスタム DynamicEntityDataSource のインスタンスを作成する
_cityBikesDataSource = new CityBikesDataSource(cityBikesUrl, UpdateIntervalSeconds);
動的エンティティの初期セットを取得する
OnConnect() オーバーライド内にロジックを追加して動的エンティティの初期セットを取得できないことを覚えているでしょうか?このメソッドが完了するまで、データセットは正式には接続されないからです。そこで、データの初期セットを取得するための別のパブリック メソッド(GetInitialBikeStations)を追加しています。これで、データソースを使用するコードからそれを呼び出すことができます。重要なのは、呼び出しを行う前にデータソースが接続されていることを確認することです。これを行うには、ConnectionStatusChanged イベントを処理し、接続後に呼び出しを行います。
// 接続が確立されたら、初期データセットをリクエストする
_cityBikesDataSource.ConnectionStatusChanged += (s, e) =>
{
if (e == ConnectionStatus.Connected)
{
_ = _cityBikesDataSource.GetInitialBikeStations();
}
};
7. データソースからの動的エンティティを表示する
.NET SDK は、DynamicEntityLayer と呼ばれる、動的エンティティを表示するように設計されたレイヤーを提供しています。このレイヤーは DynamicEntityDataSource から作成され、すべての動的エンティティと観測データの表示を管理します。データソース内で観測データが追加/削除/更新されると、レイヤーが更新されます。使用可能なレンダラー タイプ(単一、クラス分類、個別値、ディクショナリ)のいずれかを使用して、レイヤー内の動的エンティティの表示を定義できます。動的エンティティのラベルを表示して、現在の観測データに関する情報を表示することもできます。
DynamicEntityLayer をマップに追加すると、関連付けられた DynamicEntityDataSource が自動的に読み込まれ、接続されます。
シェアサイクル ステーションには、相対的な貸出可能な自転車台数を示すクラス分類レンダラーを設定しています。濃い緑色は貸出可能な自転車が多いことを意味し、灰色は貸出可能な自転車が無いことを示しています。
自転車の空き状況をクラス分類で色分け表示したマップ
8. データソースイベントの処理
データソースが観測データを追加するとき、エンティティ ID フィールドの値は観測データが適用されるエンティティを示します。このフィールドは、OnLoadAsync() オーバーライドで定義されます。動的エンティティの最初の観測データが追加されると、データソースは DynamicEntityReceived イベントを発生させます。これは、新しい観測データがデータソースに出現したことを示します。後続のすべての観測データは DynamicEntityObservationReceived イベントを発生させ、既存の動的エンティティが更新されたことを示します。
DynamicEntityReceived イベントを使用して、最初の全ステーションの貸出可能な自転車台数を計算します。
// 作成される動的エンティティをリッスンし、最初の貸出可能な自転車台数を計算する
_cityBikesDataSource.DynamicEntityReceived += (s, e) =>
{
var bikeStation = e.DynamicEntity;
var availableBikes = (int)bikeStation.Attributes["BikesAvailable"];
BikesAvailable += availableBikes;
};
DynamicEntityObservationReceived イベントを処理して、ステーションの更新に応答します。アプリでは、全体の貸出可能な自転車台数の合計を調整し、更新がある各ステーションを点滅させる関数を呼び出します。自転車が貸出される(貸出可能な台数が減る)とステーションが赤に点滅し、自転車が返却される(貸出可能な台数が増える)と青に点滅します。
// 新しい観測データをリッスンする:更新がある場合は、ステーションを点滅し、貸出可能台数の変化の値を更新する
_cityBikesDataSource.DynamicEntityObservationReceived += async (s, e) =>
{
var bikesAdded = (int)e.Observation.Attributes["InventoryChange"];
if (bikesAdded == 0) { return; }
UpdateBikeInventory(bikesAdded); // 返却された自転車よりも貸出された自転車の方が多かった場合、この値はマイナスになる
await Task.Run(() => FlashDynamicEntityObservationAsync(e.Observation.Geometry as MapPoint, bikesAdded > 0));
};
特定のエンティティの変更は、DynamicEntityChanged イベントを処理することで利用できます。これにより、エンティティに関して受信またはパージ(削除)された観測データに応答し、エンティティ自体がパージ(データソースから削除)されたときに通知を受け取ることができます。今回のアプリではこのイベントを使用していませんが、お気に入りのステーションが更新されたときに「お気に入り」画面(CollectionView)でお気に入りのカードを点滅させるような使い方ができます。
お気に入りのシェアサイクル ステーションで利用可能な自転車台数
スムーズな更新の表示
HELLO CYCLING のシェアサイクル ステーションの情報は 5 分程度の間隔で更新されているようでした。通常、観測データは、大きな更新グループとして受信されるため、大量の更新が一度に表示(点滅)されることになります。より一貫性のあるスムーズな表示を行うために、データ取得のタイマーの間隔を 5 分に設定し、それらの更新を保存し、別のタイマーを使用してアプリにゆっくりと各ステーションの更新を表示(点滅)するようにします。この方法により、最新情報の反映が損なわれる可能性がありますが、全体的なユーザーエクスペリエンスが向上します。表示の方法は GitHub のコードを確認してください。
DynamicEntity04.mp4Play Video
シェアサイクル ステーションの情報は一定の間隔で更新されます
まとめ
この記事を、.NET SDK でリアルタイム アプリを開発するプロセスの理解に役立てていただければ幸いです。DynamicEntityDataSource クラスを拡張することにより、独自のデータソースを作成するために必要なオーバーライドは少しになります。カスタム データソースを既存の API の一部として使用する利点がいくつか理解していただけたかと思います。最も大きな点は、データ管理と表示の詳細の多くが自動的に処理されることです。
様々なフィードをアプリに取り込む方法は他にもありますが、動的エンティティを使用すると、多くの作業を API にオフロードできます。カスタム データソースが完成したら、それを既存の .NET SDK の API にプラグインして、多くの追加機能を利用できます。DynamicEntityLayer でデータソースを表示し、データ更新の通知を受け取り、ローカル データ キャッシュを管理できます。車両などの移動する動的エンティティの場合、DynamicEntityLayer に軌跡を表示するなどの表示オプションも使用できます。
飛行機の移動をモニタリングしている例
ArcGIS のネイティブアプリ開発用の各 SDK(.NET/Kotlin/Swift)には、優れたアプリの構築に役立つ豊富なガイド、API リファレンス、チュートリアル、サンプルが用意されています。これらの SDK を初めて使用する場合は、ArcGIS Developers サイトにアクセスして無料の開発者アカウントを作成して、アプリの開発を始めてください。
※ 本記事は米国Esri社の「Craft your own dynamic entity data source for ArcGIS Maps SDKs for Native Apps」を抄訳・編集した記事です
関連リンク
- ArcGIS Maps SDKs for Native Apps で独自の動的エンティティ データソースを作成する(英語)
- ArcGIS Maps SDKs for Native Apps を使用した 8 つの重要なリアルタイム テクニック(英語)
- カスタム DynamicEntityDataSource のサンプル: .NET / Swift
- DynamicEntityLayer のサンプル: .NET / Swift
- 動的エンティティの操作のガイド: .NET / Kotlin / Swift
- 製品ページ