tile_grid/
tms.rs

1use crate::hilbert::HilbertIterator;
2use crate::quadkey::check_quadkey_support;
3use crate::tile::{BoundingBox, Coords, Xyz};
4use crate::tile_matrix_set::{ordered_axes_inverted, TileMatrixSetOps};
5use crate::tms_iterator::XyzIterator;
6use crate::transform::{merc_tile_ul, Error::TransformationUnsupported, Transform, Transformer};
7use ogcapi_types::common::Crs;
8use ogcapi_types::tiles::{
9    BoundingBox2D, CornerOfOrigin, OrderedAxes, TileMatrix, TileMatrixSet, TitleDescriptionKeywords,
10};
11use std::convert::AsRef;
12use std::f64::consts::PI;
13use std::num::{NonZeroU16, NonZeroU64};
14
15/// Tile Matrix Set API.
16#[derive(Debug)]
17pub struct Tms {
18    pub tms: TileMatrixSet,
19    pub is_quadtree: bool,
20    // CRS transformation attributes
21    data_crs: Crs,
22    geographic_crs: Crs, // default=WGS84_CRS
23    to_geographic: Option<Transformer>,
24    from_geographic: Option<Transformer>,
25}
26
27#[derive(thiserror::Error, Debug)]
28pub enum TmsError {
29    #[error("Invalid tile zoom identifier: `{0}`")]
30    InvalidZoomId(String),
31    #[error("Invalid zoom level: `{0}`")]
32    InvalidZoom(u8),
33    #[error("Point ({0}, {1}) is outside bounds {2:?}")]
34    PointOutsideBounds(f64, f64, BoundingBox),
35    #[error(transparent)]
36    TransformationError(#[from] crate::transform::Error),
37    #[error("Zero width or height")]
38    NonZeroError,
39    // #[error("Raised when math errors occur beyond ~85 degrees N or S")]
40    // InvalidLatitudeError,
41    // #[error("TileMatrix not found for level: {0} - Unable to construct tileMatrix for TMS with variable scale")]
42    // InvalidZoomError(u8),
43    // #[error("Raised when errors occur in parsing a function's tile arg(s)")]
44    // TileArgParsingError,
45    // #[error("Raised when a custom TileMatrixSet doesn't support quadkeys")]
46    // NoQuadkeySupport,
47    // #[error("Raised when errors occur in computing or parsing quad keys")]
48    // QuadKeyError,
49}
50
51pub type Result<T> = std::result::Result<T, TmsError>;
52
53impl Clone for Tms {
54    // Custom impl because `Clone` is not implemented for `Proj`
55    fn clone(&self) -> Tms {
56        Tms::init(&self.tms).expect("Repeating initialization")
57    }
58}
59
60pub enum Matrix<'a> {
61    Predefined(&'a TileMatrix),
62    Calculated(TileMatrix),
63}
64
65impl AsRef<TileMatrix> for Matrix<'_> {
66    fn as_ref(&self) -> &TileMatrix {
67        match self {
68            Matrix::Predefined(m) => m,
69            Matrix::Calculated(m) => m,
70        }
71    }
72}
73
74pub enum ZoomLevelStrategy {
75    Lower,
76    Upper,
77    Auto,
78}
79
80impl Tms {
81    /// Prepare transformations and check if TileMatrixSet supports quadkeys.
82    pub(crate) fn init(data: &TileMatrixSet) -> Result<Self> {
83        let is_quadtree = check_quadkey_support(&data.tile_matrices);
84        let data_crs = data.crs.clone();
85        let geographic_crs = Crs::default(); // data.get("_geographic_crs", WGS84_CRS)
86        let to_geographic = Transformer::from_crs(&data_crs, &geographic_crs, true).ok();
87        let from_geographic = Transformer::from_crs(&geographic_crs, &data_crs, true).ok();
88        let mut tms = data.clone();
89        Self::sort_tile_matrices(&mut tms)?;
90        // Check bounding box CRS (TODO: should we store it?)
91        if let Some(bounding_box) = &tms.bounding_box {
92            if let Some(crs) = &bounding_box.crs {
93                if *crs != tms.crs {
94                    let _transform = Transformer::from_crs(crs, &tms.crs, true)?;
95                }
96            }
97        }
98        Ok(Self {
99            tms,
100            is_quadtree,
101            data_crs,
102            geographic_crs,
103            to_geographic,
104            from_geographic,
105        })
106    }
107
108    /// Sort matrices by identifier
109    fn sort_tile_matrices(tms: &mut TileMatrixSet) -> Result<()> {
110        // Check zoom identifier format
111        for m in &tms.tile_matrices {
112            m.id.parse::<u8>()
113                .map_err(|_e| TmsError::InvalidZoomId(m.id.clone()))?;
114        }
115        tms.tile_matrices.sort_by(|a, b| {
116            a.id.parse::<u8>()
117                .unwrap()
118                .cmp(&b.id.parse::<u8>().unwrap())
119        });
120        Ok(())
121    }
122
123    /// Iterate over matrices
124    pub fn matrices(&self) -> &Vec<TileMatrix> {
125        &self.tms.tile_matrices
126    }
127
128    /// Fetch CRS from epsg
129    pub fn crs(&self) -> &Crs {
130        &self.tms.crs
131    }
132
133    /// TileMatrixSet minimum TileMatrix identifier
134    pub fn minzoom(&self) -> u8 {
135        self.tms.tile_matrices[0].id.parse::<u8>().unwrap()
136    }
137    /// TileMatrixSet maximum TileMatrix identifier
138    pub fn maxzoom(&self) -> u8 {
139        self.tms.tile_matrices[self.tms.tile_matrices.len() - 1]
140            .id
141            .parse::<u8>()
142            .unwrap()
143    }
144
145    /// Check if CRS has inverted AXIS (lat,lon) instead of (lon,lat).
146    fn invert_axis(&self) -> bool {
147        self.tms.crs_axis_inverted()
148    }
149
150    /// Construct a custom TileMatrixSet.
151    ///
152    /// # Arguments
153    /// * `crs` - Tile Matrix Set coordinate reference system
154    /// * `extent` - Bounding box of the Tile Matrix Set, (left, bottom, right, top).
155    /// * `tile_width` - Width of each tile of this tile matrix in pixels (default is 256).
156    /// * `tile_height` - Height of each tile of this tile matrix in pixels (default is 256).
157    /// * `matrix_scale` - Tiling schema coalescence coefficient (default: [1, 1] for EPSG:3857).
158    ///     Should be set to [2, 1] for EPSG:4326.
159    ///     see: <http:///docs.opengeospatial.org/is/17-083r2/17-083r2.html#14>
160    /// * `extent_crs` - pyproj.CRS
161    ///     Extent's coordinate reference system, as a pyproj CRS object.
162    ///     (default: same as input crs)
163    /// * `minzoom` - Tile Matrix Set minimum zoom level (default is 0).
164    /// * `maxzoom` - Tile Matrix Set maximum zoom level (default is 24).
165    /// * `title` - Tile Matrix Set title (default is 'Custom TileMatrixSet')
166    /// * `id` - Tile Matrix Set identifier (default is 'Custom')
167    /// * `ordered_axes`
168    /// * `geographic_crs` - Geographic (lat,lon) coordinate reference system (default is EPSG:4326)
169    #[allow(clippy::too_many_arguments)]
170    pub fn custom(
171        extent: Vec<f64>,
172        crs: &Crs,
173        tile_width: u16,               // = 256,
174        tile_height: u16,              // = 256,
175        matrix_scale: Option<Vec<u8>>, // = None,
176        extent_crs: Option<&Crs>,      // = None,
177        minzoom: u8,                   // = 0,
178        maxzoom: u8,                   // = 24,
179        title: &str,                   // = "Custom TileMatrixSet",
180        id: &str,                      // = "Custom",
181        ordered_axes: Option<OrderedAxes>,
182        geographic_crs: &Crs, // = WGS84_CRS,
183    ) -> Result<Self> {
184        let matrix_scale = matrix_scale.unwrap_or(vec![1, 1]);
185        let bbox = transformed_bbox(&extent, crs, extent_crs)?;
186        let width = (bbox.right - bbox.left).abs();
187        let height = (bbox.top - bbox.bottom).abs();
188        let resolutions: Vec<f64> = (minzoom..=maxzoom)
189            .map(|zoom| {
190                f64::max(
191                    width
192                        / (tile_width as f64 * matrix_scale[0] as f64)
193                        / 2_u64.pow(zoom as u32) as f64,
194                    height
195                        / (tile_height as f64 * matrix_scale[1] as f64)
196                        / 2_u64.pow(zoom as u32) as f64,
197                )
198            })
199            .collect();
200        Self::custom_resolutions(
201            extent,
202            crs,
203            tile_width,
204            tile_height,
205            extent_crs,
206            resolutions,
207            title,
208            id,
209            ordered_axes,
210            geographic_crs,
211        )
212    }
213
214    /// Construct a custom TileMatrixSet with given resolutions
215    #[allow(clippy::too_many_arguments)]
216    pub fn custom_resolutions(
217        extent: Vec<f64>,
218        crs: &Crs,
219        tile_width: u16,
220        tile_height: u16,
221        extent_crs: Option<&Crs>,
222        resolutions: Vec<f64>,
223        title: &str,
224        id: &str,
225        ordered_axes: Option<OrderedAxes>,
226        geographic_crs: &Crs,
227    ) -> Result<Self> {
228        let mut tms = TileMatrixSet {
229            title_description_keywords: TitleDescriptionKeywords {
230                title: Some(title.to_string()),
231                description: None,
232                keywords: None,
233            },
234            id: id.to_string(),
235            uri: None,
236            crs: crs.clone(),
237            ordered_axes: ordered_axes.clone(),
238            well_known_scale_set: None,
239            bounding_box: None,
240            tile_matrices: Vec::with_capacity(resolutions.len()),
241        };
242
243        let is_inverted = if let Some(ordered_axes) = &ordered_axes {
244            ordered_axes_inverted(ordered_axes)
245        } else {
246            tms.crs_axis_inverted()
247        };
248
249        tms.bounding_box = Some(if is_inverted {
250            BoundingBox2D {
251                crs: Some(extent_crs.unwrap_or(crs).clone()),
252                ordered_axes: ordered_axes.clone(),
253                lower_left: [extent[1], extent[0]],
254                upper_right: [extent[3], extent[2]],
255            }
256        } else {
257            BoundingBox2D {
258                crs: Some(extent_crs.unwrap_or(crs).clone()),
259                ordered_axes: ordered_axes.clone(),
260                lower_left: [extent[0], extent[1]],
261                upper_right: [extent[2], extent[3]],
262            }
263        });
264
265        let bbox = transformed_bbox(&extent, crs, extent_crs)?;
266
267        let x_origin = if !is_inverted { bbox.left } else { bbox.top };
268        let y_origin = if !is_inverted { bbox.top } else { bbox.left };
269        let corner_of_origin = if !is_inverted {
270            None
271        } else {
272            Some(CornerOfOrigin::BottomLeft)
273        };
274
275        let mpu = meters_per_unit(crs);
276        for (zoom, res) in resolutions.iter().enumerate() {
277            let unitheight = tile_height as f64 * res;
278            let unitwidth = tile_width as f64 * res;
279            let maxy = ((bbox.top - bbox.bottom - 0.01 * unitheight) / unitheight).ceil() as u64;
280            let maxx = ((bbox.right - bbox.left - 0.01 * unitwidth) / unitwidth).ceil() as u64;
281            tms.tile_matrices.push(TileMatrix {
282                title_description_keywords: TitleDescriptionKeywords {
283                    title: None,
284                    description: None,
285                    keywords: None,
286                },
287                id: zoom.to_string(),
288                scale_denominator: res * mpu / 0.00028,
289                cell_size: *res,
290                corner_of_origin: corner_of_origin.clone(),
291                point_of_origin: [x_origin, y_origin],
292                tile_width: NonZeroU16::new(tile_width).ok_or(TmsError::NonZeroError)?,
293                tile_height: NonZeroU16::new(tile_height).ok_or(TmsError::NonZeroError)?,
294                matrix_width: NonZeroU64::new(maxx).ok_or(TmsError::NonZeroError)?,
295                matrix_height: NonZeroU64::new(maxy).ok_or(TmsError::NonZeroError)?,
296                variable_matrix_widths: None,
297            });
298        }
299
300        let mut tms = Tms::init(&tms)?;
301        tms.geographic_crs = geographic_crs.clone();
302        Ok(tms)
303    }
304
305    /// Return the TileMatrix for a specific zoom without automatic tile matrix extension.
306    pub fn matrix_z(&self, zoom: u8) -> Option<&TileMatrix> {
307        self.tms
308            .tile_matrices
309            .iter()
310            .find(|&m| m.id == zoom.to_string())
311    }
312
313    /// Return the TileMatrix for a specific zoom.
314    pub fn matrix(&self, zoom: u8) -> Matrix<'_> {
315        if let Some(m) = self.matrix_z(zoom) {
316            return Matrix::Predefined(m);
317        }
318
319        let matrix_scale = (1..self.tms.tile_matrices.len())
320            .map(|idx| {
321                (self.tms.tile_matrices[idx].scale_denominator
322                    / self.tms.tile_matrices[idx - 1].scale_denominator)
323                    .round() // FIXME: round ndigits=2
324            })
325            .collect::<Vec<_>>();
326        if matrix_scale.len() > 1 {
327            // TODO: always true, error in morecantile?
328            // panic!(
329            //     "TileMatrix not found for level: {} - Unable to construct tileMatrix for TMS with variable scale",
330            //     zoom
331            // );
332        }
333
334        let mut tile_matrix = self.tms.tile_matrices.last().unwrap().clone();
335        let factor = 1.0 / matrix_scale[0];
336        while tile_matrix.id != zoom.to_string() {
337            tile_matrix = TileMatrix {
338                title_description_keywords: TitleDescriptionKeywords {
339                    title: None,
340                    description: None,
341                    keywords: None,
342                },
343                id: (tile_matrix.id.parse::<u8>().unwrap() + 1).to_string(),
344                scale_denominator: tile_matrix.scale_denominator / factor,
345                cell_size: tile_matrix.cell_size, // FIXME
346                corner_of_origin: tile_matrix.corner_of_origin,
347                point_of_origin: tile_matrix.point_of_origin,
348                tile_width: tile_matrix.tile_width,
349                tile_height: tile_matrix.tile_height,
350                matrix_width: NonZeroU64::new(
351                    (u64::from(tile_matrix.matrix_width) as f64 * factor).round() as u64,
352                )
353                .unwrap(),
354                matrix_height: NonZeroU64::new(
355                    (u64::from(tile_matrix.matrix_height) as f64 * factor).round() as u64,
356                )
357                .unwrap(),
358                variable_matrix_widths: None,
359            }
360        }
361
362        Matrix::Calculated(tile_matrix)
363    }
364
365    /// Tile resolution for a TileMatrix.
366    //
367    // From note g in <http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2>:
368    //   The pixel size of the tile can be obtained from the scaleDenominator
369    //   by multiplying the later by 0.28 10-3 / metersPerUnit.
370    pub fn resolution(&self, matrix: &TileMatrix) -> f64 {
371        matrix.scale_denominator * 0.28e-3 / meters_per_unit(self.crs())
372    }
373
374    /// Tile resolution for a specific zoom.
375    pub fn resolution_z(&self, zoom: u8) -> Option<f64> {
376        self.matrix_z(zoom).map(|m| self.resolution(m))
377    }
378
379    /// Get TMS zoom level corresponding to a specific resolution.
380    ///
381    /// # Arguments
382    /// * `res` - Resolution in TMS unit.
383    /// * `max_z` - Maximum zoom level (default is tms maxzoom).
384    /// * `zoom_level_strategy` - Strategy to determine zoom level (same as in GDAL 3.2).
385    ///         LOWER will select the zoom level immediately below the theoretical computed non-integral zoom level.
386    ///         On the contrary, UPPER will select the immediately above zoom level.
387    ///         Defaults to AUTO which selects the closest zoom level.
388    ///         ref: <https://gdal.org/drivers/raster/cog.html#raster-cog>
389    /// * `min_z` - Minimum zoom level (default is tms minzoom).
390    ///
391    /// # Returns:
392    /// * TMS zoom for a given resolution.
393    pub fn zoom_for_res(
394        &self,
395        res: f64,
396        max_z: Option<u8>,
397        zoom_level_strategy: &ZoomLevelStrategy,
398        min_z: Option<u8>,
399    ) -> Result<u8> {
400        let max_z = max_z.unwrap_or(self.maxzoom());
401        let min_z = min_z.unwrap_or(self.minzoom());
402        let mut zoom_level = min_z;
403        let mut matrix_res = 0.0;
404        for z in min_z..=max_z {
405            zoom_level = z;
406            matrix_res = self.resolution(self.matrix(zoom_level).as_ref());
407            if res > matrix_res || (res - matrix_res).abs() / matrix_res <= 1e-8 {
408                break;
409            }
410        }
411        if zoom_level > 0 && (res - matrix_res).abs() / matrix_res > 1e-8 {
412            match zoom_level_strategy {
413                ZoomLevelStrategy::Lower => {
414                    zoom_level = u8::max(zoom_level - 1, min_z);
415                }
416                ZoomLevelStrategy::Upper => {
417                    zoom_level = u8::min(zoom_level, max_z);
418                }
419                ZoomLevelStrategy::Auto => {
420                    if (self.resolution(self.matrix(u8::max(zoom_level - 1, min_z)).as_ref()) / res)
421                        < (res / matrix_res)
422                    {
423                        zoom_level = u8::max(zoom_level - 1, min_z);
424                    }
425                }
426            }
427        }
428        Ok(zoom_level)
429    }
430
431    /// Transform point(x,y) to geographic longitude and latitude.
432    fn lnglat(&self, x: f64, y: f64, truncate: bool /* =False */) -> Result<Coords> {
433        let Some(transformer) = &self.to_geographic else {
434            return Err(self.transform_error_to_geographic());
435        };
436        point_in_bbox(Coords::new(x, y), self.xy_bbox(), DEFAULT_BBOX_PREC)?;
437        let (mut lng, mut lat) = transformer.transform(x, y)?;
438
439        if truncate {
440            (lng, lat) = self.truncate_lnglat(lng, lat)?;
441        }
442
443        Ok(Coords::new(lng, lat))
444    }
445
446    /// Transform geographic longitude and latitude coordinates to TMS CRS
447    pub fn xy(&self, lng: f64, lat: f64) -> Result<Coords> {
448        let Some(transformer) = &self.from_geographic else {
449            return Err(self.transform_error_from_geographic());
450        };
451        point_in_bbox(Coords::new(lng, lat), self.xy_bbox(), DEFAULT_BBOX_PREC)?;
452
453        let (x, y) = transformer.transform(lng, lat)?;
454
455        Ok(Coords::new(x, y))
456    }
457
458    /// Transform geographic longitude and latitude coordinates to TMS CRS. Truncate geographic coordinates to TMS geographic bbox.
459    pub fn xy_truncated(&self, lng: f64, lat: f64) -> Result<Coords> {
460        let (lng, lat) = self.truncate_lnglat(lng, lat)?;
461        self.xy(lng, lat)
462    }
463
464    /// Truncate geographic coordinates to TMS geographic bbox.
465    //
466    // Adapted from <https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py>
467    pub fn truncate_lnglat(&self, lng: f64, lat: f64) -> Result<(f64, f64)> {
468        let mut lng = lng;
469        let mut lat = lat;
470        let bbox = self.bbox()?;
471        if lng > bbox.right {
472            lng = bbox.right;
473        } else if lng < bbox.left {
474            lng = bbox.left;
475        }
476
477        if lat > bbox.top {
478            lat = bbox.top;
479        } else if lat < bbox.bottom {
480            lat = bbox.bottom;
481        }
482
483        Ok((lng, lat))
484    }
485
486    /// Get the tile containing a Point (in TMS CRS).
487    ///
488    /// # Arguments
489    /// * `xcoord`, ycoord - A `X` and `Y` pair in TMS coordinate reference system.
490    /// * `zoom` - The zoom level.
491    pub fn xy_tile(&self, xcoord: f64, ycoord: f64, zoom: u8) -> Xyz {
492        let m = self.matrix(zoom);
493        let matrix = m.as_ref();
494        let res = self.resolution(matrix);
495
496        let origin_x: f64 = if self.invert_axis() {
497            matrix.point_of_origin[1]
498        } else {
499            matrix.point_of_origin[0]
500        };
501        let origin_y = if self.invert_axis() {
502            matrix.point_of_origin[0]
503        } else {
504            matrix.point_of_origin[1]
505        };
506
507        let xtile = if !xcoord.is_infinite() {
508            ((xcoord - origin_x) / (res * u16::from(matrix.tile_width) as f64)).floor()
509        } else {
510            0.0
511        };
512        let ytile = if !ycoord.is_infinite() {
513            ((origin_y - ycoord) / (res * u16::from(matrix.tile_height) as f64)).floor()
514        } else {
515            0.0
516        };
517
518        // avoid out-of-range tiles
519        let xtile = if xtile < 0.0 { 0 } else { xtile as u64 };
520
521        let ytile = if ytile < 0.0 { 0 } else { ytile as u64 };
522
523        let xtile = if xtile > matrix.matrix_width.into() {
524            matrix.matrix_width.into()
525        } else {
526            xtile
527        };
528
529        let ytile = if ytile > matrix.matrix_height.into() {
530            matrix.matrix_height.into()
531        } else {
532            ytile
533        };
534
535        Xyz::new(xtile, ytile, zoom)
536    }
537
538    /// Get the tile for a given geographic longitude and latitude pair.
539    ///
540    /// # Arguments
541    /// * `lng`, `lat` : A longitude and latitude pair in geographic coordinate reference system.
542    /// * `zoom` : The zoom level.
543    pub fn tile(&self, lng: f64, lat: f64, zoom: u8) -> Result<Xyz> {
544        let xy = self.xy(lng, lat)?;
545        Ok(self.xy_tile(xy.x, xy.y, zoom))
546    }
547
548    /// Get the tile for a given geographic longitude and latitude pair. Truncate inputs to limits of TMS geographic bounds.
549    ///
550    /// # Arguments
551    /// * `lng`, `lat` : A longitude and latitude pair in geographic coordinate reference system.
552    /// * `zoom` : The zoom level.
553    pub fn tile_truncated(&self, lng: f64, lat: f64, zoom: u8) -> Result<Xyz> {
554        let xy = self.xy_truncated(lng, lat)?;
555        Ok(self.xy_tile(xy.x, xy.y, zoom))
556    }
557
558    /// Return the upper left coordinate of the tile in TMS coordinate reference system.
559    ///
560    /// # Arguments
561    /// * `tile`: (x, y, z) tile coordinates or a Tile object we want the upper left coordinates of.
562    pub fn xy_ul(&self, tile: &Xyz) -> Coords {
563        let m = self.matrix(tile.z);
564        let matrix = m.as_ref();
565        let res = self.resolution(matrix);
566
567        let origin_x = if self.invert_axis() {
568            matrix.point_of_origin[1]
569        } else {
570            matrix.point_of_origin[0]
571        };
572        let origin_y = if self.invert_axis() {
573            matrix.point_of_origin[0]
574        } else {
575            matrix.point_of_origin[1]
576        };
577
578        let xcoord = origin_x + tile.x as f64 * res * u16::from(matrix.tile_width) as f64;
579        let ycoord = origin_y - tile.y as f64 * res * u16::from(matrix.tile_height) as f64;
580        Coords::new(xcoord, ycoord)
581    }
582
583    /// Return the bounding box of the tile in TMS coordinate reference system.
584    ///
585    /// # Arguments
586    /// * `tile`: Tile object we want the bounding box of.
587    pub fn xy_bounds(&self, tile: &Xyz) -> BoundingBox {
588        let top_left = self.xy_ul(tile);
589        let bottom_right = self.xy_ul(&Xyz::new(tile.x + 1, tile.y + 1, tile.z));
590        BoundingBox::new(top_left.x, bottom_right.y, bottom_right.x, top_left.y)
591    }
592
593    /// Return the upper left coordinates of the tile in geographic coordinate reference system.
594    ///
595    /// # Arguments
596    /// * `tile` - (x, y, z) tile coordinates or a Tile object we want the upper left geographic coordinates of.
597    pub fn ul(&self, tile: &Xyz) -> Result<Coords> {
598        let coords = if self.data_crs.as_srid() == 3857 && self.geographic_crs.as_srid() == 4326 {
599            let (lon, lat) = merc_tile_ul(tile.x as u32, tile.y as u32, tile.z);
600            Coords::new(lon, lat)
601        } else {
602            let xy = self.xy_ul(tile);
603            self.lnglat(xy.x, xy.y, false)?
604        };
605        Ok(coords)
606    }
607
608    /// Return the bounding box of the tile in geographic coordinate reference system.
609    ///
610    /// # Arguments
611    /// * `tile` - Tile object we want the bounding box of.
612    pub fn bounds(&self, tile: &Xyz) -> Result<BoundingBox> {
613        let top_left = self.ul(tile)?;
614        let bottom_right = self.ul(&Xyz::new(tile.x + 1, tile.y + 1, tile.z))?;
615        Ok(BoundingBox::new(
616            top_left.x,
617            bottom_right.y,
618            bottom_right.x,
619            top_left.y,
620        ))
621    }
622
623    /// Return TMS bounding box in TileMatrixSet's CRS.
624    pub fn xy_bbox(&self) -> BoundingBox {
625        let (left, bottom, right, top) = if let Some(bounding_box) = &self.tms.bounding_box {
626            let (left, bottom) = if self.invert_axis() {
627                (&bounding_box.lower_left[1], &bounding_box.lower_left[0])
628            } else {
629                (&bounding_box.lower_left[0], &bounding_box.lower_left[1])
630            };
631            let (right, top) = if self.invert_axis() {
632                (&bounding_box.upper_right[1], &bounding_box.upper_right[0])
633            } else {
634                (&bounding_box.upper_right[0], &bounding_box.upper_right[1])
635            };
636            if let Some(crs) = &bounding_box.crs {
637                if crs != self.crs() {
638                    // Verified in init function
639                    let transform =
640                        Transformer::from_crs(crs, self.crs(), true).expect("Transformer");
641                    let (left, bottom, right, top) = transform
642                        .transform_bounds(*left, *bottom, *right, *top /* , Some(21) */)
643                        .expect("Transformer");
644                    (left, bottom, right, top)
645                } else {
646                    (*left, *bottom, *right, *top)
647                }
648            } else {
649                (*left, *bottom, *right, *top)
650            }
651        } else {
652            let zoom = self.minzoom();
653            let m = self.matrix(zoom);
654            let matrix = m.as_ref();
655            let top_left = self.xy_ul(&Xyz::new(0, 0, zoom));
656            let bottom_right = self.xy_ul(&Xyz::new(
657                u64::from(matrix.matrix_width),
658                u64::from(matrix.matrix_height),
659                zoom,
660            ));
661            (top_left.x, bottom_right.y, bottom_right.x, top_left.y)
662        };
663        BoundingBox {
664            left,
665            bottom,
666            right,
667            top,
668        }
669    }
670
671    /// Return TMS bounding box in geographic coordinate reference system.
672    pub fn bbox(&self) -> Result<BoundingBox> {
673        let Some(transformer) = &self.to_geographic else {
674            return Err(self.transform_error_to_geographic());
675        };
676        let xy_bbox = self.xy_bbox();
677        let bbox = transformer.transform_bounds(
678            xy_bbox.left,
679            xy_bbox.bottom,
680            xy_bbox.right,
681            xy_bbox.top,
682        )?;
683        Ok(BoundingBox::new(bbox.0, bbox.1, bbox.2, bbox.3))
684    }
685
686    /// Check if a bounds intersects with the TMS bounds.
687    pub fn intersect_tms(&self, bbox: &BoundingBox) -> bool {
688        let tms_bounds = self.xy_bbox();
689        bbox.left < tms_bounds.right
690            && bbox.right > tms_bounds.left
691            && bbox.top > tms_bounds.bottom
692            && bbox.bottom < tms_bounds.top
693    }
694
695    /// Get the tiles overlapped by a geographic bounding box
696    //
697    // Original code from <https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py#L424>
698    ///
699    /// # Arguments
700    /// * `west`, `south`, `east`, `north` - Bounding values in decimal degrees (geographic CRS).
701    /// * `zooms` - One or more zoom levels.
702    /// * `truncate` : Whether or not to truncate inputs to web mercator limits.
703    ///
704    /// # Notes
705    /// A small epsilon is used on the south and east parameters so that this
706    /// function yields exactly one tile when given the bounds of that same tile.
707    pub fn tiles(
708        &self,
709        west: f64,
710        south: f64,
711        east: f64,
712        north: f64,
713        zooms: &[u8],
714        truncate: bool, /* = False */
715    ) -> Result<impl Iterator<Item = Xyz>> {
716        let mut tiles: Vec<Xyz> = Vec::new();
717        let bbox = self.bbox()?;
718        let bboxes = if west > east {
719            vec![
720                (bbox.left, south, east, north),
721                (west, south, bbox.right, north),
722            ]
723        } else {
724            vec![(west, south, east, north)]
725        };
726        let get_tile = if truncate {
727            Tms::tile_truncated
728        } else {
729            Tms::tile
730        };
731        for bb in bboxes {
732            let w = bb.0.max(bbox.left);
733            let s = bb.1.max(bbox.bottom);
734            let e = bb.2.min(bbox.right);
735            let n = bb.3.min(bbox.top);
736            for z in zooms {
737                let ul_tile = get_tile(self, w + LL_EPSILON, n - LL_EPSILON, *z)?;
738                let lr_tile = get_tile(self, e - LL_EPSILON, s + LL_EPSILON, *z)?;
739                for i in ul_tile.x..=lr_tile.x {
740                    for j in ul_tile.y..=lr_tile.y {
741                        tiles.push(Xyz::new(i, j, *z));
742                    }
743                }
744            }
745        }
746        Ok(tiles.into_iter())
747    }
748
749    /// Get the tile limits overlapped by a geographic bounding box
750    fn extent_limits(
751        &self,
752        extend: &BoundingBox,
753        minzoom: u8,
754        maxzoom: u8,
755        truncate: bool, /* = False */
756    ) -> Result<Vec<MinMax>> {
757        if extend.left > extend.right || minzoom > maxzoom {
758            return Ok(Vec::new()); // TODO: Handle extend over date line
759        }
760        let bbox = self.bbox()?;
761        let get_tile = if truncate {
762            Tms::tile_truncated
763        } else {
764            Tms::tile
765        };
766        let w = extend.left.max(bbox.left);
767        let s = extend.bottom.max(bbox.bottom);
768        let e = extend.right.min(bbox.right);
769        let n = extend.top.min(bbox.top);
770        let limits = (minzoom..=maxzoom)
771            .map(|z| {
772                let ul_tile = get_tile(self, w + LL_EPSILON, n - LL_EPSILON, z)?;
773                let lr_tile = get_tile(self, e - LL_EPSILON, s + LL_EPSILON, z)?;
774                Ok(MinMax {
775                    x_min: ul_tile.x,
776                    x_max: lr_tile.x,
777                    y_min: ul_tile.y,
778                    y_max: lr_tile.y,
779                })
780            })
781            .collect::<Result<Vec<MinMax>>>()?;
782        Ok(limits)
783    }
784
785    /// Get the tile limits overlapped by a bounding box in TMS CRS
786    fn extent_limits_xy(&self, extend: &BoundingBox, minzoom: u8, maxzoom: u8) -> Vec<MinMax> {
787        if extend.left > extend.right || minzoom > maxzoom {
788            return Vec::new(); // TODO: Handle extend over date line
789        }
790        let bbox = self.xy_bbox();
791        let w = extend.left.max(bbox.left);
792        let s = extend.bottom.max(bbox.bottom);
793        let e = extend.right.min(bbox.right);
794        let n = extend.top.min(bbox.top);
795        (minzoom..=maxzoom)
796            .map(|z| {
797                let res = self.resolution(self.matrix(z).as_ref()) / 10.0;
798                let ul_tile = self.xy_tile(w + res, n - res, z);
799                let lr_tile = self.xy_tile(e - res, s + res, z);
800                MinMax {
801                    x_min: ul_tile.x,
802                    x_max: lr_tile.x,
803                    y_min: ul_tile.y,
804                    y_max: lr_tile.y,
805                }
806            })
807            .collect()
808    }
809
810    /// Get iterator over all tiles overlapped by a geographic bounding box
811    pub fn xyz_iterator_geographic(
812        &self,
813        extend: &BoundingBox,
814        minzoom: u8,
815        maxzoom: u8,
816    ) -> Result<XyzIterator> {
817        let limits = self.extent_limits(extend, minzoom, maxzoom, false)?;
818        Ok(XyzIterator::new(minzoom, maxzoom, limits))
819    }
820
821    /// Get iterator over all tiles overlapped by a bounding box in TMS CRS
822    pub fn xyz_iterator(&self, extend: &BoundingBox, minzoom: u8, maxzoom: u8) -> XyzIterator {
823        let limits = self.extent_limits_xy(extend, minzoom, maxzoom);
824        XyzIterator::new(minzoom, maxzoom, limits)
825    }
826
827    /// Get hilbert iterator over all tiles
828    pub fn hilbert_iterator(&self, minzoom: u8, maxzoom: u8) -> HilbertIterator {
829        HilbertIterator::new(minzoom, maxzoom)
830    }
831
832    // def feature(
833    //     self,
834    //     tile: Tile,
835    //     fid: Optional[str] = None,
836    //     props: Optional[Dict] = None,
837    //     buffer: Optional[NumType] = None,
838    //     precision: Optional[int] = None,
839    //     projected: bool = False,
840    // ) -> Dict:
841    //     """
842    //     Get the GeoJSON feature corresponding to a tile.
843    //
844    //     Originally from <https://github.com/mapbox/mercantile/blob/master/mercantile/__init__.py>
845    //
846    //     Parameters
847    //     ----------
848    //     tile : Tile or sequence of int
849    //         May be be either an instance of Tile or 3 ints, X, Y, Z.
850    //     fid : str, optional
851    //         A feature id.
852    //     props : dict, optional
853    //         Optional extra feature properties.
854    //     buffer : float, optional
855    //         Optional buffer distance for the GeoJSON polygon.
856    //     precision: float
857    //         If >= 0, geometry coordinates will be rounded to this number of decimal,
858    //         otherwise original coordinate values will be preserved (default).
859    //     projected : bool, optional
860    //         Return coordinates in TMS projection. Default is false.
861    //
862    //     Returns
863    //     -------
864    //     dict
865    //
866    //     """
867    //     west, south, east, north = self.xy_bounds(tile)
868    //
869    //     if not projected:
870    //         west, south, east, north = self._to_geographic.transform_bounds(
871    //             west, south, east, north, densify_pts=21
872    //         )
873    //
874    //     if buffer:
875    //         west -= buffer
876    //         south -= buffer
877    //         east += buffer
878    //         north += buffer
879    //
880    //     if precision and precision >= 0:
881    //         west, south, east, north = (
882    //             round(v, precision) for v in (west, south, east, north)
883    //         )
884    //
885    //     bbox = [min(west, east), min(south, north), max(west, east), max(south, north)]
886    //     geom = bbox_to_feature(west, south, east, north)
887    //
888    //     xyz = str(tile)
889    //     feat: Dict[str, Any] = {
890    //         "type": "Feature",
891    //         "bbox": bbox,
892    //         "id": xyz,
893    //         "geometry": geom,
894    //         "properties": {
895    //             "title": f"XYZ tile {xyz}",
896    //             "grid_name": self.identifier,
897    //             "grid_crs": self.crs.to_string(),
898    //         },
899    //     }
900    //
901    //     if projected:
902    //         warnings.warn(
903    //             "CRS is no longer part of the GeoJSON specification."
904    //             "Other projection than EPSG:4326 might not be supported.",
905    //             UserWarning,
906    //         )
907    //         feat.update(
908    //             {"crs": {"type": "EPSG", "properties": {"code": self.crs.to_epsg()}}}
909    //         )
910    //
911    //     if props:
912    //         feat["properties"].update(props)
913    //
914    //     if fid is not None:
915    //         feat["id"] = fid
916    //
917    //     return feat
918
919    /// Return TileMatrix Extrema.
920    ///
921    /// # Arguments
922    /// * `zoom` - The zoom level.
923    fn minmax(&self, zoom: u8) -> MinMax {
924        let matrix = self.matrix(zoom);
925        let m = matrix.as_ref();
926        MinMax {
927            x_min: 0,
928            x_max: u64::from(m.matrix_width).saturating_sub(1),
929            y_min: 0,
930            y_max: u64::from(m.matrix_height).saturating_sub(1),
931        }
932    }
933
934    /// Check if a tile is valid.
935    pub fn is_valid(&self, tile: &Xyz) -> bool {
936        if tile.z < self.minzoom() {
937            return false;
938        }
939
940        let extrema = self.minmax(tile.z);
941        let validx = extrema.x_min <= tile.x && tile.x <= extrema.x_max;
942        let validy = extrema.y_min <= tile.y && tile.y <= extrema.y_max;
943
944        validx && validy
945    }
946
947    /// The neighbors of a tile
948    ///
949    /// The neighbors function makes no guarantees regarding neighbor tile
950    /// ordering.
951    ///
952    /// The neighbors function returns up to eight neighboring tiles, where
953    /// tiles will be omitted when they are not valid.
954    ///
955    /// # Arguments
956    /// * `tile` - instance of Tile
957    pub fn neighbors(&self, tile: &Xyz) -> Vec<Xyz> {
958        let extrema = self.minmax(tile.z);
959
960        let mut tiles = Vec::new();
961        for x in tile.x.saturating_sub(1)..=tile.x.saturating_add(1) {
962            for y in tile.y.saturating_sub(1)..=tile.y.saturating_add(1) {
963                if x == tile.x && y == tile.y
964                    || x < extrema.x_min
965                    || y < extrema.y_min
966                    || x > extrema.x_max
967                    || y > extrema.y_max
968                {
969                    continue;
970                }
971
972                tiles.push(Xyz::new(x, y, tile.z));
973            }
974        }
975
976        tiles
977    }
978
979    /// Get the parent of a tile
980    ///
981    /// The parent is the tile of one zoom level lower that contains the
982    /// given "child" tile.
983    ///
984    /// # Arguments
985    /// * `tile` - instance of Tile
986    /// * `zoom` - Determines the *zoom* level of the returned parent tile.
987    ///     This defaults to one lower than the tile (the immediate parent).
988    pub fn parent(&self, tile: &Xyz, zoom: Option<u8> /*  = None */) -> Result<Vec<Xyz>> {
989        if tile.z == self.minzoom() {
990            return Ok(vec![]);
991        }
992
993        if let Some(zoom) = zoom {
994            if tile.z <= zoom {
995                // zoom must be less than that of the input tile
996                return Err(TmsError::InvalidZoom(zoom));
997            }
998        } else if tile.z == 0 {
999            return Err(TmsError::InvalidZoom(0));
1000        }
1001
1002        let target_zoom = match zoom {
1003            Some(zoom) => zoom,
1004            None => tile.z - 1,
1005        };
1006
1007        let res = self.resolution(self.matrix(tile.z).as_ref()) / 10.0;
1008
1009        let bbox = self.xy_bounds(tile);
1010        let ul_tile = self.xy_tile(bbox.left + res, bbox.top - res, target_zoom);
1011        let lr_tile = self.xy_tile(bbox.right - res, bbox.bottom + res, target_zoom);
1012
1013        let mut tiles = Vec::new();
1014        for i in ul_tile.x..=lr_tile.x {
1015            for j in ul_tile.y..=lr_tile.y {
1016                tiles.push(Xyz::new(i, j, target_zoom));
1017            }
1018        }
1019
1020        Ok(tiles)
1021    }
1022
1023    /// Get the children of a tile
1024    ///
1025    /// The children are ordered: top-left, top-right, bottom-right, bottom-left.
1026    ///
1027    /// # Arguments
1028    /// * `tile` - instance of Tile
1029    /// * `zoom` - Determines the *zoom* level of the returned parent tile.
1030    ///     This defaults to one lower than the tile (the immediate parent).
1031    pub fn children(&self, tile: &Xyz, zoom: Option<u8>) -> Result<Vec<Xyz>> {
1032        let mut tiles = Vec::new();
1033
1034        if let Some(zoom) = zoom {
1035            if tile.z > zoom {
1036                // zoom must be greater than that of the input tile
1037                return Err(TmsError::InvalidZoom(zoom));
1038            }
1039        }
1040
1041        let target_zoom = match zoom {
1042            Some(z) => z,
1043            None => tile.z + 1,
1044        };
1045
1046        let bbox = self.xy_bounds(tile);
1047        let res = self.resolution(self.matrix(tile.z).as_ref()) / 10.0;
1048
1049        let ul_tile = self.xy_tile(bbox.left + res, bbox.top - res, target_zoom);
1050        let lr_tile = self.xy_tile(bbox.right - res, bbox.bottom + res, target_zoom);
1051
1052        for i in ul_tile.x..=lr_tile.x {
1053            for j in ul_tile.y..=lr_tile.y {
1054                tiles.push(Xyz::new(i, j, target_zoom));
1055            }
1056        }
1057
1058        Ok(tiles)
1059    }
1060
1061    fn transform_error_to_geographic(&self) -> TmsError {
1062        TransformationUnsupported(self.data_crs.clone(), self.geographic_crs.clone()).into()
1063    }
1064
1065    fn transform_error_from_geographic(&self) -> TmsError {
1066        TransformationUnsupported(self.geographic_crs.clone(), self.data_crs.clone()).into()
1067    }
1068}
1069
1070#[derive(Debug)]
1071pub(crate) struct MinMax {
1072    pub x_min: u64,
1073    pub x_max: u64,
1074    pub y_min: u64,
1075    pub y_max: u64,
1076}
1077
1078impl TryFrom<&TileMatrixSet> for Tms {
1079    type Error = TmsError;
1080    fn try_from(tms: &TileMatrixSet) -> Result<Tms> {
1081        Tms::init(tms)
1082    }
1083}
1084
1085fn transformed_bbox(extent: &[f64], crs: &Crs, extent_crs: Option<&Crs>) -> Result<BoundingBox> {
1086    let (mut left, mut bottom, mut right, mut top) = (extent[0], extent[1], extent[2], extent[3]);
1087    if let Some(extent_crs) = extent_crs {
1088        if extent_crs != crs {
1089            let transform = Transformer::from_crs(extent_crs, crs, true)?;
1090            (left, bottom, right, top) =
1091                transform.transform_bounds(left, bottom, right, top /* Some(21) */)?;
1092        }
1093    }
1094    Ok(BoundingBox::new(left, bottom, right, top))
1095}
1096
1097/// Coefficient to convert the coordinate reference system (CRS)
1098/// units into meters (metersPerUnit).
1099//
1100// See http://docs.ogc.org/is/17-083r4/17-083r4.html#6-1-1-1-%C2%A0-tile-matrix-in-a-two-dimensional-space
1101// From note g in <http://docs.opengeospatial.org/is/17-083r2/17-083r2.html#table_2>:
1102//     If the CRS uses meters as units of measure for the horizontal dimensions,
1103//     then metersPerUnit=1; if it has degrees, then metersPerUnit=2pa/360
1104//     (a is the Earth maximum radius of the ellipsoid).
1105pub fn meters_per_unit(crs: &Crs) -> f64 {
1106    const SEMI_MAJOR_METRE: f64 = 6378137.0; /* crs.ellipsoid.semi_major_metre */
1107    let unit_name = if crs.as_srid() == 4326 {
1108        "degree" // FIXME: crs.axis_info[0].unit_name;
1109    } else {
1110        "metre"
1111    };
1112    match unit_name {
1113        "metre" => 1.0,
1114        "degree" => 2.0 * PI * SEMI_MAJOR_METRE / 360.0,
1115        "foot" => 0.3048,
1116        "US survey foot" => 0.30480060960121924,
1117        _ => panic!(
1118            "CRS {crs:?} with Unit Name `{}` is not supported",
1119            unit_name
1120        ),
1121    }
1122}
1123
1124const LL_EPSILON: f64 = 1e-11;
1125
1126pub const DEFAULT_BBOX_PREC: u8 = 5;
1127
1128/// Check if a point is in a bounding box.
1129pub fn point_in_bbox(point: Coords, bbox: BoundingBox, precision: u8 /* = 5 */) -> Result<()> {
1130    fn round_to_prec(number: f64, precision: u8) -> f64 {
1131        let factor = 10.0_f64.powi(precision as i32);
1132        (number * factor).round() / factor
1133    }
1134    let inside = round_to_prec(point.x, precision) >= round_to_prec(bbox.left, precision)
1135        && round_to_prec(point.x, precision) <= round_to_prec(bbox.right, precision)
1136        && round_to_prec(point.y, precision) >= round_to_prec(bbox.bottom, precision)
1137        && round_to_prec(point.y, precision) <= round_to_prec(bbox.top, precision);
1138    if inside {
1139        Ok(())
1140    } else {
1141        Err(TmsError::PointOutsideBounds(point.x, point.y, bbox))
1142    }
1143}