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
15pub 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
31pub 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
47pub 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
101fn 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
115fn parse_wkt_literal(value: &str) -> Option<Geometry> {
117 let mut value = value.trim_start();
118 if let Some(val) = value.strip_prefix('<') {
119 let (system, val) = val.split_once('>').unwrap_or((val, ""));
121 if system != "http://www.opengis.net/def/crs/OGC/1.3/CRS84" {
122 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 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 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}