UNPKG

hkopendata

Version:

Access different Opendata API and data in Hong Kong

652 lines (615 loc) 24.4 kB
require("./lib/prototype"); const DEFAULT_SEARCH_INDEX = { airports: "iata", airlines: "icao", stations: "code", default: "name", } function _getAxiosInstance() { return global.axiosInstance || require("./utils").CreateAxiosInstance(); } function _cleanInvalidJsonString(text) { if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1) return text.replace(/(\r|\n)/g, ""); } function APIRequest(url, params, post) { return new Promise((resolve, reject) => { let data = { params, }, method = "get"; if (post) { data = params; method = "post"; } _getAxiosInstance()[method](url, data) .then((res) => { if (typeof res.data === "string") { try { resolve(JSON.parse(_cleanInvalidJsonString(res.data))) } catch (e) { reject(res.data); } } else { resolve(res.data) } }) .catch((err) => { if (err.response && "data" in err.response) reject(err.response.data); else reject(err); }) }) } function CSVFetch(url, opts) { return new Promise((resolve, reject) => { _getAxiosInstance()({ url, responseType: 'arraybuffer', }).then((res) => { return CSVToArray(res.data, opts) }) .then(res => resolve(res)) .catch((err) => reject(err)) }) } function CSVToArray(data, opts) { return new Promise((resolve, reject) => { const parse = require("csv-parse"); opts = { ...{ bom: true, }, ...opts } if ("encoding" in opts) { const iconv = require("iconv-lite"); data = iconv.decode(data, opts.encoding); delete opts.encoding; } if (typeof opts.preprocess === "function") data = opts.preprocess(data); let result = {}; parse(data, opts, (err, res) => { if (err) reject(err); else { if (!opts.noHeader) { result.header = res.shift(); } result.body = res.filter(v => v.join("") != ""); resolve(result); } }) }) } function XMLFetch(url, params, opts) { return new Promise((resolve, reject) => { _getAxiosInstance()(url, { responseType: 'text', params, }).then((res) => { return XMLToJson(res.data, opts) }) .then(res => resolve(res)) .catch((err) => reject(err)) }) } function XMLToJson(data, opts) { opts = opts || {}; const xmlParser = require("fast-xml-parser"); return xmlParser.parse(data, opts); } function _processMatchText(input) { return input.toString().toLowerCase().replace(/\s/g, "") } function _identical(src, val) { if (src && typeof src === "object") { return Object.keys(src).reduce((p, c) => p || _identical(src[c], val), false) } return src && val && _processMatchText(src) == _processMatchText(val) } function _contains(src, val) { if (src && typeof src === "object") { return Object.keys(src).reduce((p, c) => p || _contains(src[c], val), false) } return src && val && _processMatchText(src).indexOf(_processMatchText(val)) != -1 } function _inRange(val, min, max) { return parseFloat(val) >= (max > min ? min : max) && parseFloat(val) <= (max > min ? max : min) } function _replaceKey(input, params) { if (typeof input !== 'string') return input; for (let key in params) { input = input.replace(new RegExp("{" + key + "}", "g"), params[key]); } return input; } function ReplaceURL(url, params) { if (typeof params === "object") { for (let key in params) { params[key] = _replaceKey(params[key], params); } url = _replaceKey(url, params); } return url.replace(/\{[a-z_-]+\}/g, '') .replace(/\/+/g, '/') .replace(/:\//, '://'); } function MatchData(src, opts, partial) { let valid = true, match = _identical; if (partial) match = _contains; if (typeof opts === "object") { for (let item in src) { if (item in opts) { valid = valid && match(src[item], opts[item]) } else if (Array.isArray(src[item])) { valid = valid && src[item].reduce((p, c) => p || MatchData(c, opts, partial), false) } } return valid; } return false } function GetDataJson(type, isArray) { let obj = isArray ? [] : {}, data = HasDataJson(type); if (data !== false) obj = data; return obj; } function HasDataJson(type) { const DATA = require("../data/"); if (type in DATA.list) type = DATA.list[type]; let data = DATA.Get(type); return (Array.isArray(data) && data.length > 0) || (typeof data === "object" && Object.keys(data).length > 0) ? data : false; } function UpdateDataJson(type, data) { const DATA = require("../data/"); DATA.Set(type, data); } function SearchDataJson(type, opts) { let params = {}, data = HasDataJson(type) || []; if (typeof opts === "undefined") { return false; } else if (typeof opts === "object") { params = opts; } else { params[type in DEFAULT_SEARCH_INDEX ? DEFAULT_SEARCH_INDEX[type] : DEFAULT_SEARCH_INDEX.default] = opts; } return data.filter((item) => MatchData(item, params)); } function ValidateParameters(params, valid, validOpt) { let result = { error: false, message: "", }; for (let key in valid) { if (!valid[key].test(params[key])) { result.error = true; result.message = "Incorrect parameter: " + key } } if (typeof validOpt === "object") { for (let key in params) { if (key in validOpt && !validOpt[key].test(params[key])) { result.error = true; result.message = "Incorrect parameter: " + key } } } return result; } function parseRenameRegexp(key) { let arr = key.split("$f-"); if (arr.length == 1) { return new RegExp(arr[0]); } else { return new RegExp(arr[0], arr[1]); } } function RenameFields(data, config) { if (Array.isArray(data)) return data.map(v => RenameFields(v, config)) else if (typeof data !== "object" || data === null) return data; let result = {}, keys = [], regexs = []; for (let type in config) { if (type == "regex") { Object.keys(config[type]).map(v => regexs.push(parseRenameRegexp(v))) } else if (/latitude|longitude|easting|northing/.test(type)) { keys = keys.concat(config[type]) } else { keys = keys.concat(Object.keys(config[type])) } } for (let key in data) { let hasKey = keys.indexOf(key) != -1, hasRegex = regexs.reduce((p, c) => { return p || c.test(key) }, false); if (!hasKey && !hasRegex) { result[key] = data[key]; } } for (let type in config) { for (let key in config[type]) { let m; if (type == "regex") { m = parseRenameRegexp(key); Object.keys(data).filter(v => m.test(v) && (data[v] === null || data[v].toString().trimChar(" -") != "")).map((v) => { result[v.replace(m, config[type][key])] = data[v]; delete data[v]; }) } else if (type == "latitude" || type == "longitude") { if (config[type][key] in data) { const CoordinateValue = require("./_class/CoordinateValue"); if (!("coordinate" in result)) result.coordinate = {} result.coordinate[type] = new CoordinateValue(data[config[type][key]]).toCoor(); } } else if (type == "easting" || type == "northing") { if (config[type][key] in data) { const CoordinateValue = require("./_class/CoordinateValue"); if (!("coordinateHK" in result)) result.coordinateHK = { _type: "tmerc", _system: "hk1980", } result.coordinateHK[type] = new CoordinateValue(data[config[type][key]]).toCoor(); } } else if (key in data && (data[key] === null || data[key].toString().trimChar(" -") != "")) { if (type == "number") { if (typeof data[key] === "string") { m = data[key].replace(/[^\d.-]/g, "").match(/([0-9.]+)/); result[config[type][key]] = m ? parseFloat(m[1]) : data[key]; } else { result[config[type][key]] = data[key]; } } else if (type == "minmax") { m = data[key].match(/([0-9.]+)/g); result[config[type][key]] = { min: parseFloat(m[0]), max: parseFloat(m[1]), }; } else if (type == "boolean") { result[config[type][key]] = data[key] === "0" ? false : Boolean(data[key]); } else { result[config[type][key]] = data[key]; } } } } return result; } function StrToWeekTime(str) { let tempStr = str, index = 0, result = {}, chiNum = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9 }, regexWeek = { mon: /monday|mon|weekday|一/gi, tue: /tuesday|tue|weekday|二/gi, wed: /wednesday|wed|weekday|三/gi, thu: /thursday|thu|weekday|四/gi, fri: /friday|fri|weekday|五/gi, sat: /saturday|sat|weekend|六/gi, sun: /sunday|sun|weekend|日/gi, ph: /(public|general) holiday|p\.h\.|公(衆|眾)假期/gi }, weekday, time, m; let available = { mon: false, tue: false, wed: false, thu: false, fri: false, sat: false, sun: false, ph: false, }, unavailable = []; let customFix = { "midnight": /12(:00)?\s*(midnight|mn)|(午夜|凌晨)12時/, "十一時": "+一時", "下午": "卞午", "Mon to Fri: 10": "Mon to 10", "下午 2:00午膳": "2:00午膳", "下個": "下一個", "daily": "每日", "same day": "同日", "working day": "工作日", "{{special}}": /(日間賽馬|保養)日/g, "下午6時30分": "下午6時3 0分", "09:00 a.m.to 07:30 p.m. Except opening at 09:30 a.m. on Tue & Fri": "09:00 a.m.\\*\\* to 07:30 p.m.", "上午9時 至下午7時30分。除星期二和星期五由上午9時30分開放": "上午9時\\*\\* 至下午7時30分", "tue and fri 09:30am to 06:30pm": "Except opening at 09:30 a.m. on Tue & Fri", "星期二和五由上午9時30分至下午6時30分開放": "除星期二和星期五由上午9時30分開放", "minutes": /\d+\s*(minute|分鐘)/gi, "非公假": /non(-|\s)*public holiday|非公眾假期/gi, "{{order}}": /第[0-9一二三四五六七八九十]+|\d?1st|\d?2nd|\d?3rd|[0-9]+th|初[一二三四五六七八九十]+|session \d+/gi, "{{replace}}": /ceremonies|Space-\d|[0-9一二三四五六七八九十]+\s*(month|月份?)|\(\d+\)|[0-9.]+ hour per session|以[0-9.]+小時([0-9.]分+)?為1節計算|(AMS on Sunday and Public Holiday|If (.*) falls on a public holiday|如(.*)(為|適逢)公眾假期|Weekly Cleansing Day|每(周|週)大清(潔日|洗)|Session breaks|暫停開放時段)(.*)/gi, "{{exclude-a}}": /((星期)?[一二三四五六日](、|及|\s)*)*公(衆|眾)假期(除外|休息)/gi, "{{exclude-b}}": /((星期)?[一二三四五六日](、|及|\s)*)(除外|休息)/gi, "{{exclude-c}}": /(except|exclude|excluding|closed? on)\s*(((mon(day)*|tue(sday)*|wed(nesday)*|thu(rsday)*|fri(day)*|sat(urday)*|sun(day)*|weekend|weekday)s?(and|&|,|\s)*)*(public|general) holiday|p\.h\.)/gi, "{{exclude-d}}": /(except|exclude|excluding|closed? on)\s*(((mon(day)*|tue(sday)*|wed(nesday)*|thu(rsday)*|fri(day)*|sat(urday)*|sun(day)*|weekend|weekday)s?(and|&|,|\s)*))/gi, }; tempStr = tempStr.replace(/<br[\/ ]*>/gi, " "); for (let key in customFix) { let regex; if (typeof customFix[key] === "string") { regex = new RegExp(customFix[key], "gi"); } else { regex = customFix[key]; } if (key.indexOf("{{exclude") != -1 && regex.test(tempStr)) { unavailable = Object.keys(regexWeek).filter(u => tempStr.match(regex).reduce((p, c) => p || regexWeek[u].test(c), false)) } tempStr = tempStr.replace(regex, key) } if (/24\s*小\s*時|24\s*(hour|hr)s?|全日|full day/i.test(tempStr)) { for (let key in available) { available[key] = true } return available; } // process all time time = tempStr.match(/(([上下正中]午|[早晚]上)?\s*([0-9一二三四五六七八九十]+\s*時[正]?\s*([0-9一二三四五六七八九十]+\s*分)?|[0-9]{1,2}:?[0-9]{0,2})(\s*([apmn.]{2,4}|noon))?)|(午夜|midnight)/gi) if (time) { time = time.map((v, i) => { let hour, min = 0, morning = false, hour24 = true, processed = false; if (/上午|am|早上/.test(v.replace(/\./gi, ""))) { morning = true; hour24 = false; processed = true; } else if (/[下正中]午|pm|nn|noon|晚上/i.test(v.replace(/\./g, ""))) { hour24 = false; processed = true; } else if (/午夜|midnight/i.test(v.replace(/\./g, ""))) { processed = true; time[i] = "24:00"; } tempStr = tempStr.replace(v, "{{" + (processed ? "TIME" : "time") + i + "}}"); if (m = time[i].match(/([0-9一二三四五六七八九十]+)\s*時[正]?\s*(([0-9一二三四五六七八九十]+)\s*分)?/)) { let n; if (/[0-9]+/.test(m[1])) { hour = parseInt(m[1]); } else if (/[一二三四五六七八九十]+/.test(m[1])) { n = m[1].match(/(([一二三四五六七八九])?(十))?([一二三四五六七八九])?/); hour = (!!n[1] ? (!!n[2] ? chiNum[n[2]] : 1) * 10 : 0) + (!!n[4] ? chiNum[n[4]] : 0); } else { hour = m[1]; } if (!!m[3]) { if (/[0-9]+/.test(m[3])) { min = parseInt(m[3]); } else if (/[一二三四五六七八九十]+/.test(m[3])) { n = m[3].match(/(([一二三四五六七八九])?(十))?([一二三四五六七八九])?/); min = (!!n[1] ? (!!n[2] ? chiNum[n[2]] : 1) * 10 : 0) + (!!n[4] ? chiNum[n[4]] : 0); } else { min = m[3]; } } } else if (m = time[i].match(/([0-9]{1,2}):?([0-9]{0,2})/)) { hour = parseInt(m[1]); if (!!m[2]) min = parseInt(m[2]) } if (!hour24) { if (morning && hour >= 12) morning = false; if (hour >= 12) hour -= 12; if (!morning) hour += 12; } return ("00" + hour).slice(-2) + ":" + ("00" + min).slice(-2) }) if (m = tempStr.match(/(\{\{time(\d+)\}\})( |to|至|-)+(\{\{time(\d+)\}\})/gi)) { m.map(v => { let n = v.match(/\{\{time(\d+)\}\}/gi), t1 = time[n[0].match(/(\d+)/)[0]], t2 = time[n[1].match(/(\d+)/)[0]], h1 = parseInt(t1.slice(0, 2)), h2 = parseInt(t2.slice(0, 2)); if (h1 > h2 && h1 >= 12 && h2 < 12) { if (!/TIME/.test(n[1])) h2 += 12; } time[n[1].match(/(\d+)/)[0]] = h2 + t2.slice(2); }) } } // process all weekday weekday = tempStr.match(/(星期)*[一二三四五六日]|mon(days?)*|tue(sdays?)*|wed(nesdays?)*|thu(rsdays?)*|fri(days?)*|sat(urdays?)*|sun(days?)*|(public|general) holiday|p\.h\.|公(衆|眾)假期/gi) if (weekday) { weekday = weekday.map((v, i) => { tempStr = tempStr.replace(v, "{{day" + i + "}}"); return Object.keys(regexWeek).filter(u => regexWeek[u].test(v))[0] }) if (m = tempStr.match(/(\{\{day(\d+)\}\})( |to|至|-)+(\{\{day(\d+)\}\})/gi)) { m.map(v => { let n = v.match(/(\{\{day(\d+)\}\})( |to|至|-)+(\{\{day(\d+)\}\})/i), s = n[2], e = n[5], include = false, start = weekday[s], end = weekday[e]; Object.keys(regexWeek).map(u => { if (u == end) { include = false; } else if (include && unavailable.indexOf(u) == -1) { weekday.push(u) tempStr = tempStr.replace("{{day" + s + "}}", "{{day" + s + "}}{{day" + (weekday.length - 1) + "}}") } else if (u == start) { include = true; }; }) }) } } if (!weekday && !time) return str; if (m = tempStr.match(/\{\{(day|time)\d+\}\}/gi)) { let hasTime = false; m.map(v => { if (/time/i.test(v)) hasTime = true; else if (/day/.test(v) && hasTime) { hasTime = false; index++; } if (!(index in result)) result[index] = { weekday: [], time: [] } if (/day/.test(v)) result[index].weekday.push(weekday[v.match(/\d+/)[0]]) if (/time/i.test(v)) result[index].time.push(time[v.match(/\d+/)[0]]) }) } let specified = Object.keys(result).reduce((p, c) => p.concat(result[c].weekday), []), reversed = false, respecified = []; if (specified.length != specified.filter((v, i, l) => l.indexOf(v) == l.lastIndexOf(v)).length) available._multiple = true; for (let key in result) { if (reversed && key != "0") { result[key].weekday = result[key].weekday.filter(v => respecified.indexOf(v) == -1); } if (result[key].weekday.length == 0) { if (specified.indexOf("mon") != -1 && specified.indexOf("tue") != -1 && specified.indexOf("wed") != -1 && specified.indexOf("thu") != -1 && specified.indexOf("fri") != -1 && key == "0") { respecified = ["mon", "tue", "wed", "thu", "fri"]; result[key].weekday = [...respecified]; reversed = true; } else if (reversed) { result[key].weekday = specified.filter(v => respecified.indexOf(v) == -1); respecified = [...specified]; } else { result[key].weekday = Object.keys(regexWeek).filter(v => specified.indexOf(v) == -1 && unavailable.indexOf(v) == -1) } } result[key].weekday.map(u => { if (result[key].time.length == 0) { available[u] = false; } else { available[u] = result[key].time.sort() .filter((v, i, l) => l.indexOf(v) == l.lastIndexOf(v)) .reduce((p, c, i) => p += (i == 0 ? "" : i % 2 == 1 ? "-" : ";") + c, "") .split(";"); } }) } return available; } function ParseSearchFields(data, config) { let temp; config = config || {}; if (data && typeof data === "object") { temp = {}; if (config.value) { for (let key in config.value) { if (key in data) { let name = config.value[key].name || key, value = config.value[key].accepted[data[key]]; temp[name] = typeof value !== "undefined" ? value : data[key]; if (name in data) delete data[name]; delete data[key]; } } } if (config.boolean) { config.boolean.map(item => { let key = item, name = item; if (typeof item == "object") { key = item.key; name = item.name; } if (key in data) { temp[name] = data[key] == true; delete data[key]; } }) } if (config.boundary) { config.boundary.map(item => { let key = item, name = item; if (typeof item == "object") { key = item.key; name = item.name; } if (key in data) { let arr = data[key]; if (typeof arr == "string") { arr = arr.split(","); } if (Array.isArray(arr)) { if (arr.length == 2) { temp[name] = []; arr.map(v => { if (Array.isArray(v)) { if (v.length == 2) { temp[name].push(v) } } else if (typeof v === "object") { if ("northing" in v && "easting" in v) { temp[name].push([ v.easting, v.northing, ]) } else if ("longitude" in v && "latitude" in v) { temp[name].push([ v.longitude, v.latitude, ]) } } }) } else if (arr.length == 4) { temp[name] = [ [arr[0], arr[1]], [arr[2], arr[3]], ]; } } delete data[key]; } }) } if (config.rename) { for (let key in config.rename) { if (key in data) { let name = config.rename[key]; if (name in data) delete data[name]; temp[name] = data[key]; delete data[key]; } } } for (let key in data) { temp[key] = data[key]; } } else if (typeof data !== "undefined") { temp = data; } return temp; } module.exports = { APIRequest, CSVFetch, XMLFetch, MatchData, StrToWeekTime, ReplaceURL, RenameFields, GetDataJson, UpdateDataJson, HasDataJson, SearchDataJson, ValidateParameters, ParseSearchFields, Equal: _identical, IsBetween: _inRange }