From d6e82680ca259e26eadd78fe2f97aa2b6d5b53ad Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Fri, 3 Nov 2023 15:33:21 +0100 Subject: [PATCH 1/4] feat: allow announcing of live trains with no formation data --- .../systems/stations/KeTechPhil.tsx | 179 ++++++++++++++---- 1 file changed, 138 insertions(+), 41 deletions(-) diff --git a/src/announcement-data/systems/stations/KeTechPhil.tsx b/src/announcement-data/systems/stations/KeTechPhil.tsx index 56e9f743e..b89247a76 100644 --- a/src/announcement-data/systems/stations/KeTechPhil.tsx +++ b/src/announcement-data/systems/stations/KeTechPhil.tsx @@ -19,7 +19,7 @@ interface INextTrainAnnouncementOptions { terminatingStationCode: string vias: CallingAtPoint[] callingAt: CallingAtPoint[] - coaches: string + coaches: string | null } interface SplitInfoStop { @@ -27,8 +27,8 @@ interface SplitInfoStop { shortPlatform: string requestStop: boolean portion: { - position: 'any' | 'front' | 'middle' | 'rear' - length: number + position: 'any' | 'front' | 'middle' | 'rear' | 'unknown' + length: number | null } } @@ -483,7 +483,11 @@ export default class KeTechPhil extends StationAnnouncementSystem { return files } - private async getShortPlatforms(callingPoints: CallingAtPoint[], terminatingStation: string, overallLength: number): Promise { + private async getShortPlatforms( + callingPoints: CallingAtPoint[], + terminatingStation: string, + overallLength: number | null, + ): Promise { const files: AudioItem[] = [] const splitData = this.getSplitInfo(callingPoints, terminatingStation, overallLength) @@ -584,7 +588,11 @@ export default class KeTechPhil extends StationAnnouncementSystem { return files } - private async getRequestStops(callingPoints: CallingAtPoint[], terminatingStation: string, overallLength: number): Promise { + private async getRequestStops( + callingPoints: CallingAtPoint[], + terminatingStation: string, + overallLength: number | null, + ): Promise { const files: AudioItem[] = [] const splitData = this.getSplitInfo(callingPoints, terminatingStation, overallLength) @@ -610,19 +618,19 @@ export default class KeTechPhil extends StationAnnouncementSystem { private getSplitInfo( callingPoints: CallingAtPoint[], terminatingStation: string, - overallLength: number, + overallLength: number | null, ): { divideType: CallingAtPoint['splitType'] stopsUpToSplit: SplitInfoStop[] splitA: { stops: SplitInfoStop[] - position: 'front' | 'middle' | 'rear' - length: number + position: 'front' | 'middle' | 'rear' | 'unknown' + length: number | null } | null splitB: { stops: SplitInfoStop[] - position: 'front' | 'middle' | 'rear' - length: number + position: 'front' | 'middle' | 'rear' | 'unknown' + length: number | null } | null } { // If there are no splits, return an empty array @@ -664,6 +672,38 @@ export default class KeTechPhil extends StationAnnouncementSystem { }), ) + if (overallLength === null) { + return { + divideType: dividePoint!!.splitType, + stopsUpToSplit: stopsUntilFormationChange.map(p => ({ + crsCode: p.crsCode, + shortPlatform: p.shortPlatform ?? '', + requestStop: p.requestStop ?? false, + portion: { position: 'any', length: null }, + })), + splitB: { + stops: (dividePoint!!.splitCallingPoints ?? []).map(p => ({ + crsCode: p.crsCode, + shortPlatform: p.shortPlatform ?? '', + requestStop: p.requestStop ?? false, + portion: { position: 'unknown', length: null }, + })), + position: 'unknown', + length: null, + }, + splitA: { + stops: stopsAfterFormationChange.map(p => ({ + crsCode: p.crsCode, + shortPlatform: p.shortPlatform ?? '', + requestStop: p.requestStop ?? false, + portion: { position: aPos, length: aCount }, + })), + position: 'unknown', + length: null, + }, + } + } + const [bPos, bCount] = (dividePoint!!.splitForm ?? 'front.1').split('.').map((x, i) => (i === 1 ? parseInt(x) : x)) as [string, number] const aPos = bPos === 'front' ? 'rear' : 'front' const aCount = Math.min(Math.max(1, overallLength - bCount), 12) @@ -702,7 +742,7 @@ export default class KeTechPhil extends StationAnnouncementSystem { private async getCallingPointsWithSplits( callingPoints: CallingAtPoint[], terminatingStation: string, - overallLength: number, + overallLength: number | null, ): Promise { const files: AudioItem[] = [] @@ -724,13 +764,21 @@ export default class KeTechPhil extends StationAnnouncementSystem { switch (splitData.divideType) { case 'splitTerminates': - files.push( - 'e.where the train will divide', - { id: 'w.please make sure you travel in the correct part of this train', opts: { delayStart: 400 } }, - { id: `s.please note that the ${splitData.splitB!!.position}`, opts: { delayStart: 400 } }, - `m.${splitData.splitB!!.length === 1 ? 'coach' : `${splitData.splitB!!.length} coaches`} will detach at`, - `station.e.${splitPoint.crsCode}`, - ) + files.push('e.where the train will divide', { + id: 'w.please make sure you travel in the correct part of this train', + opts: { delayStart: 400 }, + }) + + if (splitData.splitB!!.position === 'unknown') { + files.push({ id: `s.please note that`, opts: { delayStart: 400 } }, `m.coaches`, `m.will be detached and will terminate at`) + } else { + files.push( + { id: `s.please note that the ${splitData.splitB!!.position}`, opts: { delayStart: 400 } }, + `m.${splitData.splitB!!.length === 1 ? 'coach' : `${splitData.splitB!!.length} coaches`} will detach at`, + ) + } + + files.push(`station.e.${splitPoint.crsCode}`) break case 'splits': @@ -773,18 +821,26 @@ export default class KeTechPhil extends StationAnnouncementSystem { ? [] : [ ...listStops(Array.from(aPortionStops)), - `m.should travel in the ${splitData.splitA!!.position}`, - `platform.s.${splitData.splitA!!.length}`, - 'e.coaches of the train', + ...(splitData.splitA!!.position === 'unknown' + ? ['w.please listen for announcements on board the train'] + : [ + `m.should travel in the ${splitData.splitA!!.position}`, + ...(splitData.splitA!!.length === null ? [] : [`platform.s.${splitData.splitA!!.length}`]), + 'e.coaches of the train', + ]), ] const bFiles = bPortionStops.size === 0 ? [] : [ ...listStops(Array.from(bPortionStops)), - `m.should travel in the ${splitData.splitB!!.position}`, - `platform.s.${splitData.splitB!!.length}`, - 'e.coaches of the train', + ...(splitData.splitB!!.position === 'unknown' + ? ['w.please listen for announcements on board the train'] + : [ + `m.should travel in the ${splitData.splitB!!.position}`, + ...(splitData.splitB!!.length === null ? [] : [`platform.s.${splitData.splitB!!.length}`]), + 'e.coaches of the train', + ]), ] if (splitData.splitA!!.position === 'front') { @@ -803,7 +859,11 @@ export default class KeTechPhil extends StationAnnouncementSystem { return files } - private async getCallingPoints(callingPoints: CallingAtPoint[], terminatingStation: string, overallLength: number): Promise { + private async getCallingPoints( + callingPoints: CallingAtPoint[], + terminatingStation: string, + overallLength: number | null, + ): Promise { const files: AudioItem[] = [] const callingPointsWithSplits = await this.getCallingPointsWithSplits(callingPoints, terminatingStation, overallLength) @@ -856,7 +916,13 @@ export default class KeTechPhil extends StationAnnouncementSystem { ) try { - files.push(...(await this.getCallingPoints(options.callingAt, options.terminatingStationCode, parseInt(options.coaches.split(' ')[0])))) + files.push( + ...(await this.getCallingPoints( + options.callingAt, + options.terminatingStationCode, + options.coaches ? parseInt(options.coaches.split(' ')[0]) : null, + )), + ) } catch (e) { if (e instanceof Error) { alert(e.message) @@ -865,17 +931,31 @@ export default class KeTechPhil extends StationAnnouncementSystem { } } - files.push(...(await this.getShortPlatforms(options.callingAt, options.terminatingStationCode, parseInt(options.coaches.split(' ')[0])))) - files.push(...(await this.getRequestStops(options.callingAt, options.terminatingStationCode, parseInt(options.coaches.split(' ')[0])))) - - const coaches = options.coaches.split(' ')[0] - - // Platforms share the same audio as coach numbers files.push( - { id: 's.this train is formed of', opts: { delayStart: 250 } }, - `platform.s.${coaches}`, - `e.${coaches == '1' ? 'coach' : 'coaches'}`, + ...(await this.getShortPlatforms( + options.callingAt, + options.terminatingStationCode, + options.coaches ? parseInt(options.coaches.split(' ')[0]) : null, + )), ) + files.push( + ...(await this.getRequestStops( + options.callingAt, + options.terminatingStationCode, + options.coaches ? parseInt(options.coaches.split(' ')[0]) : null, + )), + ) + + if (options.coaches) { + const coaches = options.coaches.split(' ')[0] + + // Platforms share the same audio as coach numbers + files.push( + { id: 's.this train is formed of', opts: { delayStart: 250 } }, + `platform.s.${coaches}`, + `e.${coaches == '1' ? 'coach' : 'coaches'}`, + ) + } files.push( { id: `s.platform ${options.platform} for the`, opts: { delayStart: 250 } }, @@ -3783,10 +3863,28 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem if (!services) return - const firstUnannounced = services.find( - s => !nextTrainAnnounced[s.rsid] && !s.isCancelled && s.etd !== 'Delayed' && s.platform !== null && !!s.length, - ) - if (!firstUnannounced) return + const firstUnannounced = services.find(s => { + if (nextTrainAnnounced[s.rsid]) { + console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it was announced recently`) + return false + } + if (s.isCancelled) { + console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it is cancelled`) + return false + } + if (s.etd === 'Delayed') { + console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it has no estimated time`) + return false + } + if (s.platform === null) { + console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it has no confirmed platform`) + return false + } + }) + if (!firstUnannounced) { + console.log('[Live Trains] No suitable unannounced services found') + return + } console.log(firstUnannounced) @@ -3800,7 +3898,7 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem hour: h === '00' ? '00 - midnight' : h, min: m === '00' ? '00 - hundred' : m, toc: system.AVAILABLE_TOCS.find(t => t.toLowerCase() === firstUnannounced.operator.toLowerCase()) ?? '', - coaches: `${firstUnannounced.length} coaches`, + coaches: firstUnannounced.length ? `${firstUnannounced.length} coaches` : null, platform: system.platforms.includes(firstUnannounced.platform.toLowerCase()) ? firstUnannounced.platform.toLowerCase() : '1', terminatingStationCode: firstUnannounced.destination[0].crs, vias: [], @@ -3869,7 +3967,6 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem At the moment, we also won't announce services which:
  • have no platform allocated in data feeds (common at larger stations, even at the time of departure)
  • -
  • have no train formation info (num of coaches)
  • are marked as cancelled or have an estimated time of "delayed"
  • have already been announced by the system in the last hour (only affects services which suddenly get delayed)
From 789aef40f05454fa7c670f2b2570d8fe3327fb70 Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Fri, 3 Nov 2023 15:39:49 +0100 Subject: [PATCH 2/4] fix: wrong file for short platforms --- src/announcement-data/systems/stations/KeTechPhil.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/announcement-data/systems/stations/KeTechPhil.tsx b/src/announcement-data/systems/stations/KeTechPhil.tsx index b89247a76..b0481b183 100644 --- a/src/announcement-data/systems/stations/KeTechPhil.tsx +++ b/src/announcement-data/systems/stations/KeTechPhil.tsx @@ -550,7 +550,7 @@ export default class KeTechPhil extends StationAnnouncementSystem { files.push('m.due to a short platform at', `station.m.${plats[0]}`, 'm.customers for this station', ...s.split(',')) } else { files.push( - 'm.due to short platforms customers for', + 's.due to short platforms customers for', ...this.pluraliseAudio( plats.map(crs => ({ id: crs, opts: { delayStart: 100 } })), 100, From face2f58e4816a5726a4780672ae3c8f9d90eead Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Fri, 3 Nov 2023 16:39:19 +0100 Subject: [PATCH 3/4] feat: announce when arriving services are delayed; additional platform support --- .../systems/stations/KeTechPhil.tsx | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/announcement-data/systems/stations/KeTechPhil.tsx b/src/announcement-data/systems/stations/KeTechPhil.tsx index b0481b183..1c6cd3019 100644 --- a/src/announcement-data/systems/stations/KeTechPhil.tsx +++ b/src/announcement-data/systems/stations/KeTechPhil.tsx @@ -15,6 +15,7 @@ interface INextTrainAnnouncementOptions { platform: string hour: string min: string + isDelayed: boolean toc: string terminatingStationCode: string vias: CallingAtPoint[] @@ -903,8 +904,23 @@ export default class KeTechPhil extends StationAnnouncementSystem { if (options.chime !== 'none') files.push(`sfx - ${options.chime} chimes`) + const plat = parseInt(options.platform) + + const platFiles: AudioItem[] = [] + + if (plat <= 12) { + platFiles.push({ id: `s.platform ${options.platform} for the`, opts: { delayStart: 250 } }) + if (options.isDelayed) platFiles.push('m.delayed') + } else { + platFiles.push( + { id: `s.platform`, opts: { delayStart: 250 } }, + `platform.s.${options.platform}`, + options.isDelayed ? `m.for the delayed` : `m.for the`, + ) + } + files.push( - `s.platform ${options.platform} for the`, + ...platFiles, ...(await this.getFilesForBasicTrainInfo( options.hour, options.min, @@ -958,7 +974,7 @@ export default class KeTechPhil extends StationAnnouncementSystem { } files.push( - { id: `s.platform ${options.platform} for the`, opts: { delayStart: 250 } }, + ...platFiles, ...(await this.getFilesForBasicTrainInfo( options.hour, options.min, @@ -972,7 +988,9 @@ export default class KeTechPhil extends StationAnnouncementSystem { await this.playAudioFiles(files, download) } - readonly platforms = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].flatMap(x => [`${x}`, `${x}a`, `${x}b`, `${x}c`, `${x}d`]).concat(['a', 'b']) + readonly platforms = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + .flatMap(x => [`${x}`, `${x}a`, `${x}b`, `${x}c`, `${x}d`]) + .concat(['13', '14', '15', '16', '17', '18', '19', '20', 'a', 'b']) readonly stations = [ 'AAP', 'AAT', @@ -3633,6 +3651,11 @@ export default class KeTechPhil extends StationAnnouncementSystem { .map(m => ({ title: m.toString().padStart(2, '0'), value: m.toString().padStart(2, '0') })), type: 'select', }, + isDelayed: { + name: 'Delayed?', + default: false, + type: 'boolean', + }, toc: { name: 'TOC', default: '', From f6c6c26a827310c613d54f4b0976411d02f8438a Mon Sep 17 00:00:00 2001 From: David Wheatley Date: Fri, 3 Nov 2023 16:39:37 +0100 Subject: [PATCH 4/4] feat: better live trains logging, correct delay mins calc --- .../systems/stations/KeTechPhil.tsx | 105 ++++++++++++++---- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/src/announcement-data/systems/stations/KeTechPhil.tsx b/src/announcement-data/systems/stations/KeTechPhil.tsx index 1c6cd3019..44d765a42 100644 --- a/src/announcement-data/systems/stations/KeTechPhil.tsx +++ b/src/announcement-data/systems/stations/KeTechPhil.tsx @@ -3863,17 +3863,54 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem } }, [removeOldIds]) + function calculateDelayMins(std: string, etd: string): number { + const isDelayed = etd !== 'On time' && etd !== std + if (!isDelayed) return 0 + + const hasRealEta = (etd as string).includes(':') + + if (!hasRealEta) return 0 + + const sTime = std.split(':') + console.log(sTime) + + const eTime = etd.split(':') + console.log(eTime) + + const [h, m] = sTime.map(x => parseInt(x)) + const [eH, eM] = eTime.map(x => parseInt(x)) + + console.log(`[Delay Mins] ${h}:${m} (${std}) -> ${eH}:${eM} (${etd}) = ${eH * 60 + eM - (h * 60 + m)}`) + + let delayMins = Math.abs(eH * 60 + eM - (h * 60 + m)) + + if (delayMins < 0) { + // crosses over midnight + return calculateDelayMins(std, '23:59') + calculateDelayMins('00:00', etd) + } + + return delayMins + } + useEffect(() => { if (!hasEnabledFeature) return const checkAndPlay = async () => { - if (isPlaying) return + if (isPlaying) { + console.log('[Live Trains] Still playing an announcement; skipping this check') + return + } + + console.log('[Live Trains] Checking for new services') const resp = await fetch( `https://national-rail-api.davwheat.dev/departures/${selectedCrs}?expand=true&numServices=3&timeOffset=0&timeWindow=10`, ) - if (!resp.ok) return + if (!resp.ok) { + console.warn("[Live Trains] Couldn't fetch data from API") + return + } let services @@ -3881,29 +3918,38 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem const data = await resp.json() services = data.trainServices } catch { + console.warn("[Live Trains] Couldn't parse JSON from API") return } - if (!services) return + if (!services) { + console.log('[Live Trains] No services in API response') + return + } + + console.log(`[Live Trains] ${services.length} services found`) const firstUnannounced = services.find(s => { - if (nextTrainAnnounced[s.rsid]) { - console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it was announced recently`) + if (nextTrainAnnounced[s.serviceIdGuid]) { + console.log(`[Live Trains] Skipping ${s.serviceIdGuid} (${s.std} to ${s.destination[0].locationName}) as it was announced recently`) return false } if (s.isCancelled) { - console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it is cancelled`) + console.log(`[Live Trains] Skipping ${s.serviceIdGuid} (${s.std} to ${s.destination[0].locationName}) as it is cancelled`) return false } if (s.etd === 'Delayed') { - console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it has no estimated time`) + console.log(`[Live Trains] Skipping ${s.serviceIdGuid} (${s.std} to ${s.destination[0].locationName}) as it has no estimated time`) return false } if (s.platform === null) { - console.log(`[Live Trains] Skipping ${s.rsid} (${s.std} to ${s.destination[0].locationName}) as it has no confirmed platform`) + console.log(`[Live Trains] Skipping ${s.serviceIdGuid} (${s.std} to ${s.destination[0].locationName}) as it has no confirmed platform`) return false } + + return true }) + if (!firstUnannounced) { console.log('[Live Trains] No suitable unannounced services found') return @@ -3911,15 +3957,20 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem console.log(firstUnannounced) - markTrainIdAnnounced(firstUnannounced.rsid) + markTrainIdAnnounced(firstUnannounced.serviceIdGuid) const h = firstUnannounced.std.split(':')[0] const m = firstUnannounced.std.split(':')[1] + const delayMins = calculateDelayMins(firstUnannounced.std, firstUnannounced.etd) + + console.log(`[Live Trains] Is delayed by ${delayMins} mins`) + const options: INextTrainAnnouncementOptions = { chime: 'four', hour: h === '00' ? '00 - midnight' : h, min: m === '00' ? '00 - hundred' : m, + isDelayed: delayMins > 5, toc: system.AVAILABLE_TOCS.find(t => t.toLowerCase() === firstUnannounced.operator.toLowerCase()) ?? '', coaches: firstUnannounced.length ? `${firstUnannounced.length} coaches` : null, platform: system.platforms.includes(firstUnannounced.platform.toLowerCase()) ? firstUnannounced.platform.toLowerCase() : '1', @@ -3946,24 +3997,36 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem setIsPlaying(true) try { - console.log('PLAYING') + console.log( + `[Live Trains] Playing announcement for ${firstUnannounced.serviceIdGuid} (${firstUnannounced.std} to ${firstUnannounced.destination[0].locationName})`, + ) await nextTrainHandler(options) } catch (e) { - console.log('FAILED') + console.warn(`[Live Trains] Error playing announcement for ${firstUnannounced.serviceIdGuid}; see below`) console.error(e) setIsPlaying(false) } - console.log('COMPLETE') + console.log(`[Live Trains] Announcement for ${firstUnannounced.serviceIdGuid} complete`) setIsPlaying(false) } - const refreshInterval = setInterval(checkAndPlay, 30_000) + const refreshInterval = setInterval(checkAndPlay, 10_000) checkAndPlay() return () => { clearInterval(refreshInterval) } - }, [hasEnabledFeature, nextTrainAnnounced, markTrainIdAnnounced, system, nextTrainHandler, selectedCrs, isPlaying, setIsPlaying]) + }, [ + hasEnabledFeature, + nextTrainAnnounced, + markTrainIdAnnounced, + system, + nextTrainHandler, + selectedCrs, + isPlaying, + setIsPlaying, + calculateDelayMins, + ]) return (
@@ -3986,14 +4049,12 @@ function LiveTrainAnnouncements({ nextTrainHandler, system }: LiveTrainAnnouncem This page will auto-announce all departures in the next 10 minutes from the selected station. Departures outside this timeframe will appear on the board below, but won't be announced until closer to the time.

-

- At the moment, we also won't announce services which: -

    -
  • have no platform allocated in data feeds (common at larger stations, even at the time of departure)
  • -
  • are marked as cancelled or have an estimated time of "delayed"
  • -
  • have already been announced by the system in the last hour (only affects services which suddenly get delayed)
  • -
-

+

At the moment, we also won't announce services which:

+
    +
  • have no platform allocated in data feeds (common at larger stations, even at the time of departure)
  • +
  • are marked as cancelled or have an estimated time of "delayed"
  • +
  • have already been announced by the system in the last hour (only affects services which suddenly get delayed)
  • +

We also can't handle splits (we'll only announce the main portion), request stops, short platforms, delays (e.g., "for the delayed") and many more features. As I said, it's a beta!