use super::{ExtCtx, ExtensionDef, RuntimeStrategy, any_null, arg_cube, arg_f64, cube, no_such};
use crate::relational::SqlValue;
use crate::sql::error::Result;
pub static DEF: ExtensionDef = ExtensionDef {
name: "earthdistance",
default_version: "1.2",
comment: "calculate great-circle distances on the surface of the Earth",
requires: &["cube"],
functions: &[
"earth",
"sec_to_gc",
"gc_to_sec",
"ll_to_earth",
"latitude",
"longitude",
"earth_distance",
"earth_box",
],
types: &[],
gucs: &[],
trusted: true,
call: Some(call),
strategy: RuntimeStrategy::Native,
};
const EARTH_RADIUS: f64 = 6378168.0;
fn call(_ctx: &ExtCtx, name: &str, args: &[SqlValue]) -> Result<SqlValue> {
if any_null(args) {
return Ok(SqlValue::Null);
}
match name {
"earth" => Ok(SqlValue::Float8(EARTH_RADIUS)),
"sec_to_gc" => Ok(SqlValue::Float8(sec_to_gc(arg_f64(args, 0, name)?))),
"gc_to_sec" => Ok(SqlValue::Float8(gc_to_sec(arg_f64(args, 0, name)?))),
"ll_to_earth" => {
let lat = arg_f64(args, 0, name)?;
let lon = arg_f64(args, 1, name)?;
Ok(cube::point(ll_to_earth(lat, lon)))
}
"latitude" => {
let (_, _, z) = surface_coords(args, name)?;
let ratio = z / EARTH_RADIUS;
let deg = if ratio < -1.0 {
-90.0
} else if ratio > 1.0 {
90.0
} else {
ratio.asin().to_degrees()
};
Ok(SqlValue::Float8(deg))
}
"longitude" => {
let (x, y, _) = surface_coords(args, name)?;
Ok(SqlValue::Float8(y.atan2(x).to_degrees()))
}
"earth_distance" => {
let a = corners(args, 0, name)?;
let b = corners(args, 1, name)?;
Ok(SqlValue::Float8(sec_to_gc(cube::distance(&a, &b))))
}
"earth_box" => {
let c = corners(args, 0, name)?;
let radius = arg_f64(args, 1, name)?;
Ok(cube::enlarge(&c, gc_to_sec(radius), 3).value())
}
_ => Err(no_such(name)),
}
}
fn sec_to_gc(sec: f64) -> f64 {
if sec <= 0.0 {
0.0
} else if sec >= 2.0 * EARTH_RADIUS {
std::f64::consts::PI * EARTH_RADIUS
} else {
2.0 * EARTH_RADIUS * (sec / (2.0 * EARTH_RADIUS)).asin()
}
}
fn gc_to_sec(gc: f64) -> f64 {
if gc <= 0.0 {
0.0
} else if gc >= std::f64::consts::PI * EARTH_RADIUS {
2.0 * EARTH_RADIUS
} else {
2.0 * EARTH_RADIUS * (gc / (2.0 * EARTH_RADIUS)).sin()
}
}
fn ll_to_earth(lat: f64, lon: f64) -> Vec<f64> {
let (rlat, rlon) = (lat.to_radians(), lon.to_radians());
vec![
EARTH_RADIUS * rlat.cos() * rlon.cos(),
EARTH_RADIUS * rlat.cos() * rlon.sin(),
EARTH_RADIUS * rlat.sin(),
]
}
fn surface_coords(args: &[SqlValue], func: &str) -> Result<(f64, f64, f64)> {
let c = corners(args, 0, func)?;
Ok((c.min(0), c.min(1), c.min(2)))
}
fn corners(args: &[SqlValue], idx: usize, func: &str) -> Result<cube::Corners> {
let (ll, ur) = arg_cube(args, idx, func)?;
Ok(cube::Corners { ll, ur })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relational::SqlType;
use chrono::Utc;
use std::cell::RefCell;
use std::collections::HashMap;
fn invoke(name: &str, args: &[SqlValue]) -> Result<SqlValue> {
let vars = RefCell::new(HashMap::new());
let ctx = ExtCtx {
now: Utc::now(),
vars: &vars,
};
call(&ctx, name, args)
}
fn f(n: f64) -> SqlValue {
SqlValue::Float8(n)
}
fn f8(v: SqlValue) -> f64 {
match v {
SqlValue::Float8(x) => x,
other => panic!("expected float8, got {other:?}"),
}
}
fn ll(lat: f64, lon: f64) -> SqlValue {
invoke("ll_to_earth", &[f(lat), f(lon)]).unwrap()
}
fn close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() <= tol,
"expected {expected}, got {actual} (tolerance {tol})"
);
}
#[test]
fn earth_radius_matches_pg() {
assert_eq!(f8(invoke("earth", &[]).unwrap()), 6378168.0);
}
#[test]
fn ll_to_earth_matches_pg() {
assert_eq!(ll(0.0, 0.0).to_text().unwrap(), "(6378168, 0, 0)");
match ll(45.0, 45.0) {
SqlValue::Cube { ll: p, .. } => {
close(p[0], 3189084.000000001, 1e-3);
close(p[1], 3189084.0, 1e-3);
close(p[2], 4510045.844347039, 1e-3);
}
other => panic!("expected cube, got {other:?}"),
}
}
#[test]
fn latitude_longitude_round_trip() {
close(
f8(invoke("latitude", &[ll(45.0, 100.0)]).unwrap()),
45.0,
1e-9,
);
assert_eq!(f8(invoke("longitude", &[ll(45.0, 100.0)]).unwrap()), 100.0);
close(
f8(invoke("latitude", &[ll(-33.8688, 151.2093)]).unwrap()),
-33.8688,
1e-9,
);
close(
f8(invoke("longitude", &[ll(-33.8688, 151.2093)]).unwrap()),
151.2093,
1e-9,
);
close(
f8(invoke("latitude", &[ll(90.0, 0.0)]).unwrap()),
90.0,
1e-9,
);
}
#[test]
fn earth_distance_matches_pg() {
close(
f8(invoke("earth_distance", &[ll(0.0, 0.0), ll(0.0, 180.0)]).unwrap()),
20037605.732161503,
1e-3,
);
close(
f8(invoke(
"earth_distance",
&[ll(51.5074, -0.1278), ll(40.7128, -74.0060)],
)
.unwrap()),
5576489.226133242,
1e-3,
);
close(
f8(invoke(
"earth_distance",
&[ll(48.8566, 2.3522), ll(52.5200, 13.4050)],
)
.unwrap()),
878450.5582390272,
1e-3,
);
assert_eq!(
f8(invoke("earth_distance", &[ll(10.0, 10.0), ll(10.0, 10.0)]).unwrap()),
0.0
);
}
#[test]
fn sec_gc_conversions_match_pg() {
assert_eq!(f8(invoke("sec_to_gc", &[f(0.0)]).unwrap()), 0.0);
close(
f8(invoke("sec_to_gc", &[f(2.0 * 6378168.0 + 1.0)]).unwrap()),
20037605.732161503,
1e-3,
);
close(
f8(invoke("sec_to_gc", &[f(1_000_000.0)]).unwrap()),
1001027.0713076061,
1e-3,
);
assert_eq!(f8(invoke("gc_to_sec", &[f(0.0)]).unwrap()), 0.0);
assert_eq!(
f8(invoke("gc_to_sec", &[f(std::f64::consts::PI * 6378168.0 + 1.0)]).unwrap()),
12756336.0
);
close(
f8(invoke("gc_to_sec", &[f(1_000_000.0)]).unwrap()),
998976.0861827147,
1e-3,
);
}
#[test]
fn earth_box_bounds_radius_searches() {
let bx = invoke("earth_box", &[ll(0.0, 0.0), f(1000.0)]).unwrap();
let near = ll(0.001, 0.001);
let far = ll(1.0, 1.0);
let contains = |a: &SqlValue, b: &SqlValue| {
super::super::cube::operator("@>", a, b)
.unwrap()
.to_text()
.unwrap()
};
assert_eq!(contains(&bx, &near), "t");
assert_eq!(contains(&bx, &far), "f");
}
#[test]
fn null_arguments_yield_null() {
assert!(invoke("sec_to_gc", &[SqlValue::Null]).unwrap().is_null());
assert!(
invoke("ll_to_earth", &[f(1.0), SqlValue::Null])
.unwrap()
.is_null()
);
assert!(
invoke("earth_distance", &[SqlValue::Null, ll(0.0, 0.0)])
.unwrap()
.is_null()
);
}
#[test]
fn every_registered_function_is_routed() {
for name in DEF.functions {
let args = match *name {
"earth" => vec![],
"sec_to_gc" | "gc_to_sec" => vec![f(1.0)],
"ll_to_earth" => vec![f(1.0), f(2.0)],
"earth_distance" => vec![ll(0.0, 0.0), ll(1.0, 1.0)],
"earth_box" => vec![ll(0.0, 0.0), f(1000.0)],
_ => vec![ll(0.0, 0.0)],
};
assert!(invoke(name, &args).is_ok(), "{name} not routed");
}
}
#[test]
fn requires_cube_in_the_registry() {
assert_eq!(DEF.requires, ["cube"]);
}
#[test]
fn accepts_text_cube_arguments() {
let v = SqlValue::Text("(6378168, 0, 0)".into());
close(f8(invoke("latitude", &[v]).unwrap()), 0.0, 1e-12);
let bad = SqlValue::from_text("a=>1", &SqlType::HStore).unwrap();
assert!(invoke("latitude", &[bad]).is_err());
}
}