1use ndarray::Array2;
10
11use crate::colormap::LineColor;
12use crate::Error;
13
14pub const LINE_SPACING: f64 = 6.0;
16pub const FIG_DPI: f64 = 100.0;
18pub const DEFAULT_SIZE_SCALE: f64 = 20.0;
20pub const SUBPLOT_LEFT: f64 = 0.125;
22pub const SUBPLOT_RIGHT: f64 = 0.9;
23pub const SUBPLOT_BOTTOM: f64 = 0.11;
24pub const SUBPLOT_TOP: f64 = 0.88;
25pub const AXES_MARGIN: f64 = 0.05;
27
28#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
30pub struct FigureLayout {
31 pub width_px: f64,
33 pub height_px: f64,
34 pub axes: [f64; 4],
36 pub xlim: [f64; 2],
38 pub ylim: [f64; 2],
39}
40
41impl FigureLayout {
42 pub fn to_px(&self, x: f64, y: f64) -> (f64, f64) {
44 let fx = (x - self.xlim[0]) / (self.xlim[1] - self.xlim[0]);
45 let fy = (y - self.ylim[0]) / (self.ylim[1] - self.ylim[0]);
46 let ax_x0 = self.axes[0];
47 let ax_x1 = self.axes[2];
48 let ax_y0 = self.axes[1]; let ax_y1 = self.axes[3]; (
51 ax_x0 + fx * (ax_x1 - ax_x0),
52 ax_y0 + (1.0 - fy) * (ax_y1 - ax_y0),
53 )
54 }
55
56 pub fn frac_to_px(&self, fx: f64, fy: f64) -> (f64, f64) {
58 let x = self.axes[0] + fx * (self.axes[2] - self.axes[0]);
59 let y = self.axes[1] + (1.0 - fy) * (self.axes[3] - self.axes[1]);
60 (x, y)
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
66pub struct RidgeRow {
67 pub baseline: f64,
68 pub y: Vec<f64>,
70}
71
72impl RidgeRow {
73 pub fn runs(&self) -> Vec<(usize, usize)> {
75 let mut runs = Vec::new();
76 let mut start: Option<usize> = None;
77 for (i, v) in self.y.iter().enumerate() {
78 if v.is_finite() {
79 if start.is_none() {
80 start = Some(i);
81 }
82 } else if let Some(s) = start.take() {
83 if i > s {
84 runs.push((s, i)); }
86 }
87 }
88 if let Some(s) = start {
89 if self.y.len() > s {
90 runs.push((s, self.y.len()));
91 }
92 }
93 runs
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum ColorKind {
101 Gradient,
103 Elevation,
105}
106
107#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
108pub struct RidgeScene {
109 pub rows: Vec<RidgeRow>,
110 pub n_points: usize,
111 pub vmin: f64,
113 pub vmax: f64,
114 pub layout: FigureLayout,
115}
116
117impl RidgeScene {
118 pub fn from_grid(
121 processed: &Array2<f64>,
122 bbox_ratio: f64,
123 size_scale: f64,
124 frame: Frame,
125 ) -> RidgeScene {
126 let (nrows, ncols) = processed.dim();
127 let rows: Vec<RidgeRow> = processed
128 .outer_iter()
129 .enumerate()
130 .map(|(i, src)| {
131 let baseline = -LINE_SPACING * i as f64;
132 RidgeRow {
133 baseline,
134 y: src.iter().map(|&v| v + baseline).collect(),
135 }
136 })
137 .collect();
138
139 let (mut vmin, mut vmax) = processed
140 .iter()
141 .copied()
142 .filter(|v| v.is_finite())
143 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
144 (lo.min(v), hi.max(v))
145 });
146 if !vmin.is_finite() {
147 vmin = 0.0;
148 vmax = 1.0;
149 }
150
151 let (xmin, xmax_data, ymin) = match frame {
160 Frame::Window => (0.0, (ncols - 1) as f64, -LINE_SPACING * (nrows - 1) as f64),
161 Frame::Land => {
162 let (first, last) = rows
163 .iter()
164 .flat_map(|row| row.y.iter().enumerate())
165 .filter(|&(_, y)| y.is_finite())
166 .fold((usize::MAX, 0usize), |(first, last), (c, _)| {
167 (first.min(c), last.max(c))
168 });
169 let lowest = rows
170 .iter()
171 .filter(|row| row.y.iter().any(|y| y.is_finite()))
172 .map(|row| row.baseline)
173 .fold(f64::INFINITY, f64::min);
174 if first == usize::MAX {
175 (0.0, (ncols - 1) as f64, -LINE_SPACING * (nrows - 1) as f64)
178 } else {
179 (first as f64, last as f64, lowest)
180 }
181 }
182 };
183 let ymax_data = rows
184 .iter()
185 .flat_map(|row| row.y.iter())
186 .copied()
187 .filter(|y| y.is_finite())
188 .fold(f64::NEG_INFINITY, f64::max);
189 let ymax_data = if ymax_data.is_finite() {
191 ymax_data
192 } else {
193 vmax
194 };
195 let dx = (xmax_data - xmin) * AXES_MARGIN;
196 let dy = (ymax_data - ymin) * AXES_MARGIN;
197
198 let width_px = size_scale * FIG_DPI;
199 let height_px = size_scale * bbox_ratio * FIG_DPI;
200 let layout = FigureLayout {
201 width_px,
202 height_px,
203 axes: [
204 SUBPLOT_LEFT * width_px,
205 (1.0 - SUBPLOT_TOP) * height_px,
206 SUBPLOT_RIGHT * width_px,
207 (1.0 - SUBPLOT_BOTTOM) * height_px,
208 ],
209 xlim: [xmin - dx, xmax_data + dx],
210 ylim: [ymin - dy, ymax_data + dy],
211 };
212
213 RidgeScene {
214 rows,
215 n_points: ncols,
216 vmin,
217 vmax,
218 layout,
219 }
220 }
221
222 pub fn gradient_color(&self, line: &LineColor, idx: usize) -> crate::colormap::Rgb {
224 match line {
225 LineColor::Solid(rgb) => *rgb,
226 LineColor::Map(cm) => {
227 let denom = self.rows.len().saturating_sub(1).max(1) as f64;
228 cm.at(idx as f64 / denom)
229 }
230 }
231 }
232
233 pub fn elevation_color(&self, line: &LineColor, value: f64) -> crate::colormap::Rgb {
235 let LineColor::Map(cm) = line else {
236 return [0, 0, 0];
237 };
238 let t = if self.vmax > self.vmin {
239 (value - self.vmin) / (self.vmax - self.vmin)
240 } else {
241 0.0
242 };
243 cm.at(t)
244 }
245
246 pub fn label_color(&self, line: &LineColor) -> crate::colormap::Rgb {
248 match line {
249 LineColor::Solid(rgb) => *rgb,
250 LineColor::Map(cm) => cm.at(0.0),
251 }
252 }
253}
254
255#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
257pub enum Frame {
258 #[default]
261 Window,
262 Land,
266}
267
268#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
270#[serde(rename_all = "lowercase")]
271pub enum Fit {
272 Reshape,
274 Plane,
277}
278
279#[allow(clippy::too_many_arguments)]
281pub fn build_scene(
282 source: &dyn crate::srtm::TileSource,
283 bbox: &crate::Bbox,
284 num_lines: usize,
285 elevation_pts: usize,
286 viewpoint_angle: f64,
287 crop: bool,
288 interpolation: u32,
289 lock_resolution: bool,
290 water_ntile: f64,
291 lake_flatness: i32,
292 vertical_ratio: f64,
293 size_scale: f64,
294 frame: Frame,
295) -> Result<RidgeScene, Error> {
296 build_scene_fit(
297 source,
298 bbox,
299 num_lines,
300 elevation_pts,
301 viewpoint_angle,
302 crop,
303 interpolation,
304 lock_resolution,
305 Fit::Reshape,
306 water_ntile,
307 lake_flatness,
308 vertical_ratio,
309 size_scale,
310 frame,
311 )
312}
313
314#[allow(clippy::too_many_arguments)]
318pub fn build_scene_fit(
319 source: &dyn crate::srtm::TileSource,
320 bbox: &crate::Bbox,
321 num_lines: usize,
322 elevation_pts: usize,
323 viewpoint_angle: f64,
324 crop: bool,
325 interpolation: u32,
326 lock_resolution: bool,
327 fit: Fit,
328 water_ntile: f64,
329 lake_flatness: i32,
330 vertical_ratio: f64,
331 size_scale: f64,
332 frame: Frame,
333) -> Result<RidgeScene, Error> {
334 if !bbox.is_valid() {
335 return Err(Error::InvalidBbox(*bbox));
336 }
337 let rotating = viewpoint_angle.rem_euclid(360.0) != 0.0;
338 let mut values = match fit {
339 Fit::Plane => {
340 let _ = lock_resolution;
341 crate::grid::sample(source, bbox, num_lines, elevation_pts)
342 }
343 Fit::Reshape => {
344 let (mut lines, mut pts) = (num_lines, elevation_pts);
345 if !lock_resolution && crate::grid::swap_for_angle(viewpoint_angle) {
346 std::mem::swap(&mut lines, &mut pts);
347 }
348 crate::grid::sample(source, bbox, lines, pts)
349 }
350 };
351 if rotating {
352 values = match fit {
353 Fit::Plane => {
354 crate::rotate::rotate_fixed_plane(&values, viewpoint_angle, interpolation)
355 }
356 Fit::Reshape => crate::rotate::rotate(&values, viewpoint_angle, !crop, interpolation),
357 };
358 }
359 let processed = crate::preprocess::preprocess(
360 &values,
361 water_ntile,
362 lake_flatness,
363 vertical_ratio,
364 1.0, None, )?;
367 Ok(RidgeScene::from_grid(
368 &processed,
369 bbox.ratio(),
370 size_scale,
371 frame,
372 ))
373}
374
375#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::srtm::SyntheticSource;
379
380 #[test]
381 fn runs_split_at_gaps() {
382 let row = RidgeRow {
383 baseline: -6.0,
384 y: vec![1.0, f64::NAN, f64::NAN, 2.0, 3.0, f64::NAN, 4.0],
385 };
386 assert_eq!(row.runs(), vec![(0, 1), (3, 5), (6, 7)]);
387 }
388
389 #[test]
390 fn scene_geometry() {
391 let src = SyntheticSource { side: 1201 };
392 let scene = build_scene(
393 &src,
394 &crate::DEFAULT_BBOX,
395 20,
396 30,
397 0.0,
398 false,
399 0,
400 false,
401 10.0,
402 3,
403 40.0,
404 DEFAULT_SIZE_SCALE,
405 Frame::Window,
406 )
407 .unwrap();
408 assert_eq!(scene.rows.len(), 20);
409 assert_eq!(scene.n_points, 30);
410 assert_eq!(scene.rows[0].baseline, 0.0);
412 assert_eq!(scene.rows[7].baseline, -42.0);
413 assert_eq!(scene.layout.width_px, 2000.0);
415 assert_eq!(scene.layout.height_px, 2000.0 * crate::DEFAULT_BBOX.ratio());
416 }
417
418 fn water_edged_grid() -> Array2<f64> {
421 Array2::from_shape_fn((6, 10), |(r, c)| {
422 if r < 2 || r == 5 || c < 3 {
423 f64::NAN
424 } else {
425 20.0
426 }
427 })
428 }
429
430 #[test]
431 fn window_frame_is_the_whole_requested_window() {
432 let scene =
435 RidgeScene::from_grid(&water_edged_grid(), 1.0, DEFAULT_SIZE_SCALE, Frame::Window);
436 assert_eq!(
437 scene.layout.xlim,
438 [-9.0 * AXES_MARGIN, 9.0 + 9.0 * AXES_MARGIN]
439 );
440 assert_eq!(
444 scene.layout.ylim,
445 [-30.0 - 38.0 * AXES_MARGIN, 8.0 + 38.0 * AXES_MARGIN]
446 );
447 }
448
449 #[test]
450 fn land_frame_reproduces_the_legacy_crop() {
451 let scene =
455 RidgeScene::from_grid(&water_edged_grid(), 1.0, DEFAULT_SIZE_SCALE, Frame::Land);
456 assert_eq!(
458 scene.layout.xlim,
459 [3.0 - 6.0 * AXES_MARGIN, 9.0 + 6.0 * AXES_MARGIN]
460 );
461 assert_eq!(
463 scene.layout.ylim,
464 [-24.0 - 32.0 * AXES_MARGIN, 8.0 + 32.0 * AXES_MARGIN]
465 );
466 }
467
468 #[test]
469 fn water_masking_never_moves_the_frame() {
470 let land = Array2::from_elem((5, 7), 40.0);
475 let half_water =
476 Array2::from_shape_fn((5, 7), |(_, c)| if c < 3 { f64::NAN } else { 40.0 });
477 let a = RidgeScene::from_grid(&land, 1.0, DEFAULT_SIZE_SCALE, Frame::Window);
478 let b = RidgeScene::from_grid(&half_water, 1.0, DEFAULT_SIZE_SCALE, Frame::Window);
479 assert_eq!(
480 a.layout, b.layout,
481 "the frame is the window, whether the water is there or not"
482 );
483 }
484}