【Flutter/Dart】Google地図アプリを作ろう

# 1.はじめに
これまでに7つのAndroidStudio&Flutter&Dart記事を書いている。以下は内容、

- 【Flutter/Dart】環境構築方法の紹介
https://qiita.com/my_programming/items/ad1493e3fef2fb63e036

- 【Flutter/Dart】簡単なプロジェクトの設定と共有方法の紹介
https://qiita.com/my_programming/items/cec09574a3b6e39be277

- 【Flutter/Dart】エミュレータの言語設定をかえよう
https://qiita.com/my_programming/items/6c67fed22c624fd9494b

- 【Flutter/Dart】Dartの文法について学ぼう
https://qiita.com/my_programming/items/9ba25114ef217d077ca5

- 【Flutter/Dart】タスク管理アプリを作ろう
https://qiita.com/my_programming/items/9ed2276f6e9076f43444

- **【Flutter/Dart】Google地図アプリを作ろう（今回説明する内容）**
- 【Flutter/Dart】情報管理アプリ＋Google地図アプリを作ろう
https://qiita.com/my_programming/items/5bd1bb7b789b642635b4

今回は、FlutterでGoogleMapプラグインを使った地図アプリをを作成する。
-地図をアプリで利用するためのAPIキー取得
-地図をアプリで表示するためのコーディング
-地図で現在地を取得するためのコーディング
-地図で場所を検索するためのコーディング
-地図に経路を表示するためのコーディングし経路長さを画面に表示

# 2.地図アプリを作ろう
## 2-1.地図をアプリで利用するためのAPIキー取得
まず[GoogleCloudPlatform（GCP）](GoogleCloudPlatform（GCP）)にアクセスして新しいプロジェクトを作成。
<img width="300" alt="step2_1_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/63bbd4fb-fcdb-d281-2db0-727d479d4509.png">

アカウントの作成は国とニーズを適当に選んで二つのチェックボタンをオンにしてから`続行`ボタンを押す。
<img width="300" alt="step2_1_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/cbd1f2f0-3124-7b72-f52d-581f5abec6c2.png">

以下の画面では右下のスキップを押す。
<img width="300" alt="step2_1_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/42e0001c-050c-d24e-ccd7-b700d6882e1c.png">

以下の画面では赤四角のエリアをクリックしてプロジェクトを作成。
<img width="300" alt="step2_1_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/38b998c5-b81b-abe5-4efd-3161cd84492f.png">

以下の画面では右上の`新しいプロジェクト`を押す。
<img width="300" alt="step2_1_5.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/2f52e173-0749-41b2-c052-a42eb8d0ad36.png">

以下の画面では左側のメニューから`APIとサービス`に移動し`ライブラリ`を選択。
<img width="300" alt="step2_1_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/1abf12da-1ddd-b74e-e9be-f79c9ca3ec21.png">

`Maps SDK for Android`を検索。
<img width="300" alt="step2_1_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/ec2c93a0-3354-dfbd-c3ff-8fb8b02abbb9.png">

MapプラットフォームでマップSDKを`有効にする`ボタンを押す。
<img width="300" alt="step2_1_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/d79acaf5-dfc6-b502-9f01-66477c39e2de.png">

左側のメニューから`認証情報`に移動し`認証情報を作成`を選択。
<img width="300" alt="step2_1_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/df7a456c-10a5-6def-7003-209287042064.png">

APIキーを選択しAPIキーをコピー。
<img width="300" alt="step2_1_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/44458028-3c3d-e748-413c-d814c496e8d0.png">

ルートを表示するための`DirectionsAPI`も同様に有効にしておく。
<img width="300" alt="step2_1_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/bba9b453-91b2-9f34-368e-0ba069a1b3e5.png">

## 2-2.地図をアプリで表示するためのコーディング
・新しいFlutterプロジェクトを作成する。プロジェクト名はmy_appとする。
<img width="300" alt="step2_2_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/5fbf6cb9-1277-f9cc-b983-0451e0b00d24.png">

・ここからはgoogle_maps_flutterというプラグインを実装していく。google_maps_flutterは地図を画面に表示する役立つ。プラグインをpubspec.yamlファイルに追加する。プラグインの登録が初回であれば、以下の[Dart packages](`https://pub.dev/`)にアクセスして`Search Packages`バーでgoogle_maps_flutterを検索したのち一番上の名前をクリック。次画面で名前横にあるクリップアイコンを押してプラグインのバージョンをコピー。
<img width="300" alt="step2_2_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/4a7dc270-8e16-20c9-f9a0-5306a81cd384.png">

・AndroidStudioに戻りプロジェクトのtestフォルダにあるpubspec.yamlを開いた後の該当する行付近にコピーした情報をペースト。プラグインの必要な機能はpubspec.yamlの右上にある青い字の`Pub get`を押すことによってプロジェクトに追加される。
``` pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  google_maps_flutter: ^2.1.8 //　追加
```

<img width="150" alt="step2_2_4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c4de249f-eb5e-7d58-f6d1-b75fc9d27adb.png">

