GTFSと、リアルタイムな位置情報オープンデータ、RTFSリアルタイムを表示するwebアプリを作りました。(GTFS = General Transit Feed Specification)
「GTFSmap - 宇野自動車」
IchigoJamで開発されたバスロケーションシステム用デバイス「いちごロケ」が活躍する岡山県の「宇野バス」さんの様子を表示。
「GTFSmap - 宇野自動車」
拡大した様子。建物がなんちゃって3D表示されてます。右クリックしながらマウス操作でグリグリ回転できて楽しいです。
マーカーが奥行き無関係になっているのは直したい。地図ライブラリ「MapLibre」はオープンソースなので直してプルリクすることもできます。
Googleが提唱するGTFSは、GitHub上「google/transit」でその仕様策定が進められています。「GTFS リアルタイムとは」に多言語でまとめられています。 変更プロセスの概要、基本方針など、世界中でひとつのものを作るプロセスにワクワクします。
GTFS公開している方、ブラウザから直接取得できるようにHTTPS化と、CORS対応していただけるとうれしいです。
最近のブラウザではHTTPSとHTTPを混ぜて使用することができず、CORS対応のヘッダーがないと取得できません。
「Access-Control-Allow-Origin: *」とHTTPのレスポンスヘッダーに付けていただけると、プロキシーを通さず直接アクセスできるようになります。
「GTFSリアルタイム一覧」(公共交通オープンデータ一覧 (GTFS、標準的なバス情報フォーマット(GTFS-JP))より抜粋)
HTTPS対応5事業者のアクセスを試みましたが、CORS対応しているところはまだなかったので、今回はすべてプロキシー経由としました。
こういったことも、ドキュメントとして追記検討も良さそうですね。
実現するために準備したもの
1. zlib.js のESモジュール化 (src on GitHub)
GTFSはCSVファイル(拡張子はtxt)をzip圧縮してあるので、展開するコードzlib.jsのunzip.min.jsをESモジュール化して、unzip.jsを作成。
改造ポイントは2つ、this を globalThis に変えて、export。Denoやブラウザから同じコードでシンプルに使えます。
import { unzip } from "https://taisukef.github.io/zlib.js/es/unzip.js"; const data = await Deno.readFile("wakayama.zip"); // or fetch const zips = unzip(data); const filenames = zips.getFilenames(); console.log(filenames); const plain = new TextDecoder().decode(zips.decompress(filenames[0])); console.log(plain);
2. gtfs2json 作成 (src on GitHub)
1で準備した unzip.js と CSV.js を使って簡単。
import { CSV } from "https://js.sabae.cc/CSV.js"; import { unzip } from "https://taisukef.github.io/zlib.js/es/unzip.js"; const gtfs2json = (bin) => { const res = {}; const zips = unzip(bin); const fns = zips.getFilenames(); for (const fn of fns) { const data = new TextDecoder().decode(zips.decompress(fn)); const csv = CSV.decode(data); const json = CSV.toJSON(csv); const name = fn.substring(0, fn.length - 4); // cut .txt res[name] = json; } return res; }; export { gtfs2json };
3. GTFSリアルタイムをJSON化するコードを作成 (src on GitHub)
Node.jsに対応している gtfs-realtime-bindings を発見、Protocol Buffers にしか依存していないので、移植は楽。
ちょっと前に移植した protobuf-es.js がすぐに役立った。importとexportを変更してできあがり。JavaScript / Deno対応とプルリク。
4. GTFSモジュールとしてまとめる
URLを渡すとJSONでそれぞれ返すモジュールにすると便利。
import { GtfsRealtimeApi } from "https://taisukef.github.io/gtfs-realtime-bindings/deno/gtfs-realtime.js"; import { gtfs2json } from "./gtfs2json.js"; import { fetchBin } from "./fetchBin.js"; const GTFS = { async fetch(url) { const data = await fetchBin(url); const json = gtfs2json(data); return json; }, async fetchRT(url) { const data = await fetchBin(url); const feed = GtfsRealtimeApi.transit_realtime.FeedMessage.decode(data); return feed; }, }; export { GTFS };
5. webアプリ、GTFSマップを作る (src on GitHub)
あとは、GTFSモジュール、Maplibre、Geo3x3を使って組み立てるだけ!
import { GTFS } from "https://taisukef.github.io/gtfs-map/GTFS.js"; import { maplibregl } from "https://taisukef.github.io/maplibre-gl-js/maplibre-gl-es.js"; import { sleep } from "https://js.sabae.cc/sleep.js"; import { Geo3x3 } from "https://taisukef.github.io/Geo3x3/Geo3x3.mjs"; onload = async () => { const url_gtfs = "http://www3.unobus.co.jp/opendata/GTFS-JP.zip"; const url_gtfsrt = "http://www3.unobus.co.jp/GTFS/GTFS_RT-VP.bin"; const update_interval_sec = 15; // 地図表示 (MapLibre API: https://maplibre.org/maplibre-gl-js-docs/api/ ) const mapgl = maplibregl; const map = new mapgl.Map({ container: "map", hash: true, style: 'https://taisukef.github.io/gsivectortile-3d-like-building/building3d.json', zoom: 15, minZoom: 4, maxZoom: 17.99, bearing: -40, pitch: 60, }); // GTFS取得とタイトル表示 const gtfs = await GTFS.fetch(url_gtfs); console.log(gtfs); title.textContent = "GTFSmap - " + gtfs.agency[0]?.agency_name; // バス停表示 const lls = new mapgl.LngLatBounds(); for (const e of gtfs.stops) { const ll = [e.stop_lon, e.stop_lat]; const marker = new mapgl.Marker({ color: "#ff9999" }).setLngLat(ll).addTo(map); lls.extend(ll); const geo3x3 = Geo3x3.encode(e.stop_lat, e.stop_lon, 20); const popup = new mapgl.Popup({ className: "popup" }) .setLngLat(ll) .setHTML(`<a href=${e.stop_url}>${e.stop_name}</a><br><a href=https://geo3x3.com/#${geo3x3}>${geo3x3}</a>`) .setMaxWidth("300px"); marker.setPopup(popup); } map.fitBounds(lls); // update_interval_sec秒おきにGTFSrealtimeでバス情報の取得と表示 for (;;) { const feed = await GTFS.fetchRT(url_gtfsrt); for (const e of feed.entity) { const pos = e.vehicle.position; const ll = [pos.longitude, pos.latitude]; const marker = new mapgl.Marker().setLngLat(ll).addTo(map); const popup = new mapgl.Popup({ className: "popup" }) .setLngLat(ll) .setHTML(JSON.stringify(e.vehicle)) .setMaxWidth("300px"); marker.setPopup(popup); } await sleep(update_interval_sec * 1000); } };
index.htmlを手元において、ブラウザで開くだけで動きますよ!
ブラウザでデバッガーを立ち上げると、JSON化されたGTFSデータを読めます。
「GTFSmap 和歌山バス」
こちら、和歌山のバスのGTFSを表示してみるサンプルです。みなさんのまち、GTFSオープンデータはありますか?