, * supermarket_avg_pence: ?float, * major_avg_pence: ?float, * supermarket_gap_pence: ?float, * stations_within_radius: int * } */ public function snapshot(string $fuelType, float $lat, float $lng, int $radiusKm = 25): array { $nationalAvg = $this->nationalAverage($fuelType); $localAvg = $this->localAverage($fuelType, $lat, $lng, 50); $cheapest = $this->cheapestNearby($fuelType, $lat, $lng, $radiusKm, 5); [$superAvg, $majorAvg] = $this->brandSplit($fuelType, $lat, $lng, $radiusKm); $stationCount = $this->stationCountWithin($fuelType, $lat, $lng, $radiusKm); return [ 'national_avg_pence' => $nationalAvg, 'local_avg_pence' => $localAvg, 'local_minus_national_pence' => $localAvg !== null && $nationalAvg !== null ? round($localAvg - $nationalAvg, 1) : null, 'cheapest_nearby' => $cheapest, 'supermarket_avg_pence' => $superAvg, 'major_avg_pence' => $majorAvg, 'supermarket_gap_pence' => $superAvg !== null && $majorAvg !== null ? round($superAvg - $majorAvg, 1) : null, 'stations_within_radius' => $stationCount, ]; } private function nationalAverage(string $fuelType): ?float { $avg = DB::table('station_prices_current') ->where('fuel_type', $fuelType) ->avg('price_pence'); return $avg === null ? null : round((float) $avg / 100, 1); } private function localAverage(string $fuelType, float $lat, float $lng, int $km): ?float { [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km); $avg = DB::table('station_prices_current') ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id') ->where('station_prices_current.fuel_type', $fuelType) ->whereRaw($within, $bindings) ->avg('station_prices_current.price_pence'); return $avg === null ? null : round((float) $avg / 100, 1); } /** * @return array */ private function cheapestNearby(string $fuelType, float $lat, float $lng, int $km, int $limit): array { [$distance, $distanceBindings] = HaversineQuery::distanceKm($lat, $lng); [$within, $withinBindings] = HaversineQuery::withinKm($lat, $lng, $km); $rows = DB::table('station_prices_current') ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id') ->where('station_prices_current.fuel_type', $fuelType) ->whereRaw($within, $withinBindings) ->selectRaw( 'stations.node_id, stations.trading_name as name, stations.brand_name as brand, ' .'station_prices_current.price_pence, '.$distance.' as distance_km', $distanceBindings, ) ->orderBy('station_prices_current.price_pence') ->limit($limit) ->get(); return $rows->map(fn ($r): array => [ 'node_id' => (string) $r->node_id, 'name' => $r->name === null ? null : (string) $r->name, 'brand' => $r->brand === null ? null : (string) $r->brand, 'price_pence' => (int) $r->price_pence, 'distance_km' => round((float) $r->distance_km, 2), ])->all(); } /** @return array{0: ?float, 1: ?float} [supermarket_avg, major_avg] */ private function brandSplit(string $fuelType, float $lat, float $lng, int $km): array { [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km); $rows = DB::table('station_prices_current') ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id') ->where('station_prices_current.fuel_type', $fuelType) ->whereRaw($within, $bindings) ->selectRaw('stations.is_supermarket, AVG(station_prices_current.price_pence) as avg_pence') ->groupBy('stations.is_supermarket') ->get(); $super = null; $major = null; foreach ($rows as $r) { $avg = round((float) $r->avg_pence / 100, 1); if ((int) $r->is_supermarket === 1) { $super = $avg; } else { $major = $avg; } } return [$super, $major]; } private function stationCountWithin(string $fuelType, float $lat, float $lng, int $km): int { [$within, $bindings] = HaversineQuery::withinKm($lat, $lng, $km); return DB::table('station_prices_current') ->join('stations', 'station_prices_current.station_id', '=', 'stations.node_id') ->where('station_prices_current.fuel_type', $fuelType) ->whereRaw($within, $bindings) ->count(); } }