・`android/app/build.gradle`を開き`minSdkVersion`に20を設定する。
``` android/app/build.gradle
    defaultConfig {
        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.my_app"
        // You can update the following values to match your application needs.
        // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
        minSdkVersion 20 // flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }
```

・`android/app/src/main/AndroidManifest.xml`に以下のAPIキーを追加する。なお、APIキーは取得したものに置き換えること。
``` android/app/src/main/AndroidManifest.xml
<application android:label="map_app3" android:name="${applicationName}" android:icon="@mipmap/ic_launcher">
// 追加はここから
<meta-data android:name="com.google.android.geo.API_KEY" android:value="APIキー"/>
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
```

・`android/app/src/main/AndroidManifest.xml`に以下の権限設定のコードを追加する。これにより高精度な位置情報を取得できるようになる。これでGoogleMap Widgetをアプリに追加する準備が完了。
``` android/app/src/main/AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.map_app3">
// 追加はここから
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
```

・実際にGoogleMapを表示するために以下のコードをmain.dartに記入。main関数からStatelessWidgetのMyaApp Widgetを呼び出し、次いでMyAppからStatefulWidgetのMapView Widgetを呼び出してcreateState()から_MapViewState Widgetを実行する。Container WidgetでGoogleMap Widgetが画面全体を占めるように高さと幅を画面のサイズに合わせている。
``` lib/main.dart
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Maps',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MapView(),
    );
  }
}

class MapView extends StatefulWidget {
  @override
  _MapViewState createState() => _MapViewState();
}

class _MapViewState extends State<MapView> {
  @override
  Widget build(BuildContext context) {
    // 画面の幅と高さを決定する
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;

    return Container(
      height: height,
      width: width,
      child: Scaffold(
        body: Stack(
          children: <Widget>[
            // ここに地図を追加
          ],
        ),
      ),
    );
  }
}
```
・以下は実行結果
<img width="100" alt="step2_2_6.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/226dcb63-9a9d-461a-97cc-a31fd32b0f78.png">

・StackにてGoogleMap Widgetをバックグラウンドに保持した上で他の必要なウィジェットを追加していく。`// ここに地図を追加`の箇所をGoogleMap Widgetに置き換える。
１つめはmain.dartの冒頭
``` lib/main.dart
import 'package:flutter/material.dart';
// Google Mapsのパッケージをインポートする
import 'package:google_maps_flutter/google_maps_flutter.dart'; // 追加
```
２つめはStatefulWidgetの宣言直後と後半
```lib/main.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0)); // 追加
  // マップの表示制御用
  late GoogleMapController mapController; // 追加

  @override
  Widget build(BuildContext context) {
    // 画面の幅と高さを決定する
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;

    return Container(
      height: height,
      width: width,
      child: Scaffold(
        body: Stack(
          children: <Widget>[
            // 追加
            GoogleMap(
              initialCameraPosition: _initialLocation,
              myLocationEnabled: true,
              myLocationButtonEnabled: false,
              mapType: MapType.normal,
              zoomGesturesEnabled: true,
              zoomControlsEnabled: false,
              onMapCreated: (GoogleMapController controller) {
                mapController = controller;
              },
            ),
          ],
        ),
      ),
    );
  }
}
```
以下は実行結果
<img width="100" alt="step2_2_7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/8a0af444-1124-7e7f-18c1-f0bd7dbb421e.png">

・Googleマップウィジェットで定義されているパラメータの説明は以下の通り。mapControllerマップビューのカメラ位置を制御するために使用される。
<img width="300" alt="step2_2_8.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/98dfadce-233b-1310-4a21-659b0ca6dd6b.png">

## 2-3.地図で現在地を取得するためのコーディング
・ズームボタンと現在位置ボタンを表示するには、それらを子としてStackウィジェットに追加し、そのウィジェットに応じて配置する。以下にボタンを設計するためのベースとなるコードを示す。
```dart
// ボタンの凡例
ClipOval(
  child: Material(
    color: Colors.orange.shade100, // ボタンを押す前のカラー
    child: InkWell(
      splashColor: Colors.orange, // ボタンを押した後のカラー
      child: SizedBox(
        width: 56,
        height: 56,
        child: Icon(Icons.my_location),
      ),
      onTap: () {
        // ここに押された時の機能を追加する
      },
    ),
  ),
),
```

・`// ここに押された時の機能を追加する`にズームインとズームアウトするためのコードを示す。
```
// ズームイン動作
mapController.animateCamera(
  CameraUpdate.zoomIn(),
);

// ズームアウト動作
mapController.animateCamera(
  CameraUpdate.zoomOut(),
);
```

・`// ここに押された時の機能を追加する`にカメラポジションを新しい位置に移すために必要なコード。
```
// 指定した緯度・経度にカメラを移動する
mapController.animateCamera(
  CameraUpdate.newCameraPosition(
    CameraPosition(
        target: LatLng(
           35.65872865514525, // 仮の緯度。後で変更
           139.74543290592266 // 仮の経度。後で変更
        ),
      zoom: 18.0,
    ),
  ),
);
```

