GTFS-JPについてのメモ
地域交通の未来と公共交通オープンデータ - HackMDの発表資料「地域公共交通の未来と公共交通オープンデータ」を見て、GTSF-JPに興味を持ちました。 GTFS-JPの仕様書を確認し、Deck.GLで少し使ってみたので、メモしておきます。
GTFS-JPの概要
GTFS(General Transit Feed Specification)は公共交通機関の時刻表と地理的情報に関するオープンフォーマットであり、GTFSを日本の「標準的なバス情報フォーマット」として拡張されたものがGTFS-JPです。 国土交通省・公共交通政策ホームページでは、「静的バス情報フォーマット(GTFS-JP)」と「動的バス情報フォーマット(GTFSリアルタイム)」についての仕様が示されています。
- GTFS-JP仕様書:http://www.mlit.go.jp/sogoseisaku/transport/sosei_transport_tk_000112.html
- GTFS-JPサイト:https://www.gtfs.jp/
- GTFS-Githubプロジェクト:GitHub - google/transit
静的バス情報フォーマット(GTFS-JP)
GTFS-JPは、バス路線の事業者やバス停、バス路線の情報を最大17個のCSVファイルで整理し、Zip形式で配布されます。 配布データは単なるCSVなので、テキスト処理で諸情報を入手することができます。
- agency.txt(事業者情報)
- stops.txt(停留所・標柱情報)
- routes.txt(経路情報)
- trips.txt(便情報)
- office_jp.txt(営業所情報)
- stop_times.txt(通過時刻情報)
- calendar.txt(運行区分情報)
- fareattributes.txt(運賃情報)
- farerules.txt(運賃定義情報)
- feed_info.txt(提供者情報)
- agency_jp.txt(事業者追加情報)
- routes_jp.txt(経路追加情報)
- calendar_dates.txt(運行日情報)
- shapes.txt(描写情報)
- frequencies.txt(運行間隔情報)
- transfers.txt(乗換情報)
- translations.txt(翻訳者情報)
動的バス情報フォーマット(GTFSリアルタイム)
GTFSリアルタイムでは、遅延等のルート最新情報や⾞両位置情報、運⾏情報等のリアルタイム除法をProtocol Bufferというデータ構造が規定されたバイナリデータ(*.bin)で配布されます。 なお、GTFSリアルタイムについては、バイナリデータ読み取り用クラスを生成する「gtfs-realtime-bindings」がMavenRepositiryに登録されています。 mvnrepository.com
とりあえず、試してみる
仕様書に目を通したところで、Bus-Vision | 両備バス | 岡電バス | 中鉄バス | 岡山 | バスロケから、静的データ、リアルタイムデータを配布している両備バス(https://www.ryobi-holdings.jp/bus/)のデータを取得して触ってみました。
GTFS-JPからJSONを生成
手っ取り早く試すため、下記のコードを作成し、GTFS-JPのZIPファイルをJSONに変換しました。
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.transit.realtime.GtfsRealtime.FeedEntity; import com.google.transit.realtime.GtfsRealtime.FeedMessage; public class GtfsJpParser { private static Pattern csvDiv=Pattern.compile("\"([^\"\\\\]*(\\\\.[^\"\\\\]*)*)\"|([^,]+)|,|"); /* * GTFSリアルタイムの処理(出力テスト) */ public static void getJsonGtfsjpRealtime(String _url) throws IOException{ URL url = new URL(_url); FeedMessage feed = FeedMessage.parseFrom(url.openStream()); for (FeedEntity entity : feed.getEntityList()) { if (entity.hasTripUpdate()) { System.out.println(entity.getTripUpdate()); } } } /* * GTFS-JPの処理(URL) */ public static String getJsonGtfsjpStatic(String _url) throws IOException,ParseException{ URL url=new URL(_url); InputStream is=url.openStream(); return getJsonGtfsjpStatic(is); } /* * GTFS-JPの処理(ファイル) */ public static String getJsonGtfsjpStatic(File f) throws IOException,ParseException{ FileInputStream is=null; try{ is=new FileInputStream(f); return getJsonGtfsjpStatic(is); }finally{ is.close(); } } public static String getJsonGtfsjpStatic(InputStream is) throws IOException,ParseException{ ZipInputStream zis=new ZipInputStream(is); ZipEntry ze; byte[] buf= new byte[1024]; StringBuffer sb=new StringBuffer(); Map<String,Object> map=new HashMap<String,Object>(); while ((ze=zis.getNextEntry()) != null) { int size = 0; while((size=zis.read(buf))>0){ String str=new String(buf,0,size); sb.append(str); } zis.closeEntry(); BufferedReader br=new BufferedReader(new StringReader(sb.toString())); String[][] csv=parseCsv(br); map.put(ze.getName().replace(".txt", ""),json2Csv(csv)); sb.delete(0, sb.length()); } Gson gson=new GsonBuilder().setPrettyPrinting().create(); return gson.toJson(map); } private static List<Map<String,Object>> json2Csv(String[][] csv)throws ParseException{ List<Map<String,Object>> list=new ArrayList<Map<String,Object>>(); String[] title=csv[0]; for(int i=1;i<csv.length;i++){ list.add(createMap(title,csv[i])); } return list; } private static Map<String,Object> createMap(String[] title,String[] val)throws ParseException{ Map<String,Object> ret=new HashMap<String,Object>(); for(int i=0;i<val.length;i++){ if(title[i]==null)continue; if(title[i].endsWith("_lon")||title[i].endsWith("_lat")){ ret.put(title[i], Double.parseDouble(val[i])); }else if(title[i].endsWith("_date")){ ret.put(title[i], val[i]); }else if(title[i].endsWith("_time")){ ret.put(title[i], val[i]); }else{ ret.put(title[i], val[i]); } } return ret; } private static String[][] parseCsv(BufferedReader reader)throws IOException{ ArrayList<String[]> list=new ArrayList<String[]>(); String line; while((line=reader.readLine())!=null){ String[] sp=split(csvDiv,line); list.add(sp); } if(list.size()==0)return new String[0][0]; return list.toArray(new String[list.size()][]); } private static String[] split(Pattern pattern,String line){ Matcher matcher=pattern.matcher(line); List<String> list=new ArrayList<String>(); int index=0; int com=0; while(index<line.length()){ if(matcher.find(index+com)){ String s=matcher.group().replaceAll("\"", ""); index=matcher.end(); list.add(s); com=1; } } return list.toArray(new String[list.size()]); } }
Deck.GLで可視化
下記のコードを作成し、バス停とShape.txtで規定されている路線をDeck.GLで可視化してみました。 なお、GTFS-JPデータのサイズは、両備バスのデータをJSONに変換した場合で50MBくらいになりますので、本来はDBに格納して利用するのが正しいやりかただと思います。
<!doctype html> <html class="no-js" lang="ja"> <head> <meta charset="utf-8"> <meta http-equiv="x-ua-compatible" content="ie=edge"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <title>Traffic-Bus</title> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.css" /> <script src="https://code.jquery.com/jquery-3.4.0.js" integrity="sha256-DYZMCC8HTC+QDr5QNaIcfR7VSPtcISykd+6eSmBW5qo=" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/mapbox-gl/0.53.1/mapbox-gl.js"></script> <script src="https://cdn.jsdelivr.net/npm/deck.gl@7.0.0/dist.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/d3-scale/1.0.7/d3-scale.js"></script> <style type="text/css"> html, body { padding: 0; margin: 0; width: 100%; height: 100%; } #panel { position: absolute; background: #ffffff00; top: 0; left: 0; margin: 4px; padding: 4px; line-height: 1; width:260px; height:26px; z-index: 2; text-align: center; vertical-align: middle; } #tooltip { font-family: Helvetica, Arial, sans-serif; font-size: 12px; position: absolute; padding: 4px; margin: 8px; background: rgba(0, 0, 0, 0.8); color: #fff; max-width: 300px; z-index: 9; pointer-events: none; } </style> </head> <body> <div id="app" style="width:100%;height:100%;"></div> <div id="tooltip"></div> </body> <script type="text/javascript"> const LAT=34.6; const LNG=135.5; let c_lon=0; let c_lat=0; const route_name={}; const deckgl = new deck.DeckGL({ container: 'app', mapboxApiAccessToken: "アクセストークン", mapStyle: "mapbox://styles/mapbox/dark-v9", longitude: LNG, latitude: LAT, zoom: 11, pitch: 40, bearing: -10, onViewStateChange: ({viewState}) => { return viewState; } }); const loadData = () => { d3.json("okayama_ryoubi.json", (error, response)=>{ setRouteName(response.routes); let stop=renderStopLayer(getStopData(response.stops)); let shape=renderRouteLayer(getRouteData(response.shapes)); deckgl.setProps({ layers: [shape,stop], viewState: { longitude: c_lon, latitude: c_lat, zoom: 11, transitionInterpolator: new FlyToInterpolator(), transitionDuration: 3000 } }) }); }; const renderStopLayer = (data) => { const pointlayer = new deck.ScatterplotLayer({ id: 'stop', data, fp64: false, opacity: 0.8, stroked: true, filled: true, radiusScale: 6, radiusMinPixels: 1, radiusMaxPixels: 100, lineWidthMinPixels: 1, getPosition: d => d.coordinates, getRadius: 10, getFillColor: [255, 140, 0], getLineColor: [0, 0, 0], pickable: true, onHover: updateTooltipStop }); return pointlayer; }; const renderRouteLayer = (data) => { const pathLayer = new deck.PathLayer({ id: 'route', data, pickable: true, widthScale: 20, widthMinPixels: 2, getPath: d => d.path, getColor: d => d.color, getWidth: 4, pickable: false, }); return pathLayer; }; const getStopData=(feature) =>{ let data=[]; const n=feature.length; for(let i=0;i<n;i++){ let obj={}; obj.coordinates=[feature[i].stop_lon,feature[i].stop_lat]; c_lon +=feature[i].stop_lon; c_lat +=feature[i].stop_lat; obj.name=feature[i].stop_name; obj.zone_id=feature[i].zone_id; obj.stop_id=feature[i].stop_id; data.push(obj); } c_lon=c_lon/n; c_lat=c_lat/n; return data; }; const getRouteData=(feature) =>{ let data=[]; const n=feature.length; let path=[]; let sec=0; for(let i=0;i<n;i++){ let c=Number(feature[i].shape_pt_sequence); if(c==sec+1){ sec++; path.push([feature[i].shape_pt_lon,feature[i].shape_pt_lat]); }else{ let obj={}; obj.path=path; obj.color=[0,0,255]; obj.id=feature[i].shape_id; data.push(obj); sec=c; path=[]; } } if(path.length>0){ let obj={}; obj.path=path; obj.color=[0,0,255]; obj.id=feature[feature.length-1].shape_id; data.push(obj); } return data; }; const setRouteName=(feature) =>{ const n=feature.length; for(let i=0;i<n;i++){ let id=feature[i].route_id; let name=feature[i].route_long_name; id=id.substr(id.length-10,10); if(!(id in route_name)){ route_name[id]=name; } } }; const updateTooltipStop=({x, y, object}) => { const tooltip = document.getElementById("tooltip"); if (object) { tooltip.style.visibility="visible"; tooltip.style.top = y+"px"; tooltip.style.left = x+"px"; tooltip.innerHTML = "<p>"+object.name+"<br />"+object.stop_id+"</p>"; } else { tooltip.style.visibility="hidden"; tooltip.innerHTML = ""; } }; const updateTooltipRoute=({x, y, object}) => { const tooltip = document.getElementById("tooltip"); if (object) { tooltip.style.visibility="visible"; tooltip.style.top = y+"px"; tooltip.style.left = x+"px"; tooltip.innerHTML = "<p>"+route_name[object.id]+"</p>"; } else { tooltip.style.visibility="hidden"; tooltip.innerHTML = ""; } }; loadData(); </script> </html>
さいごに
だいたい、GTFS-JPの概要を把握できたので、今後、DBを作成し、Deck.GLのTripsLayerを試したみたいと思います。