use std::collections::HashMap;
use itertools::Itertools;
use mappers::{projections::LambertConformalConic, Projection};
#[derive(Clone, Debug)]
pub struct PlateCareeProjection {
pub latitudes: RegularCoordinateIterator,
pub longitudes: RegularCoordinateIterator,
pub projection_name: String,
pub projection_params: HashMap<String, f64>,
}
#[derive(Clone, Debug)]
pub struct LambertConformalConicProjection {
pub x: RegularCoordinateIterator,
pub y: RegularCoordinateIterator,
pub projection: LambertConformalConic,
pub projection_name: String,
pub projection_params: HashMap<String, f64>,
}
#[derive(Clone, Debug)]
pub enum LatLngProjection {
PlateCaree(PlateCareeProjection),
LambertConformal(LambertConformalConicProjection),
}
impl LatLngProjection {
pub fn is_regular_latlng_grid(&self) -> bool {
match self {
LatLngProjection::PlateCaree(_) => true,
LatLngProjection::LambertConformal(_) => false,
}
}
pub fn lat_lng(&self) -> (Vec<f64>, Vec<f64>) {
match self {
LatLngProjection::PlateCaree(projection) => {
let lats: Vec<f64> = projection.latitudes.clone().collect();
let lon_start = projection.longitudes.start;
let lngs: Vec<f64> = projection
.longitudes
.clone()
.map(|lon| {
if lon >= 360.0 {
lon - 360.0
} else if lon < 0.0 && lon_start >= 0.0 {
lon + 360.0
} else {
lon
}
})
.collect();
(lats, lngs)
}
LatLngProjection::LambertConformal(projection) => projection
.y
.clone()
.flat_map(|y_coord| {
let mut x = projection.x.clone();
x.current_index = 0;
x.clone()
.map(|x_coord| {
let projected = projection
.projection
.inverse_project(x_coord, y_coord)
.expect("Failed to inverse project from xy to lnglat");
(projected.1, projected.0)
})
.collect::<Vec<(f64, f64)>>()
})
.unzip(),
}
}
fn longitude_wrap_roll(&self) -> Option<usize> {
match self {
LatLngProjection::PlateCaree(_) => wrap_roll(&self.lat_lng().1),
LatLngProjection::LambertConformal(_) => None,
}
}
pub fn lat_lng_adjusted(&self, adjust: bool) -> (Vec<f64>, Vec<f64>) {
let (lats, lngs) = self.lat_lng();
if !adjust {
return (lats, lngs);
}
(lats, adjust_longitude_values(lngs))
}
pub fn adjust_data_longitude(&self, data: Vec<f64>, adjust: bool) -> Vec<f64> {
if !adjust {
return data;
}
match (self, self.longitude_wrap_roll()) {
(LatLngProjection::PlateCaree(projection), Some(roll)) => {
let nx = projection.longitudes.count;
let ny = projection.latitudes.count;
rotate_rows_left(&data, ny, nx, roll)
}
_ => data,
}
}
pub fn x(&self) -> Vec<f64> {
match self {
LatLngProjection::PlateCaree(projection) => {
let lon_start = projection.longitudes.start;
projection
.longitudes
.clone()
.map(|lon| {
if lon >= 360.0 {
lon - 360.0
} else if lon < 0.0 && lon_start >= 0.0 {
lon + 360.0
} else {
lon
}
})
.collect()
}
LatLngProjection::LambertConformal(projection) => projection.x.clone().collect(),
}
}
pub fn y(&self) -> Vec<f64> {
match self {
LatLngProjection::PlateCaree(projection) => projection.latitudes.clone().collect(),
LatLngProjection::LambertConformal(projection) => projection.y.clone().collect(),
}
}
pub fn project_xy(&self, x: f64, y: f64) -> (f64, f64) {
match self {
LatLngProjection::PlateCaree(_) => (x, y),
LatLngProjection::LambertConformal(projection) => {
let projected = projection.projection.project(x, y).unwrap();
(projected.1, projected.0)
}
}
}
pub fn project_latlng(&self, lat: f64, lng: f64) -> (f64, f64) {
match self {
LatLngProjection::PlateCaree(_) => (lng, lat),
LatLngProjection::LambertConformal(projection) => {
let projected = projection.projection.inverse_project(lng, lat).unwrap();
(projected.1, projected.0)
}
}
}
pub fn bbox(&self) -> (f64, f64, f64, f64) {
match self {
LatLngProjection::PlateCaree(_) | LatLngProjection::LambertConformal(_) => {
let (lat, lng) = self.lat_lng();
let (min_lat, max_lat) = lat.into_iter().minmax().into_option().unwrap();
let (min_lng, max_lng) = lng.into_iter().minmax().into_option().unwrap();
(min_lng, min_lat, max_lng, max_lat)
}
}
}
pub fn latlng_start(&self) -> (f64, f64) {
match self {
LatLngProjection::PlateCaree(projection) => {
(projection.latitudes.start, projection.longitudes.start)
}
LatLngProjection::LambertConformal(projection) => {
self.project_xy(projection.x.start, projection.y.start)
}
}
}
pub fn latlng_end(&self) -> (f64, f64) {
match self {
LatLngProjection::PlateCaree(projection) => {
(projection.latitudes.end, projection.longitudes.end)
}
LatLngProjection::LambertConformal(projection) => {
self.project_xy(projection.x.end, projection.y.end)
}
}
}
pub fn proj_name(&self) -> String {
match self {
LatLngProjection::PlateCaree(projection) => projection.projection_name.clone(),
LatLngProjection::LambertConformal(projection) => projection.projection_name.clone(),
}
}
pub fn proj_params(&self) -> HashMap<String, f64> {
match self {
LatLngProjection::PlateCaree(projection) => projection.projection_params.clone(),
LatLngProjection::LambertConformal(projection) => projection.projection_params.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct RegularCoordinateIterator {
start: f64,
step: f64,
end: f64,
current_index: usize,
count: usize,
}
impl RegularCoordinateIterator {
pub fn new(start: f64, step: f64, count: usize) -> Self {
Self {
start,
step,
end: start + (step * (count - 1) as f64),
current_index: 0,
count,
}
}
}
impl Iterator for RegularCoordinateIterator {
type Item = f64;
fn next(&mut self) -> Option<Self::Item> {
if self.count == 0 || self.current_index == self.count {
return None;
}
let coordinate = self.start + self.step * self.current_index as f64;
self.current_index += 1;
Some(coordinate)
}
}
fn wrap_longitude(lon: f64) -> f64 {
if lon >= 180.0 {
lon - 360.0
} else {
lon
}
}
fn wrap_roll(lons: &[f64]) -> Option<usize> {
let nx = lons.len();
if nx < 2 {
return None;
}
let dx = lons[1] - lons[0];
if dx <= 0.0 {
return None;
}
if (dx * nx as f64 - 360.0).abs() >= dx / 4.0 {
return None;
}
let (roll, _) =
lons.iter()
.enumerate()
.fold((0usize, f64::INFINITY), |(roll, min), (i, &lon)| {
let wrapped = wrap_longitude(lon);
if wrapped < min {
(i, wrapped)
} else {
(roll, min)
}
});
Some(roll)
}
pub fn adjust_longitude_values(longitudes: Vec<f64>) -> Vec<f64> {
match wrap_roll(&longitudes) {
Some(roll) => {
let wrapped: Vec<f64> = longitudes.iter().map(|&lon| wrap_longitude(lon)).collect();
rotate_left(&wrapped, roll)
}
None => longitudes,
}
}
fn rotate_left(values: &[f64], roll: usize) -> Vec<f64> {
let n = values.len();
if n == 0 || roll.is_multiple_of(n) {
return values.to_vec();
}
let roll = roll % n;
(0..n).map(|i| values[(i + roll) % n]).collect()
}
fn rotate_rows_left(data: &[f64], ny: usize, nx: usize, roll: usize) -> Vec<f64> {
if nx == 0 || roll.is_multiple_of(nx) || ny * nx != data.len() {
return data.to_vec();
}
let roll = roll % nx;
let mut out = vec![0.0; data.len()];
for r in 0..ny {
let row = &data[r * nx..(r + 1) * nx];
for c in 0..nx {
out[r * nx + c] = row[(c + roll) % nx];
}
}
out
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
#[test]
fn test_regular_grid_iterator() {
let iter = super::RegularCoordinateIterator::new(0.0, 1.0, 10);
let coords = iter.collect::<Vec<_>>();
assert_eq!(coords.len(), 10);
assert!((coords[0] - 0.0).abs() < f64::EPSILON);
assert!((coords[9] - 9.0).abs() < f64::EPSILON);
}
#[test]
fn test_regular_grid_latlng_projection() {
let latitudes = super::RegularCoordinateIterator::new(0.0, 1.0, 10);
let longitudes = super::RegularCoordinateIterator::new(0.0, 2.0, 5);
let projection = super::LatLngProjection::PlateCaree(
crate::utils::iter::projection::PlateCareeProjection {
latitudes,
longitudes,
projection_name: "latlon".into(),
projection_params: HashMap::from([("a".into(), 6367470.), ("b".into(), 6367470.)]),
},
);
let (lats, lngs) = projection.lat_lng();
assert_eq!(lats.len(), 10);
assert_eq!(lngs.len(), 5);
}
#[test]
fn test_start_end_regular_grid() {
let start = 0.0;
let step = 0.25;
let end = 359.75;
let count = 1440;
let iter = super::RegularCoordinateIterator::new(start, step, count);
assert!((iter.end - end).abs() < f64::EPSILON);
}
#[test]
fn test_wrapped_longitude_grid() {
let latitudes = super::RegularCoordinateIterator::new(90.0, -0.25, 721);
let longitudes = super::RegularCoordinateIterator::new(180.0, 0.25, 1440);
let projection = super::LatLngProjection::PlateCaree(
crate::utils::iter::projection::PlateCareeProjection {
latitudes,
longitudes,
projection_name: "latlon".into(),
projection_params: HashMap::from([("a".into(), 6367470.), ("b".into(), 6367470.)]),
},
);
let (lats, lngs) = projection.lat_lng();
assert_eq!(lats.len(), 721);
assert!((lats[0] - 90.0).abs() < f64::EPSILON);
assert!((lats[720] - (-90.0)).abs() < f64::EPSILON);
assert_eq!(lngs.len(), 1440);
assert!((lngs[0] - 180.0).abs() < f64::EPSILON); assert!((lngs[720] - 0.0).abs() < f64::EPSILON); assert!((lngs[1439] - 179.75).abs() < f64::EPSILON);
for lng in &lngs {
assert!(
*lng >= 0.0 && *lng < 360.0,
"Longitude {} out of range",
lng
);
}
}
fn platecaree(lon_start: f64, lon_step: f64, lon_count: usize) -> super::LatLngProjection {
let latitudes = super::RegularCoordinateIterator::new(90.0, -1.0, 5);
let longitudes = super::RegularCoordinateIterator::new(lon_start, lon_step, lon_count);
super::LatLngProjection::PlateCaree(crate::utils::iter::projection::PlateCareeProjection {
latitudes,
longitudes,
projection_name: "latlon".into(),
projection_params: HashMap::new(),
})
}
#[test]
fn test_longitude_wrap_roll() {
assert_eq!(platecaree(0.0, 0.25, 1440).longitude_wrap_roll(), Some(720));
assert_eq!(platecaree(0.0, 0.5, 720).longitude_wrap_roll(), Some(360));
assert_eq!(platecaree(0.0, 3.0, 120).longitude_wrap_roll(), Some(60));
assert_eq!(platecaree(180.0, 0.25, 1440).longitude_wrap_roll(), Some(0));
assert_eq!(
platecaree(-180.0, 0.25, 1440).longitude_wrap_roll(),
Some(0)
);
}
#[test]
fn test_longitude_wrap_roll_ineligible() {
assert_eq!(platecaree(0.0, 0.25, 360).longitude_wrap_roll(), None);
assert_eq!(platecaree(359.75, -0.25, 1440).longitude_wrap_roll(), None);
assert_eq!(platecaree(0.0, 0.25, 1441).longitude_wrap_roll(), None);
}
#[test]
fn test_lat_lng_adjusted_monotonic() {
let projection = platecaree(0.0, 0.5, 720);
let (_, native) = projection.lat_lng();
let (lats, lngs) = projection.lat_lng_adjusted(true);
assert_eq!(lats, projection.lat_lng().0);
assert_eq!(lngs.len(), 720);
assert!((lngs[0] - (-180.0)).abs() < f64::EPSILON);
assert!((lngs[719] - 179.5).abs() < f64::EPSILON);
for w in lngs.windows(2) {
assert!(w[1] > w[0], "not monotonic at {} -> {}", w[0], w[1]);
}
for &lng in &lngs {
assert!((-180.0..180.0).contains(&lng), "out of range: {lng}");
}
let mut from_native: Vec<f64> = native.iter().map(|&l| super::wrap_longitude(l)).collect();
let mut adjusted = lngs.clone();
from_native.sort_by(|a, b| a.partial_cmp(b).unwrap());
adjusted.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(from_native, adjusted);
}
#[test]
fn test_longitude_wrap_off_column_split() {
let projection = platecaree(0.5, 1.0, 360);
assert_eq!(projection.longitude_wrap_roll(), Some(180));
let (_, lngs) = projection.lat_lng_adjusted(true);
assert_eq!(lngs.len(), 360);
assert!((lngs[0] - (-179.5)).abs() < f64::EPSILON);
assert!((lngs[359] - 179.5).abs() < f64::EPSILON);
for w in lngs.windows(2) {
assert!(w[1] > w[0], "not monotonic at {} -> {}", w[0], w[1]);
}
for &lng in &lngs {
assert!((-180.0..180.0).contains(&lng), "out of range: {lng}");
}
assert_eq!(super::adjust_longitude_values(lngs.clone()), lngs);
}
#[test]
fn test_lat_lng_adjusted_noop_when_disabled_or_ineligible() {
let global = platecaree(0.0, 0.5, 720);
assert_eq!(global.lat_lng_adjusted(false), global.lat_lng());
let regional = platecaree(0.0, 0.25, 360);
assert_eq!(regional.lat_lng_adjusted(true), regional.lat_lng());
}
#[test]
fn test_rotate_rows_left() {
let data = vec![0.0, 1.0, 2.0, 3.0, 10.0, 11.0, 12.0, 13.0];
let rolled = super::rotate_rows_left(&data, 2, 4, 1);
assert_eq!(rolled, vec![1.0, 2.0, 3.0, 0.0, 11.0, 12.0, 13.0, 10.0]);
assert_eq!(super::rotate_rows_left(&data, 2, 4, 0), data);
assert_eq!(super::rotate_rows_left(&data, 2, 4, 4), data);
assert_eq!(super::rotate_rows_left(&data, 3, 4, 1), data);
}
}