・上記のボタンを`main.dart`のStatefulWidgetに統合したコードは以下。
``` lib/maind.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
  // マップの表示制御用
  late GoogleMapController mapController;

  @override
  Widget build(BuildContext context) {
    // 画面の幅と高さを決定する
    var height = MediaQuery.of(context).size.height;
    var width = MediaQuery.of(context).size.width;

    return Container(
      height: height,
      width: width,
      child: Scaffold(
        body: Stack(
          children: <Widget>[
            GoogleMap(
              initialCameraPosition: _initialLocation,
              myLocationEnabled: true,
              myLocationButtonEnabled: false,
              mapType: MapType.normal,
              zoomGesturesEnabled: true,
              zoomControlsEnabled: false,
              onMapCreated: (GoogleMapController controller) {
                mapController = controller;
              },
            ),
            
            // ここからボタンを表示するためのコードを追加
            // ズームイン・ズームアウトのボタンを配置
            SafeArea(
              child: Align(
                alignment: Alignment.centerRight,
                child: Padding(
                  padding: const EdgeInsets.only(right: 10.0, bottom: 100.0),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.end,
                    children: <Widget>[
                      // ズームインボタン
                      ClipOval(
                        child: Material(
                          color: Colors.blue.shade100, // ボタンを押す前のカラー
                          child: InkWell(
                            splashColor: Colors.blue, // ボタンを押した後のカラー
                            child: SizedBox(
                              width: 50,
                              height: 50,
                              child: Icon(Icons.add),
                            ),
                            onTap: () {
                              mapController.animateCamera(
                                CameraUpdate.zoomIn(),
                              );
                            },
                          ),
                        ),
                      ),
                      SizedBox(height: 20),
                      //　ズームアウトボタン
                      ClipOval(
                        child: Material(
                          color: Colors.blue.shade100, // ボタンを押す前のカラー
                          child: InkWell(
                            splashColor: Colors.blue, // ボタンを押した後のカラー
                            child: SizedBox(
                              width: 50,
                              height: 50,
                              child: Icon(Icons.remove),
                            ),
                            onTap: () {
                              mapController.animateCamera(
                                CameraUpdate.zoomOut(),
                              );
                            },
                          ),
                        ),
                      )
                    ],
                  ),
                ),
              ),
            ),
            SafeArea(
              child: Align(
                alignment: Alignment.bottomRight,
                child: Padding(
                  padding: const EdgeInsets.only(right: 10.0, bottom: 10.0),
                  // 現在地表示ボタン
                  child: ClipOval(
                    child: Material(
                      color: Colors.orange.shade100, // ボタンを押す前のカラー
                      child: InkWell(
                        splashColor: Colors.blue, // ボタンを押した後のカラー
                        child: SizedBox(
                          width: 50,
                          height: 50,
                          child: Icon(Icons.my_location),
                        ),
                        onTap: () {
                          mapController.animateCamera(
                            CameraUpdate.newCameraPosition(
                              CameraPosition(
                                target: LatLng(
                                    35.65872865514525, // 仮の緯度。後で変更
                                    139.74543290592266 // 仮の経度。後で変更
                                ),
                                zoom: 18.0,
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
```

以下は実行画面。
<img width="100" alt="step2_2_9.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/e502acae-9211-fc66-f9eb-d4b39342efed.png">

現在地ボタンを押すことで設定している座標に画面が移動する。さらにズームインとズームアウトのボタンをすことで画面中の地図のサイズを変えることができる。
<img width="100" alt="step2_2_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/c172cd12-a2e4-f2fb-2256-d098f4555b75.png">

・ここからはgeolocatorというプラグインを実装していく。geolocatorはユーザーの現在値を取得するのに役立つ。プラグインをpubspec.yamlファイルに追加する。追加したら`Pub get`を押す。アプリが起動すると画面が自動的に検出された場所に移動するように作成していく。
<img width="300" alt="step2_2_11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/d3e38210-0bc6-98e1-4f94-bfd3b5a473b7.png">

``` pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  google_maps_flutter: ^2.1.8
  geolocator: ^9.0.0 //　追加
```
・android/app/build.gradleを開きcompileSdkVersionに33を設定する。

・パッケージをimport。

``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// Geolocatorのパッケージをインポートする
import 'package:geolocator/geolocator.dart';
```

・取得した現在の場所を格納する変数を定義。
``` lib/main.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
  // マップの表示制御用
  late GoogleMapController mapController;

  // 現在位置の記憶用
  late Position _currentPosition; // 追加

```

・ユーザーの現在の場所を取得。`late Position _currentPosition;`の後に、
``` lib/main.dart
// 現在位置の取得方法
_getCurrentLocation() async {
  await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high)
      .then((Position position) async {
    setState(() {
      // 位置を変数に格納する
      _currentPosition = position;

      print('CURRENT POS: $_currentPosition');

      // カメラを現在位置に移動させる場合
      mapController.animateCamera(
        CameraUpdate.newCameraPosition(
          CameraPosition(
            target: LatLng(position.latitude, position.longitude),
            zoom: 18.0,
          ),
        ),
      );
    });
    // await _getAddress();
  }).catchError((e) {
    print(e);
  });
}
```
・このメソッドをに追加しinitStateでアプリが起動するとすぐにユーザーの現在の場所を取得し画面をその場所に移動させる。上記の直後に以下を記載、

``` lib/main.dart
@override
void initState() {
  super.initState();
  _getCurrentLocation();
}
```

・現在値を取得するためのカスタムメソッドに経度と緯度を渡す。mapControllerの中身を書き換える。
``` lib/main.dart
mapController.animateCamera(
      CameraUpdate.newCameraPosition(
        CameraPosition(
          target: LatLng(
            _currentPosition.latitude,
            _currentPosition.longitude,
          ),
          zoom: 18.0,
        ),
      ),
    );
```
・これでアプリが起動するときにエミュレータがもっている現在値情報に応じて画面を移動させることができる様になる。

## 2-4.地図で場所を検索するためのコーディング
・ここからはgeocoding_flutterというプラグインを実装していく。geocoding_flutterは場所の住所と座標（経度と緯度）を相互に変換できる。プラグインをpubspec.yamlファイルに追加する。
<img width="300" alt="step2_2_12.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/91061b90-3f24-13af-0531-21907fa6f851.png">

``` pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  google_maps_flutter: ^2.1.8
  geolocator: ^9.0.0
  geocoding: ^2.0.4 // 追加
```

・パッケージをimport。
``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// geolocatorをインポートする
import 'package:geolocator/geolocator.dart';
```

・利用を始めるためにはTexitFieldに住所を入力する必要がある。また、場所を取得するためにTextEditingControllerと以下のコードを定義する。`class _MapViewState extends State<MapView>`の最初に一行追加。
``` lib/main.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
  // マップの表示制御用
  late GoogleMapController mapController;
  // 現在位置の記憶用
  late Position _currentPosition;
  // 場所の記憶用
  final startAddressController = TextEditingController(); // 追加
  final destinationAddressController = TextEditingController(); // 追加
  final startAddressFocusNode = FocusNode(); // 追加
  final desrinationAddressFocusNode = FocusNode(); // 追加
  String _currentAddress = ''; // 追加
  String _startAddress = ''; // 追加
  String _destinationAddress = ''; // 追加
  String? _placeDistance; // 追加
```

・`_getCurrentLocation() async`の後に追加。
``` lib/main.dart
// アドレスの取得方法
_getAddress() async {
  try {
    // 座標を使用して場所を取得する
    List<Placemark> p = await placemarkFromCoordinates(
        _currentPosition.latitude, _currentPosition.longitude);

    // 最も確率の高い結果を取得
    Placemark place = p[0];

    setState(() {

      // アドレスの構造化
      _currentAddress =
          "${place.name}, ${place.locality}, ${place.postalCode}, ${place.country}";
      
      // TextFieldのテキストを更新
      startAddressController.text = _currentAddress;

      // ユーザーの現在地を出発地とする設定
      _startAddress = _currentAddress;
    });
  } catch (e) {
    print(e);
  }
}
```

・`_getCurrentLocation() async`の後半で`// await _getAddress();`となっているコメントアウトをなくしてアクティブにする。

・テキストフィールドを作成していく。以下をボタンUIの下に記載。
``` lib/main.dart
// 開智位置と目的位置を入力するためのUI
SafeArea(
    child: Align(
      alignment: Alignment.topCenter,
      child: Padding(
        padding: const EdgeInsets.only(top: 10.0),
        child: Container(
          decoration: BoxDecoration(
            color: Colors.black38
          ),
          width: width * 0.85,
          child: Padding(
            padding: const EdgeInsets.only(top: 5.0, bottom: 5.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text(
                  '場所検索',
                  style: TextStyle(fontSize: 20.0, color: Colors.white),
                ),
                SizedBox(height: 10),
                _textField(
                    label: '開始位置',
                    hint: '開始位置を入力',
                    prefixIcon: Icon(Icons.directions_walk),
                    suffixIcon: IconButton(
                      icon: Icon(Icons.my_location),
                      onPressed: () {
                        startAddressController.text = _currentAddress;
                        _startAddress = _currentAddress;
                      },
                    ),
                    controller: startAddressController,
                    focusNode: startAddressFocusNode,
                    width: width,
                    locationCallback: (String value) {
                      setState(() {
                        _startAddress = value;
                      });
                    }),
                SizedBox(height: 10),
                _textField(
                    label: '目的位置',
                    hint: '目的位置を入力',
                    prefixIcon: Icon(Icons.directions_walk),
                    controller: destinationAddressController,
                    focusNode: desrinationAddressFocusNode,
                    width: width,
                    locationCallback: (String value) {
                      setState(() {
                        _destinationAddress = value;
                      });
                    }),
                SizedBox(height: 10),
                Visibility(
                  visible: _placeDistance == null ? false : true,
                  child: Text(
                    'DISTANCE: $_placeDistance km',
                    style: TextStyle(
                      fontSize: 16,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                SizedBox(height: 5),
                ElevatedButton(
                  onPressed: (_startAddress != '' &&
                      _destinationAddress != '')
                      ? () async {
                  }
                      : null,
                  child: Padding(
                    padding: const EdgeInsets.all(8.0),
                    child: Text(
                      'ルート検索'.toUpperCase(),
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 20.0,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  ),
```

