use crate::core::surface::Surface;
use crate::foundation::{BBox, GeoError, GridGeometry, HasHistory, OperationHistory, Result};
use crate::io::PolygonData;
use geo::prelude::*;
use geo::{Coord, LineString, Point, Polygon};
use indexmap::IndexMap;
use ndarray::Array2;
use std::path::Path;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PolygonSet {
polys: Vec<Polygon<f64>>,
#[serde(default)]
attrs: IndexMap<String, Vec<f64>>,
#[serde(default)]
history: OperationHistory,
}
impl PolygonSet {
pub fn from_rings(rings: Vec<Vec<[f64; 3]>>) -> PolygonSet {
let polys = rings
.into_iter()
.filter(|r| r.len() >= 3)
.map(|r| {
let coords: Vec<Coord<f64>> =
r.iter().map(|c| Coord { x: c[0], y: c[1] }).collect();
Polygon::new(LineString::new(coords), Vec::new())
})
.collect();
PolygonSet {
polys,
attrs: IndexMap::new(),
history: OperationHistory::from_entry("polygons.from_rings"),
}
}
pub(crate) fn from_polygon_data(data: PolygonData) -> PolygonSet {
PolygonSet::from_rings(data.into_rings())
}
pub fn from_grid_geometry(geom: &GridGeometry) -> PolygonSet {
let ni = geom.ncol.saturating_sub(1);
let nj = geom.nrow.saturating_sub(1);
let corners = [
geom.node_xy(0, 0),
geom.node_xy(ni, 0),
geom.node_xy(ni, nj),
geom.node_xy(0, nj),
];
let ring = corners
.into_iter()
.map(|(x, y)| [x, y, 0.0])
.collect::<Vec<_>>();
let mut out = PolygonSet::from_rings(vec![ring]);
out.history = OperationHistory::from_entry("polygons.from_grid_geometry");
out
}
pub fn convex_hull_xy(points: Vec<[f64; 2]>) -> Option<PolygonSet> {
let hull = convex_hull(points);
if hull.len() < 3 {
return None;
}
let ring = hull.into_iter().map(|p| [p[0], p[1], 0.0]).collect();
let mut out = PolygonSet::from_rings(vec![ring]);
out.history = OperationHistory::from_entry("polygons.convex_hull_xy");
Some(out)
}
pub fn load_geojson(path: impl AsRef<Path>) -> Result<PolygonSet> {
let data = crate::io::vector::load_polygon_rings_geojson(path.as_ref())?;
let mut out = PolygonSet::from_polygon_data(data);
out.history = OperationHistory::from_entry(format!(
"polygons.load_geojson(path={})",
path.as_ref().display()
));
Ok(out)
}
pub fn load_irap_polygons(path: impl AsRef<Path>) -> Result<PolygonSet> {
let data = crate::io::xyz::load_polygons(path.as_ref())?;
let mut out = PolygonSet::from_polygon_data(data);
out.history = OperationHistory::from_entry(format!(
"polygons.load_irap_polygons(path={})",
path.as_ref().display()
));
Ok(out)
}
pub fn load_cps3_lines(path: impl AsRef<Path>) -> Result<PolygonSet> {
let data = crate::io::cps3::load_cps3_lines(path.as_ref())?;
let mut out = PolygonSet::from_polygon_data(data);
out.history = OperationHistory::from_entry(format!(
"polygons.load_cps3_lines(path={})",
path.as_ref().display()
));
Ok(out)
}
pub fn load_shapefile(path: impl AsRef<Path>) -> Result<PolygonSet> {
let data = crate::io::vector::load_polygon_rings_shapefile(path.as_ref())?;
let mut out = PolygonSet::from_polygon_data(data);
out.history = OperationHistory::from_entry(format!(
"polygons.load_shapefile(path={})",
path.as_ref().display()
));
Ok(out)
}
pub fn contains(&self, x: f64, y: f64) -> bool {
let p = Point::new(x, y);
self.polys.iter().any(|poly| poly.contains(&p))
}
pub fn len(&self) -> usize {
self.polys.len()
}
pub fn is_empty(&self) -> bool {
self.polys.is_empty()
}
pub fn area(&self) -> f64 {
self.polys.iter().map(|p| p.unsigned_area()).sum()
}
pub fn area_values(&self) -> Vec<f64> {
self.polys.iter().map(|p| p.unsigned_area()).collect()
}
pub fn attr(&self, name: &str) -> Option<&[f64]> {
self.attrs.get(name).map(Vec::as_slice)
}
pub fn set_attr(&mut self, name: &str, values: Vec<f64>) -> Result<()> {
if values.len() != self.polys.len() {
return Err(GeoError::Parse(format!(
"polygon attribute '{name}' has {} rows, expected {}",
values.len(),
self.polys.len()
)));
}
self.attrs.insert(name.to_string(), values);
self.record_history(format!("polygons.set_attr(name={name})"));
Ok(())
}
pub fn attr_names(&self) -> Vec<&str> {
self.attrs.keys().map(String::as_str).collect()
}
pub fn history(&self) -> &[String] {
self.history.entries()
}
pub(crate) fn record_history(&mut self, entry: impl Into<String>) {
self.history.push(entry.into());
}
pub fn rings(&self) -> Vec<Vec<[f64; 3]>> {
self.polys
.iter()
.map(|p| p.exterior().coords().map(|c| [c.x, c.y, 0.0]).collect())
.collect()
}
pub fn bbox(&self) -> BBox {
let mut b = BBox {
xmin: f64::INFINITY,
ymin: f64::INFINITY,
xmax: f64::NEG_INFINITY,
ymax: f64::NEG_INFINITY,
};
let mut any = false;
for poly in &self.polys {
if let Some(rect) = poly.bounding_rect() {
any = true;
b.xmin = b.xmin.min(rect.min().x);
b.ymin = b.ymin.min(rect.min().y);
b.xmax = b.xmax.max(rect.max().x);
b.ymax = b.ymax.max(rect.max().y);
}
}
if any {
b
} else {
BBox {
xmin: f64::NAN,
ymin: f64::NAN,
xmax: f64::NAN,
ymax: f64::NAN,
}
}
}
pub fn clip(&self, surface: &Surface) -> Surface {
let geom = &surface.geom;
let src = surface.values();
let mut out = Array2::from_elem((geom.ncol, geom.nrow), f64::NAN);
for j in 0..geom.nrow {
for i in 0..geom.ncol {
let (x, y) = geom.node_xy(i, j);
if self.contains(x, y) {
out[[i, j]] = src[[i, j]];
}
}
}
let mut clipped = Surface::from_values_unchecked(geom.clone(), out);
let mut history = surface.operation_history().clone();
history.extend_prefixed("mask", &self.history);
history.push("polygons.clip(surface)".to_string());
clipped.set_history(history);
clipped
}
}
impl HasHistory for PolygonSet {
fn operation_history(&self) -> &OperationHistory {
&self.history
}
fn operation_history_mut(&mut self) -> &mut OperationHistory {
&mut self.history
}
}
pub(crate) fn convex_hull(mut pts: Vec<[f64; 2]>) -> Vec<[f64; 2]> {
pts.sort_by(|a, b| a[0].total_cmp(&b[0]).then(a[1].total_cmp(&b[1])));
pts.dedup();
if pts.len() < 3 {
return pts;
}
let cross = |o: [f64; 2], a: [f64; 2], b: [f64; 2]| {
(a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])
};
let mut lower: Vec<[f64; 2]> = Vec::new();
for &p in &pts {
while lower.len() >= 2 && cross(lower[lower.len() - 2], lower[lower.len() - 1], p) <= 0.0 {
lower.pop();
}
lower.push(p);
}
let mut upper: Vec<[f64; 2]> = Vec::new();
for &p in pts.iter().rev() {
while upper.len() >= 2 && cross(upper[upper.len() - 2], upper[upper.len() - 1], p) <= 0.0 {
upper.pop();
}
upper.push(p);
}
lower.pop();
upper.pop();
lower.extend(upper);
lower
}
#[cfg(test)]
mod tests {
use super::*;
use crate::foundation::GridGeometry;
fn unit_square() -> PolygonSet {
PolygonSet::from_rings(vec![vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[1.0, 1.0, 0.0],
[0.0, 1.0, 0.0],
]])
}
#[test]
fn area_of_unit_square_is_one() {
approx::assert_relative_eq!(unit_square().area(), 1.0);
}
#[test]
fn rings_returns_exterior_vertices() {
let rings = unit_square().rings();
assert_eq!(rings.len(), 1);
assert_eq!(rings[0].first(), rings[0].last());
assert!(rings[0].iter().all(|c| c[2] == 0.0));
for corner in [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]] {
assert!(rings[0]
.iter()
.any(|c| c[0] == corner[0] && c[1] == corner[1]));
}
}
#[test]
fn contains_interior_excludes_boundary() {
let s = unit_square();
assert!(s.contains(0.5, 0.5)); assert!(!s.contains(2.0, 2.0)); assert!(!s.contains(0.0, 0.0)); assert!(!s.contains(0.5, 0.0)); assert!(!s.contains(1.0, 0.5)); }
#[test]
fn bbox_covers_square() {
let b = unit_square().bbox();
approx::assert_relative_eq!(b.xmin, 0.0);
approx::assert_relative_eq!(b.ymin, 0.0);
approx::assert_relative_eq!(b.xmax, 1.0);
approx::assert_relative_eq!(b.ymax, 1.0);
}
#[test]
fn empty_set_is_nan_and_zero() {
let s = PolygonSet::from_rings(Vec::new());
assert!(s.bbox().xmin.is_nan());
approx::assert_relative_eq!(s.area(), 0.0);
assert!(!s.contains(0.0, 0.0));
}
#[test]
fn clip_masks_nodes_outside_polygon() {
let geom = GridGeometry {
xori: 0.0,
yori: 0.0,
xinc: 1.0,
yinc: 1.0,
ncol: 3,
nrow: 3,
rotation_deg: 0.0,
yflip: false,
};
let surf = Surface::constant(geom, 5.0);
let poly = PolygonSet::from_rings(vec![vec![
[-0.5, -0.5, 0.0],
[1.5, -0.5, 0.0],
[1.5, 1.5, 0.0],
[-0.5, 1.5, 0.0],
]]);
let clipped = poly.clip(&surf);
let v = clipped.values();
assert_eq!(v[[0, 0]], 5.0);
assert_eq!(v[[1, 1]], 5.0);
assert!(v[[2, 0]].is_nan());
assert!(v[[0, 2]].is_nan());
assert!(v[[2, 2]].is_nan());
}
}