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