cartography 0.11.1

Cartography is a map rendering library for Geographic features expressed using [georust](https://georust.org/) libraries.
Documentation
//! Zoom-level-aware layer that delegates to different sub-layers per zoom band.

use crate::{BoxedLayer, Feature, Layer, Projection};
use ccutils::containers::RefOrValue;

struct ZoomBand<TFeature: Feature>
{
  /// This band is selected when `zoom_level < max_zoom`.
  max_zoom: f64,
  layer: BoxedLayer<TFeature>,
}

/// A [`Layer`] that selects a sub-layer based on the current zoom level.
///
/// Bands must be provided in ascending `max_zoom` order (the builder sorts
/// them automatically). The first band whose `max_zoom` exceeds the requested
/// zoom level is used. A final band with `max_zoom = f64::INFINITY` acts as
/// the full-resolution fallback.
///
/// # Example
///
/// ```rust,ignore
/// let lod = ZoomSplitLayerBuilder::new()
///     .add_band(8.0,  coarse_layer)
///     .add_band(13.0, medium_layer)
///     .add_band(f64::INFINITY, full_layer)
///     .build();
/// ```
pub struct ZoomSplitLayer<TFeature: Feature>
{
  bands: Vec<ZoomBand<TFeature>>,
  projection: Projection,
}

impl<TFeature: Feature> ZoomSplitLayer<TFeature>
{
  fn new(bands: Vec<ZoomBand<TFeature>>) -> Self
  {
    let projection = bands
      .first()
      .map(|b| b.layer.projection().clone())
      .unwrap_or_else(Projection::wgs84);
    Self { bands, projection }
  }
}

impl<TFeature: Feature> Layer<TFeature> for ZoomSplitLayer<TFeature>
{
  fn projection(&self) -> &Projection
  {
    &self.projection
  }

  fn features<'a>(
    &'a self,
    rect: geo::Rect,
    zoom_level: f64,
  ) -> Box<dyn Iterator<Item = RefOrValue<'a, TFeature>> + 'a>
  {
    match self.bands.iter().find(|b| zoom_level < b.max_zoom)
    {
      Some(band) => band.layer.features(rect, zoom_level),
      None => Box::new(std::iter::empty()),
    }
  }
}

/// Builder for [`ZoomSplitLayer`].
pub struct ZoomSplitLayerBuilder<TFeature: Feature>
{
  bands: Vec<ZoomBand<TFeature>>,
}

impl<TFeature: Feature> Default for ZoomSplitLayerBuilder<TFeature>
{
  fn default() -> Self
  {
    Self::new()
  }
}

impl<TFeature: Feature> ZoomSplitLayerBuilder<TFeature>
{
  /// Create an empty builder.
  pub fn new() -> Self
  {
    Self { bands: Vec::new() }
  }

  /// Add a zoom band. This band will be active when `zoom_level < max_zoom`.
  /// Use `f64::INFINITY` for the catch-all full-resolution band.
  pub fn add_band(mut self, max_zoom: f64, layer: impl Layer<TFeature> + 'static) -> Self
  {
    self.bands.push(ZoomBand {
      max_zoom,
      layer: Box::new(layer),
    });
    self
  }

  /// Sorts bands by `max_zoom` and returns the finished layer.
  pub fn build(mut self) -> ZoomSplitLayer<TFeature>
  {
    self
      .bands
      .sort_by(|a, b| a.max_zoom.partial_cmp(&b.max_zoom).unwrap());
    ZoomSplitLayer::new(self.bands)
  }
}

#[cfg(test)]
mod tests
{
  use super::*;
  use crate::{FeaturesVecLayer, GeometryRef};
  use std::sync::{Arc, Mutex};

  struct Pt(geo::Geometry);

  impl GeometryRef for Pt
  {
    fn geometry_ref(&self) -> &geo::Geometry
    {
      &self.0
    }
  }

  fn pt(x: f64, y: f64) -> Pt
  {
    Pt(geo::Geometry::Point(geo::Point::new(x, y)))
  }

  /// A layer that counts how often `features()` was called and at what zoom.
  struct SpyLayer
  {
    inner: FeaturesVecLayer<Pt>,
    calls: Arc<Mutex<Vec<f64>>>,
  }

  impl SpyLayer
  {
    fn new(features: Vec<Pt>) -> (Self, Arc<Mutex<Vec<f64>>>)
    {
      let calls = Arc::new(Mutex::new(Vec::new()));
      (
        Self {
          inner: features.into(),
          calls: calls.clone(),
        },
        calls,
      )
    }
  }

