From 3bc795317a3903f1edf7f0a27b8529a6e184c3a0 Mon Sep 17 00:00:00 2001 From: FuelBoard Contributor Date: Tue, 11 Aug 2026 19:49:26 +0100 Subject: [PATCH] Distance picker disabled in Closest mode; whole-country pool for nearest-first (radius never empties Closest) --- FuelBoard/ContentView.swift | 42 +++++++++++++----------------------- FuelBoard/StationsView.swift | 30 ++++++++++++++++++++------ 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/FuelBoard/ContentView.swift b/FuelBoard/ContentView.swift index bac33a9..f1ade4f 100644 --- a/FuelBoard/ContentView.swift +++ b/FuelBoard/ContentView.swift @@ -21,47 +21,35 @@ struct ContentView: View { @State private var locationManager = LocationManager() @StateObject private var monitor = ProximityMonitor() - /// Stations within the CHOSEN search radius (miles) of the current location - /// — the same pool the list shows, so "cheapest" (RAG, TOP, deltas) matches - /// exactly what's on screen. STRICT: no fallback to out-of-radius stations - /// (a cached fetch around another location must never leak far results in). - private var radiusScopedStations: [FuelStation] { - guard let location else { return [] } + /// The pool the list draws from. In Cheapest mode the chosen miles radius + /// bounds it ("best price within X miles"); in Closest mode the radius is + /// redundant — the whole country sorted nearest-first, because "nearest" + /// must never answer with an empty state. STRICT: no fallback to + /// out-of-radius stations in Cheapest mode. + private var poolStations: [FuelStation] { + let selling = stations.filter { $0.prices[selectedFuel] != nil } + guard let location else { return selling } + if sortMode == .closest { return selling } // radius disabled in Closest let radiusKM = Double(stationLimit) * 1.60934 // chosen miles → km - return stations.filter { + return selling.filter { $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM } } private var cheapestPrice: Double? { - radiusScopedStations.compactMap { $0.prices[selectedFuel] }.min() + poolStations.compactMap { $0.prices[selectedFuel] }.min() } private var displayedStations: [FuelStation] { - // Relay already returns every station within the selected miles radius - // (sorted nearest-first); no local cap needed. sortedStations } private var sortedStations: [FuelStation] { - // Stations selling the selected fuel, scoped STRICTLY to the chosen - // miles radius from the current location. Local enforcement matters: - // the cache can hold stations from a previous, larger radius (or a - // different area), and the relay filter only applies at fetch time. - let selling = stations.filter { $0.prices[selectedFuel] != nil } - let available: [FuelStation] - if let location { - let radiusKM = Double(stationLimit) * 1.60934 // miles → km - available = selling.filter { - $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM - } - } else { - available = selling - } + let pool = poolStations switch sortMode { case .closest: - guard let location else { return available.sorted { $0.prices[selectedFuel]! < $1.prices[selectedFuel]! } } - return available.sorted { lhs, rhs in + guard let location else { return pool.sorted { $0.prices[selectedFuel]! < $1.prices[selectedFuel]! } } + return pool.sorted { lhs, rhs in // Closest first; price only breaks ties. let lDist = lhs.distanceKM(to: location.lat, lng2: location.lng) let rDist = rhs.distanceKM(to: location.lat, lng2: location.lng) @@ -69,7 +57,7 @@ struct ContentView: View { return lhs.prices[selectedFuel]! < rhs.prices[selectedFuel]! } case .cheapest: - return available.sorted { lhs, rhs in + return pool.sorted { lhs, rhs in // Cheapest first; distance only breaks ties. let lPrice = lhs.prices[selectedFuel]! let rPrice = rhs.prices[selectedFuel]! diff --git a/FuelBoard/StationsView.swift b/FuelBoard/StationsView.swift index 8caca6b..2558e0d 100644 --- a/FuelBoard/StationsView.swift +++ b/FuelBoard/StationsView.swift @@ -27,9 +27,15 @@ struct StationsView: View { List { Section { if let location { - Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) within \(stationLimit) miles — tap a station for directions.") - .font(.footnote) - .foregroundStyle(.secondary) + if sortMode == .closest { + Text("Closest \(selectedFuel.displayName) stations — tap a station for directions.") + .font(.footnote) + .foregroundStyle(.secondary) + } else { + Text("Cheapest \(selectedFuel.displayName) within \(stationLimit) miles — tap a station for directions.") + .font(.footnote) + .foregroundStyle(.secondary) + } } else { Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) — tap a station for directions.") .font(.footnote) @@ -57,13 +63,25 @@ struct StationsView: View { } } .pickerStyle(.segmented) + // In Closest mode the whole country is the pool — the + // radius is meaningless ("nearest" must never return an + // empty state), so the picker is disabled but its value is + // kept for when the user switches back to Cheapest. + .disabled(sortMode == .closest) + .opacity(sortMode == .closest ? 0.5 : 1) .onChange(of: stationLimit) { _, newValue in FuelStore.saveStationLimit(newValue) } .padding(.vertical, 2) - Text("\(totalCount) stations within \(stationLimit) miles") - .font(.caption2) - .foregroundStyle(.secondary) + if sortMode == .closest { + Text("Distance only applies to Cheapest") + .font(.caption2) + .foregroundStyle(.secondary) + } else { + Text("\(totalCount) stations within \(stationLimit) miles") + .font(.caption2) + .foregroundStyle(.secondary) + } } Section("Stations") {