Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.sparql.expr.ExprEvalException;
import org.apache.jena.sparql.expr.NodeValue;
import org.apache.jena.sparql.function.FunctionBase1;

/** Implements geof:centroid. */
public class CentroidFF extends FunctionBase1 {

@Override
public NodeValue exec(NodeValue v) {

try {
GeometryWrapper geometry = GeometryWrapper.extract(v);
GeometryWrapper centroid = geometry.centroid();
return centroid.asNodeValue();
} catch (DatatypeFormatException ex) {
throw new ExprEvalException(ex.getMessage(), ex);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
import org.apache.jena.sparql.expr.NodeValue;
import org.apache.sis.geometry.DirectPosition2D;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateXY;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
Expand Down Expand Up @@ -685,6 +686,31 @@ public GeometryWrapper convexHull() {
return new GeometryWrapper(parsingGeo, xyGeo, srsInfo.getSrsURI(), geometryDatatypeURI, dimensionInfo);
}

/**
* Returns the planar, two-dimensional centroid in the source SRS and datatype.
* Geographic coordinates are treated as planar coordinates, without projection.
*
* @throws DatatypeFormatException if a non-empty XY centroid cannot be
* represented as GML in the source CRS
*/
public GeometryWrapper centroid() {
Point centroid = xyGeometry.getCentroid();
GeometryFactory factory = CustomGeometryFactory.theInstance();
Point xyCentroid = centroid.isEmpty()
? factory.createPoint(new CustomCoordinateSequence(CoordinateSequenceDimensions.XY))
: factory.createPoint(new CoordinateXY(centroid.getX(), centroid.getY()));
// GML derives its coordinate dimension from the CRS. It cannot encode
// this XY result in a three-dimensional CRS without inventing Z.
if (!xyCentroid.isEmpty()
&& GMLDatatype.URI.equals(geometryDatatypeURI)
&& srsInfo.getCrs().getCoordinateSystem().getDimension() != 2) {
throw new DatatypeFormatException(
"A two-dimensional centroid cannot be represented as GML in the source CRS.");
}
Geometry parsingCentroid = GeometryReverse.check(xyCentroid, srsInfo);
return new GeometryWrapper(parsingCentroid, xyCentroid, getSrsURI(), geometryDatatypeURI, DimensionInfo.XY_POINT);
}

/**
*
* @param targetGeometry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
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;
import org.apache.jena.geosparql.geof.nontopological.filter_functions.CentroidFF;
import org.apache.jena.geosparql.geof.nontopological.filter_functions.ConvexHullFF;
import org.apache.jena.geosparql.geof.nontopological.filter_functions.DifferenceFF;
import org.apache.jena.geosparql.geof.nontopological.filter_functions.DistanceFF;
Expand Down Expand Up @@ -52,6 +53,7 @@ public static void loadFilterFunctions(FunctionRegistry registry) {
// Non Topological Filter Functions
registry.put(Geof.BOUNDARY_NAME, BoundaryFF.class);
registry.put(Geof.BUFFER_NAME, BufferFF.class);
registry.put(Geof.CENTROID_NAME, CentroidFF.class);
registry.put(Geof.CONVEXHULL_NAME, ConvexHullFF.class);
registry.put(Geof.DIFFERENCE_NAME, DifferenceFF.class);
registry.put(Geof.DISTANCE_NAME, DistanceFF.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public interface Geof {
public static final String BUFFER_NAME = GEOF_URI + "buffer";
public static final String DIFFERENCE_NAME = GEOF_URI + "difference";
public static final String BOUNDARY_NAME = GEOF_URI + "boundary";
public static final String CENTROID_NAME = GEOF_URI + "centroid";
public static final String CONVEXHULL_NAME = GEOF_URI + "convexHull";
public static final String GETSRID_NAME = GEOF_URI + "getSRID";

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
/*
* 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.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;

import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
import org.apache.jena.geosparql.implementation.CentroidTestData;
import org.apache.jena.geosparql.implementation.GeometryWrapper;
import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
import org.apache.jena.geosparql.implementation.jts.CoordinateSequenceDimensions;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.NodeFactory;
import org.apache.jena.query.QueryBuildException;
import org.apache.jena.query.QueryExecution;
import org.apache.jena.query.QuerySolution;
import org.apache.jena.query.ResultSet;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.sparql.expr.ExprEvalException;
import org.apache.jena.sparql.expr.NodeValue;
import org.junit.BeforeClass;
import org.junit.Test;

public class CentroidFFTest {
@BeforeClass
public static void setup() {
GeoSPARQLConfig.setupNoIndex();
}

@Test
public void pointAndMultiPointUseCoordinateMean() {
assertCentroid("POINT (2 4)", 2, 4);
assertCentroid("MULTIPOINT ((0 0), (6 0), (0 6))", 2, 2);
}

@Test
public void lineCentroidIsLengthWeighted() {
assertCentroid("LINESTRING (0 0, 8 0, 8 2)", 4.8, 0.2);
assertCentroid("MULTILINESTRING ((0 0, 8 0), (8 0, 8 2))", 4.8, 0.2);
}

@Test
public void polygonCentroidUsesArea() {
assertCentroid("POLYGON ((0 0, 6 0, 0 6, 0 0))", 2, 2);
}

@Test
public void polygonHolesSubtractArea() {
// Outer area 16 centred at (2,2), hole area 1 centred at (1.5,1.5).
assertCentroid("POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))",
30.5 / 15, 30.5 / 15);
}

@Test
public void multiPolygonCentroidIsAreaWeighted() {
assertCentroid("MULTIPOLYGON (((0 0, 2 0, 2 2, 0 2, 0 0)), ((4 0, 8 0, 8 2, 4 2, 4 0)))",
13.0 / 3, 1);
}

@Test
public void mixedCollectionUsesHighestDimensionalMembers() {
assertCentroid("GEOMETRYCOLLECTION (POINT (90 90), LINESTRING (0 0, 8 0))", 4, 0);
assertCentroid("GEOMETRYCOLLECTION (POINT (90 90), POLYGON ((0 0, 6 0, 0 6, 0 0)))", 2, 2);
}

@Test
public void resultDropsZAndMAndHasPointMetadata() {
for (String wkt : new String[] { "POINT Z (2 4 99)", "POINT M (2 4 99)", "POINT ZM (2 4 99 100)",
"LINESTRING ZM (0 4 9 8, 4 4 7 6)" }) {
assertCentroid(wkt, 2, 4);
}
}

@Test
public void authorityAxisOrderIsRetainedInTheResult() {
String crs = "<http://www.opengis.net/def/crs/EPSG/0/4326> ";
GeometryWrapper result = centroid(crs + "LINESTRING (10 100, 20 120)");
assertEquals("http://www.opengis.net/def/crs/EPSG/0/4326", result.getSrsURI());
assertEquals(15, result.getParsingGeometry().getCoordinate().getX(), 0);
assertEquals(110, result.getParsingGeometry().getCoordinate().getY(), 0);
assertEquals(110, result.getXYGeometry().getCoordinate().getX(), 0);
assertEquals(15, result.getXYGeometry().getCoordinate().getY(), 0);
}

@Test
public void projectedCrsIsPreserved() {
String crs = "<http://www.opengis.net/def/crs/EPSG/0/27700> ";
GeometryWrapper result = centroid(crs + "LINESTRING (100 200, 300 400)");
assertEquals("http://www.opengis.net/def/crs/EPSG/0/27700", result.getSrsURI());
assertEquals(200, result.getXYGeometry().getCoordinate().getX(), 0);
assertEquals(300, result.getXYGeometry().getCoordinate().getY(), 0);
}

@Test
public void geographicCentroidIsPlanar() {
assertCentroid("MULTIPOINT ((170 10), (-170 10))", 0, 10);
}

@Test
public void emptyInputsReturnAnEmptyXyPoint() {
for (String wkt : new String[] { "POINT EMPTY", "LINESTRING EMPTY", "POLYGON EMPTY",
"MULTIPOINT EMPTY", "MULTILINESTRING EMPTY", "MULTIPOLYGON EMPTY", "GEOMETRYCOLLECTION EMPTY",
"POINT Z EMPTY", "POINT M EMPTY", "POINT ZM EMPTY" }) {
GeometryWrapper result = centroid(wkt);
assertPointMetadata(result);
assertTrue(wkt, result.isEmpty());
assertEquals("POINT EMPTY", result.asNodeValue().asNode().getLiteralLexicalForm());
}
}

@Test
public void gmlResultRetainsDatatypeAndAxisOrder() {
String gml = """
'<gml:LineString xmlns:gml="http://www.opengis.net/gml/3.2"
srsName="http://www.opengis.net/def/crs/EPSG/0/4326">
<gml:posList>10 100 20 120</gml:posList>
</gml:LineString>'^^geo:gmlLiteral
""".replace("\n", " ");
Node node = evaluate("geof:centroid(" + gml + ")");
assertNotNull(node);
assertEquals("http://www.opengis.net/ont/geosparql#gmlLiteral", node.getLiteralDatatypeURI());
GeometryWrapper result = GeometryWrapper.extract(node);
assertPointMetadata(result);
assertEquals("http://www.opengis.net/def/crs/EPSG/0/4326", result.getSrsURI());
assertEquals(15, result.getParsingGeometry().getCoordinate().getX(), 0);
assertEquals(110, result.getParsingGeometry().getCoordinate().getY(), 0);
}

@Test
public void threeDimensionalWktCrsRetainsCrsWithAnXyResult() {
GeometryWrapper result = centroid("<http://www.opengis.net/def/crs/EPSG/0/4979> POINT Z (10 100 7)");
assertPointMetadata(result);
assertEquals("http://www.opengis.net/def/crs/EPSG/0/4979", result.getSrsURI());
assertEquals(10, result.getParsingGeometry().getCoordinate().getX(), 0);
assertEquals(100, result.getParsingGeometry().getCoordinate().getY(), 0);
}

@Test
public void gmlRejectsAnXyResultInAThreeDimensionalCrs() {
NodeValue value = NodeValue.makeNode(CentroidTestData.GML_POINT_Z_EPSG_4979, GMLDatatype.INSTANCE);
assertThrows(ExprEvalException.class, () -> new CentroidFF().exec(value));
assertNull(evaluate("geof:centroid('" + CentroidTestData.GML_POINT_Z_EPSG_4979 + "'^^geo:gmlLiteral)"));
}

@Test
public void centroidComposesWithOtherGeometryFunctions() {
assertEquals(NodeValue.makeInteger(0).asNode(), evaluate(
"geof:dimension(geof:centroid('POLYGON ((0 0, 6 0, 0 6, 0 0))'^^geo:wktLiteral))"));
}

@Test
public void invalidArgumentsRaiseExpressionErrors() {
CentroidFF function = new CentroidFF();
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));
}
for (String value : new String[] { "42", "'POINT (1 2)'", "<urn:geometry>", "'invalid'^^geo:wktLiteral", "?missing" }) {
assertNull(evaluate("geof:centroid(" + value + ")"));
}
}

@Test
public void wrongArityIsRejectedAtQueryBuild() {
assertThrows(QueryBuildException.class, () -> evaluate("geof:centroid()"));
assertThrows(QueryBuildException.class,
() -> evaluate("geof:centroid('POINT EMPTY'^^geo:wktLiteral, 1)"));
}

private static void assertCentroid(String wkt, double x, double y) {
GeometryWrapper result = centroid(wkt);
assertPointMetadata(result);
assertFalse(wkt, result.isEmpty());
assertEquals(wkt, x, result.getXYGeometry().getCoordinate().getX(), 1e-6);
assertEquals(wkt, y, result.getXYGeometry().getCoordinate().getY(), 1e-6);
}

private static void assertPointMetadata(GeometryWrapper result) {
assertEquals("Point", result.getGeometryType());
assertEquals(0, result.getTopologicalDimension());
assertEquals(CoordinateSequenceDimensions.XY, result.getCoordinateSequenceDimensions());
}

private static GeometryWrapper centroid(String wkt) {
Node node = evaluate("geof:centroid('" + wkt + "'^^geo:wktLiteral)");
assertNotNull(wkt, node);
assertEquals(WKTDatatype.URI, node.getLiteralDatatypeURI());
return GeometryWrapper.extract(node);
}

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(expression, results.hasNext());
QuerySolution solution = results.next();
assertFalse(expression, results.hasNext());
return solution.contains("result") ? solution.get("result").asNode() : null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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;

public final class CentroidTestData {
public static final String EMPTY_GML_POLYGON_EPSG_4979 =
"<gml:Polygon xmlns:gml=\"http://www.opengis.net/gml/3.2\" srsName=\"http://www.opengis.net/def/crs/EPSG/0/4979\"/>";
public static final String GML_POINT_Z_EPSG_4979 =
"<gml:Point xmlns:gml=\"http://www.opengis.net/gml/3.2\" srsName=\"http://www.opengis.net/def/crs/EPSG/0/4979\"><gml:pos>10 100 7</gml:pos></gml:Point>";

private CentroidTestData() {
}
}
Loading
Loading