実装イメージ
Routes APIとは
ドキュメントリンクはこちらから
今回はCompute Routeを使用します
- RoutesAPIは、既存のDirections APIとDistance Matrix APIを拡張・統合した新しいAPI
- 2023年3月に一般提供が開始された新しいAP
- より有益で柔軟なルート検索と到着予測時刻の精度向上を目的としている
必要なパッケージ
参考文献に記載
pubspec.yaml
dependencies:
google_maps_flutter: # googlemap表示用
flutter_polyline_points: # ポリラインの処理
freezed_annotation: # データクラス作成用
json_annotation: # データクラス作成用
flutter_riverpod: # 状態管理用
dev_dependencies:
build_runner: # # データクラス自動生成用
freezed: # データクラス作成用
json_serializable: # データクラス作成用
Compute Routeのリクエスト(抜粋)
リクエストヘッダー
- APIKeyは事前に取得して、有効化してください
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': 'YourAPIKey',
'X-Goog-FieldMask': 'routes.legs.polyline,routes.optimized_intermediate_waypoint_index'
},
リクエストボディ
Key | 説明 | 値の例 |
---|---|---|
origin | 出発地点:必須 | {"address": origin} |
destination | 目的地:必須 | {"address": destination} |
intermediates | 停止地点、中間地点、通過地点 最大25件 | [{"address": address1},{"address": address2}] |
travelMode | 移動手段 | "DRIVE","BICYCLE","WALK","TWO_WHEELER","TRANSIT" |
routingPreference | ルートの計算方法 | "TRAFFIC_AWARE" |
departureTime | 出発時間 (RFC3339 UTC「Zulu」形式のタイムスタンプ) | departureTime(Timestmp) |
computeAlternativeRoutes | 経路に加えて代替経路を計算するかどうかを指定 | boolean |
routeModifiers | ルートの計算方法に影響を与える条件のセット | {"avoidTolls": boolean, "avoidHighways": boolean, "avoidFerries": boolean} |
optimizeWaypointOrder | trueなら指定された中間ウェイポイントの順序を変更して、ルートの総コストを最小化 | boolean |
languageCode | 言語コード | "ja-JP"など |
units | 単位 | "METRIC" |
Compute Routeのレスポンス(抜粋)
今回はencodedPolyline
を使います
{
"routes": [
{
"route": {
"distanceMeters": 772,
"duration": "165s",
"polyline": {
"encodedPolyline": "_p~iF~ps|U_ulLnnqC_mqNvxq`@",
}
}
}
],
"fallbackInfo": {},
"geocodingResults": {}
}
Flutter側のレスポンス用のクラス
ドキュメントのレスポンスを参考に
@freezed
を使用しflutter pub run build_runner build --delete-conflicting-outputs
でfromJsonメソッドなどを自動生成します。
success_response.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'success_response.freezed.dart';
part 'success_response.g.dart';
@freezed
class SuccessResponse with _$SuccessResponse {
factory SuccessResponse({
@Default([]) List<Route> routes, // 今回ほしいのはコレ
}) = _SuccessResponse;
factory SuccessResponse.fromJson(Map<String, dynamic> json) =>
_$SuccessResponseFromJson(json);
}
route.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'route.freezed.dart';
part 'route.g.dart';
@freezed
class Route with _$Route {
factory Route({
@Default([]) List<RouteLeg> legs,
@Default([]) List<int> optimizedIntermediateWaypointIndex,
}) = _Route;
factory Route.fromJson(Map<String, dynamic> json) => _$RouteFromJson(json);
}
route_leg.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'route_leg.freezed.dart';
part 'route_leg.g.dart';
@freezed
class RouteLeg with _$RouteLeg {
const factory RouteLeg({
Polyline? polyline,
}) = _RouteLeg;
factory RouteLeg.fromJson(Map<String, dynamic> json) => _$RouteLegFromJson(json);
}
polyline.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'polyline.freezed.dart';
part 'polyline.g.dart';
@freezed
class Polyline with _$Polyline {
factory Polyline({
required String? encodedPolyline,
}) = _Polyline;
factory Polyline.fromJson(Map<String, dynamic> json) =>
_$PolylineFromJson(json);
}
API呼び出し用メソッドを扱うサービスクラス作成
route_api_service.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
final routeApiServiceProvider =
Provider<RouteApiService>((ref) => RouteApiService());
class RouteApiService {
final client = http.Client();
Future<SuccessResponse> computeRoute(String origin, String destination,
List<String> intermediates, String departureTime) async {
const String url =
"https://routes.googleapis.com/directions/v2:computeRoutes";
try {
var response = await client.post(
Uri.parse(url),
headers: {
'Content-Type': 'application/json',
'X-Goog-Api-Key': 'YourAPIKEY',
'X-Goog-FieldMask':
'routes.legs.polyline,routes.optimized_intermediate_waypoint_index'
},
body: json.encode({
"origin": {"address": origin},
"destination": {"address": destination},
"intermediates": intermediates
.where((intermediate) => intermediate.isNotEmpty)
.map((intermediate) => {"address": intermediate})
.toList(),
"travelMode": "DRIVE",
"routingPreference": "TRAFFIC_AWARE",
"departureTime": departureTime,
"computeAlternativeRoutes": false,
"routeModifiers": {
"avoidTolls": false,
"avoidHighways": false,
"avoidFerries": false
},
"optimizeWaypointOrder": true,
"languageCode": "ja-JP",
"units": "METRIC"
}),
);
if (response.statusCode == 200) {
return SuccessResponse.fromJson(json.decode(response.body));
} else {
final error = json.decode(utf8.decode(response.bodyBytes))['error'];
final errStr =
"${error['code']} ${error['status']}:\n${error['message']}";
throw errStr;
}
} catch (e) {
if (kDebugMode) {
print(e.toString());
}
rethrow;
}
}
}
画面の状態を定義
map_page_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
part 'map_page_state.freezed.dart';
@freezed
class MapPageState with _$MapPageState {
const factory MapPageState.init() = MapPageStateInit;
const factory MapPageState.data(Set<Polyline> polylines) = MapPageStateData;
const factory MapPageState.error(String message) = MapPageStateError;
}
画面のロジックを定義
map_page_notifier.dart
final mapPageStateNotifierProvider =
StateNotifierProvider.autoDispose<MapPageStateNotifier, MapPageState>(
(ref) => MapPageStateNotifier(
routeApiService: ref.watch(routeApiServiceProvider),
),
);
class MapPageStateNotifier extends StateNotifier<MapPageState> {
final RouteApiService routeApiService;
MapPageStateNotifier({required this.routeApiService})
: super(const MapPageState.init()) {
init();
}
final TextEditingController originController = TextEditingController();
final TextEditingController destinationController = TextEditingController();
final TextEditingController intermediate1Controller = TextEditingController();
final TextEditingController intermediate2Controller = TextEditingController();
final TextEditingController intermediate3Controller = TextEditingController();
List<Color> polylinesColors = [
Colors.red,
Colors.blue,
Colors.yellow,
Colors.green,
];
void init() {
state = const MapPageState.data({});
}
@override
void dispose() {
originController.dispose();
destinationController.dispose();
intermediate1Controller.dispose();
intermediate2Controller.dispose();
intermediate3Controller.dispose();
super.dispose();
}
Future<void> computeRoute() async {
if (originController.text.isEmpty || destinationController.text.isEmpty) {
return;
}
try {
SuccessResponse response = await routeApiService.computeRoute(
originController.text,
destinationController.text,
[
intermediate1Controller.text,
intermediate2Controller.text,
intermediate3Controller.text
],
DateTime.now().add(Duration(minutes: 1)).toUtc().toIso8601String());
if (response.routes.isNotEmpty) {
final encodedPolylines = response.routes
.expand((route) => route.legs)
.map((leg) => leg.polyline?.encodedPolyline)
.where((polyline) => polyline != null)
.toList();
int polylineId = 0;
final polylines = <Polyline>{}; // flutter_polyline_pointsのPolylineクラス
for (var polyline in encodedPolylines) {
PolylinePoints polylinePoints = PolylinePoints();
List<PointLatLng> decodedPoints =
polylinePoints.decodePolyline(polyline!); // flutter_polyline_pointsのメソッドを使用してデコードする
List<LatLng> points = decodedPoints
.map((point) => LatLng(point.latitude, point.longitude))
.toList();
polylines.add(
Polyline(
polylineId: PolylineId("$polylineId"), // uniqueにするためにidを付与
points: points,
width: 5,
color: polylinesColors[polylineId]),
);
polylineId++;
}
state = MapPageState.data(polylines);
}
} catch (e) {
state = MapPageState.error(e.toString());
}
}
}
画面を定義
map_page_view.dart
class MapPageView extends ConsumerStatefulWidget {
const MapPageView({super.key});
@override
_MapViewState createState() => _MapViewState();
}
class _MapViewState extends ConsumerState<MapPageView> {
@override
Widget build(BuildContext context) {
final mapPageState = ref.watch(mapPageStateNotifierProvider);
final notifier = ref.watch(mapPageStateNotifierProvider.notifier);
return Scaffold(
body: Row(
children: [
Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
TextFormField(
controller: notifier.originController,
decoration: const InputDecoration(
labelText: '出発地点',
),
),
const SizedBox(height: 8),
TextFormField(
controller: notifier.intermediate1Controller,
decoration: const InputDecoration(
labelText: '経由地1',
),
),
const SizedBox(height: 8),
TextFormField(
controller: notifier.intermediate2Controller,
decoration: const InputDecoration(
labelText: '経由地2',
),
),
const SizedBox(height: 8),
TextFormField(
controller: notifier.intermediate3Controller,
decoration: const InputDecoration(
labelText: '経由地3',
),
),
const SizedBox(height: 8),
TextFormField(
controller: notifier.destinationController,
decoration: const InputDecoration(
labelText: '目的地',
),
),
const SizedBox(height: 8),
ElevatedButton(
onPressed: notifier.computeRoute,
child: const Text('ポリラインを表示'),
),
],
),
),
),
Expanded(
flex: 2,
child: mapPageState.when(
init: () => const Center(child: CircularProgressIndicator()),
data: (polylines) => GoogleMap(
initialCameraPosition: const CameraPosition(
target: LatLng(35.6809591, 139.7673068),
zoom: 6,
),
polylines: polylines,
),
error: (e) => Center(child: Text('Error: $e')),
),
),
],
),
);
}
}
参考文献