Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,24 @@ public static boolean equals(Geography g1, Geography g2) {
return pred.S2_equals(toShapeIndex(g1), toShapeIndex(g2), s2Options());
}

/**
* Spherical "distance within" test. Returns true iff the minimum geodesic distance between g1 and
* g2 (in meters) is less than or equal to {@code distanceMeters}.
*/
public static boolean dWithin(Geography g1, Geography g2, double distanceMeters) {
if (g1 == null || g2 == null) return false;
Double d = distance(g1, g2);
return d != null && d <= distanceMeters;
}

/**
* Spherical "within" test. Returns true iff g1 is fully inside g2 on the sphere. OGC convention:
* {@code ST_Within(A, B) == ST_Contains(B, A)}.
*/
public static boolean within(Geography g1, Geography g2) {
return contains(g2, g1);
}

/** Return EWKT for geography object */
public static String asEWKT(Geography geography) {
return geography.toEWKT();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,129 @@ public void contains_nullHandling() throws ParseException {
assertFalse(Functions.contains(null, g1));
}

// ─── Level 3: ST_DWithin ─────────────────────────────────────────────────

@Test
public void dWithin_twoPointsOneDegreeApart() throws ParseException {
Geography g1 = Constructors.geogFromWKT("POINT (0 0)", 4326);
Geography g2 = Constructors.geogFromWKT("POINT (0 1)", 4326);
// 1° of latitude ≈ 111_195 m on the sphere
assertFalse(Functions.dWithin(g1, g2, 100_000.0));
assertTrue(Functions.dWithin(g1, g2, 200_000.0));
}

@Test
public void dWithin_pointInsidePolygon() throws ParseException {
Geography poly = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326);
Geography pt = Constructors.geogFromWKT("POINT (0.5 0.5)", 4326);
// Distance is zero when one contains the other; any positive threshold should pass.
assertTrue(Functions.dWithin(poly, pt, 1.0));
}

@Test
public void dWithin_boundaryInclusive() throws ParseException {
// distance == threshold ⇒ true (inclusive <=)
Geography g1 = Constructors.geogFromWKT("POINT (0 0)", 4326);
Geography g2 = Constructors.geogFromWKT("POINT (0 1)", 4326);
double actual = Functions.distance(g1, g2);
assertTrue(Functions.dWithin(g1, g2, actual));
assertFalse(Functions.dWithin(g1, g2, actual - 1.0));
}

@Test
public void dWithin_antimeridianCrossing() throws ParseException {
// Two points straddling the antimeridian: great-circle distance ~22 km,
// planar distance ~40_000 km — succeeding at 50 km proves we use spherical distance.
Geography g1 = Constructors.geogFromWKT("POINT (179.9 0)", 4326);
Geography g2 = Constructors.geogFromWKT("POINT (-179.9 0)", 4326);
assertTrue(Functions.dWithin(g1, g2, 50_000.0));
}

@Test
public void dWithin_nullHandling() throws ParseException {
Geography g = Constructors.geogFromWKT("POINT (0 0)", 4326);
assertFalse(Functions.dWithin(g, null, 1e6));
assertFalse(Functions.dWithin(null, g, 1e6));
assertFalse(Functions.dWithin(null, null, 1e6));
}

@Test
public void dWithin_reflexiveZeroThreshold() throws ParseException {
// A point is trivially within distance 0 of itself (distance == 0, threshold == 0, <= is
// inclusive).
Geography g = Constructors.geogFromWKT("POINT (10 20)", 4326);
assertTrue(Functions.dWithin(g, g, 0.0));
}

@Test
public void dWithin_negativeDistance() throws ParseException {
// No two geographies can be at a negative geodesic distance, so any negative threshold =>
// false.
Geography g1 = Constructors.geogFromWKT("POINT (0 0)", 4326);
Geography g2 = Constructors.geogFromWKT("POINT (0 0)", 4326);
assertFalse(Functions.dWithin(g1, g2, -1.0));
}

@Test
public void dWithin_nanDistance() throws ParseException {
// NaN threshold => all comparisons are false.
Geography g1 = Constructors.geogFromWKT("POINT (0 0)", 4326);
Geography g2 = Constructors.geogFromWKT("POINT (0 1)", 4326);
assertFalse(Functions.dWithin(g1, g2, Double.NaN));
}

// ─── Level 3: ST_Within ──────────────────────────────────────────────────

@Test
public void within_pointInPolygon() throws ParseException {
Geography pt = Constructors.geogFromWKT("POINT (0.5 0.5)", 4326);
Geography poly = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326);
assertTrue(Functions.within(pt, poly));
}

@Test
public void within_pointOutsidePolygon() throws ParseException {
Geography pt = Constructors.geogFromWKT("POINT (2 2)", 4326);
Geography poly = Constructors.geogFromWKT("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))", 4326);
assertFalse(Functions.within(pt, poly));
}

@Test
public void within_isContainsSwapped() throws ParseException {
// OGC parity: within(A, B) == contains(B, A) for every input pair.
Geography poly = Constructors.geogFromWKT("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", 4326);
Geography inside = Constructors.geogFromWKT("POINT (1 1)", 4326);
Geography outside = Constructors.geogFromWKT("POINT (3 3)", 4326);
assertEquals(Functions.contains(poly, inside), Functions.within(inside, poly));
assertEquals(Functions.contains(poly, outside), Functions.within(outside, poly));
}

