rustial-engine 0.0.1

Framework-agnostic 2.5D map engine for rustial
Documentation
//! Elevation source trait and built-in implementations.

use crate::terrain::error::TerrainError;
use rustial_math::{ElevationGrid, TileId};

/// Failure counters for terrain elevation sources.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ElevationSourceFailureDiagnostics {
    /// Number of network/transport failures.
    pub network_failures: usize,
    /// Number of decode failures.
    pub decode_failures: usize,
    /// Number of unsupported-format failures.
    pub unsupported_format_failures: usize,
    /// Number of uncategorized failures.
    pub other_failures: usize,
    /// Number of completed responses ignored because no pending tile matched.
    pub ignored_completed_responses: usize,
}

/// Diagnostics snapshot for an elevation source.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ElevationSourceDiagnostics {
    /// Number of requests waiting in a queue before transport dispatch.
    pub queued_requests: usize,
    /// Number of requests currently in-flight.
    pub in_flight_requests: usize,
    /// Configured maximum concurrent requests, or 0 if not applicable.
    pub max_concurrent_requests: usize,
    /// Number of known requests tracked by the source.
    pub known_requests: usize,
    /// Number of requests cancelled while in-flight.
    pub cancelled_in_flight_requests: usize,
    /// Categorized failure counters.
    pub failure_diagnostics: ElevationSourceFailureDiagnostics,
}

/// A source of elevation data for terrain tiles.
///
/// Same polling pattern as `TileSource`: the engine does not own an
/// async runtime.
pub trait ElevationSource: Send + Sync {
    /// Start fetching elevation data for a tile. Returns immediately.
    fn request(&self, id: TileId);

    /// Poll for completed elevation fetches.
    fn poll(&self) -> Vec<(TileId, Result<ElevationGrid, TerrainError>)>;

    /// Cancel a queued request for a terrain tile if it has not yet been sent.
    ///
    /// Returns `true` if the source found and removed a queued request.
    /// In-flight requests are unaffected and return `false`.
    fn cancel(&self, _id: TileId) -> bool {
        false
    }

    /// Optional source diagnostics for debugging terrain fetch/decode behavior.
    fn diagnostics(&self) -> Option<ElevationSourceDiagnostics> {
        None
    }
}

/// Encoding scheme for Terrain-RGB PNG tiles.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerrainRgbEncoding {
    /// AWS Terrain Tiles / Terrarium.
    /// `elevation = (R * 256 + G + B / 256) - 32768`
    Terrarium,
    /// Mapbox Terrain-RGB / MapTiler.
    /// `elevation = -10000 + (R * 65536 + G * 256 + B) * 0.1`
    Mapbox,
}

impl TerrainRgbEncoding {
    /// Decode a single pixel's RGB values to elevation in meters.
    ///
    /// Values are clamped to \[-500, +9\_100\] meters to filter out
    /// "no data" sentinels (Terrarium encodes `(0,0,0)` as −32 768 m)
    /// and corrupt pixels.  −500 m is well below any real land surface
    /// (Dead Sea ≈ −430 m) and prevents extreme depressions that create
    /// visible gaps between terrain tiles.
    #[inline]
    pub fn decode(&self, r: u8, g: u8, b: u8) -> f32 {
        let raw = match self {
            TerrainRgbEncoding::Terrarium => {
                (r as f32 * 256.0 + g as f32 + b as f32 / 256.0) - 32768.0
            }
            TerrainRgbEncoding::Mapbox => {
                -10000.0 + (r as f32 * 65536.0 + g as f32 * 256.0 + b as f32) * 0.1
            }
        };
        raw.clamp(-500.0, 9_100.0)
    }
}

/// A flat elevation source that always returns zero elevation.
pub struct FlatElevationSource {
    width: u32,
    height: u32,
    pending: std::sync::Mutex<Vec<TileId>>,
}

impl FlatElevationSource {
    /// Create a flat elevation source with the given grid resolution.
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            width,
            height,
            pending: std::sync::Mutex::new(Vec::new()),
        }
    }
}

impl ElevationSource for FlatElevationSource {
    fn request(&self, id: TileId) {
        if let Ok(mut pending) = self.pending.lock() {
            pending.push(id);
        }
    }

    fn poll(&self) -> Vec<(TileId, Result<ElevationGrid, TerrainError>)> {
        let tiles = if let Ok(mut pending) = self.pending.lock() {
            std::mem::take(&mut *pending)
        } else {
            return Vec::new();
        };

        tiles
            .into_iter()
            .map(|id| {
                let grid = ElevationGrid::flat(id, self.width, self.height);
                (id, Ok(grid))
            })
            .collect()
    }

    fn diagnostics(&self) -> Option<ElevationSourceDiagnostics> {
        let pending = self.pending.lock().map(|p| p.len()).unwrap_or(0);
        Some(ElevationSourceDiagnostics {
            queued_requests: 0,
            in_flight_requests: pending,
            max_concurrent_requests: 0,
            known_requests: pending,
            cancelled_in_flight_requests: 0,
            failure_diagnostics: ElevationSourceFailureDiagnostics::default(),
        })
    }

    fn cancel(&self, id: TileId) -> bool {
        if let Ok(mut pending) = self.pending.lock() {
            let before = pending.len();
            pending.retain(|queued| *queued != id);
            return pending.len() != before;
        }
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn terrarium_decode_sea_level() {
        // Sea level in Terrarium: R=128, G=0, B=0
        // elev = (128 * 256 + 0 + 0/256) - 32768 = 32768 - 32768 = 0
        let elev = TerrainRgbEncoding::Terrarium.decode(128, 0, 0);
        assert!((elev - 0.0).abs() < 0.01);
    }

    #[test]
    fn terrarium_decode_positive() {
        // 1000m in Terrarium: (R*256 + G + B/256) = 33768
        // R = 131, G = 232, B=0: 131*256 + 232 + 0 = 33768
        let elev = TerrainRgbEncoding::Terrarium.decode(131, 232, 0);
        assert!((elev - 1000.0).abs() < 1.0);
    }

    #[test]
    fn mapbox_decode_sea_level() {
        // Sea level in Mapbox: elev = -10000 + (R*65536 + G*256 + B) * 0.1 = 0
        // => R*65536 + G*256 + B = 100000
        // R = 1, G = 134, B = 160 => 65536 + 34304 + 160 = 100000
        let elev = TerrainRgbEncoding::Mapbox.decode(1, 134, 160);
        assert!((elev - 0.0).abs() < 0.1);
    }

    #[test]
    fn mapbox_decode_positive() {
        // 1000m: -10000 + x * 0.1 = 1000 => x = 110000
        // R = 1, G = 173, B = 208 => 65536 + 44288 + 208 = 110032
        // So elev ? 1003.2
        let elev = TerrainRgbEncoding::Mapbox.decode(1, 173, 208);
        assert!((elev - 1003.2).abs() < 1.0);
    }

    #[test]
    fn flat_source_returns_zero() {
        let source = FlatElevationSource::new(3, 3);
        source.request(TileId::new(5, 10, 10));
        let results = source.poll();
        assert_eq!(results.len(), 1);
        let (id, grid) = &results[0];
        assert_eq!(*id, TileId::new(5, 10, 10));
        let grid = grid.as_ref().unwrap();
        assert_eq!(grid.sample(0.5, 0.5), Some(0.0));
    }
}