1#![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#[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#[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#[must_use]
33pub fn ll(x: u32, y: u32, z: u8) -> LngLat {
34 ul(x, y + 1, z)
35}
36
37#[must_use]
39pub fn ur(x: u32, y: u32, z: u8) -> LngLat {
40 ul(x + 1, y, z)
41}
42
43#[must_use]
45pub fn lr(x: u32, y: u32, z: u8) -> LngLat {
46 ul(x + 1, y + 1, z)
47}
48
49#[must_use]
51pub fn minmax(zoom: u8) -> (u32, u32) {
52 (0, 2_u32.pow(u32::from(zoom)) - 1)
53}
54
55#[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#[must_use]
65#[inline]
66pub fn flipy(y: u32, z: u8) -> u32 {
67 2_u32.pow(u32::from(z)) - 1 - y
68}
69
70#[must_use]
72#[inline]
73pub fn yflip(y: u32, z: u8) -> u32 {
74 flipy(y, z)
75}
76
77#[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#[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#[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#[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#[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#[must_use]
245pub const fn truncate_lng(lng: f64) -> f64 {
246 lng.clamp(-180.0, 180.0)
247}
248
249#[must_use]
251pub const fn truncate_lat(lat: f64) -> f64 {
252 lat.clamp(-90.0, 90.0)
253}
254
255#[must_use]
257pub const fn truncate_lnglat(lnglat: &LngLat) -> LngLat {
258 LngLat::new(truncate_lng(lnglat.lng()), truncate_lat(lnglat.lat()))
259}
260
261#[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#[must_use]
277pub fn children1_zorder(x: u32, y: u32, z: u8) -> [Tile; 4] {
278 [
279 utile!(x * 2, y * 2, z + 1), utile!(x * 2 + 1, y * 2, z + 1), utile!(x * 2, y * 2 + 1, z + 1), utile!(x * 2 + 1, y * 2 + 1, z + 1), ]
284}
285#[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), utile!(xtile * 2 + 1, ytile * 2, ztile + 1), utile!(xtile * 2, ytile * 2 + 1, ztile + 1), utile!(xtile * 2 + 1, ytile * 2 + 1, ztile + 1), ]);
338 tiles.remove(0);
339 }
340 tiles
341}
342
343#[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), utile!(xtile * 2 + 1, ytile * 2, ztile + 1), utile!(xtile * 2 + 1, ytile * 2 + 1, ztile + 1), utile!(xtile * 2, ytile * 2 + 1, ztile + 1), ]);
372 tiles.remove(0);
373 }
374 tiles
375}
376
377#[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#[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
438pub 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#[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#[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#[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#[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#[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; 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; }
663
664 Some(utile!(nx_i as u32, ny_i as u32, z))
665 })
666 .collect()
667}
668
669pub 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#[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 x = x.rem_euclid(z2);
702
703 (x, y, z)
704}
705
706pub 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#[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#[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
762pub 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 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
817pub 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
830pub 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 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#[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#[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}