#![deny(clippy::missing_const_for_fn)]
use std::f64::consts::PI;
use std::num::FpCategory;
use crate::bbox::{BBox, WebBBox};
use crate::constants::{DEG2RAD, EARTH_CIRCUMFERENCE, EARTH_RADIUS, LL_EPSILON};
use crate::errors::UtilesCoreResult;
use crate::point2d;
use crate::sibling_relationship::SiblingRelationship;
use crate::tile_zbox::{TileZBox, TileZBoxes};
use crate::utile;
use crate::zoom::ZoomOrZooms;
use crate::Point2d;
use crate::{LngLat, Tile, UtilesCoreError};
#[must_use]
pub fn ult(x: u32, y: u32, z: u8) -> (f64, f64) {
let z2 = f64::from(2_u32.pow(u32::from(z)));
let lon_deg = (f64::from(x) / z2) * 360.0 - 180.0;
let lat_rad = ((1.0 - 2.0 * f64::from(y) / z2) * PI).sinh().atan();
(lon_deg, lat_rad.to_degrees())
}
#[must_use]
pub fn ul(x: u32, y: u32, z: u8) -> LngLat {
let (lon_deg, lat_deg) = ult(x, y, z);
LngLat::new(lon_deg, lat_deg)
}
#[must_use]
pub fn ll(x: u32, y: u32, z: u8) -> LngLat {
ul(x, y + 1, z)
}
#[must_use]
pub fn ur(x: u32, y: u32, z: u8) -> LngLat {
ul(x + 1, y, z)
}
#[must_use]
pub fn lr(x: u32, y: u32, z: u8) -> LngLat {
ul(x + 1, y + 1, z)
}
#[must_use]
pub fn minmax(zoom: u8) -> (u32, u32) {
(0, 2_u32.pow(u32::from(zoom)) - 1)
}
#[must_use]
pub fn valid(x: u32, y: u32, z: u8) -> bool {
let (minx, maxx) = minmax(z);
let (miny, maxy) = minmax(z);
x >= minx && x <= maxx && y >= miny && y <= maxy
}
#[must_use]
#[inline]
pub fn flipy(y: u32, z: u8) -> u32 {
2_u32.pow(u32::from(z)) - 1 - y
}
#[must_use]
#[inline]
pub fn yflip(y: u32, z: u8) -> u32 {
flipy(y, z)
}
#[must_use]
#[inline]
pub const fn int_2_offset_zoom(i: u64) -> (u64, u8) {
if i == 0 {
return (0, 0);
}
let mut acc: u64 = 0;
let mut z: u8 = 0;
loop {
let num_tiles: u64 = (1 << z) * (1 << z);
if acc + num_tiles > i {
return (i - acc, z);
}
acc += num_tiles;
z += 1;
}
}
#[must_use]
pub fn xyz2rmid(x: u32, y: u32, z: u8) -> u64 {
if z == 0 {
return u64::from(x) + u64::from(y) * 2u64.pow(u32::from(z));
}
let base_id: u64 = (4u64.pow(u32::from(z)) - 1) / 3;
base_id + u64::from(x) + u64::from(y) * 2u64.pow(u32::from(z))
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn rmid2xyz(i: u64) -> (u32, u32, u8) {
if i == 0 {
return (0, 0, 0);
}
let (i_o, z) = int_2_offset_zoom(i);
let pow_z = 2u64.pow(u32::from(z));
let x = i_o % pow_z;
let y = i_o / pow_z;
(x as u32, y as u32, z)
}
#[must_use]
pub fn bbox2zoom(bbox: (u32, u32, u32, u32)) -> u8 {
let max_zoom = 28;
let (west, south, east, north) = bbox;
for z in 0..max_zoom {
let mask = 1 << (32 - (z + 1));
if (west & mask) != (east & mask) || (south & mask) != (north & mask) {
return z;
}
}
max_zoom
}
#[must_use]
pub fn bounds(x: u32, y: u32, z: u8) -> (f64, f64, f64, f64) {
let ul_corner = ul(x, y, z);
let lr_corner = ul(x + 1, y + 1, z);
(
ul_corner.lng(),
lr_corner.lat(),
lr_corner.lng(),
ul_corner.lat(),
)
}
#[must_use]
pub fn truncate_lng(lng: f64) -> f64 {
lng.clamp(-180.0, 180.0)
}
#[must_use]
pub fn truncate_lat(lat: f64) -> f64 {
lat.clamp(-90.0, 90.0)
}
#[must_use]
pub fn truncate_lnglat(lnglat: &LngLat) -> LngLat {
LngLat::new(truncate_lng(lnglat.lng()), truncate_lat(lnglat.lat()))
}
#[must_use]
pub fn parent(x: u32, y: u32, z: u8, n: Option<u8>) -> Option<Tile> {
let n = n.unwrap_or(0);
if n == 0 {
if z == 0 {
None
} else {
Some(utile!(x >> 1, y >> 1, z - 1))
}
} else {
parent(x >> 1, y >> 1, z - 1, Some(n - 1))
}
}
#[must_use]
pub fn children1_zorder(x: u32, y: u32, z: u8) -> [Tile; 4] {
[
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), ]
}
#[must_use]
pub fn children_zorder(x: u32, y: u32, z: u8, zoom: Option<u8>) -> Vec<Tile> {
let zoom = zoom.unwrap_or(z + 1);
let tile = utile!(x, y, z);
let mut tiles = vec![tile];
while tiles[0].z < zoom {
let (xtile, ytile, ztile) = (tiles[0].x, tiles[0].y, tiles[0].z);
tiles.append(&mut vec![
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), ]);
tiles.remove(0);
}
tiles
}
#[must_use]
pub fn children(x: u32, y: u32, z: u8, zoom: Option<u8>) -> Vec<Tile> {
let zoom = zoom.unwrap_or(z + 1);
let tile = utile!(x, y, z);
let mut tiles = vec![tile];
while tiles[0].z < zoom {
let (xtile, ytile, ztile) = (tiles[0].x, tiles[0].y, tiles[0].z);
tiles.append(&mut vec![
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), ]);
tiles.remove(0);
}
tiles
}
#[must_use]
pub fn siblings(x: u32, y: u32, z: u8) -> Vec<Tile> {
let sibrel = SiblingRelationship::from((x, y));
match sibrel {
SiblingRelationship::UpperLeft => vec![
utile!(x + 1, y, z),
utile!(x, y + 1, z),
utile!(x + 1, y + 1, z),
],
SiblingRelationship::UpperRight => vec![
utile!(x - 1, y, z),
utile!(x, y + 1, z),
utile!(x - 1, y + 1, z),
],
SiblingRelationship::LowerLeft => vec![
utile!(x + 1, y, z),
utile!(x, y - 1, z),
utile!(x + 1, y - 1, z),
],
SiblingRelationship::LowerRight => vec![
utile!(x - 1, y, z),
utile!(x, y - 1, z),
utile!(x - 1, y - 1, z),
],
}
}
#[must_use]
pub fn bbox_truncate(
west: f64,
south: f64,
east: f64,
north: f64,
truncate: Option<bool>,
) -> (f64, f64, f64, f64) {
let trunc = truncate.unwrap_or(false);
let mut west = west;
let mut east = east;
let mut south = south;
let mut north = north;
if trunc {
if west < -180.0 {
west = -180.0;
}
if east > 180.0 {
east = 180.0;
}
if south < -90.0 {
south = -90.0;
}
if north > 90.0 {
north = 90.0;
}
}
(west, south, east, north)
}
pub fn _xy(lng: f64, lat: f64, truncate: Option<bool>) -> UtilesCoreResult<(f64, f64)> {
let (lng, lat) = if truncate.unwrap_or(false) {
(truncate_lng(lng), truncate_lat(lat))
} else {
(lng, lat)
};
let sinlat = lat.to_radians().sin();
let yish = (1.0 + sinlat) / (1.0 - sinlat);
match yish.classify() {
FpCategory::Infinite | FpCategory::Nan => {
Err(UtilesCoreError::LngLat2WebMercator(
"Y can not be computed: lat={lat}".to_string(),
))
}
_ => {
let y = 0.5 - 0.25 * (yish.ln()) / PI;
let x = lng / 360.0 + 0.5;
Ok((x, y))
}
}
}
#[must_use]
pub fn lnglat2webmercator(lng: f64, lat: f64) -> (f64, f64) {
let x = EARTH_RADIUS * lng.to_radians();
let y = if (lat - 90.0).abs() < f64::EPSILON {
f64::INFINITY
} else if (lat + 90.0).abs() < f64::EPSILON {
f64::NEG_INFINITY
} else {
EARTH_RADIUS * (PI * 0.25 + 0.5 * lat.to_radians()).tan().ln()
};
(x, y)
}
#[must_use]
#[inline]
pub fn webmercator2lnglat(x: f64, y: f64) -> (f64, f64) {
let lng = x / EARTH_RADIUS * 180.0 / PI;
let lat = (2.0 * (y / EARTH_RADIUS).exp().atan() - PI * 0.5) * 180.0 / PI;
(lng, lat)
}
#[must_use]
pub fn xy(lng: f64, lat: f64, truncate: Option<bool>) -> (f64, f64) {
let (lng, lat) = if truncate.unwrap_or(false) {
(truncate_lng(lng), truncate_lat(lat))
} else {
(lng, lat)
};
lnglat2webmercator(lng, lat)
}
#[must_use]
pub fn lnglat(x: f64, y: f64, truncate: Option<bool>) -> LngLat {
let (lng, lat) = webmercator2lnglat(x, y);
if truncate.is_some() {
truncate_lnglat(&LngLat::new(lng, lat))
} else {
LngLat::new(lng, lat)
}
}
enum TileEdgeInfo {
Bottom,
BottomLeft,
BottomRight,
Left,
Right,
Top,
TopLeft,
TopRight,
Middle,
}
fn tile_edge_info(x: u32, y: u32, z: u8) -> TileEdgeInfo {
if x == 0 && y == 0 {
return TileEdgeInfo::TopLeft;
}
let max_xy = 2u32.pow(u32::from(z));
if x == max_xy && y == max_xy {
return TileEdgeInfo::BottomRight;
}
match (x, y) {
(max, 0) if max == max_xy => TileEdgeInfo::TopRight,
(0, max) if max == max_xy => TileEdgeInfo::BottomLeft,
(0, _) => TileEdgeInfo::Left,
(max, _) if max == max_xy => TileEdgeInfo::Right,
(_, 0) => TileEdgeInfo::Top,
(_, max) if max == max_xy => TileEdgeInfo::Bottom,
_ => TileEdgeInfo::Middle,
}
}
fn neighbors_middle_tile(x: u32, y: u32, z: u8) -> Vec<Tile> {
vec![
utile!(x + 1, y, z),
utile!(x, y + 1, z),
utile!(x + 1, y + 1, z),
utile!(x - 1, y, z),
utile!(x, y - 1, z),
utile!(x - 1, y - 1, z),
utile!(x + 1, y - 1, z),
utile!(x - 1, y + 1, z),
]
}
#[must_use]
pub fn neighbors(x: u32, y: u32, z: u8) -> Vec<Tile> {
if z == 0 {
return Vec::new();
}
let edge_info = tile_edge_info(x, y, z);
match edge_info {
TileEdgeInfo::Middle => neighbors_middle_tile(x, y, z),
TileEdgeInfo::TopLeft => vec![
utile!(x + 1, y, z),
utile!(x, y + 1, z),
utile!(x + 1, y + 1, z),
],
TileEdgeInfo::TopRight => vec![
utile!(x - 1, y, z),
utile!(x, y + 1, z),
utile!(x - 1, y + 1, z),
],
TileEdgeInfo::BottomLeft => vec![
utile!(x + 1, y, z),
utile!(x, y - 1, z),
utile!(x + 1, y - 1, z),
],
TileEdgeInfo::BottomRight => vec![
utile!(x - 1, y, z),
utile!(x, y - 1, z),
utile!(x - 1, y - 1, z),
],
TileEdgeInfo::Left => vec![
utile!(x + 1, y, z),
utile!(x, y + 1, z),
utile!(x + 1, y + 1, z),
utile!(x, y - 1, z),
utile!(x + 1, y - 1, z),
],
TileEdgeInfo::Right => vec![
utile!(x - 1, y, z),
utile!(x, y + 1, z),
utile!(x - 1, y + 1, z),
utile!(x, y - 1, z),
utile!(x - 1, y - 1, z),
],
TileEdgeInfo::Top => vec![
utile!(x + 1, y, z),
utile!(x, y + 1, z),
utile!(x + 1, y + 1, z),
utile!(x - 1, y, z),
utile!(x - 1, y + 1, z),
],
TileEdgeInfo::Bottom => vec![
utile!(x + 1, y, z),
utile!(x, y - 1, z),
utile!(x + 1, y - 1, z),
utile!(x - 1, y, z),
utile!(x - 1, y - 1, z),
],
}
}
pub fn tile(
lng: f64,
lat: f64,
zoom: u8,
truncate: Option<bool>,
) -> Result<Tile, UtilesCoreError> {
Tile::from_lnglat_zoom(lng, lat, zoom, truncate)
}
#[must_use]
pub fn lnglat2tile_frac(lng: f64, lat: f64, z: u8) -> (f64, f64, u8) {
let sin = (lat * DEG2RAD).sin();
let z2 = 2f64.powi(i32::from(z));
let mut x = z2 * (lng / 360.0 + 0.5);
let y = z2 * (0.5 - (0.25 * ((1.0 + sin) / (1.0 - sin)).ln()) / PI);
x = x.rem_euclid(z2);
(x, y, z)
}
pub fn bounding_tile(
bbox: BBox,
truncate: Option<bool>,
) -> Result<Tile, UtilesCoreError> {
let (west, south, east, north) =
bbox_truncate(bbox.west, bbox.south, bbox.east, bbox.north, truncate);
let tmin = tile(west, north, 32, truncate)?;
let tmax = tile(east - LL_EPSILON, south + LL_EPSILON, 32, truncate)?;
let cell = (tmin.x, tmin.y, tmax.x, tmax.y);
let z = bbox2zoom(cell);
if z == 0 {
Ok(utile!(0, 0, 0))
} else {
let x = cell.0 >> (32 - z);
let y = cell.1 >> (32 - z);
Ok(utile!(x, y, z))
}
}
#[must_use]
pub fn xyz2bbox(x: u32, y: u32, z: u8) -> WebBBox {
let tile_size = EARTH_CIRCUMFERENCE / 2.0_f64.powi(i32::from(z));
let left = f64::from(x) * tile_size - EARTH_CIRCUMFERENCE / 2.0;
let top = EARTH_CIRCUMFERENCE / 2.0 - f64::from(y) * tile_size;
WebBBox::new(left, top - tile_size, left + tile_size, top)
}
#[must_use]
pub fn as_zooms(zoom_or_zooms: ZoomOrZooms) -> Vec<u8> {
match zoom_or_zooms {
ZoomOrZooms::Zoom(zoom) => {
vec![zoom]
}
ZoomOrZooms::Zooms(zooms) => zooms,
}
}
fn tiles_range_zoom(
minx: u32,
maxx: u32,
miny: u32,
maxy: u32,
zoom: u8,
) -> impl Iterator<Item = (u32, u32, u8)> {
(minx..=maxx).flat_map(move |i| (miny..=maxy).map(move |j| (i, j, zoom)))
}
pub fn tile_ranges(
bounds: (f64, f64, f64, f64),
zooms: ZoomOrZooms,
) -> Result<TileZBoxes, UtilesCoreError> {
let zooms = as_zooms(zooms);
let bboxes: Vec<BBox> = BBox::from(bounds)
.bboxes()
.into_iter()
.map(|bbox| {
BBox {
north: bbox.north.min(85.051_129),
south: bbox.south.max(-85.051_129),
east: bbox.east.min(180.0),
west: bbox.west.max(-180.0),
}
})
.collect();
let ranges: Vec<TileZBox> = bboxes
.into_iter()
.flat_map(move |bbox| {
let zooms = zooms.clone();
zooms.into_iter().map(move |zoom| {
let upper_left_lnglat = LngLat {
xy: point2d! { x: bbox.west, y: bbox.north },
};
let lower_right_lnglat = LngLat {
xy: point2d! { x: bbox.east, y: bbox.south },
};
let top_left_tile = Tile::from_lnglat_zoom(
upper_left_lnglat.lng(),
upper_left_lnglat.lat(),
zoom,
Some(false),
)?;
let bottom_right_tile = Tile::from_lnglat_zoom(
lower_right_lnglat.lng() - LL_EPSILON,
lower_right_lnglat.lat() + LL_EPSILON,
zoom,
Some(false),
)?;
Ok(TileZBox::new(
top_left_tile.x,
bottom_right_tile.x,
top_left_tile.y,
bottom_right_tile.y,
zoom,
))
})
})
.collect::<Result<Vec<TileZBox>, UtilesCoreError>>()?;
Ok(TileZBoxes::from(ranges))
}
pub fn tiles_count(
bounds: (f64, f64, f64, f64),
zooms: ZoomOrZooms,
) -> Result<u64, UtilesCoreError> {
let ranges = tile_ranges(bounds, zooms)?;
Ok(ranges.length())
}
pub fn tiles(
bounds: (f64, f64, f64, f64),
zooms: ZoomOrZooms,
) -> impl Iterator<Item = Tile> {
let zooms = as_zooms(zooms);
let bboxthing = BBox {
north: bounds.3,
south: bounds.1,
east: bounds.2,
west: bounds.0,
};
let bboxes: Vec<BBox> = bboxthing
.bboxes()
.into_iter()
.map(|bbox| {
BBox {
north: bbox.north.min(85.051_129),
south: bbox.south.max(-85.051_129),
east: bbox.east.min(180.0),
west: bbox.west.max(-180.0),
}
})
.collect();
bboxes.into_iter().flat_map(move |bbox| {
let zooms = zooms.clone();
zooms.into_iter().flat_map(move |zoom| {
let upper_left_lnglat = LngLat {
xy: point2d! { x: bbox.west, y: bbox.north },
};
let lower_right_lnglat = LngLat {
xy: point2d! { x: bbox.east, y: bbox.south },
};
let top_left_tile = Tile::from_lnglat_zoom(
upper_left_lnglat.lng(),
upper_left_lnglat.lat(),
zoom,
Some(false),
);
let bottom_right_tile = Tile::from_lnglat_zoom(
lower_right_lnglat.lng() - LL_EPSILON,
lower_right_lnglat.lat() + LL_EPSILON,
zoom,
Some(false),
);
match (top_left_tile, bottom_right_tile) {
(Ok(top_left), Ok(bottom_right)) => tiles_range_zoom(
top_left.x,
bottom_right.x,
top_left.y,
bottom_right.y,
zoom,
)
.map(move |(x, y, z)| Tile { x, y, z })
.collect::<Vec<_>>()
.into_iter(),
_ => Vec::new().into_iter(),
}
})
})
}
#[must_use]
pub fn to_id(x: u32, y: u32, z: u8) -> u64 {
let dim = 2u64 * (1u64 << z);
((dim * u64::from(y) + u64::from(x)) * 32u64) + u64::from(z)
}
#[allow(clippy::cast_possible_truncation)]
#[must_use]
pub fn from_id(id: u64) -> Tile {
let z = (id % 32) as u8;
let dim = 2u64 * (1u64 << z);
let xy = (id - u64::from(z)) / 32u64;
let x = u32::try_from(xy % dim).expect("should never happen");
let y = ((xy - u64::from(x)) / dim) as u32;
utile!(x, y, z)
}