From a43baaf9d46d745bb1ea10ed387979c806ea86f6 Mon Sep 17 00:00:00 2001 From: Benjamin Howe Date: Wed, 17 Jul 2024 22:01:00 +0100 Subject: [PATCH] Migrate NRE departures script to JavaScript (#56) --- .gitignore | 3 + README.md | 2 +- _config.yml | 1 + .../api/nreDepartures/v1/[[departures]].js | 121 ++++++++++++++++++ 4 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 functions/api/nreDepartures/v1/[[departures]].js 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..1b60a6b --- /dev/null +++ b/functions/api/nreDepartures/v1/[[departures]].js @@ -0,0 +1,121 @@ +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((nreService) => { + const service = {}; + + // verify that the service still calls at the filter station (if applicable) + if (crsFilter) { + const allCallingPoints = [] + nreService["subsequentCallingPoints"].forEach((callingPoints => { + console.log(callingPoints["callingPoint"]); + allCallingPoints.push(...callingPoints["callingPoint"]); + })) + const callingPoint = allCallingPoints.filter(cp => cp["crs"] === crsFilter)[0]; + if (callingPoint["isCancelled"]) { + return; + } + } + + // cancellations + service["cancelled"] = nreService["isCancelled"]; + if ("cancelReason" in nreService) { + if (nreService["cancelReason"].includes(CANCEL_REASON_PREFIX)) { + service["cancelReason"] = `Due to ${nreService["cancelReason"].substring(CANCEL_REASON_PREFIX.length)}`; + } else { + service["cancelReason"] = nreService["cancelReason"]; + } + } + + // timings + service["etd"] = nreService["etd"]; + service["std"] = nreService["std"]; + service["timeForSort"] = service["std"]; + if (service["etd"].includes(":")) { + service["timeForSort"] = service["etd"]; + } + + // other bits + service["destination"] = nreService["destination"][0]["locationName"]; // TODO: handle multiple destinations + service["length"] = nreService["length"].toString(); + if (service["length"] === "0") { + service["length"] = "?"; + } + service["operator"] = nreService["operator"]; + service["operatorCode"] = nreService["operatorCode"]; + service["platform"] = "?"; + if ("platform" in nreService) { + service["platform"] = nreService["platform"]; + } + + services.push(service); + }); + } + + 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"];