RouteAPIのPolylineをMap上に表示する


## 実装イメージ
![スクリーンショット 2024-11-03 133932.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/844247/cd6e719b-44ef-cc6a-feaa-2bdf984b5933.png)


## Routes APIとは
ドキュメントリンクは[こちら](https://developers.google.com/maps/documentation/routes?utm_source=Website&utm_medium=Blog&utm_campaign=Maps-global-demandgen-website-cs-GMP_UTM_Campaign_Blog&utm_content=routes_api_ga_docs&hl=ja)から
今回は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のレスポンス(抜粋)
[ドキュメント](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=ja&_gl=1*1flibu*_up*MQ..*_ga*OTYwNjgyNjA4LjE3MjkyOTYzNDI.*_ga_NRWSTWS78N*MTcyOTI5NjM0Mi4xLjAuMTcyOTI5NjM0Mi4wLjAuMA..#response-body)

今回は`encodedPolyline`を使います

```json
{
	"routes": [
		{
			"route": {
				"distanceMeters": 772,
				"duration": "165s",
				"polyline": {
					"encodedPolyline": "_p~iF~ps|U_ulLnnqC_mqNvxq`@",
				}
			}
		}
	],
	"fallbackInfo": {},
	"geocodingResults": {}
}
```


---
## Flutter側のレスポンス用のクラス
[ドキュメントのレスポンス](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=ja&_gl=1*17jqwz5*_up*MQ..*_ga*NTY0NTI1MjAzLjE3Mjk3NzQ4NzE.*_ga_NRWSTWS78N*MTcyOTc3NDg3MS4xLjAuMTcyOTc3NDg3MS4wLjAuMA..#response-body)を参考に
`@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](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=ja&_gl=1*1372acn*_up*MQ..*_ga*NTY0NTI1MjAzLjE3Mjk3NzQ4NzE.*_ga_NRWSTWS78N*MTcyOTc3NDg3MS4xLjAuMTcyOTc3NDg3MS4wLjAuMA..#route)
```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);
}
```
[RouteLeg](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=ja&_gl=1*1eupnrq*_up*MQ..*_ga*NTY0NTI1MjAzLjE3Mjk3NzQ4NzE.*_ga_NRWSTWS78N*MTcyOTc3NDg3MS4xLjAuMTcyOTc3NDg3MS4wLjAuMA..#routeleg)
```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](https://developers.google.com/maps/documentation/routes/reference/rest/v2/TopLevel/computeRoutes?hl=ja&_gl=1*1eupnrq*_up*MQ..*_ga*NTY0NTI1MjAzLjE3Mjk3NzQ4NzE.*_ga_NRWSTWS78N*MTcyOTc3NDg3MS4xLjAuMTcyOTc3NDg3MS4wLjAuMA..#polyline)
```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')),
            ),
          ),
        ],
      ),
    );
  }
}
```
---


## 参考文献
https://zenn.dev/slowhand/articles/e98e4dc4b52376

https://developers.google.com/maps/documentation/routes?utm_source=Website&utm_medium=Blog&utm_campaign=Maps-global-demandgen-website-cs-GMP_UTM_Campaign_Blog&utm_content=routes_api_ga_docs&hl=ja

https://pub.dev/packages/flutter_polyline_points

https://pub.dev/packages/google_maps_flutter

https://pub.dev/packages/freezed_annotation

https://pub.dev/packages/freezed

https://pub.dev/packages/json_serializable

https://pub.dev/packages/build_runner