-
Notifications
You must be signed in to change notification settings - Fork 757
[GH-2926] Add ST_BoxIntersects and ST_BoxContains for Box2D #2932
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| */ | ||
| package org.apache.sedona.common; | ||
|
|
||
| import org.apache.sedona.common.geometryObjects.Box2D; | ||
| import org.apache.sedona.common.sphere.Spheroid; | ||
| import org.locationtech.jts.geom.*; | ||
| import org.locationtech.jts.operation.relate.RelateOp; | ||
|
|
@@ -27,6 +28,55 @@ public static boolean contains(Geometry leftGeometry, Geometry rightGeometry) { | |
| return leftGeometry.contains(rightGeometry); | ||
| } | ||
|
|
||
| /** | ||
| * Closed-interval bbox intersection: true if {@code a} and {@code b} overlap on <em>both</em> the | ||
| * X and Y axes (matches PostGIS {@code &&} on box2d). Edge- and corner-touching boxes count as | ||
| * intersecting. | ||
| * | ||
| * <p>Both arguments must have ordered bounds ({@code xmin <= xmax} and {@code ymin <= ymax}). | ||
| * Sedona's Box2D type allows inverted bounds ({@code xmin > xmax}) — that ordering is reserved | ||
| * for a future antimeridian-wraparound semantics on geography bboxes (cf. sedona-db's {@code | ||
| * WraparoundInterval}). Until those semantics ship, planar predicates throw on inverted input | ||
| * rather than silently returning misleading results. SQL callers see NULL in/out null | ||
| * propagation; this Java entry point throws on null. | ||
| */ | ||
| public static boolean boxIntersects(Box2D a, Box2D b) { | ||
| requireOrderedPlanarBox(a, "a"); | ||
| requireOrderedPlanarBox(b, "b"); | ||
| return !(a.getXMax() < b.getXMin() | ||
| || a.getXMin() > b.getXMax() | ||
| || a.getYMax() < b.getYMin() | ||
| || a.getYMin() > b.getYMax()); | ||
| } | ||
|
|
||
| /** | ||
| * True if {@code a} fully contains {@code b} on <em>both</em> the X and Y axes (closed intervals; | ||
| * matches PostGIS {@code ~} on box2d). Equal boxes contain each other. | ||
| * | ||
| * <p>Same ordered-bound contract as {@link #boxIntersects(Box2D, Box2D)} — inverted bounds throw | ||
| * because planar containment with inverted intervals has no defined meaning until antimeridian | ||
| * wraparound semantics ship. | ||
| */ | ||
| public static boolean boxContains(Box2D a, Box2D b) { | ||
| requireOrderedPlanarBox(a, "a"); | ||
| requireOrderedPlanarBox(b, "b"); | ||
| return a.getXMin() <= b.getXMin() | ||
| && a.getYMin() <= b.getYMin() | ||
| && a.getXMax() >= b.getXMax() | ||
| && a.getYMax() >= b.getYMax(); | ||
|
Comment on lines
+60
to
+66
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same fix in 6726b1b — |
||
| } | ||
|
|
||
| private static void requireOrderedPlanarBox(Box2D box, String argName) { | ||
| if (box.getXMin() > box.getXMax() || box.getYMin() > box.getYMax()) { | ||
| throw new IllegalArgumentException( | ||
| "Box2D argument '" | ||
| + argName | ||
| + "' has inverted bounds (xmin > xmax or ymin > ymax). Planar Box2D predicates " | ||
| + "require ordered intervals; inverted bounds are reserved for future antimeridian " | ||
| + "wraparound semantics."); | ||
| } | ||
| } | ||
|
|
||
| public static boolean intersects(Geometry leftGeometry, Geometry rightGeometry) { | ||
| return leftGeometry.intersects(rightGeometry); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,7 @@ | |
| import static org.apache.sedona.common.Functions.crossesDateLine; | ||
| import static org.junit.Assert.*; | ||
|
|
||
| import org.apache.sedona.common.geometryObjects.Box2D; | ||
| import org.junit.Test; | ||
| import org.locationtech.jts.geom.Coordinate; | ||
| import org.locationtech.jts.geom.Geometry; | ||
|
|
@@ -32,6 +33,55 @@ public class PredicatesTest extends TestBase { | |
|
|
||
| private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); | ||
|
|
||
| @Test | ||
| public void testBoxIntersects() { | ||
| Box2D a = new Box2D(0.0, 0.0, 5.0, 5.0); | ||
|
|
||
| // Full overlap | ||
| assertTrue(Predicates.boxIntersects(a, new Box2D(1.0, 1.0, 2.0, 2.0))); | ||
| // Partial overlap | ||
| assertTrue(Predicates.boxIntersects(a, new Box2D(3.0, 3.0, 7.0, 7.0))); | ||
| // Edge-touching (closed intervals) | ||
| assertTrue(Predicates.boxIntersects(a, new Box2D(5.0, 0.0, 10.0, 5.0))); | ||
| // Corner-touching (closed intervals) | ||
| assertTrue(Predicates.boxIntersects(a, new Box2D(5.0, 5.0, 10.0, 10.0))); | ||
| // Disjoint on X | ||
| assertFalse(Predicates.boxIntersects(a, new Box2D(6.0, 0.0, 10.0, 5.0))); | ||
| // Disjoint on Y | ||
| assertFalse(Predicates.boxIntersects(a, new Box2D(0.0, 6.0, 5.0, 10.0))); | ||
| } | ||
|
Comment on lines
+36
to
+52
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added |
||
|
|
||
| @Test | ||
| public void testBoxContains() { | ||
| Box2D outer = new Box2D(0.0, 0.0, 10.0, 10.0); | ||
|
|
||
| assertTrue(Predicates.boxContains(outer, new Box2D(2.0, 2.0, 5.0, 5.0))); | ||
| // Boundaries are inclusive | ||
| assertTrue(Predicates.boxContains(outer, new Box2D(0.0, 0.0, 10.0, 10.0))); | ||
| assertTrue(Predicates.boxContains(outer, new Box2D(0.0, 0.0, 1.0, 1.0))); | ||
| // Outside on X | ||
| assertFalse(Predicates.boxContains(outer, new Box2D(-1.0, 0.0, 5.0, 5.0))); | ||
| // Crosses boundary on X | ||
| assertFalse(Predicates.boxContains(outer, new Box2D(5.0, 0.0, 11.0, 5.0))); | ||
| } | ||
|
|
||
| @Test | ||
| public void testBoxPredicatesRejectInvertedBounds() { | ||
| // Box2D allows xmin > xmax (reserved for future antimeridian wraparound); planar predicates | ||
| // refuse to evaluate them rather than silently returning misleading results. | ||
| Box2D normal = new Box2D(0.0, 0.0, 5.0, 5.0); | ||
| Box2D wrapX = new Box2D(170.0, 10.0, -170.0, 20.0); // longitude crosses antimeridian | ||
| Box2D wrapY = new Box2D(0.0, 5.0, 5.0, 0.0); // ymin > ymax | ||
|
|
||
| IllegalArgumentException ex1 = | ||
| assertThrows(IllegalArgumentException.class, () -> Predicates.boxIntersects(wrapX, normal)); | ||
| assertTrue(ex1.getMessage().contains("inverted bounds")); | ||
|
|
||
| IllegalArgumentException ex2 = | ||
| assertThrows(IllegalArgumentException.class, () -> Predicates.boxContains(normal, wrapY)); | ||
| assertTrue(ex2.getMessage().contains("inverted bounds")); | ||
| } | ||
|
|
||
| @Test | ||
| public void testDWithinSuccess() { | ||
| Geometry point1 = GEOMETRY_FACTORY.createPoint(new Coordinate(1, 1)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 6726b1b — went with option (a).
boxIntersects/boxContainsnow callrequireOrderedPlanarBoxand throwIllegalArgumentExceptionwith a clear message on inverted bounds. Defining wraparound semantics on a planar predicate without the geography work in #2929 would be premature; failing fast is the honest contract for now.