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


The following commit(s) were added to refs/heads/main by this push:
     new a0f5aa1940 GH-4230: Add GeoSPARQL 1.1 geometryN function
a0f5aa1940 is described below

commit a0f5aa19408bcbbf3454223eed69c8de2126e904
Author: Edmond Chuc <[email protected]>
AuthorDate: Sun Sep 27 18:28:12 2026 +1000

    GH-4230: Add GeoSPARQL 1.1 geometryN function
    
    GH-4230: Add GeoSPARQL 1.1 geometryN function
    
    Add one-based member selection through geof:geometryN, retaining the 
selected member's datatype, SRS and layout. Accept only xsd:integer indexes, 
including integer-derived types such as xsd:short. Round-trip empty members in 
homogeneous WKT Multi geometries, including empty aggregates whose Z/M/ZM 
marker differs from the enclosing collection, and empty GML polygons whose CRS 
is 3D.
    
    Move geometry member tests into the relevant test classes
    
    Put WKT and GML regression tests in their reader and writer test classes, 
and move dimension and geometry selection tests into their existing test 
classes. Preserve the coordinate layout of all-empty GML Surface patches.
---
 .../geometry_property/GeometryNFF.java             |  51 ++++
 .../geosparql/implementation/DimensionInfo.java    |  30 +++
 .../geosparql/implementation/GeometryWrapper.java  |  21 ++
 .../function_registration/GeometryProperty.java    |   6 +-
 .../implementation/parsers/gml/GMLReader.java      |  10 +-
 .../implementation/parsers/gml/GMLWriter.java      |   8 +-
 .../implementation/parsers/wkt/WKTReader.java      | 132 +++++++---
 .../implementation/parsers/wkt/WKTWriter.java      | 101 +++-----
 .../geosparql/implementation/vocabulary/Geof.java  |   3 +
 .../geometry_property/GeometryNFFTest.java         | 283 +++++++++++++++++++++
 .../implementation/DimensionInfoTest.java          |  57 +++++
 .../implementation/GeometryWrapperTest.java        |  87 +++++++
 .../implementation/parsers/gml/GMLReaderTest.java  |  18 ++
 .../implementation/parsers/gml/GMLWriterTest.java  |  54 ++++
 .../implementation/parsers/wkt/WKTReaderTest.java  |  84 ++++++
 .../implementation/parsers/wkt/WKTWriterTest.java  | 100 ++++++++
 16 files changed, 940 insertions(+), 105 deletions(-)

diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFF.java
new file mode 100644
index 0000000000..9807ab6119
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFF.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.ExprEvalTypeException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase2;
+
+/** Implements geof:geometryN. */
+public class GeometryNFF extends FunctionBase2 {
+
+    @Override
+    public NodeValue exec(NodeValue value, NodeValue index) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return geometry.getGeometryN(memberIndex(index)).asNodeValue();
+        } catch (DatatypeFormatException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        } catch (IllegalArgumentException | ArithmeticException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+
+    private static int memberIndex(NodeValue index) {
+        if (!index.isInteger()) {
+            throw new ExprEvalTypeException("Geometry member index must be an 
xsd:integer: " + index);
+        }
+        return index.getInteger().intValueExact();
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/DimensionInfo.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/DimensionInfo.java
index d06566742b..cac4b14ba5 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/DimensionInfo.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/DimensionInfo.java
@@ -28,6 +28,11 @@ import java.util.Objects;
 
 import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
 import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
 import org.locationtech.jts.geom.CoordinateXY;
 import org.locationtech.jts.geom.Geometry;
 
@@ -90,6 +95,31 @@ public class DimensionInfo implements Serializable {
         }
     }
 
+    /**
+     * Finds dimension metadata from coordinate sequences, preserving Z/M 
layouts
+     * even when the sequence is empty. Collections use their first member's
+     * layout, layout retained on an empty aggregate, or the fallback when they
+     * have no members.
+     */
+    public static DimensionInfo find(Geometry geometry, 
CoordinateSequenceDimensions fallback) {
+        CoordinateSequence sequence = null;
+        if (geometry instanceof Point point) {
+            sequence = point.getCoordinateSequence();
+        } else if (geometry instanceof LineString line) {
+            sequence = line.getCoordinateSequence();
+        } else if (geometry instanceof Polygon polygon) {
+            sequence = polygon.getExteriorRing().getCoordinateSequence();
+        } else if (geometry instanceof GeometryCollection collection && 
collection.getNumGeometries() > 0) {
+            fallback = find(collection.getGeometryN(0), 
fallback).getDimensions();
+        } else if (geometry.getUserData() instanceof 
CoordinateSequenceDimensions retained) {
+            // WKTReader stores parsed layout here; empty aggregates have no 
sequence.
+            fallback = retained;
+        }
+        CoordinateSequenceDimensions dimensions = sequence == null ? fallback
+            : findCoordinateSequenceDimensions(sequence.getDimension(), 
sequence.getDimension() - sequence.getMeasures());
+        return new DimensionInfo(dimensions, geometry.getDimension());
+    }
+
     public static DimensionInfo find(Coordinate coordinate, Geometry geometry) 
{
         // Empty geometries have no coordinate; use the existing XY default.
         CoordinateSequenceDimensions coordDims = 
CoordinateSequenceDimensions.find(
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
index d609982e5e..8978171e54 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
@@ -49,6 +49,7 @@ import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.CoordinateXY;
 import org.locationtech.jts.geom.Envelope;
 import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.IntersectionMatrix;
 import org.locationtech.jts.geom.Point;
@@ -475,6 +476,26 @@ public class GeometryWrapper implements Serializable {
         return GeometryCoordinateExtrema.maxZ(this);
     }
 
+    /**
+     * Selects a direct geometry member using a one-based index, without 
flattening
+     * nested collections. Index 1 selects an atomic geometry itself, including
+     * an empty atomic geometry. Retains the source datatype and SRS and uses 
the
+     * selected member's coordinate layout.
+     *
+     * @throws IllegalArgumentException if the index is outside the member 
range.
+     */
+    public GeometryWrapper getGeometryN(int index) {
+        if (index < 1 || index > parsingGeometry.getNumGeometries()) {
+            throw new IllegalArgumentException("Geometry member index is out 
of range: " + index);
+        }
+        if (!(parsingGeometry instanceof GeometryCollection)) {
+            return this;
+        }
+        Geometry member = parsingGeometry.getGeometryN(index - 1);
+        DimensionInfo dimensions = DimensionInfo.find(member, 
dimensionInfo.getDimensions());
+        return new GeometryWrapper(member, getSrsURI(), geometryDatatypeURI, 
dimensions);
+    }
+
     /**
      *
      * @return GeometryDatatype of the literal.
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
index 3985e40717..b595cbadd6 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
@@ -28,6 +28,7 @@ import 
org.apache.jena.geosparql.geo.topological.property_functions.geometry_pro
 import 
org.apache.jena.geosparql.geo.topological.property_functions.geometry_property.SpatialDimensionPF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.CoordinateDimensionFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.DimensionFF;
+import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.GeometryNFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.Is3DFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsEmptyFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsMeasuredFF;
@@ -70,7 +71,7 @@ public class GeometryProperty {
     /**
      * Registers {@code geof:} expression functions with the supplied registry.
      *
-     * @param registry - the FunctionRegistry to be used
+     * @param registry the FunctionRegistry to use
      */
     public static void loadFilterFunctions(FunctionRegistry registry) {
 
@@ -83,13 +84,14 @@ public class GeometryProperty {
         registry.put(Geof.IS_SIMPLE, IsSimpleFF.class);
         registry.put(Geof.IS_EMPTY, IsEmptyFF.class);
         registry.put(Geof.IS_VALID, IsValidFF.class);
-
         registry.put(Geof.MIN_X, MinXFF.class);
         registry.put(Geof.MIN_Y, MinYFF.class);
         registry.put(Geof.MIN_Z, MinZFF.class);
         registry.put(Geof.MAX_X, MaxXFF.class);
         registry.put(Geof.MAX_Y, MaxYFF.class);
         registry.put(Geof.MAX_Z, MaxZFF.class);
+
+        registry.put(Geof.GEOMETRY_N, GeometryNFF.class);
     }
 
 }
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
index e6e470062c..085f32a580 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
@@ -481,7 +481,7 @@ public class GMLReader implements ParserReader {
             Element exteriorLinearRingElement = 
exteriorElement.getChild("LinearRing", GML_NAMESPACE);
             exteriorLinearRing = buildLinearRing(exteriorLinearRingElement, 
dims);
         } else {
-            exteriorLinearRing = GEOMETRY_FACTORY.createLinearRing();
+            exteriorLinearRing = GEOMETRY_FACTORY.createLinearRing(new 
CustomCoordinateSequence(dims));
         }
         //Interior shell - [0..*]
         List<Element> interiorElements = gmlElement.getChildren("interior", 
GML_NAMESPACE);
@@ -536,7 +536,7 @@ public class GMLReader implements ParserReader {
                 Geometry exteriorGeom = buildSurfacePatch(exteriorElement, 
dims, srsInfo);
                 exteriorLinearRing = 
GEOMETRY_FACTORY.createLinearRing(exteriorGeom.getCoordinates());
             } else {
-                exteriorLinearRing = GEOMETRY_FACTORY.createLinearRing();
+                exteriorLinearRing = GEOMETRY_FACTORY.createLinearRing(new 
CustomCoordinateSequence(dims));
             }
 
             //Interior shell - [0..*]
@@ -559,6 +559,12 @@ public class GMLReader implements ParserReader {
             polys.add(polygon);
         }
 
+        // The union of empty patches is a GeometryCollection in JTS.
+        // Return an empty patch directly so its coordinate layout is retained.
+        if (polys.stream().allMatch(Polygon::isEmpty)) {
+            return polys.get(0);
+        }
+
         //Unionise all the polygons on the surface together.
         Geometry unionGeom = CascadedPolygonUnion.union(polys);
         Polygon unionPolygon;
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriter.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriter.java
index 5e25164c8d..3278a8746b 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriter.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriter.java
@@ -182,7 +182,7 @@ public class GMLWriter {
         Element gmlRoot = new Element(multiPoint.getGeometryType(), 
GML_NAMESPACE);
         gmlRoot.setAttribute("srsName", srsName);
 
-        if (!multiPoint.isEmpty()) {
+        if (multiPoint.getNumGeometries() > 0) {
 
             int geomCount = multiPoint.getNumGeometries();
             for (int i = 0; i < geomCount; i++) {
@@ -206,7 +206,7 @@ public class GMLWriter {
         Element gmlRoot = new Element("MultiCurve", GML_NAMESPACE);
         gmlRoot.setAttribute("srsName", srsName);
 
-        if (!multiLineString.isEmpty()) {
+        if (multiLineString.getNumGeometries() > 0) {
 
             int geomCount = multiLineString.getNumGeometries();
             for (int i = 0; i < geomCount; i++) {
@@ -231,7 +231,7 @@ public class GMLWriter {
         Element gmlRoot = new Element("MultiSurface", GML_NAMESPACE);
         gmlRoot.setAttribute("srsName", srsName);
 
-        if (!multiPolygon.isEmpty()) {
+        if (multiPolygon.getNumGeometries() > 0) {
 
             int geomCount = multiPolygon.getNumGeometries();
             for (int i = 0; i < geomCount; i++) {
@@ -256,7 +256,7 @@ public class GMLWriter {
         Element gmlRoot = new Element("MultiGeometry", GML_NAMESPACE);
         gmlRoot.setAttribute("srsName", srsName);
 
-        if (!geometryCollection.isEmpty()) {
+        if (geometryCollection.getNumGeometries() > 0) {
 
             int geomCount = geometryCollection.getNumGeometries();
             for (int i = 0; i < geomCount; i++) {
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReader.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReader.java
index 1069230c21..009ea9b4da 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReader.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReader.java
@@ -20,7 +20,9 @@
  */
 package org.apache.jena.geosparql.implementation.parsers.wkt;
 
+import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.List;
 import java.util.Objects;
 import org.apache.jena.datatypes.DatatypeFormatException;
 import org.apache.jena.geosparql.implementation.DimensionInfo;
@@ -30,9 +32,11 @@ import 
org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory;
 import org.apache.jena.geosparql.implementation.parsers.ParserReader;
 import org.apache.jena.geosparql.implementation.vocabulary.SRS_URI;
 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.Point;
 import org.locationtech.jts.geom.Polygon;
 
 /**
@@ -51,6 +55,7 @@ public class WKTReader implements ParserReader {
     protected WKTReader(String geometryType, String dimensionString, String 
coordinates, String srsURI) {
         this.dims = convertDimensionString(dimensionString);
         this.geometry = buildGeometry(geometryType, coordinates);
+        retainEmptyAggregateLayout(geometry, dims);
         this.dimensionInfo = new DimensionInfo(dims, geometry.getDimension());
         this.srsURI = srsURI;
     }
@@ -125,8 +130,7 @@ public class WKTReader implements ParserReader {
                     geo = buildPolygon(coordinates);
                     break;
                 case "multipoint":
-                    CustomCoordinateSequence multiPointSequence = new 
CustomCoordinateSequence(dims, clean(coordinates));
-                    geo = 
GEOMETRY_FACTORY.createMultiPoint(multiPointSequence);
+                    geo = buildMultiPoint(coordinates);
                     break;
                 case "multilinestring":
                     geo = buildMultiLineString(coordinates);
@@ -146,55 +150,121 @@ public class WKTReader implements ParserReader {
         return geo;
     }
 
+    /** Empty Multi/collection has no sequence for Z/M/ZM; stash the parsed 
layout for DimensionInfo.find. */
+    private static void retainEmptyAggregateLayout(Geometry geometry, 
CoordinateSequenceDimensions dims) {
+        if (geometry instanceof GeometryCollection collection && 
collection.getNumGeometries() == 0) {
+            geometry.setUserData(dims);
+        }
+    }
+
     private String clean(String unclean) {
         return unclean.replace(")", "").replace("(", "").trim();
     }
 
     private Geometry buildGeometryCollection(String coordinates) throws 
DatatypeFormatException {
-
         if (coordinates.isEmpty()) {
             return GEOMETRY_FACTORY.createGeometryCollection(new Geometry[0]);
         }
-
-        //Split coordinates
-        String tidied = coordinates.substring(1, coordinates.length() - 1);
-        tidied = tidied.replaceAll("[\\ ]?,[\\ ]?", ","); //Remove spaces 
around commas
-        String[] partCoordinates = tidied.split("\\),(?=[^\\(])"); //Split 
whenever there is a ), but not ),(
-
-        Geometry[] geometries = new Geometry[partCoordinates.length];
-
-        for (int i = 0; i < partCoordinates.length; i++) {
-            WKTReader partWKTInfo = extract(partCoordinates[i]);
+        String[] members = splitMembers(coordinates);
+        Geometry[] geometries = new Geometry[members.length];
+        for (int i = 0; i < members.length; i++) {
+            WKTReader partWKTInfo = extract(members[i]);
             geometries[i] = partWKTInfo.geometry;
         }
         return GEOMETRY_FACTORY.createGeometryCollection(geometries);
     }
 
-    private Geometry buildMultiLineString(String coordinates) {
+    private Geometry buildMultiPoint(String coordinates) {
+        if (coordinates.isEmpty()) {
+            return GEOMETRY_FACTORY.createMultiPoint(new Point[0]);
+        }
+        String[] members = splitMembers(coordinates);
+        Point[] points = new Point[members.length];
+        for (int i = 0; i < members.length; i++) {
+            points[i] = 
GEOMETRY_FACTORY.createPoint(memberSequence(members[i]));
+        }
+        return GEOMETRY_FACTORY.createMultiPoint(points);
+    }
 
+    private Geometry buildMultiLineString(String coordinates) {
         if (coordinates.isEmpty()) {
             return GEOMETRY_FACTORY.createMultiLineString(new LineString[0]);
         }
-
-        String[] splitCoordinates = splitCoordinates(coordinates);
-        LineString[] lineStrings = splitLineStrings(splitCoordinates);
+        String[] members = splitMembers(coordinates);
+        LineString[] lineStrings = new LineString[members.length];
+        for (int i = 0; i < members.length; i++) {
+            lineStrings[i] = 
GEOMETRY_FACTORY.createLineString(memberSequence(members[i]));
+        }
         return GEOMETRY_FACTORY.createMultiLineString(lineStrings);
     }
 
     private Geometry buildMultiPolygon(String coordinates) {
-
         if (coordinates.isEmpty()) {
             return GEOMETRY_FACTORY.createMultiPolygon(new Polygon[0]);
         }
+        String[] members = splitMembers(coordinates);
+        Polygon[] polygons = new Polygon[members.length];
+        for (int i = 0; i < members.length; i++) {
+            polygons[i] = isEmptyMember(members[i])
+                    ? GEOMETRY_FACTORY.createPolygon(emptySequence())
+                    : buildPolygon(members[i]);
+        }
+        return GEOMETRY_FACTORY.createMultiPolygon(polygons);
+    }
+
+    private CustomCoordinateSequence memberSequence(String member) {
+        return isEmptyMember(member) ? emptySequence() : new 
CustomCoordinateSequence(dims, clean(member));
+    }
 
-        String trimmed = coordinates.replace(")) ,", ")),");
-        String[] multiCoordinates = trimmed.split("\\)\\),");
-        Polygon[] polygons = new Polygon[multiCoordinates.length];
-        for (int i = 0; i < multiCoordinates.length; i++) {
-            polygons[i] = buildPolygon(multiCoordinates[i]);
+    private CustomCoordinateSequence emptySequence() {
+        return new CustomCoordinateSequence(dims);
+    }
+
+    private static boolean isEmptyMember(String member) {
+        return "empty".equals(member.trim());
+    }
+
+    private static String[] splitMembers(String coordinates) {
+        String inner = coordinates.trim();
+        if (inner.startsWith("(") && inner.endsWith(")")) {
+            inner = inner.substring(1, inner.length() - 1);
         }
+        List<String> parts = new ArrayList<>();
+        StringBuilder current = new StringBuilder();
+        int depth = 0;
+        for (int i = 0; i < inner.length(); i++) {
+            char c = inner.charAt(i);
+            if (c == '(') {
+                depth++;
+            } else if (c == ')') {
+                depth--;
+                if (depth < 0) {
+                    throw new DatatypeFormatException("Unbalanced parentheses 
in WKT: " + coordinates);
+                }
+            }
+            if (c == ',' && depth == 0) {
+                addMember(parts, current.toString());
+                current.setLength(0);
+            } else {
+                current.append(c);
+            }
+        }
+        if (depth != 0) {
+            throw new DatatypeFormatException("Unbalanced parentheses in WKT: 
" + coordinates);
+        }
+        String last = current.toString();
+        if (!last.isEmpty() || !parts.isEmpty()) {
+            addMember(parts, last);
+        }
+        return parts.toArray(String[]::new);
+    }
 
-        return GEOMETRY_FACTORY.createMultiPolygon(polygons);
+    private static void addMember(List<String> parts, String raw) {
+        String member = raw.trim();
+        if (member.isEmpty()) {
+            throw new DatatypeFormatException("Missing WKT member; use EMPTY 
for an empty geometry.");
+        }
+        parts.add(member);
     }
 
     private Polygon buildPolygon(String coordinates) {
@@ -227,20 +297,6 @@ public class WKTReader implements ParserReader {
 
     }
 
-    private LineString[] splitLineStrings(String[] splitCoordinates) {
-
-        LineString[] lineStrings = new LineString[splitCoordinates.length];
-
-        for (int i = 0; i < splitCoordinates.length; i++) {
-            CustomCoordinateSequence sequence = new 
CustomCoordinateSequence(dims, clean(splitCoordinates[i]));
-            LineString lineString = 
GEOMETRY_FACTORY.createLineString(sequence);
-            lineStrings[i] = lineString;
-        }
-
-        return lineStrings;
-
-    }
-
     private LinearRing[] splitLinearRings(CoordinateSequenceDimensions dims, 
String[] splitCoordinates) {
 
         LinearRing[] linearRings = new LinearRing[splitCoordinates.length];
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriter.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriter.java
index 1889f994a8..77e6cd13de 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriter.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriter.java
@@ -20,6 +20,7 @@
  */
 package org.apache.jena.geosparql.implementation.parsers.wkt;
 
+import org.apache.jena.geosparql.implementation.DimensionInfo;
 import org.apache.jena.geosparql.implementation.GeometryWrapper;
 import org.apache.jena.geosparql.implementation.SRSInfo;
 import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
@@ -121,10 +122,6 @@ public class WKTWriter {
         return sb.toString();
     }
 
-    private static String buildWKT(final String geometryType, final 
CoordinateSequence coordSeq) {
-        return buildWKT(geometryType, coordSeq, "");
-    }
-
     private static String buildWKT(final String geometryType, final 
CoordinateSequence coordSeq, final String dimensionString) {
 
         CustomCoordinateSequence coordSequence = (CustomCoordinateSequence) 
coordSeq;
@@ -132,9 +129,7 @@ public class WKTWriter {
 
         StringBuilder sb = new StringBuilder(geometryType);
 
-        if (!wktText.equals(" EMPTY")) {
-            sb.append(dimensionString);
-        }
+        sb.append(dimensionString);
 
         sb.append(wktText);
 
@@ -147,12 +142,10 @@ public class WKTWriter {
 
         if (isIncludeGeometryType) {
             sb.append("POLYGON");
+            sb.append(dimensionString);
         }
 
         if (!polygon.isEmpty()) {
-            if (isIncludeGeometryType) {
-                sb.append(dimensionString);
-            }
             sb.append("(");
 
             //Find exterior shell
@@ -172,31 +165,33 @@ public class WKTWriter {
 
             sb.append(")");
         } else {
-            sb.append(" EMPTY");
+            sb.append(isIncludeGeometryType ? " EMPTY" : "EMPTY");
         }
         return sb.toString();
     }
 
+    private static String memberText(final CoordinateSequence coordSeq) {
+        CustomCoordinateSequence coordSequence = (CustomCoordinateSequence) 
coordSeq;
+        if (coordSequence.getSize() == 0) {
+            return "EMPTY";
+        }
+        return convertToWKTText(coordSequence);
+    }
+
     private static String buildMultiPoint(final MultiPoint multiPoint, final 
String dimensionString) {
 
         StringBuilder sb = new StringBuilder("MULTIPOINT");
+        sb.append(dimensionString);
 
-        if (!multiPoint.isEmpty()) {
-
-            sb.append(dimensionString);
+        int geomCount = multiPoint.getNumGeometries();
+        if (geomCount > 0) {
             sb.append("(");
-            //Find first point
-            Point point = (Point) multiPoint.getGeometryN(0);
-            CustomCoordinateSequence coordSequence = 
(CustomCoordinateSequence) point.getCoordinateSequence();
-
-            sb.append(buildWKT("", coordSequence));
-            //Encode remaining points
-            int geomCount = multiPoint.getNumGeometries();
-            for (int i = 1; i < geomCount; i++) {
-                sb.append(", ");
-                point = (Point) multiPoint.getGeometryN(i);
-                coordSequence = (CustomCoordinateSequence) 
point.getCoordinateSequence();
-                sb.append(buildWKT("", coordSequence));
+            for (int i = 0; i < geomCount; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                Point point = (Point) multiPoint.getGeometryN(i);
+                sb.append(memberText(point.getCoordinateSequence()));
             }
             sb.append(")");
         } else {
@@ -209,23 +204,17 @@ public class WKTWriter {
     private static String buildMultiLineString(final MultiLineString 
multiLineString, final String dimensionString) {
 
         StringBuilder sb = new StringBuilder("MULTILINESTRING");
+        sb.append(dimensionString);
 
-        if (!multiLineString.isEmpty()) {
-            sb.append(dimensionString);
+        int geomCount = multiLineString.getNumGeometries();
+        if (geomCount > 0) {
             sb.append("(");
-
-            //Find first linestring
-            LineString lineString = (LineString) 
multiLineString.getGeometryN(0);
-            CustomCoordinateSequence coordSequence = 
(CustomCoordinateSequence) lineString.getCoordinateSequence();
-
-            sb.append(buildWKT("", coordSequence));
-            //Encode remaining points
-            int geomCount = multiLineString.getNumGeometries();
-            for (int i = 1; i < geomCount; i++) {
-                sb.append(", ");
-                lineString = (LineString) multiLineString.getGeometryN(i);
-                coordSequence = (CustomCoordinateSequence) 
lineString.getCoordinateSequence();
-                sb.append(buildWKT("", coordSequence));
+            for (int i = 0; i < geomCount; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                LineString lineString = (LineString) 
multiLineString.getGeometryN(i);
+                sb.append(memberText(lineString.getCoordinateSequence()));
             }
             sb.append(")");
         } else {
@@ -238,21 +227,16 @@ public class WKTWriter {
     private static String buildMultiPolygon(final MultiPolygon multiPolygon, 
final String dimensionString) {
 
         StringBuilder sb = new StringBuilder("MULTIPOLYGON");
+        sb.append(dimensionString);
 
-        if (!multiPolygon.isEmpty()) {
-            sb.append(dimensionString);
+        int geomCount = multiPolygon.getNumGeometries();
+        if (geomCount > 0) {
             sb.append("(");
-
-            //Find first polygon
-            Polygon polygon = (Polygon) multiPolygon.getGeometryN(0);
-
-            sb.append(buildPolygon(polygon, false, dimensionString));
-            //Encode remaining points
-            int geomCount = multiPolygon.getNumGeometries();
-            for (int i = 1; i < geomCount; i++) {
-                sb.append(", ");
-                polygon = (Polygon) multiPolygon.getGeometryN(i);
-
+            for (int i = 0; i < geomCount; i++) {
+                if (i > 0) {
+                    sb.append(", ");
+                }
+                Polygon polygon = (Polygon) multiPolygon.getGeometryN(i);
                 sb.append(buildPolygon(polygon, false, dimensionString));
             }
             sb.append(")");
@@ -266,21 +250,20 @@ public class WKTWriter {
     private static String buildGeometryCollection(final GeometryCollection 
geometryCollection, final CoordinateSequenceDimensions dimensions) {
 
         StringBuilder sb = new StringBuilder("GEOMETRYCOLLECTION");
+        sb.append(CoordinateSequenceDimensions.convertDimensions(dimensions));
 
-        if (!geometryCollection.isEmpty()) {
-            String dimensionString = 
CoordinateSequenceDimensions.convertDimensions(dimensions);
-            sb.append(dimensionString);
+        if (geometryCollection.getNumGeometries() > 0) {
 
             Geometry geometry = geometryCollection.getGeometryN(0);
 
             sb.append("(");
-            sb.append(expand(geometry, dimensions));
+            sb.append(expand(geometry, DimensionInfo.find(geometry, 
dimensions).getDimensions()));
 
             int geomCount = geometryCollection.getNumGeometries();
             for (int i = 1; i < geomCount; i++) {
                 sb.append(", ");
                 geometry = geometryCollection.getGeometryN(i);
-                sb.append(expand(geometry, dimensions));
+                sb.append(expand(geometry, DimensionInfo.find(geometry, 
dimensions).getDimensions()));
             }
             sb.append(")");
         } else {
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
index 00d45ecf9b..abcb958a99 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
@@ -94,6 +94,9 @@ public interface Geof {
     public static final String MAX_Y = GEOF_URI + "maxY";
     public static final String MAX_Z = GEOF_URI + "maxZ";
 
+    //GeoSPARQL 1.1 geometry member access:
+    public static final String GEOMETRY_N = GEOF_URI + "geometryN";
+
     //upcoming GeoSPARQL 1.1 Datatype transformation functions:
     public static final String AS_GEOJSON = GEOF_URI + "asGeoJSON";
 }
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFFTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFFTest.java
new file mode 100644
index 0000000000..3d70b24917
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryNFFTest.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.query.QueryBuildException;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.ExprEvalTypeException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class GeometryNFFTest {
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void selectsFirstAndLastMembersUsingOneBasedIndices() {
+        String wkt = "MULTIPOINT ((1 2), (3 4), (5 6))";
+        assertGeometry("POINT (1 2)", select(wkt, "1"));
+        assertGeometry("POINT (5 6)", select(wkt, "3"));
+    }
+
+    @Test
+    public void acceptsIntegerAndIntegerDerivedIndexTypes() {
+        String wkt = "MULTIPOINT ((1 2), (3 4))";
+        assertGeometry("POINT (3 4)", select(wkt, "2"));
+        assertGeometry("POINT (3 4)", select(wkt, "'2'^^xsd:short"));
+    }
+
+    @Test
+    public void 
nonIntegerNumericIndexesRaiseExpressionErrorsAndLeaveBindUnbound() {
+        String wkt = "MULTIPOINT ((1 2), (3 4))";
+        for (String index : new String[] { "2.0", "2e0", "'2'^^xsd:float" }) {
+            assertNull(index, select(wkt, index));
+        }
+        GeometryNFF function = new GeometryNFF();
+        NodeValue geometry = NodeValue.makeNode(wkt, WKTDatatype.INSTANCE);
+        for (NodeValue index : new NodeValue[] {
+                NodeValue.makeDecimal("2.0"), NodeValue.makeDouble(2e0), 
NodeValue.makeFloat(2f) }) {
+            assertThrows(index.toString(), ExprEvalTypeException.class, () -> 
function.exec(geometry, index));
+        }
+    }
+
+    @Test
+    public void atomicIndexOnePreservesOriginalLiteral() {
+        for (String wkt : new String[] { "POINT (1 2)", "LINESTRING (0 0, 1 
1)",
+                "POLYGON ((0 0, 1 0, 0 1, 0 0))", "POINT Z EMPTY", "POINT M 
EMPTY", "POINT ZM EMPTY" }) {
+            assertEquals(wkt, NodeValue.makeNode(wkt, 
WKTDatatype.INSTANCE).asNode(), select(wkt, "1"));
+        }
+    }
+
+    @Test
+    public void selectedMemberHasItsOwnTopologicalDimension() {
+        assertGeometry("LINESTRING (1 2, 3 4)", select(
+            "GEOMETRYCOLLECTION (POINT (9 9), LINESTRING (1 2, 3 4))", "2"));
+    }
+
+    @Test
+    public void selectsMembersOfMultiLinesAndMultiPolygons() {
+        assertGeometry("LINESTRING (3 4, 5 6)", select("MULTILINESTRING ((0 0, 
1 1), (3 4, 5 6))", "2"));
+        assertGeometry("POLYGON ((0 0, 1 0, 0 1, 0 0))", select("MULTIPOLYGON 
(((0 0, 1 0, 0 1, 0 0)))", "1"));
+    }
+
+    @Test
+    public void selectedMembersRetainZAndM() {
+        for (String marker : new String[] { "Z", "M", "ZM" }) {
+            String coordinates = marker.equals("ZM") ? "1 2 3 4" : "1 2 3";
+            assertGeometry("POINT " + marker + " (" + coordinates + ")",
+                select("GEOMETRYCOLLECTION (POINT " + marker + " (" + 
coordinates + "))", "1"));
+        }
+    }
+
+    @Test
+    public void selectedMultiPointMemberRetainsMeasureWithoutInventingZ() {
+        assertGeometry("POINT M (3 4 9)", select("MULTIPOINT M ((1 2 7), (3 4 
9))", "2"));
+    }
+
+    @Test
+    public void selectedMultiPointMemberRetainsZAndMeasure() {
+        assertGeometry("POINT ZM (3 4 9 11)", select("MULTIPOINT ZM ((1 2 7 
8), (3 4 9 11))", "2"));
+    }
+
+    @Test
+    public void emptySelectedMembersRetainCoordinateMarkers() {
+        for (String marker : new String[] { "Z", "M", "ZM" }) {
+            for (String type : new String[] { "POINT", "LINESTRING", 
"POLYGON", "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON", 
"GEOMETRYCOLLECTION" }) {
+                String member = type + " " + marker + " EMPTY";
+                assertGeometry(member, select("GEOMETRYCOLLECTION (" + member 
+ ", POINT (1 2))", "1"));
+            }
+        }
+    }
+
+    @Test
+    public void authorityAxisOrderAndZSurviveSerialization() {
+        String crs = "<http://www.opengis.net/def/crs/EPSG/0/4979> ";
+        assertGeometry(crs + "POINT Z (10 100 7)", select(crs + "MULTIPOINT Z 
((10 100 7), (20 120 9))", "1"));
+    }
+
+    @Test
+    public void gmlResultRetainsSourceDatatypeAndCrs() {
+        String gml = """
+            '<gml:MultiPoint xmlns:gml="http://www.opengis.net/gml/3.2";
+                srsName="http://www.opengis.net/def/crs/EPSG/0/4979";>
+              <gml:pointMember><gml:Point><gml:pos srsDimension="3">10 100 
7</gml:pos></gml:Point></gml:pointMember>
+            </gml:MultiPoint>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        Node result = evaluate("geof:geometryN(" + gml + ", 1)");
+        assertGeometry("<http://www.opengis.net/def/crs/EPSG/0/4979> POINT Z 
(10 100 7)", result, GMLDatatype.URI);
+    }
+
+    @Test
+    public void selectedEmptyGmlCollectionRetainsItsMember() {
+        String gml = """
+            '<gml:MultiGeometry xmlns:gml="http://www.opengis.net/gml/3.2";
+                srsName="http://www.opengis.net/def/crs/EPSG/0/4979";>
+              <gml:geometryMember><gml:MultiGeometry>
+                <gml:geometryMember><gml:Point/></gml:geometryMember>
+              </gml:MultiGeometry></gml:geometryMember>
+            </gml:MultiGeometry>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        Node result = evaluate("geof:geometryN(" + gml + ", 1)");
+        assertNotNull(result);
+        GeometryWrapper selected = GeometryWrapper.extract(result);
+        assertEquals(1, selected.getParsingGeometry().getNumGeometries());
+        assertEquals("Point", selected.getGeometryN(1).getGeometryType());
+        assertEquals(3, selected.getGeometryN(1).getCoordinateDimension());
+    }
+
+    @Test
+    public void selectedEmptyGmlMultiGeometriesRetainAllMembers() {
+        String[][] types = {
+            { "MultiPoint", "pointMember", "Point", "MultiPoint" },
+            { "MultiCurve", "curveMember", "LineString", "MultiLineString" },
+            { "MultiSurface", "surfaceMember", "Polygon", "MultiPolygon" }
+        };
+        for (String[] type : types) {
+            String member = "<gml:" + type[1] + "><gml:" + type[2] + 
"/></gml:" + type[1] + ">";
+            String gml = """
+                '<gml:MultiGeometry xmlns:gml="http://www.opengis.net/gml/3.2";
+                    srsName="http://www.opengis.net/def/crs/EPSG/0/4979";>
+                  
<gml:geometryMember><gml:%s>%s%s</gml:%s></gml:geometryMember>
+                </gml:MultiGeometry>'^^geo:gmlLiteral
+                """.formatted(type[0], member, member, type[0]).replace("\n", 
" ");
+            GeometryWrapper source = GeometryWrapper.extract(gml.substring(1, 
gml.indexOf("'^^")), GMLDatatype.URI);
+            GeometryWrapper expected = source.getGeometryN(1);
+            assertEquals(type[0], 2, 
expected.getParsingGeometry().getNumGeometries());
+            Node result = evaluate("geof:geometryN(" + gml + ", 1)");
+            assertNotNull(type[0], result);
+            assertEquals("http://www.opengis.net/ont/geosparql#gmlLiteral";, 
result.getLiteralDatatypeURI());
+            GeometryWrapper selected = GeometryWrapper.extract(result);
+            assertEquals(type[3], selected.getGeometryType());
+            assertEquals("http://www.opengis.net/def/crs/EPSG/0/4979";, 
selected.getSrsURI());
+            assertEquals(type[0], 2, 
selected.getParsingGeometry().getNumGeometries());
+            for (int index = 1; index <= 2; index++) {
+                GeometryWrapper child = selected.getGeometryN(index);
+                assertEquals(type[2], child.getGeometryType());
+                assertTrue(child.isEmpty());
+                assertEquals(type[0], CoordinateSequenceDimensions.XYZ, 
child.getCoordinateSequenceDimensions());
+            }
+        }
+    }
+
+    @Test
+    public void invalidIndicesRaiseExpressionErrorsAndLeaveBindUnbound() {
+        for (String index : new String[] { "0", "-1", "3", "1.5", 
"1.00000000000000000001",
+                "2147483648", "999999999999999999999999999999", 
"'NaN'^^xsd:double",
+                "'INF'^^xsd:double", "'-INF'^^xsd:float", "'1'", "true", 
"<urn:index>" }) {
+            assertNull(index, select("MULTIPOINT ((1 2), (3 4))", index));
+        }
+        GeometryNFF function = new GeometryNFF();
+        NodeValue geometry = NodeValue.makeNode("POINT (1 2)", 
WKTDatatype.INSTANCE);
+        for (NodeValue index : new NodeValue[] { NodeValue.makeInteger(0), 
NodeValue.makeInteger(2) }) {
+            assertThrows(ExprEvalException.class, () -> 
function.exec(geometry, index));
+        }
+        for (NodeValue index : new NodeValue[] { NodeValue.makeDecimal("1.5"), 
NodeValue.makeDouble(Double.NaN),
+                NodeValue.makeString("1") }) {
+            assertThrows(ExprEvalTypeException.class, () -> 
function.exec(geometry, index));
+        }
+    }
+
+    @Test
+    public void emptyCollectionsHaveNoMemberAtIndexOne() {
+        for (String wkt : new String[] { "MULTIPOINT EMPTY", "MULTILINESTRING 
EMPTY", "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY" }) {
+            assertNull(wkt, select(wkt, "1"));
+        }
+    }
+
+    @Test
+    public void invalidGeometriesRaiseExpressionErrors() {
+        GeometryNFF function = new GeometryNFF();
+        for (NodeValue value : new NodeValue[] { NodeValue.makeString("POINT 
(1 2)"), NodeValue.makeInteger(1),
+                NodeValue.makeNode(NodeFactory.createURI("urn:geometry")), 
NodeValue.makeNode("invalid", WKTDatatype.INSTANCE) }) {
+            assertThrows(ExprEvalException.class, () -> function.exec(value, 
NodeValue.makeInteger(1)));
+        }
+        for (String value : new String[] { "'POINT (1 2)'", "42", 
"<urn:geometry>", "'invalid'^^geo:wktLiteral", "?missing" }) {
+            assertNull(evaluate("geof:geometryN(" + value + ", 1)"));
+        }
+        assertNull(select("POINT (1 2)", "?missing"));
+    }
+
+    @Test
+    public void wrongArityIsRejectedAtQueryBuild() {
+        for (String arguments : new String[] { "", "'POINT 
EMPTY'^^geo:wktLiteral", "'POINT EMPTY'^^geo:wktLiteral, 1, 2" }) {
+            assertThrows(QueryBuildException.class, () -> 
evaluate("geof:geometryN(" + arguments + ")"));
+        }
+    }
+
+    private static Node select(String wkt, String index) {
+        return evaluate("geof:geometryN('" + wkt + "'^^geo:wktLiteral, " + 
index + ")");
+    }
+
+    private static void assertGeometry(String expectedWkt, Node result) {
+        assertGeometry(expectedWkt, result, WKTDatatype.URI);
+    }
+
+    private static void assertGeometry(String expectedWkt, Node result, String 
datatypeURI) {
+        assertNotNull(expectedWkt, result);
+        assertEquals(expectedWkt, datatypeURI, result.getLiteralDatatypeURI());
+        GeometryWrapper expected = GeometryWrapper.extract(expectedWkt, 
WKTDatatype.URI);
+        GeometryWrapper actual = GeometryWrapper.extract(result);
+        assertEquals(expected.getSrsURI(), actual.getSrsURI());
+        assertEquals(expected.getCoordinateSequenceDimensions(), 
actual.getCoordinateSequenceDimensions());
+        assertEquals(expected.getTopologicalDimension(), 
actual.getTopologicalDimension());
+        assertEquals(expected.getParsingGeometry().toText(), 
actual.getParsingGeometry().toText());
+        // Serialize both through WKT to compare Z/M ordinates across input 
datatypes.
+        
assertEquals(org.apache.jena.geosparql.implementation.parsers.wkt.WKTWriter.write(expected),
+                     
org.apache.jena.geosparql.implementation.parsers.wkt.WKTWriter.write(actual));
+    }
+
+    private static Node evaluate(String expression) {
+        String query = """
+            PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
+            PREFIX geo: <http://www.opengis.net/ont/geosparql#>
+            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+            SELECT ?result WHERE { BIND(%s AS ?result) }
+            """.formatted(expression);
+        try (QueryExecution execution = QueryExecution.create(query, 
ModelFactory.createDefaultModel())) {
+            ResultSet results = execution.execSelect();
+            assertTrue(expression, results.hasNext());
+            QuerySolution solution = results.next();
+            assertFalse(expression, results.hasNext());
+            return solution.contains("result") ? 
solution.get("result").asNode() : null;
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/DimensionInfoTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/DimensionInfoTest.java
index a169a35921..8abc3dd492 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/DimensionInfoTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/DimensionInfoTest.java
@@ -22,6 +22,10 @@ package org.apache.jena.geosparql.implementation;
 
 import java.util.Arrays;
 import java.util.List;
+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.parsers.wkt.WKTReader;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.*;
@@ -102,4 +106,57 @@ public class DimensionInfoTest {
         assertEquals(expResult, result);
     }
 
+    @Test
+    public void testFindGeometryPreservesEmptyCoordinateSequenceLayout() {
+        GeometryFactory factory = CustomGeometryFactory.theInstance();
+        Geometry[] geometries = {
+            factory.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZ)),
+            factory.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYM)),
+            factory.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZM))
+        };
+        CoordinateSequenceDimensions[] expected = {
+            CoordinateSequenceDimensions.XYZ,
+            CoordinateSequenceDimensions.XYM,
+            CoordinateSequenceDimensions.XYZM
+        };
+
+        for (int i = 0; i < geometries.length; i++) {
+            assertEquals(geometries[i].getGeometryType(), expected[i],
+                    DimensionInfo.find(geometries[i], 
CoordinateSequenceDimensions.XY).getDimensions());
+        }
+    }
+
+    @Test
+    public void testFindGeometryUsesLayoutRetainedOnEmptyAggregate() {
+        String[] wkts = {
+            "MULTIPOINT Z EMPTY", "MULTILINESTRING M EMPTY",
+            "MULTIPOLYGON ZM EMPTY", "GEOMETRYCOLLECTION Z EMPTY"
+        };
+        CoordinateSequenceDimensions[] expected = {
+            CoordinateSequenceDimensions.XYZ,
+            CoordinateSequenceDimensions.XYM,
+            CoordinateSequenceDimensions.XYZM,
+            CoordinateSequenceDimensions.XYZ
+        };
+
+        for (int i = 0; i < wkts.length; i++) {
+            Geometry geometry = WKTReader.extract(wkts[i]).getGeometry();
+            assertEquals(wkts[i], expected[i],
+                    DimensionInfo.find(geometry, 
CoordinateSequenceDimensions.XY).getDimensions());
+        }
+    }
+
+    @Test
+    public void testFindGeometryUsesFirstCollectionMemberLayout() {
+        GeometryFactory factory = CustomGeometryFactory.theInstance();
+        Point measured = factory.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYM, "1 2 3"));
+        Point elevated = factory.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZ, "4 5 6"));
+        Geometry collection = factory.createGeometryCollection(new Geometry[] 
{ measured, elevated });
+
+        assertEquals(CoordinateSequenceDimensions.XYM,
+                DimensionInfo.find(collection, 
CoordinateSequenceDimensions.XY).getDimensions());
+        assertEquals(CoordinateSequenceDimensions.XYZ,
+                DimensionInfo.find(elevated, 
CoordinateSequenceDimensions.XY).getDimensions());
+    }
+
 }
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryWrapperTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryWrapperTest.java
index 79d46b3603..066ba38802 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryWrapperTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryWrapperTest.java
@@ -22,6 +22,7 @@ package org.apache.jena.geosparql.implementation;
 
 import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
 import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
 import org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory;
 import org.apache.jena.geosparql.implementation.vocabulary.SRS_URI;
 import org.apache.jena.geosparql.implementation.vocabulary.Unit_URI;
@@ -32,12 +33,15 @@ import org.apache.sis.referencing.CRS;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.locationtech.jts.geom.Coordinate;
 import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.Point;
 import org.opengis.geometry.MismatchedDimensionException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
@@ -526,5 +530,88 @@ public class GeometryWrapperTest {
         String result = instance.getUTMZoneURI();
         assertEquals(expResult, result);
     }
+
+    @Test
+    public void testGetGeometryNOnAtomicGeometry() {
+        GeometryWrapper point = GeometryWrapper.extract("POINT Z EMPTY", 
WKTDatatype.URI);
+        GeometryWrapper selected = point.getGeometryN(1);
+
+        assertEquals(point.getGeometryDatatypeURI(), 
selected.getGeometryDatatypeURI());
+        assertEquals(point.getSrsURI(), selected.getSrsURI());
+        assertEquals(point.getCoordinateSequenceDimensions(), 
selected.getCoordinateSequenceDimensions());
+        assertEquals("Point", selected.getGeometryType());
+        assertTrue(selected.isEmpty());
+        assertThrows(IllegalArgumentException.class, () -> 
point.getGeometryN(0));
+        assertThrows(IllegalArgumentException.class, () -> 
point.getGeometryN(2));
+    }
+
+    @Test
+    public void testGetGeometryNSelectsDirectNestedMember() {
+        GeometryWrapper source = GeometryWrapper.extract(
+                "GEOMETRYCOLLECTION (POINT (9 9), GEOMETRYCOLLECTION (POINT (1 
2), POINT (3 4)))",
+                WKTDatatype.URI);
+        GeometryWrapper selected = source.getGeometryN(2);
+
+        assertEquals("GeometryCollection", selected.getGeometryType());
+        assertEquals(2, selected.getParsingGeometry().getNumGeometries());
+        assertEquals(3.0, 
selected.getGeometryN(2).getParsingGeometry().getCoordinate().getX(), 0.0);
+        assertThrows(IllegalArgumentException.class, () -> 
source.getGeometryN(3));
+    }
+
+    @Test
+    public void testGetGeometryNUsesSelectedMemberLayoutAndTopology() {
+        GeometryWrapper source = GeometryWrapper.extract(
+                "GEOMETRYCOLLECTION (POINT Z (1 2 3), LINESTRING M (1 2 7, 3 4 
9))",
+                WKTDatatype.URI);
+        GeometryWrapper point = source.getGeometryN(1);
+        GeometryWrapper line = source.getGeometryN(2);
+
+        assertEquals(CoordinateSequenceDimensions.XYZ, 
point.getCoordinateSequenceDimensions());
+        assertEquals(CoordinateSequenceDimensions.XYM, 
line.getCoordinateSequenceDimensions());
+        assertEquals(0, point.getTopologicalDimension());
+        assertEquals(1, line.getTopologicalDimension());
+        assertEquals(source.getGeometryDatatypeURI(), 
line.getGeometryDatatypeURI());
+        assertEquals(source.getSrsURI(), line.getSrsURI());
+    }
+
+    @Test
+    public void testGetGeometryNRetainsExplicitLayoutOfEmptyAggregate() {
+        String[] members = {
+            "MULTIPOINT Z EMPTY", "MULTILINESTRING M EMPTY",
+            "MULTIPOLYGON ZM EMPTY", "GEOMETRYCOLLECTION Z EMPTY"
+        };
+        for (String member : members) {
+            GeometryWrapper selected = GeometryWrapper.extract(
+                    "GEOMETRYCOLLECTION (" + member + ", POINT (1 2))", 
WKTDatatype.URI).getGeometryN(1);
+            GeometryWrapper expected = GeometryWrapper.extract(member, 
WKTDatatype.URI);
+            assertEquals(member, expected.getCoordinateSequenceDimensions(), 
selected.getCoordinateSequenceDimensions());
+            assertEquals(member, expected.getGeometryType(), 
selected.getGeometryType());
+        }
+    }
+
+    @Test
+    public void testGetGeometryNPreservesZWhenNormalizingAuthorityAxisOrder() {
+        GeometryWrapper selected = GeometryWrapper.extract(
+                "<http://www.opengis.net/def/crs/EPSG/0/4979> MULTIPOINT Z 
((10 100 7), (20 120 9))",
+                WKTDatatype.URI).getGeometryN(2);
+        Point parsing = (Point) selected.getParsingGeometry();
+        Point normalized = (Point) selected.getXYGeometry();
+
+        assertEquals(CoordinateSequenceDimensions.XYZ, 
selected.getCoordinateSequenceDimensions());
+        assertEquals(20.0, parsing.getX(), 0.0);
+        assertEquals(120.0, parsing.getY(), 0.0);
+        assertEquals(9.0, parsing.getCoordinateSequence().getZ(0), 0.0);
+        assertEquals(120.0, normalized.getX(), 0.0);
+        assertEquals(20.0, normalized.getY(), 0.0);
+        assertEquals(9.0, normalized.getCoordinateSequence().getZ(0), 0.0);
+    }
+
+    @Test
+    public void testGetGeometryNRejectsIndicesForEmptyCollection() {
+        GeometryWrapper empty = GeometryWrapper.extract("GEOMETRYCOLLECTION 
EMPTY", WKTDatatype.URI);
+        assertThrows(IllegalArgumentException.class, () -> 
empty.getGeometryN(1));
+        assertThrows(IllegalArgumentException.class, () -> 
empty.getGeometryN(-1));
+        assertThrows(IllegalArgumentException.class, () -> 
empty.getGeometryN(Integer.MAX_VALUE));
+    }
     
 }
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReaderTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReaderTest.java
index 97e397b49e..3b467e070c 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReaderTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReaderTest.java
@@ -32,6 +32,7 @@ import org.jdom2.JDOMException;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -615,6 +616,23 @@ public class GMLReaderTest {
         assertEquals(expResult, result);
     }
 
+    @Test
+    public void testEmptyPolygonRetainsThreeDimensionalCrsLayout() throws 
JDOMException, IOException {
+        String[] gml = {
+            "<gml:Polygon xmlns:gml=\"http://www.opengis.net/gml/3.2\"; 
srsName=\"http://www.opengis.net/def/crs/EPSG/0/4979\"; />",
+            "<gml:Surface xmlns:gml=\"http://www.opengis.net/gml/3.2\"; 
srsName=\"http://www.opengis.net/def/crs/EPSG/0/4979\";><gml:patches><gml:PolygonPatch
 /></gml:patches></gml:Surface>",
+            "<gml:Surface xmlns:gml=\"http://www.opengis.net/gml/3.2\"; 
srsName=\"http://www.opengis.net/def/crs/EPSG/0/4979\";><gml:patches><gml:PolygonPatch
 /><gml:PolygonPatch /></gml:patches></gml:Surface>"
+        };
+
+        for (String literal : gml) {
+            Geometry geometry = GMLReader.extract(literal).getGeometry();
+            assertEquals(literal, "Polygon", geometry.getGeometryType());
+            assertTrue(literal, geometry.isEmpty());
+            assertEquals(literal, CoordinateSequenceDimensions.XYZ,
+                    DimensionInfo.find(geometry, 
CoordinateSequenceDimensions.XY).getDimensions());
+        }
+    }
+
     /**
      * Test of buildMultiPointEmpty method, of class GMLReader.
      *
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriterTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriterTest.java
index 2dcce2b560..db7ef964e4 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriterTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLWriterTest.java
@@ -20,6 +20,7 @@
  */
 package org.apache.jena.geosparql.implementation.parsers.gml;
 
+import java.io.IOException;
 import org.apache.jena.geosparql.implementation.DimensionInfo;
 import org.apache.jena.geosparql.implementation.GeometryWrapper;
 import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
@@ -27,9 +28,12 @@ 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.jdom2.JDOMException;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -37,6 +41,7 @@ import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.LineString;
 import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
 
 /**
@@ -292,4 +297,53 @@ public class GMLWriterTest {
         assertEquals(expResult, result);
     }
 
+    @Test
+    public void testWriteMultiGeometriesContainingOnlyEmptyMembers() throws 
JDOMException, IOException {
+        Point emptyPoint = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        LineString emptyLine = GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Polygon emptyPolygon = GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Geometry[] geometries = {
+            GEOMETRY_FACTORY.createMultiPoint(new Point[] { emptyPoint, 
emptyPoint }),
+            GEOMETRY_FACTORY.createMultiLineString(new LineString[] { 
emptyLine, emptyLine }),
+            GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] { emptyPolygon, 
emptyPolygon }),
+            GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] { 
emptyPoint, emptyLine })
+        };
+
+        for (Geometry geometry : geometries) {
+            GeometryWrapper wrapper = new GeometryWrapper(geometry, 
SRS_URI.WGS84_CRS, GMLDatatype.URI,
+                    new DimensionInfo(2, 2, geometry.getDimension()));
+            Geometry reparsed = 
GMLReader.extract(GMLWriter.write(wrapper)).getGeometry();
+            assertEquals(geometry.getGeometryType(), 2, 
reparsed.getNumGeometries());
+            assertEquals(geometry.getGeometryType(), 
geometry.getGeometryType(), reparsed.getGeometryType());
+            assertTrue(geometry.getGeometryType(), 
reparsed.getGeometryN(0).isEmpty());
+            assertTrue(geometry.getGeometryType(), 
reparsed.getGeometryN(1).isEmpty());
+        }
+    }
+
+    @Test
+    public void testWriteMultiGeometriesWithMixedEmptyMembers() throws 
JDOMException, IOException {
+        Point emptyPoint = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        LineString emptyLine = GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Polygon emptyPolygon = GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Geometry[] geometries = {
+            GEOMETRY_FACTORY.createMultiPoint(new Point[] {
+                GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY, "1 2")), emptyPoint 
}),
+            GEOMETRY_FACTORY.createMultiLineString(new LineString[] {
+                GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY, "0 0, 1 1")), 
emptyLine }),
+            GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {
+                GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY,
+                        "0 0, 1 0, 0 1, 0 0")), emptyPolygon })
+        };
+
+        for (Geometry geometry : geometries) {
+            GeometryWrapper wrapper = new GeometryWrapper(geometry, 
SRS_URI.WGS84_CRS, GMLDatatype.URI,
+                    new DimensionInfo(2, 2, geometry.getDimension()));
+            Geometry reparsed = 
GMLReader.extract(GMLWriter.write(wrapper)).getGeometry();
+            assertEquals(geometry.getGeometryType(), 2, 
reparsed.getNumGeometries());
+            assertEquals(geometry.getGeometryType(), 
geometry.getGeometryType(), reparsed.getGeometryType());
+            assertFalse(geometry.getGeometryType(), 
reparsed.getGeometryN(0).isEmpty());
+            assertTrue(geometry.getGeometryType(), 
reparsed.getGeometryN(1).isEmpty());
+        }
+    }
+
 }
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReaderTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReaderTest.java
index 7e02359666..78cd465cb5 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReaderTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTReaderTest.java
@@ -20,6 +20,7 @@
  */
 package org.apache.jena.geosparql.implementation.parsers.wkt;
 
+import org.apache.jena.datatypes.DatatypeFormatException;
 import org.apache.jena.geosparql.implementation.DimensionInfo;
 import 
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
 import org.apache.jena.geosparql.implementation.jts.CustomCoordinateSequence;
@@ -27,6 +28,9 @@ import 
org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory;
 import org.junit.After;
 import org.junit.AfterClass;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -363,6 +367,86 @@ public class WKTReaderTest {
         assertEquals(expResult, result);
     }
 
+    @Test
+    public void testExtractMultiGeometriesWithEmptyMembers() {
+        String[] wkts = {
+            "MULTIPOINT ZM ((1 2 3 4), EMPTY, (5 6 7 8))",
+            "MULTILINESTRING ZM ((1 2 3 4, 2 3 4 5), EMPTY, (5 6 7 8, 6 7 8 
9))",
+            "MULTIPOLYGON ZM (((1 2 3 4, 2 2 3 4, 1 3 3 4, 1 2 3 4)), EMPTY, 
((5 6 7 8, 6 6 7 8, 5 7 7 8, 5 6 7 8)))"
+        };
+
+        for (String wkt : wkts) {
+            Geometry multi = WKTReader.extract(wkt).getGeometry();
+            assertEquals(wkt, 3, multi.getNumGeometries());
+            assertFalse(wkt, multi.getGeometryN(0).isEmpty());
+            assertTrue(wkt, multi.getGeometryN(1).isEmpty());
+            assertFalse(wkt, multi.getGeometryN(2).isEmpty());
+            assertEquals(wkt, 1.0, 
multi.getGeometryN(0).getCoordinate().getX(), 0.0);
+            assertEquals(wkt, 5.0, 
multi.getGeometryN(2).getCoordinate().getX(), 0.0);
+            assertEquals(wkt, CoordinateSequenceDimensions.XYZM,
+                    DimensionInfo.find(multi.getGeometryN(1), 
CoordinateSequenceDimensions.XY).getDimensions());
+        }
+    }
+
+    @Test
+    public void testExtractNestedCollectionWithoutFlatteningMembers() {
+        Geometry collection = WKTReader.extract(
+                "GEOMETRYCOLLECTION (MULTIPOINT (EMPTY, (1 2)), POINT (3 
4))").getGeometry();
+
+        assertEquals(2, collection.getNumGeometries());
+        Geometry multi = collection.getGeometryN(0);
+        assertEquals("MultiPoint", multi.getGeometryType());
+        assertEquals(2, multi.getNumGeometries());
+        assertTrue(multi.getGeometryN(0).isEmpty());
+        assertEquals(1.0, multi.getGeometryN(1).getCoordinate().getX(), 0.0);
+        assertEquals(3.0, collection.getGeometryN(1).getCoordinate().getX(), 
0.0);
+    }
+
+    @Test
+    public void testExtractAllEmptyMembersRetainsCoordinateMarkers() {
+        String[] markers = { "Z", "M", "ZM" };
+        CoordinateSequenceDimensions[] layouts = {
+            CoordinateSequenceDimensions.XYZ,
+            CoordinateSequenceDimensions.XYM,
+            CoordinateSequenceDimensions.XYZM
+        };
+        for (int i = 0; i < markers.length; i++) {
+            for (String type : new String[] { "MULTIPOINT", "MULTILINESTRING", 
"MULTIPOLYGON" }) {
+                Geometry multi = WKTReader.extract(type + " " + markers[i] + " 
(EMPTY, EMPTY)").getGeometry();
+                assertEquals(type, 2, multi.getNumGeometries());
+                for (int member = 0; member < 2; member++) {
+                    assertTrue(type, multi.getGeometryN(member).isEmpty());
+                    assertEquals(type, layouts[i], 
DimensionInfo.find(multi.getGeometryN(member),
+                            CoordinateSequenceDimensions.XY).getDimensions());
+                }
+            }
+        }
+    }
+
+    @Test
+    public void testRejectMissingMultiGeometryMembers() {
+        for (String wkt : new String[] {
+                "MULTIPOINT ((1 2),, (3 4))", "MULTIPOINT (, (1 2))", 
"MULTIPOINT ((1 2),)",
+                "MULTILINESTRING ((0 0, 1 1),, (2 2, 3 3))", "MULTILINESTRING 
(, (0 0, 1 1))",
+                "MULTILINESTRING ((0 0, 1 1),)",
+                "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)),, ((2 2, 3 2, 2 3, 2 
2)))",
+                "MULTIPOLYGON (, ((0 0, 1 0, 0 1, 0 0)))",
+                "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)),)" }) {
+            assertThrows(wkt, DatatypeFormatException.class, () -> 
WKTReader.extract(wkt));
+        }
+    }
+
+    @Test
+    public void testRejectUnbalancedMultiGeometryParentheses() {
+        for (String wkt : new String[] {
+                "MULTIPOINT ((1 2)", "MULTIPOINT ((1 2)))",
+                "MULTILINESTRING ((0 0, 1 1)", "MULTILINESTRING ((0 0, 1 1)))",
+                "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0))",
+                "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0))))" }) {
+            assertThrows(wkt, DatatypeFormatException.class, () -> 
WKTReader.extract(wkt));
+        }
+    }
+
     /**
      * Test of buildPointEmpty method, of class WKTReader.
      */
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriterTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriterTest.java
index 80044d1b29..c87d92b33c 100644
--- 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriterTest.java
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/parsers/wkt/WKTWriterTest.java
@@ -37,6 +37,7 @@ import org.locationtech.jts.geom.Geometry;
 import org.locationtech.jts.geom.GeometryFactory;
 import org.locationtech.jts.geom.LineString;
 import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.Point;
 import org.locationtech.jts.geom.Polygon;
 
 /**
@@ -418,4 +419,103 @@ public class WKTWriterTest {
         assertEquals(expResult, result);
     }
 
+    @Test
+    public void testWriteMultiGeometriesContainingOnlyEmptyMembers() {
+        Point emptyPoint = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        LineString emptyLine = GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Polygon emptyPolygon = GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Geometry[] geometries = {
+            GEOMETRY_FACTORY.createMultiPoint(new Point[] { emptyPoint, 
emptyPoint }),
+            GEOMETRY_FACTORY.createMultiLineString(new LineString[] { 
emptyLine, emptyLine }),
+            GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] { emptyPolygon, 
emptyPolygon }),
+            GEOMETRY_FACTORY.createGeometryCollection(new Geometry[] { 
emptyPoint, emptyLine })
+        };
+        String[] expected = {
+            "MULTIPOINT(EMPTY, EMPTY)",
+            "MULTILINESTRING(EMPTY, EMPTY)",
+            "MULTIPOLYGON(EMPTY, EMPTY)",
+            "GEOMETRYCOLLECTION(POINT EMPTY, LINESTRING EMPTY)"
+        };
+
+        for (int i = 0; i < geometries.length; i++) {
+            GeometryWrapper wrapper = new GeometryWrapper(geometries[i], 
SRS_URI.DEFAULT_WKT_CRS84,
+                    WKTDatatype.URI, new DimensionInfo(2, 2, 
geometries[i].getDimension()));
+            assertEquals(expected[i], WKTWriter.write(wrapper));
+        }
+    }
+
+    @Test
+    public void testWriteEmptyGeometryRetainsCoordinateMarker() {
+        Point emptyPoint = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XYZM));
+        GeometryWrapper wrapper = new GeometryWrapper(emptyPoint, 
SRS_URI.DEFAULT_WKT_CRS84,
+                WKTDatatype.URI, new 
DimensionInfo(CoordinateSequenceDimensions.XYZM, 0));
+
+        assertEquals("POINT ZM EMPTY", WKTWriter.write(wrapper));
+    }
+
+    @Test
+    public void testWriteAllEmptyMultiGeometriesRetainsCoordinateMarkers() {
+        String[] markers = { "Z", "M", "ZM" };
+        CoordinateSequenceDimensions[] layouts = {
+            CoordinateSequenceDimensions.XYZ,
+            CoordinateSequenceDimensions.XYM,
+            CoordinateSequenceDimensions.XYZM
+        };
+        for (int i = 0; i < markers.length; i++) {
+            Point point = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(layouts[i]));
+            LineString line = GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(layouts[i]));
+            Polygon polygon = GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(layouts[i]));
+            Geometry[] geometries = {
+                GEOMETRY_FACTORY.createMultiPoint(new Point[] { point, point 
}),
+                GEOMETRY_FACTORY.createMultiLineString(new LineString[] { 
line, line }),
+                GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] { polygon, 
polygon })
+            };
+            String[] types = { "MULTIPOINT", "MULTILINESTRING", "MULTIPOLYGON" 
};
+            for (int j = 0; j < geometries.length; j++) {
+                Geometry geometry = geometries[j];
+                GeometryWrapper wrapper = new GeometryWrapper(geometry, 
SRS_URI.DEFAULT_WKT_CRS84,
+                        WKTDatatype.URI, new DimensionInfo(layouts[i], 
geometry.getDimension()));
+                assertEquals(types[j], types[j] + " " + markers[i] + "(EMPTY, 
EMPTY)", WKTWriter.write(wrapper));
+            }
+        }
+    }
+
+    @Test
+    public void testWriteMixedEmptyMultiGeometryMembers() {
+        Point emptyPoint = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        LineString emptyLine = GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Polygon emptyPolygon = GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY));
+        Geometry[] geometries = {
+            GEOMETRY_FACTORY.createMultiPoint(new Point[] {
+                GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY, "1 2")), emptyPoint 
}),
+            GEOMETRY_FACTORY.createMultiLineString(new LineString[] {
+                GEOMETRY_FACTORY.createLineString(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY, "0 0, 1 1")), 
emptyLine }),
+            GEOMETRY_FACTORY.createMultiPolygon(new Polygon[] {
+                GEOMETRY_FACTORY.createPolygon(new 
CustomCoordinateSequence(CoordinateSequenceDimensions.XY,
+                        "0 0, 1 0, 0 1, 0 0")), emptyPolygon })
+        };
+        String[] expected = {
+            "MULTIPOINT((1 2), EMPTY)",
+            "MULTILINESTRING((0 0, 1 1), EMPTY)",
+            "MULTIPOLYGON(((0 0, 1 0, 0 1, 0 0)), EMPTY)"
+        };
+        for (int i = 0; i < geometries.length; i++) {
+            GeometryWrapper wrapper = new GeometryWrapper(geometries[i], 
SRS_URI.DEFAULT_WKT_CRS84,
+                    WKTDatatype.URI, new DimensionInfo(2, 2, 
geometries[i].getDimension()));
+            assertEquals(expected[i], WKTWriter.write(wrapper));
+        }
+    }
+
+    @Test
+    public void testWriteCollectionRetainsMemberSpecificLayouts() {
+        Geometry emptyMeasuredMulti = WKTReader.extract("MULTILINESTRING M 
EMPTY").getGeometry();
+        Point elevated = GEOMETRY_FACTORY.createPoint(new 
CustomCoordinateSequence(
+                CoordinateSequenceDimensions.XYZ, "1 2 3"));
+        Geometry collection = GEOMETRY_FACTORY.createGeometryCollection(new 
Geometry[] { emptyMeasuredMulti, elevated });
+        GeometryWrapper wrapper = new GeometryWrapper(collection, 
SRS_URI.DEFAULT_WKT_CRS84,
+                WKTDatatype.URI, new 
DimensionInfo(CoordinateSequenceDimensions.XY, collection.getDimension()));
+
+        assertEquals("GEOMETRYCOLLECTION(MULTILINESTRING M EMPTY, POINT Z(1 2 
3))", WKTWriter.write(wrapper));
+    }
+
 }

Reply via email to