1use crate::render::Rect;
5
6use super::{AxisSpan, Coord};
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum MapProjection {
11 Quick,
15 Mercator,
18}
19
20pub struct CoordMap {
23 pub projection: MapProjection,
24 x_span: Option<AxisSpan>,
25 y_span: Option<AxisSpan>,
26}
27
28impl CoordMap {
29 pub fn new(projection: MapProjection) -> Self {
30 CoordMap {
31 projection,
32 x_span: None,
33 y_span: None,
34 }
35 }
36
37 fn ratio(&self) -> f64 {
38 let (Some(x), Some(y)) = (self.x_span, self.y_span) else {
39 return 1.0;
40 };
41 let dx = (x.max - x.min).abs();
42 if dx < f64::EPSILON {
43 return 1.0;
44 }
45 match self.projection {
46 MapProjection::Quick => {
47 let mid = ((y.min + y.max) / 2.0).to_radians().cos().abs().max(1e-6);
48 (y.max - y.min).abs() / dx / mid
49 }
50 MapProjection::Mercator => (mercator(y.max) - mercator(y.min)).abs() / dx,
51 }
52 }
53
54 fn warp_y(&self, n: f64) -> f64 {
56 let Some(s) = self
57 .y_span
58 .filter(|s| s.max > s.min && (s.pmax - s.pmin).abs() > 1e-12)
59 else {
60 return n;
61 };
62 if self.projection != MapProjection::Mercator {
63 return n;
64 }
65 let v = s.min + (n - s.pmin) / (s.pmax - s.pmin) * (s.max - s.min);
66 if v < s.min || v > s.max {
67 return n;
68 }
69 let (a, b) = (mercator(s.min), mercator(s.max));
70 s.pmin + (mercator(v) - a) / (b - a) * (s.pmax - s.pmin)
71 }
72}
73
74fn mercator(lat: f64) -> f64 {
76 let phi = lat.clamp(-85.05, 85.05).to_radians();
77 (std::f64::consts::FRAC_PI_4 + phi / 2.0)
78 .tan()
79 .ln()
80 .to_degrees()
81}
82
83impl Coord for CoordMap {
84 fn transform(&self, point: (f64, f64), plot_area: &Rect) -> (f64, f64) {
85 let r = self
86 .data_rect(plot_area)
87 .expect("coord_map always letterboxes");
88 let (nx, ny) = (point.0, self.warp_y(point.1));
89 (r.x + nx * r.width, r.y + (1.0 - ny) * r.height)
90 }
91
92 fn set_domains(&mut self, x: Option<AxisSpan>, y: Option<AxisSpan>) {
93 self.x_span = x;
94 self.y_span = y;
95 }
96
97 fn fixed_aspect(&self) -> Option<f64> {
98 Some(self.ratio())
99 }
100
101 fn data_rect(&self, plot_area: &Rect) -> Option<Rect> {
102 let data_aspect = self.ratio();
103 let pixel_aspect = plot_area.height / plot_area.width;
104 let (w, h, off_x, off_y) = if pixel_aspect > data_aspect {
105 let h = plot_area.width * data_aspect;
106 (plot_area.width, h, 0.0, (plot_area.height - h) / 2.0)
107 } else {
108 let w = plot_area.height / data_aspect;
109 (w, plot_area.height, (plot_area.width - w) / 2.0, 0.0)
110 };
111 Some(Rect {
112 x: plot_area.x + off_x,
113 y: plot_area.y + off_y,
114 width: w,
115 height: h,
116 })
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn span(min: f64, max: f64) -> AxisSpan {
125 AxisSpan {
126 min,
127 max,
128 pmin: 0.0,
129 pmax: 1.0,
130 }
131 }
132
133 #[test]
134 fn quickmap_aspect_is_one_over_cos_lat() {
135 let mut c = CoordMap::new(MapProjection::Quick);
137 c.set_domains(Some(span(0.0, 10.0)), Some(span(55.0, 65.0)));
138 assert!((c.ratio() - 2.0).abs() < 1e-9, "{}", c.ratio());
139 }
140
141 #[test]
142 fn mercator_stretches_high_latitudes() {
143 let mut c = CoordMap::new(MapProjection::Mercator);
144 c.set_domains(Some(span(0.0, 10.0)), Some(span(0.0, 60.0)));
145 let mid = c.warp_y(0.5);
147 let want = mercator(30.0) / mercator(60.0);
148 assert!((mid - want).abs() < 1e-9 && mid < 0.5, "{mid}");
149 }
150}