・ここまでのコードを実行した結果は以下
<img width="100" alt="step2_2_13.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/cb641542-ec5f-a91d-d4ac-392034c0fa72.png">

・ルートの開始位置と目的位置の情報を格納するための変数を作成する。これらの変数は地図上に開始位置と目的位置のマーカーを表示するのに役立つ。
``` lib/main.dart
// 住所からプレースマークを取得する
List<Location> startPlacemark = await locationFromAddress(_startAddress);
List<Location> destinationPlacemark = await locationFromAddress(_destinationAddress);

// 出発地と目的地の緯度・経度を格納する
double startLatitude = startPlacemark[0].latitude;
double startLongitude = startPlacemark[0].longitude;
double destinationLatitude = destinationPlacemark[0].latitude;
double destinationLongitude = destinationPlacemark[0].longitude;
```

・次にマーカーをリストでデータ保持できる変数markersを作成。
``` lib/main.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
  // マップの表示制御用
  late GoogleMapController mapController;
  // 現在位置の記憶用
  late Position _currentPosition;
  // 現在位置&スタート位置の記憶用
  final startAddressController = TextEditingController();
  String _currentAddress = '';
  String _startAddress = '';
  String _destinationAddress = '';
  Set<Marker> markers = {};　// 追加
```

・次にstartMarkerとdestinationMarkerを作成。
``` lib/main.dart
String startCoordinatesString = '($startLatitude, $startLongitude)';
String destinationCoordinatesString = '($destinationLatitude, $destinationLongitude)';

// 開始位置のマーカー
Marker startMarker = Marker(
  markerId: MarkerId(startCoordinatesString),
  position: LatLng(startLatitude, startLongitude),
  infoWindow: InfoWindow(
    title: 'Start $startCoordinatesString',
    snippet: _startAddress,
  ),
  icon: BitmapDescriptor.defaultMarker,
);

// 目的位置のマーカー
Marker destinationMarker = Marker(
  markerId: MarkerId(destinationCoordinatesString),
  position: LatLng(destinationLatitude, destinationLongitude),
  infoWindow: InfoWindow(
    title: 'Destination $destinationCoordinatesString',
    snippet: _destinationAddress,
  ),
  icon: BitmapDescriptor.defaultMarker,
);
```
・次にmarkersにstartMarkerとdestinationMarkerを定義。
``` lib/main.dart
// マーカーをリストに追加する
markers.add(startMarker);
markers.add(destinationMarker);
```


・開始位置と目的位置のマーカーが大きいときに方方のマーカーは画面外になり表示されなくなる場合がある。そこで、両方が表示されるように地図の表示サイズを調節する。
``` lib/main.dart
// フレームに対する相対位置を確認するための計算を行い、それに応じてカメラをパン＆ズーム
double miny = (startLatitude <= destinationLatitude)
    ? startLatitude
    : destinationLatitude;
double minx = (startLongitude <= destinationLongitude)
    ? startLongitude
    : destinationLongitude;
double maxy = (startLatitude <= destinationLatitude)
    ? destinationLatitude
    : startLatitude;
double maxx = (startLongitude <= destinationLongitude)
    ? destinationLongitude
    : startLongitude;

double southWestLatitude = miny;
double southWestLongitude = minx;

double northEastLatitude = maxy;
double northEastLongitude = maxx;

// マップのカメラビュー内に2つのロケーションを収容
mapController.animateCamera(
  CameraUpdate.newLatLngBounds(
    LatLngBounds(
      northeast: LatLng(northEastLatitude, northEastLongitude),
      southwest: LatLng(southWestLatitude, southWestLongitude),
    ),
    100.0,
  ),
);
```

・tartMarkerとdestinationMarkerからサイズ調整までを含んだ一連のコードをStatefulWidget内に記述。

