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