Skip to main content

geonative_processing/raster/
pyramid.rs

1//! Pyramid (overview) generation.
2//!
3//! Takes a full-resolution `RasterTile` and produces a chain of
4//! half-resolution tiles — one per pyramid level — using a resampling
5//! kernel.
6//!
7//! ## Convention
8//!
9//! - **Level 0** is the input (full resolution).
10//! - **Level N** has dimensions `width >> N` × `height >> N` (rounded
11//!   down, minimum 1).
12//! - We stop at the level where both dimensions are ≤ `min_dimension`
13//!   (default 256) — no point in serving a 64×64 thumbnail at zoom 0 of
14//!   a global mosaic.
15
16use geonative_core::raster::{RasterTile, ResamplingMethod};
17
18use crate::raster::resample;
19
20#[derive(Debug, Clone)]
21pub struct PyramidOptions {
22    /// Resampling kernel to use for each downsample step. Default
23    /// `Average` (good for continuous imagery); use `Mode` for
24    /// categorical / classification rasters.
25    pub method: ResamplingMethod,
26    /// Stop adding levels once both dimensions are ≤ this. Default 256.
27    pub min_dimension: u32,
28    /// Hard cap on the number of overview levels (excludes level 0).
29    /// Default 16 (covers global → 256×256 thumbnail).
30    pub max_levels: u8,
31}
32
33impl Default for PyramidOptions {
34    fn default() -> Self {
35        Self {
36            method: ResamplingMethod::Average,
37            min_dimension: 256,
38            max_levels: 16,
39        }
40    }
41}
42
43/// Produce a chain of overview tiles from `base` (treated as level 0).
44/// The returned `Vec` does NOT include the base — index 0 of the result is
45/// the level-1 overview (half resolution).
46pub fn build_overviews(base: &RasterTile, opts: PyramidOptions) -> Vec<RasterTile> {
47    let mut out = Vec::new();
48    let mut current = base.clone();
49    for _ in 0..opts.max_levels {
50        let next_w = (current.width / 2).max(1);
51        let next_h = (current.height / 2).max(1);
52        if next_w <= opts.min_dimension && next_h <= opts.min_dimension {
53            // Add one final overview at this scale, then stop.
54            let overview = resample::resample_tile(&current, next_w, next_h, opts.method);
55            out.push(overview);
56            break;
57        }
58        if next_w == current.width && next_h == current.height {
59            // No progress — input is already at minimum size
60            break;
61        }
62        let overview = resample::resample_tile(&current, next_w, next_h, opts.method);
63        out.push(overview.clone());
64        current = overview;
65    }
66    out
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72    use geonative_core::raster::{Band, BandDescriptor, GeoTransform, PixelType};
73    use geonative_core::Crs;
74
75    fn make_tile(width: u32, height: u32) -> RasterTile {
76        RasterTile {
77            width,
78            height,
79            bands: vec![Band::new(
80                BandDescriptor::new(Some("v".into()), PixelType::U8),
81                vec![100u8; (width * height) as usize],
82            )],
83            geo_transform: GeoTransform::north_up(0.0, 0.0, 1.0, 1.0),
84            crs: Crs::Epsg(3857),
85        }
86    }
87
88    #[test]
89    fn builds_overviews_until_min_dimension() {
90        let base = make_tile(1024, 1024);
91        let overviews = build_overviews(
92            &base,
93            PyramidOptions {
94                min_dimension: 256,
95                ..PyramidOptions::default()
96            },
97        );
98        // 1024 → 512 → 256 → 128 ... we stop at the first ≤256.
99        // Sequence: 512, 256 (final, since both ≤ 256). So 2 levels.
100        assert_eq!(overviews.len(), 2);
101        assert_eq!(overviews[0].width, 512);
102        assert_eq!(overviews[1].width, 256);
103    }
104
105    #[test]
106    fn respects_max_levels_cap() {
107        let base = make_tile(8192, 8192);
108        let overviews = build_overviews(
109            &base,
110            PyramidOptions {
111                min_dimension: 64,
112                max_levels: 3,
113                ..PyramidOptions::default()
114            },
115        );
116        assert_eq!(overviews.len(), 3);
117        // 8192 → 4096 → 2048 → 1024
118        assert_eq!(overviews[2].width, 1024);
119    }
120
121    #[test]
122    fn stops_at_min_dimension_input() {
123        let base = make_tile(128, 128);
124        let overviews = build_overviews(
125            &base,
126            PyramidOptions {
127                min_dimension: 256,
128                ..PyramidOptions::default()
129            },
130        );
131        // Input is already ≤ min_dimension → one final downsample then stop
132        assert_eq!(overviews.len(), 1);
133    }
134
135    #[test]
136    fn pixel_size_scales_correctly_through_pyramid() {
137        let base = make_tile(512, 512);
138        let overviews = build_overviews(&base, PyramidOptions::default());
139        // Original pixel_size [1, -1]; each level halves resolution → pixel size doubles
140        // 512 → 256 (1 level), pixel size [2, -2]
141        assert_eq!(overviews[0].geo_transform.pixel_size, [2.0, -2.0]);
142    }
143}