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 00000000000..9807ab6119a --- /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 d06566742b5..cac4b14ba5a 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 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 static int findCoordinateDimension(CoordinateSequenceDimensions dims) { } } + /** + * 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 d609982e5e8..8978171e540 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.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 double getMaxZ() { 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 3985e407170..b595cbadd60 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_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 static void loadPropertyFunctions(PropertyFunctionRegistry registry) { /** * 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 static void loadFilterFunctions(FunctionRegistry registry) { 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 e6e470062cc..085f32a5804 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 @@ private static Polygon buildPolygon(Element gmlElement, CoordinateSequenceDimens 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 interiorElements = gmlElement.getChildren("interior", GML_NAMESPACE); @@ -536,7 +536,7 @@ private static Polygon buildSurface(Element gmlElement, CoordinateSequenceDimens 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 @@ private static Polygon buildSurface(Element gmlElement, CoordinateSequenceDimens 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 5e25164c8d8..3278a8746bb 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 @@ private static Element buildMultiPoint(final MultiPoint multiPoint, final String 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 @@ private static Element buildMultiLineString(final MultiLineString multiLineStrin 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 @@ private static Element buildMultiPolygon(final MultiPolygon multiPolygon, final 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 @@ private static Element buildMultiGeometry(final GeometryCollection geometryColle 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 1069230c210..009ea9b4dab 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.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 @@ private Geometry buildGeometry(String geometryType, String coordinates) throws D 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 @@ private Geometry buildGeometry(String geometryType, String coordinates) throws D 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 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 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 @@ private String[] splitCoordinates(String coordinates) { } - 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 1889f994a83..77e6cd13de2 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 @@ private static String convertToWKTText(CustomCoordinateSequence coordSequence) { 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 @@ private static String buildWKT(final String geometryType, final CoordinateSequen StringBuilder sb = new StringBuilder(geometryType); - if (!wktText.equals(" EMPTY")) { - sb.append(dimensionString); - } + sb.append(dimensionString); sb.append(wktText); @@ -147,12 +142,10 @@ private static String buildPolygon(final Polygon polygon, final boolean isInclud 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 @@ private static String buildPolygon(final Polygon polygon, final boolean isInclud 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 @@ private static String buildMultiPoint(final MultiPoint multiPoint, final String 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 @@ private static String buildMultiLineString(final MultiLineString multiLineString 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 @@ private static String buildMultiPolygon(final MultiPolygon multiPolygon, final S 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 00d45ecf9bb..abcb958a999 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 00000000000..3d70b249178 --- /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 = " "; + assertGeometry(crs + "POINT Z (10 100 7)", select(crs + "MULTIPOINT Z ((10 100 7), (20 120 9))", "1")); + } + + @Test + public void gmlResultRetainsSourceDatatypeAndCrs() { + String gml = """ + ' + 10 100 7 + '^^geo:gmlLiteral + """.replace("\n", " "); + Node result = evaluate("geof:geometryN(" + gml + ", 1)"); + assertGeometry(" POINT Z (10 100 7)", result, GMLDatatype.URI); + } + + @Test + public void selectedEmptyGmlCollectionRetainsItsMember() { + String gml = """ + ' + + + + '^^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 = ""; + String gml = """ + ' + %s%s + '^^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", "" }) { + 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", "", "'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: + PREFIX geo: + PREFIX xsd: + 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 a169a359218..8abc3dd4921 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 @@ 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 void testFindCollection() { 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 79d46b36030..066ba388022 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 @@ 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.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 void testGetUTMZoneURI_crs84() throws FactoryException, MismatchedDimensi 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( + " 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 97e397b49e7..3b467e070c2 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.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 void testBuildPolygonEmpty() throws JDOMException, IOException { assertEquals(expResult, result); } + @Test + public void testEmptyPolygonRetainsThreeDimensionalCrsLayout() throws JDOMException, IOException { + String[] gml = { + "", + "", + "" + }; + + 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 2dcce2b5603..db7ef964e4c 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.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.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 void testWriteMultiGeometryEmpty() { 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 7e02359666b..78cd465cb58 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.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 void testExtractGeometryCollection() { 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 80044d1b29f..c87d92b33cb 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.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 void testWriteGeometryCollectionEmpty() { 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)); + } + }