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,

Reply via email to