google mapをflutter webで使う

公式のパッケージである[google_maps_flutter](https://pub.dev/packages/google_maps_flutter)は2020/8/1時点で、webに対応してません。iOS、androidでmapが表示されていたとしても、webではmapが表示されません。
そこで、javascript apiのrapperである[google_maps](https://pub.dev/packages/google_maps)を使用して、map表示を試みてみました。
google_maps_flutterとgoogle_mapsとの実装の違いで混乱する方がいらっしゃるかと思ったので、代表的な機能のgoogle_mapsでの実装方法をまとめました。


<img width="474" alt="スクリーンショット 2020-08-05 0.19.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/8360d8dc-6e55-5ac8-1bec-14d661319cd3.png">

##使用環境

```dart:sample0.dart
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel beta, 1.20.0-7.3.pre, on Mac OS X 10.15.5 19F101, locale ja-JP)
 
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.3)
[✓] Xcode - develop for iOS and macOS (Xcode 11.6)
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.0)
[✓] VS Code (version 1.47.0)
[✓] Connected device (2 available)
```

##javascript apiのimport
index.htmlにjavascript apiのimportします。GCPで取得したapi keyをqueryに指定してください。

```html:index.html
<script src="https://maps.googleapis.com/maps/api/js?key=[api key]"></script>
```

##packageのimport
google mapを表示するための必要なpackageをimportします。

```dart:sample1.dart
import 'package:google_maps/google_maps.dart' as jsMap;
import 'dart:ui' as ui;
import 'dart:html';
```

##map本体
html elementをdartに内に埋め込む[HtmlElementView](https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html)を使用して、mapを表示します。


3行目の**platformViewRegistry**は静的解析で未定義のエラーが出ますが、無視してください。かなり気持ち悪いですが、ちゃんと動きますので。

HtmlElementViewで作ったhtml elementはhtmlIdで一意となるので、再表示する際はhtmlIdを異なるものにしてください。そうしないと直前の表示が残ってしまいます。

```dart:sample2.dart
Widget _map() {
  final String htmlId = "map";
  ui.platformViewRegistry.registerViewFactory(htmlId, (int viewId) {
    final mapOptions = gMap.MapOptions()
      ..zoom = 15.0
      ..center = gMap.LatLng(35.7560423, 139.7803552);

    final elem = DivElement()..id = htmlId;
    final map = gMap.GMap(elem, mapOptions);
    return elem;
  });
  return HtmlElementView(viewType: htmlId);
}
```

##mapにIconを表示する
Icon objectを作って、Maker objectに指定します。Maker objectにはmap objectも追加します。Iconは複数表示可能です。ただこのやり方では、Iconのrotationが指定できません。(flutter mobileやswiftでは可能)
rotationの指定方法は**sample5.dart**を参考ください。

```dart:sample3.dart
final _icon = gMap.Icon()
  ..scaledSize = gMap.Size(40, 40)
  ..url =
            "https://lh3.googleusercontent.com/ogw/ADGmqu_RzXtbUv4nHU9XjdbNtDNQ5XAIlOh_1jJNci48=s64-c-mo";

gMap.Marker(gMap.MarkerOptions()
  ..anchorPoint = gMap.Point(0.5, 0.5)
  ..icon = _icon
  ..position = gMap.LatLng(35.7560423, 139.7803552)
  ..map = map
  ..title = htmlId);
```

Icon imageに、pubspec.yamlでassetsに指定したimageを使いたい場合は、pubspec.yamlで指定してるpathの先頭に**assets/**を追加します。url propertyを参照してください。

```dart:sample4.dart
final _icon = gMap.Icon()
  ..scaledSize = gMap.Size(40, 40)
  ..url = "assets/[pubspec.yamlで指定しているassetsのpath]";
            
```
##Iconを回す

Google Maps JavaScript APIを使ったことがある人はご存知だと思いますが、
Iconのrotationを変更するにはsvgのpath指定でmarkerのIconを作成する必要がります。
**sample5.dart**では正方形を45°傾けています。path property に正方形を適用しています。rotaion property に回転の角度である45°を適用しています。


```dart:sample5.dart
final svgMarker = gMap.GSymbol()
  ..rotation = 45
  ..path = 'M 10 10 H 90 V 90 H 10 L 10 10'
  ..fillColor = 'blue'
  ..fillOpacity = 0.8
  ..scale = 1
  ..strokeColor = 'blue'
  ..strokeWeight = 14;

  gMap.Marker(gMap.MarkerOptions()
    ..position = map.center
    ..icon = svgMarker
    ..map = map);
            
```

##地図上のコントローラーを非表示にする
sample7.dartでは、デフォルトで表示される地図上のコントローラーを非表示にしています。

```dart:sample7.dart
  final mapOptions = gMap.MapOptions()
  ..mapTypeControl = false
  ..fullscreenControl = false
  ..streetViewControl = false
  ..zoomControl = false
  ..zoom = 15.0
  ..center = gMap.LatLng(35.7560423, 139.7803552);
            
```

**mapTypeControl** で表示非表示を制御します。<img width="208" alt="スクリーンショット 2020-08-02 11.26.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/401375f4-e164-8c8e-075f-f6e0ed929c17.png">
**fullscreenControl** で表示非表示を制御します。<img width="148" alt="スクリーンショット 2020-08-02 11.26.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/ced6c652-8d15-49d2-16f1-edcf532f8e89.png">
**streetViewControl** で表示非表示を制御します。<img width="81" alt="スクリーンショット 2020-08-02 11.27.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/51441c2b-4b98-2837-898d-5b771f1e7e48.png">
**zoomControl** で表示非表示を制御します。<img width="67" alt="スクリーンショット 2020-08-02 11.27.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/278927/b317dfe0-3d9f-b0d2-d093-2e97803055ef.png">

##polyLineを引く
google map directions apiは使わず、２点間を繋ぐpolyLineをひきました。
Polyline objectのpath propertyに繋ぎたい緯度経度を設定します。
Polyline objectのmap propertyにmap objectを設定すれば完了です。

```dart:sample8.dart
final polyline = gMap.Polyline(gMap.PolylineOptions()
  ..path = [
  gMap.LatLng(35.7560423, 139.7803552),
  gMap.LatLng(35.7713573, 139.7754953)
  ]
  ..strokeColor = "#75A9FF"
  ..strokeOpacity = 1.0
  ..strokeWeight = 3);
polyline.map = map;            
```
##Cameraのeventをハンドリングする
Streamでmapに対するeventを取得できます。
私はmapのcenterが動いた時のeventと、mapをdrag開始した時のevent、dragを終了した時のeventが必要だったので、**onCenterChanged**、**onDragstart**、**onDragend**を使いました。javascript apiで使用できるeventはおそらく使えそうです。GMapのpropertyを漁ってみてください。

```dart:sample9.dart
map.onCenterChanged.listen((event) {
  print(map.center.lat);
  print(map.center.lng);
  print(map.zoom);
});

map.onDragstart.listen((event) {});

map.onDragend.listen((event) {});

            
```
##Circleを表示する

CircleOptions objectにcenterの緯度経度と半径であるradius(m)、あとmap objectを指定すれば表示されます。複数表示可能です。

```dart:sample10.dart
final areaCircle = gMap.CircleOptions()
  ..map = map
  ..center = gMap.LatLng(35.7560423, 139.7803552)
  ..radius = 400;
gMap.Circle(areaCircle);
```

##Polygonを表示する
PolygonOptions objectに表示したい矩形の頂点を、LatLngのListとしてpathsパラメータに設定します。そして、map objectを指定すれば表示されます。

```dart:sample11.dart
final areaPolygon = gMap.PolygonOptions()
  ..strokeColor = "#000000"
  ..fillColor = '#000000'
  ..fillOpacity = 0.3
  ..paths = [
    gMap.LatLng(35.7560423, 139.7803552),
    gMap.LatLng(35.7560424, 139.7803554),
    gMap.LatLng(35.7560425, 139.7803555),
    gMap.LatLng(35.7560426, 139.7803556),
  ]
  ..map = store.map;
gMap.Polygon(areaPolygon);
```


##自分の位置を取得する

pub.devで公開されている[location](https://pub.dev/packages/location)は、webでは使えませんでした。
ググった結果、[ここ](https://stackoverflow.com/questions/60209381/get-users-location-for-flutter-web)を参考にすれば、簡単に位置情報が取得できます。

##Geolocatorを使って、緯度経度から2点間の距離を算出する
pub.devで公開されている[geolocator](https://pub.dev/packages/geolocator)が、webでは使えません。phpで該当の実装を行っている方がいたので、それをdartで書き直して使いました。

```dart:sample12.dart
import 'dart:math';

class ManualGeolocator {
  //２地点間の距離(m)を求める
  //ヒュベニの公式から求めるバージョン
  //mode 測地系 true:世界(default) false:日本
  //@return double 距離(m)
  static double distanceBetween(lat1, lon1, lat2, lon2, {mode = true}) {
    // 緯度経度をラジアンに変換
    final radLat1 = lat1 * pi / 180; // 緯度１
    final radLon1 = lon1 * pi / 180; // 経度１
    final radLat2 = lat2 * pi / 180; // 緯度２
    final radLon2 = lon2 * pi / 180; // 経度２

    // 緯度差
    final radLatDiff = radLat1 - radLat2;

    // 経度差算
    final radLonDiff = radLon1 - radLon2;

    // 平均緯度
    final radLatAve = (radLat1 + radLat2) / 2.0;

    // 測地系による値の違い
    final a = mode ? 6378137.0 : 6377397.155; // 赤道半径
    final b = mode ? 6356752.314140356 : 6356078.963; // 極半径
    final e2 = mode ? 0.00669438002301188 : 0.00667436061028297; // 第一離心率^2
    final a1e2 = mode ? 6335439.32708317 : 6334832.10663254; // 赤道上の子午線曲率半径

    final sinLat = sin(radLatAve);
    final w2 = 1.0 - e2 * (sinLat * sinLat);
    final m = a1e2 / (sqrt(w2) * w2); // 子午線曲率半径M
    final n = a / sqrt(w2); // 卯酉線曲率半径

    final t1 = m * radLatDiff;
    final t2 = n * cos(radLatAve) * radLonDiff;
    final dist = sqrt((t1 * t1) + (t2 * t2));

    return dist;
  }
}
```

##Geocoderを使って、緯度経度から住所を取得する
pub.devで公開されている[geocoder](https://pub.dev/packages/geocoder)が、webでは使えません。
なので、web APIの[Geocoding API](https://developers.google.com/maps/documentation/geocoding/overview)を使いました。

Geocoding APIのreturn値は大きなjsonです。jsonからdartのmodel classを簡単に作ってくれるwebサービスがあるので紹介しておきます。[ここ](https://medium.com/@prafullkumar77/flutter-automate-json-to-dart-entity-class-776512fa6371)

##Complete example

```dart:sample13.dart
Widget _map() {
    final String htmlId = "map";
    ui.platformViewRegistry.registerViewFactory(htmlId, (int viewId) {
      final mapOptions = gMap.MapOptions()
        ..zoom = 15.0
        ..center = gMap.LatLng(35.7560423, 139.7803552);

      final elem = DivElement()..id = htmlId;
      final map = gMap.GMap(elem, mapOptions);

      final _icon = gMap.Icon()
        ..scaledSize = gMap.Size(40, 40)
        ..url =
            "https://lh3.googleusercontent.com/ogw/ADGmqu_RzXtbUv4nHU9XjdbNtDNQ5XAIlOh_1jJNci48=s64-c-mo";

      gMap.Marker(gMap.MarkerOptions()
        ..anchorPoint = gMap.Point(0.5, 0.5)
        ..icon = _icon
        ..position = gMap.LatLng(35.7560423, 139.7803552)
        ..map = map
        ..title = htmlId);

      gMap.Marker(gMap.MarkerOptions()
        ..anchorPoint = gMap.Point(0.5, 0.5)
        ..icon = _icon
        ..position = gMap.LatLng(35.7713573, 139.7754953)
        ..map = map
        ..title = htmlId);

      final polyline = gMap.Polyline(gMap.PolylineOptions()
        ..path = [
          gMap.LatLng(35.7560423, 139.7803552),
          gMap.LatLng(35.7713573, 139.7754953)
        ]
        ..strokeColor = "#75A9FF"
        ..strokeOpacity = 1.0
        ..strokeWeight = 3);
      polyline.map = map;

      final areaCircle = gMap.CircleOptions()
        ..map = map
        ..center = gMap.LatLng(35.7560423, 139.7803552)
        ..radius = 400;
      gMap.Circle(areaCircle);

      map.onCenterChanged.listen((event) {
        print(map.center.lat);
        print(map.center.lng);
        print(map.zoom);
      });

      map.onDragstart.listen((event) {});

      map.onDragend.listen((event) {});

      final svgMarker = gMap.GSymbol()
        ..rotation = 45
        ..path = 'M 10 10 H 90 V 90 H 10 L 10 10'
        ..fillColor = 'blue'
        ..fillOpacity = 0.8
        ..scale = 1
        ..strokeColor = 'blue'
        ..strokeWeight = 14;

      gMap.Marker(gMap.MarkerOptions()
        ..position = map.center
        ..icon = svgMarker
        ..map = map);

      return elem;
    });
    return HtmlElementView(viewType: htmlId);
  }
```

##できそうでできなかったこと
####custom styleを適用すること####
GMapのStyledMapTypeOptionsやStyledMapTypeを試してみましたが、styleは変更できませんでした。
##その他、flutter webで気をつけること
~~####flutter web用のpackageをimportしているとiOS、androidのbuildでこける####
最初にimportした**dart:html**、**dart:js**がmobileでは使えないからのようです。
firestore系のpackageがweb用のpackageをimportしているのですが、それらもmobileでのbuildの妨げになります。mobileとwebのsourceは分けて管理するのがいいかもしれません。~~
#####追記#####
[Support Flutter Cross-Platform](https://medium.com/@parthdave93/support-flutter-cross-platform-b55ea3cccf41)に記載の方法を使えば、webとmobileでimportするpackageを制御できます。