use crate::constants::{EPSILON, EPSILON_RAD, MAX_H3_RES, M_2PI, M_PI, M_PI_2};
use crate::latlng::{constrain_lng, great_circle_distance_km};
use crate::types::BBox;
use crate::{CellBoundary, GeoLoop, GeoPolygon, H3Error, LatLng, MAX_CELL_BNDRY_VERTS};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum LongitudeNormalization {
None,
East,
West,
}
pub(crate) fn bbox_width_rads(bbox: &BBox) -> f64 {
if bbox_is_transmeridian(bbox) {
bbox.east - bbox.west + M_2PI
} else {
bbox.east - bbox.west
}
}
pub(crate) fn bbox_height_rads(bbox: &BBox) -> f64 {
bbox.north - bbox.south
}
pub(crate) fn bbox_is_transmeridian(bbox: &BBox) -> bool {
bbox.east < bbox.west
}
#[inline]
pub(crate) fn bbox_center(bbox: &BBox, center: &mut LatLng) {
center.lat = (bbox.north + bbox.south) * 0.5;
let east_for_center = if bbox_is_transmeridian(bbox) {
bbox.east + M_2PI
} else {
bbox.east
};
center.lng = constrain_lng((east_for_center + bbox.west) * 0.5); }
#[inline]
#[must_use]
pub(crate) fn bbox_contains_point(bbox: &BBox, point: &LatLng) -> bool {
if point.lat < bbox.south - EPSILON_RAD || point.lat > bbox.north + EPSILON_RAD {
return false;
}
if bbox_is_transmeridian(bbox) {
point.lng >= bbox.west - EPSILON_RAD || point.lng <= bbox.east + EPSILON_RAD
} else {
point.lng >= bbox.west - EPSILON_RAD && point.lng <= bbox.east + EPSILON_RAD
}
}
fn _hex_radius_km_approx(res: i32) -> Result<f64, H3Error> {
if res < 0 || res > MAX_H3_RES {
return Err(H3Error::ResDomain);
}
Ok(crate::latlng::get_hexagon_edge_length_avg_km(res)? * 0.8) }
pub(crate) fn bbox_normalization(
a: &BBox,
b: &BBox,
a_normalization: &mut LongitudeNormalization,
b_normalization: &mut LongitudeNormalization,
) {
let a_is_transmeridian = bbox_is_transmeridian(a);
let b_is_transmeridian = bbox_is_transmeridian(b);
let a_to_b_trends_east = (a.west - b.east).abs() < (b.west - a.east).abs();
*a_normalization = if !a_is_transmeridian {
LongitudeNormalization::None
} else if b_is_transmeridian {
LongitudeNormalization::East } else if a_to_b_trends_east {
LongitudeNormalization::East
} else {
LongitudeNormalization::West
};
*b_normalization = if !b_is_transmeridian {
LongitudeNormalization::None
} else if a_is_transmeridian {
LongitudeNormalization::East
} else if a_to_b_trends_east {
LongitudeNormalization::West
} else {
LongitudeNormalization::East
};
}
fn normalize_lng_for_comparison(lng: f64, normalization: LongitudeNormalization) -> f64 {
match normalization {
LongitudeNormalization::None => lng,
LongitudeNormalization::East => {
if lng < 0.0 {
lng + M_2PI
} else {
lng
}
}
LongitudeNormalization::West => {
if lng > 0.0 {
lng - M_2PI
} else {
lng
}
}
}
}
#[inline]
#[must_use]
pub(crate) fn bbox_contains_bbox(a: &BBox, b: &BBox) -> bool {
if a.north < b.north || a.south > b.south {
return false;
}
let mut a_norm = LongitudeNormalization::None;
let mut b_norm = LongitudeNormalization::None;
bbox_normalization(a, b, &mut a_norm, &mut b_norm);
normalize_lng_for_comparison(a.west, a_norm) <= normalize_lng_for_comparison(b.west, b_norm)
&& normalize_lng_for_comparison(a.east, a_norm) >= normalize_lng_for_comparison(b.east, b_norm)
}
#[inline]
#[must_use]
pub(crate) fn bbox_overlaps_bbox(a: &BBox, b: &BBox) -> bool {
if a.north < b.south || a.south > b.north {
return false;
}
let mut a_norm = LongitudeNormalization::None;
let mut b_norm = LongitudeNormalization::None;
bbox_normalization(a, b, &mut a_norm, &mut b_norm);
if normalize_lng_for_comparison(a.east, a_norm) < normalize_lng_for_comparison(b.west, b_norm)
|| normalize_lng_for_comparison(a.west, a_norm) > normalize_lng_for_comparison(b.east, b_norm)
{
return false;
}
true
}
#[inline]
#[must_use]
pub(crate) fn bbox_equals(b1: &BBox, b2: &BBox) -> bool {
(b1.north - b2.north).abs() < EPSILON_RAD &&
(b1.south - b2.south).abs() < EPSILON_RAD &&
(b1.east - b2.east).abs() < EPSILON_RAD && (b1.west - b2.west).abs() < EPSILON_RAD
}
#[inline]
#[must_use]
pub(crate) fn bbox_to_cell_boundary(bbox: &BBox) -> CellBoundary {
let mut vertices = [LatLng::default(); MAX_CELL_BNDRY_VERTS];
vertices[0] = LatLng {
lat: bbox.south,
lng: bbox.west,
}; vertices[1] = LatLng {
lat: bbox.south,
lng: bbox.east,
}; vertices[2] = LatLng {
lat: bbox.north,
lng: bbox.east,
}; vertices[3] = LatLng {
lat: bbox.north,
lng: bbox.west,
};
CellBoundary {
num_verts: 4,
verts: vertices,
}
}
pub(crate) fn bbox_hex_estimate(bbox: &BBox, res: i32) -> Result<i64, H3Error> {
if res < 0 || res > MAX_H3_RES {
return Err(H3Error::ResDomain);
}
let avg_cell_area_km2 = crate::latlng::get_hexagon_area_avg_km2(res)?;
if avg_cell_area_km2 < EPSILON {
return Ok(1);
}
let height_km = great_circle_distance_km(
&LatLng {
lat: bbox.north,
lng: bbox.west,
},
&LatLng {
lat: bbox.south,
lng: bbox.west,
},
);
let width_km = great_circle_distance_km(
&LatLng {
lat: bbox.south,
lng: bbox.east,
}, &LatLng {
lat: bbox.south,
lng: bbox.west,
},
);
let bbox_area_km2_approx = height_km * width_km;
if bbox_area_km2_approx < EPSILON {
return Ok(1);
}
let estimate = (bbox_area_km2_approx / avg_cell_area_km2).ceil();
if !estimate.is_finite() {
return Err(H3Error::Failed); }
Ok(estimate.max(1.0) as i64) }
pub(crate) fn line_hex_estimate(origin: &LatLng, destination: &LatLng, res: i32) -> Result<i64, H3Error> {
if res < 0 || res > MAX_H3_RES {
return Err(H3Error::ResDomain);
}
let avg_edge_len_km = crate::latlng::get_hexagon_edge_length_avg_km(res)?;
if avg_edge_len_km < EPSILON {
return Ok(1);
}
let line_dist_km = great_circle_distance_km(origin, destination);
let estimate = (line_dist_km / (2.0 * avg_edge_len_km)).ceil();
if !estimate.is_finite() {
return Err(H3Error::Failed);
}
Ok(estimate.max(1.0) as i64)
}
pub(crate) fn bbox_from_geoloop(geoloop: &GeoLoop, bbox: &mut BBox) {
if geoloop.num_verts == 0 {
*bbox = BBox::default();
return;
}
bbox.south = f64::MAX;
bbox.west = f64::MAX; bbox.north = -f64::MAX;
bbox.east = -f64::MAX;
let mut min_lng_regular = f64::MAX; let mut max_lng_regular = -f64::MAX; let mut crosses_antimeridian_arc = false;
for i in 0..geoloop.num_verts {
let p = geoloop.verts[i];
if p.lat < bbox.south {
bbox.south = p.lat;
}
if p.lat > bbox.north {
bbox.north = p.lat;
}
if p.lng < min_lng_regular {
min_lng_regular = p.lng;
}
if p.lng > max_lng_regular {
max_lng_regular = p.lng;
}
if i < geoloop.num_verts - 1 {
let next_p = geoloop.verts[i + 1];
if (p.lng - next_p.lng).abs() > M_PI {
crosses_antimeridian_arc = true;
}
} else {
if geoloop.num_verts > 1 {
let first_p = geoloop.verts[0];
if (p.lng - first_p.lng).abs() > M_PI {
crosses_antimeridian_arc = true;
}
}
}
}
if crosses_antimeridian_arc {
bbox.west = f64::MAX; bbox.east = -f64::MAX; let mut has_pos_lng = false;
let mut has_neg_lng = false;
for i in 0..geoloop.num_verts {
let p_lng = geoloop.verts[i].lng;
if p_lng > 0.0 {
if p_lng < bbox.west {
bbox.west = p_lng;
}
has_pos_lng = true;
}
if p_lng < 0.0 {
if p_lng > bbox.east {
bbox.east = p_lng;
}
has_neg_lng = true;
}
}
if !has_pos_lng {
bbox.west = bbox.east;
} if !has_neg_lng {
bbox.east = bbox.west;
} } else {
bbox.west = min_lng_regular;
bbox.east = max_lng_regular;
}
}
pub(crate) fn scale_bbox(bbox: &mut BBox, scale: f64) {
let width = bbox_width_rads(bbox);
let height = bbox_height_rads(bbox);
let width_buffer = (width * scale - width) * 0.5;
let height_buffer = (height * scale - height) * 0.5;
bbox.north += height_buffer;
if bbox.north > M_PI_2 {
bbox.north = M_PI_2;
}
bbox.south -= height_buffer;
if bbox.south < -M_PI_2 {
bbox.south = -M_PI_2;
}
bbox.east += width_buffer;
bbox.east = constrain_lng(bbox.east);
bbox.west -= width_buffer;
bbox.west = constrain_lng(bbox.west);
}
pub(crate) fn bboxes_from_geo_polygon(polygon: &GeoPolygon, bboxes_out: &mut [BBox]) {
if bboxes_out.is_empty() {
return; }
bbox_from_geoloop(&polygon.geoloop, &mut bboxes_out[0]);
for i in 0..polygon.num_holes {
if (i + 1) < bboxes_out.len() {
bbox_from_geoloop(&polygon.holes[i], &mut bboxes_out[i + 1]);
} else {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constants::{EPSILON_RAD, M_PI};
use crate::latlng::{_set_geo_degs, geo_almost_equal};
use crate::LatLng;
#[test]
fn test_bbox_width_height_rads() {
let bbox = BBox {
north: (1.0_f64).to_radians(),
south: 0.0,
east: (1.0_f64).to_radians(),
west: 0.0,
};
assert!((bbox_width_rads(&bbox) - (1.0_f64).to_radians()).abs() < EPSILON_RAD);
assert!((bbox_height_rads(&bbox) - (1.0_f64).to_radians()).abs() < EPSILON_RAD);
let transmeridian_bbox = BBox {
north: 0.1,
south: -0.1,
east: -M_PI + 0.2,
west: M_PI - 0.2,
}; assert!(bbox_is_transmeridian(&transmeridian_bbox));
assert!(
(bbox_width_rads(&transmeridian_bbox) - (0.4)).abs() < EPSILON_RAD,
"Transmeridian width. Got: {}",
bbox_width_rads(&transmeridian_bbox)
);
}
#[test]
fn test_bbox_center() {
let mut center = LatLng::default();
let bbox1 = BBox {
north: 1.0,
south: 0.8,
east: 1.0,
west: 0.8,
}; let expected1 = LatLng { lat: 0.9, lng: 0.9 };
bbox_center(&bbox1, &mut center);
assert!(geo_almost_equal(¢er, &expected1));
let bbox_tm = BBox {
north: 0.1,
south: -0.1,
east: -M_PI + 0.1,
west: M_PI - 0.1,
}; let expected_tm = LatLng { lat: 0.0, lng: -M_PI };
bbox_center(&bbox_tm, &mut center);
assert!(
geo_almost_equal(¢er, &expected_tm),
"Center transmeridian. Got: {:?}, Expected: {:?}",
center,
expected_tm
);
}
#[test]
fn test_bbox_contains_point() {
let bbox = BBox {
north: 0.1,
south: -0.1,
east: 0.2,
west: -0.2,
};
let inside = LatLng { lat: 0.0, lng: 0.0 };
let outside_lat = LatLng { lat: 0.5, lng: 0.0 };
let outside_lng = LatLng { lat: 0.0, lng: 0.5 };
assert!(bbox_contains_point(&bbox, &inside));
assert!(!bbox_contains_point(&bbox, &outside_lat));
assert!(!bbox_contains_point(&bbox, &outside_lng));
let trans_bbox = BBox {
north: 0.1,
south: -0.1,
east: -M_PI + 0.1,
west: M_PI - 0.1,
}; let inside_tm_east = LatLng {
lat: 0.0,
lng: -M_PI + 0.05,
};
let inside_tm_west = LatLng {
lat: 0.0,
lng: M_PI - 0.05,
};
let outside_tm = LatLng { lat: 0.0, lng: 0.0 }; assert!(bbox_contains_point(&trans_bbox, &inside_tm_east));
assert!(bbox_contains_point(&trans_bbox, &inside_tm_west));
assert!(!bbox_contains_point(&trans_bbox, &outside_tm));
}
}