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 a14883ca2f GH-4204: Add three GeoSPARQL 1.1 geometry metadata functions
a14883ca2f is described below

commit a14883ca2fbfb4ac59e3f4122bdd662ec92c28e3
Author: Edmond Chuc <[email protected]>
AuthorDate: Sat Sep 26 02:55:41 2026 +1000

    GH-4204: Add three GeoSPARQL 1.1 geometry metadata functions
---
 .../filter_functions/geometry_property/Is3DFF.java |  41 ++++++
 .../geometry_property/IsMeasuredFF.java            |  41 ++++++
 .../geometry_property/NumGeometriesFF.java         |  41 ++++++
 .../geosparql/implementation/GeometryWrapper.java  |  31 +++++
 .../function_registration/GeometryProperty.java    |  16 ++-
 .../geosparql/implementation/vocabulary/Geof.java  |   6 +-
 .../GeometryMetadataErrorsTest.java                | 147 +++++++++++++++++++++
 .../geometry_property/GeometryMetadataFFTest.java  | 145 ++++++++++++++++++++
 .../implementation/GeometryMetadataTest.java       | 114 ++++++++++++++++
 9 files changed, 574 insertions(+), 8 deletions(-)

diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/Is3DFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/Is3DFF.java
new file mode 100644
index 0000000000..d043dd3547
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/Is3DFF.java
@@ -0,0 +1,41 @@
+/*
+ * 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.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+
+/** Implements geof:is3D. */
+public class Is3DFF extends FunctionBase1 {
+
+    @Override
+    public NodeValue exec(NodeValue value) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return NodeValue.makeBoolean(geometry.is3D());
+        } catch (DatatypeFormatException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/IsMeasuredFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/IsMeasuredFF.java
new file mode 100644
index 0000000000..bede084228
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/IsMeasuredFF.java
@@ -0,0 +1,41 @@
+/*
+ * 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.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+
+/** Implements geof:isMeasured. */
+public class IsMeasuredFF extends FunctionBase1 {
+
+    @Override
+    public NodeValue exec(NodeValue value) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return NodeValue.makeBoolean(geometry.isMeasured());
+        } catch (DatatypeFormatException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/NumGeometriesFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/NumGeometriesFF.java
new file mode 100644
index 0000000000..a7ce3b3bd9
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/NumGeometriesFF.java
@@ -0,0 +1,41 @@
+/*
+ * 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.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+
+/** Implements geof:numGeometries. */
+public class NumGeometriesFF extends FunctionBase1 {
+
+    @Override
+    public NodeValue exec(NodeValue value) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return NodeValue.makeInteger(geometry.getNumGeometries());
+        } catch (DatatypeFormatException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
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 6d72d2773b..9336bcc664 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
@@ -387,6 +387,37 @@ public class GeometryWrapper implements Serializable {
         return parsingGeometry.getGeometryType();
     }
 
+    /**
+     * Returns whether the coordinate layout includes Z, including for empty 
geometries.
+     * Uses the wrapper's dimension metadata without aggregating member 
layouts.
+     * A WKT collection without a Z/M marker has XY metadata even if members
+     * declare their own Z/M layouts.
+     */
+    public boolean is3D() {
+        CoordinateSequenceDimensions dimensions = 
getCoordinateSequenceDimensions();
+        return dimensions == CoordinateSequenceDimensions.XYZ || dimensions == 
CoordinateSequenceDimensions.XYZM;
+    }
+
+    /**
+     * Returns whether the coordinate layout includes M, including for empty 
geometries.
+     * Uses the wrapper's dimension metadata without aggregating member 
layouts.
+     * A WKT collection without a Z/M marker has XY metadata even if members
+     * declare their own Z/M layouts.
+     */
+    public boolean isMeasured() {
+        CoordinateSequenceDimensions dimensions = 
getCoordinateSequenceDimensions();
+        return dimensions == CoordinateSequenceDimensions.XYM || dimensions == 
CoordinateSequenceDimensions.XYZM;
+    }
+
+    /**
+     * Returns the number of direct members of a Multi-geometry or 
GeometryCollection.
+     * Atomic geometries count as one, including empty atomic geometries; 
nested
+     * collections are not flattened. A collection with no members counts as 
zero.
+     */
+    public int getNumGeometries() {
+        return parsingGeometry.getNumGeometries();
+    }
+
     /**
      * Returns the minimum ordinate of the first SRS dimension (X)
      * across all geometry members.
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 d22e772dae..3985e40717 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,7 +28,9 @@ 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.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;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsSimpleFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsValidFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MaxXFF;
@@ -37,6 +39,7 @@ import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_prop
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinXFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinYFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinZFF;
+import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.NumGeometriesFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.SpatialDimensionFF;
 import org.apache.jena.geosparql.implementation.vocabulary.Geo;
 import org.apache.jena.geosparql.implementation.vocabulary.Geof;
@@ -44,13 +47,13 @@ import org.apache.jena.sparql.function.FunctionRegistry;
 import org.apache.jena.sparql.pfunction.PropertyFunctionRegistry;
 
 /**
- *
- *
+ * Registers {@code geo:} property functions and {@code geof:}
+ * expression functions with ARQ.
  */
 public class GeometryProperty {
 
     /**
-     * This method loads all the Geometry property property functions.
+     * Registers {@code geo:} property functions with the supplied registry.
      *
      * @param registry - the PropertyFunctionRegistry to be used
      */
@@ -65,14 +68,15 @@ public class GeometryProperty {
     }
 
     /**
-     * Registers {@code geof:} geometry metadata expression functions for use 
in
-     * {@code FILTER}, {@code BIND}, and projection expressions.
-     * Includes GeoSPARQL 1.1 geometry metadata functions and the isValid 
extension.
+     * Registers {@code geof:} expression functions with the supplied registry.
      *
      * @param registry - the FunctionRegistry to be used
      */
     public static void loadFilterFunctions(FunctionRegistry registry) {
 
+        registry.put(Geof.IS_3D, Is3DFF.class);
+        registry.put(Geof.IS_MEASURED, IsMeasuredFF.class);
+        registry.put(Geof.NUM_GEOMETRIES, NumGeometriesFF.class);
         registry.put(Geof.DIMENSION, DimensionFF.class);
         registry.put(Geof.COORDINATE_DIMENSION, CoordinateDimensionFF.class);
         registry.put(Geof.SPATIAL_DIMENSION, SpatialDimensionFF.class);
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 83d19e5dd8..b2a018e44b 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
@@ -74,8 +74,10 @@ public interface Geof {
     public static final String CONVEXHULL_NAME = GEOF_URI + "convexHull";
     public static final String GETSRID_NAME = GEOF_URI + "getSRID";
 
-    //Geometry Property function symbols:
-    //N.B. These functions are not part of the GeoSPARQL standard but have 
been included for convenience using GeometryLiterals.
+    // Geometry property function symbols:
+    public static final String IS_3D = GEOF_URI + "is3D";
+    public static final String IS_MEASURED = GEOF_URI + "isMeasured";
+    public static final String NUM_GEOMETRIES = GEOF_URI + "numGeometries";
     public static final String DIMENSION = GEOF_URI + "dimension";
     public static final String COORDINATE_DIMENSION = GEOF_URI + 
"coordinateDimension";
     public static final String SPATIAL_DIMENSION = GEOF_URI + 
"spatialDimension";
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataErrorsTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataErrorsTest.java
new file mode 100644
index 0000000000..f4f78eb726
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataErrorsTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+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.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GeometryMetadataErrorsTest {
+    @Parameterized.Parameters(name = "function: {0}")
+    public static List<Object[]> functions() {
+        return List.of(
+            new Object[] { "is3D", new Is3DFF() },
+            new Object[] { "isMeasured", new IsMeasuredFF() },
+            new Object[] { "numGeometries", new NumGeometriesFF() }
+        );
+    }
+
+    private final String name;
+    private final FunctionBase1 function;
+
+    public GeometryMetadataErrorsTest(String name, FunctionBase1 function) {
+        this.name = name;
+        this.function = function;
+    }
+
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void iriArgumentRaisesExpressionError() {
+        assertThrows(ExprEvalException.class, () -> 
function.exec(NodeValue.makeNode(NodeFactory.createURI("urn:not-a-literal"))));
+    }
+
+    @Test
+    public void numericArgumentRaisesExpressionError() {
+        assertThrows(ExprEvalException.class, () -> 
function.exec(NodeValue.makeInteger(42)));
+    }
+
+    @Test
+    public void stringWithoutGeometryDatatypeRaisesExpressionError() {
+        assertThrows(ExprEvalException.class, () -> 
function.exec(NodeValue.makeString("POINT (1 2)")));
+    }
+
+    @Test
+    public void malformedWktRaisesExpressionError() {
+        NodeValue malformed = NodeValue.makeNode("invalid", 
WKTDatatype.INSTANCE);
+        assertThrows(ExprEvalException.class, () -> function.exec(malformed));
+    }
+
+    @Test
+    public void iriArgumentLeavesBindUnbound() {
+        assertUnbound("<urn:not-a-literal>");
+    }
+
+    @Test
+    public void numericArgumentLeavesBindUnbound() {
+        assertUnbound("42");
+    }
+
+    @Test
+    public void stringWithoutGeometryDatatypeLeavesBindUnbound() {
+        assertUnbound("'POINT (1 2)'");
+    }
+
+    @Test
+    public void malformedWktLeavesBindUnbound() {
+        assertUnbound("'invalid'^^geo:wktLiteral");
+    }
+
+    @Test
+    public void unboundArgumentLeavesBindUnbound() {
+        assertUnbound("?missing");
+    }
+
+    @Test
+    public void missingArgumentIsRejectedAtQueryBuild() {
+        assertThrows(QueryBuildException.class, () -> evaluate("geof:" + name 
+ "()"));
+    }
+
+    @Test
+    public void extraArgumentIsRejectedAtQueryBuild() {
+        assertThrows(QueryBuildException.class,
+                     () -> evaluate("geof:" + name + "('POINT 
EMPTY'^^geo:wktLiteral, 1)"));
+    }
+
+    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#>
+            SELECT ?result WHERE { BIND(%s AS ?result) }
+            """.formatted(expression);
+        try (QueryExecution execution = QueryExecution.create(query, 
ModelFactory.createDefaultModel())) {
+            ResultSet results = execution.execSelect();
+            assertTrue("Expected one solution for " + expression, 
results.hasNext());
+            QuerySolution solution = results.next();
+            assertFalse("Expected only one solution for " + expression, 
results.hasNext());
+            return solution.contains("result") ? 
solution.get("result").asNode() : null;
+        }
+    }
+
+    private void assertUnbound(String argument) {
+        String expression = "geof:" + name + "(" + argument + ")";
+        assertNull(expression, evaluate(expression));
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataFFTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataFFTest.java
new file mode 100644
index 0000000000..e5c3b7d548
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataFFTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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.assertTrue;
+
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+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.junit.BeforeClass;
+import org.junit.Test;
+
+public class GeometryMetadataFFTest {
+    private static final String PREFIXES = """
+        PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
+        PREFIX geo: <http://www.opengis.net/ont/geosparql#>
+        """;
+
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void is3DDistinguishesZFromM() {
+        assertResult("geof:is3D('POINT Z (1 2 3)'^^geo:wktLiteral)", "true", 
XSDDatatype.XSDboolean);
+        assertResult("geof:is3D('POINT M (1 2 3)'^^geo:wktLiteral)", "false", 
XSDDatatype.XSDboolean);
+    }
+
+    @Test
+    public void isMeasuredDistinguishesMFromZ() {
+        assertResult("geof:isMeasured('POINT M (1 2 3)'^^geo:wktLiteral)", 
"true", XSDDatatype.XSDboolean);
+        assertResult("geof:isMeasured('POINT Z (1 2 3)'^^geo:wktLiteral)", 
"false", XSDDatatype.XSDboolean);
+    }
+
+    @Test
+    public void emptyPointsRetainZAndMFlags() {
+        assertFlags("POINT EMPTY", false, false);
+        assertFlags("POINT Z EMPTY", true, false);
+        assertFlags("POINT M EMPTY", false, true);
+        assertFlags("POINT ZM EMPTY", true, true);
+    }
+
+    @Test
+    public void emptyCollectionsRetainDeclaredZAndMFlags() {
+        assertFlags("GEOMETRYCOLLECTION EMPTY", false, false);
+        assertFlags("GEOMETRYCOLLECTION Z EMPTY", true, false);
+        assertFlags("GEOMETRYCOLLECTION M EMPTY", false, true);
+        assertFlags("GEOMETRYCOLLECTION ZM EMPTY", true, true);
+    }
+
+    @Test
+    public void permissiveWktCollectionLayoutDoesNotAggregateMemberLayouts() {
+        // Mixed member layouts are a Jena WKT extension, not an SFA 
conformance case.
+        assertFlags("GEOMETRYCOLLECTION (POINT Z (1 2 3), POINT M (4 5 6))", 
false, false);
+    }
+
+    @Test
+    public void numGeometriesCountsMembersAsAnInteger() {
+        assertResult("geof:numGeometries('MULTIPOINT ((1 2), (3 
4))'^^geo:wktLiteral)",
+                     "2", XSDDatatype.XSDinteger);
+    }
+
+    @Test
+    public void numGeometriesDistinguishesEmptyAtomsFromCollections() {
+        assertResult("geof:numGeometries('POINT EMPTY'^^geo:wktLiteral)", "1", 
XSDDatatype.XSDinteger);
+        assertResult("geof:numGeometries('GEOMETRYCOLLECTION 
EMPTY'^^geo:wktLiteral)", "0", XSDDatatype.XSDinteger);
+        assertResult("geof:numGeometries('MULTIPOINT EMPTY'^^geo:wktLiteral)", 
"0", XSDDatatype.XSDinteger);
+        assertResult("geof:numGeometries('GEOMETRYCOLLECTION (POINT 
EMPTY)'^^geo:wktLiteral)",
+                     "1", XSDDatatype.XSDinteger);
+    }
+
+    @Test
+    public void gmlPointUsesMetadataFunctions() {
+        String point = """
+            '<gml:Point xmlns:gml="http://www.opengis.net/gml/3.2";
+                        srsName="http://www.opengis.net/def/crs/OGC/1.3/CRS84";>
+               <gml:pos>1 2</gml:pos>
+             </gml:Point>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        assertResult("geof:numGeometries(" + point + ")", "1", 
XSDDatatype.XSDinteger);
+        assertResult("geof:is3D(" + point + ")", "false", 
XSDDatatype.XSDboolean);
+        assertResult("geof:isMeasured(" + point + ")", "false", 
XSDDatatype.XSDboolean);
+        assertResult("geof:isEmpty(" + point + ")", "false", 
XSDDatatype.XSDboolean);
+    }
+
+    @Test
+    public void threeDimensionalGmlPointHasZButNoMeasure() {
+        String point = """
+            '<gml:Point xmlns:gml="http://www.opengis.net/gml/3.2";
+                        srsName="http://www.opengis.net/def/crs/EPSG/0/4979";>
+               <gml:pos srsDimension="3">2 1 3</gml:pos>
+             </gml:Point>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        assertResult("geof:is3D(" + point + ")", "true", 
XSDDatatype.XSDboolean);
+        assertResult("geof:isMeasured(" + point + ")", "false", 
XSDDatatype.XSDboolean);
+        assertResult("geof:isEmpty(" + point + ")", "false", 
XSDDatatype.XSDboolean);
+    }
+
+    private static void assertFlags(String wkt, boolean hasZ, boolean hasM) {
+        assertResult("geof:is3D('" + wkt + "'^^geo:wktLiteral)", 
Boolean.toString(hasZ), XSDDatatype.XSDboolean);
+        assertResult("geof:isMeasured('" + wkt + "'^^geo:wktLiteral)", 
Boolean.toString(hasM), XSDDatatype.XSDboolean);
+    }
+
+    private static void assertResult(String expression, String lexicalForm, 
XSDDatatype datatype) {
+        Node expected = NodeFactory.createLiteralDT(lexicalForm, datatype);
+        assertEquals(expression, expected, evaluate(expression));
+    }
+
+    private static Node evaluate(String expression) {
+        String query = PREFIXES + "SELECT ?result WHERE { BIND(" + expression 
+ " AS ?result) }";
+        try (QueryExecution execution = QueryExecution.create(query, 
ModelFactory.createDefaultModel())) {
+            ResultSet results = execution.execSelect();
+            assertTrue("Expected one solution for " + expression, 
results.hasNext());
+            QuerySolution solution = results.next();
+            assertFalse("Expected only one solution for " + 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/GeometryMetadataTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryMetadataTest.java
new file mode 100644
index 0000000000..1c48994941
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryMetadataTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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.implementation;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.junit.Test;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryFactory;
+
+public class GeometryMetadataTest {
+
+    @Test
+    public void coordinateLayoutsDistinguishZAndM() {
+        assertLayout("POINT (1 2)", false, false);
+        assertLayout("POINT Z (1 2 3)", true, false);
+        assertLayout("POINT M (1 2 3)", false, true);
+        assertLayout("POINT ZM (1 2 3 4)", true, true);
+        assertLayout("<http://www.opengis.net/def/crs/EPSG/0/4979> POINT Z (2 
1 3)", true, false);
+    }
+
+    @Test
+    public void emptyPointsRetainDeclaredCoordinateLayouts() {
+        assertLayout("POINT EMPTY", false, false);
+        assertLayout("POINT Z EMPTY", true, false);
+        assertLayout("POINT M EMPTY", false, true);
+        assertLayout("POINT ZM EMPTY", true, true);
+    }
+
+    @Test
+    public void collectionsUseTheirDeclaredCoordinateLayout() {
+        assertLayout("GEOMETRYCOLLECTION Z (POINT Z (1 2 3), LINESTRING Z (1 2 
3, 4 5 6))", true, false);
+        assertLayout("GEOMETRYCOLLECTION M (POINT M (1 2 3))", false, true);
+        assertLayout("GEOMETRYCOLLECTION ZM (POINT ZM (1 2 3 4))", true, true);
+    }
+
+    @Test
+    public void permissiveWktCollectionLayoutDoesNotAggregateMemberLayouts() {
+        // Mixed member layouts are a Jena WKT extension, not an SFA 
conformance case.
+        String wkt = "GEOMETRYCOLLECTION (POINT Z (1 2 3), POINT M (4 5 6))";
+        // WKT collection metadata comes from the collection marker, not its 
members.
+        GeometryWrapper geometry = geometry(wkt);
+        assertEquals(2, geometry.getCoordinateDimension());
+        assertEquals(2, geometry.getSpatialDimension());
+        assertLayout(wkt, false, false);
+    }
+
+    @Test
+    public void atomicGeometriesCountAsOneIncludingEmpties() {
+        assertCount("POINT (1 2)", 1);
+        assertCount("POINT EMPTY", 1);
+        assertCount("LINESTRING EMPTY", 1);
+        assertCount("POLYGON EMPTY", 1);
+    }
+
+    @Test
+    public void collectionsWithoutMembersCountAsZero() {
+        assertCount("MULTIPOINT EMPTY", 0);
+        assertCount("MULTILINESTRING EMPTY", 0);
+        assertCount("MULTIPOLYGON EMPTY", 0);
+        assertCount("GEOMETRYCOLLECTION EMPTY", 0);
+    }
+
+    @Test
+    public void collectionsCountDirectMembersIncludingEmptyMembers() {
+        assertCount("MULTIPOINT ((1 2), (3 4))", 2);
+        assertCount("MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))", 2);
+        assertCount("MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)))", 1);
+        assertCount("GEOMETRYCOLLECTION (POINT EMPTY)", 1);
+    }
+
+    @Test
+    public void nestedCollectionsAreNotFlattened() {
+        GeometryFactory factory = new GeometryFactory();
+        Geometry point = geometry("POINT (1 2)").getParsingGeometry();
+        Geometry nested = factory.createGeometryCollection(new Geometry[] { 
point, point });
+        Geometry collection = factory.createGeometryCollection(new Geometry[] 
{ point, nested });
+        GeometryWrapper wrapper = new GeometryWrapper(collection, 
WKTDatatype.URI);
+        assertEquals(2, wrapper.getNumGeometries());
+    }
+
+    private static GeometryWrapper geometry(String wkt) {
+        return GeometryWrapper.extract(wkt, WKTDatatype.URI);
+    }
+
+    private static void assertLayout(String wkt, boolean hasZ, boolean hasM) {
+        GeometryWrapper geometry = geometry(wkt);
+        assertEquals(wkt, hasZ, geometry.is3D());
+        assertEquals(wkt, hasM, geometry.isMeasured());
+    }
+
+    private static void assertCount(String wkt, int expected) {
+        assertEquals(wkt, expected, geometry(wkt).getNumGeometries());
+    }
+}

Reply via email to