20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2022-08-15

1.はじめに

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

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

2.地図アプリを作ろう

2-1.地図をアプリで利用するためのAPIキー取得

まずGoogleCloudPlatform(GCP)にアクセスして新しいプロジェクトを作成。
step2_1_1.png

アカウントの作成は国とニーズを適当に選んで二つのチェックボタンをオンにしてから続行ボタンを押す。
step2_1_2.png

以下の画面では右下のスキップを押す。
step2_1_3.png

以下の画面では赤四角のエリアをクリックしてプロジェクトを作成。
step2_1_4.png

以下の画面では右上の新しいプロジェクトを押す。
step2_1_5.png

以下の画面では左側のメニューからAPIとサービスに移動しライブラリを選択。
step2_1_6.png

Maps SDK for Androidを検索。
step2_1_7.png

MapプラットフォームでマップSDKを有効にするボタンを押す。
step2_1_8.png

左側のメニューから認証情報に移動し認証情報を作成を選択。
step2_1_9.png

APIキーを選択しAPIキーをコピー。
step2_1_10.png

ルートを表示するためのDirectionsAPIも同様に有効にしておく。
step2_1_12.png

2-2.地図をアプリで表示するためのコーディング

・新しいFlutterプロジェクトを作成する。プロジェクト名はmy_appとする。
step2_2_1.png

・ここからはgoogle_maps_flutterというプラグインを実装していく。google_maps_flutterは地図を画面に表示する役立つ。プラグインをpubspec.yamlファイルに追加する。プラグインの登録が初回であれば、以下のDart packagesにアクセスしてSearch Packagesバーでgoogle_maps_flutterを検索したのち一番上の名前をクリック。次画面で名前横にあるクリップアイコンを押してプラグインのバージョンをコピー。
step2_2_2.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 // 追加
step2_2_4.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>[
            // ここに地図を追加
          ],
        ),
      ),
    );
  }
}

・以下は実行結果
step2_2_6.png

・StackにてGoogleMap Widgetをバックグラウンドに保持した上で他の必要なウィジェットを追加していく。// ここに地図を追加の箇所をGoogleMap Widgetに置き換える。
1つめはmain.dartの冒頭

lib/main.dart
import 'package:flutter/material.dart';
// Google Mapsのパッケージをインポートする
import 'package:google_maps_flutter/google_maps_flutter.dart'; // 追加

2つめは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;
              },
            ),
          ],
        ),
      ),
    );
  }
}

以下は実行結果
step2_2_7.png

・Googleマップウィジェットで定義されているパラメータの説明は以下の通り。mapControllerマップビューのカメラ位置を制御するために使用される。
step2_2_8.png

2-3.地図で現在地を取得するためのコーディング

・ズームボタンと現在位置ボタンを表示するには、それらを子としてStackウィジェットに追加し、そのウィジェットに応じて配置する。以下にボタンを設計するためのベースとなるコードを示す。

// ボタンの凡例
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,
                              ),
                            ),
                          );
                        },
                      ),
                    ),
                  ),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

以下は実行画面。
step2_2_9.png

現在地ボタンを押すことで設定している座標に画面が移動する。さらにズームインとズームアウトのボタンをすことで画面中の地図のサイズを変えることができる。
step2_2_10.png

・ここからはgeolocatorというプラグインを実装していく。geolocatorはユーザーの現在値を取得するのに役立つ。プラグインをpubspec.yamlファイルに追加する。追加したらPub getを押す。アプリが起動すると画面が自動的に検出された場所に移動するように作成していく。
step2_2_11.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ファイルに追加する。
step2_2_12.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,
                      ),
                    ),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    ),
  ),

・ここまでのコードを実行した結果は以下
step2_2_13.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;
    },
  ),

・ここまでのコードを実行した結果は以下
step2_2_14.png

2-5.地図に経路を表示するためのコーディング

・ここからはflutter_polyline_pointsというプラグインを実装していく。flutter_polyline_pointsはGoogleMapで描画するために使われる。プラグインをpubspec.yamlファイルに追加する。追加したらPub getを押す。

step2_2_16.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,
          ),
        ),
      ),
    ),

・ここまでのコードを実行した結果は以下。ルートが青い線で表示されている。
step2_2_17.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つの地理座標間の距離を計算するために参考元から数式を引用。

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,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    ),

・ここまでのコードを実行した結果は以下。
step2_2_18.png

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

3.まとめ

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

20
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?