@Test
public void within_nullHandling() throws ParseException {
Geography g = Constructors.geogFromWKT("POINT (1 1)", 4326);
assertFalse(Functions.within(g, null));
assertFalse(Functions.within(null, g));
assertFalse(Functions.within(null, null));
}

@Test
public void within_polygonInPolygon() throws ParseException {
Geography inner = Constructors.geogFromWKT("POLYGON ((1 1, 2 1, 2 2, 1 2, 1 1))", 4326);
Geography outer = Constructors.geogFromWKT("POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0))", 4326);
assertTrue(Functions.within(inner, outer));
// Swapped: the outer polygon is NOT within the inner one.
assertFalse(Functions.within(outer, inner));
}

@Test
public void within_overlappingNotContained() throws ParseException {
// Two polygons that intersect but neither is contained in the other.
Geography a = Constructors.geogFromWKT("POLYGON ((0 0, 2 0, 2 2, 0 2, 0 0))", 4326);
Geography b = Constructors.geogFromWKT("POLYGON ((1 1, 3 1, 3 3, 1 3, 1 1))", 4326);
assertFalse(Functions.within(a, b));
assertFalse(Functions.within(b, a));
}

// ─── Level 4: ST_Buffer ──────────────────────────────────────────────────

@Test
Expand Down
2 changes: 2 additions & 0 deletions docs/api/sql/geography/Geography-Functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,6 @@ These functions operate on geography type objects.
| [ST_Distance](Geography-Functions/ST_Distance.md) | Double | Return the minimum geodesic distance between two geographies in meters. | v1.9.0 |
| [ST_Length](Geography-Functions/ST_Length.md) | Double | Return the spherical length of a geography in meters, summed along great-circle edges. | v1.9.1 |
| [ST_Contains](Geography-Functions/ST_Contains.md) | Boolean | Test whether geography A fully contains geography B. | v1.9.0 |
| [ST_DWithin](Geography-Functions/ST_DWithin.md) | Boolean | Test whether two geographies are within a given geodesic distance (in meters) of each other. | v1.9.1 |
| [ST_Within](Geography-Functions/ST_Within.md) | Boolean | Test whether geography A is fully within geography B. | v1.9.1 |
| [ST_Equals](Geography-Functions/ST_Equals.md) | Boolean | Test whether two geographies are spatially equal. | v1.9.1 |
65 changes: 65 additions & 0 deletions docs/api/sql/geography/Geography-Functions/ST_DWithin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# ST_DWithin

Introduction: Tests whether two geographies are within a given geodesic distance (in meters) of each other on the sphere. The minimum great-circle distance between any two points on the two geographies is compared against the threshold; the test is inclusive (returns true when the minimum distance equals the threshold).

![ST_DWithin returning true](../../../../image/ST_DWithin_geography/ST_DWithin_geography_true.svg "ST_DWithin returning true")
![ST_DWithin returning false](../../../../image/ST_DWithin_geography/ST_DWithin_geography_false.svg "ST_DWithin returning false")

Format:

`ST_DWithin (A: Geography, B: Geography, distance: Double)`

Return type: `Boolean`

Since: `v1.9.1`

SQL Example

```sql
SELECT ST_DWithin(
ST_GeogFromWKT('POINT (0 0)', 4326),
ST_GeogFromWKT('POINT (0 1)', 4326),
200000.0
);
```

Output:

```
true
```

The same pair of points with a tighter threshold:

```sql
SELECT ST_DWithin(
ST_GeogFromWKT('POINT (0 0)', 4326),
ST_GeogFromWKT('POINT (0 1)', 4326),
100000.0
);
```

Output:

```
false
```
50 changes: 50 additions & 0 deletions docs/api/sql/geography/Geography-Functions/ST_Within.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

# ST_Within

Introduction: Tests whether geography `A` is fully within geography `B` using S2 spherical boolean operations. Returns true when every point of `A`'s interior lies in `B`'s interior. By OGC convention, `ST_Within(A, B)` is equivalent to `ST_Contains(B, A)`, and shares the same boundary semantics.

Boundary semantics on the sphere are inherited from S2's boolean operations and depend on each ring's vertex orientation: along an edge that is "owned" by `B`'s boundary the test returns true, and along the opposite edge it returns false. Do not rely on a specific result for points that lie exactly on `B`'s boundary; for predictable behavior, use a strict interior point or expand `B` slightly with `ST_Buffer` before testing.

![ST_Within returning true](../../../../image/ST_Within_geography/ST_Within_geography_true.svg "ST_Within returning true")
![ST_Within returning false](../../../../image/ST_Within_geography/ST_Within_geography_false.svg "ST_Within returning false")

Format:

`ST_Within (A: Geography, B: Geography)`

Return type: `Boolean`

Since: `v1.9.1`

SQL Example — interior point:

Comment thread
zhangfengcdt marked this conversation as resolved.
```sql
SELECT ST_Within(
ST_GeogFromWKT('POINT (0.5 0.5)', 4326),
ST_GeogFromWKT('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))', 4326)
);
```

Output:

```
true
```
42 changes: 42 additions & 0 deletions docs/image/ST_DWithin_geography/ST_DWithin_geography_false.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions docs/image/ST_DWithin_geography/ST_DWithin_geography_true.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading