use crate::tile::TileBounds;
use geo::{BoundingRect, Coord, Geometry, LineString, MultiPolygon, Polygon};
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::overlay::FloatOverlay;
type IOverlayPoint = [f64; 2];
type IOverlayContour = Vec<IOverlayPoint>;
type IOverlayShape = Vec<IOverlayContour>;
type IOverlayShapes = Vec<IOverlayShape>;
#[inline]
fn polygon_to_ioverlay(poly: &Polygon<f64>) -> IOverlayShape {
let mut shape = Vec::with_capacity(1 + poly.interiors().len());
let exterior: IOverlayContour = poly.exterior().coords().map(|c| [c.x, c.y]).collect();
shape.push(exterior);
for hole in poly.interiors() {
let hole_contour: IOverlayContour = hole.coords().map(|c| [c.x, c.y]).collect();
shape.push(hole_contour);
}
shape
}
#[inline]
fn bounds_to_clip_box(bounds: &TileBounds) -> IOverlayShape {
vec![vec![
[bounds.lng_min, bounds.lat_min],
[bounds.lng_max, bounds.lat_min],
[bounds.lng_max, bounds.lat_max],
[bounds.lng_min, bounds.lat_max],
[bounds.lng_min, bounds.lat_min], ]]
}
fn ioverlay_to_geometry(shapes: IOverlayShapes) -> Option<Geometry<f64>> {
let valid_shapes: Vec<_> = shapes
.into_iter()
.filter(|shape| !shape.is_empty() && !shape[0].is_empty())
.collect();
if valid_shapes.is_empty() {
return None;
}
let polygons: Vec<Polygon<f64>> = valid_shapes
.into_iter()
.filter_map(ioverlay_shape_to_polygon)
.collect();
match polygons.len() {
0 => None,
1 => Some(Geometry::Polygon(polygons.into_iter().next().unwrap())),
_ => Some(Geometry::MultiPolygon(MultiPolygon::new(polygons))),
}
}
fn ioverlay_shape_to_polygon(shape: IOverlayShape) -> Option<Polygon<f64>> {
if shape.is_empty() {
return None;
}
let exterior = contour_to_linestring(&shape[0])?;
let holes: Vec<LineString<f64>> = shape[1..]
.iter()
.filter_map(contour_to_linestring)
.collect();
Some(Polygon::new(exterior, holes))
}
fn contour_to_linestring(contour: &IOverlayContour) -> Option<LineString<f64>> {
if contour.len() < 3 {
return None;
}
let mut coords: Vec<Coord<f64>> = contour.iter().map(|p| Coord { x: p[0], y: p[1] }).collect();
if coords.first() != coords.last() {
if let Some(first) = coords.first().cloned() {
coords.push(first);
}
}
if coords.len() < 4 {
return None;
}
Some(LineString::new(coords))
}
pub fn clip_polygon_ioverlay(poly: &Polygon<f64>, bounds: &TileBounds) -> Option<Geometry<f64>> {
let subj = polygon_to_ioverlay(poly);
let clip = bounds_to_clip_box(bounds);
let mut overlay = FloatOverlay::with_subj_and_clip_custom(
&[subj],
&[clip],
Default::default(),
Default::default(),
);
let result: IOverlayShapes = overlay.overlay(OverlayRule::Intersect, FillRule::EvenOdd);
ioverlay_to_geometry(result)
}
pub fn clip_multipolygon_ioverlay(
multi: &MultiPolygon<f64>,
bounds: &TileBounds,
) -> Option<Geometry<f64>> {
let subj_shapes: Vec<IOverlayShape> = multi.0.iter().map(polygon_to_ioverlay).collect();
if subj_shapes.is_empty() {
return None;
}
let clip = bounds_to_clip_box(bounds);
let mut overlay = FloatOverlay::with_subj_and_clip_custom(
&subj_shapes,
&[clip],
Default::default(),
Default::default(),
);
let result: IOverlayShapes = overlay.overlay(OverlayRule::Intersect, FillRule::EvenOdd);
ioverlay_to_geometry(result)
}
pub fn union_polygons_ioverlay(polys: &[Polygon<f64>]) -> Option<Geometry<f64>> {
let mut rect: Option<geo::Rect<f64>> = None;
for p in polys {
let r = p.bounding_rect()?;
rect = Some(match rect {
None => r,
Some(acc) => geo::Rect::new(
geo::coord! { x: acc.min().x.min(r.min().x), y: acc.min().y.min(r.min().y) },
geo::coord! { x: acc.max().x.max(r.max().x), y: acc.max().y.max(r.max().y) },
),
});
}
let rect = rect?;
let pad = rect.width().max(rect.height()).max(f64::MIN_POSITIVE) * 0.5;
let bounds = TileBounds::new(
rect.min().x - pad,
rect.min().y - pad,
rect.max().x + pad,
rect.max().y + pad,
);
let subj_shapes: Vec<IOverlayShape> = polys.iter().map(polygon_to_ioverlay).collect();
let clip = bounds_to_clip_box(&bounds);
let mut overlay = FloatOverlay::with_subj_and_clip_custom(
&subj_shapes,
&[clip],
Default::default(),
Default::default(),
);
let result: IOverlayShapes = overlay.overlay(OverlayRule::Intersect, FillRule::NonZero);
ioverlay_to_geometry(result)
}
pub fn repair_polygon_ioverlay(poly: &Polygon<f64>) -> Option<Geometry<f64>> {
let rect = poly.bounding_rect()?;
let pad = rect.width().max(rect.height()).max(f64::MIN_POSITIVE) * 0.5;
let bounds = TileBounds::new(
rect.min().x - pad,
rect.min().y - pad,
rect.max().x + pad,
rect.max().y + pad,
);
clip_polygon_ioverlay(poly, &bounds)
}
#[cfg(test)]
mod tests {
use super::*;
use geo::Coord;
fn make_square(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Polygon<f64> {
Polygon::new(
LineString::new(vec![
Coord { x: min_x, y: min_y },
Coord { x: max_x, y: min_y },
Coord { x: max_x, y: max_y },
Coord { x: min_x, y: max_y },
Coord { x: min_x, y: min_y },
]),
vec![],
)
}
#[test]
fn test_clip_fully_inside() {
let poly = make_square(1.0, 1.0, 2.0, 2.0);
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let result = clip_polygon_ioverlay(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
assert!(p.exterior().0.len() >= 4);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_fully_outside() {
let poly = make_square(100.0, 100.0, 200.0, 200.0);
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let result = clip_polygon_ioverlay(&poly, &bounds);
assert!(result.is_none());
}
#[test]
fn test_clip_partial() {
let poly = make_square(-5.0, -5.0, 5.0, 5.0);
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let result = clip_polygon_ioverlay(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
assert!(p.exterior().0.len() >= 4);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_self_intersecting_bowtie() {
let bowtie = Polygon::new(
LineString::new(vec![
Coord { x: -1.0, y: -1.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: 1.0, y: -1.0 },
Coord { x: -1.0, y: -1.0 },
]),
vec![],
);
let bounds = TileBounds::new(-2.0, -2.0, 2.0, 2.0);
let result = clip_polygon_ioverlay(&bowtie, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::MultiPolygon(mp) => {
assert_eq!(mp.0.len(), 2, "Bowtie should split into 2 triangles");
}
Geometry::Polygon(_) => {
}
other => panic!("Expected Polygon or MultiPolygon, got {:?}", other),
}
}
#[test]
fn test_repair_self_intersecting_bowtie() {
use geo::Validation;
let bowtie = Polygon::new(
LineString::new(vec![
Coord { x: -1.0, y: -1.0 },
Coord { x: 1.0, y: 1.0 },
Coord { x: -1.0, y: 1.0 },
Coord { x: 1.0, y: -1.0 },
Coord { x: -1.0, y: -1.0 },
]),
vec![],
);
assert!(!bowtie.is_valid(), "fixture must self-intersect");
let repaired = repair_polygon_ioverlay(&bowtie).expect("bowtie repairs to non-empty");
match repaired {
Geometry::MultiPolygon(mp) => {
assert_eq!(mp.0.len(), 2, "bowtie should split into 2 triangles");
for p in &mp.0 {
assert!(p.is_valid(), "repaired part must be valid");
}
}
other => panic!("expected MultiPolygon, got {other:?}"),
}
}
#[test]
fn test_repair_valid_polygon_stays_equivalent() {
use geo::{Area, Validation};
let square = make_square(0.0, 0.0, 10.0, 10.0);
let repaired = repair_polygon_ioverlay(&square).expect("valid input survives repair");
match repaired {
Geometry::Polygon(p) => {
assert!(p.is_valid());
assert!((p.unsigned_area() - 100.0).abs() < 1e-9);
}
other => panic!("expected Polygon, got {other:?}"),
}
}
#[test]
fn test_clip_u_shape_splits() {
let u_shape = Polygon::new(
LineString::new(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 0.0, y: 2.0 },
Coord { x: 0.3, y: 2.0 },
Coord { x: 0.3, y: 0.5 },
Coord { x: 0.7, y: 0.5 },
Coord { x: 0.7, y: 2.0 },
Coord { x: 1.0, y: 2.0 },
Coord { x: 1.0, y: 0.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let bounds = TileBounds::new(-0.1, 1.0, 1.1, 2.5);
let result = clip_polygon_ioverlay(&u_shape, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::MultiPolygon(mp) => {
assert_eq!(
mp.0.len(),
2,
"U-shape clipped across opening should produce 2 polygons"
);
}
other => panic!("Expected MultiPolygon with 2 polygons, got {:?}", other),
}
}
#[test]
fn test_clip_multipolygon() {
let poly1 = make_square(1.0, 1.0, 2.0, 2.0);
let poly2 = make_square(5.0, 5.0, 6.0, 6.0);
let multi = MultiPolygon::new(vec![poly1, poly2]);
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let result = clip_multipolygon_ioverlay(&multi, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::MultiPolygon(mp) => {
assert_eq!(mp.0.len(), 2, "Both polygons should be preserved");
}
_ => panic!("Expected MultiPolygon"),
}
}
}