This is an automated email from the ASF dual-hosted git repository. jsorel pushed a commit to branch feat/toShape in repository https://gitbox.apache.org/repos/asf/sis.git
commit d3b3ce30b90c7923c71ab7c3602442baa821455e Author: jsorel <johann.so...@geomatys.com> AuthorDate: Tue Nov 23 11:51:35 2021 +0100 feat(JTS): add fromAwt functions --- .../org/apache/sis/internal/feature/jts/JTS.java | 157 +++++++++++++++++++++ .../apache/sis/internal/feature/jts/JTSTest.java | 129 +++++++++++++++++ 2 files changed, 286 insertions(+) diff --git 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 index b5c85c2..c7f609f 100644 --- 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 @@ -17,6 +17,12 @@ 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 org.opengis.metadata.Identifier; import org.opengis.util.FactoryException; @@ -35,8 +41,13 @@ 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; /** @@ -308,6 +319,7 @@ public final class JTS extends Static { ArgumentChecks.ensureNonNull("geometry", geometry); return new JTSShape(geometry, transform); } + /** * Create a view of the JTS geometry as a Java2D Shape applying a decimation on the fly. * @@ -319,4 +331,149 @@ public final class JTS extends Static { 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)); + } + } + } + + /** + * Extract the next point, line or ring from iterator. + */ + 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); + } + } + } + } diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSTest.java index 48795a8..d90eded 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/internal/feature/jts/JTSTest.java @@ -16,8 +16,17 @@ */ package org.apache.sis.internal.feature.jts; +import java.awt.Font; +import java.awt.Graphics2D; import java.awt.Shape; +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.PathIterator; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; import java.util.Collections; import org.opengis.util.FactoryException; import org.opengis.referencing.operation.TransformException; @@ -37,6 +46,7 @@ import org.apache.sis.test.TestCase; import org.junit.Test; import static org.junit.Assert.*; +import org.locationtech.jts.geom.Envelope; /** @@ -419,4 +429,123 @@ public final strictfp class JTSTest extends TestCase { assertTrue(ite.isDone()); } + + /** + * Tests {@link JTS#fromAwt(org.locationtech.jts.geom.GeometryFactory, java.awt.Shape, double)} with a point type shape. + */ + @Test + public void testFromAwtPoint() { + final GeneralPath path = new GeneralPath(); + path.moveTo(10, 20); + + final Geometry candidate = JTS.fromAwt(GF, path, 0.0001); + final Geometry expected = GF.createPoint(new Coordinate(10,20)); + assertEquals(expected, candidate); + } + + /** + * Tests {@link JTS#fromAwt(org.locationtech.jts.geom.GeometryFactory, java.awt.Shape, double)} with a line type shape. + */ + @Test + public void testFromAwtLine() { + final Line2D shape = new Line2D.Double(1, 2, 3, 4); + final Geometry geometry = JTS.fromAwt(GF, shape, 0.1); + assertTrue(geometry instanceof LineString); + final LineString ls = (LineString) geometry; + final Coordinate[] coordinates = ls.getCoordinates(); + assertEquals(2, coordinates.length); + assertEquals(new Coordinate(1,2), coordinates[0]); + assertEquals(new Coordinate(3,4), coordinates[1]); + } + + /** + * Tests {@link JTS#fromAwt(org.locationtech.jts.geom.GeometryFactory, java.awt.Shape, double)} with a rectangle type shape. + */ + @Test + public void testFromAwtRectangle() { + final Rectangle2D shape = new Rectangle2D.Double(1,2,10,20); + final Geometry geometry = JTS.fromAwt(GF, shape, 0.1); + assertTrue(geometry instanceof Polygon); + final Polygon ls = (Polygon) geometry; + final Coordinate[] coordinates = ls.getCoordinates(); + assertEquals(5, coordinates.length); + assertEquals(new Coordinate(1,2), coordinates[0]); + assertEquals(new Coordinate(11,2), coordinates[1]); + assertEquals(new Coordinate(11,22), coordinates[2]); + assertEquals(new Coordinate(1,22), coordinates[3]); + assertEquals(new Coordinate(1,2), coordinates[4]); + } + + /** + * Tests {@link JTS#fromAwt(org.locationtech.jts.geom.GeometryFactory, java.awt.Shape, double)} with a rectangle with a hole shape. + */ + @Test + public void testFromAwtRectangleWithHole() { + 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 = JTS.fromAwt(GF, shape, 0.1); + assertTrue(geometry instanceof Polygon); + final Polygon ls = (Polygon) geometry; + final LinearRing exteriorRing = ls.getExteriorRing(); + assertEquals(1, ls.getNumInteriorRing()); + final LinearRing interiorRing = ls.getInteriorRingN(0); + + final Coordinate[] coordinatesExt = exteriorRing.getCoordinates(); + assertEquals(5, coordinatesExt.length); + assertEquals(new Coordinate(1,2), coordinatesExt[0]); + assertEquals(new Coordinate(1,22), coordinatesExt[1]); + assertEquals(new Coordinate(11,22), coordinatesExt[2]); + assertEquals(new Coordinate(11,2), coordinatesExt[3]); + assertEquals(new Coordinate(1,2), coordinatesExt[4]); + + final Coordinate[] coordinatesInt = interiorRing.getCoordinates(); + assertEquals(5, coordinatesInt.length); + assertEquals(new Coordinate(7,6), coordinatesInt[0]); + assertEquals(new Coordinate(7,9), coordinatesInt[1]); + assertEquals(new Coordinate(5,9), coordinatesInt[2]); + assertEquals(new Coordinate(5,6), coordinatesInt[3]); + assertEquals(new Coordinate(7,6), coordinatesInt[4]); + } + + /** + * Tests {@link JTS#fromAwt(org.locationtech.jts.geom.GeometryFactory, java.awt.Shape, double)} with a text shape. + */ + @Test + public void testFromAwtText() { + final BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); + final Graphics2D g = img.createGraphics(); + final FontRenderContext fontRenderContext = g.getFontRenderContext(); + final Font font = new Font("Monospaced", Font.PLAIN, 12); + final GlyphVector glyphs = font.createGlyphVector(fontRenderContext, "Labi"); + final Shape shape = glyphs.getOutline(); + final GeneralPath gp = new GeneralPath(); + gp.append(shape.getPathIterator(null, 0.1), false); + final Rectangle2D bounds2D = gp.getBounds2D(); + + final Geometry geometry = JTS.fromAwt(GF, shape, 0.1); + assertTrue(geometry instanceof MultiPolygon); + final MultiPolygon mp = (MultiPolygon) geometry; + assertEquals(5, mp.getNumGeometries()); //4 characters but 'i' is split in two ploygons + Geometry l = mp.getGeometryN(0); + Geometry a = mp.getGeometryN(1); + Geometry b = mp.getGeometryN(2); + Geometry i0 = mp.getGeometryN(3); + Geometry i1 = mp.getGeometryN(4); + assertTrue(l instanceof Polygon); + assertTrue(a instanceof Polygon); + assertTrue(b instanceof Polygon); + assertTrue(i0 instanceof Polygon); + assertTrue(i1 instanceof Polygon); + //a must contain a hole + assertEquals(1, ((Polygon) a).getNumInteriorRing()); + + //check bounding box + final Envelope env = geometry.getEnvelopeInternal(); + assertEquals(bounds2D.getMinX(), env.getMinX(), 0.0); + assertEquals(bounds2D.getMaxX(), env.getMaxX(), 0.0); + assertEquals(bounds2D.getMinY(), env.getMinY(), 0.0); + assertEquals(bounds2D.getMaxY(), env.getMaxY(), 0.0); + } }