diff --git a/app/Http/Controllers/Api/StationController.php b/app/Http/Controllers/Api/StationController.php index e7db66e..e792863 100644 --- a/app/Http/Controllers/Api/StationController.php +++ b/app/Http/Controllers/Api/StationController.php @@ -9,6 +9,7 @@ use App\Http\Resources\Api\StationResource; use App\Models\Search; use App\Models\Station; use App\Models\User; +use App\Services\HaversineQuery; use App\Services\NationalFuelPredictionService; use App\Services\PlanFeatures; use App\Services\PostcodeService; @@ -43,14 +44,12 @@ class StationController extends Controller $radius = $request->radius(); $sort = $request->sort(); + [$distanceSql, $distanceBindings] = HaversineQuery::distanceKm($lat, $lng); + $all = Station::query() ->selectRaw( - 'stations.*, spc.price_pence, spc.fuel_type, spc.price_effective_at, - (6371 * acos(GREATEST(-1.0, LEAST(1.0, - cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) - + sin(radians(?)) * sin(radians(lat)) - )))) AS distance_km', - [$lat, $lng, $lat], + "stations.*, spc.price_pence, spc.fuel_type, spc.price_effective_at, {$distanceSql} AS distance_km", + $distanceBindings, ) ->join('station_prices_current as spc', function (JoinClause $join) use ($fuelType): void { $join->on('stations.node_id', '=', 'spc.station_id') diff --git a/app/Services/HaversineQuery.php b/app/Services/HaversineQuery.php new file mode 100644 index 0000000..e8d684c --- /dev/null +++ b/app/Services/HaversineQuery.php @@ -0,0 +1,41 @@ +join('stations', 'station_prices_current.station_id', '=', 'stations.node_id') ->where('station_prices_current.fuel_type', $fuelType->value) - ->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat]) + ->whereRaw($radiusSql, $radiusBindings) ->avg('station_prices_current.price_pence'); if ($avg !== null) { @@ -392,11 +394,13 @@ class NationalFuelPredictionService private function computeRegionalMomentumSignal(FuelType $fuelType, float $lat, float $lng): array { // Regional momentum: compare trend of stations within 50km vs national trend + [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50); + $rows = DB::table('station_prices') ->join('stations', 'station_prices.station_id', '=', 'stations.node_id') ->where('station_prices.fuel_type', $fuelType->value) ->where('station_prices.price_effective_at', '>=', now()->subDays(14)) - ->whereRaw('(6371 * acos(CASE WHEN (cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))) > 1.0 THEN 1.0 ELSE (cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))) END)) <= 50', [$lat, $lng, $lat, $lat, $lng, $lat]) + ->whereRaw($radiusSql, $radiusBindings) ->selectRaw('DATE(station_prices.price_effective_at) as day, AVG(station_prices.price_pence) as avg_price') ->groupBy('day') ->orderBy('day') @@ -668,11 +672,13 @@ class NationalFuelPredictionService $dateString = $date->toDateString(); if ($lat !== null && $lng !== null) { + [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50); + $regional = DB::table('station_prices') ->join('stations', 'station_prices.station_id', '=', 'stations.node_id') ->where('station_prices.fuel_type', $fuelType->value) ->whereDate('station_prices.price_effective_at', $dateString) - ->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat]) + ->whereRaw($radiusSql, $radiusBindings) ->avg('station_prices.price_pence'); if ($regional !== null) { @@ -697,11 +703,13 @@ class NationalFuelPredictionService $usedRegional = false; if ($lat !== null && $lng !== null) { + [$radiusSql, $radiusBindings] = HaversineQuery::withinKm($lat, $lng, 50); + $rows = DB::table('station_prices') ->join('stations', 'station_prices.station_id', '=', 'stations.node_id') ->where('station_prices.fuel_type', $fuelType->value) ->where('station_prices.price_effective_at', '>=', now()->subDays($days)->startOfDay()) - ->whereRaw('(6371 * acos(LEAST(1.0, cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) + sin(radians(?)) * sin(radians(lat))))) <= 50', [$lat, $lng, $lat]) + ->whereRaw($radiusSql, $radiusBindings) ->selectRaw('DATE(station_prices.price_effective_at) as day, AVG(station_prices.price_pence) as avg_price') ->groupBy('day') ->orderBy('day')