use crate::error::PostgisError;
use serde::{Deserialize, Serialize};
pub const DEFAULT_SRID: i32 = 4326;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Point {
pub x: f64,
pub y: f64,
pub srid: i32,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Self {
x,
y,
srid: DEFAULT_SRID,
}
}
pub fn with_srid(x: f64, y: f64, srid: i32) -> Self {
Self { x, y, srid }
}
pub fn euclidean_distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
pub fn haversine_distance(&self, other: &Point) -> f64 {
const EARTH_RADIUS_M: f64 = 6_371_000.0;
let to_rad = |deg: f64| deg * std::f64::consts::PI / 180.0;
let lat1 = to_rad(self.y);
let lat2 = to_rad(other.y);
let dlat = to_rad(other.y - self.y);
let dlon = to_rad(other.x - self.x);
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().asin();
EARTH_RADIUS_M * c
}
pub fn to_ewkt(&self) -> String {
format!("SRID={};POINT({} {})", self.srid, self.x, self.y)
}
pub fn to_wkt(&self) -> String {
format!("POINT({} {})", self.x, self.y)
}
pub fn midpoint(&self, other: &Point) -> Point {
Point::with_srid(
(self.x + other.x) / 2.0,
(self.y + other.y) / 2.0,
self.srid,
)
}
pub fn bearing(&self, other: &Point) -> f64 {
let to_rad = |deg: f64| deg * std::f64::consts::PI / 180.0;
let to_deg = |rad: f64| rad * 180.0 / std::f64::consts::PI;
let lat1 = to_rad(self.y);
let lat2 = to_rad(other.y);
let dlon = to_rad(other.x - self.x);
let y = dlon.sin() * lat2.cos();
let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
let bearing = to_deg(y.atan2(x));
(bearing + 360.0) % 360.0
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LineString {
pub points: Vec<Point>,
pub srid: i32,
}
impl LineString {
pub fn new(points: Vec<Point>) -> Self {
let srid = points.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID);
Self { points, srid }
}
pub fn euclidean_length(&self) -> f64 {
self.points
.windows(2)
.map(|w| w[0].euclidean_distance(&w[1]))
.sum()
}
pub fn haversine_length(&self) -> f64 {
self.points
.windows(2)
.map(|w| w[0].haversine_distance(&w[1]))
.sum()
}
pub fn to_ewkt(&self) -> String {
let coords: Vec<String> = self
.points
.iter()
.map(|p| format!("{} {}", p.x, p.y))
.collect();
format!("SRID={};LINESTRING({})", self.srid, coords.join(", "))
}
pub fn to_wkt(&self) -> String {
let coords: Vec<String> = self
.points
.iter()
.map(|p| format!("{} {}", p.x, p.y))
.collect();
format!("LINESTRING({})", coords.join(", "))
}
pub fn point_count(&self) -> usize {
self.points.len()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Polygon {
pub rings: Vec<Vec<Point>>,
pub srid: i32,
}
impl Polygon {
pub fn new(outer: Vec<Point>) -> Self {
let srid = outer.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID);
Self {
rings: vec![outer],
srid,
}
}
pub fn with_holes(outer: Vec<Point>, holes: Vec<Vec<Point>>) -> Self {
let srid = outer.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID);
let mut rings = vec![outer];
rings.extend(holes);
Self { rings, srid }
}
pub fn shoelace_area(&self) -> f64 {
if self.rings.is_empty() {
return 0.0;
}
let outer = &self.rings[0];
if outer.len() < 3 {
return 0.0;
}
let mut sum = 0.0;
for i in 0..outer.len() {
let j = (i + 1) % outer.len();
sum += outer[i].x * outer[j].y;
sum -= outer[j].x * outer[i].y;
}
(sum / 2.0).abs()
}
pub fn contains_point(&self, point: &Point) -> bool {
if self.rings.is_empty() {
return false;
}
let outer = &self.rings[0];
let mut inside = false;
let mut j = outer.len() - 1;
for i in 0..outer.len() {
let intersect = (outer[i].y > point.y) != (outer[j].y > point.y)
&& point.x
< (outer[j].x - outer[i].x) * (point.y - outer[i].y)
/ (outer[j].y - outer[i].y)
+ outer[i].x;
if intersect {
inside = !inside;
}
j = i;
}
if inside {
for hole in self.rings.iter().skip(1) {
let mut hole_inside = false;
let mut j = hole.len() - 1;
for i in 0..hole.len() {
let intersect = (hole[i].y > point.y) != (hole[j].y > point.y)
&& point.x
< (hole[j].x - hole[i].x) * (point.y - hole[i].y)
/ (hole[j].y - hole[i].y)
+ hole[i].x;
if intersect {
hole_inside = !hole_inside;
}
j = i;
}
if hole_inside {
return false;
}
}
}
inside
}
pub fn to_ewkt(&self) -> String {
let rings: Vec<String> = self
.rings
.iter()
.map(|ring| {
let coords: Vec<String> = ring.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("({})", coords.join(", "))
})
.collect();
format!("SRID={};POLYGON({})", self.srid, rings.join(", "))
}
pub fn to_wkt(&self) -> String {
let rings: Vec<String> = self
.rings
.iter()
.map(|ring| {
let coords: Vec<String> = ring.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("({})", coords.join(", "))
})
.collect();
format!("POLYGON({})", rings.join(", "))
}
pub fn perimeter(&self) -> f64 {
if self.rings.is_empty() {
return 0.0;
}
let outer = &self.rings[0];
if outer.len() < 2 {
return 0.0;
}
let mut perim = 0.0;
for i in 0..outer.len() {
let j = (i + 1) % outer.len();
perim += outer[i].euclidean_distance(&outer[j]);
}
perim
}
pub fn ring_count(&self) -> usize {
self.rings.len()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Geometry {
Point(Point),
LineString(LineString),
Polygon(Polygon),
MultiPoint(Vec<Point>),
MultiLineString(Vec<LineString>),
MultiPolygon(Vec<Polygon>),
}
impl Geometry {
pub fn srid(&self) -> i32 {
match self {
Geometry::Point(p) => p.srid,
Geometry::LineString(ls) => ls.srid,
Geometry::Polygon(poly) => poly.srid,
Geometry::MultiPoint(pts) => pts.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID),
Geometry::MultiLineString(lss) => lss.first().map(|ls| ls.srid).unwrap_or(DEFAULT_SRID),
Geometry::MultiPolygon(polys) => polys.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID),
}
}
pub fn validate_srid(&self) -> Result<(), PostgisError> {
let expected = self.srid();
let check = |srid: i32| -> Result<(), PostgisError> {
if srid != expected {
Err(PostgisError::SridMismatch {
expected,
actual: srid,
})
} else {
Ok(())
}
};
match self {
Geometry::MultiPoint(pts) => {
for p in pts {
check(p.srid)?;
}
}
Geometry::MultiLineString(lss) => {
for ls in lss {
check(ls.srid)?;
}
}
Geometry::MultiPolygon(polys) => {
for p in polys {
check(p.srid)?;
}
}
_ => {}
}
Ok(())
}
pub fn type_name(&self) -> &'static str {
match self {
Geometry::Point(_) => "Point",
Geometry::LineString(_) => "LineString",
Geometry::Polygon(_) => "Polygon",
Geometry::MultiPoint(_) => "MultiPoint",
Geometry::MultiLineString(_) => "MultiLineString",
Geometry::MultiPolygon(_) => "MultiPolygon",
}
}
pub fn to_wkt(&self) -> String {
match self {
Geometry::Point(p) => p.to_wkt(),
Geometry::LineString(ls) => ls.to_wkt(),
Geometry::Polygon(poly) => poly.to_wkt(),
Geometry::MultiPoint(pts) => {
let coords: Vec<String> = pts.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("MULTIPOINT({})", coords.join(", "))
}
Geometry::MultiLineString(lss) => {
let lines: Vec<String> = lss
.iter()
.map(|ls| {
let coords: Vec<String> = ls
.points
.iter()
.map(|p| format!("{} {}", p.x, p.y))
.collect();
format!("({})", coords.join(", "))
})
.collect();
format!("MULTILINESTRING({})", lines.join(", "))
}
Geometry::MultiPolygon(polys) => {
let polygons: Vec<String> = polys
.iter()
.map(|poly| {
let rings: Vec<String> = poly
.rings
.iter()
.map(|ring| {
let coords: Vec<String> =
ring.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("({})", coords.join(", "))
})
.collect();
format!("({})", rings.join(", "))
})
.collect();
format!("MULTIPOLYGON({})", polygons.join(", "))
}
}
}
pub fn bounding_box(&self) -> Option<(f64, f64, f64, f64)> {
let points: Vec<&Point> = match self {
Geometry::Point(p) => vec![p],
Geometry::LineString(ls) => ls.points.iter().collect(),
Geometry::Polygon(poly) => poly.rings.iter().flatten().collect(),
Geometry::MultiPoint(pts) => pts.iter().collect(),
Geometry::MultiLineString(lss) => lss.iter().flat_map(|ls| ls.points.iter()).collect(),
Geometry::MultiPolygon(polys) => polys
.iter()
.flat_map(|p| p.rings.iter().flatten())
.collect(),
};
if points.is_empty() {
return None;
}
let mut min_x = points[0].x;
let mut min_y = points[0].y;
let mut max_x = points[0].x;
let mut max_y = points[0].y;
for p in &points[1..] {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
Some((min_x, min_y, max_x, max_y))
}
pub fn from_ewkt(ewkt: &str) -> Result<Self, PostgisError> {
let (srid, wkt) = if let Some(semi) = ewkt.find(';') {
let srid_str = &ewkt[..semi];
let wkt = &ewkt[semi + 1..];
if !srid_str.starts_with("SRID=") {
return Err(PostgisError::Query(format!(
"invalid EWKT SRID prefix: {}",
srid_str
)));
}
let srid: i32 = srid_str[5..]
.parse()
.map_err(|e| PostgisError::Query(format!("invalid SRID: {}", e)))?;
(srid, wkt)
} else {
(DEFAULT_SRID, ewkt)
};
let wkt = wkt.trim();
let upper = wkt.to_uppercase();
if upper.starts_with("POINT") {
let coords = extract_paren_content(&upper, "POINT")?;
let nums = parse_coord_pair(&coords)?;
Ok(Geometry::Point(Point::with_srid(nums.0, nums.1, srid)))
} else if upper.starts_with("LINESTRING") {
let coords = extract_paren_content(&upper, "LINESTRING")?;
let points = parse_coord_list(&coords)?
.into_iter()
.map(|(x, y)| Point::with_srid(x, y, srid))
.collect();
Ok(Geometry::LineString(LineString { points, srid }))
} else if upper.starts_with("POLYGON") {
let rings_str = extract_paren_content(&upper, "POLYGON")?;
let rings = parse_polygon_rings(&rings_str, srid)?;
Ok(Geometry::Polygon(Polygon { rings, srid }))
} else if upper.starts_with("MULTIPOINT") {
let coords = extract_paren_content(&upper, "MULTIPOINT")?;
let points = parse_coord_list(&coords)?
.into_iter()
.map(|(x, y)| Point::with_srid(x, y, srid))
.collect();
Ok(Geometry::MultiPoint(points))
} else {
Err(PostgisError::Query(format!(
"unsupported WKT type in: {}",
wkt
)))
}
}
pub fn to_ewkt(&self) -> String {
match self {
Geometry::Point(p) => p.to_ewkt(),
Geometry::LineString(ls) => ls.to_ewkt(),
Geometry::Polygon(poly) => poly.to_ewkt(),
Geometry::MultiPoint(pts) => {
let srid = pts.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID);
let coords: Vec<String> = pts.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("SRID={};MULTIPOINT({})", srid, coords.join(", "))
}
Geometry::MultiLineString(lss) => {
let srid = lss.first().map(|ls| ls.srid).unwrap_or(DEFAULT_SRID);
let lines: Vec<String> = lss
.iter()
.map(|ls| {
let coords: Vec<String> = ls
.points
.iter()
.map(|p| format!("{} {}", p.x, p.y))
.collect();
format!("({})", coords.join(", "))
})
.collect();
format!("SRID={};MULTILINESTRING({})", srid, lines.join(", "))
}
Geometry::MultiPolygon(polys) => {
let srid = polys.first().map(|p| p.srid).unwrap_or(DEFAULT_SRID);
let polygons: Vec<String> = polys
.iter()
.map(|poly| {
let rings: Vec<String> = poly
.rings
.iter()
.map(|ring| {
let coords: Vec<String> =
ring.iter().map(|p| format!("{} {}", p.x, p.y)).collect();
format!("({})", coords.join(", "))
})
.collect();
format!("({})", rings.join(", "))
})
.collect();
format!("SRID={};MULTIPOLYGON({})", srid, polygons.join(", "))
}
}
}
}
fn extract_paren_content(upper_wkt: &str, type_name: &str) -> Result<String, PostgisError> {
let start = upper_wkt
.find(type_name)
.ok_or_else(|| PostgisError::Query(format!("missing type name {}", type_name)))?
+ type_name.len();
let rest = &upper_wkt[start..];
let rest = rest.trim_start();
if !rest.starts_with('(') {
return Err(PostgisError::Query(format!(
"missing opening paren after {}: {}",
type_name, rest
)));
}
let mut depth = 0i32;
let mut end = 0usize;
for (i, c) in rest.chars().enumerate() {
match c {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
end = i;
break;
}
}
_ => {}
}
}
if depth != 0 {
return Err(PostgisError::Query(format!(
"unbalanced parens in {}: {}",
type_name, rest
)));
}
Ok(rest[1..end].to_string())
}
fn parse_coord_pair(s: &str) -> Result<(f64, f64), PostgisError> {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 2 {
return Err(PostgisError::Query(format!(
"expected 2 coords, got {}: {}",
parts.len(),
s
)));
}
let x: f64 = parts[0]
.parse()
.map_err(|e| PostgisError::Query(format!("invalid x coord: {}", e)))?;
let y: f64 = parts[1]
.parse()
.map_err(|e| PostgisError::Query(format!("invalid y coord: {}", e)))?;
Ok((x, y))
}
fn parse_coord_list(s: &str) -> Result<Vec<(f64, f64)>, PostgisError> {
s.split(',')
.map(|pair| parse_coord_pair(pair.trim()))
.collect()
}
fn parse_polygon_rings(s: &str, srid: i32) -> Result<Vec<Vec<Point>>, PostgisError> {
let mut rings = Vec::new();
let mut depth = 0i32;
let mut current = String::new();
for c in s.chars() {
match c {
'(' => {
depth += 1;
if depth == 1 {
current.clear();
} else {
current.push(c);
}
}
')' => {
depth -= 1;
if depth == 0 {
let ring: Vec<Point> = parse_coord_list(¤t)?
.into_iter()
.map(|(x, y)| Point::with_srid(x, y, srid))
.collect();
rings.push(ring);
} else {
current.push(c);
}
}
_ if depth >= 1 => {
current.push(c);
}
_ => {} }
}
if rings.is_empty() {
return Err(PostgisError::Query(format!("no rings parsed from: {}", s)));
}
Ok(rings)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point_ewkt() {
let p = Point::new(116.404, 39.915);
assert_eq!(p.to_ewkt(), "SRID=4326;POINT(116.404 39.915)");
}
#[test]
fn test_point_euclidean_distance() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(3.0, 4.0);
let dist = p1.euclidean_distance(&p2);
assert!((dist - 5.0).abs() < 1e-10);
}
#[test]
fn test_point_haversine_distance() {
let beijing = Point::new(116.404, 39.915);
let shanghai = Point::new(121.474, 31.230);
let dist = beijing.haversine_distance(&shanghai);
assert!(dist > 1_000_000.0 && dist < 1_200_000.0);
}
#[test]
fn test_linestring_length() {
let ls = LineString::new(vec![
Point::new(0.0, 0.0),
Point::new(3.0, 4.0),
Point::new(3.0, 9.0),
]);
let len = ls.euclidean_length();
assert!((len - 10.0).abs() < 1e-10);
}
#[test]
fn test_polygon_area() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 3.0),
Point::new(0.0, 3.0),
]);
let area = poly.shoelace_area();
assert!((area - 12.0).abs() < 1e-10);
}
#[test]
fn test_polygon_contains_point() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 3.0),
Point::new(0.0, 3.0),
]);
assert!(poly.contains_point(&Point::new(2.0, 1.5)));
assert!(!poly.contains_point(&Point::new(5.0, 1.5)));
}
#[test]
fn test_polygon_with_hole() {
let outer = vec![
Point::new(0.0, 0.0),
Point::new(10.0, 0.0),
Point::new(10.0, 10.0),
Point::new(0.0, 10.0),
];
let hole = vec![
Point::new(3.0, 3.0),
Point::new(7.0, 3.0),
Point::new(7.0, 7.0),
Point::new(3.0, 7.0),
];
let poly = Polygon::with_holes(outer, vec![hole]);
assert!(poly.contains_point(&Point::new(1.0, 1.0)));
assert!(!poly.contains_point(&Point::new(5.0, 5.0)));
}
#[test]
fn test_geometry_srid_validation() {
let g = Geometry::MultiPoint(vec![
Point::with_srid(0.0, 0.0, 4326),
Point::with_srid(1.0, 1.0, 3857),
]);
assert!(matches!(
g.validate_srid(),
Err(PostgisError::SridMismatch {
expected: 4326,
actual: 3857
})
));
}
#[test]
fn test_geometry_ewkt() {
let g = Geometry::Point(Point::new(116.404, 39.915));
assert_eq!(g.to_ewkt(), "SRID=4326;POINT(116.404 39.915)");
assert_eq!(g.type_name(), "Point");
}
#[test]
fn test_point_wkt() {
let p = Point::new(1.0, 2.0);
assert_eq!(p.to_wkt(), "POINT(1 2)");
}
#[test]
fn test_point_midpoint() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(4.0, 6.0);
let mid = p1.midpoint(&p2);
assert!((mid.x - 2.0).abs() < 1e-10);
assert!((mid.y - 3.0).abs() < 1e-10);
}
#[test]
fn test_point_bearing_north() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(0.0, 1.0);
let bearing = p1.bearing(&p2);
assert!((bearing - 0.0).abs() < 1e-6);
}
#[test]
fn test_point_bearing_east() {
let p1 = Point::new(0.0, 0.0);
let p2 = Point::new(1.0, 0.0);
let bearing = p1.bearing(&p2);
assert!((bearing - 90.0).abs() < 1e-6);
}
#[test]
fn test_linestring_wkt() {
let ls = LineString::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]);
assert_eq!(ls.to_wkt(), "LINESTRING(0 0, 1 1)");
}
#[test]
fn test_linestring_point_count() {
let ls = LineString::new(vec![Point::new(0.0, 0.0), Point::new(1.0, 1.0)]);
assert_eq!(ls.point_count(), 2);
}
#[test]
fn test_polygon_wkt() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 3.0),
Point::new(0.0, 3.0),
]);
assert!(poly.to_wkt().starts_with("POLYGON("));
}
#[test]
fn test_polygon_perimeter() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 3.0),
Point::new(0.0, 3.0),
]);
let perim = poly.perimeter();
assert!((perim - 14.0).abs() < 1e-10);
}
#[test]
fn test_polygon_ring_count() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 0.0),
Point::new(0.0, 1.0),
]);
assert_eq!(poly.ring_count(), 1);
}
#[test]
fn test_geometry_to_wkt_point() {
let g = Geometry::Point(Point::new(1.0, 2.0));
assert_eq!(g.to_wkt(), "POINT(1 2)");
}
#[test]
fn test_geometry_to_wkt_linestring() {
let g = Geometry::LineString(LineString::new(vec![
Point::new(0.0, 0.0),
Point::new(1.0, 1.0),
]));
assert_eq!(g.to_wkt(), "LINESTRING(0 0, 1 1)");
}
#[test]
fn test_geometry_bounding_box_point() {
let g = Geometry::Point(Point::new(3.0, 5.0));
let bb = g.bounding_box().unwrap();
assert_eq!(bb, (3.0, 5.0, 3.0, 5.0));
}
#[test]
fn test_geometry_bounding_box_polygon() {
let poly = Polygon::new(vec![
Point::new(0.0, 0.0),
Point::new(4.0, 0.0),
Point::new(4.0, 3.0),
Point::new(0.0, 3.0),
]);
let g = Geometry::Polygon(poly);
let bb = g.bounding_box().unwrap();
assert_eq!(bb, (0.0, 0.0, 4.0, 3.0));
}
#[test]
fn test_geometry_bounding_box_empty() {
let g = Geometry::MultiPoint(vec![]);
assert!(g.bounding_box().is_none());
}
}