1. 前言
??在我們平時使用美團,餓了么等app進行訂餐,或者使用貓眼進行訂電影票的時候,都有一個距離的排序,表明該家店距離我們當前的位置,這種基于地理位置的服務,統(tǒng)一被稱為LBS(Location Based Service),而LBS的實現(xiàn)則是借助于GIS,WC(無線通信)等信息技術來實現(xiàn)。而今天我們所要討論的就是這個距離的實現(xiàn)。
GIS,Geographic information system,地理信息系統(tǒng)。
2. 計算方式
??由于地球是一個橢圓形,我們在計算的時候有點麻煩,所以我們更常用的方式是將地球作為一個球形來計算,而計算球面上任意兩點之間的距離的公式通常有兩種:Great-circle distance和Haversine formula,而目前大多數(shù)公司都是用的是Haversine公式,原因可以參考:
Great-circle distance公式用到了大量余弦函數(shù), 而兩點間距離很短時(比如地球表面上相距幾百米的兩點),余弦函數(shù)會得出0.999…的結果, 會導致較大的舍入誤差。而Haversine公式采用了正弦函數(shù),即使距離很小,也能保持足夠的有效數(shù)字。
而有關這兩者的介紹可以參考維基百科:Haversine formula 維基百科,Great-circle distance 維基百科。而最終該公式的形式為:

