【Flutter】flutter_mapで出来ることって？flutter_mapざっくり解説。

[DeNA 24新卒 Advent Calendar 2023 ](https://qiita.com/advent-calendar/2023/dena-24-newgrad)の 15日目の記事です。

この記事では、Flutterのマップパッケージである、[flutter_map](https://pub.dev/packages/flutter_map)について、さらっと紹介します。

https://pub.dev/packages/flutter_map

flutter_mapは高頻度でアップデートが行われ、使い方などもバージョンによって結構変わってしまいます。
この記事ではflutter_mapのバージョンは6.1.0ですので、「あれ？コード通り実装したのに、そんな関数ないって言われる！」みたいにならないよう、ご注意ください！

[flutter_map Docs](https://docs.fleaflet.dev/)

https://docs.fleaflet.dev/

## 目次
  - [環境](#環境)
  - [事前準備](#事前準備)
      - [flutter_mapのインストール](#flutter_mapのインストール)
      - [latlong2のインストール](#latlong2のインストール)
  - [Mapの表示(FlutterMapウィジェットについて)](#mapの表示fluttermapウィジェットについて)
  - [基本的な機能](#基本的な機能)
    - [マップの拡大・縮小,回転を設定する](#マップの拡大縮小回転を設定する)
    - [マップ上に回転しないピンを立てる](#マップ上に回転しないピンを立てる)
    - [ピンの押下時に処理を行う](#ピンの押下時に処理を行う)
    - [マップに線を引く](#マップに線を引く)
    - [マップを操作する](#マップを操作する)
    - [マップ操作をアニメーション付きで行う](#マップ操作をアニメーション付きで行う)
    - [現在地を表示する](#現在地を表示する)
- [今回作成したコード](#今回作成したコード)
- [おわりに](#おわりに)

## 環境
* Flutter 3.16.4
* Dart 3.2.3
* flutter_map 6.1.0

## 事前準備
一応flutter環境に異常がないかチェックしておきましょう。
```
flutter doctor
```
``• No issues found!``
が表示されれば問題ないです。

自分の場合、iPhoneのOSをiOS17.1.2に上げた（上げてしまった）ところ、Xcodeのバージョンを15.0以上に上げざるを得なくなり、Xcodeのパスを変更する必要がありました。

Xcodeのバージョン変更は以下のコマンドで出来ます。
/Application/Xcode-15.0.0.appの部分は、適宜自分の環境に合わせて変更してください。
```
sudo xcode-select --switch /Application/Xcode-15.0.0.app
```

### flutter_mapのインストール
flutter_mapのドキュメント準拠の方法で行います。
```
flutter pub add flutter_map
```
このコマンドにより、pubspec.yamlのdependenciesにflutter_mapが追加されます。

あとはコードの最上部に以下を記述してインポートするだけ。
```
import 'package:flutter_map/flutter_map.dart';
```
簡単！

### latlong2のインストール
もう一つ、[latlong2](https://pub.dev/packages/latlong2)というパッケージもインストールしましょう。
```
flutter pub add latlong2
```
ざっくり言えば、これは緯度経度をセットにした型が使用できるようになるパッケージです。
flutter_mapでは頻繁に使用することになります。
同じくあとは以下をインポートするだけ！
```
import 'package:latlong2/latlong.dart';
```

## Mapの表示(FlutterMapウィジェットについて)
さて、セットアップがこれで完了しました。
まずはFlutterMapウィジェットでマップを表示しましょう。
今回はOpenStreetMapを使用しています。
``` map.dart
Scaffold(
   appBar: AppBar(
      backgroundColor: Colors.white,
      title: Text(widget.title),
   ),
   body: FlutterMap(
      options: const MapOptions(
         // 名古屋駅の緯度経度です。
         initialCenter: LatLng(35.170915, 136.881537),
         initialZoom: 10.0,
      ),
      children: [
         TileLayer(
            urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
         ),
      ],
   ),
);
```

ScaffoldのbodyにMapを表示するWidgetを指定しています。
TileLayerのurlTemplateを変更することで、MapBoxやBingマップなどのマップ背景に変更できます。

また、FlutterMapウィジェットには引数として以下の4つが指定出来ます。
* **required MapOptions options**
マップの設定変数。マップの初期位置、マップの初期ズーム倍率であったり、マップをタップした時に実行する関数の設定などの、特定の場合に実行する関数の設定も行える。

* **required List\<Widget> children**
マップ上にレイヤーとして表示するWidgetを複数指定できる。例えば、背景地図の設定や、現在地のマーカー、マップ上に表示するピン、ルート案内用の線などなど。
1つ注意点として、レイヤーの順序は、引数に指定した順に重ねられるため、最初に指定したWidgetはレイヤーが一番下に、最後に指定したWidgetはレイヤーが一番上になる。

* **MapController mapController**
マップの動きを制御する際に用いるコントローラー。引数として設定したMapController型の変数を使って、mapController.animateTo()で、マップの中心を動かしたり出来る。

* **List\<Widget> nonRotatedChildren**
**この引数は非推奨です。次のバージョンで削除されるようです。**
~~マップ上にレイヤーとして表示するWidgetを複数指定できる。List\<Widget> childrenとの違いとして、マップを回転させても、このレイヤーのWidgetは回転せずに固定されている。~~

## 基本的な機能
ここからが本題ですね。
基本的には、FlutterMapウィジェットの引数にオプションやWidgetを指定したり、mapControllerを操作して色々やります。
なので、マップアプリによくあるような機能の大半は、FlutterMapウィジェットを理解することで実装出来ます。

### マップの拡大・縮小,回転を設定する
maxZoomからminZoomの間で拡大・縮小が行われるようになります。
```map.dart
options: const MapOptions(
   // 初期ズーム設定
   initialZoom: 10.0,
   // 拡大設定
   maxZoom: 12.0
   // 縮小設定
   minZoom: 8.0,
   // 初期回転角度。以下の場合、180度回転して表示される
   initialRotation: 180.0,
),
```

### マップ上に回転しないピンを立てる
マップ上の設定した位置に、回転しないピンを立てます。
```map.dart
children: [
   TileLayer(
      urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
   ),
   const MarkerLayer(
      markers: [
         Marker(
            width: 30.0,
            height: 30.0,
            // ピンの位置を設定
            point: LatLng(35.170915, 136.881537), 
            child: Icon(
               Icons.location_on,
               color: Colors.red,
               // ここでピンのサイズを調整
               size: 50,
            ),
            // マップを回転させた時にピンも回転するのが rotate: false,
            // マップを回転させた時にピンは常に同じ向きなのが rotate: true,
            rotate: true,
         ),
      ],
   ),
],
```

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/dd88ec2d-b2d5-98f2-e96e-4005bcfb9567.png" width="300">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/6fe085c4-7200-bd2b-1944-ca9a6c137247.png" width="300">



注意点として、
* マップを回転させた時にピンも回転するのが rotate: false,
* マップを回転させた時にピンは常に同じ向きなのが rotate: true,
です。

次はマップ上のタップした場所に、ピンを立ててみましょう。
```map.dart
class _MyHomePageState extends State<MyHomePage> {
  // マーカー用のList
  List<Marker> addMarkers = [];

  // ピンを追加する関数
  void _addMarker(LatLng latlng) {
    // setStateを行うことで、マップを更新している
    setState(() {
      addMarkers.add(
        Marker(
          width: 30.0,
          height: 30.0,
          point: latlng,
          child: const Icon(
            Icons.location_on,
            color: Colors.blue,
            size: 50,
          ),
          rotate: true,
        ),
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: Text(widget.title),
      ),
      body: FlutterMap(
        options: MapOptions(
          initialCenter: const LatLng(35.170915, 136.881537),
          initialZoom: 10.0,
          
          // マップをタップした際の処理
          // pointはタップした位置がLatLng型で受け取れるので、引数にpointを渡す
          onTap: (tapPosition, point) {
            _addMarker(point);
          },
        ),
        children: [
          TileLayer(
            urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          ),
          //　MarkerLayerに追加したピンを指定する
          MarkerLayer(markers: addMarkers),
        ],
      ),
    );
  }
}
```
![RPReplay_Final1702560031.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/b09ff306-2290-78f4-a4c8-c528df85faf8.gif)


これで、マップ上のタップした位置に、動的にピンを追加することができます。

### ピンの押下時に処理を行う
では、追加したピンをタップしたら、何かしらの処理を実行させましょう。
よくある例としては、ピンをタップしたらモーダルを表示させたり、ページを遷移したり、などがあります。
今回は簡単に、showDialogでアラートボックスを表示させてみましょう。

```map.dart

void _addMarker(LatLng latlng) {
  setState(() {
    addMarkers.add(
      Marker(
        width: 30.0,
        height: 30.0,
        point: latlng,
        // GestureDetectorで、タップ時の処理を追加
        child: GestureDetector(
          onTap: () {
            // アラート表示処理
            _showAlert(latlng);
          },
          child: const Icon(
            Icons.location_on,
            color: Colors.blue,
            size: 50,
          ),
        ),
      ),
    );
  });
}

void _showAlert(LatLng latlng) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('ピンの位置'),
      content: Text('緯度: ${latlng.latitude}, 経度: ${latlng.longitude}'),
      actions: <Widget>[
        TextButton(
          child: const Text('閉じる'),
          onPressed: () => Navigator.of(context).pop(),
        ),
      ],
    ),
  );
}
```

![RPReplay_Final1702561651.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/e03ed580-ea7d-2812-9607-4922c9d056a7.gif)


こんな感じで、ピンの情報を取得してアラートに表示することも可能です。

### マップに線を引く
次に、指定したマップ上の位置に、線を引いてみましょう。

```map.dart
MarkerLayer(markers: addMarkers),
// 線を表示するレイヤー
PolylineLayer(
   polylines: [
      Polyline(
         points: [
            const LatLng(35.1, 136.85),
            const LatLng(35.2, 136.80),
            const LatLng(35.3, 136.89),
            const LatLng(35.4, 136.82),
         ],
         strokeWidth: 16.0,
         color: Colors.black,
      ),
   ],
),
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/21301fab-8583-4f1c-d8d6-93fcb06da504.png" width="300">


これで4地点に黒い線が引かれました。
ピンと同様、動的に線を引くことも可能です。
なので、Map上のタップした箇所を結ぶ線を引いたり、最短経路を表示したり、なんてことにも使えちゃいます。
実際、僕の作成したマップアプリでは、サーバーで現在地と指定したピンの最短経路を計算して、それをマップ上に表示する、という機能も実装しました。

### マップを操作する
追加したピンをタップした時、マップの中心を動かしてみましょう。
```map.dart
class _MyHomePageState extends State<MyHomePage> {
  List<Marker> addMarkers = [];
  
  // MapControllerのインスタンス作成
  final MapController mapController = MapController();

  void _addMarker(LatLng latlng) {
    setState(() {
      addMarkers.add(
        Marker(
          width: 30.0,
          height: 30.0,
          point: latlng,
          child: GestureDetector(
            onTap: () {
              // mapControllerを用いて処理を実行する
              // mapController.camera.zoomは、現在のズーム倍率を取得する
              mapController.move(latlng, mapController.camera.zoom);
            },
            child: const Icon(
              Icons.location_on,
              color: Colors.blue,
              size: 50,
            ),
          ),
        ),
      );
    });
  }

// ... 省略

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: Text(widget.title),
      ),
      body: FlutterMap(
        // mapControllerをFlutterMapに指定
        mapController: mapController,
        options: MapOptions(
        // ... 省略
```

![RPReplay_Final1702568678.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/fff42657-b32d-8513-e4f3-9b6a2ec2efa2.gif)


マップの中心の移動や、ズームの変更、マップの回転などもできます。
また、指定した2つの位置が画面上に映るようにズーム倍率を変更することも出来ます。

```map.dart
mapController.fitCamera(
   // 地点1と地点2を画面内に収める
   CameraFit.bounds(
      bounds: LatLngBounds(
         const LatLng(地点1),
         const LatLng(地点2),
      ),
   ),
);

```

### マップ操作をアニメーション付きで行う
さきほどのマップの中心を移動させる処理は、中心へと瞬間的に移動するような動作をしていました。
これにアニメーションをつけ、徐々にマップの中心が指定した位置に移動するような実装も行えます。
そのために、[flutter_map_animations](https://pub.dev/packages/flutter_map_animations)という、flutter_mapのアニメーション用パッケージを使用します。

https://pub.dev/packages/flutter_map_animations

このパッケージを使用すると、マップ操作にアニメーションがつき、滑らかにマップが遷移、回転、ズームするようになります。

#### インストール方法
```
flutter pub add flutter_map_animations
```
上記を実行し、
```
import 'package:flutter_map_animations/flutter_map_animations.dart';
```
をプロジェクト最上部に追記するだけです。

そうすると、MapControllerとは別に、AnimatedMapControllerが使用できるようになります。
これを用いて、さまざまなアニメーションが使用できます。
例として、追加したピンをタップした時にマップの中心を動かす処理に、アニメーションをつけてみます。
```map.dart
import 'package:flutter_map_animations/flutter_map_animations.dart';

// ... 省略

// with TickerProviderStateMixinを追加する
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  List<Marker> addMarkers = [];
  // AnimatedMapControllerのインスタンス作成
  late final _animatedMapController = AnimatedMapController(vsync: this);

  void _addMarker(LatLng latlng) {
    setState(() {
      addMarkers.add(
        Marker(
          width: 30.0,
          height: 30.0,
          point: latlng,
          child: GestureDetector(
            onTap: () {
              // animateToで、引数の位置に滑らかにマップの中心を移動する
              _animatedMapController.animateTo(dest: latlng);
            },
            
// ... 省略
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: Text(widget.title),
      ),
      body: FlutterMap(
        // _animatedMapController.mapControllerを指定する。
        mapController: _animatedMapController.mapController,
// ... 省略
```

![RPReplay_Final1702566589.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2708333/68c92437-8250-dbf8-ecdc-72bd9b0cf318.gif)

このように、滑らかに移動してるのがわかると思います。
アニメーションがあるだけで、かなりリッチなアプリに見えますね・・・

### 現在地を表示する
flutter_mapパッケージ自体には、現在地を表示する機能は実装されていません。
そのため、現在地表示用のパッケージとflutter_mapを組み合わせることで実装出来ます。
この記事が好評であれば、現在地の表示方法についても含めて、また記事を書こうと思います。


## 今回作成したコード
```map.dart
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_animations/flutter_map_animations.dart';
import 'package:latlong2/latlong.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.amber,
      ),
      home: const MyHomePage(title: 'flutter_mapテスト'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
  List<Marker> addMarkers = [];
  // MapControllerのインスタンス作成
  late final _animatedMapController = AnimatedMapController(vsync: this);

  void _addMarker(LatLng latlng) {
    setState(() {
      addMarkers.add(
        Marker(
          width: 30.0,
          height: 30.0,
          point: latlng,
          child: GestureDetector(
            onTap: () {
              _animatedMapController.animateTo(dest: latlng);
            },
            child: const Icon(
              Icons.location_on,
              color: Colors.blue,
              size: 50,
            ),
          ),
        ),
      );
    });
  }

  void _showAlert(LatLng latlng) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('ピンの位置'),
        content: Text('緯度: ${latlng.latitude}, 経度: ${latlng.longitude}'),
        actions: <Widget>[
          TextButton(
            child: const Text('閉じる'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Colors.white,
        title: Text(widget.title),
      ),
      body: FlutterMap(
        // mapControllerをFlutterMapに指定
        mapController: _animatedMapController.mapController,
        options: MapOptions(
          initialCenter: const LatLng(35.170915, 136.881537),
          initialZoom: 10.0,
          maxZoom: 12.0,
          minZoom: 8.0,
          onTap: (tapPosition, point) {
            _addMarker(point);
          },
        ),
        children: [
          TileLayer(
            urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
          ),
          const MarkerLayer(
            markers: [
              Marker(
                width: 30.0,
                height: 30.0,
                point: LatLng(35.170915, 136.881537), // ピンの位置を設定
                child: Icon(
                  Icons.location_on,
                  color: Colors.red,
                  size: 50,
                ),
                rotate: true,
              )
            ],
          ),
          MarkerLayer(markers: addMarkers),
          PolylineLayer(
            polylines: [
              Polyline(
                points: [
                  const LatLng(35.1, 136.85),
                  const LatLng(35.2, 136.80),
                  const LatLng(35.3, 136.89),
                  const LatLng(35.4, 136.82),
                ],
                strokeWidth: 16.0,
                color: Colors.black,
              ),
            ],
          ),
        ],
      ),
    );
  }
}


```

## おわりに
flutter_mapを使用すると、手軽に地図アプリが実装出来ちゃいます。嬉しい。
しかし、冒頭でも話しましたがflutter_mapは高頻度でアップデートが行われ、使い方などもバージョンによって結構変わってしまいます。
少しでも最新のflutter_mapパッケージの参考に、flutterでのマップアプリケーションの作成の手助けになれば幸いです！