Skip to content

Commit

Permalink
Merge pull request #271 from davwheat/feat/rtt-import
Browse files Browse the repository at this point in the history
  • Loading branch information
davwheat authored Sep 22, 2024
2 parents 84216d8 + 209aafb commit 2bca628
Show file tree
Hide file tree
Showing 16 changed files with 964 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,5 @@ static/temp/
.wrangler/

~$*.xlsx

.dev.vars
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,19 @@ You'll be able to access the website at [http://local.davw.network:8787](http://
will always resolve to your local machine, and is used to ensure that the website works correctly with the audio files and backend API during
local development.

#### Additional steps

Some features require additional work in order to test locally.

##### Realtime Trains importing

You'll need to create a `.dev.vars` file at the root of the repository with your [RTT API](https://api.rtt.io/) username and password:

```bash
RTT_API_USERNAME=rttapi_username
RTT_API_PASSWORD=your_password
```

### Website contributions

This site is created with the React Framework using Gatsby. If you're not familiar with React or Gatsby, you may want to research them before
Expand Down
182 changes: 182 additions & 0 deletions functions/api/get-service-rtt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import TiplocToStation from './tiploc_to_station.json'

interface RttError {
error: string
}

export interface RttResponse {
serviceUid: string
runDate: string
serviceType: string
isPassenger: boolean
trainIdentity: string
powerType: string
trainClass: string
atocCode: string
atocName: string
performanceMonitored: boolean
origin: Origin[]
destination: Destination[]
locations: Location[]
}

interface Origin {
tiploc: string
description: string
workingTime: string
publicTime: string
crs?: string
}

interface Destination {
tiploc: string
description: string
workingTime: string
publicTime: string
crs?: string
}

type LocationDisplayAs = 'CALL' | 'PASS' | 'ORIGIN' | 'DESTINATION' | 'STARTS' | 'TERMINATES' | 'CANCELLED_CALL' | 'CANCELLED_PASS'

interface Location {
tiploc: string
crs?: string
description: string
origin: Origin[]
destination: Destination[]
isCall: boolean
isPublicCall: boolean
platform?: string
gbttBookedArrival?: string
gbttBookedArrivalNextDay?: true
gbttBookedDeparture?: string
gbttBookedDepartureNextDay?: true
realtimeDeparture?: string
realtimeDepartureNextDay?: true
realtimeArrival?: string
realtimeArrivalNextDay?: true
path?: string
associations?: Association[]
displayAs: LocationDisplayAs
realtimeActivated?: true
realtimeGbttArrivalLateness?: number
realtimeGbttDepartureLateness?: number
}

interface Association {
type: AssociationType
associatedUid: string
associatedRunDate: string
service?: RttResponse
}

type AssociationType = 'divide' | 'join'

async function fetchRttService(
serviceUid: string,
runDate: string,
username: string,
password: string,
followAssociations: boolean = true,
): Promise<RttResponse> {
// YYYY-MM-DD
const year = runDate.slice(0, 4)
const month = runDate.slice(5, 7)
const day = runDate.slice(8, 10)

const req = await fetch(`https://api.rtt.io/api/v1/json/service/${serviceUid}/${year}/${month}/${day}`, {
headers: {
Accept: 'application/json',
Authorization: `Basic ${btoa(`${username}:${password}`)}`,
'User-Agent': 'railannouncements.co.uk',
Server: 'cloudflare pages function',
},
})
if (!req.ok) {
throw new Error(`Failed to fetch RTT service: ${req.status} ${req.statusText}`)
}
const response: RttResponse | RttError = await req.json()

if ('error' in response) {
throw new Error(response.error)
}

// Fill in CRS data
response.origin.forEach(origin => {
const station = TiplocToStation[origin.tiploc]
if (station) {
origin.crs = station.crs
}
})
response.destination.forEach(destination => {
const station = TiplocToStation[destination.tiploc]
if (station) {
destination.crs = station.crs
}
})
response.locations ??= []
response.locations.forEach(location => {
location.origin.forEach(origin => {
const station = TiplocToStation[origin.tiploc]
if (station) {
origin.crs = station.crs
}
})
location.destination.forEach(destination => {
const station = TiplocToStation[destination.tiploc]
if (station) {
destination.crs = station.crs
}
})
})

if (followAssociations) {
await Promise.all(
response.locations
.flatMap(location => {
return location.associations?.map((association): Promise<any> => {
return (async () => {
association.service = await fetchRttService(association.associatedUid, association.associatedRunDate, username, password, false)
})()
})
})
.filter(Boolean),
)
}

return response
}

