#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AreaOfUse {
pub west: f64,
pub south: f64,
pub east: f64,
pub north: f64,
pub name: &'static str,
}
impl AreaOfUse {
pub const fn new(west: f64, south: f64, east: f64, north: f64, name: &'static str) -> Self {
Self {
west,
south,
east,
north,
name,
}
}
pub fn contains(&self, lon_deg: f64, lat_deg: f64) -> bool {
let lat_ok = lat_deg >= self.south && lat_deg <= self.north;
if !lat_ok {
return false;
}
if self.west <= self.east {
lon_deg >= self.west && lon_deg <= self.east
} else {
lon_deg >= self.west || lon_deg <= self.east
}
}
pub fn contains_with_tolerance(&self, lon_deg: f64, lat_deg: f64, tolerance_deg: f64) -> bool {
lon_deg >= self.west - tolerance_deg
&& lon_deg <= self.east + tolerance_deg
&& lat_deg >= self.south - tolerance_deg
&& lat_deg <= self.north + tolerance_deg
}
pub fn validate(&self, lon_deg: f64, lat_deg: f64) -> core::result::Result<(), String> {
if self.contains(lon_deg, lat_deg) {
Ok(())
} else {
Err(format!(
"Coordinate ({}, {}) is outside the area of use '{}' \
(bounds: west={}, south={}, east={}, north={})",
lon_deg, lat_deg, self.name, self.west, self.south, self.east, self.north
))
}
}
}
const fn utm_north_area(zone: u32) -> AreaOfUse {
let central_meridian = (zone as i32 - 1) * 6 - 177;
let west = central_meridian - 3;
let east = central_meridian + 3;
AreaOfUse::new(west as f64, 0.0, east as f64, 84.0, "UTM North zone")
}
const fn utm_south_area(zone: u32) -> AreaOfUse {
let central_meridian = (zone as i32 - 1) * 6 - 177;
let west = central_meridian - 3;
let east = central_meridian + 3;
AreaOfUse::new(west as f64, -80.0, east as f64, 0.0, "UTM South zone")
}
pub fn area_of_use_for_epsg(code: u32) -> Option<AreaOfUse> {
match code {
4326 => Some(AreaOfUse::new(-180.0, -90.0, 180.0, 90.0, "World")),
3857 => Some(AreaOfUse::new(
-180.0,
-85.06,
180.0,
85.06,
"World between 85.06°S and 85.06°N",
)),
4269 => Some(AreaOfUse::new(
-172.54,
14.92,
-47.74,
86.46,
"North America",
)),
4258 => Some(AreaOfUse::new(-16.1, 32.88, 40.18, 84.73, "Europe")),
4283 => Some(AreaOfUse::new(93.41, -60.55, 173.34, -8.47, "Australia")),
7844 => Some(AreaOfUse::new(93.41, -60.55, 173.34, -8.47, "Australia")),
4612 => Some(AreaOfUse::new(122.38, 17.09, 157.65, 46.05, "Japan")),
6668 => Some(AreaOfUse::new(122.38, 17.09, 157.65, 46.05, "Japan")),
4167 => Some(AreaOfUse::new(160.6, -55.95, -171.2, -25.88, "New Zealand")),
2154 => Some(AreaOfUse::new(-9.86, 41.15, 10.38, 51.56, "France")),
27700 => Some(AreaOfUse::new(-7.56, 49.96, 1.78, 60.84, "United Kingdom")),
3035 => Some(AreaOfUse::new(-35.58, 24.6, 44.83, 84.73, "Europe")),
2193 => Some(AreaOfUse::new(166.37, -47.33, 178.63, -34.1, "New Zealand")),
26910 => Some(AreaOfUse::new(
-126.0,
0.0,
-120.0,
84.0,
"UTM zone 10N (NAD83)",
)),
26917 => Some(AreaOfUse::new(
-84.0,
0.0,
-78.0,
84.0,
"UTM zone 17N (NAD83)",
)),
2229 => Some(AreaOfUse::new(
-121.42,
32.76,
-114.12,
35.81,
"USA - California zone 5",
)),
2277 => Some(AreaOfUse::new(
-105.73,
29.78,
-93.51,
32.27,
"USA - Texas Central",
)),
4490 => Some(AreaOfUse::new(73.62, 16.7, 134.77, 53.56, "China")),
5174 => Some(AreaOfUse::new(124.53, 33.14, 131.01, 38.45, "South Korea")),
24047 => Some(AreaOfUse::new(96.0, 0.0, 102.0, 84.0, "UTM zone 47N")),
28406 => Some(AreaOfUse::new(30.0, 40.0, 36.0, 80.0, "Russia zone 6")),
3006 => Some(AreaOfUse::new(10.03, 54.96, 24.17, 69.07, "Sweden")),
2157 => Some(AreaOfUse::new(-10.56, 51.39, -5.34, 55.43, "Ireland")),
23700 => Some(AreaOfUse::new(16.11, 45.74, 22.9, 48.58, "Hungary")),
31467 => Some(AreaOfUse::new(7.5, 47.27, 10.51, 55.06, "Germany zone 3")),
32601..=32660 => {
let zone = code - 32600;
Some(utm_north_area(zone))
}
32701..=32760 => {
let zone = code - 32700;
Some(utm_south_area(zone))
}
_ => None,
}
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_wgs84_contains_world() {
let area = area_of_use_for_epsg(4326).expect("WGS84 should be defined");
assert!(area.contains(0.0, 0.0));
assert!(area.contains(180.0, 90.0));
assert!(area.contains(-180.0, -90.0));
}
#[test]
fn test_web_mercator_excludes_poles() {
let area = area_of_use_for_epsg(3857).expect("Web Mercator should be defined");
assert!(!area.contains(0.0, 86.0));
assert!(!area.contains(0.0, -86.0));
assert!(area.contains(0.0, 50.0));
}
#[test]
fn test_utm_zone_33n() {
let area = area_of_use_for_epsg(32633).expect("UTM 33N should be defined");
assert!(area.contains(15.0, 45.0));
assert!(!area.contains(0.0, 45.0));
}
#[test]
fn test_utm_zone_1s() {
let area = area_of_use_for_epsg(32701).expect("UTM 1S should be defined");
assert!(area.contains(-177.0, -45.0));
assert!(!area.contains(-177.0, 45.0));
}
#[test]
fn test_british_national_grid() {
let area = area_of_use_for_epsg(27700).expect("BNG should be defined");
assert!(area.contains(-1.0, 51.5));
assert!(!area.contains(10.0, 51.5));
}
#[test]
fn test_france_lambert93() {
let area = area_of_use_for_epsg(2154).expect("Lambert-93 should be defined");
assert!(area.contains(2.35, 48.86));
assert!(!area.contains(20.0, 48.0));
}
#[test]
fn test_unknown_epsg_returns_none() {
assert!(area_of_use_for_epsg(99999).is_none());
}
#[test]
fn test_contains_with_tolerance() {
let area = area_of_use_for_epsg(27700).expect("BNG should be defined");
assert!(!area.contains(-8.0, 55.0));
assert!(area.contains_with_tolerance(-8.0, 55.0, 1.0));
}
#[test]
fn test_validate_ok() {
let area = area_of_use_for_epsg(4326).expect("WGS84 should be defined");
assert!(area.validate(0.0, 0.0).is_ok());
}
#[test]
fn test_validate_error() {
let area = area_of_use_for_epsg(3857).expect("Web Mercator should be defined");
let result = area.validate(0.0, 90.0);
assert!(result.is_err());
let msg = result.expect_err("should be error");
assert!(msg.contains("outside the area of use"));
}
#[test]
fn test_nz_bounds() {
let area = area_of_use_for_epsg(2193).expect("NZTM should be defined");
assert!(area.contains(174.77, -41.29));
}
#[test]
fn test_japan_bounds() {
let area = area_of_use_for_epsg(6668).expect("JGD2011 should be defined");
assert!(area.contains(139.69, 35.69));
}
#[test]
fn test_utm_north_all_zones_valid() {
for zone in 1..=60 {
let code = 32600 + zone;
let area = area_of_use_for_epsg(code).expect("UTM North zone should be defined");
let cm = (zone as i32 - 1) * 6 - 177;
assert!(area.contains(cm as f64, 42.0));
assert_eq!(area.south, 0.0);
assert_eq!(area.north, 84.0);
}
}
#[test]
fn test_utm_south_all_zones_valid() {
for zone in 1..=60 {
let code = 32700 + zone;
let area = area_of_use_for_epsg(code).expect("UTM South zone should be defined");
let cm = (zone as i32 - 1) * 6 - 177;
assert!(area.contains(cm as f64, -42.0));
assert_eq!(area.south, -80.0);
assert_eq!(area.north, 0.0);
}
}
#[test]
fn test_area_of_use_new() {
let area = AreaOfUse::new(-10.0, -20.0, 30.0, 40.0, "Test area");
assert_eq!(area.west, -10.0);
assert_eq!(area.south, -20.0);
assert_eq!(area.east, 30.0);
assert_eq!(area.north, 40.0);
assert_eq!(area.name, "Test area");
}
}