用react做捷運即時資訊的地圖
TypeScript, react-leaflet和串接PTX的API
主要內容
- 用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-controlyarn 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圖片
圖標內容
內容包含: 捷運站名稱、往返程的進佔預估時間
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>
功能(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的處理
// ...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 的值即可
// ...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);
// ...
<MapPart
position={mapViewPort.center}
zoom={mapViewPort.zoom as number}
markerListData={mrtStations}
onSearchMrtStations={handleFetchMrtStations}
onViewportChanged={handleSetMapViewPort}
/>