refactoring to pure implementation
This commit is contained in:
parent
9358e8f445
commit
f2e4c87f1c
64
README.md
64
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
|
34
pom.xml
34
pom.xml
@ -6,13 +6,13 @@
|
||||
<properties>
|
||||
<slf4j.log4j12.version>1.6.2</slf4j.log4j12.version>
|
||||
</properties>
|
||||
<groupId>com.github.deng0515001</groupId>
|
||||
<artifactId>lnglat2Geo</artifactId>
|
||||
<version>1.0.5</version>
|
||||
<groupId>com.virjar</groupId>
|
||||
<artifactId>geolib</artifactId>
|
||||
<version>1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>lnglat2Geo</name>
|
||||
<name>geolib</name>
|
||||
<description>lnglat to Geo</description>
|
||||
<url>https://github.com/deng0515001/lnglat2Geo</url>
|
||||
<url>https://github.com/virjar/geoLibChina</url>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
@ -20,36 +20,12 @@
|
||||
<artifactId>s2-geometry-library-java</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<!-- 移植scala代码,tuple暂时用它来替代 -->
|
||||
<dependency>
|
||||
<groupId>com.speedment.common</groupId>
|
||||
<artifactId>tuple</artifactId>
|
||||
<version>3.2.10</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.9</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.22</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>fastjson</artifactId>
|
||||
<version>1.2.79</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>1.3.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<licenses>
|
||||
|
@ -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<Double, Double> 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<BusinessArea> 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<AdminNode> 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<Admin> 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<Integer, AdminNode> adminData() {
|
||||
return GeoTransImpl.adminData;
|
||||
}
|
||||
|
||||
public static Map<String, String> countryCode() {
|
||||
return GeoTransImpl.countryCode;
|
||||
}
|
||||
|
||||
// public static districtBoundary():Map[Int,List[List[Long]]]=
|
||||
//
|
||||
// {
|
||||
// AdminDataProvider.AdminLoader.loadBoundarySrc.map(s = > (s.code, s.boundary))
|
||||
// .toMap
|
||||
// }
|
||||
}
|
@ -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<Integer, List<BusinessAreaData>> cityBusinessArea = ILoader.Storage.getOrLoad().getAreaGroups()
|
||||
.stream().collect(Collectors.toMap(s -> s.cityAdCode, s -> s.areas));
|
||||
|
||||
public static Map<Long, Integer> boundaryAdminCell = ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryAdminCell();
|
||||
|
||||
public static Map<Long, List<Tuple3<Long, Integer, Integer>>> boundaryData =
|
||||
ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryData();
|
||||
|
||||
public static Map<Long, List<Long>> boundaryIndex = ILoader.Storage.getOrLoad().getOrCreateRuntimeBoundaryIndex();
|
||||
public static Map<String, String> cityLevelData = ILoader.Storage.getOrLoad().getCityLevel();
|
||||
|
||||
public static Map<Integer, AdminNode> adminData = ILoader.Storage.getOrLoad().getAdminData()
|
||||
.stream().collect(
|
||||
Collectors.toMap(adminNode -> adminNode.id, adminNode -> adminNode)
|
||||
);
|
||||
|
||||
public static Map<Integer, AdminNode> streetData = ILoader.Storage.getOrLoad().getStreetData().stream().collect(
|
||||
Collectors.toMap(adminNode -> adminNode.id, adminNode -> adminNode)
|
||||
);
|
||||
|
||||
public static Map<String, String> countryCode = ILoader.Storage.getOrLoad().getCountryCode();
|
||||
|
||||
|
||||
public static Map<String, String> cityNameMap = new HashMap<String, String>() {{
|
||||
put("重庆市", "");
|
||||
put("上海市", "上海城区");
|
||||
put("北京市", "北京城区");
|
||||
put("天津市", "天津城区");
|
||||
put("那曲市", "那曲地区");
|
||||
}};
|
||||
|
||||
public static Map<String, String> districtNameMap = new HashMap<String, String>() {
|
||||
{
|
||||
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<BusinessArea> determineAreaByCityId(Double lon, Double lat, int cityId, int radius, CoordinateSystem coordSys) {
|
||||
if (cityId == -1) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Tuple2<Double, Double> tuple2 = GeoUtils.toGCJ02(lon, lat, coordSys);
|
||||
Location location = new Location(tuple2.get0(), tuple2.get1());
|
||||
List<BusinessAreaData> 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<Double, Double> 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<Long> keys = new HashSet<>();
|
||||
//必须大于2000m,否则会出现格子半径过小选择错误问题
|
||||
int maxLevel = 2000;
|
||||
while (keys.isEmpty() && maxLevel < 200000) {
|
||||
keys = S2Utils.getCellId(s2LatLng, maxLevel, GeoTransImpl.min_level)
|
||||
.stream()
|
||||
.flatMap((Function<Long, Stream<Long>>) aLong ->
|
||||
boundaryIndex.getOrDefault(aLong, Collections.emptyList()).stream())
|
||||
.collect(Collectors.toSet());
|
||||
maxLevel = maxLevel * 2;
|
||||
}
|
||||
|
||||
if (keys.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
List<Tuple2<Tuple4<Tuple2<Double, Double>, Tuple2<Double, Double>, 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<Tuple2<Long, Double>, Stream<Tuple4<Long, Long, Integer, Boolean>>>)
|
||||
startPoint -> boundaryData.getOrDefault(startPoint.get0(), Collections.emptyList())
|
||||
.stream()
|
||||
.flatMap((Function<Tuple3<Long, Integer, Integer>, Stream<Tuple4<Long, Long, Integer, Boolean>>>)
|
||||
value -> {
|
||||
List<Tuple4<Long, Long, Integer, Boolean>> 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<Tuple4<Tuple2<Double, Double>, Tuple2<Double, Double>, 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<Double, Double>, Tuple2<Double, Double>, Integer, Boolean> line1 = lines.get(0);
|
||||
Tuple2<Double, Double> start = line1.get0();
|
||||
Tuple2<Double, Double> 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<Double, Double>, Tuple2<Double, Double>, Integer, Boolean> line1 = lines.get(0);
|
||||
Tuple4<Tuple2<Double, Double>, Tuple2<Double, Double>, Integer, Boolean> line2 = lines.get(1);
|
||||
|
||||
// 终点相同,为国内两个相邻区域,终点不同,为国界线
|
||||
Tuple2<Double, Double> start;
|
||||
if (line1.get1().equals(line2.get1())) {
|
||||
start = line1.get0();
|
||||
} else {
|
||||
start = line2.get1();
|
||||
}
|
||||
|
||||
Tuple2<Double, Double> 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<Double, Double>, Tuple2<Double, Double>, Integer, Boolean> line1 =
|
||||
s.getValue().stream().filter(Tuple4::get3).findFirst().get();
|
||||
Tuple4<Tuple2<Double, Double>, Tuple2<Double, Double>, Integer, Boolean> line2 =
|
||||
s.getValue().stream().filter(it -> !it.get3()).findFirst().get();
|
||||
|
||||
Tuple2<Double, Double> start = line2.get1();
|
||||
Tuple2<Double, Double> end = line1.get1();
|
||||
Tuple2<Double, Double> 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<AdminNode> 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<Admin> 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<AdminNode> 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<AdminNode> buildAdminNode(String city, String district, String street, String province, boolean isFullMatch) {
|
||||
List<AdminNode> 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<AdminNode> 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<AdminNode> 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<AdminNode> 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<Double, Double> 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<Integer> 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);
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -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<List<Long>> boundary;
|
||||
|
||||
}
|
@ -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";
|
||||
}
|
@ -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<Integer> 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<Integer> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public void setChildren(List<Integer> children) {
|
||||
this.children = children;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
@ -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<BusinessAreaData> areas;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.dengxq.lnglat2Geo.entity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BusinessAreaInfo {
|
||||
public Admin admin;
|
||||
public List<BusinessArea> areas;
|
||||
|
||||
public BusinessAreaInfo(Admin admin, List<BusinessArea> areas) {
|
||||
this.admin = admin;
|
||||
this.areas = areas;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;// 墨卡托坐标系
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package com.dengxq.lnglat2Geo.entity.enums;
|
||||
|
||||
public enum DistrictLevel {
|
||||
/**
|
||||
* 街道*
|
||||
*/
|
||||
Country,
|
||||
/**
|
||||
* 省,自治区*
|
||||
*/
|
||||
Province,
|
||||
/**
|
||||
* 地级市*
|
||||
*/
|
||||
City,
|
||||
/**
|
||||
* 区,县,县级市*
|
||||
*/
|
||||
District,
|
||||
Street
|
||||
}
|
@ -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<byte[]> 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<AdminNode> 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<Integer> children = adminNode.children;
|
||||
Leb128.writeUnsignedLeb128(buffer, children.size());
|
||||
for (Integer integer : children) {
|
||||
Leb128.writeSignedLeb128(buffer, integer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<AdminNode> readAdminNodes(ByteBuffer buffer) {
|
||||
int size = Leb128.readUnsignedLeb128(buffer);
|
||||
DistrictLevel[] districtLevels = DistrictLevel.values();
|
||||
ArrayList<AdminNode> 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<Integer> 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<AdminBoundary> adminBoundaries, ByteBuffer buffer) {
|
||||
Leb128.writeUnsignedLeb128(buffer, adminBoundaries.size());
|
||||
for (AdminBoundary adminBoundary : adminBoundaries) {
|
||||
//code
|
||||
Leb128.writeSignedLeb128(buffer, adminBoundary.code);
|
||||
List<List<Long>> boundary = adminBoundary.boundary;
|
||||
Leb128.writeUnsignedLeb128(buffer, boundary.size());
|
||||
for (List<Long> line : boundary) {
|
||||
Leb128.writeUnsignedLeb128(buffer, line.size());
|
||||
for (Long point : line) {
|
||||
buffer.putLong(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<AdminBoundary> readAdminBoundaries(ByteBuffer buffer) {
|
||||
int size = Leb128.readUnsignedLeb128(buffer);
|
||||
ArrayList<AdminBoundary> 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<List<Long>> boundary = new ArrayList<>(boundarySize);
|
||||
adminBoundary.setBoundary(boundary);
|
||||
for (int j = 0; j < boundarySize; j++) {
|
||||
int lineSize = Leb128.readUnsignedLeb128(buffer);
|
||||
ArrayList<Long> line = new ArrayList<>(lineSize);
|
||||
boundary.add(line);
|
||||
for (int z = 0; z < lineSize; z++) {
|
||||
line.add(buffer.getLong());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return adminBoundaries;
|
||||
}
|
||||
|
||||
|
||||
private void writeAreaGroups(List<BusinessAreaGroup> areaGroups, ByteBuffer buffer) {
|
||||
Leb128.writeUnsignedLeb128(buffer, areaGroups.size());
|
||||
for (BusinessAreaGroup businessAreaGroup : areaGroups) {
|
||||
Leb128.writeSignedLeb128(buffer, businessAreaGroup.cityAdCode);
|
||||
List<BusinessAreaData> 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<BusinessAreaGroup> readAreaGroups(ByteBuffer buffer) {
|
||||
int size = Leb128.readUnsignedLeb128(buffer);
|
||||
List<BusinessAreaGroup> 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<BusinessAreaData> 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<CellAdmin> cellAdmins, ByteBuffer buffer) {
|
||||
Leb128.writeUnsignedLeb128(buffer, cellAdmins.size());
|
||||
for (CellAdmin cellAdmin : cellAdmins) {
|
||||
Leb128.writeSignedLeb128(buffer, cellAdmin.adCode);
|
||||
buffer.putLong(cellAdmin.cellId);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CellAdmin> readCellAdmins(ByteBuffer buffer) {
|
||||
int size = Leb128.readUnsignedLeb128(buffer);
|
||||
List<CellAdmin> 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<String, String> map, ByteBuffer buffer) {
|
||||
Leb128.writeUnsignedLeb128(buffer, map.size());
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
writeString(buffer, entry.getKey());
|
||||
writeString(buffer, entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, String> readMap(ByteBuffer buffer) {
|
||||
Map<String, String> 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<Long, List<Tuple3<Long, Integer, Integer>>> data, ByteBuffer buffer) {
|
||||
buffer.putInt(data.size());
|
||||
for (Map.Entry<Long, List<Tuple3<Long, Integer, Integer>>> entry : data.entrySet()) {
|
||||
Long key = entry.getKey();
|
||||
buffer.putLong(key);
|
||||
List<Tuple3<Long, Integer, Integer>> value = entry.getValue();
|
||||
buffer.putInt(value.size());
|
||||
for (Tuple3<Long, Integer, Integer> tuple3 : value) {
|
||||
buffer.putLong(tuple3.get0());
|
||||
buffer.putInt(tuple3.get1());
|
||||
buffer.putInt(tuple3.get2());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, List<Tuple3<Long, Integer, Integer>>> readRuntimeBoundaryData(ByteBuffer buffer) {
|
||||
int mapSize = buffer.getInt();
|
||||
Map<Long, List<Tuple3<Long, Integer, Integer>>> ret = new HashMap<>(mapSize);
|
||||
for (int i = 0; i < mapSize; i++) {
|
||||
long key = buffer.getLong();
|
||||
int listSize = buffer.getInt();
|
||||
List<Tuple3<Long, Integer, Integer>> 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<Long, List<Long>> data, ByteBuffer buffer) {
|
||||
buffer.putInt(data.size());
|
||||
for (Map.Entry<Long, List<Long>> entry : data.entrySet()) {
|
||||
Long key = entry.getKey();
|
||||
buffer.putLong(key);
|
||||
List<Long> value = entry.getValue();
|
||||
buffer.putInt(value.size());
|
||||
for (Long aLong : value) {
|
||||
buffer.putLong(aLong);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, List<Long>> readRuntimeBoundaryIndex(ByteBuffer buffer) {
|
||||
int mapSize = buffer.getInt();
|
||||
Map<Long, List<Long>> ret = new HashMap<>(mapSize);
|
||||
for (int i = 0; i < mapSize; i++) {
|
||||
long key = buffer.getLong();
|
||||
int listSize = buffer.getInt();
|
||||
List<Long> 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<Long, Integer> data, ByteBuffer buffer) {
|
||||
buffer.putInt(data.size());
|
||||
for (Map.Entry<Long, Integer> entry : data.entrySet()) {
|
||||
buffer.putLong(entry.getKey());
|
||||
buffer.putInt(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, Integer> readRuntimeBoundaryAdminCell(ByteBuffer buffer) {
|
||||
int mapSize = buffer.getInt();
|
||||
Map<Long, Integer> ret = new HashMap<>(mapSize);
|
||||
for (int i = 0; i < mapSize; i++) {
|
||||
ret.put(buffer.getLong(), buffer.getInt());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
/**
|
||||
* 离线数据包,包括行政区折线图、行政等级划分、商圈、城市编码、城市级别等
|
||||
* <p>
|
||||
* 他可以由二进制或者json两种格式转换加载,并且可以序列化到二进制和json两种格式,
|
||||
* 其中二进制格式将会伴随发布包发布,作为默认的离线数据资源,
|
||||
* json格式则主要用于调试和升级二进制格式使用
|
||||
*/
|
||||
@Data
|
||||
public class GeoData {
|
||||
|
||||
/**
|
||||
* 行政区域数据
|
||||
*/
|
||||
private List<AdminNode> adminData;
|
||||
|
||||
/**
|
||||
* 街道数据
|
||||
*/
|
||||
private List<AdminNode> streetData;
|
||||
|
||||
/**
|
||||
* 多边形边界线,规定每个区域的范围
|
||||
*/
|
||||
private List<AdminBoundary> adminBoundaries;
|
||||
|
||||
|
||||
/**
|
||||
* 商圈
|
||||
*/
|
||||
private List<BusinessAreaGroup> areaGroups;
|
||||
|
||||
/**
|
||||
* s2到行政编码的缓存,他不是必须的,但是可以提高计算速度
|
||||
*/
|
||||
private List<CellAdmin> cellAdmins;
|
||||
|
||||
/**
|
||||
* 城市级别
|
||||
*/
|
||||
private Map<String, String> cityLevel;
|
||||
|
||||
/**
|
||||
* 国家编码
|
||||
*/
|
||||
private Map<String, String> countryCode;
|
||||
|
||||
// 以下为extension数据,他是根据上面的字段计算而来,因为他们的计算比较耗时,我们把计算好的数据缓存到这里,
|
||||
// json 格式不序列化这些字段,bin需要序列化这些字段
|
||||
private Map<Long, List<Tuple3<Long, Integer, Integer>>> runtimeBoundaryData;
|
||||
|
||||
private Map<Long, List<Long>> runtimeBoundaryIndex;
|
||||
|
||||
private Map<Long, Integer> runtimeBoundaryAdminCell;
|
||||
|
||||
public void clearRuntime() {
|
||||
runtimeBoundaryData = null;
|
||||
runtimeBoundaryIndex = null;
|
||||
runtimeBoundaryAdminCell = null;
|
||||
}
|
||||
|
||||
public synchronized Map<Long, List<Tuple3<Long, Integer, Integer>>> getOrCreateRuntimeBoundaryData() {
|
||||
if (runtimeBoundaryData == null) {
|
||||
runtimeBoundaryData = Utils.parseBoundaryData(adminBoundaries);
|
||||
}
|
||||
return runtimeBoundaryData;
|
||||
}
|
||||
|
||||
public synchronized Map<Long, List<Long>> getOrCreateRuntimeBoundaryIndex() {
|
||||
if (runtimeBoundaryIndex == null) {
|
||||
runtimeBoundaryIndex = Utils.buildBoundaryIndex(getOrCreateRuntimeBoundaryData());
|
||||
}
|
||||
return runtimeBoundaryIndex;
|
||||
}
|
||||
|
||||
public synchronized Map<Long, Integer> getOrCreateRuntimeBoundaryAdminCell() {
|
||||
if (runtimeBoundaryAdminCell == null) {
|
||||
runtimeBoundaryAdminCell = Utils.buildBoundaryAdminCell(cellAdmins);
|
||||
}
|
||||
return runtimeBoundaryAdminCell;
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
package com.dengxq.lnglat2Geo.loader;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 数据加载和dump的顶层,支持从json文件和bin文件中加载,以及dump到json和bin文件中
|
||||
* 实现可视化格式和二进制格式的相互转换。
|
||||
* <p>
|
||||
* 我们导入和更新数据来自于可视化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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -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<String, Integer> 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<String, Integer> 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<String, Integer> 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;
|
||||
}
|
||||
}
|
@ -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<Double, Double> 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<Double, Double> 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<Double, Double> 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<Double, Double> 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<Double, Double> 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<Double, Double> toWGS84(Double lng, Double lat, CoordinateSystem coordType) {
|
||||
switch (coordType) {
|
||||
case GCJ02:
|
||||
return GeoUtils.gcj02ToWgs84(lng, lat);
|
||||
case BD09:
|
||||
Tuple2<Double, Double> 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);
|
||||
}
|
||||
}
|
@ -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;// 返回点到线的距离(利用三角形面积公式求高)
|
||||
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<Integer, Double> capHeightMap = new HashMap<Integer, Double>() {{
|
||||
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<Long> 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, Stream<Long>>) 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<S2CellId> childrenCellId(S2CellId s2CellId, int curLevel, int desLevel) {
|
||||
List<S2CellId> 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;
|
||||
}
|
||||
}
|
@ -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<Long, Integer> buildBoundaryAdminCell(List<CellAdmin> data) {
|
||||
return data.stream().collect(Collectors.toMap(s -> s.cellId, s -> s.adCode));
|
||||
}
|
||||
|
||||
public static Map<Long, List<Long>> buildBoundaryIndex(Map<Long, List<Tuple3<Long, Integer, Integer>>> boundaryData) {
|
||||
Map<Long, List<Tuple2<Long, Long>>> map = boundaryData.keySet().stream().map(s -> Tuples.of(
|
||||
new S2CellId(s).parent(GeoTransImpl.min_level).id(), s
|
||||
)).collect(Collectors.groupingBy(Tuple2::get0));
|
||||
Map<Long, List<Long>> result = new HashMap<>();
|
||||
for (Map.Entry<Long, List<Tuple2<Long, Long>>> entry : map.entrySet()) {
|
||||
result.put(entry.getKey(), entry.getValue().stream().map(Tuple2::get1).collect(Collectors.toList()));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Map<Long, List<Tuple3<Long, Integer, Integer>>> parseBoundaryData(List<AdminBoundary> adminBoundaryInJavas) {
|
||||
Map<Long, List<Tuple4<Long, Long, Integer, Boolean>>> map = adminBoundaryInJavas
|
||||
.stream()
|
||||
.flatMap((Function<AdminBoundary, Stream<Tuple4<Long, Long, Integer, Boolean>>>)
|
||||
adminBoundary -> adminBoundary.boundary.stream().flatMap(
|
||||
(Function<List<Long>, Stream<Tuple4<Long, Long, Integer, Boolean>>>)
|
||||
input -> {
|
||||
List<Tuple4<Long, Long, Integer, Boolean>> 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<Long, List<Tuple3<Long, Integer, Integer>>> ret = new HashMap<>();
|
||||
|
||||
for (Map.Entry<Long, List<Tuple4<Long, Long, Integer, Boolean>>> entry : map.entrySet()) {
|
||||
List<Tuple4<Long, Long, Integer, Boolean>> value = entry.getValue();
|
||||
|
||||
Map<Long, List<Tuple3<Long, Integer, Boolean>>> sssMap = value.stream().map(longLongIntegerBooleanTuple4 -> Tuples.of(longLongIntegerBooleanTuple4.get1(), longLongIntegerBooleanTuple4.get2(), longLongIntegerBooleanTuple4.get3())).collect(Collectors.groupingBy(new Function<Tuple3<Long, Integer, Boolean>, Long>() {
|
||||
@Override
|
||||
public Long apply(Tuple3<Long, Integer, Boolean> longIntegerBooleanTuple3) {
|
||||
return longIntegerBooleanTuple3.get0();
|
||||
}
|
||||
}));
|
||||
|
||||
List<Tuple3<Long, Integer, Integer>> ret1 = new ArrayList<>();
|
||||
ret.put(entry.getKey(), ret1);
|
||||
for (Map.Entry<Long, List<Tuple3<Long, Integer, Boolean>>> sss : sssMap.entrySet()) {
|
||||
List<Tuple2<Integer, Boolean>> 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;
|
||||
|
||||
}
|
||||
|
||||
}
|
244
src/main/java/com/virjar/geolib/GeoBinBuilder.java
Normal file
244
src/main/java/com/virjar/geolib/GeoBinBuilder.java
Normal file
@ -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<Integer, Admin> adminData = new HashMap<>();
|
||||
private final Map<Integer, Boundary> boundaryData = new HashMap<>();
|
||||
|
||||
/**
|
||||
* <a href="https://baike.baidu.com/item/%E8%83%A1%E7%84%95%E5%BA%B8%E7%BA%BF">黑河-腾冲线</a>
|
||||
*/
|
||||
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<Long> 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<Long> pylonWith12 = new ArrayList<>();
|
||||
Set<Long> 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<Long, Integer> 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<BoundaryLine> 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<Long> 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<Admin> 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<BoundaryLine> boundaryLines = new ArrayList<>();
|
||||
private final Map<Long, Integer> quickCachePoints = new HashMap<>();
|
||||
}
|
||||
|
||||
public static void log(String msg) {
|
||||
System.out.println(LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) + ":" + msg);
|
||||
}
|
||||
|
||||
}
|
332
src/main/java/com/virjar/geolib/GeoTrans.java
Normal file
332
src/main/java/com/virjar/geolib/GeoTrans.java
Normal file
@ -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<String, Integer> normalizeNames = loadAddressData();
|
||||
|
||||
|
||||
/**
|
||||
* 给定任意一个行政编码,返回格式化的行政区域定义
|
||||
*
|
||||
* @param code 行政编码
|
||||
* @return 行政区域定义
|
||||
*/
|
||||
public static Admin getAdmin(int code) {
|
||||
return geoDb.getAdmin(code);
|
||||
}
|
||||
|
||||
public static List<Admin> 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<Admin> 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<BoundaryLine> lines = new HashSet<>();
|
||||
//必须大于2000m,否则会出现格子半径过小选择错误问题
|
||||
int maxRadius = 2000;
|
||||
while (lines.isEmpty() && maxRadius < 200000) {
|
||||
lines = GeoUtils.cellIdsWithCapLevel(s2LatLng, maxRadius, GeoDb.S2_LEVEL)
|
||||
.stream()
|
||||
.flatMap((Function<Long, Stream<BoundaryLine>>) start ->
|
||||
geoDb.queryLines(start).stream())
|
||||
.collect(Collectors.toSet());
|
||||
maxRadius = maxRadius * 2;
|
||||
}
|
||||
if (lines.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// step2,这些线段,求距离目标点位最近的一批线段
|
||||
double minDistance = Double.MAX_VALUE;
|
||||
List<BoundaryLine> 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<BoundaryLine> 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<BoundaryLine> 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<String, Integer> loadAddressData() {
|
||||
ConcurrentHashMap<String, Integer> 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<String, Integer> 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();
|
||||
}
|
||||
}
|
35
src/main/java/com/virjar/geolib/bean/Admin.java
Normal file
35
src/main/java/com/virjar/geolib/bean/Admin.java
Normal file
@ -0,0 +1,35 @@
|
||||
package com.virjar.geolib.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 行政区域定义
|
||||
*/
|
||||
@Data
|
||||
public class Admin implements Comparable<Admin> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
27
src/main/java/com/virjar/geolib/bean/Boundary.java
Normal file
27
src/main/java/com/virjar/geolib/bean/Boundary.java
Normal file
@ -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<List<Long>> pylons = new ArrayList<>();
|
||||
}
|
17
src/main/java/com/virjar/geolib/bean/BoundaryLine.java
Normal file
17
src/main/java/com/virjar/geolib/bean/BoundaryLine.java
Normal file
@ -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;
|
||||
}
|
||||
}
|
25
src/main/java/com/virjar/geolib/bean/GeoAdmin.java
Normal file
25
src/main/java/com/virjar/geolib/bean/GeoAdmin.java
Normal file
@ -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);
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
143
src/main/java/com/virjar/geolib/core/AdminNormalizer.java
Normal file
143
src/main/java/com/virjar/geolib/core/AdminNormalizer.java
Normal file
@ -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<Admin> adminList;
|
||||
private final GeoDb geoDb;
|
||||
|
||||
private final Map<String, Integer> fullNameMap = new HashMap<>();
|
||||
private final Map<String, Integer> fullLevelMap = new HashMap<>();
|
||||
|
||||
private final Map<String, String> renameDistrictMap = new HashMap<String, String>() {
|
||||
{
|
||||
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<String, Integer> getFullNameMap() {
|
||||
return Collections.unmodifiableMap(fullNameMap);
|
||||
}
|
||||
|
||||
private void buildNameCache() {
|
||||
Map<String, List<Admin>> 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<Admin> 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<String, String> 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;
|
||||
}
|
||||
}
|
211
src/main/java/com/virjar/geolib/core/GeoDb.java
Normal file
211
src/main/java/com/virjar/geolib/core/GeoDb.java
Normal file
@ -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<Integer, AdminEx> adminMap = new HashMap<>();
|
||||
|
||||
private final Map<Long, Integer> 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<Admin> getChildren(int code) {
|
||||
AdminEx adminEx = adminMap.get(code);
|
||||
if (adminEx == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableSet(adminEx.children);
|
||||
}
|
||||
|
||||
public List<Admin> allAdminList() {
|
||||
return new ArrayList<>(adminMap.values());
|
||||
}
|
||||
|
||||
public Integer quickHint(Long s2Id) {
|
||||
return quickCache.get(s2Id);
|
||||
}
|
||||
|
||||
public List<BoundaryLine> queryLines(long point) {
|
||||
List<BoundaryLine> 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<AdminEx> 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<AdminEx> readAdminList(ByteBuffer buffer) {
|
||||
int size = Leb128.readUnsignedLeb128(buffer);
|
||||
Admin.DistrictLevel[] districtLevels = Admin.DistrictLevel.values();
|
||||
ArrayList<AdminEx> 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<WeakReference<byte[]>> buffLocal = new ThreadLocal<>();
|
||||
|
||||
private static byte[] localBuff(int len) {
|
||||
if (len < 1024) {
|
||||
len = 1024;
|
||||
}
|
||||
WeakReference<byte[]> 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<Admin> children = new TreeSet<>();
|
||||
}
|
||||
}
|
189
src/main/java/com/virjar/geolib/core/GeoUtils.java
Normal file
189
src/main/java/com/virjar/geolib/core/GeoUtils.java
Normal file
@ -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<Integer, Double> capHeightMap = new HashMap<Integer, Double>() {{
|
||||
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<Long> 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, Stream<Long>>) 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<S2CellId> childrenCellId(S2CellId s2CellId, int curLevel, int desLevel) {
|
||||
List<S2CellId> 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<Long> 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;
|
||||
}
|
||||
}
|
@ -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.
|
||||
* <p>
|
||||
* Leb128编码方式能够显著提升数字类型的空间大小,他的核心原理是我们程序运行的数字大多数在一个很小的范围(大部分在千以内)
|
||||
* 故小数字短编码,大数字长编码。可以使用一个byte存储long类型的数字
|
||||
* <p>
|
||||
* Leb128带来的问题也是数据区域更加不可读,但是这是我们优化存储空间的有效手段
|
||||
* <p>
|
||||
* <p>
|
||||
* 同样是否需要使用Leb128也是参考我们对数据的范围本身的大小参考
|
||||
*/
|
||||
public final class Leb128 {
|
||||
private Leb128() {
|
||||
}
|
BIN
src/main/resources/geo-vj00.bin.zip
Normal file
BIN
src/main/resources/geo-vj00.bin.zip
Normal file
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue
Block a user