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()
@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]!
+24 -6
View File
@@ -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") {