maps-engine-rust 0.1.0

Zero-dependency map engine core in Rust — Web Mercator projection, tiles, geofencing, clustering, navigation math.
Documentation
//! Slippy-map tile math — XYZ tiles, bounds, quadkeys.

use crate::projection::{from_pixels, to_pixels, TILE_SIZE};

/// A slippy-map tile identifier.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Tile {
    pub x: u32,
    pub y: u32,
    pub z: u8,
}

impl Tile {
    /// Tile containing the given lon/lat at zoom `z`.
    pub fn from_lon_lat(lon: f64, lat: f64, z: u8) -> Self {
        let (px, py) = to_pixels(lon, lat, z);
        let n = 2u32.pow(z as u32);
        let size = TILE_SIZE as f64;
        Tile {
            x: (px / size).floor() as u32 % n,
            y: (py / size).floor() as u32 % n,
            z,
        }
    }

    /// Geographic bounds of the tile as (west, south, east, north).
    pub fn bounds(&self) -> (f64, f64, f64, f64) {
        let size = TILE_SIZE as f64;
        let (w, n) = from_pixels(self.x as f64 * size, self.y as f64 * size, self.z);
        let (e, s) = from_pixels(
            (self.x + 1) as f64 * size,
            (self.y + 1) as f64 * size,
            self.z,
        );
        (w, s, e, n)
    }

    /// Microsoft-style quadkey for the tile.
    pub fn quadkey(&self) -> String {
        let mut key = String::with_capacity(self.z as usize);
        for i in (1..=self.z).rev() {
            let mut digit = 0;
            let mask = 1 << (i - 1);
            if self.x & mask != 0 {
                digit += 1;
            }
            if self.y & mask != 0 {
                digit += 2;
            }
            key.push(char::from_digit(digit, 10).unwrap());
        }
        key
    }

    /// The four direct children at `z + 1`.
    pub fn children(&self) -> [Tile; 4] {
        let z = self.z + 1;
        let (x, y) = (self.x * 2, self.y * 2);
        [
            Tile { x, y, z },
            Tile { x: x + 1, y, z },
            Tile { x, y: y + 1, z },
            Tile {
                x: x + 1,
                y: y + 1,
                z,
            },
        ]
    }

    /// Parent tile, or `None` at zoom 0.
    pub fn parent(&self) -> Option<Tile> {
        if self.z == 0 {
            None
        } else {
            Some(Tile {
                x: self.x / 2,
                y: self.y / 2,
                z: self.z - 1,
            })
        }
    }
}

/// All tiles intersecting a lon/lat bounding box at zoom `z`.
/// Bounds: (west, south, east, north).
pub fn tiles_in_bounds(west: f64, south: f64, east: f64, north: f64, z: u8) -> Vec<Tile> {
    let nw = Tile::from_lon_lat(west, north, z);
    let se = Tile::from_lon_lat(east, south, z);
    let mut out = Vec::new();
    for x in nw.x..=se.x {
        for y in nw.y..=se.y {
            out.push(Tile { x, y, z });
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hanoi_tile_at_z14() {
        let t = Tile::from_lon_lat(105.85, 21.02, 14);
        assert_eq!(t.z, 14);
        let (w, s, e, n) = t.bounds();
        assert!(w <= 105.85 && 105.85 <= e);
        assert!(s <= 21.02 && 21.02 <= n);
    }

    #[test]
    fn quadkey_roundtrip_shape() {
        let t = Tile { x: 3, y: 5, z: 3 };
        assert_eq!(t.quadkey().len(), 3);
        assert_eq!(t.quadkey(), "213");
    }

    #[test]
    fn parent_children_inverse() {
        let t = Tile::from_lon_lat(105.85, 21.02, 10);
        let kids = t.children();
        assert_eq!(kids.len(), 4);
        for k in kids {
            assert_eq!(k.parent(), Some(t));
        }
        assert_eq!(Tile { x: 0, y: 0, z: 0 }.parent(), None);
    }

    #[test]
    fn bounds_cover_expected_tiles() {
        let tiles = tiles_in_bounds(105.8, 21.0, 105.9, 21.05, 14);
        assert!(!tiles.is_empty());
        assert!(tiles.len() < 100);
    }
}