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(processed: &Array2<f64>, bbox_ratio: f64, size_scale: f64) -> RidgeScene {
121 let (nrows, ncols) = processed.dim();
122 let rows: Vec<RidgeRow> = processed
123 .outer_iter()
124 .enumerate()
125 .map(|(i, src)| {
126 let baseline = -LINE_SPACING * i as f64;
127 RidgeRow {
128 baseline,
129 y: src.iter().map(|&v| v + baseline).collect(),
130 }
131 })
132 .collect();
133
134 let (mut vmin, mut vmax) = processed
135 .iter()
136 .copied()
137 .filter(|v| v.is_finite())
138 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), v| {
139 (lo.min(v), hi.max(v))
140 });
141 if !vmin.is_finite() {
142 vmin = 0.0;
143 vmax = 1.0;
144 }
145
146 let (xmin, xmax_data, ymax_data) = rows
150 .iter()
151 .flat_map(|row| row.y.iter().enumerate())
152 .filter(|&(_, y)| y.is_finite())
153 .fold(
154 (usize::MAX, 0usize, f64::NEG_INFINITY),
155 |(xmin, xmax, ymax), (c, &y)| (xmin.min(c), xmax.max(c), ymax.max(y)),
156 );
157 let ymin = rows
160 .iter()
161 .filter(|row| row.y.iter().any(|y| y.is_finite()))
162 .map(|row| row.baseline)
163 .fold(f64::INFINITY, f64::min);
164 let (xmin, xmax_data, ymin, ymax_data) = if xmin == usize::MAX {
165 (0, ncols - 1, -LINE_SPACING * (nrows - 1) as f64, vmax)
167 } else {
168 (xmin, xmax_data, ymin, ymax_data)
169 };
170 let dx = (xmax_data - xmin) as f64 * AXES_MARGIN;
171 let dy = (ymax_data - ymin) * AXES_MARGIN;
172
173 let width_px = size_scale * FIG_DPI;
174 let height_px = size_scale * bbox_ratio * FIG_DPI;
175 let layout = FigureLayout {
176 width_px,
177 height_px,
178 axes: [
179 SUBPLOT_LEFT * width_px,
180 (1.0 - SUBPLOT_TOP) * height_px,
181 SUBPLOT_RIGHT * width_px,
182 (1.0 - SUBPLOT_BOTTOM) * height_px,
183 ],
184 xlim: [xmin as f64 - dx, xmax_data as f64 + dx],
185 ylim: [ymin - dy, ymax_data + dy],
186 };
187
188 RidgeScene {
189 rows,
190 n_points: ncols,
191 vmin,
192 vmax,
193 layout,
194 }
195 }
196
197 pub fn gradient_color(&self, line: &LineColor, idx: usize) -> crate::colormap::Rgb {
199 match line {
200 LineColor::Solid(rgb) => *rgb,
201 LineColor::Map(cm) => {
202 let denom = self.rows.len().saturating_sub(1).max(1) as f64;
203 cm.at(idx as f64 / denom)
204 }
205 }
206 }
207
208 pub fn elevation_color(&self, line: &LineColor, value: f64) -> crate::colormap::Rgb {
210 let LineColor::Map(cm) = line else {
211 return [0, 0, 0];
212 };
213 let t = if self.vmax > self.vmin {
214 (value - self.vmin) / (self.vmax - self.vmin)
215 } else {
216 0.0
217 };
218 cm.at(t)
219 }
220
221 pub fn label_color(&self, line: &LineColor) -> crate::colormap::Rgb {
223 match line {
224 LineColor::Solid(rgb) => *rgb,
225 LineColor::Map(cm) => cm.at(0.0),
226 }
227 }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
232#[serde(rename_all = "lowercase")]
233pub enum Fit {
234 Reshape,
236 Plane,
239}
240
241#[allow(clippy::too_many_arguments)]
243pub fn build_scene(
244 source: &dyn crate::srtm::TileSource,
245 bbox: &crate::Bbox,
246 num_lines: usize,
247 elevation_pts: usize,
248 viewpoint_angle: f64,
249 crop: bool,
250 interpolation: u32,
251 lock_resolution: bool,
252 water_ntile: f64,
253 lake_flatness: i32,
254 vertical_ratio: f64,
255 size_scale: f64,
256) -> Result<RidgeScene, Error> {
257 build_scene_fit(
258 source,
259 bbox,
260 num_lines,
261 elevation_pts,
262 viewpoint_angle,
263 crop,
264 interpolation,
265 lock_resolution,
266 Fit::Reshape,
267 water_ntile,
268 lake_flatness,
269 vertical_ratio,
270 size_scale,
271 )
272}
273
274#[allow(clippy::too_many_arguments)]
278pub fn build_scene_fit(
279 source: &dyn crate::srtm::TileSource,
280 bbox: &crate::Bbox,
281 num_lines: usize,
282 elevation_pts: usize,
283 viewpoint_angle: f64,
284 crop: bool,
285 interpolation: u32,
286 lock_resolution: bool,
287 fit: Fit,
288 water_ntile: f64,
289 lake_flatness: i32,
290 vertical_ratio: f64,
291 size_scale: f64,
292) -> Result<RidgeScene, Error> {
293 if !bbox.is_valid() {
294 return Err(Error::InvalidBbox(*bbox));
295 }
296 let rotating = viewpoint_angle.rem_euclid(360.0) != 0.0;
297 let mut values = match fit {
298 Fit::Plane => {
299 let _ = lock_resolution;
300 crate::grid::sample(source, bbox, num_lines, elevation_pts)
301 }
302 Fit::Reshape => {
303 let (mut lines, mut pts) = (num_lines, elevation_pts);
304 if !lock_resolution && crate::grid::swap_for_angle(viewpoint_angle) {
305 std::mem::swap(&mut lines, &mut pts);
306 }
307 crate::grid::sample(source, bbox, lines, pts)
308 }
309 };
310 if rotating {
311 values = match fit {
312 Fit::Plane => {
313 crate::rotate::rotate_fixed_plane(&values, viewpoint_angle, interpolation)
314 }
315 Fit::Reshape => crate::rotate::rotate(&values, viewpoint_angle, !crop, interpolation),
316 };
317 }
318 let processed = crate::preprocess::preprocess(
319 &values,
320 water_ntile,
321 lake_flatness,
322 vertical_ratio,
323 1.0, None, )?;
326 Ok(RidgeScene::from_grid(&processed, bbox.ratio(), size_scale))
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use crate::srtm::SyntheticSource;
333
334 #[test]
335 fn runs_split_at_gaps() {
336 let row = RidgeRow {
337 baseline: -6.0,
338 y: vec![1.0, f64::NAN, f64::NAN, 2.0, 3.0, f64::NAN, 4.0],
339 };
340 assert_eq!(row.runs(), vec![(0, 1), (3, 5), (6, 7)]);
341 }
342
343 #[test]
344 fn scene_geometry() {
345 let src = SyntheticSource { side: 1201 };
346 let scene = build_scene(
347 &src,
348 &crate::DEFAULT_BBOX,
349 20,
350 30,
351 0.0,
352 false,
353 0,
354 false,
355 10.0,
356 3,
357 40.0,
358 DEFAULT_SIZE_SCALE,
359 )
360 .unwrap();
361 assert_eq!(scene.rows.len(), 20);
362 assert_eq!(scene.n_points, 30);
363 assert_eq!(scene.rows[0].baseline, 0.0);
365 assert_eq!(scene.rows[7].baseline, -42.0);
366 assert_eq!(scene.layout.width_px, 2000.0);
368 assert_eq!(scene.layout.height_px, 2000.0 * crate::DEFAULT_BBOX.ratio());
369 }
370}