Results filter is now search radius in miles (5/10/15); relay fetch scoped to radius, limit 500; changing miles re-fetches; caption shows stations within N miles
This commit is contained in:
@@ -39,7 +39,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var displayedStations: [FuelStation] {
|
private var displayedStations: [FuelStation] {
|
||||||
Array(sortedStations.prefix(stationLimit))
|
// Relay already returns every station within the selected miles radius
|
||||||
|
// (sorted nearest-first); no local cap needed.
|
||||||
|
sortedStations
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sortedStations: [FuelStation] {
|
private var sortedStations: [FuelStation] {
|
||||||
@@ -145,6 +147,10 @@ struct ContentView: View {
|
|||||||
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
// No re-fetch needed — one response carries E5/E10/DIESEL prices.
|
||||||
WidgetCenter.shared.reloadAllTimelines()
|
WidgetCenter.shared.reloadAllTimelines()
|
||||||
}
|
}
|
||||||
|
.onChange(of: stationLimit) { _, _ in
|
||||||
|
// Search radius changed (miles) — cached stations may not cover it.
|
||||||
|
Task { await refresh(force: true) }
|
||||||
|
}
|
||||||
.onChange(of: alertsEnabled) { _, newValue in
|
.onChange(of: alertsEnabled) { _, newValue in
|
||||||
FuelStore.saveAlertsEnabled(newValue)
|
FuelStore.saveAlertsEnabled(newValue)
|
||||||
monitor.setEnabled(newValue)
|
monitor.setEnabled(newValue)
|
||||||
@@ -182,7 +188,11 @@ struct ContentView: View {
|
|||||||
isLoading = true
|
isLoading = true
|
||||||
defer { isLoading = false }
|
defer { isLoading = false }
|
||||||
do {
|
do {
|
||||||
let fetched = try await FuelPriceProvider.active.fetchStations(near: location?.lat, lng: location?.lng, fuel: selectedFuel)
|
let fetched = try await FuelPriceProvider.active.fetchStations(
|
||||||
|
near: location?.lat, lng: location?.lng,
|
||||||
|
fuel: selectedFuel,
|
||||||
|
radiusKM: Double(stationLimit) * 1.60934 // miles → km
|
||||||
|
)
|
||||||
stations = fetched
|
stations = fetched
|
||||||
FuelStore.saveStations(fetched)
|
FuelStore.saveStations(fetched)
|
||||||
FuelStore.saveLastRefresh()
|
FuelStore.saveLastRefresh()
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ struct StationsView: View {
|
|||||||
|
|
||||||
if !stations.isEmpty {
|
if !stations.isEmpty {
|
||||||
Divider()
|
Divider()
|
||||||
Picker("Show", selection: $stationLimit) {
|
Picker("Within", selection: $stationLimit) {
|
||||||
ForEach([10, 25, 50, 75, 100], id: \.self) { count in
|
ForEach(FuelStore.stationRadiusOptions, id: \.self) { miles in
|
||||||
Text("\(count)").tag(count)
|
Text("\(miles) miles").tag(miles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
.pickerStyle(.segmented)
|
||||||
@@ -90,7 +90,7 @@ struct StationsView: View {
|
|||||||
FuelStore.saveStationLimit(newValue)
|
FuelStore.saveStationLimit(newValue)
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
Text("Showing \(stations.count) of \(totalCount) stations")
|
Text("\(totalCount) stations within \(stationLimit) miles")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import Foundation
|
|||||||
|
|
||||||
protocol FuelPriceProviding {
|
protocol FuelPriceProviding {
|
||||||
/// Fetch stations with prices. `location` may be nil (sort by price only).
|
/// Fetch stations with prices. `location` may be nil (sort by price only).
|
||||||
/// Throws on failure so callers can fall back to cached/sample data.
|
/// `radiusKM` bounds the search area (used by the relay). Throws on failure
|
||||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation]
|
/// so callers can fall back to cached/sample data.
|
||||||
|
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double) async throws -> [FuelStation]
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FuelPriceProvider {
|
enum FuelPriceProvider {
|
||||||
@@ -31,15 +32,15 @@ enum FuelPriceProvider {
|
|||||||
struct RelayFuelProvider: FuelPriceProviding {
|
struct RelayFuelProvider: FuelPriceProviding {
|
||||||
var baseURL = URL(string: "http://192.168.1.131:8788")!
|
var baseURL = URL(string: "http://192.168.1.131:8788")!
|
||||||
|
|
||||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] {
|
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double) async throws -> [FuelStation] {
|
||||||
var components = URLComponents(url: baseURL.appendingPathComponent("api/v1/stations"), resolvingAgainstBaseURL: false)!
|
var components = URLComponents(url: baseURL.appendingPathComponent("api/v1/stations"), resolvingAgainstBaseURL: false)!
|
||||||
var query: [URLQueryItem] = [URLQueryItem(name: "fuel", value: fuel.rawValue)]
|
var query: [URLQueryItem] = [URLQueryItem(name: "fuel", value: fuel.rawValue)]
|
||||||
if let lat, let lng {
|
if let lat, let lng {
|
||||||
query.append(URLQueryItem(name: "lat", value: String(lat)))
|
query.append(URLQueryItem(name: "lat", value: String(lat)))
|
||||||
query.append(URLQueryItem(name: "lng", value: String(lng)))
|
query.append(URLQueryItem(name: "lng", value: String(lng)))
|
||||||
query.append(URLQueryItem(name: "radius", value: "50"))
|
query.append(URLQueryItem(name: "radius", value: String(radiusKM)))
|
||||||
}
|
}
|
||||||
query.append(URLQueryItem(name: "limit", value: "100"))
|
query.append(URLQueryItem(name: "limit", value: "500"))
|
||||||
components.queryItems = query
|
components.queryItems = query
|
||||||
|
|
||||||
let (data, response) = try await URLSession.shared.data(from: components.url!)
|
let (data, response) = try await URLSession.shared.data(from: components.url!)
|
||||||
@@ -106,7 +107,7 @@ private struct RelayResponse: Codable {
|
|||||||
/// Real England-wide data comes from the relay (full-UK Fuel Finder CSV/API).
|
/// Real England-wide data comes from the relay (full-UK Fuel Finder CSV/API).
|
||||||
/// Prices in pence/litre.
|
/// Prices in pence/litre.
|
||||||
struct SampleFuelProvider: FuelPriceProviding {
|
struct SampleFuelProvider: FuelPriceProviding {
|
||||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] {
|
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double) async throws -> [FuelStation] {
|
||||||
try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch
|
try await Task.sleep(nanoseconds: 300_000_000) // simulate fetch
|
||||||
return Self.sampleStations
|
return Self.sampleStations
|
||||||
}
|
}
|
||||||
@@ -200,7 +201,7 @@ struct FuelFinderProvider: FuelPriceProviding {
|
|||||||
let clientID: String
|
let clientID: String
|
||||||
let clientSecret: String
|
let clientSecret: String
|
||||||
|
|
||||||
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType) async throws -> [FuelStation] {
|
func fetchStations(near lat: Double?, lng: Double?, fuel: FuelType, radiusKM: Double) async throws -> [FuelStation] {
|
||||||
// TODO: OAuth token → GET /v1/prices → map to FuelStation.
|
// TODO: OAuth token → GET /v1/prices → map to FuelStation.
|
||||||
// The live API requires authentication; see notes above.
|
// The live API requires authentication; see notes above.
|
||||||
throw FuelProviderError.notImplemented
|
throw FuelProviderError.notImplemented
|
||||||
|
|||||||
+10
-6
@@ -140,7 +140,7 @@ struct FuelStore {
|
|||||||
static let locationKey = "fuelboard.lastLocation" // "lat,lng,timestamp"
|
static let locationKey = "fuelboard.lastLocation" // "lat,lng,timestamp"
|
||||||
static let fuelKey = "fuelboard.selectedFuel" // FuelType raw value
|
static let fuelKey = "fuelboard.selectedFuel" // FuelType raw value
|
||||||
static let sortModeKey = "fuelboard.sortMode" // SortMode raw value
|
static let sortModeKey = "fuelboard.sortMode" // SortMode raw value
|
||||||
static let stationLimitKey = "fuelboard.stationLimit" // Int (10/25/50/75/100)
|
static let stationLimitKey = "fuelboard.stationLimitMiles" // Int miles (5/10/15)
|
||||||
static let favouritesKey = "fuelboard.favourites" // [FuelStation] JSON
|
static let favouritesKey = "fuelboard.favourites" // [FuelStation] JSON
|
||||||
static let alertsEnabledKey = "fuelboard.alertsEnabled" // Bool
|
static let alertsEnabledKey = "fuelboard.alertsEnabled" // Bool
|
||||||
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
|
static let alertsRadiusKey = "fuelboard.alertsRadius" // Double km
|
||||||
@@ -209,17 +209,21 @@ struct FuelStore {
|
|||||||
saveString(mode.rawValue, service: sortModeKey)
|
saveString(mode.rawValue, service: sortModeKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Station count limit
|
// MARK: Station search radius (miles)
|
||||||
|
|
||||||
|
/// Search radius options — the results filter shows stations within this
|
||||||
|
/// many miles of the current location.
|
||||||
|
static let stationRadiusOptions = [5, 10, 15]
|
||||||
|
|
||||||
static func loadStationLimit() -> Int {
|
static func loadStationLimit() -> Int {
|
||||||
if let raw = loadString(service: stationLimitKey), let value = Int(raw), [10, 25, 50, 75, 100].contains(value) {
|
if let raw = loadString(service: stationLimitKey), let value = Int(raw), stationRadiusOptions.contains(value) {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return 25
|
return 5
|
||||||
}
|
}
|
||||||
|
|
||||||
static func saveStationLimit(_ limit: Int) {
|
static func saveStationLimit(_ miles: Int) {
|
||||||
saveString(String(limit), service: stationLimitKey)
|
saveString(String(miles), service: stationLimitKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Favourites
|
// MARK: Favourites
|
||||||
|
|||||||
Reference in New Issue
Block a user