1use crate::bounds::Bbox;
2use crate::crs::Crs;
3use crate::error::{AgError, AgResult};
4use std::str::FromStr;
5use tile_grid::{tms as registry, Tms as InnerTms, Xyz};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TileMatrixSet {
10 WebMercatorQuad,
12 WorldCrs84Quad,
14}
15
16impl TileMatrixSet {
17 pub fn name(self) -> &'static str {
19 match self {
20 Self::WebMercatorQuad => "WebMercatorQuad",
21 Self::WorldCrs84Quad => "WorldCRS84Quad",
22 }
23 }
24
25 pub fn parse_name(s: &str) -> AgResult<Self> {
27 match s {
28 "WebMercatorQuad" | "webmercatorquad" => Ok(Self::WebMercatorQuad),
29 "WorldCRS84Quad" | "worldcrs84quad" => Ok(Self::WorldCrs84Quad),
30 other => Err(AgError::UnknownTms(other.into())),
31 }
32 }
33
34 pub fn crs(self) -> Crs {
36 match self {
37 Self::WebMercatorQuad => Crs::epsg(3857),
38 Self::WorldCrs84Quad => Crs::epsg(4326),
39 }
40 }
41}
42
43impl FromStr for TileMatrixSet {
44 type Err = AgError;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 Self::parse_name(s)
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct TileCoord {
64 pub tms: TileMatrixSet,
66 pub z: u8,
68 pub x: u32,
70 pub y: u32,
72}
73
74pub struct Tms {
76 tile_matrix_set: TileMatrixSet,
77 inner: InnerTms,
78}
79
80impl Tms {
81 pub fn new(tile_matrix_set: TileMatrixSet) -> AgResult<Self> {
83 let inner = registry()
84 .lookup(tile_matrix_set.name())
85 .map_err(|e| AgError::UnknownTms(format!("{}: {}", tile_matrix_set.name(), e)))?;
86 Ok(Self {
87 tile_matrix_set,
88 inner,
89 })
90 }
91
92 pub fn cached(tile_matrix_set: TileMatrixSet) -> AgResult<&'static Tms> {
97 use std::sync::OnceLock;
98 fn slot(v: TileMatrixSet) -> &'static OnceLock<Tms> {
99 static WEB_MERCATOR: OnceLock<Tms> = OnceLock::new();
100 static WORLD_CRS84: OnceLock<Tms> = OnceLock::new();
101 match v {
102 TileMatrixSet::WebMercatorQuad => &WEB_MERCATOR,
103 TileMatrixSet::WorldCrs84Quad => &WORLD_CRS84,
104 }
105 }
106 let cell = slot(tile_matrix_set);
107 if let Some(t) = cell.get() {
108 return Ok(t);
109 }
110 let built = Tms::new(tile_matrix_set)?;
113 Ok(cell.get_or_init(|| built))
114 }
115
116 pub fn tile_matrix_set(&self) -> TileMatrixSet {
118 self.tile_matrix_set
119 }
120
121 pub fn crs(&self) -> Crs {
123 self.tile_matrix_set.crs()
124 }
125
126 pub fn tile_bbox(&self, z: u8, x: u32, y: u32) -> AgResult<Bbox> {
128 self.validate_tile(z, x, y)?;
129 let bb = self.inner.xy_bounds(&Xyz::new(x.into(), y.into(), z));
130 Ok(Bbox::new(bb.left, bb.bottom, bb.right, bb.top))
131 }
132
133 pub fn resolution(&self, z: u8) -> AgResult<f64> {
135 self.inner
136 .resolution_z(z)
137 .ok_or(AgError::TileOutOfBounds { z, x: 0, y: 0 })
138 }
139
140 pub(crate) fn validate_tile(&self, z: u8, x: u32, y: u32) -> AgResult<()> {
141 let matrix = self
143 .inner
144 .matrix_z(z)
145 .ok_or(AgError::TileOutOfBounds { z, x, y })?;
146 let width: u64 = matrix.matrix_width.into();
147 let height: u64 = matrix.matrix_height.into();
148 if u64::from(x) >= width || u64::from(y) >= height {
149 return Err(AgError::TileOutOfBounds { z, x, y });
150 }
151 Ok(())
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use approx::assert_relative_eq;
159
160 #[test]
161 fn unknown_tms_errors() {
162 assert!("foo".parse::<TileMatrixSet>().is_err());
163 }
164
165 #[test]
166 fn webmercatorquad_tile_0_0_0_is_full_world() {
167 let tms = Tms::new(TileMatrixSet::WebMercatorQuad).unwrap();
168 let bb = tms.tile_bbox(0, 0, 0).unwrap();
169 assert_relative_eq!(bb.xmin, -20037508.342789244, epsilon = 1e-3);
170 assert_relative_eq!(bb.xmax, 20037508.342789244, epsilon = 1e-3);
171 }
172
173 #[test]
174 fn tile_bounds_validate_both_axes_and_preserve_input() {
175 for (set, width) in [
176 (TileMatrixSet::WebMercatorQuad, 1),
177 (TileMatrixSet::WorldCrs84Quad, 2),
178 ] {
179 let tms = Tms::new(set).unwrap();
180 for z in [0, 5] {
181 let width = width << z;
182 let height = 1 << z;
183 assert!(tms.tile_bbox(z, width - 1, height - 1).is_ok());
184 for (x, y) in [(width, 0), (0, height), (width, height)] {
185 assert!(matches!(tms.tile_bbox(z, x, y),
186 Err(AgError::TileOutOfBounds { z: ez, x: ex, y: ey })
187 if (ez, ex, ey) == (z, x, y)));
188 }
189 }
190 for z in [33, u8::MAX] {
191 assert!(matches!(tms.tile_bbox(z, 17, 19),
192 Err(AgError::TileOutOfBounds { z: ez, x: 17, y: 19 }) if ez == z));
193 }
194 }
195 }
196
197 #[test]
198 fn resolution_decreases_with_zoom() {
199 let tms = Tms::new(TileMatrixSet::WebMercatorQuad).unwrap();
200 let r0 = tms.resolution(0).unwrap();
201 let r10 = tms.resolution(10).unwrap();
202 assert!(r0 > r10);
203 }
204}