utiles_core/
fns.rs

1//! Core util(e)ity functions for working with web mercator tiles, bounding boxes, et al
2#![deny(clippy::missing_const_for_fn)]
3use std::f64::consts::PI;
4use std::num::FpCategory;
5
6use crate::bbox::{BBox, WebBBox};
7use crate::constants::{DEG2RAD, EARTH_CIRCUMFERENCE, EARTH_RADIUS, LL_EPSILON};
8use crate::errors::UtilesCoreResult;
9use crate::point2d;
10use crate::sibling_relationship::SiblingRelationship;
11use crate::tile_zbox::{TileZBox, TileZBoxes};
12use crate::utile;
13use crate::zoom::ZoomOrZooms;
14use crate::Point2d;
15use crate::{LngLat, Tile, UtilesCoreError};
16
17/// Return upper left lnglat as tuple `(f64, f64)` from x,y,z
18#[must_use]
19pub fn ult(x: u32, y: u32, z: u8) -> (f64, f64) {
20    let z2 = f64::from(2_u32.pow(u32::from(z)));
21    let lon_deg = (f64::from(x) / z2) * 360.0 - 180.0;
22    let lat_rad = ((1.0 - 2.0 * f64::from(y) / z2) * PI).sinh().atan();
23    (lon_deg, lat_rad.to_degrees())
24}
25
26/// Return upper left lnglat as `LngLat` from x,y,z
27#[must_use]
28pub fn ul(x: u32, y: u32, z: u8) -> LngLat {
29    let (lon_deg, lat_deg) = ult(x, y, z);
30    LngLat::new(lon_deg, lat_deg)
31}
32
33/// Return lower left lnglat as `LngLat` from x,y,z
34#[must_use]
35pub fn ll(x: u32, y: u32, z: u8) -> LngLat {
36    ul(x, y + 1, z)
37}
38
39/// Return upper right lnglat as `LngLat` from x,y,z
40#[must_use]
41pub fn ur(x: u32, y: u32, z: u8) -> LngLat {
42    ul(x + 1, y, z)
43}
44
45/// Return lower right lnglat as `LngLat` from x,y,z
46#[must_use]
47pub fn lr(x: u32, y: u32, z: u8) -> LngLat {
48    ul(x + 1, y + 1, z)
49}
50
51/// Return tuple (min, max) x/y for a zoom-level
52#[must_use]
53pub fn minmax(zoom: u8) -> (u32, u32) {
54    (0, 2_u32.pow(u32::from(zoom)) - 1)
55}
56
57/// Return true if x, y, z is a valid tile coordinate
58#[must_use]
59pub fn valid(x: u32, y: u32, z: u8) -> bool {
60    let (minx, maxx) = minmax(z);
61    let (miny, maxy) = minmax(z);
62    x >= minx && x <= maxx && y >= miny && y <= maxy
63}
64
65/// Return the inverted/y-flipped y coordinate for a given y and z
66#[must_use]
67#[inline]
68pub fn flipy(y: u32, z: u8) -> u32 {
69    2_u32.pow(u32::from(z)) - 1 - y
70}
71
72/// Return the y-flipped y coordinate for a given y and z
73#[must_use]
74#[inline]
75pub fn yflip(y: u32, z: u8) -> u32 {
76    flipy(y, z)
77}
78
79/// Return cumulative base-tile-id for a tile and the zoom level of the tile
80///
81/// Base-tile-id is the sum of all tiles in all zoom levels below the zoom level
82/// of the tile.
83#[must_use]
84#[inline]
85pub const fn int_2_offset_zoom(i: u64) -> (u64, u8) {
86    if i == 0 {
87        return (0, 0);
88    }
89    let mut acc: u64 = 0;
90    let mut z: u8 = 0;
91    loop {
92        let num_tiles: u64 = (1 << z) * (1 << z);
93        if acc + num_tiles > i {
94            return (i - acc, z);
95        }
96        acc += num_tiles;
97        z += 1;
98    }
99}
100
101/// Calculate the row-major-id for a tile which is the
102/// index of the tile for the zoom level PLUS the total number of tiles
103/// in all zoom levels below it for the zoom level.
104///
105/// (x, y, z) => x + y * 2^z + 1 + 2^z * 2^z
106/// (0,0,0) is 0
107/// (0,0,1) is 1
108/// (0,1,1) is 2
109///
110/// # Examples
111/// ```
112/// use utiles_core::xyz2rmid;
113/// let zzz = xyz2rmid(0, 0, 0);
114/// assert_eq!(zzz, 0);
115/// ```
116///
117/// ```
118/// use utiles_core::xyz2rmid;
119/// let xyz_0_0_1 = xyz2rmid(0, 0, 1);
120/// assert_eq!(xyz_0_0_1, 1);
121/// ```
122///
123/// ```
124/// use utiles_core::xyz2rmid;
125/// let xyz_0_1_1 = xyz2rmid(1, 0, 1);
126/// assert_eq!(xyz_0_1_1, 2);
127/// ```
128///
129/// ```
130/// use utiles_core::xyz2rmid;
131/// let xyz_1_0_1 = xyz2rmid(0, 1, 1);
132/// assert_eq!(xyz_1_0_1, 3);
133/// ```
134///
135/// ```
136/// use utiles_core::xyz2rmid;
137/// let xyz_1_1_1 = xyz2rmid(1, 1, 1);
138/// assert_eq!(xyz_1_1_1, 4);
139/// ```
140///
141/// ```
142/// use utiles_core::xyz2rmid;
143/// let one_two_three = xyz2rmid(1, 2, 3);
144/// assert_eq!(one_two_three, 38);
145/// ```
146///
147/// ```
148/// use utiles_core::xyz2rmid;
149/// let last_tile_in_z12 = xyz2rmid(4095, 4095, 12);
150/// assert_eq!(last_tile_in_z12, 22369621 - 1); // total tiles thru z12 - 1
151/// ```
152#[must_use]
153pub fn xyz2rmid(x: u32, y: u32, z: u8) -> u64 {
154    if z == 0 {
155        return u64::from(x) + u64::from(y) * 2u64.pow(u32::from(z));
156    }
157    let base_id: u64 = (4u64.pow(u32::from(z)) - 1) / 3;
158    base_id + u64::from(x) + u64::from(y) * 2u64.pow(u32::from(z))
159}
160
161/// Calculate the xyz of the tile from a row-major-id
162///
163/// # Examples
164/// ```
165/// use utiles_core::rmid2xyz;
166/// let zzz = rmid2xyz(0);
167/// assert_eq!(zzz, (0, 0, 0));
168/// ```
169///
170/// ```
171/// use utiles_core::rmid2xyz;
172/// let xyz_0_0_1 = rmid2xyz(1);
173/// assert_eq!(xyz_0_0_1, (0, 0, 1));
174/// ```
175///
176/// ```
177/// use utiles_core::rmid2xyz;
178/// let xyz_0_1_1 = rmid2xyz(2);
179/// assert_eq!(xyz_0_1_1, (1, 0, 1));
180/// ```
181///
182/// ```
183/// use utiles_core::rmid2xyz;
184/// let xyz_1_0_1 = rmid2xyz(3);
185/// assert_eq!(xyz_1_0_1, (0, 1, 1));
186/// ```
187///
188/// ```
189/// use utiles_core::rmid2xyz;
190/// let xyz_1_1_1 = rmid2xyz(4);
191/// assert_eq!(xyz_1_1_1, (1, 1, 1));
192/// ```
193///
194/// ```
195/// use utiles_core::rmid2xyz;
196/// let one_two_three = rmid2xyz(38);
197/// assert_eq!(one_two_three, (1, 2, 3));
198/// ```
199///
200/// ```
201/// use utiles_core::rmid2xyz;
202/// let last_tile_in_z12 = rmid2xyz(22369621 - 1); // total tiles thru z12 - 1
203/// assert_eq!(last_tile_in_z12, (4095, 4095, 12));
204/// ```
205#[must_use]
206#[allow(clippy::cast_possible_truncation)]
207pub fn rmid2xyz(i: u64) -> (u32, u32, u8) {
208    if i == 0 {
209        return (0, 0, 0);
210    }
211    let (i_o, z) = int_2_offset_zoom(i);
212    let pow_z = 2u64.pow(u32::from(z));
213    let x = i_o % pow_z;
214    let y = i_o / pow_z;
215    (x as u32, y as u32, z)
216}
217
218/// Calculate the zoom level for the bounding-tile of a bbox
219#[must_use]
220pub fn bbox2zoom(bbox: (u32, u32, u32, u32)) -> u8 {
221    let max_zoom = 28;
222    let (west, south, east, north) = bbox;
223    for z in 0..max_zoom {
224        let mask = 1 << (32 - (z + 1));
225        if (west & mask) != (east & mask) || (south & mask) != (north & mask) {
226            return z;
227        }
228    }
229    max_zoom
230}
231
232/// Return the bbox tuple given x, y, z.
233#[must_use]
234pub fn bounds(x: u32, y: u32, z: u8) -> (f64, f64, f64, f64) {
235    let ul_corner = ul(x, y, z);
236    let lr_corner = ul(x + 1, y + 1, z);
237    (
238        ul_corner.lng(),
239        lr_corner.lat(),
240        lr_corner.lng(),
241        ul_corner.lat(),
242    )
243}
244
245/// Truncate a longitude to the valid range of -180 to 180.
246#[must_use]
247pub fn truncate_lng(lng: f64) -> f64 {
248    lng.clamp(-180.0, 180.0)
249}
250
251/// Truncate a latitude to the valid range of -90 to 90.
252#[must_use]
253pub fn truncate_lat(lat: f64) -> f64 {
254    lat.clamp(-90.0, 90.0)
255}
256
257/// Truncate a `LngLat` to valid range of longitude and latitude.
258#[must_use]
259pub fn truncate_lnglat(lnglat: &LngLat) -> LngLat {
260    LngLat::new(truncate_lng(lnglat.lng()), truncate_lat(lnglat.lat()))
261}
262
263/// Return the parent tile of a tile given x, y, z, and n (number of ancestors).
264#[must_use]
265pub fn parent(x: u32, y: u32, z: u8, n: Option<u8>) -> Option<Tile> {
266    let n = n.unwrap_or(0);
267    if n == 0 {
268        if z == 0 {
269            None
270        } else {
271            Some(utile!(x >> 1, y >> 1, z - 1))
272        }
273    } else {
274        parent(x >> 1, y >> 1, z - 1, Some(n - 1))
275    }
276}
277/// Return the 4 direct children of a tile
278#[must_use]
279pub fn children1_zorder(x: u32, y: u32, z: u8) -> [Tile; 4] {
280    [
281        utile!(x * 2, y * 2, z + 1),         // top-left
282        utile!(x * 2 + 1, y * 2, z + 1),     // top-right
283        utile!(x * 2, y * 2 + 1, z + 1),     // bottom-left
284        utile!(x * 2 + 1, y * 2 + 1, z + 1), // bottom-right
285    ]
286}
287/// Return the children of a tile given x, y, z, and zoom in z-order.
288///
289/// # Examples
290/// ```
291/// use utiles_core::{children_zorder, utile, Tile};
292/// let children = children_zorder(0, 0, 0, Some(1));
293/// assert_eq!(children.len(), 4);
294/// assert_eq!(children, vec![
295///     utile!(0, 0, 1),
296///     utile!(1, 0, 1),
297///     utile!(0, 1, 1),
298///     utile!(1, 1, 1),
299/// ]);
300/// ```
301///
302/// ```
303/// use utiles_core::{children_zorder, utile, Tile};
304/// let children = children_zorder(0, 0, 0, Some(2));
305/// assert_eq!(children.len(), 16);
306/// let expected = [
307///     utile!(0,0,2),
308///     utile!(1,0,2),
309///     utile!(0,1,2),
310///     utile!(1,1,2),
311///     utile!(2,0,2),
312///     utile!(3,0,2),
313///     utile!(2,1,2),
314///     utile!(3,1,2),
315///     utile!(0,2,2),
316///     utile!(1,2,2),
317///     utile!(0,3,2),
318///     utile!(1,3,2),
319///     utile!(2,2,2),
320///     utile!(3,2,2),
321///     utile!(2,3,2),
322///     utile!(3,3,2),
323/// ];
324/// assert_eq!(children, expected);
325/// ```
326///
327#[must_use]
328pub fn children_zorder(x: u32, y: u32, z: u8, zoom: Option<u8>) -> Vec<Tile> {
329    let zoom = zoom.unwrap_or(z + 1);
330    let tile = utile!(x, y, z);
331    let mut tiles = vec![tile];
332    while tiles[0].z < zoom {
333        let (xtile, ytile, ztile) = (tiles[0].x, tiles[0].y, tiles[0].z);
334        tiles.append(&mut vec![
335            utile!(xtile * 2, ytile * 2, ztile + 1), // top-left
336            utile!(xtile * 2 + 1, ytile * 2, ztile + 1), // top-right
337            utile!(xtile * 2, ytile * 2 + 1, ztile + 1), // bottom-left
338            utile!(xtile * 2 + 1, ytile * 2 + 1, ztile + 1), // bottom-right
339        ]);
340        tiles.remove(0);
341    }
342    tiles
343}
344
345/// Return the children of a tile given x, y, z, and zoom; returns children
346/// in stupid `a, b, d, c` orderl; but this is the mercantile way... and I
347/// am not gonna fix it right now
348///
349/// # Examples
350/// ```
351/// use utiles_core::{children, utile, Tile};
352/// let children = children(0, 0, 0, Some(1));
353/// assert_eq!(children.len(), 4);
354/// assert_eq!(children, vec![
355///     utile!(0, 0, 1),
356///     utile!(1, 0, 1),
357///     utile!(1, 1, 1),
358///     utile!(0, 1, 1),
359/// ]);
360/// ```
361#[must_use]
362pub fn children(x: u32, y: u32, z: u8, zoom: Option<u8>) -> Vec<Tile> {
363    let zoom = zoom.unwrap_or(z + 1);
364    let tile = utile!(x, y, z);
365    let mut tiles = vec![tile];
366    while tiles[0].z < zoom {
367        let (xtile, ytile, ztile) = (tiles[0].x, tiles[0].y, tiles[0].z);
368        tiles.append(&mut vec![
369            utile!(xtile * 2, ytile * 2, ztile + 1), // top-left
370            utile!(xtile * 2 + 1, ytile * 2, ztile + 1), // top-right
371            utile!(xtile * 2 + 1, ytile * 2 + 1, ztile + 1), // bottom-right
372            utile!(xtile * 2, ytile * 2 + 1, ztile + 1), // bottom-left
373        ]);
374        tiles.remove(0);
375    }
376    tiles
377}
378
379/// Return the siblings of a tile given x, y, z
380///
381/// Siblings are tiles that share the same parent, NOT neighbors.
382#[must_use]
383pub fn siblings(x: u32, y: u32, z: u8) -> Vec<Tile> {
384    let sibrel = SiblingRelationship::from((x, y));
385    match sibrel {
386        SiblingRelationship::UpperLeft => vec![
387            utile!(x + 1, y, z),
388            utile!(x, y + 1, z),
389            utile!(x + 1, y + 1, z),
390        ],
391        SiblingRelationship::UpperRight => vec![
392            utile!(x - 1, y, z),
393            utile!(x, y + 1, z),
394            utile!(x - 1, y + 1, z),
395        ],
396        SiblingRelationship::LowerLeft => vec![
397            utile!(x + 1, y, z),
398            utile!(x, y - 1, z),
399            utile!(x + 1, y - 1, z),
400        ],
401        SiblingRelationship::LowerRight => vec![
402            utile!(x - 1, y, z),
403            utile!(x, y - 1, z),
404            utile!(x - 1, y - 1, z),
405        ],
406    }
407}
408
409/// Truncate a bounding box to the valid range of longitude and latitude.
410#[must_use]
411pub fn bbox_truncate(
412    west: f64,
413    south: f64,
414    east: f64,
415    north: f64,
416    truncate: Option<bool>,
417) -> (f64, f64, f64, f64) {
418    let trunc = truncate.unwrap_or(false);
419    let mut west = west;
420    let mut east = east;
421    let mut south = south;
422    let mut north = north;
423    if trunc {
424        if west < -180.0 {
425            west = -180.0;
426        }
427        if east > 180.0 {
428            east = 180.0;
429        }
430        if south < -90.0 {
431            south = -90.0;
432        }
433        if north > 90.0 {
434            north = 90.0;
435        }
436    }
437    (west, south, east, north)
438}
439
440/// Convert lng lat to web mercator x and y
441///
442/// # Errors
443///
444/// Returns error if y can not be computed.
445pub fn _xy(lng: f64, lat: f64, truncate: Option<bool>) -> UtilesCoreResult<(f64, f64)> {
446    let (lng, lat) = if truncate.unwrap_or(false) {
447        (truncate_lng(lng), truncate_lat(lat))
448    } else {
449        (lng, lat)
450    };
451    let sinlat = lat.to_radians().sin();
452    let yish = (1.0 + sinlat) / (1.0 - sinlat);
453    match yish.classify() {
454        FpCategory::Infinite | FpCategory::Nan => {
455            Err(UtilesCoreError::LngLat2WebMercator(
456                "Y can not be computed: lat={lat}".to_string(),
457            ))
458        }
459        _ => {
460            let y = 0.5 - 0.25 * (yish.ln()) / PI;
461            let x = lng / 360.0 + 0.5;
462            Ok((x, y))
463        }
464    }
465}
466
467/// Convert lng lat to web mercator x and y
468#[must_use]
469pub fn lnglat2webmercator(lng: f64, lat: f64) -> (f64, f64) {
470    let x = EARTH_RADIUS * lng.to_radians();
471    let y = if (lat - 90.0).abs() < f64::EPSILON {
472        f64::INFINITY
473    } else if (lat + 90.0).abs() < f64::EPSILON {
474        f64::NEG_INFINITY
475    } else {
476        EARTH_RADIUS * (PI * 0.25 + 0.5 * lat.to_radians()).tan().ln()
477    };
478    (x, y)
479}
480
481/// Convert web mercator x and y to longitude and latitude.
482///
483/// # Examples
484/// ```
485/// use utiles_core::webmercator2lnglat;
486/// let (lng, lat) = webmercator2lnglat(0.5, 0.5);
487/// assert!((lng - 0.0).abs() < 0.0001, "lng: {}", lng);
488/// assert!((lat - 0.0).abs() < 0.0001, "lat: {}", lat);
489/// ```
490///
491#[must_use]
492#[inline]
493pub fn webmercator2lnglat(x: f64, y: f64) -> (f64, f64) {
494    let lng = x / EARTH_RADIUS * 180.0 / PI;
495    let lat = (2.0 * (y / EARTH_RADIUS).exp().atan() - PI * 0.5) * 180.0 / PI;
496    (lng, lat)
497}
498
499/// Convert longitude and latitude to web mercator x and y with optional truncation.
500///
501/// Name "xy" comes from mercantile python library.
502#[must_use]
503pub fn xy(lng: f64, lat: f64, truncate: Option<bool>) -> (f64, f64) {
504    let (lng, lat) = if truncate.unwrap_or(false) {
505        (truncate_lng(lng), truncate_lat(lat))
506    } else {
507        (lng, lat)
508    };
509    lnglat2webmercator(lng, lat)
510}
511
512/// Convert web mercator x and y to longitude and latitude with optional truncation.
513#[must_use]
514pub fn lnglat(x: f64, y: f64, truncate: Option<bool>) -> LngLat {
515    let (lng, lat) = webmercator2lnglat(x, y);
516    if truncate.is_some() {
517        truncate_lnglat(&LngLat::new(lng, lat))
518    } else {
519        LngLat::new(lng, lat)
520    }
521}
522
523enum TileEdgeInfo {
524    Bottom,
525    BottomLeft,
526    BottomRight,
527    Left,
528    Right,
529    Top,
530    TopLeft,
531    TopRight,
532    Middle,
533}
534
535fn tile_edge_info(x: u32, y: u32, z: u8) -> TileEdgeInfo {
536    if x == 0 && y == 0 {
537        return TileEdgeInfo::TopLeft;
538    }
539    let max_xy = 2u32.pow(u32::from(z));
540    if x == max_xy && y == max_xy {
541        return TileEdgeInfo::BottomRight;
542    }
543    match (x, y) {
544        (max, 0) if max == max_xy => TileEdgeInfo::TopRight,
545        (0, max) if max == max_xy => TileEdgeInfo::BottomLeft,
546        (0, _) => TileEdgeInfo::Left,
547        (max, _) if max == max_xy => TileEdgeInfo::Right,
548        (_, 0) => TileEdgeInfo::Top,
549        (_, max) if max == max_xy => TileEdgeInfo::Bottom,
550        _ => TileEdgeInfo::Middle,
551    }
552}
553
554fn neighbors_middle_tile(x: u32, y: u32, z: u8) -> Vec<Tile> {
555    vec![
556        utile!(x + 1, y, z),
557        utile!(x, y + 1, z),
558        utile!(x + 1, y + 1, z),
559        utile!(x - 1, y, z),
560        utile!(x, y - 1, z),
561        utile!(x - 1, y - 1, z),
562        utile!(x + 1, y - 1, z),
563        utile!(x - 1, y + 1, z),
564    ]
565}
566
567/// Return neighbors of a tile (non-wrapping).
568#[must_use]
569pub fn neighbors(x: u32, y: u32, z: u8) -> Vec<Tile> {
570    if z == 0 {
571        return Vec::new();
572    }
573    let edge_info = tile_edge_info(x, y, z);
574    match edge_info {
575        TileEdgeInfo::Middle => neighbors_middle_tile(x, y, z),
576        TileEdgeInfo::TopLeft => vec![
577            utile!(x + 1, y, z),
578            utile!(x, y + 1, z),
579            utile!(x + 1, y + 1, z),
580        ],
581        TileEdgeInfo::TopRight => vec![
582            utile!(x - 1, y, z),
583            utile!(x, y + 1, z),
584            utile!(x - 1, y + 1, z),
585        ],
586        TileEdgeInfo::BottomLeft => vec![
587            utile!(x + 1, y, z),
588            utile!(x, y - 1, z),
589            utile!(x + 1, y - 1, z),
590        ],
591        TileEdgeInfo::BottomRight => vec![
592            utile!(x - 1, y, z),
593            utile!(x, y - 1, z),
594            utile!(x - 1, y - 1, z),
595        ],
596        TileEdgeInfo::Left => vec![
597            utile!(x + 1, y, z),
598            utile!(x, y + 1, z),
599            utile!(x + 1, y + 1, z),
600            utile!(x, y - 1, z),
601            utile!(x + 1, y - 1, z),
602        ],
603        TileEdgeInfo::Right => vec![
604            utile!(x - 1, y, z),
605            utile!(x, y + 1, z),
606            utile!(x - 1, y + 1, z),
607            utile!(x, y - 1, z),
608            utile!(x - 1, y - 1, z),
609        ],
610        TileEdgeInfo::Top => vec![
611            utile!(x + 1, y, z),
612            utile!(x, y + 1, z),
613            utile!(x + 1, y + 1, z),
614            utile!(x - 1, y, z),
615            utile!(x - 1, y + 1, z),
616        ],
617        TileEdgeInfo::Bottom => vec![
618            utile!(x + 1, y, z),
619            utile!(x, y - 1, z),
620            utile!(x + 1, y - 1, z),
621            utile!(x - 1, y, z),
622            utile!(x - 1, y - 1, z),
623        ],
624    }
625}
626
627/// Return Tile struct from longitude, latitude, and zoom.
628///
629/// # Errors
630///
631/// Returns error if the lnglat can not be converted to web mercator.
632pub fn tile(
633    lng: f64,
634    lat: f64,
635    zoom: u8,
636    truncate: Option<bool>,
637) -> Result<Tile, UtilesCoreError> {
638    Tile::from_lnglat_zoom(lng, lat, zoom, truncate)
639}
640
641/// Converts longitude, latitude, and zoom level to fractional tile coordinates.
642///
643/// # Examples
644/// ```
645/// use utiles_core::lnglat2tile_frac;
646/// let (xf, yf, z) =lnglat2tile_frac(-95.939_655_303_955_08, 41.260_001_085_686_97, 9);
647/// assert!((xf - 119.552_490_234_375).abs() < 0.0001, "xf: {}", xf);
648/// assert!((yf - 191.471_191_406_25).abs() < 0.0001, "yf: {}", yf);
649/// assert!(z == 9);
650/// ```
651#[must_use]
652pub fn lnglat2tile_frac(lng: f64, lat: f64, z: u8) -> (f64, f64, u8) {
653    let sin = (lat * DEG2RAD).sin();
654    let z2 = 2f64.powi(i32::from(z));
655    let mut x = z2 * (lng / 360.0 + 0.5);
656    let y = z2 * (0.5 - (0.25 * ((1.0 + sin) / (1.0 - sin)).ln()) / PI);
657
658    // Wrap Tile X using rem_euclid
659    x = x.rem_euclid(z2);
660
661    (x, y, z)
662}
663
664/// Return the bounding tile for a bounding box.
665///
666/// # Errors
667///
668/// Returns error if the bounding tile can not be calculated for points on the bbox
669pub fn bounding_tile(
670    bbox: BBox,
671    truncate: Option<bool>,
672) -> Result<Tile, UtilesCoreError> {
673    let (west, south, east, north) =
674        bbox_truncate(bbox.west, bbox.south, bbox.east, bbox.north, truncate);
675    let tmin = tile(west, north, 32, truncate)?;
676    let tmax = tile(east - LL_EPSILON, south + LL_EPSILON, 32, truncate)?;
677
678    let cell = (tmin.x, tmin.y, tmax.x, tmax.y);
679    let z = bbox2zoom(cell);
680    if z == 0 {
681        Ok(utile!(0, 0, 0))
682    } else {
683        let x = cell.0 >> (32 - z);
684        let y = cell.1 >> (32 - z);
685        Ok(utile!(x, y, z))
686    }
687}
688
689/// Return web-mercator bbox from x, y, z.
690#[must_use]
691pub fn xyz2bbox(x: u32, y: u32, z: u8) -> WebBBox {
692    let tile_size = EARTH_CIRCUMFERENCE / 2.0_f64.powi(i32::from(z));
693    let left = f64::from(x) * tile_size - EARTH_CIRCUMFERENCE / 2.0;
694    let top = EARTH_CIRCUMFERENCE / 2.0 - f64::from(y) * tile_size;
695
696    WebBBox::new(left, top - tile_size, left + tile_size, top)
697}
698
699/// Return zooms-vec from a `ZoomOrZooms` enum
700#[must_use]
701pub fn as_zooms(zoom_or_zooms: ZoomOrZooms) -> Vec<u8> {
702    match zoom_or_zooms {
703        ZoomOrZooms::Zoom(zoom) => {
704            vec![zoom]
705        }
706        ZoomOrZooms::Zooms(zooms) => zooms,
707    }
708}
709
710fn tiles_range_zoom(
711    minx: u32,
712    maxx: u32,
713    miny: u32,
714    maxy: u32,
715    zoom: u8,
716) -> impl Iterator<Item = (u32, u32, u8)> {
717    (minx..=maxx).flat_map(move |i| (miny..=maxy).map(move |j| (i, j, zoom)))
718}
719
720/// Return `TileRanges` from a bounding box and zoom(s).
721///
722/// # Errors
723///
724/// Returns error tiles cannot be calculated due to invalid bbox/zbox
725pub fn tile_ranges(
726    bounds: (f64, f64, f64, f64),
727    zooms: ZoomOrZooms,
728) -> Result<TileZBoxes, UtilesCoreError> {
729    let zooms = as_zooms(zooms);
730    let bboxes: Vec<BBox> = BBox::from(bounds)
731        .bboxes()
732        .into_iter()
733        .map(|bbox| {
734            // clip to web mercator extent
735            BBox {
736                north: bbox.north.min(85.051_129),
737                south: bbox.south.max(-85.051_129),
738                east: bbox.east.min(180.0),
739                west: bbox.west.max(-180.0),
740            }
741        })
742        .collect();
743    let ranges: Vec<TileZBox> = bboxes
744        .into_iter()
745        .flat_map(move |bbox| {
746            let zooms = zooms.clone();
747            zooms.into_iter().map(move |zoom| {
748                let upper_left_lnglat = LngLat {
749                    xy: point2d! { x: bbox.west, y: bbox.north },
750                };
751                let lower_right_lnglat = LngLat {
752                    xy: point2d! { x: bbox.east, y: bbox.south },
753                };
754                let top_left_tile = Tile::from_lnglat_zoom(
755                    upper_left_lnglat.lng(),
756                    upper_left_lnglat.lat(),
757                    zoom,
758                    Some(false),
759                )?;
760                let bottom_right_tile = Tile::from_lnglat_zoom(
761                    lower_right_lnglat.lng() - LL_EPSILON,
762                    lower_right_lnglat.lat() + LL_EPSILON,
763                    zoom,
764                    Some(false),
765                )?;
766                Ok(TileZBox::new(
767                    top_left_tile.x,
768                    bottom_right_tile.x,
769                    top_left_tile.y,
770                    bottom_right_tile.y,
771                    zoom,
772                ))
773            })
774        })
775        .collect::<Result<Vec<TileZBox>, UtilesCoreError>>()?;
776    Ok(TileZBoxes::from(ranges))
777}
778
779/// Return the number of tiles for a bounding box and zoom(s).
780///
781/// # Errors
782///
783/// Returns error if the number of tiles cannot be calculated due to invalid bbox/zbox
784pub fn tiles_count(
785    bounds: (f64, f64, f64, f64),
786    zooms: ZoomOrZooms,
787) -> Result<u64, UtilesCoreError> {
788    let ranges = tile_ranges(bounds, zooms)?;
789    Ok(ranges.length())
790}
791
792/// Return an iterator of tiles for a bounding box and zoom(s).
793pub fn tiles(
794    bounds: (f64, f64, f64, f64),
795    zooms: ZoomOrZooms,
796) -> impl Iterator<Item = Tile> {
797    let zooms = as_zooms(zooms);
798    let bboxthing = BBox {
799        north: bounds.3,
800        south: bounds.1,
801        east: bounds.2,
802        west: bounds.0,
803    };
804    let bboxes: Vec<BBox> = bboxthing
805        .bboxes()
806        .into_iter()
807        .map(|bbox| {
808            // clip to web mercator extent
809            BBox {
810                north: bbox.north.min(85.051_129),
811                south: bbox.south.max(-85.051_129),
812                east: bbox.east.min(180.0),
813                west: bbox.west.max(-180.0),
814            }
815        })
816        .collect();
817    bboxes.into_iter().flat_map(move |bbox| {
818        let zooms = zooms.clone();
819        zooms.into_iter().flat_map(move |zoom| {
820            let upper_left_lnglat = LngLat {
821                xy: point2d! { x: bbox.west, y: bbox.north },
822            };
823            let lower_right_lnglat = LngLat {
824                xy: point2d! { x: bbox.east, y: bbox.south },
825            };
826            let top_left_tile = Tile::from_lnglat_zoom(
827                upper_left_lnglat.lng(),
828                upper_left_lnglat.lat(),
829                zoom,
830                Some(false),
831            );
832            let bottom_right_tile = Tile::from_lnglat_zoom(
833                lower_right_lnglat.lng() - LL_EPSILON,
834                lower_right_lnglat.lat() + LL_EPSILON,
835                zoom,
836                Some(false),
837            );
838
839            match (top_left_tile, bottom_right_tile) {
840                (Ok(top_left), Ok(bottom_right)) => tiles_range_zoom(
841                    top_left.x,
842                    bottom_right.x,
843                    top_left.y,
844                    bottom_right.y,
845                    zoom,
846                )
847                .map(move |(x, y, z)| Tile { x, y, z })
848                .collect::<Vec<_>>()
849                .into_iter(),
850                _ => Vec::new().into_iter(),
851            }
852        })
853    })
854}
855
856/// Convert tile xyz to u64 tile id (based on mapbox coverage implementation)
857#[must_use]
858pub fn to_id(x: u32, y: u32, z: u8) -> u64 {
859    let dim = 2u64 * (1u64 << z);
860    ((dim * u64::from(y) + u64::from(x)) * 32u64) + u64::from(z)
861}
862
863/// Convert tile u64 id to tile xyz
864///
865/// # Panics
866///
867/// Errors on integer conversion error (should not happen) should not happen
868#[allow(clippy::cast_possible_truncation)]
869#[must_use]
870pub fn from_id(id: u64) -> Tile {
871    let z = (id % 32) as u8;
872    let dim = 2u64 * (1u64 << z);
873    let xy = (id - u64::from(z)) / 32u64;
874    let x = u32::try_from(xy % dim).expect("should never happen");
875    let y = ((xy - u64::from(x)) / dim) as u32;
876    utile!(x, y, z)
877}