This is an automated email from the ASF dual-hosted git repository. amanin pushed a commit to branch fix/world-envelope-to-polygon in repository https://gitbox.apache.org/repos/asf/sis.git
commit f5a308bb58915c894a24a31d5ada6f56a44b0bf0 Author: Alexis Manin <alexis.ma...@geomatys.com> AuthorDate: Thu Jul 28 19:19:31 2022 +0200 fix(Core): add control points if required when converting an envelope to a polygon. On axes with potential wrap-around, if the envelope is very large, the conversion to a rectangle without additional control points create an ambiguity. When trying to link two points of the rectangle using shortest path, the result could not follow rectangle edges, due to the wrap-around. Adding control points mitigate that problem. Note: the current solution is only a quick fix, surely there is room for improvement. For example, the current solution only consider a single wrap-around period. --- .../apache/sis/internal/feature/Geometries.java | 44 +++++++++++++++++----- .../sis/internal/filter/GeometryConverter.java | 13 +++++++ .../sis/filter/BinarySpatialFilterTestCase.java | 23 +++++++++++ .../sis/internal/feature/GeometriesTestCase.java | 36 ++++++++++++++---- 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java index 4a2a9a2b91..9a14b019db 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Geometries.java @@ -21,6 +21,7 @@ import java.io.Serializable; import java.nio.ByteBuffer; import java.util.Optional; import java.util.Iterator; +import org.apache.sis.internal.referencing.WraparoundAxesFinder; import org.opengis.geometry.DirectPosition; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; @@ -388,19 +389,39 @@ public abstract class Geometries<G> implements Serializable { * Creates a polyline made of points describing a rectangle whose start point is the lower left corner. * The sequence of points describes each corner, going in clockwise direction and repeating the starting * point to properly close the ring. - * - * @param xd dimension of first axis. - * @param yd dimension of second axis. + * In case a wrap-around ambiguity resides, control points are also added in the middle of the rectangle edges. + * + * @param xd dimension of first axis. + * @param yd dimension of second axis. + * @param xPeriod Maximum span on <em>first</em> axis before triggering a wrap-around. + * If no wrap-around is possible, please set it to {@link Double#POSITIVE_INFINITY}. + * @param yPeriod Maximum span on <em>second</em> axis before triggering a wrap-around. + * If no wrap-around is possible, please set it to {@link Double#POSITIVE_INFINITY}. * @return a polyline made of a sequence of 5 points describing the given rectangle. */ - private GeometryWrapper<G> createGeometry2D(final Envelope envelope, final int xd, final int yd) { + private GeometryWrapper<G> createGeometry2D(final Envelope envelope, final int xd, final int yd, double xPeriod, double yPeriod) { final DirectPosition lc = envelope.getLowerCorner(); final DirectPosition uc = envelope.getUpperCorner(); final double xmin = lc.getOrdinate(xd); final double ymin = lc.getOrdinate(yd); final double xmax = uc.getOrdinate(xd); final double ymax = uc.getOrdinate(yd); - return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { + final boolean applyXWrapAround = xPeriod / 2 < xmax - xmin; + final boolean applyYWrapAround = yPeriod / 2 < ymax - ymin; + if (applyXWrapAround && applyYWrapAround) { + final double xmid = (xmin + xmax) / 2; + final double ymid = (ymin + ymax) / 2; + return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { + xmin, ymin, xmin, ymid, xmin, ymax, xmid, ymax, xmax, ymid, xmax, ymax, xmax, ymid, xmax, ymin, xmid, ymin, xmin, ymin}))); + } else if (applyXWrapAround) { + final double xmid = (xmin + xmax) / 2; + return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { + xmin, ymin, xmin, ymax, xmid, ymax, xmax, ymax, xmax, ymin, xmid, ymin, xmin, ymin}))); + } else if (applyYWrapAround) { + final double ymid = (ymin + ymax) / 2; + return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { + xmin, ymin, xmin, ymid, xmin, ymax, xmax, ymax, xmax, ymid, xmax, ymin, xmin, ymin}))); + } else return createWrapper(createPolyline(true, BIDIMENSIONAL, Vector.create(new double[] { xmin, ymin, xmin, ymax, xmax, ymax, xmax, ymin, xmin, ymin}))); } @@ -441,32 +462,37 @@ public abstract class Geometries<G> implements Serializable { */ } } + + final double[] periods = crs == null ? null : new WraparoundAxesFinder(crs).periods(); + double xPeriod = periods != null && periods.length > 0 && periods[0] > 0 ? periods[0] : Double.POSITIVE_INFINITY; + double yPeriod = periods != null && periods.length > 1 && periods[1] > 0 ? periods[1] : Double.POSITIVE_INFINITY; + final GeometryWrapper<G> result; switch (strategy) { case NORMALIZE: { throw new IllegalArgumentException(); } case NONE: { - result = createGeometry2D(envelope, xd, yd); + result = createGeometry2D(envelope, xd, yd, xPeriod, yPeriod); break; } default: { final GeneralEnvelope ge = new GeneralEnvelope(envelope); ge.normalize(); ge.wraparound(strategy); - result = createGeometry2D(ge, xd, yd); + result = createGeometry2D(ge, xd, yd, xPeriod, yPeriod); break; } case SPLIT: { final Envelope[] parts = AbstractEnvelope.castOrCopy(envelope).toSimpleEnvelopes(); if (parts.length == 1) { - result = createGeometry2D(parts[0], xd, yd); + result = createGeometry2D(parts[0], xd, yd, xPeriod, yPeriod); break; } @SuppressWarnings({"unchecked", "rawtypes"}) final GeometryWrapper<G>[] polygons = new GeometryWrapper[parts.length]; for (int i=0; i<parts.length; i++) { - polygons[i] = createGeometry2D(parts[i], xd, yd); + polygons[i] = createGeometry2D(parts[i], xd, yd, xPeriod, yPeriod); polygons[i].setCoordinateReferenceSystem(crs); } result = createMultiPolygon(polygons); diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java index 2d362d29b5..1fdfc0ca4c 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java @@ -19,6 +19,7 @@ package org.apache.sis.internal.filter; import java.util.List; import java.util.Collection; import java.util.Collections; +import org.opengis.geometry.DirectPosition; import org.opengis.util.ScopedName; import org.opengis.geometry.Envelope; import org.opengis.metadata.extent.GeographicBoundingBox; @@ -149,6 +150,18 @@ final class GeometryConverter<R,G> extends Node implements Optimization.OnExpres envelope = new ImmutableEnvelope((GeographicBoundingBox) value); } else if (value instanceof Envelope) { envelope = (Envelope) value; + } else if (value instanceof DirectPosition) { + final DirectPosition pt = (DirectPosition) value; + final Object geometry; + if (pt.getDimension() == 2) { + geometry = library.createPoint(pt.getOrdinate(0), pt.getOrdinate(1)); + } else if (pt.getDimension() == 3) { + geometry = library.createPoint(pt.getOrdinate(0), pt.getOrdinate(1), pt.getOrdinate(2)); + } else throw new InvalidFilterValueException(Errors.format( + Errors.Keys.IllegalClass_2, library.rootClass, Classes.getClass(value))); + final GeometryWrapper<G> wrapper = library.castOrWrap(geometry); + if (pt.getCoordinateReferenceSystem() != null) wrapper.setCoordinateReferenceSystem(pt.getCoordinateReferenceSystem()); + return wrapper; } else try { return library.castOrWrap(value); } catch (ClassCastException e) { diff --git a/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java index 5879bc6d82..be5f44f78e 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java +++ b/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java @@ -18,6 +18,11 @@ package org.apache.sis.filter; import javax.measure.Quantity; import javax.measure.quantity.Length; +import org.apache.sis.geometry.DirectPosition2D; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.setup.GeometryLibrary; +import org.apache.sis.test.Assume; +import org.opengis.filter.DistanceOperatorName; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.geometry.Envelope2D; @@ -347,4 +352,22 @@ public abstract strictfp class BinarySpatialFilterTestCase<G> extends TestCase { BinarySpatialOperator<Feature> overlaps = factory.overlaps(literal(Polygon.CONTAINS), right); assertSerializedEquals(overlaps); } + + /** + * Ensures that a world geographic envelope, once converted to a polygon and reprojected, remain coherent. + * This is a regression test. In the past, the operation pipeline [envelope -> polygon -> reprojected polygon] + * caused the result to degenerate to single line following the anti-meridian. + */ + @Test + public void testSpatialContextDoesNotDegenerateEnvelope() throws Exception { + Assume.assumeTrue("Require reprojection. Only supported for JTS for now", library.library == GeometryLibrary.JTS); + final Envelope e1 = new Envelope2D(HardCodedCRS.WGS84, -180, -90, 360, 180); + final DistanceFilter<?, G> within = new DistanceFilter<>(DistanceOperatorName.WITHIN, + library, factory.literal(e1), + factory.literal(new DirectPosition2D(HardCodedCRS.WGS84, 44, 2)), + Quantities.create(1.0, Units.METRE)); + + final GeneralEnvelope envInCtx = within.context.transform(within.expression1.apply(null)).getEnvelope(); + assertNotEquals(envInCtx.getMinimum(0), envInCtx.getMaximum(0), 1000); + } } diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java index 73a9d0c714..a0633569e2 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/GeometriesTestCase.java @@ -18,6 +18,7 @@ package org.apache.sis.internal.feature; import java.util.Arrays; import java.util.Iterator; +import org.apache.sis.geometry.Envelope2D; import org.opengis.geometry.Envelope; import org.apache.sis.setup.GeometryLibrary; import org.apache.sis.geometry.GeneralEnvelope; @@ -193,7 +194,9 @@ public abstract strictfp class GeometriesTestCase extends TestCase { assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS, 165, 32, 165, 33, 190, 33, 190, 32, 165, 32); assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS_LOWER, -195, 32, -195, 33, -170, 33, -170, 32, -195, 32); assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS_UPPER, 165, 32, 165, 33, 190, 33, 190, 32, 165, 32); - assertToGeometryEquals(e, WraparoundMethod.EXPAND, -180, 32, -180, 33, 180, 33, 180, 32, -180, 32); + + assertToGeometryEquals(e, WraparoundMethod.EXPAND, -180, 32, -180, 33, 0, 33, 180, 33, 180, 32, 0, 32, -180, 32); + assertToGeometryEquals(e, WraparoundMethod.SPLIT, 165, 32, 165, 33, 180, 33, 180, 32, 165, 32, -180, 32, -180, 33, -170, 33, -170, 32, -180, 32); e.setRange(0, 177, -170); @@ -202,7 +205,9 @@ public abstract strictfp class GeometriesTestCase extends TestCase { assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS, -183, -42, -183, 2, -170, 2, -170, -42, -183, -42); assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS_UPPER, 177, -42, 177, 2, 190, 2, 190, -42, 177, -42); assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS_LOWER, -183, -42, -183, 2, -170, 2, -170, -42, -183, -42); - assertToGeometryEquals(e, WraparoundMethod.EXPAND, -180, -42, -180, 2, 180, 2, 180, -42, -180, -42); + + assertToGeometryEquals(e, WraparoundMethod.EXPAND, -180, -42, -180, 2, 0, 2, 180, 2, 180, -42, 0, -42, -180, -42); + assertToGeometryEquals(e, WraparoundMethod.SPLIT, 177, -42, 177, 2, 180, 2, 180, -42, 177, -42, -180, -42, -180, 2, -170, 2, -170, -42, -180, -42); } @@ -218,11 +223,28 @@ public abstract strictfp class GeometriesTestCase extends TestCase { e.setRange(1, 1000, 1007); // Time e.setRange(2, 2, 3); // Latitude e.setRange(3, 89, 19); // Longitude (span anti-meridian). - assertToGeometryEquals(e, WraparoundMethod.NONE, 2, 89, 2, 19, 3, 19, 3, 89, 2, 89); - assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS, 2, -271, 2, 19, 3, 19, 3, -271, 2, -271); - assertToGeometryEquals(e, WraparoundMethod.EXPAND, 2, -180, 2, 180, 3, 180, 3, -180, 2, -180); - assertToGeometryEquals(e, WraparoundMethod.SPLIT, 2, 89, 2, 180, 3, 180, 3, 89, 2, 89, - 2, -180, 2, 19, 3, 19, 3, -180, 2, -180); + + assertToGeometryEquals(e, WraparoundMethod.NONE, 2, 89, 2, 19, 3, 19, 3, 89, 2, 89); + + assertToGeometryEquals(e, WraparoundMethod.CONTIGUOUS, 2, -271, 2, -126, 2, 19, 3, 19, 3, -126, 3, -271, 2, -271); + + assertToGeometryEquals(e, WraparoundMethod.EXPAND, 2, -180, 2, 0, 2, 180, 3, 180, 3, 0, 3, -180, 2, -180); + + assertToGeometryEquals(e, WraparoundMethod.SPLIT, 2, 89, 2, 180, 3, 180, 3, 89, 2, 89, + 2, -180, 2, -80.5, 2, 19, 3, 19, 3, -80.5, 3, -180, 2, -180); + } + + @Test + public void testWorldWGS84ToGeometry2D() { + Envelope2D env2d = new Envelope2D(HardCodedCRS.WGS84, -180, -90, 360, 180); + for (WraparoundMethod method : new WraparoundMethod[] { WraparoundMethod.NONE, WraparoundMethod.CONTIGUOUS, WraparoundMethod.EXPAND, WraparoundMethod.SPLIT}) { + assertToGeometryEquals(env2d, method, -180, -90, -180, 90, 0, 90, 180, 90, 180, -90, 0, -90, -180, -90); + } + + env2d = new Envelope2D(HardCodedCRS.WGS84_LATITUDE_FIRST, -90, -180, 180, 360); + for (WraparoundMethod method : new WraparoundMethod[] { WraparoundMethod.NONE, WraparoundMethod.CONTIGUOUS, WraparoundMethod.EXPAND, WraparoundMethod.SPLIT}) { + assertToGeometryEquals(env2d, method, -90, -180, -90, 0, -90, 180, 90, 180, 90, 0, 90, -180, -90, -180); + } } /**