Java根据经纬度计算两点之间的距离
1. 前⾔
  在我们平时使⽤美团,饿了么等app进⾏订餐,或者使⽤猫眼进⾏订电影票的时候,都有⼀个距离的排序,表明该家店距离我们当前的位置,这种基于地理位置的服务,统⼀被称为LBS(Location Based Service),⽽LBS的实现则是借助于GIS,WC(⽆线通信)等信息技术来实现。⽽今天我们所要讨论的就是这个距离的实现。
GIS,Geographic information system,地理信息系统。
2. 计算⽅式
  由于地球是⼀个椭圆形,我们在计算的时候有点⿇烦,所以我们更常⽤的⽅式是将地球作为⼀个球形来计算,⽽计算球⾯上任意两点之间的距离的公式通常有两种:Great-circle distance和Haversine formula,⽽⽬前⼤多数公司都是⽤的是Haversine公式,原因可以参考:
Great-circle distance公式⽤到了⼤量余弦函数, ⽽两点间距离很短时(⽐如地球表⾯上相距⼏百⽶的两点),余弦函数会得出0.999…的结果, 会导致较⼤的舍⼊误差。⽽Haversine公式采⽤了正弦函数,即使距离很⼩,也能保持⾜够的有效数字。
⽽有关这两者的介绍可以参考:Haversine formula ,Great-circle distance 。⽽最终该公式的形式为:
⾄于为什么是这种形式,其实⽬前⽹上有许多推导公式,感兴趣的可以看⼀下推导过程,顺便回忆⼀下⾃⼰当年学过的数学知识:
1. 关于已知两点经纬度求球⾯最短距离的公式推导
2. 根据经纬度计算两点之间的距离的公式推导过程以及google.maps的测距函数
另外,还有⼀种⽅式是 Vincenty’s formulae,该⽅式也是⽤于计算球体表⾯两点之间距离的⽅式,⽽它所基于的就是地球是扁球体的形状,因此这种⽅式⽐假设地球是球体的⽅式应该更加准确,但实现起来⽐较⿇烦。感兴趣的可以查看下:Vincenty’s formulae
3. Java实现
接下来,我们来看⼀下该公式的Java实现:
public final class DistanceUtils {
/**
* 地球半径,单位 km
*/
private static final double EARTH_RADIUS = 6378.137;
/**
* 根据经纬度,计算两点间的距离
*
* @param longitude1 第⼀个点的经度
* @param latitude1  第⼀个点的纬度
* @param longitude2 第⼆个点的经度
* @param latitude2  第⼆个点的纬度
* @return 返回距离单位千⽶
*/
public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {
// 纬度
double lat1 = Radians(latitude1);
double lat2 = Radians(latitude2);
// 经度
double lng1 = Radians(longitude1);
double lng2 = Radians(longitude2);
// 纬度之差
double a = lat1 - lat2;
// 经度之差
double b = lng1 - lng2;
// 计算两点距离的公式
double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 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);
}
}
由于平时我们⽤到数学函数的地⽅不多,所以这⾥我们来简单介绍下⽤到的⼏个函数:
Math.pow(x,y)      //这个函数是求x的y次⽅
Math.sin          //正弦函数
Math.sqrt          //求平⽅根函数
Math.asin          //反正弦函数
由于三⾓函数中特定的关联关系,Haversine公式的最终实现⽅式可以有多种,⽐如借助转⾓度的函数atan2:
public static double getDistance2(double longitude1, double latitude1,
double longitude2, double latitude2) {
double latDistance = Radians(longitude1 - longitude2);
double lngDistance = Radians(latitude1 - latitude2);
double a = Math.sin(latDistance / 2) * Math.sin(latDistance / 2)
+ Radians(longitude1)) * Radians(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类型的,所以还可以借助und ⽅法进⾏四舍五⼊为long类型,然后精确到⽶:
// ......
// 弧长乘地球半径(6378137)
s =  s * EARTH_RADIUS;
// 返回类型: long,单位: ⽶
und(s * 10000) / 10000;
接下来说⼏点概念:
3.1 地球半径
  由于地球不是⼀个完美的球体,所以并不能⽤⼀个特别准确的值来表⽰地球的实际半径,不过由于地球的形状很接近球体,⽤
[6357km] 到 [6378km]的范围值可以涵盖需要的所有半径。并且通常情况下,地球半径有⼏个常⽤值:
1.极半径,从地球中⼼⾄南极或北极的距离, 相当于6356.7523km;
2.⾚道半径,从地球中⼼到⾚道的距离,⼤约6378.137km;
3.平均半径,6371.393km,表⽰地球中⼼到地球表⾯所有各点距离的平均值;
RE,地球半径,有时被使⽤作为距离单位, 特别是在天⽂学和地质学中常⽤,⼤概距离是6370.856km;
所以我们通过地球半径进⾏计算的时候,通常情况下,我们可以使⽤上⾯的每⼀个值都可以进⾏计算,不过或多或少都会有误差的,但这样的误差是也是允许存在的。这⾥参考⾃:-地球半径
4. MySQL实现
同样,在MySQL中实现该功能,计算公式还是通过Haversine公式。不过在Google Map中,已经提供了相应的实现⽅式,我们先来看⼀下。
4.1 Google Map实现
⾸先,我们需要先创建表结构:
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。然后,进⾏初始化数据:
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.83 6090');
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');
然后就可以根据经纬度值,然后基于Haversine公式来查询数据,假设我们要查询latitude=37.38714,longitude=-122.083235,范围在25英⾥内的前20条数据:
SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122)
) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS dista nce
FROM markers
HAVING distance < 25
ORDER BY distance
LIMIT 0 , 20;
⽽如果我们要查询公⾥,将3959英⾥也就是地球半径,修改为6371即可。
Google Maps地址:Creating a Store Locator on Google Maps php&MySQL
mysql下载app4.2 st_distance函数
  MySQL其实在很早就提供了这种存储经纬度及相关运算的功能,这种数据类型叫做空间数据类型,⽽对应的索引被称为空间索引,但由于MySQL之前的版本对InnoDB⽀持的并不是太好,所以使⽤的并不多。不过MySQL5.6和MySQL5.7对此进⾏了优化,添加了
st_distance等相关函数来⽀持经纬度相关的计算。
  这⾥只来看⼀下st_distance函数的使⽤,其他相关的函数我会专门写⼀篇⽂章来学习。我们还是拿上⾯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的空间数据类型,先不多说这块。就这样,我们只需要通过st_distance函数就计算出了我们所需要查询的结果,不过这⾥需要说⼀下:
st_distance 函数返回的单位是degrees,也就是空间单位的度数,我们如果要将degrees转换为⽶或者
千⽶的话,需要乘以
EARTH_RADIUS * PI/180, EARTH_RADIUS 也就是地球半径,⾄于是⽶还是千⽶,就看该变量的单位。
该运算其实就相当于对地球半径进⾏弧度与⾓度的转换,也就是Radians,⽽上⾯我们写的111195其实是⼀个有误差的值,该值就是通过该计算得出的结果;我们可以简单看⼀下toRadians实现:
public static double toRadians(double angdeg) {
return angdeg / 180.0 * PI;
}
这⾥的转换参考⾃:Stackoverflow - Get Distance in Meters instead of degrees in Spatialite
其实,MySQL有提供直接查询结果是⽶的函数:st_distance_sphere,并且该函数的计算结果要⽐st_distance转换为⽶的结果更精确。不过该函数是MySQL5.7之后才引⼊的,5.7之前还是需要通过计算转换成⽶。更多可参考官⽅⽂档地址:
MySQL 5.7 ST_Distance_Sphere(g1, g2 [, radius])
5 Geohash算法
  Geohash是⽬前⽐较主流的范围搜索的算法,⽐如说搜索附近500⽶内的地点这种问题。Geohash算法将⼆维的经纬度编码为⼀个字符串,每个字符串代表了某⼀矩形区域,也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样在查询的时候就可以对该字符串做索引,然后根据该字符串进⾏过滤。
Geohash算法的最⼤⽤途其实就是附近地址搜索了,不过,从geohash的编码算法中可以看出它的⼀个缺点,也就是边界问题:虽然两个地点距离很近,但恰好位于分界点的两侧,这样geohash字符串就会不相同,然后匹配的时候就会有问题。不过要解决这个问题也很简单,就是计算的时候,计算出8个分别分布在周围8个区域的地点。
在实际应⽤中,可以先根据Geohash筛选出附近的地点,然后再算出距离附近地点的距离。⽽如果要计算Geohash,可以通过 spatial4j
⼯具包来实现,deLatLon(lat, lon),默认精度是12位,其中lucene就使⽤了spatial4j⼯具包来计算距离。
<dependency>
<groupId>org.locationtech.spatial4j</groupId>
<artifactId>spatial4j</artifactId>
<version>0.7</version>
</dependency>
有管GeoHash算法,可参考:
1. Geohash -
2. GeoHash介绍-核⼼原理解析
3. Github-Java实现Geohash算法- github/GongDexing/Geohash
4. 其他
其实实现距离的⽅式有好多种,⽐如说:
mysql sql查询
mysql+geohash
mysql 空间索引 (MySQL5.7版本以上)
PostgreSQL/mongodb + geohash
redis+geohash
Lucene/Solr/ES + Spatial/geohash
  并且,这种基于搜索排序的功能其实正是Lucene这种搜索引擎和⾮关系型数据库所擅长的。⽽对MySQL⽽⾔,⼀直以来MySQL在GIS上的功能⽀持都⽐较弱,并且仅有MyISAM引擎⽀持,不过MySQL5.7之后发⽣了改变,提供了InnoDB引擎的GIS⽀持。所以,针对MySQL的这块功能,等接下来专门来学习⼀下。
本⽂参考⾃:
1. ⼏个地理位置信息处理⽅案的对⽐和分析
2. 空间索引 - 各数据库空间索引使⽤报告
3. 美团技术团队-地理空间距离计算优化

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。