-
Notifications
You must be signed in to change notification settings - Fork 29
/
Copy pathAreaCityQuery.java
1701 lines (1491 loc) · 67.4 KB
/
AreaCityQuery.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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) {