Сортировка по расстоянию в POINT в Laravel 5.1 через связанные таблицы

Я пытаюсь заказать events по их удаленности от отправленного пользователем почтового индекса и расстояния.

Я приложил образец своих таблиц базы данных и их взаимосвязей, поскольку вы можете видеть, что geom связан с несколькими адресами через postcode, и адреса могут быть связаны с несколькими таблицами (в данном случае с таблицей событий).

Сортировка по расстоянию в POINT в Laravel 5.1 через связанные таблицы

Я беру почтовый индекс от конечного пользователя, а также радиус в милях для получения соответствующих событий. Вот пример того, как я добиваюсь этого в 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]);

Насколько сложно было бы разделить geo_location (point) на широту и долготу? Я использую формулу для расчета расстояний с более чем 2 200 географическими местоположениями. Он измеряет любое расстояние от любой точки до любой точки только с помощью sql. За доли секунды. Упорядочено по расстоянию. Но в таблице есть латинские и длинные ячейки. То, как вы сохраняете свои географические данные, не будет работать с формулой.

Dimitri Mostrey 31.07.2018 20:29

@DimitriMostrey Вы подняли хороший вопрос, я не думаю, что будет сложно разделить его на две колонки, хорошо знать, что у меня есть это решение (я не рассматривал его раньше).

HelloSpeakman 01.08.2018 13:00
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
8
2
719
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я постараюсь решить вашу проблему. Но. Как я уже упоминал в комментарии, вам придется разделить 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,

Dimitri Mostrey 05.08.2018 11:28

Вы можете задаться вопросом, как база данных справляется с такой формулой. В моем случае с 2.200 гео-записями это занимает 0,016 сек. Кеширования не требуется. Конечно, поля lat и lng индексируются.

Dimitri Mostrey 05.08.2018 11:33

Знаешь что. Я начинаю задумываться. Может быть, вам не нужно разделять на широту и долготу. Что делать, если вы используете $lat = $geom->geo_location['lat']; и в формуле вместо geoms.lat вы используете geoms.geo_location['lat'];, но опять же, я не могу проверить это сам.

Dimitri Mostrey 05.08.2018 11:43

Другие вопросы по теме