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 185ca9ff747b0dfa5c0fb75c125819afec2cd217
Author: Edmond Chuc <[email protected]>
AuthorDate: Mon Sep 28 13:46:15 2026 +1000

    GH-4265: Add GeoSPARQL 1.1 area functions
    
    Implement geof:area and geof:metricArea.
---
 .../nontopological/filter_functions/AreaFF.java    |  53 +++++
 .../filter_functions/MetricAreaFF.java             |  43 +++++
 .../implementation/AreaUnitsOfMeasure.java         |  76 ++++++++
 .../geosparql/implementation/GeometryArea.java     |  90 +++++++++
 .../geosparql/implementation/GeometryWrapper.java  |  19 ++
 .../function_registration/NonTopological.java      |   4 +
 .../geosparql/implementation/vocabulary/Geof.java  |   3 +
 .../implementation/vocabulary/Unit_URI.java        |  12 ++
 .../filter_functions/AreaFFTest.java               | 164 ++++++++++++++++
 .../filter_functions/AreaFunctionTestSupport.java  |  51 +++++
 .../filter_functions/MetricAreaFFTest.java         | 107 +++++++++++
 .../geosparql/implementation/GeometryAreaTest.java | 213 +++++++++++++++++++++
 12 files changed, 835 insertions(+)

diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFF.java
new file mode 100644
index 0000000000..3e1fe0bf55
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFF.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.geof.nontopological.filter_functions;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.UnitsConversionException;
+import org.apache.jena.geosparql.implementation.registry.UnitsURIException;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase2;
+
+/** Implements geof:area with an explicit area unit IRI or xsd:anyURI literal. 
*/
+public class AreaFF extends FunctionBase2 {
+
+    @Override
+    public NodeValue exec(NodeValue value, NodeValue units) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            String unitsURI;
+            if (units.isIRI()) {
+                unitsURI = units.asNode().getURI();
+            } else if (units.asNode().isLiteral()
+                    && 
XSDDatatype.XSDanyURI.getURI().equals(units.asNode().getLiteralDatatypeURI())) {
+                unitsURI = units.asNode().getLiteralLexicalForm();
+            } else {
+                throw new ExprEvalException("Expected a unit IRI or xsd:anyURI 
literal: " + units);
+            }
+            return NodeValue.makeDouble(geometry.area(unitsURI));
+        } catch (DatatypeFormatException | UnitsConversionException | 
UnitsURIException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFF.java
new file mode 100644
index 0000000000..4a19f52dac
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFF.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.geof.nontopological.filter_functions;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.geosparql.implementation.UnitsConversionException;
+import org.apache.jena.geosparql.implementation.registry.UnitsURIException;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+
+/** Implements geof:metricArea in square metres. */
+public class MetricAreaFF extends FunctionBase1 {
+
+    @Override
+    public NodeValue exec(NodeValue value) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return NodeValue.makeDouble(geometry.area());
+        } catch (DatatypeFormatException | UnitsConversionException | 
UnitsURIException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/AreaUnitsOfMeasure.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/AreaUnitsOfMeasure.java
new file mode 100644
index 0000000000..eda857c922
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/AreaUnitsOfMeasure.java
@@ -0,0 +1,76 @@
+/*
+ * 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.HashMap;
+import java.util.Map;
+
+import javax.measure.Unit;
+import javax.measure.quantity.Area;
+
+import org.apache.jena.geosparql.implementation.registry.UnitsRegistry;
+import org.apache.jena.geosparql.implementation.registry.UnitsURIException;
+import org.apache.jena.geosparql.implementation.vocabulary.Unit_URI;
+import org.apache.sis.measure.Units;
+
+/**
+ * Explicit area units accepted by {@code geof:area}.
+ *
+ * This registry is separate from {@link UnitsRegistry}, which does not provide
+ * quantity-typed lookup. Units added only to {@code UnitsRegistry} are not
+ * accepted as area units here.
+ */
+final class AreaUnitsOfMeasure {
+    private static final Map<String, Unit<Area>> UNITS = new HashMap<>();
+
+    static {
+        UNITS.put(Unit_URI.SQUARE_METRE_QUDT, Units.SQUARE_METRE);
+        UNITS.put(Unit_URI.SQUARE_KILOMETRE_QUDT, square(Units.KILOMETRE));
+        UNITS.put(Unit_URI.SQUARE_CENTIMETRE_QUDT, square(Units.CENTIMETRE));
+        UNITS.put(Unit_URI.SQUARE_MILLIMETRE_QUDT, square(Units.MILLIMETRE));
+        UNITS.put(Unit_URI.SQUARE_FOOT_QUDT, square(Units.FOOT));
+        UNITS.put(Unit_URI.SQUARE_YARD_QUDT, square(Units.FOOT.multiply(3)));
+        UNITS.put(Unit_URI.SQUARE_INCH_QUDT, square(Units.INCH));
+        UNITS.put(Unit_URI.SQUARE_MILE_QUDT, square(Units.STATUTE_MILE));
+        UNITS.put(Unit_URI.HECTARE_QUDT, Units.HECTARE);
+        UNITS.put(Unit_URI.ACRE_QUDT, 
Units.FOOT.pow(2).multiply(43560).asType(Area.class));
+    }
+
+    private AreaUnitsOfMeasure() {
+    }
+
+    static Unit<Area> getUnit(String uri) {
+        Unit<Area> unit = UNITS.get(uri);
+        if (unit != null) {
+            return unit;
+        }
+        try {
+            UnitsRegistry.getUnit(uri);
+        } catch (UnitsURIException ex) {
+            throw new UnitsURIException("Unrecognised area unit URI: " + uri, 
ex);
+        }
+        throw new UnitsConversionException("Area requires an area unit: " + 
uri);
+    }
+
+    private static Unit<Area> square(Unit<?> lengthUnit) {
+        return lengthUnit.pow(2).asType(Area.class);
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryArea.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryArea.java
new file mode 100644
index 0000000000..501e9f40d8
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryArea.java
@@ -0,0 +1,90 @@
+/*
+ * 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 javax.measure.IncommensurableException;
+import javax.measure.Unit;
+import javax.measure.quantity.Area;
+
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.referencing.CRS;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.cs.CoordinateSystem;
+
+/**
+ * Calculates planar area for polygonal geometries and converts to target area 
units.
+ *
+ * <p>Area is calculated for {@link Polygon} and {@link MultiPolygon} 
geometries;
+ * empty geometries and non-polygonal types return zero. Non-empty polygonal 
geometries
+ * require a non-geographic horizontal CRS with equivalent linear units on 
both axes.
+ */
+final class GeometryArea {
+    private GeometryArea() {
+    }
+
+    static double calculate(GeometryWrapper geometry, String targetUnitUri) {
+        Unit<Area> targetUnit = AreaUnitsOfMeasure.getUnit(targetUnitUri);
+        Geometry xyGeometry = geometry.getXYGeometry();
+        if (!(xyGeometry instanceof Polygon || xyGeometry instanceof 
MultiPolygon)
+                || xyGeometry.isEmpty()) {
+            return 0.0;
+        }
+        if (geometry.getSrsInfo().isGeographic()) {
+            throw new UnitsConversionException("Area is not supported for 
geographic coordinate reference systems.");
+        }
+        Unit<Area> sourceUnit = 
equivalentHorizontalAxisUnits(geometry).getUnit()
+                .pow(2).asType(Area.class);
+        return Quantities.create(xyGeometry.getArea(), sourceUnit)
+                .to(targetUnit).getValue().doubleValue();
+    }
+
+    private static UnitsOfMeasure 
equivalentHorizontalAxisUnits(GeometryWrapper geometry) {
+        CoordinateReferenceSystem horizontalCrs = CRS.getHorizontalComponent(
+                geometry.getSrsInfo().getCrs());
+        if (horizontalCrs == null || horizontalCrs.getCoordinateSystem() == 
null
+                || horizontalCrs.getCoordinateSystem().getDimension() != 2) {
+            throw new UnitsConversionException(
+                    "Area requires a two-dimensional horizontal source 
coordinate system.");
+        }
+        CoordinateSystem coordinateSystem = 
horizontalCrs.getCoordinateSystem();
+        Unit<?> firstAxisUnit = coordinateSystem.getAxis(0).getUnit();
+        Unit<?> secondAxisUnit = coordinateSystem.getAxis(1).getUnit();
+        try {
+            if (!firstAxisUnit.isCompatible(secondAxisUnit)
+                    || 
!firstAxisUnit.getConverterToAny(secondAxisUnit).isIdentity()) {
+                throw new UnitsConversionException(
+                        "Area requires equivalent linear units on both 
horizontal source axes.");
+            }
+        } catch (IncommensurableException e) {
+            throw new UnitsConversionException(
+                    "Area requires equivalent linear units on both horizontal 
source axes.", e);
+        }
+        UnitsOfMeasure sourceUnits = new UnitsOfMeasure(horizontalCrs);
+        if (!sourceUnits.isLinearUnits()) {
+            throw new UnitsConversionException(
+                    "Area requires linear units on both horizontal source 
axes.");
+        }
+        return sourceUnits;
+    }
+}
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 038d471348..411d5319e9 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
@@ -810,6 +810,25 @@ public class GeometryWrapper implements Serializable {
         return new GeometryWrapper(parsingGeo, xyGeo, srsInfo.getSrsURI(), 
geometryDatatypeURI, dimensionInfo);
     }
 
+    /** Polygon or MultiPolygon area defaulting to square metres. */
+    public double area() {
+        return area(Unit_URI.SQUARE_METRE_QUDT);
+    }
+
+    /**
+     * Returns Polygon or MultiPolygon area in the requested area unit.
+     *
+     * Empty and non-polygonal geometries return zero. Nonempty polygons 
require
+     * a non-geographic horizontal CRS with equivalent linear units on both 
axes.
+     *
+     * @param unitsURI URI of an explicit area unit.
+     * @throws 
org.apache.jena.geosparql.implementation.registry.UnitsURIException if the unit 
URI is unknown.
+     * @throws UnitsConversionException if the source CRS or target units are 
unsupported.
+     */
+    public double area(String unitsURI) {
+        return GeometryArea.calculate(this, unitsURI);
+    }
+
     /**
      * Returns the planar centroid in the source SRS and datatype. A result in 
a
      * three-dimensional CRS has {@code Z=0}; {@code Z} and {@code M} are 
ignored
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
index fd893806ae..7397e06c7b 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/NonTopological.java
@@ -20,6 +20,7 @@
  */
 package org.apache.jena.geosparql.implementation.function_registration;
 
+import org.apache.jena.geosparql.geof.nontopological.filter_functions.AreaFF;
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.AsGeoJSONFF;
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.BoundaryFF;
 import org.apache.jena.geosparql.geof.nontopological.filter_functions.BufferFF;
@@ -30,6 +31,7 @@ import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.DistanceFF
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.EnvelopFF;
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.GetSRIDFF;
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.IntersectionFF;
+import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.MetricAreaFF;
 import 
org.apache.jena.geosparql.geof.nontopological.filter_functions.SymmetricDifferenceFF;
 import org.apache.jena.geosparql.geof.nontopological.filter_functions.UnionFF;
 import org.apache.jena.geosparql.implementation.vocabulary.Geof;
@@ -62,6 +64,8 @@ public class NonTopological {
         registry.put(Geof.INTERSECTION_NAME, IntersectionFF.class);
         registry.put(Geof.SYMDIFFERENCE_NAME, SymmetricDifferenceFF.class);
         registry.put(Geof.UNION_NAME, UnionFF.class);
+        registry.put(Geof.AREA_NAME, AreaFF.class);
+        registry.put(Geof.METRIC_AREA_NAME, MetricAreaFF.class);
         registry.put(Geof.AS_GEOJSON, AsGeoJSONFF.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 aa3384ed29..c7eae6da3f 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
@@ -75,6 +75,9 @@ public interface Geof {
     public static final String CONVEXHULL_NAME = GEOF_URI + "convexHull";
     public static final String GETSRID_NAME = GEOF_URI + "getSRID";
 
+    public static final String AREA_NAME = GEOF_URI + "area";
+    public static final String METRIC_AREA_NAME = GEOF_URI + "metricArea";
+
     // Geometry property function symbols:
     public static final String GEOMETRY_TYPE = GEOF_URI + "geometryType";
     public static final String IS_3D = GEOF_URI + "is3D";
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Unit_URI.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Unit_URI.java
index f2f4270137..03727200eb 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Unit_URI.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Unit_URI.java
@@ -53,6 +53,18 @@ public interface Unit_URI {
     public static final String NAUTICAL_MILE_URL = GeoSPARQL_URI.UOM_URI + 
"nauticalMile";
     public static final String US_SURVEY_FOOT_URL = GeoSPARQL_URI.UOM_URI + 
"surveyFootUS";
 
+    // Area units from QUDT.
+    public static final String SQUARE_METRE_QUDT = 
"http://qudt.org/vocab/unit/M2";;
+    public static final String SQUARE_KILOMETRE_QUDT = 
"http://qudt.org/vocab/unit/KiloM2";;
+    public static final String SQUARE_CENTIMETRE_QUDT = 
"http://qudt.org/vocab/unit/CentiM2";;
+    public static final String SQUARE_MILLIMETRE_QUDT = 
"http://qudt.org/vocab/unit/MilliM2";;
+    public static final String SQUARE_FOOT_QUDT = 
"http://qudt.org/vocab/unit/FT2";;
+    public static final String SQUARE_YARD_QUDT = 
"http://qudt.org/vocab/unit/YD2";;
+    public static final String SQUARE_INCH_QUDT = 
"http://qudt.org/vocab/unit/IN2";;
+    public static final String SQUARE_MILE_QUDT = 
"http://qudt.org/vocab/unit/MI2";;
+    public static final String HECTARE_QUDT = "http://qudt.org/vocab/unit/HA";;
+    public static final String ACRE_QUDT = "http://qudt.org/vocab/unit/AC";;
+
     //URN references in: 
https://sis.apache.org/apidocs/org/apache/sis/measure/Units.html
     //Angular
     public static final String RADIAN_URN = "urn:ogc:def:uom:EPSG::9101";
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFFTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFFTest.java
new file mode 100644
index 0000000000..267a4386e3
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFFTest.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.geof.nontopological.filter_functions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.vocabulary.Unit_URI;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.query.QueryBuildException;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class AreaFFTest {
+    private final AreaFF function = new AreaFF();
+    private static final String PROJECTED = 
"<http://www.opengis.net/def/crs/EPSG/0/27700> ";
+    private static final String POLYGON = 
"'<http://www.opengis.net/def/crs/EPSG/0/27700> POLYGON ((0 0, 1000 0, 1000 
1000, 0 1000, 0 0))'^^geo:wktLiteral";
+    private static final String SQUARE_METRE = "<" + 
Unit_URI.SQUARE_METRE_QUDT + ">";
+
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void polygonReturnsDoubleSquareMetres() {
+        String polygon = "'" + PROJECTED + "POLYGON ((0 0, 3 0, 3 4, 0 4, 0 
0))'^^geo:wktLiteral";
+        assertEquals(NodeValue.makeDouble(12).asNode(), evaluate(polygon, 
SQUARE_METRE));
+    }
+
+    @Test
+    public void nonPolygonalAndEmptyGeometriesReturnZero() {
+        for (String wkt : new String[] { "POINT (1 2)", PROJECTED + "POLYGON 
EMPTY" }) {
+            assertEquals(NodeValue.makeDouble(0).asNode(), evaluate("'" + wkt 
+ "'^^geo:wktLiteral", SQUARE_METRE));
+        }
+    }
+
+    @Test
+    public void geographicPolygonRaisesExpressionError() {
+        String wkt = "POLYGON ((0 0, 1 0, 1 1, 0 0))";
+        assertNull(evaluate("'" + wkt + "'^^geo:wktLiteral", SQUARE_METRE));
+        assertThrows(ExprEvalException.class,
+                () -> function.exec(NodeValue.makeNode(wkt, 
WKTDatatype.INSTANCE),
+                        
NodeValue.makeNode(NodeFactory.createURI(Unit_URI.SQUARE_METRE_QUDT))));
+    }
+
+    @Test
+    public void gmlPolygonIsAccepted() {
+        String gml = """
+            '<gml:Polygon xmlns:gml="http://www.opengis.net/gml/3.2";
+                srsName="http://www.opengis.net/def/crs/EPSG/0/27700";>
+              <gml:exterior><gml:LinearRing><gml:posList>0 0 3 0 3 4 0 4 0 
0</gml:posList></gml:LinearRing></gml:exterior>
+            </gml:Polygon>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        assertEquals(NodeValue.makeDouble(12).asNode(), evaluate(gml, 
SQUARE_METRE));
+    }
+
+    @Test
+    public void malformedOrUnboundGeometryLeavesBindUnbound() {
+        for (String value : new String[] { "42", "'POINT (1 2)'", 
"<urn:geometry>", "'invalid'^^geo:wktLiteral", "?missing" }) {
+            assertNull(value, evaluate(value, SQUARE_METRE));
+        }
+    }
+
+    @Test
+    public void invalidGeometryArgumentsRaiseExpressionErrors() {
+        NodeValue squareMetre = 
NodeValue.makeNode(NodeFactory.createURI(Unit_URI.SQUARE_METRE_QUDT));
+        for (NodeValue value : new NodeValue[] { NodeValue.makeInteger(42), 
NodeValue.makeString("POINT (1 2)"),
+                NodeValue.makeNode(NodeFactory.createURI("urn:geometry")), 
NodeValue.makeNode("invalid", WKTDatatype.INSTANCE) }) {
+            assertThrows(ExprEvalException.class, () -> function.exec(value, 
squareMetre));
+        }
+    }
+
+    @Test
+    public void wrongArityIsRejectedAtQueryBuild() {
+        String geometry = "'POINT EMPTY'^^geo:wktLiteral";
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:area()"));
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:area(" + geometry + ")"));
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:area(" + geometry + ", 1, 2)"));
+    }
+
+    @Test
+    public void explicitAreaUnitsConvertTheAreaQuantity() {
+        Object[][] expected = {
+                { Unit_URI.SQUARE_METRE_QUDT, 1_000_000.0 },
+                { Unit_URI.SQUARE_KILOMETRE_QUDT, 1.0 },
+                { Unit_URI.SQUARE_CENTIMETRE_QUDT, 10_000_000_000.0 },
+                { Unit_URI.SQUARE_MILLIMETRE_QUDT, 1_000_000_000_000.0 },
+                { Unit_URI.SQUARE_FOOT_QUDT, 10_763_910.416709722 },
+                { Unit_URI.SQUARE_YARD_QUDT, 1_195_990.0463010803 },
+                { Unit_URI.SQUARE_INCH_QUDT, 1_550_003_100.0062 },
+                { Unit_URI.SQUARE_MILE_QUDT, 0.38610215854244585 },
+                { Unit_URI.HECTARE_QUDT, 100.0 },
+                { Unit_URI.ACRE_QUDT, 247.10538146716534 },
+        };
+        for (Object[] entry : expected) {
+            String uri = (String) entry[0];
+            double area = (double) entry[1];
+            Node result = evaluate(POLYGON, "<" + uri + ">");
+            assertEquals(uri, area, ((Number) 
result.getLiteralValue()).doubleValue(), area * 1e-12);
+        }
+    }
+
+    @Test
+    public void undeclaredOgcLookingAreaUriIsRejected() {
+        String uri = "http://www.opengis.net/def/uom/OGC/1.0/squareMetre";;
+        assertThrows(ExprEvalException.class,
+                () -> function.exec(NodeValue.makeNode("POINT EMPTY", 
WKTDatatype.INSTANCE),
+                        NodeValue.makeNode(NodeFactory.createURI(uri))));
+        assertNull(evaluate(POLYGON, "<" + uri + ">"));
+    }
+
+    @Test
+    public void anyUriUnitLiteralIsAccepted() {
+        assertEquals(NodeValue.makeDouble(1).asNode(), evaluate(POLYGON, "'" + 
Unit_URI.SQUARE_KILOMETRE_QUDT + "'^^xsd:anyURI"));
+    }
+
+    @Test
+    public void invalidUnitsRaiseExpressionErrorsEvenForEmptyGeometry() {
+        NodeValue empty = NodeValue.makeNode("POINT EMPTY", 
WKTDatatype.INSTANCE);
+        for (NodeValue unit : new NodeValue[] { 
NodeValue.makeString(Unit_URI.SQUARE_METRE_QUDT), NodeValue.makeInteger(1),
+                NodeValue.makeNode(NodeFactory.createURI("urn:unknown-unit")),
+                
NodeValue.makeNode(NodeFactory.createURI(Unit_URI.KILOMETRE_URL)) }) {
+            assertThrows(ExprEvalException.class, () -> function.exec(empty, 
unit));
+        }
+    }
+
+    @Test
+    public void invalidUnitsLeaveBindUnboundForEmptyAndNonemptyInputs() {
+        for (String unit : new String[] { "'" + Unit_URI.SQUARE_METRE_QUDT + 
"'", "1", "<urn:unknown-unit>",
+                                        "<" + Unit_URI.KILOMETRE_URL + ">", 
"?missing" }) {
+            assertNull(unit, evaluate(POLYGON, unit));
+            assertNull(unit, evaluate("'POINT EMPTY'^^geo:wktLiteral", unit));
+        }
+    }
+
+    private Node evaluate(String geometry, String unit) {
+        return AreaFunctionTestSupport.evaluate("geof:area(" + geometry + ", " 
+ unit + ")");
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFunctionTestSupport.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFunctionTestSupport.java
new file mode 100644
index 0000000000..eb6d60346e
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/AreaFunctionTestSupport.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.geof.nontopological.filter_functions;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.jena.graph.Node;
+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;
+
+final class AreaFunctionTestSupport {
+    private AreaFunctionTestSupport() {
+    }
+
+    static Node evaluate(String expression) {
+        String query = """
+            PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
+            PREFIX geo: <http://www.opengis.net/ont/geosparql#>
+            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
+            SELECT ?result WHERE { BIND(%s AS ?result) }
+            """.formatted(expression);
+        try (QueryExecution execution = QueryExecution.create(query, 
ModelFactory.createDefaultModel())) {
+            ResultSet results = execution.execSelect();
+            assertTrue(expression, results.hasNext());
+            QuerySolution solution = results.next();
+            assertFalse(expression, results.hasNext());
+            return solution.contains("result") ? 
solution.get("result").asNode() : null;
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFFTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFFTest.java
new file mode 100644
index 0000000000..af9019e246
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/nontopological/filter_functions/MetricAreaFFTest.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.geof.nontopological.filter_functions;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+
+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.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class MetricAreaFFTest {
+    private static final String PROJECTED = 
"<http://www.opengis.net/def/crs/EPSG/0/27700> ";
+    private final MetricAreaFF function = new MetricAreaFF();
+
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void polygonReturnsDoubleSquareMetres() {
+        String polygon = "'" + PROJECTED + "POLYGON ((0 0, 3 0, 3 4, 0 4, 0 
0))'^^geo:wktLiteral";
+        assertEquals(NodeValue.makeDouble(12).asNode(), evaluate(polygon));
+    }
+
+    @Test
+    public void nonPolygonalAndEmptyGeometriesReturnZero() {
+        for (String wkt : new String[] { "POINT (1 2)", PROJECTED + "POLYGON 
EMPTY" }) {
+            assertEquals(NodeValue.makeDouble(0).asNode(), evaluate("'" + wkt 
+ "'^^geo:wktLiteral"));
+        }
+    }
+
+    @Test
+    public void geographicPolygonRaisesExpressionError() {
+        String wkt = "POLYGON ((0 0, 1 0, 1 1, 0 0))";
+        assertNull(evaluate("'" + wkt + "'^^geo:wktLiteral"));
+        assertThrows(ExprEvalException.class, () -> 
exec(NodeValue.makeNode(wkt, WKTDatatype.INSTANCE)));
+    }
+
+    @Test
+    public void gmlPolygonIsAccepted() {
+        String gml = """
+            '<gml:Polygon xmlns:gml="http://www.opengis.net/gml/3.2";
+                srsName="http://www.opengis.net/def/crs/EPSG/0/27700";>
+              <gml:exterior><gml:LinearRing><gml:posList>0 0 3 0 3 4 0 4 0 
0</gml:posList></gml:LinearRing></gml:exterior>
+            </gml:Polygon>'^^geo:gmlLiteral
+            """.replace("\n", " ");
+        assertEquals(NodeValue.makeDouble(12).asNode(), evaluate(gml));
+    }
+
+    @Test
+    public void malformedOrUnboundGeometryLeavesBindUnbound() {
+        for (String value : new String[] { "42", "'POINT (1 2)'", 
"<urn:geometry>", "'invalid'^^geo:wktLiteral", "?missing" }) {
+            assertNull(value, evaluate(value));
+        }
+    }
+
+    @Test
+    public void invalidGeometryArgumentsRaiseExpressionErrors() {
+        for (NodeValue value : new NodeValue[] { NodeValue.makeInteger(42), 
NodeValue.makeString("POINT (1 2)"),
+                NodeValue.makeNode(NodeFactory.createURI("urn:geometry")), 
NodeValue.makeNode("invalid", WKTDatatype.INSTANCE) }) {
+            assertThrows(ExprEvalException.class, () -> exec(value));
+        }
+    }
+
+    @Test
+    public void wrongArityIsRejectedAtQueryBuild() {
+        String geometry = "'POINT EMPTY'^^geo:wktLiteral";
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:metricArea()"));
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:metricArea(" + geometry + ", 1)"));
+        assertThrows(QueryBuildException.class, () -> 
AreaFunctionTestSupport.evaluate("geof:metricArea(" + geometry + ", 1, 2)"));
+    }
+
+    private NodeValue exec(NodeValue value) {
+        return function.exec(value);
+    }
+
+    private Node evaluate(String geometry) {
+        return AreaFunctionTestSupport.evaluate("geof:metricArea(" + geometry 
+ ")");
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryAreaTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryAreaTest.java
new file mode 100644
index 0000000000..c39434160d
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryAreaTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.registry.UnitsURIException;
+import org.apache.jena.geosparql.implementation.vocabulary.Unit_URI;
+import org.apache.sis.referencing.CRS;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+public class GeometryAreaTest {
+    private static final String EPSG_32634 = 
"http://www.opengis.net/def/crs/EPSG/0/32634";;
+    private static final String EPSG_2227 = 
"http://www.opengis.net/def/crs/EPSG/0/2227";;
+    private static final String UNKNOWN_UNIT = 
"http://example.com/unit/unknown";;
+
+    @BeforeClass
+    public static void initializeJena() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void polygonAreaUsesTheExplicitTargetAreaUnit() {
+        GeometryWrapper polygon = geometry("<" + EPSG_32634 + "> POLYGON(("
+                + "500000 4600000,501000 4600000,501000 4601000,"
+                + "500000 4601000,500000 4600000))");
+
+        assertEquals(1.0, polygon.area(Unit_URI.SQUARE_KILOMETRE_QUDT), 0.0);
+    }
+
+    @Test
+    public void areaAccumulatesMultiPolygonShellsAndHoles() {
+        GeometryWrapper multiPolygon = geometry("<" + EPSG_32634 + "> 
MULTIPOLYGON("
+                + "((500000 4600000,500010 4600000,500010 4600010,"
+                + "500000 4600010,500000 4600000),"
+                + "(500004 4600004,500006 4600004,500006 4600006,"
+                + "500004 4600006,500004 4600004)),"
+                + "((500020 4600000,500023 4600000,500023 4600004,"
+                + "500020 4600004,500020 4600000)))");
+
+        assertEquals(108.0, multiPolygon.area(), 0.0);
+    }
+
+    @Test
+    public void emptyGeometriesReturnZeroRegardlessOfCrs() {
+        for (String type : List.of("POINT", "LINESTRING", "POLYGON", 
"MULTIPOINT",
+                "MULTILINESTRING", "MULTIPOLYGON", "GEOMETRYCOLLECTION")) {
+            assertEquals(0.0, geometry(type + " EMPTY").area(), 0.0);
+            assertEquals(0.0, geometry("<" + EPSG_32634 + "> " + type + " 
EMPTY").area(), 0.0);
+        }
+    }
+
+    @Test
+    public void zAndMDoNotContributeToArea() {
+        for (String wkt : List.of(
+                "POLYGON Z ((0 0 1, 3 0 2, 3 4 3, 0 4 4, 0 0 1))",
+                "POLYGON M ((0 0 1, 3 0 2, 3 4 3, 0 4 4, 0 0 1))",
+                "POLYGON ZM ((0 0 1 9, 3 0 2 8, 3 4 3 7, 0 4 4 6, 0 0 1 9))")) 
{
+            assertEquals(12.0, geometry("<" + EPSG_32634 + "> " + wkt).area(), 
0.0);
+        }
+    }
+
+    @Test
+    public void 
areaIneligibleGeometryTypesReturnZeroWithoutSourceCrsCalculation() {
+        for (String wkt : List.of(
+                "POINT(1 1)",
+                "LINESTRING(0 0,1 1)",
+                "MULTIPOINT((0 0),(1 1))",
+                "MULTILINESTRING((0 0,1 1),(2 2,3 3))",
+                "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 1,1 0,0 0)))")) {
+            assertEquals(0.0, geometry(wkt).area(), 0.0);
+        }
+    }
+
+    @Test
+    public void eligibleAreaRejectsDifferentHorizontalAxisScales() throws 
Exception {
+        GeometryWrapper polygon = geometryWithCrs(
+                "<" + EPSG_32634 + "> POLYGON((0 0,0 10,10 10,10 0,0 0))",
+                CRS.fromWKT("ENGCRS[\"Mixed axis scale\","
+                        + "EDATUM[\"Engineering datum\"],CS[Cartesian,2],"
+                        + "AXIS[\"x\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],"
+                        + 
"AXIS[\"y\",north,ORDER[2],LENGTHUNIT[\"foot\",0.3048]]]"));
+
+        assertThrows(UnitsConversionException.class,
+                polygon::area);
+    }
+
+    @Test
+    public void projectedNonMetreAreaConvertsTheSourceAreaQuantity() {
+        GeometryWrapper polygon = geometry("<" + EPSG_2227 + "> POLYGON(("
+                + "6300000 2000000,6300003 2000000,6300003 2000004,"
+                + "6300000 2000004,6300000 2000000))");
+
+        assertEquals(1.114840939359298,
+                polygon.area(), 1e-12);
+    }
+
+    @Test
+    public void multiPolygonAreaIsAccumulatedBeforeUnitConversion() {
+        GeometryWrapper multiPolygon = geometry("<" + EPSG_32634 + "> 
MULTIPOLYGON("
+                + "((0 0,0.02 0,0.02 0.02,0 0.02,0 0)),"
+                + "((1 0,1.02 0,1.02 0.02,1 0.02,1 0)))");
+
+        assertEquals(0.0000000008,
+                multiPolygon.area(Unit_URI.SQUARE_KILOMETRE_QUDT), 1e-20);
+    }
+
+    @Test
+    public void compoundCrsUsesItsTwoDimensionalHorizontalAxisUnits() throws 
Exception {
+        CoordinateReferenceSystem compoundCrs = CRS.compound(
+                CRS.forCode(EPSG_32634), CRS.forCode("EPSG:5703"));
+        GeometryWrapper polygon = geometryWithCrs(
+                "<" + EPSG_32634 + "> POLYGON Z ((0 0 5,0 10 5,10 10 5,10 0 
5,0 0 5))",
+                compoundCrs);
+
+        assertEquals(100.0, polygon.area(), 0.0);
+    }
+
+    @Test
+    public void eligibleGeographicAreaIsRejected() {
+        for (String wkt : List.of(
+                "POLYGON((0 0,0 1,1 1,1 0,0 0))",
+                "MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)))")) {
+            assertThrows(UnitsConversionException.class, geometry(wkt)::area);
+        }
+    }
+
+    @Test
+    public void emptyEligibleAreaSkipsSourceAxisValidation() throws Exception {
+        CoordinateReferenceSystem mixedScaleCrs = CRS.fromWKT("ENGCRS[\"Mixed 
axis scale\","
+                + "EDATUM[\"Engineering datum\"],CS[Cartesian,2],"
+                + "AXIS[\"x\",east,ORDER[1],LENGTHUNIT[\"metre\",1]],"
+                + "AXIS[\"y\",north,ORDER[2],LENGTHUNIT[\"foot\",0.3048]]]");
+
+        assertEquals(0.0, geometryWithCrs(
+                "<" + EPSG_32634 + "> POLYGON EMPTY", mixedScaleCrs).area(), 
0.0);
+        assertEquals(0.0, geometryWithCrs(
+                "<" + EPSG_32634 + "> MULTIPOLYGON EMPTY", 
mixedScaleCrs).area(), 0.0);
+    }
+
+    @Test
+    public void zeroAreaStillValidatesTargetUnits() {
+        for (GeometryWrapper geometry : List.of(
+                geometry("POINT(1 1)"),
+                geometry("<" + EPSG_32634 + "> POLYGON EMPTY"))) {
+            assertThrows(UnitsConversionException.class,
+                    () -> geometry.area(Unit_URI.DEGREE_URL));
+            assertThrows(UnitsURIException.class,
+                    () -> geometry.area(UNKNOWN_UNIT));
+        }
+    }
+
+    private GeometryWrapper geometry(String wkt) {
+        return GeometryWrapper.extract(wkt, WKTDatatype.URI);
+    }
+
+    private GeometryWrapper geometryWithCrs(String wkt, 
CoordinateReferenceSystem crs) {
+        return new GeometryWrapperWithSrsInfo(geometry(wkt), new 
TestSrsInfo(crs));
+    }
+
+    private static final class GeometryWrapperWithSrsInfo extends 
GeometryWrapper {
+        private final SRSInfo srsInfo;
+
+        private GeometryWrapperWithSrsInfo(GeometryWrapper geometry, SRSInfo 
srsInfo) {
+            super(geometry);
+            this.srsInfo = srsInfo;
+        }
+
+        @Override
+        public SRSInfo getSrsInfo() {
+            return srsInfo;
+        }
+    }
+
+    private static final class TestSrsInfo extends SRSInfo {
+        private final CoordinateReferenceSystem crs;
+
+        private TestSrsInfo(CoordinateReferenceSystem crs) {
+            super(EPSG_32634);
+            this.crs = crs;
+        }
+
+        @Override
+        public CoordinateReferenceSystem getCrs() {
+            return crs;
+        }
+    }
+}

Reply via email to