获取经纬度

百度地图提供了web开发、Android开发、IOS开发和服务端接口方面的对接方案,在此讲述服务端接口中的Web服务API。

Web服务API为开发者提供http/https接口,即开发者通过http/https形式发起检索请求,获取返回json或xml格式的检索数据。可以基于JavaScript、C#、C++、Java等语言的地图应用开发。

获取密钥

传送门:百度地图开放平台

  • 登录百度地图开放平台在左侧导航栏中,点击”获取密钥”

  • 登录并获得激活邮件后,点击邮件中的跳转链接,来到如下界面,点击"申请密钥"

  • 来到”创建应用”界面,如下所述做配置后,按”提交”


点击复制密钥,供程序代码使用

注:百度地图api个人认证AK(免费),日配额限制6000个。使用达到上限需要第二天再用,或者更换AK(即密钥)。

示例代码一:地址获取

用户可通过将结构化地址(省/市/区/街道/门牌号)解析为对应的位置坐标。地址结构越完整,地址内容越准确,解析的坐标精度越高。如:北京市海淀区上地十街十号。

传送门:接口参数文档错误码对照表

  • 调用链接,新用户使用3.0版本接口
package cn.goitman.utils;

import com.alibaba.fastjson.JSON;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

/**
* @author Nicky
* @version 1.0
* @className Geolocation
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 地理位置类
* @date 2021/10/28 10:15
*/
public class Geolocation {
/**
* 百度地图密钥
*/
static final String AK = "";
/**
* 百度地图API
*/
static final String URL = "https://api.map.baidu.com/geocoding/v3/?output=json&";

static Map<String, Object> map = new HashMap<String, Object>();
static Map<String, String> hashMap = new HashMap<>();

public static void main(String[] args) {
String address = "广州天河城";
Map<String, String> msgMap = getCoordinate(address);
System.out.println("msgMap 数据:" + msgMap);
System.out.println("'" + address + "'的经纬度为:" + msgMap.get("lng") + "," + msgMap.get("lat"));
}

/**
* 根据地址,获取相关信息
* json数据格式
* {"status":0,"result":{"location":{"lng":113.009109696642,"lat":28.192963234242119},"precise":0,"confidence":50,"comprehension":0,"level":"NoClass"}}
*
* status:成功返回0
* lng:经度
* lat:纬度
* precise:1为精确查找、0为模糊打点
* confidence:误差范围
* comprehension:地址精确程度,分值范围0-100,分值越大,服务对地址精确程度越高
* level:地址类型
*/
public static Map<String, String> getCoordinate(String address) {
Map<String, String> fieldMap = new HashMap<>();
if (address != null && !"".equals(address)) {
// \s*为空字符串,如' '
address = address.replaceAll("\\s*", "").replace("#", "栋");
String param = "address=" + address + "&ak=" + AK;
String json = loadJSON(URL + param);
if (json != null && !"".equals(json)) {
map = JSON.parseObject(json, Map.class);
fieldMap = analyticalField(map);
}
}
return fieldMap;
}

/**
* json数据转化成Map
*/
private static Map<String, String> analyticalField(Map<String, Object> map) {
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
for (Map.Entry<String, Object> entry : entrySet) {
if ("result".equals(entry.getKey()) || "location".equals(entry.getKey())) {
analyticalField(JSON.parseObject(entry.getValue().toString(), Map.class));
} else {
if ("lng".equals(entry.getKey()) || "lat".equals(entry.getKey())) {
DecimalFormat df = new DecimalFormat("#.######");
hashMap.put(entry.getKey(), df.format(entry.getValue()));
} else {
hashMap.put(entry.getKey(), entry.getValue().toString());
}
}
}
return hashMap;
}

/**
* 请求百度地图链接,获取相关信息
*/
public static String loadJSON(String urlStr) {
StringBuilder builder = new StringBuilder();
BufferedReader in = null;
String inputLine = null;
try {
URL url = new URL(urlStr);
URLConnection urlConnection = url.openConnection();
in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), StandardCharsets.UTF_8));
while ((inputLine = in.readLine()) != null) {
builder.append(inputLine);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return builder.toString();
}
}

运行结果:

msgMap 数据:{lng=113.330431, level=商圈, confidence=50, precise=0, comprehension=100, lat=23.138092, status=0}
'广州天河城'的经纬度为:113.33043123.138092

在广州的一定知道,天河区、越秀区、番禺区都有”天河城”,那这个经纬度是哪个区的呢?

'广州天河城'的经纬度为:   113.33043123.138092
'广州天河天河城'的经纬度为:113.33043123.138092
'广州越秀天河城'的经纬度为:113.2764623.125892
'广州番禺天河城'的经纬度为:113.35477123.00962

据上述结果可知不指定区域的话,定位到的经纬度是天河区的天河城,两种可能性:

  1. 用权重值来判断,毕竟天河区的天河城是最繁华的地段嘛!
  2. 关键字分词解析求交集,与elasticsearch搜索引擎同理(倾向于此点)

示例代码二:IP获取

利用IP获取大致位置,调用API接口,返回请求参数中指定上网IP的大致位置信息(一般为城市级别),位置信息包括:经纬度、省、市等地址信息。
目前该服务同时支持IPv4和IPv6来获取位置信息。普通IP定位服务目前不支持海外场景
如果请求参数中未指定上网IP,则默认返回当前服务请求来源的IP所对应的大致位置信息。
注意:该服务所返回的经纬度信息只是一个大概的位置,一般为城市中心点。

传送门:接口参数文档

拿示例代码一修改下URL调用地址analyticalField方法

/**
* 区域API
*/
static final String URL = "http://api.map.baidu.com/location/ip?&coor=bd09ll&";
/**
* json数据转化成Map
*/
private static Map<String, String> analyticalField(Map<String, Object> map) {
Set<Map.Entry<String, Object>> entrySet = map.entrySet();
for (Map.Entry<String, Object> entry : entrySet) {
if ("content".equals(entry.getKey()) || "address_detail".equals(entry.getKey())
|| "point".equals(entry.getKey())) {
analyticalField(JSON.parseObject(entry.getValue().toString(), Map.class));
} else {
hashMap.put(entry.getKey(), entry.getValue().toString());
}
}
return hashMap;
}

测试结果:

public static void main(String[] args) {
String addr = "203.168.30.174";
Map<String, String> msgMap = getCoordinate(addr);
System.out.println("msgMap 数据:" + msgMap);
System.out.println(String.format("IP地址区域为:%s;经纬度为:%s", StringUtils.isEmpty(msgMap.get("city"))?msgMap.get("address"):msgMap.get("city"),msgMap.get("x")+","+msgMap.get("y")));
}
msgMap 数据:{address=广东省, province=广东省, adcode=440000, city=, street=, district=, street_number=, x=113.27143134, city_code=, y=23.13533631, status=0}
IP地址区域为:广东省;经纬度为:113.2714313423.13533631

注意:返回结果中有两个address键值对,因用HashMap封装数据可知,后者键值覆盖了前者键值

后述

在实际项目开发应用中,可批量处理多个地址信息获取到对应的经纬度,从而保存到数据库中。如果项目需要批量获取经纬度,下面代码需修改

loadJSON()方法每次都会新建URL对象,并开启一个openConnection()到远程目标的连接,数据量大的话肯定会造成内存溢出。

解决方案:改用线程池,使用同一URL对象,每个线程预建一个openConnection();线程启动都使用openConnection()得到同一URLConnection对象。

Geohash算法

基本原理

Geohashes是一种将经纬度坐标编码成一个字符串的方式

经度范围是东经180到西经180,纬度范围是南纬90到北纬90,设定西经与南纬为负,所以地球上的经度范围就是[-180,180],纬度范围就是[-90,90]。

如果以本初子午线(0经线)、赤道为界,纬度范围(-90,0)用二进制0代表(0,90)用二进制1代表经度范围(-180,0)用二进制0代表(0,180)用二进制1代表,那么地球可以分成如下4个部分

继续将(-90,0)分成(-90,-45)、(-45,0)(0,90)分成(0,45)、(45,90)(-180,0)分成(-180,-90)、(-90,0)(0,180)分成(0,90)、(90,180)依次小块范围内递归对半划分

Geohash算法通过将经纬度编码,地理位置分区,划分的次数越多,区域越多,区域面积越小了,精确度越高。

延伸问题

如图,如红点位置,区域内还有一个黄点。相邻区域内的绿点明显离红点更近。但因为黄点的编码和红点一样,最终找到的将是黄点。
问题来了,编码相近的两个点,真实距离并不一定很近,这需要实际计算出两个点的距离。要解决这个问题,首先要查找出红点周边8个区域,再根据Geohash筛选出附近点的经纬度,相互计算得出哪个点离红点更近(示例代码三)即可。

算法步骤

将经纬度变成二进制

