用react做捷運即時資訊的地圖

TypeScript, react-leaflet和串接PTX的API

Juo Penguin
12 min readAug 24, 2020

主要內容

  • 用TypeScript建立component/containers 的props型別
  • 為沒有@types的套件自訂 types(react-leaflet-control部分)
  • 使用react-leaflet的地圖(Map)、圖標(Marker)、跳出視窗(Popup)
  • 串接PTX(高雄捷運)與初步認識OData寫法

索引index

事前準備

  • 初始化專案

組件(components)

  • 按鈕
  • 地圖
  • 圖標
  • 圖標的跳出視窗
  • 組件整合

功能(containers)

  • 使用PTX API 事前準備
  • 按鈕點擊取得地圖目前的相關參數,並發送request給API
  • 處理API資料
  • 最後整合

事前準備

初始化專案

用create-react-app啟動專案

npx create-react-app {專案名稱} --template typescript
// 如果不用typescript開發,則不需要加上--template typescript

套件安裝(leaflet)

yarn add leaflet
yarn add react-leaflet
yarn add react-leaflet-control
yarn add @types/leaflet
yarn add @types/react-leaflet

其他相關套件安裝

yarn add node-sass 
yarn add jssha
yarn add @material-ui/core // 不一定要安裝

組件(components)

按鈕

按鈕放置於地圖右下角的Controls裡面,並使用react-leaflet-control這個套件的components

備註:因為此套件並沒有 types的檔案,必須另外定義此套件的types
1. 在根目錄底下建立custom-types的資料夾
2. 修改tsconfig.json以直接支援此custom-types的資料夾中的types

// ...
"typeRoots": ["node_modules/@types", "./custom-types"],

3. ./custom-hooks底下新增react-leaflet-control的資料夾並新增index.d.ts
4. 加入以下內容讓Control這個component擁有我們自訂的type

declare module 'react-leaflet-control' {
class Control extends React.Component<ControlProps, any> {
}
type ControlProps = {
position: string
}
export default Control;
}

按鈕component: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/ButtonsPart/MrtSearchButton.tsx

按鈕外層ControlButtons(置於地圖右下角,position可以自己改): https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/ButtonsPart/ControlButtons.tsx

地圖

建立MapWrapper的通用組件,預留props.children,並提供擴充的props給LeafletMap組件

MapWrapper: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/MapPart/MapWrapper.tsx

圖標

建立一個捷運站點的Marker,其中的icon可以透過替換其中的iconUrl來更換icon圖片

MrtMarker: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/MapPart/MrtMarker.tsx

圖標內容

內容包含: 捷運站名稱、往返程的進佔預估時間
MrtMarkerContent: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/MapPart/MrtMarkerContent.tsx

組件整合

整合上述所有組件為單一的component,方便給container使用

<MapWrapper
position={position}
zoom={zoom}
onViewportChanged={onViewportChanged}
>
<MrtMarkerList
markerListData={markerListData}
/>
<ControlButtons onSearchMrtStations={onSearchMrtStations} />
</MapWrapper>

MapPart: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/components/MapPart/MapPart.tsx

功能(containers)

使用PTX API 事前準備

  • 申請PTX ID & PTX KEY
    https://ptx.transportdata.tw/PTX/Management/AccountApply申請會員,相關APP ID 和 APP KEY在帳號啟用後會直接寄到信箱中。
  • PTX的request Auth Header
    PTX採用HMAC認證授權機制,因此必須安裝jssha並額外撰寫header
    header範例檔
  • PTX API 支援OData語法
    此範例中API其中一項參數使用的filter即為OData語法,有關OData語法可以參考相關文件

按鈕點擊後發送request給API,取得捷運即時資料

主要需要以下3個步驟來取得資料

  • 取得目前地圖的相關資訊(useMapPosition)
    用useMapPosition這個hook去紀錄map的viewPort(包含position和zoom),用zoom取得現在的radius,position轉換為lat, lon,將以上三個作為API參數。
    備註:zoom, position的轉換會在最後整合才進行
