From 59897bf6a3cdcec5df7588a2d851a0a7407123ec Mon Sep 17 00:00:00 2001 From: Claus Stadler Date: Tue, 30 Sep 2025 18:12:50 +0200 Subject: [PATCH] GH-3473: GeoSparql: Remove need for 'a geo:Geometry' in GenericPropertyFunction; Spatial Indexer UI: Replace without selection clears index. --- .../apache/jena/rdfs/DatasetGraphRDFS.java | 4 + .../jena/sparql/core/DatasetGraphQuads.java | 2 +- .../sparql/core/mem/DatasetGraphInMemory.java | 4 +- .../org/apache/jena/sparql/util/Context.java | 40 ++- .../jena/rdfs/TestDatasetGraphRDFS.java | 7 + .../query/BenchmarkSpatialQueries.java | 242 ++++++++++++++++++ .../geosparql/query/SpatialQueryTask.java | 27 ++ .../geosparql/query/SpatialQueryTask550.java | 95 +++++++ .../query/SpatialQueryTaskCurrent.java | 84 ++++++ .../mod/geosparql/SpatialIndexerService.java | 22 +- .../main/resources/spatial-indexer/index.html | 7 +- .../GenericGeometryPropertyFunction.java | 60 ++--- .../topological/GenericPropertyFunction.java | 152 +++++------ .../SpatialObjectGeometryLiteral.java | 62 ++--- .../access/AccessGeoSPARQL.java | 232 +++++++++++++++++ .../implementation/access/AccessWGS84.java | 155 +++++++++++ .../index/QueryRewriteIndex.java | 31 +-- .../spatial/SpatialIndexFindUtils.java | 112 ++------ .../spatial/index/v2/STRtreeUtils.java | 2 +- .../spatial/index/v2/SpatialIndexLib.java | 4 +- .../GenericSpatialPropertyFunction.java | 59 ++--- .../simple_features/SfPFMiscSparqlTest.java | 99 +++++++ 22 files changed, 1172 insertions(+), 330 deletions(-) create mode 100644 jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/BenchmarkSpatialQueries.java create mode 100644 jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask.java create mode 100644 jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask550.java create mode 100644 jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTaskCurrent.java create mode 100644 jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessGeoSPARQL.java create mode 100644 jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessWGS84.java create mode 100644 jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/property_functions/simple_features/SfPFMiscSparqlTest.java diff --git a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java index f5e96bf7117..1ca880b9c88 100644 --- a/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java +++ b/jena-arq/src/main/java/org/apache/jena/rdfs/DatasetGraphRDFS.java @@ -67,6 +67,10 @@ public Graph getGraph(Node graphNode) { return new GraphRDFS(base, setup); } + @Override + public Iterator find() + { return find(Node.ANY, Node.ANY, Node.ANY, Node.ANY); } + // Quad-centric access @Override public Iterator find(Quad quad) { diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/DatasetGraphQuads.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/DatasetGraphQuads.java index 76da75d6bc7..de7ee171004 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/core/DatasetGraphQuads.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/DatasetGraphQuads.java @@ -44,7 +44,7 @@ public void removeGraph(Node graphName) { @Override public void addGraph(Node graphName, Graph graph) { - graph.find().forEachRemaining(t -> add(Quad.create(graphName, t))); + graph.find().forEach(t -> add(Quad.create(graphName, t))); } // @Override diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/DatasetGraphInMemory.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/DatasetGraphInMemory.java index 07738018a18..7d0dc6935b0 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/DatasetGraphInMemory.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/mem/DatasetGraphInMemory.java @@ -346,10 +346,10 @@ public Graph getUnionGraph() { } private Consumer addGraph(final Node name) { - return g -> g.find().mapWith(t -> new Quad(name, t)).forEachRemaining(this::add); + return g -> g.find().mapWith(t -> new Quad(name, t)).forEach(this::add); } - private final Consumer removeGraph = g -> g.find().forEachRemaining(g::delete); + private final Consumer removeGraph = g -> g.find().forEach(g::delete); @Override public void addGraph(final Node graphName, final Graph graph) { diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/util/Context.java b/jena-arq/src/main/java/org/apache/jena/sparql/util/Context.java index fd2a5f261e3..370d93bf494 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/util/Context.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/util/Context.java @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Function; import org.apache.jena.atlas.lib.Lib; import org.apache.jena.atlas.logging.Log; @@ -389,12 +390,6 @@ public void clear() { context.clear(); } - /** Atomic compute. */ - @SuppressWarnings("unchecked") - public V compute(Symbol key, BiFunction remappingFunction) { - return (V)context.compute(key, remappingFunction); - } - @Override public String toString() { String x = ""; @@ -439,13 +434,36 @@ public static AtomicBoolean getCancelSignal(Context context) { } } + /** Atomic compute. */ + @SuppressWarnings("unchecked") + public V compute(Symbol key, BiFunction remappingFunction) { + Object obj = context.compute(key, remappingFunction); + return (V)obj; + } + + /** Atomic computeIfAbsent. */ + @SuppressWarnings("unchecked") + public V computeIfAbsent(Symbol key, Function mappingFunction) { + Object obj = context.computeIfAbsent(key, mappingFunction); + return (V)obj; + } + + /** Atomic computeIfPresent. */ + @SuppressWarnings("unchecked") + public V computeIfPresent(Symbol key, BiFunction remappingFunction) { + Object obj = context.computeIfPresent(key, remappingFunction); + return (V)obj; + } + + /** Get the context's cancel signal. Create and set one if needed. Context must not be null. */ public static AtomicBoolean getOrSetCancelSignal(Context context) { - AtomicBoolean cancelSignal = getCancelSignal(context); - if (cancelSignal == null) { - cancelSignal = new AtomicBoolean(false); - context.set(ARQConstants.symCancelQuery, cancelSignal); + try { + AtomicBoolean result = context.computeIfAbsent(ARQConstants.symCancelQuery, sym -> new AtomicBoolean(false)); + return result; + } catch (ClassCastException ex) { + Log.error(Context.class, "Class cast exception: Expected AtomicBoolean for cancel control: "+ex.getMessage()); + return null; } - return cancelSignal; } /** Merge an outer (defaults to the system global context) diff --git a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java index 574bce81a6e..095a366c3e3 100644 --- a/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java +++ b/jena-arq/src/test/java/org/apache/jena/rdfs/TestDatasetGraphRDFS.java @@ -24,6 +24,7 @@ import static org.apache.jena.rdfs.engine.ConstRDFS.rdfType; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.PrintStream; @@ -77,6 +78,12 @@ public static void beforeClass() { Iter.consume(iter3); } + @Test public void dsg_find_all() { + List baseQuads = Iter.toList(dsg.getBase().find()); + List inferredQuads = Iter.toList(dsg.find()); + assertNotEquals(baseQuads, inferredQuads); + } + @Test public void dsg_find_graph() { List x = test(node("g"), node("a"), rdfType, null); assertTrue(hasNG(x, node("g"))) ; diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/BenchmarkSpatialQueries.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/BenchmarkSpatialQueries.java new file mode 100644 index 00000000000..476fa8b6136 --- /dev/null +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/BenchmarkSpatialQueries.java @@ -0,0 +1,242 @@ +/* + * 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 + * + * http://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. + */ +package org.apache.jena.geosparql.query; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.geosparql.implementation.jts.CustomGeometryFactory; +import org.apache.jena.geosparql.implementation.vocabulary.Geo; +import org.apache.jena.geosparql.spatial.index.v2.GeometryGenerator; +import org.apache.jena.geosparql.spatial.index.v2.GeometryGenerator.GeometryType; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.graph.Triple; +import org.apache.jena.riot.RDFDataMgr; +import org.apache.jena.riot.RDFFormat; +import org.apache.jena.sparql.graph.GraphFactory; +import org.apache.jena.system.G; +import org.apache.jena.vocabulary.RDF; +import org.locationtech.jts.geom.Envelope; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.util.AffineTransformation; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; +import org.openjdk.jmh.results.format.ResultFormatType; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; +import org.openjdk.jmh.runner.options.TimeValue; + +/** + * Benchmarking of spatial queries against test data. + */ +@State(Scope.Benchmark) +public class BenchmarkSpatialQueries { + + private static Map idToQuery = new LinkedHashMap<>(); + + private Node featureNode = NodeFactory.createURI("urn:test:geosparql:feature1"); + private Node geometryNode = NodeFactory.createURI("urn:test:geosparql:geometry1"); + + private static final String q1 = """ + PREFIX geo: + PREFIX ogcsf: + + SELECT * + WHERE { + ?s geo:sfWithin . + } + """; + + private static final String q2 = """ + PREFIX geo: + PREFIX ogcsf: + + SELECT * + WHERE { + ?s a ogcsf:Point . + ?s geo:sfWithin . + } + """; + + static { + idToQuery.put("q1", q1); + idToQuery.put("q2", q2); + } + + /** Essentially the size of the data. One geometry mix includes every WKT geometry type once (with different coordinates). */ + @Param({ + "10000", + }) + public long p1_geoMixes; + + @Param({ + "q1", + "q2", + }) + public String p2_queryId; + + @Param({ + "off", + "virtual", + "materialized" + }) + public String p3_inferences; + + @Param({ + "false", + "true" + }) + public boolean p4_index; + + @Param({ + "current", + "5.5.0" + }) + public String p5_jenaVersion; + + private SpatialQueryTask task; + + @Benchmark + public void run() throws Exception { + long count = task.exec(); + if (true) { + System.out.println("Counted: " + count); + } + } + + private static GeometryWrapper toWrapperWkt(Geometry geometry) { + GeometryWrapper result = new GeometryWrapper(geometry, Geo.WKT); + return result; + } + + @Setup(Level.Trial) + public void setupTrial() throws Exception { + Envelope dataBbox = new Envelope(-175, 175, -85, 85); + Map config = GeometryGenerator.createConfig(p1_geoMixes); + Graph graph = GraphFactory.createDefaultGraph(); + GeometryGenerator.generateGraph(graph, dataBbox, config); + + // Build a search-bbox by scaling the data-generation-bbox down. + Geometry dataBboxGeom = CustomGeometryFactory.theInstance().toGeometry(dataBbox); + double x = dataBboxGeom.getCentroid().getX(); + double y = dataBboxGeom.getCentroid().getY(); + Geometry searchBboxGeom = AffineTransformation.scaleInstance(0.25, 0.25, x, y).transform(dataBboxGeom); + + // Add search bbox and feature/resource to the benchmark data. + Node searchBboxNode = toWrapperWkt(searchBboxGeom).asNode(); + graph.add(featureNode, Geo.HAS_GEOMETRY_NODE, geometryNode); + graph.add(geometryNode, Geo.AS_WKT_NODE, searchBboxNode); + + // Post process test data: + // - Add "geom a Point" triples to geometry resources with a Point WKT literal. + // - Add explicit Geometry type to all geometry resources (required by jena-geosparql 5.5.0 and earlier). + Node Point = NodeFactory.createURI("http://www.opengis.net/ont/sf#Point"); + Graph extraGraph = GraphFactory.createDefaultGraph(); + try (Stream stream = graph.stream(null, Geo.AS_WKT_NODE, null)) { + stream.forEach(t -> { + GeometryWrapper gw = GeometryWrapper.extract(t.getObject()); + String geoType = gw.getGeometryType(); + if (geoType.equals("Point")) { + extraGraph.add(t.getSubject(), RDF.Nodes.type, Point); + } + + extraGraph.add(t.getSubject(), RDF.Nodes.type, Geo.GEOMETRY_NODE); + }); + } + G.addInto(graph, extraGraph); + + String data; + RDFFormat fmt = RDFFormat.TURTLE_PRETTY; + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + RDFDataMgr.write(out, graph, fmt); + out.flush(); + data = new String(out.toByteArray(), StandardCharsets.UTF_8); + } + + task = switch (p5_jenaVersion) { + case "current" -> new SpatialQueryTaskCurrent(); + case "5.5.0" -> new SpatialQueryTask550(); + default -> throw new RuntimeException("No task registered for this jena version:" + p5_jenaVersion); + }; + + task.setData(data); + + switch (p3_inferences) { + case "off": task.setInferenceMode(false, false); break; + case "virtual": task.setInferenceMode(true, false); break; + case "materialized": task.setInferenceMode(true, true); break; + default: + throw new IllegalArgumentException("Unsupported inference mode: " + p3_inferences); + } + + task.setIndex(p4_index); + + String queryString = idToQuery.get(p2_queryId); + task.setQuery(queryString); + } + + @TearDown(Level.Trial) + public void tearDownTrial() throws Exception { + } + + public static ChainedOptionsBuilder getDefaults(Class c) { + return new OptionsBuilder() + // Specify which benchmarks to run. + // You can be more specific if you'd like to run only one benchmark per test. + .include(c.getName()) + // Set the following options as needed + .mode(Mode.AverageTime) + .timeUnit(TimeUnit.SECONDS) + .warmupTime(TimeValue.NONE) + .warmupIterations(5) + .measurementIterations(5) + .measurementTime(TimeValue.NONE) + .threads(1) + .forks(1) + .shouldFailOnError(true) + .shouldDoGC(true) + //.jvmArgs("-XX:+UnlockDiagnosticVMOptions", "-XX:+PrintInlining") + .jvmArgs("-Xmx8G") + //.addProfiler(WinPerfAsmProfiler.class) + .resultFormat(ResultFormatType.JSON) + .result(c.getSimpleName() + "_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".json"); + } + + public static void main(String[] args) throws RunnerException { + Options opt = getDefaults(BenchmarkSpatialQueries.class).build(); + new Runner(opt).run(); + } +} diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask.java new file mode 100644 index 00000000000..0fe6e4dde05 --- /dev/null +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask.java @@ -0,0 +1,27 @@ +/* + * 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 + * + * http://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. + */ + +package org.apache.jena.geosparql.query; + +public interface SpatialQueryTask { + void setData(String trigString) throws Exception; + void setInferenceMode(boolean enableInferences, boolean materialize) throws Exception; + void setQuery(String queryString) throws Exception; + void setIndex(boolean isEnabled); + long exec(); +} diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask550.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask550.java new file mode 100644 index 00000000000..77bb1a6c6a5 --- /dev/null +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTask550.java @@ -0,0 +1,95 @@ +/* + * 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 + * + * http://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. + */ + +package org.apache.jena.geosparql.query; + +import java.util.stream.Stream; + +import org.apache.shadedJena550.geosparql.configuration.GeoSPARQLOperations; +import org.apache.shadedJena550.geosparql.spatial.SpatialIndexException; +import org.apache.shadedJena550.geosparql.spatial.index.v2.SpatialIndexLib; +import org.apache.shadedJena550.graph.Graph; +import org.apache.shadedJena550.rdfs.RDFSFactory; +import org.apache.shadedJena550.riot.Lang; +import org.apache.shadedJena550.riot.RDFParser; +import org.apache.shadedJena550.sparql.core.DatasetGraph; +import org.apache.shadedJena550.sparql.core.DatasetGraphFactory; +import org.apache.shadedJena550.sparql.core.Quad; +import org.apache.shadedJena550.sparql.exec.QueryExec; +import org.apache.shadedJena550.sparql.exec.RowSetOps; + +public class SpatialQueryTask550 + implements SpatialQueryTask +{ + private DatasetGraph baseDsg = null; + private DatasetGraph effectiveDsg = null; + private String query; + + @Override + public void setData(String trigString) throws Exception { + baseDsg = RDFParser.create().fromString(trigString).lang(Lang.TRIG).toDatasetGraph(); + } + + @Override + public void setQuery(String queryString) throws Exception { + this.query = queryString; + } + + @Override + public void setInferenceMode(boolean enableInferences, boolean materialize) { + if (enableInferences) { + Graph vocab = GeoSPARQLOperations.loadGeoSPARQLSchema().getGraph(); + DatasetGraph virtualDsg = RDFSFactory.datasetRDFS(baseDsg, vocab); + if (materialize) { + effectiveDsg = DatasetGraphFactory.create(); + + // Bugged in 5.5.0 because find() is not overridden to yield inferences: + // effectiveDsg.addAll(virtualDsg); + + try (Stream stream = virtualDsg.stream(null, null, null, null)) { + stream.forEach(effectiveDsg::add); + } + } else { + effectiveDsg = virtualDsg; + } + } else { + effectiveDsg = baseDsg; + } + + // RDFDataMgr.write(System.err, effectiveDsg, RDFFormat.TRIG_PRETTY); + } + + @Override + public void setIndex(boolean isEnabled) { + if (isEnabled) { + try { + SpatialIndexLib.buildSpatialIndex(effectiveDsg); + } catch (SpatialIndexException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public long exec() { + try (QueryExec qe = QueryExec.dataset(effectiveDsg).query(query).build()) { + long count = RowSetOps.count(qe.select()); + return count; + } + } +} diff --git a/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTaskCurrent.java b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTaskCurrent.java new file mode 100644 index 00000000000..e90f64fca90 --- /dev/null +++ b/jena-benchmarks/jena-benchmarks-jmh/src/test/java/org/apache/jena/geosparql/query/SpatialQueryTaskCurrent.java @@ -0,0 +1,84 @@ +/* + * 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 + * + * http://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. + */ + +package org.apache.jena.geosparql.query; + +import org.apache.jena.geosparql.configuration.GeoSPARQLOperations; +import org.apache.jena.geosparql.spatial.SpatialIndexException; +import org.apache.jena.geosparql.spatial.index.v2.SpatialIndexLib; +import org.apache.jena.graph.Graph; +import org.apache.jena.rdfs.RDFSFactory; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.exec.RowSetOps; + +public class SpatialQueryTaskCurrent + implements SpatialQueryTask +{ + private DatasetGraph baseDsg = null; + private DatasetGraph effectiveDsg = null; + private String query; + + @Override + public void setData(String ttlString) throws Exception { + baseDsg = RDFParser.create().fromString(ttlString).lang(Lang.TRIG).toDatasetGraph(); + } + + @Override + public void setQuery(String queryString) throws Exception { + this.query = queryString; + } + + @Override + public void setInferenceMode(boolean enableInferences, boolean materialize) { + if (enableInferences) { + Graph vocab = GeoSPARQLOperations.loadGeoSPARQLSchema().getGraph(); + DatasetGraph virtualDsg = RDFSFactory.datasetRDFS(baseDsg, vocab); + if (materialize) { + effectiveDsg = DatasetGraphFactory.create(); + effectiveDsg.addAll(virtualDsg); + } else { + effectiveDsg = virtualDsg; + } + } else { + effectiveDsg = baseDsg; + } + } + + @Override + public void setIndex(boolean isEnabled) { + if (isEnabled) { + try { + SpatialIndexLib.buildSpatialIndex(effectiveDsg); + } catch (SpatialIndexException e) { + throw new RuntimeException(e); + } + } + } + + @Override + public long exec() { + try (QueryExec qe = QueryExec.dataset(effectiveDsg).query(query).build()) { + long count = RowSetOps.count(qe.select()); + return count; + } + } +} diff --git a/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/java/org/apache/jena/fuseki/mod/geosparql/SpatialIndexerService.java b/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/java/org/apache/jena/fuseki/mod/geosparql/SpatialIndexerService.java index 87eaa33aa10..9787f1dba72 100644 --- a/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/java/org/apache/jena/fuseki/mod/geosparql/SpatialIndexerService.java +++ b/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/java/org/apache/jena/fuseki/mod/geosparql/SpatialIndexerService.java @@ -120,18 +120,26 @@ private class EndpointClients { public SpatialIndexerService() {} - private static > Set extractGraphsFromRequest(DatasetGraph dsg, HttpAction action) { + /** + * Extract the explicit set of graphs from the action w.r.t. the dataset graph. + * + * @param dsg The dataset graph. + * @param action The HTTP action. + * @param emptySelectionToAllGraphs Select all graphs if the request specifies an empty selection of graphs. + * @return The explicit set of graphs w.r.t. the dataset graph. + */ + private static Set extractGraphsFromRequest(DatasetGraph dsg, HttpAction action, boolean emptySelectionToAllGraphs) { String uris = action.getRequest().getParameter(HttpNames.paramGraph); Collection strs; if (uris == null || uris.isBlank()) { strs = List.of(Quad.defaultGraphIRI.toString(), Quad.unionGraph.toString()); } else { - TypeToken> typeToken = new TypeToken>(){}; + TypeToken> typeToken = new TypeToken<>(){}; strs = gsonForSse.fromJson(uris, typeToken); } List rawGraphNodes = strs.stream().map(NodeFactory::createURI).distinct().toList(); // If the set of specified graphs is empty then index all. - if (rawGraphNodes.isEmpty()) { + if (rawGraphNodes.isEmpty() && emptySelectionToAllGraphs) { rawGraphNodes = List.of(Quad.defaultGraphIRI, Quad.unionGraph); } @@ -453,7 +461,7 @@ protected BasicTask scheduleIndexTask(HttpAction action, SpatialIndexerComputati long graphCount = indexComputation.getGraphNodes().size(); - TaskListener taskListener = new TaskListener() { + TaskListener taskListener = new TaskListener<>() { @Override public void onStateChange(BasicTask task) { switch (task.getTaskState()) { @@ -475,6 +483,7 @@ public void onStateChange(BasicTask task) { if (logger.isInfoEnabled()) { logger.info("Indexing task of {} graphs terminated.", graphCount); } + break; } default: break; @@ -496,6 +505,8 @@ protected void doIndex(HttpAction action) throws Exception { ServletOps.error(HttpSC.SERVICE_UNAVAILABLE_503, msg); } else { boolean isReplaceMode = isReplaceMode(action); + boolean isUpdateMode = !isReplaceMode; + int threadCount = getThreadCount(action); // Only SpatialIndexPerGraph can be updated. @@ -503,7 +514,6 @@ protected void doIndex(HttpAction action) throws Exception { // If not then raise an exception // that informs that only replace mode can be used in this situation. if (!(index instanceof SpatialIndexPerGraph)) { - boolean isUpdateMode = !isReplaceMode; if (isUpdateMode) { throw new RuntimeException("Cannot update existing spatial index because its type is unsupported. Consider replacing the index."); } @@ -516,7 +526,7 @@ protected void doIndex(HttpAction action) throws Exception { String srsURI = index.getSrsInfo().getSrsURI(); - List graphNodes = new ArrayList<>(Txn.calculateRead(dsg, () -> extractGraphsFromRequest(dsg, action))); + List graphNodes = new ArrayList<>(Txn.calculateRead(dsg, () -> extractGraphsFromRequest(dsg, action, isUpdateMode))); SpatialIndexerComputation task = new SpatialIndexerComputation(dsg, srsURI, graphNodes, threadCount); action.log.info(String.format("[%d] spatial index: computation request accepted.", action.id)); diff --git a/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/resources/spatial-indexer/index.html b/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/resources/spatial-indexer/index.html index be6caaacfb2..fdebc8dd554 100644 --- a/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/resources/spatial-indexer/index.html +++ b/jena-fuseki2/jena-fuseki-mod-geosparql/src/main/resources/spatial-indexer/index.html @@ -218,8 +218,9 @@

