This is an automated email from the ASF dual-hosted git repository. prantogg pushed a commit to branch orient-ccw in repository https://gitbox.apache.org/repos/asf/sedona-spatialbench.git
commit 4cbcee5123deaf719128ea907346b229421dc043 Author: Pranav Toggi <[email protected]> AuthorDate: Sat Nov 8 10:13:55 2025 -0800 add logic to handle antimeridian crossing geometries --- spatialbench/src/spatial/geometry.rs | 19 +- spatialbench/src/spatial/utils/affine.rs | 15 -- spatialbench/src/spatial/utils/antimeridian.rs | 258 +++++++++++++++++++++++++ spatialbench/src/spatial/utils/mod.rs | 2 + 4 files changed, 277 insertions(+), 17 deletions(-) diff --git a/spatialbench/src/spatial/geometry.rs b/spatialbench/src/spatial/geometry.rs index a0f9aa5..b753d57 100644 --- a/spatialbench/src/spatial/geometry.rs +++ b/spatialbench/src/spatial/geometry.rs @@ -1,4 +1,7 @@ -use crate::spatial::utils::{apply_affine, round_coordinates, wrap_around_longitude}; +use crate::spatial::utils::{ + apply_affine, clamp_polygon_to_dateline, crosses_dateline, round_coordinates, + wrap_around_longitude, +}; use crate::spatial::{GeomType, SpatialConfig}; use geo::orient::Direction; use geo::{coord, Geometry, LineString, Orient, Point, Polygon}; @@ -6,7 +9,7 @@ use rand::rngs::StdRng; use rand::Rng; use std::f64::consts::PI; -const GEOMETRY_PRECISION: f64 = 1_000_000_000.0; +pub const GEOMETRY_PRECISION: f64 = 1_000_000_000.0; pub fn emit_geom( center01: (f64, f64), @@ -54,6 +57,12 @@ pub fn generate_box_geom( .collect(); let mut polygon = Polygon::new(LineString::from(coords), vec![]); + + // Handle polygons crossing the dateline + if crosses_dateline(&polygon) { + polygon = clamp_polygon_to_dateline(&polygon); + } + polygon = polygon.orient(Direction::Default); Geometry::Polygon(polygon) } @@ -94,6 +103,12 @@ pub fn generate_polygon_geom( } let mut polygon = Polygon::new(LineString::from(ring), vec![]); + + // Handle polygons crossing the dateline + if crosses_dateline(&polygon) { + polygon = clamp_polygon_to_dateline(&polygon); + } + polygon = polygon.orient(Direction::Default); Geometry::Polygon(polygon) } diff --git a/spatialbench/src/spatial/utils/affine.rs b/spatialbench/src/spatial/utils/affine.rs index 3e04e4a..5100cad 100644 --- a/spatialbench/src/spatial/utils/affine.rs +++ b/spatialbench/src/spatial/utils/affine.rs @@ -15,18 +15,3 @@ pub fn round_coordinates(x: f64, y: f64, precision: f64) -> (f64, f64) { round_coordinate(y, precision), ) } - -/// Wraps a longitude value to ensure it stays within the valid range of [-180, 180] degrees. -/// -/// Longitude is a circular coordinate: -/// - If longitude exceeds 180°, it wraps around from the eastern hemisphere back to the western hemisphere. -/// - If longitude is below -180°, it wraps around from the western hemisphere back to the eastern hemisphere. -pub fn wrap_around_longitude(mut lon: f64) -> f64 { - while lon > 180.0 { - lon -= 360.0; - } - while lon < -180.0 { - lon += 360.0; - } - lon -} diff --git a/spatialbench/src/spatial/utils/antimeridian.rs b/spatialbench/src/spatial/utils/antimeridian.rs new file mode 100644 index 0000000..b56db3e --- /dev/null +++ b/spatialbench/src/spatial/utils/antimeridian.rs @@ -0,0 +1,258 @@ +use geo::{Centroid, LineString, Polygon}; + +/// Wraps a longitude value to ensure it stays within the valid range of [-180, 180] degrees. +/// +/// Longitude is a circular coordinate: +/// - If longitude exceeds 180°, it wraps around from the eastern hemisphere back to the western hemisphere. +/// - If longitude is below -180°, it wraps around from the western hemisphere back to the eastern hemisphere. +pub fn wrap_around_longitude(mut lon: f64) -> f64 { + while lon > 180.0 { + lon -= 360.0; + } + while lon < -180.0 { + lon += 360.0; + } + lon +} + +/// Checks if a polygon crosses the dateline (antimeridian at ±180°) +pub fn crosses_dateline(polygon: &Polygon) -> bool { + let coords = polygon.exterior().coords(); + let mut has_east = false; + let mut has_west = false; + + for coord in coords { + if (coord.x > 90.0 && coord.x <= 180.0) || coord.x < -180.0 { + has_east = true; + } + if coord.x > 180.0 || (coord.x >= -180.0 && coord.x < -90.0) { + has_west = true; + } + if has_east && has_west { + return true; + } + } + false +} + +/// Clamps a polygon's longitude coordinates to one side of the antimeridian (±180°). +/// +/// When a polygon crosses the dateline, this function constrains its coordinates to remain +/// on either the eastern (0° to 180°) or western (-180° to 0°) hemisphere based on where +/// the polygon's centroid is located. +/// +/// # Behavior +/// - If the centroid is in the eastern hemisphere (≥ 0°), coordinates are clamped to [0°, 180°] +/// or kept at their original values if already within [-180°, 0°] +/// - If the centroid is in the western hemisphere (< 0°), coordinates are clamped to [-180°, 0°] +/// or kept at their original values if already within [0°, 180°] +/// - Latitude values (y-coordinates) remain unchanged +/// +pub fn clamp_polygon_to_dateline(polygon: &Polygon) -> Polygon { + let centroid = polygon.centroid().expect("Polygon should have centroid"); + let east_bound = centroid.x() >= 0.0; + let keep_east = (centroid.x() >= 0.0 && centroid.x() <= 180.0) || (centroid.x() < -180.0); + + let exterior_coords: Vec<_> = polygon + .exterior() + .coords() + .map(|coord| { + let clamped_x = if keep_east { + if east_bound { + coord.x.clamp(0.0, 180.0) + } else { + coord.x.max(-180.0) + } + } else if east_bound { + coord.x.min(180.0) + } else { + coord.x.clamp(-180.0, 0.0) + }; + geo::Coord { + x: clamped_x, + y: coord.y, + } + }) + .collect(); + + if exterior_coords.len() >= 4 { + Polygon::new(LineString::from(exterior_coords), vec![]) + } else { + polygon.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use geo::polygon; + + #[test] + fn test_wrap_around_longitude_positive_overflow() { + assert_eq!(wrap_around_longitude(190.0), -170.0); + assert_eq!(wrap_around_longitude(240.0), -120.0); + } + + #[test] + fn test_wrap_around_longitude_negative_overflow() { + assert_eq!(wrap_around_longitude(-190.0), 170.0); + assert_eq!(wrap_around_longitude(-240.0), 120.0); + } + + #[test] + fn test_wrap_around_longitude_within_range() { + assert_eq!(wrap_around_longitude(0.0), 0.0); + assert_eq!(wrap_around_longitude(90.0), 90.0); + assert_eq!(wrap_around_longitude(-90.0), -90.0); + assert_eq!(wrap_around_longitude(180.0), 180.0); + assert_eq!(wrap_around_longitude(-180.0), -180.0); + } + + #[test] + fn test_crosses_dateline_no_crossing() { + 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), + ]; + assert!(!crosses_dateline(&poly)); + } + + #[test] + fn test_crosses_dateline_crossing() { + let mut poly = polygon![ + (x: 170.0, y: 10.0), + (x: -170.0, y: 10.0), + (x: -170.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + assert!(crosses_dateline(&poly)); + + poly = polygon![ + (x: -160.0, y: 10.0), + (x: -170.0, y: 10.0), + (x: -170.0, y: 20.0), + (x: -160.0, y: 20.0), + (x: -160.0, y: 10.0), + ]; + assert!(!crosses_dateline(&poly)); + } + + #[test] + fn test_clamp_polygon_to_dateline_positive_side() { + let mut poly = polygon![ + (x: 170.0, y: 10.0), + (x: 180.0, y: 10.0), + (x: 180.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + let mut clamped = clamp_polygon_to_dateline(&poly); + + // Polygon should be preserved appropriately + assert_eq!(clamped, poly); + + poly = polygon![ + (x: 170.0, y: 10.0), + (x: 185.0, y: 10.0), + (x: 185.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + clamped = clamp_polygon_to_dateline(&poly); + + let expected = polygon![ + (x: 170.0, y: 10.0), + (x: 180.0, y: 10.0), + (x: 180.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + + // Polygon should be clamped appropriately + assert_eq!(clamped, expected); + } + + #[test] + fn test_clamp_polygon_to_dateline_with_centroid_on_dateline() { + // East bound polygon + let mut poly = polygon![ + (x: 170.0, y: 10.0), + (x: 190.0, y: 10.0), + (x: 190.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + let mut clamped = clamp_polygon_to_dateline(&poly); + + let mut expected = polygon![ + (x: 170.0, y: 10.0), + (x: 180.0, y: 10.0), + (x: 180.0, y: 20.0), + (x: 170.0, y: 20.0), + (x: 170.0, y: 10.0), + ]; + + // Polygon should be preserved appropriately + assert_eq!(clamped, expected); + + // West bound polygon + poly = polygon![ + (x: -170.0, y: 10.0), + (x: -190.0, y: 10.0), + (x: -190.0, y: 20.0), + (x: -170.0, y: 20.0), + (x: -170.0, y: 10.0), + ]; + clamped = clamp_polygon_to_dateline(&poly); + + expected = polygon![ + (x: -170.0, y: 10.0), + (x: -180.0, y: 10.0), + (x: -180.0, y: 20.0), + (x: -170.0, y: 20.0), + (x: -170.0, y: 10.0), + ]; + + // Polygon should be clamped appropriately + assert_eq!(clamped, expected); + } + + #[test] + fn test_clamp_polygon_to_dateline_negative_side() { + let mut poly = polygon![ + (x: -170.0, y: 10.0), + (x: -180.0, y: 10.0), + (x: -180.0, y: 20.0), + (x: -170.0, y: 20.0), + (x: -170.0, y: 10.0), + ]; + let mut clamped = clamp_polygon_to_dateline(&poly); + + // Polygon should be preserved appropriately + assert_eq!(clamped, poly); + + poly = polygon![ + (x: -170.0, y: 10.0), + (x: -185.0, y: 10.0), + (x: -185.0, y: 20.0), + (x: -170.0, y: 20.0), + (x: -170.0, y: 10.0), + ]; + clamped = clamp_polygon_to_dateline(&poly); + + let expected = polygon![ + (x: -170.0, y: 10.0), + (x: -180.0, y: 10.0), + (x: -180.0, y: 20.0), + (x: -170.0, y: 20.0), + (x: -170.0, y: 10.0), + ]; + + // Polygon should be clamped appropriately + assert_eq!(clamped, expected); + } +} diff --git a/spatialbench/src/spatial/utils/mod.rs b/spatialbench/src/spatial/utils/mod.rs index 349537a..ab79032 100644 --- a/spatialbench/src/spatial/utils/mod.rs +++ b/spatialbench/src/spatial/utils/mod.rs @@ -1,7 +1,9 @@ pub mod affine; +mod antimeridian; pub mod continent; pub mod random; pub use affine::*; +pub use antimeridian::*; pub use continent::*; pub use random::*;
