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()));
+    }
+}

Reply via email to