use std::fmt;
use serde::{Deserialize, Serialize};
use crate::geo::{GeoError, Polygon, ewkb};
#[derive(Debug, Clone, PartialEq)]
pub struct MultiPolygon {
pub(crate) polygons: Vec<Polygon>,
}
impl MultiPolygon {
pub fn new(polygons: Vec<Polygon>) -> Result<Self, GeoError> {
if polygons.is_empty() {
return Err(GeoError::InvalidMultiPolygon {
reason: "need at least 1 polygon",
});
}
Ok(Self { polygons })
}
pub fn polygons(&self) -> &[Polygon] {
&self.polygons
}
pub fn to_ewkb_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(ewkb::multipolygon_byte_len(self));
ewkb::encode_multipolygon_into(self, &mut buf);
buf
}
pub fn from_ewkb_bytes(bytes: &[u8]) -> Result<Self, GeoError> {
ewkb::decode_multipolygon(bytes)
}
}
impl fmt::Display for MultiPolygon {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("MULTIPOLYGON(")?;
for (i, poly) in self.polygons.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_str("(")?;
for (ri, ring) in poly.rings().iter().enumerate() {
if ri > 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(")")?;
}
f.write_str(")")
}
}
impl Serialize for MultiPolygon {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.polygons.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for MultiPolygon {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let polygons: Vec<Polygon> = Vec::deserialize(deserializer)?;
MultiPolygon::new(polygons).map_err(serde::de::Error::custom)
}
}
crate::geo::impl_geography_codec!(MultiPolygon, ewkb::encode_multipolygon_into);
#[cfg(all(test, feature = "spatial"))]
mod tests {
use super::*;
use crate::geo::{GeoError, GeoPoint};
fn p(lat: f64, lon: f64) -> GeoPoint {
GeoPoint::new(lat, lon).unwrap()
}
fn square() -> Polygon {
Polygon::with_ring(vec![p(0.0, 0.0), p(0.0, 1.0), p(1.0, 1.0), p(0.0, 0.0)]).unwrap()
}
fn triangle() -> Polygon {
Polygon::with_ring(vec![p(2.0, 0.0), p(2.0, 1.0), p(3.0, 0.5), p(2.0, 0.0)]).unwrap()
}
#[test]
fn new_with_empty_vec_errors() {
assert!(matches!(
MultiPolygon::new(vec![]),
Err(GeoError::InvalidMultiPolygon { .. })
));
}
#[test]
fn new_with_one_polygon_succeeds() {
let mp = MultiPolygon::new(vec![square()]).unwrap();
assert_eq!(mp.polygons().len(), 1);
}
#[test]
fn new_with_multiple_polygons_succeeds() {
let mp = MultiPolygon::new(vec![square(), triangle()]).unwrap();
assert_eq!(mp.polygons().len(), 2);
}
#[test]
fn ewkb_round_trip() {
let mp = MultiPolygon::new(vec![square(), triangle()]).unwrap();
let bytes = mp.to_ewkb_bytes();
let decoded = MultiPolygon::from_ewkb_bytes(&bytes).unwrap();
assert_eq!(decoded.polygons().len(), mp.polygons().len());
for (a, b) in mp.polygons().iter().zip(decoded.polygons().iter()) {
assert_eq!(a.rings().len(), b.rings().len());
for (ra, rb) in a.rings().iter().zip(b.rings().iter()) {
assert_eq!(ra.len(), rb.len());
for (pa, pb) in ra.iter().zip(rb.iter()) {
assert!((pa.lat - pb.lat).abs() < 1e-9);
assert!((pa.lon - pb.lon).abs() < 1e-9);
}
}
}
}
#[test]
fn display_matches_wkt_prefix() {
let mp = MultiPolygon::new(vec![square()]).unwrap();
let s = format!("{mp}");
assert!(s.starts_with("MULTIPOLYGON("));
}
#[test]
fn serde_json_round_trip() {
let original = MultiPolygon::new(vec![square()]).unwrap();
let json = serde_json::to_string(&original).unwrap();
let decoded: MultiPolygon = serde_json::from_str(&json).unwrap();
assert_eq!(original, decoded);
}
#[test]
fn serde_json_rejects_empty_array() {
let json = r#"[]"#;
assert!(serde_json::from_str::<MultiPolygon>(json).is_err());
}
}