use ndarray::Array2;
use crate::colormap::LineColor;
use crate::Error;
pub const LINE_SPACING: f64 = 6.0;
pub const FIG_DPI: f64 = 100.0;
pub const DEFAULT_SIZE_SCALE: f64 = 20.0;
pub const SUBPLOT_LEFT: f64 = 0.125;
pub const SUBPLOT_RIGHT: f64 = 0.9;
pub const SUBPLOT_BOTTOM: f64 = 0.11;
pub const SUBPLOT_TOP: f64 = 0.88;
pub const AXES_MARGIN: f64 = 0.05;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct FigureLayout {
pub width_px: f64,
pub height_px: f64,
pub axes: [f64; 4],
pub xlim: [f64; 2],
pub ylim: [f64; 2],
}
impl FigureLayout {
pub fn to_px(&self, x: f64, y: f64) -> (f64, f64) {
let fx = (x - self.xlim[0]) / (self.xlim[1] - self.xlim[0]);
let fy = (y - self.ylim[0]) / (self.ylim[1] - self.ylim[0]);
let ax_x0 = self.axes[0];
let ax_x1 = self.axes[2];
let ax_y0 = self.axes[1]; let ax_y1 = self.axes[3]; (
ax_x0 + fx * (ax_x1 - ax_x0),
ax_y0 + (1.0 - fy) * (ax_y1 - ax_y0),
)
}
pub fn frac_to_px(&self, fx: f64, fy: f64) -> (f64, f64) {
let x = self.axes[0] + fx * (self.axes[2] - self.axes[0]);
let y = self.axes[1] + (1.0 - fy) * (self.axes[3] - self.axes[1]);
(x, y)
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RidgeRow {
pub baseline: f64,
pub y: Vec<f64>,
}
impl RidgeRow {
pub fn runs(&self) -> Vec<(usize, usize)> {
let mut runs = Vec::new();
let mut start: Option<usize> = None;
for (i, v) in self.y.iter().enumerate() {
if v.is_finite() {
if start.is_none() {
start = Some(i);
}
} else if let Some(s) = start.take() {
if i > s {
runs.push((s, i)); }
}
}
if let Some(s) = start {
if self.y.len() > s {
runs.push((s, self.y.len()));
}
}
runs
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ColorKind {
Gradient,
Elevation,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RidgeScene {
pub rows: Vec<RidgeRow>,
pub n_points: usize,
pub vmin: f64,
pub vmax: f64,
pub layout: FigureLayout,
}
impl RidgeScene {
pub fn from_grid(processed: &Array2<f64>, bbox_ratio: f64, size_scale: f64) -> RidgeScene {
let (nrows, ncols) = processed.dim();
let mut rows = Vec::with_capacity(nrows);
let mut vmin = f64::INFINITY;
let mut vmax = f64::NEG_INFINITY;
for i in 0..nrows {
let baseline = -LINE_SPACING * i as f64;
let mut y = Vec::with_capacity(ncols);
for c in 0..ncols {
let v = processed[(i, c)];
if v.is_finite() {
vmin = vmin.min(v);
vmax = vmax.max(v);
}
y.push(v + baseline);
}
rows.push(RidgeRow { baseline, y });
}
if !vmin.is_finite() {
vmin = 0.0;
vmax = 1.0;
}
let mut xmin = usize::MAX;
let mut xmax_data = 0usize;
let mut ymin = f64::INFINITY;
let mut ymax_data = f64::NEG_INFINITY;
for row in &rows {
let mut has_data = false;
for (c, &y) in row.y.iter().enumerate() {
if y.is_finite() {
has_data = true;
if y > ymax_data {
ymax_data = y;
}
if c < xmin {
xmin = c;
}
if c > xmax_data {
xmax_data = c;
}
}
}
if has_data && row.baseline < ymin {
ymin = row.baseline;
}
}
let (xmin, xmax_data, ymin, ymax_data) = if xmin == usize::MAX {
(0, ncols - 1, -LINE_SPACING * (nrows - 1) as f64, vmax)
} else {
(xmin, xmax_data, ymin, ymax_data)
};
let dx = (xmax_data - xmin) as f64 * AXES_MARGIN;
let dy = (ymax_data - ymin) * AXES_MARGIN;
let width_px = size_scale * FIG_DPI;
let height_px = size_scale * bbox_ratio * FIG_DPI;
let layout = FigureLayout {
width_px,
height_px,
axes: [
SUBPLOT_LEFT * width_px,
(1.0 - SUBPLOT_TOP) * height_px,
SUBPLOT_RIGHT * width_px,
(1.0 - SUBPLOT_BOTTOM) * height_px,
],
xlim: [xmin as f64 - dx, xmax_data as f64 + dx],
ylim: [ymin - dy, ymax_data + dy],
};
RidgeScene {
rows,
n_points: ncols,
vmin,
vmax,
layout,
}
}
pub fn gradient_color(&self, line: &LineColor, idx: usize) -> crate::colormap::Rgb {
match line {
LineColor::Solid(rgb) => *rgb,
LineColor::Map(cm) => {
let denom = self.rows.len().saturating_sub(1).max(1) as f64;
cm.at(idx as f64 / denom)
}
}
}
pub fn elevation_color(&self, line: &LineColor, value: f64) -> crate::colormap::Rgb {
let LineColor::Map(cm) = line else {
return [0, 0, 0];
};
let t = if self.vmax > self.vmin {
(value - self.vmin) / (self.vmax - self.vmin)
} else {
0.0
};
cm.at(t)
}
pub fn label_color(&self, line: &LineColor) -> crate::colormap::Rgb {
match line {
LineColor::Solid(rgb) => *rgb,
LineColor::Map(cm) => cm.at(0.0),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Fit {
Reshape,
Plane,
}
#[allow(clippy::too_many_arguments)]
pub fn build_scene(
source: &dyn crate::srtm::TileSource,
bbox: &crate::Bbox,
num_lines: usize,
elevation_pts: usize,
viewpoint_angle: f64,
crop: bool,
interpolation: u32,
lock_resolution: bool,
water_ntile: f64,
lake_flatness: i32,
vertical_ratio: f64,
size_scale: f64,
) -> Result<RidgeScene, Error> {
build_scene_fit(
source,
bbox,
num_lines,
elevation_pts,
viewpoint_angle,
crop,
interpolation,
lock_resolution,
Fit::Reshape,
water_ntile,
lake_flatness,
vertical_ratio,
size_scale,
)
}
#[allow(clippy::too_many_arguments)]
pub fn build_scene_fit(
source: &dyn crate::srtm::TileSource,
bbox: &crate::Bbox,
num_lines: usize,
elevation_pts: usize,
viewpoint_angle: f64,
crop: bool,
interpolation: u32,
lock_resolution: bool,
fit: Fit,
water_ntile: f64,
lake_flatness: i32,
vertical_ratio: f64,
size_scale: f64,
) -> Result<RidgeScene, Error> {
if !bbox.is_valid() {
return Err(Error::InvalidBbox(*bbox));
}
let rotating = viewpoint_angle.rem_euclid(360.0) != 0.0;
let mut values = match fit {
Fit::Plane => {
let _ = lock_resolution;
crate::grid::sample(source, bbox, num_lines, elevation_pts)
}
Fit::Reshape => {
let (mut lines, mut pts) = (num_lines, elevation_pts);
if !lock_resolution && crate::grid::swap_for_angle(viewpoint_angle) {
std::mem::swap(&mut lines, &mut pts);
}
crate::grid::sample(source, bbox, lines, pts)
}
};
if rotating {
values = match fit {
Fit::Plane => {
crate::rotate::rotate_fixed_plane(&values, viewpoint_angle, interpolation)
}
Fit::Reshape => crate::rotate::rotate(&values, viewpoint_angle, !crop, interpolation),
};
}
let processed = crate::preprocess::preprocess(
&values,
water_ntile,
lake_flatness,
vertical_ratio,
1.0, None, )?;
Ok(RidgeScene::from_grid(&processed, bbox.ratio(), size_scale))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::srtm::SyntheticSource;
#[test]
fn runs_split_at_gaps() {
let row = RidgeRow {
baseline: -6.0,
y: vec![1.0, f64::NAN, f64::NAN, 2.0, 3.0, f64::NAN, 4.0],
};
assert_eq!(row.runs(), vec![(0, 1), (3, 5), (6, 7)]);
}
#[test]
fn scene_geometry() {
let src = SyntheticSource { side: 1201 };
let scene = build_scene(
&src,
&crate::DEFAULT_BBOX,
20,
30,
0.0,
false,
0,
false,
10.0,
3,
40.0,
DEFAULT_SIZE_SCALE,
)
.unwrap();
assert_eq!(scene.rows.len(), 20);
assert_eq!(scene.n_points, 30);
assert_eq!(scene.rows[0].baseline, 0.0);
assert_eq!(scene.rows[7].baseline, -42.0);
assert_eq!(scene.layout.width_px, 2000.0);
assert_eq!(scene.layout.height_px, 2000.0 * crate::DEFAULT_BBOX.ratio());
}
}