  impl Layer<Pt> for SpyLayer
  {
    fn projection(&self) -> &Projection
    {
      self.inner.projection()
    }

    fn features<'a>(
      &'a self,
      rect: geo::Rect,
      zoom_level: f64,
    ) -> Box<dyn Iterator<Item = RefOrValue<'a, Pt>> + 'a>
    {
      self.calls.lock().unwrap().push(zoom_level);
      self.inner.features(rect, zoom_level)
    }
  }

  fn world_rect() -> geo::Rect
  {
    geo::Rect::new(
      geo::coord! { x: -180.0, y: -90.0 },
      geo::coord! { x: 180.0, y: 90.0 },
    )
  }

  #[test]
  fn selects_coarse_band_at_low_zoom()
  {
    let (coarse, coarse_calls) = SpyLayer::new(vec![pt(0.0, 0.0)]);
    let (fine, _fine_calls) = SpyLayer::new(vec![]);

    let lod = ZoomSplitLayerBuilder::new()
      .add_band(8.0, coarse)
      .add_band(f64::INFINITY, fine)
      .build();

    let _: Vec<_> = lod.features(world_rect(), 5.0).collect();

    assert_eq!(*coarse_calls.lock().unwrap(), vec![5.0]);
  }

  #[test]
  fn selects_fine_band_at_high_zoom()
  {
    let (coarse, coarse_calls) = SpyLayer::new(vec![]);
    let (fine, fine_calls) = SpyLayer::new(vec![pt(1.0, 1.0)]);

    let lod = ZoomSplitLayerBuilder::new()
      .add_band(8.0, coarse)
      .add_band(f64::INFINITY, fine)
      .build();

    let _: Vec<_> = lod.features(world_rect(), 10.0).collect();

    assert!(coarse_calls.lock().unwrap().is_empty());
    assert_eq!(*fine_calls.lock().unwrap(), vec![10.0]);
  }

  #[test]
  fn boundary_zoom_picks_upper_band()
  {
    let (coarse, coarse_calls) = SpyLayer::new(vec![]);
    let (fine, fine_calls) = SpyLayer::new(vec![]);

    let lod = ZoomSplitLayerBuilder::new()
      .add_band(8.0, coarse)
      .add_band(f64::INFINITY, fine)
      .build();

    let _: Vec<_> = lod.features(world_rect(), 8.0).collect();

    assert!(coarse_calls.lock().unwrap().is_empty());
    assert!(!fine_calls.lock().unwrap().is_empty());
  }

  #[test]
  fn builder_sorts_out_of_order_bands()
  {
    let (coarse, coarse_calls) = SpyLayer::new(vec![pt(0.0, 0.0)]);
    let (fine, _) = SpyLayer::new(vec![]);

    let lod = ZoomSplitLayerBuilder::new()
      .add_band(f64::INFINITY, fine)
      .add_band(8.0, coarse)
      .build();

    let _: Vec<_> = lod.features(world_rect(), 3.0).collect();

    assert!(!coarse_calls.lock().unwrap().is_empty());
  }

  #[test]
  fn empty_builder_returns_empty_iterator()
  {
    let lod: ZoomSplitLayer<Pt> = ZoomSplitLayerBuilder::new().build();
    let result: Vec<_> = lod.features(world_rect(), 10.0).collect();
    assert!(result.is_empty());
  }

  #[test]
  fn three_band_selection()
  {
    let (coarse, coarse_calls) = SpyLayer::new(vec![]);
    let (medium, medium_calls) = SpyLayer::new(vec![]);
    let (fine, fine_calls) = SpyLayer::new(vec![]);

    let lod = ZoomSplitLayerBuilder::new()
      .add_band(8.0, coarse)
      .add_band(13.0, medium)
      .add_band(f64::INFINITY, fine)
      .build();

    let _: Vec<_> = lod.features(world_rect(), 6.0).collect();
    let _: Vec<_> = lod.features(world_rect(), 10.0).collect();
    let _: Vec<_> = lod.features(world_rect(), 15.0).collect();

    assert_eq!(coarse_calls.lock().unwrap().len(), 1);
    assert_eq!(medium_calls.lock().unwrap().len(), 1);
    assert_eq!(fine_calls.lock().unwrap().len(), 1);
  }
}