php LBS(附近地理位置)功能實現的一些思路

在開發中常常會遇到把數據庫已有經緯度的地方進行距離排序而後返回給用戶
例如一些外賣app打開會返回附近的商店,這個是怎麼作到的呢?php

思路一:

根據用戶當前的位置,用計算經緯度距離的算法逐一計算比對距離,而後進行排序。這裏能夠參考下面這個算法:git

<?php
/**
 * 查找兩個經緯度之間的距離
 *
 * @param  $latitude1   float  起始緯度
 * @param  $longitude1  float  起始經度
 * @param  $latitude2   float  目標緯度
 * @param  $longitude2  float  目標經度
 * @return array(miles=>英里,feet=>英尺,yards=>碼,kilometers=>千米,meters=>米)
 * @example
 *
 *         $point1 = array('lat' => 40.770623, 'long' => -73.964367);
 *         $point2 = array('lat' => 40.758224, 'long' => -73.917404);
 *         $distance = getDistanceBetweenPointsNew($point1['lat'], $point1['long'], $point2['lat'], $point2['long']);
 *         foreach ($distance as $unit => $value) {
 *             echo $unit.': '.number_format($value,4);
 *         }
 *
 *         The example returns the following:
 *
 *         miles: 2.6025       //英里
 *         feet: 13,741.4350   //英尺
 *         yards: 4,580.4783   //碼
 *         kilometers: 4.1884  //千米
 *         meters: 4,188.3894  //米
 *
 */
function getDistanceBetweenPointsNew($latitude1, $longitude1, $latitude2, $longitude2) {
    $theta = $longitude1 - $longitude2;
    $miles = (sin(deg2rad($latitude1)) * sin(deg2rad($latitude2))) + (cos(deg2rad($latitude1)) * cos(deg2rad($latitude2)) * cos(deg2rad($theta)));
    $miles = acos($miles);
    $miles = rad2deg($miles);
    $miles = $miles * 60 * 1.1515;
    $feet = $miles * 5280;
    $yards = $feet / 3;
    $kilometers = $miles * 1.609344;
    $meters = $kilometers * 1000;
    return compact('miles', 'feet', 'yards', 'kilometers', 'meters');
}
?>

這個思路是要每次都獲取所有數據,而後進行不斷的循環計算,對於大數據量來講簡直是噩夢。算法

思路二:

利用二維的經緯度轉換成一維的數據,而後直接sql查詢,無須一一比對。
例如經緯度 110.993736,21.495705 => w7yfm9pjt9b4
這就是geohash算法,這裏簡單說一下,geohash是一種地理位置編碼,經過數學的方法進行必定的轉換,使其與經緯度對應,變成一串可比對的字符串。
這裏不作深刻的瞭解,大概知道一下就好,轉換出來的編碼有必定的規律,例如同一個省份的前幾位字符是同樣的,字符數類似越多,證實距離越近。相似於公民身份證同樣。
有興趣的能夠自行搜索瞭解一下。
下面直接給出轉換的php代碼sql

<?php
/**
 * Encode and decode geohashes
 *
 */
class Geohash {
    private $coding = "0123456789bcdefghjkmnpqrstuvwxyz";
    private $codingMap = array();

    public function Geohash() {
        //build map from encoding char to 0 padded bitfield
        for ($i = 0; $i < 32; $i++) {
            $this->codingMap[substr($this->coding, $i, 1)] = str_pad(decbin($i), 5, "0", STR_PAD_LEFT);
        }

    }

