このページは、ゆるいバスナビWebアプリ「ちどりーど」で使用した技術情報を共有するために作りました。
(関連ページ)
ちどりーど:GTFS-RT・OpenTripPlannerを使ったかんたんバス案内アプリ(コンセプト)
UDC2018でのちどりーど発表資料、時間切れで出せなかった動画付きw(2018-03-15および16)
(ちどりーどはアプリケーション部門の情熱枠でしぶとく爪痕を残しました👍)
ちどりーどサイトおよび乗車モードのデモはこちら。
※こちらは3/20まで閉鎖し、新サイトに移行しました!!!
ちどりーどサイト全コード@Github
Leafletでのマーカー、現在位置watch、layers、Classのextendの使い方などなど、一通り辿ってますので、参考になるかなっと。
公共交通オープンデータ最前線 in インターナショナルオープンデータデイ2019 での登壇資料 (2019-3-2 東京大学 生産技術研究所 An棟2階 コンベンションホール)
「GTFSリアルタイムで変わるバスの世界 - Code for Sagaでのバスオープンデータに対する取り組み」資料
ちどりーどって?
飲食店でバスボタンを押すと、最寄りのバス停までナビして、バスに乗車後もどこにいるのかナビするという、ゆるいバスナビWebアプリ「ちどりーど」。
旧Webサイトは閉鎖しました。
2019年2月の時点で佐賀県を走るコミュニティバスを含めた会社9社中2社がGTFSデータを出してます。そのため、バス停表示も佐賀市と武雄、太良方面に偏っています。宇宙科学館、竹崎ガニ、武雄のあの図書館はOKです。
小城方面をカバーする、昭和バスもデータ公開に前向きなので、とても期待しています。
佐賀市交通局のバスには、GPS付端末(スマホ?)が搭載されており、現在地と通過時間などリアルタイム情報を発信します。
こちらのページで佐賀市営バスの現在位置をとりあえず確認できます。
ちどりーどの設定(ニックネームと目的のバスをスマホに記録するだけ)を行うと、バスをクリックすると、どこに向かっているか分かるようになります。
環境
実用的なWebアプリを作るには、結局httpsにする必要があります。
しかし、暗号化通信をOpenTripPlanner APIに対応させる際に少し困りました。
以下のソフトウェアは、2018年12月2019年3月時点で全て最新のバージョンを使いました。
さくらのクラウド 2core 4GB 20GB SSD ストレージプラン(お金出す分使いやすかった)- WebARENA 1core 1GB 20GB SSD (OTPはメモリ喰いとのことですが、佐賀のバスデータ程度で、デモ表示くらいしかしない場合は問題ありません。それよりも、クライアントの処理性能がものを言うようです。)
- ubuntu 18.04.1 LTS
- java openjdk version "1.8.0_191"
- https化に必要な証明書 (無料のCertbot (let's encrypt) を使用)
- DNSでのホスト名登録 市販のサービスを利用
- Apache/2.4.29 オープンデータがhttp通信の場合、CORSで引っかかるため、ProxyPassなどで自サイトからファイルをダウンロードしているかのように見せかけてください。というわけで、ウェブアプリ作成の利便のため、オープンデータのプロバイダの方々には、できるだけセキュア通信(https)でデータを発行してもらえるとうれしいです。
- php 7.2
- npm (GTFS-RTのpbファイルや、OTPクエリ結果中のエンコードされた経路をWebブラウザで使用するとき、Browserifyのようなrequire補完ツールが必要。)
- leaflet(マーカーなどのプラグインはGithubにあるものを使用しました。)
- OpenTripPlanner(https対応の設定にしても動かなかったため、Apacheでmod-rewriteとphpの合わせ技でなんとか対応)
サーバとドメイン以外は無料!
言語
- JavaScript
- HTML5
- PHP
- CSS3
今回バックエンド(データベース)は使用しませんでした。もっと速くて厳密なものを作ろうとするなら市販mBaaS、PostGISなどの利用を考えた方がいいと思います。
また、使い回しなど考えると、Webアプリ(PWA)となるべくメジャーな言語でシンプルに作るということも必要かなと。
このページに来てくれる人は、GTFSとOpenTripPlanner(以下、OTP)、leafletに関心ある人が大半だと思いますので、その辺を重点的に書こうと思います。
OTP導入
- Javaインストール (javaの起動コマンドは512MBです。)
- OTP用ディレクトリ設置
- 例えば佐賀市のバスデータセットページに行って、静的GTFSファイル設置(zipデータのままディレクトリに置く。佐賀市のデータは大きくないので、データのクリッピングの必要はなく、そのまま使用。)
- 動的GTFSファイル(*.pb)をOTPで使用するため、router-config.jsonを作成する
- 公式サイトの説明通りに、OTPをインストール
ちどりーどの場合、OTP用ディレクトリで以下のコマンドを実行。
alert.pbは、実際の値が検証できず本当に正しいかよくわかりません。何か運行トラブルがあれば出てくるのでしょう。
vehicle.pb、route.pbは以下の書き方でなんとか動作してます。また、佐賀市バスサーバの負荷を気にして、知人にプロキシ立ててもらっています。
{
"routingDefaults": {
"numItineraries": 5,
"walkSpeed": 1.5,
"stairsReluctance": 4.0,
"carDropoffTime": 240
},
"updaters": [
// GTFS-RT service alerts (frequent polling)
{
"type": "real-time-alerts",
"frequencySec": 30,
"url": "{下のサーバのプロキシ}/alert.pb",
/*"url": "http://opendata.sagabus.info/alert.pb",*/
"feedId": "1"
},
// Polling for GTFS-RT TripUpdates)
{
"type": "stop-time-updater",
"frequencySec": 60,
"sourceType": "gtfs-http",
"url": "{下のサーバのプロキシ}/route.pb",
/*"url": "http://opendata.sagabus.info/route.pb",*/
"feedId": "1"
},
// Polling for GTFS-RT TripUpdates)
{
/*"type": "stop-time-updater",*/
"type": "real-time-alerts",
"frequencySec": 30,
"sourceType": "gtfs-http",
"url": "{下のサーバのプロキシ}/vehicle.pb",
/*"url": "http://opendata.sagabus.info/vehicle.pb",*/
"feedId": "1"
}
]
}
wget https://repo1.maven.org/maven2/org/opentripplanner/otp/1.3.0/otp-1.3.0-shaded.jar
java -Xmx512M -jar otp-1.3.0-shaded.jar --build /{homedir}/otp --inMemory --server &
javaコマンドでは、公式ページにあるようなポート変更できるオプションを実行可能なのですが、secureにはならなかったです。
そのため、例えば、JR佐賀駅ー佐賀大学まで経路検索をhttpsで行う際、一旦内部でhttpのOTPにリダイレクトし、PHPで結果をエコーというなんちゃってプロキシ的な処理を行いました。(注:OTPへのリクエストで、日付が過去だと結果がでてこないので、適当にURL内を書き換えてリクエストしてください。)
また、--serverオプションで一応ブラウザインターフェースは使わないように設定しました。
OTPインストール後、apiで{自分のサーバ}:8080/otp/routers/default
として、きちんとXMLデータが出てくればインストール成功です。ちなみに、OTPに使っている佐賀市のバスデータ(佐賀市営バスと祐徳バスの2社のみ)範囲(<polygon>
タグ内の座標データ)をleafletに描画したページはこちら。
OTPインストール後のデータ確認例
<RouterInfo>
<routerId>default</routerId>
<polygon>
<type>Polygon</type>
<coordinates>130.218366</coordinates>
<coordinates>32.954426</coordinates>
<coordinates>130.202581</coordinates>
<coordinates>32.958972</coordinates>
<coordinates>130.108315</coordinates>
<coordinates>32.995287</coordinates>
<coordinates>130.044807</coordinates>
<coordinates>33.041737</coordinates>
<coordinates>129.97879</coordinates>
<coordinates>33.095585</coordinates>
<coordinates>129.948997</coordinates>
<coordinates>33.141351</coordinates>
<coordinates>129.944592</coordinates>
<coordinates>33.154726</coordinates>
<coordinates>129.948124</coordinates>
<coordinates>33.194944</coordinates>
<coordinates>129.967549</coordinates>
<coordinates>33.2409</coordinates>
<coordinates>129.991168</coordinates>
<coordinates>33.254302</coordinates>
<coordinates>130.32584</coordinates>
<coordinates>33.34</coordinates>
<coordinates>130.36864</coordinates>
<coordinates>33.24735</coordinates>
<coordinates>130.36946</coordinates>
<coordinates>33.24497</coordinates>
<coordinates>130.37085</coordinates>
<coordinates>33.23484</coordinates>
<coordinates>130.37042</coordinates>
<coordinates>33.22955</coordinates>
<coordinates>130.37036</coordinates>
<coordinates>33.22939</coordinates>
<coordinates>130.3703</coordinates>
<coordinates>33.22923</coordinates>
<coordinates>130.220234</coordinates>
<coordinates>32.955407</coordinates>
<coordinates>130.218366</coordinates>
<coordinates>32.954426</coordinates>
</polygon>
<buildTime>1546721408790</buildTime>
<transitServiceStarts>1514732400</transitServiceStarts>
<transitServiceEnds>1585666800</transitServiceEnds>
<transitModes>
<transitModes>BUS</transitModes>
</transitModes>
<centerLatitude>33.22022</centerLatitude>
<centerLongitude>130.25746</centerLongitude>
<hasParkRide>false</hasParkRide>
<travelOptions>
<travelOptions>
<value>TRANSIT,WALK</value>
<name>TRANSIT</name>
</travelOptions>
<travelOptions>
<value>BUS,WALK</value>
<name>BUS</name>
</travelOptions>
<travelOptions>
<value>WALK</value>
<name>WALK</name>
</travelOptions>
<travelOptions>
<value>BICYCLE</value>
<name>BICYCLE</name>
</travelOptions>
<travelOptions>
<value>CAR</value>
<name>CAR</name>
</travelOptions>
<travelOptions>
<value>TRANSIT,BICYCLE</value>
<name>TRANSIT_BICYCLE</name>
</travelOptions>
<travelOptions>
<value>CAR,WALK,TRANSIT</value>
<name>KISSRIDE</name>
</travelOptions>
</travelOptions>
<lowerLeftLongitude>129.944592</lowerLeftLongitude>
<lowerLeftLatitude>32.954426</lowerLeftLatitude>
<upperRightLongitude>130.37085</upperRightLongitude>
<upperRightLatitude>33.34</upperRightLatitude>
<hasBikePark>false</hasBikePark>
<hasBikeSharing>false</hasBikeSharing>
</RouterInfo>
九州の佐賀県のところに地図の中心を持ってくると、赤い枠線と青い点があります。青い点はバス停情報です。あちこちクリックするとGTFS-RTのバス停時刻が出ます。
Protocol Buffersのデコード
リアルタイムの情報は、バスのGPSから集約した情報を、定型の機械可読かつ小さいサイズに圧縮したProtocol Buffersというスキーマ言語で書かれた形式にして逐次配信されます。これを解読するコードは当然ありますが、スクリプト内にrequireがあるため、ブラウザで使えません。
一旦Webにあるテンプレートを見つけて、それにデコードと欲しいデータを定期的にLocalStorageにダウンロードするコードを追加しました(getVehicle.js)。それを、npmとbrowserifyでrequire対応のファイル(vehicle.js)に変換し、WebページにJavaScriptコードとして追加しました。
npm install -g browserify
npm install gtfs-realtime-bindings
browserify getVehicle.js -o vehicle.js
var GtfsRealtimeBindings = require('gtfs-realtime-bindings');
var request = require('request');
var vehicles = new Array();
function getVehiclePosistion() {
var requestSettings = {
method: 'GET',
url: '{GTFS-RTファイルを置いてあるサーバ}/vehicle.pb',
encoding: null
};
request(requestSettings, function (error, response, body) {
if (!error && response.statusCode == 200) {
vehicles = [];
var feed = GtfsRealtimeBindings.FeedMessage.decode(body);
feed.entity.forEach(function(entity) {
//vehicle.pbのうち欲しいデータだけ配列にプッシュしてます。
vehicles.push([entity.vehicle.vehicle.id,entity.vehicle.position.latitude,entity.vehicle.position.longitude,entity.vehicle.trip.trip_id,entity.vehicle.timestamp.low]);
});
localStorage.setItem('vehicle', JSON.stringify(vehicles));
}
});
}
//30秒くらいでデータが更新されているようなのですが、早めに更新してます。
setInterval(getVehiclePosistion,7000);
htmlファイルに以下のタグを埋め込み。
<script src="vehicle.js"></script>
乗車モードデモ画面
ちどりーどは、佐賀のバスデータを入れているため、基本佐賀に来ないと使えないし、実際にバスに乗ってみないと面白さが伝わらないと思われるアプリです。(データを入れ替えればどこのデータでも動きます。比較的小さなデータセットを想定しています。)そのため、デモモードを用意しています。
ここでは、設定なしでそのままダイレクトに解説できる「乗車モードデモ」のコードについて書きます。実際の乗車モードは、解説文などが入ってません。
乗車モードとは
バスに乗って5m/sec以上のスピードを検知すると画面が切り替わって、地図上にバスと自分(スマホ)の現在位置、到着したいバス停までの直線距離がわかるので、ちゃんと正しい方向に帰っているか確認できます。
スピード検知
スピードは現在位置を取得する関数navigator.geolocation.watchPosition
の中に含まれている、position.coords.speed
を使用します。
(自転車や普通車に乗ったり、走ったりして閾値を5m/secに決めました。普通車より少し遅めにした方が反応がいいことがわかりましたw)
バス位置の特定、バス停までの距離
https://qiita.com/chiyoyo/items/b10bd3864f3ce5c56291 を参考に、JavaScriptに書き換えました。
function checkDistance(lat1, lon1, lat2, lon2) {
var mode=true;
// 緯度経度をラジアンに変換
var radLat1 = lat1 * (Math.PI / 180); // 緯度1
var radLon1 = lon1 * (Math.PI / 180); // 経度1
var radLat2 = lat2 * (Math.PI / 180); // 緯度2
var radLon2 = lon2 * (Math.PI / 180); // 経度2
// 緯度差
var radLatDiff = radLat1 - radLat2;
// 経度差算
var radLonDiff = radLon1 - radLon2;
// 平均緯度
var radLatAve = (radLat1 + radLat2) / 2.0;
// 測地系による値の違い
var a = 6378137.0; //6377397.155; // 赤道半径
var e2 = 0.00669438002301188; //0.00667436061028297; // 第一離心率^2
var a1e2 = 6335439.32708317; // 赤道上の子午線曲率半径
var sinLat = Math.sin(radLatAve);
var W2 = 1.0 - e2 * (sinLat*sinLat);
var M = a1e2 / (Math.sqrt(W2)*W2); // 子午線曲率半径M
var N = a / Math.sqrt(W2); // 卯酉線曲率半径
var t1 = M * radLatDiff;
var t2 = N * Math.cos(radLatAve) * radLonDiff;
var dist = Math.sqrt((t1*t1) + (t2*t2));
return dist;
}
stop_times.txt
比較的重い(13.9MB)バス系統IDデータセットstop_times.txtをデータベースに入れずに取得するためのスクリプトをPHPで作成。
stop_times.txtとは、GTFSファイル群の一つで、バス系統ID、通過バス停の順番などが入っています。
OTPでは通過バス停の順番が出てこないようでした。順番のデータが欲しいのに。
"trip_id","arrival_time","departure_time","stop_id","stop_sequence","stop_headsign","pickup_type","drop_off_type","shape_dist_traveled","timepoint"
"1_平日_06時31分_系統11","06:31:00","06:31:00","1001002-03","1","23 犬井道・大詫間(大崎・西古賀・川副 経由)","0","1","",""
"1_平日_06時31分_系統11","06:31:00","06:31:00","1002003-10","2","23 犬井道・大詫間(大崎・西古賀 経由)","0","0","",""
"1_平日_06時31分_系統11","06:32:00","06:32:00","1002004-20","3","23 犬井道・大詫間(大崎・西古賀 経由)","0","0","",""
...
<?php //stop_times.txtから指定したtrip_idを抽出
//クエリを整理
$queryStr = urldecode($_SERVER['QUERY_STRING']);
$queryStr = str_replace('tid=', '', $queryStr);
//stop_times.txtは大量のバス系統IDが入ったファイルのため、普通はこの辺をデータベースに入れて運用する
$st = file('./busData/stop_times.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$data = array();
foreach($st as $stval) {
if (mb_strpos($stval, $queryStr) !== false) {
$el = explode(',', str_replace('"', '', $stval));
$data[] = $el;
}
}
print json_encode($data,JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES);
?>
乗車モードデモnuDemo.html全文
Leafletで通過番号付きバス停を描画します。
その上にバスアイコンを設置して、5秒ごとに再描画します。これで、バスが動いているようにします。
アラームのデモもつけているので、バスイケメンによるメッセージも出ます。
画面下部にはバスと到着予定のバス停との直線距離が出ますが、本当は直線距離でなくて、ルート上の距離を出すべきなのでしょう。
ちなみにバスイケメンとはこんなお方
乗車モードデモnuDemo.html全文
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, maximum-scale=1, user-scalable=no">
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com; style-src * 'unsafe-inline'; script-src * 'unsafe-inline' 'unsafe-eval'">
<meta http-equiv="refresh" content="300;URL=nuDemo.html">
<link rel="stylesheet" href="css/leaflet.css" type="text/css">
<script src="js/leaflet.js" type="text/javascript">
</script>
<link rel="stylesheet" href="css/L.Icon.Pulse.css" type="text/css">
<script type="text/javascript" src="js/L.Icon.Pulse.js">
</script>
<script type="text/javascript" src="js/Leaflet.Icon.Glyph.js">
</script>
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous" type="text/css">
<link rel="stylesheet" type="text/css" href="css/style.css">
<script type="text/javascript" src="js/base.js">
</script>
<script src="js/vehicle.js" type="text/javascript">
</script>
<script src="js/motion.js" type="text/javascript">
</script>
<style type="text/css">
body {
padding: 0;
margin: 0;
}
html, body, #map {
height: 100%;
width: 100vw;
}
#realBs {
position: absolute;
z-index: 10000;
top: 15px;
right: 15px;
}
#realBs2 {
position: absolute;
z-index: 10000;
top: 80px;
right: 15px;
}
#info {
position: absolute;
z-index: 10000;
bottom: 0px;
width: 100%;
font-size: small;
background-color: rgba(255,0,0,0.7);
}
#result {
position: absolute;
z-index: 10001;
background-color: rgba(0,0,0,0.9);
}
#demoMessage {
position: absolute;
top: 4rem;
left: 50px;
max-width: 80%;
z-index: 10000;
background-color: rgba(255,255,255,0.8);
display: inline-box;
justify-content: center;
align-items: center;
border: solid 3px red;
border-radius: 10px;
color: black;
}
#demoMessage p {
margin: 5;
}
#demoMessageTop {
position: absolute;
z-index: 500;
top: 0px;
width: 100%;
background-color: green;
}
#demoMessageTop p {
font-size: small;
line-height: 1em;
margin-left: 70px;
margin-right: 70px;
margin-top: 0;
margin-bottom: 0;
}
</style>
<script type="text/javascript">
//乗車モードデモ画面
//1. JR佐賀駅から佐賀大学前までにルート固定
//佐賀大学 33.242264,130.291517
//2. その路線上のバス停を描画
//3. 車両現在地と目的バス停間の距離を計測
//乗車後のアクション
//trip_id引用
const station = [33.264007,130.296483];
const univ = [33.242264,130.291517];
let chara; //ナビキャラ設定
if (localStorage.getItem('navichara')) {
chara = localStorage.getItem('navichara') + '.png';
} else {
chara = 'n2.png';
}
let nickname; //お名前設定
if (localStorage.getItem('nickname')) {
nickname = localStorage.getItem('nickname');
} else {
nickname = 'テスト';
}
//初期化
localStorage.removeItem('tripIdList');
let now = new Date();
//プラン: ルート検索する
let routeQuery = readXML('https://chido.summarcat.work/otp/routers/default/plan?fromPlace=' + station[0] + ',' + station[1] + '&toPlace=' + univ[0] + ',' + univ[1] + '&time=' + now.getHours() + ':' + now.getMinutes() + '&date=' + (now.getMonth() + 1) + '-' + now.getDate() + '-' + now.getFullYear() + '&mode=WALK,TRANSIT&maxWalkDistance=500&arriveBy=false&numItineraries=1');
let item = routeQuery['plan']['itineraries'];
var toStop = new Array(); //バス停の情報
var tripId; //通過バスの系統ID
//ルート検索結果から最初に出現するバス停とバスの系統IDを取得する。
item.some(function(el) {
el['legs'].some(function(el2) {
if (el2['mode'] == 'BUS') {
toStop = [el2['to']['stopId'],el2['to']['name'],el2['to']['lat'],el2['to']['lon']];
tripId = el2['tripId'];
return true;
}
});
});
function findMyBus() {
document.getElementById('info').innerHTML = '5秒程度お待ちください…';
//現在地から佐賀大学に向かっているバスの中で、もっとも近いバスを探索
var vlat,vlon;
let tid = tripId.split(':');
//乗車バス系統ID(trip_id)をgetTripId.phpから抽出
//stop_id群のうち、現在地との最短距離にあるstop_idが乗車バス停とする
function getTripIdListFromStopTimes(trip_id) {
let stopTimes = readXML('https://chido.summarcat.work/getTripId.php?tid=' + trip_id);
return stopTimes; //系統のリスト
}
//系統のリストを保存
var tripIdList = getTripIdListFromStopTimes(tid[1]);
localStorage.setItem('tripIdList', JSON.stringify(tripIdList));
localStorage.setItem('stopidRideOn', JSON.stringify(toStop));
}
//地図とバス表示
function showMap() {
document.getElementById('demoMessageTop').innerHTML = '<p>デモのためバスは5秒ごとに動かしています<\/p>';
//地図描画
var mymap = L.map('map');
L.control.scale({imperial:false}).addTo(mymap);
//車両位置をセット
function setBusLocation(){
mymap.setView([station[0], station[1]], 13);
//OpenStreetMapをロード
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: 'Map data © <a href="http://openstreetmap.org" target="_blank">OpenStreetMap<\/a> contributors, '
}).addTo(mymap);
var pulsingIcon = L.icon.pulse({iconSize:[15,15],color:'#1199fb'});
var busIcon = L.icon({iconUrl: 'img/bus24x24g.png',iconSize: [40,40], zIndexOffset: 10010});
var iconStation = L.icon.glyph({prefix: 'fa' ,glyph: 'train'});
var iconSagaUniv = L.icon.glyph({prefix: 'fa' ,glyph: 'university'});
//ランドマーク
L.marker([station[0],station[1]],{icon: iconStation, zIndexOffset: 10000}).addTo(mymap).bindPopup('<p>JR佐賀駅<\/p>');
L.marker([univ[0],univ[1]],{icon: iconSagaUniv, zIndexOffset: 10000}).addTo(mymap).bindPopup('<p>佐賀大学<\/p>');
//バス停
var tripList = new Array();
tripList = JSON.parse(localStorage.getItem('tripIdList'));
var cn = 0;
var arr = new Array();
var vehicles1 = new Array();
var flag = 0;
while (cn < tripList.length) {
//tripList[cn][3] 標柱id
//arr = findBSLocationFromStopId(tripList[cn][3].replace(/\"/g,''));
arr = findBSLocationFromStopId(tripList[cn][3]);
if (arr != null) {
let latBs = arr[4];
let lngBs = arr[5];
let hyochu = arr[2];
let arrival = tripList[cn][2];
let a = arrival.split(':');
//目的のバス停があれば表示を強調
let iconNumber = L.divIcon({html: '<div>' + (cn+1) + '<\/div>', className: 'numIcon', iconSize: [30,30]});
let iconNumber2 = L.divIcon({html: '<div>' + (cn+1) + '<\/div>', className: 'numIcon2', iconSize: [30,30]});
if (hyochu != toStop[1]) {
if (cn == 0) {
L.marker([latBs,lngBs],{icon: iconNumber, zIndexOffset: -1000}).addTo(mymap).bindPopup('<p>「乗車したバス停」<br>'+ hyochu + '<br>' + a[0] + ':' + a[1] + '発<\/p>').openPopup();
} else {
//その他のバス停
L.marker([latBs,lngBs],{icon: iconNumber, zIndexOffset: -1000}).addTo(mymap).bindPopup('<p>'+ hyochu + '<br>' + a[0] + ':' + a[1] + '発</p>');
}
} else {
//目的のバス停
L.marker([latBs,lngBs],{icon: iconNumber2, zIndexOffset: -1000}).addTo(mymap).bindPopup('<p>「帰りのバス停」<br>' + hyochu + '<br>' + a[0] + ':' + a[1] + '発<\/p>');
flag = cn + 1;
}
vehicles1.push([latBs,lngBs,(cn+1)]);
}
cn++;
}
//リアルタイム車両位置表示
var layerGroup = L.layerGroup().addTo(mymap);
var count = 0;
setInterval(refreshBusLocation, 5000);
function refreshBusLocation() {
document.getElementById('info').innerHTML = '';
layerGroup.clearLayers();
bsList = toStop[1];
//車両
let latBr1 = vehicles1[count][0];
let lngBr1 = vehicles1[count][1];
let sidNum = vehicles1[count][2]; //バス停番号
vInfo = '<p align="center">佐賀に来て市営バスに乗ってみて!<br><img src="/img/busOmiyage.jpg" width="50%"><\/p>';
L.marker([latBr1, lngBr1],{icon:busIcon, zIndexOffset: 10100}).bindPopup(vInfo).addTo(layerGroup);
//バスと到着バス停間の距離をinfoに表示
var distance = checkDistance(toStop[2],toStop[3],latBr1,lngBr1);
if (flag > sidNum) {
document.getElementById('info').innerHTML = toStop[1] + 'まであと' + Math.ceil(distance) + 'm!';
} else {
document.getElementById('info').innerHTML = '';
}
//アラーム画面(デモモード独自の処理)
if (distance < 1100 && distance >= 900 && flag > sidNum) {
document.getElementById('result').innerHTML = '<p class="messagesNickname2">' + nickname + 'さん<\/p><p class="messages2">あと1km!<\/p><img class="focus" src="img/' + chara + '">' + '<audio id="player" autoplay><source src="/sound/warning1.mp3" type="audio/mp3"><\/audio>';
setTimeout(function(){
document.getElementById('result').innerHTML = '';
document.getElementById('info').innerHTML = toStop[1] + 'まであと' + Math.ceil(distance) + 'm!';
}, 5000);
}
if (distance < 300 && flag >= sidNum) {
document.getElementById('result').innerHTML = '<p class="messagesNickname2">' + nickname + 'さん<\/p><p class="messages2">すぐ降りて!<\/p><img class="focus" src="img/' + chara + '">' + '<audio id="player" autoplay><source src="/sound/warning1.mp3" type="audio/mp3"><\/audio>';
setTimeout(function(){
document.getElementById('result').innerHTML = '';
}, 5000);
}
if (count >= 0 && count <= 2) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>バスに乗ると、乗車したバスを自分の現在地とバスの距離から特定します。<\/p><\/div>';
}
if (count > 2 && count <= 4) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>設定画面で決めた「帰りのバス停」までのバスルートと位置が表示されます。<\/p><\/div>';
}
if (count > 4 && count <= 6) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>目的のバス停近くになるとアラームが出ます。<\/p><\/div>';
}
if (count > 6 && count <= 8) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>バスに乗った後も自分のバスの位置とバス停を確認して不安を解消します!<\/p><\/div>';
}
if (count > 14 && count <= 16) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>赤い円は停車するバス停で、数字は停車する順番です。<\/p><\/div>';
}
if (count > 16 && count <= 18) {
document.getElementById('dm').innerHTML = '<div id="demoMessage"><p>ちなみに表示中のルートはOpenTripPlanner(オープンソースのルート検索ツール)で検索しています。<\/p><\/div>';
}
if (count > 18) {
document.getElementById('dm').innerHTML = '';
}
count++;
if (count >= vehicles1.length) {
count = 0;
}
}
}
setBusLocation();
}
</script>
<title>ぬっ。</title>
</head>
<body onload="findMyBus(); showMap();">
<div id="demoMessageTop"></div>
<div id="dm"></div>
<div id="result"></div>
<div id="realBs">
<button class="btnCircle" type="button" onclick="goHomePage()">戻る</button>
</div>
<div id="realBs2"></div>
<div id="map"></div>
<div id="info"></div>
</body>
</html>
OTPのpoint情報を使った経路描画
ちどりーどには、ナビアプリによくあるOTP経路表示があります。
JR佐賀駅でバスボタンを押して、佐賀大学前まで向かうとします。
経路パネルは以下の画像となります。
この画面で赤い帯になっている「Map」をクリックすると以下の地図が出ます。
この青線は、point情報をデコードして組み立てた結果です。
この部分の実装をします。
バスボタンを押下した時、OTPに以下のリクエストを投げています。(注:OTPへのリクエストで、日付が過去だと結果がでてこないので、適当にURL内を書き換えてリクエストしてください。)
そこで、numItinerariesを1に変更して直接ブラウザにクエリを投げてみます。
その結果は以下の通りになります。JR佐賀駅から佐賀大学前までの経路1つを要求しました。
JSONデータの構成は、(目を凝らしてみると)最初に経路のサマリー、次に経路の構成パーツとなってます。
クエリ結果
{"requestParameters":{"date":"02-17-2019","mode":"TRANSIT,WALK","arriveBy":"true","optimize":"QUICK","fromPlace":"33.264007,130.296483","toPlace":"33.2456,130.29308","time":"15:15","maxWalkDistance":"500","numItineraries":"1","cutoffSec":"1800"},"plan":{"date":1550384100000,"from":{"name":"Origin","lon":130.296483,"lat":33.264007,"orig":"","vertexType":"NORMAL"},"to":{"name":"Destination","lon":130.29308,"lat":33.2456,"orig":"","vertexType":"NORMAL"},"itineraries":[{"duration":1057,"startTime":1550381193000,"endTime":1550382250000,"walkTime":277,"transitTime":780,"waitingTime":0,"walkDistance":406.6788607339437,"walkLimitExceeded":false,"elevationLost":0.0,"elevationGained":0.0,"transfers":0,"fare":{"fare":{"regular":{"currency":{"currency":"JPY","defaultFractionDigits":0,"currencyCode":"JPY","symbol":"JPY"},"cents":200}},"details":{"regular":[{"fareId":"1:1_200","price":{"currency":{"currency":"JPY","defaultFractionDigits":0,"currencyCode":"JPY","symbol":"JPY"},"cents":200},"routes":["1:100000511"]}]}},"legs":[{"startTime":1550381193000,"endTime":1550381400000,"departureDelay":0,"arrivalDelay":0,"realTime":false,"distance":306.14026988862497,"pathway":false,"mode":"WALK","route":"","agencyTimeZoneOffset":32400000,"interlineWithPreviousLeg":false,"from":{"name":"Origin","lon":130.296483,"lat":33.264007,"departure":1550381193000,"orig":"","vertexType":"NORMAL"},"to":{"name":"佐賀駅バスセンター(4)","stopId":"1:1001002-04","lon":130.29926,"lat":33.26442,"arrival":1550381400000,"departure":1550381400000,"zoneId":"1001002-04","stopIndex":0,"stopSequence":1,"vertexType":"TRANSIT"},"legGeometry":{"points":"{~_jEylgzWf@qS","length":2},"rentedBike":false,"transitLeg":false,"duration":207.0,"steps":[{"distance":306.14026988862497,"relativeDirection":"DEPART","streetName":"佐賀駅北口(3) => 佐賀駅バスセンター(4)","absoluteDirection":"EAST","stayOn":false,"area":false,"bogusName":false,"lon":130.295976,"lat":33.26462,"elevation":[]}]},{"startTime":1550381400000,"endTime":1550382180000,"departureDelay":0,"arrivalDelay":0,"realTime":false,"distance":3374.192579452829,"pathway":false,"mode":"BUS","route":"西与賀線 [511]","agencyName":"佐賀市交通局","agencyUrl":"http://www.bus.saga.saga.jp","agencyTimeZoneOffset":32400000,"routeType":3,"routeId":"1:100000511","interlineWithPreviousLeg":false,"headsign":"11 佐賀大学・西与賀(大財町・辻の堂・久富 経由)","agencyId":"3000020412015","tripId":"1:1_土日祝_14時30分_系統511","serviceDate":"20190217","from":{"name":"佐賀駅バスセンター(4)","stopId":"1:1001002-04","lon":130.29926,"lat":33.26442,"arrival":1550381400000,"departure":1550381400000,"zoneId":"1001002-04","stopIndex":0,"stopSequence":1,"vertexType":"TRANSIT"},"to":{"name":"佐賀大学前(3)","stopId":"1:1003018-03","lon":130.29268,"lat":33.24476,"arrival":1550382180000,"departure":1550382180000,"zoneId":"1003018-03","stopIndex":11,"stopSequence":12,"vertexType":"TRANSIT"},"legGeometry":{"points":"s}_jEkahzWCsChCg@rGg@SsAWsA_AaHWyALWLKVOXGZKtB_@pK_BbKuB~C_@dAO`@E`@ApOqBp@QtIgA\\Dv@MHj@Dl@HxATtDBf@Dj@LnA??j@`NXvFNzBTxBDzBN`C\\|FLxBRzBAfAF~@\\zH~BU~@I~@KjBe@nCCvCShD[`BOn@Cb@CFb@J|A","length":57},"routeLongName":"西与賀線 [511]","rentedBike":false,"transitLeg":true,"duration":780.0,"steps":[]},{"startTime":1550382180000,"endTime":1550382250000,"departureDelay":0,"arrivalDelay":0,"realTime":false,"distance":100.53859084531872,"pathway":false,"mode":"WALK","route":"","agencyTimeZoneOffset":32400000,"interlineWithPreviousLeg":false,"from":{"name":"佐賀大学前(3)","stopId":"1:1003018-03","lon":130.29268,"lat":33.24476,"arrival":1550382180000,"departure":1550382180000,"zoneId":"1003018-03","stopIndex":11,"stopSequence":12,"vertexType":"TRANSIT"},"to":{"name":"Destination","lon":130.29308,"lat":33.2456,"arrival":1550382250000,"orig":"","vertexType":"NORMAL"},"legGeometry":{"points":"wb|iEexfzWgDqA","length":2},"rentedBike":false,"transitLeg":false,"duration":70.0,"steps":[{"distance":100.53859084531872,"relativeDirection":"DEPART","streetName":"佐賀大学前(3) => 佐賀大学前(1)","absoluteDirection":"NORTH","stayOn":false,"area":false,"bogusName":false,"lon":130.29268,"lat":33.24476,"elevation":[]}]}],"tooSloped":false}]},"debugOutput":{"precalculationTime":1,"pathCalculationTime":5,"pathTimes":[3],"renderingTime":0,"totalTime":6,"timedOut":false},"elevationMetadata":{"ellipsoidToGeoidDifference":31.919708147843863,"geoidElevation":false}}
上記クエリ結果の中に、以下のlegGeometry
項目があります。
"legGeometry":{"points":"{~_jEylgzWf@qS","length":2}
"legGeometry":{"points":"s}_jEkahzWCsChCg@rGg@SsAWsA_AaHWyALWLKVOXGZKtB_@pK_BbKuB~C_@dAO`@E`@ApOqBp@QtIgA\\Dv@MHj@Dl@HxATtDBf@Dj@LnA??j@`NXvFNzBTxBDzBN`C\\|FLxBRzBAfAF~@\\zH~BU~@I~@KjBe@nCCvCShD[`BOn@Cb@CFb@J|A","length":57}
(もう一つのlegGeometryは省略)
legGeometryとは、経路を構成するパーツで、地理座標を圧縮および暗号化(エンコード)したものです。文字化けではありません。
上記legGeometry.pointsを全部デコードして、座標に戻すと、地図に線を描画できます。
デコードには、こちらのソースコードを利用しました。
ちどりーどでは、クエリ結果が返ってきたときに、legGeometry.pointsを集めて繋げ、一旦LocalStorageに格納します。
そして、次のMapを描画する際に、mapboxの上記スクリプトをbrowserifyしたスクリプトでデータをデコードし、座標に変換します。
あとは、Leafletでlineを描画するという流れにしています。
browserifyする前のスクリプトは以下の通りです。
//github.com/mapbox/polyline
//OTP中のpointsのシェイプデータをデコードする
var polyline = require('@mapbox/polyline');
var pe = localStorage.getItem('polylineEnc');
if (pe != null) {
localStorage.setItem('polyline',JSON.stringify(polyline.decode(pe)));
}
変換結果はそれぞれ以下の通りです。
[33.26462,130.29597],[33.26442,130.29926](全2座標)
[33.26442, 130.29926],[33.26444, 130.3],[33.26375, 130.3002],[33.26237, 130.3004],[33.26247, 130.30082],[33.26259, 130.30124],[33.26291, 130.30269],[33.26303, 130.30314],[33.26296, 130.30326],[33.26289, 130.30332],[33.26277, 130.3034],[33.26264, 130.30344],[33.2625, 130.3035],[33.26191, 130.30366],[33.2599, 130.30414], [33.25796, 130.30473],[33.25716, 130.30489],[33.25681, 130.30497],[33.25664, 130.305],[33.25647, 130.30501],[33.25382, 130.30558],[33.25357, 130.30567],[33.25186, 130.30603],[33.25171, 130.306],[33.25143, 130.30607],[33.25138, 130.30585],[33.25135, 130.30562],[33.2513, 130.30517],[33.25119, 130.30426],[33.25117, 130.30406],[33.25114, 130.30384],[33.25107, 130.30344],[33.25107, 130.30344],[33.25085, 130.30103],[33.25072, 130.29979],[33.25064, 130.29917],[33.25053, 130.29856] ...(全57座標 一部省略)