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 d774384789 GH-4278: Add geof:transform function
d774384789 is described below
commit d77438478938cc651760f4a61e5d9ee7acff072b
Author: Edmond Chuc <[email protected]>
AuthorDate: Thu Oct 1 00:49:49 2026 +1000
GH-4278: Add geof:transform function
Support IRI and xsd:anyURI target CRS arguments, preserve geometry
datatypes, and validate GML coordinate layouts. Include direct-function and
SPARQL BIND tests.
---
.../filter_functions/TransformFF.java | 74 ++++
.../function_registration/NonTopological.java | 2 +
.../geosparql/implementation/vocabulary/Geof.java | 1 +
.../filter_functions/TransformFFTest.java | 426 +++++++++++++++++++++
4 files changed, 503 insertions(+)
diff --git
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFF.java
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFF.java
new file mode 100644
index 0000000000..f35407515a
--- /dev/null
+++
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFF.java
@@ -0,0 +1,74 @@
+/*
+ * 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.nontopological.filter_functions;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
+import org.apache.jena.geosparql.implementation.registry.SRSRegistry;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase2;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.util.FactoryException;
+
+/** Implements geof:transform. */
+public class TransformFF extends FunctionBase2 {
+
+ @Override
+ public NodeValue exec(NodeValue value, NodeValue target) {
+ try {
+ String targetURI = targetURI(target);
+ GeometryWrapper geometry = GeometryWrapper.extract(value);
+ if (!targetURI.equals(geometry.getSrsURI())
+ && (!geometry.getSrsInfo().isSRSRecognised()
+ ||
!SRSRegistry.getSRSInfo(targetURI).isSRSRecognised())) {
+ throw new ExprEvalException("Source or target CRS is not
recognized.");
+ }
+ GeometryWrapper transformed = geometry.transform(targetURI);
+ // GML is parsed using the CRS dimension, but transformation
retains the source coordinate layout.
+ if (GMLDatatype.URI.equals(transformed.getGeometryDatatypeURI())
+ && !transformed.isEmpty()
+ && transformed.getCoordinateDimension() !=
transformed.getCRS().getCoordinateSystem().getDimension()) {
+ throw new ExprEvalException("GML transform result has a
coordinate dimension incompatible with the target CRS.");
+ }
+ return transformed.asNodeValue();
+ } catch (DatatypeFormatException | FactoryException |
MismatchedDimensionException | TransformException ex) {
+ throw new ExprEvalException(ex.getMessage(), ex);
+ }
+ }
+
+ private static String targetURI(NodeValue target) {
+ if (target.isIRI()) {
+ return target.asNode().getURI();
+ }
+ if (target.asNode().isLiteral()
+ &&
XSDDatatype.XSDanyURI.getURI().equals(target.asNode().getLiteralDatatypeURI()))
{
+ String uri = target.asNode().getLiteralValue().toString();
+ if (!uri.isBlank()) {
+ return uri;
+ }
+ }
+ throw new ExprEvalException("Target CRS must be an IRI or nonempty
xsd:anyURI literal.");
+ }
+}
diff --git
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
index 7397e06c7b..617e3dee48 100644
---
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
+++
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
@@ -33,6 +33,7 @@ import
org.apache.jena.geosparql.geof.nontopological.filter_functions.GetSRIDFF;
import
org.apache.jena.geosparql.geof.nontopological.filter_functions.IntersectionFF;
import
org.apache.jena.geosparql.geof.nontopological.filter_functions.MetricAreaFF;
import
org.apache.jena.geosparql.geof.nontopological.filter_functions.SymmetricDifferenceFF;
+import
org.apache.jena.geosparql.geof.nontopological.filter_functions.TransformFF;
import org.apache.jena.geosparql.geof.nontopological.filter_functions.UnionFF;
import org.apache.jena.geosparql.implementation.vocabulary.Geof;
import org.apache.jena.sparql.function.FunctionRegistry;
@@ -63,6 +64,7 @@ public class NonTopological {
registry.put(Geof.GETSRID_NAME, GetSRIDFF.class);
registry.put(Geof.INTERSECTION_NAME, IntersectionFF.class);
registry.put(Geof.SYMDIFFERENCE_NAME, SymmetricDifferenceFF.class);
+ registry.put(Geof.TRANSFORM_NAME, TransformFF.class);
registry.put(Geof.UNION_NAME, UnionFF.class);
registry.put(Geof.AREA_NAME, AreaFF.class);
registry.put(Geof.METRIC_AREA_NAME, MetricAreaFF.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 c7eae6da3f..2ad20e022b 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,6 +74,7 @@ public interface Geof {
public static final String CENTROID_NAME = GEOF_URI + "centroid";
public static final String CONVEXHULL_NAME = GEOF_URI + "convexHull";
public static final String GETSRID_NAME = GEOF_URI + "getSRID";
+ public static final String TRANSFORM_NAME = GEOF_URI + "transform";
public static final String AREA_NAME = GEOF_URI + "area";
public static final String METRIC_AREA_NAME = GEOF_URI + "metricArea";
diff --git
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFFTest.java
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFFTest.java
new file mode 100644
index 0000000000..cd3a405765
--- /dev/null
+++
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/TransformFFTest.java
@@ -0,0 +1,426 @@
+/*
+ * 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.nontopological.filter_functions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.index.GeometryLiteralIndex;
+import
org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
+import org.apache.jena.geosparql.implementation.vocabulary.SRS_URI;
+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.junit.BeforeClass;
+import org.junit.Test;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.referencing.operation.TransformException;
+
+public class TransformFFTest {
+ private static final String CRS84 = SRS_URI.DEFAULT_WKT_CRS84;
+ private static final String WGS84 =
"http://www.opengis.net/def/crs/EPSG/0/4326";
+ private static final String WGS84_3D =
"http://www.opengis.net/def/crs/EPSG/0/4979";
+ private static final String WEB_MERCATOR =
"http://www.opengis.net/def/crs/EPSG/0/3857";
+ private static final String UTM34N =
"http://www.opengis.net/def/crs/EPSG/0/32634";
+ private static final String UNKNOWN = "http://example.org/crs/unknown";
+ private final TransformFF function = new TransformFF();
+
+ @BeforeClass
+ public static void setup() {
+ GeoSPARQLConfig.setupNoIndex();
+ }
+
+ @Test
+ public void projectedPointAndReverse() {
+ NodeValue point = wkt("POINT(24.5887755 41.4035958)");
+ NodeValue projected = function.exec(point, iri(UTM34N));
+ GeometryWrapper result = GeometryWrapper.extract(projected);
+ assertEquals(WKTDatatype.URI, result.getGeometryDatatypeURI());
+ assertEquals(UTM34N, result.getSrsURI());
+ Point projectedPoint = (Point)result.getParsingGeometry();
+ assertEquals(799997.8, projectedPoint.getX(), 1.0);
+ assertEquals(4589779.63, projectedPoint.getY(), 1.0);
+
+ NodeValue reversed = function.exec(projected,
NodeValue.makeNode(CRS84, XSDDatatype.XSDanyURI));
+ GeometryWrapper reversedGeometry = GeometryWrapper.extract(reversed);
+ assertEquals(CRS84, reversedGeometry.getSrsURI());
+ Point reversedPoint = (Point)reversedGeometry.getParsingGeometry();
+ assertEquals(24.5887755, reversedPoint.getX(), 1e-5);
+ assertEquals(41.4035958, reversedPoint.getY(), 1e-5);
+ }
+
+ @Test
+ public void axisOrderAndVertices() {
+ NodeValue source = wkt("<" + WGS84 + "> LINESTRING(41 24, 42 25)");
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(source,
iri(CRS84)));
+ LineString line = (LineString)result.getParsingGeometry();
+ CoordinateSequence coordinates = line.getCoordinateSequence();
+ assertEquals(CRS84, result.getSrsURI());
+ assertEquals(2, coordinates.size());
+ assertEquals(24, coordinates.getX(0), 0.0);
+ assertEquals(41, coordinates.getY(0), 0.0);
+ assertEquals(25, coordinates.getX(1), 0.0);
+ assertEquals(42, coordinates.getY(1), 0.0);
+ }
+
+ @Test
+ public void polygonWithHoleRetainsStructure() {
+ NodeValue source = wkt("<" + WGS84 + "> POLYGON((40 20, 40 26, 44 26,
40 20),"
+ + "(41 22, 42 22, 41 23, 41 22))");
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(source,
iri(CRS84)));
+ Polygon polygon = (Polygon)result.getParsingGeometry();
+ assertEquals(1, polygon.getNumInteriorRing());
+ assertEquals(4, polygon.getExteriorRing().getNumPoints());
+ assertEquals(20, polygon.getExteriorRing().getCoordinateN(0).getX(),
0.0);
+ assertEquals(40, polygon.getExteriorRing().getCoordinateN(0).getY(),
0.0);
+ assertEquals(4, polygon.getInteriorRingN(0).getNumPoints());
+ }
+
+ @Test
+ public void multipartGeometryRetainsMembers() {
+ NodeValue source = wkt("<" + WGS84 + "> MULTILINESTRING((41 24, 42
25),(43 26, 44 27))");
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(source,
iri(CRS84)));
+ assertEquals("MultiLineString",
result.getParsingGeometry().getGeometryType());
+ assertEquals(2, result.getParsingGeometry().getNumGeometries());
+ LineString second =
(LineString)result.getParsingGeometry().getGeometryN(1);
+ assertEquals(2, second.getNumPoints());
+ assertEquals(26, second.getCoordinateN(0).getX(), 0.0);
+ assertEquals(43, second.getCoordinateN(0).getY(), 0.0);
+ }
+
+ @Test
+ public void identityRetainsWktLayout() {
+ NodeValue source = wkt("POINT ZM (1 2 3 4)");
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(source,
iri(CRS84)));
+ CoordinateSequence coordinates =
((Point)result.getParsingGeometry()).getCoordinateSequence();
+ assertEquals(WKTDatatype.URI, result.getGeometryDatatypeURI());
+ assertEquals(CRS84, result.getSrsURI());
+ assertEquals(CoordinateSequenceDimensions.XYZM,
result.getCoordinateSequenceDimensions());
+ assertEquals(1, coordinates.getX(0), 0.0);
+ assertEquals(2, coordinates.getY(0), 0.0);
+ assertEquals(3, coordinates.getZ(0), 0.0);
+ assertEquals(4, coordinates.getM(0), 0.0);
+ }
+
+ @Test
+ public void projectedPointRetainsZAndM() {
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(
+ wkt("POINT ZM (24.5887755 41.4035958 123 456)"), iri(UTM34N)));
+ CoordinateSequence coordinates =
((Point)result.getParsingGeometry()).getCoordinateSequence();
+ assertEquals(4, coordinates.getDimension());
+ assertEquals(1, coordinates.getMeasures());
+ assertEquals(123, coordinates.getZ(0), 0.0);
+ assertEquals(456, coordinates.getM(0), 0.0);
+ }
+
+ @Test
+ public void emptyLineRetainsTypeAndTargetSrs() {
+ GeometryWrapper result =
GeometryWrapper.extract(function.exec(wkt("LINESTRING EMPTY"), iri(UTM34N)));
+ assertEquals(WKTDatatype.URI, result.getGeometryDatatypeURI());
+ assertEquals(UTM34N, result.getSrsURI());
+ assertEquals("LineString",
result.getParsingGeometry().getGeometryType());
+ assertTrue(result.getParsingGeometry().isEmpty());
+ }
+
+ @Test
+ public void finitePointOutsideTargetAreaOfUse() {
+ GeometryWrapper result =
GeometryWrapper.extract(function.exec(wkt("POINT(0 0)"), iri(UTM34N)));
+ Point point = (Point)result.getParsingGeometry();
+ assertEquals(UTM34N, result.getSrsURI());
+ assertTrue(Double.isFinite(point.getX()));
+ assertTrue(Double.isFinite(point.getY()));
+ assertEquals(-1891310.54, point.getX(), 2.0);
+ assertEquals(0.0, point.getY(), 1e-8);
+ }
+
+ @Test
+ public void gmlRetainsDatatype() {
+ String gml = "<gml:Point xmlns:gml=\"http://www.opengis.net/gml/3.2\"
srsName=\"" + CRS84
+ + "\"><gml:pos>24.5887755 41.4035958</gml:pos></gml:Point>";
+ GeometryWrapper result =
GeometryWrapper.extract(function.exec(NodeValue.makeNode(gml,
GMLDatatype.INSTANCE), iri(UTM34N)));
+ assertEquals(GMLDatatype.URI, result.getGeometryDatatypeURI());
+ assertEquals(UTM34N, result.getSrsURI());
+ Point point = (Point)result.getParsingGeometry();
+ assertEquals(799997.8, point.getX(), 1.0);
+ assertEquals(4589779.63, point.getY(), 1.0);
+ }
+
+ @Test
+ public void emptyGmlPointFrom3DTo2D() {
+ assertEmptyGmlTransform(WGS84_3D, WGS84, 2);
+ }
+
+ @Test
+ public void emptyGmlPointFrom2DTo3D() {
+ assertEmptyGmlTransform(WGS84, WGS84_3D, 3);
+ }
+
+ @Test
+ public void gmlDimensionalReductionIsAnExpressionError() {
+ String gml = gmlLine(WGS84_3D, "10 20 7 30 40 9");
+ assertThrows(ExprEvalException.class,
+ () -> function.exec(NodeValue.makeNode(gml,
GMLDatatype.INSTANCE), iri(WGS84)));
+ }
+
+ @Test
+ public void gmlDimensionalReductionLeavesBindUnbound() {
+ assertNull(evaluate("geof:transform('" + gmlLine(WGS84_3D, "10 20 7 30
40 9")
+ + "'^^geo:gmlLiteral, <" + WGS84 + ">)"));
+ }
+
+ @Test
+ public void gmlTwoDimensionalTransformRetainsVertices() {
+ NodeValue result = function.exec(NodeValue.makeNode(gmlLine(WGS84, "10
20 30 40"), GMLDatatype.INSTANCE), iri(CRS84));
+ GeometryWrapper parsed =
GMLDatatype.INSTANCE.read(result.asNode().getLiteralLexicalForm());
+ assertEquals(GMLDatatype.URI, result.asNode().getLiteralDatatypeURI());
+ assertEquals(CRS84, parsed.getSrsURI());
+ CoordinateSequence coordinates =
((LineString)parsed.getParsingGeometry()).getCoordinateSequence();
+ assertEquals(2, coordinates.size());
+ assertEquals(2, coordinates.getDimension());
+ assertEquals(20, coordinates.getX(0), 0.0);
+ assertEquals(10, coordinates.getY(0), 0.0);
+ assertEquals(40, coordinates.getX(1), 0.0);
+ assertEquals(30, coordinates.getY(1), 0.0);
+ }
+
+ @Test
+ public void gmlThreeDimensionalTransformRetainsVertices() {
+ String targetURI = "http://www.opengis.net/def/crs/EPSG/0/4937";
+ NodeValue result = function.exec(NodeValue.makeNode(gmlLine(WGS84_3D,
"10 20 7 30 40 9"), GMLDatatype.INSTANCE),
+ iri(targetURI));
+ GeometryWrapper parsed =
GMLDatatype.INSTANCE.read(result.asNode().getLiteralLexicalForm());
+ assertEquals(GMLDatatype.URI, result.asNode().getLiteralDatatypeURI());
+ assertEquals(targetURI, parsed.getSrsURI());
+ CoordinateSequence coordinates =
((LineString)parsed.getParsingGeometry()).getCoordinateSequence();
+ assertEquals(2, coordinates.size());
+ assertEquals(3, coordinates.getDimension());
+ assertEquals(10, coordinates.getX(0), 1e-7);
+ assertEquals(20, coordinates.getY(0), 1e-7);
+ assertEquals(7, coordinates.getZ(0), 1e-4);
+ assertEquals(30, coordinates.getX(1), 1e-7);
+ assertEquals(40, coordinates.getY(1), 1e-7);
+ assertEquals(9, coordinates.getZ(1), 1e-4);
+ }
+
+ @Test
+ public void northPoleProjectionIsAnExpressionError() {
+ expectProjectionExpressionError("POINT(0 90)");
+ }
+
+ @Test
+ public void northPoleProjectionLeavesBindUnbound() {
+ assertNull(evaluate("geof:transform('POINT(0 90)'^^geo:wktLiteral, <"
+ WEB_MERCATOR + ">)"));
+ }
+
+ @Test
+ public void invalidLatitudeProjectionIsAnExpressionError() {
+ expectProjectionExpressionError("POINT(0 100)");
+ }
+
+ @Test
+ public void invalidLatitudeProjectionLeavesBindUnbound() {
+ assertNull(evaluate("geof:transform('POINT(0 100)'^^geo:wktLiteral, <"
+ WEB_MERCATOR + ">)"));
+ }
+
+ @Test
+ public void unknownSourceOrTargetIsAnExpressionError() {
+ expectExpressionError(wkt("<" + UNKNOWN + "> POINT(1 2)"), iri(CRS84));
+ expectExpressionError(wkt("POINT(1 2)"), iri(UNKNOWN));
+ }
+
+ @Test
+ public void unknownSrsIdentitySucceeds() {
+ GeometryWrapper result = GeometryWrapper.extract(function.exec(
+ wkt("<" + UNKNOWN + "> POINT(1 2)"), iri(UNKNOWN)));
+ assertEquals(WKTDatatype.URI, result.getGeometryDatatypeURI());
+ assertEquals(UNKNOWN, result.getSrsURI());
+ Point point = (Point)result.getParsingGeometry();
+ assertEquals(1, point.getX(), 0.0);
+ assertEquals(2, point.getY(), 0.0);
+ }
+
+ @Test
+ public void transformDoesNotCacheGeneratedLiteral() {
+ GeoSPARQLConfig.setupMemoryIndex();
+ try {
+ NodeValue source = wkt("POINT(1 2)");
+ GeometryWrapper.extract(source);
+ assertEquals(1, GeometryLiteralIndex.getPrimaryIndexSize());
+
+ NodeValue result = function.exec(source, iri(WGS84));
+ assertEquals(WKTDatatype.URI,
result.asNode().getLiteralDatatypeURI());
+ assertEquals(1, GeometryLiteralIndex.getPrimaryIndexSize());
+ } finally {
+ GeoSPARQLConfig.setupNoIndex();
+ }
+ }
+
+ @Test
+ public void anyUriTargetNormalizesSpaces() {
+ assertNormalizedAnyUriTarget(" " + CRS84 + " ");
+ }
+
+ @Test
+ public void anyUriTargetNormalizesXmlWhitespace() {
+ assertNormalizedAnyUriTarget("\t \r\n" + CRS84 + "\r\n \t");
+ }
+
+ @Test
+ public void registeredFunctionNormalizesAnyUriTarget() {
+ Node result = evaluate("geof:transform('<" + WGS84 + "> POINT(41
24)'^^geo:wktLiteral, ' "
+ + CRS84 + " '^^xsd:anyURI)");
+ assertNotNull(result);
+ assertEquals("POINT(24 41)", result.getLiteralLexicalForm());
+ }
+
+ @Test
+ public void targetMustBeIriOrAnyURI() {
+ NodeValue point = wkt("POINT(1 2)");
+ expectExpressionError(point, NodeValue.makeString(CRS84));
+ expectExpressionError(point, NodeValue.makeInteger(1));
+ expectExpressionError(point, NodeValue.makeNode(" ",
XSDDatatype.XSDanyURI));
+ expectExpressionError(point,
NodeValue.makeNode("http://example.com/%ZZ", XSDDatatype.XSDanyURI));
+ }
+
+ @Test
+ public void geometryMustBeALiteral() {
+ expectExpressionError(iri(CRS84), iri(UTM34N));
+ expectExpressionError(NodeValue.makeString("POINT(1 2)"), iri(UTM34N));
+ expectExpressionError(wkt("not valid WKT"), iri(UTM34N));
+ }
+
+ @Test
+ public void registeredFunctionReturnsGeometryLiteral() {
+ Node result = evaluate("geof:transform('<" + WGS84 + "> POINT(41
24)'^^geo:wktLiteral, <" + CRS84 + ">)");
+ assertNotNull(result);
+ assertEquals(WKTDatatype.URI, result.getLiteralDatatypeURI());
+ assertEquals("POINT(24 41)", result.getLiteralLexicalForm());
+ }
+
+ @Test
+ public void registeredFunctionAcceptsAnyURITarget() {
+ Node result = evaluate("geof:transform('<" + WGS84 + "> POINT(41
24)'^^geo:wktLiteral, '"
+ + CRS84 + "'^^xsd:anyURI)");
+ assertNotNull(result);
+ assertEquals("POINT(24 41)", result.getLiteralLexicalForm());
+ }
+
+ @Test
+ public void expressionErrorLeavesBindUnbound() {
+ assertNull(evaluate("geof:transform('POINT(1 2)'^^geo:wktLiteral, <" +
UNKNOWN + ">)"));
+ assertNull(evaluate("geof:transform('POINT(1 2)'^^geo:wktLiteral, '" +
CRS84 + "')"));
+ assertNull(evaluate("geof:transform('invalid'^^geo:wktLiteral, <" +
CRS84 + ">)"));
+ assertNull(evaluate("geof:transform('POINT(1 2)'^^geo:wktLiteral,
'http://example.com/%ZZ'^^xsd:anyURI)"));
+ }
+
+ @Test
+ public void wrongArityIsRejectedAtQueryBuild() {
+ assertThrows(QueryBuildException.class, () ->
evaluate("geof:transform('POINT(1 2)'^^geo:wktLiteral)"));
+ assertThrows(QueryBuildException.class,
+ () -> evaluate("geof:transform('POINT(1
2)'^^geo:wktLiteral, <" + CRS84 + ">, 1)"));
+ }
+
+ private void expectExpressionError(NodeValue geometry, NodeValue target) {
+ ExprEvalException error = assertThrows(ExprEvalException.class, () ->
function.exec(geometry, target));
+ assertFalse(error.getMessage().isEmpty());
+ }
+
+ private void expectProjectionExpressionError(String point) {
+ ExprEvalException error = assertThrows(ExprEvalException.class,
+ () -> function.exec(wkt(point), iri(WEB_MERCATOR)));
+ assertTrue(error.getCause() instanceof TransformException);
+ }
+
+ private void assertNormalizedAnyUriTarget(String lexicalForm) {
+ NodeValue target = NodeValue.makeNode(lexicalForm,
XSDDatatype.XSDanyURI);
+ assertEquals(CRS84, target.asNode().getLiteralValue().toString());
+ assertEquals(lexicalForm, target.asNode().getLiteralLexicalForm());
+ NodeValue source = wkt("<" + WGS84 + "> POINT(41 24)");
+ assertEquals(function.exec(source, NodeValue.makeNode(CRS84,
XSDDatatype.XSDanyURI)).asNode(),
+ function.exec(source, target).asNode());
+ }
+
+ private void assertEmptyGmlTransform(String sourceURI, String targetURI,
int dimension) {
+ String gml = "<gml:Point xmlns:gml=\"http://www.opengis.net/gml/3.2\"
srsName=\"" + sourceURI + "\"/>";
+ NodeValue source = NodeValue.makeNode(gml, GMLDatatype.INSTANCE);
+ assertEmptyGmlResult(function.exec(source, iri(targetURI)).asNode(),
targetURI, dimension);
+ assertEmptyGmlResult(evaluate("geof:transform('" + gml +
"'^^geo:gmlLiteral, <" + targetURI + ">)"),
+ targetURI, dimension);
+ }
+
+ private static void assertEmptyGmlResult(Node result, String targetURI,
int dimension) {
+ assertNotNull(result);
+ assertEquals(GMLDatatype.URI, result.getLiteralDatatypeURI());
+ assertFalse(result.getLiteralLexicalForm().contains("<gml:pos"));
+ GeometryWrapper parsed =
GMLDatatype.INSTANCE.read(result.getLiteralLexicalForm());
+ assertEquals(targetURI, parsed.getSrsURI());
+ assertEquals("Point", parsed.getGeometryType());
+ assertTrue(parsed.isEmpty());
+ assertEquals(dimension, parsed.getCoordinateDimension());
+ }
+
+ private static String gmlLine(String srsURI, String positions) {
+ return "<gml:LineString xmlns:gml=\"http://www.opengis.net/gml/3.2\"
srsName=\"" + srsURI
+ + "\"><gml:posList>" + positions +
"</gml:posList></gml:LineString>";
+ }
+
+ private static NodeValue wkt(String lexicalForm) {
+ return NodeValue.makeNode(lexicalForm, WKTDatatype.INSTANCE);
+ }
+
+ private static NodeValue iri(String uri) {
+ return NodeValue.makeNode(NodeFactory.createURI(uri));
+ }
+
+ private static Node evaluate(String expression) {
+ String query = """
+ PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
+ PREFIX geo: <http://www.opengis.net/ont/geosparql#>
+ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+ SELECT ?result WHERE { BIND(%s AS ?result) }
+ """.formatted(expression);
+ try (QueryExecution execution = QueryExecution.create(query,
ModelFactory.createDefaultModel())) {
+ ResultSet results = execution.execSelect();
+ assertTrue(expression, results.hasNext());
+ QuerySolution solution = results.next();
+ assertFalse(expression, results.hasNext());
+ return solution.contains("result") ?
solution.get("result").asNode() : null;
+ }
+ }
+}