flutter-maplibre-glのoffline_regionのサンプルを読み解く

# はじめに

FlutterのMapLibreのAPIを調べてもよくわからなかったので、サンプルを読み解いて理解したいと思います。

# 注意
2022/11/06時点で、flutter_maplibre_glのサンプルのOffline Regionsでダウンロードすると、アプリがクラッシュします。
[Example application is crashing when trying to download offline maps. #39](https://github.com/m0nac0/flutter-maplibre-gl/issues/39)で対応の状況が確認できるっぽい。
[Mapbox GL JSで地理院地図Vector風の地図を表示するサンプル](https://github.com/gsi-cyberjapan/gsivectortile-mapbox-gl-js)を参考に地理院の地図を使うようにしたところ、動作しました(が一部例外あり)

私はoffline_regions.dartの定義を以下のように変更して試しました。

```offline_regions.dart
final LatLngBounds hawaiiBounds = LatLngBounds(
  southwest: const LatLng(17.26672, -161.14746),
  northeast: const LatLng(23.76523, -153.74267),
);

final LatLngBounds santiagoBounds = LatLngBounds(
  southwest: const LatLng(-33.5597, -70.49102),
  northeast: const LatLng(-33.33282, -153.74267),
);

final LatLngBounds aucklandBounds = LatLngBounds(
  southwest: const LatLng(-36.87838, 174.73205),
  northeast: const LatLng(-36.82838, 174.79745),
);

final List<OfflineRegionDefinition> regionDefinitions = [
  OfflineRegionDefinition(
    bounds: hawaiiBounds,
    minZoom: 3.0,
    maxZoom: 8.0,
    mapStyleUrl: "スタイルのURL",
  ),
  OfflineRegionDefinition(
    bounds: santiagoBounds,
    minZoom: 10.0,
    maxZoom: 16.0,
    mapStyleUrl: "https://demotiles.maplibre.org/style.json",
  ),
  OfflineRegionDefinition(
    bounds: aucklandBounds,
    minZoom: 13.0,
    maxZoom: 16.0,
    mapStyleUrl: "https://demotiles.maplibre.org/style.json",
  ),
];

final List<String> regionNames = ['Hawaii', 'Santiago', 'Auckland'];
```

tokyoだけはなぜか真っ黒な画面になったので、OfflineRegionDefinitionで指定した情報に問題があるのかもしれない。
(今後明らかにしたい)

# 関連ソース

flutter-maplibre-gl-main/example/lib 以下にある2つのファイルが対象です。
- offline_regions.dart
- offline_region_map.dart

## offline_regions.dart

この画面のソースになります。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/277984/502cd7d8-541d-9984-55d8-759601fd7f3e.png" width="240px">

この画面には以下の処理があります
- 起動時にダウンロードされたリージョンの情報を読み込む。
    - 読み込みは _updateListOfRegions()メソッドで処理される。
- マップアイコンでマップ(offline_region_map.dart)に移動。
    - 移動は _goToMap()メソッドで処理される。
- ダウンロードアイコンで指定したリージョンの情報をダウンロード。その後佐削除アイコンに代わる。
    - ダウンロードは_deleteRegion()メソッドで処理される
    - 削除は _downloadRegion()メソッドで処理される

### 起動時にダウンロードされたリージョンの情報を読み込む _updateListOfRegions()メソッド

``` offline_regions.dart
  void _updateListOfRegions() async {
    List<OfflineRegion> offlineRegions = await getListOfRegions();
    List<OfflineRegionListItem> regionItems = [];
    for (var item in allRegions) {
      final offlineRegion = offlineRegions.firstWhereOrNull(
          (offlineRegion) => offlineRegion.metadata['name'] == item.name);
      if (offlineRegion != null) {
        regionItems.add(item.copyWith(downloadedId: offlineRegion.id));
      } else {
        regionItems.add(item);
      }
    }
    setState(() {
      _items.clear();
      _items.addAll(regionItems);
    });
  }
```

最初に、flutter_maplibre_glが提供するグローバルメソッドの[getListOfRegions()](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/getListOfRegions.html)メソッドを呼び出しています。
これ、APIリファレンスを見ても全く説明がなく、flutter_mapbox_glのドキュメントを見ても同じように説明なし。

動作から察するに、getListOfRegions()メソッドはアプリケーションのキャッシュからデータを読み込んでいると思われる。
読み込んだデータをOfflineRegionListItemクラスに変換して、メンバ変数_itemsに格納している。

OfflineRegionListItemクラスは以下のように定義されている。

```offline_regions.dart
class OfflineRegionListItem {
  OfflineRegionListItem({
    required this.offlineRegionDefinition,
    required this.downloadedId,
    required this.isDownloading,
    required this.name,
    required this.estimatedTiles,
  });

  final OfflineRegionDefinition offlineRegionDefinition;
  final int? downloadedId;
  final bool isDownloading;
  final String name;
  final int estimatedTiles;

  OfflineRegionListItem copyWith({
    int? downloadedId,
    bool? isDownloading,
  }) =>
      OfflineRegionListItem(
        offlineRegionDefinition: offlineRegionDefinition,
        name: name,
        estimatedTiles: estimatedTiles,
        downloadedId: downloadedId,
        isDownloading: isDownloading ?? this.isDownloading,
      );

  bool get isDownloaded => downloadedId != null;
}
```

変数 offlineRegionDefinition として定義されている [OfflineRegionDefinition](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/OfflineRegionDefinition-class.html)クラスは、[APIリファレンス](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/OfflineRegionDefinition-class.html)を確認すると以下の情報を保持している。

|変数        |型              |備考|
|-----------|----------------|--|
|bounds     |LatLngBounds    |このリージョンの範囲|
|mapStyleUrl|String          |スタイルのURL|
|maxZoom     |double         |ズームの最大|
|minZoom     |double         |ズームの最小|

bounds、maxZoom、minZoomは重要。
これらの情報を元に、マップのタイルなどのデータをアプリケーションのキャッシュに保持すると思われる。

変数downloadedIdは_downloadRegion()メソッドでダウンロードしたリージョンの情報のID。
変数isDownloadingはリージョンの情報をダウンロードしたかどうかの管理フラグ
変数nameは表示名
変数estimatedTilesは推定されるタイルの数。実際の処理には影響はないと思われる。

### 指定されたOfflineRegionListItemを元にキャッシュを行う _downloadRegion()メソッド

```offline_regions.dart
void _downloadRegion(OfflineRegionListItem item, int index) async {
    setState(() {
      _items.removeAt(index);
      _items.insert(index, item.copyWith(isDownloading: true));
    });

    try {
      final downloadingRegion = await downloadOfflineRegion(
        item.offlineRegionDefinition,
        metadata: {
          'name': regionNames[index],
        },
      );
      setState(() {
        _items.removeAt(index);
        _items.insert(
            index,
            item.copyWith(
              isDownloading: false,
              downloadedId: downloadingRegion.id,
            ));
      });
    } on Exception catch (_) {
      setState(() {
        _items.removeAt(index);
        _items.insert(
            index,
            item.copyWith(
              isDownloading: false,
              downloadedId: null,
            ));
      });
      return;
    }
  }
```

flutter_maplibre_glが提供するグローバルメソッドの[downloadOfflineRegion()](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/downloadOfflineRegion.html)メソッドを呼び出している。
このメソッドはAPIリファレンスを見ると、downloadOfflineRegion()の実装コードが掲載されている。
が、これみてもよくわかりません。
flutter_mapbox_glのドキュメントにはこのメソッドは載っていない・・・
なので、動作とコードから読み取るしかないようです。
見てみると、[OfflineRegionクラス](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/OfflineRegion-class.html)を返却します。
OfflineRegionクラスは以下のメンバ変数を持っています。

|変数        |型                     |備考|
|-----------|-----------------------|--|
|definition |OfflineRegionDefinition|引数で指定したOfflineRegionDefinitionオブジェクト(多分)|
|id         |int                    |ダウンロードしたデータのID|
|metadata   |Map<String, dynamic>   |引数で指定したメタデータ(多分)|

このメソッドでは、引数で指定したOfflineRegionDefinitionオブジェクトに基づいて、タイルなどの情報をキャッシュとしてダウンロードするようです。
そのキャッシュに紐づく情報がOfflineRegionオブジェクトとして返却されています。

### キャッシュされたデータを削除する _deleteRegion()メソッド

```offline_regions.dart
  void _deleteRegion(OfflineRegionListItem item, int index) async {
    setState(() {
      _items.removeAt(index);
      _items.insert(index, item.copyWith(isDownloading: true));
    });

    await deleteOfflineRegion(
      item.downloadedId!,
    );

    setState(() {
      _items.removeAt(index);
      _items.insert(
          index,
          item.copyWith(
            isDownloading: false,
            downloadedId: null,
          ));
    });
  }
```

このメソッドはシンプルですね。
OfflineRegionListItemオブジェクトの情報を初期化して、flutter_maplibre_glが提供するグローバルメソッドの[deleteOfflineRegion()](https://htmlpreview.github.io/?https://raw.githubusercontent.com/m0nac0/flutter-maplibre-gl/docs/doc/api/maplibre_gl/deleteOfflineRegion.html)メソッドを呼び出してキャッシュされたデータを削除しています。

## offline_region_map.dart
このファイルは、offline_regions.dartで生成された画面で選ばれたマップを表示しているだけです。
_OfflineRegionMapStateクラスのbuildメソッドでMaplibreMapを生成する際に、指定された画面から渡されたOfflineRegionListItemオブジェクトの位置やズームの設定を指定しています。

ここでのポイントは、cameraTargetBoundsに範囲を指定している点です。
cameraTargetBoundsを指定することで、キャッシュされた範囲外を表示させないようにしています。
この辺はアプリの使用や目的にもよるかと思います。

# まとめ
オフラインでmaplibreを使う場合、以下のような処理が必要になります。
- downloadOfflineRegion()メソッドでキャッシュする
- 画面起動時にgetListOfRegions()メソッドでキャッシュに関するOfflineRegionオブジェクトを取得する
- 描画はOfflineRegionオブジェクトの情報を元にMaplibreMapを生成して表示を行う
- 削除はdeleteOfflineRegion()メソッドで行う

さて、開発中のアプリで実装してみるぞ！

# 参考

- [flutter_maplibre_gl](https://github.com/m0nac0/flutter-maplibre-gl)
- [Flutter Maplibre GL API Document](https://htmlpreview.github.io/?https://github.com/m0nac0/flutter-maplibre-gl/blob/docs/doc/api/index.html)
- [地理院地図Vector（仮称）](https://maps.gsi.go.jp/vector/)
- [Mapbox GL JSで地理院地図Vector風の地図を表示するサンプル](https://github.com/gsi-cyberjapan/gsivectortile-mapbox-gl-js)