【问题标题】:locations within radius X around location Y per mysql within the SilverStripe ORMSilverStripe ORM中每个mysql的位置Y周围半径X内的位置
【发布时间】:2016-12-21 11:59:42
【问题描述】:

我在 SilverStripe 3.4.0 中每个 mysql 过滤位置 Y 周围半径 X 内的位置。

到目前为止,我已经实现了一个原始查询来获取圆圈中的 ID,而不是使用它们来过滤每个 SilverStripe ORM,因为我必须根据多个条件进行过滤,而地理过滤器只是其中之一。

另请参阅 Google 的“商店定位器”示例: https://developers.google.com/maps/articles/phpsqlsearch_v3

$searchDistance = '...';
$searchLat = '...';
$searchLng = '...';

$geolimitedIDs = DB::query('SELECT id, (6371 * acos(cos(radians('.$searchLat.')) * cos(radians(Latitude)) * cos( radians(Longitude) - radians('.$searchLng.')) + sin(radians('.$searchLat.')) * sin(radians(Latitude))))
    AS distance
    FROM "DataObject"
    HAVING distance < ' . $searchDistance . '
    ORDER BY distance')->column();

if($geolimitedIDs) {
    $DataObjects = $DataObjects->filter(array(
        'ID' => $geolimitedIDs
    ));
}

在 _config 中,我创建了 DataObject-Tabel MyISAM

DataObject:
  create_table_options:
    MySQLDatabase:
      'ENGINE=MyISAM'

这会提供所需的结果,但需要额外的查询。 是否可以将地理过滤器直接添加到 ORM 中的查询中?

【问题讨论】:

  • 直接向 ORM 添加过滤器是什么意思?
  • @Shadow - SilverStripe 进行延迟加载。这意味着我们可以修改查询 $DataObjects。我的意思是像 $DataObjects->where('...')
  • 由于您的查询只有一个having子句,我会使用dataobject类的having()方法。但是,您可以轻松地将 filter() 方法中的任何内容包含在查询的 where 子句中。将此条件添加到原始查询中可能更容易,而不是尝试找出如何以 SilverStripe 的方式重写原始查询。
  • 我没有说你会删除一个查询,但它会以最优化的方式做到这一点:直接在 mysql 上,因为这就是数据的来源,而不需要在 php 上进行额外的步骤。
  • 附注:您的查询和下面的所有答案(此时)计算每一行的“精确”距离(可能不如它可能的精确,但仍然如此)。虽然当有 1000 行时,单行不需要太多时间,但处理查询需要时间。进行边界框搜索(位置在适合您距离的区域的 NE、SE、SW、NW 坐标内)要快得多,然后如果需要精确的距离,则在 PHP 中过滤这个(小得多)集。

标签: mysql orm geolocation silverstripe


【解决方案1】:

我不确定您所说的“直接在 ORM 中查询”是什么意思,但如果理解为“不调用 Web 服务”,那么我可以提供以下解决方案...

请注意,我建议使用 javascript 直接在商店定位器中使用谷歌地图执行此操作,以便通过 javascript 同步完成计算。

function FilterMemberByPostCodeDistance($params, $query){
    $query->where('Member.PostCode IS NOT NULL')
        ->innerJoin('PostCodeToLocation',"SUBSTRING_INDEX(SUBSTRING_INDEX(Member.PostCode,' ', 1),' ',-1) = PostCodeToLocation.OutCode");
 
    $latitude = (float)$postCodeToLocation->Latitude;
    $longitude = (float)$postCodeToLocation->Longitude;
 
    $fTemp = floatval($params['Distance']) / 111.045;
    $fMagicSquareMinLatitude = $latitude - $fTemp;
    $fMagicSquareMaxLatitude = $latitude + $fTemp;
 
    $fTemp = 50.0 / (111.045 * cos(deg2rad($latitude)));
    $fMagicSquareMinLongitude = $longitude - $fTemp;
    $fMagicSquareMaxLongitude = $longitude + $fTemp;
 
    $query->where(
        //Magic Square - this is a simple square to filter out most out of distance values before the magic circle
        //this is done because the circle calculation is much more expensive that the square
 
        'PostCodeToLocation.Latitude  BETWEEN '.$fMagicSquareMinLatitude.' AND '.$fMagicSquareMaxLatitude.'
            AND PostCodeToLocation.Longitude BETWEEN '.$fMagicSquareMinLongitude.' AND '.$fMagicSquareMaxLongitude
 
        //Magic Circle (https://en.wikipedia.org/wiki/Haversine_formula)
        //This is what does the complicated maths to determine if the postcode is in the cirectle or not
        //not as we are using out codes only, this is a "good estimate" but not 100% accurate
 
        //.' AND acos(sin(RADIANS('.$latitude.'))
        //  * sin(RADIANS(PostCodeToLocation.Latitude))
        //  + cos(RADIANS('.$latitude.'))
        //  * cos(RADIANS(PostCodeToLocation.Latitude))
        //  * cos(RADIANS(PostCodeToLocation.Longitude)
        //  - (RADIANS('.$longitude.'))))
        //  * 6371 <= '.($params['Distance'] * 1.60934) //Kilometers
 
        //REFACTOR of above to process more upfront within PHP
        .' AND acos(sin('.deg2rad($latitude).')
           * sin(RADIANS(PostCodeToLocation.Latitude)) + '.cos(deg2rad($latitude))
        .' * cos(RADIANS(PostCodeToLocation.Latitude))
           * cos(RADIANS(PostCodeToLocation.Longitude) - '.deg2rad($longitude).'))
           * 6371 <= '.($params['Distance'] * 1.60934) //Kilometers
    );
    return $query;
}

