Расчет расстояния Haversine между двумя точками в Laravel

Я работаю над приложением Laravel, в котором мне нужно найти все продукты, которые находятся в пределах определенного радиуса от координат пользователя. Продукты имеют отношение один-ко-многим с пользователями, поэтому пользователи могут иметь несколько продуктов.

Я обнаружил, что формула haversine может вычислить расстояние между двумя точками, но я не могу заставить его работать.

У меня есть следующий запрос в моем контроллере:

$latitude = 51.0258761;
$longitude = 4.4775362;
$radius = 20000;

$products = Product::with('user')
->selectRaw("*,
( 6371 * acos( cos( radians(" . $latitude . ") ) *
cos( radians(user.latitude) ) *
cos( radians(user.longitude) - radians(" . $longitude . ") ) +
sin( radians(" . $latitude . ") ) *
sin( radians(user.latitude) ) ) )
AS distance")
->having("distance", "<", $radius)
->orderBy("distance")
->get();

Я установил радиус 20000 для тестирования, и кажется, что все продукты имеют расстояние 5687, ..
Похоже, проблема заключается в том, что широта и долгота продуктов хранятся в таблице «Пользователь», но я не уверен, как получить доступ к ним в моем запросе. я пробовал user.latitude а также ‘User-> широта’ но ничего не работает.

Вот мои модели:

Модель продукта

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
protected $fillable =
[
'soort',
'hoeveelheid',
'hoeveelheidSoort',
'prijsPerStuk',
'extra',
'foto',
'bio'];

public function User()
{
return $this->belongsTo('App\User');
}

public $timestamps = true;
}

Модель пользователя

namespace App;

use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;

class User extends Model implements AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword;

protected $table = 'users';

protected $fillable =
[
'firstName',
'lastName',
'adres',
'profilepic',
'description',
'longitude',
'latitude',
'email',
'password'
];

protected $hidden = ['password', 'remember_token'];

public function product()
{
return $this->hasMany('App\Product');
}
}

5

Решение

Это была моя реализация этого. Я выбрал для псевдонима свой запрос заранее, так что я могу воспользоваться Pagination, Кроме того, вам нужно явно выбрать столбцы, которые вы хотите получить из запроса. добавить их на ->select(), Такие как users.latitude, users.longitude, products.nameили кем бы они ни были.

Я создал область, которая выглядит примерно так:

public function scopeIsWithinMaxDistance($query, $location, $radius = 25) {

$haversine = "(6371 * acos(cos(radians($location->latitude))
* cos(radians(model.latitude))
* cos(radians(model.longitude)
- radians($location->longitude))
+ sin(radians($location->latitude))
* sin(radians(model.latitude))))";
return $query
->select() //pick the columns you want here.
->selectRaw("{$haversine} AS distance")
->whereRaw("{$haversine} < ?", [$radius]);
}

Вы можете применить эту область для любой модели с latitude а такжеlongitude,

Заменить $location->latitude с вашим latitude что вы хотите искать, и заменить $location->longitude с долготой, которую вы хотите искать против.

Заменить model.latitude а также model.longitude с моделями, которые вы хотите найти вокруг $location на основе расстояния, определенного в $radius,

Я знаю, что у вас есть работающая формула Haversine, но если вам нужно разбить на страницы, вы не сможете использовать предоставленный вами код.

Надеюсь, это поможет.

11

Другие решения

Если вы хотите вместо этого использовать внешний пакет, я предлагаю бесконечно полезную библиотеку PHPGeo. Я использовал его в проекте, который опирался на эти точные расчеты, и он работал просто отлично. Это избавляет вас от написания вычислений самостоятельно и проверено на работоспособность.

https://github.com/mjaschen/phpgeo

Вот документация для Harvesine: https://phpgeo.marcusjaschen.de/#_distance_between_two_coordinates_haversine_formula

1

Используя метод Haversine, вы можете рассчитать расстояние между двумя точками, используя эту функцию. Это работает, но я не знаю, как реализовать это в Laravel. Мысль поделиться этим в любом случае.

$lat1 //latitude of first point
$lon1 //longitude of first point
$lat2 //latitude of second point
$lon2 //longitude of second point
$unit- unit- km or mile

function point2point_distance($lat1, $lon1, $lat2, $lon2, $unit='K')
{
$theta = $lon1 - $lon2;
$dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) +  cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta));
$dist = acos($dist);
$dist = rad2deg($dist);
$miles = $dist * 60 * 1.1515;
$unit = strtoupper($unit);

if ($unit == "K")
{
return ($miles * 1.609344);
}
else if ($unit == "N")
{
return ($miles * 0.8684);
}
else
{
return $miles;
}
}
1

Я думаю, что вам нужно это построитель запросов для построения соединения. При объединении у вас есть поля обеих таблиц, доступных в вашем запросе. В настоящее время вы используете отношения с нетерпением загрузки, это предварительно загрузит связанных пользователей, но их нельзя использовать внутри SQL (Laravel фактически выполнит 2 запроса).

В любом случае, я бы не пытался вычислить формулу haversine за один шаг с SQL, это не может быть действительно эффективным, и, по моему мнению, запрос может быть трудно поддерживать. Это то, что я бы сделал вместо этого:

  1. Рассчитайте конверт с минимальной / максимальной широтой и долготой, он должен быть немного больше, чем ваш радиус поиска.
  2. Сделайте быстрый запрос с объединением продукта и пользователя и просто проверьте, находится ли местоположение пользователя в этом конверте.
  3. Для каждого элемента результирующего списка рассчитайте точное расстояние хаверсайна с помощью PHP (не SQL), удалите строки, которые находятся за пределами радиуса, и соответствующим образом отсортируйте список.
0

Это код, который я использую:

            $ownerLongitude = $request['longitude'];
$ownerLatitude = $request['latitude'];
$careType = 1;
$distance = 3;

$raw = DB::raw(' ( 6371 * acos( cos( radians(' . $ownerLatitude . ') ) *
cos( radians( latitude ) ) * cos( radians( longitude ) - radians(' . $ownerLongitude . ') ) +
sin( radians(' . $ownerLatitude . ') ) *
sin( radians( latitude ) ) ) )  AS distance');
$cares = DB::table('users')->select('*', $raw)
->addSelect($raw)->where('type', $careType)
->orderBy('distance', 'ASC')
->having('distance', '<=', $distance)->get();
0

Создайте эту функцию в вашей модели

 public static function getNearBy($lat, $lng, $distance,
$distanceIn = 'miles')
{
if ($distanceIn == 'km') {
$results = self::select(['*', DB::raw('( 0.621371 * 3959 * acos( cos( radians('.$lat.') ) * cos( radians( lat ) ) * cos( radians( lng ) - radians('.$lng.') ) + sin( radians('.$lat.') ) * sin( radians(lat) ) ) ) AS distance')])->havingRaw('distance < '.$distance)->get();
} else {
$results = self::select(['*', DB::raw('( 3959 * acos( cos( radians('.$lat.') ) * cos( radians( lat ) ) * cos( radians( lng ) - radians('.$lng.') ) + sin( radians('.$lat.') ) * sin( radians(lat) ) ) ) AS distance')])->havingRaw('distance < '.$distance)->get();
}
return $results;
}

И вы можете использовать orderby, groupBy согласно вашему требованию

0
По вопросам рекламы [email protected]