use geo::Haversine;
use geo::algorithm::line_measures::Distance;
#[cfg(feature = "spheroid")]
use geo::algorithm::line_measures::Length;
#[cfg(feature = "spheroid")]
use geo::{Geodesic, GeodesicMeasure};
use geo_types::{Geometry, Point};
use crate::error::{Error, Result};
use crate::geom::{self, Geom};
fn as_point(g: &Geom, func: &'static str) -> Result<Point<f64>> {
match g.geometry {
Geometry::Point(p) => Ok(p),
_ => Err(Error::Unsupported {
func,
reason: "only POINT arguments are supported in kenro (PostGIS accepts any geometry)"
.into(),
}),
}
}
fn decode_pair(func: &'static str, a: &[u8], b: &[u8]) -> Result<(Point<f64>, Point<f64>)> {
let (ga, gb) = (geom::decode_auto(a)?, geom::decode_auto(b)?);
if ga.srid > 0 && gb.srid > 0 && ga.srid != gb.srid {
return Err(Error::MixedSrid {
func,
a: ga.srid,
b: gb.srid,
});
}
Ok((as_point(&ga, func)?, as_point(&gb, func)?))
}
pub fn st_distance_sphere(a: &[u8], b: &[u8]) -> Result<f64> {
let (pa, pb) = decode_pair("ST_DistanceSphere", a, b)?;
Ok(Haversine.distance(pa, pb))
}
#[cfg(feature = "spheroid")]
pub fn st_distance_spheroid(a: &[u8], b: &[u8]) -> Result<f64> {
let (pa, pb) = decode_pair("ST_DistanceSpheroid", a, b)?;
Ok(Geodesic.distance(pa, pb))
}
#[cfg(feature = "spheroid")]
pub fn st_distance_spheroid_on(a: &[u8], b: &[u8], spheroid: &str) -> Result<f64> {
let (pa, pb) = decode_pair("ST_DistanceSpheroid", a, b)?;
Ok(measure_from(parse_spheroid("ST_DistanceSpheroid", spheroid)?).distance(pa, pb))
}
#[cfg(feature = "spheroid")]
pub fn st_length_spheroid(bytes: &[u8], spheroid: &str) -> Result<f64> {
const FUNC: &str = "ST_LengthSpheroid";
let g = geom::decode_auto(bytes)?;
let measure = measure_from(parse_spheroid(FUNC, spheroid)?);
let rings = |p: &geo_types::Polygon<f64>| {
measure.length(p.exterior()) + p.interiors().iter().map(|r| measure.length(r)).sum::<f64>()
};
Ok(match &g.geometry {
Geometry::LineString(l) => measure.length(l),
Geometry::MultiLineString(mls) => mls.iter().map(|l| measure.length(l)).sum(),
Geometry::Polygon(p) => rings(p),
Geometry::MultiPolygon(mp) => mp.iter().map(rings).sum(),
Geometry::Point(_) | Geometry::MultiPoint(_) => 0.0,
_ => {
return Err(Error::Unsupported {
func: FUNC,
reason: "unsupported geometry type".into(),
});
}
})
}
#[cfg(feature = "spheroid")]
fn measure_from(
(semi_major, inv_flattening): (f64, f64),
) -> impl Distance<f64, Point<f64>, Point<f64>> {
GeodesicMeasure::new(semi_major, 1.0 / inv_flattening)
}
#[cfg(feature = "spheroid")]
fn parse_spheroid(func: &'static str, text: &str) -> Result<(f64, f64)> {
let inner = text
.trim()
.strip_prefix("SPHEROID")
.and_then(|s| s.trim().strip_prefix('['))
.and_then(|s| s.trim_end().strip_suffix(']'))
.ok_or_else(|| Error::Unsupported {
func,
reason: format!("expected SPHEROID[\"name\",a,1/f], got {text:?}"),
})?;
let mut parts = inner.rsplit(',');
let inv_f = parse_number(func, text, parts.next())?;
let semi_major = parse_number(func, text, parts.next())?;
if semi_major <= 0.0 || inv_f <= 0.0 {
return Err(Error::Unsupported {
func,
reason: format!("spheroid parameters must be positive, got {text:?}"),
});
}
Ok((semi_major, inv_f))
}
#[cfg(feature = "spheroid")]
fn parse_number(func: &'static str, whole: &str, part: Option<&str>) -> Result<f64> {
part.and_then(|p| p.trim().parse::<f64>().ok())
.ok_or_else(|| Error::Unsupported {
func,
reason: format!("expected SPHEROID[\"name\",a,1/f], got {whole:?}"),
})
}
pub fn st_project(bytes: &[u8], distance: f64, azimuth: f64) -> Result<Vec<u8>> {
const FUNC: &str = "ST_Project";
let g = geom::decode_auto(bytes)?;
let p = as_point(&g, FUNC)?;
let moved = Point::new(
p.x() + distance * azimuth.sin(),
p.y() + distance * azimuth.cos(),
);
if let Some(z) = crate::functions::threed::st_z(bytes)? {
let index = crate::coords::ZIndex::at(moved.x(), moved.y(), z);
let wkb = crate::coords::write_wkb_z(&Geometry::Point(moved), &index, FUNC)?;
return Ok(crate::gpb::write_gpb(&wkb, g.srid, None, false));
}
geom::encode_canonical_gpb(
&Geom {
geometry: Geometry::Point(moved),
srid: g.srid,
has_zm: false,
},
FUNC,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::functions::io::{st_as_text, st_geom_from_text};
fn g(wkt: &str) -> Vec<u8> {
st_geom_from_text(wkt, Some(4326)).unwrap()
}
#[cfg(feature = "spheroid")]
const WGS84: &str = "SPHEROID[\"WGS 84\",6378137,298.257223563]";
#[test]
#[cfg(feature = "spheroid")]
fn sphere_and_spheroid_match_postgis() {
let (a, b) = (g("POINT(0 0)"), g("POINT(1 0)"));
assert!(
(st_distance_sphere(&a, &b).unwrap() - 111_195.079_734_63).abs() < 1e-3,
"{}",
st_distance_sphere(&a, &b).unwrap()
);
assert!(
(st_distance_spheroid(&a, &b).unwrap() - 111_319.490_793_273_57).abs() < 1e-3,
"{}",
st_distance_spheroid(&a, &b).unwrap()
);
assert!(
(st_distance_spheroid_on(&a, &b, WGS84).unwrap()
- st_distance_spheroid(&a, &b).unwrap())
.abs()
< 1e-9
);
}
#[test]
fn the_planar_functions_answer_in_degrees_which_is_the_point() {
let (a, b) = (g("POINT(0 0)"), g("POINT(1 0)"));
let planar = crate::functions::predicates::st_distance(&a, &b)
.unwrap()
.unwrap();
assert_eq!(planar, 1.0); assert!(st_distance_sphere(&a, &b).unwrap() > 100_000.0); }
#[test]
#[cfg(feature = "spheroid")]
fn length_spheroid_measures_a_degree_of_equator() {
let line = g("LINESTRING(0 0,1 0)");
let len = st_length_spheroid(&line, WGS84).unwrap();
assert!((len - 111_319.490_793_273_57).abs() < 1e-3, "{len}");
assert_eq!(st_length_spheroid(&g("POINT(0 0)"), WGS84).unwrap(), 0.0);
}
#[test]
#[cfg(feature = "spheroid")]
fn spheroid_text_is_parsed_strictly() {
let (a, b) = (g("POINT(0 0)"), g("POINT(1 0)"));
assert!(st_distance_spheroid_on(&a, &b, "WGS 84").is_err());
assert!(st_distance_spheroid_on(&a, &b, "SPHEROID[\"x\",abc,1]").is_err());
assert!(st_distance_spheroid_on(&a, &b, "SPHEROID[\"x\",-1,298]").is_err());
}
#[test]
fn project_is_planar_like_postgis_geometry_overload() {
let moved = st_project(&g("POINT(0 0)"), 100_000.0, std::f64::consts::FRAC_PI_2).unwrap();
let wkt = st_as_text(&moved).unwrap();
assert!(wkt.starts_with("POINT(100000 "), "{wkt}");
}
#[test]
fn non_point_arguments_are_a_loud_error() {
let err = st_distance_sphere(&g("LINESTRING(0 0,1 1)"), &g("POINT(0 0)")).unwrap_err();
assert!(err.to_string().contains("POINT"), "{err}");
}
}