获取行政边界经纬度数据

来源:互联网 发布:网络免费学英语哪家好 编辑:程序博客网 时间:2024/04/28 12:57

在工作中需要用到经纬度与地理位置信息的转换,目前使用的是第三方数据服务接口(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<String, Object> regionMap = new HashMap<String, Object>();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,暂不处理边界轨迹,后期手动处理,边界数暂为1LOGGER.debug("小于4条,处理");final Map<String, Object> upd = new HashMap<String, Object>();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文件及后续单独处理的方式。下面来细数高德数据遇到的坑:

  1. 在我国有些行政区名和等级是相同的,大多发生在district等级上,因此一次请求会返回多个行政区的数据。代码片段中第17行的判断就是针对这种情况,通过传入的pId和地名可以在后期方便的定位到数据;更好的做法是传入父行政区的adcode,将两个行政区adcode进行对比即可知道哪一个才是要获取的数据。
  2. 有的行政区在高德上会出现查询不到数据的情况,polyline对应的值就是空的,我们需要记录下来这个行政区,以便后期完善数据。
  3. 绝大多数情况高德返回的边界经纬度是一个经纬度值的字符串,但是也碰到过返回一个数组的情况,而且数组都是空的。JSONObject会把value的值转成字符串,因此在49行只要判断是否存在中括号就可以了(偷了个懒偷笑
  4. 一般情况下行政边界只有一个,但沿海地区及一些特殊的行政区是有多个行政区边界的。高德在经纬度数据中用“|”来分割(这个设计太坑了。。。)在处理经纬度数据时一定要检测是否存在多个行政区边界。有些地区无论是高德还是百度都是存在多个边界的,边界数低于4个则直接保存,否则还是在后期交给百度来处理,特别是沿海地区,一定要用百度的数据。
接下来的工作就是处理这些异常数据了。根据上面的异常情况,分别处理。这部分的代码比较简单,就不贴上来了,需要注意的是后期处理时在收集及批量处理异常地区不如在上面获取数据时直接处理更方便。这个也是在获取到大量数据之后考虑到的,重新获取所有数据时间消耗太大才采取了较一般的处理方式。。。
下面来说下百度行政区边界数据获取。百度的行政边界数据没有提供接口,是网友通过百度地图的相关类获取到的。根据我这边的需求修改了以下代码,对多边界数加上了分隔符以及获取的数据格式,并使用FileSaver.js保存数据文件。这里要注意的是高德返回的经纬度是gcj02坐标系,百度的经纬度数据是bd09坐标系,在存入库中需要对经纬度做一个转换。使用百度地图处理数据分为两步:
  1. 在筛选了需要通过百度来获取数据的行政区后,得到一个地名字符串。遍历这些地名,分别获取数据并将数据保存在各地名.txt的文本中。
  2. 获取到各地的数据文件后,转换坐标系,再将数据分别写入库中。
关于获取数据的文字记录到这里暂告一个段落,在下一篇将描述根据经纬度获取行政区信息。