``` lib/main.dart
  // 2地点間の距離の算出方法
  Future<bool> _RouteDistance() async {
    try {
      // 住所からプレースマークを取得する
      List<Location>? startPlacemark = await locationFromAddress(_startAddress);
      List<Location>? destinationPlacemark =
      await locationFromAddress(_destinationAddress);

      // 開始位置がユーザーの現在位置の場合、アドレスではなく、取得した現在位置の座標を使用する方が精度が良いため。
      double startLatitude = _startAddress == _currentAddress
          ? _currentPosition.latitude
          : startPlacemark[0].latitude;

      double startLongitude = _startAddress == _currentAddress
          ? _currentPosition.longitude
          : startPlacemark[0].longitude;

      double destinationLatitude = destinationPlacemark[0].latitude;
      double destinationLongitude = destinationPlacemark[0].longitude;

      String startCoordinatesString = '($startLatitude, $startLongitude)';
      String destinationCoordinatesString = '($destinationLatitude, $destinationLongitude)';

      // 開始位置用マーカー
      Marker startMarker = Marker(
        markerId: MarkerId(startCoordinatesString),
        position: LatLng(startLatitude, startLongitude),
        infoWindow: InfoWindow(
          title: 'Start $startCoordinatesString',
          snippet: _startAddress,
        ),
        icon: BitmapDescriptor.defaultMarker,
      );

      // 目的位置用マーカー
      Marker destinationMarker = Marker(
        markerId: MarkerId(destinationCoordinatesString),
        position: LatLng(destinationLatitude, destinationLongitude),
        infoWindow: InfoWindow(
          title: 'Destination $destinationCoordinatesString',
          snippet: _destinationAddress,
        ),
        icon: BitmapDescriptor.defaultMarker,
      );

      // マーカーをリストに追加する
      markers.add(startMarker);
      markers.add(destinationMarker);

      print(
        'START COORDINATES: ($startLatitude, $startLongitude)',
      );
      print(
        'DESTINATION COORDINATES: ($destinationLatitude, $destinationLongitude)',
      );

      // フレームに対する相対位置を確認するための計算を行い、それに応じてカメラをパン＆ズームする
      double miny = (startLatitude <= destinationLatitude)
          ? startLatitude
          : destinationLatitude;
      double minx = (startLongitude <= destinationLongitude)
          ? startLongitude
          : destinationLongitude;
      double maxy = (startLatitude <= destinationLatitude)
          ? destinationLatitude
          : startLatitude;
      double maxx = (startLongitude <= destinationLongitude)
          ? destinationLongitude
          : startLongitude;

      double southWestLatitude = miny;
      double southWestLongitude = minx;

      double northEastLatitude = maxy;
      double northEastLongitude = maxx;

      // マップのカメラビュー内に2つのロケーションを収容する
      mapController.animateCamera(
        CameraUpdate.newLatLngBounds(
          LatLngBounds(
            northeast: LatLng(northEastLatitude, northEastLongitude),
            southwest: LatLng(southWestLatitude, southWestLongitude),
          ),
          100.0,
        ),
      );

      // 2つのマーカーの間にラインを表示する
      await _createPolylines(startLatitude, startLongitude, destinationLatitude,  destinationLongitude);

      // 距離計算用の変数
      double totalDistance = 0.0;
      setState(() {
        _placeDistance = totalDistance.toStringAsFixed(2);
      });

      return true;
    } catch (e) {
      print(e);
    }
    return false;
  }
```

・ElevatedButtonが空欄だったのを以下のように_RouteDistanceを呼び出せる様似変更する。
``` lib/main.dart
ElevatedButton(
      onPressed: (_startAddress != '' &&
          _destinationAddress != '')
          ? () async {
        startAddressFocusNode.unfocus();
        desrinationAddressFocusNode.unfocus();
        // ここから追加
        setState(() {
          if (markers.isNotEmpty) markers.clear();
          if (polylines.isNotEmpty)
            polylines.clear();
          if (polylineCoordinates.isNotEmpty)
            polylineCoordinates.clear();
        });
        // ここまで追加

        _RouteDistance();
      }
          : null,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Text(
          'ルート検索'.toUpperCase(),
          style: TextStyle(
            color: Colors.white,
            fontSize: 20.0,
          ),
        ),
      ),
    ),
```

・そしてGoogleMapでmarkersを表示する。開始位置と目的位置に応じた場所にマーカーが表示されるはずである。
``` lib/main.dart
// Add the markers property to the widget
  GoogleMap(
    markers: Set<Marker>.from(markers), // 追加
    initialCameraPosition: _initialLocation,
    myLocationEnabled: true,
    myLocationButtonEnabled: false,
    mapType: MapType.normal,
    zoomGesturesEnabled: true,
    zoomControlsEnabled: false,
    onMapCreated: (GoogleMapController controller) {
      mapController = controller;
    },
  ),
```
・ここまでのコードを実行した結果は以下
<img width="100" alt="step2_2_14.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/b21bb083-73c7-3e55-5e8b-75e54383314e.png">

## 2-5.地図に経路を表示するためのコーディング
・ここからはflutter_polyline_pointsというプラグインを実装していく。flutter_polyline_pointsはGoogleMapで描画するために使われる。プラグインをpubspec.yamlファイルに追加する。追加したら`Pub get`を押す。

<img width="300" alt="step2_2_16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/966f9ce0-14d0-86a0-b9b5-e01c8ec54067.png">

``` pubspec.yaml
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.2
  google_maps_flutter: ^2.1.8
  geolocator: ^9.0.0
  geocoding: ^2.0.4
  flutter_polyline_points: ^1.0.0
```
・パッケージをimportする。
``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
// flutter_polyline_pointsをインポートする
import 'package:flutter_polyline_points/flutter_polyline_points.dart';