export const onRequest: PagesFunction<Env> = async ({ request, env }) => {
const { searchParams } = new URL(request.url)

try {
const uid = searchParams.get('uid')
const date = searchParams.get('date')

console.log(uid, date)

if (!uid) {
return Response.json({ error: true, message: 'Missing uid' })
}
if (!date) {
return Response.json({ error: true, message: 'Missing date' })
}
if (!date.match(/^\d{4}-\d{2}-\d{2}$/)) {
return Response.json({ error: true, message: 'Invalid date' })
}

const username = env.RTT_API_USERNAME
const password = env.RTT_API_PASSWORD

const json = await fetchRttService(uid, date, username, password)

return Response.json(json)
} catch (ex) {
if (ex instanceof Error && ex.message) {
return Response.json({ error: true, message: ex.message })
} else {
return Response.json({ error: true, message: 'Unknown error' })
}
}
}
3 changes: 3 additions & 0 deletions functions/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
interface Env {
DB: D1Database

RTT_API_USERNAME: string
RTT_API_PASSWORD: string
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"recoil-persist": "^5.1.0",
"recoil-persistence": "^0.3.0-beta.1",
"uk-railway-stations": "^1.6.0",
"use-html-dialog": "^0.2.1",
"uuid": "^9.0.1"
},
"devDependencies": {
Expand Down
7 changes: 7 additions & 0 deletions src/announcement-data/AnnouncementSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Crunker from '../helpers/crunker'
import type { ICustomAnnouncementPaneProps } from '@components/PanelPanes/CustomAnnouncementPane'
import type { ICustomButtonPaneProps } from '@components/PanelPanes/CustomButtonPane'
import type React from 'react'
import { RttResponse } from '../../functions/api/get-service-rtt'

export interface IPlayOptions {
delayStart: number
Expand Down Expand Up @@ -90,12 +91,18 @@ export interface CustomAnnouncementTab<OptionIds extends string> {
| 'deletePersonalPreset'
| 'system'
| 'defaultState'
| 'importStateFromRttService'
>
| ICustomButtonPaneProps
/**
* Merged with any personal preset to allow migration if new features are added.
*/
defaultState: Record<OptionIds, any>
importStateFromRttService?: (
rttService: RttResponse,
fromLocationIndex: number,
existingOptions: Record<OptionIds, any>,
) => Record<OptionIds, any>
}

export type CustomAnnouncementButton = {
Expand Down
54 changes: 54 additions & 0 deletions src/announcement-data/systems/stations/AmeyPhil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { AudioItem, CustomAnnouncementButton, CustomAnnouncementTab } from '../.
import DelayCodeMapping from './DarwinDelayCodes_Male1.json'
import NamedServices from './named-services.json'

import type { RttResponse } from '../../../../functions/api/get-service-rtt'
import { RttUtils } from '@data/RttUtils'

export type ChimeType = 'three' | 'four' | 'none'
export type FirstClassLocation = 'none' | 'front' | 'middle' | 'rear'

Expand Down Expand Up @@ -5585,6 +5588,7 @@ export default class AmeyPhil extends StationAnnouncementSystem {
nextTrain: {
name: 'Next train',
component: CustomAnnouncementPane,
importStateFromRttService: this.nextTrainOptionsFromRtt.bind(this),
defaultState: {
chime: 'three',
platform: this.PLATFORMS[1],
Expand Down Expand Up @@ -6354,4 +6358,54 @@ export default class AmeyPhil extends StationAnnouncementSystem {
},
} as CustomAnnouncementTab<string>,
}

/**
* @param rttService RTT service data
* @param fromStation The station from which to interpret data from
* @param existingOptions The existing options to copy other settings from (e.g., chime)
* @returns The options to use for the next train announcement
*/
private nextTrainOptionsFromRtt(
rttService: RttResponse,
fromLocationIndex: number,
existingOptions: INextTrainAnnouncementOptions,
): INextTrainAnnouncementOptions {
const originLocation = rttService.locations[fromLocationIndex]

const callingPoints = RttUtils.getCallingPoints(rttService, fromLocationIndex)
const destinationLocations = originLocation.destination.filter(d => {
if (!d.crs) {
console.warn('Destination location has no CRS', d)
return false
}
return true
})

const h = originLocation.gbttBookedDeparture!!.substring(0, 2)
const m = originLocation.gbttBookedDeparture!!.substring(2, 4)

return {
chime: existingOptions.chime,
announceShortPlatformsAfterSplit: existingOptions.announceShortPlatformsAfterSplit,
coaches: existingOptions.coaches,
firstClassLocation: existingOptions.firstClassLocation,

hour: h === '00' ? '00 - midnight' : h,
min: m === '00' ? '00 - hundred-hours' : m,
isDelayed: RttUtils.getIsDelayedDeparture(rttService, fromLocationIndex),
platform: originLocation.platform || existingOptions.platform,
callingAt: callingPoints,
vias: [],
notCallingAtStations: RttUtils.getCancelledCallingPoints(rttService, fromLocationIndex),
terminatingStationCode: destinationLocations.map(d => d.crs!!)[0],
toc: this.processTocForLiveTrains(
rttService.atocName,
rttService.atocCode,
originLocation.crs!!,
destinationLocations.map(d => d.crs!!)[0],
false,
rttService.serviceUid,
).toLowerCase(),
}
}
}
1 change: 1 addition & 0 deletions src/components/AnnouncementPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function AnnouncementPanel({ system }: IProps) {
getPersonalPresets={getPersonalPresets}
deletePersonalPreset={deletePersonalPreset}
defaultState={JSON.stringify(opts.defaultState)}
importStateFromRttService={opts.importStateFromRttService}
/>
</AnnouncementTabErrorBoundary>
)
Expand Down
Loading

0 comments on commit 2bca628

Please sign in to comment.