TDX:運輸資料流通服務,以 JS 串接公車等待時間為例
為什麼現在 Google Maps 上可以看到公車、捷運等待時間,還可以看到各個 YouBike 2.0 架子剩幾個車位?這些都是靠政府提供的 API 釋出的。我們自己能不能來撰寫類似的服務呢?
TDX(Transport Data eXchange)運輸資料流通服務平臺,是交通部為落實智慧運輸政策而制定的資料整合服務平臺。平臺上可以取得「公共運輸整合資訊」,包含公車、火車、自行車等等資訊,也可以取得「即時路況與停車資訊」,例如高速公路路況、高速公路看板上的資訊、各縣市停車場剩餘車位數等等。TDX 平臺也提供了路段編碼、圖資定位等服務,例如輸入經緯度得到這個地點的路名、輸入地址取得經緯度(Geocoding)服務等等,基本上所有跟交通有關的 API 服務都整合起來了。
TDX 和 PTX 差在哪裡?如果你是早期就開始接觸公車等資料服務的人,一定聽過交通部的 PTX 服務平臺。PTX 是 TDX 下的子服務,也就是交通部決定要提供除了公共運輸資訊以外的更多服務,包含 Traffic、Link、GIS-T 等,因此才多了一個新的入口,實際上你會發現操作時呼叫 API 仍然是 ptx.transportdata.tw 的網址喔!
如何申請 TDX 服務
TDX 服務下分為各個子服務,你可以先到「資料服務」中的「線上 API 說明」來尋找你需要的資料放在哪裡,左邊的選單會打開對應的 Swagger API 文件,說明這個 API 怎麽使用。最上面也有「資料使用葵花寶典」,包含了使用這些服務的常見問題。
以提供公車資料的 PTX 服務來說,葵花寶典裡就寫明了非會員僅能在 Swagger 上試用 API,且有 50 次/日的限制,必須註冊不同等級的會員才能開始呼叫 API。目前的會員分為三種等級,原則上都是免費的,不過申請要承擔不同的會員義務。這邊我們會以一般會員來示範,畢竟練習時一天 20,000 次的服務也算相當足夠了。
從葵花寶典的說明就可以來到註冊頁面,一般會員註冊就需要填寫滿多資料的,而且根據平臺的說明,目前是專案小組進行審核,如果資料亂填可能會拿不到 Token。總之我們一步一步來,首先是基本的資料。
接著是服務單位,如果是學生的話,可以選擇學術單位,並留下學校資訊;如果已經出社會又不方便留下公司資料的朋友,可以直接選擇「自由業」,並填寫自己單位的名稱或是個人姓名就可以了。
接著是用途說明,選擇一個最適合你的。不然就選擇程式練習吧。
最後則是最複雜的部分,這邊需要填寫你即將開發的服務名稱、你想要申請什麼服務,以及你要開發的服務名稱。
申請後三個工作日內就會拿到結果,並在 Email 中收到申請的 App ID 和 App Key 可以用來開發了。
如果你不是使用 PTX 服務,而是使用 Traffic 服務的話,帳號和 App Key 可能要另外申請。
PTX 服務認證
拿到 App ID 和 App Key 後就可以開始拿資料了,只是取資料的時候必須使用平臺提供的 HMAC 認證授權機制。HMAC 授權機制在每次取得 API 時要在 header 塞兩個參數:Authorization
和 X-Date
,其中 x-Date 就是當下時間的 GMT String,長得像「Wed, 19 Apr 2017 08:37:50 GMT」,而 Authorization 則是:
hmac username="APP ID", algorithm="hmac-sha1", headers="x-date", signature="Base64(HMAC-SHA1("x-date: " + x-date , APP Key))"
這一段複雜的文字,其中 signature
的內容要換成 HMAC-SHA1 算出來的值再 Base64 Encode 過。此外,Signature 具有時效性,葵花寶典裡面建議你每次 Call API 都要簽一個新的 Signature,我建議撰寫一個 getSignature
或 getAuthorizationHeader
的方法起來放,每次都可以呼叫它。
拿到服務認證後,就可以開始 Call API 了,例如我要取得高雄市所有市區公車路線:
const API_URL = "https://ptx.transportdata.tw/MOTC/v2/Bus/Route/City/Kaohsiung?$top=30&$format=JSON";
async function() getBusRoutes {
const { data } = await axios.get(API_URL, {
headers: getAuthorizationHeader()
});
console.log(data);
}
如果你想拿其他的 API,只要到前面的「API 服務說明」,取得你要的服務對應的 API 網址,並把 API_URL
替換掉就可以了。至於那個 getAuthorizationHeader
的 function 怎麼寫,想要自己來的朋友可以挑戰看看,只要解決三個問題:
- 如何拿到日期的 GMT String
- 如何做 HMAC-SHA1 計算
- 如何做 Base 64 Encode
剰下就是字串組合的問題了。當然,如果你覺得太簡單,不想自己來,也可以直接拿我的範例程式碼:
- Node.js + axios 版本:https://github.com/ptxmotc/Sample-code/blob/master/Node.js/sample.js
- jQuery Ajax 版本:https://github.com/ptxmotc/Sample-code/blob/master/JavaScript/Sample.js
實際撰寫服務
最後舉一個例子吧,假設我要做一個讓高雄大學學生可以順利搭公車去換捷運的服務,我知道大部分的學生都會搭紅 56 公車去楠梓加工區站,所以我要做一個簡單的「紅 56 公車時刻表」。
首先我會需要「/v2/Bus/EstimatedTimeOfArrival/City/{City}/{RouteName} 取得指定[縣市],[路線名稱]的公車預估到站資料(N1)」這個 API,取得「紅 56 公車」的時刻表。
async function getTimeTable() {
const { data } = await axios.get("https://ptx.transportdata.tw/MOTC/v2/Bus/EstimatedTimeOfArrival/City/Kaohsiung/紅56?$top=50&$format=JSON", {
headers: getAuthorizationHeader()
})
return data;
}
接著我就可以拿到像這樣的資料(節錄):
[
{
"PlateNumb": "863-V2",
"StopUID": "KHH12241",
"StopID": "12241",
"StopName": {
"Zh_tw": "大學十七街口",
"En": "Dasyue 17th St. Intersection"
},
"RouteUID": "KHH856",
"RouteID": "856",
"RouteName": {
"Zh_tw": "紅56A",
"En": "Red56A"
},
"SubRouteUID": "KHH856",
"SubRouteID": "856",
"SubRouteName": {
"Zh_tw": "紅56A",
"En": "Red56A"
},
"Direction": 0,
"EstimateTime": 720,
"StopSequence": 16,
"StopStatus": 0,
"NextBusTime": "2021-09-21T17:13:00+08:00",
"Estimates": [
{
"PlateNumb": "863-V2",
"EstimateTime": 720,
"IsLastBus": false
}
],
"SrcUpdateTime": "2021-09-21T16:56:42+08:00",
"UpdateTime": "2021-09-21T16:56:58+08:00"
},
{}
]
我可以簡單用 React 把它畫在網頁上:
function Home() {
const [data, setData] = useState([]);
useEffect(async () => { setData(await getTimeTable()) }, [])
return (
<table>
<thead>
<tr>
<th>站牌</th>
<th>時間</th>
</tr>
</thead>
<tbody>
{data.map((s) => (
<tr>
<td>{s.StopName.Zh_tw}</td>
<td>{s.NextBusTime}</td>
</tr>
))}
</tbody>
</table>
)
}
另外,高雄大學總共有 4 個公車站可以搭到紅 56 公車,要怎麼知道使用者在哪一個站牌?我們可以用 Geolocation API 來取得使用者的經緯度,再使用「/v2/Bus/Stop/NearBy 取得指定[位置,範圍]的全臺公車站牌資料)」來找到最近的站牌。使用 Geolocation 的程式碼大概會長這樣:
function getNearestStop() {
if ("geolocation" in navigator) {
function onSuccess(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
console.log(lat, lng);
}
function onError() {
alert('Geolocation error');
}
navigator.geolocation.getCurrentPosition(onSuccess, onError);
} else {
alert('Geolocation is not available');
}
}
拿到經緯度後,將 lat
、lng
拿來傳給 /Bus/Stop/NearBy
的 API,將剛才的 onSuccess
function 改成
async function success(position) {
const { data } = axios.get(`https://ptx.transportdata.tw/MOTC/v2/Bus/Stop/NearBy?$top=30&$spatialFilter=nearby(${lat},${lng},150)&$format=JSON`)
}
console.log(data);
就會拿到這樣的資訊(節錄):
[
{
"StopUID": "KHH14722",
"StopID": "14722",
"AuthorityID": "009",
"StopName": {
"Zh_tw": "高雄大學(大學南路)",
"En": "National University of Kaohsiung (Dasyue S. Rd.)"
},
"StopPosition": {
"PositionLon": 120.285549,
"PositionLat": 22.73233,
"GeoHash": "wsj934m32"
},
"StationID": "4846",
"City": "Kaohsiung",
"CityCode": "KHH",
"LocationCityCode": "KHH",
"UpdateTime": "2021-09-21T16:00:06+08:00",
"VersionID": 1343
}
]
人不在測試地點怎麼辦?用 Chrome DevTools 送出假的經緯度給 Geolocation API。點選 DevTools 下方的選單,選擇 Sensors,就可以在 Location 中假造經緯度。
取得站牌後,就可以在網頁上顯示可能最近的站牌:
function Home() {
const [data, setData] = useState([]);
const [nearestStopUID, setNearestStopUID] = useState('');
useEffect(async () => {
setData(await getTimeTable());
// getNearestStop 拿到站牌後,記得 setNearestStopUID(data[0].StopUID)
}, [])
return (
<table>
<thead>
<tr>
<th>站牌</th>
<th>時間</th>
</tr>
</thead>
<tbody>
{data.map((s) => (
<tr>
<td>
{s.StopName.Zh_tw}
{s.StopUID === nearestStopUID ? ' 現在位置' : null}
</td>
<td>{s.NextBusTime}</td>
</tr>
))}
</tbody>
</table>
)
}
以上就是基本的 TDX 公車服務的使用,記得要在你的服務上顯名標示使用交通部 TDX/PTX 服務的資料,才能符合會員的義務喔!