spargeo/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(test(attr(deny(warnings))))]
3#![cfg_attr(docsrs, feature(doc_auto_cfg))]
4#![doc(html_favicon_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
5#![doc(html_logo_url = "https://raw.githubusercontent.com/oxigraph/oxigraph/main/logo.svg")]
6
7use geo::{Geometry, Relate};
8use geojson::GeoJson;
9use oxigraph::model::{Literal, NamedNodeRef, Term};
10use oxigraph::sparql::QueryOptions;
11use spareval::QueryEvaluator;
12use std::str::FromStr;
13use wkt::TryFromWkt;
14
15/// Registers GeoSPARQL extension functions in the [`QueryOptions`]
16pub fn register_geosparql_functions(options: QueryOptions) -> QueryOptions {
17    options
18        .with_custom_function(geosparql_functions::SF_EQUALS.into(), geof_sf_equals)
19        .with_custom_function(geosparql_functions::SF_DISJOINT.into(), geof_sf_disjoint)
20        .with_custom_function(
21            geosparql_functions::SF_INTERSECTS.into(),
22            geof_sf_intersects,
23        )
24        .with_custom_function(geosparql_functions::SF_TOUCHES.into(), geof_sf_touches)
25        .with_custom_function(geosparql_functions::SF_CROSSES.into(), geof_sf_crosses)
26        .with_custom_function(geosparql_functions::SF_WITHIN.into(), geof_sf_within)
27        .with_custom_function(geosparql_functions::SF_CONTAINS.into(), geof_sf_contains)
28        .with_custom_function(geosparql_functions::SF_OVERLAPS.into(), geof_sf_overlaps)
29}
30
31/// Registers GeoSPARQL extension functions in the [`QueryEvaluator`]
32pub fn add_geosparql_functions(evaluator: QueryEvaluator) -> QueryEvaluator {
33    evaluator
34        .with_custom_function(geosparql_functions::SF_EQUALS.into(), geof_sf_equals)
35        .with_custom_function(geosparql_functions::SF_DISJOINT.into(), geof_sf_disjoint)
36        .with_custom_function(
37            geosparql_functions::SF_INTERSECTS.into(),
38            geof_sf_intersects,
39        )
40        .with_custom_function(geosparql_functions::SF_TOUCHES.into(), geof_sf_touches)
41        .with_custom_function(geosparql_functions::SF_CROSSES.into(), geof_sf_crosses)
42        .with_custom_function(geosparql_functions::SF_WITHIN.into(), geof_sf_within)
43        .with_custom_function(geosparql_functions::SF_CONTAINS.into(), geof_sf_contains)
44        .with_custom_function(geosparql_functions::SF_OVERLAPS.into(), geof_sf_overlaps)
45}
46
47/// List of GeoSPARQL functions supported and registered by [`register_geosparql_functions`]
48pub const GEOSPARQL_EXTENSION_FUNCTIONS: [NamedNodeRef<'static>; 8] = [
49    geosparql_functions::SF_EQUALS,
50    geosparql_functions::SF_DISJOINT,
51    geosparql_functions::SF_INTERSECTS,
52    geosparql_functions::SF_TOUCHES,
53    geosparql_functions::SF_CROSSES,
54    geosparql_functions::SF_WITHIN,
55    geosparql_functions::SF_CONTAINS,
56    geosparql_functions::SF_OVERLAPS,
57];
58
59fn geof_sf_equals(args: &[Term]) -> Option<Term> {
60    binary_geo_fn(args, |a, b| a.relate(&b).is_equal_topo())
61}
62
63fn geof_sf_disjoint(args: &[Term]) -> Option<Term> {
64    binary_geo_fn(args, |a, b| a.relate(&b).is_disjoint())
65}
66
67fn geof_sf_intersects(args: &[Term]) -> Option<Term> {
68    binary_geo_fn(args, |a, b| a.relate(&b).is_intersects())
69}
70
71fn geof_sf_touches(args: &[Term]) -> Option<Term> {
72    binary_geo_fn(args, |a, b| a.relate(&b).is_touches())
73}
74
75fn geof_sf_crosses(args: &[Term]) -> Option<Term> {
76    binary_geo_fn(args, |a, b| a.relate(&b).is_crosses())
77}
78
79fn geof_sf_within(args: &[Term]) -> Option<Term> {
80    binary_geo_fn(args, |a, b| a.relate(&b).is_within())
81}
82
83fn geof_sf_contains(args: &[Term]) -> Option<Term> {
84    binary_geo_fn(args, |a, b| a.relate(&b).is_contains())
85}
86
87fn geof_sf_overlaps(args: &[Term]) -> Option<Term> {
88    binary_geo_fn(args, |a, b| a.relate(&b).is_overlaps())
89}
90
91fn binary_geo_fn<R: Into<Literal>>(
92    args: &[Term],
93    operation: impl FnOnce(Geometry, Geometry) -> R,
94) -> Option<Term> {
95    let args: &[Term; 2] = args.try_into().ok()?;
96    let left = extract_argument(&args[0])?;
97    let right = extract_argument(&args[1])?;
98    Some(operation(left, right).into().into())
99}
100
101// Parse
102fn extract_argument(term: &Term) -> Option<Geometry> {
103    let Term::Literal(literal) = term else {
104        return None;
105    };
106    if literal.datatype() == geosparql::WKT_LITERAL {
107        parse_wkt_literal(literal.value().trim())
108    } else if literal.datatype() == geosparql::GEO_JSON_LITERAL {
109        parse_geo_json_literal(literal.value().trim())
110    } else {
111        None
112    }
113}
114
115// Parse a WKT literal including reference system http://www.opengis.net/def/crs/OGC/1.3/CRS84
116fn parse_wkt_literal(value: &str) -> Option<Geometry> {
117    let mut value = value.trim_start();
118    if let Some(val) = value.strip_prefix('<') {
119        // We have a reference system
120        let (system, val) = val.split_once('>').unwrap_or((val, ""));
121        if system != "http://www.opengis.net/def/crs/OGC/1.3/CRS84" {
122            // We only support CRS84
123            return None;
124        }
125        value = val.trim_start();
126    }
127    Geometry::try_from_wkt_str(value).ok()
128}
129
130fn parse_geo_json_literal(value: &str) -> Option<Geometry> {
131    GeoJson::from_str(value).ok()?.try_into().ok()
132}
133
134mod geosparql {
135    //! [GeoSpatial](https://opengeospatial.github.io/ogc-geosparql/) vocabulary.
136    use oxigraph::model::NamedNodeRef;
137
138    pub const GEO_JSON_LITERAL: NamedNodeRef<'_> =
139        NamedNodeRef::new_unchecked("http://www.opengis.net/ont/geosparql#geoJSONLiteral");
140    pub const WKT_LITERAL: NamedNodeRef<'_> =
141        NamedNodeRef::new_unchecked("http://www.opengis.net/ont/geosparql#wktLiteral");
142}
143
144mod geosparql_functions {
145    //! [GeoSpatial](https://opengeospatial.github.io/ogc-geosparql/) functions vocabulary.
146    use oxigraph::model::NamedNodeRef;
147
148    pub const SF_CONTAINS: NamedNodeRef<'_> =
149        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfContains");
150    pub const SF_CROSSES: NamedNodeRef<'_> =
151        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfCrosses");
152    pub const SF_DISJOINT: NamedNodeRef<'_> =
153        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfDisjoint");
154    pub const SF_EQUALS: NamedNodeRef<'_> =
155        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfEquals");
156    pub const SF_INTERSECTS: NamedNodeRef<'_> =
157        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfIntersects");
158    pub const SF_OVERLAPS: NamedNodeRef<'_> =
159        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfOverlaps");
160    pub const SF_TOUCHES: NamedNodeRef<'_> =
161        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfTouches");
162    pub const SF_WITHIN: NamedNodeRef<'_> =
163        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfWithin");
164}