use std::fmt;
use serde::{Deserialize, Serialize};
use crate::geo::{GeoError, GeoPoint, ewkb};
#[derive(Debug, Clone, PartialEq)]
pub struct Polygon {
pub(crate) rings: Vec<Vec<GeoPoint>>,
}
fn validate_ring(ring: &[GeoPoint], label: &'static str) -> Result<(), GeoError> {
if ring.len() < 4 {
return Err(GeoError::InvalidPolygon { reason: label });
}
if ring[0] != *ring.last().expect("ring len >= 4") {
return Err(GeoError::InvalidPolygon {
reason: "ring is not closed (first point != last point)",
});
}
Ok(())
}
impl Polygon {
pub fn closed(points: &[GeoPoint]) -> Result<Self, GeoError> {
if points.len() < 3 {
return Err(GeoError::InvalidPolygon {
reason: "need at least 3 distinct vertices",
});
}
let mut ring = points.to_vec();
if ring[0] != *ring.last().expect("len >= 3") {
ring.push(ring[0]);
}
validate_ring(&ring, "outer ring must have >= 4 points after closing")?;
Ok(Self { rings: vec![ring] })
}
pub fn with_ring(points: Vec<GeoPoint>) -> Result<Self, GeoError> {
validate_ring(
&points,
"outer ring needs at least 4 points and must be closed",
)?;
Ok(Self {
rings: vec![points],
})
}
pub fn with_holes(outer: Vec<GeoPoint>, holes: Vec<Vec<GeoPoint>>) -> Result<Self, GeoError> {
let mut rings = Vec::with_capacity(1 + holes.len());
rings.push(outer);
rings.extend(holes);
Self::from_rings(rings)
}
pub fn from_rings(rings: Vec<Vec<GeoPoint>>) -> Result<Self, GeoError> {
if rings.is_empty() {
return Err(GeoError::InvalidPolygon {
reason: "Polygon needs at least one ring",
});
}
validate_ring(
&rings[0],
"outer ring needs at least 4 points and must be closed",
)?;
for hole in &rings[1..] {
validate_ring(hole, "hole ring needs at least 4 points and must be closed")?;
}
Ok(Self { rings })
}
pub fn rings(&self) -> &[Vec<GeoPoint>] {
&self.rings
}
pub fn outer(&self) -> &[GeoPoint] {
&self.rings[0]
}
pub fn holes(&self) -> &[Vec<GeoPoint>] {
&self.rings[1..]
}
pub fn to_ewkb_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(ewkb::polygon_byte_len(self));
ewkb::encode_polygon_into(self, &mut buf);
buf
}
pub fn from_ewkb_bytes(bytes: &[u8]) -> Result<Self, GeoError> {
ewkb::decode_polygon(bytes)
}
}
impl fmt::Display for Polygon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("POLYGON(")?;
for (i, ring) in self.rings.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str("(")?;
for (j, p) in ring.iter().enumerate() {
if j > 0 {
f.write_str(", ")?;
}
write!(f, "{} {}", p.lon, p.lat)?;
}
f.write_str(")")?;
}
f.write_str(")")
}
}
impl Serialize for Polygon {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.rings.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Polygon {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let rings: Vec<Vec<GeoPoint>> = Vec::deserialize(deserializer)?;
Polygon::from_rings(rings).map_err(serde::de::Error::custom)
}
}
crate::geo::impl_geography_codec!(Polygon, ewkb::encode_polygon_into);
#[cfg(all(test, feature = "spatial"))]
mod tests {
use super::*;
use crate::geo::GeoError;
fn p(lat: f64, lon: f64) -> GeoPoint {
GeoPoint::new(lat, lon).unwrap()
}
fn square_ring() -> Vec<GeoPoint> {
vec![p(0.0, 0.0), p(0.0, 1.0), p(1.0, 1.0), p(0.0, 0.0)]
}
fn open_triangle() -> Vec<GeoPoint> {
vec![p(0.0, 0.0), p(0.0, 1.0), p(1.0, 0.5)]
}
#[test]
fn closed_auto_closes_open_ring() {
let poly = Polygon::closed(&open_triangle()).unwrap();
let outer = poly.outer();
assert_eq!(outer.len(), 4, "auto-close must append the first point");
assert_eq!(outer[0], outer[outer.len() - 1], "ring must be closed");
}
#[test]
fn closed_accepts_already_closed_ring() {
let poly = Polygon::closed(&square_ring()).unwrap();
assert_eq!(poly.outer().len(), 4);
}
#[test]
fn closed_rejects_fewer_than_three_points() {
assert!(matches!(
Polygon::closed(&[p(0.0, 0.0), p(1.0, 1.0)]),
Err(GeoError::InvalidPolygon { .. })
));
}
#[test]
fn with_ring_accepts_valid_closed_ring() {
let poly = Polygon::with_ring(square_ring()).unwrap();
assert_eq!(poly.outer().len(), 4);
assert!(poly.holes().is_empty());
}
#[test]
fn with_ring_rejects_unclosed_ring() {
assert!(matches!(
Polygon::with_ring(open_triangle()),
Err(GeoError::InvalidPolygon { .. })
));
}
#[test]
fn with_ring_rejects_too_few_points() {
let short = vec![p(0.0, 0.0), p(1.0, 0.0), p(0.5, 1.0)];
assert!(matches!(
Polygon::with_ring(short),
Err(GeoError::InvalidPolygon { .. })
));
}
#[test]
fn with_holes_accepts_outer_no_holes() {
let poly = Polygon::with_holes(square_ring(), vec![]).unwrap();
assert!(poly.holes().is_empty());
}
#[test]
fn with_holes_accepts_outer_and_one_hole() {
let hole = vec![p(0.1, 0.1), p(0.1, 0.2), p(0.2, 0.2), p(0.1, 0.1)];
let poly = Polygon::with_holes(square_ring(), vec![hole]).unwrap();
assert_eq!(poly.holes().len(), 1);
}
#[test]
fn with_holes_rejects_invalid_hole() {
let bad_hole = vec![p(0.1, 0.1), p(0.2, 0.2)]; assert!(matches!(
Polygon::with_holes(square_ring(), vec![bad_hole]),
Err(GeoError::InvalidPolygon { .. })
));
}
#[test]
fn ewkb_round_trip_no_holes() {
let poly = Polygon::with_ring(square_ring()).unwrap();
let bytes = poly.to_ewkb_bytes();
let decoded = Polygon::from_ewkb_bytes(&bytes).unwrap();
assert_eq!(decoded.rings().len(), 1);
let outer = decoded.outer();
assert_eq!(outer.len(), 4);
for (a, b) in poly.outer().iter().zip(outer.iter()) {
assert!((a.lat - b.lat).abs() < 1e-9);
assert!((a.lon - b.lon).abs() < 1e-9);
}
}
#[test]
fn ewkb_round_trip_with_hole() {
let hole = vec![p(0.1, 0.1), p(0.1, 0.2), p(0.2, 0.2), p(0.1, 0.1)];
let poly = Polygon::with_holes(square_ring(), vec![hole]).unwrap();
let bytes = poly.to_ewkb_bytes();
let decoded = Polygon::from_ewkb_bytes(&bytes).unwrap();
assert_eq!(decoded.rings().len(), 2);
assert_eq!(decoded.holes().len(), 1);
}
#[test]
fn display_matches_wkt_no_holes() {
let poly = Polygon::with_ring(square_ring()).unwrap();
let s = format!("{poly}");
assert!(s.starts_with("POLYGON("));
assert!(s.contains("0 0"));
}
#[test]
fn serde_json_round_trip() {
let poly = Polygon::with_ring(square_ring()).unwrap();
let json = serde_json::to_string(&poly).unwrap();
let decoded: Polygon = serde_json::from_str(&json).unwrap();
assert_eq!(poly, decoded);
}
}