ブログ

クラウドエンジニアがFlutterで
ルート探索APIをコールし、結果を表示してみた

2023年 9月 19日

テクノロジー

当社ではPiomatix LBS APIとして、配車・配送ルートの最適化を実現するAPIを提供しています。
Piomatix LBS API
社内利用向けに、このAPIをコールし地図上に結果を表示するWebアプリケーションをFlutterで構築しています。
その実装事例に関して、紹介します。

© OpenStreetMap contributors

目次

1. 自己紹介

社歴10年、クラウドエンジニア歴約3年の中堅エンジニア。
普段はAWSを利用したサービスのバックエンド開発などを担当。
フルスタックエンジニアを目指してFlutter勉強中(歴半年程度)。

2. 地図を表示する

地図を表示するためのライブラリとしてflutter_mapを利用します。
flutter_map
このライブラリを使うことで簡単に地図表示をすることができます。

TilelayerOpenStreetMapのタイルサーバーを指定します。
また、MapOptionscenterで表示する地図の中心を指定できます。
今回はパイオニア川越事業所を中心に表示するようにしました。

冒頭に定義している変数(markers/lines/result/selectO)は後ほど利用します。

map_page.dart

import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; class MapPage extends StatefulWidget { const MapPage({super.key}); @override State createState() => _MapPageState(); } class _MapPageState extends State { // マーカー格納用 final markers = []; // ルート形状格納用 final lines = []; // APIレスポンス格納用 final result = {}; // Outputボタンの状態管理用 bool selectO = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Routing Demo'), backgroundColor: const Color.fromARGB(255, 185, 20, 64), ), body: FlutterMap( options: MapOptions( // 地図の中心座標 center: LatLng(35.93234838643877, 139.47170684564531), zoom: 16, maxZoom: 18, ), children: [ // 地図表示のタイルレイヤー TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.example.app', ), ], ), ); } }

地図を表示することができました。

3. 地点を設定する

ルート探索をするために、出発地、目的地を設定する必要があります。
Flutter_mapは任意の地点にマーカーを表示することが可能です。

マーカーを追加する関数を定義します。

map_page.dart

void addMarker(int number, LatLng latlon) { // アイコンの定義 final Icon icon = Icon(Icons.flag, color: number == 0 ? Colors.green : Colors.pink, size: 40); // マーカーを追加して表示更新 setState(() { markers.add(Marker(point: latlon, builder: ((_) => icon))); }); }

この関数をMapOptionsonLongPressで使います。このオプションは地図上を長押ししたときの処理を定義することができます。
コールバックとして、緯度経度の情報が得られるため、マーカーにその情報を付与しています。

さらに、FlutterMapchildrenに、MarkerLayerを追加します。
これで地図上にマーカーを表示することができるようになります。
ルート形状を表示するためのPolylineLayerも追加しておきます。(ここでは使用しませんが、"5.ルート探索結果を表示する"で使用します)

また、設定したマーカーを削除できるよう地点クリアボタンを地図に重ねて右上に表示するようにし、Stackウィジェットでbody全体を囲います。

map_page.dart

body: Stack(children: [ FlutterMap( mapController: MapController(), options: MapOptions( // 地図の中心座標 center: LatLng(35.93234838643877, 139.47170684564531), zoom: 14, maxZoom: 18, //地図上長押しでマーカーを追加 onLongPress: (tapPosition, point) { if (markers.length < 2) { addMarker(markers.length, point); } }), children: [ // 地図表示のタイルレイヤー TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'com.example.app', ), // ルート形状を表示するレイヤー PolylineLayer(polylines: lines), // 出発地/目的地のマーカーを表示するレイヤー MarkerLayer(markers: markers), ], ), if (markers.isNotEmpty) ...{ // 地点クリアボタン Container( alignment: Alignment.topRight, child: ElevatedButton( style: ElevatedButton.styleFrom( minimumSize: const Size(100, 50), backgroundColor: Colors.grey, elevation: 10, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(5.0), )), // ボタン押下時の処理 onPressed: () { setState(() { markers.clear(); result.clear(); lines.clear(); selectO = false; }); }, child: const Text('地点クリア', style: TextStyle(fontSize: 20))), ) }, ])

地図上任意の場所を長押しすることで、出発地(緑色の旗)、目的地(ピンク色の旗)を順に設定できるようになりました。

4.ルート探索APIを呼ぶ

出発地と目的地の設定ができたので、ルート探索APIをコールします。

API通信は以下の記事を参考にRetrofitを利用して実装しました(本投稿では詳細の説明はしません)。
Retrofit

APIの定義をします。

lbs_api.dart

import 'package:dio/dio.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:retrofit/retrofit.dart'; import 'routing_model.dart'; part 'lbs_api.g.dart'; class API { API._internal() { _dio = Dio(); // ヘッダーの設定 _dio.options.headers['Authorization'] = /*省略*/ client = LbsApiClient(_dio, baseUrl: /*省略*/); } static final _instance = API._internal(); factory API() => _instance; late Dio _dio; late LbsApiClient client; } @RestApi() abstract class LbsApiClient { factory LbsApiClient(Dio dio, {String baseUrl}) = _LbsApiClient; @POST('/navicore/calcRoute') Future routing(@Body() RoutingRequest req); }

APIのリクエスト/レスポンスモデルの定義をします。

RoutingRequestモデルがリクエストボディに含む内容で、それぞれ以下を表します。
 userID: ユーザーの識別子
 pointinfo: 地点情報
その他に時刻や探索条件、車両条件等も設定可能ですが、今回は含めていません。
設定しない場合はルート探索APIで設定されているデフォルトの値でAPIコールされます。

RoutingRequestモデルに関連するPointInfo/LatLonモデルの各要素は以下を表します。
 start: 出発地
 destination: 目的地
 lat: 緯度
 lon: 経度
PointInfoには立寄地も設定可能ですが、今回は含めていません。

routing_model.dart

import 'package:flutter_map/flutter_map.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:latlong2/latlong.dart'; part 'routing_model.g.dart'; @JsonSerializable() class RoutingRequest { final String userID; final PointInfo pointinfo; RoutingRequest(this.userID, this.pointinfo); factory RoutingRequest.fromJson(Map<String, dynamic> json) => _$RoutingRequestFromJson(json); Map toJson() => _$RoutingRequestToJson(this); } @JsonSerializable() class PointInfo { final LatLon start; final LatLon destination; PointInfo(this.start, this.destination); factory PointInfo.fromJson(Map json) => _$PointInfoFromJson(json); Map toJson() => _$PointInfoToJson(this); // マーカーからPointInfoを生成 static PointInfo? fromMarkers(List<Marker> markers) { try { final LatLon start = LatLon(markers[0].point.latitude, markers[0].point.longitude); final LatLon destination = LatLon(markers[1].point.latitude, markers[1].point.longitude); return PointInfo(start, destination); } catch (_) { return null; } } } @JsonSerializable() class LatLon { final double lat; final double lon; LatLon(this.lat, this.lon); factory LatLon.fromJson(Map json) => _$LatLonFromJson(json); Map toJson() => _$LatLonToJson(this); }

RoutingResponseモデルがレスポンスに含まれる内容で、それぞれ以下を表します。
 userID: ユーザーの識別子
 errorCode: 探索エラーメッセージコード。0の場合は探索成功。
 length: 区間距離
 requireTime: 所要時間
 pathPointList: 形状点列(緯度経度のリスト)

routing_model.dart

@JsonSerializable() class RoutingResponse { final String userID; final int errorCode; final double? length; final int? requireTime; final List<Address>? pathPointList; RoutingResponse(this.userID, this.errorCode, this.length, this.requireTime, this.pathPointList); factory RoutingResponse.fromJson(Map json) => _$RoutingResponseFromJson(json); Map toJson() => _$RoutingResponseToJson(this); }

準備が整いましたのでmap_page.dartにAPIをコールする部分を実装します。 Stackウィジェットの要素として追加します。 出発地と目的地が設定されているときにOutputボタン押すことでルート探索APIを呼ぶようにしています。

map_page.dart

// Outputボタン Padding( padding: const EdgeInsets.all(4), child: ElevatedButton( style: ElevatedButton.styleFrom( minimumSize: const Size(100, 40), backgroundColor: selectO ? Colors.amber : Colors.grey, elevation: 10, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20.0), )), onPressed: markers.length > 1 ? () async { if (selectO) { // ボタン選択済みの場合は非選択にし結果を消去 setState(() { selectO = false; lines.clear(); result.clear(); }); } else { setState(() => selectO = true); // markersからpointInfoを生成 final pointInfo = PointInfo.fromMarkers(markers); if (pointInfo != null) { // APIコール final response = await API().client.routing( RoutingRequest("Routing-demo", pointInfo)); // 結果を表示 } } } : null, child: const Text('Output', style: TextStyle(fontSize: 20))), ),

出発地と目的地を設定し、Outputボタンを押してみます。
開発者コンソール上からルート探索APIをコールし、結果が得られていることが確認できました。これを地図上に表示します。

5. ルート探索結果を表示する

ルート形状を描画するための点群の情報はPathPointListに含まれるため、これをflutter_mapが表示できるようPolylineに変換する関数を定義します。

map_page.dart

Polyline buildPolyline(List<Address> pathPointList) { // 緯度経度をLatLng型にしてList化 final latlngs = pathPointList.map((e) => LatLng(e.latitude, e.longitude)).toList(); return Polyline(points: latlngs, strokeWidth: 6, color: Colors.red); }

距離/所要時間はDataTableウィジェットを使用して、表として表示します。

output_table.dart

import 'package:flutter/material.dart'; class RoutingOutputTable extends StatefulWidget { final Map result; const RoutingOutputTable({required this.result, super.key}); @override State createState() => _RoutingOutputTable(); } class _RoutingOutputTable extends State { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(4), child: DataTable( headingRowHeight: 35, headingRowColor: MaterialStateProperty.resolveWith((states) { return Colors.white; }), dataRowHeight: 30, dataRowColor: MaterialStateProperty.resolveWith((states) { return Colors.white; }), columns: const [ DataColumn( label: Text(''), ), DataColumn( label: Expanded(child: Center(child: Text('距離[km]'))), ), DataColumn( label: Expanded(child: Center(child: Text('時間[min]'))), ), ], rows: [ DataRow( cells: [ const DataCell(Text('ルート探索API')), DataCell( Center(child: Text((widget.result['distance'] ?? 'N.D.')))), DataCell( Center(child: Text((widget.result['time'] ?? 'N.D.')))), ], ), ], )); } }

map_page.dartに表示する処理を実装します。
APIコール下の"// 結果を表示"に続けて以下を追加します。

map_page.dart

if (response.errorCode == 0) { setState(() { // 距離 result['distance'] = (response.length! / 1000) .toStringAsFixed(2); // 所要時間 result['time'] = (response.requireTime! / 60) .toStringAsFixed(1); // ルート lines.add( buildPolyline(response.pathPointList!)); }); }

responseerrorCodeがゼロの場合、ルート探索が成功です。エラー処理に関してはここでは割愛しています。
レスポンスから距離/所要時間を求めて、resultに設定することで表に表示します。
ルート形状は前述の関数を呼び出した結果をlinesに追加することで表示します。

最後に、Stackウィジェットの要素として以下を追加します。
結果の表を右下に表示できるようになります。

map_page.dart

if (selectO) ...{ Container( alignment: Alignment.bottomRight, child: RoutingOutputTable(result: result), ) }

以上で、完了です。

ルート探索APIをコールし、結果を地図上に表示することができました。

LBS APIは、パイオニアのナビゲーションシステム開発において構築してきた、独自のルートテクノロジーをAPIを通じて利用できるサービスです。実装方法などについて、お気軽にお問い合わせください。

関連記事

導入事例 株式会社電脳交通様 高精度なルートテクノロジーで、タクシー配車の効率アップに貢献 クラウド型タクシー配車システムにPiomatix LBS APIを採用

お問い合わせ・
資料ダウンロード

「Piomatix LBS API」に関する
お問い合わせ・資料ダウンロードはこちら。