The 5 haversine SQL fragments duplicated across StationController and NationalFuelPredictionService disagreed on float-clamping (LEAST only, GREATEST/LEAST, vs. CASE WHEN). Centralised in App\Services\HaversineQuery with the safe GREATEST(-1.0, LEAST(1.0, …)) form everywhere. withinKm() embeds the radius as a numeric literal (sprintf %F) because PDO + SQLite binds float parameters as strings by default, which breaks numeric comparison against the haversine expression — a NULL filter would silently match all rows. Coordinates remain bound positionally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
42 lines
1.4 KiB
PHP
42 lines
1.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
/**
|
|
* Builds canonical haversine SQL fragments for distance and within-radius
|
|
* filtering. Centralises the float-clamping (GREATEST/LEAST) and the column
|
|
* naming convention used across prediction and station search queries.
|
|
*
|
|
* Assumes the joined/queried table exposes columns `lat` and `lng`.
|
|
*/
|
|
final class HaversineQuery
|
|
{
|
|
private const string DISTANCE_KM_SQL =
|
|
'(6371 * acos(GREATEST(-1.0, LEAST(1.0, '
|
|
.'cos(radians(?)) * cos(radians(lat)) * cos(radians(lng) - radians(?)) '
|
|
.'+ sin(radians(?)) * sin(radians(lat))))))';
|
|
|
|
/**
|
|
* Bare distance-in-km expression. Caller adds aliasing or comparison.
|
|
*
|
|
* @return array{0: string, 1: array{float, float, float}}
|
|
*/
|
|
public static function distanceKm(float $lat, float $lng): array
|
|
{
|
|
return [self::DISTANCE_KM_SQL, [$lat, $lng, $lat]];
|
|
}
|
|
|
|
/**
|
|
* `<= {km}` predicate suitable for whereRaw. The radius is embedded as a
|
|
* numeric literal because PDO + SQLite's whereRaw binds floats as strings
|
|
* by default, which breaks numeric comparison against the haversine
|
|
* expression. The `float` parameter is type-checked and not user input.
|
|
*
|
|
* @return array{0: string, 1: array{float, float, float}}
|
|
*/
|
|
public static function withinKm(float $lat, float $lng, float $km): array
|
|
{
|
|
return [self::DISTANCE_KM_SQL.' <= '.sprintf('%F', $km), [$lat, $lng, $lat]];
|
|
}
|
|
}
|