至于為什么是這種形式,其實目前網(wǎng)上有許多推導公式,感興趣的可以看一下推導過程,順便回憶一下自己當年學過的數(shù)學知識:
1. 關于已知兩點經(jīng)緯度求球面最短距離的公式推導
2. 根據(jù)經(jīng)緯度計算兩點之間的距離的公式推導過程以及google.maps的測距函數(shù)
而如果要考慮到高度的影響的話,可以參考:https://stackoverflow.com/questions/3694380/calculating-distance-between-two-points-using-latitude-longitude-what-am-i-doi
??另外,還有一種方式是 Vincenty's formulae,該方式也是用于計算球體表面兩點之間距離的方式,而它所基于的就是地球是扁球體的形狀,因此這種方式比假設地球是球體的方式應該更加準確,但實現(xiàn)起來比較麻煩。感興趣的可以查看下維基百科:Vincenty's formulae 維基百科
3. Java實現(xiàn)
接下來,我們來看一下該公式的Java實現(xiàn):
public final class DistanceUtils {
/**
* 地球半徑,單位 km
*/
private static final double EARTH_RADIUS = 6378.137;
/**
* 根據(jù)經(jīng)緯度,計算兩點間的距離
*
* @param longitude1 第一個點的經(jīng)度
* @param latitude1 第一個點的緯度
* @param longitude2 第二個點的經(jīng)度
* @param latitude2 第二個點的緯度
* @return 返回距離 單位千米
*/
public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {
// 緯度
double lat1 = Math.toRadians(latitude1);
double lat2 = Math.toRadians(latitude2);
// 經(jīng)度
double lng1 = Math.toRadians(longitude1);
double lng2 = Math.toRadians(longitude2);
// 緯度之差
double a = lat1 - lat2;
// 經(jīng)度之差
double b = lng1 - lng2;
// 計算兩點距離的公式
double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) +
Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(b / 2), 2)));
// 弧長乘地球半徑, 返回單位: 千米
s = s * EARTH_RADIUS;
return s;
}
public static void main(String[] args) {
double d = getDistance(116.308479, 39.983171, 116.353454, 39.996059);
System.out.println(d);
}
}
由于平時我們用到數(shù)學函數(shù)的地方不多,所以這里我們來簡單介紹下用到的幾個函數(shù):
Math.pow(x,y) //這個函數(shù)是求x的y次方
Math.toRadians //將一個角度測量的角度轉換成以弧度表示的近似角度
Math.sin //正弦函數(shù)
Math.cos //余弦函數(shù)
Math.sqrt //求平方根函數(shù)
Math.asin //反正弦函數(shù)
由于三角函數(shù)中特定的關聯(lián)關系,Haversine公式的最終實現(xiàn)方式可以有多種,比如借助轉角度的函數(shù)atan2:
public static double getDistance2(double longitude1, double latitude1,
double longitude2, double latitude2) {
double latDistance = Math.toRadians(longitude1 - longitude2);
double lngDistance = Math.toRadians(latitude1 - latitude2);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Math.cos(Math.toRadians(longitude1)) * Math.cos(Math.toRadians(longitude2))
* Math.sin(lngDistance / 2) * Math.sin(lngDistance / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return c * EARTH_RADIUS;
}
返回的單位是千米,如果想返回米,可以修改地球半徑的單位從千米到米,并且由于該結果是double類型的,所以還可以借助Math.round方法進行四舍五入為long類型,然后精確到米:
// ......
// 弧長乘地球半徑(6378137)
s = s * EARTH_RADIUS;
// 返回類型: long,單位: 米
return Math.round(s * 10000) / 10000;
接下來說幾點概念:
3.1 地球半徑
??由于地球不是一個完美的球體,所以并不能用一個特別準確的值來表示地球的實際半徑,不過由于地球的形狀很接近球體,用[6357km] 到 [6378km]的范圍值可以涵蓋需要的所有半徑。并且通常情況下,地球半徑有幾個常用值:
- 極半徑,從地球中心至南極或北極的距離, 相當于6356.7523km;
- 赤道半徑,從地球中心到赤道的距離,大約6378.137km;
- 平均半徑,6371.393km,表示地球中心到地球表面所有各點距離的平均值;
- RE,地球半徑,有時被使用作為距離單位, 特別是在天文學和地質學中常用,大概距離是6370.856km;
所以我們通過地球半徑進行計算的時候,通常情況下,我們可以使用上面的每一個值都可以進行計算,不過或多或少都會有誤差的,但這樣的誤差是也是允許存在的。這里參考自維基百科:維基百科-地球半徑
4. MySQL實現(xiàn)
同樣,在MySQL中實現(xiàn)該功能,計算公式還是通過Haversine公式。不過在Google Map中,已經(jīng)提供了相應的實現(xiàn)方式,我們先來看一下。
4.1 Google Map實現(xiàn)
首先,我們需要先創(chuàng)建表結構:
CREATE TABLE `markers` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`name` VARCHAR( 60 ) NOT NULL ,
`address` VARCHAR( 80 ) NOT NULL ,
`lat` FLOAT( 10, 6 ) NOT NULL ,
`lng` FLOAT( 10, 6 ) NOT NULL
) ENGINE = MYISAM ;
當然存儲引擎可以是InnoDB。然后,進行初始化數(shù)據(jù):
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('1','Heir Apparel','Crowea Pl, Frenchs Forest NSW 2086','-33.737885','151.235260');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('2','BeeYourself Clothing','Thalia St, Hassall Grove NSW 2761','-33.729752','150.836090');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('3','Dress Code','Glenview Avenue, Revesby, NSW 2212','-33.949448','151.008591');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('4','The Legacy','Charlotte Ln, Chatswood NSW 2067','-33.796669','151.183609');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('5','Fashiontasia','Braidwood Dr, Prestons NSW 2170','-33.944489','150.854706');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('6','Trish & Tash','Lincoln St, Lane Cove West NSW 2066','-33.812222','151.143707');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('7','Perfect Fit','Darley Rd, Randwick NSW 2031','-33.903557','151.237732');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('8','Buena Ropa!','Brodie St, Rydalmere NSW 2116','-33.815521','151.026642');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('9','Coxcomb and Lily Boutique','Ferrers Rd, Horsley Park NSW 2175','-33.829525','150.873764');
INSERT INTO `markers` (`id`, `name`, `address`, `lat`, `lng`) VALUES ('10','Moda Couture','Northcote Rd, Glebe NSW 2037','-33.873882','151.177460');
然后就可以根據(jù)經(jīng)緯度值,然后基于Haversine公式來查詢數(shù)據(jù),假設我們要查詢latitude=37.38714,longitude=-122.083235,范圍在25英里內的前20條數(shù)據(jù):
SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance
FROM markers
HAVING distance < 25
ORDER BY distance
LIMIT 0 , 20;
而如果我們要查詢公里,將3959英里也就是地球半徑,修改為6371即可。
Google Maps地址:Creating a Store Locator on Google Maps php&MySQL
4.2 st_distance函數(shù)
??MySQL其實在很早就提供了這種存儲經(jīng)緯度及相關運算的功能,這種數(shù)據(jù)類型叫做空間數(shù)據(jù)類型,而對應的索引被稱為空間索引,但由于MySQL之前的版本對InnoDB支持的并不是太好,所以使用的并不多。不過MySQL5.6和MySQL5.7對此進行了優(yōu)化,添加了st_distance等相關函數(shù)來支持經(jīng)緯度相關的計算。
??這里只來看一下st_distance函數(shù)的使用,其他相關的函數(shù)我會專門寫一篇文章來學習。我們還是拿上面Google Maps所建的表來測試,來按照距離進行查詢:
SELECT
s.*,
(st_distance(point(lng, lat), point(-122.083235, 37.38714) ) * 111195) AS distance
FROM
markers s
ORDER BY
distance
其中,point是MySQL的空間數(shù)據(jù)類型,先不多說這塊。就這樣,我們只需要通過st_distance函數(shù)就計算出了我們所需要查詢的結果,不過這里需要說一下:
- st_distance 函數(shù)返回的單位是degrees,也就是空間單位的度數(shù),我們如果要將degrees轉換為米或者千米的話,需要乘以
EARTH_RADIUS * PI/180, EARTH_RADIUS 也就是地球半徑,至于是米還是千米,就看該變量的單位。- 該運算其實就相當于對地球半徑進行弧度與角度的轉換,也就是
Math.toRadians,而上面我們寫的111195其實是一個有誤差的值,該值就是通過該計算得出的結果;我們可以簡單看一下toRadians實現(xiàn):
public static double toRadians(double angdeg) {
return angdeg / 180.0 * PI;
}
這里的轉換參考自:Stackoverflow - Get Distance in Meters instead of degrees in Spatialite
其實,MySQL有提供直接查詢結果是米的函數(shù):
st_distance_sphere,并且該函數(shù)的計算結果要比st_distance轉換為米的結果更精確。不過該函數(shù)是MySQL5.7之后才引入的,5.7之前還是需要通過計算轉換成米。更多可參考官方文檔地址:
MySQL 5.7 ST_Distance_Sphere(g1, g2 [, radius])
5 Geohash算法
??Geohash是目前比較主流的范圍搜索的算法,比如說搜索附近500米內的地點這種問題。Geohash算法將二維的經(jīng)緯度編碼為一個字符串,每個字符串代表了某一矩形區(qū)域,也就是說,這個矩形區(qū)域內所有的點(經(jīng)緯度坐標)都共享相同的GeoHash字符串,這樣在查詢的時候就可以對該字符串做索引,然后根據(jù)該字符串進行過濾。
Geohash算法的最大用途其實就是附近地址搜索了,不過,從geohash的編碼算法中可以看出它的一個缺點,也就是邊界問題:雖然兩個地點距離很近,但恰好位于分界點的兩側,這樣geohash字符串就會不相同,然后匹配的時候就會有問題。不過要解決這個問題也很簡單,就是計算的時候,計算出8個分別分布在周圍8個區(qū)域的地點。
??在實際應用中,可以先根據(jù)Geohash篩選出附近的地點,然后再算出距離附近地點的距離。而如果要計算Geohash,可以通過 spatial4j 工具包來實現(xiàn),GeohashUtils.encodeLatLon(lat, lon),默認精度是12位,其中l(wèi)ucene就使用了spatial4j工具包來計算距離。
<dependency>
<groupId>org.locationtech.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.7</version>
</dependency>
有管GeoHash算法,可參考:
1. Geohash - 維基百科
2. GeoHash介紹-核心原理解析
3. Github-Java實現(xiàn)Geohash算法- github.com/GongDexing/Geohash
6. 其他
其實實現(xiàn)距離的方式有好多種,比如說:
- mysql sql查詢
- mysql+geohash
- mysql 空間索引 (MySQL5.7版本以上)
- PostgreSQL/mongodb + geohash
- redis+geohash
- Lucene/Solr/ES + Spatial/geohash
??并且,這種基于搜索排序的功能其實正是Lucene這種搜索引擎和非關系型數(shù)據(jù)庫所擅長的。而對MySQL而言,一直以來MySQL在GIS上的功能支持都比較弱,并且僅有MyISAM引擎支持,不過MySQL5.7之后發(fā)生了改變,提供了InnoDB引擎的GIS支持。所以,針對MySQL的這塊功能,等接下來專門來學習一下。
本文參考自:
1. 幾個地理位置信息處理方案的對比和分析
2. 空間索引 - 各數(shù)據(jù)庫空間索引使用報告
3. 美團技術團隊-地理空間距離計算優(yōu)化