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() { }
