diff --git a/README.md b/README.md index 1327bf5..17c0e7c 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,27 @@ # ChinaGeoLib -修改自: https://github.com/deng0515001/lnglat2Geo, 非常感谢原作者的贡献,我完全照搬了原作者的算法实现,但是我不能理解这些算法的原理。 - -修改点如下: +修改自: https://github.com/deng0515001/lnglat2Geo, 非常感谢原作者的贡献.修改点如下: 1. 使用java完全重写Scala代码,原因如下 1. 作为lib,java和scala依赖是单项的,Scala程序员可以看懂java,但是java程序员不一定能看懂Scala 2. 作为lib库,需要考虑库的精简,他引入了Scala的runtime,将会带来额外的jar包依赖 3. GeoTrans的标准API并不是可以无缝提供给java端使用(部分API java无法调用,因为有很多scala的特性) 4. scala语法糖很舒服,但是进行代码cr能发现很多带有性能风险的用法,以及Scala函数式上代码结构不清晰(简写大括号、同函数if-else分支深度过多等) -2. 重新设计数据的序列化和反序列化 - 1. 提供json和bin两种格式数据接入,并提供两种格式的转换桥梁。开发状态可以使用json,生产使用bin - 2. 删除java的Serializable进行数据序列化的方法,工程实践上他从来不是稳定可靠的方式 +2. 数据文件格式变化 + 1. 删除java的Serializable进行数据序列化的方法,工程实践上他从来不是稳定可靠的方式 1. 当遇到字节码降级的时候,他会反序列化失败(如android 语法脱糖过程) 2. 可能由于jvm实现具体细节导致不稳定 3. 效率和性能并不是最优 4. lib库混淆时,他会反序列化失败 - 3. 使用Leb128编码:这在原作者的二进制数据格式上带来了5个百分点的优化(原作者:40.46%,现方案:35.67%) - 4. 二进制反序列化提速:数据初始化只需要6.5秒(原作者:45秒) - 1. 分别耗时在数据加载到内存,反序列化, 我们手写序列化和反序列化精确控制数据格式和组织方式(原始数据加载大约500毫秒即可完成,文件大小61M) - 2. 数据结构计算:我把计算结果缓存到本地文件系统。数据计算时间:25s降低到5s,缓存文件大小:178M - -## 离线数据编辑和修改 -``` - // 把数据dump到json中,根据自己的需要编辑这些文件 - new JSONLoader().dump(new File("src/main/resources/json/"), geoData); - - //编辑文件完成后,在程序运行前,使用jsonloader替换binloader,用于加载json格式的离线数据 - ILoader.Storage.setLoader(new JSONLoader()); - - // 代码测试没问题,使用BinLoader再把数据dump为二进制格式 - new BinLoader().dump(new File("src/main/resources/geolib.data.bin"), geoData); - - //线上,取消JSONLoader -``` - - -## 原作者readme - -经纬度转省市区县乡镇,速度快 -- 单线程50000次/秒;精度高 -- 准确率99.9999% - -还包含如下功能: - -1:查询某个经纬度附近所有商圈,按距离排序 - -2:给定城市,输出城市级别 - -3:输入任何地区的全称/简称/code,输出该地区的全部信息 - -4:获取所有行政区划关系数据等 - -使用方法: import com.dengxq.lnglat2Geo.GeoTrans 里面的所有方法均为公有接口 - -接口文档,参考博客: https://blog.csdn.net/deng0515001/article/details/99606156 - -jar包依赖:https://mvnrepository.com/artifact/com.github.deng0515001/lnglat2Geo - -有问题直接联系:QQ:451408963@qq.com - + 2. 文件模型即内存模型:线性hash表,线段数据加载到内存即为map对象,不需要进行一次全数据库解析构建,使得数据启动加载时间由40s减少到500ms + 3. 报文大小优化:目前数据包大小4.1M,原作者项目80M + 1. Leb128和zip + 2. 多边形精度模糊:作者使用level 12的s2进行多边形描述,但是其数据包中多边形线段数据的精度远高于level 12,导致大量点位是不需要的。本代码则会在构建数据包的时候模糊到level 12 + 3. 不再支持乡镇、街道、商圈等功能,原作者的数据方案是全数据遍历,故用户自己根据自己的业务来实现这些功能可能更加合理。所以进一步减少了数据大小 +3. 核心算法的变化:本质核心算法依然flow原作者,但是做了一些改动 + 1. 避免大量元组转换,这是Scala喜欢的,但是在阅读代码的时候真的是很难读懂这些元组的含义 + 2. s2点位缓存:原作者缓存点位数据没有解释其来源,本仓库则根据我的理解预先产生缓存点位,让缓存点位命中算法闭环 + 3. 基于模糊字符串相似度判定的文本地址归一化能力(地址转经纬度) +4. 其他 + 1. 依赖精简:除开必要的Google S2,不再依赖任意lib库 + 2. 代码量精简:仅存6个功能性java代码文件 + 3. 数据资源验证和转换:多边形数据打包检查多边形方向、模糊和调整s2 level \ No newline at end of file diff --git a/pom.xml b/pom.xml index b50d699..d4d2600 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ 1.6.2 - com.github.deng0515001 - lnglat2Geo - 1.0.5 + com.virjar + geolib + 1.0 jar - lnglat2Geo + geolib lnglat to Geo - https://github.com/deng0515001/lnglat2Geo + https://github.com/virjar/geoLibChina @@ -20,36 +20,12 @@ s2-geometry-library-java 1.0.0 - - - com.speedment.common - tuple - 3.2.10 - - - org.apache.commons - commons-lang3 - 3.9 - - - org.projectlombok lombok 1.18.22 provided - - - com.alibaba - fastjson - 1.2.79 - - - org.apache.commons - commons-io - 1.3.2 - diff --git a/src/main/java/com/dengxq/lnglat2Geo/GeoTrans.java b/src/main/java/com/dengxq/lnglat2Geo/GeoTrans.java deleted file mode 100644 index 83c538d..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/GeoTrans.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.dengxq.lnglat2Geo; - -import com.dengxq.lnglat2Geo.entity.*; -import com.dengxq.lnglat2Geo.entity.enums.CoordinateSystem; -import com.dengxq.lnglat2Geo.entity.enums.DistrictLevel; -import com.dengxq.lnglat2Geo.utils.GeoUtils; -import com.speedment.common.tuple.Tuple2; - -import java.util.List; -import java.util.Map; - -public class GeoTrans { - - - /** - * 判断经纬度的行政区划 - * - * @param lon 经度 - * @param lat 纬度 - * @param needStreet 是否需要街道信息 - * @param coordSys 输入经纬度的坐标系 - * @return 行政区划 - */ - public static Admin determineAdmin(Double lon, Double lat, CoordinateSystem coordSys, Boolean needStreet) { - if (needStreet == null) { - needStreet = true; - } - return GeoTransImpl.determineAdmin(lon, lat, needStreet, coordSys); - } - - /** - * 给出附近的所有商圈信息 - * - * @param lon 经度 - * @param lat 纬度 - * @param radius 需要商圈的半径 - * @param coordSys 输入经纬度的坐标系 CoordinateSystem - * @param needStreet 是否需要返回行政区划的街道信息 - * @return - */ - public static BusinessAreaInfo aroundBusinessAreas(Double lon, Double lat, Integer radius, CoordinateSystem coordSys, Boolean needStreet) { - if (radius == null) { - radius = 4000; - } - if (needStreet == null) { - needStreet = true; - } - Tuple2 gcj02LonLat = GeoUtils.toGCJ02(lon, lat, coordSys); - Admin admin = determineAdmin(gcj02LonLat.get0(), gcj02LonLat.get1(), CoordinateSystem.GCJ02, needStreet); - //return GeoTransImpl.determineAreaByAdmin(gcj02LonLat._1, gcj02LonLat._2, admin, radius); - return new BusinessAreaInfo(admin, GeoTransImpl.determineAreaByCityId(lon, lat, admin.cityCode, radius, CoordinateSystem.GCJ02)); - } - - /** - * 给出附近的所有商圈信息 - * - * @param lon 经度 - * @param lat 纬度 - * @param radius 需要商圈的半径 - * @param coordSys 输入经纬度的坐标系 CoordinateSystem - * @param cityID 输入城市adcode - * @return - */ - public static List aroundBusinessAreasByCityID( - Double lon, Double lat, Integer radius, CoordinateSystem coordSys, Integer cityID) { - if (radius == null) { - radius = 4000; - } - return GeoTransImpl.determineAreaByCityId(lon, lat, cityID, radius, coordSys); - } - - /** - * 获取城市级别 - * - * @param adcodeOrName 城市adcode或者城市名 - * @return 城市级别 - */ - public static String getCityLevel(String adcodeOrName) { - return GeoTransImpl.getCityLevel(adcodeOrName); - } - - /** - * 根据地区code返回规范数据 - * - * @param adcode 地区code - * @return - */ - public static AdminNode normalizeName(int adcode) { - return GeoTransImpl.normalizeName(adcode); - } - - /** - * 根据地区name返回规范化的地区信息 - * - * @return 规范化的地区信息 - */ - public static java.util.List normalizeName(String name, DistrictLevel level, Boolean isFullMatch) { - return GeoTransImpl.normalizeName(name, level, isFullMatch); - } - - /** - * 根据所有信息返回规范化的地区信息 - * - * @param province 省名 可为空 - * @param city 城市名 可为空 - * @param district 区县名 可为空 - * @param street 街道名 可为空 - * @param isFullMatch 所有输入区域是简称还是全名 - * @return 规范化的地区信息,可能有多个或不存在 - */ - public static java.util.List normalizeName(String province, String city, String district, - String street, Boolean isFullMatch) { - province = trimToEmpty(province); - city = trimToEmpty(city); - district = trimToEmpty(district); - street = trimToEmpty(street); - if (isFullMatch == null) { - isFullMatch = false; - } - return GeoTransImpl.normalizeName(province, city, district, street, isFullMatch); - } - - private static String trimToEmpty(String input) { - if (input == null) { - return ""; - } - return input; - } - - /** - * 获取所有行政区划数据 - * - * @return 所有行政区划数据,不包含街道 - */ - public static Map adminData() { - return GeoTransImpl.adminData; - } - - public static Map countryCode() { - return GeoTransImpl.countryCode; - } - -// public static districtBoundary():Map[Int,List[List[Long]]]= -// -// { -// AdminDataProvider.AdminLoader.loadBoundarySrc.map(s = > (s.code, s.boundary)) -// .toMap -// } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/GeoTransImpl.java b/src/main/java/com/dengxq/lnglat2Geo/GeoTransImpl.java deleted file mode 100644 index 0c78a59..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/GeoTransImpl.java +++ /dev/null @@ -1,428 +0,0 @@ -package com.dengxq.lnglat2Geo; - -import com.dengxq.lnglat2Geo.entity.*; -import com.dengxq.lnglat2Geo.entity.enums.CoordinateSystem; -import com.dengxq.lnglat2Geo.entity.enums.DistrictLevel; -import com.dengxq.lnglat2Geo.loader.ILoader; -import com.dengxq.lnglat2Geo.utils.GeoUtils; -import com.dengxq.lnglat2Geo.utils.LineUtils; -import com.dengxq.lnglat2Geo.utils.S2Utils; -import com.google.common.geometry.S2CellId; -import com.google.common.geometry.S2LatLng; -import com.speedment.common.tuple.Tuple2; -import com.speedment.common.tuple.Tuple3; -import com.speedment.common.tuple.Tuple4; -import com.speedment.common.tuple.Tuples; -import org.apache.commons.lang3.StringUtils; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class GeoTransImpl { - public static final int min_level = 12; - public static Map> cityBusinessArea = ILoader.Storage.getOrLoad().getAreaGroups() - .stream().collect(Collectors.toMap(s -> s.cityAdCode, s -> s.areas)); - - public static Map boundaryAdminCell = ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryAdminCell(); - - public static Map>> boundaryData = - ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryData(); - - public static Map> boundaryIndex = ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryIndex(); - public static Map cityLevelData = ILoader.Storage.getOrLoad().getCityLevel(); - - public static Map adminData = ILoader.Storage.getOrLoad().getAdminData() - .stream().collect( - Collectors.toMap(adminNode -> adminNode.id, adminNode -> adminNode) - ); - - public static Map streetData = ILoader.Storage.getOrLoad().getStreetData().stream().collect( - Collectors.toMap(adminNode -> adminNode.id, adminNode -> adminNode) - ); - - public static Map countryCode = ILoader.Storage.getOrLoad().getCountryCode(); - - - public static Map cityNameMap = new HashMap() {{ - put("重庆市", ""); - put("上海市", "上海城区"); - put("北京市", "北京城区"); - put("天津市", "天津城区"); - put("那曲市", "那曲地区"); - }}; - - public static Map districtNameMap = new HashMap() { - { - put("云州区", "大同县"); - put("平城区", "城区"); - put("云冈区", "南郊区"); - put("余江区", "余江县"); - put("马龙区", "马龙县"); - put("光明区", "宝安区"); - put("怀仁区", "怀仁县"); - put("彬州市", "彬县"); - put("海安市", "海安县"); - put("漠河市", "漠河县"); - put("京山市", "京山县"); - put("济阳区", "济阳县"); - put("潞州区", "城区"); - put("上党区", "长治县"); - put("屯留区", "屯留县"); - put("潞城区", "潞城市"); - put("滦州市", "滦县"); - put("潜山市", "潜山县"); - put("邹平市", "邹平县"); - put("荔浦市", "荔浦县"); - put("兴仁市", "兴仁县"); - put("水富市", "水富县"); - put("华亭市", "华亭县"); - put("积石山县", "积石山保安族东乡族撒拉族自治县"); - put("元江县", "元江哈尼族彝族傣族自治县"); - put("双江县", "双江拉祜族佤族布朗族傣族自治县"); - put("孟连县", "孟连傣族拉祜族佤族自治县"); - put("镇沅县", "镇沅彝族哈尼族拉祜族自治县"); - put("大柴旦行政委员会", "海西蒙古族藏族自治州直辖"); - put("冷湖行政委员会", "海西蒙古族藏族自治州直辖"); - put("茫崖行政委员会", "海西蒙古族藏族自治州直辖"); - put("上饶县", "广信区"); - put("达孜区", "达孜县"); - put("色尼区", "那曲县"); - } - }; - - - public static List determineAreaByCityId(Double lon, Double lat, int cityId, int radius, CoordinateSystem coordSys) { - if (cityId == -1) { - return Collections.emptyList(); - } - Tuple2 tuple2 = GeoUtils.toGCJ02(lon, lat, coordSys); - Location location = new Location(tuple2.get0(), tuple2.get1()); - List businessAreaDataList = cityBusinessArea.getOrDefault(cityId, Collections.emptyList()); - - // todo 可以被优化,整个城市过滤太粗糙了 - return businessAreaDataList.stream() - .map( - s -> new BusinessArea(s.name, s.areaCode, - GeoUtils.distance(s.center, location).intValue() - )) - .filter(businessArea -> businessArea.distance <= radius) - .collect(Collectors.toList()); - - } - - public static int determineAdminCode(double lonIn, double latIn, CoordinateSystem coordSys) { - if (coordSys == null) { - coordSys = CoordinateSystem.GCJ02; - } - Tuple2 gcj02LonLat = GeoUtils.toGCJ02(lonIn, latIn, coordSys); - Double lon = gcj02LonLat.get0(); - Double lat = gcj02LonLat.get1(); - - S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lon); - long id = S2CellId.fromLatLng(s2LatLng).parent(GeoTransImpl.min_level).id(); - long id2 = S2CellId.fromLatLng(s2LatLng).parent(GeoTransImpl.min_level - 2).id(); - - if (GeoUtils.outOfChina(lon, lat)) { - return -1; - } - if (boundaryAdminCell.containsKey(id)) { - return boundaryAdminCell.getOrDefault(id, -1); - } - if (boundaryAdminCell.containsKey(id2)) { - return boundaryAdminCell.getOrDefault(id2, -1); - } - - Set keys = new HashSet<>(); - //必须大于2000m,否则会出现格子半径过小选择错误问题 - int maxLevel = 2000; - while (keys.isEmpty() && maxLevel < 200000) { - keys = S2Utils.getCellId(s2LatLng, maxLevel, GeoTransImpl.min_level) - .stream() - .flatMap((Function>) aLong -> - boundaryIndex.getOrDefault(aLong, Collections.emptyList()).stream()) - .collect(Collectors.toSet()); - maxLevel = maxLevel * 2; - } - - if (keys.isEmpty()) { - return -1; - } - - - List, Tuple2, Integer, Boolean>, Double>> lines1 = - keys.stream() - .map(aLong -> Tuples.of(aLong, new S2CellId(aLong).toLatLng().getEarthDistance(s2LatLng))) - .sorted(Comparator.comparing(Tuple2::get1)) - .limit(5) - .flatMap((Function, Stream>>) - startPoint -> boundaryData.getOrDefault(startPoint.get0(), Collections.emptyList()) - .stream() - .flatMap((Function, Stream>>) - value -> { - List> s = Arrays.asList( - Tuples.of(startPoint.get0(), value.get0(), value.get1(), true), - Tuples.of(startPoint.get0(), value.get0(), value.get2(), false) - ); - return s.stream(); - })) - .map(line -> { - S2LatLng start = new S2CellId(line.get0()).toLatLng(); - S2LatLng end = new S2CellId(line.get1()).toLatLng(); - Double dis = LineUtils.pointToLineDis(start.lngDegrees(), start.latDegrees(), end.lngDegrees(), end.latDegrees(), lon, lat); - - return Tuples.of(Tuples.of(Tuples.of(start.lngDegrees(), start.latDegrees()), - Tuples.of(end.lngDegrees(), end.latDegrees()), - line.get2(), line.get3() - ), dis); - }) - .collect(Collectors.toList()); - - // 取出所有距离最短的线段 - Double minDis = lines1.stream().map(tuple4DoubleTuple2 -> tuple4DoubleTuple2.get1()).min(Double::compareTo).get(); - - List, Tuple2, Integer, Boolean>> lines = - lines1.stream() - .filter(s -> Objects.equals(s.get1(), minDis)) - .map(tuple2 -> tuple2.get0()) - .collect(Collectors.groupingBy(Tuple4::get0)) - .values() - .stream() - .max(Comparator.comparingInt(List::size)) - .get(); - - - if (lines.size() == 1) { - // 国内海外边界 - Tuple4, Tuple2, Integer, Boolean> line1 = lines.get(0); - Tuple2 start = line1.get0(); - Tuple2 end = line1.get1(); - // 三点用行列式判断旋转方向 - double angle = (start.get0() - lon) * (end.get1() - lat) - (end.get0() - lon) * (start.get1() - lat); - if ((angle < 0) == line1.get3()) - return line1.get2(); - return -1; - } - if (lines.size() == 2) { - // 两条线段,如果终点不同,则一定是国内和海外,并且点到线段距离最短点为起点,终点相同,则为国内两个区域边界 - Tuple4, Tuple2, Integer, Boolean> line1 = lines.get(0); - Tuple4, Tuple2, Integer, Boolean> line2 = lines.get(1); - - // 终点相同,为国内两个相邻区域,终点不同,为国界线 - Tuple2 start; - if (line1.get1().equals(line2.get1())) { - start = line1.get0(); - } else { - start = line2.get1(); - } - - Tuple2 end = line1.get1(); - - // 三点用行列式判断旋转方向 - double angle = (start.get0() - lon) * (end.get1() - lat) - (end.get0() - lon) * (start.get1() - lat); - if ((angle < 0) == line1.get3()) - return line1.get2(); - else if (line1.get1().equals(line2.get1()) && !line1.get3().equals(line2.get3())) - return line2.get2(); - else return -1; - } - //多区域顶点 判断 - - - return lines.stream().collect(Collectors.groupingBy(Tuple4::get2)).entrySet().stream().map(s -> { - - Tuple4, Tuple2, Integer, Boolean> line1 = - s.getValue().stream().filter(Tuple4::get3).findFirst().get(); - Tuple4, Tuple2, Integer, Boolean> line2 = - s.getValue().stream().filter(it -> !it.get3()).findFirst().get(); - - Tuple2 start = line2.get1(); - Tuple2 end = line1.get1(); - Tuple2 point = line1.get0(); - - Double dis1 = LineUtils.lineDis(start.get0(), start.get1(), point.get0(), point.get1()); - Double dis2 = LineUtils.lineDis(end.get0(), end.get1(), point.get0(), point.get1()); - if (dis1 > dis2) - start = Tuples.of(point.get0() + dis2 / dis1 * (start.get0() - point.get0()), point.get1() + dis2 / dis1 * (start.get1() - point.get1())); - else - end = Tuples.of(point.get0() + dis1 / dis2 * (end.get0() - point.get0()), point.get1() + dis1 / dis2 * (end.get1() - point.get1())); - double angle = (start.get0() - lon) * (end.get1() - lat) - (end.get0() - lon) * (start.get1() - lat); - return Tuples.of(s.getKey(), angle); - }).min((o1, o2) -> o1.get1().compareTo(o2.get1())).get().get0(); - } - - public static String getCityLevel(String adcode_or_name) { - return cityLevelData.getOrDefault(adcode_or_name, "未知"); - } - - public static String getCityLevel(Admin admin) { - return getCityLevel(String.valueOf(admin.cityCode)); - } - - public static AdminNode normalizeName(int adcode) { - AdminNode adminNode = adminData.get(adcode); - if (adminNode != null) { - return adminNode; - } - return streetData.get(adcode); - } - - public static List normalizeName(String name, DistrictLevel level, boolean isFullMatch) { - return adminData.values().stream() - .filter(it -> it.level.equals(level)) - .filter(s -> isFullMatch ? s.name.equals(name) : s.shortName.contains(name) || s.name.contains(name)) - .collect(Collectors.toList()); - } - - - public static List normalizeName(String provinceIn, String cityIn, String districtIn, String streetIn, boolean isFullMatch) { - String province = provinceIn == null || provinceIn.equals("未知") ? "" : provinceIn; - String city = cityIn == null || cityIn.equals("未知") ? "" : cityNameMap.getOrDefault(cityIn, cityIn); - String district = districtIn == null || districtIn.equals("未知") ? "" : districtNameMap.getOrDefault(districtIn, districtIn); - String street = streetIn == null || streetIn.equals("未知") ? "" : streetIn; - - - List adminNodes - = buildAdminNode(city, district, street, province, isFullMatch); - - - return adminNodes.stream().map(admin -> { - if (admin.level == DistrictLevel.Province) { - return Admin.createProvince(admin.name, admin.id, admin.center); - } - if (admin.level == DistrictLevel.City) { - AdminNode province1 = adminData.get(admin.parentId); - return Admin.createCity(province1.name, admin.name, province1.id, admin.id, admin.center); - } - if (admin.level == DistrictLevel.District) { - AdminNode city1 = adminData.get(admin.parentId); - AdminNode province1 = (city1.level == DistrictLevel.City) ? adminData.get(city1.parentId) : city1; - return Admin.createDistrict(province1.name, city1.name, admin.name, province1.id, city1.id, admin.id, admin.center); - } - if (admin.level == DistrictLevel.Street) { - AdminNode district1 = adminData.get(admin.parentId); - AdminNode city1 = (district1.level == DistrictLevel.District) ? adminData.get(district1.parentId) : district1; - AdminNode province1 = (city1.level == DistrictLevel.City) ? adminData.get(city1.parentId) : city1; - return Admin.createStreet(province1.name, city1.name, district1.name, admin.name, province1.id, city1.id, district1.id, admin.id, admin.center); - } - return Admin.createOversea(); - }).collect(Collectors.toList()); - } - - - private static List buildAdminNode(String city, String district, String street, String province, boolean isFullMatch) { - List provinceAd = adminData.values().stream().filter(s -> s.level == (DistrictLevel.Province)).filter(s -> StringUtils.isEmpty(province) || s.name.equals(province) || (!isFullMatch && s.shortName.equals(province))).collect(Collectors.toList()); - - if (StringUtils.isEmpty(city) && StringUtils.isEmpty(district) && StringUtils.isEmpty(street)) { - return provinceAd; - } - List cityAd = provinceAd.stream() - .flatMap(s -> s.children.stream().map(adCode -> adminData.getOrDefault(adCode, streetData.get(adCode)))) - .filter(s -> - // todo 原文这里的逻辑组合太复杂了,先保持原样看下情况 - (!(s.level == (DistrictLevel.City))) - || (s.level == (DistrictLevel.City)) - && ( - StringUtils.isEmpty(city) - || s.name.equals(city) - || (!isFullMatch && s.shortName.equals(city)) - ) - ) - .collect(Collectors.toList()); - if (cityAd.isEmpty()) { - return provinceAd; - } - - if (StringUtils.isEmpty(district) && StringUtils.isEmpty(street)) { - return cityAd.stream() - .filter(s -> s.level == (DistrictLevel.Province) || s.level == (DistrictLevel.City)) - .collect(Collectors.toList()); - } - - List districtAd = cityAd.stream().flatMap(s -> { - if (s.level == (DistrictLevel.City) && StringUtils.isEmpty(street)) { - return s.children.stream().map(adCode -> adminData.get(adCode)); - } else if (s.level == (DistrictLevel.City)) { - return s.children.stream().map(adCode -> adminData.getOrDefault(adCode, streetData.get(adCode))); - } else { - return Stream.of(s); - } - }) - .filter(s -> s != null) - .filter(s -> (s.level != (DistrictLevel.District)) || - s.level == (DistrictLevel.District) && - (StringUtils.isEmpty(district) || s.name.equals(district) || (!isFullMatch && s.shortName.equals(district))) - ).collect(Collectors.toList()); - - if (districtAd.isEmpty()) { - return cityAd; - } - - if (StringUtils.isEmpty(street)) { - return - districtAd.stream().filter( - s -> s.level == (DistrictLevel.Province) || s.level == (DistrictLevel.City) || s.level == (DistrictLevel.District) - ).collect(Collectors.toList()); - } - - List streetAd = districtAd.stream().flatMap(s -> { - if (s.level == (DistrictLevel.District)) { - return s.children.stream().map(adCode -> streetData.get(adCode)); - } else { - return Stream.of(s); - } - }).filter(s -> s.name.equals(street) || (!isFullMatch && s.shortName.equals(street))) - .collect(Collectors.toList()); - - if (streetAd.isEmpty()) { - return districtAd; - } - return streetAd; - } - - - public static Admin determineAdmin(double lon, double lat, boolean needStreet, CoordinateSystem coordSys) { - Tuple2 wgs84LonLat = GeoUtils.toWGS84(lon, lat, coordSys); - int code = GeoTransImpl.determineAdminCode(wgs84LonLat.get0(), wgs84LonLat.get1(), null); - if (code == -1) { - return Admin.createOversea(); - } - AdminNode district = adminData.get(code); - AdminNode city = (district.level == DistrictLevel.District) ? adminData.get(district.parentId) : district; - AdminNode province = (city.level == DistrictLevel.City) ? adminData.get(city.parentId) : city; - - int streetCode = 0; - String streetName = ""; - - if (needStreet) { - List children = district.children; - if (children != null && !children.isEmpty()) { - AdminNode minAdminNode = null; - double minDistance = Double.MAX_VALUE; - for (Integer integer : children) { - AdminNode adminNode = streetData.get(integer); - if (minAdminNode == null) { - minAdminNode = adminNode; - minDistance = GeoUtils.distance(adminNode.center, new Location(wgs84LonLat.get0(), wgs84LonLat.get1())); - continue; - } - Double distance = GeoUtils.distance(adminNode.center, new Location(wgs84LonLat.get0(), wgs84LonLat.get1())); - if (distance < minDistance) { - minAdminNode = adminNode; - minDistance = distance; - } - } - streetCode = minAdminNode.id; - streetName = minAdminNode.name; - } - - - } - - if (streetCode > 0) { - return Admin.createStreet(province.name, city.name, district.name, streetName, province.id, city.id, district.id, streetCode, district.center); - } - return Admin.createDistrict(province.name, city.name, district.name, province.id, city.id, district.id, district.center); - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/Admin.java b/src/main/java/com/dengxq/lnglat2Geo/entity/Admin.java deleted file mode 100644 index 8246232..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/Admin.java +++ /dev/null @@ -1,244 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import com.dengxq.lnglat2Geo.utils.AdminUtils; - -import java.util.Objects; - -public class Admin { - - private static final String CHINA_NAME = "中国"; - private static final String CHINA_ID = "CN"; - private static final String OVERSEA_NAME_VAL = "海外"; - private static final String UNKNOWN_NAME_VAL = "未知"; - private static final int UNKNOWN_ID_VAL = -1; - private static final Location UNKNOWN_LOCATION_VAL = null; - - - public String country; - public String province; - public String city; - public String district; - public String town; - public String level; - public String countryCode; - public int provinceCode; - public int cityCode; - public int districtCode; - public int townCode; - public Location center; - - public Admin(String country, String province, String city, String district, String town, String level, String countryCode, int provinceCode, int cityCode, int districtCode, int townCode, Location center) { - this.country = country; - this.province = province; - this.city = city; - this.district = district; - this.town = town; - this.level = level; - this.countryCode = countryCode; - this.provinceCode = provinceCode; - this.cityCode = cityCode; - this.districtCode = districtCode; - this.townCode = townCode; - this.center = center; - } - - - public Admin() { - } - - public boolean hasCenter() { - return center != Admin.UNKNOWN_LOCATION_VAL; - } - - public boolean hasProvince() { - return !Objects.equals(province, Admin.UNKNOWN_NAME_VAL); - } - - - public boolean hasCity() { - return !Objects.equals(city, Admin.UNKNOWN_NAME_VAL); - } - - - public boolean hasDistrict() { - return !Objects.equals(district, Admin.UNKNOWN_NAME_VAL); - } - - - public boolean hasCityId() { - return cityCode != Admin.UNKNOWN_ID_VAL; - } - - - public boolean hasDistrictId() { - return districtCode != Admin.UNKNOWN_ID_VAL; - } - - - public boolean hasTown() { - return !Objects.equals(town, Admin.UNKNOWN_NAME_VAL); - } - - - public String shortProvince() { - return AdminUtils.shortProvince(province); - } - - public String shortCity() { - return AdminUtils.shortCity(city); - } - - public Admin toShort() { - return new Admin(country, - AdminUtils.shortProvince(province), - AdminUtils.shortCity(city), - AdminUtils.shortDistrict(district), - AdminUtils.shortStreet(town), - level, countryCode, provinceCode, cityCode, districtCode, townCode, center - ); - } - // def toNameString: String = s"$country${if (hasProvince) province else ""} - // ${if (hasCity) city else ""} - // ${if (hasDistrict) district else ""} - // ${if (hasTown) town else ""}" - - public String toNameString() { - StringBuilder sb = new StringBuilder(country); - if (hasProvince()) { - sb.append(province); - } - if (hasCity()) { - sb.append(city); - } - if (hasDistrict()) { - sb.append(district); - } - if (hasTown()) { - sb.append(town); - } - return sb.toString(); - } - - @Override - public String toString() { - return toNameString(); - } - - public static Admin createOversea() { - return new Admin(OVERSEA_NAME_VAL, - OVERSEA_NAME_VAL, - OVERSEA_NAME_VAL, - OVERSEA_NAME_VAL, - OVERSEA_NAME_VAL, - AdminLevel.Oversea, - "", - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_LOCATION_VAL); - } - - public static Admin createCountry(String country, String countryID, Location center) { - return new Admin(country, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - AdminLevel.Country, - countryID, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - center); - } - - public static Admin createProvince(String province, int provinceId, Location center) { - return new Admin( - CHINA_NAME, - province, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - AdminLevel.Province, - CHINA_ID, - provinceId, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - center - ); - } - - public static Admin createCity(String province, String city, int provinceId, int cityId, Location center) { - return new Admin( - CHINA_NAME, - province, - city, - UNKNOWN_NAME_VAL, - UNKNOWN_NAME_VAL, - AdminLevel.City, - CHINA_ID, - provinceId, - cityId, - UNKNOWN_ID_VAL, - UNKNOWN_ID_VAL, - center - ); - } - - public static Admin createProvincialCity(String province, String city, int provinceId, int cityId, Location center) { - return new Admin( - CHINA_NAME, - province, - city, - city, - UNKNOWN_NAME_VAL, - AdminLevel.ProvincialCity, - CHINA_ID, - provinceId, - cityId, - cityId, - UNKNOWN_ID_VAL, - center - ); - } - - public static Admin createDistrict(String province, String city, String district, - int provinceId, int cityId, int districtId, Location center) { - return new Admin( - CHINA_NAME, - province, - city, - district, - UNKNOWN_NAME_VAL, - AdminLevel.District, - CHINA_ID, - provinceId, - cityId, - districtId, - UNKNOWN_ID_VAL, - center - ); - } - - public static Admin createStreet(String province, String city, String district, String town, - int provinceId, int cityId, int districtId, int streetId, Location center) { - return new Admin( - CHINA_NAME, - province, - city, - district, - town, - AdminLevel.Street, - CHINA_ID, - provinceId, - cityId, - districtId, - streetId, - center - ); - } - -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminBoundary.java b/src/main/java/com/dengxq/lnglat2Geo/entity/AdminBoundary.java deleted file mode 100644 index e0b7816..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminBoundary.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import lombok.Data; - -import java.util.List; - -@Data -public class AdminBoundary { - public int code; - public List> boundary; - -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminLevel.java b/src/main/java/com/dengxq/lnglat2Geo/entity/AdminLevel.java deleted file mode 100644 index 80b9783..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminLevel.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -public class AdminLevel { - /** 海外* */ - public static final String Oversea = "oversea"; - /** 国家* */ - public static final String Country = "country"; - /** 省,自治区* */ - public static final String Province = "province"; - /** 地级市* */ - public static final String City = "city"; - /** 省辖市(属县级市) see:https://baike.baidu.com/item/省直辖县级行政单位* */ - public static final String ProvincialCity = "provincialcity"; - /** 区,县,县级市* */ - public static final String District = "district"; - /** 街道* */ - public static final String Street = "street"; -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminNode.java b/src/main/java/com/dengxq/lnglat2Geo/entity/AdminNode.java deleted file mode 100644 index ba532e1..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/AdminNode.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import com.dengxq.lnglat2Geo.entity.enums.DistrictLevel; - -import java.util.ArrayList; -import java.util.List; - -public class AdminNode { - public int id; - public String name; - public String shortName; - public Location center; - public DistrictLevel level; - public int parentId; - public List children = new ArrayList<>(); - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getShortName() { - return shortName; - } - - public void setShortName(String shortName) { - this.shortName = shortName; - } - - public Location getCenter() { - return center; - } - - public void setCenter(Location center) { - this.center = center; - } - - public DistrictLevel getLevel() { - return level; - } - - public void setLevel(DistrictLevel level) { - this.level = level; - } - - public int getParentId() { - return parentId; - } - - public void setParentId(int parentId) { - this.parentId = parentId; - } - - public List getChildren() { - return children; - } - - public void setChildren(List children) { - this.children = children; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/Bound.java b/src/main/java/com/dengxq/lnglat2Geo/entity/Bound.java deleted file mode 100644 index 28d3fa3..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/Bound.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -public class Bound { - public Location mix; - public Location max; - - public Bound(Location mix, Location max) { - this.mix = mix; - this.max = max; - } - - public Location getMix() { - return mix; - } - - public Location getMax() { - return max; - } - - public void setMix(Location mix) { - this.mix = mix; - } - - public void setMax(Location max) { - this.max = max; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessArea.java b/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessArea.java deleted file mode 100644 index 8a00026..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessArea.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -public class BusinessArea { - public String name; - public Integer areaCode; - public Integer distance; - - public BusinessArea(String name, Integer areaCode, Integer distance) { - this.name = name; - this.areaCode = areaCode; - this.distance = distance; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaData.java b/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaData.java deleted file mode 100644 index bc3a338..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaData.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import lombok.Data; - -@Data -public class BusinessAreaData { - // 商圈名称 - public String name; - //商圈中心点 - public Location center; - //areaCode 商圈ID - public int areaCode; - -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaGroup.java b/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaGroup.java deleted file mode 100644 index f54c388..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaGroup.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class BusinessAreaGroup { - public int cityAdCode; - public List areas; -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaInfo.java b/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaInfo.java deleted file mode 100644 index 00a7e7e..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/BusinessAreaInfo.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - -import java.util.List; - -public class BusinessAreaInfo { - public Admin admin; - public List areas; - - public BusinessAreaInfo(Admin admin, List areas) { - this.admin = admin; - this.areas = areas; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/CellAdmin.java b/src/main/java/com/dengxq/lnglat2Geo/entity/CellAdmin.java deleted file mode 100644 index e27f733..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/CellAdmin.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.dengxq.lnglat2Geo.entity; - - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class CellAdmin { - public int adCode; - public long cellId; - - -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/Azimuth.java b/src/main/java/com/dengxq/lnglat2Geo/entity/enums/Azimuth.java deleted file mode 100644 index 5960932..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/Azimuth.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.dengxq.lnglat2Geo.entity.enums; - -import lombok.Getter; - -public enum Azimuth { - North(0), - NorthEast(45), - East(90), - SouthEast(135), - South(180), - SouthWest(225), - West(270), - NorthWest(315); - - @Getter - private int code; - - Azimuth(int value) { - this.code = value; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/CoordinateSystem.java b/src/main/java/com/dengxq/lnglat2Geo/entity/enums/CoordinateSystem.java deleted file mode 100644 index 503f276..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/CoordinateSystem.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.dengxq.lnglat2Geo.entity.enums; - -public enum CoordinateSystem { - WGS84, // GPS 坐标系 - GCJ02, // 国测局坐标系(火星坐标系) - BD09,// 百度坐标系 - BJ54,// 北京54坐标系 - XIAN80,// 西安80坐标系 - CGCS2000,// 2000国家大地坐标系 - XYZ,// 笛卡尔坐标系 - MERCATOR;// 墨卡托坐标系 -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/DistrictLevel.java b/src/main/java/com/dengxq/lnglat2Geo/entity/enums/DistrictLevel.java deleted file mode 100644 index 95c05ae..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/enums/DistrictLevel.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.dengxq.lnglat2Geo.entity.enums; - -public enum DistrictLevel { - /** - * 街道* - */ - Country, - /** - * 省,自治区* - */ - Province, - /** - * 地级市* - */ - City, - /** - * 区,县,县级市* - */ - District, - Street -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/loader/BinLoader.java b/src/main/java/com/dengxq/lnglat2Geo/loader/BinLoader.java deleted file mode 100644 index 9d19d15..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/loader/BinLoader.java +++ /dev/null @@ -1,414 +0,0 @@ -package com.dengxq.lnglat2Geo.loader; - -import com.dengxq.lnglat2Geo.entity.*; -import com.dengxq.lnglat2Geo.entity.enums.DistrictLevel; -import com.dengxq.lnglat2Geo.utils.Leb128; -import com.dengxq.lnglat2Geo.utils.Md5Util; -import com.speedment.common.tuple.Tuple3; -import com.speedment.common.tuple.Tuples; -import org.apache.commons.io.IOUtils; - -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * 二进制格式加载,相比json更快,更节省空间, - * 但是文件内容无法查看 - */ -public class BinLoader implements ILoader { - private static final int M = (1 << 20); - - private String sourcePath = "geolib.data.bin"; - - public BinLoader(String sourcePath) { - this.sourcePath = sourcePath; - } - - public BinLoader() { - } - - @Override - public GeoData load() { - try (InputStream stream = BinLoader.class.getClassLoader().getResourceAsStream(sourcePath)) { - if (stream == null) { - throw new RuntimeException("can not find data resource: " + sourcePath); - } - byte[] bytes = IOUtils.toByteArray(stream); - ByteBuffer buffer = ByteBuffer.wrap(bytes); - GeoData geoData = new GeoData(); - geoData.setAdminData(readAdminNodes(buffer)); - geoData.setStreetData(readAdminNodes(buffer)); - geoData.setAdminBoundaries(readAdminBoundaries(buffer)); - geoData.setAreaGroups(readAreaGroups(buffer)); - geoData.setCellAdmins(readCellAdmins(buffer)); - geoData.setCityLevel(readMap(buffer)); - geoData.setCountryCode(readMap(buffer)); - - // 对于扩展数据,我们默认是不会打入到二进制中,因为他会导致我们文件太大 - // 但是我们会把他缓存到缓存文件中,这样多次运行就会比较快 - String dataMd5 = Md5Util.getHashWithInputStream(new ByteArrayInputStream(bytes)); - handleExtension(geoData, dataMd5); - return geoData; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void handleExtension(GeoData geoData, String md5) throws IOException { - String userHome = System.getProperty("user.home"); - File base; - if (userHome != null && !userHome.trim().isEmpty()) { - base = new File(userHome); - } else { - base = new File("."); - } - File extensionPart = new File(base, md5 + ".xcgeo"); - if (extensionPart.exists()) { - // 如果存在缓存,那么把缓存加载到内存 - loadExtension(geoData, extensionPart); - } else { - new Thread("createExtension") { - @Override - public void run() { - // 如果没有缓存文件,那么使用新的线程建立缓存文件 - storeExtension(geoData, extensionPart); - } - }.start(); - - } - } - - private void loadExtension(GeoData geoData, File file) throws IOException { - try (FileChannel fileChannel = new RandomAccessFile(file, "r").getChannel()) { - MappedByteBuffer buffer = fileChannel - .map(FileChannel.MapMode.READ_ONLY, 0, file.length()); - geoData.setRuntimeBoundaryData(readRuntimeBoundaryData(buffer)); - geoData.setRuntimeBoundaryIndex(readRuntimeBoundaryIndex(buffer)); - geoData.setRuntimeBoundaryAdminCell(readRuntimeBoundaryAdminCell(buffer)); - } - } - - private void storeExtension(GeoData geoData, File file) { - ByteBuffer buffer = ByteBuffer.allocate(300 * M); - writeRuntimeBoundaryData(geoData.getOrCreateRuntimeBoundaryData(), buffer); - writeRuntimeBoundaryIndex(geoData.getOrCreateRuntimeBoundaryIndex(), buffer); - writeRuntimeBoundaryAdminCell(geoData.getOrCreateRuntimeBoundaryAdminCell(), buffer); - saveByteBuffer(buffer, file); - } - - @Override - public void dump(File path, GeoData geoData) { - // dump不需要考虑性能,分配50M的内存空间 - ByteBuffer buffer = ByteBuffer.allocate(70 * M); - writeAdminNodes(geoData.getAdminData(), buffer); - writeAdminNodes(geoData.getStreetData(), buffer); - writeAdminBoundaries(geoData.getAdminBoundaries(), buffer); - writeAreaGroups(geoData.getAreaGroups(), buffer); - writeCellAdmins(geoData.getCellAdmins(), buffer); - writeMap(geoData.getCityLevel(), buffer); - writeMap(geoData.getCountryCode(), buffer); - - saveByteBuffer(buffer, path); - } - - private static void saveByteBuffer(ByteBuffer buffer, File file) { - try (FileOutputStream fileOutputStream = new FileOutputStream(file)) { - buffer.flip(); - byte[] bytes = localBuff(4096); - - while (buffer.hasRemaining()) { - int count = Math.min(bytes.length, buffer.remaining()); - buffer.get(bytes, 0, count); - fileOutputStream.write(bytes, 0, count); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static final ThreadLocal buffLocal = new ThreadLocal<>(); - - private static byte[] localBuff(int len) { - if (len < 1024) { - len = 1024; - } - byte[] bytes = buffLocal.get(); - if (bytes != null && bytes.length >= len) { - return bytes; - } - bytes = new byte[len]; - buffLocal.set(bytes); - return bytes; - } - - - private static String readString(ByteBuffer buffer) { - int len = Leb128.readUnsignedLeb128(buffer); - if (len == 0) { - return ""; - } - // 读需要使用缓存的内存变量,减少gc消耗,写不需要关注, - // 这里提供了一个4k的缓存 - byte[] buf = localBuff(len); - buffer.get(buf, 0, len); - return new String(buf, 0, len, StandardCharsets.UTF_8); - } - - private static void writeString(ByteBuffer buffer, String str) { - if (str == null) { - str = ""; - } - byte[] bytes = str.getBytes(StandardCharsets.UTF_8); - Leb128.writeUnsignedLeb128(buffer, bytes.length); - if (bytes.length > 0) { - buffer.put(bytes); - } - } - - private void writeAdminNodes(List adminNodes, ByteBuffer buffer) { - Leb128.writeUnsignedLeb128(buffer, adminNodes.size()); - for (AdminNode adminNode : adminNodes) { - Leb128.writeUnsignedLeb128(buffer, adminNode.id); - writeString(buffer, adminNode.name); - writeString(buffer, adminNode.shortName); - - // 经纬度,我们使用float编码,因为实际上看起来,目前的数据应该在float上 - buffer.putFloat(adminNode.center.lng.floatValue()); - buffer.putFloat(adminNode.center.lat.floatValue()); - - buffer.put((byte) adminNode.level.ordinal()); - Leb128.writeUnsignedLeb128(buffer, adminNode.parentId); - - List children = adminNode.children; - Leb128.writeUnsignedLeb128(buffer, children.size()); - for (Integer integer : children) { - Leb128.writeSignedLeb128(buffer, integer); - } - } - } - - private List readAdminNodes(ByteBuffer buffer) { - int size = Leb128.readUnsignedLeb128(buffer); - DistrictLevel[] districtLevels = DistrictLevel.values(); - ArrayList adminNodes = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - AdminNode adminNode = new AdminNode(); - adminNodes.add(adminNode); - adminNode.setId(Leb128.readUnsignedLeb128(buffer)); - adminNode.setName(readString(buffer)); - adminNode.setShortName(readString(buffer)); - adminNode.setCenter(new Location((double) buffer.getFloat(), (double) buffer.getFloat())); - adminNode.setLevel(districtLevels[buffer.get()]); - adminNode.setParentId(Leb128.readUnsignedLeb128(buffer)); - - int childrenSize = Leb128.readUnsignedLeb128(buffer); - ArrayList children = new ArrayList<>(childrenSize); - adminNode.setChildren(children); - for (int j = 0; j < childrenSize; j++) { - children.add(Leb128.readSignedLeb128(buffer)); - } - } - return adminNodes; - } - - - private void writeAdminBoundaries(List adminBoundaries, ByteBuffer buffer) { - Leb128.writeUnsignedLeb128(buffer, adminBoundaries.size()); - for (AdminBoundary adminBoundary : adminBoundaries) { - //code - Leb128.writeSignedLeb128(buffer, adminBoundary.code); - List> boundary = adminBoundary.boundary; - Leb128.writeUnsignedLeb128(buffer, boundary.size()); - for (List line : boundary) { - Leb128.writeUnsignedLeb128(buffer, line.size()); - for (Long point : line) { - buffer.putLong(point); - } - } - } - } - - public List readAdminBoundaries(ByteBuffer buffer) { - int size = Leb128.readUnsignedLeb128(buffer); - ArrayList adminBoundaries = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - AdminBoundary adminBoundary = new AdminBoundary(); - adminBoundaries.add(adminBoundary); - adminBoundary.setCode(Leb128.readSignedLeb128(buffer)); - - int boundarySize = Leb128.readUnsignedLeb128(buffer); - ArrayList> boundary = new ArrayList<>(boundarySize); - adminBoundary.setBoundary(boundary); - for (int j = 0; j < boundarySize; j++) { - int lineSize = Leb128.readUnsignedLeb128(buffer); - ArrayList line = new ArrayList<>(lineSize); - boundary.add(line); - for (int z = 0; z < lineSize; z++) { - line.add(buffer.getLong()); - } - } - - } - - return adminBoundaries; - } - - - private void writeAreaGroups(List areaGroups, ByteBuffer buffer) { - Leb128.writeUnsignedLeb128(buffer, areaGroups.size()); - for (BusinessAreaGroup businessAreaGroup : areaGroups) { - Leb128.writeSignedLeb128(buffer, businessAreaGroup.cityAdCode); - List areas = businessAreaGroup.areas; - Leb128.writeUnsignedLeb128(buffer, areas.size()); - for (BusinessAreaData businessAreaData : areas) { - writeString(buffer, businessAreaData.name); - buffer.putFloat(businessAreaData.center.lng.floatValue()); - buffer.putFloat(businessAreaData.center.lat.floatValue()); - Leb128.writeSignedLeb128(buffer, businessAreaData.areaCode); - } - } - } - - public List readAreaGroups(ByteBuffer buffer) { - int size = Leb128.readUnsignedLeb128(buffer); - List ret = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - BusinessAreaGroup areaGroup = new BusinessAreaGroup(); - ret.add(areaGroup); - areaGroup.setCityAdCode(Leb128.readSignedLeb128(buffer)); - int businessAreaDataSize = Leb128.readUnsignedLeb128(buffer); - List areas = new ArrayList<>(businessAreaDataSize); - areaGroup.setAreas(areas); - for (int j = 0; j < businessAreaDataSize; j++) { - BusinessAreaData businessAreaData = new BusinessAreaData(); - areas.add(businessAreaData); - businessAreaData.setName(readString(buffer)); - businessAreaData.setCenter(new Location((double) buffer.getFloat(), (double) buffer.getFloat())); - businessAreaData.setAreaCode(Leb128.readSignedLeb128(buffer)); - } - } - return ret; - } - - private void writeCellAdmins(List cellAdmins, ByteBuffer buffer) { - Leb128.writeUnsignedLeb128(buffer, cellAdmins.size()); - for (CellAdmin cellAdmin : cellAdmins) { - Leb128.writeSignedLeb128(buffer, cellAdmin.adCode); - buffer.putLong(cellAdmin.cellId); - } - } - - private List readCellAdmins(ByteBuffer buffer) { - int size = Leb128.readUnsignedLeb128(buffer); - List ret = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - CellAdmin cellAdmin = new CellAdmin(Leb128.readSignedLeb128(buffer), - buffer.getLong()); - ret.add(cellAdmin); - } - return ret; - } - - - private void writeMap(Map map, ByteBuffer buffer) { - Leb128.writeUnsignedLeb128(buffer, map.size()); - for (Map.Entry entry : map.entrySet()) { - writeString(buffer, entry.getKey()); - writeString(buffer, entry.getValue()); - } - } - - private Map readMap(ByteBuffer buffer) { - Map ret = new HashMap<>(); - int size = Leb128.readUnsignedLeb128(buffer); - for (int i = 0; i < size; i++) { - ret.put(readString(buffer), readString(buffer)); - } - return ret; - } - - // 请注意,runtime文件不进行leb128,因为runtime大约会在200M左右,对他进行压缩没有意义 - // 反而leb128会有一些计算出现 - private void writeRuntimeBoundaryData(Map>> data, ByteBuffer buffer) { - buffer.putInt(data.size()); - for (Map.Entry>> entry : data.entrySet()) { - Long key = entry.getKey(); - buffer.putLong(key); - List> value = entry.getValue(); - buffer.putInt(value.size()); - for (Tuple3 tuple3 : value) { - buffer.putLong(tuple3.get0()); - buffer.putInt(tuple3.get1()); - buffer.putInt(tuple3.get2()); - } - } - } - - private Map>> readRuntimeBoundaryData(ByteBuffer buffer) { - int mapSize = buffer.getInt(); - Map>> ret = new HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - long key = buffer.getLong(); - int listSize = buffer.getInt(); - List> line = new ArrayList<>(listSize); - ret.put(key, line); - - for (int j = 0; j < listSize; j++) { - line.add(Tuples.of(buffer.getLong(), buffer.getInt(), buffer.getInt())); - } - } - return ret; - } - - private void writeRuntimeBoundaryIndex(Map> data, ByteBuffer buffer) { - buffer.putInt(data.size()); - for (Map.Entry> entry : data.entrySet()) { - Long key = entry.getKey(); - buffer.putLong(key); - List value = entry.getValue(); - buffer.putInt(value.size()); - for (Long aLong : value) { - buffer.putLong(aLong); - } - } - } - - private Map> readRuntimeBoundaryIndex(ByteBuffer buffer) { - int mapSize = buffer.getInt(); - Map> ret = new HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - long key = buffer.getLong(); - int listSize = buffer.getInt(); - List line = new ArrayList<>(listSize); - ret.put(key, line); - for (int j = 0; j < listSize; j++) { - line.add(buffer.getLong()); - } - } - return ret; - } - - private void writeRuntimeBoundaryAdminCell(Map data, ByteBuffer buffer) { - buffer.putInt(data.size()); - for (Map.Entry entry : data.entrySet()) { - buffer.putLong(entry.getKey()); - buffer.putInt(entry.getValue()); - } - } - - private Map readRuntimeBoundaryAdminCell(ByteBuffer buffer) { - int mapSize = buffer.getInt(); - Map ret = new HashMap<>(mapSize); - for (int i = 0; i < mapSize; i++) { - ret.put(buffer.getLong(), buffer.getInt()); - } - return ret; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/loader/GeoData.java b/src/main/java/com/dengxq/lnglat2Geo/loader/GeoData.java deleted file mode 100644 index a29b20a..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/loader/GeoData.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.dengxq.lnglat2Geo.loader; - -import com.dengxq.lnglat2Geo.entity.AdminBoundary; -import com.dengxq.lnglat2Geo.entity.AdminNode; -import com.dengxq.lnglat2Geo.entity.BusinessAreaGroup; -import com.dengxq.lnglat2Geo.entity.CellAdmin; -import com.dengxq.lnglat2Geo.utils.Utils; -import com.speedment.common.tuple.Tuple3; -import lombok.Data; - -import java.util.List; -import java.util.Map; - -/** - * 离线数据包,包括行政区折线图、行政等级划分、商圈、城市编码、城市级别等 - *

