use crate::bounds::Bbox;
use crate::crs::Crs;
use crate::error::{AgError, AgResult};
use std::str::FromStr;
use tile_grid::{tms as registry, Tms as InnerTms, Xyz};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TileMatrixSet {
WebMercatorQuad,
WorldCrs84Quad,
}
impl TileMatrixSet {
pub fn name(self) -> &'static str {
match self {
Self::WebMercatorQuad => "WebMercatorQuad",
Self::WorldCrs84Quad => "WorldCRS84Quad",
}
}
pub fn parse_name(s: &str) -> AgResult<Self> {
match s {
"WebMercatorQuad" | "webmercatorquad" => Ok(Self::WebMercatorQuad),
"WorldCRS84Quad" | "worldcrs84quad" => Ok(Self::WorldCrs84Quad),
other => Err(AgError::UnknownTms(other.into())),
}
}
pub fn crs(self) -> Crs {
match self {
Self::WebMercatorQuad => Crs::epsg(3857),
Self::WorldCrs84Quad => Crs::epsg(4326),
}
}
}
impl FromStr for TileMatrixSet {
type Err = AgError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse_name(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileCoord {
pub tms: TileMatrixSet,
pub z: u8,
pub x: u32,
pub y: u32,
}
pub struct Tms {
tile_matrix_set: TileMatrixSet,
inner: InnerTms,
}
impl Tms {
pub fn new(tile_matrix_set: TileMatrixSet) -> AgResult<Self> {
let inner = registry()
.lookup(tile_matrix_set.name())
.map_err(|e| AgError::UnknownTms(format!("{}: {}", tile_matrix_set.name(), e)))?;
Ok(Self {
tile_matrix_set,
inner,
})
}
pub fn cached(tile_matrix_set: TileMatrixSet) -> AgResult<&'static Tms> {
use std::sync::OnceLock;
fn slot(v: TileMatrixSet) -> &'static OnceLock<Tms> {
static WEB_MERCATOR: OnceLock<Tms> = OnceLock::new();
static WORLD_CRS84: OnceLock<Tms> = OnceLock::new();
match v {
TileMatrixSet::WebMercatorQuad => &WEB_MERCATOR,
TileMatrixSet::WorldCrs84Quad => &WORLD_CRS84,
}
}
let cell = slot(tile_matrix_set);
if let Some(t) = cell.get() {
return Ok(t);
}
let built = Tms::new(tile_matrix_set)?;
Ok(cell.get_or_init(|| built))
}
pub fn tile_matrix_set(&self) -> TileMatrixSet {
self.tile_matrix_set
}
pub fn crs(&self) -> Crs {
self.tile_matrix_set.crs()
}
pub fn tile_bbox(&self, z: u8, x: u32, y: u32) -> AgResult<Bbox> {
self.validate_tile(z, x, y)?;
let bb = self.inner.xy_bounds(&Xyz::new(x.into(), y.into(), z));
Ok(Bbox::new(bb.left, bb.bottom, bb.right, bb.top))
}
pub fn resolution(&self, z: u8) -> AgResult<f64> {
self.inner
.resolution_z(z)
.ok_or(AgError::TileOutOfBounds { z, x: 0, y: 0 })
}
pub(crate) fn validate_tile(&self, z: u8, x: u32, y: u32) -> AgResult<()> {
let matrix = self
.inner
.matrix_z(z)
.ok_or(AgError::TileOutOfBounds { z, x, y })?;
let width: u64 = matrix.matrix_width.into();
let height: u64 = matrix.matrix_height.into();
if u64::from(x) >= width || u64::from(y) >= height {
return Err(AgError::TileOutOfBounds { z, x, y });
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn unknown_tms_errors() {
assert!("foo".parse::<TileMatrixSet>().is_err());
}
#[test]
fn webmercatorquad_tile_0_0_0_is_full_world() {
let tms = Tms::new(TileMatrixSet::WebMercatorQuad).unwrap();
let bb = tms.tile_bbox(0, 0, 0).unwrap();
assert_relative_eq!(bb.xmin, -20037508.342789244, epsilon = 1e-3);
assert_relative_eq!(bb.xmax, 20037508.342789244, epsilon = 1e-3);
}
#[test]
fn tile_bounds_validate_both_axes_and_preserve_input() {
for (set, width) in [
(TileMatrixSet::WebMercatorQuad, 1),
(TileMatrixSet::WorldCrs84Quad, 2),
] {
let tms = Tms::new(set).unwrap();
for z in [0, 5] {
let width = width << z;
let height = 1 << z;
assert!(tms.tile_bbox(z, width - 1, height - 1).is_ok());
for (x, y) in [(width, 0), (0, height), (width, height)] {
assert!(matches!(tms.tile_bbox(z, x, y),
Err(AgError::TileOutOfBounds { z: ez, x: ex, y: ey })
if (ez, ex, ey) == (z, x, y)));
}
}
for z in [33, u8::MAX] {
assert!(matches!(tms.tile_bbox(z, 17, 19),
Err(AgError::TileOutOfBounds { z: ez, x: 17, y: 19 }) if ez == z));
}
}
}
#[test]
fn resolution_decreases_with_zoom() {
let tms = Tms::new(TileMatrixSet::WebMercatorQuad).unwrap();
let r0 = tms.resolution(0).unwrap();
let r10 = tms.resolution(10).unwrap();
assert!(r0 > r10);
}
}