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
commit 509136074c4bd8b4771c08d20da4b2821fccdf37 Author: Edmond Chuc <[email protected]> AuthorDate: Mon Sep 14 16:47:13 2026 +1000 GH-4221: Add six GeoSPARQL 1.1 coordinate extrema functions --- .../filter_functions/geometry_property/MaxXFF.java | 41 +++ .../filter_functions/geometry_property/MaxYFF.java | 41 +++ .../filter_functions/geometry_property/MaxZFF.java | 41 +++ .../filter_functions/geometry_property/MinXFF.java | 41 +++ .../filter_functions/geometry_property/MinYFF.java | 41 +++ .../filter_functions/geometry_property/MinZFF.java | 41 +++ .../implementation/GeometryCoordinateExtrema.java | 123 +++++++++ .../geosparql/implementation/GeometryWrapper.java | 56 ++++ .../function_registration/GeometryProperty.java | 19 +- .../geosparql/implementation/vocabulary/Geof.java | 8 + .../GeometryCoordinateExtremaFFTest.java | 284 +++++++++++++++++++ .../GeometryCoordinateExtremaTest.java | 307 +++++++++++++++++++++ 12 files changed, 1040 insertions(+), 3 deletions(-) diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxXFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxXFF.java new file mode 100644 index 0000000000..6476d6539f --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxXFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:maxX. */ +public class MaxXFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMaxX()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxYFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxYFF.java new file mode 100644 index 0000000000..81749c66ff --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxYFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:maxY. */ +public class MaxYFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMaxY()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxZFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxZFF.java new file mode 100644 index 0000000000..d0fdf20aa1 --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MaxZFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:maxZ. */ +public class MaxZFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMaxZ()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinXFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinXFF.java new file mode 100644 index 0000000000..49dacf554f --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinXFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:minX. */ +public class MinXFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMinX()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinYFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinYFF.java new file mode 100644 index 0000000000..6be3c482d4 --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinYFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:minY. */ +public class MinYFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMinY()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinZFF.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinZFF.java new file mode 100644 index 0000000000..5dcd13f90a --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/MinZFF.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; + +/** Implements geof:minZ. */ +public class MinZFF extends FunctionBase1 { + + @Override + public NodeValue exec(NodeValue value) { + try { + GeometryWrapper geometry = GeometryWrapper.extract(value); + return NodeValue.makeDouble(geometry.getMinZ()); + } catch (DatatypeFormatException | IllegalStateException ex) { + throw new ExprEvalException(ex.getMessage(), ex); + } + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtrema.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtrema.java new file mode 100644 index 0000000000..035c6d751d --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtrema.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.implementation; + +import java.util.function.DoubleBinaryOperator; + +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateSequenceFilter; +import org.locationtech.jts.geom.Geometry; + +/** + * Finds coordinate extrema in the input geometry's SRS axis order. + * X and Y denote the first and second SRS dimensions, respectively. + */ +final class GeometryCoordinateExtrema { + private GeometryCoordinateExtrema() { + } + + static double maxX(GeometryWrapper geometry) { + return extreme(geometry, 0, Math::max); + } + + static double maxY(GeometryWrapper geometry) { + return extreme(geometry, 1, Math::max); + } + + static double maxZ(GeometryWrapper geometry) { + return zExtreme(geometry, Math::max); + } + + static double minX(GeometryWrapper geometry) { + return extreme(geometry, 0, Math::min); + } + + static double minY(GeometryWrapper geometry) { + return extreme(geometry, 1, Math::min); + } + + static double minZ(GeometryWrapper geometry) { + return zExtreme(geometry, Math::min); + } + + private static double extreme(GeometryWrapper geometry, int ordinate, + DoubleBinaryOperator accumulator) { + return extreme(geometry.getParsingGeometry(), ordinate, accumulator, false); + } + + private static double zExtreme(GeometryWrapper geometry, DoubleBinaryOperator accumulator) { + return extreme(geometry.getParsingGeometry(), 2, accumulator, true); + } + + private static double extreme(Geometry geometry, int ordinate, + DoubleBinaryOperator accumulator, boolean skipNaN) { + ExtremaFilter filter = new ExtremaFilter(ordinate, accumulator, skipNaN); + geometry.apply(filter); + return filter.result(); + } + + private static final class ExtremaFilter implements CoordinateSequenceFilter { + private final int ordinate; + private final DoubleBinaryOperator accumulator; + private final boolean skipNaN; + private double result; + private boolean found; + + private ExtremaFilter(int ordinate, DoubleBinaryOperator accumulator, boolean skipNaN) { + this.ordinate = ordinate; + this.accumulator = accumulator; + this.skipNaN = skipNaN; + } + + @Override + public void filter(CoordinateSequence sequence, int index) { + if (skipNaN && !sequence.hasZ()) { + return; + } + double value = skipNaN ? sequence.getZ(index) : sequence.getOrdinate(index, ordinate); + if (Double.isInfinite(value) || (!skipNaN && Double.isNaN(value))) { + throw new IllegalStateException("Geometry has a non-finite ordinate at ordinate index " + ordinate); + } + if (skipNaN && Double.isNaN(value)) { + return; + } + result = found ? accumulator.applyAsDouble(result, value) : value; + found = true; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public boolean isGeometryChanged() { + return false; + } + + private double result() { + if (!found) { + throw new IllegalStateException("Geometry has no eligible ordinate"); + } + return result; + } + } +} 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 daedac38ca..6d72d2773b 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java @@ -387,6 +387,62 @@ public class GeometryWrapper implements Serializable { return parsingGeometry.getGeometryType(); } + /** + * Returns the minimum ordinate of the first SRS dimension (X) + * across all geometry members. + * @throws IllegalStateException if the geometry has no coordinates or the evaluated ordinate is NaN or infinite. + */ + public double getMinX() { + return GeometryCoordinateExtrema.minX(this); + } + + /** + * Returns the minimum ordinate of the second SRS dimension (Y) + * across all geometry members. + * @throws IllegalStateException if the geometry has no coordinates or the evaluated ordinate is NaN or infinite. + */ + public double getMinY() { + return GeometryCoordinateExtrema.minY(this); + } + + /** + * Returns the minimum finite Z ordinate from the original coordinates across all geometry members. + * Members without a Z ordinate are skipped; M is not treated as Z. + * NaN values are skipped as missing Z ordinates; infinite Z values are rejected. + * @throws IllegalStateException if no finite Z exists or a Z ordinate is infinite. + */ + public double getMinZ() { + return GeometryCoordinateExtrema.minZ(this); + } + + /** + * Returns the maximum ordinate of the first SRS dimension (X) + * across all geometry members. + * @throws IllegalStateException if the geometry has no coordinates or the evaluated ordinate is NaN or infinite. + */ + public double getMaxX() { + return GeometryCoordinateExtrema.maxX(this); + } + + /** + * Returns the maximum ordinate of the second SRS dimension (Y) + * across all geometry members. + * @throws IllegalStateException if the geometry has no coordinates or the evaluated ordinate is NaN or infinite. + */ + public double getMaxY() { + return GeometryCoordinateExtrema.maxY(this); + } + + /** + * Returns the maximum finite Z ordinate from the original coordinates across all geometry members. + * Members without a Z ordinate are skipped; M is not treated as Z. + * NaN values are skipped as missing Z ordinates; infinite Z values are rejected. + * @throws IllegalStateException if no finite Z exists or a Z ordinate is infinite. + */ + public double getMaxZ() { + return GeometryCoordinateExtrema.maxZ(this); + } + /** * * @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 c7c3fbe089..d22e772dae 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 @@ -31,6 +31,12 @@ import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_prop import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsEmptyFF; import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsSimpleFF; import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsValidFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MaxXFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MaxYFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MaxZFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinXFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinYFF; +import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.MinZFF; import org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.SpatialDimensionFF; import org.apache.jena.geosparql.implementation.vocabulary.Geo; import org.apache.jena.geosparql.implementation.vocabulary.Geof; @@ -59,9 +65,9 @@ public class GeometryProperty { } /** - * This method loads all the Geometry property filter functions.<br> - * N.B. These functions are not part of the GeoSPARQL standard but have been - * included for convenience using GeometryLiterals. + * Registers {@code geof:} geometry metadata expression functions for use in + * {@code FILTER}, {@code BIND}, and projection expressions. + * Includes GeoSPARQL 1.1 geometry metadata functions and the isValid extension. * * @param registry - the FunctionRegistry to be used */ @@ -73,6 +79,13 @@ 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); } } 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 ab13061da7..83d19e5dd8 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 @@ -83,6 +83,14 @@ public interface Geof { public static final String IS_SIMPLE = GEOF_URI + "isSimple"; public static final String IS_VALID = GEOF_URI + "isValid"; + //GeoSPARQL 1.1 coordinate extrema: + public static final String MIN_X = GEOF_URI + "minX"; + public static final String MIN_Y = GEOF_URI + "minY"; + public static final String MIN_Z = GEOF_URI + "minZ"; + public static final String MAX_X = GEOF_URI + "maxX"; + public static final String MAX_Y = GEOF_URI + "maxY"; + public static final String MAX_Z = GEOF_URI + "maxZ"; + //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/GeometryCoordinateExtremaFFTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryCoordinateExtremaFFTest.java new file mode 100644 index 0000000000..418893b171 --- /dev/null +++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryCoordinateExtremaFFTest.java @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertThrows; +import static org.junit.Assume.assumeFalse; + +import java.util.List; + +import org.apache.jena.geosparql.configuration.GeoSPARQLConfig; +import org.apache.jena.geosparql.implementation.datatype.WKTDatatype; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.QueryBuildException; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QuerySolution; +import org.apache.jena.query.ResultSet; +import org.apache.jena.rdf.model.ModelFactory; +import org.apache.jena.sparql.expr.ExprEvalException; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.function.FunctionBase1; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class GeometryCoordinateExtremaFFTest { + @Parameterized.Parameters(name = "function: {0}") + public static List<Object[]> functions() { + return List.of( + new Object[] { "minX", new MinXFF() }, + new Object[] { "minY", new MinYFF() }, + new Object[] { "minZ", new MinZFF() }, + new Object[] { "maxX", new MaxXFF() }, + new Object[] { "maxY", new MaxYFF() }, + new Object[] { "maxZ", new MaxZFF() } + ); + } + + private final String name; + private final FunctionBase1 function; + + public GeometryCoordinateExtremaFFTest(String name, FunctionBase1 function) { + this.name = name; + this.function = function; + } + + @BeforeClass + public static void setup() { + GeoSPARQLConfig.setupNoIndex(); + } + + @Test + public void iriArgumentRaisesExpressionError() { + assertThrows(ExprEvalException.class, () -> function.exec(NodeValue.makeNode(NodeFactory.createURI("urn:not-a-literal")))); + } + + @Test + public void numericArgumentRaisesExpressionError() { + assertThrows(ExprEvalException.class, () -> function.exec(NodeValue.makeInteger(42))); + } + + @Test + public void stringWithoutGeometryDatatypeRaisesExpressionError() { + assertThrows(ExprEvalException.class, () -> function.exec(NodeValue.makeString("POINT (1 2)"))); + } + + @Test + public void malformedWktRaisesExpressionError() { + NodeValue malformed = NodeValue.makeNode("invalid", WKTDatatype.INSTANCE); + assertThrows(ExprEvalException.class, () -> function.exec(malformed)); + } + + @Test + public void iriArgumentLeavesBindUnbound() { + assertUnbound("<urn:not-a-literal>"); + } + + @Test + public void numericArgumentLeavesBindUnbound() { + assertUnbound("42"); + } + + @Test + public void stringWithoutGeometryDatatypeLeavesBindUnbound() { + assertUnbound("'POINT (1 2)'"); + } + + @Test + public void malformedWktLeavesBindUnbound() { + assertUnbound("'invalid'^^geo:wktLiteral"); + } + + @Test + public void unboundArgumentLeavesBindUnbound() { + assertUnbound("?missing"); + } + + @Test + public void missingArgumentIsRejectedAtQueryBuild() { + assertThrows(QueryBuildException.class, () -> evaluate("geof:" + name + "()")); + } + + @Test + public void extraArgumentIsRejectedAtQueryBuild() { + assertThrows(QueryBuildException.class, + () -> evaluate("geof:" + name + "('POINT EMPTY'^^geo:wktLiteral, 1)")); + } + + @Test + public void extremaReturnDoubleLiterals() { + double expected = switch (name) { + case "minX" -> -9; + case "maxX" -> -2; + case "minY" -> 3; + case "maxY" -> 8; + case "minZ" -> -7; + case "maxZ" -> -1; + default -> throw new AssertionError(name); + }; + assertEquals(NodeValue.makeDouble(expected).asNode(), + evaluate("geof:" + name + "('LINESTRING ZM (-9 8 -7 100, -2 3 -1 -100)'^^geo:wktLiteral)")); + } + + @Test + public void epsg4326LineStringExtremaFollowSrsDimensionOrder() { + String argument = "'<http://www.opengis.net/def/crs/EPSG/0/4326> " + + "LINESTRING (10 100, 20 120)'^^geo:wktLiteral"; + if (name.endsWith("Z")) { + assertUnbound(argument); + } else { + double expected = switch (name) { + case "minX" -> 10; + case "maxX" -> 20; + case "minY" -> 100; + case "maxY" -> 120; + default -> throw new AssertionError(name); + }; + assertEquals(NodeValue.makeDouble(expected).asNode(), + evaluate("geof:" + name + "(" + argument + ")")); + } + } + + @Test + public void infiniteOrdinateRaisesExpressionErrorAndLeavesBindUnbound() { + for (String infinity : new String[] { "Infinity", "-Infinity" }) { + String coordinate = switch (name.charAt(name.length() - 1)) { + case 'X' -> infinity + " 1 1"; + case 'Y' -> "1 " + infinity + " 1"; + case 'Z' -> "1 1 " + infinity; + default -> throw new AssertionError(name); + }; + String wkt = "LINESTRING Z (-4 -4 -4, " + coordinate + ", 9 9 9)"; + assertThrows(wkt, ExprEvalException.class, + () -> function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertUnbound("'" + wkt + "'^^geo:wktLiteral"); + } + } + + @Test + public void nanXYOrdinateRaisesExpressionErrorAndLeavesBindUnbound() { + assumeFalse("Only X/Y extrema are covered by this test", name.endsWith("Z")); + String coordinate = name.endsWith("Y") ? "2 NaN" : "NaN 2"; + String wkt = "LINESTRING (1 1, " + coordinate + ", 3 3)"; + assertThrows(wkt, ExprEvalException.class, + () -> function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertUnbound("'" + wkt + "'^^geo:wktLiteral"); + } + + @Test + public void emptyGeometryRaisesExpressionErrorAndLeavesBindUnbound() { + assertThrows(ExprEvalException.class, + () -> function.exec(NodeValue.makeNode("POINT Z EMPTY", WKTDatatype.INSTANCE))); + assertUnbound("'POINT Z EMPTY'^^geo:wktLiteral"); + } + + @Test + public void layoutsWithoutZStillHaveXYExtrema() { + for (String wkt : new String[] { "POINT (1 2)", "POINT M (1 2 99)" }) { + if (name.endsWith("Z")) { + assertThrows(wkt, ExprEvalException.class, + () -> function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertUnbound("'" + wkt + "'^^geo:wktLiteral"); + } else { + double expected = name.endsWith("X") ? 1 : 2; + assertEquals(wkt, NodeValue.makeDouble(expected).asNode(), + evaluate("geof:" + name + "('" + wkt + "'^^geo:wktLiteral)")); + } + } + } + + @Test + public void multiPointExtremaDistinguishZFromMeasures() { + for (boolean hasZ : new boolean[] { false, true }) { + String wkt = hasZ ? "MULTIPOINT ZM ((1 2 -4 10), (3 4 9 20))" + : "MULTIPOINT M ((1 2 10), (3 4 20))"; + if (name.endsWith("Z") && !hasZ) { + assertThrows(ExprEvalException.class, + () -> function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertUnbound("'" + wkt + "'^^geo:wktLiteral"); + } else { + double value = switch (name) { + case "minX" -> 1; + case "maxX" -> 3; + case "minY" -> 2; + case "maxY" -> 4; + case "minZ" -> -4; + case "maxZ" -> 9; + default -> throw new AssertionError(name); + }; + NodeValue expected = NodeValue.makeDouble(value); + assertEquals(expected, function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertEquals(expected.asNode(), evaluate("geof:" + name + "('" + wkt + "'^^geo:wktLiteral)")); + } + } + } + + @Test + public void mixedCollectionExtremaDoNotDependOnMemberOrder() { + for (String wkt : new String[] { + "GEOMETRYCOLLECTION (POINT Z (1 2 3))", + "GEOMETRYCOLLECTION (POINT (1 2), POINT M (1 2 999), POINT Z (1 2 3))", + "GEOMETRYCOLLECTION (POINT Z (1 2 3), POINT M (1 2 999), POINT (1 2))" }) { + NodeValue expected = NodeValue.makeDouble(name.endsWith("X") ? 1 : name.endsWith("Y") ? 2 : 3); + assertEquals(expected, function.exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE))); + assertEquals(expected.asNode(), evaluate("geof:" + name + "('" + wkt + "'^^geo:wktLiteral)")); + } + } + + @Test + public void gmlExtremaFollowSrsDimensionOrder() { + String point = """ + '<gml:Point xmlns:gml="http://www.opengis.net/gml/3.2" + srsName="http://www.opengis.net/def/crs/EPSG/0/4979"> + <gml:pos srsDimension="3">10 100 -4</gml:pos> + </gml:Point>'^^geo:gmlLiteral + """.replace("\n", " "); + double expected = name.endsWith("X") ? 10 : name.endsWith("Y") ? 100 : -4; + assertEquals(NodeValue.makeDouble(expected).asNode(), evaluate("geof:" + name + "(" + point + ")")); + } + + private static Node evaluate(String expression) { + String query = """ + PREFIX geof: <http://www.opengis.net/def/function/geosparql/> + PREFIX geo: <http://www.opengis.net/ont/geosparql#> + SELECT ?result WHERE { BIND(%s AS ?result) } + """.formatted(expression); + try (QueryExecution execution = QueryExecution.create(query, ModelFactory.createDefaultModel())) { + ResultSet results = execution.execSelect(); + assertTrue("Expected one solution for " + expression, results.hasNext()); + QuerySolution solution = results.next(); + assertFalse("Expected only one solution for " + expression, results.hasNext()); + return solution.contains("result") ? solution.get("result").asNode() : null; + } + } + + private void assertUnbound(String argument) { + String expression = "geof:" + name + "(" + argument + ")"; + assertNull(expression, evaluate(expression)); + } +} diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtremaTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtremaTest.java new file mode 100644 index 0000000000..865eba6ca2 --- /dev/null +++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryCoordinateExtremaTest.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.apache.jena.geosparql.implementation; + +import org.apache.jena.geosparql.implementation.datatype.WKTDatatype; +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.junit.Test; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.CoordinateSequence; +import org.locationtech.jts.geom.CoordinateXYM; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.GeometryFactory; +import org.locationtech.jts.geom.impl.CoordinateArraySequence; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class GeometryCoordinateExtremaTest { + @Test + public void measuredMultiPointPreservesMWithoutCreatingZ() { + GeometryWrapper geometry = GeometryWrapper.extract("MULTIPOINT M ((1 2 10), (3 4 20))", WKTDatatype.URI); + for (int i = 0; i < 2; i++) { + Coordinate coordinate = geometry.getParsingGeometry().getGeometryN(i).getCoordinate(); + assertEquals(10 * (i + 1), coordinate.getM(), 0); + assertEquals(Double.NaN, coordinate.getZ(), 0); + } + assertThrows(IllegalStateException.class, geometry::getMinZ); + assertThrows(IllegalStateException.class, geometry::getMaxZ); + } + + @Test + public void zExtremaIgnoreMeasuresInMixedJtsCollections() { + GeometryFactory factory = new GeometryFactory(); + for (double measure : new double[] { -1000, 1000, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY }) { + Geometry xyz = factory.createPoint(new Coordinate(1, 2, 99)); + Geometry xym = factory.createPoint(new CoordinateXYM(3, 4, measure)); + for (Geometry[] members : new Geometry[][] { { xyz, xym }, { xym, xyz } }) { + Geometry mixed = factory.createGeometryCollection(members); + GeometryWrapper geometry = GeometryWrapperFactory.createGeometry(mixed, + SRS_URI.DEFAULT_WKT_CRS84, WKTDatatype.URI); + assertEquals(99, geometry.getMinZ(), 0); + assertEquals(99, geometry.getMaxZ(), 0); + } + } + } + + @Test + public void factoryHandlesEmptyCollectionsWithoutCoordinates() { + GeometryFactory factory = new GeometryFactory(); + Geometry[] members = { + factory.createPoint(new CoordinateArraySequence(0, 3, 0)), + factory.createPoint(new CoordinateArraySequence(0, 3, 1)) + }; + for (Geometry empty : new Geometry[] { + factory.createGeometryCollection(), + factory.createGeometryCollection(new Geometry[] { factory.createGeometryCollection() }), + factory.createGeometryCollection(members) }) { + GeometryWrapper geometry = GeometryWrapperFactory.createGeometry(empty, + SRS_URI.DEFAULT_WKT_CRS84, WKTDatatype.URI); + assertThrows(IllegalStateException.class, geometry::getMinX); + assertThrows(IllegalStateException.class, geometry::getMaxX); + assertThrows(IllegalStateException.class, geometry::getMinY); + assertThrows(IllegalStateException.class, geometry::getMaxY); + assertThrows(IllegalStateException.class, geometry::getMinZ); + assertThrows(IllegalStateException.class, geometry::getMaxZ); + } + } + + @Test + public void factoryZExtremaFindLaterValuesWhenFirstZIsMissing() { + GeometryFactory factory = new GeometryFactory(); + CoordinateSequence coordinates = new CoordinateArraySequence(2, 3, 0); + for (int i = 0; i < coordinates.size(); i++) { + coordinates.setOrdinate(i, 0, i); + coordinates.setOrdinate(i, 1, i); + coordinates.setOrdinate(i, 2, i == 0 ? Double.NaN : 9); + } + Geometry line = factory.createLineString(coordinates); + Geometry homogeneousCollection = factory.createGeometryCollection(new Geometry[] { + factory.createPoint(new Coordinate(0, 0, Double.NaN)), line }); + for (Geometry source : new Geometry[] { line, homogeneousCollection }) { + for (String srs : new String[] { SRS_URI.DEFAULT_WKT_CRS84, + "http://www.opengis.net/def/crs/EPSG/0/4979" }) { + GeometryWrapper geometry = GeometryWrapperFactory.createGeometry(source, srs, WKTDatatype.URI); + assertEquals(9, geometry.getMinZ(), 0); + assertEquals(9, geometry.getMaxZ(), 0); + } + } + } + + @Test + public void factoryPointPreservesZInAuthorityAxisOrder() { + GeometryWrapper geometry = GeometryWrapperFactory.createPoint(new Coordinate(100, 10, -4), + "http://www.opengis.net/def/crs/EPSG/0/4979", WKTDatatype.URI); + assertEquals(10, geometry.getMinX(), 0); + assertEquals(10, geometry.getMaxX(), 0); + assertEquals(100, geometry.getMinY(), 0); + assertEquals(100, geometry.getMaxY(), 0); + assertEquals(-4, geometry.getMinZ(), 0); + assertEquals(-4, geometry.getMaxZ(), 0); + } + + @Test + public void factoryGeometryPreservesZAndMeasureInAuthorityAxisOrder() { + CustomCoordinateSequence coordinates = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XYZM, "100 10 -4 7"); + Geometry point = CustomGeometryFactory.theInstance().createPoint(coordinates); + GeometryWrapper geometry = GeometryWrapperFactory.createGeometry(point, + "http://www.opengis.net/def/crs/EPSG/0/4979", WKTDatatype.URI); + assertEquals(10, geometry.getMinX(), 0); + assertEquals(10, geometry.getMaxX(), 0); + assertEquals(100, geometry.getMinY(), 0); + assertEquals(100, geometry.getMaxY(), 0); + assertEquals(-4, geometry.getMinZ(), 0); + assertEquals(-4, geometry.getMaxZ(), 0); + assertEquals(7, geometry.getParsingGeometry().getCoordinate().getM(), 0); + } + + @Test + public void xyExtremaFollowSrsDimensionOrder() { + GeometryWrapper crs84Point = GeometryWrapper.extract( + "<http://www.opengis.net/def/crs/OGC/1.3/CRS84> POINT(10 100)", + WKTDatatype.URI); + GeometryWrapper epsg4326Point = GeometryWrapper.extract( + "<http://www.opengis.net/def/crs/EPSG/0/4326> POINT(10 100)", + WKTDatatype.URI); + + assertEquals(10.0, crs84Point.getMinX(), 0.0); + assertEquals(10.0, crs84Point.getMaxX(), 0.0); + assertEquals(100.0, crs84Point.getMinY(), 0.0); + assertEquals(100.0, crs84Point.getMaxY(), 0.0); + assertEquals(10.0, epsg4326Point.getMinX(), 0.0); + assertEquals(10.0, epsg4326Point.getMaxX(), 0.0); + assertEquals(100.0, epsg4326Point.getMinY(), 0.0); + assertEquals(100.0, epsg4326Point.getMaxY(), 0.0); + } + + @Test + public void xyExtremaRejectNaNAlongsideFiniteValues() { + CustomCoordinateSequence coordinatesX = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XY, "1 1,NaN 2,3 3"); + GeometryWrapper nanX = wrapper( + CustomGeometryFactory.theInstance().createLineString(coordinatesX), + CoordinateSequenceDimensions.XY); + assertThrows(IllegalStateException.class, nanX::getMinX); + assertThrows(IllegalStateException.class, nanX::getMaxX); + assertEquals(1, nanX.getMinY(), 0); + assertEquals(3, nanX.getMaxY(), 0); + + CustomCoordinateSequence coordinatesY = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XY, "1 1,2 NaN,3 3"); + GeometryWrapper nanY = wrapper( + CustomGeometryFactory.theInstance().createLineString(coordinatesY), + CoordinateSequenceDimensions.XY); + assertThrows(IllegalStateException.class, nanY::getMinY); + assertThrows(IllegalStateException.class, nanY::getMaxY); + assertEquals(1, nanY.getMinX(), 0); + assertEquals(3, nanY.getMaxX(), 0); + } + + @Test + public void zExtremaIgnoreMissingZSentinels() { + CustomCoordinateSequence coordinates = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XYZ, + "0 0 NaN,1 1 -4,2 2 NaN,3 3 9"); + GeometryWrapper geometry = wrapper( + CustomGeometryFactory.theInstance().createLineString(coordinates), + CoordinateSequenceDimensions.XYZ); + + assertEquals(-4.0, geometry.getMinZ(), 0.0); + assertEquals(9.0, geometry.getMaxZ(), 0.0); + } + + @Test + public void zExtremaFailWhenAllZValuesAreMissing() { + CustomCoordinateSequence coordinates = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XYZ, + "0 0 NaN,1 1 NaN"); + GeometryWrapper geometry = wrapper( + CustomGeometryFactory.theInstance().createLineString(coordinates), + CoordinateSequenceDimensions.XYZ); + + assertThrows(IllegalStateException.class, + () -> geometry.getMinZ()); + assertThrows(IllegalStateException.class, + () -> geometry.getMaxZ()); + } + + @Test + public void zExtremaRejectInfinityAlongsideFiniteValues() { + for (String infinity : new String[] { "Infinity", "-Infinity" }) { + CustomCoordinateSequence coordinates = new CustomCoordinateSequence( + CoordinateSequenceDimensions.XYZ, "0 0 -4,1 1 " + infinity + ",2 2 9"); + GeometryWrapper geometry = wrapper( + CustomGeometryFactory.theInstance().createLineString(coordinates), + CoordinateSequenceDimensions.XYZ); + assertThrows(infinity, IllegalStateException.class, geometry::getMinZ); + assertThrows(infinity, IllegalStateException.class, geometry::getMaxZ); + } + } + + @Test + public void zExtremaRequireZBearingSequences() { + CoordinateSequence coordinates = new CoordinateArraySequence( + new Coordinate[]{new Coordinate(1, 2, 99)}, 3, 1); + GeometryWrapper geometry = wrapper( + CustomGeometryFactory.theInstance().createPoint(coordinates), + CoordinateSequenceDimensions.XYM); + + assertThrows(IllegalStateException.class, + () -> geometry.getMinZ()); + assertThrows(IllegalStateException.class, + () -> geometry.getMaxZ()); + } + + @Test + public void allExtremaScanEveryCoordinateIncludingNegativeValues() { + GeometryWrapper geometry = GeometryWrapper.extract("LINESTRING ZM (-9 8 -7 100, -2 3 -1 -100)", WKTDatatype.URI); + assertEquals(-9, geometry.getMinX(), 0); + assertEquals(-2, geometry.getMaxX(), 0); + assertEquals(3, geometry.getMinY(), 0); + assertEquals(8, geometry.getMaxY(), 0); + assertEquals(-7, geometry.getMinZ(), 0); + assertEquals(-1, geometry.getMaxZ(), 0); + } + + @Test + public void threeDimensionalExtremaFollowSrsDimensionOrder() { + GeometryWrapper geometry = GeometryWrapper.extract( + "<http://www.opengis.net/def/crs/EPSG/0/4979> LINESTRING Z (10 100 -4, 20 120 9)", WKTDatatype.URI); + assertEquals(10, geometry.getMinX(), 0); + assertEquals(20, geometry.getMaxX(), 0); + assertEquals(100, geometry.getMinY(), 0); + assertEquals(120, geometry.getMaxY(), 0); + assertEquals(-4, geometry.getMinZ(), 0); + assertEquals(9, geometry.getMaxZ(), 0); + } + + @Test + public void nestedCollectionsIncludeAllDescendantsAndSkipEmptyMembers() { + var factory = CustomGeometryFactory.theInstance(); + Geometry first = GeometryWrapper.extract("POINT Z (-9 8 -7)", WKTDatatype.URI).getParsingGeometry(); + Geometry second = GeometryWrapper.extract("POINT Z (-2 3 -1)", WKTDatatype.URI).getParsingGeometry(); + Geometry nested = factory.createGeometryCollection(new Geometry[] { second, factory.createPoint() }); + GeometryWrapper geometry = wrapper(factory.createGeometryCollection(new Geometry[] { first, nested }), + CoordinateSequenceDimensions.XYZ); + assertEquals(-9, geometry.getMinX(), 0); + assertEquals(-2, geometry.getMaxX(), 0); + assertEquals(3, geometry.getMinY(), 0); + assertEquals(8, geometry.getMaxY(), 0); + assertEquals(-7, geometry.getMinZ(), 0); + assertEquals(-1, geometry.getMaxZ(), 0); + } + + @Test + public void emptyGeometriesHaveNoExtrema() { + for (String wkt : new String[] { "POINT Z EMPTY", "LINESTRING Z EMPTY", "POLYGON Z EMPTY", + "MULTIPOINT Z EMPTY", "GEOMETRYCOLLECTION Z EMPTY" }) { + GeometryWrapper geometry = GeometryWrapper.extract(wkt, WKTDatatype.URI); + assertThrows(wkt, IllegalStateException.class, geometry::getMinX); + assertThrows(wkt, IllegalStateException.class, geometry::getMaxX); + assertThrows(wkt, IllegalStateException.class, geometry::getMinY); + assertThrows(wkt, IllegalStateException.class, geometry::getMaxY); + assertThrows(wkt, IllegalStateException.class, geometry::getMinZ); + assertThrows(wkt, IllegalStateException.class, geometry::getMaxZ); + } + } + + @Test + public void polygonZExtremaIncludeInteriorRings() { + GeometryWrapper geometry = GeometryWrapper.extract( + "POLYGON Z ((0 0 1, 10 0 2, 10 10 3, 0 10 4, 0 0 1), " + + "(2 2 -9, 2 3 20, 3 2 6, 2 2 -9))", WKTDatatype.URI); + assertEquals(0, geometry.getMinX(), 0); + assertEquals(10, geometry.getMaxX(), 0); + assertEquals(0, geometry.getMinY(), 0); + assertEquals(10, geometry.getMaxY(), 0); + assertEquals(-9, geometry.getMinZ(), 0); + assertEquals(20, geometry.getMaxZ(), 0); + } + + private static GeometryWrapper wrapper(Geometry geometry, CoordinateSequenceDimensions dimensions) { + return new GeometryWrapper(geometry, SRS_URI.DEFAULT_WKT_CRS84, WKTDatatype.URI, + new DimensionInfo(dimensions, geometry.getDimension())); + } +}
