GeoTools 实战(四):空间查询与 CQL 过滤全解析(查询篇)

📅 2026/7/9 3:14:49
GeoTools 实战(四):空间查询与 CQL 过滤全解析(查询篇)
目录一、前言为什么需要空间查询二、核心概念速览2.1 CQL vs ECQL2.2 FilterFactory22.3 查询是懒加载的三、Maven 依赖四、完整可运行代码4.1 完整代码五、代码逐段讲解5.1 属性查询ECQL.toFilter5.2 BBOX 查询地图框选5.3 INTERSECTS 查询空间关系5.4 DWITHIN 查询距离查询5.5 组合查询AND / OR六、运行结果示例七、常见坑位与注意事项7.1 DWITHIN 的单位陷阱7.2 BBOX 与 INTERSECTS 的区别7.3 属性名大小写敏感7.4 ECQL 字符串中的引号转义7.5 查询结果为空但明明有数据八、工程实践建议九、小结一、前言为什么需要空间查询在前面的文章中我们已经掌握了Shapefile 的读取与写入空间数据格式互转Shp / GeoJSON / WKT坐标系与投影转换但在实际工程中仅仅“能读写”是不够的。你一定会遇到这些需求查询某个范围内的所有要素地图框选查询距离某点 500 米内的所有 POI周边搜索查询人口大于 1000 万的城市属性过滤查询与某条道路相交的所有地块空间关系如果只用 Java 循环遍历 if 判断不仅代码丑陋而且性能极差。GeoTools 提供了 CQL / ECQL 查询语言让你可以像写 SQL 一样做空间查询而且查询是由 DataStore 引擎优化的性能远超内存遍历。二、核心概念速览2.1 CQL vs ECQLCQLCommon Query Language是 OGC 定义的一种面向文本的空间查询语言。GeoTools 对其做了扩展称为 ECQLExtended CQL支持更丰富的语法。简单对比// CQL属性过滤FilterfilterCQL.toFilter(POPULATION 10000000);// ECQL支持更多空间函数FilterfilterECQL.toFilter(DWITHIN(the_geom, POINT(116.4 39.9), 500, kilometers));2.2 FilterFactory2除了用字符串写 CQLGeoTools 还提供了 FilterFactory2 这个 API可以编程式构建查询条件避免字符串拼接的麻烦也更类型安全。FilterFactory2ffCommonFactoryFinder.getFilterFactory2();Filterfilterff.bbox(ff.property(the_geom),115,38,120,42,EPSG:4326);2.3 查询是懒加载的GeoTools 的 FeatureCollection 是“懒加载”的调用getFeatures(filter)时并不会立刻把所有数据读入内存而是返回一个游标真正遍历时才会从数据源读取。这对大文件非常重要。三、Maven 依赖除了之前的基础依赖外本篇需要额外引入gt-cqldependencygroupIdorg.geotools/groupIdartifactIdgt-cql/artifactIdversion29.3/version/dependencydependencygroupIdorg.geotools/groupIdartifactIdgt-shapefile/artifactIdversion29.3/version/dependencydependencygroupIdorg.geotools/groupIdartifactIdgt-epsg-hsql/artifactIdversion29.3/version/dependencydependencygroupIdorg.locationtech.jts/groupIdartifactIdjts-core/artifactIdversion1.19.0/version/dependency四、完整可运行代码以下代码基于 GeoTools 29.3 JTS 1.19.0使用你提供的测试数据包含 5 种典型查询场景。4.1 完整代码importorg.geotools.data.DataStore;importorg.geotools.data.DataStoreFinder;importorg.geotools.data.FeatureSource;importorg.geotools.factory.CommonFactoryFinder;importorg.geotools.feature.FeatureCollection;importorg.geotools.feature.FeatureIterator;importorg.geotools.filter.text.ecql.ECQL;importorg.locationtech.jts.geom.*;importorg.locationtech.jts.io.WKTReader;importorg.opengis.feature.simple.SimpleFeature;importorg.opengis.feature.simple.SimpleFeatureType;importorg.opengis.filter.Filter;importorg.opengis.filter.FilterFactory2;importjava.io.File;importjava.io.Serializable;importjava.nio.charset.StandardCharsets;importjava.util.HashMap;importjava.util.Map;publicclassGeoToolsSpatialQueryDemo{privatestaticfinalGeometryFactoryGFnewGeometryFactory();privatestaticfinalFilterFactory2FFCommonFactoryFinder.getFilterFactory2();publicstaticvoidmain(String[]args)throwsException{StringshpPathD:/testdata/testshp/ne_110m_admin_0_tiny_countries/ne_110m_admin_0_tiny_countries.shp;DataStoredataStorenull;try{dataStoreopenShapefile(shpPath);StringtypeNamedataStore.getTypeNames()[0];FeatureSourceorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturesourcedataStore.getFeatureSource(typeName);// 1. 属性查询CQLqueryByAttribute(source);// 2. BBOX 查询地图框选queryByBBox(source);// 3. 空间关系查询INTERSECTSqueryByIntersects(source);// 4. 距离查询DWITHINqueryByDistance(source);// 5. 组合查询ANDqueryByCombined(source);}finally{if(dataStore!null){dataStore.dispose();}}}// 工具方法 privatestaticDataStoreopenShapefile(Stringpath)throwsException{MapString,SerializableparamsnewHashMap();params.put(url,newFile(path).toURI().toURL());params.put(charset,StandardCharsets.UTF_8.name());returnDataStoreFinder.getDataStore(params);}// 查询示例 // 1. 属性查询privatestaticvoidqueryByAttribute(FeatureSourceorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturesource)throwsException{FilterfilterECQL.toFilter(NAME LIKE %Bah%);FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefcsource.getFeatures(filter);System.out.println(【属性查询】NAME 包含 Bah 的数量: fc.size());printFeatures(fc);}// 2. BBOX 查询地图框选privatestaticvoidqueryByBBox(FeatureSourceorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturesource)throwsException{FilterfilterFF.bbox(FF.property(the_geom),50,-30,120,30,// minX, minY, maxX, maxYEPSG:4326);FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefcsource.getFeatures(filter);System.out.println(\n【BBOX 查询】包围盒内要素数量: fc.size());printFeatures(fc);}// 3. INTERSECTS 查询privatestaticvoidqueryByIntersects(FeatureSourceorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturesource)throwsException{Polygonpolygon(Polygon)newWKTReader(GF).read(POLYGON ((-24.30650713186804524 11.15202787725273481, -24.01438682373617439 -26.82361217989013369, 22.43274216923086328 -30.91329649373626864, 23.60122340175831823 9.10718572032966733, 23.60122340175831823 9.10718572032966733, -24.30650713186804524 11.15202787725273481)));FilterfilterFF.intersects(FF.property(the_geom),FF.literal(polygon));FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefcsource.getFeatures(filter);System.out.println(\n【INTERSECTS 查询】相交要素数量: fc.size());printFeatures(fc);}// 4. 距离查询DWITHINprivatestaticvoidqueryByDistance(FeatureSourceorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturesource)throwsException{PointcenterGF.createPoint(newCoordinate(116.4,39.9));FilterfilterFF.dwithin(FF.property(the_geom),FF.literal(center),500,// 500 公里kilometers);FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefcsource.getFeatures(filter);System.out.println(\n【DWITHIN 查询】500km 内要素数量: fc.size());printFeatures(fc);}// 5. 组合查询privatestaticvoidqueryByCombined(FeatureSourceSimpleFeatureType,SimpleFeaturesource)throwsException{FilterspatialFF.bbox(FF.property(the_geom),50,-30,120,30,EPSG:4326);FilterattributeECQL.toFilter(NAME LIKE %s%);FiltercombinedFF.and(spatial,attribute);FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefcsource.getFeatures(combined);System.out.println(\n【组合查询】BBOX NAME 数量: fc.size());printFeatures(fc);}// 打印辅助privatestaticvoidprintFeatures(FeatureCollectionorg.opengis.feature.simple.SimpleFeatureType,SimpleFeaturefc){try(FeatureIteratorSimpleFeatureitfc.features()){inti0;while(it.hasNext()i3){SimpleFeaturefit.next();System.out.println( - f.getAttribute(NAME));i;}}}}五、代码逐段讲解5.1 属性查询ECQL.toFilterECQL 语法示例FilterfilterECQL.toFilter(NAME LIKE %Bah%);常用 ECQL 属性操作符NAME China // 精确匹配 NAME LIKE %Chi% // 模糊匹配 POPULATION 10000000 // 数值比较 AREA BETWEEN 100 AND 500 // 范围查询 NAME IN (China, India) // IN 查询5.2 BBOX 查询地图框选FilterfilterFF.bbox(FF.property(the_geom),50,-30,120,30,EPSG:4326);BBOX 参数说明参数 1几何字段名通常是the_geom参数 2‑5minX,minY,maxX,maxY参数 6坐标系必须是 EPSG 编码BBOX 本质上是一个矩形范围查询等价于the_geom ST_MakeEnvelope(...)5.3 INTERSECTS 查询空间关系FilterfilterFF.intersects(FF.property(the_geom),FF.literal(polygon));除了 INTERSECTSGeoTools 还支持以下空间关系判断FF.contains(a, b) // a 包含 b FF.within(a, b) // a 被 b 包含 FF.intersects(a, b) // a 与 b 相交 FF.disjoint(a, b) // a 与 b 不相交 FF.touches(a, b) // a 与 b 相切 FF.crosses(a, b) // a 与 b 交叉 FF.overlaps(a, b) // a 与 b 重叠5.4 DWITHIN 查询距离查询FilterfilterFF.dwithin(FF.property(the_geom),FF.literal(center),500,kilometers);DWITHIN 参数说明参数 1几何字段名参数 2参考几何点/线/面参数 3距离值参数 4距离单位kilometers/meters/miles/degrees注意如果使用 EPSG:4326度单位是kilometers或meters时GeoTools 会自动进行内部转换。但如果直接写数值不加单位默认是度极易出错。5.5 组合查询AND / ORFilterspatialFF.bbox(FF.property(the_geom),50,-30,120,30,EPSG:4326);FilterattributeECQL.toFilter(NAME LIKE %s%);FiltercombinedFF.and(spatial,attribute);支持的逻辑组合FF.and(filter1, filter2) // 与 FF.or(filter1, filter2) // 或 FF.not(filter) // 非六、运行结果示例这里使用测试数据ne_110m_admin_0_tiny_countries.shp运行结果如下七、常见坑位与注意事项7.1 DWITHIN 的单位陷阱这是最容易踩的坑。如果你写的是FF.dwithin(prop,geom,500,kilometers)// 正确但如果写成FF.dwithin(prop,geom,500,degrees)// 等价于 500 度约 55500 公里或者FF.dwithin(prop,geom,500)// 默认单位取决于 CRS建议始终显式指定单位不要依赖默认值。7.2 BBOX 与 INTERSECTS 的区别BBOX 是“包围盒相交”INTERSECTS 是“几何相交”。在绝大多数情况下应该用 INTERSECTSBBOX 只适合做第一层粗筛。7.3 属性名大小写敏感Shapefile 的属性名在 DBF 中是大小写敏感的。如果字段名是NAME写name会匹配不到。建议先用source.getSchema()查看实际字段名。7.4 ECQL 字符串中的引号转义在 Java 字符串中写 ECQL单引号需要转义ECQL.toFilter(NAME China)// 正确ECQL.toFilter(NAME China)// 错误7.5 查询结果为空但明明有数据常见原因坐标系不一致BBOX 的 CRS 与数据的 CRS 不同字段名拼写错误DWITHIN 距离单位搞错数据本身的几何字段不叫the_geom八、工程实践建议优先使用 ECQL 做属性过滤FilterFactory2 做空间过滤各取所长。组合查询时先空间后属性空间过滤通常能砍掉更多数据。DWITHIN 的距离单位永远显式指定不要依赖默认。查询结果用 try-with-resources 或 finally 关闭 FeatureIterator防止资源泄露。对于超大 Shapefile先用 BBOX 粗筛再用 INTERSECTS 精算。生产环境中建议将常用查询条件封装为方法避免重复拼接 ECQL 字符串。九、小结本篇我们系统学习了 GeoTools 的空间查询能力CQL / ECQL 是 GeoTools 的“空间 SQL”远比 Java 循环高效。FilterFactory2 提供了类型安全的编程式查询 API。BBOX 适合粗筛INTERSECTS 适合精算DWITHIN 适合周边搜索。组合查询AND/OR可以实现复杂的业务条件。单位陷阱、大小写敏感、坐标系不一致是最常见的问题来源。掌握空间查询后你的 GeoTools 工具箱已经覆盖了“读写 格式转换 坐标转换 查询”下一篇我们将进入更激动人心的领域基于 GeoTools 的空间运算与叠加分析。