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());
+ }
+}