如(116.390705,39.923201)纬度的范围是(-90,90),其中间值为0。对于纬度39.923201,在区间(0,90)中,因此得到一个1;(0,90)区间的中间值为45度,纬度39.923201小于45,因此得到一个0,依次递归拆分20次计算,得到纬度的二进制表示,如下表:

得到纬度的二进制为:

10111 00011 00011 11001

同理得到经度116.390705的二进制为:

11010 01011 00010 00100

将经纬度合并

经度占偶数位,纬度占奇数位,0也是偶数位。

11100 11101 00100 01111 00000 01101 01011 00001

Base32进行编码

Geohashes把整个世界分为32个单元的格子(4行8列),每一个格子都用一个字母或者数字标识。

Base32编码用上述32个格子值(0-9、b-z(去掉a,i,l,o))进行编码。先将上一步合并后得到的二进制转换为十进制数据,然后对应生成Base32码。

5个二进制位转换成一个base32码。上例最终得到的值为

wx4g0ec1

示例代码三

package cn.goitman.utils;

import java.util.ArrayList;
import java.util.List;

/**
* @author Nicky
* @version 1.0
* @className GeoHashUtil
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description GeoHash编码类
* @date 2021/10/28 10:15
*/
public class GeoHashUtil {

// 最大经度
public final double Max_Lng = 180;
// 最小经度
public final double Min_Lng = -180;
// 最大纬度
public final double Max_Lat = 90;
// 最小纬度
public final double Min_Lat = -90;

// 经度或纬度二进制长度
private final int length = 20;

private final double lngUnit = (Max_Lng - Min_Lng) / (1 << 20);
private final double latUnit = (Max_Lat - Min_Lat) / (1 << 20);

// 用0-9、b-z(去掉a, i, l, o,分别代表十进制数10 ~ 31)这32个字母进行编码
private final String[] base32Lookup =
{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
"b", "c", "d", "e", "f", "g", "h",
"j", "k", "m", "n", "p", "q", "r",
"s", "t", "u", "v", "w", "x", "y", "z"};

/**
* 将经度或纬度转换为二进制编码,递归二分区间法
*/
private void convert(double min, double max, double value, List<Character> list) {
if (list.size() > (length - 1)) {
return;
}
double mid = (max + min) / 2;
if (value < mid) {
// 左区间
list.add('0');
convert(min, mid, value, list);
} else {
// 右区间
list.add('1');
convert(mid, max, value, list);
}
}

/**
* Base32编码长度为:经度和纬度二进制长度相加 lng + late = num,再除于5(5个二进制位转换成一个base32码),如(20 + 20) / 5 = 8
*/
private String base32Encode(final String str) {
String unit = "";
StringBuilder sb = new StringBuilder();
for (int start = 0; start < str.length(); start = start + 5) {
// 截取5个二进制位
unit = str.substring(start, start + 5);
// 根据十进制数,获取Base32编码表中对应值
sb.append(base32Lookup[convertToIndex(unit)]);
}
return sb.toString();
}

/**
* 将5个二进制位转化成十进制数
*/
private int convertToIndex(String str) {
int length = str.length();
int result = 0;
for (int index = 0; index < length; index++) {
result += str.charAt(index) == '0' ? 0 : 1 << (length - 1 - index);
}
return result;
}

/**
* 先合并经纬度的二进制编码(经度占偶数位,纬度占奇数位,0也是偶数位),后Base32编码
*/
public String encode(double lng, double lat) {
List<Character> lngList = new ArrayList<Character>();
List<Character> latList = new ArrayList<Character>();
convert(Min_Lng, Max_Lng, lng, lngList);
convert(Min_Lat, Max_Lat, lat, latList);
StringBuilder sb = new StringBuilder();
for (int index = 0; index < latList.size(); index++) {
sb.append(lngList.get(index)).append(latList.get(index));
}
return base32Encode(sb.toString());
}

/**
* 边界问题,根据经纬度计算出原点及周围8个区域的Geohash值
*/
public List<String> around(double lng, double lat) {
List<String> list = new ArrayList<String>();
list.add(encode(lng, lat + latUnit));
list.add(encode(lng, lat - latUnit));
list.add(encode(lng + lngUnit, lat));
list.add(encode(lng - lngUnit, lat));
// 原点
list.add(encode(lng, lat));
list.add(encode(lng + lngUnit, lat + latUnit));
list.add(encode(lng - lngUnit, lat + latUnit));
list.add(encode(lng + lngUnit, lat - latUnit));
list.add(encode(lng - lngUnit, lat - latUnit));
return list;
}

public static void main(String[] args) {
System.out.println(new GeoHashUtil().encode(116.3967, 44.9999));
System.out.println(new GeoHashUtil().around(116.3967, 44.9999));
}
}

