mirror of
https://github.com/kennethnym/aris.git
synced 2026-02-02 13:11:17 +00:00
refactor: migrate aris-data-source-tfl to aris-source-tfl
Migrates TFL package from old DataSource interface to new FeedSource interface for use with FeedEngine. Changes: - Rename package from @aris/data-source-tfl to @aris/source-tfl - Replace TflDataSource class with TflSource implementing FeedSource - Add dependency on @aris/source-location for LocationKey - Use normalized priority values (0-1) instead of arbitrary numbers - Update tests for FeedSource interface - Update README.md with new package name Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
136
packages/aris-source-tfl/src/tfl-source.ts
Normal file
136
packages/aris-source-tfl/src/tfl-source.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import type { Context, FeedSource } from "@aris/core"
|
||||
|
||||
import { contextValue } from "@aris/core"
|
||||
import { LocationKey } from "@aris/source-location"
|
||||
|
||||
import type {
|
||||
ITflApi,
|
||||
StationLocation,
|
||||
TflAlertData,
|
||||
TflAlertFeedItem,
|
||||
TflAlertSeverity,
|
||||
TflLineId,
|
||||
TflSourceOptions,
|
||||
} from "./types.ts"
|
||||
|
||||
import { TflApi } from "./tfl-api.ts"
|
||||
|
||||
const SEVERITY_PRIORITY: Record<TflAlertSeverity, number> = {
|
||||
closure: 1.0,
|
||||
"major-delays": 0.8,
|
||||
"minor-delays": 0.6,
|
||||
}
|
||||
|
||||
/**
|
||||
* A FeedSource that provides TfL (Transport for London) service alerts.
|
||||
*
|
||||
* Depends on location source for proximity-based sorting. Produces feed items
|
||||
* for tube, overground, and Elizabeth line disruptions.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const tflSource = new TflSource({
|
||||
* apiKey: process.env.TFL_API_KEY!,
|
||||
* lines: ["northern", "victoria", "jubilee"],
|
||||
* })
|
||||
*
|
||||
* const engine = new FeedEngine()
|
||||
* .register(locationSource)
|
||||
* .register(tflSource)
|
||||
*
|
||||
* const { items } = await engine.refresh()
|
||||
* ```
|
||||
*/
|
||||
export class TflSource implements FeedSource<TflAlertFeedItem> {
|
||||
readonly id = "tfl"
|
||||
readonly dependencies = ["location"]
|
||||
|
||||
private readonly client: ITflApi
|
||||
private readonly lines?: TflLineId[]
|
||||
|
||||
constructor(options: TflSourceOptions) {
|
||||
if (!options.client && !options.apiKey) {
|
||||
throw new Error("Either client or apiKey must be provided")
|
||||
}
|
||||
this.client = options.client ?? new TflApi(options.apiKey!)
|
||||
this.lines = options.lines
|
||||
}
|
||||
|
||||
async fetchItems(context: Context): Promise<TflAlertFeedItem[]> {
|
||||
const [statuses, stations] = await Promise.all([
|
||||
this.client.fetchLineStatuses(this.lines),
|
||||
this.client.fetchStations(),
|
||||
])
|
||||
|
||||
const location = contextValue(context, LocationKey)
|
||||
|
||||
const items: TflAlertFeedItem[] = statuses.map((status) => {
|
||||
const closestStationDistance = location
|
||||
? findClosestStationDistance(status.lineId, stations, location.lat, location.lng)
|
||||
: null
|
||||
|
||||
const data: TflAlertData = {
|
||||
line: status.lineId,
|
||||
lineName: status.lineName,
|
||||
severity: status.severity,
|
||||
description: status.description,
|
||||
closestStationDistance,
|
||||
}
|
||||
|
||||
return {
|
||||
id: `tfl-alert-${status.lineId}-${status.severity}`,
|
||||
type: "tfl-alert",
|
||||
priority: SEVERITY_PRIORITY[status.severity],
|
||||
timestamp: context.time,
|
||||
data,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort by severity (desc), then by proximity (asc) if location available
|
||||
items.sort((a, b) => {
|
||||
if (b.priority !== a.priority) {
|
||||
return b.priority - a.priority
|
||||
}
|
||||
if (a.data.closestStationDistance !== null && b.data.closestStationDistance !== null) {
|
||||
return a.data.closestStationDistance - b.data.closestStationDistance
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
return items
|
||||
}
|
||||
}
|
||||
|
||||
function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||||
const R = 6371 // Earth's radius in km
|
||||
const dLat = ((lat2 - lat1) * Math.PI) / 180
|
||||
const dLng = ((lng2 - lng1) * Math.PI) / 180
|
||||
const a =
|
||||
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||
Math.cos((lat1 * Math.PI) / 180) *
|
||||
Math.cos((lat2 * Math.PI) / 180) *
|
||||
Math.sin(dLng / 2) *
|
||||
Math.sin(dLng / 2)
|
||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
|
||||
return R * c
|
||||
}
|
||||
|
||||
function findClosestStationDistance(
|
||||
lineId: TflLineId,
|
||||
stations: StationLocation[],
|
||||
userLat: number,
|
||||
userLng: number,
|
||||
): number | null {
|
||||
const lineStations = stations.filter((s) => s.lines.includes(lineId))
|
||||
if (lineStations.length === 0) return null
|
||||
|
||||
let minDistance = Infinity
|
||||
for (const station of lineStations) {
|
||||
const distance = haversineDistance(userLat, userLng, station.lat, station.lng)
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance
|
||||
}
|
||||
}
|
||||
|
||||
return minDistance
|
||||
}
|
||||
Reference in New Issue
Block a user