This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git

commit b5ce1a9c9a331eae15b8f3c0ac6150ca108dca28
Author: Edmond Chuc <[email protected]>
AuthorDate: Mon Sep 14 16:45:47 2026 +1000

    GH-4221: Preserve Z/M and coordinate layouts when reversing axes
---
 .../geosparql/implementation/GeometryReverse.java  | 165 +++------------------
 .../jts/CustomCoordinateSequence.java              |   9 +-
 .../implementation/GeometryReverseTest.java        | 101 +++++++++++++
 .../jts/CustomCoordinateSequenceTest.java          |  23 +++
 4 files changed, 153 insertions(+), 145 deletions(-)

diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryReverse.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryReverse.java
index 96f384b47d..5baae92b4e 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryReverse.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryReverse.java
@@ -21,13 +21,9 @@
 package org.apache.jena.geosparql.implementation;
 
 import org.apache.jena.geosparql.implementation.registry.SRSRegistry;
-import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.CoordinateSequenceFilter;
 import org.locationtech.jts.geom.Geometry;
-import org.locationtech.jts.geom.GeometryCollection;
-import org.locationtech.jts.geom.GeometryFactory;
-import org.locationtech.jts.geom.LineString;
-import org.locationtech.jts.geom.LinearRing;
-import org.locationtech.jts.geom.Polygon;
 
 /**
  *
@@ -81,149 +77,32 @@ public class GeometryReverse {
     }
 
     /**
-     * Reverses coordinate order of the supplied geometry and produces a new
-     * geometry.
+     * Swaps X and Y in a copy of the supplied geometry, preserving Z, M,
+     * coordinate layouts, and collection structure. The input is not modified.
      *
      * @param geometry
-     * @return Geometry in x,y coordinate order.
+     * @return Geometry with X and Y exchanged.
      */
     public static Geometry reverseGeometry(Geometry geometry) {
-
-        if (geometry.isEmpty()) {
-            return geometry.copy();
-        }
-
-        GeometryFactory factory = geometry.getFactory();
-        Geometry finalGeometry;
-        Coordinate[] coordinates;
-
-        String type = geometry.getGeometryType();
-
-        switch (type) {
-            case "LineString":
-                coordinates = getReversedCoordinates(geometry);
-                finalGeometry = factory.createLineString(coordinates);
-                break;
-            case "LinearRing":
-                coordinates = getReversedCoordinates(geometry);
-                finalGeometry = factory.createLinearRing(coordinates);
-                break;
-            case "MultiPoint":
-                coordinates = getReversedCoordinates(geometry);
-                finalGeometry = 
factory.createMultiPointFromCoords(coordinates);
-                break;
-            case "Polygon":
-                finalGeometry = reversePolygon(geometry, factory);
-                break;
-            case "Point":
-                coordinates = getReversedCoordinates(geometry);
-                finalGeometry = factory.createPoint(coordinates[0]);
-                break;
-            case "MultiPolygon":
-                Polygon[] polygons = unpackPolygons((GeometryCollection) 
geometry);
-                finalGeometry = factory.createMultiPolygon(polygons);
-                break;
-            case "MultiLineString":
-                LineString[] lineString = 
unpackLineStrings((GeometryCollection) geometry);
-                finalGeometry = factory.createMultiLineString(lineString);
-                break;
-            case "GeometryCollection":
-                Geometry[] geometries = 
unpackGeometryCollection((GeometryCollection) geometry);
-                finalGeometry = factory.createGeometryCollection(geometries);
-                break;
-            default:
-                finalGeometry = geometry;
-                break;
-        }
-
-        return finalGeometry;
-    }
-
-    private static Coordinate[] getReversedCoordinates(Geometry geometry) {
-
-        Coordinate[] original = geometry.getCoordinates();
-        Coordinate[] reversed = new Coordinate[original.length];
-
-        for (int i = 0; i < original.length; i++) {
-            reversed[i] = new Coordinate(original[i].y, original[i].x);
-        }
-
-        return reversed;
-
-    }
-
-    private static Polygon reversePolygon(Geometry geometry, GeometryFactory 
factory) {
-
-        Polygon finalGeometry;
-        Polygon polygon = (Polygon) geometry;
-        if (polygon.getNumInteriorRing() == 0) {
-            //There are no interior rings so perform the standard reversal.
-            Coordinate[] coordinates = getReversedCoordinates(geometry);
-            finalGeometry = factory.createPolygon(coordinates);
-        } else {
-
-            LineString exteriorRing = polygon.getExteriorRing();
-            Coordinate[] reversedExteriorCoordinates = 
getReversedCoordinates(exteriorRing);
-            LinearRing reversedExteriorRing = 
factory.createLinearRing(reversedExteriorCoordinates);
-
-            LinearRing[] reversedInteriorRings = new 
LinearRing[polygon.getNumInteriorRing()];
-            for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
-                LineString interiorRing = polygon.getInteriorRingN(i);
-                Coordinate[] reversedInteriorCoordinates = 
getReversedCoordinates(interiorRing);
-                LinearRing reversedInteriorRing = 
factory.createLinearRing(reversedInteriorCoordinates);
-                reversedInteriorRings[i] = reversedInteriorRing;
+        Geometry reversed = geometry.copy();
+        reversed.apply(new CoordinateSequenceFilter() {
+            @Override
+            public void filter(CoordinateSequence sequence, int index) {
+                double x = sequence.getX(index);
+                sequence.setOrdinate(index, 0, sequence.getY(index));
+                sequence.setOrdinate(index, 1, x);
             }
 
-            finalGeometry = factory.createPolygon(reversedExteriorRing, 
reversedInteriorRings);
-        }
-
-        return finalGeometry;
-    }
-
-    private static Polygon[] unpackPolygons(GeometryCollection geoCollection) {
-
-        GeometryFactory factory = geoCollection.getFactory();
-
-        int count = geoCollection.getNumGeometries();
-        Polygon[] polygons = new Polygon[count];
-
-        for (int i = 0; i < count; i++) {
-            Geometry geometry = geoCollection.getGeometryN(i);
-            Polygon polygon = reversePolygon(geometry, factory);
-            polygons[i] = polygon;
-        }
-
-        return polygons;
-    }
-
-    private static LineString[] unpackLineStrings(GeometryCollection 
geoCollection) {
-
-        GeometryFactory factory = geoCollection.getFactory();
-
-        int count = geoCollection.getNumGeometries();
-        LineString[] lineStrings = new LineString[count];
-
-        for (int i = 0; i < count; i++) {
-            Geometry geometry = geoCollection.getGeometryN(i);
-            Coordinate[] coordinates = getReversedCoordinates(geometry);
-            LineString lineString = factory.createLineString(coordinates);
-            lineStrings[i] = lineString;
-        }
-
-        return lineStrings;
-    }
-
-    private static Geometry[] unpackGeometryCollection(GeometryCollection 
geoCollection) {
-
-        int count = geoCollection.getNumGeometries();
-        Geometry[] geometries = new Geometry[count];
-
-        for (int i = 0; i < count; i++) {
-            Geometry geometry = geoCollection.getGeometryN(i);
-            geometries[i] = reverseGeometry(geometry);
-        }
+            @Override
+            public boolean isDone() {
+                return false;
+            }
 
-        return geometries;
+            @Override
+            public boolean isGeometryChanged() {
+                return true;
+            }
+        });
+        return reversed;
     }
