时空查询之ECQL

前言

  ECQL 是 CQL 的扩展,CQL 是 OGC 标准查询语言,而 ECQL 是 GeoTools 为更好的方便查询,在编程实现时扩展了 CQL,主要扩展在于其移除了 CQL 的一些限制(属性必须在比较运算符的左边,不能创建 Id Filter 进行查询等限制),也和 SQL 更相似。所以可简单认为 CQL 是书面上的标准,而 ECQL 是事实上的标准。

谓词篇

时间查询主要有以下几个查询谓词:

谓词作用
T TEQUALS Time测试 T 和给定时间相等,相当于 T == Time。
T BEFORE Time测试 T 在给定时间之前,相当于 T < Time。
T BEFORE OR DURING Time Period测试 T 在给定时间段之前或其中,相当于 T <= TimePeriod[1]。
T DURING Time Period测试 T 在给定时间段其中,相当于 TimePeriod[0] <= T <= TimePeriod[1]。
T DURING OR AFTER Time Period测试 T 在给定时间段其中或之后,相当于 TimePeriod[0] <= T。
T AFTER Time测试 T 在给定时间之后,相当于 T > Time。

时间段以 / 分隔符区分前后两个时间,时间格式一般为 yyyy-MM-dd'T'HH:mm:ss.SSS'Z'。

空间查询主要有以下几个查询谓词:

谓词作用
INTERSECTS(A: Geometry, B: Geometry)测试 A 与 B 相交,与 DISJOINT 相反。
DISJOINT(A: Geometry, B: Geometry)测试 A 与 B 不相交,与 INTERSECTS 相反。
CONTAINS(A: Geometry, B: Geometry)测试 A 包含 B,与 WITHIN 相反。
WITHIN(A: Geometry, B: Geometry)测试 B 包含 A,即 A 在 B 中,与 CONTAINS 相反。
TOUCHES(A: Geometry, B: Geometry)测试 A 的边界是否与 B 的边界接触,但内部不相交。
CROSSES(A: Geometry, B: Geometry)测试 A 与 B 是否相交,但不存在包含关系。
OVERLAPS(A: Geometry, B: Geometry)测试 A 与 B 是否重叠,需满足 A 与 B 是同一类型(如都是 POLYGON),并且相交区域同样是 A 和 B 的类型(只能是 POLYGON,不能是 POINT)。
EQUALS(A: Geometry, B: Geometry)测试 A 与 B 完全相等。
RELATE(A: Geometry, B: Geometry, nineIntersectionModel: String)测试 A 与 B 是否满足 DE-9IM 模型,该模型可模拟上述所有情况。
DWITHIN(A: Geometry, B: Geometry, distance: double, units: String)测试 A 与 B 的最短距离是否不超过多少距离,单位有(feet, meters, statute miles, nautical miles, kilometers)。
BEYOND(A: Geometry, B: Geometry, distance: Double, units: String)测试 A 与 B 的最短距离是否超过多少距离。
BBOX(A: Geometry, leftBottomLng: Double, leftBottomLat: Double, rightTopLng: Double, rightTopLat: Double, crs="EPSG:4326")测试 A 是否与给定 box 相交。

Geometry 是指 WKT 格式的数据,主要有以下几种:

类型示例
POINTPOINT(6 10)
LINESTRINGLINESTRING(3 4,10 50,20 25)
POLYGONPOLYGON((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2))
MULTIPOINTMULTIPOINT(3.5 5.6, 4.8 10.5)
MULTILINESTRINGMULTILINESTRING((3 4,10 50,20 25),(-5 -8,-10 -8,-15 -4))
MULTIPOLYGONMULTIPOLYGON(((1 1,5 1,5 5,1 5,1 1),(2 2,2 3,3 3,3 2,2 2)),((6 3,9 2,9 4,6 3)))
GEOMETRYCOLLECTIONGEOMETRYCOLLECTION(POINT(4 6),LINESTRING(4 6,7 10))

※注: POLYGON 中的边界点必须闭合,即首尾点相同,若存在多个边界,则需要遵循 逆时针,顺时针,逆时针,顺时针... 的点排列顺序,逆时针封闭,顺时针开孔,以形成具有岛和洞的复杂多边形。

  由于 WKT 标准只支持二维的坐标,为支持三维坐标以及齐次线性计算,所以在 PostGIS 中又有 EWKT 标准实现,EWKT 扩展了 WKT,带 Z 结尾用来支持三维坐标,带 M 结尾用来支持齐次线性计算,如 POINTZ(6 10 3)POINTM(6 10 1)POINTZM(6 10 3 1),同时还支持坐标内嵌空间参考系,如 SRID=4326;LINESTRING(-134.921387 58.687767, -135.303391 59.092838)。GeoTools 19.0 之后也默认以 EWKT 进行解析和编码。

查询篇

属性字段查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 查询属性 ATTR1 小于 7 的数据
Filter filter = ECQL.toFilter("ATTR1 < (1 + ((3 / 2) * 4))" );

// 查询属性 ATTR1 小于属性 ATTR2 绝对值的数据
Filter filter = ECQL.toFilter("ATTR1 < abs(ATTR2)" );

// 查询属性 ATTR1 为 test 字符串的数据
Filter filter = ECQL.toFilter("ATTR1 == 'test'" );

// 查询属性 ATTR1 在 10 和 20 之间的数据
Filter filter = ECQL.toFilter( "ATTR1 BETWEEN 10 AND 20" );
Filter filter = ECQL.toFilter( "ATTR1 >= 10 AND ATTR1 <= 20" );

// 多条件查询
Filter filter = ECQL.toFilter("ATTR1 < 10 AND ATTR2 < 2 OR ATTR3 > 10" );