- * 他可以由二进制或者json两种格式转换加载,并且可以序列化到二进制和json两种格式, - * 其中二进制格式将会伴随发布包发布,作为默认的离线数据资源, - * json格式则主要用于调试和升级二进制格式使用 - */ -@Data -public class GeoData { - - /** - * 行政区域数据 - */ - private List adminData; - - /** - * 街道数据 - */ - private List streetData; - - /** - * 多边形边界线,规定每个区域的范围 - */ - private List adminBoundaries; - - - /** - * 商圈 - */ - private List areaGroups; - - /** - * s2到行政编码的缓存,他不是必须的,但是可以提高计算速度 - */ - private List cellAdmins; - - /** - * 城市级别 - */ - private Map cityLevel; - - /** - * 国家编码 - */ - private Map countryCode; - - // 以下为extension数据,他是根据上面的字段计算而来,因为他们的计算比较耗时,我们把计算好的数据缓存到这里, - // json 格式不序列化这些字段,bin需要序列化这些字段 - private Map>> runtimeBoundaryData; - - private Map> runtimeBoundaryIndex; - - private Map runtimeBoundaryAdminCell; - - public void clearRuntime() { - runtimeBoundaryData = null; - runtimeBoundaryIndex = null; - runtimeBoundaryAdminCell = null; - } - - public synchronized Map>> getOrCreateRuntimeBoundaryData() { - if (runtimeBoundaryData == null) { - runtimeBoundaryData = Utils.parseBoundaryData(adminBoundaries); - } - return runtimeBoundaryData; - } - - public synchronized Map> getOrCreateRuntimeBoundaryIndex() { - if (runtimeBoundaryIndex == null) { - runtimeBoundaryIndex = Utils.buildBoundaryIndex(getOrCreateRuntimeBoundaryData()); - } - return runtimeBoundaryIndex; - } - - public synchronized Map getOrCreateRuntimeBoundaryAdminCell() { - if (runtimeBoundaryAdminCell == null) { - runtimeBoundaryAdminCell = Utils.buildBoundaryAdminCell(cellAdmins); - } - return runtimeBoundaryAdminCell; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/loader/ILoader.java b/src/main/java/com/dengxq/lnglat2Geo/loader/ILoader.java deleted file mode 100644 index 97c7cd5..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/loader/ILoader.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.dengxq.lnglat2Geo.loader; - -import lombok.Setter; - -import java.io.File; - -/** - * 数据加载和dump的顶层,支持从json文件和bin文件中加载,以及dump到json和bin文件中 - * 实现可视化格式和二进制格式的相互转换。 - *

- * 我们导入和更新数据来自于可视化json,他易于编辑,但是文本文件提及太大。 - * 线上使用二进制格式,能快速加载和解析,但是他没办法编辑和直接查看 - */ -public interface ILoader { - /** - * 从一个class path路径加载离线数据 - * - * @return 数据 - */ - GeoData load(); - - /** - * 将离线数据dump到文件中,并且这个文件可以被load再次识别 - * - * @param path 文件 - * @param geoData 数据结构对象 - */ - void dump(File path, GeoData geoData); - - class Storage { - private static GeoData geoData = null; - @Setter - private static ILoader loader = new BinLoader(); - - public static void clear() { - geoData = null; - } - - public static GeoData getOrLoad() { - if (geoData != null) { - return geoData; - } - synchronized (Storage.class) { - if (geoData != null) { - return geoData; - } - geoData = loader.load(); - } - return geoData; - } - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/loader/JSONLoader.java b/src/main/java/com/dengxq/lnglat2Geo/loader/JSONLoader.java deleted file mode 100644 index a2ed90e..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/loader/JSONLoader.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.dengxq.lnglat2Geo.loader; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.parser.ParserConfig; -import com.alibaba.fastjson.serializer.SerializerFeature; -import com.dengxq.lnglat2Geo.entity.AdminBoundary; -import com.dengxq.lnglat2Geo.entity.AdminNode; -import com.dengxq.lnglat2Geo.entity.BusinessAreaGroup; -import com.dengxq.lnglat2Geo.entity.CellAdmin; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.IOUtils; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.Map; - -/** - * 从json资源中加载数据,生产不会使用它 - */ -public class JSONLoader implements ILoader { - - private static String loadResource(String key) { - key = "json/" + key; - try (InputStream inputStream = JSONLoader.class.getClassLoader().getResourceAsStream(key)) { - if (inputStream == null) { - throw new RuntimeException("resource not exist: " + key); - } - return IOUtils.toString(inputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static final String FILE_ADMIN_DATA = "adminData.json"; - private static final String FILE_STREET_DATA = "streetData.json"; - private static final String FILE_CELL_ADMINS = "cellAdmins.json"; - private static final String FILE_ADMIN_BOUNDARIES = "adminBoundaries.json"; - private static final String FILE_AREA_GROUPS = "areaGroups.json"; - private static final String FILE_CITY_LEVEL_DATA = "cityLevelData.json"; - private static final String FILE_COUNTRY_CODE_DATA = "countryCode.json"; - - @SuppressWarnings("unchecked") - @Override - public GeoData load() { - GeoData geoData = new GeoData(); - geoData.setAdminData(JSON.parseArray(loadResource(FILE_ADMIN_DATA), AdminNode.class)); - geoData.setStreetData(JSON.parseArray(loadResource(FILE_STREET_DATA), AdminNode.class)); - - geoData.setAdminBoundaries(JSON.parseArray(loadResource(FILE_ADMIN_BOUNDARIES), AdminBoundary.class)); - geoData.setAreaGroups(JSON.parseArray(loadResource(FILE_AREA_GROUPS), BusinessAreaGroup.class)); - geoData.setCellAdmins(JSON.parseArray(loadResource(FILE_CELL_ADMINS), CellAdmin.class)); - - geoData.setCityLevel(JSON.parseObject(loadResource(FILE_CITY_LEVEL_DATA), Map.class)); - geoData.setCountryCode(JSON.parseObject(loadResource(FILE_COUNTRY_CODE_DATA), Map.class)); - return geoData; - } - - @Override - public void dump(File path, GeoData geoData) { - try { - // 在测试环境,我们大概率不希望所有数据都存储在一个大json中,所以这里我们分割多个文件存储 - // 二进制不具备可读性,故合并所有数据在同一个文件 - dumpObject(new File(path, FILE_ADMIN_DATA), geoData.getAdminData()); - dumpObject(new File(path, FILE_STREET_DATA), geoData.getStreetData()); - dumpObject(new File(path, FILE_ADMIN_BOUNDARIES), geoData.getAdminBoundaries()); - dumpObject(new File(path, FILE_AREA_GROUPS), geoData.getAreaGroups()); - dumpObject(new File(path, FILE_CELL_ADMINS), geoData.getCellAdmins()); - dumpObject(new File(path, FILE_CITY_LEVEL_DATA), geoData.getCityLevel()); - dumpObject(new File(path, FILE_COUNTRY_CODE_DATA), geoData.getCountryCode()); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private void dumpObject(File file, Object obj) throws IOException { - FileUtils.writeStringToFile(file, JSON.toJSONString(obj, SerializerFeature.PrettyFormat)); - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/test/Test.java b/src/main/java/com/dengxq/lnglat2Geo/test/Test.java deleted file mode 100644 index 1cd7911..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/test/Test.java +++ /dev/null @@ -1,173 +0,0 @@ -package com.dengxq.lnglat2Geo.test; - -import com.dengxq.lnglat2Geo.GeoTrans; -import com.dengxq.lnglat2Geo.GeoTransImpl; -import com.dengxq.lnglat2Geo.entity.Admin; -import com.dengxq.lnglat2Geo.entity.enums.CoordinateSystem; -import com.dengxq.lnglat2Geo.entity.Location; -import com.dengxq.lnglat2Geo.loader.BinLoader; -import com.dengxq.lnglat2Geo.loader.GeoData; -import com.dengxq.lnglat2Geo.loader.ILoader; -import com.dengxq.lnglat2Geo.utils.AdminUtils; -import com.speedment.common.tuple.Tuple3; - -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -public class Test { - public static void main(String[] args) { - //loaderTest(); - testCase(); - } - - private static void loaderTest() { - GeoData geoData = ILoader.Storage.getOrLoad(); - //new JSONLoader().dump(new File("src/main/resources/data.json"), geoData); - new BinLoader().dump(new File("src/main/resources/data_bak.bin"), geoData); -// //数据dump到json文件中 -// new JSONLoader().dump(new File("src/main/resources/json/"), geoData); -// ILoader.Storage.setLoader(new JSONLoader()); -// new BinLoader().dump(new File("src/main/resources/data.bin"), geoData); - } - - private static void testCase() { - long start = System.currentTimeMillis(); - ILoader.Storage.getOrLoad(); - System.out.println("data load cost = " + (System.currentTimeMillis() - start)); - start = System.currentTimeMillis(); - Map>> boundaryData = GeoTransImpl.boundaryData; - System.out.println("data init cost = " + (System.currentTimeMillis() - start)); - - shortCityTest(); - System.out.println("\n\n\n"); - normalizeNameTest(); - System.out.println("\n\n\n"); - determineAdminTest(); - } - - - private static void determineAdminTest() { - - - long start1 = System.currentTimeMillis(); - System.out.println(GeoTrans.determineAdmin(119.718017578125, 36.25756282630298, CoordinateSystem.GCJ02, true)); - System.out.println("determineAdmin cost = " + (System.currentTimeMillis() - start1)); - - - String str = "112.989382,28.147062;109.046404,35.042294;106.559531,29.607832;119.481842,33.652686;116.525612,39.824004;109.090599,35.080281;113.508112,37.892087;123.417829,41.791227;120.517459,30.459049;113.865295,35.290525;110.290043,20.015764;108.934191,34.294362;117.183897,34.264914;126.587992,45.757869;115.859063,28.695778;106.771075,26.584885; 108.92224,34.233088;113.809742,23.067213;118.778811,32.089465;113.715261,35.2587"; - for (String s : str.split(";")) { - String[] parts = s.split(","); - Admin ss = GeoTrans.determineAdmin(Double.parseDouble(parts[0]), Double.parseDouble(parts[1]) - , CoordinateSystem.WGS84, true); - System.out.println(ss); - } - - - Location[] testLocation = new Location[]{ - new Location(121.1572265625, 23.9260130330), // 中国台湾省南投县仁爱乡 - new Location(112.567757, 35.096176), // 济源 - new Location(116.9565868378, 39.6513677208), // 天津市武清区河西务镇 - new Location(100.4315185547, 21.7594997307), // 中国云南省西双版纳傣族自治州勐海县勐混镇 - new Location(85.5670166016, 41.5548386631), // 中国新疆维吾尔自治区巴音郭楞蒙古自治州库尔勒市 普惠乡 - new Location(117.9969406128, 27.7447712551), // 中国福建省南平市武夷山市 崇安街道 - new Location(110.8520507813, 34.0526594214), // 河南省三门峡市卢氏县 瓦窑沟乡下河村 - new Location(116.4811706543, 39.9255352817), // 北京市朝阳区 六里屯街道甜水园 - new Location(116.3362348080, 40.0622912084), // 北京市昌平区 回龙观地区吉晟别墅社区 - new Location(116.3362830877, 40.0594500522), // 北京市北京市昌平区 建材城西路65号 - new Location(116.3325601816, 40.0397393499), // 北京市海淀区 清河街道 - new Location(117.0977783203, 36.5085323575), // 山东省济南市历城区 - new Location(118.6358642578, 35.8356283889), // 山东省临沂市沂水县 - new Location(119.7853088379, 36.3029520437), // 山东省潍坊市高密市柏城镇 - new Location(119.8567199707, 36.2808142593), // 山东省青岛市胶州市胶西镇 - new Location(120.3892135620, 36.2777698228), // 山东省青岛市城阳区流亭街道于家社区 - new Location(120.152983, 36.119759), // 海外 - new Location(98.774694, 23.706633) // 海外 - }; - - Admin[] testResult = new Admin[]{ - Admin.createProvincialCity("台湾省", "南投县", 710008, 710008, new Location(0.0, 0.0)), - Admin.createProvincialCity("河南省", "济源市", 419001, 419001, new Location(112.602256, 35.067199)), - Admin.createDistrict("天津市", "天津城区", "武清区", 120100, 120100, 120114, new Location(117.044387, 39.384119)), - Admin.createDistrict("云南省", "西双版纳傣族自治州", "勐海县", 532800, 532800, 532822, new Location(100.452547, 21.957353)), - Admin.createDistrict("新疆维吾尔自治区", "巴音郭楞蒙古自治州", "库尔勒市", 652800, 652800, 652801, new Location(86.174633, 41.725891)), - Admin.createDistrict("福建省", "南平市", "武夷山市", 350700, 350700, 350782, new Location(118.035309, 27.756647)), - Admin.createDistrict("河南省", "三门峡市", "卢氏县", 411200, 411200, 411224, new Location(111.047858, 34.054324)), - Admin.createDistrict("北京市", "北京城区", "朝阳区", 110100, 110100, 110105, new Location(116.443205, 39.921506)), - Admin.createDistrict("北京市", "北京城区", "昌平区", 110100, 110100, 110114, new Location(116.231254, 40.220804)), - Admin.createDistrict("北京市", "北京城区", "昌平区", 110100, 110100, 110114, new Location(116.231254, 40.220804)), - Admin.createDistrict("北京市", "北京城区", "海淀区", 110100, 110100, 110108, new Location(116.298262, 39.95993)), - Admin.createDistrict("山东省", "济南市", "历城区", 370100, 370100, 370112, new Location(117.06523, 36.680259)), - Admin.createDistrict("山东省", "临沂市", "沂水县", 371300, 371300, 371323, new Location(118.627917, 35.79045)), - Admin.createDistrict("山东省", "潍坊市", "高密市", 370700, 370700, 370785, new Location(119.755597, 36.382594)), - Admin.createDistrict("山东省", "青岛市", "胶州市", 370200, 370200, 370281, new Location(120.033382, 36.26468)), - Admin.createDistrict("山东省", "青岛市", "城阳区", 370200, 370200, 370214, new Location(120.396256, 36.307559)), - Admin.createDistrict("海外", "海外", "海外", -1, -1, -1, new Location(120.396256, 36.307559)), - Admin.createDistrict("海外", "海外", "海外", -1, -1, -1, new Location(120.396256, 36.307559)) - }; - System.out.println("\n\n\n"); - - for (int i = 0; i < testLocation.length; i++) { - Location location = testLocation[i]; - Admin result = testResult[i]; - - Admin admin = GeoTrans.determineAdmin(location.lng, location.lat, CoordinateSystem.GCJ02, true); - System.out.println(admin); - System.out.println(result); - - if (!(Objects.equals(admin.province, result.province))) { - throw new AssertionError(); - } - if (!(Objects.equals(admin.city, result.city))) { - throw new AssertionError(); - } - if (!(admin.cityCode == result.cityCode)) { - throw new AssertionError(); - } - if (!(Objects.equals(admin.district, result.district))) { - throw new AssertionError(); - } - if (!(admin.districtCode == result.districtCode)) { - throw new AssertionError(); - } - } - - } - - private static void normalizeNameTest() { - for (Admin admin : GeoTrans.normalizeName("", "攀枝花", "", "", false)) { - System.out.println(admin); - } - for (Admin admin : GeoTrans.normalizeName("", "北京", "海淀", "", false)) { - System.out.println(admin); - } - - } - - - private static void shortCityTest() { - String[] testSource1 = new String[]{"襄樊市", "恩施州", "昌吉回族自治州", "海北藏族自治州", "克孜勒苏柯尔克孜自治州", "文山壮族苗族自治州", "海西蒙古族藏族自治州", "海南藏族自治州", "博尔塔拉蒙古自治州", "西双版纳傣族自治州", "玉树藏族自治州", "果洛藏族自治州", "怒江傈僳族自治州", "迪庆藏族自治州", "楚雄彝族自治州", "大理白族自治州", "德宏傣族景颇族自治州", "黄南藏族自治州", "湘西土家族苗族自治州", "伊犁哈萨克自治州", "延边朝鲜族自治州", "红河哈尼族彝族自治州", "黔西南布依族苗族自治州", "恩施土家族苗族自治州", "黔南布依族苗族自治州", "临夏回族自治州", "甘孜藏族自治州", "黔东南苗族侗族自治州", "凉山彝族自治州", "甘南藏族自治州", "阿坝藏族羌族自治州", "巴音郭楞蒙古自治州"}; - String[] testResult1 = new String[]{"襄阳", "恩施", "昌吉", "海北", "克孜勒苏", "文山", "海西", "海南", "博尔塔拉", "西双版纳", "玉树", "果洛", "怒江", "迪庆", "楚雄", "大理", "德宏", "黄南", "湘西", "伊犁", "延边", "红河", "黔西南", "恩施", "黔南", "临夏", "甘孜", "黔东南", "凉山", "甘南", "阿坝", "巴音郭楞"}; - - String[] testSource2 = new String[]{"嘉模堂区", "新竹县", "嘉义市", "风顺堂区", "花王堂区", "云林县", "台北市", "望德堂区", "圣方济各堂区", "屏东县", "南投县", "桃园县", "花地玛堂区", "台中市", "大堂区", "新竹市", "基隆市", "彰化县", "苗栗县", "台南市", "台东县", "宜兰县", "澎湖县", "嘉义县", "路凼填海区", "花莲县", "高雄市", "新北市", "三沙市", "昆玉市", "铁门关市", "湾仔区", "油尖旺区", "中西区", "双河市", "东区", "观塘区", "九龙城区", "屯门区", "深水埗区", "黄大仙区", "葵青区", "南区", "荃湾区", "沙田区", "大埔区", "西贡区", "海口市", "东沙群岛", "可克达拉市", "舟山市", "珠海市", "拉萨市", "阳泉市", "长治市", "三亚市", "运城市", "山南市", "盘锦市", "临汾市", "汕头市", "忻州市", "银川市", "克拉玛依市", "哈密市", "石嘴山市", "晋中市", "乌鲁木齐市", "朔州市", "林芝市", "吕梁市", "上海城区", "昌吉回族自治州", "鞍山市", "潮州市", "阿克苏地区", "苏州市", "吐鲁番市", "深圳市", "连云港市", "晋城市", "邯郸市", "海北藏族自治州", "本溪市", "昌都市", "大同市", "揭阳市", "日喀则市", "石家庄市", "大兴安岭地区", "临沧市", "韶关市", "那曲地区", "和田地区", "辽阳市", "克孜勒苏柯尔克孜自治州", "文山壮族苗族自治州", "阿勒泰地区", "邢台市", "太原市", "营口市", "南通市", "衡水市", "鄂州市", "鹤岗市", "海西蒙古族藏族自治州", "海南藏族自治州", "大庆市", "盐城市", "博尔塔拉蒙古自治州", "七台河市", "莱芜市", "葫芦岛市", "泰州市", "松原市", "东营市", "白城市", "西双版纳傣族自治州", "昆明市", "抚顺市", "玉树藏族自治州", "阿里地区", "枣庄市", "锦州市", "保山市", "黄石市", "宿迁市", "西宁市", "果洛藏族自治州", "无锡市", "扬州市", "徐州市", "海东市", "随州市", "大连市", "肇庆市", "沧州市", "淮安市", "丹东市", "广州市", "河源市", "汕尾市", "朝阳市", "普洱市", "厦门市", "秦皇岛市", "白山市", "宁波市", "阜新市", "丽江市", "伊春市", "怒江傈僳族自治州", "菏泽市", "濮阳市", "四平市", "威海市", "江门市", "孝感市", "聊城市", "迪庆藏族自治州", "鸡西市", "楚雄彝族自治州", "廊坊市", "云浮市", "鹤壁市", "温州市", "张家界市", "大理白族自治州", "镇江市", "景德镇市", "铁岭市", "株洲市", "泰安市", "吴忠市", "德宏傣族景颇族自治州", "黄南藏族自治州", "萍乡市", "漯河市", "惠州市", "齐齐哈尔市", "梅州市", "唐山市", "中卫市", "塔城地区", "清远市", "台州市", "黑河市", "淮北市", "铜陵市", "淮南市", "佛山市", "莆田市", "阳江市", "烟台市", "湖州市", "周口市", "玉溪市", "娄底市", "漳州市", "淄博市", "滨州市", "永州市", "常州市", "嘉兴市", "新余市", "潍坊市", "安庆市", "承德市", "济南市", "南昌市", "喀什地区", "咸宁市", "九江市", "通化市", "鹰潭市", "临沂市", "双鸭山市", "湘西土家族苗族自治州", "济宁市", "保定市", "德州市", "北海市", "固原市", "荆门市", "商丘市", "衡阳市", "伊犁哈萨克自治州", "湘潭市", "焦作市", "泉州市", "十堰市", "新乡市", "哈尔滨市", "日照市", "延边朝鲜族自治州", "曲靖市", "宜昌市", "辽源市", "天津城区", "亳州市", "合肥市", "抚州市", "安阳市", "西安市", "衢州市", "郴州市", "蚌埠市", "宿州市", "芜湖市", "牡丹江市", "岳阳市", "襄阳市", "黄山市", "昭通市", "荆州市", "宜春市", "黄冈市", "红河哈尼族彝族自治州", "南京市", "咸阳市", "绍兴市", "阜阳市", "安顺市", "开封市", "吉安市", "池州市", "绥化市", "许昌市", "宝鸡市", "六安市", "益阳市", "张家口市", "平顶山市", "黔西南布依族苗族自治州", "常德市", "龙岩市", "郑州市", "驻马店市", "铜川市", "马鞍山市", "防城港市", "湛江市", "赣州市", "沈阳市", "上饶市", "南平市", "恩施土家族苗族自治州", "渭南市", "佳木斯市", "金华市", "邵阳市", "长春市", "南阳市", "三门峡市", "桂林市", "黔南布依族苗族自治州", "长沙市", "安康市", "延安市", "武汉市", "六盘水市", "吉林市", "商洛市", "福州市", "贺州市", "北京城区", "青岛市", "信阳市", "洛阳市", "三明市", "柳州市", "丽水市", "雅安市", "崇左市", "宣城市", "金昌市", "滁州市", "汉中市", "怀化市", "贵港市", "贵阳市", "梧州市", "钦州市", "遵义市", "攀枝花市", "茂名市", "南宁市", "杭州市", "玉林市", "眉山市", "河池市", "张掖市", "乐山市", "临夏回族自治州", "白银市", "武威市", "天水市", "百色市", "成都市", "来宾市", "乌海市", "甘孜藏族自治州", "巴中市", "毕节市", "黔东南苗族侗族自治州", "酒泉市", "榆林市", "凉山彝族自治州", "内江市", "遂宁市", "铜仁市", "德阳市", "陇南市", "广元市", "宜宾市", "资阳市", "绵阳市", "甘南藏族自治州", "巴彦淖尔市", "广安市", "定西市", "阿坝藏族羌族自治州", "庆阳市", "泸州市", "平凉市", "巴音郭楞蒙古自治州", "自贡市", "重庆城区", "呼和浩特市", "包头市", "兰州市", "宁德市", "南充市", "达州市", "通辽市", "阿拉善盟", "兴安盟", "锡林郭勒盟", "重庆郊县", "赤峰市", "乌兰察布市", "鄂尔多斯市", "呼伦贝尔市"}; - - for (int i = 0; i < testSource1.length; i++) { - String s = AdminUtils.shortCityImp(testSource1[i]).get0(); - System.out.println(testSource1[i] + "->" + s); - if ((!s.equals(testResult1[i]))) - throw new AssertionError(); - } - - System.out.println("\n\n\n"); - - for (String test : testSource2) { - String shortCity = AdminUtils.shortCityImp(test).get0(); - System.out.println(test + "->" + shortCity); -// if ((!shortCity.equals(test))) -// throw new AssertionError(); - } - } - - -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/AdminUtils.java b/src/main/java/com/dengxq/lnglat2Geo/utils/AdminUtils.java deleted file mode 100644 index dc0f2e1..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/AdminUtils.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - -import com.dengxq.lnglat2Geo.entity.enums.DistrictLevel; -import com.speedment.common.tuple.Tuple2; -import com.speedment.common.tuple.Tuples; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class AdminUtils { - public static final String[] NATIONS = "阿昌族,鄂温克族,傈僳族,水族,白族,高山族,珞巴族,塔吉克族,保安族,仡佬族,满族,塔塔尔族,布朗族,哈尼族,毛南族,土家族,布依族,哈萨克族,门巴族,土族,朝鲜族,汉族,蒙古族,佤族,达斡尔族,赫哲族,苗族,维吾尔族,傣族,回族,仫佬族,乌孜别克族,德昂族,基诺族,纳西族,锡伯族,东乡族,京族,怒族,瑶族,侗族,景颇族,普米族,彝族,独龙族,柯尔克孜族,羌族,裕固族,俄罗斯族,拉祜族,撒拉族,藏族,鄂伦春族,黎族,畲族,壮族".split(","); - - public static final Pattern p1 = Pattern.compile("(.+)(?:省|市)$"); - public static final Pattern p2 = Pattern.compile("(.+)自治区"); - public static final Pattern p3 = Pattern.compile("(.+)特别行政区"); - - public static final Pattern c0 = Pattern.compile("^(.{2})$"); // 2 长度为2的 "东区" "南区" - public static final Pattern c1 = Pattern.compile("(.+)(?:自治州|自治县)$"); // 30 自治州 琼中黎族苗族自治县 - public static final Pattern c2 = Pattern.compile("(.+)[市|盟|州]$"); // 304 地级市, 盟; + 1恩施州 - public static final Pattern c3 = Pattern.compile("(.+)地区$"); // 8 地区 - public static final Pattern c4 = Pattern.compile("(.+)(?:群岛|填海区)$"); // 2 东沙群岛 - public static final Pattern c5 = Pattern.compile("(.+[^地郊城堂])区$"); // 20 港澳 不含"东区" "南区"2个字的 - public static final Pattern c6 = Pattern.compile("(.+)(?:城区|郊县)$"); // 6 九龙城区,上海城区,天津城区,北京城区,重庆城区,重庆郊县 - public static final Pattern c7 = Pattern.compile("(.+[^郊])县$"); // 12 台湾的xx县 - - public static final Pattern d0 = Pattern.compile("^(.{2})$"); // 2 长度为2的 "随县" - public static final Pattern d1 = Pattern.compile("(.+)[市]$"); // 304 城区 “赤水市” - public static final Pattern d2 = Pattern.compile("(.+)自治县$"); // 30 自治县 - public static final Pattern d3 = Pattern.compile("(.+)自治州直辖$"); // 30 自治州直辖 "海西蒙古族藏族自治州直辖" - public static final Pattern d4 = Pattern.compile("(.+)[区|县]$"); // 8 区县 - public static final Pattern d5 = Pattern.compile("(.+)(?:乡|镇|街道)$"); // 8 乡镇|街道 - - public static final Pattern s0 = Pattern.compile("^(.{2})$"); - public static final Pattern s1 = Pattern.compile("(.+)(?:特别行政管理区|街道办事处|旅游经济特区|民族乡|地区街道)$"); - public static final Pattern s2 = Pattern.compile("(.+)(?:镇|乡|村|街道|苏木|老街|管理区|区公所|苏木|办事处|社区|经济特区|行政管理区)$"); - - - public static String shortProvince(String province) { - Matcher matcher = p1.matcher(province); - if (matcher.matches()) { - return matcher.group(1); - } - - matcher = p2.matcher(province); - if (matcher.matches()) { - String x = matcher.group(1); - if (x.equals("内蒙古")) { - return x; - } - return replaceNations(x); - } - - matcher = p3.matcher(province); - if (matcher.matches()) { - return matcher.group(1); - } - - return province; - } - - - public static Tuple2 shortCityImp(String city) { - // 总数 383 - Matcher matcher = c0.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 0); - } - matcher = c1.matcher(city); - if (matcher.matches()) { - return Tuples.of(replaceNations(matcher.group(1)), 2); - } - matcher = c2.matcher(city); - if (matcher.matches()) { - String x = matcher.group(1); - if (x.equals("襄樊")) { - x = "襄阳"; - } - return Tuples.of(x, 1); - } - matcher = c3.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 3); - } - matcher = c4.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 4); - } - matcher = c5.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 5); - } - matcher = c6.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 6); - } - matcher = c7.matcher(city); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 7); - } - return Tuples.of(city, -1); - } - - - public static Tuple2 shortDistrictImp(String district) { - // 总数 2963 56个内蒙八旗和新疆兵团没有处理 - Matcher matcher = d0.matcher(district); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 0); - } - matcher = d1.matcher(district); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 1); - } - matcher = d2.matcher(district); - if (matcher.matches()) { - return Tuples.of(replaceNations(matcher.group(1)), 2); - } - matcher = d3.matcher(district); - if (matcher.matches()) { - return Tuples.of(replaceNations(matcher.group(1)), 3); - } - matcher = d4.matcher(district); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 4); - } - matcher = d5.matcher(district); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 5); - } - return Tuples.of(district, -1); - } - - - public static Tuple2 shortStreetImp(String street) { - // 总数 42387 - // 柘城县邵园乡人民政府, 保安镇, 鹅湖镇人民政府, 东风地区 - Matcher matcher = s0.matcher(street); - if (matcher.matches()) { - return Tuples.of(matcher.group(1), 0); - } - - matcher = s1.matcher(street); - if (matcher.matches()) { - return Tuples.of(replaceNationsNotEmpty(matcher.group(1)), 1); - } - - - matcher = s2.matcher(street); - if (matcher.matches()) { - return Tuples.of(replaceNationsNotEmpty(matcher.group(1)), 1); - } - - return Tuples.of(street, -1); - } - - - public static String replaceNations(String ncity) { - for (String y : NATIONS) { - ncity = ncity.replace(y, ""); - if (y.length() > 2) { - String replace = y.replaceAll("族", ""); - ncity = ncity.replace(replace, ""); - } - } - return ncity; - } - - - public static String replaceNationsNotEmpty(String name) { - String x = name; - for (String y : NATIONS) { - String x2 = x.replace(y, ""); - if (y.length() > 2) { - String replace = y.replaceAll("族", ""); - x2 = x2.replace(replace, ""); - } - x = x2.isEmpty() ? x : x2; - } - return x; - } - - public static String shortCity(String city) { - return shortCityImp(city).get0(); - } - - public static String shortDistrict(String district) { - return shortDistrictImp(district).get0(); - } - - public static String shortStreet(String street) { - return shortStreetImp(street).get0(); - } - - // def shortAdmin(name: String, level: DistrictLevel): String = { - // level match { - // case DistrictLevel.Province => AdminUtils1.shortProvince(name) - // case DistrictLevel.City => AdminUtils1.shortCity(name) - // case DistrictLevel.District => AdminUtils1.shortDistrict(name) - // case DistrictLevel.Street => AdminUtils1.shortStreet(name) - // case _ => name - // } - // } - - //def shortAdmin(name: String, level: DistrictLevel) - public static String shortAdmin(String name, DistrictLevel level) { -// if (DistrictLevel.Province().equals(level)) { -// return shortProvince(name); -// } else if (DistrictLevel.City().equals(level)) { -// return shortCity(name); -// } else if (DistrictLevel.District().equals(level)) { -// return shortDistrict(name); -// } else if (DistrictLevel.Street().equals(level)) { -// return shortStreet(name); -// } - return name; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/GeoUtils.java b/src/main/java/com/dengxq/lnglat2Geo/utils/GeoUtils.java deleted file mode 100644 index 27f4cfb..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/GeoUtils.java +++ /dev/null @@ -1,208 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - -import com.dengxq.lnglat2Geo.entity.enums.Azimuth; -import com.dengxq.lnglat2Geo.entity.Bound; -import com.dengxq.lnglat2Geo.entity.enums.CoordinateSystem; -import com.dengxq.lnglat2Geo.entity.Location; -import com.google.common.geometry.S2; -import com.google.common.geometry.S2Cap; -import com.google.common.geometry.S2LatLng; -import com.speedment.common.tuple.Tuple2; -import com.speedment.common.tuple.Tuples; - -import java.math.BigDecimal; -import java.math.RoundingMode; - -public class GeoUtils { - private static final double x_PI = Math.PI * 3000.0 / 180.0; - private static final double EE = 0.00669342162296594323; - private static final double A = 6378245.0; // BJZ54坐标系地球长半轴, m - public static double EQUATOR_C = 20037508.3427892; // 赤道周长, m - public static double EARTH_RADIUS = 6378137.0;//WGS84, CGCS2000坐标系地球长半轴, m - public static double EARTH_POLAR_RADIUS = 6356725.0; //极半径, m - - public static double SQRT2 = 1.414213562; - - public static double rad(Double d) { - return d * Math.PI / 180.0; - } - - public static Double distance(Location locA, Location locB) { - Double lngA = locA.lng; - Double latA = locA.lat; - Double lngB = locB.lng; - Double latB = locB.lat; - double f = rad((latA + latB) / 2); - double g = rad((latA - latB) / 2); - double l = rad((lngA - lngB) / 2); - if (g == 0 && l == 0) - return 0D; - double sg = Math.sin(g), sl = Math.sin(l), sf = Math.sin(f); - - double s, c, w, r, d, h1, h2, dis, a = EARTH_RADIUS, fl = 1 / 298.257; - sg = sg * sg; - sl = sl * sl; - sf = sf * sf; - s = sg * (1 - sl) + (1 - sf) * sl; - c = (1 - sg) * (1 - sl) + sf * sl; - w = Math.atan(Math.sqrt(s / c)); - r = Math.sqrt(s * c) / w; - d = 2 * w * a; - h1 = (3 * r - 1) / 2 / c; - h2 = (3 * r + 1) / 2 / s; - dis = d * (1 + fl * (h1 * sf * (1 - sg) - h2 * (1 - sf) * sg)); - - //return dis.formatted("%.2f").toDouble - return new BigDecimal(dis).setScale(2, RoundingMode.HALF_UP).doubleValue(); - - } - - - public static Tuple2 gcj02ToWgs84(Double lng, Double lat) { - if (outOfChina(lng, lat)) - return Tuples.of(lng, lat); - double dlat = transformLat(lng - 105.0, lat - 35.0); - double dlng = transformLng(lng - 105.0, lat - 35.0); - double radlat = lat / 180.0 * Math.PI; - double magic = Math.sin(radlat); - magic = 1 - EE * magic * magic; - double sqrtmagic = Math.sqrt(magic); - dlat = (dlat * 180.0) / ((A * (1 - EE)) / (magic * sqrtmagic) * Math.PI); - dlng = (dlng * 180.0) / (A / sqrtmagic * Math.cos(radlat) * Math.PI); - double mglat = lat + dlat; - double mglng = lng + dlng; - return Tuples.of(lng * 2 - mglng, lat * 2 - mglat); - } - - - public static Tuple2 gcj02ToBD09(Double lng, Double lat) { - if (outOfChina(lng, lat)) return Tuples.of(lng, lat); - double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI); - double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI); - double bd_lng = z * Math.cos(theta) + 0.0065; - double bd_lat = z * Math.sin(theta) + 0.006; - return Tuples.of(bd_lng, bd_lat); - } - - public static Tuple2 bd09ToGCJ02(Double lng, Double lat) { - if (outOfChina(lng, lat)) return Tuples.of(lng, lat); - double x = lng - 0.0065; - double y = lat - 0.006; - double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_PI); - double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_PI); - double gg_lng = z * Math.cos(theta); - double gg_lat = z * Math.sin(theta); - return Tuples.of(gg_lng, gg_lat); - } - - public static Tuple2 wgs84ToGCj02(Double lng, Double lat) { - double mglat, mglng; - if (outOfChina(lng, lat)) { - mglat = lat; - mglng = lng; - } else { - double dLat = transformLat(lng - 105.0, lat - 35.0); - double dLon = transformLng(lng - 105.0, lat - 35.0); - double radLat = lat / 180.0 * Math.PI; - double magic = Math.sin(radLat); - magic = 1 - EE * magic * magic; - double sqrtMagic = Math.sqrt(magic); - dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * Math.PI); - dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * Math.PI); - mglat = lat + dLat; - mglng = lng + dLon; - } - return Tuples.of(mglng, mglat); - } - - - private static Double transformLng(Double lng, Double lat) { - double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); - ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0; - ret += (20.0 * Math.sin(lng * Math.PI) + 40.0 * Math.sin(lng / 3.0 * Math.PI)) * 2.0 / 3.0; - ret += (150.0 * Math.sin(lng / 12.0 * Math.PI) + 300.0 * Math.sin(lng / 30.0 * Math.PI)) * 2.0 / 3.0; - return ret; - } - - private static Double transformLat(Double lng, Double lat) { - double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); - ret += (20.0 * Math.sin(6.0 * lng * Math.PI) + 20.0 * Math.sin(2.0 * lng * Math.PI)) * 2.0 / 3.0; - ret += (20.0 * Math.sin(lat * Math.PI) + 40.0 * Math.sin(lat / 3.0 * Math.PI)) * 2.0 / 3.0; - ret += (160.0 * Math.sin(lat / 12.0 * Math.PI) + 320 * Math.sin(lat * Math.PI / 30.0)) * 2.0 / 3.0; - return ret; - } - - - public static boolean isInChina(Double lng, Double lat) { - return !outOfChina(lng, lat); - } - - public static boolean outOfChina(Double lng, Double lat) { - return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271; - } - - private static final double kEarthCircumferenceMeters = 1000 * 40075.017; - - private static double earthMeters2Radians(Double meters) { - return (2 * S2.M_PI) * (meters / kEarthCircumferenceMeters); - } - - public static S2Cap genS2Cap(Location loc, Double radius) { - S2LatLng s2LatLng = S2LatLng.fromDegrees(loc.lat, loc.lng); - double radiusRadians = earthMeters2Radians(radius); - return S2Cap.fromAxisHeight(s2LatLng.normalized().toPoint(), (radiusRadians * radiusRadians) / 2); - } - - public static Tuple2 toGCJ02(Double lng, Double lat, CoordinateSystem coordType) { - switch (coordType) { - case WGS84: - return GeoUtils.wgs84ToGCj02(lng, lat); - case BD09: - return GeoUtils.bd09ToGCJ02(lng, lat); - default: - return Tuples.of(lng, lat); - } - } - - public static Tuple2 toWGS84(Double lng, Double lat, CoordinateSystem coordType) { - switch (coordType) { - case GCJ02: - return GeoUtils.gcj02ToWgs84(lng, lat); - case BD09: - Tuple2 d02 = GeoUtils.bd09ToGCJ02(lng, lat); - return GeoUtils.gcj02ToWgs84(d02.get0(), d02.get1()); - default: - return Tuples.of(lng, lat); - } - } - - public static Location move(Location loc, Double distance, Azimuth azimuth) { - double radLat = GeoUtils.rad(loc.lat); - double radLng = GeoUtils.rad(loc.lng); - - double ec = GeoUtils.EARTH_POLAR_RADIUS + (GeoUtils.EARTH_RADIUS - GeoUtils.EARTH_POLAR_RADIUS) * (90 - loc.lng) / 90; - double ed = ec * Math.cos(radLat); - - double dx = distance * Math.sin(azimuth.getCode() * Math.PI / 180); - double dy = distance * Math.cos(azimuth.getCode() * Math.PI / 180); - - double lng = (dx / ed + radLng) * 180 / Math.PI; - double lat = (dy / ec + radLat) * 180 / Math.PI; - - return new Location(lng, lat); - } - - public static Bound genCapBound(Location loc, Double radius) { - double swDistance = GeoUtils.SQRT2 * radius; - Location sw = move(loc, swDistance, Azimuth.SouthWest); - Location ne = move(loc, swDistance, Azimuth.NorthEast); - return new Bound(sw, ne); - } - - public static Bound genCapInnerBound(Location loc, Double radius) { - // val swDistance = SQRT2 / 2d * radius - Location sw = move(loc, radius, Azimuth.SouthWest); - Location ne = move(loc, radius, Azimuth.NorthEast); - return new Bound(sw, ne); - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/LineUtils.java b/src/main/java/com/dengxq/lnglat2Geo/utils/LineUtils.java deleted file mode 100644 index 1235f86..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/LineUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - -public class LineUtils { - public static Double lineDis(Double x1, Double y1, Double x2, Double y2) { - return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); - } - - public static Double pointToLineDis(Double x1, Double y1, Double x2, Double y2, Double x0, Double y0) { - double - a = lineDis(x1, y1, x2, y2),// 线段的长度 - b = lineDis(x1, y1, x0, y0),// 点到起点的距离 - c = lineDis(x2, y2, x0, y0);// 点到终点的距离 - //点在端点上 - if (c <= 0.000001 || b <= 0.000001) { - return 0D; - } - //直线距离过短 - if (a <= 0.000001) { - return b; - } - // 点在起点左侧,距离等于点到起点距离 - if (c * c >= a * a + b * b) { - return b; - } - //点在终点右侧,距离等于点到终点距离 - if (b * b >= a * a + c * c) { - return c; - } - //点在起点和终点中间,为垂线距离 - double k = (y2 - y1) / (x2 - x1); - double z = y1 - k * x1; - double p = (a + b + c) / 2; - // 半周长 - double s = Math.sqrt(p * (p - a) * (p - b) * (p - c));//海伦公式求面积 - return 2 * s / a;// 返回点到线的距离(利用三角形面积公式求高) - - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/Md5Util.java b/src/main/java/com/dengxq/lnglat2Geo/utils/Md5Util.java deleted file mode 100644 index 94c11fc..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/Md5Util.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; - -public class Md5Util { - private static final char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - - - public static String md5(String input) { - return getHashWithInputStream(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); - } - - public static String getHashWithInputStream(InputStream inputStream) { - try { - byte[] buffer = new byte[1024]; - MessageDigest md5 = MessageDigest.getInstance("MD5"); - - int numRead; - while ((numRead = inputStream.read(buffer)) > 0) { - md5.update(buffer, 0, numRead); - } - - inputStream.close(); - return toHexString(md5.digest()); - } catch (Exception var4) { - throw new IllegalStateException(var4); - } - } - - public static String toHexString(byte[] b) { - StringBuilder sb = new StringBuilder(b.length * 2); - - for (byte b1 : b) { - sb.append(hexChar[(b1 & 240) >>> 4]); - sb.append(hexChar[b1 & 15]); - } - - return sb.toString(); - } - - public static byte[] hexToByteArray(String inHex) { - int hexlen = inHex.length(); - byte[] result; - if (hexlen % 2 == 1) { - //奇数 - hexlen++; - result = new byte[(hexlen / 2)]; - inHex = "0" + inHex; - } else { - //偶数 - result = new byte[(hexlen / 2)]; - } - int j = 0; - for (int i = 0; i < hexlen; i += 2) { - result[j] = hexToByte(inHex.substring(i, i + 2)); - j++; - } - return result; - } - - public static byte hexToByte(String inHex) { - return (byte) Integer.parseInt(inHex, 16); - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/S2Utils.java b/src/main/java/com/dengxq/lnglat2Geo/utils/S2Utils.java deleted file mode 100644 index d4ec23b..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/S2Utils.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - -import com.google.common.geometry.*; - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class S2Utils { - - // // 预算,提升速度 - public static Map capHeightMap = new HashMap() {{ - for (int kRadius : Arrays.asList(2, 4, 8, 16, 32, 64, 128, 256)) { - double meterRadius = kRadius * 1000D; - put((int) meterRadius, capHeight(meterRadius)); - } - }}; - - - private static double capHeight(double radius) { - double rad = earthMeters2Radians(radius); - return rad * rad * 2; - } - - public static double earthMeters2Radians(Double meters) { - return (2 * S2.M_PI) * (meters / 40075017); - } - - public static double getCapHeight(int radius) { - double capHeight = capHeightMap.getOrDefault(radius, 0d); - if (capHeight == 0d) { - capHeight = capHeight(radius); - } - return capHeight; - } - public static List getCellId(S2LatLng s2LatLng, int radius, int desLevel) { - double capHeight = getCapHeight(radius); - S2Cap cap = S2Cap.fromAxisHeight(s2LatLng.toPoint(), capHeight); - S2RegionCoverer coverer = new S2RegionCoverer(); - coverer.setMaxLevel(desLevel); - coverer.setMinLevel(desLevel); - //圆形内的cell会自动做聚合,手动拆分 - return coverer.getCovering(cap) - .cellIds() - .stream() - .flatMap((Function>) s2CellId -> { - int cellLevel = getLevel(s2CellId.id()); - if (cellLevel == desLevel) { - return Stream.of(s2CellId.id()); - } else { - return childrenCellId(s2CellId, cellLevel, desLevel).stream().map(S2CellId::id); - } - }).collect(Collectors.toList()); - } - - - public static int getLevel(Long inputs) { - int n = 0; - long input = inputs; - while (input % 2 == 0) { - input = input / 2; - n += 1; - } - return 30 - n / 2; - } - - public static List childrenCellId(S2CellId s2CellId, int curLevel, int desLevel) { - List list = new ArrayList<>(); - if (curLevel < desLevel) { - long interval = (s2CellId.childEnd().id() - s2CellId.childBegin().id()) / 4; - for (int i = 0; i < 4; i++) { - long id = s2CellId.childBegin().id() + interval * i; - S2CellId cellId = new S2CellId(id); - list.addAll(childrenCellId(cellId, curLevel + 1, desLevel)); - } - } else { - list.add(s2CellId); - } - return list; - } -} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/Utils.java b/src/main/java/com/dengxq/lnglat2Geo/utils/Utils.java deleted file mode 100644 index 4fcfaac..0000000 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/Utils.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.dengxq.lnglat2Geo.utils; - -import com.dengxq.lnglat2Geo.GeoTransImpl; -import com.dengxq.lnglat2Geo.entity.AdminBoundary; -import com.dengxq.lnglat2Geo.entity.CellAdmin; -import com.google.common.geometry.S2CellId; -import com.speedment.common.tuple.Tuple2; -import com.speedment.common.tuple.Tuple3; -import com.speedment.common.tuple.Tuple4; -import com.speedment.common.tuple.Tuples; - - -import java.util.*; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class Utils { - - public static Map buildBoundaryAdminCell(List data) { - return data.stream().collect(Collectors.toMap(s -> s.cellId, s -> s.adCode)); - } - - public static Map> buildBoundaryIndex(Map>> boundaryData) { - Map>> map = boundaryData.keySet().stream().map(s -> Tuples.of( - new S2CellId(s).parent(GeoTransImpl.min_level).id(), s - )).collect(Collectors.groupingBy(Tuple2::get0)); - Map> result = new HashMap<>(); - for (Map.Entry>> entry : map.entrySet()) { - result.put(entry.getKey(), entry.getValue().stream().map(Tuple2::get1).collect(Collectors.toList())); - } - return result; - } - - public static Map>> parseBoundaryData(List adminBoundaryInJavas) { - Map>> map = adminBoundaryInJavas - .stream() - .flatMap((Function>>) - adminBoundary -> adminBoundary.boundary.stream().flatMap( - (Function, Stream>>) - input -> { - List> ret = new ArrayList<>(); - for (int i = 0; i <= input.size() - 2; i++) { - ret.add(Tuples.of(input.get(i), input.get(i + 1), adminBoundary.code, true)); - ret.add(Tuples.of(input.get(i + 1), input.get(i), adminBoundary.code, false)); - } - return ret.stream(); - } - )) - .collect(Collectors.groupingBy(Tuple4::get0)); - - Map>> ret = new HashMap<>(); - - for (Map.Entry>> entry : map.entrySet()) { - List> value = entry.getValue(); - - Map>> sssMap = value.stream().map(longLongIntegerBooleanTuple4 -> Tuples.of(longLongIntegerBooleanTuple4.get1(), longLongIntegerBooleanTuple4.get2(), longLongIntegerBooleanTuple4.get3())).collect(Collectors.groupingBy(new Function, Long>() { - @Override - public Long apply(Tuple3 longIntegerBooleanTuple3) { - return longIntegerBooleanTuple3.get0(); - } - })); - - List> ret1 = new ArrayList<>(); - ret.put(entry.getKey(), ret1); - for (Map.Entry>> sss : sssMap.entrySet()) { - List> list = sss.getValue().stream().map(longIntegerBooleanTuple3 -> - Tuples.of(longIntegerBooleanTuple3.get1(), longIntegerBooleanTuple3.get2())).sorted((o1, o2) -> o2.get1().compareTo(o1.get1())).collect(Collectors.toList()); - - if (list.size() > 2) { - throw new RuntimeException(); - } - if (list.size() == 2) { - if (!list.get(0).get1() || list.get(1).get1()) - throw new RuntimeException(); - Integer first = list.get(0).get0(); - Integer second = list.get(1).get0(); - ret1.add(Tuples.of(sss.getKey(), first, second)); - } else { - if (list.get(0).get1()) { - ret1.add(Tuples.of(sss.getKey(), list.get(0).get0(), -1)); - } else { - ret1.add(Tuples.of(sss.getKey(), -1, list.get(0).get0())); - } - } - } - } - return ret; - - } - -} diff --git a/src/main/java/com/virjar/geolib/GeoBinBuilder.java b/src/main/java/com/virjar/geolib/GeoBinBuilder.java new file mode 100644 index 0000000..fae7707 --- /dev/null +++ b/src/main/java/com/virjar/geolib/GeoBinBuilder.java @@ -0,0 +1,244 @@ +package com.virjar.geolib; + +import com.virjar.geolib.bean.Admin; +import com.virjar.geolib.bean.Boundary; +import com.virjar.geolib.bean.BoundaryLine; +import com.virjar.geolib.core.GeoDb; +import com.virjar.geolib.core.GeoUtils; +import com.virjar.geolib.core.Leb128; +import com.google.common.geometry.*; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 构建geo数据库文件,考虑geo数据库文件大小,正常情况下将geo数据加载到内存会有很大的冷启动时间,所以这里我会将geo数据编译成二进制格式, + * 并且在文件中构建内存映射格式,这样可以通过直接打开文件的方式加载格式化数据,并且绕过数据加载的解析过程, + * 当然这种方案的坏处就是我们对于数据的操作就会比较有难度了 + */ +public class GeoBinBuilder { + private static final int M = (1 << 20); + + private volatile boolean commit = false; + private final Map adminData = new HashMap<>(); + private final Map boundaryData = new HashMap<>(); + + /** + * 黑河-腾冲线 + */ + public static final long HEI_HE = S2CellId.fromLatLng(S2LatLng.fromDegrees(50.250535, 127.838467)).parent(GeoDb.S2_LEVEL).id(); + public static final long TEN_CONG = S2CellId.fromLatLng(S2LatLng.fromDegrees(24.871457, 98.310169)).parent(GeoDb.S2_LEVEL).id(); + + + public GeoBinBuilder addAdmin(Admin admin) { + if (commit) { + throw new IllegalStateException("can not add data after commit"); + } + adminData.put(admin.getId(), admin); + return this; + } + + public void addAdminBoundary(int adminCode, List pylon) { + if (commit) { + throw new IllegalStateException("can not add data after commit"); + } + + Boundary boundary = boundaryData.computeIfAbsent(adminCode, Boundary::new); + // transform cell level as 12 + List pylonWith12 = new ArrayList<>(); + Set duplicateRemove = new HashSet<>(); + for (Long point : pylon) { + Long newId = new S2CellId(point).parent(GeoDb.S2_LEVEL).id(); + if (duplicateRemove.contains(newId)) { + continue; + } + duplicateRemove.add(newId); + pylonWith12.add(newId); + } + if (!Objects.equals(pylonWith12.get(0), pylonWith12.get(pylonWith12.size() - 1))) { + // make sure the pylon close + pylonWith12.add(pylonWith12.get(0)); + } + boundary.getPylons().add(pylonWith12); + } + + public byte[] build() { + commit = true; + // 总的多边形顶点数量:141w,则线段数量越为141w + // 数据大小预估为: 141w * 20(bit) / 0.75(factor) = 180M + // 线段有两个顶点,本质上顶点数据存在两倍冗余,故数据包压缩可以带来大约50%的压缩率,压缩后大小为86M + ByteBuffer buffer = ByteBuffer.allocate(180 * M); + buffer.putLong(GeoDb.magic);// magic + buffer.putInt(0);// boundary offset + buffer.putInt(0);// hashItemSize + writeAdminNodes(adminData.values(), buffer); + log("build pylon data"); + PylonBundle pylonBundle = buildBoundaryLineList(); + + // 对于一些很小或者很大的地方,创建快速查询缓存 + // 1. 对于很小的地方(约2k个),多边形平均精度小于2km,导致无法在level 12构建合法多边形,此时直接使用s2点集合索引 + // 2. 对于很大的地方(约2.2k个),如新疆、西藏,由于多边形太大,则探测一个较大范围的level 12集合范围,此时将会比较影响查询效率,但是我们可以完全使用更高级别的s2(level 9)表达的区块, + // 直接使用s2点集合索引(高级别level区块可以表达更大范围空间,故其总量是较少的、可靠的) + writeCacheS2Table(pylonBundle.quickCachePoints, buffer); + int boundaryOffset = buffer.position(); + + + log("write pylon into hashtable start"); + int hashItemSize = writeBoundaryHashTable(buffer, pylonBundle.boundaryLines); + log("write pylon into hashtable finish"); + int endOffset = buffer.position(); + + buffer.position(8); + buffer.putInt(boundaryOffset); + buffer.putInt(hashItemSize); + buffer.position(endOffset); + + + buffer.flip(); + byte[] bytes = new byte[buffer.limit() - buffer.position()]; + buffer.get(bytes); + return bytes; + } + + + private void writeCacheS2Table(Map cache, ByteBuffer buffer) { + Leb128.writeUnsignedLeb128(buffer, cache.size()); + cache.forEach((s2Id, adminCode) -> { + buffer.putLong(s2Id); + buffer.putInt(adminCode); + }); + } + + /** + * 使用数组hash存储所有多边形线段,数组hash具有线性映射特性,所以我们可以直接打开文件把hash表释放到内存中, + * 这样可以节省项目启动大量多边形数据加载时间 + */ + private int writeBoundaryHashTable(ByteBuffer buffer, List boundaryLines) { + // 按照点位排序一下,当然实际上也可以不用排序的,排序之后让hash表范围更加稳定,可以方便二进制观察数据 + boundaryLines.sort(Comparator.comparingLong(o -> o.start)); + + // 每个条目组成,两个点位16字节,code 4个字节 + + // java的哈希惯例,使用0.75作为荷载因子,然而0.75已经会触发hash resize了,此时实际空间大小将会变成实际条目大小的 1.33倍(1/0.75 = 1.33) + // 0.75 最高冲突检测250次, 0.70 最高冲突检测次数:159 0.65 最高冲突检测:142 0.60 最高冲突检测次数:84 + // 荷载因子越大,则越节省空间;但是会导致哈西冲突变高; + int cab = (int) (boundaryLines.size() / 0.60); + int hashTableSize = cab * GeoDb.itemSize; + byte[] bytes = new byte[hashTableSize]; + ByteBuffer boundaryTable = ByteBuffer.wrap(bytes); + + // AtomicInteger maxHashCount = new AtomicInteger(0); + boundaryLines.forEach(boundaryLine -> { + int startIndex = (int) (GeoDb.linePointHash(boundaryLine.start) % cab); + for (int i = 0; i < cab; i++) { + int index = (startIndex + i) % cab; + if (boundaryTable.getLong(index * GeoDb.itemSize) != 0) { + // 已经有值了 + continue; + } + //if (i > maxHashCount.get()) { + // maxHashCount.set(i); + //} + //System.out.println("write hashTable for point: " + JSONObject.toJSONString(boundaryLine) + " hashCount:" + i); + boundaryTable.position(index * GeoDb.itemSize); + boundaryTable.putLong(boundaryLine.start); + boundaryTable.putLong(boundaryLine.end); + boundaryTable.putInt(boundaryLine.adminCode); + break; + } + }); + + //System.out.println("max hashCount: " + maxHashCount.get()); + buffer.put(bytes); + return cab; + } + + + private PylonBundle buildBoundaryLineList() { + PylonBundle pylonBundle = new PylonBundle(); + boundaryData.values().forEach(boundary -> { + int adminCode = boundary.getCode(); + + boundary.getPylons().forEach(pylon -> { + // 每个list就是一个多边形pylon,对于单个行政区域可能由多个多边形组成 + // 检查多边形的线段组成方向,设定逆时针位合法方向,如此使用行列式计算多边形面积则面积为正数 + // 请注意多边形方向是非常重要的,因为我们最终是依靠目标点位到多边形任意线段组成的三角形的面积来判定点位是否处于多边形内部,进而判定点位的行政区域 + double area = GeoUtils.area(pylon); + if (area == 0) { + // 如果面积为0,那说明多边形不构成图形,就是单纯的点/线段,出现这个问题的原因是2km的精度下多边形顶点会被重合 + // 此时说明这是一个很小的图形,此时直接存储到索引中就可以 + pylon.forEach(aLong -> pylonBundle.quickCachePoints.put(aLong, adminCode)); + return; + } + if (area < 0) { + // 如若判定面积为负数,则需要对点位集合进行逆序 + Collections.reverse(pylon); + } + + for (int i = 0; i < pylon.size() - 1; i++) { + pylonBundle.boundaryLines.add(new BoundaryLine(adminCode, pylon.get(i), pylon.get(i + 1))); + } + + + // 当多边形在黑河-腾冲线左边,则可能是地广人稀的区域,此时生成快速索引表,这个过程非常耗时(约2分钟),但是还好我们是提前预计算完成的 + List heartMapLineTriangle = new ArrayList<>(); + heartMapLineTriangle.add(HEI_HE); + heartMapLineTriangle.add(TEN_CONG); + heartMapLineTriangle.add(pylon.get(0)); + heartMapLineTriangle.add(HEI_HE);// 注意,这是三角形,但是需要4个点才能描述闭合动作(即首尾相同),同时数据结构方便了三角形行列式计算(避免执行环形数组回看) + + if (GeoUtils.area(heartMapLineTriangle) < 0) { + S2Polygon s2Polygon = new S2Polygon(new S2Loop(pylon.stream().map(aLong -> new S2CellId(aLong).toPoint()).collect(Collectors.toList()))); + + S2RegionCoverer s2RegionCoverer = new S2RegionCoverer(); + s2RegionCoverer.setMinLevel(GeoDb.S2_QUICK_CELL_START_LEVEL); + s2RegionCoverer.setMaxLevel(GeoDb.S2_QUICK_CELL_END_LEVEL); + s2RegionCoverer.getInteriorCovering(s2Polygon).forEach(s2CellId -> + pylonBundle.quickCachePoints.put(s2CellId.id(), adminCode) + ); + } + }); + }); + return pylonBundle; + } + + + private void writeAdminNodes(Collection adminNodes, ByteBuffer buffer) { + Leb128.writeUnsignedLeb128(buffer, adminNodes.size()); + for (Admin adminNode : adminNodes) { + Leb128.writeUnsignedLeb128(buffer, adminNode.getId()); + Leb128.writeUnsignedLeb128(buffer, adminNode.getParentId()); + writeString(buffer, adminNode.getName()); + writeString(buffer, adminNode.getShortName()); + buffer.putFloat(adminNode.getCenterLng()); + buffer.putFloat(adminNode.getCenterLat()); + buffer.put((byte) adminNode.getLevel().ordinal()); + } + } + + + private static void writeString(ByteBuffer buffer, String str) { + if (str == null) { + str = ""; + } + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + Leb128.writeUnsignedLeb128(buffer, bytes.length); + if (bytes.length > 0) { + buffer.put(bytes); + } + } + + private static class PylonBundle { + private final List boundaryLines = new ArrayList<>(); + private final Map quickCachePoints = new HashMap<>(); + } + + public static void log(String msg) { + System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + ":" + msg); + } + +} diff --git a/src/main/java/com/virjar/geolib/GeoTrans.java b/src/main/java/com/virjar/geolib/GeoTrans.java new file mode 100644 index 0000000..684cdcc --- /dev/null +++ b/src/main/java/com/virjar/geolib/GeoTrans.java @@ -0,0 +1,332 @@ +package com.virjar.geolib; + +import com.google.common.geometry.S2CellId; +import com.google.common.geometry.S2LatLng; +import com.virjar.geolib.bean.Admin; +import com.virjar.geolib.bean.BoundaryLine; +import com.virjar.geolib.bean.GeoAdmin; +import com.virjar.geolib.bean.Location; +import com.virjar.geolib.core.AdminNormalizer; +import com.virjar.geolib.core.GeoDb; +import com.virjar.geolib.core.GeoUtils; +import lombok.SneakyThrows; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class GeoTrans { + private static final String DB_RES_KEY = "geo-vj00.bin"; + private static final String DB_RES_ZIP_KEY = "geo-vj00.bin.zip"; + private static final String ADMIN_ADDRESS_CONFIG = "admin_address.properties"; + private static final GeoDb geoDb = openGeoDb(); + private static final AdminNormalizer adminNormalizer = new AdminNormalizer(geoDb); + + /** + * 中文名称-行政区域对应表 + */ + private static final Map normalizeNames = loadAddressData(); + + + /** + * 给定任意一个行政编码,返回格式化的行政区域定义 + * + * @param code 行政编码 + * @return 行政区域定义 + */ + public static Admin getAdmin(int code) { + return geoDb.getAdmin(code); + } + + public static List allAdminList() { + return geoDb.allAdminList(); + } + + /** + * 给定任意一个地址描述,返回最匹配这个地址的行政区域定义,请注意对于某些重名地址,转换过程可能存在歧义, + * 但你可以给定更加详细的地址描述,从而达到更加精确的匹配 + * + * @param address 地址 + * @return 行政区域定义 + */ + public static Admin normalizeName(String address) { + if (address == null || address.trim().isEmpty()) { + return null; + } + String key = GeoUtils.toHexString(address.getBytes(StandardCharsets.UTF_8)); + if (normalizeNames.containsKey(key)) { + Integer adminCode = normalizeNames.get(key); + if (adminCode == null) { + return null; + } + return getAdmin(adminCode); + } + + // 没有缓存记录,那么扫描整个admin库,再寻找最优匹配 + int adminCode = adminNormalizer.doNormalize(address); + + normalizeNames.put(key, adminCode); + storeAddressData(normalizeNames); + return getAdmin(adminCode); + } + + /** + * 给定某个行政节点,查询他的下一级节点列表 + * + * @param code 行政编码 + * @return 下级新政列表 + */ + public static Collection getChildren(int code) { + return geoDb.getChildren(code); + } + + /** + * 给定经纬度,计算行政详情(同时包含省市区) + * + * @param lng 经度 + * @param lat 纬度 + */ + public static GeoAdmin resolveGeoAdmin(double lng, double lat) { + int adminCode = resolveAdminCode(lng, lat); + if (adminCode <= 0) { + return GeoAdmin.defaultOverSea; + } + + Admin admin = getAdmin(adminCode); + if (admin == null) { + return GeoAdmin.defaultOverSea; + } + + Admin province = null, city = null, district = null; + do { + switch (admin.getLevel()) { + case District: + district = admin; + break; + case City: + city = admin; + break; + case Province: + province = admin; + break; + } + admin = getAdmin(admin.getParentId()); + } while (admin != null); + if (province == null && city == null && district == null) { + return GeoAdmin.defaultOverSea; + } + Admin maxDeep = district != null ? district : (city != null ? city : province); + return new GeoAdmin(province == null ? "未知" : province.getName(), + city == null ? "未知" : city.getName(), + district == null ? "未知" : district.getName(), + province == null ? -1 : province.getId(), + city == null ? -1 : city.getId(), + district == null ? -1 : district.getId(), + maxDeep.getCenterLng(), maxDeep.getCenterLat()); + } + + + /** + * 给定某个特定的经纬度,计算他的行政区域节点(区县级别) + * + * @param lng 经度 + * @param lat 纬度 + * @return 行政编码,如在海外则返回-1 + */ + public static int resolveAdminCode(double lng, double lat) { + if (GeoUtils.outOfChina(lng, lat)) { + return -1; + } + S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lng); + long s2Id = S2CellId.fromLatLng(s2LatLng).parent(GeoDb.S2_LEVEL).id(); + if (geoDb.quickHint(s2Id) != null) { + return geoDb.quickHint(s2Id); + } + for (int level = GeoDb.S2_QUICK_CELL_START_LEVEL; level <= GeoDb.S2_QUICK_CELL_END_LEVEL; level++) { + long testS2Id = S2CellId.fromLatLng(s2LatLng).parent(level).id(); + Integer code = geoDb.quickHint(testS2Id); + if (code != null) { + return code; + } + } + + //step1 使圆环覆盖线段库,圆环按2倍扩容,直到超过200000 + Set lines = new HashSet<>(); + //必须大于2000m,否则会出现格子半径过小选择错误问题 + int maxRadius = 2000; + while (lines.isEmpty() && maxRadius < 200000) { + lines = GeoUtils.cellIdsWithCapLevel(s2LatLng, maxRadius, GeoDb.S2_LEVEL) + .stream() + .flatMap((Function>) start -> + geoDb.queryLines(start).stream()) + .collect(Collectors.toSet()); + maxRadius = maxRadius * 2; + } + if (lines.isEmpty()) { + return -1; + } + + // step2,这些线段,求距离目标点位最近的一批线段 + double minDistance = Double.MAX_VALUE; + List minDistanceLines = new ArrayList<>(); + for (BoundaryLine boundaryLine : lines) { + S2LatLng start = new S2CellId(boundaryLine.start).toLatLng(); + S2LatLng end = new S2CellId(boundaryLine.end).toLatLng(); + double dis = GeoUtils.pointToLineDis(start.lngDegrees(), start.latDegrees(), end.lngDegrees(), end.latDegrees(), lng, lat); + if (dis > minDistance) { + continue; + } + if (dis < minDistance) { + minDistance = dis; + minDistanceLines.clear(); + } + minDistanceLines.add(boundaryLine); + } + + // step3,射线顶点问题,目标点在在射线的顶点侧,此时目标点到两条线段的最短距离都是射线顶点,然而实际上依然可以区分两条线段和目标点谁近谁远 + // + // case point(lng:135.11,lat: 43.86) + // line1(6796877348982489088 -> 6796874462764466176) : 正确值 -> 多边形外部,正确答案海外 + // line2(6796874462764466176 -> 6796880372639465472) : 需过滤 -> 如果不过滤,将会被判定为在多边形内部,解析结果到 黑龙江省鸡西市密山市 + minDistance = Double.MAX_VALUE; + List minDistanceLinesLevel1 = minDistanceLines; + minDistanceLines = new ArrayList<>(); + for (BoundaryLine boundaryLine : minDistanceLinesLevel1) { + S2LatLng start = new S2CellId(boundaryLine.start).toLatLng(); + S2LatLng end = new S2CellId(boundaryLine.end).toLatLng(); + // 根据点到线段中心点在做一次过滤,确定真实的最近点 + double dis = GeoUtils.lineDis((start.lngDegrees() + end.lngDegrees()) / 2, + (start.latDegrees() + end.latDegrees()) / 2, + lng, lat); + if (dis > minDistance) { + continue; + } + if (dis < minDistance) { + minDistance = dis; + minDistanceLines.clear(); + } + minDistanceLines.add(boundaryLine); + } + + + // step3,根据最近线段确认行政区域 + List inPylons = minDistanceLines.stream().filter(boundaryLine -> { + // 三点行列式计算面积大于0,则证明在多边形内部, + // 我们的行政区域多边形数据使用逆时针方向存储,此时如果我们的目标点(p3)在多边形内部,则(p1,p2,p3)组成三角形位逆向方向,行列式面积位为正数 + return GeoUtils.area(Arrays.asList(boundaryLine.start, boundaryLine.end, s2Id, boundaryLine.start)) >= 0; + // 面积为0,则证明在边界线上,那么当作有效判定 + }).collect(Collectors.toList()); + + if (!inPylons.isEmpty()) { + // todo 多个是否可能有多个结果命中,并且adminCode还不一致? + return inPylons.get(0).adminCode; + } + + if (minDistanceLines.size() == 1) { + // 国内海外边界 & 点位消重精度模糊导致的顶点回溯在线段上的问题 + // 关于回溯到线段上场景: a->b b->c d->d 其中c位于ab线段上,则会产生一个重合线段,此时最小距离线段搜寻可能命中bc,则bc将会让目标点命中多边形外部,则是错误结果 + // 请注意这个问题是多边形精度模糊处理导致的,如果是来自测绘局正常的多边形数据,不会出现这种奇怪多边形变种 + BoundaryLine boundaryLine = minDistanceLines.get(0); + S2LatLng start = new S2CellId(boundaryLine.start).toLatLng(); + S2LatLng end = new S2CellId(boundaryLine.end).toLatLng(); + + // 实际地球球面以米为单位的距离 + Double earthDistance = GeoUtils.distanceMeter( + new Location((start.lngDegrees() + end.lngDegrees()) / 2, (start.latDegrees() + end.latDegrees()) / 2), + s2Id + ); + if (earthDistance < 15000D) { + // 由于我们对于海外边界精准度要求并不高,所以至少距离边界少于15公里,那么当作国内看待 + // 因为实际看来在海边小岛,如果按中国大陆边界来判定的话,会存在解析到海外 + return boundaryLine.adminCode; + } + } + return -1; + } + + + private static GeoDb openGeoDb() { + File dbFile = new File(geoBaseDir(), DB_RES_KEY); + if (!dbFile.exists()) { + try (ZipInputStream zipInputStream = new ZipInputStream(openResource(DB_RES_ZIP_KEY))) { + ZipEntry nextEntry = zipInputStream.getNextEntry(); + if (nextEntry == null || !DB_RES_KEY.equals(nextEntry.getName())) { + throw new IllegalStateException("error geo db zip resource file: " + DB_RES_ZIP_KEY); + } + FileUtils.forceMkdirParent(dbFile); + try (OutputStream outputStream = Files.newOutputStream(dbFile.toPath())) { + IOUtils.copy(zipInputStream, outputStream); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + try { + return new GeoDb(dbFile); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static InputStream openResource(String name) { + InputStream resource = GeoTrans.class.getClassLoader() + .getResourceAsStream(name); + if (resource == null) { + throw new IllegalStateException("can not find resource: " + name); + } + return resource; + } + + @SneakyThrows + private static Map loadAddressData() { + ConcurrentHashMap ret = new ConcurrentHashMap<>(); + File file = new File(geoBaseDir(), ADMIN_ADDRESS_CONFIG); + if (!file.exists()) { + ret.putAll(GeoTrans.adminNormalizer.getFullNameMap()); + storeAddressData(ret); + return ret; + } + Properties properties = new Properties(); + properties.load(Files.newInputStream(file.toPath())); + for (String key : properties.stringPropertyNames()) { + String value = properties.getProperty(key, "-1"); + ret.put(key, Integer.parseInt(value)); + } + return ret; + } + + @SneakyThrows + public static void storeAddressData(Map normalizeNames) { + Properties properties = new Properties(); + normalizeNames.forEach((s, integer) -> properties.put(s, String.valueOf(integer))); + properties.store(Files.newOutputStream(new File(geoBaseDir(), ADMIN_ADDRESS_CONFIG).toPath()), "geo address config"); + } + + private static File geoBaseDir() { + String userHome = System.getProperty("user.home"); + File base; + if (userHome != null && !userHome.trim().isEmpty()) { + base = new File(userHome); + } else { + base = new File("."); + } + return new File(base, ".geo"); + } + + public static Long cellId(Double lng, Double lat) { + S2LatLng s2LatLng = S2LatLng.fromDegrees(lat, lng); + return S2CellId.fromLatLng(s2LatLng).parent(GeoDb.S2_LEVEL).id(); + } +} diff --git a/src/main/java/com/virjar/geolib/bean/Admin.java b/src/main/java/com/virjar/geolib/bean/Admin.java new file mode 100644 index 0000000..f5a0e9c --- /dev/null +++ b/src/main/java/com/virjar/geolib/bean/Admin.java @@ -0,0 +1,35 @@ +package com.virjar.geolib.bean; + +import lombok.Data; + +/** + * 行政区域定义 + */ +@Data +public class Admin implements Comparable { + private int id = -1; + private int parentId = -1; + private String name; + private String shortName; + private float centerLng; + private float centerLat; + private DistrictLevel level; + + @Override + public int compareTo(Admin o) { + return Integer.compare(id, o.id); + } + + public enum DistrictLevel { + Country("国家"), + Province("省,自治区"), + City("地级市"), + District("区,县,县级市"), + ; + public final String desc; + + DistrictLevel(String desc) { + this.desc = desc; + } + } +} diff --git a/src/main/java/com/virjar/geolib/bean/Boundary.java b/src/main/java/com/virjar/geolib/bean/Boundary.java new file mode 100644 index 0000000..01ba1a9 --- /dev/null +++ b/src/main/java/com/virjar/geolib/bean/Boundary.java @@ -0,0 +1,27 @@ +package com.virjar.geolib.bean; + +import lombok.Data; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/** + * 行政区域边界定义,由多个多边形组成一个区域范围,如描述四川省的经纬度范围 + */ +@Data +@RequiredArgsConstructor +public class Boundary { + /** + * 行政编码,如四川省 + */ + @NonNull + private Integer code; + + /** + * 多个多边形,请注意从标准上来说,一个行政区域可以是多个多边形,比如美国在太平洋有多个海岛 + * 当然在中国大陆不存在这个问题 + */ + private List> pylons = new ArrayList<>(); +} diff --git a/src/main/java/com/virjar/geolib/bean/BoundaryLine.java b/src/main/java/com/virjar/geolib/bean/BoundaryLine.java new file mode 100644 index 0000000..24f6660 --- /dev/null +++ b/src/main/java/com/virjar/geolib/bean/BoundaryLine.java @@ -0,0 +1,17 @@ +package com.virjar.geolib.bean; + +/** + * 行政区域的一条边界线段 + */ +public class BoundaryLine { + public final int adminCode; + // start - end 标记一个线段,他们都使用Google s2描述的一个经纬度点位 + public final long start; + public final long end; + + public BoundaryLine(int adminCode, long start, long end) { + this.adminCode = adminCode; + this.start = start; + this.end = end; + } +} \ No newline at end of file diff --git a/src/main/java/com/virjar/geolib/bean/GeoAdmin.java b/src/main/java/com/virjar/geolib/bean/GeoAdmin.java new file mode 100644 index 0000000..5dbe5f2 --- /dev/null +++ b/src/main/java/com/virjar/geolib/bean/GeoAdmin.java @@ -0,0 +1,25 @@ +package com.virjar.geolib.bean; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeoAdmin { + private String province; + private String city; + private String district; + + private int provinceCode; + private int cityCode; + private int districtCode; + + private float centerLng; + private float centerLat; + + + public static GeoAdmin defaultOverSea = new GeoAdmin( + "海外", "海外", "海外", + -1, -1, -1, + 0F, 0F); +} diff --git a/src/main/java/com/dengxq/lnglat2Geo/entity/Location.java b/src/main/java/com/virjar/geolib/bean/Location.java similarity index 65% rename from src/main/java/com/dengxq/lnglat2Geo/entity/Location.java rename to src/main/java/com/virjar/geolib/bean/Location.java index be3a783..e8e894b 100644 --- a/src/main/java/com/dengxq/lnglat2Geo/entity/Location.java +++ b/src/main/java/com/virjar/geolib/bean/Location.java @@ -1,4 +1,4 @@ -package com.dengxq.lnglat2Geo.entity; +package com.virjar.geolib.bean; import com.google.common.geometry.S2CellId; import com.google.common.geometry.S2LatLng; @@ -18,4 +18,11 @@ public class Location { id = S2CellId.fromLatLng(S2LatLng.fromDegrees(lat, lng)).id(); } + public Location(Long s2Id) { + id = s2Id; + S2LatLng s2LatLng = new S2CellId(s2Id).toLatLng(); + lng = s2LatLng.lngDegrees(); + lat = s2LatLng.latDegrees(); + } + } diff --git a/src/main/java/com/virjar/geolib/core/AdminNormalizer.java b/src/main/java/com/virjar/geolib/core/AdminNormalizer.java new file mode 100644 index 0000000..5171efb --- /dev/null +++ b/src/main/java/com/virjar/geolib/core/AdminNormalizer.java @@ -0,0 +1,143 @@ +package com.virjar.geolib.core; + +import com.virjar.geolib.bean.Admin; + +import java.util.*; +import java.util.stream.Collectors; + +public class AdminNormalizer { + private final List adminList; + private final GeoDb geoDb; + + private final Map fullNameMap = new HashMap<>(); + private final Map fullLevelMap = new HashMap<>(); + + private final Map renameDistrictMap = new HashMap() { + { + put("云州区", "大同县"); + put("云冈区", "南郊区"); + put("光明区", "宝安区"); + put("上党区", "长治县"); + put("大柴旦行政委员会", "海西蒙古族藏族自治州直辖"); + put("冷湖行政委员会", "海西蒙古族藏族自治州直辖"); + put("茫崖行政委员会", "海西蒙古族藏族自治州直辖"); + put("上饶县", "广信区"); + put("色尼区", "那曲县"); + put("襄樊", "襄阳"); + new ArrayList<>(entrySet()).forEach(entry -> put(entry.getValue(), entry.getKey())); + } + }; + + public AdminNormalizer(GeoDb geoDb) { + this.geoDb = geoDb; + adminList = geoDb.allAdminList(); + // 使用行政区域名称构建姓名缓存 + buildNameCache(); + } + + public Map getFullNameMap() { + return Collections.unmodifiableMap(fullNameMap); + } + + private void buildNameCache() { + Map> fullNames = new HashMap<>(); + adminList.forEach(admin -> fullNames.computeIfAbsent(admin.getName(), s -> new ArrayList<>()).add(admin)); + fullNames.forEach((s, adminList) -> { + if (adminList.size() == 1) { + // 如果有多条记录,则证明可能有歧义,暂时忽略 + fullNameMap.put(s, adminList.get(0).getId()); + } + }); + + fullNames.clear(); + adminList.forEach(admin -> fullNames.computeIfAbsent(admin.getShortName(), s -> new ArrayList<>()).add(admin)); + fullNames.forEach((s, adminList) -> { + if (adminList.size() == 1 && !fullNameMap.containsKey(s)) { + fullNameMap.put(s, adminList.get(0).getId()); + } + }); + + adminList.forEach(admin -> { + Admin tmpAdmin = admin; + int id = admin.getId(); + LinkedList fullLevel = new LinkedList<>(); + while (tmpAdmin != null) { + fullLevel.addFirst(tmpAdmin); + tmpAdmin = geoDb.getAdmin(tmpAdmin.getParentId()); + } + fullLevelMap.put(fullLevel.stream().map(Admin::getName).collect(Collectors.joining()), id); + fullLevelMap.put(fullLevel.stream().map(Admin::getShortName).collect(Collectors.joining()), id); + }); + } + + public int doNormalize(String address) { + return doNormalize(address, true); + } + + public int doNormalize(String address, boolean useRename) { + address = address.trim(); + if (fullNameMap.containsKey(address)) { + return fullNameMap.get(address); + } + String hintKey = null; + double maxScore = Double.MIN_VALUE; + for (String test : fullLevelMap.keySet()) { + double score = levenshtein(address, test); + if (score > maxScore) { + hintKey = test; + maxScore = score; + } + } + + if (hintKey != null && maxScore > 0.2) { + return fullLevelMap.get(hintKey); + } + if (useRename) { + // 处理改名地区 + for (Map.Entry entry : renameDistrictMap.entrySet()) { + address = address.replace(entry.getKey(), entry.getValue()); + } + return doNormalize(address, false); + } + return -1; + } + + + /** + * 计算两个字符串的编辑距离相似度,具体原理请网上查询编辑距离的概念 + */ + public static double levenshtein(String str1, String str2) { + int len1 = str1.length(); + int len2 = str2.length(); + int[][] dif = new int[len1 + 1][len2 + 1]; + for (int a = 0; a <= len1; a++) { + dif[a][0] = a; + } + for (int a = 0; a <= len2; a++) { + dif[0][a] = a; + } + int tmp; + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + if (str1.charAt(i - 1) == str2.charAt(j - 1)) { + tmp = 0; + } else { + tmp = 1; + } + dif[i][j] = min(dif[i - 1][j - 1] + tmp, dif[i][j - 1] + 1, dif[i - 1][j] + 1); + } + } + return 1 - dif[len1][len2] / (double) Math.max(str1.length(), str2.length()); + + } + + private static int min(int... is) { + int min = Integer.MAX_VALUE; + for (int i : is) { + if (min > i) { + min = i; + } + } + return min; + } +} diff --git a/src/main/java/com/virjar/geolib/core/GeoDb.java b/src/main/java/com/virjar/geolib/core/GeoDb.java new file mode 100644 index 0000000..26b677e --- /dev/null +++ b/src/main/java/com/virjar/geolib/core/GeoDb.java @@ -0,0 +1,211 @@ +package com.virjar.geolib.core; + +import com.virjar.geolib.bean.Admin; +import com.virjar.geolib.bean.BoundaryLine; + +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.ref.WeakReference; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * 存储所有的Geo数据 + */ +public class GeoDb implements Closeable { + public static final int S2_LEVEL = 12; + public static final int S2_QUICK_CELL_START_LEVEL = 8; + public static final int S2_QUICK_CELL_END_LEVEL = 10; + + + // it equals "geo-vj00" + public static final long magic = 0x67656f2d766a3030L; + public static final int itemSize = 8 + 8 + 4; + + /** + * enable mmap when running on 64-bit os system, + * mmap is a memory save way because of it`s zero copy(DMA) implementation and virtual memory mapping by os system. + * we will not load our GEO-db file (200M size) info memory if we not use GeoDb module + */ + public static final boolean USE_MMAP = is64bit(); + private final ByteBuffer dbBuffer; + int boundaryOffset; + int hashItemSize; + private final RandomAccessFile randomAccessFile; + + private final Map adminMap = new HashMap<>(); + + private final Map quickCache = new HashMap<>(); + + public GeoDb(File dataFile) throws IOException { + randomAccessFile = new RandomAccessFile(dataFile, "r"); + FileChannel fileChannel = randomAccessFile.getChannel(); + if (USE_MMAP) { + dbBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, dataFile.length()); + } else { + dbBuffer = ByteBuffer.allocate((int) dataFile.length()); + fileChannel.read(dbBuffer); + } + + long magic = dbBuffer.getLong(); + if (magic != GeoDb.magic) { + throw new IOException("error format: " + dataFile.getAbsolutePath() + " not geoDb binary database file," + + "expected: " + GeoDb.magic + " actual: " + magic); + } + + boundaryOffset = dbBuffer.getInt(); + hashItemSize = dbBuffer.getInt(); + // 行政节点,只有四千个左右,数据量级可控,故读取到内存再散开 + readAdminData(dbBuffer); + // 快速缓存,对于一些特定区块,可以直接map快速查询,本map 4k数据量 + readQuickCache(dbBuffer); + // 行政区域边界线数据量巨大(大约150M),故不能做内存读取解析,而是直接使用线性hashTable来执行文件索引 + } + + + public Admin getAdmin(int code) { + return adminMap.get(code); + } + + public Collection getChildren(int code) { + AdminEx adminEx = adminMap.get(code); + if (adminEx == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableSet(adminEx.children); + } + + public List allAdminList() { + return new ArrayList<>(adminMap.values()); + } + + public Integer quickHint(Long s2Id) { + return quickCache.get(s2Id); + } + + public List queryLines(long point) { + List ret = new ArrayList<>(); + + int startIndex = (int) (GeoDb.linePointHash(point) % hashItemSize); + for (int i = 0; i < hashItemSize; i++) { + int index = (startIndex + i) % hashItemSize; + long start = dbBuffer.getLong(boundaryOffset + index * itemSize); + if (start == 0) { + return ret; + } + if (start != point) { + // 哈希冲突 + continue; + } + long end = dbBuffer.getLong(boundaryOffset + index * itemSize + 8); + int adminCode = dbBuffer.getInt(boundaryOffset + index * itemSize + 16); + ret.add(new BoundaryLine(adminCode, start, end)); + } + return ret; + } + + + private void readAdminData(ByteBuffer buffer) { + List adminExes = readAdminList(buffer); + adminExes.forEach(adminEx -> adminMap.put(adminEx.getId(), adminEx)); + adminExes.forEach(adminEx -> { + int parentId = adminEx.getParentId(); + AdminEx parent = adminMap.get(parentId); + if (parent != null) { + parent.children.add(adminEx); + } + }); + } + + private void readQuickCache(ByteBuffer buffer) { + int size = Leb128.readUnsignedLeb128(buffer); + for (int i = 0; i < size; i++) { + long cellId = buffer.getLong(); + int adminCode = buffer.getInt(); + quickCache.put(cellId, adminCode); + } + } + + public static long linePointHash(long startPointS2) { + return Math.abs(startPointS2 ^ magic); + } + + private static boolean is64bit() { + // 如果需要支持android,请解开下面注释 +// try{ +// Class.forName("android.content.Context"); +// return Process.is64Bit(); +// }catch (Throwable ignore){ +// // not android env +// } + if (System.getProperty("os.name").contains("Windows")) { + return System.getenv("ProgramFiles(x86)") != null; + } else { + return System.getProperty("os.arch").contains("64"); + } + } + + @Override + public void close() throws IOException { + randomAccessFile.close(); + } + + private List readAdminList(ByteBuffer buffer) { + int size = Leb128.readUnsignedLeb128(buffer); + Admin.DistrictLevel[] districtLevels = Admin.DistrictLevel.values(); + ArrayList adminNodes = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + AdminEx adminNode = new AdminEx(); + adminNodes.add(adminNode); + adminNode.setId(Leb128.readUnsignedLeb128(buffer)); + adminNode.setParentId(Leb128.readUnsignedLeb128(buffer)); + adminNode.setName(readString(buffer)); + adminNode.setShortName(readString(buffer)); + adminNode.setCenterLng(buffer.getFloat()); + adminNode.setCenterLat(buffer.getFloat()); + adminNode.setLevel(districtLevels[buffer.get()]); + } + return adminNodes; + } + + private static String readString(ByteBuffer buffer) { + int len = Leb128.readUnsignedLeb128(buffer); + if (len == 0) { + return ""; + } + // 读需要使用缓存的内存变量,减少gc消耗,写不需要关注, + // 这里提供了一个4k的缓存 + byte[] buf = localBuff(len); + buffer.get(buf, 0, len); + return new String(buf, 0, len, StandardCharsets.UTF_8); + } + + private static final ThreadLocal> buffLocal = new ThreadLocal<>(); + + private static byte[] localBuff(int len) { + if (len < 1024) { + len = 1024; + } + WeakReference weakReference = buffLocal.get(); + byte[] bytes; + if (weakReference != null) { + bytes = weakReference.get(); + if (bytes != null && bytes.length >= len) { + return bytes; + } + } + + bytes = new byte[len]; + buffLocal.set(new WeakReference<>(bytes)); + return bytes; + } + + + private static class AdminEx extends Admin { + private final Set children = new TreeSet<>(); + } +} diff --git a/src/main/java/com/virjar/geolib/core/GeoUtils.java b/src/main/java/com/virjar/geolib/core/GeoUtils.java new file mode 100644 index 0000000..efd0fcb --- /dev/null +++ b/src/main/java/com/virjar/geolib/core/GeoUtils.java @@ -0,0 +1,189 @@ +package com.virjar.geolib.core; + +import com.virjar.geolib.bean.Location; +import com.google.common.geometry.*; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class GeoUtils { + public static double EARTH_RADIUS = 6378137.0;//WGS84, CGCS2000坐标系地球长半轴, m + private static final char[] hexChar = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + public static String toHexString(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 2); + + for (byte b1 : b) { + sb.append(hexChar[(b1 & 240) >>> 4]); + sb.append(hexChar[b1 & 15]); + } + + return sb.toString(); + } + + public static Map capHeightMap = new HashMap() {{ + for (int kRadius : Arrays.asList(2, 4, 8, 16, 32, 64, 128, 256)) { + double meterRadius = kRadius * 1000D; + put((int) meterRadius, capHeight(meterRadius)); + } + }}; + + + private static double capHeight(double radius) { + double rad = earthMeters2Radians(radius); + return rad * rad * 2; + } + + public static double earthMeters2Radians(Double meters) { + return (2 * S2.M_PI) * (meters / 40075017); + } + + public static double getCapHeight(int radius) { + double capHeight = capHeightMap.getOrDefault(radius, 0d); + if (capHeight == 0d) { + capHeight = capHeight(radius); + } + return capHeight; + } + + public static List cellIdsWithCapLevel(S2LatLng s2LatLng, int radius, int desLevel) { + double capHeight = getCapHeight(radius); + + S2Cap cap = S2Cap.fromAxisHeight(s2LatLng.toPoint(), capHeight); + S2RegionCoverer coverer = new S2RegionCoverer(); + coverer.setMaxLevel(desLevel); + coverer.setMinLevel(desLevel); + //圆形内的cell会自动做聚合,手动拆分 + return coverer.getCovering(cap) + .cellIds() + .stream() + .flatMap((Function>) s2CellId -> { + int cellLevel = getLevel(s2CellId.id()); + if (cellLevel == desLevel) { + return Stream.of(s2CellId.id()); + } else { + return childrenCellId(s2CellId, cellLevel, desLevel).stream().map(S2CellId::id); + } + }).collect(Collectors.toList()); + } + + public static int getLevel(Long inputs) { + int n = 0; + long input = inputs; + while (input % 2 == 0) { + input = input / 2; + n += 1; + } + return 30 - n / 2; + } + + public static List childrenCellId(S2CellId s2CellId, int curLevel, int desLevel) { + List list = new ArrayList<>(); + if (curLevel < desLevel) { + long interval = (s2CellId.childEnd().id() - s2CellId.childBegin().id()) / 4; + for (int i = 0; i < 4; i++) { + long id = s2CellId.childBegin().id() + interval * i; + S2CellId cellId = new S2CellId(id); + list.addAll(childrenCellId(cellId, curLevel + 1, desLevel)); + } + } else { + list.add(s2CellId); + } + return list; + } + + public static Double lineDis(Double x1, Double y1, Double x2, Double y2) { + return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2)); + } + + + public static boolean outOfChina(Double lng, Double lat) { + return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271; + } + + + public static Double pointToLineDis(Double x1, Double y1, Double x2, Double y2, Double x0, Double y0) { + double + a = lineDis(x1, y1, x2, y2),// 线段的长度 + b = lineDis(x1, y1, x0, y0),// 点到起点的距离 + c = lineDis(x2, y2, x0, y0);// 点到终点的距离 + //点在端点上 + if (c <= 0.000001 || b <= 0.000001) { + return 0D; + } + //直线距离过短 + if (a <= 0.000001) { + return b; + } + // 点在起点左侧,距离等于点到起点距离 + if (c * c >= a * a + b * b) { + return b; + } + //点在终点右侧,距离等于点到终点距离 + if (b * b >= a * a + c * c) { + return c; + } + //点在起点和终点中间,为垂线距离 + double k = (y2 - y1) / (x2 - x1); + double z = y1 - k * x1; + double p = (a + b + c) / 2; + // 半周长 + double s = Math.sqrt(p * (p - a) * (p - b) * (p - c));//海伦公式求面积 + return 2 * s / a;// 返回点到线的距离(利用三角形面积公式求高) + } + + + public static Double distanceMeter(Location location, long s2CellId) { + S2LatLng s2LatLng = new S2CellId(s2CellId).toLatLng(); + return distanceMeter(location, new Location(s2LatLng.lngDegrees(), s2LatLng.latDegrees())); + } + + public static double rad(Double d) { + return d * Math.PI / 180.0; + } + + public static Double distanceMeter(Location locA, Location locB) { + Double lngA = locA.lng; + Double latA = locA.lat; + Double lngB = locB.lng; + Double latB = locB.lat; + double f = rad((latA + latB) / 2); + double g = rad((latA - latB) / 2); + double l = rad((lngA - lngB) / 2); + if (g == 0 && l == 0) + return 0D; + double sg = Math.sin(g), sl = Math.sin(l), sf = Math.sin(f); + + double s, c, w, r, d, h1, h2, dis, a = EARTH_RADIUS, fl = 1 / 298.257; + sg = sg * sg; + sl = sl * sl; + sf = sf * sf; + s = sg * (1 - sl) + (1 - sf) * sl; + c = (1 - sg) * (1 - sl) + sf * sl; + w = Math.atan(Math.sqrt(s / c)); + r = Math.sqrt(s * c) / w; + d = 2 * w * a; + h1 = (3 * r - 1) / 2 / c; + h2 = (3 * r + 1) / 2 / s; + dis = d * (1 + fl * (h1 * sf * (1 - sg) - h2 * (1 - sf) * sg)); + + //return dis.formatted("%.2f").toDouble + return new BigDecimal(dis).setScale(2, RoundingMode.HALF_UP).doubleValue(); + + } + + public static double area(List pylon) { + double area = 0D; + for (int i = 0; i < pylon.size() - 1; i++) { + S2LatLng p1 = new S2CellId(pylon.get(i)).toLatLng(); + S2LatLng p2 = new S2CellId(pylon.get(i + 1)).toLatLng(); + area += p1.lngDegrees() * p2.latDegrees(); + area -= p2.lngDegrees() * p1.latDegrees(); + } + return area / 2; + } +} diff --git a/src/main/java/com/dengxq/lnglat2Geo/utils/Leb128.java b/src/main/java/com/virjar/geolib/core/Leb128.java similarity index 81% rename from src/main/java/com/dengxq/lnglat2Geo/utils/Leb128.java rename to src/main/java/com/virjar/geolib/core/Leb128.java index 0793907..ed4c4e3 100644 --- a/src/main/java/com/dengxq/lnglat2Geo/utils/Leb128.java +++ b/src/main/java/com/virjar/geolib/core/Leb128.java @@ -1,20 +1,8 @@ -package com.dengxq.lnglat2Geo.utils; +package com.virjar.geolib.core; import java.nio.ByteBuffer; -/** - * Reads and writes DWARFv3 LEB 128 signed and unsigned integers. See DWARF v3 - * section 7.6. - *

- * Leb128编码方式能够显著提升数字类型的空间大小,他的核心原理是我们程序运行的数字大多数在一个很小的范围(大部分在千以内) - * 故小数字短编码,大数字长编码。可以使用一个byte存储long类型的数字 - *

- * Leb128带来的问题也是数据区域更加不可读,但是这是我们优化存储空间的有效手段 - *

- *

- * 同样是否需要使用Leb128也是参考我们对数据的范围本身的大小参考 - */ public final class Leb128 { private Leb128() { } diff --git a/src/main/resources/geo-vj00.bin.zip b/src/main/resources/geo-vj00.bin.zip new file mode 100644 index 0000000..d890108 Binary files /dev/null and b/src/main/resources/geo-vj00.bin.zip differ diff --git a/src/main/resources/geolib.data.bin b/src/main/resources/geolib.data.bin deleted file mode 100644 index e70e5c3..0000000 Binary files a/src/main/resources/geolib.data.bin and /dev/null differ