diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java
new file mode 100644
index 00000000000..000766d1eec
--- /dev/null
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java
@@ -0,0 +1,42 @@
+/*
+ * 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.datatypes.xsd.XSDDatatype;
+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:geometryType. */
+public class GeometryTypeFF extends FunctionBase1 {
+
+ @Override
+ public NodeValue exec(NodeValue value) {
+ try {
+ GeometryWrapper geometry = GeometryWrapper.extract(value);
+ return NodeValue.makeNode(geometry.getGeometryTypeURI(), XSDDatatype.XSDanyURI);
+ } catch (DatatypeFormatException | IllegalArgumentException 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/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 00000000000..d043dd3547e
--- /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 00000000000..bede084228d
--- /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 00000000000..a7ce3b3bd93
--- /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 daedac38ca8..6d2f36a13af 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
@@ -78,6 +78,8 @@ public class GeometryWrapper implements Serializable {
private final String geometryDatatypeURI;
private GeometryDatatype geometryDatatype;
private String lexicalForm;
+ private final String sourceLexicalForm;
+ private String geometryTypeURI;
private String utmURI = null;
private Double latitude = null;
@@ -104,6 +106,26 @@ public GeometryWrapper(Geometry geometry, String srsURI, String geometryDatatype
this(geometry, GeometryReverse.check(geometry, srsURI.isEmpty() ? SRS_URI.DEFAULT_WKT_CRS84 : srsURI), srsURI.isEmpty() ? SRS_URI.DEFAULT_WKT_CRS84 : srsURI, geometryDatatypeURI, dimensionInfo, geometryLiteral);
}
+ /**
+ * Constructs a wrapper for the supplied geometry, with optional serialized
+ * text and an optional geometry subtype URI.
+ *
+ * @param geometry In X/Y or Y/X coordinate order of the SRS URI.
+ * @param srsURI The spatial reference system URI; an empty string uses CRS84.
+ * @param geometryDatatypeURI The geometry serialization datatype URI.
+ * @param dimensionInfo The geometry's coordinate layout and spatial and topological dimensions.
+ * @param geometryLiteral The serialized text representing this geometry, such as WKT or GML,
+ * in the format identified by geometryDatatypeURI. If null, the text
+ * is generated from the supplied geometry when serialization is requested.
+ * @param geometryTypeURI The subtype URI for this geometry in the specified datatype.
+ * If null, the datatype determines it when getGeometryTypeURI() is first called.
+ */
+ public GeometryWrapper(Geometry geometry, String srsURI, String geometryDatatypeURI, DimensionInfo dimensionInfo,
+ String geometryLiteral, String geometryTypeURI) {
+ this(geometry, srsURI, geometryDatatypeURI, dimensionInfo, geometryLiteral);
+ this.geometryTypeURI = geometryTypeURI;
+ }
+
protected GeometryWrapper(Geometry parsingGeometry, Geometry xyGeometry, String srsURI, String geometryDatatypeURI, DimensionInfo dimensionInfo) {
this(parsingGeometry, xyGeometry, srsURI, geometryDatatypeURI, dimensionInfo, null);
}
@@ -126,6 +148,7 @@ protected GeometryWrapper(Geometry parsingGeometry, Geometry xyGeometry, String
this.dimensionInfo = dimensionInfo;
this.lexicalForm = lexicalForm; //If not Initialised then required by asLiteral() etc.
+ this.sourceLexicalForm = lexicalForm;
}
/**
@@ -190,6 +213,8 @@ public GeometryWrapper(GeometryWrapper geometryWrapper) {
this.srsInfo = geometryWrapper.srsInfo;
this.dimensionInfo = geometryWrapper.dimensionInfo;
this.lexicalForm = geometryWrapper.lexicalForm;
+ this.sourceLexicalForm = geometryWrapper.sourceLexicalForm;
+ this.geometryTypeURI = geometryWrapper.geometryTypeURI;
}
/**
@@ -387,6 +412,54 @@ public String getGeometryType() {
return parsingGeometry.getGeometryType();
}
+ /**
+ * Returns the geometry subtype URI defined by this wrapper's datatype,
+ * including for typed empty geometries. The URI is supplied at construction
+ * or resolved by the datatype and cached on the first successful lookup.
+ * Specialized subtypes are not inferred from the shape of the coordinates.
+ *
+ * @return The subtype URI as a string.
+ *
+ * @throws DatatypeFormatException if the datatype cannot resolve the geometry type.
+ */
+ public String getGeometryTypeURI() {
+ if (geometryTypeURI == null) {
+ geometryTypeURI = getGeometryDatatype().getGeometryTypeURI(this);
+ }
+ return geometryTypeURI;
+ }
+
+ /**
+ * 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();
+ }
+
/**
*
* @return GeometryDatatype of the literal.
@@ -1023,6 +1096,19 @@ public DimensionInfo getDimensionInfo() {
return dimensionInfo;
}
+ /**
+ * Returns the serialized geometry text supplied to the constructor, such as
+ * WKT or GML. Unlike {@link #getLexicalForm()}, this method does not generate
+ * text when none was supplied. Calling serialization methods does not change
+ * the returned value.
+ *
+ * @return The supplied text, or null if none was supplied. An empty string
+ * means that an empty literal was supplied; it is distinct from null.
+ */
+ public String getSourceLexicalForm() {
+ return sourceLexicalForm;
+ }
+
/**
*
* @return String literal of Geometry Wrapper.
diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
index 4863c78f2c7..94bd69cfccf 100644
--- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
@@ -25,9 +25,11 @@
import org.apache.jena.datatypes.DatatypeFormatException;
import org.apache.jena.geosparql.implementation.DimensionInfo;
import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.parsers.gml.GMLGeometryTypes;
import org.apache.jena.geosparql.implementation.parsers.gml.GMLReader;
import org.apache.jena.geosparql.implementation.parsers.gml.GMLWriter;
import org.apache.jena.geosparql.implementation.vocabulary.Geo;
+import org.apache.jena.geosparql.implementation.vocabulary.GeoSPARQL_URI;
import org.jdom2.JDOMException;
import org.locationtech.jts.geom.Geometry;
@@ -85,12 +87,28 @@ public GeometryWrapper read(String geometryLiteral) {
String srsURI = gmlReader.getSrsURI();
DimensionInfo dimensionInfo = gmlReader.getDimensionInfo();
- return new GeometryWrapper(geometry, srsURI, URI, dimensionInfo, geometryLiteral);
+ String geometryTypeURI = GeoSPARQL_URI.GML_URI + gmlReader.getGmlGeometryType();
+ return new GeometryWrapper(geometry, srsURI, URI, dimensionInfo, geometryLiteral, geometryTypeURI);
} catch (JDOMException | IOException ex) {
throw new DatatypeFormatException("Illegal GML literal:" + geometryLiteral + ". " + ex.getMessage());
}
}
+ @Override
+ public String getGeometryTypeURI(GeometryWrapper geometry) {
+ String source = geometry.getSourceLexicalForm();
+ if (source != null) {
+ // Constructors accepting source text without a type URI use this fallback.
+ // read() prepopulates the wrapper's type, so normal lookups skip this path.
+ try {
+ return GeoSPARQL_URI.GML_URI + GMLReader.readGeometryType(source);
+ } catch (JDOMException | IOException ex) {
+ throw new DatatypeFormatException("Unable to read GML geometry type", ex);
+ }
+ }
+ return GeoSPARQL_URI.GML_URI + GMLGeometryTypes.fromJts(geometry.getParsingGeometry());
+ }
+
@Override
public String toString() {
return "GMLDatatype{" + URI + '}';
diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
index df0ba9d900d..3189167900e 100644
--- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
@@ -37,6 +37,19 @@ public GeometryDatatype(String uri) {
public abstract GeometryWrapper read(String geometryLiteral);
+ /**
+ * Resolves a geometry subtype URI using this datatype's type system.
+ * Datatypes supporting type resolution override this operation to define
+ * their type mapping. The default implementation throws a datatype error.
+ *
+ * @param geometry A wrapper whose geometry datatype is this datatype.
+ * @return The subtype URI as a non-null string.
+ * @throws DatatypeFormatException if type resolution is unsupported for this datatype or geometry.
+ */
+ public String getGeometryTypeURI(GeometryWrapper geometry) {
+ throw new DatatypeFormatException("Geometry type resolution is not supported for datatype: " + getURI());
+ }
+
/**
* This method Parses the Geometry Literal to the JTS Geometry
*
diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
index 7be20c1421f..7668d67a185 100644
--- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
@@ -26,6 +26,7 @@
import org.apache.jena.geosparql.implementation.parsers.wkt.WKTReader;
import org.apache.jena.geosparql.implementation.parsers.wkt.WKTWriter;
import org.apache.jena.geosparql.implementation.vocabulary.Geo;
+import org.apache.jena.geosparql.implementation.vocabulary.GeoSPARQL_URI;
import org.locationtech.jts.geom.Geometry;
/**
@@ -93,6 +94,16 @@ public GeometryWrapper read(String geometryLiteral) {
return new GeometryWrapper(geometry, srsURI, URI, dimensionInfo, geometryLiteral);
}
+ @Override
+ public String getGeometryTypeURI(GeometryWrapper geometry) {
+ String type = geometry.getGeometryType();
+ return switch (type) {
+ case "Point", "LineString", "LinearRing", "Polygon", "MultiPoint", "MultiLineString",
+ "MultiPolygon", "GeometryCollection" -> GeoSPARQL_URI.SF_URI + type;
+ default -> throw new DatatypeFormatException("Unsupported Simple Features geometry type: " + type);
+ };
+ }
+
@Override
public String toString() {
return "WKTDatatype{" + URI + '}';
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 c7c3fbe0894..5f44ec4360c 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,9 +28,13 @@
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.GeometryTypeFF;
+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.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;
@@ -38,13 +42,15 @@
import org.apache.jena.sparql.pfunction.PropertyFunctionRegistry;
/**
- *
- *
+ * Registers functions that expose geometry metadata.
+ * The {@code geo:} predicates use Jena property functions; the {@code geof:} IRIs
+ * use SPARQL expression functions.
*/
public class GeometryProperty {
/**
- * This method loads all the Geometry property property functions.
+ * Registers {@code geo:} predicates as Jena property functions used in
+ * SPARQL triple patterns to access geometry metadata.
*
* @param registry - the PropertyFunctionRegistry to be used
*/
@@ -59,14 +65,18 @@ public static void loadPropertyFunctions(PropertyFunctionRegistry registry) {
}
/**
- * This method loads all the Geometry property filter functions.
- * N.B. These functions are not part of the GeoSPARQL standard but have been
- * included for convenience using GeometryLiterals.
+ * 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.
*
* @param registry - the FunctionRegistry to be used
*/
public static void loadFilterFunctions(FunctionRegistry registry) {
+ registry.put(Geof.GEOMETRY_TYPE, GeometryTypeFF.class);
+ 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/parsers/gml/GMLGeometryTypes.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLGeometryTypes.java
new file mode 100644
index 00000000000..7a3c7cb8cfd
--- /dev/null
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLGeometryTypes.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.implementation.parsers.gml;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.locationtech.jts.geom.Geometry;
+
+/** GML element names used when serializing JTS geometries. */
+public final class GMLGeometryTypes {
+ private GMLGeometryTypes() {}
+
+ /**
+ * Returns the GML element name that the writer uses for the supplied JTS
+ * geometry, for example {@code MultiCurve} for a JTS MultiLineString.
+ * This mapping does not recover the type of an original GML literal.
+ *
+ * @param geometry The JTS geometry to represent in GML.
+ * @return The element's local name, without a namespace prefix or URI.
+ * @throws DatatypeFormatException if the writer does not support the geometry type.
+ */
+ public static String fromJts(Geometry geometry) {
+ return switch (geometry.getGeometryType()) {
+ case "Point" -> "Point";
+ case "LineString" -> "LineString";
+ case "Polygon" -> "Polygon";
+ case "MultiPoint" -> "MultiPoint";
+ case "MultiLineString" -> "MultiCurve";
+ case "MultiPolygon" -> "MultiSurface";
+ case "GeometryCollection" -> "MultiGeometry";
+ default -> throw new DatatypeFormatException("Geometry type not supported: " + geometry.getGeometryType());
+ };
+ }
+}
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 e6e470062cc..f84965c6c8b 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
@@ -63,6 +63,7 @@ public class GMLReader implements ParserReader {
//Geometry attributes
private final Geometry geometry;
+ private final String gmlGeometryType;
private final String srsURI;
private final CoordinateSequenceDimensions dims;
private final DimensionInfo dimensionInfo;
@@ -109,6 +110,7 @@ protected GMLReader(Element gmlElement) throws DatatypeFormatException, SRSInfoE
int srsDimension = crs.getCoordinateSystem().getDimension();
this.dims = CoordinateSequenceDimensions.find(srsDimension);
String geometryType = gmlElement.getName();
+ this.gmlGeometryType = geometryType;
this.geometry = buildGeometry(geometryType, gmlElement, dims, srsInfo);
this.dimensionInfo = new DimensionInfo(dims, geometry.getDimension());
@@ -117,6 +119,7 @@ protected GMLReader(Element gmlElement) throws DatatypeFormatException, SRSInfoE
protected GMLReader(Geometry geometry, int srsDimension, String srsURI) {
this.srsURI = srsURI;
this.geometry = geometry;
+ this.gmlGeometryType = null;
this.dims = CoordinateSequenceDimensions.find(srsDimension);
this.dimensionInfo = new DimensionInfo(dims, geometry.getDimension());
}
@@ -125,6 +128,18 @@ protected GMLReader(Geometry geometry, int srsDimension) {
this(geometry, srsDimension, SRS_URI.DEFAULT_WKT_CRS84);
}
+ /**
+ * Returns the local name of the root XML element read from the GML literal,
+ * such as {@code Curve}, without a namespace prefix or URI. An empty GML
+ * literal is interpreted as an empty Point and returns {@code Point}.
+ *
+ * @return The root element's local name, or null if this reader was created
+ * directly from a JTS geometry rather than XML.
+ */
+ public String getGmlGeometryType() {
+ return gmlGeometryType;
+ }
+
@Override
public Geometry getGeometry() {
return geometry;
@@ -698,6 +713,24 @@ private static List getMembers(Element gmlElement, String memberLabel)
private static final String EMPTY_GML_TEXT = "";
public static GMLReader extract(String gmlText) throws JDOMException, IOException {
+ return new GMLReader(readRootElement(gmlText));
+ }
+
+ /**
+ * Parses the XML and returns its root element's local name, such as
+ * {@code Curve}, without a namespace prefix or URI. This method does not
+ * construct a JTS geometry or validate the XML against the GML schema.
+ *
+ * @param gmlText The serialized GML text; an empty string represents an empty Point.
+ * @return The root element's local name, or {@code Point} for an empty string.
+ * @throws JDOMException if the XML cannot be parsed.
+ * @throws IOException if an I/O error occurs while reading the XML.
+ */
+ public static String readGeometryType(String gmlText) throws JDOMException, IOException {
+ return readRootElement(gmlText).getName();
+ }
+
+ private static Element readRootElement(String gmlText) throws JDOMException, IOException {
if (gmlText.isEmpty()) {
gmlText = EMPTY_GML_TEXT;
@@ -706,8 +739,7 @@ public static GMLReader extract(String gmlText) throws JDOMException, IOExceptio
SAXBuilder jdomBuilder = newSAXBuilder();
InputStream stream = new ByteArrayInputStream(gmlText.getBytes(StandardCharsets.UTF_8));
Document xmlDoc = jdomBuilder.build(stream);
- Element gmlElement = xmlDoc.getRootElement();
- return new GMLReader(gmlElement);
+ return xmlDoc.getRootElement();
}
// ---- XXE safe SAXBuilder
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 5e25164c8d8..5db2bc1508a 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
@@ -139,7 +139,7 @@ private static Element buildLineString(final CoordinateSequence coordSeq, final
private static Element buildPolygon(final Polygon polygon, final String srsName) {
- Element gmlRoot = new Element(polygon.getGeometryType(), GML_NAMESPACE);
+ Element gmlRoot = new Element(GMLGeometryTypes.fromJts(polygon), GML_NAMESPACE);
gmlRoot.setAttribute("srsName", srsName);
if (!polygon.isEmpty()) {
@@ -179,7 +179,7 @@ private static Element buildPolygon(final Polygon polygon, final String srsName)
private static Element buildMultiPoint(final MultiPoint multiPoint, final String srsName) {
- Element gmlRoot = new Element(multiPoint.getGeometryType(), GML_NAMESPACE);
+ Element gmlRoot = new Element(GMLGeometryTypes.fromJts(multiPoint), GML_NAMESPACE);
gmlRoot.setAttribute("srsName", srsName);
if (!multiPoint.isEmpty()) {
@@ -202,8 +202,7 @@ private static Element buildMultiPoint(final MultiPoint multiPoint, final String
private static Element buildMultiLineString(final MultiLineString multiLineString, final String srsName) {
- //Element gmlRoot = new Element(multiLineString.getGeometryType(), GML_NAMESPACE);
- Element gmlRoot = new Element("MultiCurve", GML_NAMESPACE);
+ Element gmlRoot = new Element(GMLGeometryTypes.fromJts(multiLineString), GML_NAMESPACE);
gmlRoot.setAttribute("srsName", srsName);
if (!multiLineString.isEmpty()) {
@@ -227,8 +226,7 @@ private static Element buildMultiLineString(final MultiLineString multiLineStrin
private static Element buildMultiPolygon(final MultiPolygon multiPolygon, final String dimensionString, final String srsName) {
- //Element gmlRoot = new Element(multiPolygon.getGeometryType(), GML_NAMESPACE);
- Element gmlRoot = new Element("MultiSurface", GML_NAMESPACE);
+ Element gmlRoot = new Element(GMLGeometryTypes.fromJts(multiPolygon), GML_NAMESPACE);
gmlRoot.setAttribute("srsName", srsName);
if (!multiPolygon.isEmpty()) {
@@ -252,8 +250,7 @@ private static Element buildMultiPolygon(final MultiPolygon multiPolygon, final
private static Element buildMultiGeometry(final GeometryCollection geometryCollection, final CoordinateSequenceDimensions dimensions, final String srsName) {
- //Element gmlRoot = new Element(geometryCollection.getGeometryType(), GML_NAMESPACE);
- Element gmlRoot = new Element("MultiGeometry", GML_NAMESPACE);
+ Element gmlRoot = new Element(GMLGeometryTypes.fromJts(geometryCollection), GML_NAMESPACE);
gmlRoot.setAttribute("srsName", srsName);
if (!geometryCollection.isEmpty()) {
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 ab13061da7c..6af9556b6bc 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,12 @@ 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 metadata expression function symbols:
+ // GeoSPARQL 1.1 geometry metadata, plus the isValid extension.
+ public static final String GEOMETRY_TYPE = GEOF_URI + "geometryType";
+ 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 00000000000..316f4e67e9f
--- /dev/null
+++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryMetadataErrorsTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.assertTrue;
+import static org.junit.Assert.assertThrows;
+
+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