平时我们在使用美团,饿了么,飞猪,携程,去哪儿等APP的时候,都会显示商家或者景点与我们当前位置的距离,这种基于地理位置的服务,统一被称为LBS(Location Based Service),而LBS的实现则是借助于GIS等信息技术实现。GIS(Geographic information system)地理信息系统。
本文要实现的是基于位置信息计算距离。
由出发点详细地址信息与目的地详细地址信息,通过高德地理信息API获取经纬度信息,通过经纬度信息计算得出大概位置距离。
由于地球是一个椭圆形,我们在计算的时候有点麻烦,所以我们更常用的方式是将地球作为一个球形来计算,而计算球面上任意两点之间的距离的公式通常有两种:Great-circle distance和Haversine formula,而目前大多数公司都是用的是Haversine公式,原因可以参考。
Great-circle distance公式用到了大量余弦函数, 而两点间距离很短时(比如地球表面上相距几百米的两点),余弦函数会得出0.999…的结果, 会导致较大的舍入误差。而Haversine公式采用了正弦函数,即使距离很小,也能保持足够的有效数字。
package com.zrj.weimob.util;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.util.HashMap;
/**
* 高德地理位置信息
* 地理/逆地理编码 :https://lbs.amap.com/api/webservice/guide/api/georegeo
* 地理编码:将详细的结构化地址转换为高德经纬度坐标。且支持对地标性名胜景区、建筑物名称解析为高德经纬度坐标。
* 逆地理编码:将经纬度转换为详细结构化的地址,且返回附近周边的POI、AOI信息。
*
* @author zrj
* @since 2021/8/12
**/
public class GeoLocation {
// 高德秘钥
private static final String APP_CODE_GAODE = "ce7addca48aa3c5a3b6a22ea299c4df7c05ccb";
/**
* 地理编码
* 根据地址获取经纬度,调用量上限:300000
*
* @param address 详细地址,必填
* @param currentCity 市,可选
* @return java.lang.String
*/
public static String GetLocationByAddress(String address, String currentCity) {
System.out.println("地理编码:address=" + address + ",currentCity=" + currentCity);
try {
HashMap parameters = new HashMap(16);
parameters.put("address", address);
parameters.put("city", currentCity);
parameters.put("key", APP_CODE_GAODE); // APP_CODE_GAODE 高德秘钥
// 高德获取地理信息
String response = HttpUtil.get("http://restapi.amap.com/v3/geocode/geo", parameters);
JSONObject responseJson = JSONObject.parseObject(response);
if (responseJson == null) {
System.err.println("列表信息获取失败,关键字:" + address + "城市:" + currentCity);
return null;
}
String status = responseJson.getString("status");
if (!"1".equals(status)) {
System.err.println("列表信息获取失败,关键字:" + address + "城市:" + currentCity);
return null;
}
JSONArray jsonArray = responseJson.getJSONArray("geocodes");
if (jsonArray == null || jsonArray.size() == 0) {
return null;
}
JSONObject object = jsonArray.getJSONObject(0);
return object.getString("location");
} catch (Exception ex) {
System.err.println("调用接口失败!" + ex.getMessage());
return null;
}
}
/**
* 逆地理编码
* 根据经纬度获取地址,调用量上限:300000
*
* @param location 经纬度:经度在前,纬度在后,经纬度间以“,”分割
* @return java.lang.String
*/
public static String GetAddressByLocation(String location) {
System.out.println("逆地理编码:location=" + location);
try {
HashMap parameters = new HashMap(16);
parameters.put("location", location);
parameters.put("key", APP_CODE_GAODE); // APP_CODE_GAODE 高德秘钥
// 高德获取地理信息
String response = HttpUtil.get("http://restapi.amap.com/v3/geocode/regeo", parameters);
JSONObject responseJson = JSONObject.parseObject(response);
if (responseJson == null) {
System.err.println("列表信息获取失败,经纬度:" + location);
return null;
}
String status = responseJson.getString("status");
if (!"1".equals(status)) {
System.err.println("列表信息获取失败,经纬度:" + location);
return null;
}
JSONObject regeocode = responseJson.getJSONObject("regeocode");
return regeocode.getString("formatted_address");
} catch (Exception ex) {
System.err.println("调用接口失败!" + ex.getMessage());
return null;
}
}
}
package com.zrj.weimob.util;
/**
* 通过经纬度计算距离
*
* @author zrj
* @since 2021/8/12
**/
public class DistanceUtils {
public static void main(String[] args) {
//double d = getDistance(116.308479, 39.983171, 116.353454, 39.996059);
double d1 = getDistance(118.90481194799801, 32.08508419316851, 118.91665658300778, 32.091265263087344);
double d2 = getDistance2(118.90481194799801, 32.08508419316851, 118.91665658300778, 32.091265263087344);
System.out.println(d1);
System.out.println(d2);
}
/**
* 地球半径,单位 km
*/
private static final double EARTH_RADIUS = 6378.137;
/**
* 基于余弦定理求两经纬度距离
* Math.pow(x,y) //这个函数是求x的y次方
* Math.toRadians //将一个角度测量的角度转换成以弧度表示的近似角度
* Math.sin //正弦函数
* Math.cos //余弦函数
* Math.sqrt //求平方根函数
* Math.asin //反正弦函数
*/
public static double getDistanceStr(String lng1, String lat1, String lng2, String lat2) {
return getDistance(new Double(lng1), new Double(lat1), new Double(lng2), new Double(lat2));
}
/**
* 根据经纬度计算两点间的距离
* 基于余弦定理求两经纬度距离
*
* @param longitude1 第一个点的经度
* @param latitude1 第一个点的纬度
* @param longitude2 第二个点的经度
* @param latitude2 第二个点的纬度
* @return 返回距离 单位千米
*/
public static double getDistance(double longitude1, double latitude1, double longitude2, double latitude2) {
System.out.println("经纬度距离:longitude1=" + longitude1 + ",latitude1=" + latitude1 + ",longitude2=" + longitude2 + ",latitude2=" + 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;
}
/**
* 根据经纬度,计算两点间的距离
* 由于三角函数中特定的关联关系,Haversine公式的最终实现方式可以有多种,比如借助转角度的函数atan2:
*
* @param longitude1 第一个点的经度
* @param latitude1 第一个点的纬度
* @param longitude2 第二个点的经度
* @param latitude2 第二个点的纬度
* @return double
*/
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;
}
}
package com.zrj.weimob.util;
import cn.hutool.core.util.StrUtil;
/**
* 根据位置计算距离
*
* @author zrj
* @since 2021/8/12
**/
public class GeoDistance {
/**
* 根据位置计算距离
*/
public static void main(String[] args) {
double distance = getDistance("南京市,仙林中心", "南京市,新街口");
}
/**
* 根据位置计算距离
*
* @param origin 出发点,市区与详细地址中间用逗号拼接,例如:南京市,仙林中心
* @param destination 目的地,与出发点相同
* @return double
*/
public static double getDistance(String origin, String destination) {
if (StrUtil.isEmpty(origin) || StrUtil.isEmpty(destination)) {
System.out.println("出发点或者目的地为空!");
return 0;
}
String[] originList = origin.split(",");
String[] destinationList = destination.split(",");
// 获取经纬度
String originLocation = GeoLocation.GetLocationByAddress(originList[1], originList[0]);
String destinationLocation = GeoLocation.GetLocationByAddress(destinationList[1], destinationList[0]);
System.out.println("经纬度:originLocation=" + originLocation + ",destinationLocation=" + destinationLocation);
// 根据经纬度计算距离
double distance = DistanceUtils.getDistanceStr(originLocation.split(",")[0], originLocation.split(",")[1], destinationLocation.split(",")[0], destinationLocation.split(",")[1]);
System.out.println(String.format("两地距离: " + String.valueOf(distance).substring(0, 3) + "KM"));
return distance;
}
}