```
・いくつかの変数を定義する。
``` lib/main.dart
class _MapViewState extends State<MapView> {
  // マップビューの初期位置
  CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
  // マップの表示制御用
  late GoogleMapController mapController;
  // 現在位置の記憶用
  late Position _currentPosition;

  // 場所の記憶用
  final startAddressController = TextEditingController();
  final destinationAddressController = TextEditingController();
  final startAddressFocusNode = FocusNode();
  final desrinationAddressFocusNode = FocusNode();
  String _currentAddress = '';
  String _startAddress = '';
  String _destinationAddress = '';
  String? _placeDistance;

  // PolylinePoints用オブジェクト
  late PolylinePoints polylinePoints;
  // 参加する座標のリスト
  List<LatLng> polylineCoordinates = [];
  // 2点間を結ぶポリラインを格納した地図
  Map<PolylineId, Polyline> polylines = {};

```

・ポリラインを作成するためのメソッドを定義します。開始位置と目的地の位置を渡す必要がある。記述箇所はStatefulWidgetの内部。
``` lib/main.dart
// 2地点間の経路を示すポリラインを作成する
  _createPolylines(
      double startLatitude,
      double startLongitude,
      double destinationLatitude,
      double destinationLongitude,
      ) async {
    polylinePoints = PolylinePoints();
    PolylineResult result = await polylinePoints.getRouteBetweenCoordinates(
      Secrets.API_KEY, // Google Maps APIキー
      PointLatLng(startLatitude, startLongitude),
      PointLatLng(destinationLatitude, destinationLongitude),
      travelMode: TravelMode.walking,
    );

    if (result.points.isNotEmpty) {
      result.points.forEach((PointLatLng point) {
        polylineCoordinates.add(LatLng(point.latitude, point.longitude));
      });
    }

    PolylineId id = PolylineId('poly');
    Polyline polyline = Polyline(
      polylineId: id,
      color: Colors.red,
      points: polylineCoordinates,
      width: 3,
    );
    polylines[id] = polyline;
  }
```

上記のコードブロック内部でSecrets.API_Keyという変数がある。Google Map APIキーのことであり、以下のコードによってここでは変数を作成する。classなのでStatefulWidgetの外部で定義する。クラスを定義する場所はわかり易さを優先してmain関数の次あたりにするとよい。
``` lib/main.dart
void main() {
  runApp(MyApp());
}

class Secrets {
  // Google Maps APIキーをここに追加
  static const API_KEY = 'GCPで取得したAPIキーを記入';
}
```

・GoogleMap上にポリラインを表示するためにコードを追加
``` lib/main.dart
  GoogleMap(
    markers: Set<Marker>.from(markers),
    initialCameraPosition: _initialLocation,
    myLocationEnabled: true,
    myLocationButtonEnabled: false,
    mapType: MapType.normal,
    zoomGesturesEnabled: true,
    zoomControlsEnabled: false,
    polylines: Set<Polyline>.of(polylines.values), // 追加
    onMapCreated: (GoogleMapController controller) {
      mapController = controller;
    },
  ),
```

・作った_createPolylinesウィジェットを呼び出すコードを_RouteDistanceウィジェット後半にに追加する。
``` lib/main.dart
// 2地点間の距離の算出方法
  Future<bool> _RouteDistance() async {

      .
      .
      .

      // マップのカメラビュー内に2つのロケーションを収容する
      mapController.animateCamera(
        CameraUpdate.newLatLngBounds(
          LatLngBounds(
            northeast: LatLng(northEastLatitude, northEastLongitude),
            southwest: LatLng(southWestLatitude, southWestLongitude),
          ),
          100.0,
        ),
      );

      // 2つのマーカーの間にラインを表示する
      await _createPolylines(startLatitude, startLongitude, destinationLatitude,  destinationLongitude);

      setState(() {
      });

      return true;
    } catch (e) {
      print(e);
    }
    return false;
  }
```
setStateが組み込まれている。これはStatefulWidgetで状態がかわったことをアプリが認識する手段であり、このタイミングで画面にラインが描画される。

・ルート検索ボタンを押すとmarkers, polylines, polylineCoordinatesを初期化するコードを追加する。
``` lib/main.dart
ElevatedButton(
      onPressed: (_startAddress != '' &&
          _destinationAddress != '')
          ? () async {
        startAddressFocusNode.unfocus();
        desrinationAddressFocusNode.unfocus();

        setState(() {
          if (markers.isNotEmpty) markers.clear();
          if (polylines.isNotEmpty)
            polylines.clear();
          if (polylineCoordinates.isNotEmpty)
            polylineCoordinates.clear();
        });

        _RouteDistance();
      }
          : null,
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Text(
          'ルート検索'.toUpperCase(),
          style: TextStyle(
            color: Colors.white,
            fontSize: 20.0,
          ),
        ),
      ),
    ),
