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);

Reply via email to