最近业务需要通过GPS坐标拿到坐标所在城市Adcode的信息,这个功能之前系统有一版是通过百度的POI查询接口实现的。由于百度POI查询接口每天有限额,而业务也可以通过在上报坐标时带上Adcode来规避查询,所以当时让业务调整方案不查询Adcode。
这个需求并不难处理,行政区划数据很容易就能获取到,比如可以从下面的地址找到下载地址:
https://www.jianshu.com/p/cb3ccbae9c88
第一步 我们下载行政区划数据,当然也可能调用地图服务商提供的接口来查询行政区划的数据,比如高德的行政区划接口,它把数据分成四级(国、省、市、区)。
第二步 整理数据,把数据分为省、市、区,对于省和市关联上对应的区划边界,比如:
adcode name center level 边界
650100 乌鲁木齐市 87.6177,43.7928 市 xxx,xxx;xxx,xxx
650000 新疆维吾尔自治区 87.6177,43.7928 省 xxx,xxx;xxxx,xxxx
第三步 考虑如何实现查询,主要考虑了三个方案PostGis,MySQL,JTS库自实现;
PostGis是空间数据库,它是通过向PostgreSQL添加空间数据类型、空间索引和空间函数的支持,将PostgreSQL转化空间数据库。它几乎可以称的上是GIS应用数据库的标准实现了。PostGis空间数据库插入空间数据时要建立空间索引,性能会受到索引插入的影响,适合不经常变动的数据,比如地图、热点POI数据;如果是人车的实时位置就不合适了。
MySQL也提供了空间函数和类型的支持;按下面的语句建立数据表,插入区划数据,就可以很方便的使用了,试着查了下,性能还不错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | CREATE TABLE `t_district` ( `id` BIGINT(21) NOT NULL AUTO_INCREMENT, `adcode` VARCHAR(6) COLLATE utf8_bin NOT NULL, `name` VARCHAR(32) COLLATE utf8_bin NOT NULL, `center` VARCHAR(64) COLLATE utf8_bin NOT NULL, `level` tinyint(2) NOT NULL COMMENT '级别,国家、省/直辖市、市、区/县4个级别) 可选值:0、1、2、3', `polyline` geometry NOT NULL COMMENT '边界', `parent` BIGINT(21) NOT NULL, `create_time` BIGINT(21) NOT NULL DEFAULT '0' COMMENT '记录创建时间', `update_time` BIGINT(21) NOT NULL DEFAULT '0' COMMENT '修改时间', PRIMARY KEY (`id`), KEY `idx_adcode` (`adcode`), KEY `idx_level` (`level`), SPATIAL KEY `idx_polyline` (`polyline`) ) ENGINE=InnoDB AUTO_INCREMENT=371 DEFAULT CHARSET=utf8 COLLATE=utf8_bin; -- 插入数据 INSERT INTO `t_district`(`adcode`,`name`,`center`,`level`,`polyline`) VALUES ('100000','其它','116.3683244,39.915085','2',ST_GEOMFROMTEXT('POLYGON((xx xx,xx xx),(xx xx)))) ... -- 查询 SELECT id, adcode, name, center, level, parent, create_time, update_time FROM t_district WHERE ST_CONTAINS(polyline, ST_GEOMFROMTEXT('POINT(116.485684 39.921122)')) |
JTS库实现,只需要在项目中引入JTS依赖库,性能在MAC本上最差情况下10000的qps轻轻松松,方案简单也不用运维PostGIS和MySQL。我们详细讨论一下如何使用JTS库实现行政区划查询
第三步 确定方案,使用JTS库自已实现,项目引入JTS库
1 2 3 4 5 | <dependency> <groupId>com.vividsolutions</groupId> <artifactId>jts</artifactId> <version>1.13</version> </dependency> |
对每个省、城市生成区划多边形数据,每个省市的边界数据放到txt文件,原始数据格式为:lon1,lat1;lon2,lat2|lon3,lat3….
例如:
1 | 116.556408,34.012041;116.556408,34.012041;116.556408,34.012041|116.556408,34.012041 |
数据文件命名规则如下,部署时,数据文件和程序代码是分开存储的
1 2 3 4 5 6 | 130300_city.txt 130400_city.txt 130500_city.txt 130600_city.txt 130700_city.txt 140000_province.txt |
读文件,构造Adcode与边界数据的映射Map
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /** * 遍历边界文件,维护adcode与polyline的映射 */ private static class Visitor extends SimpleFileVisitor<Path> { private Map<String, String> map; public Visitor(Map<String, String> map) { this.map = map; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String fileName = file.getFileName().toString(); String adcode = fileName.substring(0, fileName.indexOf('_')); List<String> strings = java.nio.file.Files.readAllLines(file); this.map.put(adcode, strings.get(0)); return super.visitFile(file, attrs); } } // 定义常量 private static volatile ImmutableMap<String, DistrictPolyline> CITY_POLYLINE_MAP; private static GeometryFactory FACTORY = new GeometryFactory(); // 读取文件 Path path = Paths.get(DATA_PATH + "/polyline"); Map<String, String> polylinesInStr = Maps.newLinkedHashMap(); java.nio.file.Files.walkFileTree(path, new Visitor(polylinesInStr)); |
构建CITY_POLYLINE_MAP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Map<String, DistrictPolyline> map = Maps.newLinkedHashMap(); CITY_ADCODE.forEach(adcode -> { String line = polylinesInStr.get(adcode); String[] polylineStrs = line.split("\\|"); Polygon[] polygons = new Polygon[polylineStrs.length]; for (int j = 0; j < polylineStrs.length; j++) { String s = polylineStrs[j]; String[] points = s.split(";"); Coordinate[] cs = new Coordinate[points.length]; for (int i = 0; i < points.length; i++) { String p = points[i]; String[] lonLat = p.split(","); cs[i] = new Coordinate(Double.parseDouble(lonLat[0]), Double.parseDouble(lonLat[1])); } LinearRing ring = FACTORY.createLinearRing(cs); Polygon polygon = FACTORY.createPolygon(ring); polygons[j] = polygon; } map.put(adcode, new DistrictPolyline(adcode, polygons)); }); polylinesInStr.clear(); CITY_POLYLINE_MAP = ImmutableMap.copyOf(map); |
根据GPS遍历多边形MAP,获取adcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public static Optional<String> getAdCode(double lon, double lat) { Coordinate coordinate = new Coordinate(lon, lat); Iterator<Map.Entry<String, DistrictPolyline>> iterator = CITY_POLYLINE_MAP.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, DistrictPolyline> next = iterator.next(); DistrictPolyline value = next.getValue(); Polygon[] polygons = value.polygons; for (Polygon polygon : polygons) { // 调用JTS库,检查坐标点是否在polygon中 boolean contains = SimplePointInAreaLocator.containsPointInPolygon(coordinate, polygon); if (contains) { // 访问计数,用于检索速度 value.count++; return Optional.of(next.getKey()); } } } return Optional.empty(); } |
优化 每次访问后访问计数+1,定时根据访问计数重置区划在map中的位置,让最多访问的行政区划最先遍历到
1 2 3 4 5 6 7 8 9 | List<DistrictPolyline> polylines = Lists.newLinkedList(CITY_POLYLINE_MAP.values()); Collections.sort(polylines, (o1, o2) -> Long.compare(o2.count, o1.count)); Map<String, DistrictPolyline> map = Maps.newLinkedHashMap(); int size = polylines.size(); for (DistrictPolyline p : polylines) { p.count = size--; map.put(p.adcode, p); } CITY_POLYLINE_MAP = ImmutableMap.copyOf(map); |