const MERCATOR_MAX_LAT: f64 = 85.05112878;
pub fn mercator(lon: f64, lat: f64) -> (f64, f64) {
let lat_clamped = lat.clamp(-MERCATOR_MAX_LAT, MERCATOR_MAX_LAT);
let x = lon * std::f64::consts::PI / 180.0;
let y = (std::f64::consts::FRAC_PI_4 + lat_clamped * std::f64::consts::PI / 360.0)
.tan()
.ln();
(x, y)
}
pub fn equirect(lon: f64, lat: f64) -> (f64, f64) {
(lon, lat)
}
pub fn graticule(opts: GraticuleOpts) -> Vec<(Vec<f64>, Vec<f64>)> {
let mut out = Vec::new();
for lon in arange(opts.lon_lo, opts.lon_hi, opts.lon_step) {
let lats = linspace(opts.lat_lo, opts.lat_hi, opts.samples);
let xs = vec![lon; lats.len()];
out.push((xs, lats));
}
for lat in arange(opts.lat_lo, opts.lat_hi, opts.lat_step) {
let lons = linspace(opts.lon_lo, opts.lon_hi, opts.samples);
let ys = vec![lat; lons.len()];
out.push((lons, ys));
}
out
}
#[derive(Debug, Clone, Copy)]
pub struct GraticuleOpts {
pub lon_step: f64,
pub lat_step: f64,
pub lon_lo: f64,
pub lon_hi: f64,
pub lat_lo: f64,
pub lat_hi: f64,
pub samples: usize,
}
impl Default for GraticuleOpts {
fn default() -> Self {
Self {
lon_step: 30.0,
lat_step: 30.0,
lon_lo: -180.0,
lon_hi: 180.0,
lat_lo: -80.0,
lat_hi: 80.0,
samples: 60,
}
}
}
fn arange(lo: f64, hi: f64, step: f64) -> Vec<f64> {
if step <= 0.0 {
return Vec::new();
}
let n = ((hi - lo) / step).floor() as usize;
(0..=n).map(|i| lo + i as f64 * step).collect()
}
fn linspace(a: f64, b: f64, n: usize) -> Vec<f64> {
if n < 2 {
return vec![a];
}
let step = (b - a) / (n as f64 - 1.0);
(0..n).map(|i| a + i as f64 * step).collect()
}
pub fn format_lon(v: f64) -> String {
let v_int = if v == v.trunc() { v as i32 } else { 0 };
if v == 0.0 {
return "0°".to_string();
}
if v > 0.0 && v <= 180.0 {
return format!("{}°E", fmt_deg(v, v_int));
}
if v < 0.0 && v >= -180.0 {
return format!("{}°W", fmt_deg(-v, -v_int));
}
if v > 180.0 {
return format!("{}°W", fmt_deg(360.0 - v, (360.0 - v) as i32));
}
format!("{}°E", fmt_deg(v + 360.0, (v + 360.0) as i32))
}
pub fn format_lat(v: f64) -> String {
let v_int = if v == v.trunc() { v as i32 } else { 0 };
if v == 0.0 {
"0°".to_string()
} else if v > 0.0 {
format!("{}°N", fmt_deg(v, v_int))
} else {
format!("{}°S", fmt_deg(-v, -v_int))
}
}
fn fmt_deg(v: f64, v_int: i32) -> String {
if v == v.trunc() {
v_int.to_string()
} else {
format!("{:.1}", v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mercator_origin_is_origin() {
let (x, y) = mercator(0.0, 0.0);
assert!(x.abs() < 1e-9);
assert!(y.abs() < 1e-9);
}
#[test]
fn mercator_45_north() {
let (_, y) = mercator(0.0, 45.0);
assert!((y - 0.881_374).abs() < 1e-4);
}
#[test]
fn equirect_is_identity() {
assert_eq!(equirect(10.0, 20.0), (10.0, 20.0));
}
}