//! Map tiles — the payload codecs behind the `Tile` cargo (SPEC §1.1). //! //! Each codec produces the opaque bytes of a [`map_schema::Tile`]; the //! [`tile`] bridge wraps them as a content-addressed tile at the right cascade //! tier (SPEC §1.2): //! //! - **MVT** — Mapbox Vector Tiles (roads, boundaries, labels) → `F3:Feature` //! - **Raster** — PNG/WebP imagery and field tiles → `F2:Field` //! - **Quantized mesh** — terrain elevation mesh → `F1:Base` //! - **3D Tiles** — Cesium 3D Tiles buildings (`F3:Feature`) and a generic //! photoreal/splat wrapper (`F4:Photoreal`) //! //! Ported from `joule-tiles`. The codecs are self-contained (no S2/geo or //! routing dependency); metering lives in `map-receipt`, not here. pub mod mvt; pub mod raster; pub mod splat; pub mod tiles3d; pub mod tile; use thiserror::Error; /// Errors from tile encoding/decoding operations. #[derive(Debug, Error)] pub enum TileError { #[error("geometry encoding error: {0}")] GeometryError(String), #[error("protobuf encoding error: {0}")] ProtobufError(String), #[error("image error: {0}")] ImageError(String), #[error("invalid tile coordinate: z={z}, x={x}, y={y}")] InvalidTileCoord { z: u8, x: u32, y: u32 }, #[error("tile not found")] NotFound, #[error("compression error: {0}")] CompressionError(String), } /// Standard tile coordinate (slippy map convention). #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct TileCoord { pub z: u8, pub x: u32, pub y: u32, } impl TileCoord { /// Create a new tile coordinate. pub fn new(z: u8, x: u32, y: u32) -> Result { let max = 1u32 << z; if x >= max || y >= max { return Err(TileError::InvalidTileCoord { z, x, y }); } Ok(Self { z, x, y }) } /// Bounding box of this tile in lat/lon degrees. pub fn bounds(&self) -> (f64, f64, f64, f64) { let n = (1u32 << self.z) as f64; let lon_min = self.x as f64 / n * 360.0 - 180.0; let lon_max = (self.x + 1) as f64 / n * 360.0 - 180.0; let lat_max = (std::f64::consts::PI * (1.0 - 2.0 * self.y as f64 / n)) .sinh() .atan() .to_degrees(); let lat_min = (std::f64::consts::PI * (1.0 - 2.0 * (self.y + 1) as f64 / n)) .sinh() .atan() .to_degrees(); (lat_min, lon_min, lat_max, lon_max) } /// Tile coordinate from lat/lon at a given zoom level. pub fn from_lat_lon(lat: f64, lon: f64, z: u8) -> Self { let n = (1u32 << z) as f64; let x = ((lon + 180.0) / 360.0 * n).floor() as u32; let lat_rad = lat.to_radians(); let y = ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n).floor() as u32; let max = (1u32 << z) - 1; Self { z, x: x.min(max), y: y.min(max), } } /// Parent tile (one zoom level up). pub fn parent(&self) -> Option { if self.z == 0 { return None; } Some(Self { z: self.z - 1, x: self.x / 2, y: self.y / 2, }) } /// Four children (one zoom level down). pub fn children(&self) -> [Self; 4] { let z = self.z + 1; let x = self.x * 2; let y = self.y * 2; [ Self { z, x, y }, Self { z, x: x + 1, y }, Self { z, x, y: y + 1 }, Self { z, x: x + 1, y: y + 1, }, ] } }