Skip to main content

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