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)にアクセスして新しいプロジェクトを作成。
アカウントの作成は国とニーズを適当に選んで二つのチェックボタンをオンにしてから続行
ボタンを押す。
以下の画面では赤四角のエリアをクリックしてプロジェクトを作成。
以下の画面では左側のメニューからAPIとサービス
に移動しライブラリ
を選択。
MapプラットフォームでマップSDKを有効にする
ボタンを押す。
ルートを表示するためのDirectionsAPI
も同様に有効にしておく。
2-2.地図をアプリで表示するためのコーディング
・新しいFlutterプロジェクトを作成する。プロジェクト名はmy_appとする。
・ここからはgoogle_maps_flutterというプラグインを実装していく。google_maps_flutterは地図を画面に表示する役立つ。プラグインをpubspec.yamlファイルに追加する。プラグインの登録が初回であれば、以下のDart packagesにアクセスしてSearch Packages
バーでgoogle_maps_flutterを検索したのち一番上の名前をクリック。次画面で名前横にあるクリップアイコンを押してプラグインのバージョンをコピー。
・AndroidStudioに戻りプロジェクトのtestフォルダにあるpubspec.yamlを開いた後の該当する行付近にコピーした情報をペースト。プラグインの必要な機能はpubspec.yamlの右上にある青い字のPub get
を押すことによってプロジェクトに追加される。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
google_maps_flutter: ^2.1.8 // 追加
・android/app/build.gradle
を開きminSdkVersion
に20を設定する。
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キーは取得したものに置き換えること。
<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をアプリに追加する準備が完了。
<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が画面全体を占めるように高さと幅を画面のサイズに合わせている。
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>[
// ここに地図を追加
],
),
),
);
}
}
・StackにてGoogleMap Widgetをバックグラウンドに保持した上で他の必要なウィジェットを追加していく。// ここに地図を追加
の箇所をGoogleMap Widgetに置き換える。
1つめはmain.dartの冒頭
import 'package:flutter/material.dart';
// Google Mapsのパッケージをインポートする
import 'package:google_maps_flutter/google_maps_flutter.dart'; // 追加
2つめはStatefulWidgetの宣言直後と後半
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;
},
),
],
),
),
);
}
}
・Googleマップウィジェットで定義されているパラメータの説明は以下の通り。mapControllerマップビューのカメラ位置を制御するために使用される。
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に統合したコードは以下。
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,
),
),
);
},
),
),
),
),
),
),
],
),
),
);
}
}
現在地ボタンを押すことで設定している座標に画面が移動する。さらにズームインとズームアウトのボタンをすことで画面中の地図のサイズを変えることができる。
・ここからはgeolocatorというプラグインを実装していく。geolocatorはユーザーの現在値を取得するのに役立つ。プラグインをpubspec.yamlファイルに追加する。追加したらPub get
を押す。アプリが起動すると画面が自動的に検出された場所に移動するように作成していく。
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。
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
// Geolocatorのパッケージをインポートする
import 'package:geolocator/geolocator.dart';
・取得した現在の場所を格納する変数を定義。
class _MapViewState extends State<MapView> {
// マップビューの初期位置
CameraPosition _initialLocation = CameraPosition(target: LatLng(0.0, 0.0));
// マップの表示制御用
late GoogleMapController mapController;
// 現在位置の記憶用
late Position _currentPosition; // 追加
・ユーザーの現在の場所を取得。late Position _currentPosition;
の後に、
// 現在位置の取得方法
_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でアプリが起動するとすぐにユーザーの現在の場所を取得し画面をその場所に移動させる。上記の直後に以下を記載、
@override
void initState() {
super.initState();
_getCurrentLocation();
}
・現在値を取得するためのカスタムメソッドに経度と緯度を渡す。mapControllerの中身を書き換える。
mapController.animateCamera(
CameraUpdate.newCameraPosition(
CameraPosition(
target: LatLng(
_currentPosition.latitude,
_currentPosition.longitude,
),
zoom: 18.0,
),
),
);
・これでアプリが起動するときにエミュレータがもっている現在値情報に応じて画面を移動させることができる様になる。
2-4.地図で場所を検索するためのコーディング
・ここからはgeocoding_flutterというプラグインを実装していく。geocoding_flutterは場所の住所と座標(経度と緯度)を相互に変換できる。プラグインを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。
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>
の最初に一行追加。
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
の後に追加。
// アドレスの取得方法
_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の下に記載。
// 開智位置と目的位置を入力するための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,
),
),
),
),
],
),
),
),
),
),
),
・ルートの開始位置と目的位置の情報を格納するための変数を作成する。これらの変数は地図上に開始位置と目的位置のマーカーを表示するのに役立つ。
// 住所からプレースマークを取得する
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を作成。
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を作成。
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を定義。
// マーカーをリストに追加する
markers.add(startMarker);
markers.add(destinationMarker);
・開始位置と目的位置のマーカーが大きいときに方方のマーカーは画面外になり表示されなくなる場合がある。そこで、両方が表示されるように地図の表示サイズを調節する。
// フレームに対する相対位置を確認するための計算を行い、それに応じてカメラをパン&ズーム
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内に記述。
// 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を呼び出せる様似変更する。
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を表示する。開始位置と目的位置に応じた場所にマーカーが表示されるはずである。
// 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;
},
),
2-5.地図に経路を表示するためのコーディング
・ここからはflutter_polyline_pointsというプラグインを実装していく。flutter_polyline_pointsはGoogleMapで描画するために使われる。プラグインをpubspec.yamlファイルに追加する。追加したらPub get
を押す。
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する。
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';
・いくつかの変数を定義する。
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の内部。
// 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関数の次あたりにするとよい。
void main() {
runApp(MyApp());
}
class Secrets {
// Google Maps APIキーをここに追加
static const API_KEY = 'GCPで取得したAPIキーを記入';
}
・GoogleMap上にポリラインを表示するためにコードを追加
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ウィジェット後半にに追加する。
// 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を初期化するコードを追加する。
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,
),
),
),
),
・ここまでのコードを実行した結果は以下。ルートが青い線で表示されている。
・マーカーを適切に配置し、ルートを描画したら、2つの場所の間の距離を計算して描画できる様にする。そのために、ルート全体をいくつかの小さな部分に分割し、それらの距離を計算してから、それらを合計する。距離計算のために三角関数を使える様にする。
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つの地理座標間の距離を計算するために参考元から数式を引用。
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つの座標間の距離を計算し、それらを合計して合計距離を求める。
// 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実行コマンドの方で受け取ることで画面に距離が表示される。
・計算された距離を画面に表示する。
// 開智位置と目的位置を入力するための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,
),
),
),
),
],
),
),
),
),
),
),
ここまでに使ったコードをgithubに置いたのでリンクを紹介
https://github.com/MY-CODE-1981/my_app
3.まとめ
FlutterではGoogleMapプラグインを使って地図アプリを簡単に作成した。そのなかで、Flutterで2つの場所のルートを見つけ距離を計算できる様にした。