mirror of
https://github.com/tonytins/amtkstat.git
synced 2026-10-24 13:42:51 -04:00
Compare commits
No commits in common. "ef98531ca28fe6a2f66d981b0d94aeecf380aef7" and "79176585b441580b2af85b148c0d8bd7e9b468d9" have entirely different histories.
ef98531ca2
...
79176585b4
6 changed files with 48 additions and 113 deletions
|
|
@ -1,4 +1,3 @@
|
||||||
# https://swiftbundler.dev/documentation/swift-bundler/configuration
|
|
||||||
format_version = 2
|
format_version = 2
|
||||||
|
|
||||||
[apps.AmtrakStatus]
|
[apps.AmtrakStatus]
|
||||||
|
|
@ -6,7 +5,6 @@ identifier = 'com.tonybark.AmtrakStatus'
|
||||||
product = 'AmtrakStatus'
|
product = 'AmtrakStatus'
|
||||||
version = '0.2.103'
|
version = '0.2.103'
|
||||||
icon = "./assets/icon.png"
|
icon = "./assets/icon.png"
|
||||||
category = "public.app-category.travel"
|
|
||||||
|
|
||||||
|
|
||||||
[[apps.AmtrakStatus.overlays]]
|
[[apps.AmtrakStatus.overlays]]
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,6 @@ struct AmtrakClient {
|
||||||
try await get(trainsBaseURL)
|
try await get(trainsBaseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchStaleStatus() async throws -> StaleData {
|
|
||||||
try await get(staleBaseURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] {
|
func fetchAllTrainStatus(num trainNum: String) async throws -> [Train] {
|
||||||
let reponse = try await fetchTrain(number: trainNum)
|
let reponse = try await fetchTrain(number: trainNum)
|
||||||
return reponse.values.flatMap(\.self)
|
return reponse.values.flatMap(\.self)
|
||||||
|
|
|
||||||
|
|
@ -26,21 +26,21 @@ struct AmtrakDateFormatting {
|
||||||
return isoFormatter.date(from: isoString)
|
return isoFormatter.date(from: isoString)
|
||||||
}
|
}
|
||||||
|
|
||||||
func time(from isoString: String?, in zone: TimeZone?, comparedTo originZone: TimeZone? = nil) -> String? {
|
func time(from isoString: String?, timeZone: TimeZone?, relativeTo otherZone: TimeZone? = nil) -> String? {
|
||||||
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
|
guard let isoString, !isoString.isEmpty, let date = isoFormatter.date(
|
||||||
from: isoString) else {
|
from: isoString) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
timeFormatter.timeZone = zone
|
timeFormatter.timeZone = timeZone
|
||||||
|
|
||||||
let formatted = timeFormatter.string(from: date)
|
let formatted = timeFormatter.string(from: date)
|
||||||
|
|
||||||
guard
|
guard
|
||||||
let zone,
|
let timeZone,
|
||||||
let originZone,
|
let otherZone,
|
||||||
zone.identifier != originZone.identifier,
|
timeZone.identifier != otherZone.identifier,
|
||||||
let abbreviation = zone.abbreviation(for: date)
|
let abbreviation = timeZone.abbreviation(for: date)
|
||||||
else {
|
else {
|
||||||
return formatted
|
return formatted
|
||||||
}
|
}
|
||||||
|
|
@ -48,29 +48,6 @@ struct AmtrakDateFormatting {
|
||||||
return "\(formatted) \(abbreviation)"
|
return "\(formatted) \(abbreviation)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func isOnTime(actual: String?, scheduled: String?) -> LateTrain {
|
|
||||||
|
|
||||||
guard
|
|
||||||
let actualDate = parsedDate(from: actual),
|
|
||||||
let scheduledDate = parsedDate(from: scheduled)
|
|
||||||
else {
|
|
||||||
return .unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
|
|
||||||
|
|
||||||
switch differenceInMinutes {
|
|
||||||
case -1...1:
|
|
||||||
return .onTime
|
|
||||||
case ..<0:
|
|
||||||
return .early(minutes: -differenceInMinutes)
|
|
||||||
default:
|
|
||||||
return .late(minutes: differenceInMinutes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func localTime(timeZone: TimeZone?) -> String {
|
func localTime(timeZone: TimeZone?) -> String {
|
||||||
let now = Date()
|
let now = Date()
|
||||||
timeFormatter.timeZone = timeZone
|
timeFormatter.timeZone = timeZone
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ struct ContentView: View {
|
||||||
|
|
||||||
let amtrakDate = AmtrakDateFormatting()
|
let amtrakDate = AmtrakDateFormatting()
|
||||||
let unknown = "Unknown"
|
let unknown = "Unknown"
|
||||||
let staleDataThreshold: TimeInterval = 60 * 60 * 24 // 24 hours
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|
@ -138,7 +137,6 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
var zoneCache: [String: TimeZone?] = [code: lookup.timeZone]
|
var zoneCache: [String: TimeZone?] = [code: lookup.timeZone]
|
||||||
let staleness = await staleTrainLookup()
|
|
||||||
|
|
||||||
var newRows: [TrainStatusRow] = []
|
var newRows: [TrainStatusRow] = []
|
||||||
for number in lookup.trainNumbers {
|
for number in lookup.trainNumbers {
|
||||||
|
|
@ -148,16 +146,15 @@ struct ContentView: View {
|
||||||
where: { $0.code == code
|
where: { $0.code == code
|
||||||
},
|
},
|
||||||
)?.platform
|
)?.platform
|
||||||
let row = await trainStatus(
|
await newRows
|
||||||
|
.append(
|
||||||
|
trainStatus(
|
||||||
for: train,
|
for: train,
|
||||||
code: code,
|
atStationCode: code,
|
||||||
zone: lookup.timeZone,
|
timeZone: lookup.timeZone,
|
||||||
cache: &zoneCache,
|
zoneCache: &zoneCache
|
||||||
staleness: staleness
|
)
|
||||||
)
|
)
|
||||||
if let row {
|
|
||||||
newRows.append(row)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -172,22 +169,34 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func trainStatus(
|
func isOnTime(arr: String?, schArr: String?) -> TrainLateness {
|
||||||
for train: Train,
|
|
||||||
code stationCode: String,
|
|
||||||
zone timeZone: TimeZone?,
|
|
||||||
cache zoneCache: inout [String: TimeZone?],
|
|
||||||
staleness: [String: TimeInterval]
|
|
||||||
) async -> TrainStatusRow? {
|
|
||||||
let leg = train.stations?.first { $0.code == stationCode }
|
|
||||||
|
|
||||||
guard
|
guard
|
||||||
!isStale(leg, timeZone: timeZone) &&
|
let actualDate = amtrakDate.parsedDate(from: arr),
|
||||||
!isDataStale(train.trainID, staleness: staleness)
|
let scheduledDate = amtrakDate.parsedDate(from: schArr)
|
||||||
else {
|
else {
|
||||||
return nil
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let differenceInMinutes = Int(actualDate.timeIntervalSince(scheduledDate) / 60)
|
||||||
|
|
||||||
|
switch differenceInMinutes {
|
||||||
|
case -1...1:
|
||||||
|
return .onTime
|
||||||
|
case ..<0:
|
||||||
|
return .early(minutes: -differenceInMinutes)
|
||||||
|
default:
|
||||||
|
return .late(minutes: differenceInMinutes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trainStatus(
|
||||||
|
for train: Train,
|
||||||
|
atStationCode code: String,
|
||||||
|
timeZone: TimeZone?,
|
||||||
|
zoneCache: inout [String: TimeZone?]
|
||||||
|
) async -> TrainStatusRow {
|
||||||
|
let leg = train.stations?.first { $0.code == code }
|
||||||
let stationName = leg?.name ?? unknown
|
let stationName = leg?.name ?? unknown
|
||||||
let track = leg?.platform ?? ""
|
let track = leg?.platform ?? ""
|
||||||
|
|
||||||
|
|
@ -201,12 +210,12 @@ struct ContentView: View {
|
||||||
|
|
||||||
let time = amtrakDate.time(
|
let time = amtrakDate.time(
|
||||||
from: leg?.arr,
|
from: leg?.arr,
|
||||||
in: timeZone,
|
timeZone: timeZone,
|
||||||
comparedTo: originZone
|
relativeTo: originZone
|
||||||
) ?? amtrakDate.time(
|
) ?? amtrakDate.time(
|
||||||
from: leg?.schArr,
|
from: leg?.schArr,
|
||||||
in: timeZone,
|
timeZone: timeZone,
|
||||||
comparedTo: originZone
|
relativeTo: originZone
|
||||||
)
|
)
|
||||||
|
|
||||||
currentStation = stationName
|
currentStation = stationName
|
||||||
|
|
@ -220,53 +229,14 @@ struct ContentView: View {
|
||||||
train: train.routeName,
|
train: train.routeName,
|
||||||
to: train.destName,
|
to: train.destName,
|
||||||
from: train.origName,
|
from: train.origName,
|
||||||
status: amtrakDate.isOnTime(
|
status: isOnTime(
|
||||||
actual: leg?.arr,
|
arr: leg?.arr,
|
||||||
scheduled: leg?.schArr
|
schArr: leg?.schArr
|
||||||
),
|
),
|
||||||
track: track.isEmpty ? "" : track
|
track: track.isEmpty ? "" : track
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isStale(_ leg: Station?, timeZone: TimeZone?) -> Bool {
|
|
||||||
guard leg?.status == .departed else { return false }
|
|
||||||
|
|
||||||
guard
|
|
||||||
let deparatureDate = amtrakDate.parsedDate(
|
|
||||||
from: leg?.dep
|
|
||||||
) ?? amtrakDate.parsedDate(from: leg?.schDep)
|
|
||||||
else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var calendar = Calendar.current
|
|
||||||
calendar.timeZone = timeZone ?? .current
|
|
||||||
return calendar
|
|
||||||
.compare(
|
|
||||||
deparatureDate,
|
|
||||||
to: Date(),
|
|
||||||
toGranularity: .day
|
|
||||||
) == .orderedAscending
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDataStale(
|
|
||||||
_ trainId: String,
|
|
||||||
staleness: [String: TimeInterval]
|
|
||||||
) -> Bool {
|
|
||||||
guard let timeSince = staleness[trainId] else { return false }
|
|
||||||
return timeSince > staleDataThreshold
|
|
||||||
}
|
|
||||||
|
|
||||||
func staleTrainLookup() async -> [String: TimeInterval] {
|
|
||||||
guard let staleData = try? await client.fetchStaleStatus() else {
|
|
||||||
return [:]
|
|
||||||
}
|
|
||||||
return Dictionary(
|
|
||||||
staleData.lastUpdatedArr.map { ( $0.trainID, $0.timeSince) },
|
|
||||||
uniquingKeysWith: { first, _ in first }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchTimeZone(forStationCode code: String, cache: inout [String: TimeZone?]) async -> TimeZone? {
|
func fetchTimeZone(forStationCode code: String, cache: inout [String: TimeZone?]) async -> TimeZone? {
|
||||||
if let cached = cache[code] {
|
if let cached = cache[code] {
|
||||||
return cached
|
return cached
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,9 @@ struct StationMeta: Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StaleData: Codable {
|
struct StaleData: Codable {
|
||||||
var activeTrains: Int
|
|
||||||
var avgLastUpdate: Double
|
var avgLastUpdate: Double
|
||||||
var lastUpdatedArr: [TrainFreshness]
|
var activeTrains: Int
|
||||||
var medianLastUpdate: Double
|
var stale: Bool
|
||||||
}
|
|
||||||
|
|
||||||
struct TrainFreshness: Codable, Sendable {
|
|
||||||
var trainID: String
|
|
||||||
var timeSince: Double
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias TrainResponse = [String: [Train]]
|
typealias TrainResponse = [String: [Train]]
|
||||||
|
|
@ -105,6 +99,6 @@ struct TrainStatusRow: Identifiable {
|
||||||
var train: String
|
var train: String
|
||||||
var to: String
|
var to: String
|
||||||
var from: String
|
var from: String
|
||||||
var status: LateTrain
|
var status: TrainLateness
|
||||||
var track: String
|
var track: String
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftCrossUI
|
import SwiftCrossUI
|
||||||
|
|
||||||
enum LateTrain {
|
enum TrainLateness {
|
||||||
case onTime
|
case onTime
|
||||||
case early(minutes: Int)
|
case early(minutes: Int)
|
||||||
case late(minutes: Int)
|
case late(minutes: Int)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue