获取经纬度
百度地图提供了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;
public class Geolocation {
static final String AK = "";
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")); }
public static Map<String, String> getCoordinate(String address) { Map<String, String> fieldMap = new HashMap<>(); if (address != null && !"".equals(address)) { 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; }
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.330431,23.138092
|
在广州的一定知道,天河区、越秀区、番禺区都有”天河城”,那这个经纬度是哪个区的呢?
'广州天河城'的经纬度为: 113.330431,23.138092 '广州天河天河城'的经纬度为:113.330431,23.138092 '广州越秀天河城'的经纬度为:113.27646,23.125892 '广州番禺天河城'的经纬度为:113.354771,23.00962
|
据上述结果可知不指定区域
的话,定位到的经纬度是天河区的天河城
,两种可能性:
- 用权重值来判断,毕竟天河区的天河城是最繁华的地段嘛!
- 关键字分词解析求交集,与elasticsearch搜索引擎同理
(倾向于此点)
示例代码二:IP获取
利用IP获取大致位置,调用API接口,返回请求参数中指定上网IP的大致位置信息(一般为城市级别),位置信息包括:经纬度、省、市等地址信息。
目前该服务同时支持IPv4和IPv6来获取位置信息。普通IP定位服务目前不支持海外场景
。
如果请求参数中未指定上网IP,则默认返回当前服务请求来源的IP所对应的大致位置信息。
注意:该服务所返回的经纬度信息只是一个大概的位置,一般为城市中心点。
传送门:接口参数文档
拿示例代码一修改下URL调用地址
和analyticalField方法
static final String URL = "http://api.map.baidu.com/location/ip?&coor=bd09ll&";
|
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.27143134,23.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次计算,得到纬度的二进制表示,如下表:
得到纬度的二进制为:
同理得到经度116.390705的二进制为:
将经纬度合并
经度占偶数位,纬度占奇数位,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码
。上例最终得到的值为
示例代码三
package cn.goitman.utils;
import java.util.ArrayList; import java.util.List;
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);
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); } }
private String base32Encode(final String str) { String unit = ""; StringBuilder sb = new StringBuilder(); for (int start = 0; start < str.length(); start = start + 5) { unit = str.substring(start, start + 5); sb.append(base32Lookup[convertToIndex(unit)]); } return sb.toString(); }
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; }
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()); }
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比直接用经纬度的高效很多,而且使用者可以发布地址编码,既能表明自己的位置,又不至于暴露自己的精确坐标,有助于隐私保护和如下特点:
- GeoHash用一个字符串表示经度和纬度两个坐标。在
数据库中可以实现在一列上应用索引
(某些情况下无法在两列上同时应用索引)
- GeoHash表示的并
不是一个点
,而是一个矩形区域
- 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;
public class DistanceUtil { 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) Math.sin Math.cos Math.sqrt Math.asin Math.round
|
源码地址:https://github.com/wangdaicong/spring-boot-project/tree/master/latitudeAndLongitude-demo