1pub mod web_mercator {
23 pub const MAX_LAT: f64 = 85.05112877980659;
28
29 #[inline]
31 pub const fn tile_count(zoom: u8) -> u32 {
32 1u32 << zoom
33 }
34
35 pub fn lonlat_to_tile(lon: f64, lat: f64, zoom: u8) -> (u32, u32) {
40 let n = tile_count(zoom);
41 let lon = lon.clamp(-180.0, 180.0);
42 let lat = lat.clamp(-MAX_LAT, MAX_LAT);
43
44 let x = (((lon + 180.0) / 360.0) * n as f64).floor() as u32;
45 let lat_rad = lat.to_radians();
46 let y =
47 ((1.0 - lat_rad.tan().asinh() / std::f64::consts::PI) / 2.0 * n as f64).floor() as u32;
48 (x.min(n - 1), y.min(n - 1))
49 }
50
51 pub fn tile_to_bounds(z: u8, x: u32, y: u32) -> (f64, f64, f64, f64) {
54 let n = tile_count(z) as f64;
55 let west = x as f64 / n * 360.0 - 180.0;
56 let east = (x as f64 + 1.0) / n * 360.0 - 180.0;
57
58 let lat_at = |yy: f64| -> f64 {
59 let m = std::f64::consts::PI * (1.0 - 2.0 * yy / n);
60 m.sinh().atan().to_degrees()
61 };
62 let north = lat_at(y as f64);
63 let south = lat_at(y as f64 + 1.0);
64 (west, south, east, north)
65 }
66}
67
68pub mod web_mercator_tms {
71 use super::web_mercator;
72
73 pub fn lonlat_to_tile(lon: f64, lat: f64, zoom: u8) -> (u32, u32) {
75 let (x, xyz_y) = web_mercator::lonlat_to_tile(lon, lat, zoom);
76 let max_y = web_mercator::tile_count(zoom) - 1;
77 (x, max_y - xyz_y)
78 }
79
80 pub fn tile_to_bounds(z: u8, x: u32, y: u32) -> (f64, f64, f64, f64) {
83 let max_y = web_mercator::tile_count(z) - 1;
84 web_mercator::tile_to_bounds(z, x, max_y - y)
85 }
86
87 #[inline]
89 pub const fn flip_y(zoom: u8, y: u32) -> u32 {
90 ((1u32 << zoom) - 1) - y
91 }
92}
93
94pub mod geodetic_tms {
98 #[inline]
100 pub const fn tile_count_x(zoom: u8) -> u32 {
101 1u32 << (zoom + 1)
102 }
103
104 #[inline]
106 pub const fn tile_count_y(zoom: u8) -> u32 {
107 1u32 << zoom
108 }
109
110 pub fn lonlat_to_tile(lon: f64, lat: f64, zoom: u8) -> (u32, u32) {
112 let lon = lon.clamp(-180.0, 180.0);
113 let lat = lat.clamp(-90.0, 90.0);
114
115 let nx = tile_count_x(zoom);
116 let ny = tile_count_y(zoom);
117
118 let x = (((lon + 180.0) / 360.0) * nx as f64).floor() as u32;
119 let y = (((lat + 90.0) / 180.0) * ny as f64).floor() as u32;
120 (x.min(nx - 1), y.min(ny - 1))
121 }
122
123 pub fn tile_to_bounds(z: u8, x: u32, y: u32) -> (f64, f64, f64, f64) {
126 let nx = tile_count_x(z) as f64;
127 let ny = tile_count_y(z) as f64;
128 let west = x as f64 / nx * 360.0 - 180.0;
129 let east = (x as f64 + 1.0) / nx * 360.0 - 180.0;
130 let south = y as f64 / ny * 180.0 - 90.0;
131 let north = (y as f64 + 1.0) / ny * 180.0 - 90.0;
132 (west, south, east, north)
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn web_mercator_origin_at_zoom_0_is_top_left() {
142 let (x, y) = web_mercator::lonlat_to_tile(-180.0, web_mercator::MAX_LAT - 0.01, 0);
144 assert_eq!((x, y), (0, 0));
145 }
146
147 #[test]
148 fn web_mercator_y_inverts_to_tms() {
149 let zoom = 3;
150 for y in 0..web_mercator::tile_count(zoom) {
151 assert_eq!(web_mercator_tms::flip_y(zoom, y), (1 << zoom) - 1 - y);
152 assert_eq!(
154 web_mercator_tms::flip_y(zoom, web_mercator_tms::flip_y(zoom, y)),
155 y
156 );
157 }
158 }
159
160 #[test]
161 fn web_mercator_tile_to_bounds_roundtrip_for_centre_points() {
162 let zoom = 4;
163 for x in 0..web_mercator::tile_count(zoom) {
164 for y in 0..web_mercator::tile_count(zoom) {
165 let (w, s, e, n) = web_mercator::tile_to_bounds(zoom, x, y);
166 let cx = (w + e) * 0.5;
167 let cy = (s + n) * 0.5;
168 let (x2, y2) = web_mercator::lonlat_to_tile(cx, cy, zoom);
169 assert_eq!((x, y), (x2, y2), "tile centre should map back to its tile");
170 }
171 }
172 }
173
174 #[test]
175 fn geodetic_tms_zoom_0_has_two_tiles_in_x() {
176 assert_eq!(geodetic_tms::tile_count_x(0), 2);
177 assert_eq!(geodetic_tms::tile_count_y(0), 1);
178 }
179
180 #[test]
181 fn geodetic_tms_y0_is_south() {
182 let (_, y) = geodetic_tms::lonlat_to_tile(0.0, -89.0, 4);
183 assert_eq!(y, 0);
184 let (_, y) = geodetic_tms::lonlat_to_tile(0.0, 89.0, 4);
185 assert_eq!(y, geodetic_tms::tile_count_y(4) - 1);
186 }
187
188 #[test]
189 fn geodetic_tms_bounds_cover_globe_at_zoom_0() {
190 let (w0, s0, e0, n0) = geodetic_tms::tile_to_bounds(0, 0, 0);
191 let (w1, _, e1, _) = geodetic_tms::tile_to_bounds(0, 1, 0);
192 assert_eq!((w0, s0, e0, n0), (-180.0, -90.0, 0.0, 90.0));
193 assert_eq!((w1, e1), (0.0, 180.0));
194 }
195}