mapbox_maps_flutterでtodoアプリを作りたい(未完成)

# はじめに
こんにちは、ぺんまるです！
今回は初めてのアドベントカレンダーの参加と初めてアプリをつくった(未完成)初めて尽くしのぺんぎんです。
そこで未熟ながら行き詰まったことを紹介します。

# 概要
アプリは位置情報を元にタスク管理をするといったアプリになります。

使用する地図パッケージはmapbox_maps_flutterになります。
一度、flutter_mapで開発をしていたのですが、リリースのことを視野に入れるとタイルサーバーの関係でmapboxを使用した方が良いと判断し切り替えることにしました。

アプリとしては地図上をロングタップすると、タスクを設定する画面がポップアップし、タスクを設定すると、地図上にピンが現れるようになります。

# 詰まった箇所
今回実現したかったのは
- 地図上にタスクを設定するピンを置く
- 現在地付近だけピンを出現させる  

下記画像は地図上に実際にピンと現在地を示すピンを置いた様子です。
<img width="250" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3556254/dd0f06fd-405c-1d61-2080-1c6862a4e574.png">

このピンを置く際に使ったのがPointAnnotationになります。
以下、公式docとgithubに使い方が書いています。

https://docs.mapbox.com/flutter/maps/guides/markers-and-annotations/

https://github.com/mapbox/mapbox-maps-flutter/tree/main/example/lib

# 地図上にピンを設定する
地図上にピンを設定するには公式docを読むと、PointAnnotationを使うと良さそうなので使ってみます。
使い方自体は公式docの通りなのですが、Mapウィジェット描画時にのみにピンが生成されるので、PointAnnotationを管理するPointAnnotationManagerをriverpodで管理しようと思います。(シングルトン？とか他の方法で管理してもいいかもしれませんが)

今回、自分はriverpodを導入しており、freezedとStateNotifierを使っています。
以下はfreezedで定義する際に以下のようなアノテーションをつけないと怒られます。
```dart: freezedの定義
@JsonKey(includeToJson: false, includeFromJson: false) @Default(null) PointAnnotationManager? currentAnnotationManager,
```
freezedで定義するときはPointAnnotationManagerがJSONシリアライズ/デシリアライズされないので、カスタムコンバーターを作る必要があります。
が、別にシリアライズする必要がないため、`@JsonKey(includeToJson: false, includeFromJson: false)`といったアノテーションを使って無効化しています。

次にPointAnnotationManagerを生成します。
公式docにもあるように_onMapCreate内で以下のようにインスタンスを生成します。
```dart:
await mapbox.annotations.createPointAnnotationManager();
```
これにより描画しているMapウィジェットとの紐付けができます。
ここで、先ほど定義したfreezedのプロパティへコピーし、StateNotifierで管理します。

このPointAnnotationMangerを使ってピンを生成していきます。


主にPointAnnotationManagerのdeleteAllメソッドとcreateMultiメソッドでList<PointAnnotationOptions>を渡して描画しています。
PointAnnotationOptionsは必須プロパティがgeometryと位置情報だけですが、imageを渡さないと何も見えないので渡します。
imageプロパティはUint8List?というバイナリデータを扱う値を渡さないといけないようです。
自分はピン画像をキャッシュするためにFutureProviderでassetsの画像を渡しました。

以下は現在地を表示するピンをFutureProviderでキャッシュしたコードです
```dart 
final currentLocationImageProvider = FutureProvider<Uint8List>((ref) async {
  final ByteData bytes = await rootBundle.load('assets/images/person.png');
  return bytes.buffer.asUint8List();
});
```

タスクを設定するピンも同じ方法で別のインスタンスを生成してfreezedとStateNotifierで管理しています。
もちろん、このままピンを全描画するとパフォーマンスの面でよろしくないと思ったので、現在地を中心とした座標を出し、付近半径何km以内のピンを出現させるということを行いました。

# 現在地を中心としたピンの取得

位置情報に関してはやりたいことがあったため(未実装)permission_handlerとlocationパッケージを使っています。

公式で現在地を表すピンのようなものが出せるようなのですが、緯度経度などの座標値は取得できなさそう？(以下は公式doc)

https://docs.mapbox.com/flutter/maps/guides/user-location/

また、用途的には頻繁に位置情報の取得をする必要はないので、10秒間隔で取得するようにしました。

```dart:
final currentLocationStreamProvider = StreamProvider<LatLng?>(
  (ref) async* {
    while (true) {
      final currentLocation = await GetLocation.getPosition();
      final latitude = currentLocation.latitude ?? 0;
      final longitude = currentLocation.longitude ?? 0;
      yield LatLng(latitude, longitude);
      await Future.delayed((const Duration(seconds: 10)));
    }
  },
);
```
現在地ピンはこのStreamProviderを別のFutureProviderで監視し、現在地の取得が行われた場合に現在地を表すピンを再生成しなおすような仕組みにしています。

今回は現在地を中心として表示するピンを制限したかったので、現在地から該当するピンの距離を計算する必要があります。

ググった結果、そこまで厳密な精度を求めないので、ハーバーサイン法というものを使って現在地とピンの距離を算出し、今回は半径3km以内のピンを表示させます。

## ハーバーサイン法とは
理解は浅いのですが、球面上の2点間の最短距離を計算するための数学的公式だそうです。
地球を完全な球で考えて2点間の距離を出しますが、実際は楕円体なので緯度ごとに距離が変わることを考慮していないようです。
しかし、今回は国内利用の想定やピン同士の距離が厳密な精度である必要はないためこちらの方法を採用しました。

```dart
class NearbyMarkerExtractor {
  final double currentLatitude;
  final double currentLongitude;

  NearbyMarkerExtractor({required this.currentLatitude, required this.currentLongitude});

  double _degToRad(double degree) => degree * (pi / 180);

  double haversine({required latitude, required longitude}) {
    const double r = 6378137.0;
    final double currentLat = _degToRad(currentLatitude);
    final double currentLon = _degToRad(currentLongitude);
    final double lat = _degToRad(latitude);
    final double lon = _degToRad(longitude);
    final num latitudeCal = pow(sin((lat - currentLat) / 2), 2);
    final double longitudeCal = cos(currentLat) * cos(lat) * pow(sin((lon - currentLon) / 2), 2);
    final double result = 2 * r * asin(sqrt(latitudeCal + longitudeCal));
    return result;
  }
}
```

このクラスの引数に現在地の緯度経度を渡し、インスタンスを生成します。
今回はデータ保存にSharedPreferencesを使用しているため、ピンの位置情報をキーとして保存しています。
そのため、SharedPreferencesのgetAllkeys()を使って全ての位置情報の取得を行っています。
そして、NearbyMarkerExtractorのhaversineメソッドで2点間の距離を計算しています。

# 最後に
他にも詰まった箇所があり、また完璧に実装できていない部分があったりしますが、ものすごく長くなりそうなのでここでやめます！
はじめてアプリのリリースをするので、また記事を書きたいと考えています！
年内リリースを目指して頑張ります！