Xamarin と ArcGIS でオフライン対応の地図アプリを作ってみる

# はじめに 🤗 
本エントリーは [Xamarin Advent Calendar 2017 9日目](http://qiita.com/advent-calendar/2017/xamarin)のエントリーです。

モバイル開発において、インターネットの繋がらない場所でデータの登録や更新を行いたいとか、現地で登録したデータをあとでクラウドに反映したいなど、ユーザーのニーズとしてはあります。普段の業務でもそういう問い合わせがあるので、オフラインについても整理しつつ、Xamarin でもオフライン対応ができるので、今回はオフラインのアプリ開発の事例について紹介したいと思います。

#オフライン対応
オフラインとは皆さんもご存知のとおりインターネットに繋がらない環境のことです。最近はインターネットの環境も整備されているので普段の生活においてはそんなに困らないと思います。それほど、オフライン対応のモバイルアプリなんて必要性を感じないかもしれませんね。
やはりそれでもオフライン対応のニーズはあります。ユースケースにもよると思いますが、良くあるケースとしては、現地調査などでインターネットに繋がりにくい環境でモバイルアプリを使用ケースがあります。現地に行き、その場でデータを更新できたりすると便利ですよね。そのアプリ事例として、Xamarin と ArcGIS Online という Esri のクラウドを利用したオフライン対応の地図アプリについて紹介したいと思います。

#Xamarin.Forms と ArcGIS Online
地図アプリの開発は、Xamarin.Forms で開発しました。地図アプリ開発には、Xamarin にも対応している Esri が提供している地図アプリの開発キット、 [ArcGIS Runtime SDK for .NET](https://developers.arcgis.com/net/latest/) を利用しました。データの置き場として、 Esri のクラウドサービス ArcGIS Online を利用しました。

実際に使用したデータは、[ArcGIS Online](http://www.arcgis.com/home/item.html?id=e45bd820da7440c5b440cca80a20fff9#visualize)で確認できます。
<img width="1122" alt="2017-12-09_1942.png" src="https://qiita-image-store.s3.amazonaws.com/0/102251/d87dc5b4-e752-fba5-5f6d-36c64e8a8a07.png">

#使用したデータの詳細
- 背景地図：タイル パッケージ
 - 背景地図は、オフライン環境においても地図が表示できるようにローカルにデータを持たせるようにしています。データは、タイル パッケージ (xamarin.tpk)という形式で、[ArcGIS Desktop](https://desktop.arcgis.com/ja/arcmap/latest/map/working-with-arcmap/about-tile-packages.htm) を使用することで作成できます。<br/>地図データには、国土地理院の地図を利用しました。<br/>
<img width="250" src="https://qiita-image-store.s3.amazonaws.com/0/102251/b693c1e8-875f-3de2-0f7d-00d8159c126c.png"><br/>
※ Xamarin.Forms でタイル パッケージを参照した例

 
- 主題図：デモ用の不動産データ （[ArcGIS Online](http://www.arcgis.com/home/item.html?id=e45bd820da7440c5b440cca80a20fff9#visualize) で配信）
- サービスの URL（REST エンドポイント）: http://services5.arcgis.com/HzGpeRqGvs5TMkVr/arcgis/rest/services/SampleData_LatLon/FeatureServer

#作成した地図アプリ
今回作成した地図アプリは、通信が制限されている状況で地図上にプロットしてポイントデータを作成し、作成したポイントデータをオンライン環境時に ArcGIS Online 上の不動産データと同期するオフラインアプリを作成します。

**実装後のアプリ**
<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/102251/98e801c4-8ac5-8a02-9869-5f08857f5579.png">

実装した機能以下のとおりです。

- タイル パッケージ（背景地図）の表示
- 不動産データの表示
- 不動産データのダウンロード
- 不動産データの編集（ポイント追加）
- 編集結果を ArcGIS Online 上の不動産データと同期

##**タイル パッケージ（背景地図）の表示**

最初に背景地図を表示します。今回は、ネットが繋がらない環境でも背景地図を表示するため**タイル パッケージ**を使用しています。ネットにつながる環境であればクラウド上で配信されている背景地図を使用することができます。

```C#:OfflineMapPage.xaml.cs
private async void Initialize()
{
    // タイル キャッシュを作成
    TileCache tileCache = new TileCache(GetTpkPath());

    // タイル キャッシュ レイヤーの作成
    ArcGISTiledLayer tileLayer = new ArcGISTiledLayer(tileCache);

    // ベースマップクラスにタイル キャッシュ レイヤーを設定
    Basemap sfBasemap = new Basemap(tileLayer);

    // マップにタイル キャッシュのベースマップを設定
    Map myMap = new Map(sfBasemap);

    // MapView に作成したマップを設定
    myMapView.Map = myMap;

}

// タイル パッケージのパスを取得する
private string GetTpkPath()
{
    #region offlinedata
    // タイル パッケージ 
    string filename = "xamarin.tpk";
    // ディレクトリを取得
    var folder =
#if NETFX_CORE
        Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#elif __ANDROID__        System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData);
#elif __IOS__
        "/Library/";
#endif
    // Return the full path; Item ID is 3f1bbf0ec70b409a975f5c91f363fe7d
    return Path.Combine(folder, "sampleData", "EditAndSyncFeatures", filename);
    #endregion offlinedata
}
```

##**不動産データの表示**

デモ用の不動産データを表示します。不動産データは、フィーチャ サービスと呼ばれるサービスで配信されているため、サービスの URL (REST エンドポイント)を参照して地図に表示します。
http://services5.arcgis.com/HzGpeRqGvs5TMkVr/arcgis/rest/services/SampleData_LatLon/FeatureServer

```C#:OfflineMapPage.xaml.cs
private async void Initialize()
{
    :
    :
    // ジオデータベースを生成するためのタスクを作成する
    _gdbSyncTask = await GeodatabaseSyncTask.CreateAsync(FEATURELAYER_SERVICE_URL);

    // サービスから地図にフィーチャ レイヤーを追加する
    foreach (IdInfo layer in _gdbSyncTask.ServiceInfo.LayerInfos)
    {
        Uri onlineTableUri = new Uri(FEATURELAYER_SERVICE_URL + "/" + layer.Id);

        // ServiceFeatureTableを作成する
        ServiceFeatureTable onlineTable = new ServiceFeatureTable(onlineTableUri);

        await onlineTable.LoadAsync();

        // ロードが成功した場合は、マップの操作レイヤーにレイヤーを追加
        if (onlineTable.LoadStatus == Esri.ArcGISRuntime.LoadStatus.Loaded)
        {
            myMap.OperationalLayers.Add(new FeatureLayer(onlineTable));
        }
    }
}
```

##**不動産データのダウンロード**

オフライン環境においてデータの参照や書き込みを行うために Runtime コンテンツ（.geodatabase）
と呼ばれるローカル上で作成されるファイルジオデータベース（SQLiteみたいなファイルです）を使用します。そのため、最初にファイルジオデータベースを作成する必要があります。作成したファイルジオデータベースを参照して地図に表示します。そのため、フィーチャ サービスのデータをダウンロードして、ダウンロードしたデータからファイルジオデータベースを新規に作成します。

```C#:OfflineMapPage.xaml.cs
private async void StartGeodatabaseGeneration()
{
    // 同期させたいレイヤーで ジオデータベースタスク オブジェクトを作成する (GeodatabaseSyncTask)
    _gdbSyncTask = await GeodatabaseSyncTask.CreateAsync(FEATURELAYER_SERVICE_URL);

    // ジオデータベース作成のためのパラメータを取得する
    Envelope extent = myMapView.GetCurrentViewpoint(ViewpointType.BoundingGeometry).TargetGeometry as Envelope;

    // ジオデータベース作成　タスクオブジェクトのデフォルトパラメータを取得する
    GenerateGeodatabaseParameters generateParams = await _gdbSyncTask.CreateDefaultGenerateGeodatabaseParametersAsync(extent);

    // ジオデータベースの作成ジョブオブジェクトを作成する
    GenerateGeodatabaseJob generateGdbJob = _gdbSyncTask.GenerateGeodatabase(generateParams, _gdbPath);

    // ジョブ変更イベントを処理する
    generateGdbJob.JobChanged += GenerateGdbJobChanged;

    //進行状況を変更したイベントをインライン（ラムダ）関数で処理してプログレスバーを表示する
    generateGdbJob.ProgressChanged += ((object sender, EventArgs e) =>
    {
        // ジョブを取得
        GenerateGeodatabaseJob job = sender as GenerateGeodatabaseJob;

        // プログレスバーの更新
        UpdateProgressBar(job.Progress);
    });

    // ジオデータベース作成のジョブをスタートする
    generateGdbJob.Start();
}

private void GenerateGdbJobChanged(object sender, EventArgs e)
{
    //ジョブオブジェクトを取得します。 HandleGenerationStatusChangeに渡されます。
    GenerateGeodatabaseJob job = sender as GenerateGeodatabaseJob;

    // スレッド化された実装の性質上、
    // UI と対話するためにディスパッチャを使用する必要がある
    Device.BeginInvokeOnMainThread(() =>
    {
        // ジョブが終了したらプログレスバーを非表示にする
        if (job.Status == JobStatus.Succeeded || job.Status == JobStatus.Failed)
        {
            myProgressBar.IsVisible = false;
        }
        else 
        {
            myProgressBar.IsVisible = true;
        }

        // ジョブステータスの残りの部分を変更
        HandleGenerationStatusChange(job);
    });
}

private async void HandleGenerationStatusChange(GenerateGeodatabaseJob job)
{
    JobStatus status = job.Status;

    // Job が成功したら作成した ローカル ジオデータベース をマップに追加する
    if (status == JobStatus.Succeeded)
    {
        // 既存のレイヤーをクリア
        //myMapView.Map.OperationalLayers.Clear();
        myMapView.Map.OperationalLayers.RemoveAt(0);

        // 新しいローカル ジオデータベース を取得
        _resultGdb = await job.GetResultAsync();

        mGdbFeatureTable = _resultGdb.GeodatabaseFeatureTables.FirstOrDefault();

        // テーブから新しいフィーチャ レイヤを作成する
        FeatureLayer layer = new FeatureLayer(mGdbFeatureTable);

        // 新しいレイヤーをマップに追加する
        myMapView.Map.OperationalLayers.Add(layer);

        // 編集機能を有効にする
        _readyForEdits = EditState.Ready;
    }

    // ジオデータベースの作成ジョブに失敗した時
    if (status == JobStatus.Failed)
    {
        // エラーメッセージの作成
        string message = "ジオデータベースの作成に失敗";

        // エラーメッセージを表示する（存在する場合）
        if (job.Error != null)
        {
            message += ": " + job.Error.Message;
        }
        else
        {
            // エラーがなければ、ジョブからのメッセージを表示する
            foreach (JobMessage m in job.Messages)
            {
                // JobMessage からテキストを取得し、出力文字列に追加します
                message += "\n" + m.Message;
            }
        }

        // メッセージを表示する
        ShowStatusMessage(message);
    }
}
```

##**不動産データの編集（ポイント追加）**

新しいポイントを上記で作成したローカルのファイルジオデータベースに追加します。
GeoViewTapped は、地図をタップしたときのイベントを実行するための処理になり、今回は地図をタップした場所にポイントを追加しています。

```C#:OfflineMapPage.xaml.cs
private void GeoViewTapped(object sender, GeoViewInputEventArgs e)
{
    if (_readyForEdits == EditState.NotReady) { return; }

    // 新しいポイントデータを作成
    var mapClickPoint = e.Location;

    MapPoint wgs84Point = (MapPoint)GeometryEngine.Project(mapClickPoint, SpatialReferences.Wgs84);

    // 新しいポイントデータを追加する
    addFeature(wgs84Point);

    // 編集状態を更新する
    _readyForEdits = EditState.Ready;

    // 同期ボタンを有効にする
    mySyncButton.IsEnabled = true;
}

private async void addFeature(MapPoint pPoint) {
    // 新しいポイントデータを作成して mGdbFeatureTable に反映する
    var attributes = new Dictionary<string, object>();
    attributes.Add("BuildingName", "Xamarin はいいぞ！");
    attributes.Add("Age", 10);

    Feature addedFeature = mGdbFeatureTable.CreateFeature(attributes, pPoint);

    try {
        await mGdbFeatureTable.AddFeatureAsync(addedFeature);
    }
    catch (Exception ex)
    {
        DisplayAlert("ポイント追加", ex.Message, "OK");
    }
    			
}
```

##**編集結果を ArcGIS Online 上の不動産データと同期**

追加したポイントデータをクラウド上の不動産データと同期するために、ローカル上のファイルジオデータベースをクラウド上のデータと同期を行います。クラウド上のデータが最新のデータに更新されます。

```C#:OfflineMapPage.xaml.cs
private void SyncGeodatabase()
{
    // Return if not ready
    if (_readyForEdits != EditState.Ready) { return; }

    // 同期タスクのパラメータを作成する
    SyncGeodatabaseParameters parameters = new SyncGeodatabaseParameters()
    {
        GeodatabaseSyncDirection = SyncDirection.Bidirectional,
        RollbackOnFailure = false
    };

    // ジオデータベース内の各フィーチャテーブルのレイヤー ID を取得してから、同期ジョブに追加する
    foreach (GeodatabaseFeatureTable table in _resultGdb.GeodatabaseFeatureTables)
    {
        // レイヤーのIDを取得する
        long id = table.ServiceLayerId;

        // CSyncLayerOption を作成する
        SyncLayerOption option = new SyncLayerOption(id);

        // オプションを追加する
        parameters.LayerOptions.Add(option);
    }

    // ジョブを作成する
    SyncGeodatabaseJob job = _gdbSyncTask.SyncGeodatabase(parameters, _resultGdb);

    // ステータス更新
    job.JobChanged += Job_JobChanged;

    // プログレスバーの更新
    job.ProgressChanged += Job_ProgressChanged;

    // 同期を開始する
    job.Start();
}
```

**アプリの実行**
同期ボタンを押下後、同期処理が実行され、同期が完了すると完了メッセージが表示されます。
<img width="350" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/102251/24f7a324-4425-7407-b1b6-423a5ebb5624.png">

同期後のデータは、デモ用の不動産データ （[ArcGIS Online](http://www.arcgis.com/home/item.html?id=e45bd820da7440c5b440cca80a20fff9#data) で配信）で確認することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/102251/538bc1a9-ae71-8d57-29a9-ea92e3965d94.png)

#まとめ
このように Xamarin と ArcGIS を活用することで比較的に簡単にオフラインに対応した地図アプリを実現することができます。今回紹介した以外にも ArcGIS を活用すればオフラインに対応した地図アプリを実現することができます。
また、ここで紹介した ArcGIS を活用した例は、[ArcGIS for Develoeprs](https://developers.arcgis.com/) を利用すればすべて無償で開発することができます。SDK もすぐに利用することは可能です。ただし、背景地図に使用したタイル パッケージは残念ながら無償で作ることはできませんが、データのダウンロードや同期などは利用することができます。

ぜひ、興味のある方は一度やってみてください！

今回作成したソースはこちらにあります。
https://github.com/valuecreation/arcgis-xamarin-forms-samples/tree/master/OfflineMap