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
13pub 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
67fn 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
81fn parse_wkt_literal(value: &str) -> Option<Geometry> {
83 let mut value = value.trim_start();
84 if let Some(val) = value.strip_prefix('<') {
85 let (system, val) = val.split_once('>').unwrap_or((val, ""));
87 if system != "http://www.opengis.net/def/crs/OGC/1.3/CRS84" {
88 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 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 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}