road distance: pin-fingerprint guard so embedded/live/cached never cross-pollute

The road-distance cache was keyed purely by station ID with no coordinate, so
a value routed to one pin could be served for a same-ID station whose stored
coordinate came from a different source (live fetch vs bundled offline dump
vs a corrected pin), reproducing Maps mismatches.
- RoadDistanceCache now stores each routed pin (CachedRoadDistance{meters,
  lat, lng}) and roadDistanceMeters() only serves a value when the displayed
  station's coordinate matches the pinned one (within ~11 m).
- Kept the existing origin-distance staleness guard.
- Verified embedded sample data (ids y1/se1/...) never collides with real
  relay IDs, and the widget STRICT radius filter drops far-offline samples, so
  no actual leak existed in practice — this closes the theoretical stale-pin
  channel and future-proofs against coordinate fixes.
This commit is contained in:
FuelBoard Contributor
2026-08-20 17:26:33 +01:00
parent 01517ba687
commit 0b27d3d4f6
3 changed files with 61 additions and 14 deletions
+34 -8
View File
@@ -999,7 +999,16 @@ struct FuelStore {
/// user position before a cached road distance is treated as stale.
static let roadDistanceOriginToleranceMeters: Double = 600
static func saveRoadDistances(sourceLat: Double, sourceLng: Double, entries: [String: Double]) {
/// How far (degrees) a station's stored coordinate may drift from the pin
/// a road distance was actually routed to before that cached value is
/// treated as belonging to a different station. Guards against one data
/// source (live fetch, bundled offline dump, or a corrected pin) serving a
/// road distance that was computed for a different coordinate under the
/// same station ID. ~1e-4 deg 11 m tolerates float/rounding jitter but
/// catches any real pin change.
static let roadDistancePinToleranceDegrees: Double = 1e-4
static func saveRoadDistances(sourceLat: Double, sourceLng: Double, entries: [String: CachedRoadDistance]) {
let cache = RoadDistanceCache(sourceLat: sourceLat, sourceLng: sourceLng,
updatedAt: Date().timeIntervalSince1970, entries: entries)
if let data = try? JSONEncoder().encode(cache) {
@@ -1017,10 +1026,11 @@ struct FuelStore {
/// Cached road distance (metres) to a station from the user's location, or
/// nil when not cached / the cache was built too far from where the user
/// is now.
static func roadDistanceMeters(for stationID: String, userLat: Double, userLng: Double) -> Double? {
/// is now / the station's own pin doesn't match the coordinate that was
/// routed.
static func roadDistanceMeters(for station: FuelStation, userLat: Double, userLng: Double) -> Double? {
guard let cache = loadRoadDistances(),
let meters = cache.entries[stationID] else { return nil }
let entry = cache.entries[station.id] else { return nil }
// The cache is only valid near the location it was built from.
let dLat = (userLat - cache.sourceLat) * .pi / 180
let dLng = (userLng - cache.sourceLng) * .pi / 180
@@ -1030,13 +1040,18 @@ struct FuelStore {
sin(dLng / 2) * sin(dLng / 2)
let originDistanceMeters = r * 2 * atan2(sqrt(a), sqrt(1 - a))
guard originDistanceMeters <= roadDistanceOriginToleranceMeters else { return nil }
return meters
// Pin fingerprint: never serve a routed value for a coordinate we
// didn't actually route to. This is the guard that keeps embedded /
// live / cached station sets from injecting each other's road metres.
guard abs(entry.lat - station.lat) <= roadDistancePinToleranceDegrees,
abs(entry.lng - station.lng) <= roadDistancePinToleranceDegrees else { return nil }
return entry.meters
}
/// Distance (km) to display for a station: cached ROAD distance when
/// available (matches Apple Maps), else straight-line haversine.
static func displayDistanceKM(station: FuelStation, userLat: Double, userLng: Double) -> Double {
if let meters = roadDistanceMeters(for: station.id, userLat: userLat, userLng: userLng) {
if let meters = roadDistanceMeters(for: station, userLat: userLat, userLng: userLng) {
return meters / 1000.0
}
return station.distanceKM(to: userLat, lng2: userLng)
@@ -1103,10 +1118,21 @@ struct FuelStore {
}
/// Cached Apple-Maps road distances for nearby stations (see
/// `FuelStore.roadDistancesKey`). `entries` maps stationID road metres.
/// A single cached road distance plus the station pin it was routed to. Keeping
/// the pin lets `roadDistanceMeters` refuse to serve a route computed for a
/// *different* coordinate under the same ID the guard that stops embedded /
/// live / cached station sets cross-contaminating the distance display.
struct CachedRoadDistance: Codable {
let meters: Double
let lat: Double
let lng: Double
}
/// `FuelStore.roadDistancesKey`). `entries` maps stationID road metres +
/// the routed pin.
struct RoadDistanceCache: Codable {
let sourceLat: Double
let sourceLng: Double
let updatedAt: TimeInterval
let entries: [String: Double]
let entries: [String: CachedRoadDistance]
}