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 610f4bc122ac8ec5434c54c582778160edaa5381 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 | 43 +++++++++++++++++----- .../geometry/wrapper/jts/ShapeConverterTest.java | 12 +++++- 2 files changed, 43 insertions(+), 12 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..2b268caf74 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,12 @@ 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.util.GeometryFixer; /** @@ -274,17 +277,28 @@ abstract class ShapeConverter { case POINT: return factory.createMultiPoint (GeometryFactory.toPointArray (geometries)); case LINESTRING: return factory.createMultiLineString (GeometryFactory.toLineStringArray(geometries)); case POLYGON: { + /* + * 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. + */ + + //sort by area, bigger geometries are the outter rings + Collections.sort(geometries, (Geometry o1, Geometry o2) -> java.lang.Double.compare(o2.getArea(), o1.getArea())); 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)); + 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); + } + } return result; } @@ -299,7 +313,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 +325,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);