map_engine/
tiles.rs

1//! Types and helpers to work with XYZ tiles.
2use crate::{
3    affine::GeoTransform, errors::MapEngineError, raster::Raster, windows::Window, MAXZOOMLEVEL,
4    SUPPORTED_FORMATS,
5};
6use gdal::spatial_ref::{CoordTransform, SpatialRef};
7use std::cmp;
8use std::f64::consts::PI;
9
10const RE: f64 = 6378137.0;
11const EPSILON: f64 = 1e-14;
12// const LL_EPSILON: f64 = 1e-11;
13/// Size of the Tile in pixels
14pub const TILE_SIZE: usize = 256;
15
16/// An XYZ web mercator tile
17#[derive(Debug, PartialEq)]
18pub struct Tile {
19    /// Column index
20    pub x: u32,
21    /// Row index
22    pub y: u32,
23    /// Zoom level
24    pub z: u32,
25    /// Image extension
26    ext: Option<String>,
27}
28
29impl Tile {
30    pub fn new(x: u32, y: u32, z: u32) -> Self {
31        Self {
32            x,
33            y,
34            z,
35            ext: Some("png".to_string()),
36        }
37    }
38
39    pub fn set_extension(&mut self, ext: &str) -> Result<(), MapEngineError> {
40        if !SUPPORTED_FORMATS.contains(&ext) {
41            return Err(MapEngineError::Msg(format!(
42                "The extension {:?} is not yet supported",
43                ext
44            )));
45        }
46        self.ext = Some(ext.to_string());
47        Ok(())
48    }
49
50    pub fn to_tuple(&self) -> (u32, u32, u32) {
51        (self.x, self.y, self.z)
52    }
53
54    /// Return the coordinates (lat, long) of the upper-left tile corner
55    pub fn ul(&self) -> (f64, f64) {
56        let (xtile, ytile, zoom) = self.to_tuple();
57        let z2: f64 = 2u32.pow(zoom).into();
58        let lon_deg = (xtile as f64) / z2 * 360.0 - 180.0;
59        let lat_rad = (PI * (1.0 - 2.0 * (ytile as f64) / z2)).sinh().atan();
60        let lat_deg = lat_rad.to_degrees();
61        (lon_deg, lat_deg)
62    }
63
64    /// Return the coordinates (mercator x, y) of the upper-left tile corner
65    pub fn ul_xy(&self) -> (f64, f64) {
66        let (lon_deg, lat_deg) = self.ul();
67        xy(lon_deg, lat_deg)
68    }
69
70    /// Return the bounds (lat, lng) of the tile
71    ///
72    /// The order of the output is (min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg)
73    pub fn bounds(&self) -> (f64, f64, f64, f64) {
74        let (xtile, ytile, zoom) = self.to_tuple();
75        let z2: f64 = 2u32.pow(zoom).into();
76
77        let min_lng_deg = (xtile as f64) / z2 * 360.0 - 180.0;
78        let max_lat_rad = (PI * (1.0 - 2.0 * (ytile as f64) / z2)).sinh().atan();
79        let max_lat_deg = max_lat_rad.to_degrees();
80
81        let max_lng_deg = ((xtile + 1) as f64) / z2 * 360.0 - 180.0;
82        let min_lat_rad = (PI * (1.0 - 2.0 * ((ytile + 1) as f64) / z2)).sinh().atan();
83        let min_lat_deg = min_lat_rad.to_degrees();
84        (min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg)
85    }
86
87    /// Return the bounds (mercator x, y) of the tile
88    ///
89    /// The order of the output is (min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg)
90    pub fn bounds_xy(&self) -> (f64, f64, f64, f64) {
91        let (min_lng_deg, max_lat_deg, max_lng_deg, min_lat_deg) = self.bounds();
92        let (min_x, max_y) = xy(min_lng_deg, max_lat_deg);
93        let (max_x, min_y) = xy(max_lng_deg, min_lat_deg);
94        (min_x, max_y, max_x, min_y)
95    }
96
97    /// Return the vertices of the tile
98    ///
99    /// The order of the vertices is: upper-left, lower-left, lower-right and upper-right
100    pub fn vertices(&self) -> [(f64, f64); 4] {
101        let (min_x, max_y, max_x, min_y) = self.bounds();
102        [
103            (min_x, max_y), // Upper-left
104            (max_x, max_y), // Upper-right
105            (max_x, min_y), // Lower-right
106            (min_x, min_y), // Lower-left
107        ]
108    }
109
110    /// Return a tile from a lower zoom level that contains this tile
111    pub fn zoom_out(&self, zoom: Option<u32>) -> Option<Self> {
112        if self.z == 0 {
113            return None;
114        };
115        let target_zoom = zoom.unwrap_or(self.z - 1);
116        let mut return_tile = Tile::new(self.x, self.y, self.z);
117        while return_tile.z > target_zoom {
118            let (xtile, ytile, ztile) = (return_tile.x, return_tile.y, return_tile.z);
119            let newx = if xtile % 2 == 0 {
120                xtile / 2
121            } else {
122                (xtile - 1) / 2
123            };
124            let newy = if ytile % 2 == 0 {
125                ytile / 2
126            } else {
127                (ytile - 1) / 2
128            };
129            let newz = ztile - 1;
130            return_tile = Tile::new(newx, newy, newz);
131        }
132        Some(return_tile)
133    }
134
135    /// Return the tiles from a higher zoom level contained by this tile
136    pub fn zoom_in(&self, zoom: Option<u32>) -> Option<Vec<Self>> {
137        if self.z == MAXZOOMLEVEL {
138            return None;
139        }
140        let target_zoom = zoom.unwrap_or(self.z + 1);
141        let mut tiles = vec![Tile::new(self.x, self.y, self.z)];
142        while tiles[0].z < target_zoom {
143            tiles = tiles
144                .iter()
145                .map(|tile| {
146                    [
147                        Tile::new(tile.x * 2, tile.y * 2, tile.z + 1),
148                        Tile::new(tile.x * 2 + 1, tile.y * 2, tile.z + 1),
149                        Tile::new(tile.x * 2 + 1, tile.y * 2 + 1, tile.z + 1),
150                        Tile::new(tile.x * 2, tile.y * 2 + 1, tile.z + 1),
151                    ]
152                })
153                .flatten()
154                .collect()
155        }
156        Some(tiles)
157    }
158
159    /// Find the `Tile` intersecting the coordinate at a given zoom level
160    pub fn from_lat_lng(lng: f64, lat: f64, zoom: u32) -> Self {
161        let (x, y) = _xy(lng, lat);
162        let z2 = 2u32.pow(zoom);
163        let xtile = if x <= 0.0 {
164            0u32
165        } else if x >= 1.0 {
166            z2 - 1
167        } else {
168            ((x + EPSILON) * (z2 as f64)).floor() as u32
169        };
170
171        let ytile = if y <= 0.0 {
172            0u32
173        } else if y >= 1.0 {
174            z2 - 1
175        } else {
176            ((y + EPSILON) * (z2 as f64)).floor() as u32
177        };
178
179        Self::new(xtile, ytile, zoom)
180    }
181
182    // pub fn to_window(&self, geo: &GeoTransform) -> Result<Window, MapEngineError> {
183    //     let mercator = GlobalMercator::new(TILE_SIZE);
184    //     let res = geo.geo[0];
185    //     let orig_zoom = mercator.zoom_for_pixel_size(&res);
186    //     let req_overview = 2f64.powf((orig_zoom as f64) - self.z as f64);
187    //     let (lng, lat) = self.ul();
188    //     let (x, y) = xy(lng, lat);
189    //     let (row, col) = geo.rowcol(x + res / 2.0, y - res / 2.0)?;
190    //     let tile_size = TILE_SIZE as f64;
191    //     let out_size = (tile_size * req_overview).ceil() as usize;
192    //     Ok(Window::new(col as isize, row as isize, out_size, out_size))
193    // }
194
195    /// Transform a `Tile` to a `Window`.
196    ///
197    /// # Arguments
198    ///
199    /// * `raster` - `Raster` used for the conversion.
200    pub fn to_window(&self, raster: &Raster) -> Result<(Window, bool), MapEngineError> {
201        let geo = raster.geo();
202        let spatial_ref = raster.spatial_ref()?;
203
204        let src_ref_code = spatial_ref.auth_code().unwrap_or(3857) as u32;
205        let src_spatial_units = spatial_ref
206            .linear_units_name()
207            .unwrap_or_else(|_| "metre".to_string());
208
209        let wgs84_crs = gdal::spatial_ref::SpatialRef::from_epsg(4326)?;
210        let src_crs = SpatialRef::from_epsg(src_ref_code)?;
211
212        let vertex_trans = gdal::spatial_ref::CoordTransform::new(&wgs84_crs, &src_crs)?;
213
214        let vertices = self.vertices();
215        let mut xs = [vertices[0].0, vertices[1].0, vertices[2].0, vertices[3].0];
216
217        let mut ys = [vertices[0].1, vertices[1].1, vertices[2].1, vertices[3].1];
218        let mut zs = [0.0f64; 4];
219
220        // Transform vertices to raster CRS
221        vertex_trans.transform_coords(&mut ys, &mut xs, &mut zs)?;
222
223        let offset = get_vertices_offset(self.z, &src_spatial_units)?;
224
225        let row_cols = get_row_cols(&xs, &ys, &offset, geo, &src_spatial_units);
226
227        let is_skewed = (row_cols[0].0 != row_cols[1].0)
228            || (row_cols[2].0 != row_cols[3].0 || (row_cols[0].1 != row_cols[3].1))
229            || (row_cols[1].1 != row_cols[2].1);
230
231        let win = Window::new(
232            cmp::min(row_cols[0].1, row_cols[3].1) as isize,
233            cmp::min(row_cols[0].0, row_cols[1].0) as isize,
234            cmp::max(row_cols[1].1 - row_cols[0].1, row_cols[2].1 - row_cols[3].1) as usize + 1,
235            cmp::max(row_cols[3].0 - row_cols[0].0, row_cols[2].0 - row_cols[1].0) as usize + 1,
236        );
237
238        Ok((win, is_skewed))
239    }
240}
241
242fn get_row_cols(
243    xs: &[f64],
244    ys: &[f64],
245    offset: &[(f64, f64)],
246    geo: &GeoTransform,
247    src_spatial_units: &str,
248) -> Vec<(i32, i32)> {
249    xs.iter()
250        .zip(ys)
251        .zip(offset)
252        .map(|((x, y), (x_off, y_off))| {
253            if src_spatial_units == "metre" {
254                // TODO: Find out why x,y gets shifted to y,x
255                geo.rowcol(y + y_off, x + x_off).unwrap()
256            } else {
257                geo.rowcol(x + x_off, y + y_off).unwrap()
258            }
259        })
260        .collect()
261}
262
263fn get_vertices_offset(
264    zoom: u32,
265    src_spatial_units: &str,
266) -> Result<[(f64, f64); 4], MapEngineError> {
267    let mercator_crs = SpatialRef::from_epsg(3857)?;
268    let wgs84_crs = gdal::spatial_ref::SpatialRef::from_epsg(4326).unwrap();
269    let tile_res_trans = CoordTransform::new(&mercator_crs, &wgs84_crs).unwrap();
270    let tile_z2: f64 = 2u32.pow(zoom).into();
271    let tile_res = (2.0 * PI * RE / TILE_SIZE as f64) / tile_z2;
272    let tile_res_m = ([tile_res], [tile_res], [0.0]);
273    let mut tile_res_deg = ([tile_res], [tile_res], [0.0]);
274
275    tile_res_trans
276        .transform_coords(
277            &mut tile_res_deg.0,
278            &mut tile_res_deg.1,
279            &mut tile_res_deg.2,
280        )
281        .unwrap();
282
283    // Shift
284    let offset_prop = 0.01;
285    let offset = if src_spatial_units == "metre" {
286        // NOTE: shift tuple
287        [
288            (
289                -tile_res_m.0[0] * offset_prop,
290                tile_res_m.1[0] * offset_prop,
291            ),
292            (
293                -tile_res_m.0[0] * offset_prop,
294                -tile_res_m.1[0] * offset_prop,
295            ),
296            (
297                tile_res_m.0[0] * offset_prop,
298                -tile_res_m.1[0] * offset_prop,
299            ),
300            (tile_res_m.0[0] * offset_prop, tile_res_m.1[0] * offset_prop),
301        ]
302    } else {
303        [
304            (
305                tile_res_deg.1[0] * offset_prop,
306                -tile_res_deg.0[0] * offset_prop,
307            ),
308            (
309                -tile_res_deg.1[0] * offset_prop,
310                -tile_res_deg.0[0] * offset_prop,
311            ),
312            (
313                -tile_res_deg.1[0] * offset_prop,
314                tile_res_deg.0[0] * offset_prop,
315            ),
316            (
317                tile_res_deg.1[0] * offset_prop,
318                tile_res_deg.0[0] * offset_prop,
319            ),
320        ]
321    };
322    Ok(offset)
323}
324
325fn xy(lng: f64, lat: f64) -> (f64, f64) {
326    let x = RE * lng.to_radians();
327    let y = if lat <= -90.0 {
328        std::f64::NEG_INFINITY
329    } else if lat >= 90.0 {
330        std::f64::INFINITY
331    } else {
332        RE * (PI * 0.25 + lat.to_radians() * 0.5).tan().ln()
333    };
334    (x, y)
335}
336
337fn _xy(lng: f64, lat: f64) -> (f64, f64) {
338    let x = lng / 360.0 + 0.5;
339    let sinlat = lat.to_radians().sin();
340    let y = 0.5 - 0.25 * (sinlat + 1.0).ln() / (1.0 - sinlat) / std::f64::consts::PI;
341    (x, y)
342}
343
344// fn tiles(west: f64, south: f64, east: f64, north: f64, zoom: u32) -> impl Iterator<Item = Tile> {
345//     let bboxes = if west > east {
346//         let bbox_west = (-180.0, south, east, north);
347//         let bbox_east = (west, south, 180.0, north);
348//         vec![bbox_west, bbox_east]
349//     } else {
350//         vec![(west, south, east, north)]
351//     };
352
353//     bboxes
354//         .iter()
355//         .map(move |(mut w, mut s, mut e, mut n)| {
356//             w = f64::max(-180.0, w);
357//             s = f64::max(-85.051129, s);
358//             e = f64::min(180.0, e);
359//             n = f64::min(85.051129, n);
360//             let u_tile = tile(w, n, zoom);
361//             let lr_tile = tile(e - LL_EPSILON, s + LL_EPSILON, zoom);
362//             let range_x = u_tile.x..=lr_tile.x;
363//             let range_y = u_tile.y..=lr_tile.y;
364//             iproduct!(range_x, range_y).map(move |(i, j)| Tile::new(i, j, zoom.clone()))
365//         })
366//         .flatten()
367// }
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372
373    #[test]
374    fn test_mercator_xy() {
375        let tile = Tile::new(1, 2, 3);
376        let (lng, lat) = tile.ul();
377        assert_eq!(xy(lng, lat), (-15028131.257091932, 10018754.171394626));
378    }
379}