运行结果:

wxfzbxvr
[y84b08j2, wxfzbxvq, wxfzbxvx, wxfzbxvp, wxfzbxvr, y84b08j8, y84b08j0, wxfzbxvw, wxfzbxvn]

可到 Geohash转换器 校验代码生成的geohash编码是否有错

精确范围

Geohash比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己的位置,又不至于暴露自己的精确坐标,有助于隐私保护和如下特点:

  1. GeoHash用一个字符串表示经度和纬度两个坐标。在数据库中可以实现在一列上应用索引(某些情况下无法在两列上同时应用索引)
  2. GeoHash表示的并不是一个点,而是一个矩形区域
  3. GeoHash编码的前缀可以表示更大的区域。如wx4g0ec1,它的前缀wx4g0e表示包含编码wx4g0ec1在内的更大范围。这个特性可以用于附近地点搜索

编码越长,表示的范围越小,位置也越精确。因此我们就可以通过比较GeoHash匹配的位数来判断两个点之间的大概距离。

看上图可知编码长度长度为8时,精度在19米左右,而当编码长度为12时,精度在0.0186米左右;

例:需要获取和(116.390705,39.923201)相距2km内的地址,只需要查找地址坐标对应的GeoHash编码前五位(如:wx4g0)即可,可根据数据情况进行选择

两点距离算法

现在还多APP都有一个距离排序功能,表明该家店距离当前的位置,这个距离是怎么计算出呢?这完全是一个数学问题

算法步骤

  • 将两点经纬度转换为三维直角坐标

假设地球球心为三维直角坐标系的原点球心与赤道上0经度点的连线为X轴球心与赤道上东经90度点的连线为Y轴球心北极点的连线为Z轴,则地面上点的直角坐标与其经纬度关系为:α为纬度,β为经度

X = cos α × cos β
Y = cos α × sin β
Z = sinα
  • 根据三维直角坐标求两点间的直线(弦长)距离

如果两点的直角坐标分别为(x1,y1,z1)和(x2,y2,z2),则它们之间的直线距离为:L为直线距离
勾股定理

  • 根据弦长求两点间的弧长距离(实际距离)

弧长与弦长的关系为:上式中角的单位为度,1度=π/180弧度,S为弧长, R为地球半径约6378.137KM

S = R × π × 2 × arcsin(0.5 × L) / 180

示例代码四

package cn.goitman.utils;

/**
* @author Nicky
* @version 1.0
* @className GeoHashUtil
* @blog goitman.cn | blog.csdn.net/minkeyto
* @description 两点距离类
* @date 2021/10/28 10:15
*/
public class DistanceUtil {
// 地球半径,单位:KM
private final static double Earth_Radius = 6378.137d;

public static double distance(double lat1, double lng1, double lat2, double lng2) {
// 两点经纬度转换为三维直角坐标
double x1 = Math.cos(lat1) * Math.cos(lng1);
double y1 = Math.cos(lat1) * Math.sin(lng1);
double z1 = Math.sin(lat1);

double x2 = Math.cos(lat2) * Math.cos(lng2);
double y2 = Math.cos(lat2) * Math.sin(lng2);
double z2 = Math.sin(lat2);

// 求两点间的弦长距离
double chordLength = Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2) + Math.pow((z1 - z2), 2));
// 由弦长求两点间的弧长距离
double arcLength = Earth_Radius * Math.PI * 2 * Math.asin(0.5 * chordLength) / 180;

return arcLength;
}

public static void main(String[] args) {
String str = null;
double distance = DistanceUtil.distance(44.9999, 116.3967, 45.0001, 116.3967);

str = String.format("两点距离为 %f KM", distance);
System.out.println(str);

str = String.format("两点距离为 %s M", Math.round(distance * 1000));
System.out.println(str);
}
}

运行结果:

两点距离为 0.022264 KM
两点距离为 22 M

用到的数学函数如下:

Math.pow(x,y)   // 求x的y次方
Math.sin // 正弦函数
Math.cos // 余弦函数
Math.sqrt // 求平方根函数
Math.asin // 反正弦函数
Math.round // 四舍五入

源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/latitudeAndLongitude-demo