功能描述
项目需要判断某个地点是否在某个区域范围内,区域范围需要支持用户编辑,所以不能直接对地址进行解析。本文采用的技术方案是利用百度地图将地址进行编码成经纬度,然后判断经纬度是否在用户绘制的区域范围内即可。
前期准备
申请百度地图账号,获取对应的key,此流程用户自行百度,操作非常简便,就不对此进行详细说明了。
技术实现
地理编码
将地址信息进行地理编码,由实际地理位置转化为经纬度信息,百度地图每天有5000额度的免费转化,如果用户量大需要用户申请额度。
@Value("${map.baidu.key}")
private String key;
@Value("${map.baidu.coordinateUrl:https://api.map.baidu.com/geocoding/v3/?address=%s&output=json&ak=%s}")
private String coordinateUrl;
/**
* 输入地址信息, 返回该地址的经纬度信息
* 地址编码
*
* @param address 地址参数
* @return 经纬度参数
*/
public CoordinateDTO getCoordinate(String address) {
String requestUrl = String.format(coordinateUrl, address, key);
String response = restTemplate.getForObject(requestUrl, String.class);
if (response == null) {
log.error("百度地图地址解析错误,解析地址是:{}, 请求结果为空", address);
throw new BizException("百度地图地址解析错误,请求地址是" + address, GlobalErrorInfo.THIRD_ERROR);
}
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.getInteger("status") != 0) {
log.error("百度地图地址解析错误,解析地址是:{}, 请求结果是:{}", address, jsonObject.toJSONString());
throw new BizException("百度地图地址解析错误,请求地址是" + address, GlobalErrorInfo.THIRD_ERROR);
}
// 百度地图有精确度字段,但同一地址多次请求返回值一致,所以精确度导致的问题需要人工处理
JSONObject result = jsonObject.getJSONObject("result");
return result.getObject("location", CoordinateDTO.class);
}
坐标位置转化
如果地址信息是其他地图的经纬度,可以将其经纬度信息转化为百度地图的经纬度信息,然后再进行判断点是否在区块范围内。
/**
* 坐标装换, 将坐标进行转换
*
* @param coordinate 需要转换的坐标
* @param from 需要转换坐标的类型
* @param to 转换后的坐标类型
* @return 转换后的坐标值
*/
public CoordinateDTO geoConv(CoordinateDTO coordinate, BaiduMapEnums from, BaiduMapEnums to) {
String params = coordinate.getLng() + "," + coordinate.getLat();
String requestUrl = String.format(geoConv, params, from.getValue(), to.getValue(), key);
String response = restTemplate.getForObject(requestUrl, String.class);
if (response == null) {
log.error("百度地图地址转换错误, 请求经纬度为:{}, 请求结果为空", params);
throw new BizException("百度地图地址转换错误, 请求经纬度为" + params, GlobalErrorInfo.THIRD_ERROR);
}
JSONObject jsonObject = JSONObject.parseObject(response);
if (jsonObject.getInteger("status") != 0) {
log.error("百度地图地址转换错误,请求经纬度为:{}, 请求结果是:{}", params, jsonObject.toJSONString());
throw new BizException("百度地图地址转换错误,请求经纬度为" + params, GlobalErrorInfo.THIRD_ERROR);
}
JSONObject result = jsonObject.getJSONArray("result").getJSONObject(0);
CoordinateDTO dto = new CoordinateDTO();
dto.setLng(result.getString("x"));
dto.setLat(result.getString("y"));
return dto;
}
@Getter
public enum BaiduMapEnums {
GPS(1, "GPS标准坐标"),
SOGOU(2, "搜狗地图坐标"),
SPARK(3, "火星坐标(gcj02),即高德地图、腾讯地图和MapABC等地图使用的坐标"),
MERCATOR(4, "3中列举的地图坐标对应的墨卡托平面坐标"),
BAIDU(5, "百度地图采用的经纬度坐标"),
BAIDU_MERCATOR(6, "百度地图采用的墨卡托平面坐标"),
TUBA(7, "图吧地图坐标"),
M51(8, "51地图坐标");
private Integer value;
private String description;
BaiduMapEnums(Integer value, String description) {
this.value = value;
this.description = description;
}
}
判断点是否在多边形内
判断给出点是否在区域范围内,采用的算法是做点的垂直方向的延长线,看看点与多边形有多少个交点,如果是奇数个,则在区域范围内,如果是偶数个,则在区域范围外。
/**
* 返回一个点是否在一个多边形区域内
*
* @param mPoints 多边形坐标点列表
* @param point 待判断点
* @return true 多边形包含这个点,false 多边形未包含这个点。
*/
public static boolean isPolygonContainsPoint(List<LatLng> mPoints, LatLng point) {
int nCross = 0;
for (int i = 0; i < mPoints.size(); i++) {
LatLng p1 = mPoints.get(i);
LatLng p2 = mPoints.get((i + 1) % mPoints.size());
// 取多边形任意一个边,做点point的垂直延长线,求解与当前边的交点个数
// p1p2是平行线段,要么没有交点,要么有无限个交点
if (p1.getLongitude().equals(p2.getLongitude()))
continue;
// point 在p1p2 底部 --> 无交点
if (point.getLongitude() < Math.min(p1.getLongitude(), p2.getLongitude()))
continue;
// point 在p1p2 顶部 --> 无交点
if (point.getLongitude() >= Math.max(p1.getLongitude(), p2.getLongitude()))
continue;
// 求解 point点水平线与当前p1p2边的交点的 X 坐标
double x = (point.getLongitude() - p1.getLongitude()) * (p2.getLatitude() - p1.getLatitude()) / (p2.getLongitude() - p1.getLongitude()) + p1.getLatitude();
if (x > point.getLatitude()) // 当x=point.x时,说明point在p1p2线段上
nCross++; // 只统计单边交点
}
// 单边交点为偶数,点在多边形之外 ---
return (nCross % 2 == 1);
}
/**
* 返回一个点是否在一个多边形边界上
*
* @param mPoints 多边形坐标点列表
* @param point 待判断点
* @return true 点在多边形边上,false 点不在多边形边上。
*/
public static boolean isPointInPolygonBoundary(List<LatLng> mPoints, LatLng point) {
for (int i = 0; i < mPoints.size(); i++) {
LatLng p1 = mPoints.get(i);
LatLng p2 = mPoints.get((i + 1) % mPoints.size());
// 取多边形任意一个边,做点point的垂直延长线,求解与当前边的交点个数
// point 在p1p2 底部 --> 无交点
if (point.getLongitude() < Math.min(p1.getLongitude(), p2.getLongitude()))
continue;
// point 在p1p2 顶部 --> 无交点
if (point.getLongitude() > Math.max(p1.getLongitude(), p2.getLongitude()))
continue;
// p1p2是平行线段,要么没有交点,要么有无限个交点
if (p1.getLongitude().equals(p2.getLongitude())) {
double minX = Math.min(p1.getLatitude(), p2.getLatitude());
double maxX = Math.max(p1.getLatitude(), p2.getLatitude());
// point在水平线段p1p2上,直接return true
if ((point.getLongitude().equals(p1.getLongitude())) && (point.getLatitude() >= minX && point.getLatitude() <= maxX)) {
return true;
}
} else { // 求解交点
double x = (point.getLongitude() - p1.getLongitude()) * (p2.getLatitude() - p1.getLatitude()) / (p2.getLongitude() - p1.getLongitude()) + p1.getLatitude();
if (x == point.getLatitude()) // 当x=point.x时,说明point在p1p2线段上
return true;
}
}
return false;
}
@TableName(value = "sys_lat_lng")
@Data
public class LatLng implements Serializable {
/**
* 纬度
*/
@TableField(value = "latitude")
private Double latitude;
/**
* 精度
*/
@TableField(value = "longitude")
private Double longitude;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
参考博客:
https://lbsyun.baidu.com/faq/api?title=webapi/guide/webservice-geocoding
https://blog.csdn.net/shao941122/article/details/51504519