#![cfg_attr(
test,
expect(
clippy::cast_possible_truncation,
reason = "sample counts (i32->usize) in test code — always positive"
)
)]
use crate::r1;
use crate::s1;
use crate::s1::{Angle, ChordAngle};
use crate::s2::edge_crossings::{self, Crossing};
use crate::s2::edge_distances;
use crate::s2::{Cap, CellId, LatLng, Point};
use std::f64::consts::{FRAC_PI_2, PI};
use std::fmt;
#[must_use]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum RectVertex {
#[default]
LowerLeft = 0,
LowerRight = 1,
UpperRight = 2,
UpperLeft = 3,
}
impl RectVertex {
pub const ALL: [RectVertex; 4] = [
RectVertex::LowerLeft,
RectVertex::LowerRight,
RectVertex::UpperRight,
RectVertex::UpperLeft,
];
#[inline]
pub fn next(self) -> RectVertex {
RectVertex::ALL[((self as usize) + 1) & 3]
}
#[inline]
pub fn prev(self) -> RectVertex {
RectVertex::ALL[((self as usize) + 3) & 3]
}
pub fn iter() -> impl Iterator<Item = RectVertex> {
RectVertex::ALL.iter().copied()
}
}
const VALID_LAT_RANGE: r1::Interval = r1::Interval {
lo: -FRAC_PI_2,
hi: FRAC_PI_2,
};
#[must_use]
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Rect {
pub lat: r1::Interval,
pub lng: s1::Interval,
}
impl PartialEq for Rect {
fn eq(&self, other: &Self) -> bool {
self.lat == other.lat && self.lng == other.lng
}
}
impl Default for Rect {
fn default() -> Self {
Self::empty()
}
}
impl Rect {
#[inline]
pub fn new(lat: r1::Interval, lng: s1::Interval) -> Self {
Rect { lat, lng }
}
#[inline]
pub fn empty() -> Self {
Rect {
lat: r1::Interval::empty(),
lng: s1::Interval::empty(),
}
}
#[inline]
pub fn full() -> Self {
Rect {
lat: VALID_LAT_RANGE,
lng: s1::Interval::full(),
}
}
pub fn from_lat_lng(ll: LatLng) -> Self {
Rect {
lat: r1::Interval::from_point(ll.lat.radians()),
lng: s1::Interval::from_point(ll.lng.radians()),
}
}
pub fn from_center_size(center: LatLng, size: LatLng) -> Self {
let half = LatLng::new(size.lat * 0.5, size.lng * 0.5);
Self::from_lat_lng(center).expanded(half)
}
#[inline]
pub fn is_valid(self) -> bool {
self.lat.lo.abs() <= FRAC_PI_2
&& self.lat.hi.abs() <= FRAC_PI_2
&& self.lng.is_valid()
&& self.lat.is_empty() == self.lng.is_empty()
}
#[inline]
pub fn is_empty(self) -> bool {
self.lat.is_empty()
}
#[inline]
pub fn is_full(self) -> bool {
self.lat == VALID_LAT_RANGE && self.lng.is_full()
}
#[inline]
pub fn is_point(self) -> bool {
self.lat.lo == self.lat.hi && self.lng.lo == self.lng.hi
}
pub fn vertex(self, v: RectVertex) -> LatLng {
match v {
RectVertex::LowerLeft => LatLng::from_radians(self.lat.lo, self.lng.lo),
RectVertex::LowerRight => LatLng::from_radians(self.lat.lo, self.lng.hi),
RectVertex::UpperRight => LatLng::from_radians(self.lat.hi, self.lng.hi),
RectVertex::UpperLeft => LatLng::from_radians(self.lat.hi, self.lng.lo),
}
}
#[inline]
pub fn lo(self) -> LatLng {
LatLng::from_radians(self.lat.lo, self.lng.lo)
}
#[inline]
pub fn hi(self) -> LatLng {
LatLng::from_radians(self.lat.hi, self.lng.hi)
}
#[inline]
pub fn center(self) -> LatLng {
LatLng::from_radians(self.lat.center(), self.lng.center())
}
#[inline]
pub fn size(self) -> LatLng {
LatLng::from_radians(self.lat.length(), self.lng.length())
}
pub fn area(self) -> f64 {
if self.is_empty() {
return 0.0;
}
let cap_diff = (self.lat.hi.sin() - self.lat.lo.sin()).abs();
self.lng.length() * cap_diff
}
#[inline]
pub fn contains_lat_lng(self, ll: LatLng) -> bool {
if !ll.is_valid() {
return false;
}
self.lat.contains(ll.lat.radians()) && self.lng.contains(ll.lng.radians())
}
#[inline]
pub fn contains_point(self, p: Point) -> bool {
self.contains_lat_lng(LatLng::from_point(p))
}
#[inline]
pub fn contains(self, other: Rect) -> bool {
self.lat.contains_interval(other.lat) && self.lng.contains_interval(other.lng)
}
#[inline]
pub fn intersects(self, other: Rect) -> bool {
self.lat.intersects(other.lat) && self.lng.intersects(other.lng)
}
pub fn from_point_pair(a: LatLng, b: LatLng) -> Self {
Rect {
lat: r1::Interval::from_point_pair(a.lat.radians(), b.lat.radians()),
lng: s1::Interval::from_point_pair(a.lng.radians(), b.lng.radians()),
}
}
#[inline]
pub fn full_lat() -> r1::Interval {
VALID_LAT_RANGE
}
#[inline]
pub fn full_lng() -> s1::Interval {
s1::Interval::full()
}
#[inline]
pub fn is_inverted(self) -> bool {
self.lng.is_inverted()
}
#[inline]
pub fn interior_contains_lat_lng(self, ll: LatLng) -> bool {
self.lat.interior_contains(ll.lat.radians()) && self.lng.interior_contains(ll.lng.radians())
}
#[inline]
pub fn interior_contains_point(self, p: Point) -> bool {
self.interior_contains_lat_lng(LatLng::from_point(p))
}
#[inline]
pub fn interior_contains(self, other: Rect) -> bool {
self.lat.interior_contains_interval(other.lat)
&& self.lng.interior_contains_interval(other.lng)
}
#[inline]
pub fn interior_intersects(self, other: Rect) -> bool {
self.lat.interior_intersects(other.lat) && self.lng.interior_intersects(other.lng)
}
pub fn add_point(self, ll: LatLng) -> Rect {
if !ll.is_valid() {
return self;
}
Rect {
lat: self.lat.add_point(ll.lat.radians()),
lng: self.lng.add_point(ll.lng.radians()),
}
}
pub fn polar_closure(self) -> Rect {
if self.lat.lo == -FRAC_PI_2 || self.lat.hi == FRAC_PI_2 {
return Rect {
lat: self.lat,
lng: s1::Interval::full(),
};
}
self
}
pub fn expanded(self, margin: LatLng) -> Rect {
let lat = self.lat.expanded(margin.lat.radians());
let lng = self.lng.expanded(margin.lng.radians());
if lat.is_empty() || lng.is_empty() {
return Self::empty();
}
Rect {
lat: lat.intersection(VALID_LAT_RANGE),
lng,
}
}
pub fn expanded_by_distance(self, distance: Angle) -> Rect {
if distance.radians() >= 0.0 {
let radius = ChordAngle::from_angle(distance);
let mut r = self;
for v in RectVertex::iter() {
let cap = Cap::from_center_chord_angle(Point::from(self.vertex(v)), radius);
r = r.union(cap.rect_bound());
}
r
} else {
let lat_lo = if self.lat.lo <= VALID_LAT_RANGE.lo && self.lng.is_full() {
VALID_LAT_RANGE.lo
} else {
self.lat.lo - distance.radians()
};
let lat_hi = if self.lat.hi >= VALID_LAT_RANGE.hi && self.lng.is_full() {
VALID_LAT_RANGE.hi
} else {
self.lat.hi + distance.radians()
};
let lat_result = r1::Interval {
lo: lat_lo,
hi: lat_hi,
};
if lat_result.is_empty() {
return Self::empty();
}
let max_abs_lat = (-lat_result.lo).max(lat_result.hi);
let sin_a = (-distance.radians()).sin();
let sin_c = max_abs_lat.cos();
let max_lng_margin = if sin_a < sin_c {
(sin_a / sin_c).asin()
} else {
FRAC_PI_2
};
let lng_result = self.lng.expanded(-max_lng_margin);
if lng_result.is_empty() {
return Self::empty();
}
Rect {
lat: lat_result,
lng: lng_result,
}
}
}
pub fn union(self, other: Rect) -> Rect {
Rect {
lat: self.lat.union(other.lat),
lng: self.lng.union(other.lng),
}
}
pub fn intersection(self, other: Rect) -> Rect {
let lat = self.lat.intersection(other.lat);
let lng = self.lng.intersection(other.lng);
if lat.is_empty() || lng.is_empty() {
return Self::empty();
}
Rect { lat, lng }
}
pub fn expand_for_subregions(self) -> Rect {
if self.is_empty() {
return self;
}
const DBL_EPSILON: f64 = 2.220_446_049_250_313e-16;
let lng_gap = (PI - self.lng.length() - 2.5 * DBL_EPSILON).max(0.0);
let min_abs_lat = self.lat.lo.max(-self.lat.hi);
if 2.0 * min_abs_lat + lng_gap < 1.354e-15 {
return Rect::full();
}
let lat_gap1 = FRAC_PI_2 + self.lat.lo; let lat_gap2 = FRAC_PI_2 - self.lat.hi; if lng_gap >= FRAC_PI_2 {
if lat_gap1 + lat_gap2 < 1.687e-15 {
return Rect::full();
}
} else {
if lat_gap1.max(lat_gap2) * lng_gap < 1.765e-15 {
return Rect::full();
}
}
let lat_expansion = 9.0 * DBL_EPSILON;
let lng_expansion = if lng_gap <= 0.0 { PI } else { 0.0 };
let expanded = self.expanded(LatLng::from_radians(lat_expansion, lng_expansion));
expanded.polar_closure()
}
pub fn cap_bound(self) -> Cap {
if self.is_empty() {
return Cap::empty();
}
let (pole_z, pole_angle) = if self.lat.hi + self.lat.lo < 0.0 {
(-1.0, FRAC_PI_2 + self.lat.hi)
} else {
(1.0, FRAC_PI_2 - self.lat.lo)
};
let pole_cap = Cap::from_center_angle(
Point::new(crate::r3::Vector {
x: 0.0,
y: 0.0,
z: pole_z,
}),
Angle::from_radians((1.0 + 2.0 * f64::EPSILON) * pole_angle),
);
let lng_span = self.lng.hi - self.lng.lo;
if lng_span.rem_euclid(2.0 * PI) >= 0.0 && lng_span <= PI {
let mut mid_cap = Cap::from_point(self.center().to_point());
for v in RectVertex::iter() {
mid_cap = mid_cap.add_point(self.vertex(v).to_point());
}
if mid_cap.height() < pole_cap.height() {
return mid_cap;
}
}
pole_cap
}
#[inline]
pub fn rect_bound(self) -> Rect {
self
}
pub fn cell_union_bound(self) -> Vec<CellId> {
self.cap_bound().cell_union_bound()
}
pub fn centroid(self) -> Point {
if self.is_empty() {
return Point::default();
}
let z1 = self.lat.lo.sin();
let z2 = self.lat.hi.sin();
let r1 = self.lat.lo.cos();
let r2 = self.lat.hi.cos();
let alpha = 0.5 * self.lng.length();
let r0 = alpha.sin() * (r2 * z2 - r1 * z1 + self.lat.length());
let lng = self.lng.center();
let z = alpha * (z2 + z1) * (z2 - z1);
Point::new(crate::r3::Vector {
x: r0 * lng.cos(),
y: r0 * lng.sin(),
z,
})
}
pub fn boundary_intersects(self, v0: Point, v1: Point) -> bool {
if self.is_empty() {
return false;
}
if !self.lng.is_full() {
if intersects_lng_edge(v0, v1, self.lat, self.lng.lo) {
return true;
}
if intersects_lng_edge(v0, v1, self.lat, self.lng.hi) {
return true;
}
}
if self.lat.lo != -FRAC_PI_2 && intersects_lat_edge(v0, v1, self.lat.lo, self.lng) {
return true;
}
if self.lat.hi != FRAC_PI_2 && intersects_lat_edge(v0, v1, self.lat.hi, self.lng) {
return true;
}
false
}
pub fn get_distance_to_latlng(self, p: LatLng) -> Angle {
debug_assert!(!self.is_empty());
debug_assert!(p.is_valid());
if self.lng.contains(p.lng.radians()) {
return Angle::from_radians(
(p.lat.radians() - self.lat.hi)
.max(self.lat.lo - p.lat.radians())
.max(0.0),
);
}
let interval = s1::Interval::new(self.lng.hi, self.lng.complement_center());
let a_lng = if interval.contains(p.lng.radians()) {
self.lng.hi
} else {
self.lng.lo
};
let lo = LatLng::from_radians(self.lat.lo, a_lng).to_point();
let hi = LatLng::from_radians(self.lat.hi, a_lng).to_point();
edge_distances::distance_from_segment(p.to_point(), lo, hi)
}
pub fn get_distance(self, other: Rect) -> Angle {
debug_assert!(!self.is_empty());
debug_assert!(!other.is_empty());
if self.lng.intersects(other.lng) {
if self.lat.intersects(other.lat) {
return Angle::from_radians(0.0);
}
let (lo, hi) = if self.lat.lo > other.lat.hi {
(other.lat.hi, self.lat.lo)
} else {
(self.lat.hi, other.lat.lo)
};
return Angle::from_radians(hi - lo);
}
let lo_hi = s1::Interval::from_point_pair(self.lng.lo, other.lng.hi);
let hi_lo = s1::Interval::from_point_pair(self.lng.hi, other.lng.lo);
let (a_lng, b_lng) = if lo_hi.length() < hi_lo.length() {
(self.lng.lo, other.lng.hi)
} else {
(self.lng.hi, other.lng.lo)
};
let a_lo = LatLng::from_radians(self.lat.lo, a_lng).to_point();
let a_hi = LatLng::from_radians(self.lat.hi, a_lng).to_point();
let b_lo = LatLng::from_radians(other.lat.lo, b_lng).to_point();
let b_hi = LatLng::from_radians(other.lat.hi, b_lng).to_point();
min_angle(
edge_distances::distance_from_segment(a_lo, b_lo, b_hi),
min_angle(
edge_distances::distance_from_segment(a_hi, b_lo, b_hi),
min_angle(
edge_distances::distance_from_segment(b_lo, a_lo, a_hi),
edge_distances::distance_from_segment(b_hi, a_lo, a_hi),
),
),
)
}
pub fn get_hausdorff_distance(self, other: Rect) -> Angle {
max_angle(
self.get_directed_hausdorff_distance(other),
other.get_directed_hausdorff_distance(self),
)
}
pub fn get_directed_hausdorff_distance(self, other: Rect) -> Angle {
if self.is_empty() {
return Angle::from_radians(0.0);
}
if other.is_empty() {
return Angle::from_radians(PI); }
let lng_distance = self.lng.directed_hausdorff_distance(other.lng);
debug_assert!(lng_distance >= 0.0);
get_directed_hausdorff_distance_lng(lng_distance, self.lat, other.lat)
}
pub fn approx_eq(self, other: Rect) -> bool {
self.lat.approx_eq_with(other.lat, 1e-15) && self.lng.approx_eq_with(other.lng, 1e-15)
}
}
impl fmt::Display for Rect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[Lo{}, Hi{}]", self.lo(), self.hi())
}
}
#[inline]
fn min_angle(a: Angle, b: Angle) -> Angle {
if a.radians() <= b.radians() { a } else { b }
}
#[inline]
fn max_angle(a: Angle, b: Angle) -> Angle {
if a.radians() >= b.radians() { a } else { b }
}
fn intersects_lng_edge(a: Point, b: Point, lat: r1::Interval, lng: f64) -> bool {
let c = LatLng::from_radians(lat.lo, lng).to_point();
let d = LatLng::from_radians(lat.hi, lng).to_point();
edge_crossings::crossing_sign(a, b, c, d) == Crossing::Cross
}
fn intersects_lat_edge(a: Point, b: Point, lat: f64, lng: s1::Interval) -> bool {
let mut z = edge_crossings::robust_cross_prod(a, b).0.normalize();
if z.z < 0.0 {
z = -z;
}
let north = crate::r3::Vector {
x: 0.0,
y: 0.0,
z: 1.0,
};
let y = edge_crossings::robust_cross_prod(Point(z), Point(north))
.0
.normalize();
let x = y.cross(z);
debug_assert!(x.z >= 0.0);
let sin_lat = lat.sin();
if sin_lat.abs() >= x.z {
return false; }
debug_assert!(x.z > 0.0);
let cos_theta = sin_lat / x.z;
let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
let theta = sin_theta.atan2(cos_theta);
let ab_theta =
s1::Interval::from_point_pair(a.0.dot(y).atan2(a.0.dot(x)), b.0.dot(y).atan2(b.0.dot(x)));
if ab_theta.contains(theta) {
let isect = x * cos_theta + y * sin_theta;
if lng.contains(isect.y.atan2(isect.x)) {
return true;
}
}
if ab_theta.contains(-theta) {
let isect = x * cos_theta - y * sin_theta;
if lng.contains(isect.y.atan2(isect.x)) {
return true;
}
}
false
}
fn get_directed_hausdorff_distance_lng(lng_diff: f64, a: r1::Interval, b: r1::Interval) -> Angle {
debug_assert!(lng_diff >= 0.0);
debug_assert!(lng_diff <= PI);
if lng_diff == 0.0 {
return Angle::from_radians(a.directed_hausdorff_distance(b));
}
let b_lng = lng_diff;
let b_lo = LatLng::from_radians(b.lo, b_lng).to_point();
let b_hi = LatLng::from_radians(b.hi, b_lng).to_point();
let a_lo = LatLng::from_radians(a.lo, 0.0).to_point();
let a_hi = LatLng::from_radians(a.hi, 0.0).to_point();
let mut max_distance = edge_distances::distance_from_segment(a_lo, b_lo, b_hi);
max_distance = max_angle(
max_distance,
edge_distances::distance_from_segment(a_hi, b_lo, b_hi),
);
if lng_diff <= FRAC_PI_2 {
if a.contains(0.0) && b.contains(0.0) {
max_distance = max_angle(max_distance, Angle::from_radians(lng_diff));
}
} else {
let p = get_bisector_intersection(b, b_lng);
let p_lat = LatLng::latitude(p).radians();
if a.contains(p_lat) {
max_distance = max_angle(max_distance, p.distance(b_lo));
}
if p_lat > a.lo {
let d = get_interior_max_distance(r1::Interval::new(a.lo, p_lat.min(a.hi)), b_lo);
if d.radians() >= 0.0 {
max_distance = max_angle(max_distance, d);
}
}
if p_lat < a.hi {
let d = get_interior_max_distance(r1::Interval::new(p_lat.max(a.lo), a.hi), b_hi);
if d.radians() >= 0.0 {
max_distance = max_angle(max_distance, d);
}
}
}
max_distance
}
fn get_bisector_intersection(lat: r1::Interval, lng: f64) -> Point {
let lng = lng.abs();
let lat_center = lat.center();
let ortho_bisector = if lat_center >= 0.0 {
LatLng::from_radians(lat_center - FRAC_PI_2, lng)
} else {
LatLng::from_radians(-lat_center - FRAC_PI_2, lng - PI)
};
let ortho_lng = Point(crate::r3::Vector {
x: 0.0,
y: -1.0,
z: 0.0,
});
edge_crossings::robust_cross_prod(ortho_lng, ortho_bisector.to_point())
}
fn get_interior_max_distance(a_lat: r1::Interval, b: Point) -> Angle {
if a_lat.is_empty() || b.x() >= 0.0 {
return Angle::from_radians(-1.0);
}
let intersection_point = Point(
crate::r3::Vector {
x: -b.x(),
y: 0.0,
z: -b.z(),
}
.normalize(),
);
if a_lat.interior_contains(LatLng::latitude(intersection_point).radians()) {
b.distance(intersection_point)
} else {
Angle::from_radians(-1.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::FRAC_PI_4;
fn is_send_sync<T: Sized + Send + Sync + Unpin>() {}
#[test]
fn rect_is_send_sync() {
is_send_sync::<Rect>();
}
fn float64_near(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() <= eps
}
#[test]
fn test_empty_full_valid() {
let empty = Rect::empty();
let full = Rect::full();
assert!(empty.is_valid());
assert!(empty.is_empty());
assert!(!empty.is_full());
assert!(full.is_valid());
assert!(!full.is_empty());
assert!(full.is_full());
}
#[test]
fn test_from_lat_lng() {
let ll = LatLng::from_degrees(23.5, 48.0);
let r = Rect::from_lat_lng(ll);
assert!(r.is_point());
assert!(r.contains_lat_lng(ll));
}
#[test]
fn test_from_center_size() {
let r = Rect::from_center_size(
LatLng::from_degrees(80.0, 170.0),
LatLng::from_degrees(40.0, 60.0),
);
assert!(r.approx_eq(Rect::new(
r1::Interval::new(60.0_f64.to_radians(), 90.0_f64.to_radians()),
s1::Interval::new(140.0_f64.to_radians(), (-160.0_f64).to_radians()),
)));
}
#[test]
fn test_accessors() {
let r = Rect::new(
r1::Interval::new(0.0, FRAC_PI_2),
s1::Interval::new(-FRAC_PI_2, FRAC_PI_2),
);
assert!(float64_near(r.lat.lo, 0.0, 1e-15));
assert!(float64_near(r.lat.hi, FRAC_PI_2, 1e-15));
assert!(float64_near(r.lng.lo, -FRAC_PI_2, 1e-15));
assert!(float64_near(r.lng.hi, FRAC_PI_2, 1e-15));
}
#[test]
fn test_area() {
assert_eq!(Rect::empty().area(), 0.0);
assert!(float64_near(Rect::full().area(), 4.0 * PI, 1e-14,));
}
#[test]
fn test_contains_lat_lng() {
let r = Rect::new(
r1::Interval::new(0.0, FRAC_PI_4),
s1::Interval::new(-PI, 0.0),
);
assert!(r.contains_lat_lng(LatLng::from_radians(FRAC_PI_4 / 2.0, -FRAC_PI_2)));
assert!(!r.contains_lat_lng(LatLng::from_radians(FRAC_PI_2, -FRAC_PI_2)));
}
#[test]
fn test_contains_rect() {
let full = Rect::full();
let empty = Rect::empty();
let r = Rect::new(
r1::Interval::new(0.0, FRAC_PI_4),
s1::Interval::new(-PI, 0.0),
);
assert!(full.contains(r));
assert!(!empty.contains(r));
assert!(r.contains(empty));
assert!(r.contains(r));
}
#[test]
fn test_intersects() {
let r1 = Rect::new(
r1::Interval::new(0.0, FRAC_PI_4),
s1::Interval::new(-PI, 0.0),
);
let r2 = Rect::new(
r1::Interval::new(FRAC_PI_4 / 2.0, FRAC_PI_2),
s1::Interval::new(-FRAC_PI_2, FRAC_PI_2),
);
assert!(r1.intersects(r2));
assert!(!r1.intersects(Rect::empty()));
}
#[test]
fn test_union() {
let r1 = Rect::from_lat_lng(LatLng::from_degrees(10.0, 20.0));
let r2 = Rect::from_lat_lng(LatLng::from_degrees(30.0, 40.0));
let u = r1.union(r2);
assert!(u.contains_lat_lng(LatLng::from_degrees(10.0, 20.0)));
assert!(u.contains_lat_lng(LatLng::from_degrees(30.0, 40.0)));
}
#[test]
fn test_polar_closure() {
let r = Rect::new(
r1::Interval::new(0.0, FRAC_PI_4),
s1::Interval::new(0.0, FRAC_PI_2),
);
assert_eq!(r.polar_closure().lng.lo, r.lng.lo);
let r2 = Rect::new(
r1::Interval::new(0.0, FRAC_PI_2),
s1::Interval::new(0.0, FRAC_PI_2),
);
assert!(r2.polar_closure().lng.is_full());
}
#[test]
fn test_expanded() {
let r = Rect::from_lat_lng(LatLng::from_degrees(0.0, 0.0));
let e = r.expanded(LatLng::from_degrees(10.0, 20.0));
assert!(e.contains_lat_lng(LatLng::from_degrees(5.0, 10.0)));
}
#[test]
fn test_cap_bound() {
assert!(Rect::full().cap_bound().is_full());
assert!(Rect::empty().cap_bound().is_empty());
}
#[test]
fn test_centroid() {
let empty_centroid = Rect::empty().centroid();
assert!(empty_centroid.approx_eq(Point::default()));
assert!(Rect::full().centroid().vector().norm() < 1e-15);
}
#[test]
fn test_display() {
let r = Rect::from_lat_lng(LatLng::from_degrees(10.0, 20.0));
let s = format!("{r}");
assert!(s.contains("Lo"));
assert!(s.contains("Hi"));
}
#[test]
fn test_approx_equal() {
assert!(Rect::empty().approx_eq(Rect::empty()));
assert!(Rect::full().approx_eq(Rect::full()));
assert!(!Rect::empty().approx_eq(Rect::full()));
}
fn rect_from_degrees(lat_lo: f64, lng_lo: f64, lat_hi: f64, lng_hi: f64) -> Rect {
fn normalize_latlng(lat_deg: f64, lng_deg: f64) -> LatLng {
let lat = lat_deg.to_radians().clamp(-FRAC_PI_2, FRAC_PI_2);
let lng_rad = lng_deg.to_radians();
let lng = lng_rad - (lng_rad / (2.0 * PI)).round_ties_even() * (2.0 * PI);
LatLng::from_radians(lat, lng)
}
let lo = normalize_latlng(lat_lo, lng_lo);
let hi = normalize_latlng(lat_hi, lng_hi);
Rect::new(
r1::Interval::new(lo.lat.radians(), hi.lat.radians()),
s1::Interval::new(lo.lng.radians(), hi.lng.radians()),
)
}
#[test]
fn test_vertex_basic() {
let r = Rect::new(
r1::Interval::new(0.0, FRAC_PI_2),
s1::Interval::new(-PI, 0.0),
);
let eps = 1e-15;
let v0 = r.vertex(RectVertex::LowerLeft);
assert!(float64_near(v0.lat.radians(), 0.0, eps));
assert!(float64_near(v0.lng.radians().abs(), PI, eps));
assert_eq!(
r.vertex(RectVertex::LowerRight),
LatLng::from_radians(0.0, 0.0)
);
assert_eq!(
r.vertex(RectVertex::UpperRight),
LatLng::from_radians(FRAC_PI_2, 0.0)
);
let v3 = r.vertex(RectVertex::UpperLeft);
assert!(float64_near(v3.lat.radians(), FRAC_PI_2, eps));
assert!(float64_near(v3.lng.radians().abs(), PI, eps));
}
#[test]
fn test_vertex_ccw_order() {
use crate::s2::predicates;
for i in 0..4i32 {
let lat = FRAC_PI_4 * f64::from(i - 2);
let lng = FRAC_PI_2 * f64::from(i - 2) + 0.2;
let r = Rect::new(
r1::Interval::new(lat, lat + FRAC_PI_4),
s1::Interval::new(
((lng + PI) % (2.0 * PI)) - PI,
((lng + FRAC_PI_2 + PI) % (2.0 * PI)) - PI,
),
);
for v in RectVertex::iter() {
let prev = r.vertex(v.prev()).to_point();
let curr = r.vertex(v).to_point();
let next = r.vertex(v.next()).to_point();
let dir = predicates::robust_sign(prev, curr, next);
assert_eq!(dir as i32, 1, "Vertices not in CCW order at i={i}, v={v:?}");
}
}
}
#[test]
fn test_add_point_incremental() {
let mut r = Rect::empty();
r = r.add_point(LatLng::from_degrees(0.0, 0.0));
assert!(r.is_point());
r = r.add_point(LatLng::from_radians(0.0, -FRAC_PI_2));
assert!(!r.is_point());
r = r.add_point(LatLng::from_radians(FRAC_PI_4, -PI));
r = r.add_point(LatLng::from_degrees(90.0, 0.0));
assert!(r.approx_eq(rect_from_degrees(0.0, -180.0, 90.0, 0.0)));
}
#[test]
fn test_cap_bound_at_center() {
let r = rect_from_degrees(-45.0, -45.0, 45.0, 45.0);
let cap = r.cap_bound();
let expected = Cap::from_center_height(Point::from_coords(1.0, 0.0, 0.0), 0.5);
assert!(
cap.approx_eq(expected),
"cap_bound for 45deg square: got center={:?} height={}, expected center={:?} height={}",
cap.center(),
cap.height(),
expected.center(),
expected.height(),
);
}
#[test]
fn test_cap_bound_near_pole() {
let r = rect_from_degrees(88.0, -80.0, 89.0, 80.0);
let cap = r.cap_bound();
let expected =
Cap::from_center_angle(Point::from_coords(0.0, 0.0, 1.0), Angle::from_degrees(2.0));
assert!(
cap.approx_eq(expected),
"cap_bound near pole: got height={}, expected height={}",
cap.height(),
expected.height(),
);
}
#[test]
fn test_cap_bound_wide_longitude() {
let r = rect_from_degrees(-30.0, -150.0, -10.0, 50.0);
let cap = r.cap_bound();
let expected = Cap::from_center_angle(
Point::from_coords(0.0, 0.0, -1.0),
Angle::from_degrees(80.0),
);
assert!(
cap.approx_eq(expected),
"cap_bound wide lng: got height={}, expected height={}",
cap.height(),
expected.height(),
);
}
#[test]
fn test_cap_bound_hemisphere() {
let r = rect_from_degrees(-10.0, -100.0, 0.0, 100.0);
let cap = r.cap_bound();
assert!(
cap.chord_radius().length2() >= ChordAngle::RIGHT.length2() - 1e-14,
"Hemisphere cap should have radius ~>= RIGHT, got {:?}",
cap.chord_radius(),
);
}
#[test]
fn test_intersection() {
let r1 = rect_from_degrees(0.0, -90.0, 45.0, 0.0);
let r2 = rect_from_degrees(20.0, -45.0, 60.0, 45.0);
let isect = r1.intersection(r2);
assert!(isect.approx_eq(rect_from_degrees(20.0, -45.0, 45.0, 0.0)));
let r3 = rect_from_degrees(50.0, 10.0, 60.0, 20.0);
assert!(r1.intersection(r3).is_empty());
}
#[test]
fn test_contains_point() {
let r = rect_from_degrees(-30.0, -60.0, 30.0, 60.0);
let inside = LatLng::from_degrees(0.0, 0.0).to_point();
let outside = LatLng::from_degrees(50.0, 0.0).to_point();
assert!(r.contains_point(inside));
assert!(!r.contains_point(outside));
}
#[test]
fn test_get_center_size() {
let r = rect_from_degrees(-10.0, -20.0, 30.0, 40.0);
let center = r.center();
assert!(float64_near(center.lat.degrees(), 10.0, 1e-10));
assert!(float64_near(center.lng.degrees(), 10.0, 1e-10));
let size = r.size();
assert!(float64_near(size.lat.degrees(), 40.0, 1e-10));
assert!(float64_near(size.lng.degrees(), 60.0, 1e-10));
}
#[test]
fn test_area_specific_values() {
let r = rect_from_degrees(0.0, 0.0, 1.0, 1.0);
let expected_area = 1.0_f64.to_radians() * (1.0_f64.to_radians().sin() - 0.0_f64.sin());
assert!(
float64_near(r.area(), expected_area, 1e-10),
"1° square area: got {}, expected {}",
r.area(),
expected_area,
);
let north = rect_from_degrees(0.0, -180.0, 90.0, 180.0);
assert!(
float64_near(north.area(), 2.0 * PI, 1e-10),
"Northern hemisphere area: got {}, expected {}",
north.area(),
2.0 * PI,
);
}
#[test]
fn test_contains_antimeridian() {
let r = rect_from_degrees(20.0, 170.0, 40.0, -170.0);
assert!(r.contains_lat_lng(LatLng::from_degrees(30.0, 180.0)));
assert!(r.contains_lat_lng(LatLng::from_degrees(30.0, -180.0)));
assert!(r.contains_lat_lng(LatLng::from_degrees(30.0, 175.0)));
assert!(r.contains_lat_lng(LatLng::from_degrees(30.0, -175.0)));
assert!(!r.contains_lat_lng(LatLng::from_degrees(30.0, 0.0)));
}
fn test_interval_ops(
x: Rect,
y: Rect,
expected_contains: bool,
expected_intersects: bool,
expected_union: Rect,
expected_intersection: Rect,
) {
assert_eq!(x.contains(y), expected_contains, "Contains({x}, {y})");
assert_eq!(x.intersects(y), expected_intersects, "Intersects({x}, {y})");
assert_eq!(
x.contains(y),
x.union(y).approx_eq(x),
"Contains ↔ Union==self for ({x}, {y})"
);
assert_eq!(
x.intersects(y),
!x.intersection(y).is_empty(),
"Intersects ↔ Intersection non-empty for ({x}, {y})"
);
assert!(
x.union(y).approx_eq(expected_union),
"Union({x}, {y}): got {}, expected {expected_union}",
x.union(y),
);
assert!(
x.intersection(y).approx_eq(expected_intersection),
"Intersection({x}, {y}): got {}, expected {expected_intersection}",
x.intersection(y),
);
}
#[test]
fn test_interval_ops_basic() {
let r1 = rect_from_degrees(0.0, -180.0, 90.0, 0.0);
let r1_mid = rect_from_degrees(45.0, -90.0, 45.0, -90.0);
test_interval_ops(r1, r1_mid, true, true, r1, r1_mid);
let req_m180 = rect_from_degrees(0.0, -180.0, 0.0, -180.0);
test_interval_ops(r1, req_m180, true, true, r1, req_m180);
let rnorth_pole = rect_from_degrees(90.0, 0.0, 90.0, 0.0);
test_interval_ops(r1, rnorth_pole, true, true, r1, rnorth_pole);
test_interval_ops(
r1,
rect_from_degrees(-10.0, -1.0, 1.0, 20.0),
false,
true,
rect_from_degrees(-10.0, 180.0, 90.0, 20.0),
rect_from_degrees(0.0, -1.0, 1.0, 0.0),
);
test_interval_ops(
rect_from_degrees(-15.0, -160.0, -15.0, -150.0),
rect_from_degrees(20.0, 145.0, 25.0, 155.0),
false,
false,
rect_from_degrees(-15.0, 145.0, 25.0, -150.0),
Rect::empty(),
);
test_interval_ops(
rect_from_degrees(70.0, -10.0, 90.0, -140.0),
rect_from_degrees(60.0, 175.0, 80.0, 5.0),
false,
true,
rect_from_degrees(60.0, -180.0, 90.0, 180.0),
rect_from_degrees(70.0, 175.0, 80.0, 5.0),
);
test_interval_ops(
rect_from_degrees(12.0, 30.0, 60.0, 60.0),
rect_from_degrees(0.0, 0.0, 30.0, 18.0),
false,
false,
rect_from_degrees(0.0, 0.0, 60.0, 60.0),
Rect::empty(),
);
test_interval_ops(
rect_from_degrees(0.0, 0.0, 18.0, 42.0),
rect_from_degrees(30.0, 12.0, 42.0, 60.0),
false,
false,
rect_from_degrees(0.0, 0.0, 42.0, 60.0),
Rect::empty(),
);
}
#[test]
fn test_approx_equal_empty_rects() {
assert!(Rect::empty().approx_eq(rect_from_degrees(1.0, 5.0, 1.0, 5.0)));
assert!(rect_from_degrees(1.0, 5.0, 1.0, 5.0).approx_eq(Rect::empty()));
assert!(
!rect_from_degrees(1.0, 5.0, 1.0, 5.0).approx_eq(rect_from_degrees(2.0, 7.0, 2.0, 7.0))
);
}
#[test]
fn test_union_contains_both() {
let pairs = [
(
rect_from_degrees(0.0, 0.0, 10.0, 10.0),
rect_from_degrees(5.0, 5.0, 15.0, 15.0),
),
(
rect_from_degrees(-90.0, -180.0, 0.0, 0.0),
rect_from_degrees(0.0, 0.0, 90.0, 180.0),
),
(
rect_from_degrees(20.0, 170.0, 30.0, -170.0),
rect_from_degrees(25.0, -175.0, 35.0, 175.0),
),
];
for (r1, r2) in &pairs {
let u = r1.union(*r2);
assert!(u.contains(*r1), "Union should contain r1: {r1}");
assert!(u.contains(*r2), "Union should contain r2: {r2}");
}
}
#[test]
fn test_expanded_basic() {
let r = rect_from_degrees(10.0, 20.0, 30.0, 40.0);
let e = r.expanded(LatLng::from_degrees(5.0, 10.0));
assert!(e.approx_eq(rect_from_degrees(5.0, 10.0, 35.0, 50.0)));
assert!(
Rect::empty()
.expanded(LatLng::from_degrees(1.0, 1.0))
.is_empty()
);
}
#[test]
fn test_polar_closure_at_poles() {
let r = rect_from_degrees(85.0, 10.0, 90.0, 20.0);
let pc = r.polar_closure();
assert!(
pc.lng.is_full(),
"North pole rect should have full longitude"
);
assert!(float64_near(pc.lat.lo, 85.0_f64.to_radians(), 1e-15));
let r2 = rect_from_degrees(-90.0, 10.0, -80.0, 20.0);
let pc2 = r2.polar_closure();
assert!(
pc2.lng.is_full(),
"South pole rect should have full longitude"
);
}
#[test]
fn test_centroid_hemisphere() {
let r = rect_from_degrees(0.0, -180.0, 90.0, 180.0);
let c = r.centroid();
assert!(float64_near(c.0.x, 0.0, 1e-10));
assert!(float64_near(c.0.y, 0.0, 1e-10));
assert!(
c.0.z > 0.0,
"Centroid of N hemisphere should have positive z"
);
}
#[test]
fn test_area_empty_and_full() {
assert_eq!(Rect::empty().area(), 0.0);
assert!(
(Rect::full().area() - 4.0 * PI).abs() < 1e-10,
"Full rect area should be 4π, got {}",
Rect::full().area(),
);
}
#[test]
fn test_area_quarter_sphere() {
let r = rect_from_degrees(0.0, 0.0, 90.0, 90.0);
assert!(
(r.area() - FRAC_PI_2).abs() < 1e-10,
"Quarter sphere area should be π/2, got {}",
r.area(),
);
}
#[test]
fn test_polar_closure_non_polar() {
let r = rect_from_degrees(-89.0, 0.0, 89.0, 1.0);
assert_eq!(r, r.polar_closure());
}
#[test]
fn test_polar_closure_south_pole() {
let r = rect_from_degrees(-90.0, -30.0, -45.0, 100.0);
let closed = r.polar_closure();
assert_eq!(closed.lng.lo, -PI);
assert_eq!(closed.lng.hi, PI);
}
#[test]
fn test_polar_closure_both_poles() {
let r = rect_from_degrees(-90.0, -145.0, 90.0, -144.0);
assert_eq!(r.polar_closure(), Rect::full());
}
fn make_point(lat_deg: f64, lng_deg: f64) -> Point {
LatLng::from_degrees(lat_deg, lng_deg).to_point()
}
fn point_rect_from_degrees(lat_deg: f64, lng_deg: f64) -> Rect {
let ll = LatLng::from_degrees(lat_deg, lng_deg).normalized();
Rect::from_lat_lng(ll)
}
#[test]
fn test_boundary_intersects_spherical_lune() {
let rect = rect_from_degrees(-90.0, 100.0, 90.0, 120.0);
assert!(!rect.boundary_intersects(make_point(60.0, 60.0), make_point(90.0, 60.0)));
assert!(!rect.boundary_intersects(make_point(-60.0, 110.0), make_point(60.0, 110.0)));
assert!(rect.boundary_intersects(make_point(-60.0, 95.0), make_point(60.0, 110.0)));
assert!(rect.boundary_intersects(make_point(60.0, 115.0), make_point(80.0, 125.0)));
}
#[test]
fn test_boundary_intersects_north_hemisphere() {
let rect = rect_from_degrees(0.0, -180.0, 90.0, 180.0);
assert!(!rect.boundary_intersects(make_point(60.0, -180.0), make_point(90.0, -180.0)));
assert!(!rect.boundary_intersects(make_point(60.0, -170.0), make_point(60.0, 170.0)));
assert!(rect.boundary_intersects(make_point(-10.0, -180.0), make_point(10.0, -180.0)));
}
#[test]
fn test_boundary_intersects_south_hemisphere() {
let rect = rect_from_degrees(-90.0, -180.0, 0.0, 180.0);
assert!(!rect.boundary_intersects(make_point(-90.0, -180.0), make_point(-60.0, -180.0)));
assert!(!rect.boundary_intersects(make_point(-60.0, -170.0), make_point(-60.0, 170.0)));
assert!(rect.boundary_intersects(make_point(-10.0, -180.0), make_point(10.0, -180.0)));
}
#[test]
fn test_boundary_intersects_rect_crossing_antimeridian() {
let rect = rect_from_degrees(20.0, 170.0, 40.0, -170.0);
assert!(rect.contains_point(make_point(30.0, 180.0)));
assert!(rect.boundary_intersects(make_point(25.0, 160.0), make_point(25.0, 180.0)));
assert!(rect.boundary_intersects(make_point(25.0, -160.0), make_point(25.0, -180.0)));
assert!(rect.boundary_intersects(make_point(15.0, 175.0), make_point(30.0, 175.0)));
assert!(rect.boundary_intersects(make_point(45.0, 175.0), make_point(30.0, 175.0)));
assert!(!rect.boundary_intersects(make_point(25.0, -20.0), make_point(25.0, 0.0)));
assert!(!rect.boundary_intersects(make_point(25.0, 20.0), make_point(25.0, 0.0)));
assert!(!rect.boundary_intersects(make_point(15.0, -5.0), make_point(30.0, -5.0)));
assert!(!rect.boundary_intersects(make_point(45.0, -5.0), make_point(30.0, -5.0)));
}
fn verify_get_directed_hausdorff_distance(a: Rect, b: Rect) {
let hausdorff_distance = a.get_directed_hausdorff_distance(b);
let resolution = 0.1_f64;
let mut max_distance = Angle::from_radians(0.0);
let sample_size_on_lat = (a.lat.length() / resolution) as i32 + 1;
let sample_size_on_lng = (a.lng.length() / resolution) as i32 + 1;
let delta_on_lat = a.lat.length() / f64::from(sample_size_on_lat);
let delta_on_lng = a.lng.length() / f64::from(sample_size_on_lng);
let mut lng = a.lng.lo;
for _i in 0..=sample_size_on_lng {
let mut lat = a.lat.lo;
for _j in 0..=sample_size_on_lat {
let latlng = LatLng::from_radians(lat, lng).normalized();
let distance_to_b = b.get_distance_to_latlng(latlng);
if distance_to_b.radians() >= max_distance.radians() {
max_distance = distance_to_b;
}
lat += delta_on_lat;
}
lng += delta_on_lng;
}
assert!(
max_distance.radians() <= hausdorff_distance.radians() + 1e-10,
"max_distance ({}) > hausdorff_distance ({}) + 1e-10 for {a} : {b}",
max_distance.radians(),
hausdorff_distance.radians(),
);
assert!(
max_distance.radians() >= hausdorff_distance.radians() - resolution,
"max_distance ({}) < hausdorff_distance ({}) - resolution for {a} : {b}",
max_distance.radians(),
hausdorff_distance.radians(),
);
}
#[test]
fn test_get_directed_hausdorff_distance_contained() {
let a = rect_from_degrees(-10.0, 20.0, -5.0, 90.0);
assert_eq!(
0.0,
a.get_directed_hausdorff_distance(rect_from_degrees(-10.0, 20.0, -5.0, 90.0))
.radians()
);
assert_eq!(
0.0,
a.get_directed_hausdorff_distance(rect_from_degrees(-10.0, 19.0, -5.0, 91.0))
.radians()
);
assert_eq!(
0.0,
a.get_directed_hausdorff_distance(rect_from_degrees(-11.0, 20.0, -4.0, 90.0))
.radians()
);
assert_eq!(
0.0,
a.get_directed_hausdorff_distance(rect_from_degrees(-11.0, 19.0, -4.0, 91.0))
.radians()
);
}
#[test]
fn test_get_directed_hausdorff_distance_point_to_rect() {
let a1 = point_rect_from_degrees(5.0, 8.0);
let a2 = point_rect_from_degrees(90.0, 10.0);
let b = rect_from_degrees(-85.0, -50.0, -80.0, 10.0);
assert!(
(a1.get_directed_hausdorff_distance(b).radians() - a1.get_distance(b).radians()).abs()
< 1e-15
);
assert!(
(a2.get_directed_hausdorff_distance(b).radians() - a2.get_distance(b).radians()).abs()
< 1e-15
);
let b = rect_from_degrees(4.0, -10.0, 80.0, 10.0);
assert!(
(a1.get_directed_hausdorff_distance(b).radians() - a1.get_distance(b).radians()).abs()
< 1e-15
);
assert!(
(a2.get_directed_hausdorff_distance(b).radians() - a2.get_distance(b).radians()).abs()
< 1e-15
);
let b = rect_from_degrees(70.0, 170.0, 80.0, -170.0);
assert!(
(a1.get_directed_hausdorff_distance(b).radians() - a1.get_distance(b).radians()).abs()
< 1e-15
);
assert!(
(a2.get_directed_hausdorff_distance(b).radians() - a2.get_distance(b).radians()).abs()
< 1e-15
);
}
#[test]
fn test_get_directed_hausdorff_distance_rect_to_point() {
let a = rect_from_degrees(1.0, -8.0, 10.0, 20.0);
verify_get_directed_hausdorff_distance(a, point_rect_from_degrees(5.0, 8.0));
verify_get_directed_hausdorff_distance(a, point_rect_from_degrees(-6.0, -100.0));
verify_get_directed_hausdorff_distance(a, point_rect_from_degrees(-90.0, -20.0));
verify_get_directed_hausdorff_distance(a, point_rect_from_degrees(90.0, 0.0));
}
#[test]
fn test_get_directed_hausdorff_distance_rect_to_rect_near_pole() {
let a = rect_from_degrees(-87.0, 0.0, -85.0, 3.0);
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-89.0, 1.0, -88.0, 2.0));
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-84.0, 1.0, -83.0, 2.0));
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-88.0, 90.0, -86.0, 91.0));
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-84.0, -91.0, -83.0, -90.0));
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-90.0, 181.0, -89.0, 182.0));
verify_get_directed_hausdorff_distance(a, rect_from_degrees(-84.0, 181.0, -83.0, 182.0));
}
#[test]
fn test_get_directed_hausdorff_distance_rect_to_rect_degenerate() {
verify_get_directed_hausdorff_distance(
rect_from_degrees(0.0, 10.0, 90.0, 20.0),
rect_from_degrees(-4.0, -10.0, 4.0, 0.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(-4.0, -10.0, 4.0, 0.0),
rect_from_degrees(0.0, 10.0, 90.0, 20.0),
);
let a = rect_from_degrees(-50.0, -10.0, 50.0, 10.0);
let b = rect_from_degrees(30.0, -10.0, 60.0, 10.0);
verify_get_directed_hausdorff_distance(a, b);
let c = Rect::new(a.lat, a.lng.complement());
verify_get_directed_hausdorff_distance(c, b);
verify_get_directed_hausdorff_distance(
rect_from_degrees(10.0, 170.0, 30.0, 180.0),
rect_from_degrees(-50.0, -10.0, 50.0, 10.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(10.0, -180.0, 30.0, -170.0),
rect_from_degrees(-50.0, -10.0, 50.0, 10.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(-30.0, 170.0, 30.0, 180.0),
rect_from_degrees(-10.0, -90.0, 10.0, 90.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(-30.0, -180.0, 30.0, -170.0),
rect_from_degrees(-10.0, -90.0, 10.0, 90.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(-20.0, 105.0, 20.0, 110.0),
rect_from_degrees(-30.0, 5.0, 30.0, 15.0),
);
verify_get_directed_hausdorff_distance(
rect_from_degrees(-20.0, 95.0, 20.0, 105.0),
rect_from_degrees(-30.0, 5.0, 30.0, 15.0),
);
}
#[test]
fn test_get_directed_hausdorff_distance_various_pairs() {
let test_cases: [(Rect, Rect); 8] = [
(
rect_from_degrees(10.0, 20.0, 30.0, 40.0),
rect_from_degrees(-20.0, 60.0, -10.0, 80.0),
),
(
rect_from_degrees(-30.0, -60.0, 30.0, 60.0),
rect_from_degrees(0.0, 0.0, 45.0, 90.0),
),
(
rect_from_degrees(70.0, -170.0, 85.0, 170.0),
rect_from_degrees(-85.0, -10.0, -70.0, 10.0),
),
(
rect_from_degrees(-10.0, 170.0, 10.0, -170.0),
rect_from_degrees(20.0, -30.0, 40.0, 30.0),
),
(
rect_from_degrees(-45.0, -90.0, 45.0, 90.0),
rect_from_degrees(50.0, 100.0, 60.0, 120.0),
),
(
rect_from_degrees(-1.0, 0.0, 1.0, 0.5),
rect_from_degrees(-1.0, 90.0, 1.0, 90.5),
),
(
rect_from_degrees(-10.0, -10.0, 10.0, 10.0),
rect_from_degrees(-5.0, -5.0, 5.0, 5.0),
),
(
rect_from_degrees(0.0, -180.0, 90.0, 180.0),
rect_from_degrees(-90.0, -180.0, 0.0, 180.0),
),
];
for (a, b) in &test_cases {
verify_get_directed_hausdorff_distance(*a, *b);
let a2 = Rect::new(a.lat, a.lng.complement());
let b2 = Rect::new(b.lat, b.lng.complement());
if b2.is_valid() && !b2.is_empty() {
verify_get_directed_hausdorff_distance(*a, b2);
}
if a2.is_valid() && !a2.is_empty() {
verify_get_directed_hausdorff_distance(a2, *b);
}
if a2.is_valid() && !a2.is_empty() && b2.is_valid() && !b2.is_empty() {
verify_get_directed_hausdorff_distance(a2, b2);
}
}
}
fn distance_to_lat_edge(x: LatLng, lat: f64, interval: s1::Interval) -> Angle {
if interval.contains(x.lng.radians()) {
return Angle::from_radians((x.lat.radians() - lat).abs());
}
min_angle(
x.get_distance(LatLng::from_radians(lat, interval.lo)),
x.get_distance(LatLng::from_radians(lat, interval.hi)),
)
}
fn brute_force_distance(a: Rect, b: Rect) -> Angle {
if a.intersects(b) {
return Angle::from_radians(0.0);
}
let pnt_a: [LatLng; 4] = RectVertex::ALL.map(|v| a.vertex(v));
let pnt_b: [LatLng; 4] = RectVertex::ALL.map(|v| b.vertex(v));
let lat_a = [a.lat.lo, a.lat.hi];
let lat_b = [b.lat.lo, b.lat.hi];
let lng_edge_a = [
[pnt_a[0].to_point(), pnt_a[3].to_point()],
[pnt_a[1].to_point(), pnt_a[2].to_point()],
];
let lng_edge_b = [
[pnt_b[0].to_point(), pnt_b[3].to_point()],
[pnt_b[1].to_point(), pnt_b[2].to_point()],
];
let mut min_dist = Angle::from_degrees(180.0);
for i in 0..4 {
let current_a = pnt_a[i];
let current_b = pnt_b[i];
for j in 0..2 {
let a_to_lat = distance_to_lat_edge(current_a, lat_b[j], b.lng);
let b_to_lat = distance_to_lat_edge(current_b, lat_a[j], a.lng);
let a_to_lng = edge_distances::distance_from_segment(
current_a.to_point(),
lng_edge_b[j][0],
lng_edge_b[j][1],
);
let b_to_lng = edge_distances::distance_from_segment(
current_b.to_point(),
lng_edge_a[j][0],
lng_edge_a[j][1],
);
min_dist = min_angle(
min_dist,
min_angle(a_to_lat, min_angle(b_to_lat, min_angle(a_to_lng, b_to_lng))),
);
}
}
min_dist
}
fn brute_force_rect_point_distance(a: Rect, b: LatLng) -> Angle {
if a.contains_lat_lng(b) {
return Angle::from_radians(0.0);
}
let b_to_lo_lat = distance_to_lat_edge(b, a.lat.lo, a.lng);
let b_to_hi_lat = distance_to_lat_edge(b, a.lat.hi, a.lng);
let b_to_lo_lng = edge_distances::distance_from_segment(
b.to_point(),
LatLng::from_radians(a.lat.lo, a.lng.lo).to_point(),
LatLng::from_radians(a.lat.hi, a.lng.lo).to_point(),
);
let b_to_hi_lng = edge_distances::distance_from_segment(
b.to_point(),
LatLng::from_radians(a.lat.lo, a.lng.hi).to_point(),
LatLng::from_radians(a.lat.hi, a.lng.hi).to_point(),
);
min_angle(
b_to_lo_lat,
min_angle(b_to_hi_lat, min_angle(b_to_lo_lng, b_to_hi_lng)),
)
}
fn verify_get_distance(a: Rect, b: Rect) {
let d1 = brute_force_distance(a, b);
let d2 = a.get_distance(b);
assert!(
(d1.radians() - d2.radians()).abs() < 1e-10,
"GetDistance({a}, {b}): brute_force={}, get_distance={}",
d1.radians(),
d2.radians(),
);
}
fn verify_get_rect_point_distance(a: Rect, p: LatLng) {
let p = p.normalized();
let d1 = brute_force_rect_point_distance(a, p);
let d2 = a.get_distance_to_latlng(p);
assert!(
(d1.radians() - d2.radians()).abs() < 1e-10,
"GetDistance({a}, {p}): brute_force={}, get_distance={}",
d1.radians(),
d2.radians(),
);
}
#[test]
fn test_get_distance_overlapping() {
let a = rect_from_degrees(0.0, 0.0, 2.0, 2.0);
let b = point_rect_from_degrees(0.0, 0.0);
assert_eq!(0.0, a.get_distance(a).radians());
assert_eq!(0.0, a.get_distance(b).radians());
assert_eq!(0.0, b.get_distance(b).radians());
assert_eq!(
0.0,
a.get_distance_to_latlng(LatLng::from_degrees(0.0, 0.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(0.0, 1.0, 2.0, 3.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(0.0, 2.0, 2.0, 4.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(1.0, 0.0, 3.0, 2.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(2.0, 0.0, 4.0, 2.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(1.0, 1.0, 3.0, 3.0))
.radians()
);
assert_eq!(
0.0,
a.get_distance(rect_from_degrees(2.0, 2.0, 4.0, 4.0))
.radians()
);
}
#[test]
fn test_get_distance_rect_vs_point() {
let a = rect_from_degrees(-1.0, -1.0, 2.0, 1.0);
verify_get_distance(a, point_rect_from_degrees(-2.0, -1.0));
verify_get_distance(a, point_rect_from_degrees(1.0, 2.0));
verify_get_distance(point_rect_from_degrees(-2.0, -1.0), a);
verify_get_distance(point_rect_from_degrees(1.0, 2.0), a);
verify_get_rect_point_distance(a, LatLng::from_degrees(-2.0, -1.0));
verify_get_rect_point_distance(a, LatLng::from_degrees(1.0, 2.0));
let b = rect_from_degrees(86.0, 0.0, 88.0, 2.0);
verify_get_distance(b, point_rect_from_degrees(87.0, 3.0));
verify_get_distance(b, point_rect_from_degrees(87.0, -1.0));
verify_get_distance(b, point_rect_from_degrees(89.0, 1.0));
verify_get_distance(b, point_rect_from_degrees(89.0, 181.0));
verify_get_distance(b, point_rect_from_degrees(85.0, 1.0));
verify_get_distance(b, point_rect_from_degrees(85.0, 181.0));
verify_get_distance(b, point_rect_from_degrees(90.0, 0.0));
verify_get_distance(point_rect_from_degrees(87.0, 3.0), b);
verify_get_distance(point_rect_from_degrees(87.0, -1.0), b);
verify_get_distance(point_rect_from_degrees(89.0, 1.0), b);
verify_get_distance(point_rect_from_degrees(89.0, 181.0), b);
verify_get_distance(point_rect_from_degrees(85.0, 1.0), b);
verify_get_distance(point_rect_from_degrees(85.0, 181.0), b);
verify_get_distance(point_rect_from_degrees(90.0, 0.0), b);
verify_get_rect_point_distance(b, LatLng::from_degrees(87.0, 3.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(87.0, -1.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(89.0, 1.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(89.0, 181.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(85.0, 1.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(85.0, 181.0));
verify_get_rect_point_distance(b, LatLng::from_degrees(90.0, 0.0));
let c = rect_from_degrees(88.0, 0.0, 90.0, 2.0);
verify_get_distance(c, point_rect_from_degrees(89.0, 3.0));
verify_get_distance(c, point_rect_from_degrees(89.0, 90.0));
verify_get_distance(c, point_rect_from_degrees(89.0, 181.0));
verify_get_distance(point_rect_from_degrees(89.0, 3.0), c);
verify_get_distance(point_rect_from_degrees(89.0, 90.0), c);
verify_get_distance(point_rect_from_degrees(89.0, 181.0), c);
}
#[test]
fn test_get_distance_rect_vs_rect() {
let a = rect_from_degrees(-1.0, -1.0, 2.0, 1.0);
verify_get_distance(a, rect_from_degrees(0.0, 2.0, 1.0, 3.0));
verify_get_distance(a, rect_from_degrees(-2.0, -3.0, -1.0, -2.0));
let b = rect_from_degrees(-87.0, 0.0, -85.0, 3.0);
verify_get_distance(b, rect_from_degrees(-89.0, 1.0, -88.0, 2.0));
verify_get_distance(b, rect_from_degrees(-84.0, 1.0, -83.0, 2.0));
verify_get_distance(b, rect_from_degrees(-88.0, 90.0, -86.0, 91.0));
verify_get_distance(b, rect_from_degrees(-84.0, -91.0, -83.0, -90.0));
verify_get_distance(b, rect_from_degrees(-90.0, 181.0, -89.0, 182.0));
verify_get_distance(b, rect_from_degrees(-84.0, 181.0, -83.0, 182.0));
}
#[test]
fn test_from_point_pair() {
let a = LatLng::from_degrees(10.0, 20.0);
let b = LatLng::from_degrees(30.0, 40.0);
let r = Rect::from_point_pair(a, b);
assert!(r.contains_lat_lng(a));
assert!(r.contains_lat_lng(b));
assert!(r.contains_lat_lng(LatLng::from_degrees(20.0, 30.0)));
assert!(!r.contains_lat_lng(LatLng::from_degrees(5.0, 25.0)));
let r2 = Rect::from_point_pair(b, a);
assert_eq!(r, r2);
}
#[test]
fn test_full_lat_lng() {
let fl = Rect::full_lat();
assert!(fl.contains(-FRAC_PI_2));
assert!(fl.contains(FRAC_PI_2));
assert!(!fl.contains(2.0));
let flng = Rect::full_lng();
assert!(flng.is_full());
}
#[test]
fn test_is_inverted() {
assert!(!Rect::full().is_inverted());
let r = Rect {
lat: r1::Interval::from_point_pair(0.0, 1.0),
lng: s1::Interval::new(2.0, -2.0), };
assert!(r.is_inverted());
let r2 = rect_from_degrees(10.0, 20.0, 30.0, 40.0);
assert!(!r2.is_inverted());
}
#[test]
fn test_interior_contains_lat_lng() {
let r = rect_from_degrees(10.0, 20.0, 30.0, 40.0);
assert!(r.interior_contains_lat_lng(LatLng::from_degrees(20.0, 30.0)));
assert!(!r.interior_contains_lat_lng(LatLng::from_degrees(10.0, 30.0)));
assert!(!r.interior_contains_lat_lng(LatLng::from_degrees(20.0, 20.0)));
}
#[test]
fn test_interior_contains_point() {
let r = rect_from_degrees(10.0, 20.0, 30.0, 40.0);
let inside = LatLng::from_degrees(20.0, 30.0).to_point();
assert!(r.interior_contains_point(inside));
}
#[test]
fn test_interior_contains_rect() {
let outer = rect_from_degrees(0.0, 0.0, 40.0, 40.0);
let inner = rect_from_degrees(10.0, 10.0, 30.0, 30.0);
assert!(outer.interior_contains(inner));
assert!(!outer.interior_contains(outer));
}
#[test]
fn test_interior_intersects() {
let a = rect_from_degrees(0.0, 0.0, 20.0, 20.0);
let b = rect_from_degrees(10.0, 10.0, 30.0, 30.0);
assert!(a.interior_intersects(b));
let c = rect_from_degrees(20.0, 0.0, 30.0, 20.0);
assert!(!a.interior_intersects(c));
}
}
#[cfg(test)]
mod quickcheck_tests {
use super::*;
use quickcheck_macros::quickcheck;
fn make_lat_lng(lat_deg: i32, lng_deg: i32) -> LatLng {
let lat = f64::from(lat_deg % 90).clamp(-90.0, 90.0);
let lng = f64::from(lng_deg % 180).clamp(-180.0, 180.0);
LatLng::from_degrees(lat, lng)
}
#[quickcheck]
fn prop_from_point_contains(lat: i32, lng: i32) -> bool {
let ll = make_lat_lng(lat, lng);
Rect::from_lat_lng(ll).contains_lat_lng(ll)
}
#[quickcheck]
fn prop_union_contains_both(lat1: i32, lng1: i32, lat2: i32, lng2: i32) -> bool {
let r1 = Rect::from_lat_lng(make_lat_lng(lat1, lng1));
let r2 = Rect::from_lat_lng(make_lat_lng(lat2, lng2));
let u = r1.union(r2);
u.contains(r1) && u.contains(r2)
}
#[quickcheck]
fn prop_empty_is_empty() -> bool {
Rect::empty().is_empty()
}
#[quickcheck]
fn prop_full_is_full() -> bool {
Rect::full().is_full()
}
#[quickcheck]
fn prop_area_non_negative(lat: i32, lng: i32) -> bool {
let r = Rect::from_lat_lng(make_lat_lng(lat, lng));
r.area() >= 0.0
}
#[quickcheck]
fn prop_contains_self(lat: i32, lng: i32) -> bool {
let r = Rect::from_lat_lng(make_lat_lng(lat, lng));
r.contains(r)
}
#[cfg(feature = "serde")]
#[quickcheck]
fn prop_serde_roundtrip(lat_deg: i32, lng_deg: i32) -> bool {
let lat = f64::from(lat_deg % 90).clamp(-90.0, 90.0);
let lng = f64::from(lng_deg % 180).clamp(-180.0, 180.0);
let ll = LatLng::from_degrees(lat, lng);
let r = Rect::from_lat_lng(ll);
let json1 = serde_json::to_string(&r).unwrap();
let back: Rect = serde_json::from_str(&json1).unwrap();
let json2 = serde_json::to_string(&back).unwrap();
let back2: Rect = serde_json::from_str(&json2).unwrap();
back == back2
}
#[test]
fn test_expand_for_subregions_normal() {
let r = Rect::from_center_size(
LatLng::from_degrees(45.0, 90.0),
LatLng::from_degrees(10.0, 10.0),
);
let expanded = r.expand_for_subregions();
assert!(!expanded.is_full());
assert!(expanded.lat.lo <= r.lat.lo);
assert!(expanded.lat.hi >= r.lat.hi);
}
#[test]
fn test_expand_for_subregions_nearly_antipodal() {
let r = Rect {
lat: r1::Interval::new(-0.01, 0.01),
lng: s1::Interval::new(-PI + 1e-16, PI - 1e-16),
};
let expanded = r.expand_for_subregions();
assert!(
expanded.is_full(),
"nearly-antipodal rect should expand to full"
);
}
#[test]
fn test_expand_for_subregions_empty() {
assert!(Rect::empty().expand_for_subregions().is_empty());
}
#[test]
fn test_cap_bound_all_four_vertices() {
let r = Rect::from_center_size(
LatLng::from_degrees(30.0, 50.0),
LatLng::from_degrees(40.0, 80.0),
);
let cap = r.cap_bound();
for rv in RectVertex::iter() {
let v = r.vertex(rv).to_point();
assert!(
cap.contains_point(v),
"cap_bound doesn't contain vertex {rv:?}"
);
}
}
#[test]
fn test_expanded_by_distance() {
use crate::s1::Angle;
let r = Rect::from_center_size(
LatLng::from_degrees(0.0, 0.0),
LatLng::from_degrees(10.0, 10.0),
);
let expanded = r.expanded_by_distance(Angle::from_degrees(1.0));
assert!(expanded.lat.lo < r.lat.lo);
assert!(expanded.lat.hi > r.lat.hi);
assert!(expanded.lng.lo < r.lng.lo);
assert!(expanded.lng.hi > r.lng.hi);
}
#[test]
fn test_rect_distance_to_lat_lng() {
use crate::s1::Angle;
let r = Rect::from_center_size(
LatLng::from_degrees(0.0, 0.0),
LatLng::from_degrees(10.0, 10.0),
);
let center_dist = r.get_distance_to_latlng(LatLng::from_degrees(0.0, 0.0));
assert!(center_dist.radians() < 1e-10);
let far_dist = r.get_distance_to_latlng(LatLng::from_degrees(20.0, 0.0));
assert!(far_dist > Angle::from_degrees(10.0));
}
#[cfg(feature = "serde")]
#[test]
fn test_serde_rect_vertex_roundtrip() {
for v in [
RectVertex::LowerLeft,
RectVertex::LowerRight,
RectVertex::UpperRight,
RectVertex::UpperLeft,
] {
let json = serde_json::to_string(&v).unwrap();
let back: RectVertex = serde_json::from_str(&json).unwrap();
assert_eq!(v, back);
}
}
}