use anyhow::{anyhow, Result};
use oxirs_core::model::NamedNode;
use super::queryexecutor_type::QueryExecutor;
use crate::algebra::{Binding, Expression, Literal, Term};
const GEOF_DISTANCE: &str = "http://www.opengis.net/def/function/geosparql/distance";
const OXIRS_DISTANCE_LAT_LON: &str = "http://oxirs.io/fn/geo#distanceLatLon";
const UOM_METRE: &str = "http://www.opengis.net/def/uom/OGC/1.0/metre";
const UOM_METRE_EPSG: &str = "http://www.opengis.net/def/uom/EPSG/0/9001";
const UOM_KILOMETRE: &str = "http://www.opengis.net/def/uom/OGC/1.0/kilometre";
const UOM_MILE: &str = "http://www.opengis.net/def/uom/OGC/1.0/mile";
const XSD_DOUBLE: &str = "http://www.w3.org/2001/XMLSchema#double";
const CRS84: &str = "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
const EPSG_4326: &str = "http://www.opengis.net/def/crs/EPSG/0/4326";
const EARTH_RADIUS_M: f64 = 6_371_000.0;
const METRES_PER_MILE: f64 = 1_609.344;
type LatLon = (f64, f64);
impl QueryExecutor {
pub(super) fn try_geosparql_function(
&self,
name: &str,
args: &[Expression],
binding: &Binding,
) -> Option<Result<Term>> {
match name {
GEOF_DISTANCE => Some(self.geof_distance(args, binding)),
OXIRS_DISTANCE_LAT_LON => Some(self.geo_distance_lat_lon(args, binding)),
_ => None,
}
}
fn geof_distance(&self, args: &[Expression], binding: &Binding) -> Result<Term> {
if args.len() != 3 {
return Err(anyhow!(
"geof:distance requires exactly 3 arguments (geomA, geomB, unitsIRI)"
));
}
let geom_a = self.evaluate_expression(&args[0], binding)?;
let geom_b = self.evaluate_expression(&args[1], binding)?;
let units = self.evaluate_expression(&args[2], binding)?;
let point_a = geometry_point(&geom_a)?;
let point_b = geometry_point(&geom_b)?;
let metres = haversine_metres(point_a, point_b);
let units_iri = units_iri_str(&units)?;
let value = convert_from_metres(metres, &units_iri)?;
Ok(double_literal(value))
}
fn geo_distance_lat_lon(&self, args: &[Expression], binding: &Binding) -> Result<Term> {
if args.len() != 4 {
return Err(anyhow!(
"distanceLatLon requires exactly 4 arguments (lat1, lon1, lat2, lon2)"
));
}
let lat1 = self.numeric_arg(&args[0], binding)?;
let lon1 = self.numeric_arg(&args[1], binding)?;
let lat2 = self.numeric_arg(&args[2], binding)?;
let lon2 = self.numeric_arg(&args[3], binding)?;
let metres = haversine_metres((lat1, lon1), (lat2, lon2));
Ok(double_literal(metres))
}
fn numeric_arg(&self, expr: &Expression, binding: &Binding) -> Result<f64> {
let term = self.evaluate_expression(expr, binding)?;
self.extract_numeric_value(&term)
}
}
fn haversine_metres(a: LatLon, b: LatLon) -> f64 {
let (lat_a, lon_a) = a;
let (lat_b, lon_b) = b;
let phi_a = lat_a.to_radians();
let phi_b = lat_b.to_radians();
let d_phi = (lat_b - lat_a).to_radians();
let d_lambda = (lon_b - lon_a).to_radians();
let sin_half_dphi = (d_phi / 2.0).sin();
let sin_half_dlambda = (d_lambda / 2.0).sin();
let h = sin_half_dphi * sin_half_dphi
+ phi_a.cos() * phi_b.cos() * sin_half_dlambda * sin_half_dlambda;
2.0 * EARTH_RADIUS_M * h.sqrt().asin()
}
fn geometry_point(term: &Term) -> Result<LatLon> {
match term {
Term::Literal(lit) => parse_wkt_point(&lit.value),
other => Err(anyhow!(
"geof:distance geometry argument must be a wktLiteral, got {other}"
)),
}
}
fn parse_wkt_point(raw: &str) -> Result<LatLon> {
let mut text = raw.trim();
let mut lat_first = false;
if let Some(rest) = text.strip_prefix('<') {
let close = rest
.find('>')
.ok_or_else(|| anyhow!("invalid WKT CRS prefix (unterminated '<'): {raw}"))?;
let crs = rest[..close].trim();
lat_first = match crs {
CRS84 => false,
EPSG_4326 => true,
other => {
return Err(anyhow!(
"unsupported WKT coordinate reference system '{other}' in {raw} \
(only CRS84 and EPSG:4326 are supported)"
));
}
};
text = rest[close + 1..].trim_start();
}
let open = text
.find('(')
.ok_or_else(|| anyhow!("invalid WKT (missing '('): {raw}"))?;
let keyword = text[..open].trim();
if !keyword.eq_ignore_ascii_case("point") {
return Err(anyhow!(
"unsupported WKT geometry (only POINT is supported): {raw}"
));
}
let close = text
.rfind(')')
.ok_or_else(|| anyhow!("invalid WKT (missing ')'): {raw}"))?;
if close < open {
return Err(anyhow!("invalid WKT parentheses: {raw}"));
}
let inner = text[open + 1..close].trim();
let mut coords = inner.split_whitespace();
let first = coords
.next()
.ok_or_else(|| anyhow!("WKT POINT missing first coordinate: {raw}"))?;
let second = coords
.next()
.ok_or_else(|| anyhow!("WKT POINT missing second coordinate: {raw}"))?;
if coords.next().is_some() {
return Err(anyhow!("WKT POINT expects exactly two coordinates: {raw}"));
}
let first: f64 = first
.parse()
.map_err(|_| anyhow!("invalid WKT coordinate '{first}' in {raw}"))?;
let second: f64 = second
.parse()
.map_err(|_| anyhow!("invalid WKT coordinate '{second}' in {raw}"))?;
if lat_first {
Ok((first, second))
} else {
Ok((second, first))
}
}
fn units_iri_str(term: &Term) -> Result<String> {
match term {
Term::Iri(iri) => Ok(iri.as_str().to_string()),
Term::Literal(lit) => Ok(lit.value.clone()),
other => Err(anyhow!("units argument must be an IRI, got {other}")),
}
}
fn convert_from_metres(metres: f64, units_iri: &str) -> Result<f64> {
match units_iri {
UOM_METRE | UOM_METRE_EPSG => Ok(metres),
UOM_KILOMETRE => Ok(metres / 1000.0),
UOM_MILE => Ok(metres / METRES_PER_MILE),
other => Err(anyhow!("unsupported unit of measure: {other}")),
}
}
fn double_literal(value: f64) -> Term {
Term::Literal(Literal {
value: value.to_string(),
language: None,
datatype: Some(NamedNode::new_unchecked(XSD_DOUBLE)),
})
}