Rebuild Spatial Index for Selected Graphs

} function updateApplyButtonLabel() { + const replaceMode = replaceCb.checked; const selectedGraphs = document.querySelectorAll('#graph-list input:checked'); - applyBtn.textContent = selectedGraphs.length === 0 ? 'Index all graphs' : `Index ${selectedGraphs.length} graphs`; + applyBtn.textContent = (!replaceMode && selectedGraphs.length === 0) ? 'Index all graphs' : `Index ${selectedGraphs.length} graphs`; } document.getElementById('filter').addEventListener('input', function () { @@ -303,6 +304,10 @@

Rebuild Spatial Index for Selected Graphs

updateApplyButtonLabel(); }); + replaceCb.addEventListener("change", function () { + updateApplyButtonLabel(); + }); + function updateStatus() { updateCancelButton(); updateStatusMessage(); diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericGeometryPropertyFunction.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericGeometryPropertyFunction.java index b76e5c9b32d..91d5b4f019a 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericGeometryPropertyFunction.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericGeometryPropertyFunction.java @@ -19,7 +19,7 @@ import org.apache.jena.datatypes.DatatypeFormatException; import org.apache.jena.geosparql.implementation.GeometryWrapper; -import org.apache.jena.geosparql.implementation.vocabulary.Geo; +import org.apache.jena.geosparql.implementation.access.AccessGeoSPARQL; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.Triple; @@ -28,15 +28,15 @@ import org.apache.jena.sparql.engine.QueryIterator; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.engine.binding.BindingFactory; -import org.apache.jena.sparql.engine.iterator.QueryIterConcat; +import org.apache.jena.sparql.engine.iterator.QueryIter; import org.apache.jena.sparql.engine.iterator.QueryIterNullIterator; +import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper; import org.apache.jena.sparql.engine.iterator.QueryIterSingleton; import org.apache.jena.sparql.expr.ExprEvalException; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.pfunction.PFuncSimple; import org.apache.jena.system.G; import org.apache.jena.util.iterator.ExtendedIterator; -import org.apache.jena.vocabulary.RDF; /** * @@ -62,7 +62,6 @@ public QueryIterator execEvaluated(Binding binding, Node subject, Node predicate //Subject unbound and object bound. return subjectUnbound(binding, subject, predicate, object, execCxt); } - } protected Node getGeometryLiteral(Node subject, Node predicate, Graph graph) throws ExprEvalException { @@ -73,15 +72,7 @@ protected Node getGeometryLiteral(Node subject, Node predicate, Graph graph) thr return G.getSP(graph, subject, predicate); //Check that the Geometry has a serialisation to use. - Node geomLiteral = G.getSP(graph, subject, Geo.HAS_SERIALIZATION_NODE); - - // If hasSerialization not found then check asWKT and asGML. - if (geomLiteral == null) { - geomLiteral = G.getSP(graph, subject, Geo.AS_WKT_NODE); - if (geomLiteral == null) - geomLiteral = G.getSP(graph, subject, Geo.AS_GML_NODE); - } - + Node geomLiteral = AccessGeoSPARQL.getGeoLiteral(graph, subject); if (geomLiteral != null) { GeometryWrapper geometryWrapper = GeometryWrapper.extract(geomLiteral); NodeValue predicateResult = applyPredicate(geometryWrapper); @@ -111,25 +102,23 @@ private QueryIterator bothBound(Binding binding, Node subject, Node predicate, N } private QueryIterator subjectUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { - QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt); - Graph graph = execCxt.getActiveGraph(); - ExtendedIterator subjectTriples = graph.find(null, RDF.type.asNode(), Geo.GEOMETRY_NODE); - + ExtendedIterator subjectTriples = AccessGeoSPARQL.findSpecificGeoLiterals(graph); Var subjectVar = Var.alloc(subject.getName()); - while (subjectTriples.hasNext()) { - Triple subjectTriple = subjectTriples.next(); - Binding newBind = BindingFactory.binding(binding, subjectVar, subjectTriple.getSubject()); - QueryIterator queryIter = bothBound(newBind, subjectTriple.getSubject(), predicate, object, execCxt); - queryIterConcat.add(queryIter); - } + ExtendedIterator iterator = subjectTriples + .mapWith(Triple::getSubject) + .mapWith(node -> BindingFactory.binding(binding, subjectVar, node)); - return queryIterConcat; + QueryIter queryIter = QueryIter.flatMap( + QueryIterPlainWrapper.create(iterator, execCxt), + b -> bothBound(b, b.get(subjectVar), predicate, object, execCxt), + execCxt); + + return queryIter; } private QueryIterator objectUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { - Graph graph = execCxt.getActiveGraph(); Node geometryLiteral = getGeometryLiteral(subject, predicate, graph); @@ -141,21 +130,20 @@ private QueryIterator objectUnbound(Binding binding, Node subject, Node predicat } private QueryIterator bothUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { - QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt); - Graph graph = execCxt.getActiveGraph(); - ExtendedIterator subjectTriples = graph.find(null, RDF.type.asNode(), Geo.GEOMETRY_NODE); - + ExtendedIterator subjectTriples = AccessGeoSPARQL.findSpecificGeoLiterals(graph); Var subjectVar = Var.alloc(subject.getName()); - while (subjectTriples.hasNext()) { - Triple subjectTriple = subjectTriples.next(); - Binding newBind = BindingFactory.binding(binding, subjectVar, subjectTriple.getSubject()); - QueryIterator queryIter = objectUnbound(newBind, subjectTriple.getSubject(), predicate, object, execCxt); - queryIterConcat.add(queryIter); - } + ExtendedIterator iterator = subjectTriples + .mapWith(Triple::getSubject) + .mapWith(node -> BindingFactory.binding(binding, subjectVar, node)); + + QueryIter queryIter = QueryIter.flatMap( + QueryIterPlainWrapper.create(iterator, execCxt), + b -> objectUnbound(b, b.get(subjectVar), predicate, object, execCxt), + execCxt); - return queryIterConcat; + return queryIter; } } diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java index c1185a71add..9b913abfc99 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java @@ -23,11 +23,10 @@ import org.apache.jena.atlas.iterator.Iter; import org.apache.jena.geosparql.geof.topological.GenericFilterFunction; import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.geosparql.implementation.access.AccessGeoSPARQL; +import org.apache.jena.geosparql.implementation.access.AccessWGS84; import org.apache.jena.geosparql.implementation.index.QueryRewriteIndex; -import org.apache.jena.geosparql.implementation.vocabulary.Geo; -import org.apache.jena.geosparql.implementation.vocabulary.SpatialExtension; import org.apache.jena.geosparql.spatial.SpatialIndex; -import org.apache.jena.geosparql.spatial.SpatialIndexException; import org.apache.jena.geosparql.spatial.index.v2.SpatialIndexLib; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; @@ -47,7 +46,6 @@ import org.apache.jena.sparql.util.FmtUtils; import org.apache.jena.system.G; import org.apache.jena.util.iterator.ExtendedIterator; -import org.apache.jena.vocabulary.RDF; import org.locationtech.jts.geom.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.operation.TransformException; @@ -68,38 +66,42 @@ public GenericPropertyFunction(GenericFilterFunction filterFunction) { @Override public QueryIterator execEvaluated(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { - // optionally accept bound literals for simpler usage - -// if (object.isLiteral()) { -// //These Property Functions do not accept literals as objects so exit quickly. -// return QueryIterNullIterator.create(execCxt); -// } - - if (subject.isConcrete() && object.isConcrete()) { - //Both are bound. - return bothBound(binding, subject, predicate, object, execCxt); - } else if (subject.isVariable() && object.isVariable()) { - //Both are unbound. - return bothUnbound(binding, subject, predicate, object, execCxt); + // These Property Functions accept literals as objects as an extension over GeoSPARQL 1.0. + + QueryRewriteIndex queryRewriteIndex = QueryRewriteIndex.getOrCreate(execCxt); + SpatialIndex spatialIndex = SpatialIndexLib.getSpatialIndex(execCxt.getContext()); + + if (subject.isConcrete()) { + if (object.isConcrete()) { + //Both are bound. + return bothBound(binding, subject, predicate, object, execCxt, queryRewriteIndex); + } else { + //One bound and one unbound. + return oneBoundChecked(binding, subject, predicate, object, true, execCxt, spatialIndex, queryRewriteIndex); + } } else { - //One bound and one unbound. - return oneBound(binding, subject, predicate, object, execCxt); + if (object.isConcrete()) { + //One bound and one unbound. + return oneBoundChecked(binding, object, predicate, subject, false, execCxt, spatialIndex, queryRewriteIndex); + } else { + //Both are unbound. + return bothUnbound(binding, subject, predicate, object, execCxt, spatialIndex, queryRewriteIndex); + } } } - private QueryIterator bothBound(Binding binding, boolean isSubjectBound, Node subject, Node predicate, Node object, ExecutionContext execCxt) { + private QueryIterator bothBound(Binding binding, boolean isSubjectBound, Node subject, Node predicate, Node object, ExecutionContext execCxt, QueryRewriteIndex queryRewriteIndex) { QueryIterator iter = isSubjectBound - ? bothBound(binding, subject, predicate, object, execCxt) - : bothBound(binding, object, predicate, subject, execCxt); + ? bothBound(binding, subject, predicate, object, execCxt, queryRewriteIndex) + : bothBound(binding, object, predicate, subject, execCxt, queryRewriteIndex); return iter; } - private QueryIterator bothBound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { + private QueryIterator bothBound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt, QueryRewriteIndex queryRewriteIndex) { Graph graph = execCxt.getActiveGraph(); - QueryRewriteIndex queryRewriteIndex = QueryRewriteIndex.retrieve(execCxt); Boolean isPositiveResult = queryRewrite(graph, subject, predicate, object, queryRewriteIndex); if (isPositiveResult) { - //Filter function test succeded so retain binding. + //Filter function test succeeded so retain binding. return QueryIterSingleton.create(binding, execCxt); } else { //Filter function test failed so null result. @@ -107,103 +109,83 @@ private QueryIterator bothBound(Binding binding, Node subject, Node predicate, N } } - private QueryIterator bothUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { + private QueryIterator bothUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt, SpatialIndex spatialIndex, QueryRewriteIndex queryRewriteIndex) { Var subjectVar = Var.alloc(subject.getName()); - Graph graph = execCxt.getActiveGraph(); //Search for both Features and Geometry in the Graph. Reliant upon consistent usage of SpatialObject (which is base class of Feature and Geometry) if present. - ExtendedIterator spatialTriples = findSpatialTriples(graph); - ExtendedIterator iterator = spatialTriples - .mapWith(Triple::getSubject) + ExtendedIterator iterator = findSpatialObjects(graph) .mapWith(node -> BindingFactory.binding(binding, subjectVar, node)); QueryIter queryIter = QueryIter.flatMap( QueryIterPlainWrapper.create(iterator, execCxt), - b -> oneBound(b, b.get(subjectVar), predicate, object, execCxt), + b -> oneBound(graph, b, b.get(subjectVar), predicate, object, true, execCxt, spatialIndex, queryRewriteIndex), execCxt ); return queryIter; } - private QueryIterator oneBound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) { - + /** Validate the bound node for whether it is a literal or spatial object. */ + private QueryIterator oneBoundChecked(Binding binding, Node boundNode, Node predicate, Node unboundNode, boolean isSubjectBound, ExecutionContext execCxt, SpatialIndex spatialIndex, QueryRewriteIndex queryRewriteIndex) { Graph graph = execCxt.getActiveGraph(); - Node boundNode; - Node unboundNode; - boolean isSubjectBound; - if (subject.isConcrete()) { - //Subject is bound, object is unbound. - boundNode = subject; - unboundNode = object; - isSubjectBound = true; - } else { - //Object is bound, subject is unbound. - boundNode = object; - unboundNode = subject; - isSubjectBound = false; - } - if (!(boundNode.isLiteral() || - graph.contains(boundNode, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE) || - graph.contains(boundNode, RDF.type.asNode(), Geo.FEATURE_NODE) || - graph.contains(boundNode, RDF.type.asNode(), Geo.GEOMETRY_NODE))) { - if (!graph.contains(boundNode, SpatialExtension.GEO_LAT_NODE, null)) { - //Bound node is not a Feature or a Geometry or has Geo predicates so exit. - return QueryIterNullIterator.create(execCxt); - } + // If the bound node can't match in the first place then bail out early. + // Otherwise, the whole unbound side would be scanned and tested against the bound node. + if (!(boundNode.isLiteral() || AccessGeoSPARQL.isSpatialObjectByProperties(graph, boundNode))) { + return QueryIterNullIterator.create(execCxt); } - boolean isSpatialIndex = SpatialIndexLib.isDefined(execCxt); + return oneBound(graph, binding, boundNode, predicate, unboundNode, isSubjectBound, execCxt, spatialIndex, queryRewriteIndex); + } + + private QueryIterator oneBound(Graph graph, Binding binding, Node boundNode, Node predicate, Node unboundNode, boolean isSubjectBound, ExecutionContext execCxt, SpatialIndex spatialIndex, QueryRewriteIndex queryRewriteIndex) { QueryIterator result; - if (!isSpatialIndex || filterFunction.isDisjoint() || filterFunction.isDisconnected()) { + if (spatialIndex == null || filterFunction.isDisjoint() || filterFunction.isDisconnected()) { //Disjointed so retrieve all cases. - result = findAll(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt); + result = findAll(graph, binding, boundNode, predicate, unboundNode, isSubjectBound, execCxt, queryRewriteIndex); } else { //Only retrieve those in the spatial index which are within same bounding box. - result = findIndex(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt); + result = findIndex(graph, binding, boundNode, predicate, unboundNode, isSubjectBound, execCxt, spatialIndex, queryRewriteIndex); } return result; } - private QueryIterator findAll(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) { + private QueryIterator findAll(Graph graph, Binding binding, Node boundNode, Node predicate, Node unboundNode, boolean isSubjectBound, ExecutionContext execCxt, QueryRewriteIndex queryRewriteIndex) { //Prepare the results. Var unboundVar = Var.alloc(unboundNode.getName()); //Search for both Features and Geometry in the Graph. Reliant upon consistent usage of SpatialObject (which is base class of Feature and Geometry) if present. - ExtendedIterator spatialTriples = findSpatialTriples(graph); - - ExtendedIterator iterator = spatialTriples - .mapWith(Triple::getSubject) + ExtendedIterator iterator = findSpatialObjects(graph) .mapWith(node -> BindingFactory.binding(binding, unboundVar, node)); return QueryIter.flatMap( QueryIterPlainWrapper.create(iterator, execCxt), b -> { Node spatialNode = b.get(unboundVar); - QueryIterator iter = bothBound(b, isSubjectBound, boundNode, predicate, spatialNode, execCxt); + QueryIterator iter = bothBound(b, isSubjectBound, boundNode, predicate, spatialNode, execCxt, queryRewriteIndex); return iter; }, execCxt); } - private static ExtendedIterator findSpatialTriples(Graph graph) { - ExtendedIterator spatialTriples; - if (graph.contains(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE)) { - spatialTriples = graph.find(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE); - } else if (graph.contains(null, RDF.type.asNode(), Geo.FEATURE_NODE) || graph.contains(null, RDF.type.asNode(), Geo.GEOMETRY_NODE)) { - ExtendedIterator featureTriples = graph.find(null, RDF.type.asNode(), Geo.FEATURE_NODE); - ExtendedIterator geometryTriples = graph.find(null, RDF.type.asNode(), Geo.GEOMETRY_NODE); - spatialTriples = featureTriples.andThen(geometryTriples); - } else { - //Check for Geo Predicate Features in the Graph if no GeometryLiterals found. - spatialTriples = graph.find(null, SpatialExtension.GEO_LAT_NODE, null); + private static ExtendedIterator findSpatialObjects(Graph graph) { + // The found nodes are passed to SpatialObjectGeometryLiteral.retrieve which: + // - Filters out all features unless they have a geo:hasDefaultGeometry property or wgs84 vocab. + // - Retrieves only a single specific geoLiteral for a geoResource + // There would be performance potential by leveraging the triples here for retrieve. + ExtendedIterator result = AccessGeoSPARQL.findSpecificGeoLiterals(graph); + try { + result = result.andThen(AccessGeoSPARQL.findDefaultGeoResources(graph)); + result = result.andThen(AccessWGS84.findGeoLiteralsAsTriples(graph, null)); + } catch (RuntimeException t) { + result.close(); + throw new RuntimeException(t); } - return spatialTriples; + return result.mapWith(Triple::getSubject); } - private QueryIterator findIndex(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) throws ExprEvalException { + private QueryIterator findIndex(Graph graph, Binding binding, Node boundNode, Node predicate, Node unboundNode, boolean isSubjectBound, ExecutionContext execCxt, SpatialIndex spatialIndex, QueryRewriteIndex queryRewriteIndex) throws ExprEvalException { try { //Prepare for results. Var unboundVar = Var.alloc(unboundNode); @@ -230,7 +212,6 @@ private QueryIterator findIndex(Graph graph, Node boundNode, Node unboundNode, B Node geometryLiteral = boundGeometryLiteral.getGeometryLiteral(); // Perform the search of the Spatial Index of the Dataset. - SpatialIndex spatialIndex = SpatialIndexLib.retrieve(execCxt); GeometryWrapper geom = GeometryWrapper.extract(geometryLiteral); GeometryWrapper transformedGeom = geom.transform(spatialIndex.getSrsInfo()); @@ -247,32 +228,33 @@ private QueryIterator findIndex(Graph graph, Node boundNode, Node unboundNode, B featureBinding -> { return findByFeature(graph, binding, featureBinding, isSubjectBound, boundNode, predicate, unboundVar, - execCxt, assertedNodes); + execCxt, assertedNodes, queryRewriteIndex); }, execCxt); queryIterConcat.add(queryIterator); return queryIterConcat; - } catch (MismatchedDimensionException | TransformException | FactoryException | SpatialIndexException ex) { + } catch (MismatchedDimensionException | TransformException | FactoryException ex) { throw new ExprEvalException(ex.getMessage() + ": " + FmtUtils.stringForNode(boundNode) + ", " + FmtUtils.stringForNode(unboundNode) + ", " + FmtUtils.stringForNode(predicate), ex); } } private QueryIterator findByFeature(Graph graph, Binding binding, Binding featureBinding, boolean isSubjectBound, Node boundNode, Node predicate, Var unboundVar, - ExecutionContext execCxt, Collection assertedNodes) { + ExecutionContext execCxt, Collection assertedNodes, QueryRewriteIndex queryRewriteIndex) { Node featureNode = featureBinding.get(unboundVar); QueryIterConcat featureIterConcat = new QueryIterConcat(execCxt); // Check Features directly if not already asserted if (!assertedNodes.contains(featureNode)) { - QueryIterator tmpIter = bothBound(featureBinding, isSubjectBound, boundNode, predicate, featureNode, execCxt); + QueryIterator tmpIter = bothBound(featureBinding, isSubjectBound, boundNode, predicate, featureNode, execCxt, queryRewriteIndex); featureIterConcat.add(tmpIter); } // Also test all Geometry of the Features. All, some or one Geometry may have matched. - ExtendedIterator featureGeometries = G.iterSP(graph, featureNode, Geo.HAS_GEOMETRY_NODE); + // ExtendedIterator featureGeometries = G.iterSP(graph, featureNode, Geo.HAS_GEOMETRY_NODE); + ExtendedIterator featureGeometries = AccessGeoSPARQL.findSpecificGeoResources(graph, featureNode).mapWith(Triple::getObject); QueryIterator geometriesQueryIterator = QueryIterPlainWrapper.create( Iter.map( Iter.filter( // omit asserted @@ -286,7 +268,7 @@ private QueryIterator findByFeature(Graph graph, Binding binding, Binding featur geometriesQueryIterator, b2 -> { Node geomNode = b2.get(unboundVar); - return bothBound(b2, isSubjectBound, boundNode, predicate, geomNode, execCxt); + return bothBound(b2, isSubjectBound, boundNode, predicate, geomNode, execCxt, queryRewriteIndex); }, execCxt); diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/SpatialObjectGeometryLiteral.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/SpatialObjectGeometryLiteral.java index 42484cec1e1..35373e96be2 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/SpatialObjectGeometryLiteral.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/SpatialObjectGeometryLiteral.java @@ -20,16 +20,14 @@ import java.util.Objects; import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.access.AccessGeoSPARQL; +import org.apache.jena.geosparql.implementation.access.AccessWGS84; import org.apache.jena.geosparql.implementation.datatype.GeometryDatatype; import org.apache.jena.geosparql.implementation.vocabulary.Geo; -import org.apache.jena.geosparql.implementation.vocabulary.SpatialExtension; -import org.apache.jena.geosparql.spatial.ConvertLatLon; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.system.G; -import org.apache.jena.system.RDFDataException; -import org.apache.jena.vocabulary.RDF; /** * @@ -99,13 +97,17 @@ public String toString() { * Objects). * * @param graph - * @param targetSpatialObject + * @param targetSpatialObject The spatial object. * @return SpatialObject/GeometryLiteral pair. */ + // XXX This should return an iterator over all geometry literals rather than just picking an arbitrary one. protected static final SpatialObjectGeometryLiteral retrieve(Graph graph, Node targetSpatialObject) { + if (targetSpatialObject == null) { + return new SpatialObjectGeometryLiteral(null, null); + } - Node geometry = null; - if (targetSpatialObject != null && targetSpatialObject.isLiteral()) { + // Special case: Directly supplied literal - must be a geometry. + if (targetSpatialObject.isLiteral()) { if (targetSpatialObject.getLiteralDatatype() instanceof GeometryDatatype) { return new SpatialObjectGeometryLiteral(NodeFactory.createBlankNode(), targetSpatialObject); } else { @@ -113,42 +115,28 @@ protected static final SpatialObjectGeometryLiteral retrieve(Graph graph, Node t } } - if (graph.contains(targetSpatialObject, RDF.type.asNode(), Geo.FEATURE_NODE)) { - //Target is Feature - find the default Geometry. - geometry = G.getSP(graph, targetSpatialObject, Geo.HAS_DEFAULT_GEOMETRY_NODE); + // If target has a default geometry then it is implicitly a feature. + // Use the feature's default geometry if present ... + // XXX The original code did not consider geo:hasGeometry here - does the spec really only mandate handling of default geometry? + Node geometry = G.getSP(graph, targetSpatialObject, Geo.HAS_DEFAULT_GEOMETRY_NODE); - } else if (graph.contains(targetSpatialObject, RDF.type.asNode(), Geo.GEOMETRY_NODE)) { - //Target is a Geometry. + // ... otherwise try to treat the target itself as the geometry resource. + if (geometry == null) { geometry = targetSpatialObject; } - if (geometry != null) { - //Find the Geometry Literal of the Geometry. - Node literalNode = G.getSP(graph, geometry, Geo.HAS_SERIALIZATION_NODE); - // If hasSerialization not found then check asWKT. - if (literalNode == null) - literalNode = G.getSP(graph, geometry, Geo.AS_WKT_NODE); - // If asWKT not found then check asGML. - if (literalNode == null) - literalNode = G.getSP(graph, geometry, Geo.AS_GML_NODE); - if (literalNode != null) - return new SpatialObjectGeometryLiteral(targetSpatialObject, literalNode); - } else { - //Target is not a Feature or Geometry but could have Geo Predicates. - if ( graph.contains(targetSpatialObject, SpatialExtension.GEO_LAT_NODE, null) - && graph.contains(targetSpatialObject, SpatialExtension.GEO_LON_NODE, null)) { - try { - //Extract Lat,Lon coordinate. - Node lat = G.getOneSP(graph, targetSpatialObject, SpatialExtension.GEO_LAT_NODE); - Node lon = G.getOneSP(graph, targetSpatialObject, SpatialExtension.GEO_LON_NODE); - Node latLonGeometryLiteral = ConvertLatLon.toNode(lat, lon); - return new SpatialObjectGeometryLiteral(targetSpatialObject, latLonGeometryLiteral); - } catch ( RDFDataException ex) { - throw new DatatypeFormatException(targetSpatialObject.getURI() + " has more than one geo:lat or geo:lon property."); - } - } + Node literalNode = AccessGeoSPARQL.getGeoLiteral(graph, geometry); + + // Last resort: Try the legacy WGS84 Geo Positioning vocabulary on the targetSpatialObject. + if (literalNode == null) { + literalNode = AccessWGS84.getGeoLiteral(graph, targetSpatialObject); + } + + if (literalNode != null) { + return new SpatialObjectGeometryLiteral(targetSpatialObject, literalNode); } + // No geometry literal found. return new SpatialObjectGeometryLiteral(null, null); } } diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessGeoSPARQL.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessGeoSPARQL.java new file mode 100644 index 00000000000..fd6bfceaf49 --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessGeoSPARQL.java @@ -0,0 +1,232 @@ +/* + * 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 + * + * http://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. + */ + +package org.apache.jena.geosparql.implementation.access; + +import java.util.Iterator; +import java.util.Objects; + +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.geosparql.implementation.vocabulary.Geo; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Triple; +import org.apache.jena.system.G; +import org.apache.jena.util.iterator.ExtendedIterator; + +/** + * Central place for accessing GeoSparql spatial objects in a {@link Graph}. + * + * Note: Using the "GeoLiterals" methods on RDF data that do not conform to the GeoSparql + * specification will return whatever values are present - + * regardless of whether those values are valid literals. + */ +public class AccessGeoSPARQL { + public static boolean isPredicateOfFeature(Node n) { + return n.equals(Geo.HAS_GEOMETRY_NODE) || n.equals(Geo.HAS_DEFAULT_GEOMETRY_NODE); + } + + public static boolean isPredicateOfGeoResource(Node n) { + return n.equals(Geo.AS_WKT_NODE) || n.equals(Geo.AS_GML_NODE) || n.equals(Geo.HAS_SERIALIZATION_NODE); + } + + public static boolean isTripleOfFeature(Triple t) { + return isPredicateOfFeature(t.getPredicate()); + } + + public static boolean isTripleOfGeoResource(Triple t) { + return isPredicateOfGeoResource(t.getPredicate()); + } + + /** True iff the graph contains geometry literals. */ + public static boolean containsGeoLiterals(Graph graph) { + return containsGeoLiterals(graph, null); + } + + /** True iff the node has geometry literals. Arguments must not be null. */ + public static boolean hasGeoLiterals(Graph graph, Node geometry) { + Objects.requireNonNull(geometry); + return containsGeoLiterals(graph, geometry); + } + + /** True if the node has a geometry or default geometry. Arguments must not be null. */ + public static boolean hasGeoResources(Graph graph, Node feature) { + Objects.requireNonNull(feature); + boolean result = + graph.contains(feature, Geo.HAS_DEFAULT_GEOMETRY_NODE, null) || + graph.contains(feature, Geo.HAS_GEOMETRY_NODE, null); + return result; + } + + /** + * True if the node is a geosparql spatial object by the present (geometry-related) properties. + * A mere "SpatialObject" type does not count. + * Arguments must not be null. Wgs84 does not count + */ + public static boolean isSpatialObjectByProperties(Graph graph, Node featureOrGeometry) { + return hasGeoLiterals(graph, featureOrGeometry) || hasGeoResources(graph, featureOrGeometry); + } + + /** + * Find all triples with geo:hasDefaultGeometry and geo:hasGeometry predicates. + * If a feature has a default geometry, then this method will omit all its (non-default) geometries. + */ + public static ExtendedIterator findSpecificGeoResources(Graph graph) { + // List resources that have a default geometry followed by those that + // only have a non-default one. + ExtendedIterator result = graph.find(null, Geo.HAS_DEFAULT_GEOMETRY_NODE, null); + try { + boolean hasDefaultGeometry = result.hasNext(); + ExtendedIterator it = graph.find(null, Geo.HAS_GEOMETRY_NODE, null); + + // No default geometry -> no need to filter. + result = hasDefaultGeometry + ? result.andThen(it.filterDrop(t -> G.hasProperty(graph, t.getSubject(), Geo.HAS_DEFAULT_GEOMETRY_NODE))) + : result.andThen(it); + } catch (RuntimeException t) { + result.close(); + throw new RuntimeException(t); + } + return result; + } + + public static ExtendedIterator findDefaultGeoResources(Graph graph) { + return graph.find(null, Geo.HAS_DEFAULT_GEOMETRY_NODE, null); + } + + public static ExtendedIterator findSpecificGeoResources(Graph graph, Node feature) { + Objects.requireNonNull(feature); + ExtendedIterator result = graph.find(feature, Geo.HAS_DEFAULT_GEOMETRY_NODE, null); + try { + if (!result.hasNext()) { + result.close(); + } + result = graph.find(feature, Geo.HAS_GEOMETRY_NODE, null); + } catch (RuntimeException t) { + result.close(); + throw new RuntimeException(t); + } + return result; + } + + /** + * Resolve a feature to its set of specific geometries via the following chain: + *
+     *   feature -> (geo:hasDefaultGeometry, geo:hasGeometry) ->
+     *     ({geo:asWKT, geo:asGML}, geo:hasSerialization) -> geo-literal.
+     * 
+ * + * If a geo:hasDefaultGeometry does not lead to a valid geo-literal there is no backtracking to geo:hasGeometry. + */ + public static Iterator findSpecificGeoLiteralsByFeature(Graph graph, Node feature) { + return Iter.flatMap(findSpecificGeoResources(graph, feature), + t -> findSpecificGeoLiterals(graph, t.getObject())); + } + + /** + * Iterate all triples of geometry resources with their most specific serialization form. + * The specific properties geo:asWKT and geo:asGML take precedence over the more general geo:hasSerialization. + * This means if a resource has wkt and/or gml then all geo:hasSerialization triples will be omitted for it. + */ + public static ExtendedIterator findSpecificGeoLiterals(Graph graph) { + ExtendedIterator result = graph.find(null, Geo.AS_WKT_NODE, null); + try { + result = result.andThen(graph.find(null, Geo.AS_GML_NODE, null)); + // If there is no specific serialization property use the general one. + if (!result.hasNext()) { + result.close(); + result = graph.find(null, Geo.HAS_SERIALIZATION_NODE, null); + } else { + // Append more general serializations for those resources that lack a specific one. + ExtendedIterator it = graph.find(null, Geo.HAS_SERIALIZATION_NODE, null).filterDrop(t -> + G.hasProperty(graph, t.getSubject(), Geo.AS_WKT_NODE) || + G.hasProperty(graph, t.getSubject(), Geo.AS_GML_NODE)); + result = result.andThen(it); + } + } catch (RuntimeException t) { + result.close(); + throw new RuntimeException(t); + } + return result; + } + + /** + * Iterate a given geometry resource's most specific geometry literals. + * The geometry resource must not be null. + * A specific serialization (WKT, GML) takes precedence over the more general hasSerialization property. + */ + public static ExtendedIterator findSpecificGeoLiterals(Graph graph, Node geometry) { + Objects.requireNonNull(geometry); + ExtendedIterator result = graph.find(geometry, Geo.AS_WKT_NODE, null); + try { + result = result.andThen(graph.find(geometry, Geo.AS_GML_NODE, null)); + if (!result.hasNext()) { + result.close(); + // Fallback to the more generic property. + result = graph.find(geometry, Geo.HAS_SERIALIZATION_NODE, null); + } + } catch (RuntimeException t) { + result.close(); + throw new RuntimeException(t); + } + return result; + } + + public static Node getGeoLiteral(Graph graph, Node geometry) { + Triple t = getGeoLiteralTriple(graph, geometry); + Node n = (t == null) ? null : t.getObject(); + return n; + } + + public static Triple getGeoLiteralTriple(Graph graph, Node geometry) { + Objects.requireNonNull(geometry); + + // Find the geometry literal of the geometry resource. + Triple t; + if ((t = getTripleSP(graph, geometry, Geo.HAS_SERIALIZATION_NODE)) != null) { + return t; + } + + // If hasSerialization not found then check asWKT. + if ((t = getTripleSP(graph, geometry, Geo.AS_WKT_NODE)) != null) { + return t; + } + + // If asWKT not found then check asGML. + if ((t = getTripleSP(graph, geometry, Geo.AS_GML_NODE)) != null) { + return t; + } + + return null; + } + + private static Triple getTripleSP(Graph graph, Node s, Node p) { + Node o = G.getSP(graph, s, p); + Triple t = (o == null) ? null : Triple.create(s, p, o); + return t; + } + + /** Shared code to test whether a node or graph has serialization properties. */ + private static boolean containsGeoLiterals(Graph graph, Node node) { + boolean result = + graph.contains(node, Geo.HAS_SERIALIZATION_NODE, null) || + graph.contains(node, Geo.AS_WKT_NODE, null) || + graph.contains(node, Geo.AS_GML_NODE, null); + return result; + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessWGS84.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessWGS84.java new file mode 100644 index 00000000000..b049c77bd88 --- /dev/null +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/access/AccessWGS84.java @@ -0,0 +1,155 @@ +/* + * 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 + * + * http://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. + */ + +package org.apache.jena.geosparql.implementation.access; + +import java.lang.invoke.MethodHandles; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; + +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.datatypes.DatatypeFormatException; +import org.apache.jena.geosparql.implementation.GeometryWrapper; +import org.apache.jena.geosparql.implementation.vocabulary.Geo; +import org.apache.jena.geosparql.implementation.vocabulary.SpatialExtension; +import org.apache.jena.geosparql.spatial.ConvertLatLon; +import org.apache.jena.graph.Graph; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.Triple; +import org.apache.jena.system.G; +import org.apache.jena.system.RDFDataException; +import org.apache.jena.util.iterator.ExtendedIterator; +import org.apache.jena.util.iterator.WrappedIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Central place for accessing Wgs84 point geometries in a {@link Graph}. + */ +public class AccessWGS84 { + private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + /** True iff the graph contains wgs84:{lat, long} triples. + * True does not imply that there are resources that have both lat AND long properties. */ + public static boolean containsGeoLiteralProperties(Graph graph) { + return containsGeoLiteralProperties(graph, null); + } + + /** True iff the node has wgs84:{lat, long} triples. + * True does not imply that both lat AND long are present on the node. */ + public static boolean hasGeoLiteralProperties(Graph graph, Node feature) { + Objects.requireNonNull(feature); + return containsGeoLiteralProperties(graph, feature); + } + + /** For each matching resource, build triples of format 's geo:hasGeometry geometryLiteral'. */ + // XXX geo:hasSerialization might seem a better choice but the original jena-geosparql implementation used geo:hasGeometry. + public static ExtendedIterator findGeoLiteralsAsTriples(Graph graph, Node s) { + return findGeoLiteralsAsTriples(graph, s, Geo.HAS_GEOMETRY_NODE); + } + + /** + * For each matching resource and its geometries, create triples of format 's p geometryLiteral' + * + * @param graph + * @param s The match subject. May be null. + * @param p The predicate to use for creating triples. Can be chosen freely but must not be null. + * @return Iterator of created triples (not obtained from the graph directly). + */ + public static ExtendedIterator findGeoLiteralsAsTriples(Graph graph, Node s, Node p) { + return findGeoLiterals(graph, s).mapWith(e -> Triple.create(e.getKey(), p, e.getValue().asNode())); + } + + /** + * For each matching resource, build geometry literals from the cartesian product of the WGS84 lat/long properties. + * Resources must have both properties, lat and long, to be matched by this method. + */ + public static ExtendedIterator> findGeoLiterals(Graph graph, Node s) { + // Warn about multiple lat/lon combinations only at most once per graph. + boolean enableWarnings = false; + boolean[] loggedMultipleLatLons = { false }; + ExtendedIterator latIt = graph.find(s, SpatialExtension.GEO_LAT_NODE, Node.ANY); + ExtendedIterator> result = WrappedIterator.create(Iter.iter(latIt).flatMap(triple -> { + Node feature = triple.getSubject(); + Node lat = triple.getObject(); + + // Create the cross-product between lats and lons. + ExtendedIterator lons = G.iterSP(graph, feature, SpatialExtension.GEO_LON_NODE); + + // On malformed data this can cause lots of log output. Perhaps it's better to keep validation separate from indexing. + int[] lonCounter = {0}; + ExtendedIterator> r = lons.mapWith(lon -> { + if (enableWarnings) { + if (lonCounter[0] == 1) { + if (!loggedMultipleLatLons[0]) { + LOGGER.warn("Geo predicates: multiple longitudes detected on feature " + feature + ". Further warnings will be omitted."); + loggedMultipleLatLons[0] = true; + } + } + ++lonCounter[0]; + } + GeometryWrapper geometryWrapper = ConvertLatLon.toGeometryWrapper(lat, lon); + return Map.entry(feature, geometryWrapper); + }); + return r; + })); + return result; + } + + /** + * Read lat/lon values for the given subject. Null if there are no such properties. + * Throws {@link DatatypeFormatException} when detecting incorrect use of these properties. + */ + public static Node getGeoLiteral(Graph graph, Node s) { + Node lat = null; + try { + lat = G.getZeroOrOneSP(graph, s, SpatialExtension.GEO_LAT_NODE); + } catch (RDFDataException ex) { + throw new DatatypeFormatException(s + " has more than one geo:lat property."); + } + + Node lon = null; + try { + lon = G.getZeroOrOneSP(graph, s, SpatialExtension.GEO_LON_NODE); + } catch ( RDFDataException ex) { + throw new DatatypeFormatException(s + " has more than one geo:lon property."); + } + + // Both null -> return null. + if (lat == null && lon == null) { + return null; + } + + if (lat == null) { + throw new DatatypeFormatException(s + " has a geo:lon property but is missing geo:lat."); + } + if (lon == null) { + throw new DatatypeFormatException(s + " has a geo:lat property but is missing geo:lon."); + } + Node geometryLiteral = ConvertLatLon.toNode(lat, lon); + return geometryLiteral; + } + + private static boolean containsGeoLiteralProperties(Graph graph, Node s) { + boolean result = + graph.contains(s, SpatialExtension.GEO_LAT_NODE, null) || + graph.contains(s, SpatialExtension.GEO_LON_NODE, null); + return result; + } +} diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/QueryRewriteIndex.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/QueryRewriteIndex.java index 6045174e655..c516da0cede 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/QueryRewriteIndex.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/QueryRewriteIndex.java @@ -181,7 +181,7 @@ public static final QueryRewriteIndex createDefault() { */ public static final void prepare(Dataset dataset) { Context context = dataset.getContext(); - context.set(QUERY_REWRITE_INDEX_SYMBOL, createDefault()); + set(context, createDefault()); } /** @@ -194,7 +194,7 @@ public static final void prepare(Dataset dataset) { */ public static final void prepare(Dataset dataset, String queryRewriteLabel, int maxSize, long expiryInterval) { Context context = dataset.getContext(); - context.set(QUERY_REWRITE_INDEX_SYMBOL, new QueryRewriteIndex(queryRewriteLabel, maxSize, expiryInterval)); + set(context, new QueryRewriteIndex(queryRewriteLabel, maxSize, expiryInterval)); } /** @@ -204,10 +204,9 @@ public static final void prepare(Dataset dataset, String queryRewriteLabel, int * @param execCxt * @return QueryRewriteIndex contained in the Context. */ - public static final QueryRewriteIndex retrieve(ExecutionContext execCxt) { - + public static final QueryRewriteIndex getOrCreate(ExecutionContext execCxt) { Context context = execCxt.getContext(); - return retrieve(context); + return getOrCreate(context); } /** @@ -217,10 +216,9 @@ public static final QueryRewriteIndex retrieve(ExecutionContext execCxt) { * @param dataset * @return QueryRewriteIndex contained in the Context. */ - public static final QueryRewriteIndex retrieve(Dataset dataset) { - + public static final QueryRewriteIndex getOrCreate(Dataset dataset) { Context context = dataset.getContext(); - return retrieve(context); + return getOrCreate(context); } /** @@ -230,15 +228,18 @@ public static final QueryRewriteIndex retrieve(Dataset dataset) { * @param context * @return QueryRewriteIndex contained in the Context. */ - public static final QueryRewriteIndex retrieve(Context context) { - QueryRewriteIndex queryRewriteIndex = context.get(QUERY_REWRITE_INDEX_SYMBOL, null); + public static final QueryRewriteIndex getOrCreate(Context context) { + QueryRewriteIndex queryRewriteIndex = context.computeIfAbsent(QUERY_REWRITE_INDEX_SYMBOL, k -> createDefault()); + return queryRewriteIndex; + } - if (queryRewriteIndex == null) { - queryRewriteIndex = createDefault(); - context.set(QUERY_REWRITE_INDEX_SYMBOL, queryRewriteIndex); - } + public static final QueryRewriteIndex get(Context context) { + return (context == null) ? null : context.get(QUERY_REWRITE_INDEX_SYMBOL); + } - return queryRewriteIndex; + public static final Context set(Context context, QueryRewriteIndex queryRewriteIndex) { + context.set(QUERY_REWRITE_INDEX_SYMBOL, queryRewriteIndex); + return context; } /** diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/SpatialIndexFindUtils.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/SpatialIndexFindUtils.java index cc9efa7e42e..c2264faad41 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/SpatialIndexFindUtils.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/SpatialIndexFindUtils.java @@ -17,29 +17,23 @@ */ package org.apache.jena.geosparql.spatial; -import java.lang.invoke.MethodHandles; import java.util.Iterator; import org.apache.jena.atlas.iterator.Iter; import org.apache.jena.atlas.iterator.IteratorCloseable; import org.apache.jena.geosparql.implementation.GeometryWrapper; -import org.apache.jena.geosparql.implementation.vocabulary.Geo; -import org.apache.jena.geosparql.implementation.vocabulary.SpatialExtension; +import org.apache.jena.geosparql.implementation.access.AccessGeoSPARQL; +import org.apache.jena.geosparql.implementation.access.AccessWGS84; import org.apache.jena.graph.Graph; import org.apache.jena.graph.Node; import org.apache.jena.graph.Triple; import org.apache.jena.sparql.core.DatasetGraph; -import org.apache.jena.system.G; import org.locationtech.jts.geom.Envelope; import org.opengis.geometry.MismatchedDimensionException; import org.opengis.referencing.operation.TransformException; import org.opengis.util.FactoryException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class SpatialIndexFindUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - /** * Find Spatial Index Items from all graphs in Dataset.
* @@ -47,22 +41,21 @@ public class SpatialIndexFindUtils { * @param srsURI * @return SpatialIndexItems found. */ - public static IteratorCloseable findSpatialIndexItems(DatasetGraph datasetGraph, String srsURI) { + public static IteratorCloseable findIndexItems(DatasetGraph datasetGraph, String srsURI) { Graph defaultGraph = datasetGraph.getDefaultGraph(); - IteratorCloseable itemsIter = findSpatialIndexItems(defaultGraph, srsURI); + IteratorCloseable itemsIter = findIndexItems(defaultGraph, srsURI); try { //Named Models Iterator graphNodeIt = datasetGraph.listGraphNodes(); Iterator namedGraphItemsIt = Iter.iter(graphNodeIt).flatMap(graphNode -> { Graph namedGraph = datasetGraph.getGraph(graphNode); - IteratorCloseable graphItems = findSpatialIndexItems(namedGraph, srsURI); + IteratorCloseable graphItems = findIndexItems(namedGraph, srsURI); return graphItems; }); itemsIter = Iter.iter(itemsIter).append(namedGraphItemsIt); } catch(Throwable t) { - t.addSuppressed(new RuntimeException("Failure during findSpatialIndexItems.", t)); Iter.close(itemsIter); - throw t; + throw new RuntimeException(t); } return itemsIter; } @@ -74,69 +67,33 @@ public static IteratorCloseable findSpatialIndexItems(DatasetG * @param srsURI * @return Items found in the Model in the SRS URI. */ - public static final IteratorCloseable findSpatialIndexItems(Graph graph, String srsURI) { + public static final IteratorCloseable findIndexItems(Graph graph, String srsURI) { IteratorCloseable result; // Only add one set of statements as a converted dataset will duplicate the same info. - if (graph.contains(null, Geo.HAS_GEOMETRY_NODE, null)) { - // LOGGER.info("Feature-hasGeometry-Geometry statements found."); - // if (graph.contains(null, SpatialExtension.GEO_LAT_NODE, null)) { - // LOGGER.warn("Lat/Lon Geo predicates also found but will not be added to index."); - // } - result = findGeometryIndexItems(graph, srsURI); - } else if (graph.contains(null, SpatialExtension.GEO_LAT_NODE, null)) { - // LOGGER.info("Geo predicate statements found."); - result = findGeoPredicateIndexItems(graph, srsURI); + if (AccessGeoSPARQL.containsGeoLiterals(graph)) { + result = findIndexItemsGeoSparql(graph, srsURI); + } else if (AccessWGS84.containsGeoLiteralProperties(graph)) { + result = findIndexItemsWgs84(graph, srsURI); } else { result = Iter.empty(); } return result; } - /** Print out log messages for what type of spatial data is found in the given graph. */ - public static final void checkSpatialIndexItems(Graph graph) { - // Only add one set of statements as a converted dataset will duplicate the same info. - if (graph.contains(null, Geo.HAS_GEOMETRY_NODE, null)) { - LOGGER.info("Feature-hasGeometry-Geometry statements found."); - if (graph.contains(null, SpatialExtension.GEO_LAT_NODE, null)) { - LOGGER.warn("Lat/Lon Geo predicates also found but will not be added to index."); - } - } else if (graph.contains(null, SpatialExtension.GEO_LAT_NODE, null)) { - LOGGER.info("Geo predicate statements found."); - } - } - /** * * @param graph * @param srsURI * @return SpatialIndexItem items prepared for adding to SpatialIndex. */ - public static IteratorCloseable findGeometryIndexItems(Graph graph, String srsURI) { - Iterator stmtIter = graph.find(null, Geo.HAS_GEOMETRY_NODE, null); + public static IteratorCloseable findIndexItemsGeoSparql(Graph graph, String srsURI) { + Iterator stmtIter = AccessGeoSPARQL.findSpecificGeoResources(graph); IteratorCloseable result = Iter.iter(stmtIter).flatMap(stmt -> { Node feature = stmt.getSubject(); Node geometry = stmt.getObject(); - - Iterator nodeIter = G.iterSP(graph, geometry, Geo.HAS_SERIALIZATION_NODE); - - // XXX If there is a super-property then the concrete serializations are not tried. - try { - if (!nodeIter.hasNext()) { - Iter.close(nodeIter); - - Iterator wktNodeIter = G.iterSP(graph, geometry, Geo.AS_WKT_NODE); - nodeIter = wktNodeIter; - - Iterator gmlNodeIter = G.iterSP(graph, geometry, Geo.AS_GML_NODE); - nodeIter = Iter.append(wktNodeIter, gmlNodeIter); - } - } catch (Throwable t) { - t.addSuppressed(new RuntimeException("Error encountered.", t)); - Iter.close(nodeIter); - throw t; - } - - Iterator itemIter = Iter.map(nodeIter, geometryNode -> { + Iterator serializationIter = AccessGeoSPARQL.findSpecificGeoLiterals(graph, geometry); + Iterator itemIter = Iter.map(serializationIter, triple -> { + Node geometryNode = triple.getObject(); GeometryWrapper geometryWrapper = GeometryWrapper.extract(geometryNode); SpatialIndexItem item = makeSpatialIndexItem(feature, geometryWrapper, srsURI); return item; @@ -147,42 +104,19 @@ public static IteratorCloseable findGeometryIndexItems(Graph g } /** + * * * @param graph * @param srsURI * @return Geo predicate objects prepared for adding to SpatialIndex. */ - public static IteratorCloseable findGeoPredicateIndexItems(Graph graph, String srsURI) { - // Warn about multiple lat/lon combinations only at most once per graph. - boolean enableWarnings = false; - boolean[] loggedMultipleLatLons = { false }; - Iterator latIt = graph.find(Node.ANY, SpatialExtension.GEO_LAT_NODE, Node.ANY); - IteratorCloseable result = Iter.iter(latIt).flatMap(triple -> { - Node feature = triple.getSubject(); - Node lat = triple.getObject(); - - // Create the cross-product between lats and lons. - Iterator lons = G.iterSP(graph, feature, SpatialExtension.GEO_LON_NODE); - - // On malformed data this can cause lots of log output. Perhaps it's better to keep validation separate from indexing. - int[] lonCounter = {0}; - Iterator r = Iter.iter(lons).map(lon -> { - if (enableWarnings) { - if (lonCounter[0] == 1) { - if (!loggedMultipleLatLons[0]) { - LOGGER.warn("Geo predicates: multiple longitudes detected on feature " + feature + ". Further warnings will be omitted."); - loggedMultipleLatLons[0] = true; - } - } - ++lonCounter[0]; - } - GeometryWrapper geometryWrapper = ConvertLatLon.toGeometryWrapper(lat, lon); - SpatialIndexItem item = makeSpatialIndexItem(feature, geometryWrapper, srsURI); - return item; - }); - return r; + public static IteratorCloseable findIndexItemsWgs84(Graph graph, String srsURI) { + return Iter.iter(AccessWGS84.findGeoLiterals(graph, null)).map(e -> { + Node feature = e.getKey(); + GeometryWrapper geometryWrapper = e.getValue(); + SpatialIndexItem item = makeSpatialIndexItem(feature, geometryWrapper, srsURI); + return item; }); - return result; } public static SpatialIndexItem makeSpatialIndexItem(Node feature, GeometryWrapper geometryWrapper, String srsURI) { diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/STRtreeUtils.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/STRtreeUtils.java index 37775eae533..15e40b27543 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/STRtreeUtils.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/STRtreeUtils.java @@ -40,7 +40,7 @@ public class STRtreeUtils { public static STRtree buildSpatialIndexTree(Graph graph, String srsURI) throws SpatialIndexException { try { STRtree tree; - IteratorCloseable it = SpatialIndexFindUtils.findSpatialIndexItems(graph, srsURI); + IteratorCloseable it = SpatialIndexFindUtils.findIndexItems(graph, srsURI); try { tree = buildSpatialIndexTree(it); } finally { diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/SpatialIndexLib.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/SpatialIndexLib.java index 4395de06b4a..7541912e9de 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/SpatialIndexLib.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/index/v2/SpatialIndexLib.java @@ -112,13 +112,13 @@ public static final boolean isDefined(ExecutionContext execCxt) { } /** - * Retrieve the SpatialIndex from the Context. + * Get the SpatialIndex from the Context. Fail if absent. * * @param execCxt * @return SpatialIndex contained in the Context. * @throws SpatialIndexException */ - public static final SpatialIndex retrieve(ExecutionContext execCxt) throws SpatialIndexException { + public static final SpatialIndex require(ExecutionContext execCxt) throws SpatialIndexException { Context context = execCxt.getContext(); SpatialIndex spatialIndex = (SpatialIndex) context.get(SpatialIndexConstants.symSpatialIndex, null); if (spatialIndex == null) { diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/property_functions/GenericSpatialPropertyFunction.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/property_functions/GenericSpatialPropertyFunction.java index 12625b59acc..a932ffc43ce 100644 --- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/property_functions/GenericSpatialPropertyFunction.java +++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/spatial/property_functions/GenericSpatialPropertyFunction.java @@ -17,19 +17,16 @@ */ package org.apache.jena.geosparql.spatial.property_functions; -import java.util.Arrays; import java.util.Collection; import java.util.Iterator; -import java.util.List; import java.util.stream.Stream; -import org.apache.commons.collections4.iterators.IteratorChain; +import org.apache.jena.atlas.iterator.Iter; import org.apache.jena.datatypes.DatatypeFormatException; import org.apache.jena.geosparql.implementation.GeometryWrapper; import org.apache.jena.geosparql.implementation.SRSInfo; -import org.apache.jena.geosparql.implementation.vocabulary.Geo; -import org.apache.jena.geosparql.implementation.vocabulary.SpatialExtension; -import org.apache.jena.geosparql.spatial.ConvertLatLon; +import org.apache.jena.geosparql.implementation.access.AccessGeoSPARQL; +import org.apache.jena.geosparql.implementation.access.AccessWGS84; import org.apache.jena.geosparql.spatial.SearchEnvelope; import org.apache.jena.geosparql.spatial.SpatialIndex; import org.apache.jena.geosparql.spatial.SpatialIndexException; @@ -49,8 +46,6 @@ import org.apache.jena.sparql.pfunction.PFuncSimpleAndList; import org.apache.jena.sparql.pfunction.PropFuncArg; import org.apache.jena.sparql.util.FmtUtils; -import org.apache.jena.system.G; -import org.apache.jena.util.iterator.ExtendedIterator; /** * @@ -66,7 +61,7 @@ public abstract class GenericSpatialPropertyFunction extends PFuncSimpleAndList @Override public final QueryIterator execEvaluated(Binding binding, Node subject, Node predicate, PropFuncArg object, ExecutionContext execCxt) { try { - spatialIndex = SpatialIndexLib.retrieve(execCxt); + spatialIndex = SpatialIndexLib.require(execCxt); spatialArguments = extractObjectArguments(predicate, object, spatialIndex.getSrsInfo()); return search(binding, execCxt, subject, spatialArguments.limit); } catch (SpatialIndexException ex) { @@ -110,47 +105,23 @@ private boolean checkBound(ExecutionContext execCxt, Node subject) { try { Graph graph = execCxt.getActiveGraph(); - IteratorChain spatialTriples = new IteratorChain<>(); - - //Check for Geometry and so GeometryLiterals. - if (graph.contains(subject, Geo.HAS_GEOMETRY_NODE, null)) { - //A Feature can have many geometries so add each of them. The check Geo.HAS_DEFAULT_GEOMETRY_NODE will only return one but requires the data to have these present. - List geometryNodes = G.listSP(graph, subject, Geo.HAS_GEOMETRY_NODE); - geometryNodes.forEach(geometry->{ - ExtendedIterator iter = graph.find(geometry, Geo.HAS_SERIALIZATION_NODE, null); - // Check for asWKT - if (!iter.hasNext()) { - iter = graph.find(geometry, Geo.AS_WKT_NODE, null); - } - // Check for asGML - if (!iter.hasNext()) { - iter = graph.find(geometry, Geo.AS_GML_NODE, null); - } - spatialTriples.addIterator(iter); - }); + Iterator spatialTriples; + + if (AccessGeoSPARQL.containsGeoLiterals(graph)) { + spatialTriples = AccessGeoSPARQL.findSpecificGeoLiteralsByFeature(graph, subject); + } else if (AccessWGS84.containsGeoLiteralProperties(graph)) { + spatialTriples = AccessWGS84.findGeoLiteralsAsTriples(graph, subject); } else { - //Check for Geo predicates against the feature when no geometry literals found. - if (graph.contains(subject, SpatialExtension.GEO_LAT_NODE, null) && graph.contains(subject, SpatialExtension.GEO_LON_NODE, null)) { - Node lat = G.getOneSP(graph, subject, SpatialExtension.GEO_LAT_NODE); - Node lon = G.getOneSP(graph, subject, SpatialExtension.GEO_LON_NODE); - Node latLonGeometryLiteral = ConvertLatLon.toNode(lat, lon); - Triple triple = Triple.create(subject, Geo.HAS_GEOMETRY_NODE, latLonGeometryLiteral); - spatialTriples.addIterator(Arrays.asList(triple).iterator()); - } + spatialTriples = Iter.empty(); } //Check through each Geometry and stop if one is accepted. - boolean isMatched = false; - while (spatialTriples.hasNext()) { - Triple triple = spatialTriples.next(); + boolean isMatched = Iter.anyMatch(spatialTriples, triple -> { Node geometryLiteral = triple.getObject(); GeometryWrapper targetGeometryWrapper = GeometryWrapper.extract(geometryLiteral); - isMatched = checkSecondFilter(spatialArguments, targetGeometryWrapper); - if (isMatched) { - //Stop checking when match is true. - break; - } - } + boolean isMatch = checkSecondFilter(spatialArguments, targetGeometryWrapper); + return isMatch; + }); return isMatched; } catch (DatatypeFormatException ex) { diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/property_functions/simple_features/SfPFMiscSparqlTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/property_functions/simple_features/SfPFMiscSparqlTest.java new file mode 100644 index 00000000000..a329e1cb320 --- /dev/null +++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/property_functions/simple_features/SfPFMiscSparqlTest.java @@ -0,0 +1,99 @@ +/* + * 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 + * + * http://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. + */ +package org.apache.jena.geosparql.geo.topological.property_functions.simple_features; + +import static org.junit.Assert.assertEquals; + +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.sse.SSE; +import org.junit.Test; + +/** Miscellaneous SPARQL-based tests across the simple feature family of property functions. */ +public class SfPFMiscSparqlTest { + + @Test + public void test01() { + String query = """ + PREFIX geo: + PREFIX ogcsf: + SELECT * { + ?s a ogcsf:Point . + ?s geo:sfWithin . + } ORDER BY ?s + """; + + DatasetGraph dsg = createTestDataFrance(); + Table actual = QueryExec.dataset(dsg).query(query).table(); + Table expected = SSE.parseTable("(table (row (?s ) ))"); + assertEquals(expected, actual); + } + + @Test + public void test02() { + String query = """ + PREFIX geo: + SELECT * { + ?s geo:sfWithin . + } ORDER BY ?s + """; + + // Note: sfWithin is reflexive so 'geoFrance' is really expected as a result. + DatasetGraph dsg = createTestDataFrance(); + Table actual = QueryExec.dataset(dsg).query(query).table(); + Table expected = SSE.parseTable("(table (row (?s ) ) (row (?s ) ))"); + assertEquals(expected, actual); + } + + // Test data derived from GH-3473. + private static DatasetGraph createTestDataFrance() { + String data = """ + PREFIX : + PREFIX rdfs: + PREFIX geo: + PREFIX ogcsf: + + :France + rdfs:label "France"; + geo:hasGeometry :geoFrance. + + :geoFrance a ogcsf:Polygon ; + # This is a bounding box of France. + geo:asWKT "POLYGON((-4.9423 41.3247, -4.9423 51.1496, 10.02105 51.1496, 10.0210 41.3247, -4.9423 41.3247))"^^geo:wktLiteral . + + :Strasbourg + rdfs:label "Strasbourg"; + geo:hasGeometry :geoStrasbourg. + + :geoStrasbourg a ogcsf:Point ; + geo:asWKT "POINT(7.7510 48.5819)"^^geo:wktLiteral . + + # This point is outside of France's BBOX. + :Berlin + rdfs:label "Berlin"; + geo:hasGeometry :geoBerlin. + + :geoBerlin a ogcsf:Point ; + geo:asWKT "POINT (13.4050 52.5200)"^^geo:wktLiteral . + """; + return RDFParser.create().fromString(data).lang(Lang.TRIG).toDatasetGraph(); + } +}