Distance picker disabled in Closest mode; whole-country pool for nearest-first (radius never empties Closest)

This commit is contained in:
FuelBoard Contributor
2026-08-11 19:49:26 +01:00
parent 3cff677729
commit 3bc795317a
2 changed files with 39 additions and 33 deletions
+15 -27
View File
@@ -21,47 +21,35 @@ struct ContentView: View {
@State private var locationManager = LocationManager() @State private var locationManager = LocationManager()
@StateObject private var monitor = ProximityMonitor() @StateObject private var monitor = ProximityMonitor()
/// Stations within the CHOSEN search radius (miles) of the current location /// The pool the list draws from. In Cheapest mode the chosen miles radius
/// the same pool the list shows, so "cheapest" (RAG, TOP, deltas) matches /// bounds it ("best price within X miles"); in Closest mode the radius is
/// exactly what's on screen. STRICT: no fallback to out-of-radius stations /// redundant the whole country sorted nearest-first, because "nearest"
/// (a cached fetch around another location must never leak far results in). /// must never answer with an empty state. STRICT: no fallback to
private var radiusScopedStations: [FuelStation] { /// out-of-radius stations in Cheapest mode.
guard let location else { return [] } 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 let radiusKM = Double(stationLimit) * 1.60934 // chosen miles km
return stations.filter { return selling.filter {
$0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM $0.distanceKM(to: location.lat, lng2: location.lng) <= radiusKM
} }
} }
private var cheapestPrice: Double? { private var cheapestPrice: Double? {
radiusScopedStations.compactMap { $0.prices[selectedFuel] }.min() poolStations.compactMap { $0.prices[selectedFuel] }.min()
} }
private var displayedStations: [FuelStation] { private var displayedStations: [FuelStation] {
// Relay already returns every station within the selected miles radius
// (sorted nearest-first); no local cap needed.
sortedStations sortedStations
} }
private var sortedStations: [FuelStation] { private var sortedStations: [FuelStation] {
// Stations selling the selected fuel, scoped STRICTLY to the chosen let pool = poolStations
// 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
}
switch sortMode { switch sortMode {
case .closest: case .closest:
guard let location else { return available.sorted { $0.prices[selectedFuel]! < $1.prices[selectedFuel]! } } guard let location else { return pool.sorted { $0.prices[selectedFuel]! < $1.prices[selectedFuel]! } }
return available.sorted { lhs, rhs in return pool.sorted { lhs, rhs in
// Closest first; price only breaks ties. // Closest first; price only breaks ties.
let lDist = lhs.distanceKM(to: location.lat, lng2: location.lng) let lDist = lhs.distanceKM(to: location.lat, lng2: location.lng)
let rDist = rhs.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]! return lhs.prices[selectedFuel]! < rhs.prices[selectedFuel]!
} }
case .cheapest: case .cheapest:
return available.sorted { lhs, rhs in return pool.sorted { lhs, rhs in
// Cheapest first; distance only breaks ties. // Cheapest first; distance only breaks ties.
let lPrice = lhs.prices[selectedFuel]! let lPrice = lhs.prices[selectedFuel]!
let rPrice = rhs.prices[selectedFuel]! let rPrice = rhs.prices[selectedFuel]!
+19 -1
View File
@@ -27,9 +27,15 @@ struct StationsView: View {
List { List {
Section { Section {
if let location { if let location {
Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) within \(stationLimit) miles — tap a station for directions.") if sortMode == .closest {
Text("Closest \(selectedFuel.displayName) stations — tap a station for directions.")
.font(.footnote) .font(.footnote)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} else {
Text("Cheapest \(selectedFuel.displayName) within \(stationLimit) miles — tap a station for directions.")
.font(.footnote)
.foregroundStyle(.secondary)
}
} else { } else {
Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) — tap a station for directions.") Text("\(sortMode == .closest ? "Closest" : "Cheapest") \(selectedFuel.displayName) — tap a station for directions.")
.font(.footnote) .font(.footnote)
@@ -57,14 +63,26 @@ struct StationsView: View {
} }
} }
.pickerStyle(.segmented) .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 .onChange(of: stationLimit) { _, newValue in
FuelStore.saveStationLimit(newValue) FuelStore.saveStationLimit(newValue)
} }
.padding(.vertical, 2) .padding(.vertical, 2)
if sortMode == .closest {
Text("Distance only applies to Cheapest")
.font(.caption2)
.foregroundStyle(.secondary)
} else {
Text("\(totalCount) stations within \(stationLimit) miles") Text("\(totalCount) stations within \(stationLimit) miles")
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
}
Section("Stations") { Section("Stations") {
Picker("Sort by", selection: $sortMode) { Picker("Sort by", selection: $sortMode) {