rustial 0.0.1

A geospatial map library for Rust
//! Fluent builder for constructing a [`MapState`].

use rustial_engine::{
    Camera, CameraConstraints, CameraMode, CameraProjection, GeoCoord, LayerStack, MapState,
    TerrainConfig,
};

/// A fluent builder for configuring and constructing a [`MapState`].
///
/// # Example
///
/// ```rust
/// use rustial::MapBuilder;
///
/// let state = MapBuilder::new()
///     .center(51.1, 17.0)
///     .zoom(10)
///     .build();
/// ```
pub struct MapBuilder {
    camera: Camera,
    constraints: CameraConstraints,
    layers: LayerStack,
    terrain_config: Option<TerrainConfig>,
    terrain_cache_size: usize,
    /// Explicit zoom level requested by the user (deferred until build).
    requested_zoom: Option<u8>,
}

impl MapBuilder {
    /// Create a new builder with default settings.
    pub fn new() -> Self {
        Self {
            camera: Camera::default(),
            constraints: CameraConstraints::default(),
            layers: LayerStack::new(),
            terrain_config: None,
            terrain_cache_size: 256,
            requested_zoom: None,
        }
    }

    /// Set the initial map center (latitude, longitude in degrees).
    pub fn center(mut self, lat: f64, lon: f64) -> Self {
        self.camera.set_target(GeoCoord::from_lat_lon(lat, lon));
        self
    }

    /// Set the initial zoom level (0-22).
    ///
    /// The zoom is stored and resolved at [`build`](Self::build) time so that
    /// FOV, viewport size, and projection mode are all taken into account.
    pub fn zoom(mut self, zoom: u8) -> Self {
        self.requested_zoom = Some(zoom.min(22));
        self
    }

    /// Set the camera pitch in radians.
    pub fn pitch(mut self, pitch: f64) -> Self {
        self.camera.set_pitch(pitch);
        self
    }

    /// Set the camera yaw / bearing in radians.
    pub fn yaw(mut self, yaw: f64) -> Self {
        self.camera.set_yaw(yaw);
        self
    }

    /// Set the vertical field-of-view in radians (perspective mode only).
    pub fn fov_y(mut self, radians: f64) -> Self {
        self.camera.set_fov_y(radians);
        self
    }

    /// Set the camera geographic projection.
    pub fn projection(mut self, projection: CameraProjection) -> Self {
        self.camera.set_projection(projection);
        self
    }

    /// Set the camera projection mode.
    pub fn mode(mut self, mode: CameraMode) -> Self {
        self.camera.set_mode(mode);
        self
    }

    /// Set the viewport size.
    pub fn viewport(mut self, width: u32, height: u32) -> Self {
        self.camera.set_viewport(width, height);
        self
    }

    /// Set camera constraints.
    pub fn constraints(mut self, constraints: CameraConstraints) -> Self {
        self.constraints = constraints;
        self
    }

    /// Set the layer stack.
    pub fn layers(mut self, layers: LayerStack) -> Self {
        self.layers = layers;
        self
    }

    /// Enable terrain with the given configuration.
    pub fn terrain(mut self, config: TerrainConfig) -> Self {
        self.terrain_config = Some(config);
        self
    }

    /// Set the terrain cache size.
    pub fn terrain_cache_size(mut self, size: usize) -> Self {
        self.terrain_cache_size = size;
        self
    }

    /// Build the [`MapState`].
    pub fn build(self) -> MapState {
        let mut layers = self.layers;
        let terrain_config = self.terrain_config.unwrap_or_default();

        let mut state = MapState::with_terrain(terrain_config, self.terrain_cache_size);
        // Apply camera settings via setters.
        state.set_camera_target(*self.camera.target());
        state.set_camera_projection(self.camera.projection());
        state.set_camera_distance(self.camera.distance());
        state.set_camera_pitch(self.camera.pitch());
        state.set_camera_yaw(self.camera.yaw());
        state.set_camera_fov_y(self.camera.fov_y());
        state.set_camera_mode(self.camera.mode());
        state.set_viewport(self.camera.viewport_width(), self.camera.viewport_height());
        state.set_max_pitch(self.constraints.max_pitch);
        state.set_min_distance(self.constraints.min_distance);
        state.set_max_distance(self.constraints.max_distance);

        // Resolve deferred zoom: compute the camera distance whose
        // meters-per-pixel matches the tile resolution at the requested
        // zoom level.
        if let Some(z) = self.requested_zoom {
            const C: f64 = 2.0 * std::f64::consts::PI * 6_378_137.0;
            const TILE_PX: f64 = 256.0;
            let target_mpp = C / (TILE_PX * (1u64 << z) as f64);
            let vh = state.camera().viewport_height().max(1) as f64;
            let distance = match state.camera().mode() {
                CameraMode::Perspective => {
                    target_mpp * vh / (2.0 * (state.camera().fov_y() / 2.0).tan())
                }
                CameraMode::Orthographic => target_mpp * vh / 2.0,
            };
            state.set_camera_distance(distance.clamp(
                state.constraints().min_distance,
                state.constraints().max_distance,
            ));
        }

        while let Some(layer) = layers.remove(0) {
            state.push_layer(layer);
        }

        state
    }
}

impl Default for MapBuilder {
    fn default() -> Self {
        Self::new()
    }
}

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

    #[test]
    fn builder_default() {
        let state = MapBuilder::new().build();
        assert_eq!(state.zoom_level(), 0);
    }

    #[test]
    fn builder_with_center_and_zoom() {
        let mut state = MapBuilder::new()
            .center(51.1, 17.0)
            .zoom(10)
            .viewport(1920, 1080)
            .build();
        state.update();
        assert!((state.camera().target().lat - 51.1).abs() < 1e-6);
        assert!((state.camera().target().lon - 17.0).abs() < 1e-6);
        assert!(state.zoom_level() >= 9 && state.zoom_level() <= 11);
    }

    #[test]
    fn builder_transfers_layers() {
        use rustial_engine::BackgroundLayer;

        let mut layers = LayerStack::new();
        layers.push(Box::new(BackgroundLayer::new("bg", [0.2, 0.3, 0.4, 1.0])));

        let state = MapBuilder::new().layers(layers).build();
        assert_eq!(state.layers().len(), 1);
        assert_eq!(state.background_color(), Some([0.2, 0.3, 0.4, 1.0]));
    }

    #[test]
    fn builder_transfers_camera_projection() {
        let state = MapBuilder::new()
            .projection(CameraProjection::Equirectangular)
            .build();

        assert_eq!(
            state.camera().projection(),
            CameraProjection::Equirectangular
        );
    }
}