Stop Approx. distances beside routed values; add Best Value alert mode

Distances (the "Approx. 1.5 mi" report):
- Refuse junk CoreLocation fixes (invalid accuracy, or a coarse >300 m fix
  within 3 km of the position already held; a coarse fix in another city is
  real travel and is still accepted).
- Resolve ONE display basis per render through FuelStore.distanceDisplayOrigin,
  used by rows, radius filter, sorting, savings figures and the Live Activity,
  so a stray fix can no longer re-base the list outside the route cache's 600 m
  ring while the routed values sit unused in the cache.
- Treat an incomplete route set as a state: retry it after 30 s rather than the
  full 10-minute throttle, and repair a stale incomplete flag once every
  in-radius station has a route (nothing left to route, flag stuck down).
- Widget extension resolves the same origin for fetch, radius, ordering and
  labels, and keeps its own fix in fuelboard.widgetLocation instead of
  overwriting the app's GPS fix.

Alerts:
- Best Value as a distinct alert mode on the existing geofence/notification
  pipeline, with a persisted threshold; reverts to cheapest when the feature is
  unconfigured and is not selectable while disabled.

Diagnostics (DEBUG, gated): FB_DIST_FOCUS now prints FB-DIST-ORIGIN (candidate,
accuracy, anchor, offset, resolved branch, served origin), FB_FAKE_FIX feeds a
synthetic fix through the real guard, FB_STALE_INCOMPLETE forces the stale-flag
state, and the route-fill status is surfaced in Settings behind debug mode.
This commit is contained in:
FuelBoard Contributor
2026-10-13 22:04:00 +01:00
parent 2a2823c180
commit 6f177837a5
13 changed files with 1194 additions and 99 deletions
+63
View File
@@ -253,6 +253,11 @@ struct FuelSavingsRankedStation: Equatable {
let pricePencePerLitre: Double
}
struct BestValueAlertCandidate: Equatable {
let station: FuelStation
let result: FuelSavingsResult
}
enum FuelSavingsError: Error, Equatable {
case invalidMPG
case invalidAmount
@@ -417,6 +422,64 @@ enum FuelSavingsCalculator {
}
}
}
static func bestValueAlertCandidate(
stations: [FuelStation],
fuel: FuelType,
settings: FuelSavingsSettings,
amount: FuelSavingsFillAmount,
minimumNetSavingGBP: Double,
journeyMiles: (FuelStation) -> FuelSavingsJourney?
) -> BestValueAlertCandidate? {
guard settings.isConfigured, minimumNetSavingGBP.isFinite else { return nil }
let sellers = stations.filter { $0.prices[fuel] != nil }
guard !sellers.isEmpty else { return nil }
let baseline = FuelSavingsEstimator.baselineStation(sellers, fuel: fuel, journeyMiles: journeyMiles)
var best: BestValueAlertCandidate?
var bestDistance = Double.greatestFiniteMagnitude
var bestPrice = Double.infinity
var baselineJourneyCache: FuelSavingsJourney?
var baselineJourneyLoaded = false
for station in sellers {
let journey = journeyMiles(station)
let baselineJourney: FuelSavingsJourney? = {
guard let baseline else { return nil }
if baseline.id == station.id { return journey }
if !baselineJourneyLoaded {
baselineJourneyCache = journeyMiles(baseline)
baselineJourneyLoaded = true
}
return baselineJourneyCache
}()
guard case .available(let result) = FuelSavingsEstimator.availability(
station: station,
fuel: fuel,
baseline: baseline,
settings: settings,
amount: amount,
journey: journey,
baselineJourney: baselineJourney
), result.status == .worthTheDrive,
result.netSavingGBP >= minimumNetSavingGBP else { continue }
let distance = journey?.miles ?? .greatestFiniteMagnitude
let price = station.prices[fuel] ?? .infinity
guard let current = best else {
best = BestValueAlertCandidate(station: station, result: result)
bestDistance = distance
bestPrice = price
continue
}
if result.netSavingGBP > current.result.netSavingGBP ||
(result.netSavingGBP == current.result.netSavingGBP && distance < bestDistance) ||
(result.netSavingGBP == current.result.netSavingGBP && distance == bestDistance && price < bestPrice) ||
(result.netSavingGBP == current.result.netSavingGBP && distance == bestDistance && price == bestPrice && station.id < current.station.id) {
best = BestValueAlertCandidate(station: station, result: result)
bestDistance = distance
bestPrice = price
}
}
return best
}
}
/// Turns stations into savings estimates.