TDX:運輸資料流通服務,以 JS 串接公車等待時間為例

TDX運輸資料流通服務平臺,是交通部為落實智慧運輸政策而制定的資料整合服務平臺。平臺上可以取得「公共運輸整合資訊」,包含公車、火車、自行車等等資訊,也可以取得「即時路況與停車資訊」,例如高速公路路況、高速公路看板上的資訊、各縣市停車場剩餘車位數等等。TDX 平臺也提供了路段編碼、圖資定位等服務,例如輸入經緯度得到這個地點的路名、輸入地址取得經緯度服務等等,基本上所有跟交通有關的 API 服務都整合起來了。

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 的網址喔!
tdx1

如何申請 TDX 服務

TDX 服務下分為各個子服務,你可以先到「資料服務」中的「線上 API 說明」來尋找你需要的資料放在哪裡,左邊的選單會打開對應的 Swagger API 文件,說明這個 API 怎麽使用。最上面也有「資料使用葵花寶典」,包含了使用這些服務的常見問題。

tdx2

以提供公車資料的 PTX 服務來說,葵花寶典裡就寫明了非會員僅能在 Swagger 上試用 API,且有 50 次/日的限制,必須註冊不同等級的會員才能開始呼叫 API。目前的會員分為三種等級,原則上都是免費的,不過申請要承擔不同的會員義務。這邊我們會以一般會員來示範,畢竟練習時一天 20,000 次的服務也算相當足夠了。

tdx3

從葵花寶典的說明就可以來到註冊頁面,一般會員註冊就需要填寫滿多資料的,而且根據平臺的說明,目前是專案小組進行審核,如果資料亂填可能會拿不到 Token。總之我們一步一步來,首先是基本的資料。

tdx4

接著是服務單位,如果是學生的話,可以選擇學術單位,並留下學校資訊;如果已經出社會又不方便留下公司資料的朋友,可以直接選擇「自由業」,並填寫自己單位的名稱或是個人姓名就可以了。

tdx5

接著是用途說明,選擇一個最適合你的。不然就選擇程式練習吧。

tdx6

最後則是最複雜的部分,這邊需要填寫你即將開發的服務名稱、你想要申請什麼服務,以及你要開發的服務名稱。

tdx7

申請後三個工作日內就會拿到結果,並在 Email 中收到申請的 App ID 和 App Key 可以用來開發了。

tdx8

如果你不是使用 PTX 服務,而是使用 Traffic 服務的話,帳號和 App Key 可能要另外申請。

PTX 服務認證

拿到 App ID 和 App Key 後就可以開始拿資料了,只是取資料的時候必須使用平臺提供的 HMAC 認證授權機制。HMAC 授權機制在每次取得 API 時要在 header 塞兩個參數:AuthorizationX-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,我建議撰寫一個 getSignaturegetAuthorizationHeader 的方法起來放,每次都可以呼叫它。

拿到服務認證後,就可以開始 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

剰下就是字串組合的問題了。當然,如果你覺得太簡單,不想自己來,也可以直接拿我的範例程式碼:

實際撰寫服務

最後舉一個例子吧,假設我要做一個讓高雄大學學生可以順利搭公車去換捷運的服務,我知道大部分的學生都會搭紅 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');
	}
}

拿到經緯度後,將 latlng 拿來傳給 /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 中假造經緯度。
tdx9

取得站牌後,就可以在網頁上顯示可能最近的站牌:

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 服務的資料,才能符合會員的義務喔!

我們正降低廣告比例以提升閱讀體驗。如果你喜歡這篇文章,不妨按下 Like 按鈕分享到社群,以行動支持我寫更多文章。 當然,你也可以 點此用新臺幣支持我,或 點此透過 BTC、ETH、USDC 等加密貨幣支持我
分享到: