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 = Math.toRadians(latitude1);
        double lat2 = Math.toRadians(latitude2);
        // 经度
        double lng1 = Math.toRadians(longitude1);
        double lng2 = Math.toRadians(longitude2);
        // 纬度之差
        double a = lat1 - lat2;
        // 经度之差
        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);
    }
}

用到的几个函数:

Math.pow(x,y)      //这个函数是求x的y次方
Math.toRadians     //将一个角度测量的角度转换成以弧度表示的近似角度
Math.sin           //正弦函数
Math.cos           //余弦函数
Math.sqrt          //求平方根函数
Math.asin          //反正弦函数

由于三角函数中特定的关联关系,Haversine公式的最终实现方式可以有多种,比如借助转角度的函数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;

所以我们通过地球半径进行计算的时候,通常情况下,我们可以使用上面的每一个值都可以进行计算,不过或多或少都会有误差的,但这样的误差是也是允许存在的。这里参考自维基百科:维基百科-地球半径

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.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');

然后就可以根据经纬度值,然后基于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 distance 
FROM markers 
HAVING distance < 25 
ORDER BY distance 
LIMIT 0 , 20;

而如果我们要查询公里,将3959英里也就是地球半径,修改为6371即可。

Geohash算法
Geohash是目前比较主流的范围搜索的算法,比如说搜索附近500米内的地点这种问题。Geohash算法将二维的经纬度编码为一个字符串,每个字符串代表了某一矩形区域,也就是说,这个矩形区域内所有的点(经纬度坐标)都共享相同的GeoHash字符串,这样在查询的时候就可以对该字符串做索引,然后根据该字符串进行过滤。

Geohash算法的最大用途其实就是附近地址搜索了,不过,从geohash的编码算法中可以看出它的一个缺点,也就是边界问题:虽然两个地点距离很近,但恰好位于分界点的两侧,这样geohash字符串就会不相同,然后匹配的时候就会有问题。不过要解决这个问题也很简单,就是计算的时候,计算出8个分别分布在周围8个区域的地点。

在实际应用中,可以先根据Geohash筛选出附近的地点,然后再算出距离附近地点的距离。而如果要计算Geohash,可以通过 spatial4j 工具包来实现,GeohashUtils.encodeLatLon(lat, lon),默认精度是12位,其中lucene就使用了spatial4j工具包来计算距离。

<dependency>
    <groupId>org.locationtech.spatial4j</groupId>
    <artifactId>spatial4j</artifactId>
    <version>0.7</version>
</dependency>

有管GeoHash算法,可参考:

GeoHash介绍-核心原理解析
Github-Java实现Geohash算法- github.com/GongDexing/Geohash

  • 10
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值