-
 }
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequence.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequence.java
index 11110f746f..083e643ded 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequence.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequence.java
@@ -238,7 +238,12 @@ public class CustomCoordinateSequence implements 
CoordinateSequence, Serializabl
 
     @Override
     public CustomCoordinateSequence copy() {
-        return new CustomCoordinateSequence(x, y, z, m);
+        CustomCoordinateSequence copy = new CustomCoordinateSequence(size, 
dimensions);
+        System.arraycopy(x, 0, copy.x, 0, size);
+        System.arraycopy(y, 0, copy.y, 0, size);
+        System.arraycopy(z, 0, copy.z, 0, size);
+        System.arraycopy(m, 0, copy.m, 0, size);
+        return copy;
     }
 
     public int getSize() {
@@ -506,7 +511,7 @@ public class CustomCoordinateSequence implements 
CoordinateSequence, Serializabl
     @Override
     @Deprecated
     public CustomCoordinateSequence clone() {
-        return new CustomCoordinateSequence(x, y, z, m);
+        return copy();
     }
 
     @Override
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryReverseTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryReverseTest.java
index 1ca8c335a0..9b81edce3c 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryReverseTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryReverseTest.java
@@ -20,14 +20,24 @@
  */
 package org.apache.jena.geosparql.implementation;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
+import org.apache.jena.geosparql.implementation.jts.CustomCoordinateSequence;
+import org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory;
 import org.apache.jena.geosparql.implementation.vocabulary.SRS_URI;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.GeometryComponentFilter;
 import org.locationtech.jts.geom.LineString;
 import org.locationtech.jts.geom.MultiLineString;
 import org.locationtech.jts.geom.MultiPoint;
@@ -44,6 +54,97 @@ import org.opengis.util.FactoryException;
  */
 public class GeometryReverseTest {
 
+    @Test
+    public void reversalPreservesZAndMForStandardAndCustomSequences() throws 
ParseException {
+        String[] examples = {
+            "POINT Z (100 10 -4)",
+            "LINESTRING M (100 10 7, 120 20 8)",
+            "LINESTRING ZM (100 10 -4 7, 120 20 9 8)",
+            "POLYGON ZM ((0 0 1 10, 10 0 2 20, 10 10 3 30, 0 0 1 10), "
+            + "(2 1 -9 40, 3 1 8 50, 3 2 7 60, 2 1 -9 40))",
+            "MULTIPOINT Z ((100 10 -4), (120 20 9))"
+        };
+        for (String wkt : examples) {
+            assertReversalPreservesOrdinates(new WKTReader().read(wkt));
+            assertReversalPreservesOrdinates(GeometryWrapper.extract(wkt, 
WKTDatatype.URI).getParsingGeometry());
+        }
+    }
+
+    @Test
+    public void reversalPreservesNestedMembersAndEmptyLayouts() {
+        var factory = CustomGeometryFactory.theInstance();
+        Geometry point = GeometryWrapper.extract("POINT ZM (100 10 -4 7)", 
WKTDatatype.URI).getParsingGeometry();
+        Geometry empty = factory.createPoint(new CustomCoordinateSequence(0, 
CoordinateSequenceDimensions.XYZM));
+        Geometry nested = factory.createGeometryCollection(new Geometry[] { 
empty, point });
+        Geometry collection = factory.createGeometryCollection(new Geometry[] 
{ point, nested });
+        Geometry reversed = GeometryReverse.reverseGeometry(collection);
+        assertEquals(2, reversed.getNumGeometries());
+        assertEquals(2, reversed.getGeometryN(1).getNumGeometries());
+        assertReversalPreservesOrdinates(collection);
+        assertReversalPreservesOrdinates(empty);
+    }
+
+    @Test
+    public void reversalPreservesDeclaredLayoutWithMissingZAndM() {
+        var coordinates = new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZM,
+                                                       "100 10 NaN NaN,120 20 
9 8");
+        
assertReversalPreservesOrdinates(CustomGeometryFactory.theInstance().createLineString(coordinates));
+    }
+
+    private static void assertReversalPreservesOrdinates(Geometry original) {
+        // Capture values independently so a shallow copy cannot hide input 
mutation.
+        List<CoordinateSequence> source = sequences(original);
+        List<double[]> values = new ArrayList<>();
+        for (CoordinateSequence sequence : source) {
+            for (int i = 0; i < sequence.size(); i++) {
+                values.add(new double[] { sequence.getX(i), sequence.getY(i), 
sequence.getZ(i), sequence.getM(i) });
+            }
+        }
+        original.getEnvelopeInternal(); // Populate the envelope cache before 
reversal.
+        Geometry reversed = GeometryReverse.reverseGeometry(original);
+        assertNotSame(original, reversed);
+        assertEquals(original.getGeometryType(), reversed.getGeometryType());
+        List<CoordinateSequence> result = sequences(reversed);
+        assertEquals(source.size(), result.size());
+        int coordinate = 0;
+        for (int n = 0; n < source.size(); n++) {
+            CoordinateSequence before = source.get(n);
+            CoordinateSequence after = result.get(n);
+            assertNotSame(before, after);
+            assertEquals(before.getDimension(), after.getDimension());
+            assertEquals(before.getMeasures(), after.getMeasures());
+            assertEquals(before.size(), after.size());
+            for (int i = 0; i < before.size(); i++) {
+                double[] expected = values.get(coordinate++);
+                assertEquals(expected[0], before.getX(i), 0);
+                assertEquals(expected[1], before.getY(i), 0);
+                assertEquals(expected[2], before.getZ(i), 0);
+                assertEquals(expected[3], before.getM(i), 0);
+                assertEquals(expected[1], after.getX(i), 0);
+                assertEquals(expected[0], after.getY(i), 0);
+                assertEquals(expected[2], after.getZ(i), 0);
+                assertEquals(expected[3], after.getM(i), 0);
+            }
+        }
+        if (!original.isEmpty()) {
+            assertEquals(original.getEnvelopeInternal().getMinY(), 
reversed.getEnvelopeInternal().getMinX(), 0);
+            assertEquals(original.getEnvelopeInternal().getMaxY(), 
reversed.getEnvelopeInternal().getMaxX(), 0);
+            assertEquals(original.getEnvelopeInternal().getMinX(), 
reversed.getEnvelopeInternal().getMinY(), 0);
+            assertEquals(original.getEnvelopeInternal().getMaxX(), 
reversed.getEnvelopeInternal().getMaxY(), 0);
+        }
+    }
+
+    private static List<CoordinateSequence> sequences(Geometry geometry) {
+        List<CoordinateSequence> result = new ArrayList<>();
+        geometry.apply((GeometryComponentFilter) component -> {
+            if (component instanceof Point point)
+                result.add(point.getCoordinateSequence());
+            else if (component instanceof LineString line)
+                result.add(line.getCoordinateSequence());
+        });
+        return result;
+    }
+
     public GeometryReverseTest() {
     }
 
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequenceTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequenceTest.java
index e0c0c927db..66e05fb521 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequenceTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/jts/CustomCoordinateSequenceTest.java
@@ -27,6 +27,29 @@ import static org.junit.Assert.*;
 
 public class CustomCoordinateSequenceTest {
 
+    @Test
+    @SuppressWarnings("deprecation")
+    public void clonePreservesDeclaredLayoutAndValues() {
+        for (CustomCoordinateSequence source : new CustomCoordinateSequence[] {
+                new CustomCoordinateSequence(0, 
CoordinateSequenceDimensions.XYZM),
+                new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZM, "100 10 NaN NaN,120 
20 9 8") }) {
+            CustomCoordinateSequence clone = source.clone();
+            assertNotSame(source, clone);
+            assertEquals(CoordinateSequenceDimensions.XYZM, 
clone.getDimensions());
+            assertEquals(4, clone.getDimension());
+            assertEquals(1, clone.getMeasures());
+            assertEquals(source.size(), clone.size());
+            for (int i = 0; i < source.size(); i++) {
+                for (int ordinate = 0; ordinate < 4; ordinate++) {
+                    double original = source.getOrdinate(i, ordinate);
+                    assertEquals(original, clone.getOrdinate(i, ordinate), 0);
+                    clone.setOrdinate(i, ordinate, 42);
+                    assertEquals(original, source.getOrdinate(i, ordinate), 0);
+                }
+            }
+        }
+    }
+
     public CustomCoordinateSequenceTest() {
     }
 

Reply via email to