use geo::algorithm::sweep::Intersections;
use geo::{
BooleanOps, BoundingRect, Coord, Geometry, Line, LineString, MultiLineString, MultiPolygon,
Point, Polygon, Rect,
};
use crate::ioverlay_clip;
use crate::sutherland_hodgman;
use crate::tile::TileBounds;
pub const DEFAULT_BUFFER_PIXELS: u32 = 8;
pub const DEFAULT_EXTENT: u32 = 4096;
fn has_structural_issues(poly: &Polygon<f64>, assume_simple: bool) -> bool {
let ring = poly.exterior();
if ring.0.len() < 4 {
return true;
}
for i in 0..ring.0.len() - 1 {
let curr = ring.0[i];
let next = ring.0[i + 1];
if (curr.x - next.x).abs() < 1e-10 && (curr.y - next.y).abs() < 1e-10 {
if i != ring.0.len() - 2 {
return true;
}
}
}
if !assume_simple && has_self_intersecting_edges(&ring.0) {
return true;
}
false
}
fn has_self_intersecting_edges(coords: &[Coord<f64>]) -> bool {
let n = coords.len();
if n < 4 {
return false;
}
let segments: Vec<Line<f64>> = coords
.windows(2)
.filter_map(|w| {
let (a, b) = (w[0], w[1]);
if (a.x - b.x).abs() < 1e-10 && (a.y - b.y).abs() < 1e-10 {
None
} else {
Some(Line::new(a, b))
}
})
.collect();
Intersections::from_iter(segments)
.any(|(a, b, _)| edges_intersect_properly(a.start, a.end, b.start, b.end))
}
fn edges_intersect_properly(
a1: Coord<f64>,
a2: Coord<f64>,
b1: Coord<f64>,
b2: Coord<f64>,
) -> bool {
let d1 = cross_product_sign(b1, b2, a1);
let d2 = cross_product_sign(b1, b2, a2);
let d3 = cross_product_sign(a1, a2, b1);
let d4 = cross_product_sign(a1, a2, b2);
((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
&& ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
}
fn cross_product_sign(a: Coord<f64>, b: Coord<f64>, c: Coord<f64>) -> f64 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
fn geometry_has_structural_issues(geom: &Geometry<f64>, assume_simple: bool) -> bool {
match geom {
Geometry::Polygon(p) => has_structural_issues(p, assume_simple),
Geometry::MultiPolygon(mp) => mp.0.iter().any(|p| has_structural_issues(p, assume_simple)),
_ => false,
}
}
pub fn geometry_is_simple(geom: &Geometry<f64>) -> bool {
match geom {
Geometry::Polygon(p) => !has_self_intersecting_edges(&p.exterior().0),
Geometry::MultiPolygon(mp) => {
mp.0.iter()
.all(|p| !has_self_intersecting_edges(&p.exterior().0))
}
_ => true,
}
}
fn has_boundary_connecting_edges(poly: &Polygon<f64>, bounds: &TileBounds) -> bool {
let ring = poly.exterior();
let eps = 1e-10;
for window in ring.0.windows(2) {
let p1 = window[0];
let p2 = window[1];
if (p1.x - p2.x).abs() < eps && (p1.y - p2.y).abs() < eps {
continue;
}
if (p1.x - bounds.lng_min).abs() < eps && (p2.x - bounds.lng_min).abs() < eps {
return true;
}
if (p1.x - bounds.lng_max).abs() < eps && (p2.x - bounds.lng_max).abs() < eps {
return true;
}
if (p1.y - bounds.lat_min).abs() < eps && (p2.y - bounds.lat_min).abs() < eps {
return true;
}
if (p1.y - bounds.lat_max).abs() < eps && (p2.y - bounds.lat_max).abs() < eps {
return true;
}
}
false
}
pub fn clip_geometry(
geom: &Geometry<f64>,
bounds: &TileBounds,
buffer: f64,
) -> Option<Geometry<f64>> {
clip_geometry_simple(geom, bounds, buffer, false, false)
}
pub fn clip_geometry_simple(
geom: &Geometry<f64>,
bounds: &TileBounds,
buffer: f64,
assume_simple: bool,
skip_boundary_fallback: bool,
) -> Option<Geometry<f64>> {
let buffered = TileBounds::new(
bounds.lng_min - buffer,
bounds.lat_min - buffer,
bounds.lng_max + buffer,
bounds.lat_max + buffer,
);
match geom {
Geometry::Point(p) => clip_point(p, &buffered).map(Geometry::Point),
Geometry::LineString(ls) => clip_linestring(ls, &buffered),
Geometry::Polygon(poly) => {
clip_polygon(poly, &buffered, assume_simple, skip_boundary_fallback)
}
Geometry::MultiPolygon(mp) => {
clip_multipolygon(mp, &buffered, assume_simple, skip_boundary_fallback)
.map(Geometry::MultiPolygon)
}
Geometry::MultiLineString(mls) => clip_multilinestring(mls, &buffered),
other => {
if let Some(rect) = other.bounding_rect() {
if intersects_bounds(&rect, &buffered) {
return Some(other.clone());
}
}
None
}
}
}
pub fn buffer_pixels_to_degrees(buffer_pixels: u32, tile_bounds: &TileBounds, extent: u32) -> f64 {
tile_bounds.width() * buffer_pixels as f64 / extent as f64
}
fn intersects_bounds(rect: &Rect<f64>, bounds: &TileBounds) -> bool {
rect.max().x >= bounds.lng_min
&& rect.min().x <= bounds.lng_max
&& rect.max().y >= bounds.lat_min
&& rect.min().y <= bounds.lat_max
}
fn is_fully_inside(rect: &Rect<f64>, bounds: &TileBounds) -> bool {
rect.min().x >= bounds.lng_min
&& rect.max().x <= bounds.lng_max
&& rect.min().y >= bounds.lat_min
&& rect.max().y <= bounds.lat_max
}
fn clip_point(point: &Point<f64>, bounds: &TileBounds) -> Option<Point<f64>> {
if point.x() >= bounds.lng_min
&& point.x() <= bounds.lng_max
&& point.y() >= bounds.lat_min
&& point.y() <= bounds.lat_max
{
Some(*point)
} else {
None
}
}
fn clip_linestring(ls: &LineString<f64>, bounds: &TileBounds) -> Option<Geometry<f64>> {
if let Some(rect) = ls.bounding_rect() {
if !intersects_bounds(&rect, bounds) {
return None;
}
}
let clip_rect = Rect::new(
Coord {
x: bounds.lng_min,
y: bounds.lat_min,
},
Coord {
x: bounds.lng_max,
y: bounds.lat_max,
},
);
let clip_poly = clip_rect.to_polygon();
let mls = MultiLineString::new(vec![ls.clone()]);
let clipped = clip_poly.clip(&mls, false);
if clipped.0.is_empty() {
None
} else if clipped.0.len() == 1 {
Some(Geometry::LineString(clipped.0.into_iter().next().unwrap()))
} else {
Some(Geometry::MultiLineString(clipped))
}
}
fn clip_multilinestring(mls: &MultiLineString<f64>, bounds: &TileBounds) -> Option<Geometry<f64>> {
if let Some(rect) = mls.bounding_rect() {
if !intersects_bounds(&rect, bounds) {
return None;
}
}
let clip_rect = Rect::new(
Coord {
x: bounds.lng_min,
y: bounds.lat_min,
},
Coord {
x: bounds.lng_max,
y: bounds.lat_max,
},
);
let clip_poly = clip_rect.to_polygon();
let clipped = clip_poly.clip(mls, false);
if clipped.0.is_empty() {
None
} else {
Some(Geometry::MultiLineString(clipped))
}
}
fn clip_polygon(
poly: &Polygon<f64>,
bounds: &TileBounds,
assume_simple: bool,
skip_boundary_fallback: bool,
) -> Option<Geometry<f64>> {
let poly_rect = poly.bounding_rect()?;
if !intersects_bounds(&poly_rect, bounds) {
return None;
}
let input_has_issues = has_structural_issues(poly, assume_simple);
if is_fully_inside(&poly_rect, bounds) && !input_has_issues {
return Some(Geometry::Polygon(poly.clone()));
}
if input_has_issues {
return ioverlay_clip::clip_polygon_ioverlay(poly, bounds);
}
let sh_result = sutherland_hodgman::clip_polygon_sh(poly, bounds);
let skip_boundary = assume_simple && skip_boundary_fallback;
match &sh_result {
Some(Geometry::Polygon(p)) => {
if geometry_has_structural_issues(sh_result.as_ref().unwrap(), assume_simple)
|| (!skip_boundary && has_boundary_connecting_edges(p, bounds))
{
ioverlay_clip::clip_polygon_ioverlay(poly, bounds)
} else {
sh_result
}
}
Some(Geometry::MultiPolygon(mp)) => {
let has_issues = mp.0.iter().any(|p| {
has_structural_issues(p, assume_simple)
|| (!skip_boundary && has_boundary_connecting_edges(p, bounds))
});
if has_issues {
ioverlay_clip::clip_polygon_ioverlay(poly, bounds)
} else {
sh_result
}
}
Some(_) => {
sh_result
}
None => {
None
}
}
}
fn clip_multipolygon(
mp: &MultiPolygon<f64>,
bounds: &TileBounds,
assume_simple: bool,
skip_boundary_fallback: bool,
) -> Option<MultiPolygon<f64>> {
let mp_rect = mp.bounding_rect()?;
if !intersects_bounds(&mp_rect, bounds) {
return None;
}
if is_fully_inside(&mp_rect, bounds) {
return Some(mp.clone());
}
let mut clipped_polys = Vec::new();
for poly in &mp.0 {
let poly_rect = match poly.bounding_rect() {
Some(r) => r,
None => continue, };
if !intersects_bounds(&poly_rect, bounds) {
continue;
}
if is_fully_inside(&poly_rect, bounds) {
clipped_polys.push(poly.clone());
continue;
}
match clip_polygon(poly, bounds, assume_simple, skip_boundary_fallback) {
Some(Geometry::Polygon(clipped)) => clipped_polys.push(clipped),
Some(Geometry::MultiPolygon(pieces)) => clipped_polys.extend(pieces.0),
Some(other) => debug_assert!(false, "polygon clip returned {other:?}"),
None => {}
}
}
if clipped_polys.is_empty() {
None
} else {
Some(MultiPolygon::new(clipped_polys))
}
}
use crate::world_coord::{lng_lat_to_world, WorldBounds, WorldCoord};
pub fn buffer_pixels_to_world(zoom: u8, buffer_pixels: u32, extent: u32) -> u32 {
let tile_size_world: u64 = if zoom == 0 {
crate::world_coord::WORLD_SCALE
} else {
1_u64 << (32 - zoom as u32)
};
(tile_size_world * buffer_pixels as u64 / extent as u64) as u32
}
pub fn clip_point_world(point: &WorldCoord, bounds: &WorldBounds) -> Option<WorldCoord> {
if bounds.contains(point) {
Some(*point)
} else {
None
}
}
pub fn clip_polygon_world(
exterior: &[WorldCoord],
interiors: &[Vec<WorldCoord>],
bounds: &WorldBounds,
) -> Option<(Vec<WorldCoord>, Vec<Vec<WorldCoord>>)> {
let poly_bounds = worldcoord_bbox(exterior)?;
if !bounds.intersects(&poly_bounds) {
return None;
}
if bounds.contains_bounds(&poly_bounds) {
return Some((exterior.to_vec(), interiors.to_vec()));
}
sutherland_hodgman::clip_polygon_sh_world(exterior, interiors, bounds)
}
fn worldcoord_bbox(coords: &[WorldCoord]) -> Option<WorldBounds> {
if coords.is_empty() {
return None;
}
let mut x_min = u32::MAX;
let mut y_min = u32::MAX;
let mut x_max = 0u32;
let mut y_max = 0u32;
for c in coords {
x_min = x_min.min(c.x);
y_min = y_min.min(c.y);
x_max = x_max.max(c.x);
y_max = y_max.max(c.y);
}
Some(WorldBounds::new(x_min, y_min, x_max, y_max))
}
pub fn polygon_to_world_rings(poly: &Polygon<f64>) -> (Vec<WorldCoord>, Vec<Vec<WorldCoord>>) {
let exterior: Vec<WorldCoord> = poly
.exterior()
.coords()
.map(|c| lng_lat_to_world(c.x, c.y))
.collect();
let interiors: Vec<Vec<WorldCoord>> = poly
.interiors()
.iter()
.map(|ring| ring.coords().map(|c| lng_lat_to_world(c.x, c.y)).collect())
.collect();
(exterior, interiors)
}
#[cfg(test)]
mod tests {
use super::*;
use geo::point;
fn u_multipolygon() -> MultiPolygon<f64> {
MultiPolygon::new(vec![Polygon::new(
LineString::from(vec![
(0.0, 0.0),
(4.0, 0.0),
(4.0, 5.0),
(3.0, 5.0),
(3.0, 1.0),
(1.0, 1.0),
(1.0, 5.0),
(0.0, 5.0),
(0.0, 0.0),
]),
vec![],
)])
}
#[test]
fn multipolygon_part_splitting_into_pieces_is_kept() {
let mp = u_multipolygon();
let window = TileBounds::new(-0.5, 3.0, 4.5, 6.0);
let clipped = clip_multipolygon(&mp, &window, false, false)
.expect("clip must not drop a genuinely-overlapping feature");
assert_eq!(clipped.0.len(), 2, "both prong pieces must survive");
let via_public =
clip_geometry_simple(&Geometry::MultiPolygon(mp), &window, 0.0, true, false)
.expect("public entry point must keep the feature");
match via_public {
Geometry::MultiPolygon(m) => assert_eq!(m.0.len(), 2),
other => panic!("expected MultiPolygon, got {other:?}"),
}
}
#[test]
fn tielt_winge_boundary_sliver_regression() {
use geo::Contains;
use geozero::ToGeo;
let path = std::path::Path::new("../../tests/fixtures/realdata/tielt-winge-adm4.wkb");
if !path.exists() {
eprintln!("Skipping: fixture not found");
return;
}
let geom: Geometry<f64> = geozero::wkb::Wkb(std::fs::read(path).unwrap())
.to_geo()
.unwrap();
let simple = geometry_is_simple(&geom);
let buf = |b: &TileBounds| b.width() * 8.0 / 4096.0;
let hole = point!(x: 4.9265, y: 50.9468);
let contains_hole = |g: &Geometry<f64>| match g {
Geometry::MultiPolygon(m) => m.contains(&hole),
Geometry::Polygon(p) => p.contains(&hole),
other => panic!("unexpected clip output {other:?}"),
};
let leaf = crate::tile::tile_bounds(2104, 1372, 12);
let direct = clip_geometry_simple(&geom, &leaf, buf(&leaf), simple, false)
.expect("direct leaf clip must keep the eastern sliver");
assert!(
contains_hole(&direct),
"clipped sliver must cover the reported hole point"
);
let mut cur = geom;
for (x, y, z) in [(263u32, 171u32, 9u8), (526, 343, 10), (1052, 686, 11)] {
let nb = crate::tile::tile_bounds(x, y, z);
cur = clip_geometry_simple(&cur, &nb, buf(&nb), simple, false)
.unwrap_or_else(|| panic!("cascade lost the feature at z{z} ({x},{y})"));
}
let casc = clip_geometry_simple(&cur, &leaf, buf(&leaf), simple, false)
.expect("cascade leaf clip must keep the eastern sliver");
assert!(
contains_hole(&casc),
"cascade-clipped sliver must cover the reported hole point"
);
}
fn square(minx: f64, miny: f64, maxx: f64, maxy: f64) -> Polygon<f64> {
Polygon::new(
LineString::from(vec![
(minx, miny),
(maxx, miny),
(maxx, maxy),
(minx, maxy),
(minx, miny),
]),
vec![],
)
}
#[test]
fn geometry_is_simple_true_for_simple_polygon() {
let g = Geometry::Polygon(square(0.0, 0.0, 4.0, 4.0));
assert!(geometry_is_simple(&g));
}
#[test]
fn geometry_is_simple_false_for_bowtie() {
let bowtie = Polygon::new(
LineString::from(vec![
(0.0, 0.0),
(2.0, 2.0),
(2.0, 0.0),
(0.0, 2.0),
(0.0, 0.0),
]),
vec![],
);
assert!(!geometry_is_simple(&Geometry::Polygon(bowtie)));
}
#[test]
fn geometry_is_simple_multipolygon_all_or_nothing() {
let good = square(0.0, 0.0, 1.0, 1.0);
let bowtie = Polygon::new(
LineString::from(vec![
(0.0, 0.0),
(2.0, 2.0),
(2.0, 0.0),
(0.0, 2.0),
(0.0, 0.0),
]),
vec![],
);
assert!(geometry_is_simple(&Geometry::MultiPolygon(
MultiPolygon::new(vec![good.clone(), square(5.0, 5.0, 6.0, 6.0)])
)));
assert!(!geometry_is_simple(&Geometry::MultiPolygon(
MultiPolygon::new(vec![good, bowtie])
)));
}
fn crossing_ring(n: usize) -> Vec<Coord<f64>> {
let half = n / 2;
let mut v: Vec<Coord<f64>> = Vec::with_capacity(n + 1);
for i in 0..half {
let t = i as f64 / half as f64;
v.push(Coord {
x: 10.0 * t,
y: 10.0 * t,
});
}
for i in 0..half {
let t = i as f64 / half as f64;
v.push(Coord {
x: 10.0 - 10.0 * t,
y: 0.7 + 10.0 * t,
});
}
v.push(v[0]); v
}
#[test]
fn self_intersection_detected_small_ring() {
let ring = crossing_ring(64);
assert!(has_self_intersecting_edges(&ring));
}
#[test]
fn large_self_intersecting_ring_detected() {
let ring = crossing_ring(6_000);
assert!(ring.len() > 2_048);
assert!(has_self_intersecting_edges(&ring));
}
#[test]
fn large_simple_ring_not_self_intersecting() {
let n = 20_000usize;
let mut ring: Vec<Coord<f64>> = (0..n)
.map(|i| {
let theta = std::f64::consts::TAU * (i as f64) / (n as f64);
Coord {
x: theta.cos(),
y: theta.sin(),
}
})
.collect();
ring.push(ring[0]); assert!(ring.len() > 2_048);
assert!(!has_self_intersecting_edges(&ring));
}
#[test]
fn geometry_is_simple_true_for_non_polygon() {
let g = Geometry::Point(point!(x: 1.0, y: 1.0));
assert!(geometry_is_simple(&g));
}
#[test]
fn clip_geometry_simple_byte_identical_on_simple_input() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let buffer = 0.5;
let cases = vec![
Geometry::Polygon(square(-3.0, -3.0, 5.0, 5.0)),
Geometry::Polygon(square(2.0, 2.0, 20.0, 8.0)),
Geometry::MultiPolygon(MultiPolygon::new(vec![
square(-2.0, -2.0, 4.0, 4.0),
square(6.0, 6.0, 13.0, 13.0),
])),
];
for g in cases {
assert!(geometry_is_simple(&g));
let default = clip_geometry(&g, &bounds, buffer);
let fast = clip_geometry_simple(&g, &bounds, buffer, true, false);
assert_eq!(default, fast, "assume_simple output diverged for {g:?}");
}
}
#[test]
fn test_clip_point_inside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let point = point!(x: 5.0, y: 5.0);
assert!(clip_point(&point, &bounds).is_some());
}
#[test]
fn test_clip_point_outside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let point = point!(x: 15.0, y: 5.0);
assert!(clip_point(&point, &bounds).is_none());
}
#[test]
fn test_clip_point_on_boundary() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let point = point!(x: 10.0, y: 5.0);
assert!(clip_point(&point, &bounds).is_some());
}
#[test]
fn test_clip_polygon_partial() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: -5.0, y: -5.0 },
Coord { x: 5.0, y: -5.0 },
Coord { x: 5.0, y: 5.0 },
Coord { x: -5.0, y: 5.0 },
Coord { x: -5.0, y: -5.0 },
]),
vec![],
);
let result = clip_polygon(&poly, &bounds, false, false);
assert!(result.is_some());
let clipped = match result.unwrap() {
Geometry::Polygon(p) => p,
Geometry::MultiPolygon(mp) => mp.0.into_iter().next().unwrap(),
_ => panic!("Expected polygon geometry"),
};
for coord in clipped.exterior().coords() {
assert!(
coord.x >= 0.0 && coord.x <= 10.0,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= 0.0 && coord.y <= 10.0,
"y={} out of bounds",
coord.y
);
}
}
#[test]
fn test_clip_polygon_outside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 20.0, y: 20.0 },
Coord { x: 30.0, y: 20.0 },
Coord { x: 30.0, y: 30.0 },
Coord { x: 20.0, y: 30.0 },
Coord { x: 20.0, y: 20.0 },
]),
vec![],
);
assert!(clip_polygon(&poly, &bounds, false, false).is_none());
}
#[test]
fn test_clip_polygon_fully_inside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 2.0, y: 2.0 },
Coord { x: 8.0, y: 2.0 },
Coord { x: 8.0, y: 8.0 },
Coord { x: 2.0, y: 8.0 },
Coord { x: 2.0, y: 2.0 },
]),
vec![],
);
let result = clip_polygon(&poly, &bounds, false, false);
assert!(result.is_some());
}
#[test]
fn test_clip_linestring_crossing() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let ls = LineString::from(vec![Coord { x: -5.0, y: 5.0 }, Coord { x: 15.0, y: 5.0 }]);
let result = clip_linestring(&ls, &bounds);
assert!(result.is_some());
}
#[test]
fn test_clip_linestring_outside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let ls = LineString::from(vec![Coord { x: 20.0, y: 20.0 }, Coord { x: 30.0, y: 30.0 }]);
let result = clip_linestring(&ls, &bounds);
assert!(result.is_none());
}
#[test]
fn test_clip_linestring_fully_inside() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let ls = LineString::from(vec![Coord { x: 2.0, y: 2.0 }, Coord { x: 8.0, y: 8.0 }]);
let result = clip_linestring(&ls, &bounds);
assert!(result.is_some());
}
#[test]
fn test_buffer_pixels_to_degrees() {
let bounds = TileBounds::new(0.0, 0.0, 1.0, 1.0);
let buffer = buffer_pixels_to_degrees(8, &bounds, 4096);
let expected = 8.0 / 4096.0;
assert!(
(buffer - expected).abs() < 1e-10,
"buffer={} expected={}",
buffer,
expected
);
}
#[test]
fn test_buffer_affects_clipping() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let buffer = 2.0;
let point = point!(x: 11.0, y: 5.0);
assert!(clip_point(&point, &bounds).is_none());
let result = clip_geometry(&Geometry::Point(point), &bounds, buffer);
assert!(result.is_some());
}
#[test]
fn test_clip_geometry_point() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let point = Geometry::Point(point!(x: 5.0, y: 5.0));
let result = clip_geometry(&point, &bounds, 0.0);
assert!(result.is_some());
assert!(matches!(result.unwrap(), Geometry::Point(_)));
}
#[test]
fn test_clip_geometry_polygon() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Geometry::Polygon(Polygon::new(
LineString::from(vec![
Coord { x: 5.0, y: 5.0 },
Coord { x: 15.0, y: 5.0 },
Coord { x: 15.0, y: 15.0 },
Coord { x: 5.0, y: 15.0 },
Coord { x: 5.0, y: 5.0 },
]),
vec![],
));
let result = clip_geometry(&poly, &bounds, 0.0);
assert!(result.is_some());
}
#[test]
fn test_clip_geometry_with_buffer() {
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let buffer = 1.0;
let poly = Geometry::Polygon(Polygon::new(
LineString::from(vec![
Coord { x: 10.5, y: 5.0 },
Coord { x: 12.0, y: 5.0 },
Coord { x: 12.0, y: 8.0 },
Coord { x: 10.5, y: 8.0 },
Coord { x: 10.5, y: 5.0 },
]),
vec![],
));
let result_no_buffer = clip_geometry(&poly, &bounds, 0.0);
assert!(result_no_buffer.is_none());
let result_with_buffer = clip_geometry(&poly, &bounds, buffer);
assert!(result_with_buffer.is_some());
}
#[test]
fn test_multipolygon_bbox_prefilter_skips_distant_polygons() {
let tile_bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let mut polygons = Vec::with_capacity(1000);
for i in 0..10 {
let x = 1.0 + (i as f64) * 0.8;
let y = 1.0 + (i as f64) * 0.8;
polygons.push(Polygon::new(
LineString::from(vec![
Coord { x, y },
Coord { x: x + 0.5, y },
Coord {
x: x + 0.5,
y: y + 0.5,
},
Coord { x, y: y + 0.5 },
Coord { x, y },
]),
vec![],
));
}
for i in 0..990 {
let x = 20.0 + (i as f64) * 0.18;
let y = -80.0 + (i as f64) * 0.16;
polygons.push(Polygon::new(
LineString::from(vec![
Coord { x, y },
Coord { x: x + 0.1, y },
Coord {
x: x + 0.1,
y: y + 0.1,
},
Coord { x, y: y + 0.1 },
Coord { x, y },
]),
vec![],
));
}
let mp = MultiPolygon::new(polygons);
let result = clip_multipolygon(&mp, &tile_bounds, false, false);
assert!(
result.is_some(),
"Should produce output for the intersecting polygons"
);
let clipped_mp = result.unwrap();
assert!(
clipped_mp.0.len() >= 8 && clipped_mp.0.len() <= 12,
"Expected ~10 output polygons, got {}",
clipped_mp.0.len()
);
for poly in &clipped_mp.0 {
let bbox = poly.bounding_rect().unwrap();
assert!(
bbox.min().x >= 0.0 - 0.01 && bbox.max().x <= 10.0 + 0.01,
"Output polygon x outside tile bounds: {:?}",
bbox
);
assert!(
bbox.min().y >= 0.0 - 0.01 && bbox.max().y <= 10.0 + 0.01,
"Output polygon y outside tile bounds: {:?}",
bbox
);
}
}
#[test]
fn test_multipolygon_bbox_prefilter_all_outside() {
let tile_bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let polygons: Vec<Polygon<f64>> = (0..500)
.map(|i| {
let x = 50.0 + (i as f64) * 0.2;
let y = 50.0 + (i as f64) * 0.1;
Polygon::new(
LineString::from(vec![
Coord { x, y },
Coord { x: x + 0.1, y },
Coord {
x: x + 0.1,
y: y + 0.1,
},
Coord { x, y: y + 0.1 },
Coord { x, y },
]),
vec![],
)
})
.collect();
let mp = MultiPolygon::new(polygons);
let result = clip_multipolygon(&mp, &tile_bounds, false, false);
assert!(
result.is_none(),
"All-outside multipolygon should return None"
);
}
#[test]
fn test_bbox_prefilter_large_polygon_preclip() {
let tile_bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let mut coords: Vec<Coord<f64>> = Vec::new();
for i in 0..360 {
let x = -180.0 + i as f64;
let y = -60.0 + (i as f64 * 0.1).sin() * 2.0; coords.push(Coord { x, y });
}
for i in (0..360).rev() {
let x = -180.0 + i as f64;
let y = 60.0 + (i as f64 * 0.1).cos() * 2.0; coords.push(Coord { x, y });
}
coords.push(coords[0]);
let large_poly = Polygon::new(LineString::from(coords.clone()), vec![]);
let total_input_coords = coords.len();
assert!(
total_input_coords > 700,
"Test polygon should have many coordinates, got {}",
total_input_coords
);
let result = clip_polygon(&large_poly, &tile_bounds, false, false);
assert!(result.is_some(), "Large polygon should intersect the tile");
match result.unwrap() {
Geometry::Polygon(p) => {
let output_coords = p.exterior().coords().count();
assert!(
output_coords < total_input_coords / 2,
"Clipped polygon should have fewer coords than input: {} vs {}",
output_coords,
total_input_coords
);
}
Geometry::MultiPolygon(mp) => {
let total_output: usize = mp.0.iter().map(|p| p.exterior().coords().count()).sum();
assert!(
total_output < total_input_coords / 2,
"Clipped multipolygon should have fewer coords than input: {} vs {}",
total_output,
total_input_coords
);
}
other => panic!("Expected Polygon or MultiPolygon, got {:?}", other),
}
}
fn antimeridian_polygon() -> Geometry<f64> {
Geometry::Polygon(Polygon::new(
LineString::from(vec![
Coord { x: -179.9, y: -0.1 },
Coord { x: 179.9, y: -0.1 },
Coord { x: 179.9, y: 0.1 },
Coord { x: -179.9, y: 0.1 },
Coord { x: -179.9, y: -0.1 },
]),
vec![],
))
}
#[test]
fn antimeridian_polygon_smears_into_prime_meridian_tile() {
let tile = TileBounds::new(-1.0, -1.0, 1.0, 1.0);
let clipped = clip_geometry(&antimeridian_polygon(), &tile, 0.0);
let clipped = clipped.expect(
"PIN: prime-meridian tile receives geometry from an \
antimeridian-crossing polygon (smearing)",
);
let rect = clipped.bounding_rect().unwrap();
assert!(
(rect.min().x - (-1.0)).abs() < 1e-9 && (rect.max().x - 1.0).abs() < 1e-9,
"PIN: smear spans the entire tile width, got {rect:?}"
);
}
#[test]
fn antimeridian_polygon_clips_at_edge_tile() {
let tile = TileBounds::new(178.0, -1.0, 180.0, 1.0);
let clipped = clip_geometry(&antimeridian_polygon(), &tile, 0.0);
assert!(
clipped.is_some(),
"tile at the +180° edge intersects the stored rectangle"
);
}
#[test]
fn test_sutherland_hodgman_fully_inside() {
use crate::sutherland_hodgman::clip_polygon_sh;
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 2.0, y: 2.0 },
Coord { x: 8.0, y: 2.0 },
Coord { x: 8.0, y: 8.0 },
Coord { x: 2.0, y: 8.0 },
Coord { x: 2.0, y: 2.0 },
]),
vec![],
);
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some(), "Fully inside polygon should be preserved");
match result.unwrap() {
Geometry::Polygon(p) => {
assert_eq!(
p.exterior().0.len(),
5,
"Should have 5 coords (4 vertices + close)"
);
}
other => panic!("Expected Polygon, got {:?}", other),
}
}
#[test]
fn test_sutherland_hodgman_fully_outside() {
use crate::sutherland_hodgman::clip_polygon_sh;
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 20.0, y: 20.0 },
Coord { x: 30.0, y: 20.0 },
Coord { x: 30.0, y: 30.0 },
Coord { x: 20.0, y: 30.0 },
Coord { x: 20.0, y: 20.0 },
]),
vec![],
);
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_none(), "Fully outside polygon should be empty");
}
#[test]
fn test_sutherland_hodgman_partial_clip() {
use crate::sutherland_hodgman::clip_polygon_sh;
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let poly = Polygon::new(
LineString::from(vec![
Coord { x: 5.0, y: 2.0 },
Coord { x: 15.0, y: 2.0 },
Coord { x: 15.0, y: 8.0 },
Coord { x: 5.0, y: 8.0 },
Coord { x: 5.0, y: 2.0 },
]),
vec![],
);
let result = clip_polygon_sh(&poly, &bounds);
assert!(
result.is_some(),
"Partially overlapping polygon should produce output"
);
match result.unwrap() {
Geometry::Polygon(p) => {
for coord in p.exterior().coords() {
assert!(
coord.x >= 0.0 - 0.001 && coord.x <= 10.0 + 0.001,
"x out of bounds: {}",
coord.x
);
assert!(
coord.y >= 0.0 - 0.001 && coord.y <= 10.0 + 0.001,
"y out of bounds: {}",
coord.y
);
}
}
other => panic!("Expected Polygon, got {:?}", other),
}
}
#[test]
fn test_sutherland_hodgman_large_polygon_reduction() {
use crate::sutherland_hodgman::clip_polygon_sh;
let bounds = TileBounds::new(0.0, 0.0, 10.0, 10.0);
let mut coords = Vec::new();
for i in 0..360 {
coords.push(Coord {
x: -180.0 + i as f64,
y: -50.0,
});
}
for i in (0..360).rev() {
coords.push(Coord {
x: -180.0 + i as f64,
y: 50.0,
});
}
coords.push(coords[0]);
let input_count = coords.len();
let poly = Polygon::new(LineString::from(coords), vec![]);
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some(), "Clipped polygon should not be empty");
match result.unwrap() {
Geometry::Polygon(p) => {
let output_count = p.exterior().0.len();
assert!(
output_count < input_count / 10,
"Sutherland-Hodgman should dramatically reduce coordinates: {} -> {}",
input_count,
output_count
);
}
other => panic!("Expected Polygon, got {:?}", other),
}
}
#[test]
fn test_clip_polygon_u_shape() {
let bounds = TileBounds::new(0.0, 4.0, 10.0, 6.0);
let u_shape = Polygon::new(
LineString::from(vec![
Coord { x: 1.0, y: 0.0 },
Coord { x: 2.0, y: 0.0 },
Coord { x: 2.0, y: 10.0 },
Coord { x: 1.0, y: 10.0 },
Coord { x: 1.0, y: 2.0 },
Coord { x: 8.0, y: 2.0 },
Coord { x: 8.0, y: 10.0 },
Coord { x: 9.0, y: 10.0 },
Coord { x: 9.0, y: 0.0 },
Coord { x: 1.0, y: 0.0 },
]),
vec![],
);
let result = clip_polygon(&u_shape, &bounds, false, false);
assert!(result.is_some(), "U-shape should intersect the band");
match result.unwrap() {
Geometry::MultiPolygon(mp) => {
assert_eq!(
mp.0.len(),
2,
"U-shape clipped should produce 2 separate polygons"
);
for p in mp.0.iter() {
for coord in p.exterior().coords() {
assert!(
coord.x >= 0.0 && coord.x <= 10.0,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= 4.0 - 1e-10 && coord.y <= 6.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
}
Geometry::Polygon(p) => {
for coord in p.exterior().coords() {
assert!(
coord.x >= 0.0 && coord.x <= 10.0,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= 4.0 - 1e-10 && coord.y <= 6.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
other => panic!("Expected Polygon or MultiPolygon, got {:?}", other),
}
}
fn u_shape() -> Polygon<f64> {
Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 10.0, y: 0.0 },
Coord { x: 10.0, y: 10.0 },
Coord { x: 7.0, y: 10.0 },
Coord { x: 7.0, y: 3.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 3.0, y: 10.0 },
Coord { x: 0.0, y: 10.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
)
}
#[test]
fn simple_u_fastpath_keeps_single_ring() {
let u = u_shape();
let bounds = TileBounds::new(0.0, 4.0, 10.0, 6.0);
assert!(
geometry_is_simple(&Geometry::Polygon(u.clone())),
"U-shape is a simple polygon; assume_simple is true in prod"
);
match clip_polygon(&u, &bounds, true, false).unwrap() {
Geometry::MultiPolygon(mp) => assert_eq!(mp.0.len(), 2),
other => panic!("flag off should split; got {other:?}"),
}
match clip_polygon(&u, &bounds, true, true).unwrap() {
Geometry::Polygon(_) => {}
other => panic!("flag on should keep the S-H single polygon; got {other:?}"),
}
}
#[test]
fn fastpath_u_render_equivalent() {
use geo::{Area, Contains};
let u = u_shape();
let bounds = TileBounds::new(0.0, 4.0, 10.0, 6.0);
let sh = sutherland_hodgman::clip_polygon_sh(&u, &bounds).unwrap();
let io = ioverlay_clip::clip_polygon_ioverlay(&u, &bounds).unwrap();
let notch = Point::new(5.0, 5.0);
assert!(!io.contains(¬ch), "i_overlay: mouth empty");
assert!(
!sh.contains(¬ch),
"S-H: mouth also empty (winding cancels)"
);
assert!(
(sh.unsigned_area() - io.unsigned_area()).abs() < 1e-9,
"areas must match: sh={} io={}",
sh.unsigned_area(),
io.unsigned_area()
);
}
#[test]
fn fastpath_comb_render_equivalent() {
use geo::{Area, Contains};
let comb = Polygon::new(
LineString::from(vec![
Coord { x: 0.0, y: 0.0 },
Coord { x: 15.0, y: 0.0 },
Coord { x: 15.0, y: 10.0 },
Coord { x: 12.0, y: 10.0 },
Coord { x: 12.0, y: 3.0 },
Coord { x: 9.0, y: 3.0 },
Coord { x: 9.0, y: 10.0 },
Coord { x: 6.0, y: 10.0 },
Coord { x: 6.0, y: 3.0 },
Coord { x: 3.0, y: 3.0 },
Coord { x: 3.0, y: 10.0 },
Coord { x: 0.0, y: 10.0 },
Coord { x: 0.0, y: 0.0 },
]),
vec![],
);
let bounds = TileBounds::new(0.0, 4.0, 15.0, 6.0);
assert!(
geometry_is_simple(&Geometry::Polygon(comb.clone())),
"comb must be simple to be an assume_simple case"
);
let sh = sutherland_hodgman::clip_polygon_sh(&comb, &bounds).unwrap();
let io = ioverlay_clip::clip_polygon_ioverlay(&comb, &bounds).unwrap();
let notch1 = Point::new(4.5, 5.0);
let notch2 = Point::new(10.5, 5.0);
assert!(
(sh.unsigned_area() - io.unsigned_area()).abs() < 1e-9,
"areas must match across multiple mouths"
);
assert!(
!sh.contains(¬ch1) && !sh.contains(¬ch2),
"both mouths stay empty under S-H"
);
}
mod world_tests {
use super::*;
use crate::tile::TileCoord;
use crate::world_coord::{lng_lat_to_world, WorldBounds, WorldCoord};
#[test]
fn test_buffer_pixels_to_world_zoom0() {
let buffer = buffer_pixels_to_world(0, 8, 4096);
let expected = (crate::world_coord::WORLD_SCALE * 8 / 4096) as u32;
assert_eq!(buffer, expected);
}
#[test]
fn test_buffer_pixels_to_world_zoom10() {
let buffer = buffer_pixels_to_world(10, 8, 4096);
assert_eq!(buffer, 8192);
}
#[test]
fn test_buffer_pixels_to_world_consistency_with_degrees() {
let tile = TileCoord::new(512, 512, 10);
let tile_bounds = tile.bounds();
let f64_buffer = buffer_pixels_to_degrees(8, &tile_bounds, 4096);
let world_buffer = buffer_pixels_to_world(10, 8, 4096);
let approx_world_from_f64 =
(f64_buffer * crate::world_coord::WORLD_SCALE as f64 / 360.0) as u32;
let ratio = world_buffer as f64 / approx_world_from_f64 as f64;
assert!(
(0.8..=1.2).contains(&ratio),
"Integer buffer ({}) should be roughly consistent with f64 buffer ({} -> ~{} world units), ratio={}",
world_buffer, f64_buffer, approx_world_from_f64, ratio
);
}
#[test]
fn test_clip_point_world_inside() {
let bounds = WorldBounds::new(1000, 1000, 5000, 5000);
let point = WorldCoord::new(3000, 3000);
assert!(clip_point_world(&point, &bounds).is_some());
}
#[test]
fn test_clip_point_world_outside() {
let bounds = WorldBounds::new(1000, 1000, 5000, 5000);
let point = WorldCoord::new(6000, 3000);
assert!(clip_point_world(&point, &bounds).is_none());
}
#[test]
fn test_clip_point_world_on_boundary() {
let bounds = WorldBounds::new(1000, 1000, 5000, 5000);
let point = WorldCoord::new(5000, 3000);
assert!(clip_point_world(&point, &bounds).is_some());
}
#[test]
fn test_clip_polygon_world_fully_inside() {
let bounds = WorldBounds::new(0, 0, 10000, 10000);
let exterior = vec![
WorldCoord::new(2000, 2000),
WorldCoord::new(8000, 2000),
WorldCoord::new(8000, 8000),
WorldCoord::new(2000, 8000),
WorldCoord::new(2000, 2000),
];
let result = clip_polygon_world(&exterior, &[], &bounds);
assert!(result.is_some());
let (ext, _) = result.unwrap();
assert_eq!(ext.len(), exterior.len());
}
#[test]
fn test_clip_polygon_world_fully_outside() {
let bounds = WorldBounds::new(0, 0, 10000, 10000);
let exterior = vec![
WorldCoord::new(20000, 20000),
WorldCoord::new(30000, 20000),
WorldCoord::new(30000, 30000),
WorldCoord::new(20000, 30000),
WorldCoord::new(20000, 20000),
];
let result = clip_polygon_world(&exterior, &[], &bounds);
assert!(result.is_none());
}
#[test]
fn test_clip_polygon_world_partial() {
let bounds = WorldBounds::new(1000, 1000, 5000, 5000);
let exterior = vec![
WorldCoord::new(3000, 2000),
WorldCoord::new(7000, 2000),
WorldCoord::new(7000, 4000),
WorldCoord::new(3000, 4000),
WorldCoord::new(3000, 2000),
];
let result = clip_polygon_world(&exterior, &[], &bounds);
assert!(result.is_some());
let (ext, _) = result.unwrap();
for coord in &ext {
assert!(
coord.x >= bounds.x_min && coord.x <= bounds.x_max,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= bounds.y_min && coord.y <= bounds.y_max,
"y={} out of bounds",
coord.y
);
}
}
#[test]
fn test_polygon_to_world_rings_roundtrip() {
let poly = Polygon::new(
LineString::from(vec![
Coord {
x: -73.985,
y: 40.748,
},
Coord {
x: -73.980,
y: 40.748,
},
Coord {
x: -73.980,
y: 40.752,
},
Coord {
x: -73.985,
y: 40.752,
},
Coord {
x: -73.985,
y: 40.748,
},
]),
vec![],
);
let (ext, ints) = polygon_to_world_rings(&poly);
assert_eq!(ext.len(), 5, "Should have 5 coords (4 vertices + close)");
assert!(ints.is_empty(), "Should have no holes");
for coord in &ext {
assert!(
coord.x > 0 && coord.x < u32::MAX,
"x={} should be in valid range",
coord.x
);
assert!(
coord.y > 0 && coord.y < u32::MAX,
"y={} should be in valid range",
coord.y
);
}
}
#[test]
fn test_worldcoord_bbox_computation() {
let coords = vec![
WorldCoord::new(100, 200),
WorldCoord::new(500, 100),
WorldCoord::new(300, 600),
];
let bbox = worldcoord_bbox(&coords).unwrap();
assert_eq!(bbox.x_min, 100);
assert_eq!(bbox.y_min, 100);
assert_eq!(bbox.x_max, 500);
assert_eq!(bbox.y_max, 600);
}
#[test]
fn test_worldcoord_bbox_empty() {
let coords: Vec<WorldCoord> = vec![];
assert!(worldcoord_bbox(&coords).is_none());
}
#[test]
fn test_clip_polygon_world_with_real_tile() {
let tile = TileCoord::new(150, 192, 9);
let bounds = WorldBounds::from_tile(&tile);
let buffered = WorldBounds::from_tile_with_buffer(&tile, 8, 4096);
let tile_f64 = tile.bounds();
let center_lng = (tile_f64.lng_min + tile_f64.lng_max) / 2.0;
let center_lat = (tile_f64.lat_min + tile_f64.lat_max) / 2.0;
let exterior: Vec<WorldCoord> = vec![
lng_lat_to_world(center_lng, center_lat),
lng_lat_to_world(tile_f64.lng_max + 0.5, center_lat),
lng_lat_to_world(tile_f64.lng_max + 0.5, tile_f64.lat_min - 0.5),
lng_lat_to_world(center_lng, tile_f64.lat_min - 0.5),
lng_lat_to_world(center_lng, center_lat),
];
let result = clip_polygon_world(&exterior, &[], &bounds);
assert!(result.is_some(), "Should intersect the tile");
let result_buffered = clip_polygon_world(&exterior, &[], &buffered);
assert!(
result_buffered.is_some(),
"Should intersect the buffered tile"
);
}
}
}