0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RouteAPIのPolylineをMap上に表示する

Last updated at Posted at 2024-10-19

実装イメージ

スクリーンショット 2024-11-03 133932.png

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

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

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

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')),
            ),
          ),
        ],
      ),
    );
  }
}

参考文献

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?