|
36 | 36 | import org.locationtech.jts.geom.GeometryFactory; |
37 | 37 | import org.locationtech.jts.geom.PrecisionModel; |
38 | 38 | import org.locationtech.jts.io.ParseException; |
| 39 | +import org.locationtech.jts.io.WKTReader; |
39 | 40 |
|
40 | 41 | public class RasterPredicatesTest extends RasterTestBase { |
41 | 42 | private static final GeometryFactory GEOMETRY_FACTORY = new GeometryFactory(); |
@@ -489,6 +490,164 @@ public void testRasterRasterPredicatesNoCrs() throws FactoryException { |
489 | 490 | Assert.assertFalse(RasterPredicates.rsIntersects(raster3, raster2)); |
490 | 491 | } |
491 | 492 |
|
| 493 | + @Test |
| 494 | + public void testDWithinWGS84RasterPointMeterSemantics() throws FactoryException { |
| 495 | + // 5x5 degree raster at WGS84 origin: hull (0,-5)–(5,0), centroid (2.5, -2.5). |
| 496 | + GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 497 | + |
| 498 | + // Point coincident with the raster centroid — distance is 0, so any non-negative |
| 499 | + // threshold matches and the unit cannot silently fall back to degrees. |
| 500 | + Geometry coincident = GEOMETRY_FACTORY.createPoint(new Coordinate(2.5, -2.5)); |
| 501 | + coincident.setSRID(4326); |
| 502 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, coincident, 0.0)); |
| 503 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, coincident, 1.0)); |
| 504 | + |
| 505 | + // Point ~10° east at the same latitude — geodesic distance ≈ 1 112 km. Bracket the |
| 506 | + // threshold above and below to catch unit mistakes (1° was the old buggy semantics) |
| 507 | + // and projection regressions. |
| 508 | + Geometry tenDegreesEast = GEOMETRY_FACTORY.createPoint(new Coordinate(12.5, -2.5)); |
| 509 | + tenDegreesEast.setSRID(4326); |
| 510 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, tenDegreesEast, 2_000_000.0)); |
| 511 | + Assert.assertFalse(RasterPredicates.rsDWithin(raster, tenDegreesEast, 500_000.0)); |
| 512 | + // A degree-sized threshold (the pre-fix buggy unit) must reject this pair under the |
| 513 | + // meters contract. |
| 514 | + Assert.assertFalse(RasterPredicates.rsDWithin(raster, tenDegreesEast, 10.0)); |
| 515 | + } |
| 516 | + |
| 517 | + @Test |
| 518 | + public void testDWithinSwappedOperands() throws FactoryException { |
| 519 | + GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 520 | + Geometry point = GEOMETRY_FACTORY.createPoint(new Coordinate(12.5, -2.5)); |
| 521 | + point.setSRID(4326); |
| 522 | + // The (raster, geom) and (geom, raster) overloads share the same minimum geodesic distance, |
| 523 | + // so both must agree at and around the threshold. |
| 524 | + Assert.assertEquals( |
| 525 | + RasterPredicates.rsDWithin(raster, point, 2_000_000.0), |
| 526 | + RasterPredicates.rsDWithin(raster, point, 2_000_000.0)); |
| 527 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, point, 2_000_000.0)); |
| 528 | + Assert.assertFalse(RasterPredicates.rsDWithin(raster, point, 500_000.0)); |
| 529 | + } |
| 530 | + |
| 531 | + @Test |
| 532 | + public void testDWithinProjectedRasterReprojects() throws FactoryException { |
| 533 | + // UTM 32610 (meters) raster centred near San Francisco. The predicate must reproject |
| 534 | + // both sides to WGS84 before measuring, regardless of the input CRS. |
| 535 | + GridCoverage2D raster = |
| 536 | + RasterConstructors.makeEmptyRaster( |
| 537 | + 1, "B", 751, 742, 332385, 4258815, 300, -300, 0, 0, 32610); |
| 538 | + |
| 539 | + // Point inside the raster footprint (≈ same area as the centroid): close geodesic |
| 540 | + // distance, matches even with a small threshold. |
| 541 | + Geometry nearby = GEOMETRY_FACTORY.createPoint(new Coordinate(-122.40, 37.75)); |
| 542 | + nearby.setSRID(4326); |
| 543 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, nearby, 200_000.0)); |
| 544 | + |
| 545 | + // Point in Kansas — ≈ 2 400 km from the UTM-10N raster's WGS84 centroid. |
| 546 | + Geometry farAway = GEOMETRY_FACTORY.createPoint(new Coordinate(-95.0, 39.0)); |
| 547 | + farAway.setSRID(4326); |
| 548 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, farAway, 3_000_000.0)); |
| 549 | + Assert.assertFalse(RasterPredicates.rsDWithin(raster, farAway, 1_000_000.0)); |
| 550 | + |
| 551 | + // Same Kansas point expressed in EPSG:3857 (Web Mercator). Even though neither side is |
| 552 | + // in WGS84, the predicate must still reproject and produce identical truth values. |
| 553 | + Geometry farAwayMercator = GEOMETRY_FACTORY.createPoint(new Coordinate(-10575352, 4721671)); |
| 554 | + farAwayMercator.setSRID(3857); |
| 555 | + Assert.assertTrue(RasterPredicates.rsDWithin(raster, farAwayMercator, 3_000_000.0)); |
| 556 | + Assert.assertFalse(RasterPredicates.rsDWithin(raster, farAwayMercator, 1_000_000.0)); |
| 557 | + } |
| 558 | + |
| 559 | + @Test |
| 560 | + public void testDWithinRasterRaster() throws FactoryException { |
| 561 | + // Two WGS84 rasters whose centroids are exactly 10° of longitude apart on the equator: |
| 562 | + // geodesic centroid distance ≈ 1 112 km. |
| 563 | + GridCoverage2D rasterA = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 564 | + GridCoverage2D rasterB = RasterConstructors.makeEmptyRaster(1, 5, 5, 10, 0, 1, -1, 0, 0, 4326); |
| 565 | + Assert.assertTrue(RasterPredicates.rsDWithin(rasterA, rasterB, 2_000_000.0)); |
| 566 | + Assert.assertFalse(RasterPredicates.rsDWithin(rasterA, rasterB, 500_000.0)); |
| 567 | + // Symmetry: swapping the operands does not change the truth value. |
| 568 | + Assert.assertEquals( |
| 569 | + RasterPredicates.rsDWithin(rasterA, rasterB, 2_000_000.0), |
| 570 | + RasterPredicates.rsDWithin(rasterB, rasterA, 2_000_000.0)); |
| 571 | + |
| 572 | + // Cross-CRS pair (UTM 10N + WGS84): the projected raster must be reprojected before |
| 573 | + // distance is computed. |
| 574 | + GridCoverage2D utmRaster = |
| 575 | + RasterConstructors.makeEmptyRaster( |
| 576 | + 1, "B", 751, 742, 332385, 4258815, 300, -300, 0, 0, 32610); |
| 577 | + // WGS84 raster co-located with the UTM raster's footprint near SF. |
| 578 | + GridCoverage2D wgs84Nearby = |
| 579 | + RasterConstructors.makeEmptyRaster(1, 10, 10, -122.45, 37.80, 0.01, -0.01, 0, 0, 4326); |
| 580 | + Assert.assertTrue(RasterPredicates.rsDWithin(utmRaster, wgs84Nearby, 500_000.0)); |
| 581 | + Assert.assertEquals( |
| 582 | + RasterPredicates.rsDWithin(utmRaster, wgs84Nearby, 500_000.0), |
| 583 | + RasterPredicates.rsDWithin(wgs84Nearby, utmRaster, 500_000.0)); |
| 584 | + } |
| 585 | + |
| 586 | + @Test |
| 587 | + public void testDWithinMixedOrientationMultiPolygon() throws FactoryException, ParseException { |
| 588 | + // Regression: the helper used to flip the whole multipolygon based on the first shell's |
| 589 | + // orientation, which left any later polygon with a different winding mis-oriented when |
| 590 | + // handed to S2. Two semantically-identical multipolygons (one all-CCW, one with the second |
| 591 | + // shell wound CW) must produce identical truth values. |
| 592 | + GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 593 | + WKTReader reader = new WKTReader(); |
| 594 | + Geometry allCcw = |
| 595 | + reader.read( |
| 596 | + "MULTIPOLYGON (((20 20, 21 20, 21 21, 20 21, 20 20)), " |
| 597 | + + "((30 30, 31 30, 31 31, 30 31, 30 30)))"); |
| 598 | + allCcw.setSRID(4326); |
| 599 | + Geometry mixed = |
| 600 | + reader.read( |
| 601 | + // First polygon CCW (as in `allCcw`); second polygon wound CW. |
| 602 | + "MULTIPOLYGON (((20 20, 21 20, 21 21, 20 21, 20 20)), " |
| 603 | + + "((30 30, 30 31, 31 31, 31 30, 30 30)))"); |
| 604 | + mixed.setSRID(4326); |
| 605 | + // Both forms describe the same pair of squares, so the predicate must agree on every |
| 606 | + // threshold around the actual geodesic distance. |
| 607 | + Assert.assertEquals( |
| 608 | + RasterPredicates.rsDWithin(raster, allCcw, 5_000_000.0), |
| 609 | + RasterPredicates.rsDWithin(raster, mixed, 5_000_000.0)); |
| 610 | + Assert.assertEquals( |
| 611 | + RasterPredicates.rsDWithin(raster, allCcw, 1_000_000.0), |
| 612 | + RasterPredicates.rsDWithin(raster, mixed, 1_000_000.0)); |
| 613 | + } |
| 614 | + |
| 615 | + @Test |
| 616 | + public void testDWithinEmptyMultiPolygon() throws FactoryException, ParseException { |
| 617 | + // Regression: `getGeometryN(0)` would throw IndexOutOfBoundsException on MULTIPOLYGON EMPTY. |
| 618 | + // The predicate must accept an empty multipolygon operand without crashing. |
| 619 | + GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 620 | + WKTReader reader = new WKTReader(); |
| 621 | + Geometry empty = reader.read("MULTIPOLYGON EMPTY"); |
| 622 | + empty.setSRID(4326); |
| 623 | + RasterPredicates.rsDWithin(raster, empty, 1_000_000.0); |
| 624 | + } |
| 625 | + |
| 626 | + @Test |
| 627 | + public void testDWithinDoesNotMutateCallerGeometry() throws FactoryException { |
| 628 | + // Regression: the predicate used to call setSRID(4326) directly on the caller's geometry |
| 629 | + // when no orientation change was needed. Verify SRID and coordinate identity are untouched |
| 630 | + // after the predicate runs, for both same-CRS (no transform) and cross-CRS paths. |
| 631 | + GridCoverage2D raster = RasterConstructors.makeEmptyRaster(1, 5, 5, 0, 0, 1, -1, 0, 0, 4326); |
| 632 | + |
| 633 | + // Same-CRS: no transform happens, so the predicate sees the caller's exact JTS object. |
| 634 | + Geometry sameCrsPoint = GEOMETRY_FACTORY.createPoint(new Coordinate(2.5, -2.5)); |
| 635 | + sameCrsPoint.setSRID(0); |
| 636 | + Coordinate originalCoord = sameCrsPoint.getCoordinate().copy(); |
| 637 | + RasterPredicates.rsDWithin(raster, sameCrsPoint, 1.0); |
| 638 | + Assert.assertEquals(0, sameCrsPoint.getSRID()); |
| 639 | + Assert.assertEquals(originalCoord, sameCrsPoint.getCoordinate()); |
| 640 | + |
| 641 | + // Cross-CRS: JTS.transform produces a fresh geometry internally, but the caller's object |
| 642 | + // must still be untouched. |
| 643 | + GeometryFactory mercatorFactory = new GeometryFactory(new PrecisionModel(), 3857); |
| 644 | + Geometry mercatorPoint = mercatorFactory.createPoint(new Coordinate(278300.0, -278300.0)); |
| 645 | + Coordinate originalMercatorCoord = mercatorPoint.getCoordinate().copy(); |
| 646 | + RasterPredicates.rsDWithin(raster, mercatorPoint, 1.0); |
| 647 | + Assert.assertEquals(3857, mercatorPoint.getSRID()); |
| 648 | + Assert.assertEquals(originalMercatorCoord, mercatorPoint.getCoordinate()); |
| 649 | + } |
| 650 | + |
492 | 651 | @Test |
493 | 652 | public void testIsCRSMatchesEPSGCode() throws FactoryException { |
494 | 653 | CoordinateReferenceSystem epsg4326 = CRS.decode("EPSG:4326"); |
|
0 commit comments