From 90fb3b73857e09c7c1073daa5e139782804b8ea5 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Thu, 20 Aug 2026 11:54:23 +0100 Subject: [PATCH] road distance: cover all in-radius stations, not just nearest 12 Stations past the nearest-12 cutoff silently fell back to straight-line, which explained the remaining app-vs-Apple-Maps gaps (e.g. 3.5 straight-line vs 5.9 road). Raise the per-pass cap to 40 and route every station within a 25 km straight-line radius (covers the 15 mi max search window) so any station the widget/Live Activity/list can show gets a real road distance. Also harden the new cache tests against persistent keychain state across invocations (reset in setUp) so they pass deterministically. --- FuelBoard/RoadDistanceService.swift | 20 ++++++++++++------- .../FuelBoardSharedTests/FuelBoardTests.swift | 8 ++++++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/FuelBoard/RoadDistanceService.swift b/FuelBoard/RoadDistanceService.swift index 812ce35..2205770 100644 --- a/FuelBoard/RoadDistanceService.swift +++ b/FuelBoard/RoadDistanceService.swift @@ -15,17 +15,22 @@ import MapKit import WidgetKit enum RoadDistanceService { - /// How many nearest stations to route per pass (bounds MKDirections calls). - static let candidatesPerPass = 12 + /// Upper bound on stations routed per pass, so a pass stays a bounded set of + /// route calls. Raised from 12 so stations past the old nearest-12 cutoff + /// still get real road distances instead of a straight-line fallback. + static let candidatesPerPass = 40 + /// Only route stations within this straight-line radius (km). Covers the + /// largest search radius the UI exposes (15 mi ≈ 24.1 km) plus margin, so + /// every station a widget/Live Activity/list can actually show gets routed. + static let maxRadiusKM: Double = 25 /// Don't route again more often than this (minutes). static let throttleMinutes: Double = 10 /// Recompute when the user moves more than this (metres) from the last /// source location. static let moveThresholdMeters: Double = 400 - /// Refreshes the cached road distances for the nearest `candidatesPerPass` - /// stations around `lat`/`lng`. Throttled by time + distance; safe to call - /// on every location fix. + /// Refreshes the cached road distances for the in-radius stations around + /// `lat`/`lng`. Throttled by time + distance; safe to call on every fix. static func refreshIfNeeded(stations: [FuelStation], lat: Double, lng: Double) async { guard !stations.isEmpty else { return } @@ -38,11 +43,12 @@ enum RoadDistanceService { } } - // Candidate stations: nearest by straight-line (these are what widgets - // and the Live Activity show in-radius). + // Candidate stations: the nearest-by-straight-line subset that the UI + // could actually display, capped so a pass stays bounded. let nearest = stations .sorted { $0.distanceKM(to: lat, lng2: lng) < $1.distanceKM(to: lat, lng2: lng) } .prefix(candidatesPerPass) + .filter { $0.distanceKM(to: lat, lng2: lng) <= maxRadiusKM } let origin = CLLocationCoordinate2D(latitude: lat, longitude: lng) var entries: [String: Double] = [:] diff --git a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift index 34123de..d16c7b5 100644 --- a/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift +++ b/FuelBoardTests/Tests/FuelBoardSharedTests/FuelBoardTests.swift @@ -624,6 +624,14 @@ final class OfflineDataLabelTests: XCTestCase { // MARK: - Road distance cache final class RoadDistanceCacheTests: XCTestCase { + override func setUp() { + super.setUp() + // Keychain persists across invocations, so a cache left by an earlier + // test or run would pollute these. Overwrite with an empty, far-away + // cache (source at (0,0)) so every test starts from a clean slate. + FuelStore.saveRoadDistances(sourceLat: 0, sourceLng: 0, entries: [:]) + } + private func station(_ id: String, _ lat: Double, _ lng: Double) -> FuelStation { FuelStation(id: id, name: id, brand: "X", address: "", postcode: "", lat: lat, lng: lng, prices: [:], priceUpdated: nil)