    /**
     * Decode a geohash and return an array with decimal lat,long in it
     */
    public function decode($hash) {
        //decode hash into binary string
        $binary = "";
        $hl = strlen($hash);
        for ($i = 0; $i < $hl; $i++) {
            $binary .= $this->codingMap[substr($hash, $i, 1)];
        }

        //split the binary into lat and log binary strings
        $bl = strlen($binary);
        $blat = "";
        $blong = "";
        for ($i = 0; $i < $bl; $i++) {
            if ($i % 2) {
                $blat = $blat . substr($binary, $i, 1);
            } else {
                $blong = $blong . substr($binary, $i, 1);
            }

        }

        //now concert to decimal
        $lat = $this->binDecode($blat, -90, 90);
        $long = $this->binDecode($blong, -180, 180);

        //figure out how precise the bit count makes this calculation
        $latErr = $this->calcError(strlen($blat), -90, 90);
        $longErr = $this->calcError(strlen($blong), -180, 180);

        //how many decimal places should we use? There's a little art to
        //this to ensure I get the same roundings as geohash.org
        $latPlaces = max(1, -round(log10($latErr))) - 1;
        $longPlaces = max(1, -round(log10($longErr))) - 1;

        //round it
        $lat = round($lat, $latPlaces);
        $long = round($long, $longPlaces);

        return array($lat, $long);
    }

    /**
     * Encode a hash from given lat and long
     */
    public function encode($lat, $long) {
        //how many bits does latitude need?
        $plat = $this->precision($lat);
        $latbits = 1;
        $err = 45;
        while ($err > $plat) {
            $latbits++;
            $err /= 2;
        }

        //how many bits does longitude need?
        $plong = $this->precision($long);
        $longbits = 1;
        $err = 90;
        while ($err > $plong) {
            $longbits++;
            $err /= 2;
        }

        //bit counts need to be equal
        $bits = max($latbits, $longbits);

        //as the hash create bits in groups of 5, lets not
        //waste any bits - lets bulk it up to a multiple of 5
        //and favour the longitude for any odd bits
        $longbits = $bits;
        $latbits = $bits;
        $addlong = 1;
        while (($longbits + $latbits) % 5 != 0) {
            $longbits += $addlong;
            $latbits += !$addlong;
            $addlong = !$addlong;
        }

        //encode each as binary string
        $blat = $this->binEncode($lat, -90, 90, $latbits);
        $blong = $this->binEncode($long, -180, 180, $longbits);

        //merge lat and long together
        $binary = "";
        $uselong = 1;
        while (strlen($blat) + strlen($blong)) {
            if ($uselong) {
                $binary = $binary . substr($blong, 0, 1);
                $blong = substr($blong, 1);
            } else {
                $binary = $binary . substr($blat, 0, 1);
                $blat = substr($blat, 1);
            }
            $uselong = !$uselong;
        }

        //convert binary string to hash
        $hash = "";
        for ($i = 0; $i < strlen($binary); $i += 5) {
            $n = bindec(substr($binary, $i, 5));
            $hash = $hash . $this->coding[$n];
        }

        return $hash;
    }

    /**
     * What's the maximum error for $bits bits covering a range $min to $max
     */
    private function calcError($bits, $min, $max) {
        $err = ($max - $min) / 2;
        while ($bits--) {
            $err /= 2;
        }

        return $err;
    }

    /*
     * returns precision of number
     * precision of 42 is 0.5
     * precision of 42.4 is 0.05
     * precision of 42.41 is 0.005 etc
     */
    private function precision($number) {
        $precision = 0;
        $pt = strpos($number, '.');
        if ($pt !== false) {
            $precision = -(strlen($number) - $pt - 1);
        }

        return pow(10, $precision) / 2;
    }

    /**
     * create binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
     * removing the tail recursion is left an exercise for the reader
     */
    private function binEncode($number, $min, $max, $bitcount) {
        if ($bitcount == 0) {
            return "";
        }

        #echo "$bitcount: $min $max<br>";

        //this is our mid point - we will produce a bit to say
        //whether $number is above or below this mid point
        $mid = ($min + $max) / 2;
        if ($number > $mid) {
            return "1" . $this->binEncode($number, $mid, $max, $bitcount - 1);
        } else {
            return "0" . $this->binEncode($number, $min, $mid, $bitcount - 1);
        }

    }