上面的函数使用邮政编码从公开数据中获取 lon/lat 对(此位仅适用于英国)。

class PostCodeToLocation extends DataObject{

    static $db = array(
        'OutCode'       => 'Varchar(5)',
        'Latitude'      => 'Float',
        'Longitude'     => 'Float'
    );
 
    public static $indexes = array(
        'OutCode'           => true
    );
 
    public function PopulatePostCodeToLocationTable() {
 
        DB::query('TRUNCATE TABLE PostCodeToLocation');
 
        $arrPostCodetoLocations = file(BASE_PATH .'/mysite/.../postcode_outcode_to_latlong.csv');
        if(!empty($arrPostCodetoLocations))
            foreach ($arrPostCodetoLocations as $strPostCodetoLocation) {
                list ($strOutCode,$strLatitude,$strLongitude) = explode(',',$strPostCodetoLocation);
                DB::query("INSERT INTO PostCodeToLocation (OutCode, Latitude, Longitude )
                    VALUES ('".$strOutCode."','".$strLatitude."','".$strLongitude."')"
                );
            }
    }
}

上面的数据文件在here找到。

【讨论】:

  • thx @Barry 但老实说,我更喜欢我所拥有的,而不是你的建议。为了获得位置的纬度/经度,我根据谷歌进行地理编码,但要找到圆圈中的位置,我只需查询每个 MySQL 的数据库。但我想在一个查询中做到这一点。
  • 好吧,正如我所说的“如果你的意思是……”,因为我不太确定。我认为您拥有的是最好的情况,因为您无法将 HAVING 子句放入 ORM。很抱歉误解了我有时会这样做的问题:)
  • 你每天都能学到新东西...api.silverstripe.org/3.1/class-SQLQuery.html#_having
  • 是的,@Barry 很酷,会试一试并报告。如果我失败了,我会在 IRC 上唠叨你 :)
【解决方案2】:

是的,可以使用 ORM 来实现这一点。您可以将DataQuery 拉出DataList,更改它(添加子句等),然后用它更新DataList

类似:

$dataList = MyObject::get();

$dataQuery = $dataList->dataQuery();

$dataQuery->where(...);
$dataQuery->having(...);

$dataList->setDataQuery($dataQuery);

添加和别名选择有点棘手,因为您需要根据DataQuery 修改SQLQuery,但它也应该是可能的,尽管只需添加haversin 公式作为排序将起作用。

$dataList->sort(...)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-16
    • 2013-08-30
    • 2018-12-13
    • 1970-01-01
    相关资源
    最近更新 更多