これは、ここに文書化されている速度のための追加の最適化を備えた、 Haversine 式検索を実装するスコープです。
クエリ オブジェクトから生の SQL を取得するよりクリーンな方法があればいいのにと思いますが、残念ながら、プレースホルダーが置換される前に SQL が返されるため、いくつかの呼び出しtoSql()
に依存していました。*Raw
悪くはないのですが、もう少しきれいにしてほしいです。
lat
このコードは、テーブルに列とがあることを前提としていlng
ます。
const DISTANCE_UNIT_KILOMETERS = 111.045;
const DISTANCE_UNIT_MILES = 69.0;
/**
* @param $query
* @param $lat
* @param $lng
* @param $radius numeric
* @param $units string|['K', 'M']
*/
public function scopeNearLatLng($query, $lat, $lng, $radius = 10, $units = 'K')
{
$distanceUnit = $this->distanceUnit($units);
if (!(is_numeric($lat) && $lat >= -90 && $lat <= 90)) {
throw new Exception("Latitude must be between -90 and 90 degrees.");
}
if (!(is_numeric($lng) && $lng >= -180 && $lng <= 180)) {
throw new Exception("Longitude must be between -180 and 180 degrees.");
}
$haversine = sprintf('*, (%f * DEGREES(ACOS(COS(RADIANS(%f)) * COS(RADIANS(lat)) * COS(RADIANS(%f - lng)) + SIN(RADIANS(%f)) * SIN(RADIANS(lat))))) AS distance',
$distanceUnit,
$lat,
$lng,
$lat
);
$subselect = clone $query;
$subselect
->selectRaw(DB::raw($haversine));
// Optimize the query, see details here:
// http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/
$latDistance = $radius / $distanceUnit;
$latNorthBoundary = $lat - $latDistance;
$latSouthBoundary = $lat + $latDistance;
$subselect->whereRaw(sprintf("lat BETWEEN %f AND %f", $latNorthBoundary, $latSouthBoundary));
$lngDistance = $radius / ($distanceUnit * cos(deg2rad($lat)));
$lngEastBoundary = $lng - $lngDistance;
$lngWestBoundary = $lng + $lngDistance;
$subselect->whereRaw(sprintf("lng BETWEEN %f AND %f", $lngEastBoundary, $lngWestBoundary));
$query
->from(DB::raw('(' . $subselect->toSql() . ') as d'))
->where('distance', '<=', $radius);
}
/**
* @param $units
*/
private function distanceUnit($units = 'K')
{
if ($units == 'K') {
return static::DISTANCE_UNIT_KILOMETERS;
} elseif ($units == 'M') {
return static::DISTANCE_UNIT_MILES;
} else {
throw new Exception("Unknown distance unit measure '$units'.");
}
}
これは次のように使用できます。
$places->NearLatLng($lat, $lng, $radius, $units);
$places->orderBy('distance');
生成された SQL は、おおよそ次のようになります。
select
*
from
(
select
*,
(
'111.045' * DEGREES(
ACOS(
COS(
RADIANS('45.5088')
) * COS(
RADIANS(lat)
) * COS(
RADIANS('-73.5878' - lng)
) + SIN(
RADIANS('45.5088')
) * SIN(
RADIANS(lat)
)
)
)
) AS distance
from
`places`
where lat BETWEEN 45.418746 AND 45.598854
and lng BETWEEN -73.716301 AND -73.459299
) as d
where `distance` <= 10
order by `distance` asc