use crate::wrap_lon;
pub(crate) type TBounds = (f64, f64, f64, f64);
#[must_use]
pub fn web_geo_bounds_union(bboxes: &[TBounds]) -> Option<TBounds> {
if bboxes.is_empty() {
return None;
}
let (south, north, mut ranges) = collect_minmax_lat_and_lng_ranges(bboxes);
let merged = merge_lng_ranges(&mut ranges);
if merged.len() == 1 {
let (final_west, final_east) = merged[0];
return Some((final_west, south, final_east, north));
}
let (gap_start, gap_end) = largest_lng_range_hole(&merged);
let west = wrap_lon(gap_end);
let east = wrap_lon(gap_start);
Some((west, south, east, north))
}
fn collect_minmax_lat_and_lng_ranges(
bboxes: &[TBounds],
) -> (f64, f64, Vec<(f64, f64)>) {
bboxes.iter().fold(
(f64::INFINITY, f64::NEG_INFINITY, Vec::new()),
|(min_lat, max_lat, mut ranges), &(west, south, east, north)| {
let new_min_lat = min_lat.min(south);
let new_max_lat = max_lat.max(north);
if west > east {
ranges.push((west, 180.0));
ranges.push((-180.0, east));
} else {
ranges.push((west, east));
}
(new_min_lat, new_max_lat, ranges)
},
)
}
fn merge_lng_ranges(ranges: &mut [(f64, f64)]) -> Vec<(f64, f64)> {
if ranges.is_empty() {
return Vec::new();
}
if ranges.len() == 1 {
return vec![ranges[0]];
}
ranges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let mut merged = Vec::with_capacity(ranges.len());
merged.push(ranges[0]);
ranges[1..].iter().fold(merged, |mut acc, &(start, end)| {
if let Some((_prev_start, prev_end)) = acc.last_mut() {
let abs_diff = (start - *prev_end).abs();
if abs_diff <= 0.0001 {
*prev_end = prev_end.max(end);
} else {
acc.push((start, end));
}
}
acc
})
}
fn largest_lng_range_hole(merged: &[(f64, f64)]) -> (f64, f64) {
if merged.is_empty() {
return (0.0, 0.0);
}
let (largest_gap, gap_start, gap_end) = merged.windows(2).fold(
(-1.0, 0.0, 0.0), |(acc_gap, acc_start, acc_end), w| {
let curr_end = w[0].1; let next_start = w[1].0; let gap_size = next_start - curr_end;
if gap_size > acc_gap {
(gap_size, curr_end, next_start)
} else {
(acc_gap, acc_start, acc_end)
}
},
);
let last_end = merged.last().unwrap_or(&(0.0, 0.0)).1;
let first_start = merged.first().unwrap_or(&(0.0, 0.0)).0;
let wrap_size = (first_start + 360.0) - last_end;
if wrap_size > largest_gap {
(last_end, first_start + 360.0)
} else {
(gap_start, gap_end)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn test_single_normal() {
let input = vec![(100.0, -5.0, 120.0, 10.0)];
let bbox = web_geo_bounds_union(&input).unwrap();
assert_eq!(bbox, (100.0, -5.0, 120.0, 10.0));
}
#[test]
fn test_single_crossing_am() {
let input = vec![(170.0, -10.0, -170.0, 5.0)];
let bbox = web_geo_bounds_union(&input).unwrap();
assert_eq!(bbox, (170.0, -10.0, -170.0, 5.0));
}
#[test]
fn test_multiple_merged() {
let input = vec![
(170.0, -10.0, -170.0, 10.0), (-160.0, -20.0, -100.0, 5.0),
(120.0, -15.0, 160.0, 15.0),
];
let bbox = web_geo_bounds_union(&input).unwrap();
let expected: TBounds = (120.0, -20.0, -100.0, 15.0);
assert_eq!(bbox, expected);
}
#[test]
fn test_two_not_crossing_antimeridian() {
let input = vec![(100.0, -5.0, 120.0, 10.0), (110.0, -10.0, 130.0, 5.0)];
let bbox = web_geo_bounds_union(&input).unwrap();
assert_eq!(bbox, (100.0, -10.0, 130.0, 10.0));
}
#[test]
fn test_no_bboxes() {
let input = vec![];
let bbox = web_geo_bounds_union(&input);
assert_eq!(bbox, None);
}
}