Laravel中两点之间的Haversine距离计算

时间:2016-06-17 07:59:40

标签: php mysql sql laravel haversine

我正在进行laravel应用,我需要找到所有在用户坐标半径范围内的产品。产品与用户有一对多的关系,因此用户可以拥有多种产品。

我发现半正公式可以计算出两点之间的距离,但我似乎无法使它起作用。

我的控制器中有以下查询:

$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表中,但我不确定如何在查询中访问这些产品。我已经尝试了 user.latitude 'user-&gt; latitude',但似乎没有任何效果。

以下是我的模特:

产品型号

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');
    }
}

7 个答案:

答案 0 :(得分:13)

这是我的实施。我已经选择提前为我的查询添加别名,这样我就可以利用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]);
}

您可以将此范围应用于具有latitudelongitude

的任何模型

$location->latitude替换为您要搜索的latitude,并将$location->longitude替换为您要搜索的经度。

根据model.latitude中定义的距离,将model.longitude$location替换为您希望在$radius附近找到的模型。

我知道你有一个正常运作的Haversine公式,但如果你需要Paginate,你就不能使用你提供的代码。

希望这有帮助。

答案 1 :(得分:1)

如果您愿意使用外部软件包,我建议使用无限有用的PHPGeo库。我在一个依赖于这些精确计算的项目中使用它,它工作得很好。它可以帮助您自己从头开始编写计算并进行测试。

https://github.com/mjaschen/phpgeo

以下是Harvesine的文档:https://phpgeo.marcusjaschen.de/#_distance_between_two_coordinates_haversine_formula

答案 2 :(得分: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;
      }
    }   

答案 3 :(得分:0)

我认为你需要的是query builder to build a join。通过连接,您可以在查询中使用两个表的字段。目前您正在使用relationships with eager loading,这将预加载相关用户,但它们不能在SQL中使用(Laravel实际上将执行2个查询)。

无论如何,我不会尝试使用SQL一步计算出半胱氨酸公式,但这并不是真正有效的,在我看来,查询可能会变得难以维护。这就是我要做的事情:

  1. 计算一个最小/最大纬度和经度的信封,它应该比搜索范围大一点。
  2. 使用产品和用户的联接进行快速查询,只需检查用户位置是否在此信封内。
  3. 对于结果列表的每个元素,使用PHP(而不是SQL)计算精确的半径距离,删除半径之外的行,并相应地对列表进行排序。

答案 4 :(得分: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();

答案 5 :(得分: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;
        }

您可以根据自己的要求使用orderbygroupBy

答案 6 :(得分:0)

我在Laravel中找到了解决方案。

    public function near($myLon, $myLat, $areaLon, $areaLat)
{
    $this->applyCriteria();
    $this->applyScope();

    $results = $this->model->select(DB::raw("SQRT(
        POW(69.1 * (latitude - " . $myLat . "), 2) +
        POW(69.1 * (" . $myLon . " - longitude) * COS(latitude / 57.3), 2)) AS distance, SQRT(
        POW(69.1 * (latitude - " . $areaLat . "), 2) +
        POW(69.1 * (" . $areaLon . " - longitude) * COS(latitude / 57.3), 2)) AS area"), "YOUR_TABLE.*")->get();

    $this->resetModel();
    $this->resetScope();

    return $this->parserResult($results);
}

答案以英里为单位,您将必须用数据库表的名称替换YOUR_TABLE。 谢谢,希望对您有帮助