use geo::{Coord, Geometry, LineString, MultiPolygon, Polygon};
use crate::tile::TileBounds;
#[derive(Debug, Clone, Copy)]
enum Edge {
Left,
Right,
Bottom,
Top,
}
#[inline]
fn is_inside(coord: &Coord<f64>, edge: Edge, bounds: &TileBounds) -> bool {
match edge {
Edge::Left => coord.x >= bounds.lng_min,
Edge::Right => coord.x <= bounds.lng_max,
Edge::Bottom => coord.y >= bounds.lat_min,
Edge::Top => coord.y <= bounds.lat_max,
}
}
#[inline]
fn intersect(a: &Coord<f64>, b: &Coord<f64>, edge: Edge, bounds: &TileBounds) -> Coord<f64> {
match edge {
Edge::Left => {
let t = (bounds.lng_min - a.x) / (b.x - a.x);
Coord {
x: bounds.lng_min,
y: a.y + t * (b.y - a.y),
}
}
Edge::Right => {
let t = (bounds.lng_max - a.x) / (b.x - a.x);
Coord {
x: bounds.lng_max,
y: a.y + t * (b.y - a.y),
}
}
Edge::Bottom => {
let t = (bounds.lat_min - a.y) / (b.y - a.y);
Coord {
x: a.x + t * (b.x - a.x),
y: bounds.lat_min,
}
}
Edge::Top => {
let t = (bounds.lat_max - a.y) / (b.y - a.y);
Coord {
x: a.x + t * (b.x - a.x),
y: bounds.lat_max,
}
}
}
}
fn clip_ring_against_edge(ring: &[Coord<f64>], edge: Edge, bounds: &TileBounds) -> Vec<Coord<f64>> {
if ring.is_empty() {
return Vec::new();
}
let mut output = Vec::with_capacity(ring.len());
let n = if ring.len() >= 2 && ring[0] == ring[ring.len() - 1] {
ring.len() - 1
} else {
ring.len()
};
if n == 0 {
return Vec::new();
}
let mut s = &ring[n - 1];
for e in ring.iter().take(n) {
let e_inside = is_inside(e, edge, bounds);
let s_inside = is_inside(s, edge, bounds);
match (s_inside, e_inside) {
(true, true) => {
output.push(*e);
}
(true, false) => {
output.push(intersect(s, e, edge, bounds));
}
(false, true) => {
output.push(intersect(s, e, edge, bounds));
output.push(*e);
}
(false, false) => {
}
}
s = e;
}
output
}
fn clip_ring(ring: &[Coord<f64>], bounds: &TileBounds) -> Vec<Coord<f64>> {
let edges = [Edge::Left, Edge::Right, Edge::Bottom, Edge::Top];
let mut current = ring.to_vec();
for &edge in &edges {
if current.is_empty() {
return Vec::new();
}
current = clip_ring_against_edge(¤t, edge, bounds);
}
if current.len() >= 3 {
if current[0] != current[current.len() - 1] {
current.push(current[0]);
}
current
} else {
Vec::new()
}
}
pub fn clip_polygon_sh(poly: &Polygon<f64>, bounds: &TileBounds) -> Option<Geometry<f64>> {
let clipped_exterior = clip_ring(poly.exterior().0.as_slice(), bounds);
if clipped_exterior.is_empty() {
return None;
}
let clipped_interiors: Vec<LineString<f64>> = poly
.interiors()
.iter()
.filter_map(|interior| {
let clipped = clip_ring(interior.0.as_slice(), bounds);
if clipped.len() >= 4 {
Some(LineString::new(clipped))
} else {
None
}
})
.collect();
Some(Geometry::Polygon(Polygon::new(
LineString::new(clipped_exterior),
clipped_interiors,
)))
}
pub fn clip_multipolygon_sh(
mp: &MultiPolygon<f64>,
bounds: &TileBounds,
) -> Option<MultiPolygon<f64>> {
let mut clipped_polys = Vec::new();
for poly in &mp.0 {
if let Some(Geometry::Polygon(clipped)) = clip_polygon_sh(poly, bounds) {
clipped_polys.push(clipped);
}
}
if clipped_polys.is_empty() {
None
} else {
Some(MultiPolygon::new(clipped_polys))
}
}
use crate::world_coord::{WorldBounds, WorldCoord};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClipCoord {
pub x: i64,
pub y: i64,
}
impl ClipCoord {
#[inline]
pub const fn new(x: i64, y: i64) -> Self {
Self { x, y }
}
#[inline]
pub fn from_world(coord: WorldCoord) -> Self {
Self {
x: coord.x as i64,
y: coord.y as i64,
}
}
#[inline]
pub fn to_world(&self) -> WorldCoord {
WorldCoord::new(
self.x.clamp(0, u32::MAX as i64) as u32,
self.y.clamp(0, u32::MAX as i64) as u32,
)
}
}
#[derive(Debug, Clone, Copy)]
struct ClipBounds {
x_min: i64,
y_min: i64,
x_max: i64,
y_max: i64,
}
impl ClipBounds {
fn from_world_bounds(bounds: &WorldBounds) -> Self {
Self {
x_min: bounds.x_min as i64,
y_min: bounds.y_min as i64,
x_max: bounds.x_max as i64,
y_max: bounds.y_max as i64,
}
}
}
#[inline]
fn is_inside_world(coord: &ClipCoord, edge: Edge, bounds: &ClipBounds) -> bool {
match edge {
Edge::Left => coord.x >= bounds.x_min,
Edge::Right => coord.x <= bounds.x_max,
Edge::Top => coord.y >= bounds.y_min,
Edge::Bottom => coord.y <= bounds.y_max,
}
}
#[inline]
fn intersect_world(a: &ClipCoord, b: &ClipCoord, edge: Edge, bounds: &ClipBounds) -> ClipCoord {
match edge {
Edge::Left => {
let dx = b.x - a.x;
if dx == 0 {
return ClipCoord::new(bounds.x_min, a.y);
}
let t_num = bounds.x_min - a.x;
let dy = b.y - a.y;
let y = a.y + ((t_num as i128 * dy as i128) / dx as i128) as i64;
ClipCoord::new(bounds.x_min, y)
}
Edge::Right => {
let dx = b.x - a.x;
if dx == 0 {
return ClipCoord::new(bounds.x_max, a.y);
}
let t_num = bounds.x_max - a.x;
let dy = b.y - a.y;
let y = a.y + ((t_num as i128 * dy as i128) / dx as i128) as i64;
ClipCoord::new(bounds.x_max, y)
}
Edge::Top => {
let dy = b.y - a.y;
if dy == 0 {
return ClipCoord::new(a.x, bounds.y_min);
}
let t_num = bounds.y_min - a.y;
let dx = b.x - a.x;
let x = a.x + ((t_num as i128 * dx as i128) / dy as i128) as i64;
ClipCoord::new(x, bounds.y_min)
}
Edge::Bottom => {
let dy = b.y - a.y;
if dy == 0 {
return ClipCoord::new(a.x, bounds.y_max);
}
let t_num = bounds.y_max - a.y;
let dx = b.x - a.x;
let x = a.x + ((t_num as i128 * dx as i128) / dy as i128) as i64;
ClipCoord::new(x, bounds.y_max)
}
}
}
fn clip_ring_against_edge_world(
ring: &[ClipCoord],
edge: Edge,
bounds: &ClipBounds,
) -> Vec<ClipCoord> {
if ring.is_empty() {
return Vec::new();
}
let mut output = Vec::with_capacity(ring.len());
let n = if ring.len() >= 2 && ring[0] == ring[ring.len() - 1] {
ring.len() - 1
} else {
ring.len()
};
if n == 0 {
return Vec::new();
}
let mut s = &ring[n - 1];
for e in ring.iter().take(n) {
let e_inside = is_inside_world(e, edge, bounds);
let s_inside = is_inside_world(s, edge, bounds);
match (s_inside, e_inside) {
(true, true) => {
output.push(*e);
}
(true, false) => {
output.push(intersect_world(s, e, edge, bounds));
}
(false, true) => {
output.push(intersect_world(s, e, edge, bounds));
output.push(*e);
}
(false, false) => {}
}
s = e;
}
output
}
fn clip_ring_world(coords: &[ClipCoord], bounds: &WorldBounds) -> Vec<ClipCoord> {
let clip_bounds = ClipBounds::from_world_bounds(bounds);
let edges = [Edge::Left, Edge::Right, Edge::Top, Edge::Bottom];
let mut current = coords.to_vec();
for &edge in &edges {
if current.is_empty() {
return Vec::new();
}
current = clip_ring_against_edge_world(¤t, edge, &clip_bounds);
}
if current.len() >= 3 {
if current[0] != current[current.len() - 1] {
current.push(current[0]);
}
current
} else {
Vec::new()
}
}
pub fn clip_polygon_sh_world(
exterior: &[WorldCoord],
interiors: &[Vec<WorldCoord>],
bounds: &WorldBounds,
) -> Option<(Vec<WorldCoord>, Vec<Vec<WorldCoord>>)> {
let ext_clip: Vec<ClipCoord> = exterior.iter().map(|c| ClipCoord::from_world(*c)).collect();
let clipped_ext = clip_ring_world(&ext_clip, bounds);
if clipped_ext.is_empty() {
return None;
}
let result_exterior: Vec<WorldCoord> = clipped_ext.iter().map(|c| c.to_world()).collect();
let result_interiors: Vec<Vec<WorldCoord>> = interiors
.iter()
.filter_map(|interior| {
let int_clip: Vec<ClipCoord> =
interior.iter().map(|c| ClipCoord::from_world(*c)).collect();
let clipped = clip_ring_world(&int_clip, bounds);
if clipped.len() >= 4 {
Some(clipped.iter().map(|c| c.to_world()).collect())
} else {
None
}
})
.collect();
Some((result_exterior, result_interiors))
}
#[cfg(test)]
mod tests {
use super::*;
use geo::polygon;
fn test_bounds() -> TileBounds {
TileBounds::new(0.0, 0.0, 10.0, 10.0)
}
fn unit_bounds() -> TileBounds {
TileBounds::new(0.0, 0.0, 1.0, 1.0)
}
#[test]
fn test_is_inside_left() {
let bounds = test_bounds();
assert!(is_inside(&Coord { x: 5.0, y: 5.0 }, Edge::Left, &bounds));
assert!(is_inside(&Coord { x: 0.0, y: 5.0 }, Edge::Left, &bounds)); assert!(!is_inside(&Coord { x: -1.0, y: 5.0 }, Edge::Left, &bounds));
}
#[test]
fn test_is_inside_right() {
let bounds = test_bounds();
assert!(is_inside(&Coord { x: 5.0, y: 5.0 }, Edge::Right, &bounds));
assert!(is_inside(&Coord { x: 10.0, y: 5.0 }, Edge::Right, &bounds)); assert!(!is_inside(&Coord { x: 11.0, y: 5.0 }, Edge::Right, &bounds));
}
#[test]
fn test_is_inside_bottom() {
let bounds = test_bounds();
assert!(is_inside(&Coord { x: 5.0, y: 5.0 }, Edge::Bottom, &bounds));
assert!(is_inside(&Coord { x: 5.0, y: 0.0 }, Edge::Bottom, &bounds)); assert!(!is_inside(
&Coord { x: 5.0, y: -1.0 },
Edge::Bottom,
&bounds
));
}
#[test]
fn test_is_inside_top() {
let bounds = test_bounds();
assert!(is_inside(&Coord { x: 5.0, y: 5.0 }, Edge::Top, &bounds));
assert!(is_inside(&Coord { x: 5.0, y: 10.0 }, Edge::Top, &bounds)); assert!(!is_inside(&Coord { x: 5.0, y: 11.0 }, Edge::Top, &bounds));
}
#[test]
fn test_intersect_left() {
let bounds = test_bounds();
let a = Coord { x: -5.0, y: 5.0 };
let b = Coord { x: 5.0, y: 5.0 };
let result = intersect(&a, &b, Edge::Left, &bounds);
assert!((result.x - 0.0).abs() < 1e-10);
assert!((result.y - 5.0).abs() < 1e-10);
}
#[test]
fn test_intersect_right() {
let bounds = test_bounds();
let a = Coord { x: 5.0, y: 5.0 };
let b = Coord { x: 15.0, y: 5.0 };
let result = intersect(&a, &b, Edge::Right, &bounds);
assert!((result.x - 10.0).abs() < 1e-10);
assert!((result.y - 5.0).abs() < 1e-10);
}
#[test]
fn test_intersect_bottom() {
let bounds = test_bounds();
let a = Coord { x: 5.0, y: -5.0 };
let b = Coord { x: 5.0, y: 5.0 };
let result = intersect(&a, &b, Edge::Bottom, &bounds);
assert!((result.x - 5.0).abs() < 1e-10);
assert!((result.y - 0.0).abs() < 1e-10);
}
#[test]
fn test_intersect_top() {
let bounds = test_bounds();
let a = Coord { x: 5.0, y: 5.0 };
let b = Coord { x: 5.0, y: 15.0 };
let result = intersect(&a, &b, Edge::Top, &bounds);
assert!((result.x - 5.0).abs() < 1e-10);
assert!((result.y - 10.0).abs() < 1e-10);
}
#[test]
fn test_clip_ring_fully_inside() {
let bounds = test_bounds();
let ring = 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 },
];
let result = clip_ring(&ring, &bounds);
assert!(!result.is_empty());
assert_eq!(result.len(), 5); }
#[test]
fn test_clip_ring_fully_outside() {
let bounds = test_bounds();
let ring = 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 },
];
let result = clip_ring(&ring, &bounds);
assert!(result.is_empty());
}
#[test]
fn test_clip_ring_partial_overlap() {
let bounds = test_bounds();
let ring = 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 },
];
let result = clip_ring(&ring, &bounds);
assert!(!result.is_empty());
for coord in &result {
assert!(
coord.x >= -1e-10 && coord.x <= 10.0 + 1e-10,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= -1e-10 && coord.y <= 10.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
#[test]
fn test_clip_ring_crossing_one_edge() {
let bounds = test_bounds();
let ring = vec![
Coord { x: 5.0, y: -5.0 },
Coord { x: 15.0, y: -5.0 },
Coord { x: 15.0, y: 5.0 },
Coord { x: 5.0, y: 5.0 },
Coord { x: 5.0, y: -5.0 },
];
let result = clip_ring(&ring, &bounds);
assert!(!result.is_empty());
for coord in &result {
assert!(
coord.x >= -1e-10 && coord.x <= 10.0 + 1e-10,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= -1e-10 && coord.y <= 10.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
#[test]
fn test_clip_ring_triangle() {
let bounds = test_bounds();
let ring = vec![
Coord { x: 5.0, y: 5.0 },
Coord { x: 15.0, y: 5.0 },
Coord { x: 5.0, y: 15.0 },
Coord { x: 5.0, y: 5.0 },
];
let result = clip_ring(&ring, &bounds);
assert!(!result.is_empty());
for coord in &result {
assert!(
coord.x >= -1e-10 && coord.x <= 10.0 + 1e-10,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= -1e-10 && coord.y <= 10.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
#[test]
fn test_clip_polygon_fully_inside() {
let bounds = test_bounds();
let poly = polygon![
(x: 2.0, y: 2.0),
(x: 8.0, y: 2.0),
(x: 8.0, y: 8.0),
(x: 2.0, y: 8.0),
(x: 2.0, y: 2.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
assert!(p.exterior().coords().count() >= 4);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_polygon_fully_outside() {
let bounds = test_bounds();
let poly = polygon![
(x: 20.0, y: 20.0),
(x: 30.0, y: 20.0),
(x: 30.0, y: 30.0),
(x: 20.0, y: 30.0),
(x: 20.0, y: 20.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_none());
}
#[test]
fn test_clip_polygon_partial_overlap() {
let bounds = test_bounds();
let poly = polygon![
(x: -5.0, y: -5.0),
(x: 5.0, y: -5.0),
(x: 5.0, y: 5.0),
(x: -5.0, y: 5.0),
(x: -5.0, y: -5.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
for coord in p.exterior().coords() {
assert!(
coord.x >= -1e-10 && coord.x <= 10.0 + 1e-10,
"x={} out of bounds",
coord.x
);
assert!(
coord.y >= -1e-10 && coord.y <= 10.0 + 1e-10,
"y={} out of bounds",
coord.y
);
}
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_polygon_with_hole() {
let bounds = unit_bounds();
let poly = polygon![
exterior: [
(x: -0.5, y: -0.5),
(x: 1.5, y: -0.5),
(x: 1.5, y: 1.5),
(x: -0.5, y: 1.5),
(x: -0.5, y: -0.5),
],
interiors: [
[
(x: 0.3, y: 0.3),
(x: 0.7, y: 0.3),
(x: 0.7, y: 0.7),
(x: 0.3, y: 0.7),
(x: 0.3, y: 0.3),
],
],
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
assert!(p.exterior().coords().count() >= 4);
assert_eq!(p.interiors().len(), 1);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_polygon_hole_outside_bounds() {
let bounds = unit_bounds();
let poly = polygon![
exterior: [
(x: -0.5, y: -0.5),
(x: 1.5, y: -0.5),
(x: 1.5, y: 1.5),
(x: -0.5, y: 1.5),
(x: -0.5, y: -0.5),
],
interiors: [
[
(x: 2.0, y: 2.0),
(x: 3.0, y: 2.0),
(x: 3.0, y: 3.0),
(x: 2.0, y: 3.0),
(x: 2.0, y: 2.0),
],
],
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
assert_eq!(p.interiors().len(), 0);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_multipolygon() {
let bounds = test_bounds();
let mp = MultiPolygon::new(vec![
polygon![
(x: 2.0, y: 2.0),
(x: 4.0, y: 2.0),
(x: 4.0, y: 4.0),
(x: 2.0, y: 4.0),
(x: 2.0, y: 2.0),
],
polygon![
(x: 6.0, y: 6.0),
(x: 8.0, y: 6.0),
(x: 8.0, y: 8.0),
(x: 6.0, y: 8.0),
(x: 6.0, y: 6.0),
],
]);
let result = clip_multipolygon_sh(&mp, &bounds);
assert!(result.is_some());
assert_eq!(result.unwrap().0.len(), 2);
}
#[test]
fn test_clip_multipolygon_one_outside() {
let bounds = test_bounds();
let mp = MultiPolygon::new(vec![
polygon![
(x: 2.0, y: 2.0),
(x: 4.0, y: 2.0),
(x: 4.0, y: 4.0),
(x: 2.0, y: 4.0),
(x: 2.0, y: 2.0),
],
polygon![
(x: 20.0, y: 20.0),
(x: 30.0, y: 20.0),
(x: 30.0, y: 30.0),
(x: 20.0, y: 30.0),
(x: 20.0, y: 20.0),
],
]);
let result = clip_multipolygon_sh(&mp, &bounds);
assert!(result.is_some());
assert_eq!(result.unwrap().0.len(), 1);
}
#[test]
fn test_clip_multipolygon_all_outside() {
let bounds = test_bounds();
let mp = MultiPolygon::new(vec![polygon![
(x: 20.0, y: 20.0),
(x: 30.0, y: 20.0),
(x: 30.0, y: 30.0),
(x: 20.0, y: 30.0),
(x: 20.0, y: 20.0),
]]);
let result = clip_multipolygon_sh(&mp, &bounds);
assert!(result.is_none());
}
#[test]
fn test_clip_produces_correct_area() {
use geo::Area;
let bounds = test_bounds();
let poly = polygon![
(x: 5.0, y: 5.0),
(x: 15.0, y: 5.0),
(x: 15.0, y: 15.0),
(x: 5.0, y: 15.0),
(x: 5.0, y: 5.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
let area = p.unsigned_area();
assert!(
(area - 25.0).abs() < 0.01,
"Expected area ~25.0, got {}",
area
);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_large_polygon_spanning_bounds() {
use geo::Area;
let bounds = test_bounds();
let poly = polygon![
(x: -10.0, y: -10.0),
(x: 20.0, y: -10.0),
(x: 20.0, y: 20.0),
(x: -10.0, y: 20.0),
(x: -10.0, y: -10.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
match result.unwrap() {
Geometry::Polygon(p) => {
let area = p.unsigned_area();
assert!(
(area - 100.0).abs() < 0.01,
"Expected area ~100.0, got {}",
area
);
}
_ => panic!("Expected Polygon"),
}
}
#[test]
fn test_clip_many_vertex_polygon() {
let bounds = test_bounds();
let n = 10_000;
let mut coords: Vec<Coord<f64>> = Vec::with_capacity(n + 1);
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
coords.push(Coord {
x: 5.0 + 8.0 * angle.cos(),
y: 5.0 + 8.0 * angle.sin(),
});
}
coords.push(coords[0]);
let poly = Polygon::new(LineString::new(coords), vec![]);
let start = std::time::Instant::now();
let result = clip_polygon_sh(&poly, &bounds);
let elapsed = start.elapsed();
assert!(result.is_some());
assert!(
elapsed.as_millis() < 100,
"Clipping {} vertices took {}ms (should be <100ms)",
n,
elapsed.as_millis()
);
}
#[test]
fn test_clip_huge_vertex_polygon() {
let bounds = TileBounds::new(-67.50, -66.51, -56.25, -61.61);
let n = 316_000;
let mut coords: Vec<Coord<f64>> = Vec::with_capacity(n + 1);
for i in 0..n {
let t = i as f64 / n as f64;
let x = -80.0 + t * 40.0; let y = -70.0 + (i % 100) as f64 * 0.1; coords.push(Coord { x, y });
}
coords.push(coords[0]);
let poly = Polygon::new(LineString::new(coords), vec![]);
let start = std::time::Instant::now();
let result = clip_polygon_sh(&poly, &bounds);
let elapsed = start.elapsed();
assert!(
elapsed.as_secs_f64() < 0.5,
"Clipping {} vertices took {:.3}s (should be <0.5s, i_overlay took ~10s)",
n,
elapsed.as_secs_f64()
);
eprintln!(
"SH clip of {}k vertices: {:.3}s (result: {})",
n / 1000,
elapsed.as_secs_f64(),
if result.is_some() { "some" } else { "none" }
);
}
#[test]
fn test_clip_polygon_on_boundary() {
let bounds = test_bounds();
let poly = polygon![
(x: 0.0, y: 0.0),
(x: 10.0, y: 0.0),
(x: 10.0, y: 10.0),
(x: 0.0, y: 10.0),
(x: 0.0, y: 0.0),
];
let result = clip_polygon_sh(&poly, &bounds);
assert!(result.is_some());
}
#[test]
fn test_clip_polygon_touching_corner() {
let bounds = test_bounds();
let poly = polygon![
(x: 10.0, y: 10.0),
(x: 15.0, y: 10.0),
(x: 10.0, y: 15.0),
(x: 10.0, y: 10.0),
];
let _ = clip_polygon_sh(&poly, &bounds);
}
#[test]
fn test_clip_empty_ring() {
let bounds = test_bounds();
let ring: Vec<Coord<f64>> = vec![];
let result = clip_ring(&ring, &bounds);
assert!(result.is_empty());
}
mod world_tests {
use super::*;
use crate::world_coord::{WorldBounds, WorldCoord};
fn test_world_bounds() -> WorldBounds {
WorldBounds::new(1000, 1000, 5000, 5000)
}
#[test]
fn test_clip_coord_roundtrip() {
let world = WorldCoord::new(12345, 67890);
let clip = ClipCoord::from_world(world);
let back = clip.to_world();
assert_eq!(world, back);
}
#[test]
fn test_clip_coord_clamping() {
let clip = ClipCoord::new(-100, -200);
let world = clip.to_world();
assert_eq!(world.x, 0);
assert_eq!(world.y, 0);
let clip = ClipCoord::new(u32::MAX as i64 + 100, u32::MAX as i64 + 200);
let world = clip.to_world();
assert_eq!(world.x, u32::MAX);
assert_eq!(world.y, u32::MAX);
}
#[test]
fn test_world_sh_fully_inside() {
let bounds = test_world_bounds();
let exterior = vec![
WorldCoord::new(2000, 2000),
WorldCoord::new(4000, 2000),
WorldCoord::new(4000, 4000),
WorldCoord::new(2000, 4000),
WorldCoord::new(2000, 2000), ];
let result = clip_polygon_sh_world(&exterior, &[], &bounds);
assert!(result.is_some(), "Fully inside polygon should be preserved");
let (ext, ints) = result.unwrap();
assert!(ext.len() >= 4, "Should have at least 4 vertices");
assert!(ints.is_empty(), "No holes expected");
for coord in &ext {
assert!(
coord.x >= bounds.x_min && coord.x <= bounds.x_max,
"x={} out of bounds [{}, {}]",
coord.x,
bounds.x_min,
bounds.x_max
);
assert!(
coord.y >= bounds.y_min && coord.y <= bounds.y_max,
"y={} out of bounds [{}, {}]",
coord.y,
bounds.y_min,
bounds.y_max
);
}
}
#[test]
fn test_world_sh_fully_outside() {
let bounds = test_world_bounds();
let exterior = vec![
WorldCoord::new(6000, 6000),
WorldCoord::new(8000, 6000),
WorldCoord::new(8000, 8000),
WorldCoord::new(6000, 8000),
WorldCoord::new(6000, 6000),
];
let result = clip_polygon_sh_world(&exterior, &[], &bounds);
assert!(result.is_none(), "Fully outside polygon should return None");
}
#[test]
fn test_world_sh_partial_clip_right_edge() {
let bounds = test_world_bounds(); 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_sh_world(&exterior, &[], &bounds);
assert!(
result.is_some(),
"Partially overlapping should produce output"
);
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_world_sh_partial_clip_corner() {
let bounds = test_world_bounds(); let exterior = vec![
WorldCoord::new(0, 0),
WorldCoord::new(3000, 0),
WorldCoord::new(3000, 3000),
WorldCoord::new(0, 3000),
WorldCoord::new(0, 0),
];
let result = clip_polygon_sh_world(&exterior, &[], &bounds);
assert!(result.is_some(), "Corner-overlap should produce output");
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_world_sh_with_hole() {
let bounds = WorldBounds::new(0, 0, 10000, 10000);
let exterior = vec![
WorldCoord::new(0, 0),
WorldCoord::new(12000, 0),
WorldCoord::new(12000, 12000),
WorldCoord::new(0, 12000),
WorldCoord::new(0, 0),
];
let hole = vec![
WorldCoord::new(3000, 3000),
WorldCoord::new(7000, 3000),
WorldCoord::new(7000, 7000),
WorldCoord::new(3000, 7000),
WorldCoord::new(3000, 3000),
];
let result = clip_polygon_sh_world(&exterior, &[hole], &bounds);
assert!(result.is_some());
let (_, ints) = result.unwrap();
assert_eq!(ints.len(), 1, "Hole inside bounds should be preserved");
}
#[test]
fn test_world_sh_hole_outside_bounds() {
let bounds = WorldBounds::new(0, 0, 10000, 10000);
let exterior = vec![
WorldCoord::new(0, 0),
WorldCoord::new(12000, 0),
WorldCoord::new(12000, 12000),
WorldCoord::new(0, 12000),
WorldCoord::new(0, 0),
];
let hole = 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_sh_world(&exterior, &[hole], &bounds);
assert!(result.is_some());
let (_, ints) = result.unwrap();
assert_eq!(ints.len(), 0, "Hole outside bounds should be removed");
}
#[test]
fn test_world_sh_consistency_with_f64() {
use crate::tile::TileCoord;
use crate::world_coord::lng_lat_to_world;
let tile = TileCoord::new(8, 5, 4);
let tile_bounds_f64 = tile.bounds();
let tile_bounds_world = WorldBounds::from_tile(&tile);
let tile_center_lng = (tile_bounds_f64.lng_min + tile_bounds_f64.lng_max) / 2.0;
let tile_center_lat = (tile_bounds_f64.lat_min + tile_bounds_f64.lat_max) / 2.0;
let poly_f64 = Polygon::new(
LineString::from(vec![
Coord {
x: tile_center_lng,
y: tile_center_lat,
},
Coord {
x: tile_bounds_f64.lng_max + 5.0,
y: tile_center_lat,
},
Coord {
x: tile_bounds_f64.lng_max + 5.0,
y: tile_bounds_f64.lat_min - 5.0,
},
Coord {
x: tile_center_lng,
y: tile_bounds_f64.lat_min - 5.0,
},
Coord {
x: tile_center_lng,
y: tile_center_lat,
},
]),
vec![],
);
let f64_result = clip_polygon_sh(&poly_f64, &tile_bounds_f64);
assert!(f64_result.is_some(), "f64 clip should produce output");
let world_exterior: Vec<WorldCoord> = poly_f64
.exterior()
.coords()
.map(|c| lng_lat_to_world(c.x, c.y))
.collect();
let world_result = clip_polygon_sh_world(&world_exterior, &[], &tile_bounds_world);
assert!(
world_result.is_some(),
"WorldCoord clip should produce output"
);
let f64_count = match f64_result.unwrap() {
Geometry::Polygon(p) => p.exterior().coords().count(),
_ => panic!("Expected Polygon from f64 clip"),
};
let (world_ext, _) = world_result.unwrap();
let world_count = world_ext.len();
assert!(
(f64_count as i32 - world_count as i32).unsigned_abs() <= 2,
"Vertex count mismatch: f64={}, world={}",
f64_count,
world_count
);
}
#[test]
fn test_world_sh_large_coordinate_values() {
let bounds = WorldBounds::new(
u32::MAX - 10000,
u32::MAX - 10000,
u32::MAX - 1000,
u32::MAX - 1000,
);
let exterior = vec![
WorldCoord::new(u32::MAX - 8000, u32::MAX - 8000),
WorldCoord::new(u32::MAX - 2000, u32::MAX - 8000),
WorldCoord::new(u32::MAX - 2000, u32::MAX - 2000),
WorldCoord::new(u32::MAX - 8000, u32::MAX - 2000),
WorldCoord::new(u32::MAX - 8000, u32::MAX - 8000),
];
let result = clip_polygon_sh_world(&exterior, &[], &bounds);
assert!(result.is_some(), "Should handle coordinates near u32::MAX");
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,
bounds.x_min,
bounds.x_max
);
}
}
#[test]
fn test_world_sh_performance_10k_vertices() {
let bounds = WorldBounds::new(1000, 1000, 5000, 5000);
let n = 10_000usize;
let center_x = 3000i64;
let center_y = 3000i64;
let radius = 4000i64;
let mut exterior = Vec::with_capacity(n + 1);
for i in 0..n {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64);
let x = center_x + (radius as f64 * angle.cos()) as i64;
let y = center_y + (radius as f64 * angle.sin()) as i64;
exterior.push(WorldCoord::new(
x.clamp(0, u32::MAX as i64) as u32,
y.clamp(0, u32::MAX as i64) as u32,
));
}
exterior.push(exterior[0]);
let start = std::time::Instant::now();
let result = clip_polygon_sh_world(&exterior, &[], &bounds);
let elapsed = start.elapsed();
assert!(result.is_some());
assert!(
elapsed.as_millis() < 100,
"WorldCoord SH clipping {} vertices took {}ms (should be <100ms)",
n,
elapsed.as_millis()
);
}
#[test]
fn test_world_sh_empty_input() {
let bounds = test_world_bounds();
let result = clip_polygon_sh_world(&[], &[], &bounds);
assert!(result.is_none(), "Empty exterior should return None");
}
}
}