diff --git a/.gitignore b/.gitignore index 84c3d59..d9cae96 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ _site .jekyll-metadata vendor node_modules + +# wrangler +.dev.vars .wrangler diff --git a/README.md b/README.md index 96e2dbb..113273c 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The easiest way to run Jekyll is using Docker: ### Using Wrangler -To test some of the dynamic aspects of the website (e.g. [Cloudflare Pages Functions](https://developers.cloudflare.com/pages/functions/)), it is necessary to use [Wrangler](https://developers.cloudflare.com/workers/wrangler/). +To test some of the dynamic aspects of the website (e.g. [Cloudflare Pages Functions](https://developers.cloudflare.com/pages/functions/)), it is necessary to use [Wrangler](https://developers.cloudflare.com/workers/wrangler/). You may also need [a local `.dev.vars` file](https://developers.cloudflare.com/pages/functions/bindings/#interact-with-your-secrets-locally) containing secrets. To set up a local database: diff --git a/_config.yml b/_config.yml index 8059172..a694b38 100644 --- a/_config.yml +++ b/_config.yml @@ -14,6 +14,7 @@ include: - _redirects exclude: + - .wrangler - functions - functions-src - sql diff --git a/functions/api/nreDepartures/v1/[[departures]].js b/functions/api/nreDepartures/v1/[[departures]].js new file mode 100644 index 0000000..093d69f --- /dev/null +++ b/functions/api/nreDepartures/v1/[[departures]].js @@ -0,0 +1,110 @@ +import { USER_AGENT } from "../../../../functions-src/util"; + +// TODO: allow these values to be customised through request params +const NUM_SERVICES = 4; +const TIME_OFFSET = 5; + +export async function onRequest(context) { + if (context.request.method !== "GET") { + return new Response("Invalid request method", { status: 405 }); + } + + const crsLocation = context.params.departures[0]; + if (!crsLocation.match(CRS_REGEX)) { + return new Response(`Invalid crsLocation: ${crsLocation}`, { status: 400 }); + } + const crsFilter = context.params.departures[1]; + if (crsFilter && !crsFilter.match(CRS_REGEX)) { + return new Response(`Invalid crsFilter: ${crsFilter}`, { status: 400 }); + } + + const params = { "timeOffset": TIME_OFFSET }; + if (crsFilter) { + params["filterCrs"] = crsFilter; + } + const paramsString = new URLSearchParams(params).toString(); + const response = await fetch(new Request(`https://api1.raildata.org.uk/1010-live-departure-board-dep/LDBWS/api/20220120/GetDepBoardWithDetails/${crsLocation}?${paramsString}`, { + headers: { + "user-agent": USER_AGENT, + "x-apikey": context.env.RAILDATA_LIVE_DEPARTURES_API_KEY, + }, + })); + const nreDepartures = await response.json(); + + const data = {}; + data["location"] = nreDepartures["locationName"]; + if (crsFilter) { + data["filterLocation"] = nreDepartures["filterLocationName"]; + } + const tsRe = nreDepartures["generatedAt"].match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).\d*(\+\d{2}:\d{2})$/); + data["generatedAt"] = `${tsRe[1]}-${tsRe[2]}-${tsRe[3]}T${tsRe[4]}:${tsRe[5]}:${tsRe[6]}${tsRe[7]}`; + const services = [] + if ("trainServices" in nreDepartures) { + nreDepartures["trainServices"].forEach((service) => { + const thisService = {}; + if (crsFilter) { + let allCallingPoints = [] + service["subsequentCallingPoints"].forEach((callingPoints => { + console.log(callingPoints["callingPoint"]); + allCallingPoints.push(...callingPoints["callingPoint"]); + })) + let callingPoint = allCallingPoints.filter(cp => cp["crs"] === crsFilter)[0]; + if (callingPoint["isCancelled"]) { + return; + } + } + thisService["cancelled"] = service["isCancelled"]; + if ("cancelReason" in service) { + if (service["cancelReason"].includes(CANCEL_REASON_PREFIX)) { + thisService["cancelReason"] = `Due to ${service["cancelReason"].substring(CANCEL_REASON_PREFIX.length)}`; + } + } + thisService["etd"] = service["etd"]; + thisService["std"] = service["std"]; + thisService["timeForSort"] = thisService["std"]; + if (thisService["etd"].includes(":")) { + thisService["timeForSort"] = thisService["etd"]; + } + thisService["destination"] = service["destination"][0]["locationName"]; // TODO: handle multiple destinations + thisService["length"] = service["length"].toString(); + if (thisService["length"] == "0") { + thisService["length"] = "?"; + } + thisService["operator"] = service["operator"]; + thisService["operatorCode"] = service["operatorCode"]; + thisService["platform"] = "?"; + if ("platform" in service) { + thisService["platform"] = service["platform"]; + } + services.push(thisService); + }); + } + + let servicesContainsEarlyMorning = false; + let servicesContainsLateNight = false; + services.forEach((service) => { + const serviceHour = service["timeForSort"].split(":")[0]; + if (serviceHour in SERVICE_HOURS_EARLY_MORNING) { + servicesContainsEarlyMorning = true; + } else if (serviceHour in SERVICE_HOURS_LATE_NIGHT) { + servicesContainsLateNight = true; + } + }); + if (servicesContainsLateNight && servicesContainsEarlyMorning) { + services.forEach((service, index) => { + const serviceHour = service["timeForSort"].split(":")[0]; + const serviceMinute = service["timeForSort"].split(":")[1]; + if (serviceHour in SERVICE_HOURS_EARLY_MORNING) { + this[index]["timeForSort"] = `${parseInt(serviceHour)+24}:${serviceMinute}`; + } + }); + } + + data["services"] = services.sort().splice(0, NUM_SERVICES); + return Response.json(data); +} + +const CANCEL_REASON_PREFIX = "This train has been cancelled because of " +const CRS_REGEX = /^[A-Z]{3}$/; +const SERVICE_HOURS_EARLY_MORNING = ["00", "01", "02", "03"]; +const SERVICE_HOURS_LATE_NIGHT = ["20", "21", "22", "23"];