This is an automated email from the ASF dual-hosted git repository. jsorel pushed a commit to branch feat/image2polygon in repository https://gitbox.apache.org/repos/asf/sis.git
commit db4733200aa7656384a78f871a5a76e2d3ece04a Author: jsorel <johann.so...@geomatys.com> AuthorDate: Tue Mar 25 11:49:31 2025 +0100 Improve AWT to JTS conversion logic --- .../sis/geometry/wrapper/jts/ShapeConverter.java | 179 +++++++++++++++++++-- .../geometry/wrapper/jts/ShapeConverterTest.java | 12 +- 2 files changed, 176 insertions(+), 15 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/ShapeConverter.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/ShapeConverter.java index ddb8d707d9..8b9d7548c0 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/ShapeConverter.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/ShapeConverter.java @@ -22,9 +22,21 @@ import java.util.Arrays; import java.util.ArrayList; import java.awt.geom.PathIterator; import java.awt.geom.IllegalPathStateException; +import java.util.Collections; +import java.util.Comparator; import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.GeometryFactory; import org.apache.sis.referencing.privy.AbstractShape; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.GeometryCollection; +import org.locationtech.jts.geom.LineString; +import org.locationtech.jts.geom.LinearRing; +import org.locationtech.jts.geom.MultiLineString; +import org.locationtech.jts.geom.MultiPoint; +import org.locationtech.jts.geom.MultiPolygon; +import org.locationtech.jts.geom.Point; +import org.locationtech.jts.geom.Polygon; +import org.locationtech.jts.geom.util.GeometryFixer; /** @@ -265,17 +277,25 @@ abstract class ShapeConverter { } flush(false); final int count = geometries.size(); + + Geometry result; 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++) { + result = geometries.get(0); + } else { + switch (geometryType) { + case 0: + result = factory.createEmpty(DIMENSION); + break; + default: + result = factory.createGeometryCollection(GeometryFactory.toGeometryArray (geometries)); + break; + case POINT: + result = factory.createMultiPoint (GeometryFactory.toPointArray (geometries)); + break; + case LINESTRING: + result = factory.createMultiLineString (GeometryFactory.toLineStringArray(geometries)); + break; + case POLYGON: /* * Java2D shapes and JTS geometries differ in their way to fill interior. * Java2D fills the resulting contour based on visual winding rules. @@ -284,13 +304,137 @@ abstract class ShapeConverter { * 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)); + + //sort by area, bigger geometries are the outter rings + Collections.sort(geometries, (Geometry o1, Geometry o2) -> java.lang.Double.compare(o2.getArea(), o1.getArea())); + result = geometries.get(0); + for (int i=1; i<count; i++) { + Geometry other = geometries.get(i); + if (result.intersects(other)) { + //ring is a hole + result = result.symDifference(other); + } else { + //ring is a separate polygon + result = result.union(other); + } + } + break; + } + } + + return enforce2D(result); + } + + /** + * JTS has the bad habit of expending the dimension of CoordinateSequence + * from 2D to 3D adding NaN Z values. + * Since we do not want any Z ordinates, we have to check and fix those. + * + * @param geometry to fix + */ + private <T extends Geometry> T enforce2D(T geometry) { + if (geometry instanceof Point) { + final Point pt = (Point) geometry; + final CoordinateSequence cs = pt.getCoordinateSequence(); + final CoordinateSequence cs2d = enforce2D(cs); + return (T) (cs2d != cs ? factory.createPoint(cs2d) : geometry); + } else if (geometry instanceof LinearRing) { + final LinearRing ls = (LinearRing) geometry; + final CoordinateSequence cs = ls.getCoordinateSequence(); + final CoordinateSequence cs2d = enforce2D(cs); + return (T) (cs2d != cs ? factory.createLinearRing(cs2d) : geometry); + } else if (geometry instanceof LineString) { + final LineString ls = (LineString) geometry; + final CoordinateSequence cs = ls.getCoordinateSequence(); + final CoordinateSequence cs2d = enforce2D(cs); + return (T) (cs2d != cs ? factory.createLineString(cs2d) : geometry); + } else if (geometry instanceof MultiLineString) { + final MultiLineString ml = (MultiLineString) geometry; + boolean changed = false; + final LineString[] news = new LineString[ml.getNumGeometries()]; + for (int i = 0; i < news.length; i++) { + news[i] = (LineString) ml.getGeometryN(i); + LineString cp = enforce2D(news[i]); + if (cp != news[i]) { + news[i] = cp; + changed = true; + } + } + return (T) (changed ? factory.createMultiLineString(news) : geometry); + } else if (geometry instanceof Polygon) { + final Polygon pl = (Polygon) geometry; + boolean changed = false; + final LinearRing exterior = pl.getExteriorRing(); + final LinearRing copy = enforce2D(exterior); + if (exterior != copy) { + changed = true; + } + + final LinearRing[] news = new LinearRing[pl.getNumInteriorRing()]; + for (int i = 0; i < news.length; i++) { + news[i] = pl.getInteriorRingN(i); + LinearRing cp = enforce2D(news[i]); + if (cp != news[i]) { + news[i] = cp; + changed = true; + } + } + return (T) (changed ? factory.createPolygon(copy, news) : geometry); + } else if (geometry instanceof MultiPoint) { + final MultiPoint ml = (MultiPoint) geometry; + boolean changed = false; + final Point[] news = new Point[ml.getNumGeometries()]; + for (int i = 0; i < news.length; i++) { + news[i] = (Point) ml.getGeometryN(i); + Point cp = enforce2D(news[i]); + if (cp != news[i]) { + news[i] = cp; + changed = true; + } + } + return (T) (changed ? factory.createMultiPoint(news) : geometry); + } else if (geometry instanceof MultiPolygon) { + final MultiPolygon ml = (MultiPolygon) geometry; + boolean changed = false; + final Polygon[] news = new Polygon[ml.getNumGeometries()]; + for (int i = 0; i < news.length; i++) { + news[i] = (Polygon) ml.getGeometryN(i); + Polygon cp = enforce2D(news[i]); + if (cp != news[i]) { + news[i] = cp; + changed = true; + } + } + return (T) (changed ? factory.createMultiPolygon(news) : geometry); + } else if (geometry instanceof GeometryCollection) { + final GeometryCollection ml = (GeometryCollection) geometry; + boolean changed = false; + final Geometry[] news = new Geometry[ml.getNumGeometries()]; + for (int i = 0; i < news.length; i++) { + news[i] = ml.getGeometryN(i); + Geometry cp = enforce2D(news[i]); + if (cp != news[i]) { + news[i] = cp; + changed = true; } - return result; } + return (T) (changed ? factory.createGeometryCollection(news) : geometry); + } else { + throw new UnsupportedOperationException("Unexpected JTS geometry type " + geometry.getGeometryType()); } } + private CoordinateSequence enforce2D(CoordinateSequence cs) { + if (cs.getDimension() == 2) return cs; + final int size = cs.size(); + final CoordinateSequence copy = factory.getCoordinateSequenceFactory().create(size, 2); + for (int i = 0; i < size; i++) { + copy.setOrdinate(i, 0, cs.getX(i)); + copy.setOrdinate(i, 1, cs.getY(i)); + } + return copy; + } + /** * Copies current coordinates in a new JTS geometry, * then resets {@link #length} to 0 in preparation for the next geometry. @@ -299,7 +443,7 @@ abstract class ShapeConverter { */ private void flush(final boolean isRing) { if (length != 0) { - final Geometry geometry; + Geometry geometry; if (length == DIMENSION) { geometry = factory.createPoint(toSequence(false)); geometryType |= POINT; @@ -311,6 +455,15 @@ abstract class ShapeConverter { */ geometry = factory.createPolygon(toSequence(true)); geometryType |= POLYGON; + + /* + Expensive operation but java2d is very tolerant to incoherent paths. + We need to fix those otherwise we might have errors when aggregating + holes in polygons. + */ + if (!geometry.isValid()) { + geometry = GeometryFixer.fix(geometry); + } } else { geometry = factory.createLineString(toSequence(false)); geometryType |= LINESTRING; diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/ShapeConverterTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/ShapeConverterTest.java index 9a50222b84..42de55e164 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/ShapeConverterTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/geometry/wrapper/jts/ShapeConverterTest.java @@ -26,6 +26,9 @@ import java.awt.geom.GeneralPath; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.Envelope; import org.locationtech.jts.geom.Geometry; @@ -171,16 +174,21 @@ public final class ShapeConverterTest extends TestCase { } final Geometry geometry = ShapeConverter.create(factory, shape, 0.1); assertInstanceOf(MultiPolygon.class, geometry); - final MultiPolygon mp = (MultiPolygon) 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()); + // sort on X + final List<Geometry> parts = new ArrayList<>(5); + for (int i=0; i<5; i++) parts.add(mp.getGeometryN(i)); + parts.sort((Geometry o1, Geometry o2) -> Double.compare(o1.getEnvelopeInternal().getMinX(), o2.getEnvelopeInternal().getMinX())); + for (int i=0; i<5; i++) { final String message = "Glyph #" + i; - final Geometry glyph = mp.getGeometryN(i); + final Geometry glyph = parts.get(i); assertInstanceOf(Polygon.class, glyph, message); assertEquals((i == 1 || i == 2) ? 1 : 0, // `a` and `b` should contain a hole. ((Polygon) glyph).getNumInteriorRing(), message);