在工作中需要用到经纬度与地理位置信息的转换,目前使用的是第三方数据服务接口(haoservice、百度等)。但常常因为一些问题导致线上出错,因此需要自己实现一个根据经纬度获取行政区信息的工具。本篇博客记录了在设计和实现过程中踩的坑及获得的帮助,并感谢所有提供过帮助的人。
工作准备:高德、百度数据接口(高德:http://restapi.amap.com/v3/config/district?subdistrict=1&extensions=all&level={0}&key=7de8697669288fc848e12a08f58d995e&output=json&keywords={1};百度:使用百度地图的类获取到数据,demo来自点击打开链接)。经纬度转换算法:来自网络。
两种数据接口的优点:高德数据接口返回json数据,方便后台批量获取;返回的数据精确度非常高、数据量大,绘制的边界地图非常详细,包含了下一级行政区简单信息;百度地图对沿海城市的边界范围更加大,在多岛屿地区可以通过一段边界数据概括起来,且每段行政区数据量较少,更加方便处理。缺点:高德的接口没有将沿海地区的海域范围包含进来,导致海岸线一带的边界数据圈极多,内陆地区也有许多地区行政区被切割为多个,数据处理非常困难(原因在后面会详说),数据量极大,对数据库读写造成了一些压力;百度地图对边界的处理相对高德比较粗略,只能通过js的方式获取数据,且未对数据进行包装,不方便大规模获取数据。对两种接口做了简单对比后,我决定以高德为主,百度为辅,开始了全国行政边界数据抓取(掉坑填坑)之路。
确认了基本需求后,开始搭建数据库。数据库中建了三张表:region_info,存储各行政区基本数据;region_gps,存储行政区边界经纬度,另一张表本篇不需要使用,暂且不提。表结构如下:
CREATE TABLE `region_info` (
`n_id` int(11) NOT NULL AUTO_INCREMENT,
`n_parent_id` int(11) DEFAULT '-1',
`c_name` varchar(255) DEFAULT NULL COMMENT '行政区名',
`n_level` int(2) DEFAULT NULL COMMENT '区域等级',
`c_region_code` varchar(255) DEFAULT NULL,
`c_center_gps` varchar(255) DEFAULT NULL COMMENT '中心点GPS',
`n_region_count` int(5) DEFAULT '1' COMMENT '行政区圈数',
`n_datas` int(2) DEFAULT '0' COMMENT '是否有边界数据写入',
`t_create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`n_id`),
KEY `CNAME` (`c_name`),
KEY `REGIONCODE` (`c_region_code`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
CREATE TABLE `region_gps` (
`n_id` int(11) NOT NULL AUTO_INCREMENT,
`n_gps_seq` int(11) DEFAULT NULL,
`n_lat` double DEFAULT NULL COMMENT 'gcj02',
`n_lng` double DEFAULT NULL COMMENT 'gcj02',
`c_center_gps` varchar(255) DEFAULT NULL COMMENT 'gcj02',
`c_name` varchar(255) DEFAULT NULL,
`n_region_seq` int(5) DEFAULT '0',
`t_create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`n_id`),
KEY `CNAME` (`c_name`),
KEY `CGPS_NAME` (`c_center_gps`,`c_name`),
KEY `LNG_LAT` (`n_lat`,`n_lng`),
FULLTEXT KEY `CENTER_GPS` (`c_center_gps`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
完成数据库的搭建,下面就开始获取数据了。针对高德数据接口获取数据的代码如下:
public static void getRegionInfo(String level, String name, FileWriter out, long pId) {
if (level.equals("biz_area") || level.equals("street"))
return;
String url = "http://restapi.amap.com/v3/config/district?subdistrict=1&extensions=all&level={0}&key=7de8697669288fc848e12a08f58d995e&output=json&keywords={1}";
url = StringUtils.format(url, level, name);
LOGGER.debug("url:{}", url);
try {
String result = HttpUtils.sendGet(url);
if (null != result) {
JSONObject json = JSONObject.parseObject(result);
if (null == json) {
LOGGER.error("无法解析json:" + name);
} else {
if (null != json.getJSONArray("districts") && json.getJSONArray("districts").size() > 0) {
// 需要用到的数据
JSONArray districtsJArr = json.getJSONArray("districts");
if (districtsJArr.size() > 1) {
LOGGER.error("行政区名多地重复, region_info pId=" + pId);
} else if (districtsJArr.size() == 1) {
String polylinesJson = districtsJArr.getJSONObject(0).getString("polyline");
JSONArray subdistrictsJArr = districtsJArr.getJSONObject(0).getJSONArray("districts");// 下一级的各个地区
String regionCode = districtsJArr.getJSONObject(0).getString("adcode");
String centerGps = districtsJArr.getJSONObject(0).getString("center");
name = districtsJArr.getJSONObject(0).getString("name");
level = districtsJArr.getJSONObject(0).getString("level");
final Map regionMap = new HashMap();
regionMap.put("c_region_code", regionCode);
regionMap.put("n_parent_id", pId);
regionMap.put("c_center_gps", centerGps);
regionMap.put("c_name", name);
regionMap.put("n_level", getLevel(level));
// TODO 将当前行政区信息写入数据库
long id = saveToInfo("region_info", regionMap);
DbHelper.execute(DataSourceBuilder.getInstance().getDataSourceCluster().getWriteableDataSource(),
new JdbcCallback() {
public Object doInJdbc(Connection connect) throws SQLException, Exception {
DbHelper.update(connect, "region_info", regionMap,
" c_name = '" + regionMap.get("c_name") + "' and n_level = " + regionMap.get("n_level"));
return null;
}
});
if (null == polylinesJson) {
LOGGER.error("polylines 为空");
} else {
String gpsss[] = polylinesJson.split("\\|");
if (polylinesJson.contains("[")) {
LOGGER.error("行政区有异常:" + name);
if (level.equals("district")) {
return;
} else {
for (Object obj : subdistrictsJArr) {
JSONObject subRegion = (JSONObject) obj;
name = subRegion.getString("name");
level = subRegion.getString("level");
getRegionInfo(level, name, out, id);
}
}
} else if (polylinesJson.contains("|")) {// 存在多个行政边界圈,小于3个可以存入数据库中
LOGGER.debug("行政区有分割:{}", name);
if (gpsss.length <= 3) {// 行政边界圈 > 3,暂不处理边界轨迹,后期手动处理,边界数暂为1
LOGGER.debug("小于4条,处理");
final Map upd = new HashMap();
upd.put("n_region_count", gpsss.length);
final String namee = name;
final int levell = getLevel(level);
DbHelper.execute(DataSourceBuilder.getInstance().getDataSourceCluster()
.getWriteableDataSource(), new JdbcCallback() {
public Object doInJdbc(Connection connect) throws SQLException, Exception {
DbHelper.update(connect, "region_info", upd, " c_name = '" + namee + "' and n_level = " + levell);
return null;
}
});
for (int i = 0; i < gpsss.length; i++) {
String[] gpss = gpsss[i].split(";");
// TODO 将行政区边界轨迹数据写入数据库
saveToGps("region_gps", gpss, centerGps, name, i, true);
}
}
if (level.equals("district")) {
return;
} else {
for (Object obj : subdistrictsJArr) {
JSONObject subRegion = (JSONObject) obj;
name = subRegion.getString("name");
level = subRegion.getString("level");
getRegionInfo(level, name, out, id);
}
}
} else {
String[] gpss = gpsss[gpsss.length - 1].split(";");
LOGGER.debug("{}的长度{}:", new Object[] {name, gpss.length });
// TODO 将行政区边界轨迹数据写入数据库
saveToGps("region_gps", gpss, centerGps, name, 0, true);
if (level.equals("district")) {
return;
} else {
for (Object obj : subdistrictsJArr) {
JSONObject subRegion = (JSONObject) obj;
name = subRegion.getString("name");
level = subRegion.getString("level");
getRegionInfo(level, name, out, id);
}
}
}
}
}
}
}
}
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
}
}
上面的代码片段能够递归地获取一个地区的边界及子行政区边界大多数的数据,但是对一些异常情况(坑)的处理还是存在问题的。这些坑也是在获取数据的时候遇到的,为了不影响整体的数据获取,因此采取了保存至log文件及后续单独处理的方式。下面来细数高德数据遇到的坑: