Distance picker disabled in Closest mode; whole-country pool for nearest-first (radius never empties Closest)
This commit is contained in:
+15
-27
@@ -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]!
|
||||||
|
|||||||
@@ -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 {
|
||||||
.font(.footnote)
|
Text("Closest \(selectedFuel.displayName) stations — tap a station for directions.")
|
||||||
.foregroundStyle(.secondary)
|
.font(.footnote)
|
||||||
|
.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,13 +63,25 @@ 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)
|
||||||
Text("\(totalCount) stations within \(stationLimit) miles")
|
if sortMode == .closest {
|
||||||
.font(.caption2)
|
Text("Distance only applies to Cheapest")
|
||||||
.foregroundStyle(.secondary)
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("\(totalCount) stations within \(stationLimit) miles")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section("Stations") {
|
Section("Stations") {
|
||||||
|
|||||||
Reference in New Issue
Block a user