sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ama...@apache.org
Subject [sis] 08/22: feat(Feature): Add a tool to convert envelopes to geometries
Date Thu, 14 Nov 2019 11:46:42 GMT
This is an automated email from the ASF dual-hosted git repository.

amanin pushed a commit to branch refactor/sql-store
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 0d34ed83aee1f8eadf3c40ac305277ccf149855f
Author: Alexis Manin <amanin@apache.org>
AuthorDate: Mon Oct 7 18:16:03 2019 +0200

    feat(Feature): Add a tool to convert envelopes to geometries
---
 .../java/org/apache/sis/internal/feature/ESRI.java |  52 ++++---
 .../apache/sis/internal/feature/Geometries.java    | 170 +++++++++++++++------
 .../java/org/apache/sis/internal/feature/JTS.java  |  44 +++++-
 .../org/apache/sis/internal/feature/Java2D.java    | 111 +++++++++++++-
 .../sis/internal/feature/GeometriesTestCase.java   |  94 +++++++++++-
 5 files changed, 398 insertions(+), 73 deletions(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java
index 601b6b7..ce05495 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/ESRI.java
@@ -18,25 +18,14 @@ package org.apache.sis.internal.feature;
 
 import java.util.Iterator;
 
-import org.opengis.geometry.Envelope;
-
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.math.Vector;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.util.Classes;
 
-import com.esri.core.geometry.Envelope2D;
-import com.esri.core.geometry.Geometry;
-import com.esri.core.geometry.MultiPath;
-import com.esri.core.geometry.OperatorExportToWkt;
-import com.esri.core.geometry.OperatorImportFromWkt;
-import com.esri.core.geometry.Point;
-import com.esri.core.geometry.Point2D;
-import com.esri.core.geometry.Point3D;
-import com.esri.core.geometry.Polygon;
-import com.esri.core.geometry.Polyline;
-import com.esri.core.geometry.WktExportFlags;
-import com.esri.core.geometry.WktImportFlags;
+import com.esri.core.geometry.*;
+
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 
 
 /**
@@ -88,11 +77,6 @@ final class ESRI extends Geometries<Geometry> {
         return null;
     }
 
-    @Override
-    Object tryConvertToGeometry(Envelope env) {
-        throw new UnsupportedOperationException("Not supported yet"); // "Alexis Manin (Geomatys)"
on 03/10/2019
-    }
-
     /**
      * If the given point is an implementation of this library, returns its coordinate.
      * Otherwise returns {@code null}. If non-null, the returned array may have a length
of 2 or 3.
@@ -203,6 +187,36 @@ add:    for (;;) {
         return path;
     }
 
+    @Override
+    double[] getPoints(Object geometry) {
+        if (geometry instanceof GeometryWrapper) geometry = ((GeometryWrapper) geometry).geometry;
+        ensureNonNull("Geometry", geometry);
+        if (geometry instanceof MultiVertexGeometry) {
+            MultiVertexGeometry vertices = (MultiVertexGeometry) geometry;
+            final Point2D[] coords = vertices.getCoordinates2D();
+            final double[] ordinates = new double[coords.length*2];
+            int idx = 0;
+            for (Point2D coord : coords) {
+                ordinates[idx++] = coord.x;
+                ordinates[idx++] = coord.y;
+            }
+            return ordinates;
+        }
+        throw new UnsupportedOperationException("Unsupported geometry type: "+geometry.getClass().getCanonicalName());
+    }
+
+    @Override
+    Object createMultiPolygonImpl(Object... polygonsOrLinearRings) {
+        final Polygon poly = new Polygon();
+        for (final Object polr : polygonsOrLinearRings) {
+            if (polr instanceof MultiPath) {
+                poly.add((MultiPath) polr, false);
+            } else throw new UnsupportedOperationException("Unsupported geometry type: "+polr
== null? "null" : polr.getClass().getCanonicalName());
+        }
+
+        return poly;
+    }
+
     /**
      * Parses the given WKT.
      */
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 8f4fcfb..31094c9 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
@@ -22,12 +22,18 @@ import java.util.function.Function;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
 
+import org.opengis.geometry.DirectPosition;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.Geometry;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.crs.SingleCRS;
+import org.opengis.referencing.cs.CoordinateSystemAxis;
 
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.internal.referencing.AxisDirections;
 import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.math.Vector;
+import org.apache.sis.referencing.CRS;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.util.logging.Logging;
@@ -151,13 +157,19 @@ public abstract class Geometries<G> {
         return false;
     }
 
+    /**
+     * Transforms an envelope to a polygon whose start point is lower corner, and points
composing result are the
+     * envelope corners in clockwise order.
+     * @param env The envelope to convert.
+     * @param wraparound How to resolve wrap-around ambiguities on the envelope.
+     * @return If any geometric implementation is installed, return a polygon (or two polygons
in case of
+     * {@link WrapResolution#SPLIT split handling of wrap-around}.
+     */
     public static Optional<Geometry> toGeometry(final Envelope env, WrapResolution
wraparound) {
         return findStrategy(g -> g.tryConvertToGeometry(env, wraparound))
                 .map(result -> new GeometryWrapper(result, env));
     }
 
-    abstract Object tryConvertToGeometry(final Envelope env, WrapResolution wraparound);
-
     /**
      * If the given point is an implementation of this library, returns its coordinate.
      * Otherwise returns {@code null}.
@@ -343,60 +355,132 @@ public abstract class Geometries<G> {
         return Optional.empty();
     }
 
-    private Object envelope2Polygon(final Envelope env, WrapResolution resolution) {
-        double[] ordinates;
-        double[] secondEnvelopeIfSplit = null;
-        if (WrapResolution.NONE.equals(resolution)) {
-            ordinates = new double[] {
-                    env.getMinimum(0),
-                    env.getMinimum(1),
-                    env.getMaximum(0),
-                    env.getMaximum(1)
-            };
+    /**
+     * See {@link Geometries#toGeometry(Envelope, WrapResolution)}.
+     */
+    Object tryConvertToGeometry(final Envelope env, WrapResolution resolution) {
+        // Ensure that we can isolate an horizontal part in the given envelope.
+        final int x;
+        if (env.getDimension() == 2) {
+            x = 0;
         } else {
-            final boolean xWrap = env.getMinimum(0) > env.getMaximum(0);
-            final boolean yWrap = env.getMinimum(1) > env.getMaximum(1);
-
-            //TODO
-            switch (resolution) {
-                case EXPAND:
-                case SPLIT:
-                case CONTIGUOUS:
-                default: throw new IllegalArgumentException("Unknown or unset wrap resolution:
"+resolution);
+            final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
+            if (crs == null) throw new IllegalArgumentException("Envelope with more than
2 dimensions, but without CRS: cannot isolate horizontal part.");
+            final SingleCRS hCrs = CRS.getHorizontalComponent(crs);
+            if (hCrs == null) throw new IllegalArgumentException("Cannot find an horizontal
part in given CRS");
+            x = AxisDirections.indexOfColinear(crs.getCoordinateSystem(), hCrs.getCoordinateSystem());
+        }
+
+        final int y = x+1;
+
+        final DirectPosition lc = env.getLowerCorner();
+        final DirectPosition uc = env.getUpperCorner();
+        double minX = lc.getOrdinate(x);
+        double minY = lc.getOrdinate(y);
+        double maxX = uc.getOrdinate(x);
+        double maxY = uc.getOrdinate(y);
+        double[] splittedLeft = null;
+        // We start by short-circuiting simplest case for minor simplicity/performance reason.
+        if (!WrapResolution.NONE.equals(resolution)) {
+            // ensure the envelope is correctly defined, by forcing non-authorized wrapped
axes to take entire crs span.
+            final GeneralEnvelope fixedEnv = new GeneralEnvelope(env);
+            fixedEnv.normalize();
+            int wrapAxis = -1;
+            for (int i = x ; i <= y && wrapAxis < x ; i++) {
+                if (fixedEnv.getLower(i) > fixedEnv.getUpper(i)) wrapAxis = i;
+            }
+            if (wrapAxis >= x) {
+                final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem();
+                if (crs == null) throw new IllegalArgumentException("Cannot resolve wrap-around
for an envelope without any system defined");
+                final CoordinateSystemAxis axis = crs.getCoordinateSystem().getAxis(wrapAxis);
+                final double wrapRange = axis.getMaximumValue() - axis.getMinimumValue();
+                switch (resolution) {
+                    case EXPAND:
+                        // simpler and more performant than a call to GeneralEnvelope.simplify()
+                        if (wrapAxis == x) {
+                            minX = axis.getMinimumValue();
+                            maxX = axis.getMaximumValue();
+                        } else {
+                            minY = axis.getMinimumValue();
+                            maxY = axis.getMaximumValue();
+                        }
+                        break;
+                    case SPLIT:
+                        if (wrapAxis == x) {
+                            splittedLeft = new double[]{axis.getMinimumValue(), minY, maxX,
maxY};
+                            maxX = axis.getMaximumValue();
+                        }
+                        else {
+                            splittedLeft = new double[] {minX, axis.getMinimumValue(), maxX,
maxY};
+                            maxY = axis.getMaximumValue();
+                        }
+                        break;
+                    case CONTIGUOUS:
+                        if (wrapAxis == x) maxX += wrapRange;
+                        else maxY += wrapRange;
+                        break;
+                    default:
+                        throw new IllegalArgumentException("Unknown or unset wrap resolution:
" + resolution);
+                }
             }
+        }
+
+        Vector[] points = clockwiseRing(minX, minY, maxX, maxY);
 
+        final G mainRect = createPolyline(2, points);
+        if (splittedLeft != null) {
+            minX = splittedLeft[0];
+            minY = splittedLeft[1];
+            maxX = splittedLeft[2];
+            maxY = splittedLeft[3];
+            Vector[] points2 = clockwiseRing(minX, minY, maxX, maxY);
+            final G secondRect = createPolyline(2, points2);
+            return createMultiPolygonImpl(mainRect, secondRect);
         }
 
+        /* Geotk original method had an option to insert a median point on wrappped around
axis, but we have not ported
+         * it, because in an orthonormal space, I don't see any case where it could be useful.
However, in case it
+         * have to be added, we can do it here by amending created ring(s).
+         */
+        return mainRect;
+    }
 
-        double minX = ordinates[0];
-        double minY = ordinates[1];
-        double maxX = ordinates[2];
-        double maxY = ordinates[3];
-        Vector[] points = {
+    /**
+     * Create a sequence of points describing a rectangle whose start point is the lower
left one. The sequence of
+     * points describe each corner, going in clockwise order and repeating starting point
to properly close the ring.
+     *
+     * @param minX Lower coordinate of first axis.
+     * @param minY Lower coordinate of second axis.
+     * @param maxX Upper coordinate of first axis.
+     * @param maxY Upper coordinate of second axis.
+     *
+     * @return A set of 5 points describing given rectangle.
+     */
+    private static Vector[] clockwiseRing(final double minX, final double minY, final double
maxX, final double maxY) {
+        return new Vector[]{
                 Vector.create(new double[]{minX, minY}),
                 Vector.create(new double[]{minX, maxY}),
                 Vector.create(new double[]{maxX, maxY}),
                 Vector.create(new double[]{maxX, minY}),
                 Vector.create(new double[]{minX, minY})
         };
+    }
 
-        final G mainRect = createPolyline(2, points);
-        if (secondEnvelopeIfSplit != null) {
-            minX = secondEnvelopeIfSplit[0];
-            minY = secondEnvelopeIfSplit[1];
-            maxX = secondEnvelopeIfSplit[2];
-            maxY = secondEnvelopeIfSplit[3];
-            Vector[] points2 = {
-                    Vector.create(new double[]{minX, minY}),
-                    Vector.create(new double[]{minX, maxY}),
-                    Vector.create(new double[]{maxX, maxY}),
-                    Vector.create(new double[]{maxX, minY}),
-                    Vector.create(new double[]{minX, minY})
-            };
-            final G secondRect = createPolyline(2, points2);
-            // TODO: merge then send back
-        }
+    /**
+     * Extract all points from input geometry, and return them as a contiguous set of ordinates.
+     * For rings, point order (clockwise/counter-clockwise) is implementation dependant.
+     *
+     * @param geometry The geometry to extract point from.
+     */
+    public static Optional<double[]> getOrdinates(Geometry geometry) {
+        return findStrategy(g -> g.getPoints(geometry));
+    }
 
-        return mainRect;
+    abstract double[] getPoints(Object geometry);
+
+    abstract Object createMultiPolygonImpl(final Object... polygonsOrLinearRings);
+
+    public static Object createMultiPolygon(final Object... polygonsOrLinearRings) {
+        return findStrategy(g -> g.createMultiPolygonImpl(polygonsOrLinearRings));
     }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
index 034bd65..90fa183 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/JTS.java
@@ -31,6 +31,7 @@ import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
 import org.locationtech.jts.geom.MultiLineString;
 import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
@@ -113,13 +114,6 @@ final class JTS extends Geometries<Geometry> {
         return null;
     }
 
-    @Override
-    Geometry tryConvertToGeometry(org.opengis.geometry.Envelope env, final WrapResolution
resolution) {
-        final int dim = env.getDimension();
-        if (dim > 2) throw new UnsupportedOperationException("Cannot manage more than
2 dimensions, but input envelope has "+dim);
-        throw new UnsupportedOperationException("Not yet");
-    }
-
     /**
      * If the given point is an implementation of this library, returns its coordinate.
      * Otherwise returns {@code null}. If non-null, the returned array may have a length
of 2 or 3.
@@ -273,4 +267,40 @@ add:    for (;;) {
         toLineString(coordinates, lines);
         return toGeometry(lines);
     }
+
+    @Override
+    double[] getPoints(Object geometry) {
+        if (geometry instanceof GeometryWrapper) {
+            geometry = ((GeometryWrapper) geometry).geometry;
+        }
+
+        if (geometry instanceof Geometry) {
+            Geometry geom = (Geometry) geometry;
+            final int nbPts = geom.getNumPoints();
+            final Coordinate[] coords = geom.getCoordinates();
+            double[] ordinates = new double[nbPts * 2];
+            for (int i = 0, j=0; i < nbPts ; i++) {
+                final Coordinate coord = coords[i];
+                ordinates[j++] = coord.x;
+                ordinates[j++] = coord.y;
+            }
+            return ordinates;
+        }
+
+        return null;
+    }
+
+    @Override
+    Object createMultiPolygonImpl(Object... polygonsOrLinearRings) {
+        final Polygon[] polys = new Polygon[polygonsOrLinearRings.length];
+        for (int i = 0 ; i < polys.length ; i++) {
+            Object o = polygonsOrLinearRings[i];
+            if (o instanceof GeometryWrapper) o = ((GeometryWrapper) o).geometry;
+
+            if (o instanceof Polygon) polys[i] = (Polygon) o;
+            else if (o instanceof LinearRing) polys[i] = factory.createPolygon((LinearRing)
o);
+        }
+
+        return factory.createMultiPolygon(polys);
+    }
 }
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java
index f0a70f3..38f78d8 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Java2D.java
@@ -16,20 +16,30 @@
  */
 package org.apache.sis.internal.feature;
 
-import java.util.Iterator;
 import java.awt.Shape;
 import java.awt.geom.Line2D;
 import java.awt.geom.Path2D;
+import java.awt.geom.PathIterator;
 import java.awt.geom.Point2D;
 import java.awt.geom.Rectangle2D;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.function.Consumer;
+import java.util.stream.DoubleStream;
+import java.util.stream.StreamSupport;
+
 import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.internal.feature.j2d.ShapeProperties;
 import org.apache.sis.internal.referencing.j2d.ShapeUtilities;
 import org.apache.sis.math.Vector;
+import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.Numbers;
 
+import static org.apache.sis.util.ArgumentChecks.ensureNonEmpty;
+import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
+
 
 /**
  * Centralizes usages of some (not all) Java2D geometry API by Apache SIS.
@@ -219,6 +229,29 @@ add:    for (;;) {
         return ShapeUtilities.toPrimitive(path);
     }
 
+    @Override
+    double[] getPoints(Object geometry) {
+        if (geometry instanceof GeometryWrapper) geometry = ((GeometryWrapper) geometry).geometry;
+        ensureNonNull("Geometry", geometry);
+        if (geometry instanceof Point2D) return getCoordinate(geometry);
+        if (geometry instanceof Shape) {
+            final PathIterator it = ((Shape) geometry).getPathIterator(null);
+            return StreamSupport.stream(new PathSpliterator(it), false)
+                    .flatMapToDouble(Segment::ordinates)
+                    .toArray();
+        }
+
+        throw new UnsupportedOperationException("Unsupported geometry type: "+geometry.getClass().getCanonicalName());
+    }
+
+    @Override
+    Object createMultiPolygonImpl(Object... polygonsOrLinearRings) {
+        ensureNonEmpty("Polygons or linear rings to merge", polygonsOrLinearRings);
+        if (polygonsOrLinearRings.length == 1) return polygonsOrLinearRings[0];
+        final Iterator<Object> it = Arrays.asList(polygonsOrLinearRings).iterator();
+        return tryMergePolylines(it.next(), it);
+    }
+
     /**
      * If the given object is a Java2D shape, builds its WKT representation.
      * Current implementation assumes that all closed shapes are polygons and that polygons
have no hole
@@ -238,4 +271,78 @@ add:    for (;;) {
     public Object parseWKT(final String wkt) {
         throw unsupported(2);
     }
+
+    /**
+     * An abstraction over {@link PathIterator} to use it in a streaming context.
+     */
+    private static class PathSpliterator implements Spliterator<Segment> {
+
+        private final PathIterator source;
+
+        private PathSpliterator(PathIterator source) {
+            this.source = source;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super Segment> action) {
+            if (source.isDone()) return false;
+            final double[] coords = new double[6];
+            final int segmentType = source.currentSegment(coords);
+            action.accept(new Segment(segmentType, coords));
+            source.next();
+            return true;
+        }
+
+        @Override
+        public Spliterator<Segment> trySplit() {
+            return null;
+        }
+
+        @Override
+        public long estimateSize() {
+            return Long.MAX_VALUE;
+        }
+
+        @Override
+        public int characteristics() {
+            return Spliterator.NONNULL | Spliterator.ORDERED | Spliterator.IMMUTABLE;
+        }
+    }
+
+    /**
+     * Describe a path segment as described by {@link PathIterator#currentSegment(double[])
AWT path iteration API}.
+     * Basically, the awt abstraction is really poor, so we made a wrapper around it to describe
segments.
+     */
+    private static class Segment {
+        /**
+         * This segment type ({@link PathIterator#SEG_CLOSE}, etc.).
+         */
+        final int type;
+        /**
+         * Brut points composing the segment, as returned by {@link PathIterator#currentSegment(double[])}.
+         */
+        private final double[] points;
+
+        Segment(int type, double[] points) {
+            this.type = type;
+            this.points = points;
+        }
+
+        /**
+         *
+         * @return points composing this segment, as a contiguous set of ordinates. Points
are all 2D, so every two
+         * elements in backed stream describe a point. Can be empty in case of {@link PathIterator#SEG_CLOSE
closing segment}.
+         */
+        DoubleStream ordinates() {
+            switch (type) {
+                case PathIterator.SEG_CLOSE: return DoubleStream.empty();
+                case PathIterator.SEG_QUADTO: return Arrays.stream(points, 0, 4);
+                case PathIterator.SEG_CUBICTO: return Arrays.stream(points);
+                case PathIterator.SEG_LINETO:
+                case PathIterator.SEG_MOVETO:
+                        return Arrays.stream(points, 0, 2);
+                default: throw new IllegalStateException("Unknown segment type: "+type);
+            }
+        }
+    }
 }
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 cbef788..7aa7a44 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
@@ -17,15 +17,32 @@
 package org.apache.sis.internal.feature;
 
 import java.util.Arrays;
+import java.util.EnumMap;
 import java.util.Iterator;
-import org.apache.sis.math.Vector;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.stream.Stream;
+
+import org.opengis.geometry.Envelope;
+
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.math.Vector;
+import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.TestCase;
+
 import org.junit.Test;
 
 import static java.lang.Double.NaN;
-import static org.junit.Assert.*;
+import static org.apache.sis.internal.feature.WrapResolution.CONTIGUOUS;
+import static org.apache.sis.internal.feature.WrapResolution.EXPAND;
+import static org.apache.sis.internal.feature.WrapResolution.NONE;
+import static org.apache.sis.internal.feature.WrapResolution.SPLIT;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
 
 
 /**
@@ -133,6 +150,79 @@ public abstract strictfp class GeometriesTestCase extends TestCase {
         assertWktEquals("LINESTRING (4 5, 7 9, 9 3, -1 -6)", text);
     }
 
+    @Test
+    public void testEnvelopeToLinearRing() {
+        // First, ensure that behavior is constant in case wrap-around is de-activated.
+        final GeneralEnvelope noWrapNeeded = new GeneralEnvelope(new DefaultGeographicBoundingBox(-30,
24, -60, -51));
+        final double[] expectedResult = {-30, -60, -30, -51, 24, -51, 24, -60, -30, -60};
+        for (WrapResolution method : WrapResolution.values()) assertConversion(noWrapNeeded,
method, expectedResult);
+
+        // Check behavior when wrap-around is on first axis.
+        final GeneralEnvelope wrapped = new GeneralEnvelope(CommonCRS.defaultGeographic());
+        wrapped.setEnvelope(165, 32, -170, 33);
+        final EnumMap expected = new EnumMap(WrapResolution.class);
+        expected.put(NONE, new double[]{165, 32, 165, 33, -170, 33, -170, 32, 165, 32});
+        expected.put(CONTIGUOUS, new double[]{165, 32, 165, 33, 190, 33, 190, 32, 165, 32});
+        expected.put(EXPAND, new double[]{-180, 32, -180, 33, 180, 33, 180, 32, -180, 32});
+        expected.put(SPLIT, new double[]{165, 32, 165, 33, 180, 33, 180, 32, 165, 32, -180,
32, -180, 33, -170, 33, -170, 32, -180, 32});
+        assertConversion(wrapped, expected);
+
+        wrapped.setEnvelope(177, -42, 190, 2);
+        expected.put(NONE, new double[]{177, -42, 177, 2, 190, 2, 190, -42, 177, -42});
+        expected.put(CONTIGUOUS, new double[]{177, -42, 177, 2, 190, 2, 190, -42, 177, -42});
+        expected.put(EXPAND, new double[]{-180, -42, -180, 2, 180, 2, 180, -42, -180, -42});
+        expected.put(SPLIT, new double[]{177, -42, 177, 2, 180, 2, 180, -42, 177, -42, -180,
-42, -180, 2, -170, 2, -170, -42, -180, -42});
+
+        // Check wrap-around on second axis.
+        wrapped.setCoordinateReferenceSystem(CommonCRS.WGS84.geographic());
+        wrapped.setEnvelope(2, 89, 3, 19);
+        expected.put(NONE, new double[]{2, 89, 2, 19, 3, 19, 3, 89, 2, 89});
+        expected.put(CONTIGUOUS, new double[]{2, 89, 2, 199, 3, 199, 3, 89, 2, 89});
+        expected.put(EXPAND, new double[]{2, -180, 2, 180, 3, 180, 3, -180, 2, -180});
+        expected.put(SPLIT, new double[]{2, 89, 2, 180, 3, 180, 3, 89, 2, 89, 2, -180, 2,
19, 3, 19, 3, -180, 2, -180});
+
+        // Ensure fail fast on dimension ambiguity
+        wrapped.setCoordinateReferenceSystem(null);
+        Stream.of(CONTIGUOUS, EXPAND, SPLIT)
+                .forEach(method
+                        -> expectFailFast(()
+                                -> factory.tryConvertToGeometry(wrapped, method)
+                        , IllegalArgumentException.class)
+                );
+
+        final GeneralEnvelope wrapped3d = new GeneralEnvelope(3);
+        expectFailFast(() -> factory.tryConvertToGeometry(wrapped3d, NONE), IllegalArgumentException.class);
+    }
+
+    private static void expectFailFast(Callable whichMustFail, Class<? extends Exception>...
expectedErrorTypes) {
+        try {
+            final Object result = whichMustFail.call();
+            fail("Fail fast expected, but successfully returned "+result);
+        } catch (Exception e) {
+            if (expectedErrorTypes == null || expectedErrorTypes.length < 1) return; //
Any error accepted.
+            for (Class errorType : expectedErrorTypes) {
+                if (errorType.isInstance(e)) {
+                    // Expected behavior
+                    return;
+                }
+            }
+            // Unexpected error
+            fail(String.format(
+                    "A fail fast occurred, but thrown error type is unexpected.%nError type:
%s%nStack-trace: %s",
+                    e.getClass().getCanonicalName(), e
+            ));
+        }
+    }
+
+    private void assertConversion(final Envelope source, final Map<WrapResolution, double[]>
expectedResults) {
+        expectedResults.entrySet().forEach(entry -> assertConversion(source, entry.getKey(),
entry.getValue()));
+    }
+
+    private void assertConversion(final Envelope source, final WrapResolution method, final
double[] expectedOrdinates) {
+        final double[] result = factory.getPoints(factory.tryConvertToGeometry(source, method));
+        assertArrayEquals("Point list for: "+method, expectedOrdinates, result, 1e-9);
+    }
+
     /**
      * Verifies that a WKT is equal to the expected one. If the actual WKT is a multi-lines
or multi-polygons,
      * then this method may modify the expected WKT accordingly. This adjustment is done
for the ESRI case by


Mime
View raw message