```
・ここまでのコードを実行した結果は以下。ルートが青い線で表示されている。
<img width="100" alt="step2_2_17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/a6843d40-03b7-07a3-0fe8-76d1a331ce40.png">

・マーカーを適切に配置し、ルートを描画したら、2つの場所の間の距離を計算して描画できる様にする。そのために、ルート全体をいくつかの小さな部分に分割し、それらの距離を計算してから、それらを合計する。距離計算のために三角関数を使える様にする。
``` lib/main.dart
import 'package:flutter/material.dart';
import 'package:geocoding/geocoding.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:geolocator/geolocator.dart';
import 'package:flutter_polyline_points/flutter_polyline_points.dart';
// 三角関数をimportする
import 'dart:math' show cos, sqrt, asin;

```

・2つの地理座標間の距離を計算するために[参考元](https://stackoverflow.com/questions/54138750/total-distance-calculation-from-latlng-list/54138876#54138876
)から数式を引用。
``` lib/main.dart
  double _coordinateDistance(lat1, lon1, lat2, lon2) {
    var p = 0.017453292519943295;
    var c = cos;
    var a = 0.5 -
        c((lat2 - lat1) * p) / 2 +
        c(lat1 * p) * c(lat2 * p) * (1 - c((lon2 - lon1) * p)) / 2;
    return 12742 * asin(sqrt(a));
  }

```

・上式を使用して2つの座標間の距離を計算し、それらを合計して合計距離を求める。
``` lib/main.dart
 // 2地点間の距離の算出方法
  Future<bool> _RouteDistance() async {
    try {
      .
      .
      .
      // 2つのマーカーの間にラインを表示する
      await _createPolylines(startLatitude, startLongitude, destinationLatitude,  destinationLongitude);

      // 距離計算用の変数
      double totalDistance = 0.0;
      // 小さなセグメント間の距離を加算して総距離を計算する
      for (int i = 0; i < polylineCoordinates.length - 1; i++) {
        totalDistance += _coordinateDistance(
          polylineCoordinates[i].latitude,
          polylineCoordinates[i].longitude,
          polylineCoordinates[i + 1].latitude,
          polylineCoordinates[i + 1].longitude,
        );
      }
      // 表示用の変数に計算結果を格納
      setState(() {
        _placeDistance = totalDistance.toStringAsFixed(2);
      });

      return true;
    } catch (e) {
      print(e);
    }
    return false;
  }
```
先程とちがってsetStateのなかに距離の計算結果が_placeDistanceに格納するコードが追加されている。この_placeDistanceをUI実行コマンドの方で受け取ることで画面に距離が表示される。

・計算された距離を画面に表示する。
``` lib/main.dart
// 開智位置と目的位置を入力するためのUI
    SafeArea(
      child: Align(
        alignment: Alignment.topCenter,
        child: Padding(
          padding: const EdgeInsets.only(top: 10.0),
          child: Container(
            decoration: BoxDecoration(
              color: Colors.black38
            ),
      .
      .
      .
                  SizedBox(height: 10),
                  _textField(
                      label: '目的位置',
                      hint: '目的位置を入力',
                      prefixIcon: Icon(Icons.directions_walk),
                      controller: destinationAddressController,
                      focusNode: desrinationAddressFocusNode,
                      width: width,
                      locationCallback: (String value) {
                        setState(() {
                          _destinationAddress = value;
                        });
                      }),
                  SizedBox(height: 10),
                  // ここから追加
                  Visibility(
                    visible: _placeDistance == null ? false : true,
                    child: Text(
                      'DISTANCE: $_placeDistance km',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 16
                      ),
                    ),
                  ),
                  // ここまで追加
                  SizedBox(height: 5),
                  ElevatedButton(
                    onPressed: (_startAddress != '' &&
                        _destinationAddress != '')
                        ? () async {
                      startAddressFocusNode.unfocus();
                      desrinationAddressFocusNode.unfocus();
                      setState(() {
                        if (markers.isNotEmpty) markers.clear();
                        if (polylines.isNotEmpty)
                          polylines.clear();
                        if (polylineCoordinates.isNotEmpty)
                          polylineCoordinates.clear();
                        _placeDistance = null; // 初期化用途として追加
                      });

                      _RouteDistance();
                    }
                        : null,
                    child: Padding(
                      padding: const EdgeInsets.all(8.0),
                      child: Text(
                        'ルート検索'.toUpperCase(),
                        style: TextStyle(
                          color: Colors.white,
                          fontSize: 20.0,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ),
```
・ここまでのコードを実行した結果は以下。
<img width="100" alt="step2_2_18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/892339/24d94bf1-698e-df2f-a52b-ad5a278d14fa.png">

ここまでに使ったコードをgithubに置いたのでリンクを紹介
https://github.com/MY-CODE-1981/my_app

# 3.まとめ
FlutterではGoogleMapプラグインを使って地図アプリを簡単に作成した。そのなかで、Flutterで2つの場所のルートを見つけ距離を計算できる様にした。