Skip to main content

maps_engine_rust/
tile.rs

1//! Slippy-map tile math — XYZ tiles, bounds, quadkeys.
2
3use crate::projection::{from_pixels, to_pixels, TILE_SIZE};
4
5/// A slippy-map tile identifier.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub struct Tile {
8    pub x: u32,
9    pub y: u32,
10    pub z: u8,
11}
12
13impl Tile {
14    /// Tile containing the given lon/lat at zoom `z`.
15    pub fn from_lon_lat(lon: f64, lat: f64, z: u8) -> Self {
16        let (px, py) = to_pixels(lon, lat, z);
17        let n = 2u32.pow(z as u32);
18        let size = TILE_SIZE as f64;
19        Tile {
20            x: (px / size).floor() as u32 % n,
21            y: (py / size).floor() as u32 % n,
22            z,
23        }
24    }
25
26    /// Geographic bounds of the tile as (west, south, east, north).
27    pub fn bounds(&self) -> (f64, f64, f64, f64) {
28        let size = TILE_SIZE as f64;
29        let (w, n) = from_pixels(self.x as f64 * size, self.y as f64 * size, self.z);
30        let (e, s) = from_pixels(
31            (self.x + 1) as f64 * size,
32            (self.y + 1) as f64 * size,
33            self.z,
34        );
35        (w, s, e, n)
36    }
37
38    /// Microsoft-style quadkey for the tile.
39    pub fn quadkey(&self) -> String {
40        let mut key = String::with_capacity(self.z as usize);
41        for i in (1..=self.z).rev() {
42            let mut digit = 0;
43            let mask = 1 << (i - 1);
44            if self.x & mask != 0 {
45                digit += 1;
46            }
47            if self.y & mask != 0 {
48                digit += 2;
49            }
50            key.push(char::from_digit(digit, 10).unwrap());
51        }
52        key
53    }
54
55    /// The four direct children at `z + 1`.
56    pub fn children(&self) -> [Tile; 4] {
57        let z = self.z + 1;
58        let (x, y) = (self.x * 2, self.y * 2);
59        [
60            Tile { x, y, z },
61            Tile { x: x + 1, y, z },
62            Tile { x, y: y + 1, z },
63            Tile {
64                x: x + 1,
65                y: y + 1,
66                z,
67            },
68        ]
69    }
70
71    /// Parent tile, or `None` at zoom 0.
72    pub fn parent(&self) -> Option<Tile> {
73        if self.z == 0 {
74            None
75        } else {
76            Some(Tile {
77                x: self.x / 2,
78                y: self.y / 2,
79                z: self.z - 1,
80            })
81        }
82    }
83}
84
85/// All tiles intersecting a lon/lat bounding box at zoom `z`.
86/// Bounds: (west, south, east, north).
87pub fn tiles_in_bounds(west: f64, south: f64, east: f64, north: f64, z: u8) -> Vec<Tile> {
88    let nw = Tile::from_lon_lat(west, north, z);
89    let se = Tile::from_lon_lat(east, south, z);
90    let mut out = Vec::new();
91    for x in nw.x..=se.x {
92        for y in nw.y..=se.y {
93            out.push(Tile { x, y, z });
94        }
95    }
96    out
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn hanoi_tile_at_z14() {
105        let t = Tile::from_lon_lat(105.85, 21.02, 14);
106        assert_eq!(t.z, 14);
107        let (w, s, e, n) = t.bounds();
108        assert!(w <= 105.85 && 105.85 <= e);
109        assert!(s <= 21.02 && 21.02 <= n);
110    }
111
112    #[test]
113    fn quadkey_roundtrip_shape() {
114        let t = Tile { x: 3, y: 5, z: 3 };
115        assert_eq!(t.quadkey().len(), 3);
116        assert_eq!(t.quadkey(), "213");
117    }
118
119    #[test]
120    fn parent_children_inverse() {
121        let t = Tile::from_lon_lat(105.85, 21.02, 10);
122        let kids = t.children();
123        assert_eq!(kids.len(), 4);
124        for k in kids {
125            assert_eq!(k.parent(), Some(t));
126        }
127        assert_eq!(Tile { x: 0, y: 0, z: 0 }.parent(), None);
128    }
129
130    #[test]
131    fn bounds_cover_expected_tiles() {
132        let tiles = tiles_in_bounds(105.8, 21.0, 105.9, 21.05, 14);
133        assert!(!tiles.is_empty());
134        assert!(tiles.len() < 100);
135    }
136}