Skip to content

Commit ec7846c

Browse files
authored
[GH-3075] Add geography support to SedonaFlink spatial predicates (#3077)
1 parent efeff95 commit ec7846c

2 files changed

Lines changed: 258 additions & 0 deletions

File tree

flink/src/main/java/org/apache/sedona/flink/expressions/Predicates.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.sedona.common.geometryObjects.Box3D;
2525
import org.apache.sedona.flink.Box2DTypeSerializer;
2626
import org.apache.sedona.flink.Box3DTypeSerializer;
27+
import org.apache.sedona.flink.GeographyTypeSerializer;
2728
import org.apache.sedona.flink.GeometryTypeSerializer;
2829
import org.locationtech.jts.geom.Geometry;
2930

@@ -84,6 +85,22 @@ public Boolean eval(
8485
if (a == null || b == null) return null;
8586
return org.apache.sedona.common.Predicates.box3dIntersects(a, b);
8687
}
88+
89+
@DataTypeHint("Boolean")
90+
public Boolean eval(
91+
@DataTypeHint(
92+
value = "RAW",
93+
rawSerializer = GeographyTypeSerializer.class,
94+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
95+
org.apache.sedona.common.S2Geography.Geography g1,
96+
@DataTypeHint(
97+
value = "RAW",
98+
rawSerializer = GeographyTypeSerializer.class,
99+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
100+
org.apache.sedona.common.S2Geography.Geography g2) {
101+
if (g1 == null || g2 == null) return null;
102+
return org.apache.sedona.common.geography.Functions.intersects(g1, g2);
103+
}
87104
}
88105

89106
public static class ST_Contains extends ScalarFunction {
@@ -142,6 +159,22 @@ public Boolean eval(
142159
if (a == null || b == null) return null;
143160
return org.apache.sedona.common.Predicates.box3dContains(a, b);
144161
}
162+
163+
@DataTypeHint("Boolean")
164+
public Boolean eval(
165+
@DataTypeHint(
166+
value = "RAW",
167+
rawSerializer = GeographyTypeSerializer.class,
168+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
169+
org.apache.sedona.common.S2Geography.Geography g1,
170+
@DataTypeHint(
171+
value = "RAW",
172+
rawSerializer = GeographyTypeSerializer.class,
173+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
174+
org.apache.sedona.common.S2Geography.Geography g2) {
175+
if (g1 == null || g2 == null) return null;
176+
return org.apache.sedona.common.geography.Functions.contains(g1, g2);
177+
}
145178
}
146179

147180
public static class ST_Within extends ScalarFunction {
@@ -160,10 +193,27 @@ public Boolean eval(
160193
rawSerializer = GeometryTypeSerializer.class,
161194
bridgedTo = Geometry.class)
162195
Object o2) {
196+
if (o1 == null || o2 == null) return null;
163197
Geometry geom1 = (Geometry) o1;
164198
Geometry geom2 = (Geometry) o2;
165199
return org.apache.sedona.common.Predicates.within(geom1, geom2);
166200
}
201+
202+
@DataTypeHint("Boolean")
203+
public Boolean eval(
204+
@DataTypeHint(
205+
value = "RAW",
206+
rawSerializer = GeographyTypeSerializer.class,
207+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
208+
org.apache.sedona.common.S2Geography.Geography g1,
209+
@DataTypeHint(
210+
value = "RAW",
211+
rawSerializer = GeographyTypeSerializer.class,
212+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
213+
org.apache.sedona.common.S2Geography.Geography g2) {
214+
if (g1 == null || g2 == null) return null;
215+
return org.apache.sedona.common.geography.Functions.within(g1, g2);
216+
}
167217
}
168218

169219
public static class ST_Covers extends ScalarFunction {
@@ -274,10 +324,27 @@ public Boolean eval(
274324
rawSerializer = GeometryTypeSerializer.class,
275325
bridgedTo = Geometry.class)
276326
Object o2) {
327+
if (o1 == null || o2 == null) return null;
277328
Geometry geom1 = (Geometry) o1;
278329
Geometry geom2 = (Geometry) o2;
279330
return org.apache.sedona.common.Predicates.equals(geom1, geom2);
280331
}
332+
333+
@DataTypeHint("Boolean")
334+
public Boolean eval(
335+
@DataTypeHint(
336+
value = "RAW",
337+
rawSerializer = GeographyTypeSerializer.class,
338+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
339+
org.apache.sedona.common.S2Geography.Geography g1,
340+
@DataTypeHint(
341+
value = "RAW",
342+
rawSerializer = GeographyTypeSerializer.class,
343+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
344+
org.apache.sedona.common.S2Geography.Geography g2) {
345+
if (g1 == null || g2 == null) return null;
346+
return org.apache.sedona.common.geography.Functions.equals(g1, g2);
347+
}
281348
}
282349

283350
public static class ST_OrderingEquals extends ScalarFunction {
@@ -445,6 +512,23 @@ public Boolean eval(
445512
Geometry geom2 = (Geometry) o2;
446513
return org.apache.sedona.common.Predicates.dWithin(geom1, geom2, distance, useSphere);
447514
}
515+
516+
@DataTypeHint("Boolean")
517+
public Boolean eval(
518+
@DataTypeHint(
519+
value = "RAW",
520+
rawSerializer = GeographyTypeSerializer.class,
521+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
522+
org.apache.sedona.common.S2Geography.Geography g1,
523+
@DataTypeHint(
524+
value = "RAW",
525+
rawSerializer = GeographyTypeSerializer.class,
526+
bridgedTo = org.apache.sedona.common.S2Geography.Geography.class)
527+
org.apache.sedona.common.S2Geography.Geography g2,
528+
@DataTypeHint("Double") Double distance) {
529+
if (g1 == null || g2 == null || distance == null) return null;
530+
return org.apache.sedona.common.geography.Functions.dWithin(g1, g2, distance);
531+
}
448532
}
449533

450534
public static class ST_3DDWithin extends ScalarFunction {
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.sedona.flink;
20+
21+
import static org.apache.flink.table.api.Expressions.$;
22+
import static org.apache.flink.table.api.Expressions.call;
23+
import static org.apache.flink.table.api.Expressions.lit;
24+
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertFalse;
26+
import static org.junit.Assert.assertTrue;
27+
28+
import java.util.Collections;
29+
import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
30+
import org.apache.flink.api.common.typeinfo.TypeInformation;
31+
import org.apache.flink.api.java.typeutils.RowTypeInfo;
32+
import org.apache.flink.streaming.api.datastream.DataStream;
33+
import org.apache.flink.table.api.ApiExpression;
34+
import org.apache.flink.table.api.Table;
35+
import org.apache.flink.types.Row;
36+
import org.apache.sedona.flink.expressions.Predicates;
37+
import org.apache.sedona.flink.expressions.geography.GeographyConstructors;
38+
import org.junit.BeforeClass;
39+
import org.junit.Test;
40+
41+
public class GeographyPredicateTest extends TestBase {
42+
43+
// Geography polygons follow the spherical right-hand rule: a CCW ring's interior is the enclosed
44+
// region. The polygons below are CCW so containment matches the intuitive (planar) expectation; a
45+
// CW ring would denote the complementary region on the sphere.
46+
47+
@BeforeClass
48+
public static void onceExecutedBeforeAll() {
49+
initialize();
50+
}
51+
52+
/** Evaluate a predicate call over two geography columns built from {@code wktA}/{@code wktB}. */
53+
private Object eval(String wktA, String wktB, PredicateFactory factory) {
54+
RowTypeInfo ti =
55+
new RowTypeInfo(
56+
new TypeInformation<?>[] {
57+
BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.STRING_TYPE_INFO
58+
},
59+
new String[] {"a", "b"});
60+
DataStream<Row> ds =
61+
env.fromCollection(Collections.singletonList(Row.of(wktA, wktB))).returns(ti);
62+
Table t =
63+
tableEnv
64+
.fromDataStream(ds)
65+
.select(
66+
call(GeographyConstructors.ST_GeogFromWKT.class.getSimpleName(), $("a"), lit(4326))
67+
.as("ga"),
68+
call(GeographyConstructors.ST_GeogFromWKT.class.getSimpleName(), $("b"), lit(4326))
69+
.as("gb"));
70+
return first(t.select(factory.build($("ga"), $("gb")).as("o"))).getFieldAs("o");
71+
}
72+
73+
private interface PredicateFactory {
74+
ApiExpression build(ApiExpression a, ApiExpression b);
75+
}
76+
77+
@Test
78+
public void testIntersects() {
79+
String poly = "POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))";
80+
assertTrue(
81+
(Boolean)
82+
eval(
83+
poly,
84+
"POINT (1 1)",
85+
(a, b) -> call(Predicates.ST_Intersects.class.getSimpleName(), a, b)));
86+
assertFalse(
87+
(Boolean)
88+
eval(
89+
poly,
90+
"POINT (5 5)",
91+
(a, b) -> call(Predicates.ST_Intersects.class.getSimpleName(), a, b)));
92+
}
93+
94+
@Test
95+
public void testContains() {
96+
assertTrue(
97+
(Boolean)
98+
eval(
99+
"POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
100+
"POINT (1 1)",
101+
(a, b) -> call(Predicates.ST_Contains.class.getSimpleName(), a, b)));
102+
}
103+
104+
@Test
105+
public void testWithin() {
106+
assertTrue(
107+
(Boolean)
108+
eval(
109+
"POINT (1 1)",
110+
"POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))",
111+
(a, b) -> call(Predicates.ST_Within.class.getSimpleName(), a, b)));
112+
}
113+
114+
@Test
115+
public void testEquals() {
116+
assertTrue(
117+
(Boolean)
118+
eval(
119+
"POINT (1 1)",
120+
"POINT (1 1)",
121+
(a, b) -> call(Predicates.ST_Equals.class.getSimpleName(), a, b)));
122+
assertFalse(
123+
(Boolean)
124+
eval(
125+
"POINT (1 1)",
126+
"POINT (2 2)",
127+
(a, b) -> call(Predicates.ST_Equals.class.getSimpleName(), a, b)));
128+
}
129+
130+
@Test
131+
public void testDWithin() {
132+
// Two points ~111 km apart (1 degree of latitude near the equator).
133+
assertTrue(
134+
(Boolean)
135+
eval(
136+
"POINT (0 0)",
137+
"POINT (0 1)",
138+
(a, b) -> call(Predicates.ST_DWithin.class.getSimpleName(), a, b, lit(200000.0))));
139+
assertFalse(
140+
(Boolean)
141+
eval(
142+
"POINT (0 0)",
143+
"POINT (0 1)",
144+
(a, b) -> call(Predicates.ST_DWithin.class.getSimpleName(), a, b, lit(50000.0))));
145+
}
146+
147+
@Test
148+
public void testGeometryStillWorks() {
149+
// The geometry overload must remain selectable on the same predicate.
150+
RowTypeInfo ti =
151+
new RowTypeInfo(
152+
new TypeInformation<?>[] {
153+
BasicTypeInfo.STRING_TYPE_INFO, BasicTypeInfo.STRING_TYPE_INFO
154+
},
155+
new String[] {"a", "b"});
156+
DataStream<Row> ds =
157+
env.fromCollection(
158+
Collections.singletonList(
159+
Row.of("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", "POINT (1 1)")))
160+
.returns(ti);
161+
String geom =
162+
org.apache.sedona.flink.expressions.Constructors.ST_GeomFromWKT.class.getSimpleName();
163+
Table t =
164+
tableEnv
165+
.fromDataStream(ds)
166+
.select(call(geom, $("a")).as("ga"), call(geom, $("b")).as("gb"));
167+
Object out =
168+
first(
169+
t.select(
170+
call(Predicates.ST_Contains.class.getSimpleName(), $("ga"), $("gb")).as("o")))
171+
.getFieldAs("o");
172+
assertEquals(true, out);
173+
}
174+
}

0 commit comments

Comments
 (0)