Browse Source

增加地图处理代码

JazzZhao 4 tháng trước cách đây
mục cha
commit
577d11ab79

+ 6 - 0
.gitignore

@@ -396,3 +396,9 @@ FodyWeavers.xsd
 
 # JetBrains Rider
 *.sln.iml
+
+#地图数据
+*.wkbs
+
+#java运行文件夹
+target

+ 1701 - 0
AreaCity-Query-Geometry/AreaCityQuery.java

@@ -0,0 +1,1701 @@
+package com.github.xiangyuecn.areacity.query;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.lang.reflect.Method;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Envelope;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.PrecisionModel;
+import org.locationtech.jts.index.strtree.STRtree;
+import org.locationtech.jts.io.WKBReader;
+import org.locationtech.jts.io.WKBWriter;
+import org.locationtech.jts.io.WKTWriter;
+import org.locationtech.jts.operation.distance.DistanceOp;
+
+/**
+ * 使用jts库从省市区县乡镇边界数据(AreaCity-JsSpider-StatsGov开源库)或geojson文件中查找出和任意点、线、面有相交的边界,内存占用低,性能优良。
+ * <pre>
+ * 可用于:
+ *     - 调用 QueryPoint(lng, lat) 查询一个坐标点对应的省市区名称等信息;
+ *     - 调用 ReadWKT_FromWkbsFile(where) 查询获取需要的省市区边界WKT文本数据。
+ * 
+ * 部分原理:
+ *      1. 初始化时,会将边界图形按网格动态的切分成小的图形,大幅减少查询时的几何计算量从而性能优异;
+ *      2. 内存中只会保存小的图形的外接矩形(Envelope),小的图形本身会序列化成WKB数据(根据Init方式存入文件或内存),因此内存占用很低;
+ *      3. 内存中的外接矩形(Envelope)数据会使用jts的STRTree索引,几何计算查询时,先从EnvelopeSTRTree中初步筛选出符合条件的边界,RTree性能极佳,大幅过滤掉不相关的边界;
+ *      4. 对EnvelopeSTRTree初步筛选出来的边界,读取出WKB数据反序列化成小的图形,然后进行精确的几何计算(因为是小图,所以读取和计算性能极高)。
+ * 
+ * jts库地址:https://github.com/locationtech/jts
+ * </pre>
+ * 
+ * <br>GitHub: https://github.com/xiangyuecn/AreaCity-Query-Geometry (github可换成gitee)
+ * <br>省市区县乡镇区划边界数据: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee)
+ */
+public class AreaCityQuery {
+	/** 默认提供的0-9的10个静态实例,每个实例可以分别使用一个数据文件进行初始化和查询,当然自己调用new AreaCityQuery()创建一个新实例使用也是一样的 */
+	static public final AreaCityQuery[] Instances=new AreaCityQuery[] {
+			new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery()
+			,new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery(),new AreaCityQuery()
+		};
+	
+	/**
+	 * 几何计算查询出包含此坐标点的所有边界图形的属性数据(和此坐标点相交):
+	 * <pre>
+	 * - 如果坐标点位于图形内部或边上,这个图形将匹配;
+	 * - 如果坐标点位于两个图形的边上,这两个图形将都会匹配;
+	 * - 如果图形存在孔洞,并且坐标点位于孔洞内(不含边界),这个图形将不匹配。
+	 * </pre>
+	 * 
+	 * <br>输入坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。
+	 * <br>如果还未完成初始化,或者查询出错,都会抛异常。
+	 * <br>本方法线程安全。
+	 * 
+	 * <br><br>注意:如果此坐标位于界线外侧(如海岸线、境界线)时将不会有边界图形能够匹配包含(就算距离只相差1cm),此时如果你希望能匹配到附近不远的边界图形,请使用QueryPointWithTolerance方法
+	 * 
+	 * @param lng 进度坐标值
+	 * @param lat 纬度坐标值
+	 * @param where 可以为null,可选提供一个函数,筛选属性数据(此数据已经过初步筛选),会传入属性的json字符串,如果需要去精确计算这个边界图形是否匹配就返回true,否则返回false跳过这条边界图形的精确计算
+	 * @param res 可以为null,如果提供结果对象,可通过此对象的Set_XXX属性控制某些查询行为,比如设置Set_ReturnWKTKey可以额外返回边界的WKT文本数据;并且本次查询的结果和统计数据将累加到这个结果内(性能测试用)。注意:此结果对象非线程安全
+	 */
+	public QueryResult QueryPoint(double lng, double lat, Func<String,Boolean> where, QueryResult res) throws Exception{
+		CheckInitIsOK();
+		return QueryGeometry(Factory.createPoint(new Coordinate(lng, lat)), where, res);
+	}
+	/**
+	 * 先几何计算查询出包含此坐标点的所有边界图形的属性数据,此时和QueryPoint方法功能完全一致。
+	 * <br><br>当没有边界图形包含此坐标点时,会查询出和此坐标点距离最近的边界图形的属性数据,同一级别的边界图形只会返回距离最近的一条属性数据,比如:范围内匹配到多个市,只返回最近的一个市;级别的划分依据为属性中的deep值,deep值为空的为同的一级
+	 * ;结果属性中会额外添加PointDistance(图形与坐标的距离,单位米)、PointDistanceID(图形唯一标识符)两个值;由于多进行了一次范围查询,性能会比QueryPoint方法低些。
+	 * <br><br>本方法主要用途是解决:当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),本方法将能够匹配到附近不远的边界图形数据。
+	 * 
+	 * <br><br>更多参数文档请参考QueryPoint方法,本方法线程安全。
+	 * 
+	 * @see #QueryPoint(double, double, Func, QueryResult)
+	 * @param toleranceMetre 距离范围容差值,单位米,比如取值2500,相当于一个以此坐标为中心点、半径为2.5km的圆形范围;当没有任何边界图形包含此坐标点时,会查询出与此坐标点的距离不超过此值 且 距离最近的边界图形属性数据;取值为0时不进行范围查找;取值为-1时不限制距离大小,会遍历所有数据导致性能极低
+	 */
+	public QueryResult QueryPointWithTolerance(double lng, double lat, Func<String,Boolean> where, QueryResult res, int toleranceMetre) throws Exception {
+		CheckInitIsOK();
+		if(res!=null && res.Result==null) throw new Exception("不支持无Result调用");
+		
+		int resLen0=res==null?0:res.Result.size();
+		Point point=Factory.createPoint(new Coordinate(lng, lat));
+		QueryResult res1=QueryGeometry(point, where, res);
+		if(res1.Result.size()>resLen0 || toleranceMetre==0) {
+			return res1; //查找到了的就直接返回
+		}
+		
+		Geometry geom;
+		if(toleranceMetre>0) { //以点为中心,容差为半径,构造出一个圆,扩大到容差范围进行查找
+			geom=CreateSimpleCircle(lng, lat, toleranceMetre, 24);
+		} else { //不限制范围
+			geom=CreateRect(-180, -90, 180, 90);
+		}
+		HashMap<String, Double> propDists=new HashMap<>();
+		HashMap<String, Object[]> deepDists=new HashMap<>();
+		DecimalFormat df=new DecimalFormat("0.00");
+		res1.QueryCount--;
+		res1=QueryGeometryProcess(geom, where, res1, new Func<Object[], Boolean>(){
+			@Override
+			public Boolean Exec(Object[] args) throws Exception {
+				boolean add=false;
+				String prop=(String)args[0];
+				Geometry geom=(Geometry)args[1];
+				String lineNo=(String)args[2];
+				
+				Coordinate[] ps=DistanceOp.nearestPoints(geom, point);
+				double dist=Distance(ps[0].x, ps[0].y, ps[1].x, ps[1].y);
+				Double exists=propDists.get(lineNo);
+				if(exists==null || exists>dist) {//去重,相同一条数据只取距离最近的
+					Matcher m=Exp_OkGeoCsv_Deep.matcher(prop);
+					String deep=m.find()?m.group(1):"";
+					Object[] deepExists=deepDists.get(deep);
+					if(deepExists==null || (double)deepExists[0]>dist) {//去重,同一级别只取距离最近的
+						add=true;
+						propDists.put(lineNo, dist);
+						deepDists.put(deep, new Object[] { dist, lineNo });
+						prop=prop.substring(0, prop.length()-1)+", \"PointDistanceID\": "+lineNo+", \"PointDistance\": "+df.format(dist)+"}";
+						args[0]=prop;
+					}
+				}
+				return add;
+			}
+		});
+		//清理掉结果中多余的数据,每一级取一个,同一数据取最后一个
+		HashSet<String> ids=new HashSet<>(), exists=new HashSet<>();
+		for(Object[] o : deepDists.values()) ids.add((String)o[1]);
+		for(int i=res1.Result.size()-1;i>=resLen0;i--) {
+			String prop=res1.Result.get(i);
+			Matcher m=Exp_PointDistanceID.matcher(prop); m.find();
+			String lineNo=m.group(1);
+			if(!ids.contains(lineNo) || exists.contains(lineNo)) {
+				res1.Result.remove(i);
+			}else {
+				exists.add(lineNo);
+			}
+		}
+		return res1;
+	}
+	static private Pattern Exp_PointDistanceID=Pattern.compile("\"PointDistanceID[\\s\":]+(\\d+)");
+	static private Pattern Exp_OkGeoCsv_Deep=Pattern.compile("\"deep[\\s\":]+(\\d+)");
+	
+	
+	/**
+	 * 几何计算查询出和此图形(点、线、面)有交点的所有边界图形的属性数据(包括边界相交)。
+	 * <br>
+	 * <br>所有输入坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。
+	 * <br>如果还未完成初始化,或者查询出错,都会抛异常。
+	 * <br>本方法线程安全。
+	 * 
+	 * @param geom 任意格式的图形对象(点、线、面),可以通过wkt文本进行构造:geom=new WKTReader(AreaCityQuery.Factory).read("wkt字符串")
+	 * @param where 可以为null,可选提供一个函数,筛选属性数据(此数据已经过初步筛选),会传入属性的json字符串,如果需要去精确计算这个边界图形是否匹配就返回true,否则返回false跳过这条边界图形的精确计算
+	 * @param res 可以为null,如果提供结果对象,可通过此对象的Set_XXX属性控制某些查询行为,比如设置Set_ReturnWKTKey可以额外返回边界的WKT文本数据;并且本次查询的结果和统计数据将累加到这个结果内(性能测试用)。注意:此结果对象非线程安全
+	 */
+	public QueryResult QueryGeometry(Geometry geom, Func<String,Boolean> where, QueryResult res) throws Exception{
+		return QueryGeometryProcess(geom, where, res, null);
+	}
+	/**
+	 * 几何计算查询出和此图形(点、线、面)有交点的所有边界图形的属性数据(包括边界相交)。
+	 * <br>
+	 * <br>参数功能和QueryGeometry方法一致,多了一个process参数允许在匹配计算时进行自定义计算处理
+	 * <br>更多参数文档请参考QueryGeometry方法,本方法线程安全。
+	 * 
+	 * @see #QueryGeometry(Geometry, Func, QueryResult)
+	 * @param process 当一条数据经过精确匹配后,加入到结果中前,会调用此函数进行自定义计算,返回true继续加入到结果中,返回false丢弃这条数据;提供本函数后的查询性能会比不提供时低些,因为未去重增加了重复计算量。
+	 *            <br><br><b>注意:初始化时一个完整边界图形会在网格划分后产生多个小图形,匹配的每个小图形都会算作一条数据参与自定义计算,会导致结果数据重复,因此需要自行对结果数据进行去重</b>
+	 *            <br><br>参数为一个数组:
+	 *            <br>[0]String:可读写,当前数据属性的json字符串,修改后的json内容会放到结果中
+	 *            <br>[1]Geometry:当前数据的图形对象,用于计算,为网格划分后的小图形
+	 *            <br>[2]String:为当前数据对应的完整图形的唯一标识符,用于数据去重
+	 */
+	public QueryResult QueryGeometryProcess(Geometry geom, Func<String,Boolean> where, QueryResult res, Func<Object[], Boolean> process) throws Exception{
+		CheckInitIsOK();
+		if(res==null) res=new QueryResult();
+		res.QueryCount++;
+		long t_Start=System.nanoTime();
+		if(res.StartTimeN==0) res.StartTimeN=t_Start;
+		
+		boolean returnWkt=res.Set_ReturnWKTKey!=null && res.Set_ReturnWKTKey.length()>0;
+		if(returnWkt && WkbsFilePath.length()==0) {
+			throw new Exception("Set_ReturnWKT错误,初始化时必须保存了wkbs结构化数据文件,或者用的wkbs文件初始化的,否则不允许查询WKT数据");
+		}
+		
+		//先查找Envelope,基本不消耗时间
+		@SuppressWarnings("rawtypes")
+		List list=EnvelopeSTRTree.query(geom.getEnvelopeInternal());
+		res.DurationN_EnvelopeHitQuery+=System.nanoTime()-t_Start;
+		res.EnvelopeHitCount+=list.size();
+		
+		//进行精确查找
+		String matchLines=",";
+		for(int i=0,len=list.size();i<len;i++) {
+			@SuppressWarnings("unchecked")
+			Map<String, Object> store=(Map<String, Object>)list.get(i);
+
+			byte[] wkbSub=null;
+			String[] wkbPos=getWkbPos(store);
+			String lineNo=wkbPos[0];
+			int fullPos=Integer.parseInt(wkbPos[1]);
+			int subPos=Integer.parseInt(wkbPos[2]);
+			
+			//如果wkb对应的这条数据已经有一个sub匹配了,就不需要在继续查询
+			if(process==null && matchLines.indexOf(","+lineNo+",")!=-1) {//提供了process自定义处理,不去重
+				continue;
+			}
+			
+			//提供了where筛选
+			if(where!=null) {
+				if(!where.Exec(getProp(store))) {
+					continue;
+				}
+			}
+			
+			//读取wkb数据
+			long t_IO=System.nanoTime();
+			Geometry subGeom=null;
+			if(ReadFromMemory) {
+				//从内存中得到wkb数据 或 直接存的对象
+				if(SetInitStoreInMemoryUseObject) {
+					subGeom=(Geometry)store.get("wkb");
+				} else {
+					wkbSub=(byte[])store.get("wkb");
+				}
+			} else {
+				wkbSub=ReadWkbFromFile(subPos);
+			}
+			res.DurationN_IO+=System.nanoTime()-t_IO;
+			
+			//转换回图形
+			long t_GeometryParse=System.nanoTime();
+			if(subGeom==null) {
+				subGeom=new WKBReader(Factory).read(wkbSub);
+			}
+			res.DurationN_GeometryParse+=System.nanoTime()-t_GeometryParse;
+			
+			//是否精确匹配
+			long t_Exact=System.nanoTime();
+			boolean isMatch=subGeom.intersects(geom);
+			res.DurationN_ExactHitQuery+=System.nanoTime()-t_Exact;
+			if(isMatch) {
+				String prop=getProp(store);
+				if(process!=null) { // 自定义计算
+					t_Exact=System.nanoTime();
+					Object[] args=new Object[] { prop, subGeom, lineNo };
+					if(!process.Exec(args)) {
+						isMatch=false;
+					} else {
+						prop=(String)args[0];
+					}
+					res.DurationN_ExactHitQuery+=System.nanoTime()-t_Exact;
+				}
+				
+				if(isMatch) {
+					if(returnWkt) { // 需要同时返回完整图形的wkt数据
+						t_IO=System.nanoTime();
+						byte[] wkbFull=ReadWkbFromFile(fullPos);
+						res.DurationN_IO+=System.nanoTime()-t_IO;
+						
+						t_GeometryParse=System.nanoTime();
+						Geometry fullGeom=new WKBReader(Factory).read(wkbFull);
+						res.DurationN_GeometryParse+=System.nanoTime()-t_GeometryParse;
+						
+						String wkt=new WKTWriter().write(fullGeom);
+						prop=prop.substring(0, prop.length()-1)+", \""+res.Set_ReturnWKTKey+"\": \""+wkt+"\"}";
+					}
+					
+					if(res.Result!=null) {
+						res.Result.add(prop);
+					}
+					res.ExactHitCount++;
+					
+					matchLines+=lineNo+",";
+				}
+			}
+			
+			if(res.Set_EnvelopeHitResult!=null) {//将初步筛选的结果存入数组,如果要求了的话
+				String prop=getProp(store);
+				prop="{\"_PolygonPointNum_\": "+subGeom.getNumPoints()+","+prop.substring(1);
+				res.Set_EnvelopeHitResult.add(prop);
+			}
+		}
+		
+		res.EndTimeN=System.nanoTime();
+		return res;
+	}
+	
+	
+	
+	/**
+	 * 遍历所有边界图形的属性列表查询出符合条件的属性,然后返回图形的属性+边界图形WKT文本。
+	 * <br>读取到的wkt文本,可以直接粘贴到页面内渲染显示:https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html
+	 * <br>本方法可以用来遍历所有数据,提取感兴趣的属性内容(wktKey传null只返回属性),比如查询一个区划编号id对应的城市信息(城市名称、中心点)
+	 * 
+	 * <br>
+	 * <br>注意:初始化时必须保存了wkbs结构化数据文件,或者用的wkbs文件初始化的,否则不允许查询WKT数据。
+	 * <br>如果还未完成初始化,或者查询出错,都会抛异常。
+	 * <br>本方法线程安全。
+	 * 
+	 * @param wktKey 可以为null,比如填:wkt、polygon,作为json里的key: 存放wkt文本数据;如果传入空值,将只返回属性,不查询wkt文本数据;此参数会覆盖res.Set_ReturnWKTKey值
+	 * @param res 可以为null,如果提供结果对象,可通过此对象的Set_XXX属性控制某些查询行为,并且本次查询的结果和统计数据将累加到这个结果内(性能测试用)。注意:此结果对象非线程安全
+	 * @param where 必须提供一个函数,筛选属性数据(所有数据全过一遍),会传入属性的json字符串,如果需要匹配这个边界图形就返回true,否则返回false跳过这条边界图形
+	 * @param onFind 可选提供一个回调函数,每次查询到一条wkt数据后会通过onFind回传,String[]参数为[prop,wkt];如果返回false数据将不会存入res结果中(也会忽略wktKey参数),需在回调中自行处理数据
+	 */
+	public QueryResult ReadWKT_FromWkbsFile(String wktKey, QueryResult res, Func<String,Boolean> where, Func<String[], Boolean> onFind) throws Exception{
+		CheckInitIsOK();
+		if(res==null) res=new QueryResult();
+		res.QueryCount++;
+		long t_Start=System.nanoTime();
+		if(res.StartTimeN==0) res.StartTimeN=t_Start;
+		
+		res.Set_ReturnWKTKey=wktKey;
+		boolean returnWkt=res.Set_ReturnWKTKey!=null && res.Set_ReturnWKTKey.length()>0;
+		boolean readWkt=returnWkt;
+		if(onFind!=null) {
+			readWkt=true;
+		}
+		if(readWkt && WkbsFilePath.length()==0) {
+			throw new Exception("初始化时必须保存了wkbs结构化数据文件,或者用的wkbs文件初始化的,否则不允许查询WKT数据");
+		}
+		
+		for(int i=0,iL=WKTDataStores.size();i<iL;i++) {
+			HashMap<String, Object> store=WKTDataStores.get(i);
+			
+			//属性是否符合条件
+			long t_Exact=System.nanoTime();
+			String prop=getProp(store);
+			boolean isFind=where.Exec(prop);
+			res.DurationN_ExactHitQuery+=System.nanoTime()-t_Exact;
+			if(!isFind) {
+				continue;
+			}
+			
+			String wkt=null;
+			if(readWkt) {
+				//读取wkb
+				byte[] wkbFull=null;
+				if(!store.containsKey("empty")) {
+					String[] wkbPos=getWkbPos(store);
+					int fullPos=Integer.parseInt(wkbPos[1]);
+					
+					long t_IO=System.nanoTime();
+					wkbFull=ReadWkbFromFile(fullPos);
+					res.DurationN_IO+=System.nanoTime()-t_IO;
+				}
+				
+				//转换回图形
+				long t_GeometryParse=System.nanoTime();
+				Geometry fullGeom;
+				if(wkbFull!=null) {
+					fullGeom=new WKBReader(Factory).read(wkbFull);
+				} else {
+					fullGeom=Factory.createPolygon();
+				}
+				
+				//生成wkt
+				wkt=new WKTWriter().write(fullGeom);
+				res.DurationN_GeometryParse+=System.nanoTime()-t_GeometryParse;
+			}
+			
+			boolean add=true;
+			if(onFind!=null) {
+				add=onFind.Exec(new String[] { prop, wkt });
+			}
+			if (add && res.Result!=null) {
+				if(returnWkt) {
+					prop=prop.substring(0, prop.length()-1)+", \""+res.Set_ReturnWKTKey+"\": \""+wkt+"\"}";
+				}
+				res.Result.add(prop);
+			}
+			res.ExactHitCount++;
+		}
+		
+		res.EndTimeN=System.nanoTime();
+		return res;
+	}
+	
+	
+	
+	/**
+	 * 调试用的,读取已在wkbs结构化文件中保存的网格划分图形WKT数据,用于核对网格划分情况。
+	 * <br>读取到的wkt文本,可以直接粘贴到页面内渲染显示:https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html
+	 * 
+	 * @param wktKey 可以为null,比如填:wkt、polygon,作为json里的key: 存放wkt文本数据;如果传入空值,将只返回属性,不查询wkt文本数据;此参数会覆盖res.Set_ReturnWKTKey值
+	 * @param res 可以为null,如果提供结果对象,可通过此对象的Set_XXX属性控制某些查询行为,并且本次查询的结果和统计数据将累加到这个结果内(性能测试用)。注意:此结果对象非线程安全
+	 * @param where 必须提供一个函数,筛选属性数据(所有数据全过一遍),会传入属性的json字符串,如果需要匹配这个边界图形就返回true,否则返回false跳过这条边界图形
+	 * @param onFind 可选提供一个回调函数,每次查询到一条wkt数据后会通过onFind回传,String[]参数为[prop,wkt];如果返回false数据将不会存入res结果中(也会忽略wktKey参数),需在回调中自行处理数据
+	 */
+	public QueryResult Debug_ReadGeometryGridSplitsWKT(String wktKey, QueryResult res, Func<String,Boolean> where, Func<String[], Boolean> onFind) throws Exception {
+		CheckInitIsOK();
+		if(res==null) res=new QueryResult();
+		res.QueryCount++;
+		long t_Start=System.nanoTime();
+		if(res.StartTimeN==0) res.StartTimeN=t_Start;
+		
+		res.Set_ReturnWKTKey=wktKey;
+		boolean returnWkt=res.Set_ReturnWKTKey!=null && res.Set_ReturnWKTKey.length()>0;
+		boolean readWkt=returnWkt;
+		if(onFind!=null) {
+			readWkt=true;
+		}
+		if(readWkt && WkbsFilePath.length()==0) {
+			throw new Exception("初始化时必须保存了wkbs结构化数据文件,或者用的wkbs文件初始化的,否则不允许查询WKT数据");
+		}
+		
+		for(int i=0,iL=WKTDataStores.size();i<iL;i++) {
+			HashMap<String, Object> store=WKTDataStores.get(i);
+
+			//属性是否符合条件
+			long t_Exact=System.nanoTime();
+			String prop=getProp(store);
+			boolean isFind=where.Exec(prop);
+			res.DurationN_ExactHitQuery+=System.nanoTime()-t_Exact;
+			if(!isFind) {
+				continue;
+			}
+			
+			String wkt=null;
+			if(readWkt) {
+				String[] wkbPos=getWkbPos(store);
+				ArrayList<Integer> subs=LineSubsPos.get(wkbPos[0]);
+				if(subs==null) {
+					continue;
+				}
+				
+				//读取所有的切块,转换回图形
+				ArrayList<Polygon> pols=new ArrayList<Polygon>();
+				for(int i2=0,i2L=subs.size();i2<i2L;i2++) {
+					long t_IO=System.nanoTime();
+					byte[] wkb=ReadWkbFromFile(subs.get(i2));
+					res.DurationN_IO+=System.nanoTime()-t_IO;
+					
+					long t_GeometryParse=System.nanoTime();
+					Geometry subGeom=new WKBReader(Factory).read(wkb);
+					
+					if(subGeom instanceof Polygon) {
+						pols.add((Polygon)subGeom);
+					} else {
+						for(int i3=0,i3L=subGeom.getNumGeometries();i3<i3L;i3++) {
+							pols.add((Polygon)subGeom.getGeometryN(i3));
+						}
+					}
+					res.DurationN_GeometryParse+=System.nanoTime()-t_GeometryParse;
+				}
+				Geometry geom;
+				if(pols.size()==0) {
+					geom=Factory.createPolygon();
+				} else {
+					geom=Factory.createMultiPolygon(pols.toArray(new Polygon[0]));
+				}
+				wkt=new WKTWriter().write(geom);
+			}
+			
+			boolean add=true;
+			if(onFind!=null) {
+				add=onFind.Exec(new String[] { prop, wkt });
+			}
+			if (add && res.Result!=null) {
+				if(returnWkt) {
+					prop=prop.substring(0, prop.length()-1)+", \""+res.Set_ReturnWKTKey+"\": \""+wkt+"\"}";
+				}
+				res.Result.add(prop);
+			}
+			res.ExactHitCount++;
+		}
+		
+		res.EndTimeN=System.nanoTime();
+		return res;
+	}
+	
+	
+	
+	
+	/**
+	 * 用加载数据到内存的模式进行初始化,边界图形数据存入内存中(内存占用和json数据文件大小差不多大,查询性能极高);本方法可以反复调用但只会初始化一次,每次查询前都调用即可(查询会在初始化完成后进行)
+	 * <pre>
+	 * 支持文件(utf-8):
+	 *  - *.wkbs saveWkbsFilePath生成的结构化数据文件,读取效率高。
+	 *  - *.json geojson文件,要求里面数据必须是一行一条数据
+	 *                     ,第一条数据的上一行必须是`"features": [`
+	 *                     ,最后一条数据的下一行必须是`]`打头
+	 *                     ,否则不支持解析,可尝试用文本编辑器批量替换添加换行符。
+	 * </pre>
+	 * 默认在内存中存储的是wkb格式数据(大幅减少内存占用),查询时会将wkb还原成图形对象,可通过设置 Instances[0-9].SetInitStoreInMemoryUseObject=true 来关闭这一过程减少性能损耗,在内存中直接存储图形对象,但内存占用会增大一倍多。
+	 * 
+	 * @param dataFilePath 数据文件路径(支持:*.wkbs、*.json),从这个文件读取数据;如果autoUseExistsWkbsFile=true并且saveWkbsFilePath文件存在时(已生成了结构化数据文件),可以不提供此参数
+	 * @param saveWkbsFilePath 可选提供一个.wkbs后缀的文件路径:dataFile是wkbs时不可以提供;dataFile是geojson时,加载geojson解析的数据会自动生成此结构化数据文件;如果和dataFile都不提供wkbs文件时查询中将不允许获取WKT数据
+	 * @param autoUseExistsWkbsFile 当传true时:如果检测到saveWkbsFilePath对应文件已成功生成过了,将直接使用这个wkbs文件作为dataFile(直接忽略dataFilePath参数);建议传true,这样只需要首次加载生成了结构文件,以后读取数据都非常快(数据更新时需删除wkbs文件)
+	 */
+	public void Init_StoreInMemory(String dataFilePath, String saveWkbsFilePath, boolean autoUseExistsWkbsFile) {
+		__Init(autoUseExistsWkbsFile, dataFilePath, saveWkbsFilePath, true);
+	}
+	/**
+	 * 用加载数据到结构化数据文件的模式进行初始化,推荐使用本方法初始化,边界图形数据存入结构化数据文件中,内存占用很低(查询时会反复读取文件对应内容,查询性能消耗主要在IO上,IO性能极高问题不大);本方法可以反复调用但只会初始化一次,每次查询前都调用即可(查询会在初始化完成后进行)
+	 * <pre>
+	 * 支持文件(utf-8):
+	 *  - *.wkbs saveWkbsFilePath生成的结构化数据文件,读取效率高。
+	 *  - *.json geojson文件,要求里面数据必须是一行一条数据
+	 *                     ,第一条数据的上一行必须是`"features": [`
+	 *                     ,最后一条数据的下一行必须是`]`打头
+	 *                     ,否则不支持解析,可尝试用文本编辑器批量替换添加换行符。
+	 * </pre>
+	 * 
+	 * @param dataFilePath 数据文件路径(支持:*.wkbs、*.json),从这个文件读取数据;如果autoUseExistsWkbsFile=true并且saveWkbsFilePath文件存在时(已生成了结构化数据文件),可以不提供此参数
+	 * @param saveWkbsFilePath 不提供,或一个.wkbs后缀的文件路径:dataFile是wkbs时不可以提供;dataFile是geojson时,必须提供,加载geojson解析的数据会存入此文件
+	 * @param autoUseExistsWkbsFile 当传true时:如果检测到saveWkbsFilePath对应文件已成功生成过了,将直接使用这个wkbs文件作为dataFile(直接忽略dataFilePath参数);建议传true,这样只需要首次加载生成了结构文件,以后读取数据都非常快(数据更新时需删除wkbs文件)
+	 */
+	public void Init_StoreInWkbsFile(String dataFilePath, String saveWkbsFilePath, boolean autoUseExistsWkbsFile) {
+		__Init(autoUseExistsWkbsFile, dataFilePath, saveWkbsFilePath, false);
+	}
+	
+	
+	
+	
+	
+	
+	
+	/** 版本号,主要用于wkbs结构化文件的版本 **/
+	static public final String Version="1.0";
+	
+	/** 性能优化的重要参数,用于将大的边界按网格拆分成小的边界,这个参数决定了每个小边界的坐标点数在这个值附近
+	 * <br>取值越小,查询性能越高;初始化拆分出来的Polygon会越多,占用内存也会相应增多,解析json文件、或生成wkbs文件会比较耗时。
+	 * <br>取值越大,查询性能越低;初始化拆分出来的Polygon会越少,占用内存也会越少,解析json文件、或生成wkbs文件会比较快。
+	 * <br>如果不清楚作用,请勿调整此参数;修改后,之前生成的wkbs结构化文件均会失效,初始化时会重新生成。
+	 * **/
+	public int SetGridFactor=100;
+	
+	/** init时允许使用的最大线程数量,默认为不超过5 并且 不超过cpu核心数-1;线程数不要太多, 默认就好**/
+	public int SetInitUseThreadMax=5;
+	
+	/** init采用的Init_StoreInMemory时,图形数据直接存到内存,不要转成wkb压缩内存,可进一步提升性能,但会增大一倍多的内存占用 **/
+	public boolean SetInitStoreInMemoryUseObject=false;
+	
+	/**
+	 * init状态:0未初始化,1初始化中,2初始化完成,3初始化失败(InitInfo.ErrMsg为错误消息)
+	 */
+	public int GetInitStatus() {
+		return InitLock[0];
+	}
+	/** 检查init状态是否是2已初始化完成,未完成会抛出错误原因 **/
+	public void CheckInitIsOK() throws Exception {
+		if(InitLock[0]==3) {
+			throw new Exception(InitInfo.ErrMsg);
+		}
+		if(InitLock[0]!=2) {
+			throw new Exception("需要先Init完成后,再来进行查询调用");
+		}
+	}
+	/** 将init状态设置为0(未初始化),允许重新Init **/
+	public void ResetInitStatus() {
+		synchronized (InitLock) {
+			InitLock[0] = 0;
+			EnvelopeSTRTree = null;
+			WKTDataStores = null;
+			LineSubsPos = null;
+		}
+	}
+	
+	
+	/** 是否是通过Init_StoreInMemory初始化的 **/
+	public boolean IsStoreInMemory() {
+		return GetInitStatus()==2 && ReadFromMemory;
+	}
+	/** 是否是通过Init_StoreInWkbsFile初始化的 **/
+	public boolean IsStoreInWkbsFile() {
+		return GetInitStatus()==2 && !ReadFromMemory;
+	}
+	
+	/**
+	 * init时的回调,可以绑定一个函数,接收InitInfo进度信息,回调时机:
+	 * <pre>
+	 * - 每处理一行数据会回调一次,返回false可以跳过处理一行数据,此时initInfo.CurrentLine_XX全部有值
+	 * - 处理完成时会回调一次(此时initInfo.CurrentLine_XX全部为空)
+	 * </pre>
+	 * 此回调线程安全。
+	 */
+	public Func<QueryInitInfo, Boolean> OnInitProgress;
+	/**
+	 * init时的进度信息
+	 */
+	public QueryInitInfo GetInitInfo() {
+		return InitInfo;
+	}
+	private QueryInitInfo InitInfo;
+	
+	
+	
+	
+	
+	
+	/** jts的factory,可以用来创建Geometry **/
+	static public GeometryFactory Factory=new GeometryFactory(new PrecisionModel(), 4326);
+	
+	
+	private int[] InitLock=new int[] { 0 };//0未初始化,1初始化中,2初始化完成,3初始化失败
+	private boolean ReadFromMemory;
+	private String WkbsFilePath;
+	private STRtree EnvelopeSTRTree; //所有图形的外接矩形索引
+	private List<HashMap<String,Object>> WKTDataStores; //WKT查询时需要读取的属性列表
+	private HashMap<String, ArrayList<Integer>> LineSubsPos; //每行数据grid拆分后的数据在wkbs里面的存储位置
+	private void __Init(boolean autoUseExistsWkbsFile, String dataFilePath, String saveWkbsFilePath, boolean readFromMemory) {
+		if(InitLock[0] >= 2) {
+			return;
+		}
+		synchronized (InitLock) {
+			if(InitLock[0] >= 2) {
+				return;
+			}
+			FileOutputStream fw=null;
+			FileInputStream fr=null;
+			BufferedReader read=null;
+			
+			InitLock[0]=1;
+			try {
+				InitInfo=new QueryInitInfo();
+				InitInfo.StartTimeN = System.nanoTime();
+				InitInfo.StartMemory_System = GetMemory_System();
+				InitInfo.StartMemory_JavaRuntime = GetMemory_JavaRuntime();
+				
+				ReadFromMemory=readFromMemory;
+				WkbsFilePath="";
+				
+				dataFilePath=dataFilePath==null?"":dataFilePath;
+				saveWkbsFilePath=saveWkbsFilePath==null?"":saveWkbsFilePath;
+				if(saveWkbsFilePath.length()>0) {
+					WkbsFilePath=saveWkbsFilePath;
+				}else if(IsWkbsFilePath(dataFilePath)) {
+					WkbsFilePath=dataFilePath;
+				}else if(!ReadFromMemory){
+					throw new Exception("Init_StoreInWkbsFile传入非wkbs文件时,必须提供saveWkbsFilePath");
+				}
+				if(saveWkbsFilePath.length()>0) {
+					if(!IsWkbsFilePath(saveWkbsFilePath)) {
+						throw new Exception("saveWkbsFilePath必须是.wkbs结尾");
+					}
+					if(IsWkbsFilePath(dataFilePath)) {
+						throw new Exception("dataFilePath是.wkbs文件时,不允许再提供saveWkbsFilePath");
+					}
+
+					if(autoUseExistsWkbsFile){//如果wkbs文件已存在,并且有效,就直接读取这个文件的数据
+						if(AvailableWkbsFile(saveWkbsFilePath)) {
+							dataFilePath=saveWkbsFilePath;
+							saveWkbsFilePath="";
+						}
+					}
+				}
+				InitInfo.DataFromWkbsFile=IsWkbsFilePath(dataFilePath);
+				InitInfo.HasWkbsFile=WkbsFilePath.length()>0;
+				InitInfo.FilePath_Data=dataFilePath;
+				InitInfo.FilePath_SaveWkbs=saveWkbsFilePath;
+				
+				//打开文件
+				fr=new FileInputStream(dataFilePath);
+				read=new BufferedReader(new InputStreamReader(fr, "utf-8"));
+				if(saveWkbsFilePath.length()>0) {
+					fw=new FileOutputStream(saveWkbsFilePath);
+				}
+				
+				__InitProcess(dataFilePath, read, saveWkbsFilePath, fw);
+				
+				EnvelopeSTRTree.build();//立即生成索引树
+				
+				InitLock[0]=2;
+			} catch (Exception e) {
+				InitInfo.ErrMsg="初始化发生异常:"+ErrorStack(e);
+				InitLock[0]=3;
+			} finally {
+				try { if(fw!=null) fw.close(); } catch(Exception e) {}
+				try { if(fr!=null) fr.close(); } catch(Exception e) {}
+				try { if(read!=null) read.close(); } catch(Exception e) {}
+				
+				long t_gc=System.nanoTime();
+				System.gc();//强制回收内存
+				InitInfo.DurationN_JavaGC=System.nanoTime()-t_gc;
+
+				InitInfo.CurrentLine_No=0;
+				InitInfo.CurrentLine_Text="";
+				InitInfo.CurrentLine_Prop="";
+				
+				InitInfo.EndTimeN = System.nanoTime();
+				InitInfo.EndMemory_System = GetMemory_System();
+				InitInfo.EndMemory_JavaRuntime = GetMemory_JavaRuntime();
+			}
+			
+			//初始化完成了,回调一下进度
+			if(OnInitProgress!=null) {
+				try {
+					OnInitProgress.Exec(InitInfo);
+				} catch (Exception e) { }
+			}
+		}
+	}
+	private void __InitProcess(String dataFilePath, BufferedReader dataFile, String saveWkbsFilePath, FileOutputStream saveWkbsFile) throws Exception {
+		Exception[] threadError=new Exception[] { null };
+		
+		STRtree rtree=new STRtree();
+		List<HashMap<String,Object>> wktDataStores=new ArrayList<>();
+		List<HashMap<String,Object>> emptyGeoms=new ArrayList<>();
+		HashMap<String, ArrayList<Integer>> lineSubsPos=new HashMap<>();
+		
+		boolean isWkbsFile=IsWkbsFilePath(dataFilePath);
+		String IsStartErrMsg="未识别到geojson|wkbs数据,请检查初始化传入的文件是否正确。"
+					+"注意:如果是geojson文件,要求里面数据必须是一行一条数据"
+					+",第一条数据的上一行必须是`\"features\": [`,最后一条数据的下一行必须是`]`打头"
+					+",否则不支持解析,可尝试用文本编辑器批量替换添加换行符。";
+		boolean[] IsStart=new boolean[] { false };
+		boolean[] IsEnd=new boolean[] { false };
+		int[] LineNo=new int[] { 0 };
+		HashMap<String, String[]> Strings=new HashMap<>();//prop字符串转成引用类型
+		
+		//写入wkbs文件,并记录已写入长度
+		int[] saveWkbsFileLength=new int[] { 0 };
+		Func<String, Object> SaveWkbsWrite=new Func<String, Object>() {
+			@Override
+			public Object Exec(String val) throws Exception {
+				byte[] bs=val.getBytes("utf-8");
+				saveWkbsFileLength[0]+=bs.length;
+				saveWkbsFile.write(bs);
+				return null;
+			}
+		};
+		
+		Func<Object, Object> ThreadExec=new Func<Object, Object>() {
+			@Override
+			public String Exec(Object val) throws Exception {
+				while(true) {
+					int lineNo;
+					String line;
+					synchronized (dataFile) {//先读取一行内容,文件内容之类的识别不允许并行
+						if(threadError[0]!=null) throw threadError[0];
+						if(IsEnd[0]) break;
+						
+						long t_fr=System.nanoTime();
+						line=dataFile.readLine();
+						InitInfo.DurationN_FileRead+=System.nanoTime()-t_fr;
+						if(line==null) {
+							//没有数据了
+							if(!IsStart[0]){
+								throw new Exception(IsStartErrMsg);
+							}
+							if(!IsEnd[0]){
+								throw new Exception("初始化传入的文件未发现结束位置,可能文件已损坏");
+							}
+							break;
+						}
+						lineNo=++LineNo[0];
+						line=line.trim();
+						if(line.length()==0)continue;
+
+						if(IsStart[0] && line.charAt(0)==']'){
+							//处理完成所有数据
+							IsEnd[0]=true;
+							break;
+						}
+						if(!IsStart[0]){
+							//等待开始标志
+							int fIdx=line.indexOf("\"features\"");
+							if(fIdx==0 || fIdx>0 && fIdx>=line.length()-14){
+								if(!line.endsWith("[")){
+									throw new Exception("初始化传入的文件第"+lineNo+"行风格不对,不支持处理此文件");
+								}
+								IsStart[0]=true;
+								
+								if(saveWkbsFile!=null) {//写入 wkbs 文件头,这里无需同步操作
+									SaveWkbsWrite.Exec("/*******************"
+										+"\n本wkbs文件是由 "+AreaCityQuery.class.getTypeName()+" 生成,为专用的结构化数据文件,用于边界图形数据加速解析。"
+										+"\n@Version: "+Version
+										+"\n@GridFactor: "+SetGridFactor
+										+"\n@数据文件: "+dataFilePath
+										+"\n@生成时间: "+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+										+"\n"
+										+"\nGitHub: https://github.com/xiangyuecn/AreaCity-Query-Geometry (github可换成gitee)"
+										+"\n省市区县乡镇区划边界数据: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee)"
+										+"\n*******************/"
+										+"\n"
+										+"\n\"features\": [");
+								}
+							}
+							continue;
+						}
+					}// synchronized end
+					
+					//开始处理这一行数据
+
+					long r_t1=System.nanoTime();
+					//手工提取properties
+					String[] propStrPtr;
+					String propStr,wkbPosStr=lineNo+":0:0";
+					boolean wkbTypeIsParent=false,wkbTypeIsSub=false,wkbTypeIsEmpty=false;
+					int wkbIdx=0;
+					if(isWkbsFile){
+						int i0=line.indexOf(WKB_SP_Pos);
+						String wkbType=line.substring(0, i0);
+						if(wkbType.equals("Sub")) {
+							wkbTypeIsSub=true;
+						} else if(wkbType.equals("Full")) {
+							// NOOP
+						} else if(wkbType.equals("Parent")) {
+							wkbTypeIsParent=true;
+						} else if(wkbType.equals("Empty")) {
+							wkbTypeIsEmpty=true;
+						}
+
+						i0+=WKB_SP_Pos.length();
+						int i1=line.indexOf(WKB_SP_Prop, i0);
+						wkbPosStr=line.substring(i0, i1);
+						
+						i1+=WKB_SP_Prop.length();
+						int i2=line.indexOf(WKB_SP_WKB, i1);
+						propStr=line.substring(i1, i2);
+						wkbIdx=i2+WKB_SP_WKB.length();
+					} else {
+						int i0=line.indexOf("properties\"");
+						int i1=line.indexOf("{", i0);
+						int i2=line.indexOf("}", i0);
+						propStr=line.substring(i1, i2+1);
+					}
+					//手工提取geometry类型
+					String typeStr="";
+					if(!isWkbsFile){
+						int iGeom=line.indexOf("geometry\"");
+						
+						int i0=line.indexOf("type\"", iGeom);
+						int i1=line.indexOf("\"", i0+5);
+						int i2=line.indexOf("\"", i1+1);
+						typeStr=line.substring(i1+1, i2);
+					}
+					synchronized (InitInfo) {
+						InitInfo.DurationN_FileParse+=System.nanoTime()-r_t1;
+						
+						InitInfo.CurrentLine_No=lineNo;
+						InitInfo.CurrentLine_Text=line;
+						InitInfo.CurrentLine_Prop=propStr;
+
+						//回调一下,顺带看看需不需要解析这条数据
+						if(OnInitProgress!=null) {
+							if(!OnInitProgress.Exec(InitInfo)) {
+								continue;
+							}
+						}
+						
+						//在这个同步块里面顺带处理一下字符串转引用类型,减少字符串内存占用
+						propStrPtr=Strings.get(propStr);
+						if(propStrPtr==null) {
+							propStrPtr=new String[] { propStr };
+							Strings.put(propStr, propStrPtr);
+						}
+					}
+					
+					//wkbs里面的非Sub图形,完整图形
+					if(isWkbsFile && !wkbTypeIsSub) {
+						synchronized (InitInfo) {
+							InitInfo.GeometryCount++;
+						}
+						if(!wkbTypeIsEmpty) {//empty的丢到下面统一处理
+							HashMap<String,Object> store=new HashMap<>();
+							store.put("prop", propStrPtr);
+							store.put("wkbPos", wkbPosStr);
+							synchronized (wktDataStores) {
+								wktDataStores.add(store);//存好WKT查询数据,一个数据只存一条就行了
+							}
+						}
+						if(wkbTypeIsParent) {//已经拆分了,上级完整图形无需再处理
+							continue;
+						}
+					}
+
+					//手工创建图形对象
+					long r_t2=System.nanoTime();
+					Geometry geomSrc;
+					if(isWkbsFile){
+						byte[] wkb=Hex2Bytes(line, wkbIdx);
+						geomSrc=new WKBReader(Factory).read(wkb);
+					} else {
+						if(!(typeStr.equals("Polygon") || typeStr.equals("MultiPolygon"))) {
+							throw new Exception("初始化传入的文件第"+lineNo+"行"+typeStr+"数据不是Polygon类,要求必须是Polygon或者MultiPolygon,并且json文件内一条数据一行");
+						}
+						geomSrc=JSONLineParse(Factory, line);
+					}
+					synchronized (InitInfo) {
+						InitInfo.DurationN_GeometryParse+=System.nanoTime()-r_t2;
+						if(!isWkbsFile) {
+							InitInfo.GeometryCount++;
+						}
+						
+						if(geomSrc.isEmpty()){//空的存一下属性,边界就丢弃
+							HashMap<String,Object> store=new HashMap<>();
+							store.put("prop", propStrPtr);
+							store.put("wkbPos", wkbPosStr);
+							store.put("empty", true);
+							emptyGeoms.add(store);
+							continue;
+						}
+					}
+					
+					//创建索引,将每个图形放到rtree,图形如果坐标点过多,先按网格拆成小的
+					long r_t3=System.nanoTime();
+					Geometry geomGrid=geomSrc;
+					if(!isWkbsFile) { //wkbs文件已经拆好了,非wkbs才需要按网格拆成小的
+						geomGrid=GeometryGridSplit(Factory, geomSrc, SetGridFactor);
+					}
+					int wkbMemoryLen=0;
+					int polygonNum=1;
+					if(geomGrid instanceof MultiPolygon) {
+						polygonNum = geomGrid.getNumGeometries();
+					}
+					
+					int parentPos=0;
+					if(polygonNum>1 && saveWkbsFile!=null) {//有多个Polygon时,先存一个完整的父级
+						byte[] wkb=new WKBWriter().write(geomSrc);
+						synchronized (saveWkbsFile) {
+							parentPos=saveWkbsFileLength[0]+1;//+1 换行符
+							String wkbPos=lineNo+":"+parentPos+":"+parentPos; //编号:parent:sub 数据存储位置
+							
+							SaveWkbsWrite.Exec("\nParent"+WKB_SP_Pos+wkbPos+WKB_SP_Prop+propStr+WKB_SP_WKB+Bytes2Hex(wkb));
+						}
+					}
+					
+					for(int i0=0;i0<polygonNum;i0++) {
+						Polygon polygon;
+						if(geomGrid instanceof MultiPolygon) {//MultiPolygon 拆成 Polygon 减小范围
+							polygon=(Polygon)geomGrid.getGeometryN(i0);
+						}else{
+							polygon=(Polygon)geomGrid;
+						}
+						
+						byte[] wkb=null;
+						String wkbPos=lineNo+":0:0";//编号:parent:sub 数据存储位置
+						if(saveWkbsFile!=null) {//需要保存到文件
+							synchronized (saveWkbsFile) {
+								wkbPos=(saveWkbsFileLength[0]+1)+"";//+1 换行符
+								String type="Sub";
+								if(polygonNum==1) {//自己本身就是完整的,无需parent
+									type="Full";
+									wkbPos=wkbPos+":"+wkbPos;
+								} else {
+									wkbPos=parentPos+":"+wkbPos;
+								}
+								wkbPos=lineNo+":"+wkbPos;
+								wkb=new WKBWriter().write(polygon);
+								SaveWkbsWrite.Exec("\n"+type+WKB_SP_Pos+wkbPos+WKB_SP_Prop+propStr+WKB_SP_WKB+Bytes2Hex(wkb));
+							}
+						}
+
+						HashMap<String,Object> store=new HashMap<>();
+						store.put("prop", propStrPtr);
+						if(ReadFromMemory){//写入内存
+							if(SetInitStoreInMemoryUseObject) {
+								store.put("wkb", polygon);
+							}else {
+								if(wkb==null) {
+									wkb=new WKBWriter().write(polygon);
+								}
+								wkbMemoryLen+=wkb.length;
+								store.put("wkb", wkb);
+							}
+						}
+						
+						if(isWkbsFile) {//从wkbs文件读的数据,直接给数据位置值
+							wkbPos=wkbPosStr;
+						}
+						store.put("wkbPos", wkbPos);
+						String[] wkbPosArr=getWkbPos(store);
+						
+						//构造外接矩形,放到rtree里面,非线程安全需同步操作
+						synchronized (rtree) {
+							rtree.insert(polygon.getEnvelopeInternal(), store);
+							
+							//在这个同步块里面顺带把sub添加到这行数据的引用列表中
+							ArrayList<Integer> subs=lineSubsPos.get(wkbPosArr[0]);
+							if(subs==null) {
+								subs=new ArrayList<>();
+								lineSubsPos.put(wkbPosArr[0], subs);
+							}
+							subs.add(Integer.parseInt(wkbPosArr[2]));
+						}
+						if(i0==0 && !isWkbsFile) {
+							//这个只在查询完整wkt数据时才有用,一个数据只存一条就行了,wkbs的上面已经存好了
+							synchronized (wktDataStores) {
+								wktDataStores.add(store);
+							}
+						}
+					}
+					synchronized (InitInfo) {
+						InitInfo.DurationN_Index+=System.nanoTime()-r_t3;
+
+						if(ReadFromMemory){
+							if(InitInfo.WkbMemory==-1)InitInfo.WkbMemory=0;
+							InitInfo.WkbMemory+=wkbMemoryLen;
+						}
+						
+						InitInfo.PolygonCount+=polygonNum;
+					}
+				}
+				return null;
+			}
+		};
+		
+		
+		// 开启多线程处理读取的数据,留一个核打酱油
+		int[] threadCount=new int[] { Math.max(1, Math.min(SetInitUseThreadMax, Runtime.getRuntime().availableProcessors()-1)) };
+		InitInfo.UseThreadCount=threadCount[0];
+		for(int i=0;i<threadCount[0];i++) {
+			new Thread(new Runnable() {
+				@Override
+				public void run() {
+					try {
+						ThreadExec.Exec(null);
+					} catch(Exception e) {
+						if(threadError[0]==null) {
+							threadError[0]=e;
+						}
+					} finally {
+						synchronized (threadCount) { threadCount[0]--; }
+					}
+				}
+			}).start();
+		}
+		while(threadCount[0]>0) {
+			try { Thread.sleep(10); }catch (Exception e) { }
+		}
+		if(threadError[0]!=null) {
+			throw threadError[0];
+		}
+		
+		if(!IsStart[0]){
+			throw new Exception(IsStartErrMsg);
+		}
+		if(InitInfo.GeometryCount==0){
+			throw new Exception("初始化传入的文件内没有数据");
+		}
+		
+		//统一处理empty那些图形
+		for(int i=0,iL=emptyGeoms.size();i<iL;i++) {
+			HashMap<String,Object> store=emptyGeoms.get(i);
+			wktDataStores.add(store);
+			
+			if(saveWkbsFile!=null) {
+				String propStr=getProp(store);
+				String lineNo=getWkbPos(store)[0];
+				String wkbPos=(saveWkbsFileLength[0]+1)+"";//+1 换行符
+				wkbPos=lineNo+":"+wkbPos+":"+wkbPos; //parent:sub 数据存储位置
+				store.put("wkbPos", wkbPos);
+				
+				byte[] wkb=new WKBWriter().write(Factory.createPolygon());
+				SaveWkbsWrite.Exec("\nEmpty"+WKB_SP_Pos+wkbPos+WKB_SP_Prop+propStr+WKB_SP_WKB+Bytes2Hex(wkb));
+			}
+		}
+		
+		if(saveWkbsFile!=null) {//写入 wkbs 文件结尾
+			SaveWkbsWrite.Exec("\n]");
+		}
+		LineSubsPos=lineSubsPos;
+		WKTDataStores=wktDataStores;
+		EnvelopeSTRTree=rtree;
+	}
+	static private final String WKB_SP_Prop="|Prop:",WKB_SP_Pos="|Pos:",WKB_SP_WKB="|WKB:";
+	
+	
+	private String getProp(Map<String,Object> store) {
+		return ((String[])store.get("prop"))[0];
+	}
+	/**从保存的数据中提取出位置信息**/
+	private String[] getWkbPos(Map<String,Object> store) {
+		String str=(String)store.get("wkbPos");
+		int p0=str.indexOf(':');
+		int p1=str.indexOf(':', p0+1);
+		return new String[] {
+			str.substring(0, p0)
+			,str.substring(p0+1, p1)
+			,str.substring(p1+1)
+		};
+	}
+	/**
+	 * 检测结构化数据文件是否有效
+	 */
+	private boolean AvailableWkbsFile(String path) {
+		File file=new File(path);
+		if(!file.exists())return false;
+		try(FileInputStream in=new FileInputStream(path)) {
+			byte[] buffer=new byte[8*1024];
+			int len=in.read(buffer);
+			String txt=new String(buffer, "utf-8");
+			if(!txt.contains("@Version: "+Version+"\n")) {
+				return false;
+			}
+			if(!txt.contains("@GridFactor: "+SetGridFactor+"\n")) {
+				return false;
+			}
+			
+			in.skip(-len);// 请注意,倒车
+			in.skip(file.length()-1);
+			return in.read()==']';// 成功写入了结尾符号
+		}catch (Exception e) {
+			return false;
+		}
+	}
+	/**
+	 * 从结构化数据文件中读取一条wkb数据
+	 */
+	private byte[] ReadWkbFromFile(int pos) throws Exception {
+		try(FileInputStream in=new FileInputStream(WkbsFilePath)) { // RandomAccessFile 没有区别,文件流无需缓存 新打开流不消耗性能,并发控制反而会影响性能
+			in.skip(pos);
+			ByteArrayOutputStream bs=new ByteArrayOutputStream();
+			byte[] buffer=new byte[32*1024];
+			int len=0;
+			boolean isStart=false;
+			int findLen=0;
+			char[] FindChars=WKB_SP_WKB.toCharArray();
+			while((len=in.read(buffer))!=-1) {
+				int i0=0;
+				if(!isStart) {//查找 WKB_SP_WKB
+					for(int i=0;i<len;i++) {
+						if(buffer[i]==FindChars[0]) {
+							findLen=1;
+							continue;
+						}
+						if(findLen==0)continue;
+						if(buffer[i]==FindChars[findLen]) {
+							findLen++;
+							if(findLen==FindChars.length) {
+								isStart=true;
+								i0=i+1;
+								break;
+							}
+						} else {
+							findLen=0;
+						}
+					}
+				}
+				//查找结尾的\n
+				boolean isEnd=false;
+				int i1=len;
+				for(int i=i0;i<len;i++) {
+					if(buffer[i]=='\n') {
+						isEnd=true;
+						i1=i;
+						break;
+					}
+				}
+				
+				if(i1-i0>0) {
+					bs.write(buffer, i0, i1-i0);
+				}
+				if(isEnd) {
+					byte[] byts=bs.toByteArray();
+					if(byts.length%2==1) {
+						throw new Exception("结构化数据内部存在错误");
+					}
+					return Hex2Bytes(byts, 0);
+				}
+			}
+		}
+		throw new Exception("结构化数据文件已损坏");
+	}
+	
+	
+	
+
+	
+	
+	
+	
+	
+	
+	
+	
+	
+	//===========================一些函数和方法===================================
+	
+	
+	
+	
+	/** 通用回调接口 **/
+	public interface Func<iT, oT> { oT Exec(iT val) throws Exception; }
+	
+	
+	
+	
+	/**
+	 * 将一行JSON数据转换成Geometry对象,要求一个JSON图形数据必须占用一行文本,高性能!
+	 */
+	static private Geometry JSONLineParse(GeometryFactory factory, String line) {
+		ArrayList<__ParsePolygon> multiPols=new ArrayList<>();
+		int iGeom=line.indexOf("geometry\"");
+		for(int i=line.indexOf("coordinates\"", iGeom),L=line.length();i<L;) {
+			__ParsePolygon pr=new __ParsePolygon(factory, line, i);
+			if(pr.isFind) {
+				i=pr.lastIndex;
+				multiPols.add(pr);
+			} else {
+				break;
+			}
+		}
+		
+		if(multiPols.size()==1) {
+			return multiPols.get(0).toPolygon(factory);
+		} else if(multiPols.size()>1) {
+			Polygon[] pols=new Polygon[multiPols.size()];
+			for(int i=0,L=multiPols.size();i<L;i++) {
+				pols[i]=multiPols.get(i).toPolygon(factory);
+			}
+			return factory.createMultiPolygon(pols);
+		} else {
+			return factory.createPolygon();
+		}
+	}
+	
+	static private class __ParsePolygon{
+		public __ParsePolygon(GeometryFactory factory, String line, int index) {
+			//手工提取图形坐标点
+			ArrayList<Coordinate> points=new ArrayList<>();
+			boolean isStart=false; int multiEnds=0,polEnds=0;
+			int i0=index;
+			for(int Li=line.length();i0<Li;i0++) {
+				char c=line.charAt(i0);
+				if(c==' ')continue;
+				if(c=='}')break;
+				if(!isStart) {
+					if(c=='[') isStart=true;
+					continue;
+				}
+				if(c==']') {
+					polEnds++;
+					if(polEnds==2) {// 环结束
+						LinearRing ring=factory.createLinearRing(points.toArray(new Coordinate[0]));
+						if(ring0==null) {
+							ring0=ring;
+						}else{
+							ringX.add(ring);
+						}
+						points=new ArrayList<>();
+					}
+					multiEnds++;
+					if(multiEnds==3) {// MultiPolygon结束
+						i0++;break;
+					}
+					continue;
+				}
+				polEnds=0;
+				multiEnds=0;
+				
+				if(c==',' || c=='[')continue;
+				
+				StringBuilder lng=new StringBuilder(),lat=new StringBuilder();
+				for(;i0<Li;i0++) {
+					c=line.charAt(i0);
+					if(c==' ') continue;
+					if(c==',') { i0++; break;}
+					lng.append(c);
+				}
+				for(;i0<Li;i0++) {
+					c=line.charAt(i0);
+					if(c==' ') continue;
+					if(c==']') { i0--; break; }
+					lat.append(c);
+				}
+				points.add(new Coordinate(
+						Double.parseDouble(lng.toString())
+						, Double.parseDouble(lat.toString())));
+			}
+			lastIndex=i0;
+			isFind=ring0!=null;
+		}
+		
+		public boolean isFind;
+		public int lastIndex;
+		
+		private LinearRing ring0;
+		private ArrayList<LinearRing> ringX=new ArrayList<>();
+		public Polygon toPolygon(GeometryFactory factory) {
+			if(ring0==null) {
+				return factory.createPolygon();
+			}
+			LinearRing[] holes=null;
+			if(ringX.size()>0) {
+				holes=ringX.toArray(new LinearRing[0]);
+			}
+			return factory.createPolygon(ring0, holes);
+		}
+	}
+	
+	
+	/**
+	 * 将坐标点数过多的边界,使用网格进行拆分成小块
+	 */
+	static private Geometry GeometryGridSplit(GeometryFactory factory, Geometry geom, int gridFactor) {
+		ArrayList<Polygon> pols=new ArrayList<>();
+		if(geom instanceof Polygon) {
+			__PolygonGridSplit(factory, gridFactor, pols, (Polygon)geom);
+		} else {
+			for(int i=0,L=geom.getNumGeometries();i<L;i++) {
+				__PolygonGridSplit(factory, gridFactor, pols, (Polygon)geom.getGeometryN(i));
+			}
+		}
+		if(pols.size()==1) {
+			return pols.get(0);
+		}
+		return factory.createMultiPolygon(pols.toArray(new Polygon[0]));
+	}
+	static private void __PolygonGridSplit(GeometryFactory factory, int gridFactor, ArrayList<Polygon> pols, Polygon polygon) {
+		int pointCount=polygon.getNumPoints();
+		int gridPoint=(int)Math.round(1.0*pointCount/gridFactor);//最外层的1格平均分担点数,计算最外层网格边数
+		if(gridPoint<2) {//没必要拆分了
+			pols.add(polygon);
+			return;
+		}
+		Envelope box=polygon.getEnvelopeInternal();
+		
+		//按最长的一边,对中切开,切成两块,然后递归去切
+		double width=box.getMaxX()-box.getMinX();
+		double height=box.getMaxY()-box.getMinY();
+		int gridX=1,gridY=1;//xy轴列数
+		if(width/(height*2)>1) {//x轴更长,切x轴,纬度简单*2 当做跟 经度 一样
+			gridX++;
+		} else {
+			gridY++;
+		}
+		double xStep=width/gridX;
+		double yStep=height/gridY;
+		
+		double x_0=box.getMinX(),y_00=box.getMinY();
+		double x_1=box.getMaxX(),y_1=box.getMaxY();
+		while(x_0-x_1<-xStep/2) {//注意浮点数±0.000000001的差异
+			double x0=x_0, x1=x_0+xStep; x_0=x1;
+			double y_0=y_00;
+			while(y_0-y_1<-yStep/2) {
+				double y0=y_0, y1=y_0+yStep; y_0=y1;
+				Polygon gridItem=factory.createPolygon(new Coordinate[] {
+					new Coordinate(x0, y0)
+					, new Coordinate(x0, y1)
+					, new Coordinate(x1, y1)
+					, new Coordinate(x1, y0)
+					, new Coordinate(x0, y0)
+				});
+				Geometry chunk=polygon.intersection(gridItem);
+				if(!chunk.isEmpty()) {
+					//如果有大的就继续拆分
+					if(chunk instanceof Polygon) {
+						__PolygonGridSplit(factory, gridFactor, pols, (Polygon)chunk);
+					} else {
+						for(int i2=0,L2=chunk.getNumGeometries();i2<L2;i2++) {
+							Geometry item=chunk.getGeometryN(i2);
+							if(item instanceof Polygon) { //偶尔出现LineString
+								__PolygonGridSplit(factory, gridFactor, pols, (Polygon)item);
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+	
+	
+	/** 计算两个坐标的距离,单位米 **/
+	static public double Distance(double lng1, double lat1, double lng2, double lat2) {
+		//采用Haversine formula算法,高德地图的js计算代码,比较简洁 https://www.cnblogs.com/ggz19/p/7551088.html
+		double d=Math.PI/180;
+		double f=lat1*d, h=lat2*d;
+		double i=lng2*d - lng1*d;
+		double e=(1 - Math.cos(h - f) + (1 - Math.cos(i)) * Math.cos(f) * Math.cos(h)) / 2;
+		return 2 * 6378137 * Math.asin(Math.sqrt(e));
+	}
+	/** 以坐标点为中心,简单粗略的创建一个指定半径的圆,半径单位米,pointCount为构建圆的坐标点数(比如24个点,点越多越圆,最少3个点) **/
+	static public Geometry CreateSimpleCircle(double lng, double lat, double radius, int pointCount) {
+		//球面坐标不会算,转换成三角坐标简单点,经度代表值大约:0.01≈1km 0.1≈10km 1≈100km 10≈1000km
+		double km=radius/1000;
+		double a=km<5?0.01 :km<50?0.1 :km<500?1 :10;
+		double b=Distance(lng, lat, lng+a, lat);
+		double c=Distance(lng, lat, lng, lat+a);
+		double rb=radius/b*a;
+		double rc=radius/c*a;
+		Coordinate[] arr=new Coordinate[pointCount+1];
+		double n=0,step=360.0/pointCount,N=360-step/2; //注意浮点数±0.000000001的差异
+		for(int i=0;n<N;i++,n+=step){
+			double x=lng+rb*Math.cos(n*Math.PI/180);
+			double y=lat+rc*Math.sin(n*Math.PI/180);
+			arr[i]=new Coordinate(x, y);
+		}
+		arr[pointCount]=arr[0];
+		return Factory.createPolygon(arr);
+	}
+	/** 通过两个坐标点构造一个矩形 **/
+	static public Geometry CreateRect(double lng1, double lat1, double lng2, double lat2) {
+		return Factory.createPolygon(new Coordinate[] {
+			 new Coordinate(lng1, lat1), new Coordinate(lng1, lat2)
+			,new Coordinate(lng2, lat2), new Coordinate(lng2, lat1)
+			,new Coordinate(lng1, lat1)
+		});
+	}
+	
+	
+	
+	/**判断是不是.wkbs结尾的文件路径**/
+	static private boolean IsWkbsFilePath(String path) {
+		return path.toLowerCase().endsWith(".wkbs");
+	}
+	
+	/**二进制内容转成16进制文本(大写)**/
+	static private String Bytes2Hex(byte[] bytes) {
+		StringBuffer str=new StringBuffer();
+		for (int i=0; i<bytes.length; i++) {
+			byte b=bytes[i];
+			int b1=(b>>4) & 0x0F;
+			int b2=b & 0x0F;
+			str.append((char)(b1<=9?'0'+b1:'A'+b1-10));
+			str.append((char)(b2<=9?'0'+b2:'A'+b2-10));
+		}
+		return str.toString();
+	}
+	/**16进制文本转成二进制内容,从指定位置开始转**/
+	static private byte[] Hex2Bytes(String hex, int start) {
+		byte[] val=new byte[(hex.length()-start)/2];
+		for(int i=start,n=0,len=hex.length();i<len;n++) {
+			int c1=hex.charAt(i++);
+			int c2=hex.charAt(i++);
+			
+			c1=c1<'A'?c1-'0':c1<'a'?c1-'A'+10:c1-'a'+10;
+			c2=c2<'A'?c2-'0':c2<'a'?c2-'A'+10:c2-'a'+10;
+			
+			val[n]=(byte)((c1 << 4) | c2);
+		}
+		return val;
+	}
+	/**16进制数据转成二进制内容,从指定位置开始转**/
+	static private byte[] Hex2Bytes(byte[] hex, int start) {
+		byte[] val=new byte[(hex.length-start)/2];
+		for(int i=start,n=0,len=hex.length;i<len;n++) {
+			int c1=hex[i++];
+			int c2=hex[i++];
+			
+			c1=c1<'A'?c1-'0':c1<'a'?c1-'A'+10:c1-'a'+10;
+			c2=c2<'A'?c2-'0':c2<'a'?c2-'A'+10:c2-'a'+10;
+			
+			val[n]=(byte)((c1 << 4) | c2);
+		}
+		return val;
+	}
+	
+	
+	
+	
+	/**将错误堆栈转成字符串**/
+	static private String ErrorStack(Throwable e) {
+		StringWriter writer = new StringWriter();
+		e.printStackTrace(new PrintWriter(writer));
+		return writer.toString();
+	}
+	/**纳秒显示成ms,小数点后有数字开始两位**/
+	static private String Nano(double nanoTime) {
+		String ts=nanoTime/1000000.0+"";
+		BigDecimal big=new BigDecimal(ts);
+		String s=ts.split("\\.")[1];
+		int c=0;
+		for(int i=0;i<s.length();i++) {
+			if(s.charAt(i)>'0') {
+				c+=2; break;
+			}
+			c++;
+		}
+		big=big.setScale(c, RoundingMode.HALF_UP);
+		
+		return big.toString()+"ms";
+	}
+	/**计算两个内存字节数差值,然后显示成MB**/
+	static private String Memory(long memory) {
+		return memory/1024/1024+"MB";
+	}
+	/**获取当前系统内存已用字节数**/
+	static private long GetMemory_System() {
+		try {
+			OperatingSystemMXBean osMX = ManagementFactory.getOperatingSystemMXBean();
+			Method totalFn=osMX.getClass().getMethod("getTotalPhysicalMemorySize");
+			totalFn.setAccessible(true);
+			Method freeFn=osMX.getClass().getMethod("getFreePhysicalMemorySize");
+			freeFn.setAccessible(true);
+			return (long)totalFn.invoke(osMX) - (long)freeFn.invoke(osMX);
+		}catch (Exception e) {
+			return 0;
+		}
+	}
+	/**获取当前Java Runtime内存已用字节数**/
+	static private long GetMemory_JavaRuntime() {
+		return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
+	}
+	
+	
+	
+	/** 初始化是的相关信息对象 **/
+	static public class QueryInitInfo {
+		/** 开始时间,纳秒 **/
+		public long StartTimeN;
+		/** 结束时间,纳秒 **/
+		public long EndTimeN;
+		
+		/** 开始时的系统内存已用字节数 **/
+		public long StartMemory_System;
+		/** 结束时的系统内存已用字节数 **/
+		public long EndMemory_System;
+		
+		/** 开始时的Java Runtime内存已用字节数 **/
+		public long StartMemory_JavaRuntime;
+		/** 结束时的Java Runtime内存已用字节数 **/
+		public long EndMemory_JavaRuntime;
+		/** 初始化使用的线程数量 **/
+		public int UseThreadCount=1;
+		
+		/** 当前处理的文件行数,当全部处理完成时为0 **/
+		public int CurrentLine_No;
+		/** 当前处理的行完整内容,当全部处理完成时为空字符串 **/
+		public String CurrentLine_Text="";
+		/** 当前处理的行属性内容,当全部处理完成时为空字符串 **/
+		public String CurrentLine_Prop="";
+		
+		/** 有效的图形数量,不包括空的图形 **/
+		public int GeometryCount;
+		/** 图形中的Polygon数量,一个图形包含1-n个Polygon,会用这些数量的Polygon外接矩形进行初步查找 **/
+		public int PolygonCount;
+		/** 如果缓存了wkb数据在内存,这里将会有wkb总字节数,未缓存为-1 **/
+		public int WkbMemory=-1;
+
+		/** 文件读取:文件内容读取耗时,纳秒 **/
+		public long DurationN_FileRead;
+		/** 文件解析:文件内容解析耗时,纳秒 **/
+		public long DurationN_FileParse;
+		/** 创建图形:内容解析成Geometry对象耗时,纳秒 **/
+		public long DurationN_GeometryParse;
+		/** Geometry对象进行索引耗时,纳秒 **/
+		public long DurationN_Index;
+		/** 初始化结尾调用System.gc()回收内存的耗时,纳秒 **/
+		public long DurationN_JavaGC;
+		
+		/** 初始化时提供的数据文件路径 **/
+		public String FilePath_Data;
+		/** 初始化是提供的.wkbs后缀的结构化数据文件路径 **/
+		public String FilePath_SaveWkbs;
+		
+		/**初始化时的数据是否是从wkbs结构化数据文件中读取;如果为false可能代表还未生成过wkbs文件,首次初始化可能会很慢**/
+		public boolean DataFromWkbsFile;
+		/**初始化时是否使用或保存了wkbs结构化数据文件,没有wkbs文件时查询中不允许获取WKT数据**/
+		public boolean HasWkbsFile;
+		/**初始化失败时的错误消息**/
+		public String ErrMsg="";
+		
+		/** 初始化是否出现了错误 **/
+		public boolean hasError() {
+			return ErrMsg!=null && ErrMsg.length()>0;
+		}
+		@Override
+		public String toString() {
+			StringBuilder str=new StringBuilder();
+			str.append("[v"+AreaCityQuery.Version+"]"
+					+(DataFromWkbsFile?"wkbs+":"")
+					+"已读取Geometry "+GeometryCount+" 个(Grid切分Polygon "+PolygonCount+" 个)");
+			
+			if(hasError()) {
+				String errT="\n=============\n";
+				str.append(errT+ErrMsg+errT);
+			}
+			
+			str.append("\n");
+			long tn=EndTimeN-StartTimeN;
+			str.append("Init总耗时: "+tn/1000000+"ms");
+			str.append(",平均: "+(GeometryCount==0?"-":Nano(tn*1.0/GeometryCount))+"/个Geometry,线程数: "+UseThreadCount);
+
+			if(WkbMemory!=-1)str.append("\nWKB内存: "+Memory(WkbMemory));
+			str.append("\n文件读取耗时: "+Nano(DurationN_FileRead));
+			str.append("\n文件解析耗时: "+Nano(DurationN_FileParse/UseThreadCount)+"/线程,总: "+Nano(DurationN_FileParse));
+			str.append("\n创建图形耗时: "+Nano(DurationN_GeometryParse/UseThreadCount)+"/线程,总: "+Nano(DurationN_GeometryParse));
+			str.append("\n创建索引耗时: "+Nano(DurationN_Index/UseThreadCount)+"/线程,总: "+Nano(DurationN_Index));
+			
+			str.append("\n内存占用: "+Memory(EndMemory_JavaRuntime- StartMemory_JavaRuntime)+" (Java Runtime)");
+			str.append(", "+Memory(EndMemory_System - StartMemory_System)+" (系统)");
+			str.append(", Java GC耗时: "+Nano(DurationN_JavaGC));
+			
+			str.append("\nData文件: "+FilePath_Data);
+			str.append("\nWkbs文件: "+FilePath_SaveWkbs);
+			
+			return str.toString();
+		}
+	}
+	
+	
+	
+	
+	
+	/** 查询控制+和结果信息对象 **/
+	static public class QueryResult {
+		/** 查询结果列表,为匹配的边界属性数据(prop json字符串);如果设为null将只统计数据,不返回结果 **/
+		public ArrayList<String> Result=new ArrayList<>();
+		
+		/** 查询开始时间,纳秒 **/
+		public long StartTimeN;
+		/** 查询结束时间,纳秒 **/
+		public long EndTimeN;
+
+		/** 查询过程中涉及到的IO耗时,纳秒 **/
+		public long DurationN_IO;
+		/** 查询过程中涉及到的图形对象解析耗时,纳秒 **/
+		public long DurationN_GeometryParse;
+		/** 从边界外接矩形中初步筛选耗时,纳秒 **/
+		public long DurationN_EnvelopeHitQuery;
+		/** 查询过程中精确查找耗时,纳秒 **/
+		public long DurationN_ExactHitQuery;
+		
+		/** 外接矩形中初步筛选匹配到的矩形数量 **/
+		public int EnvelopeHitCount;
+		/** 精确查找到的边界数量 **/
+		public int ExactHitCount;
+		
+		/** 本结果对象经过了几次查询(性能测试用) **/
+		public int QueryCount;
+		
+		/** 将另一个的统计数据添加到这个里面来 **/
+		public void Add(QueryResult other) {
+			StartTimeN=Math.min(StartTimeN, other.StartTimeN);
+			EndTimeN=Math.max(EndTimeN, other.EndTimeN);
+			
+			DurationN_IO+=other.DurationN_IO;
+			DurationN_GeometryParse+=other.DurationN_GeometryParse;
+			DurationN_EnvelopeHitQuery+=other.DurationN_EnvelopeHitQuery;
+			DurationN_ExactHitQuery+=other.DurationN_ExactHitQuery;
+			
+			EnvelopeHitCount+=other.EnvelopeHitCount;
+			ExactHitCount+=other.ExactHitCount;
+			QueryCount+=other.QueryCount;
+		}
+		
+		
+		/** 如果不为null,几何计算查询时将会把从边界外接矩形中初步筛选时匹配到的中间结果写入到这个数组中(这些匹配项将参与精确匹配,数量越多性能越低下) **/
+		public ArrayList<String> Set_EnvelopeHitResult=null;
+		
+		/** 查询结果中要额外包含对应的边界wkt文本,此参数会作为wkt文本在json里的key;必须初始化时保存了wkbs结构化数据文件,或者用的wkbs文件初始化的 **/
+		public String Set_ReturnWKTKey=null;
+		
+		
+		@Override
+		public String toString() {
+			StringBuilder str=new StringBuilder();
+			long tn=EndTimeN-StartTimeN;
+			str.append("查询"+QueryCount+"次共耗时: "+Nano(tn));
+			str.append(",EnvelopeHitCount: "+EnvelopeHitCount);
+			str.append(",ExactHitCount: "+ExactHitCount);
+			str.append(",IO: "+Nano(DurationN_IO));
+			str.append(",GeometryParse: "+Nano(DurationN_GeometryParse));
+			str.append(",EnvelopeHitQuery: "+Nano(DurationN_EnvelopeHitQuery));
+			str.append(",ExactHitQuery: "+Nano(DurationN_ExactHitQuery));
+			
+			if(QueryCount>1) {
+				double count=QueryCount;
+				str.append("\n单次查询耗时: "+Nano(tn/count));
+				str.append(",EnvelopeHitCount: "+Math.round(EnvelopeHitCount*100/count)/100.0);
+				str.append(",ExactHitCount: "+Math.round(ExactHitCount*100/count)/100.0);
+				str.append(",IO: "+Nano(DurationN_IO/count));
+				str.append(",GeometryParse: "+Nano(DurationN_GeometryParse/count));
+				str.append(",EnvelopeHitQuery: "+Nano(DurationN_EnvelopeHitQuery/count));
+				str.append(",ExactHitQuery: "+Nano(DurationN_ExactHitQuery/count));
+			}
+			
+			if(Set_EnvelopeHitResult!=null) {
+				str.append("\n\nEnvelopeHit初步筛选: "+Set_EnvelopeHitResult.size()+"条");
+				for(int i=0;i<Set_EnvelopeHitResult.size();i++) {
+					String txt=Set_EnvelopeHitResult.get(i);
+					str.append("\nHit["+i+"] "+(txt.length()<500?txt:txt.substring(0, 500)+" ... "+txt.length()+"字"));
+				}
+			}
+			if(Result!=null) {
+				str.append("\n\n结果 Result: "+Result.size()+"条");
+				for(int i=0;i<Result.size();i++) {
+					String txt=Result.get(i);
+					str.append("\n结果["+i+"] "+(txt.length()<500?txt:txt.substring(0, 500)+" ... "+txt.length()+"字"));
+				}
+			}
+			return str.toString();
+		}
+	}
+	
+}

+ 324 - 0
AreaCity-Query-Geometry/README.md

@@ -0,0 +1,324 @@
+**【[源GitHub仓库](https://github.com/xiangyuecn/AreaCity-Query-Geometry)】 | 【[Gitee镜像库](https://gitee.com/xiangyuecn/AreaCity-Query-Geometry)】**
+
+# :open_book:AreaCity-Query-Geometry坐标边界查询工具
+
+**本工具核心功能:使用`jts库`从`省市区县乡镇边界数据`([AreaCity-JsSpider-StatsGov开源库](https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov))或`geojson边界数据`文件中查找出和任意点、线、面有相交的矢量边界,内存占用低,性能优良(1秒可查1万个以上坐标对应的城市信息)。**
+
+- 查询一个坐标点对应的城市信息;
+- 查询一条路径经过的所有城市;
+- 查询一个矢量范围覆盖的所有城市;
+- 查询一个城市或下一级所有边界数据(WKT格式);
+- 支持通过HTTP API服务进行查询调用;
+- 支持通过Java代码进行查询调用;
+- 源码简单,包括测试`.bat|.sh`脚本共5个文件,无需IDE即可修改和运行,copy即用。
+
+
+[​](?)
+
+你可以只copy `AreaCityQuery.java` 文件到你的项目中使用(建好package目录或者修改一下package),项目中引入`jts库`,就能使用 `AreaCityQuery` 中的所有查找功能了。也可以clone整个项目代码双击 `编译和运行Test.java直接测试.bat` 即可直接运行测试(macOS、linux用终端运行`.sh`的);`scripts`里面有生成jar包的命令行脚本。
+
+**API和图形界面**:本工具已自带了一个HTTP API服务,运行测试然后通过菜单启动此服务,然后就可以直接在浏览器上访问这些接口;此API接口可以直接在 [ECharts Map四级下钻在线测试和预览](https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html) 页面的`自定义数据源`中进行调用测试,页面会立即绘制查询出来的边界图形。
+
+[​](?)
+
+源文件|是否必须|说明
+:-:|:-:|:-
+**AreaCityQuery.java**|必须|核心功能类,封装了所有核心功能,支持从`geojson`文件初始化。
+**jts-core-1.18.2.jar**|必须|唯一依赖的jts库,从[maven此链接](https://mvnrepository.com/artifact/org.locationtech.jts/jts-core/1.18.2)下载来的;<br>如需在pom.xml中引入,请参考maven链接页面内xml代码。
+Test_HttpApiServer.java|可选|测试用的HTTP API服务实现(可以删除此文件,不影响Test.java运行)。
+Test.java|可选|测试控制台程序,包含了所有功能的测试,包括启动HTTP API服务;<br>双击 `编译和运行Test.java直接测试.bat` 即可直接编译和运行此控制台程序(需装了jdk,macOS、linux用终端运行`.sh`的)。
+
+
+**如需功能定制,网站、App、小程序、前端后端开发等需求,请加下面的QQ群,联系群主(即作者),谢谢~**
+
+
+[​](?)
+
+[​](?)
+
+## 【QQ群】交流与支持
+
+欢迎加QQ群:①群 484560085、②群 626141661、③群 346847528,纯小写口令:`areacity`
+
+<img src="https://gitee.com/xiangyuecn/AreaCity-JsSpider-StatsGov/raw/master/assets/qq_group_484560085.png" width="220px">
+
+
+
+
+
+
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+## 如何使用
+
+### 前期准备:生成GeoJSON文件
+程序初始化时,需要提供一个geojson文件(.json|.geojson),如果你没有此文件,可以按以下步骤获得最新的全国省市区县乡镇边界数据json文件:
+1. 请到开源库下载省市区边界数据ok_geo.csv文件: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee);
+2. 下载开源库里面的`AreaCity-Geo格式转换工具软件`;
+3. 打开转换工具软件,选择ok_geo.csv,然后导出成geojson文件即可(默认会导出全国的省级数据,通过填写不同城市名前缀可以导出不同城市)。
+
+> AreaCityQuery支持使用多个数据文件来分别创建多个查询实例,比如省、市、区分别导出一个geojson文件,需要查询哪个级别的数据就调用对应实例进行查询;但如果你有多个性质相同geojson文件时,也可以合并成一个文件来只创建一个查询实例使用,可以通过上面下载的 `AreaCity-Geo格式转换工具软件` 中的 `高级功能`-`GeoJSON多个文件合并成一个文件` 来合并。
+
+> 通过ok_geo.csv生成的geojson文件的坐标系默认是`GCJ-02`火星坐标系,查询时所有输入坐标参数的坐标系必须也是`GCJ-02`,否则坐标可能会有比较大的偏移,导致查询结果不正确;可以在转换工具的高级功能中使用坐标系转换,比如转成`WGS-84`GPS坐标系,重新导出geojson文件,这样查询时就能准确的查询GPS坐标了。
+
+> 源码内已提供了一个 `仅供测试-全国省级GeoJSON数据-大幅简化粗略版.json` 文件(为了大幅减小文件体积,已严重精简过了,不可在其它地方使用),可以直接使用此文件测试,但只能测试到省级边界,如需正式测试或使用,请参考上面方法自行生成geojson文件。
+> 
+> 注意:如果是其他地方生成的geojson文件, 要求里面数据**必须是一行一条数据**,第一条数据的上一行必须是`"features": [`,最后一条数据的下一行必须是`]`打头,**否则不支持解析**,可尝试用文本编辑器批量替换添加换行符,改成和 `仅供测试-全国省级GeoJSON数据-大幅简化粗略版.json` 一样内容格式就行。
+
+
+[​](?)
+
+### 使用方式一:通过HTTP API服务使用
+操作步骤:
+1. 双击 `编译和运行Test.java直接测试.bat` 运行测试控制台程序(macOS、linux用终端运行`.sh`的);
+2. 根据控制台菜单命令进行初始化(需先把一个geojson文件放本程序目录内,否则要输入路径);
+3. 进入9号菜单,启动本地轻量HTTP API服务(编辑 Test.java 文件内的`HttpApiServerPort`可修改端口);
+4. 浏览器访问:`http://127.0.0.1:9527/` 查看接口文档和实例状态;
+5. 在需要的地方直接调用http地址接口,得到json响应结果;
+6. 如需外网访问,可以直接暴露端口,或使用Nginx之类的反代此http端口(通过Nginx提供https访问)。
+
+> 如需直接启动http服务,如服务器中:先解开Test.java中main函数对应的此功能注释,填写实际的json文件路径,然后运行即可;linux服务器中可使用 `nohup bash xxx.sh` 命令执行在后台运行。
+> 
+> 此HTTP API接口可以直接在 [ECharts Map四级下钻在线测试和预览](https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html) 页面的`自定义数据源`中进行调用测试,页面会立即绘制查询出来的边界图形。
+
+
+[​](?)
+
+### 使用方式二:Java直接调用
+``` java
+//先获取到查询实例,默认提供的0-9的10个静态实例,每个实例可以分别使用一个数据文件进行初始化和查询,当然自己调用new AreaCityQuery()创建一个新实例使用也是一样的
+AreaCityQuery Instance=AreaCityQuery.Instances[0];
+//static public final AreaCityQuery Instance=new AreaCityQuery(); //也可以自己创建静态实例
+
+//查询前先初始化,每个实例全局只会初始化一次,每次查询前都调用即可(查询会在初始化完成后进行),两种初始化方式根据自己业务情况二选一
+//首次初始化会从.json或.geojson文件中读取边界图形数据,速度比较慢,会自动生成.wkbs结尾的结构化文件,下次初始化就很快了
+//首次初始化生成了.wkbs文件后,后续初始化可以只使用此wkbs文件,允许不用再提供geojson文件(数据更新时需删除wkbs文件再重新用geojson文件进行初始化),具体请阅读对应初始化方法的注释文档
+Instance.Init_StoreInWkbsFile("geojson文件路径", "geojson文件路径.wkbs", true);
+//Instance.Init_StoreInMemory("geojson文件路径", "geojson文件路径.wkbs", true);
+
+//Instance.OnInitProgress=(initInfo)->{ ... } //初始化过程中的回调,可以绑定一个函数,接收初始化进度信息(编写时需在Init之前进行绑定)
+System.out.println(Instance.GetInitInfo().toString()); //打印初始化详细信息,包括性能信息
+
+//注意:以下查询中所有坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确
+//查询包含一个坐标点的所有边界图形的属性数据,可通过res参数让查询额外返回wkt格式边界数据
+//查询结果的判定:请不要假定查询结果的数量(坐标刚好在边界上可能会查询出多个省市区),也不要假定查询结果顺序(结果中省市区顺序是乱序的),请检查判定res1.Result中的结果是否符合查询的城市级别,比如查询省市区三级:结果中必须且仅有3条数据,并且省市区都有(判断deep=0省|1市|2区 来区分数据的级别),其他一律判定为查询无效
+QueryResult res1=Instance.QueryPoint(114.044346, 22.691963, null, null);
+//当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),下面这个方法将能够匹配到附近不远的边界图形数据;2500相当于一个以此坐标为中心点、半径为2.5km的圆形范围,会查询出在这个范围内和此坐标点距离最近的边界
+QueryResult res1_2=Instance.QueryPointWithTolerance(121.993491, 29.524288, null, null, 2500);
+
+//查询和一个图形(点、线、面)有交点的所有边界图形的属性数据,可通过res参数让查询额外返回wkt格式边界数据
+Geometry geom=new WKTReader(AreaCityQuery.Factory).read("LINESTRING(114.30115 30.57962, 117.254285 31.824198, 118.785633 32.064869)");
+//Geometry geom=AreaCityQuery.CreateSimpleCircle(114.044346, 22.691963, 10000, 24); //以指定坐标为中心点,创建一个半径为10km的圆面,圆面由24个坐标点粗略构成
+QueryResult res2=Instance.QueryGeometry(geom, null, null);
+
+//读取省市区的边界数据wkt格式,这个例子会筛选出武汉市所有区县
+QueryResult res3=Instance.ReadWKT_FromWkbsFile("wkt_polygon", null, (prop)->{return prop.contains("武汉市 ");}, null);
+//此方法会遍历所有边界图形的属性列表,因此可以用来遍历所有数据,提取感兴趣的属性内容,比如查询一个区划编号id对应的城市信息(城市名称、中心点)
+QueryResult res4=Instance.ReadWKT_FromWkbsFile(null, null, (prop)->{
+    prop=(","+prop.substring(1, prop.length()-1)+",").replace("\"", "").replace(" ", ""); //不解析json,简单处理
+    return prop.contains(",id:42,"); //只查询出id=42(湖北省)的属性数据(注意初始化的geojson中必须要有对应的属性名,这里是id)
+}, null);
+
+
+System.out.println(res1+"\n"+res1_2+"\n"+res2+"\n"+res3+"\n"+res4);
+//****更多的实例,请阅读 Test.java****
+//****更多功能方法,请阅读 AreaCityQuery.java 源码****
+```
+
+### 附:生成jar包
+`scripts`目录里面有生成jar包的命令行脚本,双击`生成jar包.bat`即可将源码编译成jar文件(macOS、linux用终端运行`.sh`的);脚本支持生成`带Test.java控制台程序`的、和`不带控制台程序`的两种jar,前者可以启动运行,后者可放到其他项目中用Java代码调用。
+
+### 附:关于Java程序打包发布
+本程序只支持从文件路径进行初始化,所以请将数据文件放到一个磁盘目录内,不要打包进jar中;如果是docker发布,可以在dockerfile中用VOLUME映射宿主机目录、或直接将数据文件copy进docker镜像内的目录中。
+
+
+[​](?)
+
+### 附:查询一个坐标对应城市的结果判定
+省、市、区分成三个geojson文件分别初始化三个查询实例进行查询的结果判断会简单一些;如果省市区三级数据在一个geojson文件中初始化的查询实例,查询结果`res.Result`可通过下面规则进行判断:
+- 请不要假定查询结果的数量,比如:坐标刚好在边界上可能会查询出多个省市区,结果中将超过3条数据;
+- 请不要假定查询结果顺序,结果中省市区顺序是乱序的,需要判断每条结果是省市区哪一级,通过判断结果中的`deep=0省|1市|2区`来区分这条结果是哪个级别;
+- 请检查判定查询结果中的数据是否完全符合查询的城市级别:省市区三级结果中必须且仅有3条数据,并且这3条数据省市区三级都有,否则一律可判定为查询无效。
+
+
+[​](?)
+
+### 附:.wkbs文件说明
+初始化时如果提供了`saveWkbsFilePath`参数(为.wkbs结构化文件的路径):
+- 如果此文件已存在,将自动从此文件进行初始化,初始化速度会很快(文件只读不写,可以不提供geojson文件);
+- 如果此文件不存在,将从.json或.geojson文件中读取边界图形数据,并生成此文件,速度比较慢(文件读写)。
+
+因此可以先在本地用json文件进行初始化,自动生成一个wkbs文件,然后copy wkbs文件到别的地方使用(比如服务器、只读环境中)。
+
+
+
+
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+## 性能测试
+
+> 测试数据源:AreaCity-JsSpider-StatsGov开源库[2021.220321.220428版本](https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov/releases/tag/2021.220321.220428)下载的ok_geo.csv文件按省市区导出成不同的geojson文件。
+> 
+> 测试采用开启多线程进行随机坐标点的查询(Test.java控制台5号菜单),测试机器配置:8核 2.20GHz CPU,SSD 硬盘。
+
+
+[​](?)
+
+### 测试一:Init_StoreInWkbsFile 内存占用很低(性能受IO限制)
+数据源|文件大小|数据量|内存占用|7核QPS|单核QPS|单次查询耗时
+:-:|-:|-:|-:|-:|-:|-:
+省市区三级|176MB|3632条|41MB|6212|887|1.13ms
+仅区级|107MB|3208条|24MB|13818|1974|0.51ms
+仅省级|20MB|34条|4MB|19832|2833|0.35ms
+
+> `Init_StoreInWkbsFile`:用加载数据到结构化数据文件的模式进行初始化,推荐使用本方法初始化,边界图形数据存入结构化数据文件中,内存占用很低,查询时会反复读取文件对应内容,查询性能消耗主要在IO上,IO性能极高问题不大。
+
+
+[​](?)
+
+### 测试二:Init_StoreInMemory 内存占用和json文件差不多大(性能豪放)
+数据源|文件大小|数据量|内存占用|7核QPS|单核QPS|单次查询耗时
+:-:|-:|-:|-:|-:|-:|-:
+省市区三级|176MB|3632条|161MB|77242|11034|0.091ms
+仅区级|107MB|3208条|96MB|121228|17318|0.058ms
+仅省级|20MB|34条|18MB|465940|66562|0.015ms
+
+> `Init_StoreInMemory`:用加载数据到内存的模式进行初始化,边界图形数据存入内存中,内存占用和json数据文件大小差不多大,查询性能极高;另外可通过设置 `Instances[0-9].SetInitStoreInMemoryUseObject=true` 来进一步提升性能,但内存占用会增大一倍多,省市区三级单核可达到 15000 QPS。
+
+
+[​](?)
+
+### 参考:数据库查询测试对比
+数据源|数据量|查询坐标|MySQL单次查询耗时|SQL Server单次查询耗时
+:-:|-:|:-:|-:|-:
+省市区三级|3632条|深圳-龙华区|163ms|25ms
+省市区三级|3632条|北京-房山区|173ms|47ms
+
+``` sql
+-- 查询坐标点:`POINT(114.044346 22.691963)` 深圳市 龙华区;`POINT(116.055588 39.709385)` 北京市 房山区(查询受内蒙envelope干扰影响性能);
+
+-- MySQL(8.0.19)使用MyISAM引擎并给polygon创建polygon_envelope列当做索引加速
+create procedure ptest() begin declare i int default 0; while i<100 do do (select count(*) from (select id,ext_path from 【表名】 where ST_Intersects(polygon_envelope,ST_GeomFromText('POINT(114.044346 22.691963)',0))=1 and ST_Intersects(polygon,ST_GeomFromText('POINT(114.044346 22.691963)',0))=1) as t); set i=i+1; end while; end; call ptest(); drop procedure if exists ptest;
+
+-- SQL Server(2008)在polygon列创建空间索引加速
+declare @t datetime = getdate(); declare @val varchar; declare @i int =0; while @i<100 begin; set @val=(select id,ext_path from 【表名】 where polygon.STIntersects(geometry::STGeomFromText('POINT(114.044346 22.691963)',0))=1 for xml path('')); set @i=@i+1; end; select DATEDIFF(MS, @t, GETDATE())/100.0
+```
+
+
+
+
+
+
+
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+
+# :open_book:图例
+
+控制台运行:
+
+![控制台运行](images/use-console.png)
+
+HTTP API图形界面-边界查询:
+
+![HTTP API图形界面-边界查询](images/use-map-echarts.png)
+
+HTTP API图形界面-坐标查询:
+
+![HTTP API图形界面-坐标查询](images/use-map-echarts2.png)
+
+HTTP API调用查询:
+
+![HTTP API调用查询](images/use-http-api.png)
+
+
+
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+# :open_book:知识库
+
+本工具的代码 *.java 文件全部被丢到根目录,没有创建包名目录,源码直接根目录裸奔,简单粗暴;这样的项目结构肉眼看去也算是简洁,也方便copy文件使用。
+
+编写本查询工具代码的原因是数据库查询坐标点对应的城市速度太慢了,影响数据库运行;群里有用户提到过用未知开源库可达到1ms查询一次坐标点,所以开始了本次探索历程。
+
+
+[​](?)
+
+## 关于RTree索引
+研究过程中,群成员已实现1秒几万次查询,介绍使用RTree索引来加速空间数据查找(感谢群成员`QQ: 2668361600`的介绍);通过`com.github.davidmoten:rtree`来给每个图形单独建一个索引,索引内存放此图形的每一条边,计算坐标点几何关系的时候,直接通过射线法用RTree来查找和图形相交的边数,性能极高;相对的内存占用也非常高,测试的省市区三级需要2G多内存,仅区级也需要1.3G内存,因为内存占用过大的问题,目前并未使用本方法,有兴趣可以自行用RTree实现一下,性能可提升10+倍。
+
+在[v2ex发的帖子](https://www.v2ex.com/t/863092)也有同学介绍使用`google s2 算法`来进行查询,性能也是极高的,也可以学习研究研究。
+
+jts库内部已自带了STRtree,目前已用于加速envelope的查找,进行初步数据的筛选,性能也是极高的。
+
+
+[​](?)
+
+## 性能优化过程
+对于jts库,单纯的直接用`Geometry.intersects`检测一个坐标点是否在边界图形内,性能是很低的;全国省市区3632条数据,全部检测一遍大约要10ms-15ms,这些性能损耗集中在图形几何计算中;已知intersects方法自带有envelope快速检测,会过滤掉绝大部分不包含此坐标点图形,剩下为数不多的几个图形将进行实质上的几何关系计算,边界图形的坐标点数越多,性能越低(比如内蒙的边界5万多个点,计算非常费时)。
+
+因此只要减少边界图形的坐标点,查询性能就会提升,极端的就是4个坐标点,查询性能最高;我们就可以将大的边界图形切割成很小的图形,并且尽量使图形内部大面积的切成矩形(4个坐标点),查询性能就得到很大的提升;`AreaCityQuery`内的`GeometryGridSplit`方法就是按照这个原则对边界图形进行切割拆分的。
+
+用`Debug_ReadGeometryGridSplitsWKT`方法、或测试控制台内菜单`查询: Debug 读取边界网格划分图形WKT文本数据`来导出拆分后的图形WKT数据,然后可以直接粘贴到《[ECharts Map四级下钻在线测试和预览+代码生成](https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html)》页面内渲染显示;比如内蒙的边界图形切割后的样子:
+
+![HTTP API调用查询](images/use-debug-grid-split.png)
+
+上图中内蒙的边界有5万多个坐标点,切割后生成了839个小图形,很多是矩形4个坐标点,只有在边界上的小图形坐标点相对多一点。
+
+
+[​](?)
+
+## 内存优化过程
+省市区三级 176MB 的geojson文件,3632条数据不做任何处理,全部解析成Geometry对象加载到内存后,内存占用约318MB。
+
+因为几何图形内是大量的双精度浮点数坐标点,因此可以将Geometry对象转成WKB二进制数据,再放到内存里面,另外Geometry对象的envelope对象也需要单独存一份到内存,在查询的时候,先经过envelope急速的初步筛选,筛出来的为数不多的图形,然后取出WKB二进制数据还原成Geometry对象进行几何运算,整个内存占用约140MB,减少一倍多;但此方式增加了WKB还原成Geometry对象的损耗,约0.01ms一次还原;这个就是现在的 Init_StoreInMemory 初始化数据存储方式。
+
+WKB二进制数据放内存里面始终要占用内存,因此把Geometry对象转成WKB二进制数据后存放到文件里面,内存中只存Geometry对象的envelope对象,在查询的时候,先经过envelope急速的初步筛选,筛出来的为数不多的图形,先从文件读取出对应的WKB二进制数据,然后还原成Geometry对象进行几何运算,整个内存占用约10MB;此方式增加了文件IO损耗(约0.3ms一次IO)和WKB还原成Geometry对象的损耗(约0.01ms一次还原);这个就是现在的 Init_StoreInWkbsFile 初始化数据存储方式。
+
+注:在经过性能优化后,每个边界图形切分成了很多个小图形,两种初始化方式对应的内存占用也增大了一些。
+
+
+
+
+[​](?)
+
+[​](?)
+
+[​](?)
+
+# :star:捐赠
+如果这个库有帮助到您,请 Star 一下。
+
+您也可以使用支付宝或微信打赏作者:
+
+![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-alipay.png)  ![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/donate-weixin.png)

+ 1077 - 0
AreaCity-Query-Geometry/Test.java

@@ -0,0 +1,1077 @@
+package com.github.xiangyuecn.areacity.query;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Random;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.io.WKTReader;
+
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.Func;
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryInitInfo;
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryResult;
+
+/**
+ * AreaCityQuery测试控制台主程序
+ * 
+ * GitHub: https://github.com/xiangyuecn/AreaCity-Query-Geometry (github可换成gitee)
+ * 省市区县乡镇区划边界数据: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee)
+ */
+public class Test {
+	static public void main(String[] args) throws Exception {
+		//【请在这里编写你自己的测试代码】
+		
+		
+/***【运行时直接启动http api服务】	可解开这个注释即可生效
+		// 比如在服务器里面用,使用这段代码不会显示命令行菜单,会直接启动http服务;linux服务器中可使用 `nohup bash xxx.sh` 命令执行后台运行
+		Init_FromFile(0, true, "D:/xxx/ok_geo.json"); //支持使用.json、.geojson、.wkbs文件进行初始化
+		//Init_FromFile(1, true, "D:/xxx/xx1.json"); //允许使用不同的数据文件初始化多个静态实例,api接口可以调用不同实例进行查询
+		//Init_FromFile(2, true, "D:/xxx/xx2.json"); 
+		StartHttpApiServer(HttpApiServerPort); //直接启动http api服务,可指定端口
+		if(true)return;
+***/
+		
+		
+		
+/***【基础调用示例代码】	可解开这个注释即可生效
+		String jsonFile="仅供测试-全国省级GeoJSON数据-大幅简化粗略版.json";
+		//先获取到查询实例,默认提供的0-9的10个静态实例,每个实例可以分别使用一个数据文件进行初始化和查询,当然自己调用new AreaCityQuery()创建一个新实例使用也是一样的
+		AreaCityQuery Instance=AreaCityQuery.Instances[0];
+		//static public final AreaCityQuery Instance=new AreaCityQuery(); //也可以自己创建静态实例
+
+		//查询前先初始化,每个实例全局只会初始化一次,每次查询前都调用即可(查询会在初始化完成后进行),两种初始化方式根据自己业务情况二选一
+		//首次初始化会从.json或.geojson文件中读取边界图形数据,速度比较慢,会自动生成.wkbs结尾的结构化文件,下次初始化就很快了
+		//首次初始化生成了.wkbs文件后,后续初始化可以只使用此wkbs文件,允许不用再提供geojson文件(数据更新时需删除wkbs文件再重新用geojson文件进行初始化),具体请阅读对应初始化方法的注释文档
+		Instance.Init_StoreInWkbsFile(jsonFile, jsonFile+".wkbs", true);
+		//Instance.Init_StoreInMemory("geojson文件路径", "geojson文件路径.wkbs", true);
+
+		//Instance.OnInitProgress=(initInfo)->{ ... } //初始化过程中的回调,可以绑定一个函数,接收初始化进度信息(编写时需在Init之前进行绑定)
+		System.out.println(Instance.GetInitInfo().toString()); //打印初始化详细信息,包括性能信息
+
+		//注意:以下查询中所有坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确
+		//查询包含一个坐标点的所有边界图形的属性数据,可通过res参数让查询额外返回wkt格式边界数据
+		//查询结果的判定:请不要假定查询结果的数量(坐标刚好在边界上可能会查询出多个省市区),也不要假定查询结果顺序(结果中省市区顺序是乱序的),请检查判定res1.Result中的结果是否符合查询的城市级别,比如查询省市区三级:结果中必须且仅有3条数据,并且省市区都有(判断deep=0省|1市|2区 来区分数据的级别),其他一律判定为查询无效
+		QueryResult res1=Instance.QueryPoint(114.044346, 22.691963, null, null);
+		//当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),下面这个方法将能够匹配到附近不远的边界图形数据;2500相当于一个以此坐标为中心点、半径为2.5km的圆形范围,会查询出在这个范围内和此坐标点距离最近的边界
+		QueryResult res1_2=Instance.QueryPointWithTolerance(121.993491, 29.524288, null, null, 2500);
+
+		//查询和一个图形(点、线、面)有交点的所有边界图形的属性数据,可通过res参数让查询额外返回wkt格式边界数据
+		Geometry geom=new WKTReader(AreaCityQuery.Factory).read("LINESTRING(114.30115 30.57962, 117.254285 31.824198, 118.785633 32.064869)");
+		//Geometry geom=AreaCityQuery.CreateSimpleCircle(114.044346, 22.691963, 10000, 24); //以指定坐标为中心点,创建一个半径为10km的圆面,圆面由24个坐标点粗略构成
+		QueryResult res2=Instance.QueryGeometry(geom, null, null);
+
+		//读取省市区的边界数据wkt格式,这个例子会筛选出武汉市所有区县
+		QueryResult res3=Instance.ReadWKT_FromWkbsFile("wkt_polygon", null, (prop)->{return prop.contains("武汉市 ");}, null);
+		//此方法会遍历所有边界图形的属性列表,因此可以用来遍历所有数据,提取感兴趣的属性内容,比如查询一个区划编号id对应的城市信息(城市名称、中心点)
+		QueryResult res4=Instance.ReadWKT_FromWkbsFile(null, null, (prop)->{
+		    prop=(","+prop.substring(1, prop.length()-1)+",").replace("\"", "").replace(" ", ""); //不解析json,简单处理
+		    return prop.contains(",id:42,"); //只查询出id=42(湖北省)的属性数据(注意初始化的geojson中必须要有对应的属性名,这里是id)
+		}, null);
+
+
+		System.out.println(res1+"\n"+res1_2+"\n"+res2+"\n"+res3+"\n"+res4);
+		if(true)return;
+*****/
+		
+		
+		Start(args);
+	}
+	
+	
+	static int CurrentIdx=0;
+	static AreaCityQuery Current=AreaCityQuery.Instances[CurrentIdx];
+	/** 初始化时,只读取区县级数据,否则省市区都读 **/
+	static boolean Deep2Only=false;
+	/** http api 服务端口号**/
+	static int HttpApiServerPort=9527;
+	
+	/**使用数据文件直接对实例进行初始化*/
+	static void Init_FromFile(int instance, boolean storeInWkbsFile, String dataFile) throws Exception {
+		CurrentIdx=instance;
+		Current=AreaCityQuery.Instances[instance];
+		Init__(storeInWkbsFile, dataFile);
+		Current.CheckInitIsOK();
+	}
+
+	static long BootMaxMemory;
+	/**命令行菜单调用初始化*/
+	static void Init_FromMenu(boolean storeInWkbsFile) throws Exception {
+		if(BootMaxMemory==0) BootMaxMemory=Runtime.getRuntime().maxMemory();
+		if(BootMaxMemory > 300*1000*1000) {
+			System.out.println("可以在启动参数中配置 -Xmx300m 调小内存来测试。");
+		}
+		System.out.println("========== "+(storeInWkbsFile?"Init_StoreInWkbsFile":"Init_StoreInMemory")+" ==========");
+		
+		File file=new File("./");
+		String[] files=file.list();
+		ArrayList<String> jsonFiles=new ArrayList<>();
+		ArrayList<String> wkbsFiles=new ArrayList<>();
+		for(String name : files) {
+			String str=name.toLowerCase();
+			if(str.endsWith("json") || str.endsWith("geojson")) {
+				jsonFiles.add(name);
+			}
+			if(str.endsWith("wkbs")) {
+				wkbsFiles.add(name);
+			}
+		}
+		String dataFile="";
+		
+		
+		System.out.println("Init_StoreInWkbsFile:用加载数据到结构化数据文件的模式进行初始化,推荐使用本方法初始化,边界图形数据存入结构化数据文件中,内存占用很低,查询时会反复读取文件对应内容,查询性能消耗主要在IO上,IO性能极高问题不大。");
+		System.out.println("Init_StoreInMemory:用加载数据到内存的模式进行初始化,边界图形数据存入内存中,内存占用和json数据文件大小差不多大,查询性能极高;另外可通过设置 Instances[0-9].SetInitStoreInMemoryUseObject=true 来进一步提升性能,但内存占用会增大一倍多。");
+		System.out.println(HR);
+		System.out.println("首次初始化会从.json或.geojson文件中读取边界图形数据,并生成.wkbs结尾的结构化文件,速度比较慢(文件读写);下次会直接从.wkbs文件进行初始化(文件只读不写),就很快了,可copy此.wkbs文件到别的地方使用(比如服务器、只读环境中)。");
+		System.out.println("  - 如果.wkbs文件已存在并且有效,将优先从wkbs文件中读取数据,速度很快。");
+		System.out.println("  - 你可以在当前目录内放入 .json|.geojson|.wkbs 文件(utf-8),可通过菜单进行选择初始化,否则需要输入文件路径。");
+		System.out.println("  - 当前目录:"+new File("").getAbsolutePath());
+		System.out.println("  - json文件内必须一条数据占一行,如果不是将不支持解析,请按下面的方法生成一个json文件测试。");
+		System.out.println(HR);
+		System.out.println("如何获取省市区县乡镇边界数据json文件:");
+		System.out.println("  1. 请到开源库下载省市区边界数据ok_geo.csv文件: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee);");
+		System.out.println("  2. 下载开源库里面的“AreaCity-Geo格式转换工具软件”;");
+		System.out.println("  3. 打开转换工具软件,选择ok_geo.csv,然后导出成geojson文件即可(默认会导出全国的省级数据,通过填写不同城市名前缀可以导出不同城市);");
+		System.out.println("  4. 将导出的json文件复制到本java程序目录内,然后重新运行初始化即可解析使用此文件。");
+		System.out.println(HR);
+		boolean useInputFile=false;
+		boolean useChoice=false;
+		if(jsonFiles.size()>0 || wkbsFiles.size()>0) {
+			useChoice=true;
+		} else {
+			System.out.println("在当前目录内未发现任何一个 .json|.geojson|.wkbs 文件,需手动填写文件路径。");
+			System.out.println();
+			useInputFile=true;
+		}
+		if(useChoice) {
+			System.out.println("在当前目录内发现数据文件,请选择要从哪个文件初始化,请输入文件序号:");
+			int idx=0;
+			System.out.println(idx+". 手动输入文件路径");
+			for(String name : jsonFiles) {
+				idx++;
+				System.out.println(idx+". [json]"+name);
+			}
+			for(String name : wkbsFiles) {
+				idx++;
+				System.out.println(idx+". [wkbs]"+name);
+			}
+			while(true) {
+				System.out.print("> ");
+				String txt=ReadIn();
+				if(txt.length()==0){
+					break;
+				} else if(txt.equals("0")) {
+					useInputFile=true;
+					break;
+				} else if(txt.length()>0) {
+					idx=-1;
+					try { idx=Integer.parseInt(txt); } catch (Exception e){ }
+					if(idx>0 && idx<=jsonFiles.size()) {
+						dataFile=jsonFiles.get(idx-1);
+					} else if(idx>0 && idx-jsonFiles.size()<=wkbsFiles.size()) {
+						dataFile=wkbsFiles.get(idx-jsonFiles.size()-1);
+					} else {
+						System.out.println("输入的序号无效,请重新输入!");
+						continue;
+					}
+					break;
+				}
+			}
+		}
+		if(useInputFile) {
+			System.out.println("请输入初始化要读取的一个 .json|.geojson|.wkbs 文件完整路径:");
+			while(true) {
+				System.out.print("> ");
+				String txt=ReadIn();
+				String str=txt.toLowerCase();
+				if(txt.length()==0) {
+					break;
+				} else if(str.endsWith("json") || str.endsWith("geojson")) {
+					dataFile=txt;
+				} else if (str.endsWith("wkbs")) {
+					dataFile=txt;
+				} else {
+					System.out.println("输入的文件类型无效,请重新输入!");
+					continue;
+				}
+				File f=new File(txt);
+				if(!f.exists()) {
+					dataFile="";
+					System.out.println("文件不存在,请重新输入!(未找到文件:"+f.getAbsolutePath()+")");
+					continue;
+				}
+				break;
+			}
+		}
+		
+		Init__(storeInWkbsFile, dataFile);
+	}
+	
+	/**省**/static boolean[] HasDeep0=new boolean[AreaCityQuery.Instances.length];
+	/**市**/static boolean[] HasDeep1=new boolean[AreaCityQuery.Instances.length];
+	/**区县**/static boolean[] HasDeep2=new boolean[AreaCityQuery.Instances.length];
+	/**乡镇街道**/static boolean[] HasDeep3=new boolean[AreaCityQuery.Instances.length];
+	static void Init__(boolean storeInWkbsFile, String dataFile) throws Exception {
+		String initDataFile,initSaveWkbsFile;
+		String str=dataFile.toLowerCase();
+		if(str.endsWith("json") || str.endsWith("geojson")) {
+			initDataFile=new File(dataFile).getAbsolutePath();
+			initSaveWkbsFile=initDataFile+".wkbs";
+			if(!storeInWkbsFile) {
+				System.out.println("用json文件进行Init_StoreInMemory初始化时,可选提供一个.wkbs后缀的文件路径,初始化时会自动生成此文件,如果不提供将不能查询WKT数据;直接回车提供,输入n不提供:");
+				System.out.print("> ");
+				String txt=ReadIn();
+				if(txt.toLowerCase().equals("n")) {
+					initSaveWkbsFile="";
+				}
+			}
+		} else if(str.endsWith("wkbs")) {
+			initDataFile=new File(dataFile).getAbsolutePath();
+			initSaveWkbsFile="";
+		} else {
+			System.out.println("未选择文件,不初始化,已退出!");
+			return;
+		}
+		
+		HasDeep0[CurrentIdx]=HasDeep1[CurrentIdx]=HasDeep2[CurrentIdx]=HasDeep3[CurrentIdx]=false;
+
+		//初始化回调
+		Current.OnInitProgress=new Func<QueryInitInfo, Boolean>() {
+			long logTime=0;int maxNo=0,lastNo=0;
+			@Override
+			public Boolean Exec(QueryInitInfo info) throws Exception {
+				if(logTime==0) {
+					if(info.DataFromWkbsFile) {
+						System.out.println("正在从wkbs结构化数据文件中快速读取数据...");
+					} else if(info.HasWkbsFile){
+						System.out.println("首次运行,正在生成wkbs结构化数据文件,速度可能会比较慢...");
+					} else {
+						System.out.println("正在从json文件中读取数据,未提供wkbs文件,速度可能会比较慢...");
+					}
+				}
+				if(info.CurrentLine_No!=0) {
+					maxNo=info.CurrentLine_No;
+				}
+				if(info.CurrentLine_No==0 && lastNo!=maxNo || System.currentTimeMillis()-logTime>1000) {
+					logTime=System.currentTimeMillis();
+					lastNo=maxNo;
+					System.out.println("读取第"+lastNo+"行...");
+				}
+				if(info.CurrentLine_No!=0) {
+					String prop=info.CurrentLine_Prop.replace(" ", "").replace("\"", ""); //不解析json,简单处理
+					if(!Deep2Only && !HasDeep0[CurrentIdx])HasDeep0[CurrentIdx]=prop.contains("deep:0");
+					if(!Deep2Only && !HasDeep1[CurrentIdx])HasDeep1[CurrentIdx]=prop.contains("deep:1");
+					if(!HasDeep2[CurrentIdx])HasDeep2[CurrentIdx]=prop.contains("deep:2");
+					if(!Deep2Only && !HasDeep3[CurrentIdx])HasDeep3[CurrentIdx]=prop.contains("unique_id:");
+					
+					if(Deep2Only) {
+						return prop.contains("deep:2"); //只提取区级,其他一律返回false跳过解析
+					}
+				}
+				return true;
+			}
+		};
+		//初始化,如果未生成结构化数据文件(wkbs)这里会从json数据文件自动生成,如果生成了就只会读取wkbs文件
+		if(storeInWkbsFile) {
+			Current.Init_StoreInWkbsFile(initDataFile, initSaveWkbsFile, true);
+		} else {
+			Current.Init_StoreInMemory(initDataFile, initSaveWkbsFile, true);
+		}
+
+		System.out.println("========== "+(storeInWkbsFile?"Init_StoreInWkbsFile":"Init_StoreInMemory")+" ==========");
+		System.out.println(Current.GetInitInfo().toString());
+		System.out.println();
+		System.out.println("已加载数据级别:"+(HasDeep0[CurrentIdx]?"√":"×")+"省,"+(HasDeep1[CurrentIdx]?"√":"×")+"市,"+(HasDeep2[CurrentIdx]?"√":"×")+"区县,"+(HasDeep3[CurrentIdx]?"√":"×")+"乡镇 (×为未加载,可能是数据文件中并不含此级数据)");
+		System.out.println();
+	}
+	
+	static boolean ResultHas(QueryResult res, String str) {
+		if(res.Result!=null) {
+			for(int i=0,L=res.Result.size();i<L;i++) {
+				if(res.Result.get(i).contains(str)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	} 
+	static void BaseTest(int instanceIdx) throws Exception {
+		AreaCityQuery instance=AreaCityQuery.Instances[instanceIdx];
+		int loop=100;
+		System.out.println("========== QueryPoint ==========");
+		{
+			QueryResult res=new QueryResult();
+			res.Set_EnvelopeHitResult=new ArrayList<>();
+			//res.Set_ReturnWKTKey="polygon_wkt";
+			for(int i=0;i<loop;i++) {
+				res.Result.clear();//清除一下上次的结果,只保留统计
+				res.Set_EnvelopeHitResult.clear();
+				res=instance.QueryPoint(114.044346, 22.691963, null, res);
+			}
+			System.out.println(res.toString());
+			if(HasDeep2[instanceIdx]) {
+				System.out.println(ResultHas(res, "龙华区\"")?"OK":"查询失败!");
+			}
+		}
+		
+		System.out.println();
+		System.out.println("========== QueryPointWithTolerance ==========");
+		{
+			QueryResult res=new QueryResult();
+			res.Set_EnvelopeHitResult=new ArrayList<>();
+			double lng=121.993491,lat=29.524288;
+			QueryResult res2=instance.QueryPoint(lng, lat, null, null);
+			for(int i=0;i<loop;i++) {
+				res.Result.clear();//清除一下上次的结果,只保留统计
+				res.Set_EnvelopeHitResult.clear();
+				res=instance.QueryPointWithTolerance(lng, lat, null, res, 2500);
+			}
+			System.out.println(res.toString());
+			if(HasDeep2[instanceIdx]) {
+				System.out.println(ResultHas(res, "象山县\"") && res2.Result.size()==0?"OK":"查询失败!");
+			}
+		}
+
+		System.out.println();
+		System.out.println("========== QueryGeometry ==========");
+		if(!HasDeep0[instanceIdx]){
+			System.out.println("无省级边界,其他级别返回结果会过多,不测试。");
+		}else{
+			double x0=113.305514,y0=30.564249;//河南、安徽、湖北,三省交界的一个超大矩形范围
+			double x1=117.326510,y1=32.881526;
+			Geometry geom=AreaCityQuery.Factory.createPolygon(new Coordinate[] {
+					new Coordinate(x0, y0)
+					,new Coordinate(x1, y0)
+					,new Coordinate(x1, y1)
+					,new Coordinate(x0, y1)
+					,new Coordinate(x0, y0)
+			});
+			
+			QueryResult res=new QueryResult();
+			for(int i=0;i<loop;i++) {
+				res.Result.clear();//清除一下上次的结果,只保留统计
+				res=instance.QueryGeometry(geom, new Func<String, Boolean>() {
+					@Override
+					public Boolean Exec(String prop) throws Exception {
+						int i0=prop.indexOf("deep\"");//高性能手撸json字符串
+						if(i0==-1)return false;
+						int i1=prop.indexOf(",", i0);
+						if(i1==-1)i1=prop.length();
+						return prop.substring(i0+6,i1).contains("0");//where条件过滤,只查找省级数据(deep==0,json内就是{deep:0})
+					}
+				}, res);
+			}
+			System.out.println(res.toString());
+			System.out.println(ResultHas(res, "湖北省\"")
+					&& ResultHas(res, "河南省\"")
+					&& ResultHas(res, "安徽省\"") ?"OK":"查询失败!");
+		}
+
+		System.out.println();
+		System.out.println("========== ReadWKT_FromWkbsFile ==========");
+		{
+			QueryResult res=new QueryResult();
+			for(int i=0;i<loop;i++) {
+				res.Result.clear();//清除一下上次的结果,只保留统计
+				String wktKey="plygon_wkt";
+				if(!instance.GetInitInfo().HasWkbsFile) {
+					if(i==0)System.out.println("【注意】初始化时如果没有提供wkbs文件,不能查询wkt数据");
+					wktKey="";
+				}
+				res=instance.ReadWKT_FromWkbsFile(wktKey, res, new Func<String, Boolean>() {
+					@Override
+					public Boolean Exec(String prop) throws Exception {
+						return prop.contains("北京市 朝阳区\"")
+							|| prop.contains("武汉市 洪山区\"")
+							|| prop.contains("台北市 中山区\"");
+					}
+				}, null);
+			}
+			System.out.println(res.toString());
+			if(HasDeep2[instanceIdx]) {
+				System.out.println(ResultHas(res, "北京市 朝阳区\"")
+						&& ResultHas(res, "武汉市 洪山区\"")
+						&& ResultHas(res, "台北市 中山区\"") ?"OK":"查询失败!");
+			}
+		}
+		System.out.println();
+	}
+	
+	static void LargeRndPointTest(int instanceIdx) throws Exception {
+		AreaCityQuery instance=AreaCityQuery.Instances[instanceIdx];
+		System.out.println("========== QueryPoint:1万个伪随机点测试 ==========");
+		System.out.println("伪随机:虽然是随机生成的点,但每次运行生成坐标列表都是相同的。");
+		for(int loop=0;loop<2;loop++) {
+			System.out.println((loop==0?"QueryPoint":"QueryPointWithTolerance")+"测试中,请耐心等待...");
+			
+			QueryResult res=new QueryResult();
+			double x_0=98.0,y_00=18.0;//矩形范围囊括大半个中国版图
+			double x_1=135.0,y_1=42.0;
+			int size=100;//1万点
+			double xStep=(x_1-x_0)/size;
+			double yStep=(y_1-y_00)/size;
+			while(x_0-x_1<-xStep/2) {//注意浮点数±0.000000001的差异
+				double x0=x_0, x1=x_0+xStep; x_0=x1;
+				double y_0=y_00;
+				while(y_0-y_1<-yStep/2) {
+					double y0=y_0, y1=y_0+yStep; y_0=y1;
+					
+					if(loop==0) {
+						res=instance.QueryPoint(x0, y0, null, res);
+					}else {
+						res=instance.QueryPointWithTolerance(x0, y0, null, res, 2500);
+					}
+					res.Result.clear();//只保留统计
+				}
+			}
+			res.Result=null;
+			System.out.println(res.toString());
+			System.out.println();
+		}
+	}
+	static void ThreadRun(ArrayList<Integer> idxs) throws Exception {
+		System.out.println("========== 多线程性能测试 ==========");
+		boolean[] stop=new boolean[] {false};
+		int ThreadCount=Math.max(1, Runtime.getRuntime().availableProcessors()-1);
+		QueryResult[] SecondCompletes=new QueryResult[ThreadCount];
+
+		System.out.println("通过开启 CPU核心数-1 个线程,每个线程内随机查询不同的坐标点,来达到性能测试的目的。");
+		System.out.println("正在测试中,线程数:"+ThreadCount+",按回车键结束测试...");
+		
+		//测试函数
+		Func<Object[], Object> run=new Func<Object[], Object>() {	
+			@Override
+			public Object Exec(Object[] args) throws Exception {
+				QueryResult res=(QueryResult)args[0];
+				int instanceIdx=(int)args[1];
+				AreaCityQuery instance=AreaCityQuery.Instances[instanceIdx];
+				
+				//固定坐标点测试
+				boolean allOk=true;
+				instance.QueryPoint(114.044346, 22.691963, null, res); //广东省 深圳市 龙华区
+				allOk&=ResultHas(res, "深圳市 龙华区\"");res.Result.clear();
+				
+				instance.QueryPoint(117.286491, 30.450399, null, res); //安徽省 铜陵市 郊区 飞地
+				allOk&=ResultHas(res, "铜陵市 郊区\"");res.Result.clear();
+				
+				instance.QueryPoint(116.055588, 39.709385, null, res); //北京市 房山区 星城街道 飞地
+				allOk&=ResultHas(res, "北京市 房山区\"");res.Result.clear();
+				
+				instance.QueryPoint(130.283168, 47.281807, null, res); // 黑龙江省 鹤岗市 南山区
+				allOk&=ResultHas(res, "鹤岗市 南山区\"");res.Result.clear();
+				
+				instance.QueryPoint(118.161624, 39.656532, null, res); // 河北省  唐山市 路北区
+				allOk&=ResultHas(res, "唐山市 路北区\"");res.Result.clear();
+				
+				instance.QueryPoint(81.869760, 41.812321, null, res); // 新疆 阿克苏地区 拜城县
+				allOk&=ResultHas(res, "阿克苏地区 拜城县\"");res.Result.clear();
+				
+				if(HasDeep2[instanceIdx] && !allOk) {
+					throw new Exception("查询失败!");
+				}
+				
+				
+				//随机坐标点测试
+				Random rnd=new Random();
+				int count=100;//只计算100次
+				
+				double x_0=98.0+rnd.nextDouble(),y_00=21.0+rnd.nextDouble();//矩形范围囊括大半个中国版图
+				double x_1=122.0+rnd.nextDouble(),y_1=42.0+rnd.nextDouble();
+				int size=100;//1万点
+				double xStep=(x_1-x_0)/size;
+				double yStep=(y_1-y_00)/size;
+				x_0+=(rnd.nextInt(size*size-count))*xStep; //随机选择开始位置
+				while(x_0-x_1<-xStep/2) {//注意浮点数±0.000000001的差异
+					double x0=x_0, x1=x_0+xStep; x_0=x1;
+					double y_0=y_00;
+					while(y_0-y_1<-yStep/2) {
+						double y0=y_0, y1=y_0+yStep; y_0=y1;
+						
+						instance.QueryPoint(x0, y0, null, res);
+						res.Result.clear();//只保留统计
+						
+						count--;
+						if(count==0) {
+							return null;
+						}
+					}
+				}
+				return null;
+			}
+		};
+		//更新统计显示
+		long startTime=System.currentTimeMillis();
+		long[] showProgressTime=new long[] {0};
+		Func<Object,Object> showProgress=new Func<Object, Object>() {
+			@Override
+			public Object Exec(Object val) throws Exception {
+				synchronized (showProgressTime) {
+					if(System.nanoTime()-showProgressTime[0]<1000*1000000) {
+						return null;
+					}
+					
+					QueryResult res=new QueryResult();
+					for(int i=0;i<SecondCompletes.length;i++) {
+						if(SecondCompletes[i]==null) {
+							return null;
+						}
+						SecondCompletes[i].Result=null;
+						res.Add(SecondCompletes[i]);
+					}
+					showProgressTime[0]=System.nanoTime();
+					
+					res.QueryCount=Math.max(2, res.QueryCount);
+					res.StartTimeN=0;
+					res.EndTimeN=1000L*1000000*ThreadCount;
+					String[] arr=res.toString().split("\n");
+					long dur=System.currentTimeMillis()-startTime;
+					long f=dur/60000,m=dur/1000%60;
+					String s=(f<10?"0":"")+f+":"+(m<10?"0":"")+m;
+					if(!stop[0]) {
+						System.out.print("\r"+s
+								+" QPS["+ThreadCount+"线程 "+res.QueryCount+"]"
+								+"[单线程 "+res.QueryCount/ThreadCount+"]"
+								+(idxs.size()>1?"多实例":Current.IsStoreInMemory()?"InMemory":"InWkbsFile")
+								+" "+arr[1]+"。按回车键结束测试...");
+					}
+					return null;
+				}
+			}
+		};
+		//获得一个线程执行函数
+		Func<Integer,Func<Object, Object>> newThreadRun=new Func<Integer, Func<Object, Object>>() {
+			@Override
+			public Func<Object, Object> Exec(Integer threadId) throws Exception {
+				return new Func<Object, Object>() {
+					@Override
+					public Object Exec(Object val) throws Exception {
+						QueryResult res=new QueryResult();//这个只能单线程用
+						Object[] args=new Object[] { res, CurrentIdx };
+						Random rnd=new Random();
+						long t1=System.nanoTime();
+						while(!stop[0]) {
+							try {
+								run.Exec(args);
+								
+								long t0=System.nanoTime();
+								if(t0-t1>=1000*1000000) {//1秒钟,给赋值一次统计数据
+									SecondCompletes[threadId]=res;
+									showProgress.Exec(null);
+									args[0]=res=new QueryResult();
+									args[1]=idxs.get(rnd.nextInt(idxs.size()));
+									t1=System.nanoTime();
+								}
+							} catch(Exception e) {
+								e.printStackTrace();
+							}
+						}
+						return null;
+					}
+				};
+			}
+		};
+		//开启多线程
+		int[] threadCount=new int[] { ThreadCount };
+		for(int i=0;i<ThreadCount;i++) {
+			Func<Object, Object> threadRun=newThreadRun.Exec(i);
+			new Thread(new Runnable() {
+				public void run() {
+					try {
+						threadRun.Exec(null);
+					} catch(Exception e) {
+						e.printStackTrace();
+					} finally {
+						synchronized (threadCount) { threadCount[0]--; }
+					}
+				}
+			}).start();
+		}
+		
+		ReadIn();
+		stop[0]=true;
+		System.out.println("等待线程结束...");
+		while(threadCount[0]>0) {
+			try { Thread.sleep(10); }catch (Exception e) { }
+		}
+		System.out.println("多线程性能测试已结束。");
+		System.out.println();
+	}
+	
+	
+	
+	static void Query_Point() throws Exception {
+		String idxs=GetInitInstanceIdxs();
+		System.out.println("========== 查询一个坐标点对应的省市区乡镇数据 ==========");
+		System.out.println("注意:输入坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。");
+		System.out.println("请输入一个坐标点,格式:\"lng lat\"(允许有逗号):");
+		System.out.println("  - 比如:114.044346 22.691963,为广东省 深圳市 龙华区");
+		System.out.println("  - 比如:117.286491 30.450399,为安徽省 铜陵市 郊区,在池州市 贵池区的飞地");
+		System.out.println("  - 比如:121.993491 29.524288,为浙江省 宁波市 象山县,但坐标点位于海岸线外侧,不在任何边界内,需设置tolerance才能查出");
+		System.out.println("  - 输入 tolerance=2500 设置距离范围容差值,单位米,比如2500相当于一个以此坐标为中心点、半径为2.5km的圆形范围;默认0不设置,-1不限制距离;当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),设置tolerance后,会查询出在这个范围内和此坐标点距离最近的边界数据");
+		System.out.println("  - 输入 exit 退出查询,输入 use "+idxs+" 切换查询实例");
+		int tolerance=0;
+		while(true){
+			System.out.print((idxs.length()<2?"":"[实例"+CurrentIdx+"] ")+"> ");
+			String inStr=ReadIn().trim();
+			if(inStr.length()==0) {
+				System.out.println("输入为空,请重新输入!如需退出请输入exit");
+				continue;
+			}
+			if(inStr.equals("exit")) {
+				System.out.println("bye! 已退出查询。");
+				System.out.println();
+				return;
+			}
+			if(inStr.startsWith("use ")) {
+				if(InTxt_SetCurrent(inStr, true)) {
+					System.out.println("已设置当前实例为"+CurrentIdx);
+				}
+				continue;
+			}
+			if(inStr.startsWith("tolerance")) {
+				Matcher m=Pattern.compile("^tolerance[=\\s]+([+-]*\\d+)$").matcher(inStr);
+				if(!m.find()) {
+					System.out.println("tolerance设置格式错误,请重新输入");
+				}else {
+					tolerance=Integer.parseInt(m.group(1));
+					System.out.println("已设置tolerance="+tolerance);
+				}
+				continue;
+			}
+			String[] arr=inStr.split("[,\\s]+");
+			double lng=-999,lat=-999;
+			if(arr.length==2) {
+				try {
+					lng=Double.parseDouble(arr[0]);
+					lat=Double.parseDouble(arr[1]);
+				}catch(Exception e) {
+					lng=lat=-999;
+				}
+			}
+			if(lng<-180 || lat<-90 || lng>180 || lat>90) {
+				System.out.println("输入的坐标格式不正确");
+				continue;
+			}
+			QueryResult res;
+			if(tolerance==0) {
+				res=Current.QueryPoint(lng, lat, null, null);
+			}else {
+				System.out.println("QueryPointWithTolerance tolerance="+tolerance);
+				res=Current.QueryPointWithTolerance(lng, lat, null, null, tolerance);
+			}
+			System.out.println(res.toString());
+		}
+	}
+	
+	static void Query_Geometry() throws Exception {
+		String idxs=GetInitInstanceIdxs();
+		System.out.println("========== 查询和任意一个几何图形相交的省市区乡镇数据 ==========");
+		System.out.println("注意:输入WKT的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。");
+		System.out.println("请输入一个WKT文本(Well Known Text):");
+		System.out.println("  - 比如:POINT(114.044346 22.691963),坐标点,为广东省 深圳市 龙华区");
+		System.out.println("  - 比如:LINESTRING(114.30115 30.57962, 117.254285 31.824198, 118.785633 32.064869),路径线段,武汉-合肥-南京 三个点连成的线段");
+		System.out.println("  - 比如:POLYGON((113.305514 30.564249, 113.305514 32.881526, 117.326510 32.881526, 117.326510 30.564249, 113.305514 30.564249)),范围,湖北-河南-安徽 三省交界的一个超大矩形范围");
+		System.out.println("  - 输入 exit 退出查询,输入 use "+idxs+" 切换查询实例");
+		while(true){
+			System.out.print((idxs.length()<2?"":"[实例"+CurrentIdx+"] ")+"> ");
+			String inStr=ReadIn().trim();
+			if(inStr.length()==0) {
+				System.out.println("输入为空,请重新输入!如需退出请输入exit");
+				continue;
+			}
+			if(inStr.equals("exit")) {
+				System.out.println("bye! 已退出查询。");
+				System.out.println();
+				return;
+			}
+			if(inStr.startsWith("use ")) {
+				if(InTxt_SetCurrent(inStr, true)) {
+					System.out.println("已设置当前实例为"+CurrentIdx);
+				}
+				continue;
+			}
+			Geometry geom;
+			try {
+				geom=new WKTReader(AreaCityQuery.Factory).read(inStr);
+			}catch(Exception e) {
+				System.out.println("输入的WKT解析失败:"+e.getMessage());
+				continue;
+			}
+			QueryResult res=Current.QueryGeometry(geom, null, null);
+			System.out.println(res.toString());
+		}
+	}
+	
+	static void Read_WKT() throws Exception {
+		System.out.println("========== 读取省市区乡镇边界的WKT文本数据 ==========");
+		System.out.println("遍历所有边界图形的属性列表查询出符合条件的属性,然后返回图形的属性+边界图形WKT文本。 ");
+		System.out.println("读取到的wkt文本,可以直接粘贴到页面内渲染显示:https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html");
+		System.out.println();
+		
+		ExtPathExpIn("ReadWKT", new Func<Test.ExtPathExpInArgs, Object>() {
+			@Override
+			public Object Exec(ExtPathExpInArgs args) throws Exception {
+				int[] count=new int[] { 0 };
+				Current.ReadWKT_FromWkbsFile("", null, new Func<String, Boolean>() {
+					@Override
+					public Boolean Exec(String prop) throws Exception {
+						return ExtPathMatch(prop, args.extPath_exp);
+					}
+				}, getWktReadFn("ReadWKT", args, count));
+				if(count[0] == 0) {
+					System.out.println("未找到“"+args.extPath_inputTxt+"”匹配的属性!");
+				} else {
+					System.out.println("查找完成,共"+count[0]+"条");
+				}
+				return null;
+			}
+		});
+	}
+
+	static void Query_DebugReadWKT() throws Exception {
+		System.out.println("========== Debug: 读取边界网格划分图形WKT文本数据 ==========");
+		System.out.println("调试用的,读取已在wkbs结构化文件中保存的网格划分图形WKT数据,用于核对网格划分情况。");
+		System.out.println("读取到的wkt文本,可以直接粘贴到页面内渲染显示:https://xiangyuecn.github.io/AreaCity-JsSpider-StatsGov/assets/geo-echarts.html");
+		System.out.println();
+		
+		ExtPathExpIn("GirdWKT", new Func<Test.ExtPathExpInArgs, Object>() {
+			@Override
+			public Object Exec(ExtPathExpInArgs args) throws Exception {
+				int[] count=new int[] { 0 };
+				Current.Debug_ReadGeometryGridSplitsWKT("", null, new Func<String, Boolean>() {
+					@Override
+					public Boolean Exec(String prop) throws Exception {
+						return ExtPathMatch(prop, args.extPath_exp);
+					}
+				}, getWktReadFn("GirdWKT", args, count));
+				if(count[0] == 0) {
+					System.out.println("未找到“"+args.extPath_inputTxt+"”匹配的边界!");
+				} else {
+					System.out.println("查找完成,共"+count[0]+"条");
+				}
+				return null;
+			}
+		});
+	}
+	
+	
+	static Func<String[], Boolean> getWktReadFn(String tag, ExtPathExpInArgs args, int[] count){
+		return new Func<String[], Boolean>() {
+			@Override
+			public Boolean Exec(String[] val) throws Exception {
+				count[0]++;
+				if(args.outFile!=null) {
+					String str=val[0] +"\t"+ val[1]+"\n";
+					byte[] bytes=str.getBytes("utf-8");
+					args.outFile.write(bytes);
+					
+					System.out.println(count[0]+"条"+tag+"属性:"+val[0]);
+					System.out.println("        "+bytes.length+"字节已保存到文件:"+args.outFilePath);
+				} else {
+					String str=count[0]+"条"+tag+"属性:"+val[0];
+					if(val[1].length()>500) {
+						System.out.println(str);
+						str="        WKT超长未显示("+val[1].length()+"字节),请命令后面输入\" > 文件名\"输出到文件";
+					} else {
+						str=str +"\t"+ val[1];
+					}
+					System.out.println(str);
+				}
+				return false;
+			}
+		};
+	}
+	static boolean ExtPathMatch(String prop, String exp) {
+		int i0=prop.indexOf("ext_path");
+		if(i0==-1)return false;
+		int i1=prop.indexOf(",", i0);
+		if(i1==-1)i1=prop.length();
+		return prop.substring(i0+9, i1).contains(exp);
+	}
+	static class ExtPathExpInArgs{
+		String extPath_exp;
+		String extPath_inputTxt;
+		String outFilePath;
+		FileOutputStream outFile;
+	}
+	static void ExtPathExpIn(String tag, Func<ExtPathExpInArgs, Object> fn) throws Exception {
+		String idxs=GetInitInstanceIdxs();
+		System.out.println("请输入"+tag+"要查询的城市完整名称,为ext_path值:");
+		System.out.println("  - 如:“湖北省 武汉市 洪山区”,精确查找");
+		System.out.println("  - 如:“*武汉市*”,*通配符模糊查找");
+		System.out.println("  - 如:“*”,查找全部");
+		System.out.println("  - 结尾输入“ > 文件名”可保存到文件");
+		System.out.println("  - 输入 exit 退出,输入 use "+idxs+" 切换查询实例");
+		while(true){
+			System.out.print((idxs.length()<2?"":"[实例"+CurrentIdx+"] ")+"> ");
+			String inStr=ReadIn().trim();
+			if(inStr.length()==0) {
+				System.out.println("输入为空,请重新输入!如需退出请输入exit");
+				continue;
+			}
+			if(inStr.equals("exit")) {
+				System.out.println("bye! 已退出读取。");
+				System.out.println();
+				return;
+			}
+			if(inStr.startsWith("use ")) {
+				if(InTxt_SetCurrent(inStr, true)) {
+					System.out.println("已设置当前实例为"+CurrentIdx);
+				}
+				continue;
+			}
+			ExtPathExpInArgs args=new ExtPathExpInArgs();
+			String[] ins=inStr.split(" > ");
+			args.extPath_inputTxt=ins[0];
+			
+			String outFilePath= ins.length>1?ins[1]:"";
+			FileOutputStream outFile=null;
+			if(outFilePath.length()>0) {
+				outFilePath=new File(outFilePath).getAbsolutePath();
+				outFile=new FileOutputStream(outFilePath);
+			}
+			args.outFilePath=outFilePath;
+			args.outFile=outFile;
+			
+			
+			String exp=ins[0];
+			if(exp.equals("*")) {
+				exp="";
+			} else {
+				if(exp.startsWith("*")) {
+					exp=exp.substring(1);
+				}else {
+					exp="\""+exp;
+				}
+				if(exp.endsWith("*")) {
+					exp=exp.substring(0, exp.length()-1);
+				}else {
+					exp=exp+"\"";
+				}
+			}
+			args.extPath_exp=exp;
+			
+			fn.Exec(args);
+			
+			if(outFile!=null)outFile.close();
+		}
+	}
+	
+	
+	
+	
+	
+	
+	
+	static public boolean StartHttpApiServer(int port) throws Exception {
+		String clazzName=Test.class.getPackage().getName()+".Test_HttpApiServer";
+		Method[] fns;
+		try {
+			fns=Class.forName(clazzName).getMethods();
+		}catch (Exception e) {
+			System.out.println("Test_HttpApiServer.java加载失败,不支持启动本地轻量HTTP API服务。");
+			return false;
+		}
+		Method fn=null; for(Method x : fns) if(x.getName().equals("Create")) fn=x;
+		return (boolean)fn.invoke(null, "0.0.0.0", port);
+	}
+	
+	
+	
+	
+	
+	
+	
+	static public String ReadIn() throws Exception {
+		ByteArrayOutputStream in=new ByteArrayOutputStream();
+		while(true) {
+			int byt=System.in.read();
+			if(byt=='\r') continue;
+			if(byt=='\n') {
+				break;
+			}
+			if(in.size()>=2048) {//防止内存溢出,某些环境下可能会有无限的输入
+				byte[] bytes=in.toByteArray();
+				in=new ByteArrayOutputStream();
+				in.write(bytes, bytes.length-1024, 1024);
+			}
+			in.write(byt);
+		}
+		return in.toString();
+	}
+	static public boolean InTxt_SetCurrent(String inTxt, boolean checkInit) {
+		int idx=-1; try{ idx=Integer.parseInt(inTxt.split("\\s+")[1]); }catch(Exception e) {}
+		if(idx<0 || idx>=AreaCityQuery.Instances.length) {
+			System.out.println("use实例序号无效,请重新输入!");
+			return false;
+		}
+		AreaCityQuery val=AreaCityQuery.Instances[idx];
+		if(checkInit && val.GetInitStatus()!=2) {
+			System.out.println("实例"+idx+"未初始化,可用实例["+GetInitInstanceIdxs()+"],请重新输入!");
+			return false;
+		}
+		CurrentIdx=idx;
+		Current=val;
+		return true;
+	}
+	static public String GetInitInstanceIdxs() {
+		String initIdxs="";
+		for(int i=0;i<AreaCityQuery.Instances.length;i++) {
+			AreaCityQuery item=AreaCityQuery.Instances[i];
+			if(item.GetInitStatus()==2) {
+				initIdxs+=(initIdxs.length()>0?",":"")+i;
+			}
+		}
+		return initIdxs;
+	}
+	static boolean IsCmd=false;
+	static String HR="-----------------------------------";
+	static void Start(String[] args) throws Exception {
+		if(args.length>0) {
+			System.out.print(args.length+"个启动参数");
+			for(int i=0;i<args.length;i++) {
+				if(args[i].equals("-cmd")) {
+					IsCmd=true;
+				}
+				System.out.print(",参数"+(i+1)+":"+args[i]);
+			}
+			System.out.println(IsCmd?",已进入命令行模式。":"");
+			System.out.println();
+		}
+		
+		while(true) {
+			boolean isInit=Current.GetInitStatus()==2;
+			ArrayList<Integer> idxs=new ArrayList<>();
+			int curIdx=0; String initIdxs="";
+			for(int i=0;i<AreaCityQuery.Instances.length;i++) {
+				AreaCityQuery item=AreaCityQuery.Instances[i];
+				if(item.GetInitStatus()==2) {
+					idxs.add(i);
+					initIdxs+=(initIdxs.length()>0?",":"")+i;
+				}
+				if(item==Current)curIdx=i;
+			}
+			
+			
+			System.out.println("【功能菜单】 当前静态实例Instances["+curIdx+"]");
+			System.out.println("1. "+(isInit?"重新":"")+"初始化:调用 Init_StoreInWkbsFile   -内存占用很低(性能受IO限制)"+(Current.IsStoreInWkbsFile()?"               [已初始化]":""));
+			System.out.println("2. "+(isInit?"重新":"")+"初始化:调用 Init_StoreInMemory     -内存占用和json文件差不多大(性能豪放)"+(Current.IsStoreInMemory()?"     [已初始化]":""));
+			if(isInit) {
+				System.out.println(HR);
+				System.out.println("3. 测试:基础功能测试");
+				System.out.println("4. 测试:1万个伪随机点测试");
+				System.out.println("5. 测试:多线程性能测试");
+				System.out.println(HR);
+				System.out.println("6. 查询: QueryPoint 查找坐标点所在省市区乡镇");
+				System.out.println("A. 查询: QueryGeometry 查找和图形相交的省市区乡镇");
+				System.out.println("7. 查询: ReadWKT 读取省市区乡镇边界的WKT文本数据");
+				System.out.println("8. 查询: Debug 读取边界网格划分图形WKT文本数据");
+				System.out.println(HR);
+				System.out.println("9. HTTP: 启动本地轻量HTTP API服务");
+			}
+			System.out.println(HR);
+			System.out.println("*. 输入 use 0-"+(AreaCityQuery.Instances.length-1)+" 切换静态实例,list 列出实例信息,当前"+(initIdxs.length()==0?"无已初始化实例":"["+initIdxs+"]已初始化")+"");
+			System.out.println("*. 输入 exit 退出");
+			System.out.println();
+			System.out.println("请输入菜单序号:");
+			System.out.print("> ");
+			
+			boolean waitAnyKey=true;
+			String inTxt="";
+			while(true) {
+				int byt=System.in.read();
+				inTxt+=(char)byt;
+				
+				if(byt!='\n') {
+					continue;
+				}
+				inTxt=inTxt.trim().toUpperCase();
+				try {
+					if(inTxt.equals("1") || inTxt.equals("2")) {
+						Current.ResetInitStatus();
+						System.gc();
+						
+						Init_FromMenu(inTxt.equals("1"));
+						if(Current.GetInitStatus()==2) {
+							waitAnyKey=false;
+						}
+					} else if(isInit && inTxt.equals("3")) {
+						for(int i : idxs) {
+							if(idxs.size()>1) System.out.println("【测试实例"+i+"】 Instances["+i+"]");
+							BaseTest(i);
+						}
+					} else if(isInit && inTxt.equals("4")) {
+						for(int i : idxs) {
+							if(idxs.size()>1) System.out.println("【测试实例"+i+"】 Instances["+i+"]");
+							LargeRndPointTest(i);
+						}
+					} else if(isInit && inTxt.equals("5")) {
+						ThreadRun(idxs);
+						waitAnyKey=false;
+					} else if(isInit && inTxt.equals("6")) {
+						Query_Point();
+						waitAnyKey=false;
+					} else if(isInit && inTxt.equals("A")) {
+						Query_Geometry();
+						waitAnyKey=false;
+					} else if(isInit && inTxt.equals("7")) {
+						Read_WKT();
+						waitAnyKey=false;
+					} else if(isInit && inTxt.equals("8")) {
+						Query_DebugReadWKT();
+						waitAnyKey=false;
+					} else if(isInit && inTxt.equals("9")) {
+						if(StartHttpApiServer(HttpApiServerPort)) {
+							waitAnyKey=false;
+						}
+					} else if(inTxt.startsWith("USE ")) {
+						if(InTxt_SetCurrent(inTxt, false)) {
+							waitAnyKey=false;
+						}else {
+							inTxt="";
+							System.out.print("> ");
+							continue;
+						}
+					} else if(inTxt.equals("LIST")) {
+						for(int i : idxs) {
+							AreaCityQuery item=AreaCityQuery.Instances[i];
+							QueryInitInfo info=item.GetInitInfo();
+							System.out.println((item==Current?"[当前]":"")
+									+"实例"+i+": Instances["+i+"] "+(item.IsStoreInMemory()?"Init_StoreInMemory":"Init_StoreInWkbsFile"));
+							System.out.println("    Geometry "+info.GeometryCount+" 个(Grid切分Polygon "+info.PolygonCount+" 个)");
+							System.out.println("    Data文件: "+info.FilePath_Data);
+							System.out.println("    Wkbs文件: "+info.FilePath_SaveWkbs);
+						}
+						if(idxs.size()==0) {
+							System.out.println("没有已初始化的实例信息!");
+						}
+					} else if(inTxt.equals("EXIT")) {
+						System.out.println("bye!");
+						return;
+					} else {
+						inTxt="";
+						System.out.println("序号无效,请重新输入菜单序号!");
+						System.out.print("> ");
+						continue;
+					}
+				} catch(Exception e) {
+					e.printStackTrace();
+				}
+				break;
+			}
+			
+			if(waitAnyKey) {
+				System.out.println("按任意键继续...");
+				int n=System.in.read();
+				if(n=='\r') {
+					System.in.read();
+				}
+			}
+		}
+	}
+}

+ 467 - 0
AreaCity-Query-Geometry/Test_HttpApiServer.java

@@ -0,0 +1,467 @@
+package com.github.xiangyuecn.areacity.query;
+
+//要是编译不过,就直接删掉这个文件就好了
+//要是编译不过,就直接删掉这个文件就好了
+//要是编译不过,就直接删掉这个文件就好了
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.io.WKTReader;
+
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.Func;
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryInitInfo;
+import com.github.xiangyuecn.areacity.query.AreaCityQuery.QueryResult;
+//jre rt.jar com.sun,Eclips不允许引用:Access restriction: The type 'HttpServer' is not API
+//Eclips修改项目配置 Java Compiler -> Errors/Warnings -> Deprecated and restricted API,将Error的改成Warning即可
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+
+/**
+ * AreaCityQuery测试本地轻量HTTP API服务
+ * 
+ * GitHub: https://github.com/xiangyuecn/AreaCity-Query-Geometry (github可换成gitee)
+ * 省市区县乡镇区划边界数据: https://github.com/xiangyuecn/AreaCity-JsSpider-StatsGov (github可换成gitee)
+ */
+public class Test_HttpApiServer {
+	/** 是否允许输出大量WKT数据,默认不允许,只能输出最大20M的数据;如果要设为true,请确保没有 -Xmx300m 限制Java使用小内存 **/
+	static public boolean AllowResponseBigWKT=false;
+	
+	static private String Desc;
+	static public boolean Create(String bindIP, int bindPort) {
+		Desc="========== 本地轻量HTTP API服务 ==========";
+		Desc+="\n可通过 http://127.0.0.1:"+bindPort+"/ 访问本服务、文档、实例状态,提供的接口:";
+		Desc+="\n\n  - GET /queryPoint?lng=&lat=&tolerance=&returnWKTKey=      查询出包含此坐标点的所有边界图形的属性数据;lng必填经度,lat必填纬度,returnWKTKey可选要额外返回边界的wkt文本数据放到此key下。tolerance可选,距离范围容差值,单位米,比如2500相当于一个以此坐标为中心点、半径为2.5km的圆形范围,-1不限制距离;当坐标位于界线外侧(如海岸线、境界线)时QueryPoint方法将不会有边界图形能够匹配包含此坐标(就算距离只相差1cm),传了此参数后,会查询出在这个范围内和此坐标点距离最近的边界数据,并且结果属性中会额外添加PointDistance(图形与坐标的距离,单位米)、PointDistanceID(图形唯一标识符)两个值。";
+		Desc+="\n\n  - GET /queryGeometry?wkt=&returnWKTKey=        查询出和此图形(点、线、面)有交点的所有边界图形的属性数据(包括边界相交);wkt必填任意图形,returnWKTKey可选要额外返回边界的wkt文本数据放到此key下。";
+		Desc+="\n\n  - GET /readWKT?id=&pid=&deep=&extPath=&returnWKTKey= 读取边界图形的WKT文本数据;前四个参数可以组合查询或查一个参数(边界的属性中必须要有这些字段才能查询出来),id:查询指定id|unique_id的边界;pid:查询此pid下的所有边界;deep:限制只返回特定层级的数据,取值:0省1市2区想3乡镇;extPath:查询和ext_path完全相同值的边界,首尾支持*通配符(如:*武汉*);returnWKTKey回边界的wkt文本数据放到此key下,默认值polygon_wkt,填0不返回wkt文本数据;注意:默认只允许输出最大20M的WKT数据,请参考下面的注意事项。";
+		Desc+="\n\n  - GET /debugReadGeometryGridSplitsWKT?id=&pid=&deep=&extPath=&returnWKTKey= Debug读取边界网格划分图形WKT文本数据;参数和/readWKT接口一致。";
+		Desc+="\n\n  - JSON响应:{c:0, v:{...}, m:\"错误消息\"} c=0代表接口调用成功,v为内容;c=其他值调用错误,m为错误消息。";
+		Desc+="\n\n  - 指定查询实例:接口前面加/0-"+(AreaCityQuery.Instances.length-1)+"/,或使用instance=0-"+(AreaCityQuery.Instances.length-1)+"参数来指定需要调用的静态实例,默认为AreaCityQuery.Instances[0]实例;允许同时使用多个数据文件来分别初始化多个实例,然后查询时指定需要调用哪个实例。";
+		Desc+="\n\n  - 注意:所有输入坐标参数的坐标系必须和初始化时使用的geojson数据的坐标系一致,否则坐标可能会有比较大的偏移,导致查询结果不正确。";
+		Desc+="\n\n  - 注意:如果要输出大量WKT数据,请调大Java内存,不然可能是 -Xmx300m 启动的只允许使用小内存,并且修改服务源码内的AllowResponseBigWKT=true,否则只允许输出最大20M的WKT数据。";
+		
+		System.out.println(Desc);
+		System.out.println();
+		System.out.println("绑定IP: "+bindIP+", Port: "+bindPort+", 正在启动HTTP API服务...");
+		
+		boolean startOK=false;
+		try {
+			__Start(bindIP, bindPort);
+			
+			startOK=true;
+			System.out.println("HTTP API服务正在运行,输入 exit 退出服务...");
+			while(true){
+				String inStr=Test.ReadIn().trim();
+				if(inStr.equals("exit")) {
+					System.out.println("bye! 已退出HTTP API服务。");
+					System.out.println();
+					
+					httpServer.stop(0);
+					httpServer=null;
+					break;
+				}
+				System.out.println("如需退出HTTP API服务请输入exit");
+			}
+		}catch (Exception e) {
+			e.printStackTrace();
+			if(!startOK) {
+				System.out.println("创建HTTP服务异常:"+e.getMessage());
+				System.out.println();
+				return false;
+			}
+		}
+		return true;
+	}
+	
+	
+	
+	static private void Req_queryPoint(HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
+		double lng=ToNum(query.get("lng"), 999);
+		double lat=ToNum(query.get("lat"), 999);
+		int tolerance=(int)ToLong(query.get("tolerance"), 0);
+		String returnWKTKey=query.get("returnWKTKey");
+		if(lng<-180 || lat<-90 || lng>180 || lat>90) {
+			responseErr[0]="坐标参数值无效";
+			return;
+		}
+		AreaCityQuery instance=GetInstance(query, responseErr);
+		if(instance==null) return;
+		
+		QueryResult res=new QueryResult();
+		if(returnWKTKey!=null && returnWKTKey.length()>0) {
+			res.Set_ReturnWKTKey=returnWKTKey;
+		}
+		if(tolerance==0) {
+			instance.QueryPoint(lng, lat, null, res);
+		}else {
+			instance.QueryPointWithTolerance(lng, lat, null, res, tolerance);
+		}
+		
+		response[0]=ResToJSON(res);
+	}
+	
+	static private void Req_queryGeometry(HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
+		String wkt=query.get("wkt");
+		String returnWKTKey=query.get("returnWKTKey");
+		if(wkt==null || wkt.length()==0) {
+			responseErr[0]="wkt参数无效";
+			return;
+		}
+		Geometry geom;
+		try {
+			geom=new WKTReader(AreaCityQuery.Factory).read(wkt);
+		}catch (Exception e) {
+			responseErr[0]="wkt参数解析失败:"+e.getMessage();
+			return;
+		}
+		AreaCityQuery instance=GetInstance(query, responseErr);
+		if(instance==null) return;
+		
+		QueryResult res=new QueryResult();
+		if(returnWKTKey!=null && returnWKTKey.length()>0) {
+			res.Set_ReturnWKTKey=returnWKTKey;
+		}
+		instance.QueryGeometry(geom, null, res);
+		
+		response[0]=ResToJSON(res);
+	}
+	
+	static private void Req_readWKT(boolean debugReadGrid, HashMap<String, String> query, String[] response, String[] responseErr, int[] status, String[] contentType, HashMap<String, String> respHeader) throws Exception {
+		long id=ToLong(query.get("id"), -1);
+		long pid=ToLong(query.get("pid"), -1);
+		long deep=ToLong(query.get("deep"), -1);
+		String extPath=query.get("extPath"); if(extPath==null) extPath="";
+		String returnWKTKey=query.get("returnWKTKey");
+		if(id==-1 && pid==-1 && deep==-1 && extPath.length()==0) {
+			responseErr[0]="请求参数无效";
+			return;
+		}
+		if(returnWKTKey==null || returnWKTKey.length()==0) {
+			returnWKTKey="polygon_wkt";
+		}
+		if("0".equals(returnWKTKey)) {
+			returnWKTKey=null;
+		}
+		AreaCityQuery instance=GetInstance(query, responseErr);
+		if(instance==null) return;
+		
+		String exp=extPath;
+		if(extPath!=null && extPath.length()>0) {
+			if(exp.equals("*")) {
+				exp="";
+			} else {
+				if(exp.startsWith("*")) {
+					exp=exp.substring(1);
+				}else {
+					exp="\""+exp;
+				}
+				if(exp.endsWith("*")) {
+					exp=exp.substring(0, exp.length()-1);
+				}else {
+					exp=exp+"\"";
+				}
+			}
+		}
+		String exp_=exp;
+		String extPath_=extPath;
+		
+		int[] readCount=new int[] {0};
+		boolean[] isWktSizeErr=new boolean[] {false};
+		int[] wktSize=new int[] {0};
+		Func<String, Boolean> where=new Func<String, Boolean>() {
+			@Override
+			public Boolean Exec(String prop) throws Exception {
+				String prop2=(","+prop.substring(1, prop.length()-1)+",").replace("\"", "").replace(" ", ""); //不解析json,简单处理
+				if(id!=-1) {
+					if(!prop2.contains(",id:"+id+",") && !prop2.contains(",unique_id:"+id+",")) {
+						return false;
+					}
+				}
+				if(pid!=-1) {
+					if(!prop2.contains(",pid:"+pid+",")) {
+						return false;
+					}
+				}
+				if(deep!=-1) {
+					if(!prop2.contains(",deep:"+deep+",")) {
+						return false;
+					}
+				}
+				if(extPath_.length()>0) {
+					int i0=prop.indexOf("ext_path");
+					if(i0==-1)return false;
+					int i1=prop.indexOf(",", i0);
+					if(i1==-1)i1=prop.length();
+					if(!prop.substring(i0+9, i1).contains(exp_)) {
+						return false;
+					}
+				}
+				
+				readCount[0]++;
+				if(isWktSizeErr[0]) {
+					return false;
+				}
+				return true;
+			}
+		};
+		Func<String[], Boolean> onFind=new Func<String[], Boolean>() {
+			@Override
+			public Boolean Exec(String[] val) throws Exception {
+				wktSize[0]+=val[1].length();
+				if(!AllowResponseBigWKT && wktSize[0]>20*1024*1024) {
+					isWktSizeErr[0]=true;
+					return false;
+				}
+				return true;
+			}
+		};
+		QueryResult res;
+		if(debugReadGrid) {
+			res=instance.Debug_ReadGeometryGridSplitsWKT(returnWKTKey, null, where, onFind);
+		} else {
+			res=instance.ReadWKT_FromWkbsFile(returnWKTKey, null, where, onFind);
+		}
+		
+		if(isWktSizeErr[0]) {
+			responseErr[0]="已匹配到"+readCount[0]+"条数据,但WKT数据量超过20M限制,可修改服务源码内的AllowResponseBigWKT=true来解除限制";
+			return;
+		}
+		response[0]=ResToJSON(res);
+	}
+	
+	
+	
+	static private AreaCityQuery GetInstance(HashMap<String, String> query, String[] responseErr) {
+		int idx=(int)ToLong(query.get("instance"), 0);
+		if(idx<0 || idx>=AreaCityQuery.Instances.length) {
+			responseErr[0]="AreaCityQuery实例值"+idx+"无效";
+			return null;
+		}
+		AreaCityQuery val=AreaCityQuery.Instances[idx];
+		try {
+			val.CheckInitIsOK();
+		}catch(Exception e) {
+			responseErr[0]="AreaCityQuery实例"+idx+"未初始化完成:"+e.getMessage();
+			return null;
+		}
+		return val;
+	}
+	
+	static private String ResToJSON(QueryResult res) {
+		StringBuilder json=new StringBuilder();
+		json.append("{\"list\":[");//手撸json
+		for(int i=0,L=res.Result.size();i<L;i++) {
+			if(i>0) json.append(","); 
+			json.append(res.Result.get(i));
+			res.Result.set(i, null);//已读取了结果就释放掉内存
+		}
+		json.append("]}");
+		return json.toString();
+	}
+	static private String StringInnerJson(String str) {
+		if (str==null || str.length()==0) {
+			return "";
+		}
+		int len = str.length();
+		StringBuilder sb = new StringBuilder(len * 2);
+		char chr;
+		for (int i = 0; i < len; i++) {
+			chr = str.charAt(i);
+			switch (chr) {
+				case '"':
+					sb.append('\\').append('"'); break;
+				case '\\':
+					sb.append('\\').append('\\'); break;
+				case '\n':
+					sb.append('\\').append('n'); break;
+				case '\r':
+					sb.append('\\').append('r'); break;
+				default:
+					sb.append(chr);
+					break;
+			}
+		}
+		return sb.toString();
+	}
+	static private long ToLong(String val, long def) {
+		if(val==null || val.length()==0) {
+			return def;
+		}
+		try {
+			return Long.parseLong(val);
+		}catch (Exception e) {
+			return def;
+		}
+	}
+	static private double ToNum(String val, double def) {
+		if(val==null || val.length()==0) {
+			return def;
+		}
+		try {
+			return Double.parseDouble(val);
+		}catch (Exception e) {
+			return def;
+		}
+	}
+	
+	
+	
+	
+	
+	static private HttpServer httpServer;
+	static private Pattern Exp_PathInstance=Pattern.compile("^/(\\d+)(/.+)");
+	static private void __Start(String bindIP, int bindPort) throws Exception {
+		if(httpServer!=null) {
+			try {
+				httpServer.stop(0);
+			}catch(Exception e) {}
+		}
+		Func<HttpExchange, Object> fn=new Func<HttpExchange, Object>() {
+			@Override
+			public Object Exec(HttpExchange context) throws Exception {
+				URI url=context.getRequestURI();
+				String path=url.getPath(); if(path==null||path.length()==0)path="/";
+				String queryStr=url.getQuery(); if(queryStr==null) queryStr="";
+				String method=context.getRequestMethod(); if(method==null)method="";
+				method=method.toUpperCase();
+				
+				HashMap<String, String> query=new HashMap<>();
+				String apiPath=path;
+				Matcher m=Exp_PathInstance.matcher(apiPath);
+				if(m.find()) {
+					apiPath=m.group(2);
+					query.put("instance", m.group(1));
+				}
+				
+				String[] queryArr=queryStr.split("&");
+				for(String s : queryArr) {
+					if(s.length()>0) {
+						String[] kv=s.split("=");
+						if(kv.length==2) {
+							query.put(kv[0], URLDecoder.decode(kv[1], "utf-8"));
+						}
+					}
+				}
+				
+				int[] status=new int[] { 200 };
+				String[] contentType=new String[] {"text/json; charset=utf-8"};
+				HashMap<String, String> respHeader=new HashMap<>();
+				respHeader.put("Access-Control-Allow-Origin", "*");
+				
+				
+				boolean isApi=true, isHtml=false;
+				String[] response=new String[] { "" };
+				String[] responseErr=new String[] { "" };
+				try {
+					if(!method.equals("POST") && !method.equals("GET")) {
+						isApi=false; isHtml=true;
+						response[0]="Method: "+method;
+					} else if(apiPath.equals("/queryPoint")){
+						Req_queryPoint(query, response, responseErr, status, contentType, respHeader);
+					} else if (apiPath.equals("/queryGeometry")) {
+						Req_queryGeometry(query, response, responseErr, status, contentType, respHeader);
+					} else if (apiPath.equals("/readWKT")) {
+						Req_readWKT(false, query, response, responseErr, status, contentType, respHeader);
+					} else if (apiPath.equals("/debugReadGeometryGridSplitsWKT")) {
+						Req_readWKT(true, query, response, responseErr, status, contentType, respHeader);
+					} else if (path.equals("/")) {
+						isApi=false; isHtml=true;
+						String html="\n\n\n\n【请求IP】\n"+context.getRemoteAddress().getAddress().getHostAddress();
+						if(context.getRemoteAddress().getAddress().isLoopbackAddress()) {
+							html+="\n\n【静态实例列表】仅服务器本地访问可见";
+							for(int i=0;i<AreaCityQuery.Instances.length;i++) {
+								AreaCityQuery item=AreaCityQuery.Instances[i];
+								if(item.GetInitStatus()==2) {
+									QueryInitInfo info=item.GetInitInfo();
+									html+="\n实例"+i+": Instances["+i+"] "+(item.IsStoreInMemory()?"Init_StoreInMemory":"Init_StoreInWkbsFile");
+									html+="\n    Geometry "+info.GeometryCount+" 个(Grid切分Polygon "+info.PolygonCount+" 个)";
+									html+="\n    Data文件: "+info.FilePath_Data;
+									html+="\n    Wkbs文件: "+info.FilePath_SaveWkbs;
+								}
+							}
+						}
+						html+="\n\n==========";
+						response[0]="<h1>AreaCityQuery HttpApiServer Running!</h1>"
+								+"\n<pre style='word-break:break-all;white-space:pre-wrap'>\n"
+								+Desc+html+"\n</pre>";
+					} else {
+						isApi=false; isHtml=true;
+						status[0]=404;
+						response[0]="<h1>请求路径 "+path+" 不存在!</h1>";
+					}
+				} catch (Throwable e) {
+					e.printStackTrace();
+					if(e instanceof OutOfMemoryError) {
+						System.gc();
+					}
+					responseErr[0]="接口调用异常:"+e.getMessage();
+				}
+				
+				
+				if(isApi) {
+					if(responseErr[0].length()>0) {//手撸json
+						response[0]="{\"c\":1,\"v\":null,\"m\":\""+StringInnerJson(responseErr[0])+"\"}";
+					} else {
+						response[0]="{\"c\":0,\"v\":"+response[0]+",\"m\":\"\"}";
+					}
+				}
+				if(isHtml) {
+					contentType[0]="text/html; charset=utf-8";
+				}
+				
+				respHeader.put("Content-Type", contentType[0]);
+				Headers header=context.getResponseHeaders();
+				for(Entry<String, String> kv : respHeader.entrySet()) {
+					header.set(kv.getKey(), kv.getValue());
+				}
+				
+				byte[] sendData=response[0].getBytes("utf-8");
+				context.sendResponseHeaders(status[0], sendData.length);
+				context.getResponseBody().write(sendData);
+				
+				
+				StringBuilder log=new StringBuilder();
+				log.append("["+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())+"]");
+				log.append(status[0]);
+				log.append(" "+method);
+				log.append(" "+path);
+				if(queryStr.length()>0) {
+					log.append("?"+queryStr);
+				}
+				log.append(" "+sendData.length);
+				System.out.println(log);
+				
+				return null;
+			}
+		};
+		
+		// https://www.apiref.com/java11-zh/jdk.httpserver/com/sun/net/httpserver/HttpServer.html
+		httpServer = HttpServer.create(new InetSocketAddress(bindIP, bindPort), 0);
+		httpServer.createContext("/", new HttpHandler() {
+			@Override
+			public void handle(HttpExchange context) throws IOException {
+				try {
+					fn.Exec(context);
+				} catch (Throwable e) {
+					e.printStackTrace();
+					if(e instanceof OutOfMemoryError) {
+						System.gc();
+					}
+				}
+				context.close();
+			}
+		});
+		httpServer.start();
+	}
+}

BIN
AreaCity-Query-Geometry/images/use-console.png


BIN
AreaCity-Query-Geometry/images/use-debug-grid-split.png


BIN
AreaCity-Query-Geometry/images/use-http-api.png


BIN
AreaCity-Query-Geometry/images/use-map-echarts.png


BIN
AreaCity-Query-Geometry/images/use-map-echarts2.png


BIN
AreaCity-Query-Geometry/jts-core-1.18.2.jar


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 5 - 0
AreaCity-Query-Geometry/ok_geo.csv-║╧▓в-20240607-211557.json


+ 120 - 0
AreaCity-Query-Geometry/scripts/╔·│╔jar░№.bat

@@ -0,0 +1,120 @@
+@echo off
+::在Windows系统中双击运行这个文件,自动完成java文件编译和打包成jar
+
+for %%i in (%cd%) do set dir=%%~ni
+if not "%dir%"=="scripts" (
+	echo 请到scripts目录中运行本脚本。
+	goto Pause
+)
+
+:Run
+cls
+cd ../
+setlocal enabledelayedexpansion
+
+::修改这里指定需要使用的JDK(\结尾bin目录完整路径),否则将使用已安装的默认JDK
+set jdkBinDir=
+::set jdkBinDir="C:\Program Files\Java\jdk-1.8\bin\"
+
+if "%jdkBinDir%"=="" (
+	echo 正在读取JDK版本(如需指定JDK为特定版本或目录,请修改本bat文件内jdkBinDir为JDK bin目录):
+) else (
+	echo 正在读取JDK(%jdkBinDir%)版本:
+)
+
+
+%jdkBinDir%javac -version
+if errorlevel 1 (
+	echo 需要安装JDK才能编译java文件
+	goto Pause
+)
+
+:JarN
+	echo.
+	echo 请选择需要的生成操作:
+	echo   1. 仅生成依赖jar文件(放到其他项目中Java代码调用,不含Test.java)
+	echo   2. 生成可运行jar文件(包含Test.java控制台程序)
+	echo   3. 退出
+	set step=
+	set /p step=请输入序号:
+	echo.
+	if "%step%"=="1" goto Jar1
+	if "%step%"=="2" goto Jar2
+	if "%step%"=="3" goto Pause
+	echo 序号无效!请重新输入
+	goto JarN
+
+:Clazz
+	echo 编译中...
+	%jdkBinDir%javac -encoding utf-8 -cp "./*" %Clazz_Files%
+	if errorlevel 1 (
+		echo java文件编译失败
+		goto JarN
+	)
+
+	set dir=target\classes\com\github\xiangyuecn\areacity\query
+	if exist target\classes rd /S /Q target\classes > nul
+	md %dir%
+	move *.class %dir% > nul
+
+	echo 编译完成,正在生成jar...
+	goto %Clazz_End%
+
+:Jar1
+	set Clazz_Files=AreaCityQuery.java
+	set Clazz_End=Jar1_1
+	goto Clazz
+	:Jar1_1
+	
+	set dir=target\jarLib\
+	if not exist %dir% md %dir%
+	set jarPath=%dir%areacity-query-geometry.lib.jar
+	
+	%jdkBinDir%jar cf %jarPath% -C target/classes/ com
+	if errorlevel 1 (
+		echo 生成jar失败
+	) else (
+		copy jts-core-*.jar %dir% > nul
+		echo 已生成jar,文件在源码根目录:%jarPath%,请copy这个jar + jts-core-xxx.jar 到你的项目中使用。
+	)
+	echo.
+	pause
+	goto JarN
+
+:Jar2
+	set Clazz_Files=*.java
+	set Clazz_End=Jar2_1
+	goto Clazz
+	:Jar2_1
+	
+	set dir=target\jarConsole\
+	set dir_libs=%dir%libs\
+	if not exist %dir% md %dir%
+	if not exist %dir_libs% md %dir_libs%
+	set jarPath=%dir%areacity-query-geometry.console.jar
+	
+	copy *.jar %dir_libs% > nul
+	set jarArr=
+	for /f %%a in ('dir /b "%dir_libs%"') do (set jarArr=!jarArr! libs/%%a)
+	echo Class-Path:%jarArr%
+	
+	set MANIFEST=target\classes\MANIFEST.MF
+	echo Manifest-Version: 1.0>%MANIFEST%
+	echo Class-Path:%jarArr%>>%MANIFEST%
+	echo Main-Class: com.github.xiangyuecn.areacity.query.Test>>%MANIFEST%
+	
+	%jdkBinDir%jar cfm %jarPath% target/classes/MANIFEST.MF -C target/classes/ com
+	if errorlevel 1 (
+		echo 已生成jar失败
+	) else (
+		echo 已生成jar,文件在源码根目录:%jarPath%,libs内已包含依赖的其他jar文件,使用时请全部复制。
+		echo 请到这个文件夹里面后,执行命令运行这个jar:
+		echo       java -jar areacity-query-geometry.console.jar
+	)
+	echo.
+	pause
+	goto JarN
+
+:Pause
+pause
+:End

+ 97 - 0
AreaCity-Query-Geometry/scripts/╔·│╔jar░№.sh

@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+#在Linux、macOS系统终端中运行这个文件,自动完成java文件编译和打包成jar
+
+dir=`pwd`; dir=`basename $dir`;
+if [ "$dir" != "scripts" ]; then echo "请到scripts目录中运行本脚本。"; exit; fi
+
+clear
+cd ../
+
+#修改这里指定需要使用的JDK(/结尾bin目录完整路径),否则将使用已安装的默认JDK
+jdkBinDir=""
+#jdkBinDir="/home/download/jdk-19.0.1/bin/"
+
+if [ "$jdkBinDir" == "" ]; then
+	echo "正在读取JDK版本(如需指定JDK为特定版本或目录,请修改本sh文件内jdkBinDir为JDK bin目录):"
+else
+	echo "正在读取JDK(${jdkBinDir})版本:"
+fi
+function err(){ echo -e "\e[31m$1\e[0m"; }
+
+${jdkBinDir}javac -version
+[ ! $? -eq 0 ] && { err "需要安装JDK才能编译java文件"; exit; }
+
+
+function JarN(){
+	echo ""
+	echo "请选择需要的生成操作:"
+	echo "  1. 仅生成依赖jar文件(放到其他项目中Java代码调用,不含Test.java)"
+	echo "  2. 生成可运行jar文件(包含Test.java控制台程序)"
+	echo "  3. 退出"
+	read -p "请输入序号:" step
+	echo ""
+	if [ "$step" == 1 ]; then Jar1;
+	elif [ "$step" == 2 ]; then Jar2;
+	elif [ "$step" == 3 ]; then exit;
+	else echo "序号无效!请重新输入"; fi
+	
+	read -s -n1 -p "按任意键继续...";
+	echo ""
+	JarN;
+}
+
+function Clazz(){
+	echo 编译中...
+	${jdkBinDir}javac -encoding utf-8 -cp "./*" $1
+	[ ! $? -eq 0 ] && { err "java文件编译失败"; return 1; }
+
+	dir="target/classes/com/github/xiangyuecn/areacity/query"
+	if [ -e $dir ]; then rm -r target/classes > /dev/null 2>&1; fi
+	mkdir -p $dir
+	mv *.class $dir
+	
+	echo 编译完成,正在生成jar...
+}
+
+function Jar1(){
+	Clazz AreaCityQuery.java
+	[ ! $? -eq 0 ] && { return 1; }
+	
+	dir="target/jarLib/"
+	if [ ! -e $dir ]; then mkdir -p $dir; fi
+	jarPath="${dir}areacity-query-geometry.lib.jar"
+	
+	${jdkBinDir}jar cf $jarPath -C target/classes/ com
+	[ ! $? -eq 0 ] && { err "生成jar失败"; return 1; }
+	cp jts-core-*.jar $dir
+	echo "已生成jar,文件在源码根目录:${jarPath},请copy这个jar + jts-core-xxx.jar 到你的项目中使用。"
+}
+
+function Jar2(){
+	Clazz "*.java"
+	[ ! $? -eq 0 ] && { return 1; }
+	
+	dir=target/jarConsole/
+	dir_libs=${dir}libs/
+	[ ! -e $dir ] && { mkdir -p $dir; }
+	[ ! -e $dir_libs ] && { mkdir -p $dir_libs; }
+	jarPath=${dir}areacity-query-geometry.console.jar
+	
+	cp *.jar $dir_libs
+	jarArr=""
+	for a in `ls $dir_libs`; do jarArr="${jarArr} libs/${a}"; done
+	echo Class-Path: $jarArr
+	
+	MANIFEST=target/classes/MANIFEST.MF
+	echo Manifest-Version: 1.0>$MANIFEST
+	echo Class-Path:${jarArr}>>$MANIFEST
+	echo Main-Class: com.github.xiangyuecn.areacity.query.Test>>$MANIFEST
+	
+	${jdkBinDir}jar cfm $jarPath target/classes/MANIFEST.MF -C target/classes/ com
+	[ ! $? -eq 0 ] && { err "生成jar失败"; return 1; }
+	echo "已生成jar,文件在源码根目录:${jarPath},libs内已包含依赖的其他jar文件,使用时请全部复制。"
+	echo "请到这个文件夹里面后,执行命令运行这个jar:"
+	echo "      java -jar areacity-query-geometry.console.jar"
+}
+
+JarN;

+ 32 - 0
AreaCity-Query-Geometry/start.sh

@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+#在Linux、macOS系统终端中运行这个文件,自动完成java文件编译和运行
+
+clear
+
+#修改这里指定需要使用的JDK(/结尾bin目录完整路径),否则将使用已安装的默认JDK
+jdkBinDir=""
+#jdkBinDir="/home/download/jdk-19.0.1/bin/"
+
+if [ "$jdkBinDir" == "" ]; then
+	echo "正在读取JDK版本(如需指定JDK为特定版本或目录,请修改本sh文件内jdkBinDir为JDK bin目录):"
+else
+	echo "正在读取JDK(${jdkBinDir})版本:"
+fi
+function err(){ echo -e "\e[31m$1\e[0m"; }
+
+${jdkBinDir}javac -version
+[ ! $? -eq 0 ] && { err "需要安装JDK才能编译运行java文件"; exit; }
+
+${jdkBinDir}javac -encoding utf-8 -cp "./*" *.java
+[ ! $? -eq 0 ] && { err "java文件编译失败"; exit; }
+
+dir="com/github/xiangyuecn/areacity/query"
+if [ ! -e $dir ]; then
+	mkdir -p $dir
+else
+	rm ${dir}/*.class > /dev/null 2>&1
+fi
+mv *.class ${dir}
+
+echo "java -Xmx300m Test -cmd 已限制java最大允许使用300M内存"
+${jdkBinDir}java -cp "./:./*" -Xmx300m com.github.xiangyuecn.areacity.query.Test -cmd

+ 43 - 0
AreaCity-Query-Geometry/▒р╥ы║═╘╦╨╨Test.java╓▒╜╙▓т╩╘.bat

@@ -0,0 +1,43 @@
+@echo off
+::在Windows系统中双击运行这个文件,自动完成java文件编译和运行
+
+:Run
+cls
+
+::修改这里指定需要使用的JDK(\结尾bin目录完整路径),否则将使用已安装的默认JDK
+set jdkBinDir=
+::set jdkBinDir=D:\xxxx\jdk-18_windows-x64_bin\jdk-18.0.2.1\bin\
+
+if "%jdkBinDir%"=="" (
+	echo 正在读取JDK版本(如需指定JDK为特定版本或目录,请修改本bat文件内jdkBinDir为JDK bin目录):
+) else (
+	echo 正在读取JDK(%jdkBinDir%)版本:
+)
+
+
+%jdkBinDir%javac -version
+if errorlevel 1 (
+	echo 需要安装JDK才能编译运行java文件
+	goto Pause
+)
+
+%jdkBinDir%javac -encoding utf-8 -cp "./*" *.java
+if errorlevel 1 (
+	echo java文件编译失败
+	goto Pause
+)
+
+set dir=com\github\xiangyuecn\areacity\query
+if not exist %dir% (
+	md %dir%
+) else (
+	del %dir%\*.class > nul
+)
+move *.class %dir% > nul
+
+echo java -Xmx300m Test -cmd 已限制java最大允许使用300M内存
+%jdkBinDir%java -cp "./;./*" -Xmx300m com.github.xiangyuecn.areacity.query.Test -cmd
+
+:Pause
+pause
+:End

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác