Skip to main content

rustial_engine/terrain/
elevation_source.rs

1//! Elevation source trait and built-in implementations.
2
3use crate::terrain::error::TerrainError;
4use rustial_math::{ElevationGrid, TileId};
5
6/// Failure counters for terrain elevation sources.
7#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct ElevationSourceFailureDiagnostics {
9    /// Number of network/transport failures.
10    pub network_failures: usize,
11    /// Number of decode failures.
12    pub decode_failures: usize,
13    /// Number of unsupported-format failures.
14    pub unsupported_format_failures: usize,
15    /// Number of uncategorized failures.
16    pub other_failures: usize,
17    /// Number of completed responses ignored because no pending tile matched.
18    pub ignored_completed_responses: usize,
19}
20
21/// Diagnostics snapshot for an elevation source.
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct ElevationSourceDiagnostics {
24    /// Number of requests waiting in a queue before transport dispatch.
25    pub queued_requests: usize,
26    /// Number of requests currently in-flight.
27    pub in_flight_requests: usize,
28    /// Configured maximum concurrent requests, or 0 if not applicable.
29    pub max_concurrent_requests: usize,
30    /// Number of known requests tracked by the source.
31    pub known_requests: usize,
32    /// Number of requests cancelled while in-flight.
33    pub cancelled_in_flight_requests: usize,
34    /// Categorized failure counters.
35    pub failure_diagnostics: ElevationSourceFailureDiagnostics,
36}
37
38/// A source of elevation data for terrain tiles.
39///
40/// Same polling pattern as `TileSource`: the engine does not own an
41/// async runtime.
42pub trait ElevationSource: Send + Sync {
43    /// Start fetching elevation data for a tile. Returns immediately.
44    fn request(&self, id: TileId);
45
46    /// Poll for completed elevation fetches.
47    fn poll(&self) -> Vec<(TileId, Result<ElevationGrid, TerrainError>)>;
48
49    /// Cancel a queued request for a terrain tile if it has not yet been sent.
50    ///
51    /// Returns `true` if the source found and removed a queued request.
52    /// In-flight requests are unaffected and return `false`.
53    fn cancel(&self, _id: TileId) -> bool {
54        false
55    }
56
57    /// Optional source diagnostics for debugging terrain fetch/decode behavior.
58    fn diagnostics(&self) -> Option<ElevationSourceDiagnostics> {
59        None
60    }
61}
62
63/// Encoding scheme for Terrain-RGB PNG tiles.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum TerrainRgbEncoding {
66    /// AWS Terrain Tiles / Terrarium.
67    /// `elevation = (R * 256 + G + B / 256) - 32768`
68    Terrarium,
69    /// Mapbox Terrain-RGB / MapTiler.
70    /// `elevation = -10000 + (R * 65536 + G * 256 + B) * 0.1`
71    Mapbox,
72}
73
74impl TerrainRgbEncoding {
75    /// Decode a single pixel's RGB values to elevation in meters.
76    ///
77    /// Values are clamped to \[-500, +9\_100\] meters to filter out
78    /// "no data" sentinels (Terrarium encodes `(0,0,0)` as −32 768 m)
79    /// and corrupt pixels.  −500 m is well below any real land surface
80    /// (Dead Sea ≈ −430 m) and prevents extreme depressions that create
81    /// visible gaps between terrain tiles.
82    #[inline]
83    pub fn decode(&self, r: u8, g: u8, b: u8) -> f32 {
84        let raw = match self {
85            TerrainRgbEncoding::Terrarium => {
86                (r as f32 * 256.0 + g as f32 + b as f32 / 256.0) - 32768.0
87            }
88            TerrainRgbEncoding::Mapbox => {
89                -10000.0 + (r as f32 * 65536.0 + g as f32 * 256.0 + b as f32) * 0.1
90            }
91        };
92        raw.clamp(-500.0, 9_100.0)
93    }
94}
95
96/// A flat elevation source that always returns zero elevation.
97pub struct FlatElevationSource {
98    width: u32,
99    height: u32,
100    pending: std::sync::Mutex<Vec<TileId>>,
101}
102
103impl FlatElevationSource {
104    /// Create a flat elevation source with the given grid resolution.
105    pub fn new(width: u32, height: u32) -> Self {
106        Self {
107            width,
108            height,
109            pending: std::sync::Mutex::new(Vec::new()),
110        }
111    }
112}
113
114impl ElevationSource for FlatElevationSource {
115    fn request(&self, id: TileId) {
116        if let Ok(mut pending) = self.pending.lock() {
117            pending.push(id);
118        }
119    }
120
121    fn poll(&self) -> Vec<(TileId, Result<ElevationGrid, TerrainError>)> {
122        let tiles = if let Ok(mut pending) = self.pending.lock() {
123            std::mem::take(&mut *pending)
124        } else {
125            return Vec::new();
126        };
127
128        tiles
129            .into_iter()
130            .map(|id| {
131                let grid = ElevationGrid::flat(id, self.width, self.height);
132                (id, Ok(grid))
133            })
134            .collect()
135    }
136
137    fn diagnostics(&self) -> Option<ElevationSourceDiagnostics> {
138        let pending = self.pending.lock().map(|p| p.len()).unwrap_or(0);
139        Some(ElevationSourceDiagnostics {
140            queued_requests: 0,
141            in_flight_requests: pending,
142            max_concurrent_requests: 0,
143            known_requests: pending,
144            cancelled_in_flight_requests: 0,
145            failure_diagnostics: ElevationSourceFailureDiagnostics::default(),
146        })
147    }
148
149    fn cancel(&self, id: TileId) -> bool {
150        if let Ok(mut pending) = self.pending.lock() {
151            let before = pending.len();
152            pending.retain(|queued| *queued != id);
153            return pending.len() != before;
154        }
155        false
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn terrarium_decode_sea_level() {
165        // Sea level in Terrarium: R=128, G=0, B=0
166        // elev = (128 * 256 + 0 + 0/256) - 32768 = 32768 - 32768 = 0
167        let elev = TerrainRgbEncoding::Terrarium.decode(128, 0, 0);
168        assert!((elev - 0.0).abs() < 0.01);
169    }
170
171    #[test]
172    fn terrarium_decode_positive() {
173        // 1000m in Terrarium: (R*256 + G + B/256) = 33768
174        // R = 131, G = 232, B=0: 131*256 + 232 + 0 = 33768
175        let elev = TerrainRgbEncoding::Terrarium.decode(131, 232, 0);
176        assert!((elev - 1000.0).abs() < 1.0);
177    }
178
179    #[test]
180    fn mapbox_decode_sea_level() {
181        // Sea level in Mapbox: elev = -10000 + (R*65536 + G*256 + B) * 0.1 = 0
182        // => R*65536 + G*256 + B = 100000
183        // R = 1, G = 134, B = 160 => 65536 + 34304 + 160 = 100000
184        let elev = TerrainRgbEncoding::Mapbox.decode(1, 134, 160);
185        assert!((elev - 0.0).abs() < 0.1);
186    }
187
188    #[test]
189    fn mapbox_decode_positive() {
190        // 1000m: -10000 + x * 0.1 = 1000 => x = 110000
191        // R = 1, G = 173, B = 208 => 65536 + 44288 + 208 = 110032
192        // So elev ? 1003.2
193        let elev = TerrainRgbEncoding::Mapbox.decode(1, 173, 208);
194        assert!((elev - 1003.2).abs() < 1.0);
195    }
196
197    #[test]
198    fn flat_source_returns_zero() {
199        let source = FlatElevationSource::new(3, 3);
200        source.request(TileId::new(5, 10, 10));
201        let results = source.poll();
202        assert_eq!(results.len(), 1);
203        let (id, grid) = &results[0];
204        assert_eq!(*id, TileId::new(5, 10, 10));
205        let grid = grid.as_ref().unwrap();
206        assert_eq!(grid.sample(0.5, 0.5), Some(0.0));
207    }
208}