Category Archives: GIS

GIS

如何通过GPS坐标查询Adcode

最近业务需要通过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);