2023年 9月 19日
テクノロジー
当社ではPiomatix LBS APIとして、配車・配送ルートの最適化を実現するAPIを提供しています。
Piomatix LBS API
社内利用向けに、このAPIをコールし地図上に結果を表示するWebアプリケーションをFlutterで構築しています。
その実装事例に関して、紹介します。
© OpenStreetMap contributors
社歴10年、クラウドエンジニア歴約3年の中堅エンジニア。
普段はAWSを利用したサービスのバックエンド開発などを担当。
フルスタックエンジニアを目指してFlutter勉強中(歴半年程度)。
地図を表示するためのライブラリとしてflutter_mapを利用します。
flutter_map
このライブラリを使うことで簡単に地図表示をすることができます。
Tilelayer
でOpenStreetMap
のタイルサーバーを指定します。
また、MapOptions
のcenter
で表示する地図の中心を指定できます。
今回はパイオニア川越事業所を中心に表示するようにしました。
冒頭に定義している変数(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', ), ], ), ); } }
地図を表示することができました。
ルート探索をするために、出発地、目的地を設定する必要があります。
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))); }); }
この関数をMapOptions
のonLongPress
で使います。このオプションは地図上を長押ししたときの処理を定義することができます。
コールバックとして、緯度経度の情報が得られるため、マーカーにその情報を付与しています。
さらに、FlutterMap
のchildren
に、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))), ) }, ])
地図上任意の場所を長押しすることで、出発地(緑色の旗)、目的地(ピンク色の旗)を順に設定できるようになりました。
出発地と目的地の設定ができたので、ルート探索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をコールし、結果が得られていることが確認できました。これを地図上に表示します。
ルート形状を描画するための点群の情報は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!)); }); }
response
のerrorCode
がゼロの場合、ルート探索が成功です。エラー処理に関してはここでは割愛しています。
レスポンスから距離/所要時間を求めて、result
に設定することで表に表示します。
ルート形状は前述の関数を呼び出した結果をlines
に追加することで表示します。
最後に、Stack
ウィジェットの要素として以下を追加します。
結果の表を右下に表示できるようになります。
map_page.dart
if (selectO) ...{ Container( alignment: Alignment.bottomRight, child: RoutingOutputTable(result: result), ) }
以上で、完了です。
ルート探索APIをコールし、結果を地図上に表示することができました。
LBS APIは、パイオニアのナビゲーションシステム開発において構築してきた、独自のルートテクノロジーをAPIを通じて利用できるサービスです。実装方法などについて、お気軽にお問い合わせください。