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
commit 60fe0071853d1f753d5cf9f2ca3b1848454c4b4a Merge: 8796eb1 d3b3ce3 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Sun Dec 12 16:24:34 2021 +0100 Review and merge branch 'feat/toShape' into geoapi-4.0. Implementation of `JTS.fromAwt(…)` methods moved to a separated class `ShapeConverter`. Implementation of `AbstractJTSShape` and related classes where refactored. Tests added in `JTSTest` class have been moved to dedicated classes. .../sis/internal/feature/j2d/EmptyShape.java | 4 +- .../sis/internal/feature/j2d/package-info.java | 2 +- .../sis/internal/feature/jts/AbstractJTSShape.java | 167 +++++++++++ .../feature/jts/DecimateJTSPathIterator.java | 166 +++++++++++ .../sis/internal/feature/jts/DecimateJTSShape.java | 117 ++++++++ .../org/apache/sis/internal/feature/jts/JTS.java | 48 ++- .../sis/internal/feature/jts/JTSPathIterator.java | 268 +++++++++++++++++ .../apache/sis/internal/feature/jts/JTSShape.java | 51 ++++ .../feature/jts/PackedCoordinateSequence.java | 12 + .../sis/internal/feature/jts/ShapeConverter.java | 327 +++++++++++++++++++++ .../sis/internal/feature/jts/JTSShapeTest.java | 219 ++++++++++++++ .../internal/feature/jts/ShapeConverterTest.java | 200 +++++++++++++ .../apache/sis/test/suite/FeatureTestSuite.java | 2 + 13 files changed, 1579 insertions(+), 4 deletions(-) diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java index 8760712,8760712..94642c3 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/EmptyShape.java @@@ -29,7 -29,7 +29,7 @@@ import java.util.NoSuchElementException * An empty shape. * * @author Martin Desruisseaux (Geomatys) -- * @version 1.1 ++ * @version 1.2 * @since 1.1 * @module */ @@@ -37,7 -37,7 +37,7 @@@ public final class EmptyShape implement /** * The unique empty shape instance. */ -- public static final Shape INSTANCE = new EmptyShape(); ++ public static final EmptyShape INSTANCE = new EmptyShape(); /** * For {@link #INSTANCE} construction only. diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java index 2bd1358,2bd1358..d7ee3c9 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/j2d/package-info.java @@@ -25,7 -25,7 +25,7 @@@ * may change in incompatible ways in any future version without notice. * * @author Martin Desruisseaux (Geomatys) -- * @version 1.1 ++ * @version 1.2 * * @see org.apache.sis.internal.referencing.j2d * diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/AbstractJTSShape.java index 0000000,556a4d1..5d9fea7 mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/AbstractJTSShape.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/AbstractJTSShape.java @@@ -1,0 -1,251 +1,167 @@@ + /* + * 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.jts; + ++import java.awt.Shape; ++import java.awt.Rectangle; ++import java.awt.geom.Rectangle2D; ++import java.awt.geom.PathIterator; ++import java.awt.geom.AffineTransform; ++import java.awt.geom.Point2D; ++import org.apache.sis.internal.referencing.j2d.IntervalRectangle; + import org.locationtech.jts.geom.Coordinate; + import org.locationtech.jts.geom.Envelope; + import org.locationtech.jts.geom.Geometry; ++import org.locationtech.jts.geom.GeometryFactory; + import org.locationtech.jts.geom.LinearRing; -import java.awt.Rectangle; -import java.awt.Shape; -import java.awt.geom.AffineTransform; -import java.awt.geom.GeneralPath; -import java.awt.geom.PathIterator; -import java.awt.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.util.Arrays; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.sis.util.logging.Logging; -import org.opengis.referencing.operation.MathTransform; -import org.opengis.referencing.operation.TransformException; ++ + + /** - * A thin wrapper that adapts a JTS geometry to the Shape interface so that the geometry can be used - * by java2d without coordinate cloning. ++ * A thin wrapper that adapts a JTS geometry to the {@link Shape} interface so ++ * that the geometry can be used by Java 2D without copying coordinate values. ++ * This class does not cache any value; if the JTS geometry is changed, ++ * the modifications will be immediately visible in this {@code Shape}. + * - * @author Johann Sorel (Puzzle-GIS + Geomatys) - * @version 2.0 - * @since 2.0 ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 ++ * @since 1.2 + * @module + */ -abstract class AbstractJTSShape<T extends Geometry> implements Shape, Cloneable { - - static final Logger LOGGER = Logging.getLogger("org.geotoolkit.geometry"); - - /** The wrapped JTS geometry */ - protected final T geometry; - - /** An additional AffineTransform */ - protected final MathTransform transform; - - public AbstractJTSShape(final T geom) { - this(geom, null); - } - ++abstract class AbstractJTSShape implements Shape { + /** - * Creates a new GeometryJ2D object. - * - * @param geom - the wrapped geometry ++ * A lightweight JTS geometry factory using the default ++ * {@link org.locationtech.jts.geom.impl.CoordinateArraySequenceFactory}. ++ * This factory is inefficient for large geometries (it consumes more memory) ++ * but is a little bit more straightforward for geometry with few coordinates ++ * such as a point or a rectangle, because it stores the {@code Coordinate[]}. ++ * Used for {@code contains(…)} and {@code intersects(…)} implementations only. + */ - public AbstractJTSShape(final T geom, final MathTransform trs) { - this.geometry = geom; - this.transform = (trs == null) ? JTSPathIterator.IDENTITY : trs; - } ++ private static final GeometryFactory SMALL_FACTORY = new GeometryFactory(); + + /** - * @return the current wrapped geometry ++ * The wrapped JTS geometry. + */ - public T getGeometry() { - return geometry; - } - - public MathTransform getTransform() { - return transform; - } - - protected MathTransform getInverse(){ - try { - return transform.inverse(); - } catch (org.opengis.referencing.operation.NoninvertibleTransformException ex) { - Logging.getLogger("org.geotoolkit.display2d.primitive.jts").log(Level.WARNING, ex.getMessage(), ex); - return null; - } - } ++ protected final Geometry geometry; + + /** - * {@inheritDoc } ++ * Creates a new wrapper for the given JTS geometry. ++ * ++ * @param geometry the JTS geometry to wrap. + */ - @Override - public boolean contains(final Rectangle2D r) { - return contains(r.getMinX(),r.getMinY(),r.getWidth(),r.getHeight()); ++ protected AbstractJTSShape(final Geometry geometry) { ++ this.geometry = geometry; + } + + /** - * {@inheritDoc } ++ * 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 boolean contains(final Point2D p) { - final MathTransform inverse = getInverse(); - if (inverse != null) { - final double[] a = new double[]{p.getX(), p.getY()}; - safeTransform(inverse, a, a); - final Coordinate coord = new Coordinate(a[0], a[1]); - final Geometry point = geometry.getFactory().createPoint(coord); - return geometry.contains(point); - } - - //inverse transform could not be computed - //fallback on AWT geometries - return new GeneralPath(this).contains(p); ++ public Rectangle getBounds() { ++ return getBounds2D().getBounds(); + } + + /** - * {@inheritDoc } ++ * 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 boolean contains(final double x, final double y) { - return contains(new Point2D.Double(x, y)); ++ public Rectangle2D getBounds2D() { ++ final Envelope e = geometry.getEnvelopeInternal(); ++ return new IntervalRectangle(e.getMinX(), e.getMinY(), ++ e.getMaxX(), e.getMaxY()); + } + + /** - * {@inheritDoc } ++ * Tests if the specified point is inside the boundary of the shape. ++ * This method delegates to {@link #contains(double, double)}. + */ + @Override - public boolean contains(final double x, final double y, final double w, final double h) { - return intersectOrContains(x, y, w, h, false); ++ public boolean contains(final Point2D p) { ++ return contains(p.getX(), p.getY()); + } + + /** - * {@inheritDoc } ++ * Tests if the specified point is inside the boundary of the shape. + */ + @Override - public Rectangle getBounds() { - return getBounds2D().getBounds(); ++ public boolean contains(final double x, final double y) { ++ return geometry.contains(SMALL_FACTORY.createPoint(new Coordinate(x, y))); + } + + /** - * {@inheritDoc } ++ * Tests if the specified rectangle is inside the boundary of the shape. + */ + @Override - public Rectangle2D getBounds2D() { - if (geometry == null) return null; - - final Envelope env = geometry.getEnvelopeInternal(); - final double[] p1 = new double[]{env.getMinX(), env.getMinY()}; - safeTransform(transform,p1, p1); - final double[] p2 = new double[]{env.getMaxX(), env.getMaxY()}; - safeTransform(transform,p2, p2); - - final Rectangle2D rect = new Rectangle2D.Double(p1[0], p1[1], 0, 0); - rect.add(p2[0],p2[1]); - return rect; ++ public boolean contains(final Rectangle2D r) { ++ return geometry.contains(createRect(r.getMinX(), r.getMinY(), r.getMaxX(), r.getMaxY())); + } + + /** - * {@inheritDoc } ++ * Tests if the specified rectangle is inside the boundary of the shape. + */ + @Override - public PathIterator getPathIterator(final AffineTransform at, final double flatness) { - return getPathIterator(at); ++ public boolean contains(final double x, final double y, final double width, final double height) { ++ return geometry.contains(createRect(x, y, x + width, y + height)); + } + + /** - * {@inheritDoc } ++ * Tests if the specified rectangle intersects this shape. + */ + @Override + public boolean intersects(final Rectangle2D r) { - return intersects(r.getX(),r.getY(),r.getWidth(),r.getHeight()); ++ return geometry.intersects(createRect(r.getMinX(), r.getMinY(), r.getMaxX(), r.getMaxY())); + } + + /** - * {@inheritDoc } ++ * Tests if the specified rectangle intersects this shape. + */ + @Override - public boolean intersects(final double x, final double y, final double w, final double h) { - return intersectOrContains(x, y, w, h, true); ++ public boolean intersects(final double x, final double y, final double width, final double height) { ++ return geometry.intersects(createRect(x, y, x + width, y + height)); + } + + /** - * Test rectangle intersection or containment. - * - * @param x left coordinate - * @param y bottom coordinate - * @param w width - * @param h height - * @param intersect true for intersection, false for contains - * @return true ++ * Creates a JTS polygon which is a rectangle with the given coordinates. ++ * This is a temporary shape used for union and intersection tests. + */ - private boolean intersectOrContains(final double x, final double y, final double w, final double h, boolean intersect) { - final MathTransform inverse = getInverse(); - if (inverse != null) { - final double[] p1 = new double[]{x, y}; - safeTransform(inverse, p1, p1); - final double[] p2 = new double[]{x + w, y + h}; - safeTransform(inverse, p2, p2); - - final Coordinate[] coords = { - new Coordinate(p1[0], p1[1]), - new Coordinate(p1[0], p2[1]), - new Coordinate(p2[0], p2[1]), - new Coordinate(p2[0], p1[1]), - new Coordinate(p1[0], p1[1]) - }; - final LinearRing lr = geometry.getFactory().createLinearRing(coords); - final Geometry rect = geometry.getFactory().createPolygon(lr, null); - return intersect ? geometry.intersects(rect) : geometry.contains(rect); - } - - //inverse transform could not be computed - //fallback on AWT geometries - final GeneralPath path = new GeneralPath(this); - return intersect ? path.intersects(x, y, w, h) : path.contains(x, y, w, h); ++ private static Geometry createRect(final double xmin, final double ymin, final double xmax, final double ymax) { ++ final Coordinate origin = new Coordinate(xmin, ymin); ++ final LinearRing ring = SMALL_FACTORY.createLinearRing(new Coordinate[] { ++ origin, ++ new Coordinate(xmin, ymax), ++ new Coordinate(xmax, ymax), ++ new Coordinate(xmax, ymin), ++ origin ++ }); ++ return SMALL_FACTORY.createPolygon(ring); + } + ++ /** ++ * 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. ++ * ++ * @param at optional transform to apply on coordinate values. ++ * @param flatness ignored. ++ * @return an iterator for the shape outline geometry. ++ */ + @Override - public AbstractJTSShape clone() { - return null; //TODO - } - - - protected void safeTransform(MathTransform trs, double[] in, double[] out) { - try { - trs.transform(in, 0, out, 0, 1); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(),ex); - Arrays.fill(out, Double.NaN); - } - } - - protected void safeTransform(double[] in, int offset, float[] out, int outOffset, int nb) { - try { - transform.transform(in, offset, out, outOffset, nb); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(),ex); - Arrays.fill(out, outOffset, outOffset+nb*2, Float.NaN); - } - } - - protected void safeTransform(double[] in, int offset, double[] out, int outOffset, int nb) { - try { - transform.transform(in, offset, out, outOffset, nb); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(),ex); - Arrays.fill(out, outOffset, outOffset+nb*2, Double.NaN); - } ++ public PathIterator getPathIterator(final AffineTransform at, final double flatness) { ++ return getPathIterator(at); + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSPathIterator.java index 0000000,0d8c513..136c68d mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSPathIterator.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSPathIterator.java @@@ -1,0 -1,166 +1,166 @@@ + /* + * 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.jts; + -import static java.awt.geom.PathIterator.SEG_CLOSE; -import static java.awt.geom.PathIterator.SEG_LINETO; -import static java.awt.geom.PathIterator.SEG_MOVETO; -import static java.awt.geom.PathIterator.WIND_NON_ZERO; -import org.locationtech.jts.geom.CoordinateSequence; -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.LinearRing; -import org.opengis.referencing.operation.MathTransform; ++import java.awt.geom.PathIterator; ++ + + /** - * Decimating Java2D path iterators for JTS geometries. ++ * A path iterator with applies on-the-fly decimation for faster drawing. ++ * The decimation algorithm is based on a simple distance calculation on ++ * each axis (this is not a Douglas-Peucker algorithm). + * - * @author Johann Sorel (Puzzle-GIS + Geomatys) - * @version 2.0 - * @since 2.0 ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 ++ * @since 1.2 + * @module + */ -final class DecimateJTSPathIterator { - - static final class LineString extends JTSPathIterator<org.locationtech.jts.geom.LineString> { - - private final CoordinateSequence coordinates; - private final int coordinateCount; - /** - * True if the line is a ring - */ - private final boolean isClosed; - private int lastCoord; - private int currentIndex; - private boolean done; - private final double[] resolution; - private final double[] currentCoord = new double[2]; - - /** - * Create a new LineString path iterator. - */ - public LineString(final org.locationtech.jts.geom.LineString ls, final MathTransform trs, final double[] resolution) { - super(ls, trs); - coordinates = ls.getCoordinateSequence(); - coordinateCount = coordinates.size(); - isClosed = ls instanceof LinearRing; - this.resolution = resolution; - currentCoord[0] = coordinates.getX(0); - currentCoord[1] = coordinates.getY(0); - } - - @Override - public void reset() { - done = false; - currentIndex = 0; - currentCoord[0] = coordinates.getX(0); - currentCoord[1] = coordinates.getY(0); - } ++final class DecimateJTSPathIterator implements PathIterator { ++ /** ++ * The source of line segments. ++ */ ++ private final PathIterator source; ++ ++ /** ++ * The desired resolution on each axis. ++ */ ++ private final double xRes, yRes; ++ ++ /** ++ * Previous coordinates, or NaN if none. ++ */ ++ private double px, py; ++ ++ /** ++ * Creates a new iterator. ++ */ ++ DecimateJTSPathIterator(final PathIterator source, final double xRes, final double yRes) { ++ this.source = source; ++ this.xRes = xRes; ++ this.yRes = yRes; ++ px = py = Double.NaN; ++ } + - @Override - public int getWindingRule() { - return WIND_NON_ZERO; - } ++ /** ++ * Moves the iterator to the next segment. ++ */ ++ @Override ++ public void next() { ++ source.next(); ++ } + - @Override - public boolean isDone() { - return done; - } ++ /** ++ * Returns {@code true} if iteration is finished. ++ */ ++ @Override ++ public boolean isDone() { ++ return source.isDone(); ++ } + - @Override - public void next() { ++ /** ++ * Returns the winding rule for determining the interior of the path. ++ */ ++ @Override ++ public int getWindingRule() { ++ return source.getWindingRule(); ++ } + - while (true) { - if (((currentIndex == (coordinateCount - 1)) && !isClosed) - || ((currentIndex == coordinateCount) && isClosed)) { - done = true; - break; ++ /** ++ * Returns the coordinates and type of the current path segment in the iteration. ++ * This method has a fallback for quadratic and cubic curves, but this fallback ++ * is not very good. This iterator should be used for flat shapes only. ++ * ++ * @param coords an array where to store the data returned from this method. ++ * @return the path-segment type of the current path segment. ++ */ ++ @Override ++ public int currentSegment(final double[] coords) { ++ do { ++ final int type = source.currentSegment(coords); ++ switch (type) { ++ default: { ++ px = py = Double.NaN; ++ return type; + } - - currentIndex++; - double candidateX = coordinates.getX(currentIndex); - double candidateY = coordinates.getY(currentIndex); - - if (Math.abs(candidateX - currentCoord[0]) >= resolution[0] || Math.abs(candidateY - currentCoord[1]) >= resolution[1]) { - currentCoord[0] = candidateX; - currentCoord[1] = candidateY; ++ case SEG_MOVETO: { ++ px = coords[0]; ++ py = coords[1]; ++ return SEG_MOVETO; ++ } ++ case SEG_LINETO: { ++ if (include(coords[0], coords[1])) { ++ return SEG_LINETO; ++ } + break; + } - - } - } - - @Override - public int currentSegment(final double[] coords) { - if (currentIndex == 0) { - safeTransform(currentCoord, 0, coords, 0, 1); - return SEG_MOVETO; - } else if ((currentIndex == coordinateCount) && isClosed) { - return SEG_CLOSE; - } else { - safeTransform(currentCoord, 0, coords, 0, 1); - return SEG_LINETO; + } - } ++ source.next(); ++ } while (!source.isDone()); ++ coords[0] = px; ++ coords[1] = py; ++ return SEG_LINETO; ++ } + - @Override - public int currentSegment(final float[] coords) { - if (currentIndex == 0) { - safeTransform(currentCoord, 0, coords, 0, 1); - return SEG_MOVETO; - } else if ((currentIndex == coordinateCount) && isClosed) { - return SEG_CLOSE; - } else { - safeTransform(currentCoord, 0, coords, 0, 1); - return SEG_LINETO; ++ /** ++ * Returns the coordinates and type of the current path segment in the iteration. ++ * This is a copy of {@link #currentSegment(double[])} with only the type changed. ++ * ++ * @param coords an array where to store the data returned from this method. ++ * @return the path-segment type of the current path segment. ++ */ ++ @Override ++ public int currentSegment(final float[] coords) { ++ do { ++ final int type = source.currentSegment(coords); ++ switch (type) { ++ default: { ++ px = py = Double.NaN; ++ return type; ++ } ++ case SEG_MOVETO: { ++ px = coords[0]; ++ py = coords[1]; ++ return SEG_MOVETO; ++ } ++ case SEG_LINETO: { ++ if (include(coords[0], coords[1])) { ++ return SEG_LINETO; ++ } ++ break; ++ } + } - } ++ source.next(); ++ } while (!source.isDone()); ++ coords[0] = (float) px; ++ coords[1] = (float) py; ++ return SEG_LINETO; + } + - static final class GeometryCollection extends JTSPathIterator.GeometryCollection { - - private final double[] resolution; - - public GeometryCollection(final org.locationtech.jts.geom.GeometryCollection gc, final MathTransform trs, final double[] resolution) { - super(gc, trs); - this.resolution = resolution; - reset(); - } - - /** - * Returns the specific iterator for the geometry passed. - * - * @param candidate The geometry whole iterator is requested - * - */ - @Override - protected void prepareIterator(final Geometry candidate) { - if (candidate.isEmpty()) { - currentIterator = JTSPathIterator.Empty.INSTANCE; - } else if (candidate instanceof org.locationtech.jts.geom.Point) { - currentIterator = new JTSPathIterator.Point((org.locationtech.jts.geom.Point) candidate, transform); - } else if (candidate instanceof org.locationtech.jts.geom.Polygon) { - currentIterator = new JTSPathIterator.Polygon((org.locationtech.jts.geom.Polygon) candidate, transform); - } else if (candidate instanceof org.locationtech.jts.geom.LineString) { - currentIterator = new DecimateJTSPathIterator.LineString((org.locationtech.jts.geom.LineString) candidate, transform, resolution); - } else if (candidate instanceof org.locationtech.jts.geom.GeometryCollection) { - currentIterator = new DecimateJTSPathIterator.GeometryCollection((org.locationtech.jts.geom.GeometryCollection) candidate, transform, resolution); - } else { - currentIterator = JTSPathIterator.Empty.INSTANCE; - } ++ /** ++ * Returns whether the given point should be returned in a {@link #SEG_LINETO} segment. ++ */ ++ private boolean include(final double x, final double y) { ++ if (Math.abs(px - x) < xRes && Math.abs(py - y) < yRes) { ++ return false; ++ } else { ++ px = x; ++ py = y; ++ return true; + } + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSShape.java index 0000000,03f511e..92f1276 mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSShape.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/DecimateJTSShape.java @@@ -1,0 -1,73 +1,117 @@@ + /* + * 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.jts; + -import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryCollection; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.Polygon; -import java.awt.geom.AffineTransform; ++import java.awt.Rectangle; ++import java.awt.Shape; + import java.awt.geom.PathIterator; -import org.apache.sis.internal.referencing.j2d.AffineTransform2D; -import org.opengis.referencing.operation.MathTransform; ++import java.awt.geom.AffineTransform; ++import java.awt.geom.Point2D; ++import java.awt.geom.Rectangle2D; ++ + + /** - * A thin wrapper that adapts a JTS geometry to the Shape interface so that the - * geometry can be used by java2d without coordinate cloning, coordinate - * decimation apply on the fly. ++ * A shape that apply a simple decimation on-the-fly for faster drawing. ++ * Current implementation assumes that the shape is flattened. ++ * There is some tolerance for quadratic and cubic curves, ++ * but the result may not be correct. + * - * @author Johann Sorel (Puzzle-GIS + Geomatys) - * @version 2.0 - * @since 2.0 ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 ++ * @since 1.2 + * @module + */ -class DecimateJTSShape extends JTSShape { ++final class DecimateJTSShape implements Shape { ++ /** ++ * The shape to decimate. ++ */ ++ private final Shape source; + - private final double[] resolution; ++ /** ++ * The desired resolution on each axis. ++ */ ++ private final double xRes, yRes; + + /** - * Creates a new GeometryJ2D object. ++ * Creates a new wrapper which will decimate the coordinates of the given source. + * - * @param geom - the wrapped geometry ++ * @param source the shape to decimate. + */ - public DecimateJTSShape(final Geometry geom, final double[] resolution) { - super(geom); - this.resolution = resolution; ++ public DecimateJTSShape(final Shape source, final double[] resolution) { ++ this.source = source; ++ xRes = Math.abs(resolution[0]); ++ yRes = Math.abs(resolution[1]); ++ } ++ ++ /** ++ * Returns {@code true} if resolutions are strictly positive and finite numbers. ++ */ ++ final boolean isValid() { ++ return xRes > 0 && yRes > 0 && xRes < Double.MAX_VALUE && yRes < Double.MAX_VALUE; ++ } ++ ++ @Override ++ public Rectangle getBounds() { ++ return source.getBounds(); ++ } ++ ++ @Override ++ public Rectangle2D getBounds2D() { ++ return source.getBounds2D(); ++ } ++ ++ @Override ++ public boolean contains(double x, double y) { ++ return source.contains(x, y); ++ } ++ ++ @Override ++ public boolean contains(Point2D p) { ++ return source.contains(p); ++ } ++ ++ @Override ++ public boolean intersects(double x, double y, double w, double h) { ++ return source.intersects(x, y, w, h); ++ } ++ ++ @Override ++ public boolean intersects(Rectangle2D r) { ++ return source.intersects(r); ++ } ++ ++ @Override ++ public boolean contains(double x, double y, double w, double h) { ++ return source.contains(x, y, w, h); ++ } ++ ++ @Override ++ public boolean contains(Rectangle2D r) { ++ return source.contains(r); + } + + @Override + public PathIterator getPathIterator(final AffineTransform at) { - MathTransform t = (at == null) ? null : new AffineTransform2D(at); - if (iterator == null) { - if (this.geometry.isEmpty()) { - iterator = JTSPathIterator.Empty.INSTANCE; - } else if (this.geometry instanceof Point) { - iterator = new JTSPathIterator.Point((Point) geometry, t); - } else if (this.geometry instanceof Polygon) { - iterator = new JTSPathIterator.Polygon((Polygon) geometry, t); - } else if (this.geometry instanceof LineString) { - iterator = new DecimateJTSPathIterator.LineString((LineString) geometry, t, resolution); - } else if (this.geometry instanceof GeometryCollection) { - iterator = new DecimateJTSPathIterator.GeometryCollection((GeometryCollection) geometry, t, resolution); - } - } else { - iterator.setTransform(t); - } - return iterator; ++ return new DecimateJTSPathIterator(source.getPathIterator(at), xRes, yRes); ++ } ++ ++ @Override ++ public PathIterator getPathIterator(final AffineTransform at, final double flatness) { ++ return new DecimateJTSPathIterator(source.getPathIterator(at, flatness), xRes, yRes); + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java index 24abdd1,c7f609f..fbec63a --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTS.java @@@ -16,7 -16,14 +16,8 @@@ */ package org.apache.sis.internal.feature.jts; -import java.awt.Shape; -import java.awt.geom.PathIterator; -import static java.awt.geom.PathIterator.SEG_CLOSE; -import static java.awt.geom.PathIterator.SEG_LINETO; -import static java.awt.geom.PathIterator.SEG_MOVETO; -import java.util.ArrayList; -import java.util.List; import java.util.Map; ++import java.awt.Shape; import org.opengis.metadata.Identifier; import org.opengis.util.FactoryException; import org.opengis.referencing.crs.CoordinateReferenceSystem; @@@ -33,8 -40,14 +34,10 @@@ import org.apache.sis.geometry.Envelope import org.apache.sis.internal.system.Loggers; import org.apache.sis.internal.util.Constants; import org.apache.sis.referencing.IdentifiedObjects; + import org.apache.sis.util.ArgumentChecks; -import org.locationtech.jts.geom.Coordinate; 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.Point; -import org.locationtech.jts.geom.Polygon; /** @@@ -47,7 -60,7 +50,7 @@@ * * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) -- * @version 1.1 ++ * @version 1.2 * @since 1.0 * @module */ @@@ -294,4 -307,173 +297,47 @@@ public final class JTS extends Static } return geometry; } + + /** - * Create a view of the JTS geometry as a Java2D Shape. ++ * Returns a view of the given JTS geometry as a Java2D shape. + * - * @param geometry the geometry to view as a shape, not {@code null}. - * @param transform transform to apply on coordinates, or {@code null}. - * @return the Java2D shape view ++ * @param geometry the geometry to view as a shape, not {@code null}. ++ * @return the Java2D shape view. + */ - public static Shape asShape(Geometry geometry, final MathTransform transform) { ++ public static Shape asShape(final Geometry geometry) { + ArgumentChecks.ensureNonNull("geometry", geometry); - return new JTSShape(geometry, transform); ++ return new JTSShape(geometry); + } + + /** - * Create a view of the JTS geometry as a Java2D Shape applying a decimation on the fly. ++ * Returns a view of the given JTS geometry as a Java2D shape with a decimation applied on-the-fly. + * - * @param geometry the geometry to view as a shape, not {@code null}. - * @param resolution decimation resolution, or {@code null}. - * @return the Java2D shape view ++ * @param geometry the geometry to view as a shape, not {@code null}. ++ * @param resolution decimation resolution as an array of length 2, or {@code null}. ++ * @return the Java2D shape view. + */ - public static Shape asDecimatedShape(Geometry geometry, final double[] resolution) { ++ public static Shape asDecimatedShape(final Geometry geometry, final double[] resolution) { + ArgumentChecks.ensureNonNull("geometry", geometry); - return new DecimateJTSShape(geometry, resolution); - } - - /** - * Convert a Java2D Shape to JTS Geometry. - * Commodity method for {@code fromAwt(factory, shp.getPathIterator(null, flatness)); } - * - * @param factory, factory used to create the geometry, not null - * @param shp, shape to convert, not null - * @param flatness, the maximum distance that the line segments used - * to approximate the curved segments are allowed to deviate from - * any point on the original curve - * @return JTS Geometry, not null, can be empty - * @see #fromAwt(GeometryFactory, PathIterator) - */ - public static Geometry fromAwt(GeometryFactory factory, Shape shp, double flatness) { - return fromAwt(factory, shp.getPathIterator(null, flatness)); - } - - /** - * Convert a Java2D PathIterator to JTS Geometry. - * - * @param factory, factory used to create the geometry, not null - * @param ite, Java2D Path iterator, not null - * @return JTS Geometry, not null, can be empty - */ - public static Geometry fromAwt(GeometryFactory factory, PathIterator ite) { - - final List<Geometry> geoms = new ArrayList<>(); - boolean allPolygons = true; - boolean allPoints = true; - boolean allLines = true; - while (!ite.isDone()) { - final Geometry geom = nextGeometry(factory, ite); - if (geom != null) { - geoms.add(geom); - allPolygons &= geom instanceof Polygon; - allPoints &= geom instanceof Point; - allLines &= geom instanceof LineString; - } - } - - final int count = geoms.size(); - if (count == 0) { - return factory.createEmpty(2); - } else if (count == 1) { - return geoms.get(0); - } else { - if (allPoints) { - return factory.createMultiPoint(GeometryFactory.toPointArray(geoms)); - - } else if (allPolygons) { - Geometry result = geoms.get(0); - for (int i = 1; i < count; i++) { - /* - Java2D shape and JTS have fondamental differences. - Java2D fills the resulting contour based on visual winding rules. - JTS has an absolute system where outer shell and holes are clearly separated. - We would need to process the contours as Java2D to compute the resulting JTS equivalent, - but this would require a lot of work, maybe in the futur. TODO - The SymDifference operation is what behave the most like EVEN_ODD or NON_ZERO winding rules. - */ - result = result.symDifference(geoms.get(i)); - } - return result; - - } else if (allLines) { - return factory.createMultiLineString(GeometryFactory.toLineStringArray(geoms)); - } else { - return factory.createGeometryCollection(GeometryFactory.toGeometryArray(geoms)); - } ++ final Shape shape = new JTSShape(geometry); ++ if (resolution != null) { ++ final DecimateJTSShape decimated = new DecimateJTSShape(shape, resolution); ++ if (decimated.isValid()) return decimated; + } ++ return shape; + } + + /** - * Extract the next point, line or ring from iterator. ++ * Converts a Java2D shape to a JTS geometry. If the given shape is a view created by {@link #asShape(Geometry)}, ++ * then the original geometry is returned. Otherwise a new geometry is created with a copy (not a view) of the ++ * shape coordinates. ++ * ++ * @param factory factory to use for creating the geometry, or {@code null} for the default. ++ * @param shape the Java2D shape to convert. Can not be {@code null}. ++ * @param flatness the maximum distance that line segments are allowed to deviate from curves. ++ * @return JTS geometry with shape coordinates. Never null but can be empty. + */ - private static Geometry nextGeometry(GeometryFactory factory, PathIterator ite) { - final double[] vertex = new double[6]; - - List<Coordinate> coords = null; - boolean isRing = false; - - loop: - while (!ite.isDone()) { - switch (ite.currentSegment(vertex)) { - case SEG_MOVETO: - if (coords == null) { - //start of current geometry - coords = new ArrayList<>(); - coords.add(new Coordinate(vertex[0], vertex[1])); - ite.next(); - } else { - //start of next geometry - break loop; - } - break; - case SEG_LINETO: - if (coords == null) { - throw new IllegalArgumentException("Invalid path iterator, LINETO without previous MOVETO."); - } else { - coords.add(new Coordinate(vertex[0], vertex[1])); - ite.next(); - } - break; - case SEG_CLOSE: - //end of current geometry - if (coords == null) { - throw new IllegalArgumentException("Invalid path iterator, CLOSE without previous MOVETO."); - } else { - isRing = true; - if (!coords.isEmpty()) { - if (!coords.get(0).equals2D(coords.get(coords.size()-1))) { - //close operation is sometimes called after duplicating the first point. - //dont duplicate it again - coords.add(coords.get(0).copy()); - } - } - ite.next(); - break loop; - } - default : - throw new IllegalArgumentException("Invalid path iterator, must contain only flat segments."); - } - } - - if (coords == null) { - return null; - } - - final int size = coords.size(); - switch (size) { - case 0 : return null; - case 1 : return factory.createPoint(coords.get(0)); - case 2 : return factory.createLineString(new Coordinate[]{coords.get(0),coords.get(1)}); - default : - final Coordinate[] array = coords.toArray(new Coordinate[size]); - if (isRing) { - //JTS do not care about ring orientation - // https://locationtech.github.io/jts/javadoc/org/locationtech/jts/geom/Polygon.html - return factory.createPolygon(array); - } else { - return factory.createLineString(array); - } - } ++ public static Geometry fromAWT(final GeometryFactory factory, final Shape shape, final double flatness) { ++ ArgumentChecks.ensureNonNull("shape", shape); ++ return ShapeConverter.create(factory, shape, flatness); + } - } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSPathIterator.java index 0000000,90542b4..f8818ea mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSPathIterator.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSPathIterator.java @@@ -1,0 -1,636 +1,268 @@@ + /* + * 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.jts; + -import org.locationtech.jts.geom.Geometry; ++import java.util.Iterator; ++import java.util.Collection; ++import java.util.Collections; + import java.awt.geom.PathIterator; -import static java.awt.geom.PathIterator.SEG_CLOSE; -import static java.awt.geom.PathIterator.SEG_LINETO; -import static java.awt.geom.PathIterator.SEG_MOVETO; -import static java.awt.geom.PathIterator.WIND_EVEN_ODD; -import static java.awt.geom.PathIterator.WIND_NON_ZERO; -import java.util.Arrays; -import java.util.logging.Level; -import java.util.logging.Logger; -import org.apache.sis.internal.referencing.j2d.AffineTransform2D; -import org.apache.sis.util.logging.Logging; ++import java.awt.geom.AffineTransform; ++import org.apache.sis.util.Classes; ++import org.apache.sis.util.resources.Errors; + import org.locationtech.jts.geom.CoordinateSequence; -import org.locationtech.jts.geom.LinearRing; -import org.opengis.referencing.operation.MathTransform; -import org.opengis.referencing.operation.TransformException; ++import org.locationtech.jts.geom.GeometryCollection; ++import org.locationtech.jts.geom.Geometry; ++import org.locationtech.jts.geom.LineString; ++import org.locationtech.jts.geom.Polygon; ++import org.locationtech.jts.geom.Point; ++ + + /** - * Abstract Java2D path iterator for JTS Geometry. ++ * Java2D path iterator for JTS geometry. + * - * @author Johann Sorel (Puzzle-GIS + Geomatys) - * @version 2.0 - * @since 2.0 ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 ++ * @since 1.2 + * @module + */ -abstract class JTSPathIterator<T extends Geometry> implements PathIterator { - - private static final Logger LOGGER = Logging.getLogger("org.apache.sis.internal.feature.jts"); - static final AffineTransform2D IDENTITY = new AffineTransform2D(1, 0, 0, 1, 0, 0); - - protected MathTransform transform; - protected T geometry; - - protected JTSPathIterator(final MathTransform trs) { - this(null, trs); - } - - protected JTSPathIterator(final T geometry, final MathTransform trs) { - this.transform = (trs == null) ? IDENTITY : trs; - this.geometry = geometry; - } - - public void setGeometry(final T geom) { - this.geometry = geom; ++final class JTSPathIterator implements PathIterator { ++ /** ++ * The transform to apply on returned coordinate values. ++ * Never null (may be the identity transform instead). ++ */ ++ private final AffineTransform at; ++ ++ /** ++ * Provider of coordinate sequences. ++ */ ++ private final Iterator<CoordinateSequence> sequences; ++ ++ /** ++ * The sequence of coordinate tuples to return, ++ * or {@code null} if the iteration is finished. ++ */ ++ private CoordinateSequence coordinates; ++ ++ /** ++ * Number of points to return in the sequence. ++ */ ++ private int pointCount; ++ ++ /** ++ * Index of the coordinates tuple which closes the current polygon, or -1 if none. ++ */ ++ private int closingPoint; ++ ++ /** ++ * Index of current position in the sequence of coordinate tuples. ++ */ ++ private int currentIndex; ++ ++ /** ++ * Creates a new iterator which will transform coordinates using the given transform. ++ * ++ * @param geometry the geometry on which to iterator. ++ * @param at the transform to apply, or {@code null} for the identity transform. ++ */ ++ JTSPathIterator(final Geometry geometry, final AffineTransform at) { ++ this.at = (at != null) ? at : new AffineTransform(); ++ sequences = iterator(geometry); ++ nextSequence(); + } + - public void setTransform(final MathTransform trs) { - this.transform = (trs == null) ? IDENTITY : trs; - reset(); - } - - public MathTransform getTransform() { - return transform; - } - - public T getGeometry() { - return geometry; - } - - public abstract void reset(); - - protected void safeTransform(float[] in, int offset, float[] out, int outOffset, int nb) { - try { - transform.transform(in, offset, out, outOffset, nb); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(), ex); - Arrays.fill(out, outOffset, outOffset + nb * 2, Float.NaN); ++ /** ++ * Moves to the next sequence of coordinate tuples. The {@link #coordinates} sequence ++ * should be null when this method is invoked. If there is no more sequence, then the ++ * {@link #coordinates} will be left unchanged (i.e. null). ++ */ ++ private void nextSequence() { ++ while (sequences.hasNext()) { ++ coordinates = sequences.next(); ++ pointCount = coordinates.size(); ++ closingPoint = pointCount - 1; ++ if (closingPoint < 1 || !coordinates.getCoordinate(0).equals2D(coordinates.getCoordinate(closingPoint))) { ++ closingPoint = -1; // No closing point. ++ } ++ if (pointCount > 0) { ++ return; ++ } + } + } + - protected void safeTransform(double[] in, int offset, float[] out, int outOffset, int nb) { - try { - transform.transform(in, offset, out, outOffset, nb); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(), ex); - Arrays.fill(out, outOffset, outOffset + nb * 2, Float.NaN); ++ /** ++ * Moves the iterator to the next segment. ++ */ ++ @Override ++ public void next() { ++ if (++currentIndex >= pointCount) { ++ currentIndex = 0; ++ coordinates = null; ++ nextSequence(); + } + } + - protected void safeTransform(double[] in, int offset, double[] out, int outOffset, int nb) { - try { - transform.transform(in, offset, out, outOffset, nb); - } catch (TransformException ex) { - LOGGER.log(Level.WARNING, ex.getMessage(), ex); - Arrays.fill(out, outOffset, outOffset + nb * 2, Double.NaN); - } ++ /** ++ * Returns {@code true} if iteration is finished. ++ */ ++ @Override ++ public boolean isDone() { ++ return coordinates == null; + } + - static final class Empty extends JTSPathIterator<Geometry> { - - public static final Empty INSTANCE = new Empty(); - - private Empty() { - super(null, null); - } - - @Override - public int getWindingRule() { - return WIND_NON_ZERO; - } - - @Override - public boolean isDone() { - return true; - } - - @Override - public void next() { - throw new IllegalStateException(); - } - - @Override - public int currentSegment(final double[] coords) { - return 0; - } - - @Override - public int currentSegment(final float[] coords) { - return 0; - } - - @Override - public void reset() { - } ++ /** ++ * Returns the winding rule for determining the interior of the path. ++ * Current implementation returns the same rule than the one returned ++ * by {@link org.locationtech.jts.awt.ShapeCollectionPathIterator}. ++ */ ++ @Override ++ public int getWindingRule() { ++ return WIND_EVEN_ODD; + } + - static final class Point extends JTSPathIterator<org.locationtech.jts.geom.Point> { - - private boolean done; - - /** - * Create a new Point path iterator. - */ - public Point(final org.locationtech.jts.geom.Point point, final MathTransform trs) { - super(point, trs); - } - - @Override - public int getWindingRule() { - return WIND_EVEN_ODD; - } - - @Override - public void next() { - done = true; - } - - @Override - public boolean isDone() { - return done; - } - - @Override - public int currentSegment(final double[] coords) { - coords[0] = geometry.getX(); - coords[1] = geometry.getY(); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } - - @Override - public int currentSegment(final float[] coords) { - coords[0] = (float) geometry.getX(); - coords[1] = (float) geometry.getY(); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } - - @Override - public void reset() { - done = false; - } ++ /** ++ * Returns the coordinates and type of the current path segment in the iteration. ++ * ++ * @param coords an array where to store the data returned from this method. ++ * @return the path-segment type of the current path segment. ++ */ ++ @Override ++ public int currentSegment(final double[] coords) { ++ if (currentIndex == closingPoint) { ++ return SEG_CLOSE; ++ } ++ coords[0] = coordinates.getX(currentIndex); ++ coords[1] = coordinates.getY(currentIndex); ++ at.transform(coords, 0, coords, 0, 1); ++ return (currentIndex == 0) ? SEG_MOVETO : SEG_LINETO; + } + - static final class LineString extends JTSPathIterator<org.locationtech.jts.geom.LineString> { - - private CoordinateSequence coordinates; - private int coordinateCount; - /** - * True if the line is a ring - */ - private boolean isClosed; - private int currentCoord; - private boolean done; - - /** - * Create a new LineString path iterator. - */ - public LineString(final org.locationtech.jts.geom.LineString ls, final MathTransform trs) { - super(ls, trs); - setGeometry(ls); - } - - @Override - public void setGeometry(final org.locationtech.jts.geom.LineString geom) { - super.setGeometry(geom); - if (geom != null) { - coordinates = geom.getCoordinateSequence(); - coordinateCount = coordinates.size(); - isClosed = geom instanceof LinearRing; - } - reset(); - } - - @Override - public void reset() { - done = false; - currentCoord = 0; - } - - @Override - public int getWindingRule() { - return WIND_NON_ZERO; - } - - @Override - public boolean isDone() { - return done; - } - - @Override - public void next() { - if (((currentCoord == (coordinateCount - 1)) && !isClosed) - || ((currentCoord == coordinateCount) && isClosed)) { - done = true; - } else { - currentCoord++; - } - } - - @Override - public int currentSegment(final double[] coords) { - if (currentCoord == 0) { - coords[0] = coordinates.getX(0); - coords[1] = coordinates.getY(0); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else if ((currentCoord == coordinateCount) && isClosed) { - return SEG_CLOSE; - } else { - coords[0] = coordinates.getX(currentCoord); - coords[1] = coordinates.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; - } - } - - @Override - public int currentSegment(final float[] coords) { - if (currentCoord == 0) { - coords[0] = (float) coordinates.getX(0); - coords[1] = (float) coordinates.getY(0); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else if ((currentCoord == coordinateCount) && isClosed) { - return SEG_CLOSE; - } else { - coords[0] = (float) coordinates.getX(currentCoord); - coords[1] = (float) coordinates.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; - } - } ++ /** ++ * Returns the coordinates and type of the current path segment in the iteration. ++ * ++ * @param coords an array where to store the data returned from this method. ++ * @return the path-segment type of the current path segment. ++ */ ++ @Override ++ public int currentSegment(final float[] coords) { ++ if (currentIndex == closingPoint) { ++ return SEG_CLOSE; ++ } ++ coords[0] = (float) coordinates.getX(currentIndex); ++ coords[1] = (float) coordinates.getY(currentIndex); ++ at.transform(coords, 0, coords, 0, 1); ++ return (currentIndex == 0) ? SEG_MOVETO : SEG_LINETO; + } + - static final class Polygon extends JTSPathIterator<org.locationtech.jts.geom.Polygon> { - - /** - * The rings describing the polygon geometry - */ - private org.locationtech.jts.geom.LineString[] rings; - /** - * The current ring during iteration - */ - private int currentRing; - /** - * Current line coordinate - */ - private int currentCoord; - /** - * The array of coordinates that represents the line geometry - */ - private CoordinateSequence coords; - private int csSize; - /** - * True when the iteration is terminated - */ - private boolean done; - - /** - * Create a new Polygon path iterator. - */ - public Polygon(final org.locationtech.jts.geom.Polygon p, final MathTransform trs) { - super(p, trs); - setGeometry(p); - } - - @Override - public void setGeometry(final org.locationtech.jts.geom.Polygon geom) { - this.geometry = geom; - if (geom != null) { - int numInteriorRings = geom.getNumInteriorRing(); - rings = new org.locationtech.jts.geom.LineString[numInteriorRings + 1]; - rings[0] = geom.getExteriorRing(); - - for (int i = 0; i < numInteriorRings; i++) { - rings[i + 1] = geom.getInteriorRingN(i); - } - } - reset(); - } - - @Override - public void reset() { - currentRing = 0; - currentCoord = 0; - coords = rings[0].getCoordinateSequence(); - csSize = coords.size() - 1; - done = false; - } - - @Override - public int currentSegment(final double[] coords) { - // first make sure we're not at the last element, this prevents us from exceptions - // in the case where coords.size() == 0 - if (currentCoord == csSize) { - return SEG_CLOSE; - } else if (currentCoord == 0) { - coords[0] = this.coords.getX(0); - coords[1] = this.coords.getY(0); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else { - coords[0] = this.coords.getX(currentCoord); - coords[1] = this.coords.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; - } - } - - @Override - public int currentSegment(final float[] coords) { - // first make sure we're not at the last element, this prevents us from exceptions - // in the case where coords.size() == 0 - if (currentCoord == csSize) { - return SEG_CLOSE; - } else if (currentCoord == 0) { - coords[0] = (float) this.coords.getX(0); - coords[1] = (float) this.coords.getY(0); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else { - coords[0] = (float) this.coords.getX(currentCoord); - coords[1] = (float) this.coords.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; - } - } - - @Override - public int getWindingRule() { - return WIND_EVEN_ODD; - } - - @Override - public boolean isDone() { - return done; - } - - @Override - public void next() { - if (currentCoord == csSize) { - if (currentRing < (rings.length - 1)) { - currentCoord = 0; - currentRing++; - coords = rings[currentRing].getCoordinateSequence(); - csSize = coords.size() - 1; - } else { - done = true; - } - } else { - currentCoord++; - } - } ++ /** ++ * Returns an iterator over the coordinate sequences of the given geometry. ++ * ++ * @param geometry the geometry for which to get coordinate sequences. ++ * @return coordinate sequences over the given geometry. ++ */ ++ private static Iterator<CoordinateSequence> iterator(final Geometry geometry) { ++ final Collection<CoordinateSequence> sequences; ++ if (geometry instanceof LineString) { ++ sequences = Collections.singleton(((LineString) geometry).getCoordinateSequence()); ++ } else if (geometry instanceof Point) { ++ sequences = Collections.singleton(((Point) geometry).getCoordinateSequence()); ++ } else if (geometry instanceof Polygon) { ++ return new RingIterator((Polygon) geometry); ++ } else if (geometry instanceof GeometryCollection) { ++ return new GeomIterator((GeometryCollection) geometry); ++ } else { ++ throw new IllegalArgumentException(Errors.format(Errors.Keys.UnsupportedType_1, Classes.getShortClassName(geometry))); ++ } ++ return sequences.iterator(); + } + - static final class MultiLineString extends JTSPathIterator<org.locationtech.jts.geom.MultiLineString> { - - private int coordinateCount; - //global geometry state - private int nbGeom; - private int currentGeom = -1; - private boolean done; - //sub geometry state - private CoordinateSequence currentSequence; - private int currentCoord = -1; - - /** - * Create a new MultiLineString path iterator. - */ - public MultiLineString(final org.locationtech.jts.geom.MultiLineString ls, final MathTransform trs) { - super(ls, trs); - setGeometry(ls); - } - - @Override - public void setGeometry(final org.locationtech.jts.geom.MultiLineString geom) { - super.setGeometry(geom); - if (geom != null) { - nbGeom = geom.getNumGeometries(); - nextSubGeom(); - } - reset(); - } - - private void nextSubGeom() { - if (++currentGeom >= nbGeom) { - //nothing left, we are done - currentSequence = null; - currentCoord = -1; - done = true; - } else { - final org.locationtech.jts.geom.LineString subGeom = ((org.locationtech.jts.geom.LineString) geometry.getGeometryN(currentGeom)); - currentSequence = subGeom.getCoordinateSequence(); - coordinateCount = currentSequence.size(); - - if (coordinateCount == 0) { - //no point in this line, skip it - nextSubGeom(); - } else { - currentCoord = 0; - done = false; - } - } - } - - @Override - public void reset() { - currentGeom = -1; - nextSubGeom(); - } ++ /** ++ * An iterator over the coordinate sequences of a polygon. ++ * The first coordinate sequence is the exterior ring and ++ * all other sequences are interior rings. ++ */ ++ private static final class RingIterator implements Iterator<CoordinateSequence> { ++ /** The polygon for which to return rings. */ ++ private final Polygon polygon; + - @Override - public int getWindingRule() { - return WIND_NON_ZERO; - } ++ /** Index of the interior ring, or -1 for the exterior ring. */ ++ private int interior; + - @Override - public boolean isDone() { - return done; - } - - @Override - public void next() { - if (++currentCoord >= coordinateCount) { - //we go to the size, even if we don't have a coordinate at this index, - //to indicate we close the path - //no more points in this segment - nextSubGeom(); - } ++ /** Created a new iterator for the given polygon. */ ++ RingIterator(final Polygon geometry) { ++ polygon = geometry; ++ interior = -1; + } + - @Override - public int currentSegment(final double[] coords) { - if (currentCoord == 0) { - coords[0] = currentSequence.getX(currentCoord); - coords[1] = currentSequence.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else if (currentCoord == coordinateCount) { - return SEG_CLOSE; - } else { - coords[0] = currentSequence.getX(currentCoord); - coords[1] = currentSequence.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; - } ++ /** Returns {@code true} if there is more rings to return. */ ++ @Override public boolean hasNext() { ++ return interior < polygon.getNumInteriorRing(); + } + - @Override - public int currentSegment(final float[] coords) { - if (currentCoord == 0) { - coords[0] = (float) currentSequence.getX(currentCoord); - coords[1] = (float) currentSequence.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_MOVETO; - } else if (currentCoord == coordinateCount) { - return SEG_CLOSE; ++ /** Returns the coordinate sequence of the next ring. */ ++ @Override public CoordinateSequence next() { ++ final LineString current; ++ if (interior < 0) { ++ current = polygon.getExteriorRing(); + } else { - coords[0] = (float) currentSequence.getX(currentCoord); - coords[1] = (float) currentSequence.getY(currentCoord); - safeTransform(coords, 0, coords, 0, 1); - return SEG_LINETO; ++ current = polygon.getInteriorRingN(interior); + } ++ interior++; ++ return current.getCoordinateSequence(); + } + } + - static class GeometryCollection extends JTSPathIterator<org.locationtech.jts.geom.GeometryCollection> { - - protected int nbGeom = 1; - protected int currentGeom; - protected JTSPathIterator currentIterator; - protected boolean done; - - public GeometryCollection(final org.locationtech.jts.geom.GeometryCollection gc, final MathTransform trs) { - super(gc, trs); - reset(); - } - - @Override - public void reset() { - currentGeom = 0; - done = false; - nbGeom = geometry.getNumGeometries(); - if (geometry != null && nbGeom > 0) { - prepareIterator(geometry.getGeometryN(0)); - } else { - done = true; ++ /** ++ * An iterator over the coordinate sequences of a geometry collection. ++ */ ++ private static final class GeomIterator implements Iterator<CoordinateSequence> { ++ /** The collection for which to return geometries. */ ++ private final GeometryCollection collection; ++ ++ /** Index of current geometry. */ ++ private int index; ++ ++ /** Coordinate sequences of the current geometry. */ ++ private Iterator<CoordinateSequence> current; ++ ++ /** Created a new iterator for the given collection. */ ++ GeomIterator(final GeometryCollection collection) { ++ this.collection = collection; ++ while (index < collection.getNumGeometries()) { ++ current = iterator(collection.getGeometryN(index)); ++ if (current.hasNext()) break; ++ index++; + } + } + - @Override - public void setGeometry(final org.locationtech.jts.geom.GeometryCollection geom) { - super.setGeometry(geom); - if (geom == null) { - nbGeom = 0; - } else { - nbGeom = geom.getNumGeometries(); - } - } - - /** - * Returns the specific iterator for the geometry passed. - * - * @param candidate The geometry whole iterator is requested - */ - protected void prepareIterator(final Geometry candidate) { - - //try to reuse the previous iterator. - if (candidate.isEmpty()) { - if (currentIterator instanceof JTSPathIterator.Empty) { - //nothing to do - } else { - currentIterator = JTSPathIterator.Empty.INSTANCE; - } - } else if (candidate instanceof org.locationtech.jts.geom.Point) { - if (currentIterator instanceof JTSPathIterator.Point) { - currentIterator.setGeometry(candidate); - } else { - currentIterator = new JTSPathIterator.Point((org.locationtech.jts.geom.Point) candidate, transform); - } - } else if (candidate instanceof org.locationtech.jts.geom.Polygon) { - if (currentIterator instanceof JTSPathIterator.Polygon) { - currentIterator.setGeometry(candidate); - } else { - currentIterator = new JTSPathIterator.Polygon((org.locationtech.jts.geom.Polygon) candidate, transform); ++ /** Returns {@code true} if there is more sequences to return. */ ++ @Override public boolean hasNext() { ++ while (!current.hasNext()) { ++ if (++index >= collection.getNumGeometries()) { ++ return false; + } - } else if (candidate instanceof org.locationtech.jts.geom.LineString) { - if (currentIterator instanceof JTSPathIterator.LineString) { - currentIterator.setGeometry(candidate); - } else { - currentIterator = new JTSPathIterator.LineString((org.locationtech.jts.geom.LineString) candidate, transform); - } - } else if (candidate instanceof org.locationtech.jts.geom.GeometryCollection) { - if (currentIterator instanceof JTSPathIterator.GeometryCollection) { - currentIterator.setGeometry(candidate); - } else { - currentIterator = new JTSPathIterator.GeometryCollection((org.locationtech.jts.geom.GeometryCollection) candidate, transform); - } - } else { - currentIterator = JTSPathIterator.Empty.INSTANCE; - } - - } - - @Override - public void setTransform(final MathTransform trs) { - if (currentIterator != null) { - currentIterator.setTransform(trs); ++ current = iterator(collection.getGeometryN(index)); + } - super.setTransform(trs); - } - - @Override - public int currentSegment(final double[] coords) { - return currentIterator.currentSegment(coords); - } - - @Override - public int currentSegment(final float[] coords) { - return currentIterator.currentSegment(coords); - } - - @Override - public int getWindingRule() { - return WIND_NON_ZERO; - } - - @Override - public boolean isDone() { - return done; ++ return true; + } + - @Override - public void next() { - currentIterator.next(); - - if (currentIterator.isDone()) { - if (currentGeom < (nbGeom - 1)) { - currentGeom++; - prepareIterator(geometry.getGeometryN(currentGeom)); - } else { - done = true; - } - } ++ /** Returns the coordinate sequence of the next geometry. */ ++ @Override public CoordinateSequence next() { ++ return current.next(); + } + } + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSShape.java index 0000000,2026f38..f286a6e mode 000000,100644..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSShape.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/JTSShape.java @@@ -1,0 -1,96 +1,51 @@@ + /* + * 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.jts; + + import org.locationtech.jts.geom.Geometry; -import org.locationtech.jts.geom.GeometryCollection; -import org.locationtech.jts.geom.LineString; -import org.locationtech.jts.geom.MultiLineString; -import org.locationtech.jts.geom.Point; -import org.locationtech.jts.geom.Polygon; + import java.awt.geom.AffineTransform; + import java.awt.geom.PathIterator; -import org.apache.sis.internal.referencing.j2d.AffineTransform2D; -import org.apache.sis.referencing.operation.transform.MathTransforms; -import org.opengis.referencing.operation.MathTransform; ++import org.apache.sis.internal.feature.j2d.EmptyShape; ++ + + /** + * A thin wrapper that adapts a JTS geometry to the Shape interface so that the + * geometry can be used by java2d without coordinate cloning. + * - * @author Johann Sorel (Puzzle-GIS + Geomatys) - * @version 2.0 - * @since 2.0 ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @version 1.2 ++ * @since 1.2 + * @module + */ -class JTSShape extends AbstractJTSShape<Geometry> { - - protected JTSPathIterator<? extends Geometry> iterator; ++class JTSShape extends AbstractJTSShape { + + public JTSShape(final Geometry geom) { + super(geom); + } + + /** - * Creates a new GeometryJ2D object. - * - * @param geom - the wrapped geometry - */ - public JTSShape(final Geometry geom, final MathTransform trs) { - super(geom, trs); - } - - /** + * {@inheritDoc } + */ + @Override + public PathIterator getPathIterator(final AffineTransform at) { - - final MathTransform concat; - if (at == null) { - concat = transform; ++ if (geometry.isEmpty()) { ++ return EmptyShape.INSTANCE; + } else { - concat = MathTransforms.concatenate(transform, new AffineTransform2D(at)); ++ return new JTSPathIterator(geometry, at); + } - - if (iterator == null) { - if (this.geometry.isEmpty()) { - iterator = JTSPathIterator.Empty.INSTANCE; - } else if (this.geometry instanceof Point) { - iterator = new JTSPathIterator.Point((Point) geometry, concat); - } else if (this.geometry instanceof Polygon) { - iterator = new JTSPathIterator.Polygon((Polygon) geometry, concat); - } else if (this.geometry instanceof LineString) { - iterator = new JTSPathIterator.LineString((LineString) geometry, concat); - } else if (this.geometry instanceof MultiLineString) { - iterator = new JTSPathIterator.MultiLineString((MultiLineString) geometry, concat); - } else if (this.geometry instanceof GeometryCollection) { - iterator = new JTSPathIterator.GeometryCollection((GeometryCollection) geometry, concat); - } - } else { - iterator.setTransform(concat); - } - - return iterator; + } - - @Override - public AbstractJTSShape clone() { - return new JTSShape(this.geometry, this.transform); - } - + } diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/PackedCoordinateSequence.java index de6ac65,0000000..b59b621 mode 100644,000000..100644 --- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/PackedCoordinateSequence.java +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/PackedCoordinateSequence.java @@@ -1,473 -1,0 +1,485 @@@ +/* + * 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.jts; + +import java.io.Serializable; +import java.util.Arrays; +import org.apache.sis.util.ArgumentChecks; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateXY; +import org.locationtech.jts.geom.CoordinateXYM; +import org.locationtech.jts.geom.CoordinateXYZM; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequences; + + +/** + * A JTS coordinate sequence which stores coordinates in a single {@code float[]} or {@code double[]} array. + * This class serves the same purpose than {@link org.locationtech.jts.geom.impl.PackedCoordinateSequence} + * but without caching the {@code Coordinate[]} array. + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.2 + * @since 1.2 + * @module + */ +abstract class PackedCoordinateSequence implements CoordinateSequence, Serializable { + /** + * For cross-version compatibility. + */ + private static final long serialVersionUID = 6323915437380051705L; + + /** + * Number of dimensions for this coordinate sequence. + * + * @see #getDimension() + */ + protected final int dimension; + + /** + * Whether this coordinate sequence has <var>z</var> and/or <var>M</var> coordinate values. + * This is a combination of {@link #Z_MASK} and {@link #M_MASK} bit masks. + * + * @see #hasZ() + * @see #hasM() + */ + private final int hasZM; + + /** + * Bit to set to 1 in the {@link #hasZM} mask if this coordinate sequence + * has <var>z</var> and/or <var>M</var> coordinate values. + */ + private static final int Z_MASK = 1, M_MASK = 2; // Z_MASK must be 1 for bit twiddling reason. + + /** + * Creates a new sequence initialized to a copy of the given sequence. + * This is for constructors implementing the {@link #copy()} method. + */ + PackedCoordinateSequence(final PackedCoordinateSequence original) { + dimension = original.dimension; + hasZM = original.hasZM; + } + + /** + * Creates a new coordinate sequence for the given number of dimensions. + * + * @param dimension number of dimensions, including the number of measures. + * @param measures number of <var>M</var> coordinates. + */ + PackedCoordinateSequence(final int dimension, final int measures) { + ArgumentChecks.ensurePositive("measures", measures); + ArgumentChecks.ensureBetween("dimension", Factory.BIDIMENSIONAL + measures, + Math.addExact(Factory.TRIDIMENSIONAL, measures), dimension); + this.dimension = dimension; + int hasZM = (measures == 0) ? 0 : M_MASK; + if ((dimension - measures) >= Factory.TRIDIMENSIONAL) { + hasZM |= Z_MASK; + } + this.hasZM = hasZM; + } + + /** + * Returns the number of spatial dimensions, + * which is {@value Factory#BIDIMENSIONAL} or {@value Factory#TRIDIMENSIONAL}. + */ + private static int getSpatialDimension(final int hasZM) { + return Factory.BIDIMENSIONAL | (hasZM & Z_MASK); + } + + /** + * Returns the number of dimensions for all coordinates in this sequence, + * including {@linkplain #getMeasures() measures}. + */ + @Override + public final int getDimension() { + return dimension; + } + + /** + * Returns the number of <var>M</var> coordinates. + */ + @Override + public final int getMeasures() { + return dimension - getSpatialDimension(hasZM); + } + + /** + * Returns whether this coordinate sequence has <var>z</var> coordinate values. + */ + @Override + public final boolean hasZ() { + return (hasZM & Z_MASK) != 0; + } + + /** + * Returns whether this coordinate sequence has <var>M</var> coordinate values. + */ + @Override + public final boolean hasM() { + return (hasZM & M_MASK) != 0; + } + + /** + * Returns the <var>x</var> coordinate value for the tuple at the given index. + */ + @Override + public final double getX(final int index) { + return coordinate(index * dimension + X); + } + + /** + * Returns the <var>y</var> coordinate value for the tuple at the given index. + */ + @Override + public final double getY(final int index) { + return coordinate(index * dimension + Y); + } + + /** + * Returns the <var>z</var> coordinate value for the tuple at the given index, + * or {@link java.lang.Double.NaN} if this sequence has no <var>z</var> coordinates. + */ + @Override + public final double getZ(final int index) { + return (hasZM & Z_MASK) != 0 ? coordinate(index * dimension + Z) : java.lang.Double.NaN; + } + + /** + * Returns the first <var>M</var> coordinate value for the tuple at the given index, + * or {@link java.lang.Double.NaN} if this sequence has no <var>M</var> coordinates. + */ + @Override + public final double getM(final int index) { + switch (hasZM) { + default: return java.lang.Double.NaN; + case M_MASK: return coordinate(index * dimension + Z); + case M_MASK | Z_MASK: return coordinate(index * dimension + M); + } + } + + /** + * Returns the coordinate tuple at the given index. + * + * @param index index of the coordinate tuple. + * @return coordinate tuple at the given index. + * @throws ArrayIndexOutOfBoundsException if the given index is out of bounds. + */ + @Override + public final Coordinate getCoordinate(int index) { + index *= dimension; + final double x = coordinate( index); + final double y = coordinate(++index); + switch (hasZM) { + default: return new Coordinate (x,y); + case 0: return new CoordinateXY (x,y); + case Z_MASK: return new Coordinate (x,y, coordinate(++index)); + case M_MASK: return new CoordinateXYM (x,y, coordinate(++index)); + case Z_MASK | M_MASK: return new CoordinateXYZM(x,y, coordinate(++index), coordinate(++index)); + } + } + + /** + * Copies the coordinate tuple at the given index into the specified target. + * + * @param index index of the coordinate tuple. + * @param dest where to copy the coordinates. + * @throws ArrayIndexOutOfBoundsException if the given index is out of bounds. + */ + @Override + @SuppressWarnings("fallthrough") + public final void getCoordinate(int index, final Coordinate dest) { + index *= dimension; + dest.x = coordinate( index); + dest.y = coordinate(++index); + switch (hasZM) { + case Z_MASK: dest.setZ(coordinate(++index)); break; + case Z_MASK | M_MASK: dest.setZ(coordinate(++index)); // Fall through + case M_MASK: dest.setM(coordinate(++index)); break; + } + } + + /** + * Returns the coordinate tuple at given index. + * + * @param index index of the coordinate tuple. + * @return coordinate tuple at the given index. + * @throws ArrayIndexOutOfBoundsException if the given index is out of bounds. + */ + @Override + public final Coordinate getCoordinateCopy(int index) { + return getCoordinate(index); + } + + /** + * Returns a coordinate value from the coordinate tuple at the given index. + * For performance reasons, this method does not check {@code dim} validity. + * + * @param index index of the coordinate tuple. + * @param dim index of the coordinate value in the tuple. + * @return value of the specified value in the coordinate tuple. + */ + @Override + public final double getOrdinate(final int index, final int dim) { + return coordinate(index * dimension + dim); + } + + /** + * Returns the coordinate value at the given index in the packed array. + * + * @param index index in the packed array. + * @return coordinate value at the given index. + */ + abstract double coordinate(int index); + + /** + * Sets all coordinates in this sequence. The length of the given array + * shall be equal to {@link #size()} (this is not verified). + */ + abstract void setCoordinates(Coordinate[] values); + + /** + * Sets all coordinates in this sequence. The size of the given sequence + * shall be equal to {@link #size()} (this is not verified). + */ + void setCoordinates(CoordinateSequence values) { + setCoordinates(values.toCoordinateArray()); + } + + /** + * Coordinate sequence storing values in a packed {@code double[]} array. + */ + static final class Double extends PackedCoordinateSequence { + /** For cross-version compatibility. */ + private static final long serialVersionUID = 1940132733783453171L; + + /** The packed coordinates. */ + private final double[] coordinates; + + /** Creates a new sequence initialized to a copy of the given sequence. */ + private Double(final Double original) { + super(original); + coordinates = original.coordinates.clone(); + } + ++ /** Creates a new coordinate sequence with given values. */ ++ Double(final double[] array, final int length) { ++ super(Factory.BIDIMENSIONAL, 0); ++ coordinates = Arrays.copyOf(array, length); ++ } ++ + /** Creates a new coordinate sequence for the given number of tuples. */ + Double(final int size, final int dimension, final int measures) { + super(dimension, measures); + coordinates = new double[Math.multiplyExact(size, dimension)]; + } + + /** Returns the number of coordinate tuples in this sequence. */ + @Override public int size() { + return coordinates.length / dimension; + } + + /** Returns the coordinate value at the given index in the packed array. */ + @Override double coordinate(int index) { + return coordinates[index]; + } + + /** Sets a coordinate value for the coordinate tuple at the given index. */ + @Override public void setOrdinate(int index, int dim, double value) { + coordinates[index * dimension + dim] = value; + } + + /** Sets all coordinates in this sequence. */ + @Override void setCoordinates(final Coordinate[] values) { + int t = 0; + for (final Coordinate c : values) { + for (int i=0; i<dimension; i++) { + coordinates[t++] = c.getOrdinate(i); + } + } + assert t == coordinates.length; + } + + /** Sets all coordinates in this sequence. */ + @Override void setCoordinates(final CoordinateSequence values) { + if (values instanceof org.locationtech.jts.geom.impl.PackedCoordinateSequence.Double) { + System.arraycopy(((org.locationtech.jts.geom.impl.PackedCoordinateSequence.Double) values).getRawCoordinates(), 0, coordinates, 0, coordinates.length); + } else { + super.setCoordinates(values); + } + } + + /** Expands the given envelope to include the (x,y) coordinates of this sequence. */ + @Override public Envelope expandEnvelope(final Envelope envelope) { + for (int i=0; i < coordinates.length; i += dimension) { + envelope.expandToInclude(coordinates[i], coordinates[i+1]); + } + return envelope; + } + + /** Returns a copy of this sequence. */ + @Override public CoordinateSequence copy() { + return new Double(this); + } + + /** Returns a hash code value for this sequence. */ + @Override public int hashCode() { + return Arrays.hashCode(coordinates) + super.hashCode(); + } + + /** Compares the given object with this sequence for equality. */ + @Override public boolean equals(final Object obj) { + return super.equals(obj) && Arrays.equals(((Double) obj).coordinates, coordinates); + } + } + + /** + * Coordinate sequence storing values in a packed {@code float[]} array. + */ + static final class Float extends PackedCoordinateSequence { + /** For cross-version compatibility. */ + private static final long serialVersionUID = 2625498691139718968L; + + /** The packed coordinates. */ + private final float[] coordinates; + + /** Creates a new sequence initialized to a copy of the given sequence. */ + private Float(final Float original) { + super(original); + coordinates = original.coordinates.clone(); + } + ++ /** Creates a new coordinate sequence with given values. */ ++ Float(final float[] array, final int length) { ++ super(Factory.BIDIMENSIONAL, 0); ++ coordinates = Arrays.copyOf(array, length); ++ } ++ + /** Creates a new coordinate sequence for the given number of tuples. */ + Float(final int size, final int dimension, final int measures) { + super(dimension, measures); + coordinates = new float[Math.multiplyExact(size, dimension)]; + } + + /** Returns the number of coordinate tuples in this sequence. */ + @Override public int size() { + return coordinates.length / dimension; + } + + /** Returns the coordinate value at the given index in the packed array. */ + @Override double coordinate(int index) { + return coordinates[index]; + } + + /** Sets a coordinate value for the coordinate tuple at the given index. */ + @Override public void setOrdinate(int index, int dim, double value) { + coordinates[index * dimension + dim] = (float) value; + } + + /** Sets all coordinates in this sequence. */ + @Override void setCoordinates(final Coordinate[] values) { + int t = 0; + for (final Coordinate c : values) { + for (int i=0; i<dimension; i++) { + coordinates[t++] = (float) c.getOrdinate(i); + } + } + assert t == coordinates.length; + } + + /** Sets all coordinates in this sequence. */ + @Override void setCoordinates(final CoordinateSequence values) { + if (values instanceof org.locationtech.jts.geom.impl.PackedCoordinateSequence.Float) { + System.arraycopy(((org.locationtech.jts.geom.impl.PackedCoordinateSequence.Float) values).getRawCoordinates(), 0, coordinates, 0, coordinates.length); + } else { + super.setCoordinates(values); + } + } + + /** Expands the given envelope to include the (x,y) coordinates of this sequence. */ + @Override public Envelope expandEnvelope(final Envelope envelope) { + for (int i=0; i < coordinates.length; i += dimension) { + envelope.expandToInclude(coordinates[i], coordinates[i+1]); + } + return envelope; + } + + /** Returns a copy of this sequence. */ + @Override public CoordinateSequence copy() { + return new Float(this); + } + + /** Returns a hash code value for this sequence. */ + @Override public int hashCode() { + return Arrays.hashCode(coordinates) + super.hashCode(); + } + + /** Compares the given object with this sequence for equality. */ + @Override public boolean equals(final Object obj) { + return super.equals(obj) && Arrays.equals(((Float) obj).coordinates, coordinates); + } + } + + /** + * Returns a copy of all coordinates in this sequence. + */ + @Override + public final Coordinate[] toCoordinateArray() { + final Coordinate[] coordinates = new Coordinate[size()]; + for (int i=0; i < coordinates.length; i++) { + coordinates[i] = getCoordinate(i); + } + return coordinates; + } + + /** + * Returns a string representation of this coordinate sequence. + */ + public final String toString() { + return CoordinateSequences.toString(this); + } + + /** + * Returns a hash code value for this sequence. + */ + @Override + public int hashCode() { + return (37 * dimension) ^ hasZM; + } + + /** + * Compares the given object with this sequence for equality. + */ + @Override + public boolean equals(final Object obj) { + if (obj != null && obj.getClass() == getClass()) { + final PackedCoordinateSequence other = (PackedCoordinateSequence) obj; + return other.dimension == dimension && other.hasZM == hasZM; + } + return false; + } + + /** + * Returns a copy of this sequence. + * + * @deprecated Inherits the deprecation status from JTS. + */ + @Deprecated + public final Object clone() { + return copy(); + } +} diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/ShapeConverter.java index 0000000,0000000..e48f754 new file mode 100644 --- /dev/null +++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/jts/ShapeConverter.java @@@ -1,0 -1,0 +1,327 @@@ ++/* ++ * 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.jts; ++ ++import java.awt.Shape; ++import java.util.List; ++import java.util.Arrays; ++import java.util.ArrayList; ++import java.awt.geom.PathIterator; ++import java.awt.geom.IllegalPathStateException; ++import org.apache.sis.internal.jdk9.JDK9; ++import org.apache.sis.internal.referencing.j2d.ShapeUtilities; ++import org.locationtech.jts.geom.Geometry; ++import org.locationtech.jts.geom.GeometryFactory; ++ ++ ++/** ++ * Converts a Java2D {@link Shape} to a JTS {@link Geometry}. ++ * Two subclasses exist depending on whether the geometries will store ++ * coordinates as {@code float} or {@code double} floating point numbers. ++ * ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @author Martin Desruisseaux (Geomatys) ++ * @version 1.2 ++ * @since 1.2 ++ * @module ++ */ ++abstract class ShapeConverter { ++ /** ++ * Number of dimensions of geometries built by this class. ++ */ ++ private static final int DIMENSION = Factory.BIDIMENSIONAL; ++ ++ /** ++ * Initial number of coordinate values that the buffer can hold. ++ * The buffer capacity will be expanded as needed. ++ */ ++ private static final int INITIAL_CAPACITY = 64; ++ ++ /** ++ * Bit mask of the kind of geometric objects created. ++ * Used for detecting if all objects are of the same type. ++ * ++ * @see #geometryType ++ */ ++ private static final int POINT = 1, LINESTRING = 2, POLYGON = 4; ++ ++ /** ++ * All geometries that are component of a multi-geometries. ++ * The above masks tell if the geometry can be built as a multi-line strings or multi-points. ++ */ ++ private final List<Geometry> geometries; ++ ++ /** ++ * The JTS factory for creating geometry. May be user-specified. ++ * Note that the {@link org.locationtech.jts.geom.CoordinateSequenceFactory} is ignored; ++ */ ++ private final GeometryFactory factory; ++ ++ /** ++ * Iterator over the coordinates of the Java2D shape to convert to a JTS geometry. ++ */ ++ protected final PathIterator iterator; ++ ++ /** ++ * Number of values in the {@code float[]} or {@code double[]} array stored by sub-class. ++ */ ++ protected int length; ++ ++ /** ++ * Bitmask combination of the type of all geometries built. ++ * This is a combination of {@link #POINT}, {@link #LINESTRING} and/or {@link #POLYGON}. ++ */ ++ private int geometryType; ++ ++ /** ++ * Creates a new converter from Java2D shape to JTS geometry. ++ * ++ * @param factory the JTS factory for creating geometry, or {@code null} for automatic. ++ * @param iterator iterator over the coordinates of the Java2D shape to convert to a JTS geometry. ++ * @param isFloat whether to store coordinates as {@code float} instead of {@code double}. ++ */ ++ ShapeConverter(final GeometryFactory factory, final PathIterator iterator, final boolean isFloat) { ++ this.iterator = iterator; ++ this.geometries = new ArrayList<>(); ++ this.factory = (factory != null) ? factory : Factory.INSTANCE.factory(isFloat); ++ } ++ ++ /** ++ * Converts a Java2D Shape to a JTS geometry. ++ * Coordinates are copies; this is not a view. ++ * ++ * @param factory factory to use for creating the geometry, or {@code null} for the default. ++ * @param shape the Java2D shape to convert. Can not be {@code null}. ++ * @param flatness the maximum distance that line segments are allowed to deviate from curves. ++ * @return JTS geometry with shape coordinates. Never null but can be empty. ++ */ ++ static Geometry create(final GeometryFactory factory, final Shape shape, final double flatness) { ++ if (shape instanceof JTSShape) { ++ return ((JTSShape) shape).geometry; ++ } ++ final PathIterator iterator = shape.getPathIterator(null, flatness); ++ final ShapeConverter converter; ++ if (ShapeUtilities.isFloat(shape)) { ++ converter = new ShapeConverter.Float(factory, iterator); ++ } else { ++ converter = new ShapeConverter.Double(factory, iterator); ++ } ++ return converter.build(); ++ } ++ ++ /** ++ * A converter of Java2D {@link Shape} to a JTS {@link Geometry} ++ * storing coordinates as {@code double} values. ++ */ ++ private static final class Double extends ShapeConverter { ++ /** A temporary array for the transfer of coordinate values. */ ++ private final double[] vertex; ++ ++ /** Coordinate of current geometry. The number of valid values is {@link #length}. */ ++ private double[] buffer; ++ ++ /** Creates a new converter for the given path iterator. */ ++ Double(final GeometryFactory factory, final PathIterator iterator) { ++ super(factory, iterator, false); ++ vertex = new double[6]; ++ buffer = new double[INITIAL_CAPACITY]; ++ } ++ ++ /** Delegates to {@link PathIterator#currentSegment(double[])}. */ ++ @Override int currentSegment() { ++ return iterator.currentSegment(vertex); ++ } ++ ++ /** Stores the single point obtained by the last call to {@link #currentSegment()}. */ ++ @Override void addPoint() { ++ addPoint(vertex); ++ } ++ ++ /** Implementation of {@link #addPoint()} shared with {@link #toSequence(boolean)}. */ ++ private void addPoint(final double[] source) { ++ if (length >= buffer.length) { ++ buffer = Arrays.copyOf(buffer, length * 2); ++ } ++ System.arraycopy(source, 0, buffer, length, DIMENSION); ++ length += DIMENSION; ++ } ++ ++ /** Returns a copy of current coordinate values as a JTS coordinate sequence. */ ++ @Override PackedCoordinateSequence toSequence(final boolean close) { ++ if (close && !JDK9.equals(buffer, 0, 2, buffer, length - 2, length)) { ++ addPoint(buffer); ++ } ++ return new PackedCoordinateSequence.Double(buffer, length); ++ } ++ } ++ ++ /** ++ * A converter of Java2D {@link Shape} to a JTS {@link Geometry} ++ * storing coordinates as {@code float} values. ++ */ ++ private static final class Float extends ShapeConverter { ++ /** A temporary array for the transfer of coordinate values. */ ++ private final float[] vertex; ++ ++ /** Coordinate of current geometry. The number of valid values is {@link #length}. */ ++ private float[] buffer; ++ ++ /** Creates a new converter for the given path iterator. */ ++ Float(final GeometryFactory factory, final PathIterator iterator) { ++ super(factory, iterator, false); ++ vertex = new float[6]; ++ buffer = new float[INITIAL_CAPACITY]; ++ } ++ ++ /** Delegates to {@link PathIterator#currentSegment(float[])}. */ ++ @Override int currentSegment() { ++ return iterator.currentSegment(vertex); ++ } ++ ++ /** Stores the single point obtained by the last call to {@link #currentSegment()}. */ ++ @Override void addPoint() { ++ addPoint(vertex); ++ } ++ ++ /** Implementation of {@link #addPoint()} shared with {@link #toSequence(boolean)}. */ ++ private void addPoint(final float[] source) { ++ if (length >= buffer.length) { ++ buffer = Arrays.copyOf(buffer, length * 2); ++ } ++ System.arraycopy(source, 0, buffer, length, DIMENSION); ++ length += DIMENSION; ++ } ++ ++ /** Returns a copy of current coordinate values as a JTS coordinate sequence. */ ++ @Override PackedCoordinateSequence toSequence(final boolean close) { ++ if (close && !JDK9.equals(buffer, 0, 2, buffer, length - 2, length)) { ++ addPoint(buffer); ++ } ++ return new PackedCoordinateSequence.Float(buffer, length); ++ } ++ } ++ ++ /** ++ * Returns the coordinates and type of the current path segment in the iteration. ++ * This method delegate to one of the two {@code PathIterator.currentSegment(…)} ++ * methods, depending on the precision of floating-point values. ++ */ ++ abstract int currentSegment(); ++ ++ /** ++ * Stores the single point obtained by the last call to {@link #currentSegment()}. ++ * As a consequence, {@link #length} is increased by {@value #DIMENSION}. ++ */ ++ abstract void addPoint(); ++ ++ /** ++ * Returns a copy of current coordinate values as a JTS coordinate sequence. ++ * The number of values to copy in a new array is {@link #length}. ++ * The copy is wrapped in a {@link PackedCoordinateSequence}. ++ * ++ * @param close whether to ensure that the first point is repeated as the last point. ++ * @return a JTS coordinate sequence containing a copy of current coordinate values. ++ */ ++ abstract PackedCoordinateSequence toSequence(boolean close); ++ ++ /** ++ * Iterates over all coordinates given by the {@link #iterator} and stores them in a JTS geometry. ++ * The path shall contain only straight lines; curves are not supported. ++ */ ++ private Geometry build() { ++ while (!iterator.isDone()) { ++ switch (currentSegment()) { ++ case PathIterator.SEG_MOVETO: { ++ flush(false); ++ addPoint(); ++ break; ++ } ++ case PathIterator.SEG_LINETO: { ++ if (length == 0) { ++ throw new IllegalPathStateException("LINETO without previous MOVETO."); ++ } ++ addPoint(); ++ break; ++ } ++ case PathIterator.SEG_CLOSE: { ++ flush(true); ++ break; ++ } ++ default: { ++ throw new IllegalPathStateException("Must contain only flat segments."); ++ } ++ } ++ iterator.next(); ++ } ++ flush(false); ++ final int count = geometries.size(); ++ if (count == 1) { ++ return geometries.get(0); ++ } ++ switch (geometryType) { ++ case 0: return factory.createEmpty(DIMENSION); ++ default: return factory.createGeometryCollection(GeometryFactory.toGeometryArray (geometries)); ++ case POINT: return factory.createMultiPoint (GeometryFactory.toPointArray (geometries)); ++ case LINESTRING: return factory.createMultiLineString (GeometryFactory.toLineStringArray(geometries)); ++ case POLYGON: { ++ Geometry result = geometries.get(0); ++ for (int i=1; i<count; i++) { ++ /* ++ * Java2D shapes and JTS geometries differ in their way to fill interior. ++ * Java2D fills the resulting contour based on visual winding rules. ++ * JTS has a system where outer shell and holes are clearly separated. ++ * We would need to draw contours as Java2D for computing JTS equivalent, ++ * but it would require a lot of work. In the meantime, the SymDifference ++ * operation is what behave the most like EVEN_ODD or NON_ZERO winding rules. ++ */ ++ result = result.symDifference(geometries.get(i)); ++ } ++ return result; ++ } ++ } ++ } ++ ++ /** ++ * Copies current coordinates in a new JTS geometry, ++ * then resets {@link #length} to 0 in preparation for the next geometry. ++ * ++ * @param isRing whether the geometry should be a closed polygon. ++ */ ++ private void flush(final boolean isRing) { ++ if (length != 0) { ++ final Geometry geometry; ++ if (length == DIMENSION) { ++ geometry = factory.createPoint(toSequence(false)); ++ geometryType |= POINT; ++ } else { ++ if (isRing) { ++ /* ++ * Note: JTS does not care about ring orientation. ++ * https://locationtech.github.io/jts/javadoc/org/locationtech/jts/geom/Polygon.html ++ */ ++ geometry = factory.createPolygon(toSequence(true)); ++ geometryType |= POLYGON; ++ } else { ++ geometry = factory.createLineString(toSequence(false)); ++ geometryType |= LINESTRING; ++ } ++ } ++ geometries.add(geometry); ++ length = 0; ++ } ++ } ++} diff --cc core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSShapeTest.java index 0000000,0000000..baac3c1 new file mode 100644 --- /dev/null +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSShapeTest.java @@@ -1,0 -1,0 +1,219 @@@ ++/* ++ * 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.jts; ++ ++import java.awt.Shape; ++import java.awt.geom.PathIterator; ++import org.locationtech.jts.geom.Coordinate; ++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.Polygon; ++import org.apache.sis.test.TestCase; ++import org.junit.Test; ++ ++import static org.junit.Assert.*; ++ ++ ++/** ++ * Tests {@link JTSShape}. ++ * ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @version 1.2 ++ * @since 1.2 ++ * @module ++ */ ++public final strictfp class JTSShapeTest extends TestCase { ++ /** ++ * The geometry factory used by the tests. ++ */ ++ private final GeometryFactory factory; ++ ++ /** ++ * An array of length 2 where to store (x,y) coordinates during path iteration. ++ */ ++ private final double[] buffer; ++ ++ /** ++ * Iterator over the shape to verify. Value is assigned by {@link #initialize(Geometry)}. ++ */ ++ private PathIterator iterator; ++ ++ /** ++ * Build a new test case. ++ */ ++ public JTSShapeTest() { ++ factory = new GeometryFactory(); ++ buffer = new double[2]; ++ } ++ ++ /** ++ * Initializes the test with the given geometry. ++ */ ++ private void initialize(final Geometry geometry) { ++ final Shape shape = new JTSShape(geometry); ++ iterator = shape.getPathIterator(null); ++ } ++ ++ /** ++ * Verifies that the current segment in the path iterator is of the given type. ++ * This method invokes {@link PathIterator#next()} after the comparison. ++ * ++ * @param type expected type: {@link PathIterator#SEG_MOVETO} or {@link PathIterator#SEG_LINETO}. ++ * @param x expected <var>x</var> coordinate. ++ * @param y expected <var>y</var> coordinate. ++ */ ++ private void assertSegmentEquals(final int type, final double x, final double y) { ++ assertFalse(iterator.isDone()); ++ assertEquals("type", type, iterator.currentSegment(buffer)); ++ assertEquals("x", x, buffer[0], STRICT); ++ assertEquals("y", y, buffer[1], STRICT); ++ iterator.next(); ++ } ++ ++ /** ++ * Verifies that the current segment is a {@link PathIterator#SEG_CLOSE}. ++ * This method invokes {@link PathIterator#next()} after the verification. ++ */ ++ private void assertSegmentClose() { ++ assertFalse(iterator.isDone()); ++ assertEquals("type", PathIterator.SEG_CLOSE, iterator.currentSegment(buffer)); ++ iterator.next(); ++ } ++ ++ /** ++ * Tests {@link JTSShape} with a point. ++ */ ++ @Test ++ public void testPoint() { ++ initialize(factory.createPoint(new Coordinate(10, 20))); ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 10, 20); ++ assertTrue(iterator.isDone()); ++ } ++ ++ /** ++ * Tests {@link JTSShape} with a line string. ++ */ ++ @Test ++ public void testLineString() { ++ initialize(factory.createLineString(new Coordinate[] { ++ new Coordinate(3, 1), ++ new Coordinate(7, 6), ++ new Coordinate(5, 2) ++ })); ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 3, 1); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 7, 6); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 5, 2); ++ assertTrue(iterator.isDone()); ++ } ++ ++ /** ++ * Tests {@link JTSShape} with a multi line string. ++ */ ++ @Test ++ public void testMultiLineString() { ++ final LineString line1 = factory.createLineString(new Coordinate[] { ++ new Coordinate(10, 12), ++ new Coordinate(5, 2) ++ }); ++ final LineString line2 = factory.createLineString(new Coordinate[] { ++ new Coordinate(3, 1), ++ new Coordinate(7, 6), ++ new Coordinate(5, 2) ++ }); ++ initialize(factory.createMultiLineString(new LineString[] {line1, line2})); ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 10, 12); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 5, 2); ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 3, 1); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 7, 6); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 5, 2); ++ assertTrue(iterator.isDone()); ++ } ++ ++ /** ++ * Tests {@link JTSShape} with a polygon. ++ */ ++ @Test ++ public void testPolygon() { ++ final LinearRing ring = factory.createLinearRing(new Coordinate[] { ++ new Coordinate(3, 1), ++ new Coordinate(7, 6), ++ new Coordinate(5, 2), ++ new Coordinate(3, 1) ++ }); ++ initialize(factory.createPolygon(ring, new LinearRing[0])); ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 3, 1); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 7, 6); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 5, 2); ++ assertSegmentClose(); ++ assertTrue(iterator.isDone()); ++ } ++ ++ /** ++ * Tests {@link JTSShape} with a multi-polygon. ++ */ ++ @Test ++ public void testMultiPolygon() { ++ final LinearRing ring1 = factory.createLinearRing(new Coordinate[] { ++ new Coordinate(3, 1), ++ new Coordinate(7, 6), ++ new Coordinate(5, 2), ++ new Coordinate(3, 1) ++ }); ++ final LinearRing ring2 = factory.createLinearRing(new Coordinate[] { ++ new Coordinate(12, 3), ++ new Coordinate(1, 9), ++ new Coordinate(4, 6), ++ new Coordinate(12, 3) ++ }); ++ final Polygon polygon1 = factory.createPolygon(ring1, new LinearRing[0]); ++ final Polygon polygon2 = factory.createPolygon(ring2, new LinearRing[0]); ++ initialize(factory.createMultiPolygon(new Polygon[] {polygon1, polygon2})); ++ ++ // First polygon. ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 3, 1); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 7, 6); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 5, 2); ++ assertSegmentClose(); ++ ++ // Second polygon. ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 12, 3); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 1, 9); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 4, 6); ++ assertSegmentClose(); ++ assertTrue(iterator.isDone()); ++ } ++ ++ /** ++ * Tests {@link JTS#asDecimatedShape(Geometry, double[])} with a line string. ++ */ ++ @Test ++ public void testAsDecimatedShapeLineString() { ++ final LineString line = factory.createLineString(new Coordinate[] { ++ new Coordinate(0, 0), ++ new Coordinate(1, 0), ++ new Coordinate(2, 0) ++ }); ++ final Shape shape = JTS.asDecimatedShape(line, new double[] {1.5, 1.5}); ++ iterator = shape.getPathIterator(null); ++ ++ assertSegmentEquals(PathIterator.SEG_MOVETO, 0, 0); ++ assertSegmentEquals(PathIterator.SEG_LINETO, 2, 0); ++ assertTrue(iterator.isDone()); ++ } ++} diff --cc core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/ShapeConverterTest.java index 0000000,0000000..31079f1 new file mode 100644 --- /dev/null +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/ShapeConverterTest.java @@@ -1,0 -1,0 +1,200 @@@ ++/* ++ * 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.jts; ++ ++import java.awt.Shape; ++import java.awt.Graphics2D; ++import java.awt.Font; ++import java.awt.font.FontRenderContext; ++import java.awt.font.GlyphVector; ++import java.awt.geom.Area; ++import java.awt.geom.GeneralPath; ++import java.awt.geom.Line2D; ++import java.awt.geom.Rectangle2D; ++import java.awt.image.BufferedImage; ++import org.locationtech.jts.geom.Coordinate; ++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.MultiPolygon; ++import org.locationtech.jts.geom.Polygon; ++import org.locationtech.jts.geom.Point; ++import org.apache.sis.test.TestCase; ++import org.junit.Test; ++ ++import static org.opengis.test.Assert.*; ++ ++ ++/** ++ * Tests {@link ShapeConverter}. ++ * ++ * @author Johann Sorel (Puzzle-GIS, Geomatys) ++ * @version 1.2 ++ * @since 1.2 ++ * @module ++ */ ++public final strictfp class ShapeConverterTest extends TestCase { ++ /** ++ * The geometry factory used by the tests. ++ */ ++ private final GeometryFactory factory; ++ ++ /** ++ * Creates a new test case. ++ */ ++ public ShapeConverterTest() { ++ factory = new GeometryFactory(); ++ } ++ ++ /** ++ * Verifies that the given geometry is an instance of the expected class ++ * and contains the expected coordinate values. ++ * ++ * @param shape the Java2D shape to convert with {@link ShapeConverter}. ++ * @param type expected class of the actual geometry. ++ * @param expected expected coordinates of the actual geometry. ++ */ ++ private static void assertCoordinatesEqual(final Shape shape, final Class<?> type, final Coordinate... expected) { ++ assertCoordinatesEqual(ShapeConverter.create(null, shape, 0.0001), type, expected); ++ } ++ ++ /** ++ * Verifies that the given geometry is an instance of the expected class ++ * and contains the expected coordinate values. ++ * ++ * @param geometry the JTS geometry to test. ++ * @param type expected class of the actual geometry. ++ * @param expected expected coordinates of the actual geometry. ++ */ ++ private static void assertCoordinatesEqual(final Geometry geometry, final Class<?> type, final Coordinate... expected) { ++ assertInstanceOf("Geometry class", type, geometry); ++ assertArrayEquals("Coordinates", expected, geometry.getCoordinates()); ++ } ++ ++ /** ++ * Tests {@link ShapeConverter} with a point. ++ */ ++ @Test ++ public void testPoint() { ++ final GeneralPath shape = new GeneralPath(); ++ shape.moveTo(10, 20); ++ assertCoordinatesEqual(shape, Point.class, ++ new Coordinate(10, 20)); ++ } ++ ++ /** ++ * Tests {@link ShapeConverter} with a line. ++ */ ++ @Test ++ public void testLine() { ++ final Line2D shape = new Line2D.Double(1, 2, 3, 4); ++ assertCoordinatesEqual(shape, LineString.class, ++ new Coordinate(1, 2), ++ new Coordinate(3, 4)); ++ } ++ ++ /** ++ * Tests {@link ShapeConverter} with a rectangle. ++ */ ++ @Test ++ public void testRectangle() { ++ final Rectangle2D shape = new Rectangle2D.Double(1, 2, 10, 20); ++ assertCoordinatesEqual(shape, Polygon.class, ++ new Coordinate( 1, 2), ++ new Coordinate(11, 2), ++ new Coordinate(11, 22), ++ new Coordinate( 1, 22), ++ new Coordinate( 1, 2)); ++ } ++ ++ /** ++ * Tests {@link ShapeConverter} with a rectangle with a hole shape. ++ */ ++ @Test ++ public void testRectangleWithHole() { ++ final Rectangle2D contour = new Rectangle2D.Double(1, 2, 10, 20); ++ final Rectangle2D hole = new Rectangle2D.Double(5, 6, 2, 3); ++ final Area shape = new Area(contour); ++ shape.subtract(new Area(hole)); ++ ++ final Geometry geometry = ShapeConverter.create(factory, shape, 0.0001); ++ assertInstanceOf("Geometry class", Polygon.class, geometry); ++ final Polygon polygon = (Polygon) geometry; ++ assertEquals(1, polygon.getNumInteriorRing()); ++ ++ assertCoordinatesEqual(polygon.getExteriorRing(), LinearRing.class, ++ new Coordinate(1, 2), ++ new Coordinate(1, 22), ++ new Coordinate(11, 22), ++ new Coordinate(11, 2), ++ new Coordinate(1, 2)); ++ ++ assertCoordinatesEqual(polygon.getInteriorRingN(0), LinearRing.class, ++ new Coordinate(7, 6), ++ new Coordinate(7, 9), ++ new Coordinate(5, 9), ++ new Coordinate(5, 6), ++ new Coordinate(7, 6)); ++ } ++ ++ /** ++ * Tests {@link ShapeConverter} with the shape of an arbitrary text. ++ * We use that as an easy way to create relatively complex shapes. ++ * The arbitrary text is "Labi": 4 letters, 5 polygons (because "i" is made ++ * of 2 detached polygons),* with 2 polygons ("a" and "b") having a hole. ++ */ ++ @Test ++ public void testText() { ++ final Shape shape; ++ final Graphics2D handler = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB).createGraphics(); ++ try { ++ final FontRenderContext fontRenderContext = handler.getFontRenderContext(); ++ final Font font = new Font("Monospaced", Font.PLAIN, 12); ++ final GlyphVector glyphs = font.createGlyphVector(fontRenderContext, "Labi"); ++ shape = glyphs.getOutline(); ++ } finally { ++ handler.dispose(); ++ } ++ final Geometry geometry = ShapeConverter.create(factory, shape, 0.1); ++ assertInstanceOf("Geometry class", MultiPolygon.class, geometry); ++ final MultiPolygon mp = (MultiPolygon) geometry; ++ /* ++ * The "Labi" text contaons 4 characters but 'i' is split in two ploygons, ++ * for a total of 5 polygons. Two letters ("a" and "b") are polyogns whith ++ * hole inside them. ++ */ ++ assertEquals(5, mp.getNumGeometries()); ++ for (int i=0; i<5; i++) { ++ final String message = "Glyph #" + i; ++ final Geometry glyph = mp.getGeometryN(i); ++ assertInstanceOf(message, Polygon.class, glyph); ++ assertEquals(message, (i == 1 || i == 2) ? 1 : 0, // 'a' and 'b' should contain a hole. ++ ((Polygon) glyph).getNumInteriorRing()); ++ } ++ /* ++ * Compare the bounding boxes. ++ */ ++ final Rectangle2D bounds2D = shape.getBounds2D(); ++ final Envelope env = geometry.getEnvelopeInternal(); ++ assertEquals(bounds2D.getMinX(), env.getMinX(), STRICT); ++ assertEquals(bounds2D.getMaxX(), env.getMaxX(), STRICT); ++ assertEquals(bounds2D.getMinY(), env.getMinY(), STRICT); ++ assertEquals(bounds2D.getMaxY(), env.getMaxY(), STRICT); ++ } ++} diff --cc core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java index 9f2cf45,9f2cf45..c95df3d --- 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 @@@ -74,6 -74,6 +74,8 @@@ import org.junit.runners.Suite org.apache.sis.internal.feature.esri.FactoryTest.class, org.apache.sis.internal.feature.jts.FactoryTest.class, org.apache.sis.internal.feature.jts.JTSTest.class, ++ org.apache.sis.internal.feature.jts.JTSShapeTest.class, ++ org.apache.sis.internal.feature.jts.ShapeConverterTest.class, org.apache.sis.feature.builder.CharacteristicTypeBuilderTest.class, org.apache.sis.feature.builder.AttributeTypeBuilderTest.class, org.apache.sis.feature.builder.AssociationRoleBuilderTest.class,