    /**
     * decodes binary encoding of number as detailed in http://en.wikipedia.org/wiki/Geohash#Example
     * removing the tail recursion is left an exercise for the reader
     */
    private function binDecode($binary, $min, $max) {
        $mid = ($min + $max) / 2;

        if (strlen($binary) == 0) {
            return $mid;
        }

        $bit = substr($binary, 0, 1);
        $binary = substr($binary, 1);

        if ($bit == 1) {
            return $this->binDecode($binary, $mid, $max);
        } else {
            return $this->binDecode($binary, $min, $mid);
        }

    }
}
?>

把每個經緯度都轉換成geohash編碼並儲存起來,比對的時候直接sql數據庫

$sql = 'select * from xxx where geohash like "'.$like_geohash.'%"';

這裏like_geohash位數越多說明越精確。
下面是geohash經度距離換算關係,好比geohash若是有7位數,說明範圍在76米左右,八位數則是19米,能夠根據這個進行查詢。api

geohash長度 Lat位數 Lng位數 Lat偏差 Lng偏差 km偏差
1 2 3 ±23 ±23 ±2500
2 5 5 ± 2.8 ±5.6 ±630
3 7 8 ± 0.70 ± 0.7 ±78
4 10 10 ± 0.087 ± 0.18 ±20
5 12 13 ± 0.022 ± 0.022 ±2.4
6 15 15 ± 0.0027 ± 0.0055 ±0.61
7 17 18 ±0.00068 ±0.00068 ±0.076
8 20 20 ±0.000086 ±0.000172 ±0.01911
9 22 23 ±0.000021 ±0.000021 ±0.00478
10 25 25 ±0.00000268 ±0.00000536 ±0.0005971
11 27 28 ±0.00000067 ±0.00000067 ±0.0001492
12 30 30 ±0.00000008 ±0.00000017 ±0.0000186

這個思路明顯優於第一個思路,且查詢起來速度很是快,也不用管有多大的數據,直接在數據庫裏面進行like查詢就好,不過要作好索引才行,缺點也是比較明顯
沒法控制想要的精確訪問,對於返回的數據沒法進行距離的前後排序,不過已經能知足必定的需求,後期再結合思路一也能夠作到距離的前後。app

思路三:

前面兩種方法都是經過很生硬的數學方法進行比對,所計算的也都是直線距離,可是現實並非數學那樣理想。
現實中兩個很靠近的經緯度中間也有可能隔着一條跨不過去的河致使要繞很遠的路,這時就要考慮實際狀況。
很慶幸有些地圖廠商已經幫咱們考慮到了,因此還能夠藉助第三方api。
這裏簡單說一下高德地圖的[雲圖服務API](http://lbs.amap.com/api/yuntu)
一、註冊高德地圖帳戶,並申請雲圖key。
二、建立雲地圖,也就是把你如今的數據放到高德地圖上 [雲圖存儲API](http://lbs.amap.com/api/yuntu/guide/data/storage),這裏能夠手動建立也能調用相關的api建立。
能夠把數據導出excel而後批量上傳,固然後期若是要新增儘可能仍是用它提供的接口進行增量添加。建立完成大概會生成這樣一張表,有tableid,這個後面查詢接口須要使用,字段能夠自定義,方便業務邏輯。

三、使用高德api進行查詢你的雲地圖 [數據檢索](http://lbs.amap.com/api/yuntu/guide/data/search) ,這裏使用周邊檢索,能夠根據你當前的位置進行檢索。
過程其實也不復雜,就是把數據放到高德,高德幫你完成了距離的排序,固然它提供的是比較實際的距離。具體實現須要研究一下高德提供的接口。
這個思路能夠解決精準度問題,但開發成本大,還要跑一遍第三方去獲取數據,可能會犧牲必定效率,具體取捨,仁者見仁吧。ide

相關文章
相關標籤/搜索