Я пытаюсь заказать events по их удаленности от отправленного пользователем почтового индекса и расстояния.
Я приложил образец своих таблиц базы данных и их взаимосвязей, поскольку вы можете видеть, что geom связан с несколькими адресами через postcode, и адреса могут быть связаны с несколькими таблицами (в данном случае с таблицей событий).
Я беру почтовый индекс от конечного пользователя, а также радиус в милях для получения соответствующих событий. Вот пример того, как я добиваюсь этого в Eloquent.
/**
* Extend locale method which initially only gets lat/long for given postcode to search
*
* @param \Illuminate\Database\Eloquent\Builder $query The query builder
* @param \App\Http\Requests\SearchRequest $request The search request
* @return void
*/
protected function locale(Builder $query, SearchRequest $request)
{
$postcode = $this->formatPostcode($request->postcode);
$geom = Geom::query()->where('postcode', $postcode)->first();
if (! $geom || Cache::has('postcodeAPIFailed')) {
return;
}
$lat = $geom->geo_location['lat'];
$long = $geom->geo_location['long'];
// Top-left point of bounding box
$lat1 = $lat - ($request->within / 69);
$long1 = $long - $request->within / abs(cos(deg2rad($lat)) * 69);
// Bottom-right point of bounding box
$lat2 = $lat + ($request->within / 69);
$long2 = $long + $request->within / abs(cos(deg2rad($lat)) * 69);
$query->whereHas('address', function (Builder $query) use ($request, $lat, $long, $lat1, $long1, $lat2, $long2) {
$query->whereHas('geom', function (Builder $query) use ($request, $lat, $long, $lat1, $long1, $lat2, $long2) {
$query->whereRaw('st_within(geo_location, envelope(linestring(point(?, ?), point(?, ?))))', [$long1, $lat1, $long2, $lat2]);
});
});
}
В контроллере после получения результатов поиска мы вычисляем расстояния для каждого из результатов.
if ($request->has('postcode')) {
$postcodeDistances = $this->getDistances($results, $request);
}
Это создает массив с key of postcode и value of distance, то есть $postcodeDistances['L1 0AA'] = '3';, мы отправляем этот массив в представление.
Затем в представлении мы используем следующую логику для отображения расстояния в записи, где это применимо.
@if ($postcodeDistances)
<span>
{{ $postcodeDistances[$result->address->postcode] }}
mile{{ $postcodeDistances[$result->address->postcode] != 1 ? 's' : '' }} away
</span>
@endif
Я пробовал несколько методов, но мне не удалось обновить свой function locale() для упорядочивания по расстоянию. Я подумал, может быть, я могу привязать расстояние к коллекции и использовать метод Laravel, чтобы упорядочить коллекции таким образом, но достижение этого на уровне базы данных было бы идеальным, если последнее даже возможно.
Моя первая попытка заключалась в том, чтобы добавить поле выбора расстояния после whereHas('geom') и упорядочить по новому полю
$query->addSelect(\DB::raw("ST_DISTANCE_SPHERE(geo_location, POINT({$long}, {$lat})) AS distance"));
Я получаю следующую ошибку:
SQLSTATE[21000]: Cardinality violation: 1241 Operand should contain 2 column(s) (SQL: select count(*) as aggregate from `event` where (select count(*) from `address` where `address`.`addressable_id` = `event`.`id` and `address`.`addressable_type` = event and (select count(*), ST_DISTANCE_SPHERE(geo_location, POINT(-2.717472, 53.427078)) AS distance from `geom` where `geom`.`postcode` = `address`.`postcode` and st_within(geo_location, envelope(linestring(point(-3.6903924055016, 52.847367855072), point(-1.7445515944984, 54.006788144928))))) >= 1) >= 1 and (select count(*) from `organisation` where `event`.`organisation_id` = `organisation`.`id` and `status` = 1) >= 1 and `event_template_id` is not null and `date_start` >= 2018-07-31 00:00:00 and `status` in (1, 5))
Я также попытался использовать вместо этого orderByRaw в том же месте, хотя я не получал сообщения об ошибке, результаты не были упорядочены соответствующим образом.
$query->orderByRaw('ST_DISTANCE_SPHERE(geo_location, POINT(?, ?)) ASC', [$long, $lat]);
@DimitriMostrey Вы подняли хороший вопрос, я не думаю, что будет сложно разделить его на две колонки, хорошо знать, что у меня есть это решение (я не рассматривал его раньше).





Я постараюсь решить вашу проблему. Но. Как я уже упоминал в комментарии, вам придется разделить geo_location на lat и lng.
Когда это будет сделано, формула будет следующей. Это позволит рассчитать расстояние в км.
$distance = 50; //max distance in km
$limit = 100; //the amount of selected records
$earthRadiusKm = 6371;
$earthRadiusMiles = 3959;
$postcode = $this->formatPostcode($request->postcode);
$geom = Geom::query()->where('postcode', $postcode)->first();
$lat = $geom->lat;
$lng = $geom->lng;
//assuming your Geom model db name is geoms and the 'id' is id
$postcodeDistances = \DB::table('geoms')->selectRaw("
geoms.id, ( $earthRadiusKm * acos( cos( radians($lat) ) * cos( radians( geoms.lat ) )
* cos( radians( geoms.lng ) - radians($lng) ) + sin( radians($lat) ) *
sin(radians(geoms.lat)) ) ) AS distance, lat, lng
")->havingRaw("distance < $distance")->orderBy('distance')->limit($limit)->get();
Для вашего интереса, если вам нужны результаты в британских милях, я добавил радиус Земли в обе метрики. Поскольку в формуле действительно используется радиус Земли, большие расстояния будут более точными.
Результат (json) должен выглядеть так (результат 2 записи, первая координата всегда является отправной точкой). К вашему сведению, координаты находятся на Филиппинах.
all: [
{#3627
+"id": 1128,
+"distance": 0.0,
+"lat": "15.6672998",
+"lng": "120.7349950",
},
{#3595
+"id": 1535,
+"distance": 9.564007130831,
+"lat": "15.6732128",
+"lng": "120.6458749",
},
]
Вы можете добавить больше полей в результаты. В этом примере возвращается только идентификатор. Вы, конечно, могли бы добавить что-то вроде geoms.id, geoms.postcode,
Вы можете задаться вопросом, как база данных справляется с такой формулой. В моем случае с 2.200 гео-записями это занимает 0,016 сек. Кеширования не требуется. Конечно, поля lat и lng индексируются.
Знаешь что. Я начинаю задумываться. Может быть, вам не нужно разделять на широту и долготу. Что делать, если вы используете $lat = $geom->geo_location['lat']; и в формуле вместо geoms.lat вы используете geoms.geo_location['lat'];, но опять же, я не могу проверить это сам.
Насколько сложно было бы разделить geo_location (point) на широту и долготу? Я использую формулу для расчета расстояний с более чем 2 200 географическими местоположениями. Он измеряет любое расстояние от любой точки до любой точки только с помощью sql. За доли секунды. Упорядочено по расстоянию. Но в таблице есть латинские и длинные ячейки. То, как вы сохраняете свои географические данные, не будет работать с формулой.