通过关联表在Laravel 5.1中的POINT上按距离排序

时间:2018-07-31 13:02:44

标签: laravel-5 eloquent geometry sql-order-by point

我正在尝试按照events与用户提交的邮政编码和距离的距离来订购。

我已附加了一个数据库表及其关系的示例,如您所见,geom通过postcode与多个地址相关联,并且地址可以与多个表相关联(在这种情况下,事件表)。

Sample Database Tables

我正在获取最终用户的邮政编码以及以英里为单位的半径来检索适当的事件,这是我如何在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 postcodevalue 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]);

1 个答案:

答案 0 :(得分:3)

我将去解决您的问题。但。正如我在评论中提到的那样,您将必须将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",
   },
]