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

8
我正在尝试按照用户提交的邮政编码和距离,对“事件”按照距离排序。
我附上了我的数据库表及其关系的样本。如您所见,“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);
}

这将生成一个数组,其中 postcode 为键,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
将geo_location(点)分割成纬度和经度有多难?我使用一个公式计算超过2,200个地理位置之间的距离。它只使用SQL测量任何点到任何点的距离。在一瞬间按距离排序。但是表格中有纬度和经度单元格。您保存地理数据的方式无法与该公式配合使用。 - Dimitri Mostrey
@DimitriMostrey 你提出了一个很好的观点,我不认为将其分成两列会很困难,知道我有这个解决方案很好(我以前没有考虑过)。 - HelloSpeakman
1个回答

5

我来尝试解决你的问题。但是,正如我在评论中提到的那样,你需要将geo_location拆分为纬度和经度。

完成后,公式如下。这将计算距离(单位:千米)。

$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",
   },
]

您可以在结果中添加更多字段。在此示例中,仅返回id。当然,您也可以添加更多,例如geoms.id,geoms.postcode - Dimitri Mostrey
你可能会想知道数据库如何处理这样的公式。在我的情况下,有2,200个地理条目,它只需要0.016秒。没有缓存参与。当然,latlng字段是索引的。 - Dimitri Mostrey
你知道吗,我开始怀疑了。也许你不需要将lat和lng分开。如果你这样做:$lat = $geom->geo_location['lat'];,然后在公式中,用geoms.geo_location['lat']代替geoms.lat,但是我自己无法测试这个方法。 - Dimitri Mostrey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接