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