amtkstat/Sources/AmtrakStatus/ContentView.swift
Tony Bark 3533e5d4b8 Train status now properly gives minutes (or hours) a train is early or late
- Convert AmtrakDateFormatting API from static to instance properties/methods and added parsedDate(from:) to return Date.
- Update time(from:) and date(from:) to instance methods. Improve isOnTime logic to compute actual vs scheduled differences (minutes/hours) and report early/late/on-time.
- Updated ContentView to create an AmtrakDateFormatting instance and use the new API. Remove the Train.serviceDate extension.
2026-08-23 20:32:33 -04:00

183 lines
5.8 KiB
Swift

import SwiftCrossUI
import Foundation
struct ContentView: View {
@State var client = AmtrakClient()
@State var stationCode = "ASD" // Ashland, VA
@State var rows: [TrainStatusRow] = []
@State var isLoading = false
@State var errorMessage: String?
@State var currentStation: String = ""
let unknown = "Unknown"
var body: some View {
VStack {
HStack {
Text("Station Code:")
TextField("e.g. ASD", text: $stationCode)
.frame(width: 100)
Button("Submit") {
Task {
await loadTrainStatus(forStationCode: normalizedStationCode)
}
}.disabled(normalizedStationCode.isEmpty || isLoading)
}.padding(10)
// Most popular Virtual Railfan stations
HStack {
Button("Ashland, VA") {
Task {
await loadTrainStatus(forStationCode: "ASD")
}
}
Button("San Juan Capistrano, CA") {
Task {
await loadTrainStatus(forStationCode: "SNC")
}
}
Button("La Plata, MO") {
Task {
await loadTrainStatus(forStationCode: "LAP")
}
}
}
Table(rows) {
TableColumn("Time", value: \TrainStatusRow.arrival)
TableColumn("Number", value: \TrainStatusRow.trainNum)
TableColumn("Train", value: \TrainStatusRow.routeName)
TableColumn("To", value: \TrainStatusRow.origin)
TableColumn("From", value: \TrainStatusRow.destination)
TableColumn("Status", value: \TrainStatusRow.status)
TableColumn("Track", value: \TrainStatusRow.platform)
}.frame(minWidth: 600)
.overlay(alignment: .bottomTrailing) {
if isLoading {
ProgressView()
.frame(alignment: .topTrailing)
.padding(10)
}
}
HStack {
if let errorMessage {
Text(errorMessage)
} else if !currentStation.isEmpty {
Text(currentStation)
}
}.padding(10)
}
.frame(idealWidth: 500)
.onAppear {
Task {
await loadTrainStatus(forStationCode: normalizedStationCode)
}
}
}
var normalizedStationCode: String {
stationCode.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
}
func loadTrainStatus(forStationCode code: String) async {
guard !code.isEmpty else {
return
}
isLoading = true
errorMessage = nil
defer { isLoading = false }
do {
let trainNumbers = try await client.fetchStationTrainNumbers(
stationCode: code,
)
guard !trainNumbers.isEmpty else {
errorMessage = "\(code) has no trains listed right now"
rows = []
return
}
var newRows: [TrainStatusRow] = []
for number in trainNumbers {
let trains = try await client.fetchAllTrainStatus(num: number)
for train in trains {
let platform = train.stations?.first(
where: { $0.code == code
},
)?.platform
newRows.append(trainStatus(for: train, atStationCode: code))
}
}
rows = newRows
} catch let AmtrakError.stationNotFound(code) {
errorMessage = "Station \(code) not found"
rows = []
} catch {
errorMessage = "Couldn't load train status: \(error.localizedDescription)"
rows = []
}
}
func isOnTime(arr: String?, schArr: String?) -> String {
let amtrakDate = AmtrakDateFormatting()
guard
let actualDate = amtrakDate.parsedDate(from: arr),
let scheduledDate = amtrakDate.parsedDate(from: schArr)
else {
return "On Time"
}
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
switch differenceInMinutes {
case -1...1:
return "On Time"
case ..<0:
return "\(-differenceInMinutes) min early"
case 0..<60:
return "\(differenceInMinutes) min late"
default:
let hours = differenceInMinutes / 60
return "\(hours) hr late"
}
}
func trainStatus(for train: Train, atStationCode code: String) -> TrainStatusRow {
let leg = train.stations?.first { $0.code == code }
let amtrakDate = AmtrakDateFormatting()
let stationName = leg?.name ?? unknown
let arrival = amtrakDate.time(from: leg?.arr) ?? amtrakDate.time(
from: leg?.schArr
)
let depature = amtrakDate.time(from: leg?.dep) ?? amtrakDate.time(
from: leg?.schDep
)
currentStation = "\(stationName) station"
return TrainStatusRow(
trainID: train.trainID,
trainNum: train.trainNum,
routeName: train.routeName,
status: isOnTime(arr: leg?.arr, schArr: leg?.schArr),
platform: leg?.platform ?? "",
arrival: arrival ?? "",
departure: depature ?? "",
origin: train.origName,
destination: train.destName,
// serviceDate: train.serviceDate
)
}
}