// useMapPosition// ...
const [mapViewPort, setMapViewPort] = useState({
zoom: DEFAULT_MAP_ZOOM as number | undefined | null,
center: options.position,
});

useMapPosition: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/lib/useMapPosition.ts

  • 將地圖相關資訊(lat, lon, radius)作為API參數,發送request(getNearMrtStations)
// APIconst getMrtNearStationsUrl = ({ lat, lon, radius, }: GetMrtNearStationsUrlOptions) =>`https://ptx.transportdata.tw/MOTC/v2/Rail/Metro/Station/KRTC?&\$format=JSON&\$spatialFilter=nearby(StationPosition,${lat},${lon},${radius})`

API: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/constants/API.ts

用API先取得站點資訊

// getNearMrtStations// ...
const header = getPTXAuthHeader();
const nearStationUri = getMrtNearStationsUrl(options);
const mrtNearStations = await fetchData<SingleMrtStationRawData[]>({
uri: nearStationUri,
requestInit: {
headers: header,
},
defaultRes: [],
useCorsPrefix: false,
});

getNearMrtStations: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/lib/getNearMrtStations.ts

  • 取得站點資訊後,再用站點id作為API參數,重新組裝新的API發送另一個request取得各個站點的站點即時資訊(getNearMrtStations, PTXHandlers)

重新組裝後的站點即時資訊API範例(查詢到的站點id為R7, R8),其中的$filter部分為OData查詢語法:

https://ptx.transportdata.tw/MOTC/v2/Rail/Metro/LiveBoard/KRTC?&\$format=JSON&\$filter=StationID eq ‘R7’ or StationID eq ‘R8’

getNearMrtStations

// ...
if(stationIds.length > 0) {
// 會取得兩倍的資料(含去返程 departure, destination)
const stationsLiveInfoUri = PTXHandlers.getAPIWithStationIDsFilter(MRT_STATIONS_LIVE_INFO_API, stationIds);
// ...
}

其中的PTXHandlers負責處理部分PTX 資料 和加工PTX 的 API: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/lib/PTXHandlers.ts

處理API資料

在MrtStationsHandlers處理相關API取得的資料,此為比較細節的資料對應處理而已,就不贅述了

MrtStationsHandlers: https://github.com/ms0223900/ptx-map-sample/blob/master/src/ptx-map-sample/lib/MrtStationsHandlers.ts

最後整合

  • 整合取得API資料(useQueryMrtStations)

透過參數傳入map的相關參數(和useMapPosition分離更好做測試),其中用useFetch去做相關fetch的處理和API的處理

useQueryMrtStations

// ...const handleQueryMrtStations = useCallback(async () => {
const fetchOptions: GetNearStationsOptions = {
lat: position[0],
lon: position[1],
radius,
};
const res = await getNearMrtStations(fetchOptions);
return res;
}, [position, radius]);
const fetchedState = useFetch<SingleMrtMarkerData[]>({
apiPath: '', // just for fulfill type
initResponseData: [],
fetchFn: handleQueryMrtStations,
});
  • 最後整合成container(usePtxMapSample, PtxMapSample)

最後將useMapPostion, useQueryMrtStations整合在usePtxMapSample,container直接使用hook return 的值即可

usePtxMapSample

// ...const {
mapViewPort,
handleSetMapViewPort,
} = useMapPosition({
position: DEFAULT_MAP_CENTER,
zoom: DEFAULT_MAP_ZOOM,
});
const {
center, zoom
} = mapViewPort;
const queryParams = {
position: center, zoom,
};
const queriedMrtStationsState = useQueryMrtStations(queryParams);

PtxMapSample(container)

// ...
<MapPart
position={mapViewPort.center}
zoom={mapViewPort.zoom as number}
markerListData={mrtStations}
onSearchMrtStations={handleFetchMrtStations}
onViewportChanged={handleSetMapViewPort}
/>

--

--

Juo Penguin
Juo Penguin

Written by Juo Penguin

不挑食的雜食者,近期的目標是瘦10公斤。

No responses yet