refactoring to pure implementation

This commit is contained in:
virjar 2023-01-28 14:55:43 +08:00
parent 9358e8f445
commit f2e4c87f1c
41 changed files with 1253 additions and 2681 deletions

View File

@ -1,59 +1,27 @@
# ChinaGeoLib
修改自: https://github.com/deng0515001/lnglat2Geo 非常感谢原作者的贡献,我完全照搬了原作者的算法实现,但是我不能理解这些算法的原理。
修改点如下:
修改自: https://github.com/deng0515001/lnglat2Geo 非常感谢原作者的贡献.修改点如下:
1. 使用java完全重写Scala代码原因如下
1. 作为libjava和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
有问题直接联系QQ451408963@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
View File

@ -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>

View File

@ -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
// }
}

View File

@ -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);
}
}

View File

@ -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
);
}
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;// 墨卡托坐标系
}

View File

@ -1,21 +0,0 @@
package com.dengxq.lnglat2Geo.entity.enums;
public enum DistrictLevel {
/**
* 街道*
*/
Country,
/**
* ,自治区*
*/
Province,
/**
* 地级市*
*/
City,
/**
* ,,县级市*
*/
District,
Street
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;// 返回点到线的距离利用三角形面积公式求高
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View 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集合范围,此时将会比较影响查询效率但是我们可以完全使用更高级别的s2level 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);
}
}

View 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();
}
}

View 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;
}
}
}

View 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<>();
}

View 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;
}
}

View 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);
}

View File

@ -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();
}
}

View 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;
}
}

View 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<>();
}
}

View 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;
}
}

View File

@ -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() {
}

Binary file not shown.

Binary file not shown.