spargeo/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(test(attr(deny(warnings))))]
3#![cfg_attr(docsrs, feature(doc_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 oxrdf::{Literal, NamedNodeRef, Term};
10use std::str::FromStr;
11use wkt::TryFromWkt;
12
13/// GeoSPARQL functions in name and implementation pairs
14pub const GEOSPARQL_EXTENSION_FUNCTIONS: [(NamedNodeRef<'static>, fn(&[Term]) -> Option<Term>); 8] = [
15    (geosparql_functions::SF_EQUALS, geof_sf_equals),
16    (geosparql_functions::SF_DISJOINT, geof_sf_disjoint),
17    (geosparql_functions::SF_INTERSECTS, geof_sf_intersects),
18    (geosparql_functions::SF_TOUCHES, geof_sf_touches),
19    (geosparql_functions::SF_CROSSES, geof_sf_crosses),
20    (geosparql_functions::SF_WITHIN, geof_sf_within),
21    (geosparql_functions::SF_CONTAINS, geof_sf_contains),
22    (geosparql_functions::SF_OVERLAPS, geof_sf_overlaps),
23];
24
25fn geof_sf_equals(args: &[Term]) -> Option<Term> {
26    binary_geo_fn(args, |a, b| a.relate(&b).is_equal_topo())
27}
28
29fn geof_sf_disjoint(args: &[Term]) -> Option<Term> {
30    binary_geo_fn(args, |a, b| a.relate(&b).is_disjoint())
31}
32
33fn geof_sf_intersects(args: &[Term]) -> Option<Term> {
34    binary_geo_fn(args, |a, b| a.relate(&b).is_intersects())
35}
36
37fn geof_sf_touches(args: &[Term]) -> Option<Term> {
38    binary_geo_fn(args, |a, b| a.relate(&b).is_touches())
39}
40
41fn geof_sf_crosses(args: &[Term]) -> Option<Term> {
42    binary_geo_fn(args, |a, b| a.relate(&b).is_crosses())
43}
44
45fn geof_sf_within(args: &[Term]) -> Option<Term> {
46    binary_geo_fn(args, |a, b| a.relate(&b).is_within())
47}
48
49fn geof_sf_contains(args: &[Term]) -> Option<Term> {
50    binary_geo_fn(args, |a, b| a.relate(&b).is_contains())
51}
52
53fn geof_sf_overlaps(args: &[Term]) -> Option<Term> {
54    binary_geo_fn(args, |a, b| a.relate(&b).is_overlaps())
55}
56
57fn binary_geo_fn<R: Into<Literal>>(
58    args: &[Term],
59    operation: impl FnOnce(Geometry, Geometry) -> R,
60) -> Option<Term> {
61    let args: &[Term; 2] = args.try_into().ok()?;
62    let left = extract_argument(&args[0])?;
63    let right = extract_argument(&args[1])?;
64    Some(operation(left, right).into().into())
65}
66
67// Parse
68fn extract_argument(term: &Term) -> Option<Geometry> {
69    let Term::Literal(literal) = term else {
70        return None;
71    };
72    if literal.datatype() == geosparql::WKT_LITERAL {
73        parse_wkt_literal(literal.value().trim())
74    } else if literal.datatype() == geosparql::GEO_JSON_LITERAL {
75        parse_geo_json_literal(literal.value().trim())
76    } else {
77        None
78    }
79}
80
81// Parse a WKT literal including reference system http://www.opengis.net/def/crs/OGC/1.3/CRS84
82fn parse_wkt_literal(value: &str) -> Option<Geometry> {
83    let mut value = value.trim_start();
84    if let Some(val) = value.strip_prefix('<') {
85        // We have a reference system
86        let (system, val) = val.split_once('>').unwrap_or((val, ""));
87        if system != "http://www.opengis.net/def/crs/OGC/1.3/CRS84" {
88            // We only support CRS84
89            return None;
90        }
91        value = val.trim_start();
92    }
93    Geometry::try_from_wkt_str(value).ok()
94}
95
96fn parse_geo_json_literal(value: &str) -> Option<Geometry> {
97    GeoJson::from_str(value).ok()?.try_into().ok()
98}
99
100mod geosparql {
101    //! [GeoSpatial](https://opengeospatial.github.io/ogc-geosparql/) vocabulary.
102    use oxrdf::NamedNodeRef;
103
104    pub const GEO_JSON_LITERAL: NamedNodeRef<'_> =
105        NamedNodeRef::new_unchecked("http://www.opengis.net/ont/geosparql#geoJSONLiteral");
106    pub const WKT_LITERAL: NamedNodeRef<'_> =
107        NamedNodeRef::new_unchecked("http://www.opengis.net/ont/geosparql#wktLiteral");
108}
109
110mod geosparql_functions {
111    //! [GeoSpatial](https://opengeospatial.github.io/ogc-geosparql/) functions vocabulary.
112    use oxrdf::NamedNodeRef;
113
114    pub const SF_CONTAINS: NamedNodeRef<'_> =
115        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfContains");
116    pub const SF_CROSSES: NamedNodeRef<'_> =
117        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfCrosses");
118    pub const SF_DISJOINT: NamedNodeRef<'_> =
119        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfDisjoint");
120    pub const SF_EQUALS: NamedNodeRef<'_> =
121        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfEquals");
122    pub const SF_INTERSECTS: NamedNodeRef<'_> =
123        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfIntersects");
124    pub const SF_OVERLAPS: NamedNodeRef<'_> =
125        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfOverlaps");
126    pub const SF_TOUCHES: NamedNodeRef<'_> =
127        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfTouches");
128    pub const SF_WITHIN: NamedNodeRef<'_> =
129        NamedNodeRef::new_unchecked("http://www.opengis.net/def/function/geosparql/sfWithin");
130}