sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Implementation of a java.awt.Shape backed by an array of coordinates that we can handle more easily than with java.awt.geom.Path2D. It introduce a class hierarchy similar to the OGC geometry classes (Polyline, Polygon, MultiPolygons), but it is not the intent to provide an OGC geometries implementation now. Those classes are needed for simplifying `IsolineTracer` (in a next commit).
Date Sat, 26 Dec 2020 00:01:09 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 728f70e  Implementation of a java.awt.Shape backed by an array of coordinates that
we can handle more easily than with java.awt.geom.Path2D. It introduce a class hierarchy similar
to the OGC geometry classes (Polyline, Polygon, MultiPolygons), but it is not the intent to
provide an OGC geometries implementation now. Those classes are needed for simplifying `IsolineTracer`
(in a next commit).
728f70e is described below

commit 728f70eb0183b0d8f072434c29d98d603fd7f9b3
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Dec 26 00:57:06 2020 +0100

    Implementation of a java.awt.Shape backed by an array of coordinates that we can handle
more easily than with java.awt.geom.Path2D.
    It introduce a class hierarchy similar to the OGC geometry classes (Polyline, Polygon,
MultiPolygons), but it is not the intent to
    provide an OGC geometries implementation now. Those classes are needed for simplifying
`IsolineTracer` (in a next commit).
---
 .../sis/internal/feature/AbstractGeometry.java     |  41 ++++
 .../sis/internal/feature/GeometryWrapper.java      |   2 +-
 .../apache/sis/internal/feature/j2d/FlatShape.java |  95 ++++++++
 .../sis/internal/feature/j2d/MultiPolylines.java   | 156 ++++++++++++
 .../sis/internal/feature/j2d/PathBuilder.java      | 269 +++++++++++++++++++++
 .../apache/sis/internal/feature/j2d/Polygon.java   |  41 ++++
 .../apache/sis/internal/feature/j2d/Polyline.java  | 205 ++++++++++++++++
 .../sis/internal/feature/j2d/FlatShapeTest.java    | 132 ++++++++++
 .../java/org/apache/sis/test/FeatureAssert.java    |  35 +++
 .../apache/sis/test/suite/FeatureTestSuite.java    |   1 +
 10 files changed, 976 insertions(+), 1 deletion(-)

diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AbstractGeometry.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AbstractGeometry.java
new file mode 100644
index 0000000..1ec2055
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/AbstractGeometry.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature;
+
+
+/**
+ * Base class of all geometries in Apache SIS. A geometry may be either an implementation
provided
+ * directly by Apache SIS, or a wrapper around an external library such as Java Topology
Suite (JTS)
+ * or ESRI API.
+ *
+ * <p>In current version, this class is defined solely for tracking geometries implementations
or wrappers
+ * in Apache SIS code base. {@code AbstractGeometry} API will be expanded in future versions,
in particular
+ * by implementing the {@link org.opengis.geometry.Geometry} interface. This work is pending
GeoAPI revisions
+ * (as of GeoAPI 3.0, the {@code Geometry} interface has not been updated to latest ISO standards).</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public abstract class AbstractGeometry {
+    /**
+     * Creates a new geometry.
+     */
+    protected AbstractGeometry() {
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java
index 8c0ac25..67073c4 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/GeometryWrapper.java
@@ -52,7 +52,7 @@ import org.apache.sis.util.Debug;
  * @since 0.8
  * @module
  */
-public abstract class GeometryWrapper<G> implements Geometry {
+public abstract class GeometryWrapper<G> extends AbstractGeometry implements Geometry
{
     /**
      * Creates a new geometry object.
      */
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
new file mode 100644
index 0000000..b3ed57b
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/FlatShape.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import java.awt.Rectangle;
+import java.awt.Shape;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.AffineTransform;
+import org.apache.sis.internal.feature.AbstractGeometry;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+
+
+/**
+ * A shape made of straight lines. This shape does not contain any Bézier curve.
+ * Consequently the flatness factor of path iterator can be ignored.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+abstract class FlatShape extends AbstractGeometry implements Shape {
+    /**
+     * Cached values of shape bounds.
+     *
+     * @see #getBounds2D()
+     */
+    final IntervalRectangle bounds;
+
+    /**
+     * Creates a shape with the given bounds.
+     * The given argument is stored by reference; it is not cloned.
+     *
+     * @param  bounds  the shape bounds (not cloned).
+     */
+    FlatShape(final IntervalRectangle bounds) {
+        this.bounds = bounds;
+    }
+
+    /**
+     * Returns an integer rectangle that completely encloses the shape.
+     * There is no guarantee that the rectangle is the smallest bounding box that encloses
the shape.
+     */
+    @Override
+    public final Rectangle getBounds() {
+        return bounds.getBounds();
+    }
+
+    /**
+     * Returns a rectangle that completely encloses the shape.
+     * There is no guarantee that the rectangle is the smallest bounding box that encloses
the shape.
+     */
+    @Override
+    public final Rectangle2D getBounds2D() {
+        return bounds.getBounds2D();
+    }
+
+    /**
+     * Tests if the specified point is inside the boundary of the shape.
+     * This method delegates to {@link #contains(double, double)}.
+     */
+    @Override
+    public final boolean contains(final Point2D p) {
+        return contains(p.getX(), p.getY());
+    }
+
+    /**
+     * Returns an iterator for the shape outline geometry. The flatness factor is ignored
on the assumption
+     * that this shape does not contain any Bézier curve, as stipulated in {@code FlatShape}
class contract.
+     *
+     * @param  at        an optional transform to apply on coordinate values.
+     * @param  flatness  ignored.
+     * @return an iterator for the shape outline geometry.
+     */
+    @Override
+    public final PathIterator getPathIterator(final AffineTransform at, final double flatness)
{
+        return getPathIterator(at);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
new file mode 100644
index 0000000..626ac3e
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/MultiPolylines.java
@@ -0,0 +1,156 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.awt.Shape;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.AffineTransform;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+
+
+/**
+ * Collection of polylines or polygons as a Java2D {@link Shape}.
+ * This class has some similarities with {@link java.awt.geom.Path2D}
+ * with the following differences:
+ *
+ * <ul>
+ *   <li>No synchronization.</li>
+ *   <li>Line segments only (no Bézier curves).</li>
+ * </ul>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class MultiPolylines extends FlatShape {
+    /**
+     * The polylines or polygons in this collection.
+     *
+     * @todo Store in a RTree or QuadTree with {@link Polyline#bounds} as keys.
+     *       Replace loops in {@code contains(…)} and {@code intersect(…)} methods by
use of this tree.
+     */
+    private final Polyline[] polylines;
+
+    /**
+     * Creates a collection of polylines.
+     * The given argument is stored by reference; it is not cloned.
+     *
+     * @param  polylines  the polylines. This array is not cloned.
+     */
+    public MultiPolylines(final Polyline[] polylines) {
+        super(new IntervalRectangle());
+        this.polylines = polylines;
+        bounds.setRect(polylines[0].bounds);
+        for (int i=1; i<polylines.length; i++) {
+            bounds.add(polylines[i].bounds);
+        }
+    }
+
+    /**
+     * Tests if the given coordinates are inside the boundary of this shape.
+     */
+    @Override
+    public boolean contains(final double x, final double y) {
+        if (bounds.contains(x, y)) {
+            for (final Polyline p : polylines) {
+                if (p.contains(x, y)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tests if the interior of this shape intersects the interior of the given rectangle.
+     * May conservatively return {@code true} if an intersection is probable but accurate
+     * answer would be too costly to compute.
+     */
+    @Override
+    public boolean intersects(final double x, final double y, final double w, final double
h) {
+        if (bounds.intersects(x, y, w, h)) {
+            for (final Polyline p : polylines) {
+                if (p.intersects(x, y, w, h)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tests if the interior of this shape intersects the interior of the given rectangle.
+     * May conservatively return {@code true} if an intersection is probable but accurate
+     * answer would be too costly to compute.
+     */
+    @Override
+    public boolean intersects(final Rectangle2D r) {
+        if (bounds.intersects(r)) {
+            for (final Polyline p : polylines) {
+                if (p.intersects(r)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tests if the interior of this shape entirely contains the interior of the given rectangle.
+     * May conservatively return {@code false} if an accurate answer would be too costly
to compute.
+     */
+    @Override
+    public boolean contains(final double x, final double y, final double w, final double
h) {
+        if (bounds.contains(x, y, w, h)) {
+            for (final Polyline p : polylines) {
+                if (p.contains(x, y, w, h)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tests if the interior of this shape entirely contains the interior of the given rectangle.
+     * May conservatively return {@code false} if an accurate answer would be too costly
to compute.
+     */
+    @Override
+    public boolean contains(final Rectangle2D r) {
+        if (bounds.contains(r)) {
+            for (final Polyline p : polylines) {
+                if (p.contains(r)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns an iterator over coordinates in this multi-polylines.
+     */
+    @Override
+    public PathIterator getPathIterator(final AffineTransform at) {
+        final Iterator<Polyline> it = Arrays.asList(polylines).iterator();
+        return it.hasNext() ? new Polyline.Iter(at, it.next(), it) : new Polyline.Iter();
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
new file mode 100644
index 0000000..247d8d5
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/PathBuilder.java
@@ -0,0 +1,269 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import java.util.List;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.awt.Shape;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+
+
+/**
+ * Builds a {@link Polyline}, {@link Polygon} or {@link MultiPolylines} from given coordinates.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public class PathBuilder {
+    /**
+     * Number of coordinates in a tuple.
+     */
+    private static final int DIMENSION = 2;
+
+    /**
+     * The coordinates as (x,y) tuples. The number of valid coordinates is given by {@link
#size}
+     * and this array is expanded as needed. Shall not contains {@link Double#NaN} values.
+     */
+    private double[] coordinates;
+
+    /**
+     * Number of valid coordinates. This is twice the amount of points.
+     */
+    private int size;
+
+    /**
+     * The polylines built from the coordinates.
+     */
+    private final List<Polyline> polylines;
+
+    /**
+     * Creates a new builder.
+     */
+    public PathBuilder() {
+        coordinates = new double[100];
+        polylines = new ArrayList<>();
+    }
+
+    /**
+     * Verifies that {@link #size} is even, positive and smaller than the given limit.
+     * This method is used for assertions.
+     */
+    private boolean validSize(final int limit) {
+        return size >= 0 && size <= limit && (size & 1) == 0;
+    }
+
+    /**
+     * Appends the given coordinates to current polyline, omitting repetitive points.
+     * Coordinates are added to the same polyline than the one updated by previous calls
+     * to this method, unless {@link #createPolyline(boolean)} has been invoked before.
+     * The {@link #filterChunk(double[], int, int)} method is invoked after the points have
been added
+     * for allowing subclasses to apply customized filtering in addition to the above-cited
removal
+     * of repetitive points.
+     *
+     * <h4>NaN coordinate values</h4>
+     * If the given array contains {@link Double#NaN} values, then the coordinates before
and after NaNs are stored
+     * in two distinct polylines. This is an exception to above paragraph saying that this
method does not create
+     * new polyline. The {@link #filterChunk(double[], int, int)} method will be invoked
for each of those polylines.
+     *
+     * @param  source   coordinates to copy.
+     * @param  limit    index after the last coordinate to copy. Must be an even number.
+     * @param  reverse  whether to copy (x,y) tuples in reverse order.
+     * @throws TransformException if {@link #filterFull(double[], int)} wanted to apply a
coordinate operation
+     *         and that transform failed.
+     */
+    public final void append(final double[] source, final int limit, final boolean reverse)
throws TransformException {
+        assert limit >= 0 && (limit & 1) == 0 : limit;
+        int offset = size;
+        if (limit >= coordinates.length - offset) {
+            coordinates = Arrays.copyOf(coordinates, Math.addExact(offset, Math.max(offset,
limit)));
+        }
+        final double[] coordinates = this.coordinates;
+        double px, py;              // Previous point.
+        if (offset != 0) {
+            px = coordinates[offset - 2];
+            py = coordinates[offset - 1];
+        } else {
+            px = py = Double.NaN;
+        }
+        for (int i=0; i<limit;) {
+            final double x, y;
+            if (reverse) {
+                y = source[limit - ++i];
+                x = source[limit - ++i];
+            } else {
+                x = source[i++];
+                y = source[i++];
+            }
+            if (x != px || y != py) {
+                if (Double.isNaN(x) || Double.isNaN(y)) {
+                    if (offset != 0) {
+                        size = filterChunk(coordinates, size, offset);
+                        assert validSize(offset) : size;
+                        createPolyline(false);
+                        offset = 0;
+                    }
+                } else {
+                    coordinates[offset++] = x;
+                    coordinates[offset++] = y;
+                }
+                px = x;
+                py = y;
+            }
+        }
+        size = filterChunk(coordinates, size, offset);
+        assert validSize(offset) : size;
+    }
+
+    /**
+     * Applies a custom filtering on the coordinates added by a call to {@link #append(double[],
int, boolean)}.
+     * The default implementation does nothing. Subclasses can override this method for changing
or removing some
+     * coordinate values.
+     *
+     * <p>This method is invoked at least once per {@link #append(double[], int, boolean)}
call.
+     * Consequently it is not necessarily invoked with the coordinates of a complete polyline
or polygon,
+     * because caller can build a polyline with multiple calls to {@code append(…)}.
+     * If those {@code append(…)} calls correspond to some logical chunks (at users choice),
+     * this {@code filterChunk(…)} method allows users to exploit this subdivision in their
processing.</p>
+     *
+     * @param  coordinates  the coordinates to filter. Values can be modified in-place.
+     * @param  lower        index of first coordinate to filter. Always even.
+     * @param  upper        index after the last coordinate to filter. Always even.
+     * @return number of valid coordinates after filtering.
+     *         Should be {@code upper}, unless some coordinates have been removed.
+     *         Must be an even number ≥ lower and ≤ upper.
+     */
+    protected int filterChunk(double[] coordinates, int lower, int upper) {
+        return upper;
+    }
+
+    /**
+     * Applies a custom filtering on the coordinates of a polyline or polygon.
+     * The default implementation does nothing. Subclasses can override this method for changing
or removing some
+     * coordinate values. For example a subclass could decimate points using Ramer–Douglas–Peucker
algorithm.
+     * Contrarily to {@link #filterChunk(double[], int, int)}, this method is invoked when
the coordinates of
+     * the full polyline or polygon are available. If polyline points need to be transformed
before to build
+     * the final geometry, this is the right place to do so.
+     *
+     * @param  coordinates  the coordinates to filter. Values can be modified in-place.
+     * @param  upper        index after the last coordinate to filter. Always even.
+     * @return number of valid coordinates after filtering.
+     *         Should be {@code upper}, unless some coordinates have been removed.
+     *         Must be an even number ≥ 0 and ≤ upper.
+     * @throws TransformException if this method wanted to apply a coordinate operation
+     *         and that transform failed.
+     */
+    protected int filterFull(double[] coordinates, int upper) throws TransformException {
+        return upper;
+    }
+
+    /**
+     * Creates a new polyline or polygon with the coordinates added by {@link #append(double[],
int, boolean)}.
+     * If the first point and last point have the same coordinates, then the polyline is
automatically closed as
+     * a polygon. After this method call, next calls to {@code append(…)} will add coordinates
in a new polyline.
+     *
+     * @param  close  whether to force a polygon even if source and last points are different.
+     * @throws TransformException if {@link #filterFull(double[], int)} wanted to apply a
coordinate operation
+     *         and that transform failed.
+     */
+    public final void createPolyline(boolean close) throws TransformException {
+        size = filterFull(coordinates, size);
+        assert validSize(coordinates.length) : size;
+        /*
+         * If the point would be alone, discard the lonely point because it would be invisible
+         * (a "move to" operation without "line to"). If there is two points, they should
not
+         * be equal because `append(…)` filtered repetitive points.
+         */
+        if (size >= 2*DIMENSION) {
+            double xmin = coordinates[0];
+            double ymin = coordinates[1];
+            if (xmin == coordinates[size - 2] &&
+                ymin == coordinates[size - 1])
+            {
+                size -= DIMENSION;
+                close = true;
+            }
+            double xmax = xmin;
+            double ymax = ymin;
+            for (int i=DIMENSION; i<size;) {
+                final double x = coordinates[i++];
+                final double y = coordinates[i++];
+                if (x < xmin) xmin = x;
+                if (x > xmax) xmax = x;
+                if (y < ymin) ymin = y;
+                if (y > ymax) ymax = y;
+            }
+            final IntervalRectangle bounds = new IntervalRectangle(xmin, ymin, xmax, ymax);
+            final double[] points = Arrays.copyOf(coordinates, size);
+            polylines.add(close ? new Polygon(bounds, points) : new Polyline(bounds, points));
+        }
+        size = 0;
+    }
+
+    /**
+     * Returns a shape containing all polylines or polygons added to this builder.
+     * The {@link #createPolyline(boolean)} method should be invoked before this method
+     * for making sure that there is no pending polyline.
+     *
+     * @return the polyline, polygon or collector of polylines.
+     *         May be {@code null} if no polyline or polygon has been created.
+     */
+    public final Shape build() {
+        switch (polylines.size()) {
+            case 0:  return null;
+            case 1:  return polylines.get(0);
+            default: return new MultiPolylines(polylines.toArray(new Polyline[polylines.size()]));
+        }
+    }
+
+    /**
+     * Returns a string representation of the polyline under construction for debugging purposes.
+     * Current implementation formats only the first and last points, and tells how many
points are between.
+     */
+    @Override
+    public String toString() {
+        return toString(coordinates, size);
+    }
+
+    /**
+     * Returns a string representation of the given coordinates for debugging purposes.
+     * Current implementation formats only the first and last points, and tells how many
+     * points are between.
+     *
+     * @param  coordinates  the coordinates for which to return a string representation.
+     * @param  size         index after the last valid coordinate in {@code coordinates}.
+     * @return a string representation for debugging purposes.
+     */
+    public static String toString(final double[] coordinates, final int size) {
+        final StringBuilder b = new StringBuilder(30).append('[');
+        if (size >= DIMENSION) {
+            b.append((float) coordinates[0]).append(", ").append((float) coordinates[1]);
+            final int n = size - DIMENSION;
+            if (n >= DIMENSION) {
+                b.append(", ");
+                if (size >= DIMENSION*3) {
+                    b.append(" … (").append(size / DIMENSION - 2).append(" pts) … ");
+                }
+                b.append((float) coordinates[n]).append(", ").append((float) coordinates[n+1]);
+            }
+        }
+        return b.append(']').toString();
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
new file mode 100644
index 0000000..eb5f633
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polygon.java
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+
+
+/**
+ * A polygons as a Java2D {@link java.awt.Shape}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+final class Polygon extends Polyline {
+    /**
+     * Creates a new polygon with the given coordinates.
+     * The given arguments are stored by reference; they are not cloned.
+     *
+     * @param  bounds       the polygon bounds (not cloned).
+     * @param  coordinates  the coordinate values as (x,y) tuples (not cloned).
+     */
+    Polygon(final IntervalRectangle bounds, final double[] coordinates) {
+        super(bounds, coordinates);
+    }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
new file mode 100644
index 0000000..c34cc7e
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/Polyline.java
@@ -0,0 +1,205 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import java.util.Iterator;
+import java.util.Collections;
+import java.awt.Shape;
+import java.awt.geom.Rectangle2D;
+import java.awt.geom.PathIterator;
+import java.awt.geom.AffineTransform;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+
+
+/**
+ * A polylines or polygons as a Java2D {@link Shape}. This class has some similarities
+ * with {@link java.awt.geom.Path2D} with the following differences:
+ *
+ * <ul>
+ *   <li>No synchronization.</li>
+ *   <li>Line segments only (no Bézier curves).</li>
+ *   <li>No multi-polylines (e.g. no "move to" operation in the middle).</li>
+ *   <li>Naive {@code intersect(…)} and {@code contains(…)} methods.</li>
+ * </ul>
+ *
+ * The {@code intersect(…)} and {@code contains(…)} methods may be improved in a future
version.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+class Polyline extends FlatShape {
+    /**
+     * The coordinate values as (x,y) tuples.
+     */
+    private final double[] coordinates;
+
+    /**
+     * Creates a new polylines with the given coordinates.
+     * The given arguments are stored by reference; they are not cloned.
+     * The array shall not be empty.
+     *
+     * @param  bounds       the polyline bounds (not cloned).
+     * @param  coordinates  the coordinate values as (x,y) tuples (not cloned).
+     */
+    Polyline(final IntervalRectangle bounds, final double[] coordinates) {
+        super(bounds);
+        assert coordinates.length != 0;         // Required by our PathIterator.
+        this.coordinates = coordinates;
+    }
+
+    /**
+     * Delegates operations to {@link Rectangle2D} bounds. This naive implementation
+     * is not compliant with Java2D contract. We may need to revisit in a future version.
+     */
+    @Override public final boolean contains  (Rectangle2D r)                          {return
bounds.contains  (r);}
+    @Override public final boolean intersects(Rectangle2D r)                          {return
bounds.intersects(r);}
+    @Override public final boolean contains  (double x, double y)                     {return
bounds.contains  (x,y);}
+    @Override public final boolean contains  (double x, double y, double w, double h) {return
bounds.contains  (x,y,w,h);}
+    @Override public final boolean intersects(double x, double y, double w, double h) {return
bounds.intersects(x,y,w,h);}
+
+    /**
+     * Returns an iterator over coordinates in this polyline.
+     */
+    @Override
+    public final PathIterator getPathIterator(final AffineTransform at) {
+        return new Iter(at, this, Collections.emptyIterator());
+    }
+
+    /**
+     * Iterator over polyline(s) or polygon(s) coordinates. This implementation requires
that all {@link Polyline}
+     * instances have non-empty coordinates array, otherwise {@link ArrayIndexOutOfBoundsException}
will occur.
+     */
+    static final class Iter implements PathIterator {
+        /**
+         * The transform to apply on each coordinate tuple.
+         */
+        private final AffineTransform at;
+
+        /**
+         * Next polylines on which to iterate, or an empty iterator if none.
+         */
+        private final Iterator<Polyline> polylines;
+
+        /**
+         * Coordinates to return in calls to {@link #currentSegment(double[])}.
+         */
+        private double[] coordinates;
+
+        /**
+         * Current position in {@link #coordinates} array.
+         */
+        private int position;
+
+        /**
+         * {@code true} if {@link #currentSegment(double[])} shall return {@link #SEG_CLOSE}.
+         */
+        private boolean closing;
+
+        /**
+         * Whether current coordinates make a polygon. If {@code true}, then iteration shall
+         * emit a closing {@link #SEG_CLOSE} type before to move to next polyline or polygon.
+         */
+        private boolean isPolygon;
+
+        /**
+         * Whether iteration is finished.
+         */
+        private boolean isDone;
+
+        /**
+         * Creates an empty iterator.
+         */
+        Iter() {
+            at        = null;
+            polylines = null;
+            isDone    = true;
+        }
+
+        /**
+         * Creates a new iterator.
+         *
+         * @param  at     the transform to apply on each coordinate tuple.
+         * @param  first  the first polyline or polygon.
+         * @param  next   all other polylines or polygons.
+         */
+        Iter(final AffineTransform at, final Polyline first, final Iterator<Polyline>
next) {
+            this.at     = (at != null) ? at : new AffineTransform();
+            polylines   = next;
+            coordinates = first.coordinates;
+            isPolygon   = (first instanceof Polygon);
+        }
+
+        /**
+         * Arbitrary winding rule, since enclosing class do not yet compute shape interior.
+         */
+        @Override
+        public int getWindingRule() {
+            return WIND_NON_ZERO;
+        }
+
+        /**
+         * Returns {@code true} if there is no more points to iterate.
+         */
+        @Override
+        public boolean isDone() {
+            return isDone;
+        }
+
+        /**
+         * Moves to the next point.
+         */
+        @Override
+        public void next() {
+            if ((position += 2) >= coordinates.length) {
+                if (isPolygon) {
+                    closing = !closing;
+                    if (closing) return;
+                }
+                if (polylines.hasNext()) {
+                    final Polyline next = polylines.next();
+                    isPolygon   = (next instanceof Polygon);
+                    coordinates = next.coordinates;
+                    position    = 0;
+                } else {
+                    isDone = true;
+                }
+            }
+        }
+
+        /**
+         * Returns coordinates of current line segment.
+         */
+        @Override
+        public int currentSegment(final float[] coords) {
+            if (closing) return SEG_CLOSE;
+            at.transform(coordinates, position, coords, 0, 1);
+            return (position == 0) ? SEG_MOVETO : SEG_LINETO;
+        }
+
+        /**
+         * Returns coordinates of current line segment.
+         */
+        @Override
+        public int currentSegment(final double[] coords) {
+            if (closing) return SEG_CLOSE;
+            at.transform(coordinates, position, coords, 0, 1);
+            return (position == 0) ? SEG_MOVETO : SEG_LINETO;
+        }
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
new file mode 100644
index 0000000..84f3ebc
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/j2d/FlatShapeTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.
+ */
+package org.apache.sis.internal.feature.j2d;
+
+import java.awt.geom.Path2D;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.FeatureAssert.*;
+
+
+/**
+ * Tests {@link FlatShape} subclasses and builder.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since   1.1
+ * @module
+ */
+public final strictfp class FlatShapeTest extends TestCase {
+    /**
+     * Tests {@link Polyline} using {@link Path2D} as a reference implementation.
+     */
+    @Test
+    public void testPolyline() {
+        testSingle(false);
+    }
+
+    /**
+     * Tests {@link Polygon} using {@link Path2D} as a reference implementation.
+     */
+    @Test
+    public void testPolylgon() {
+        testSingle(true);
+    }
+
+    /**
+     * Implementation of {@link #testPolyline()} and {@link #testPolylgon()}.
+     */
+    private static void testSingle(final boolean closed) {
+        final double[] coordinates = {
+            4,5, 6,3, 8,5, -2,5, 10,4
+        };
+        final IntervalRectangle bounds = new IntervalRectangle(-2,3, 19,5);
+        final Polyline p = closed ? new Polygon (bounds, coordinates)
+                                  : new Polyline(bounds, coordinates);
+
+        final Path2D.Double r = new Path2D.Double(Path2D.WIND_NON_ZERO);
+        createReferenceShape(r, coordinates, closed);
+        assertPathEquals(r.getPathIterator(null), p.getPathIterator(null), STRICT);
+    }
+
+    /**
+     * Appends to the given {@link Path2D} (taken as a reference implementation)
+     * a polyline defined by the specified coordinates.
+     *
+     * @param  r            the reference implementation where to append coordinates.
+     * @param  coordinates  the (x,y) tuples to append.
+     * @param  closed       whether to invoke {@link Path2D#closePath()} after coordinates.
+     */
+    private static void createReferenceShape(final Path2D r, final double[] coordinates,
final boolean closed) {
+        for (int i=0; i<coordinates.length;) {
+            final double x = coordinates[i++];
+            final double y = coordinates[i++];
+            if (i == 2) r.moveTo(x, y);
+            else        r.lineTo(x, y);
+        }
+        if (closed) {
+            r.closePath();
+        }
+    }
+
+    /**
+     * Tests {@link MultiPolylines}.
+     */
+    @Test
+    public void testMultiPolylines() {
+        final double[][] coordinates = {
+            {4,5, 6,3, 8,5, -2,5, 10,4},
+            {9,3, 7,5, -4,3},
+            {3,5, 6,1, -2,7, 3,8}
+        };
+        final IntervalRectangle bounds = new IntervalRectangle();           // Dummy bounds
for this test.
+        final Polyline[] polylines = new Polyline[coordinates.length];
+        final Path2D.Double r = new Path2D.Double(Path2D.WIND_NON_ZERO);
+        for (int i=0; i < polylines.length; i++) {
+            polylines[i] = new Polyline(bounds, coordinates[i]);
+            createReferenceShape(r, coordinates[i], false);
+        }
+        final MultiPolylines p = new MultiPolylines(polylines);
+        assertPathEquals(r.getPathIterator(null), p.getPathIterator(null), STRICT);
+    }
+
+    /**
+     * Tests {@link PathBuilder}.
+     *
+     * @throws TransformException never thrown in this test.
+     */
+    @Test
+    public void testPathBuilder() throws TransformException {
+        final PathBuilder b = new PathBuilder();
+        b.append(new double[] {4,5, 4,5, 6,3, 8,5, -2,5, 10,4}, 8, false);      // Ignore
last 2 points.
+        b.append(new double[] {9,3, 7,5, 7,5, 7,5, 3,8}, 10, true);             // Add points
in reverse order.
+        b.append(new double[] {-2,5}, 2, false);
+        b.createPolyline(true);
+        b.append(new double[] {3,5, 6,1, -2,7, Double.NaN, Double.NaN, Double.NaN, Double.NaN,
+                               3,8, 10,4, 6,4}, 16, false);
+        b.createPolyline(true);
+
+        final Path2D.Double r = new Path2D.Double(Path2D.WIND_NON_ZERO);
+        createReferenceShape(r, new double[]{4,5, 6,3, 8,5, 3,8, 7,5, 9,3, -2,5}, true);
+        createReferenceShape(r, new double[]{3,5, 6,1, -2,7}, false);
+        createReferenceShape(r, new double[]{3,8, 10,4, 6,4}, true);
+        assertPathEquals(r.getPathIterator(null), b.build().getPathIterator(null), STRICT);
+    }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java b/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
index 58c8d65..985799e 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/FeatureAssert.java
@@ -20,6 +20,7 @@ import java.awt.Point;
 import java.awt.Rectangle;
 import java.awt.image.Raster;
 import java.awt.image.RenderedImage;
+import java.awt.geom.PathIterator;
 import org.opengis.coverage.grid.SequenceType;
 import org.apache.sis.image.PixelIterator;
 
@@ -166,4 +167,38 @@ public strictfp class FeatureAssert extends ReferencingAssert {
                 + "— matrix indices (" + i + ", " + j + ") band " + band
                 + ": expected " + expected + " but found " + actual;
     }
+
+    /**
+     * Asserts that the path is equal to given reference.
+     *
+     * @param  expected   expected geometry outline.
+     * @param  actual     actual geometry outline.
+     * @param  tolerance  tolerance threshold for floating point value comparisons.
+     */
+    @SuppressWarnings("fallthrough")
+    public static void assertPathEquals(final PathIterator expected, final PathIterator actual,
final double tolerance) {
+        assertEquals("getWindingRule", expected.getWindingRule(), actual.getWindingRule());
+        final double[] buffer = new double[6];
+        final double[] values = new double[6];
+        while (!expected.isDone()) {
+            assertFalse("isDone", actual.isDone());
+            final int type = expected.currentSegment(buffer);
+            assertEquals("currentSegment", type, actual.currentSegment(values));
+            switch (type) {
+                case PathIterator.SEG_CUBICTO: assertEquals("x₃", buffer[4], values[4],
tolerance);
+                                               assertEquals("y₃", buffer[5], values[5],
tolerance);
+                case PathIterator.SEG_QUADTO:  assertEquals("x₂", buffer[2], values[2],
tolerance);
+                                               assertEquals("y₂", buffer[3], values[3],
tolerance);
+                case PathIterator.SEG_LINETO:  assertEquals("x₁", buffer[0], values[0],
tolerance);
+                                               assertEquals("y₁", buffer[1], values[1],
tolerance); break;
+                case PathIterator.SEG_MOVETO:  assertEquals("x₀", buffer[0], values[0],
tolerance);
+                                               assertEquals("y₀", buffer[1], values[1],
tolerance);
+                case PathIterator.SEG_CLOSE:   break;
+                default: fail("Unexpected type: " + type);
+            }
+            expected.next();
+            actual.next();
+        }
+        assertTrue("isDone", actual.isDone());
+    }
 }
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 67cea45..0dc881f 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -65,6 +65,7 @@ import org.junit.runners.Suite;
     org.apache.sis.internal.filter.sqlmm.SQLMMTest.class,
     org.apache.sis.internal.feature.AttributeConventionTest.class,
     org.apache.sis.internal.feature.j2d.ShapePropertiesTest.class,
+    org.apache.sis.internal.feature.j2d.FlatShapeTest.class,
     org.apache.sis.internal.feature.j2d.FactoryTest.class,
     org.apache.sis.internal.feature.esri.FactoryTest.class,
     org.apache.sis.internal.feature.jts.FactoryTest.class,


Mime
View raw message