1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
use float_cmp::approx_eq;
use crate::{ellipsoids::Ellipsoid, Projection, ProjectionError};
/// This is a modified version of the [`Azimuthal Equidistant`](crate::projections::AzimuthalEquidistant) projection,
/// defined for islands of Micronesia and described by [Snyder (1987)](https://pubs.er.usgs.gov/publication/pp1395).
/// It does not use Geodesic calculations so it is faster than the original projection, but significantly
/// diverges from the AEQD projection at bigger scales.
///
/// The azimuthal equidistant projection is an azimuthal map projection.
/// It has the useful properties that all points on the map are at proportionally
/// correct distances from the center point, and that all points on the map are at the
/// correct azimuth (direction) from the center point. A useful application for this
/// type of projection is a polar projection which shows all meridians (lines of longitude) as straight,
/// with distances from the pole represented correctly [(Wikipedia, 2022)](https://en.wikipedia.org/wiki/Azimuthal_equidistant_projection).
///
/// Summary by [Snyder (1987)](https://pubs.er.usgs.gov/publication/pp1395):
///
/// - Azimuthal.
/// - Distances measured from the center are true.
/// - Distances not measured along radii from the center are not correct.
/// - The center of projection is the only point without distortion.
/// - Directions from the center are true (except on some oblique and equatorial ellipsoidal forms).
/// - Neither equal-area nor conformal.
/// - All meridians on the polar aspect, the central meridian on other aspects, and the Equator on the equatorial aspect are straight lines.
/// - Parallels on the polar projection are circles spaced at true intervals (equidistant for the sphere).
/// - The outer meridian of a hemisphere on the equatorial aspect (for the sphere) is a circle.
/// - All other meridians and parallels are complex curves.
/// - Not a perspective projection.
/// - Point opposite the center is shown as a circle (for the sphere) surrounding the map.
/// - Used in the polar aspect for world maps and maps of polar hemispheres.
/// - Used in the oblique aspect for atlas maps of continents and world maps for aviation and radio use.
/// - Known for many centuries in the polar aspect.
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct ModifiedAzimuthalEquidistant {
lon_0: f64,
lat_0: f64,
n_1: f64,
g: f64,
ellps: Ellipsoid,
}
impl ModifiedAzimuthalEquidistant {
/// `ModifiedAzimuthalEquidistant` projection constructor.
///
/// To reduce computational overhead of projection functions this
/// constructor is non-trivial and tries to do as much projection computations as possible.
/// Thus creating a new structure can involve a significant computational overhead.
/// When projecting multiple coordinates only one instance of the structure should be created
/// and cloned/borrowed as needed.
///
/// # Arguments
///
/// - `ref_lon`, `ref_lat` - Reference longitude and latitude. Point (0, 0) on the map will be at this coordinates.
/// - `ellps` - Reference [`Ellipsoid`].
///
/// # Errors
///
/// Returns [`ProjectionError::IncorrectParams`] with additional information when:
///
/// - one or more longitudes are not within -180..180 range.
/// - one or more latitudes are not within -90..90 range.
/// - one or more arguments are not finite.
pub fn new(ref_lon: f64, ref_lat: f64, ellps: Ellipsoid) -> Result<Self, ProjectionError> {
if !(-180.0..180.0).contains(&ref_lon) {
return Err(ProjectionError::IncorrectParams(
"longitude must be between -180..180",
));
}
if !(-90.0..90.0).contains(&ref_lat) {
return Err(ProjectionError::IncorrectParams(
"latitude must be between -90..90",
));
}
if !ref_lon.is_finite() || !ref_lat.is_finite() {
return Err(ProjectionError::IncorrectParams(
"one of arguments is not finite",
));
}
let lon_0 = ref_lon.to_radians();
let lat_0 = ref_lat.to_radians();
let n_1 = ellps.A / (1.0 - (ellps.E.powi(2) * (lat_0.sin()).powi(2))).sqrt();
let g = ellps.E * lat_0.sin() / (1.0 - ellps.E.powi(2)).sqrt();
Ok(ModifiedAzimuthalEquidistant {
lon_0,
lat_0,
n_1,
g,
ellps,
})
}
}
impl Projection for ModifiedAzimuthalEquidistant {
fn project_unchecked(&self, lon: f64, lat: f64) -> (f64, f64) {
let lon = lon.to_radians();
let lat = lat.to_radians();
let n = self.ellps.A / (1.0 - (self.ellps.E.powi(2) * (lat.sin()).powi(2))).sqrt();
let psi = (((1.0 - self.ellps.E.powi(2)) * lat.tan())
+ ((self.ellps.E.powi(2) * self.n_1 * self.lat_0.sin()) / (n * lat.cos())))
.atan();
let az = ((lon - self.lon_0).sin())
.atan2((self.lat_0.cos() * psi.tan()) - (self.lat_0.sin() * (lon - self.lon_0).cos()));
let s = if approx_eq!(f64, az.sin(), 0.0) {
((self.lat_0.cos() * psi.sin()) - (self.lat_0.sin() * psi.cos()))
.asin()
.abs()
* az.cos().signum()
} else {
(((lon - self.lon_0).sin() * psi.cos()) / (az.sin())).asin()
};
let h = self.ellps.E * self.lat_0.cos() * az.cos() / (1.0 - self.ellps.E.powi(2)).sqrt();
let c = (self.n_1 * s)
* (1.0 - (s.powi(2) * h.powi(2) * (1.0 - h.powi(2)) / 6.0)
+ ((s.powi(3) / 8.0) * self.g * h * (1.0 - 2.0 * h.powi(2)))
+ ((s.powi(4) / 120.0)
* ((h.powi(2) * (4.0 - 7.0 * h.powi(2)))
- (3.0 * self.g.powi(2) * (1.0 - 7.0 * h.powi(2)))))
- ((s.powi(5) / 48.0) * self.g * h));
let x = c * az.sin();
let y = c * az.cos();
(x, y)
}
fn inverse_project_unchecked(&self, x: f64, y: f64) -> (f64, f64) {
let c = (x * x + y * y).sqrt();
let az = x.atan2(y);
let big_a =
-self.ellps.E * self.ellps.E * ((self.lat_0.cos()).powi(2)) * ((az.cos()).powi(2))
/ (1.0 - self.ellps.E * self.ellps.E);
let big_b = 3.0
* self.ellps.E
* self.ellps.E
* (1.0 - big_a)
* self.lat_0.sin()
* self.lat_0.cos()
* az.cos()
/ (1.0 - self.ellps.E * self.ellps.E);
let big_d = c / self.n_1;
let big_e = big_d
- (big_a * (1.0 + big_a) * big_d.powi(3) / 6.0)
- (big_b * (1.0 + 3.0 * big_a) * big_d.powi(4) / 24.0);
let big_f = 1.0 - (big_a * big_e * big_e / 2.0) - (big_b * big_e.powi(3) / 6.0);
let psi =
((self.lat_0.sin() * big_e.cos()) + (self.lat_0.cos() * big_e.sin() * az.cos())).asin();
let lon = self.lon_0 + (az.sin() * big_e.sin() / psi.cos()).asin();
let lat = ((1.0 - (self.ellps.E * self.ellps.E * big_f * self.lat_0.sin() / psi.sin()))
* psi.tan()
/ (1.0 - self.ellps.E * self.ellps.E))
.atan();
let lon = lon.to_degrees();
let lat = lat.to_degrees();
(lon, lat)
}
}