// 查询属性 ATTR1 为 silver 或 oil 或 gold 的数据
Filter filter = ECQL.toFilter("ATTR1 IN ('silver','oil', 'gold' )");

// 以 ID 主键进行查询
Filter filter = ECQL.toFilter("IN ('river.1', 'river.2')");
Filter filter = ECQL.toFilter("IN (300, 301)");

模糊查询

1
2
3
4
5
6
7
8
9
10
11
// 查询属性 ATTR1 包含 abc 字符串的数据
Filter filter = ECQL.toFilter( "ATTR1 LIKE '%abc%'" );

// 查询属性 ATTR1 开头不为 abc 字符串的数据
Filter filter = ECQL.toFilter( "ATTR1 NOT LIKE 'abc%'" );

// 查询属性 cityName 开头为 new 的数据,忽略 new 的大小写
Filter filter = ECQL.toFilter("cityName ILIKE 'new%'");

// 测试字符串是否包含
Filter filter = ECQL.toFilter("'aabbcc' LIKE '%bb%'");

空属性查询

1
2
3
4
5
6
7
8
// 查询有属性 ATTR1 存在的数据
Filter filter = ECQL.toFilter( "ATTR1 EXISTS" );

// 查询属性 ATTR1 不存在的数据
Filter filter = ECQL.toFilter( "ATTR1 DOES-NOT-EXIST" );

// 查询 Name 为 NULL 的数据
Filter filter = ECQL.toFilter("Name IS NULL");

时间查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 查询时间属性 dtg 等于的数据
Filter filter = ECQL.toFilter( "dtg TEQUALS 2006-11-30T01:30:00Z" );

// 查询时间属性 dtg 在之后的数据
Filter filter = ECQL.toFilter("dtg AFTER 2006-11-30T01:30:00Z");

// 查询时间属性 dtg 在之前的数据
Filter filter = ECQL.toFilter("dtg BEFORE 2006-11-30T01:30:00Z");

// 查询时间属性 dtg 在之间的数据,+3:00 代表 GMT 时间 +3 小时,以 Z 结尾的时间就是 GMT 时间
Filter filter = ECQL.toFilter( "dtg DURING 2006-11-30T00:30:00+03:00/2006-11-30T01:30:00+03:00 ");

// 查询时间属性 dtg 等于的数据
Filter filter = ECQL.toFilter("dtg = 1981-06-20");

// 查询时间属性 dtg 小于等于的数据
Filter filter = ECQL.toFilter("dtg <= 1981-06-20T12:30:01Z");

空间查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 查询空间属性 geom 包含点的数据
Filter filter = ECQL.toFilter( "CONTAINS(geom, POINT(1 2))" );

// 查询空间属性 geom 与 box 相交的数据
Filter filter = ECQL.toFilter( "BBOX(geom, 10,20,30,40)" );

// 查询空间属性 geom 与点最短距离不超过 10 千米的数据
Filter filter = ECQL.toFilter( "DWITHIN(geom, POINT(1 2), 10, kilometers)" );

// 查询空间属性 geom 与线相交的数据(geom 也必须是线)
Filter filter = ECQL.toFilter( "CROSS(geom, LINESTRING(1 2, 10 15))" );

// 查询空间属性 geom 与 GEOMETRYCOLLECTION 相交的数据(geom 也必须是 GEOMETRYCOLLECTION)
Filter filter = ECQL.toFilter( "INTERSECT(geom, GEOMETRYCOLLECTION (POINT (10 10),POINT (30 30),LINESTRING (15 15, 20 20)) )" );

// 查询空间属性 geom 与线相交的数据
Filter filter = ECQL.toFilter( "CROSSES(geom, LINESTRING(1 2, 10 15))" );

// 查询空间属性 geom 与 GEOMETRYCOLLECTION 相交的数据
Filter filter = ECQL.toFilter( "INTERSECTS(geom, GEOMETRYCOLLECTION (POINT (10 10),POINT (30 30),LINESTRING (15 15, 20 20)) )" );

// 查询空间属性 geom 与包含线的数据
Filter filter = ECQL.toFilter("RELATE(geom, LINESTRING (-134.921387 58.687767, -135.303391 59.092838), T*****FF*)");

  在 GeoTools 中,可通过 FilterFactory 来构造 Filter,而不是直接写字符串,具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2();

// 相当于 Filter filter1 = ECQL.toFilter("ATTR1 = 1 AND ATTR2 < 4" );
List<Filter> filterList = ECQL.toFilterList("ATTR1=1; ATTR2<4");
Filter filter1 = ff.and(filterList);

// 相当于 Filter filter2 = ECQL.toFilter( "BBOX(geom, 10,20,30,40)" );
Filter filter2 = ff.bbox("geom", 10, 20, 30, 40, "EPSG:4326");

// 相当于 Filter filter3 = ECQL.toFilter( "dtg DURING 2006-11-29T00:30:00Z/2006-11-30T00:30:00Z");
Date startTime = ZonedDateTime.of(2006, 11, 29, 0, 30, 0, 0, ZoneOffset.UTC);
Date endTime = Date.from(startTime.plusDays(1).toInstant());
Filter filter3 = ff.between(ff.property("dtg"), ff.literal(startTime), ff.literal(endTime));

后记

  基本可认为 CQL 和 SQL 中查询条件差不多,虽然不支持分组查询等复杂 SQL 特性,但对于一般的时空查询基本够用,CQL 中还有些空间操作函数就不继续写了,如取面积,取缓冲区,取交集,取长度等等,有需要的可自行查询 uDig Common Query Language

参考资料

GeoTools CQL

GeoTools ECQL

GeoServer ECQL Reference / GeoServer 属性查询和空间查询支持 CQL / ECQL过滤器语言

WKT解读

GEOS库学习之三:空间关系、DE-9IM和谓词