UNPKG

hkopendata

Version:

Access different Opendata API and data in Hong Kong

263 lines (250 loc) 8.4 kB
// https://opendata.mtr.com.hk/doc/Next_Train_DataDictionary.pdf // eta // https://opendata.mtr.com.hk/doc/Next_Train_API_Spec.pdf // eta const cmn = require("../../common"); const BASE_URL = "https://opendata.mtr.com.hk/data/{data}.csv"; const BASE_URL2 = "https://rt.data.gov.hk/v1/transport/mtr/getSchedule.php"; const ENDPOINT = { "route": "mtr_lines_and_stations", "route-stop": "mtr_lines_and_stations", "fare": "mtr_lines_fares", "ae-fare": "airport_express_fares", } const VALID = { type: /^(route|route-stop|eta|fare|ae-fare)$/ } const VALID_OPT = { route: /^[A-z]+$/, stop: /^[A-z]+$/, dir: /^[0-3]$/, from: /^[0-9]+$/, to: /^[0-9]+$/, lang: /^(en|tc)$/, } const FIELDS = { regex: { "^oct_con_": "octo_", "^oct_": "octo_", "^single_con_": "single_" }, text: { "sequence": "seq", "line code": "route", "station code": "stop", "direction": "dir", "src_station_id": "from", "dest_station_id": "to", "st_from_id": "from", "st_to_id": "to", } } const PARAMS = { type: "route", dir: 0, } const SEARCH_CONFIG = { value: { type: { accepted: ["route", "route-stop", "eta", "fare", "ae-fare"] } }, } function validateParameters(params) { params = cmn.ParseSearchFields(params, SEARCH_CONFIG); let result = cmn.ValidateParameters(params, VALID, VALID_OPT); if (/^(route-stop|eta)$/.test(params.type) && !("route" in params)) { result.error = true; result.message = "Missing route"; } if (/^eta$/.test(params.type) && !("stop" in params)) { result.error = true; result.message = "Missing stop"; } if (/fare$/.test(params.type) && (!("from" in params) || !("to" in params))) { result.error = true; result.message = "Missing origin (from) and destination (to)"; } else if (/fare$/.test(params.type) && "from" in params && "to" in params && params.from == params.to) { result.error = true; result.message = "Same origin (from) and destination (to)"; } if (!result.error) { if ("route" in params) params[params.type == "eta" ? "line" : "route"] = params.route.toUpperCase(); if ("stop" in params) params[params.type == "eta" ? "sta" : "stop"] = params.stop.toUpperCase(); if ("dir" in params) { if (/2|3/.test(params.dir) && !/EAL|TKL/.test(params.route)) { params.dir = params.dir % 2; } } if (params.type == "eta") { result.data = { ...params } } else { result.data = { ...{ data: ENDPOINT[params.type] }, ...params } } } return result; } function search(data, opts) { return new Promise((resolve, reject) => { let processed = validateParameters({ ...PARAMS, ...data }), params; if (processed.error) { reject(processed); } else { params = processed.data; if (params.type == "eta") { cmn.APIRequest(BASE_URL2, params) .then((res) => { try { resolve(processData2(res, params)) } catch (e) { reject(e); } }) .catch((err) => reject(err)) } else { cmn.CSVFetch(cmn.ReplaceURL(BASE_URL, params)) .then((res) => { resolve(processData(res, params)) }) .catch((err) => reject(err)) } } }) } function processData(data, params) { let result = [], type = params.type; data.body.map(row => { let temp = {}, item = {}; data.header.map((head, i) => item[head.toLowerCase()] = row[i]) item = cmn.RenameFields(item, FIELDS); for (let key in item) { let m, ckey = key.toCamelCase(); if (m = key.match(/^(chinese|english) (.+)$/)) { ckey = m[2].toCamelCase(); if (!(ckey in temp)) temp[ckey] = {} temp[ckey][m[1] == "chinese" ? "tc" : "en"] = item[key].decodeEntities(); } else if (m = key.match(/(octo|single)_(.+)_fare/)) { let t = m[1] == "octo" ? "octopus" : "cash", p = m[2]; switch (m[2]) { case "pwd": p = "disability"; break; case "adt": p = "adult"; break; case "std": p = "student"; break; case "chd": p = "child"; break; } if (!("fare" in temp)) temp.fare = {}; if (!(t in temp.fare)) temp.fare[t] = {}; temp.fare[t][p] = item[key].parseNumber(2); } else if (/seq/.test(key)) { temp[ckey] = parseInt(item[key]); } else if (key == "dir") { let dirs = ["DT", "UT"], dir = dirs.indexOf(item[key]); if (dir == -1) { dirs.map((v, i) => { if (item[key].indexOf(v) != -1) dir = i + 2; }) } temp[ckey] = dir; } else { temp[ckey] = item[key].decodeEntities(); } } result.push(temp); }) if (/route/.test(type)) { let temp = {}; result.map(v => { if (!(v.route in temp)) temp[v.route] = {}; if (!(v.dir in temp[v.route])) temp[v.route][v.dir] = {}; temp[v.route][v.dir][v.seq] = v; }) result = []; for (let route in temp) { let r = Object.keys(temp[route]) .sort((a, b) => a - b) .map(dir => Object.keys(temp[route][dir]) .sort((a, b) => a - b) .map(v => temp[route][dir][v]) ); temp[route] = r; } if (params.route in temp) { result = temp[params.route][params.dir]; if (type == "route") { result = [{ route: result[0].route, origin: result[0].name, destination: result[result.length - 1].name, companyCode: "MTR" }]; } else { result = result.map(v => { return { _id: v.stationId, code: v.stop, name: v.name } }); } }; } else if (/fare$/.test(type)) { result = result.filter(v => v.from == params.from && v.to == params.to) .map(v => { for (let t in v.fare) { ["elderly", "disability", "student"].map(u => { if (!(u in v.fare[t])) v.fare[t][u] = v.fare[t].adult; }) } return v.fare }); } return result; } function processData2(data, params) { if (data.status == "0") { let message = data.message; switch (data.message) { case "File No Exist!": message = "Invalid route/stop"; } throw message; } else { data = data.data[`${params.line}-${params.sta}`]; let result = []; let dir = params.dir % 2 == 0 ? "DOWN" : "UP"; if (dir in data) { result = data[dir] .filter(v => v.valid == "Y") .sort((a, b) => parseInt(a.seq) - parseInt(b.seq)) .map(v => { return { platform: v.plat, destination: v.dest, eta: v.time } }); } return result; } } module.exports = search;