rustial 0.0.1

A geospatial map library for Rust
//! Thread-safe handle wrapping [`MapState`].

use rustial_engine::interaction::{InteractionEvent, InteractionEventKind};
use rustial_engine::{
    EaseToOptions, FitBoundsOptions, FlyToOptions, GeoBounds, GeoCoord, ListenerId, MapState,
};
use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};

/// Error returned when the internal `RwLock` is poisoned.
///
/// A poisoned lock means another thread panicked while holding the lock.
/// The inner guard is still accessible via [`into_inner`](PoisonError::into_inner)
/// if you want to recover.
pub type LockPoisonedError<T> = PoisonError<T>;

/// Thread-safe handle to the map state.
///
/// Provides shared read access for renderers and exclusive write
/// access for input processing, using a `RwLock` internally.
///
/// # Lock poisoning
///
/// If a thread panics while holding a read or write guard, the lock
/// becomes "poisoned".  [`read`](Self::read) and [`write`](Self::write)
/// return `Err(PoisonError)` in this case so the caller can detect
/// the situation rather than silently receiving stale data.
pub struct MapHandle {
    inner: RwLock<MapState>,
}

impl MapHandle {
    /// Create a new handle wrapping the given state.
    pub fn new(state: MapState) -> Self {
        Self {
            inner: RwLock::new(state),
        }
    }

    /// Acquire a shared read lock on the state.
    ///
    /// Returns `Err` if the lock is poisoned (a thread panicked while
    /// holding it).
    pub fn read(
        &self,
    ) -> Result<RwLockReadGuard<'_, MapState>, LockPoisonedError<RwLockReadGuard<'_, MapState>>>
    {
        self.inner.read()
    }

    /// Acquire an exclusive write lock on the state.
    ///
    /// Returns `Err` if the lock is poisoned (a thread panicked while
    /// holding it).
    pub fn write(
        &self,
    ) -> Result<RwLockWriteGuard<'_, MapState>, LockPoisonedError<RwLockWriteGuard<'_, MapState>>>
    {
        self.inner.write()
    }

    // -- Camera convenience methods ---------------------------------------

    /// Begin a fly-to animation.
    ///
    /// Convenience wrapper that acquires a write lock internally.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn fly_to(&self, options: FlyToOptions) {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .fly_to(options);
    }

    /// Begin an ease-to animation.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn ease_to(&self, options: EaseToOptions) {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .ease_to(options);
    }

    /// Immediately jump the camera to the given state.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn jump_to(&self, target: GeoCoord, distance: f64, pitch: Option<f64>, yaw: Option<f64>) {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .jump_to(target, distance, pitch, yaw);
    }

    /// Fit the camera to a geographic bounding box.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn fit_bounds(&self, bounds: &GeoBounds, options: &FitBoundsOptions) {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .fit_bounds(bounds, options);
    }

    // -- Query convenience methods ----------------------------------------

    /// Project a geographic coordinate to screen pixels.
    ///
    /// Returns `None` if the point is behind the camera.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn geo_to_screen(&self, geo: &GeoCoord) -> Option<(f64, f64)> {
        self.inner
            .read()
            .expect("MapHandle: lock poisoned")
            .geo_to_screen(geo)
    }

    /// Unproject a screen pixel to a geographic coordinate (flat ground).
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn screen_to_geo(&self, px: f64, py: f64) -> Option<GeoCoord> {
        self.inner
            .read()
            .expect("MapHandle: lock poisoned")
            .screen_to_geo(px, py)
    }

    /// Current integer zoom level (0–22).
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn zoom_level(&self) -> u8 {
        self.inner
            .read()
            .expect("MapHandle: lock poisoned")
            .zoom_level()
    }

    // -- Event subscription convenience -----------------------------------

    /// Subscribe to interaction events of a given kind.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn on<F>(&self, kind: InteractionEventKind, callback: F) -> ListenerId
    where
        F: Fn(&InteractionEvent) + Send + Sync + 'static,
    {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .on(kind, callback)
    }

    /// Subscribe to a single occurrence of an interaction event kind.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn once<F>(&self, kind: InteractionEventKind, callback: F) -> ListenerId
    where
        F: Fn(&InteractionEvent) + Send + Sync + 'static,
    {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .once(kind, callback)
    }

    /// Unsubscribe a previously registered event listener.
    ///
    /// # Panics
    ///
    /// Panics if the lock is poisoned.
    pub fn off(&self, id: ListenerId) -> bool {
        self.inner
            .write()
            .expect("MapHandle: lock poisoned")
            .off(id)
    }
}

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

    #[test]
    fn read_write() {
        let handle = MapHandle::new(MapState::new());
        {
            let state = handle.read().expect("read");
            assert_eq!(state.zoom_level(), 0);
        }
        {
            let mut state = handle.write().expect("write");
            state.update();
        }
    }

    #[test]
    fn multiple_readers() {
        let handle = MapHandle::new(MapState::new());
        let r1 = handle.read().expect("read 1");
        let r2 = handle.read().expect("read 2");
        assert_eq!(r1.zoom_level(), r2.zoom_level());
    }
}