1pub const TILE_SIZE: f64 = 256.0;
8pub const MIN_ZOOM: u8 = 2;
9pub const MAX_ZOOM: u8 = 18;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct TileId {
13 pub z: u8,
14 pub x: u32,
15 pub y: u32,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
19pub struct LatLng {
20 pub lat: f64,
21 pub lng: f64,
22}
23
24#[derive(Debug, Clone, Copy)]
25pub struct Viewport {
26 pub width: f32,
27 pub height: f32,
28 pub center: LatLng,
29 pub zoom: u8,
30}
31
32pub fn lng_to_tile_x(lng: f64, z: u8) -> f64 {
34 let n = 2f64.powi(z as i32);
35 (lng + 180.0) / 360.0 * n
36}
37
38pub fn lat_to_tile_y(lat: f64, z: u8) -> f64 {
41 let lat = lat.clamp(-85.05112878, 85.05112878);
42 let rad = lat.to_radians();
43 let n = 2f64.powi(z as i32);
44 (1.0 - (rad.tan() + 1.0 / rad.cos()).ln() / std::f64::consts::PI) / 2.0 * n
45}
46
47pub fn tile_x_to_lng(x: f64, z: u8) -> f64 {
48 let n = 2f64.powi(z as i32);
49 x / n * 360.0 - 180.0
50}
51
52pub fn tile_y_to_lat(y: f64, z: u8) -> f64 {
53 let n = 2f64.powi(z as i32);
54 let rad = (std::f64::consts::PI * (1.0 - 2.0 * y / n)).sinh().atan();
55 rad.to_degrees()
56}
57
58pub fn viewport_center_tile(v: &Viewport) -> (f64, f64) {
61 (
62 lng_to_tile_x(v.center.lng, v.zoom),
63 lat_to_tile_y(v.center.lat, v.zoom),
64 )
65}
66
67pub fn latlng_to_viewport_pixel(v: &Viewport, p: LatLng) -> (f32, f32) {
70 let (cx, cy) = viewport_center_tile(v);
71 let px = lng_to_tile_x(p.lng, v.zoom);
72 let py = lat_to_tile_y(p.lat, v.zoom);
73 let dx = (px - cx) * TILE_SIZE;
74 let dy = (py - cy) * TILE_SIZE;
75 (
76 (v.width as f64 / 2.0 + dx) as f32,
77 (v.height as f64 / 2.0 + dy) as f32,
78 )
79}
80
81pub fn viewport_pixel_to_latlng(v: &Viewport, px: f32, py: f32) -> LatLng {
83 let (cx, cy) = viewport_center_tile(v);
84 let dx = (px as f64 - v.width as f64 / 2.0) / TILE_SIZE;
85 let dy = (py as f64 - v.height as f64 / 2.0) / TILE_SIZE;
86 LatLng {
87 lng: tile_x_to_lng(cx + dx, v.zoom),
88 lat: tile_y_to_lat(cy + dy, v.zoom),
89 }
90}
91
92pub fn visible_tiles(v: &Viewport) -> Vec<TileId> {
94 let (cx, cy) = viewport_center_tile(v);
95 let half_w = v.width as f64 / 2.0 / TILE_SIZE;
96 let half_h = v.height as f64 / 2.0 / TILE_SIZE;
97 let min_x = (cx - half_w).floor() as i64;
98 let max_x = (cx + half_w).ceil() as i64;
99 let min_y = (cy - half_h).floor() as i64;
100 let max_y = (cy + half_h).ceil() as i64;
101 let n = 1i64 << v.zoom;
102 let mut out = Vec::with_capacity(((max_x - min_x + 1) * (max_y - min_y + 1)) as usize);
103
104 for ty in min_y..=max_y {
105 if ty < 0 || ty >= n {
106 continue;
107 }
108 for tx in min_x..=max_x {
109 if tx < 0 || tx >= n {
110 continue;
111 }
112 out.push(TileId {
113 z: v.zoom,
114 x: tx as u32,
115 y: ty as u32,
116 });
117 }
118 }
119
120 out
121}
122
123pub fn fit_bounds(pins: &[LatLng], viewport_w: f32, viewport_h: f32) -> (LatLng, u8) {
127 if pins.is_empty() {
128 return (
129 LatLng {
130 lat: 20.0,
131 lng: 0.0,
132 },
133 MIN_ZOOM,
134 );
135 }
136
137 if pins.len() == 1 {
138 return (pins[0], 13);
139 }
140
141 let (mut min_lat, mut max_lat) = (f64::MAX, f64::MIN);
142 let (mut min_lng, mut max_lng) = (f64::MAX, f64::MIN);
143 for p in pins {
144 min_lat = min_lat.min(p.lat);
145 max_lat = max_lat.max(p.lat);
146 min_lng = min_lng.min(p.lng);
147 max_lng = max_lng.max(p.lng);
148 }
149
150 let center = LatLng {
151 lat: (min_lat + max_lat) / 2.0,
152 lng: (min_lng + max_lng) / 2.0,
153 };
154
155 for z in (MIN_ZOOM..=MAX_ZOOM).rev() {
156 let v = Viewport {
157 width: viewport_w,
158 height: viewport_h,
159 center,
160 zoom: z,
161 };
162 let (tl_px, tl_py) = latlng_to_viewport_pixel(
163 &v,
164 LatLng {
165 lat: max_lat,
166 lng: min_lng,
167 },
168 );
169 let (br_px, br_py) = latlng_to_viewport_pixel(
170 &v,
171 LatLng {
172 lat: min_lat,
173 lng: max_lng,
174 },
175 );
176 let bbox_w = (br_px - tl_px).abs();
177 let bbox_h = (br_py - tl_py).abs();
178 if bbox_w <= viewport_w * 0.8 && bbox_h <= viewport_h * 0.8 {
179 return (center, z);
180 }
181 }
182
183 (center, MIN_ZOOM)
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn zoom0_is_one_tile() {
192 assert!((lng_to_tile_x(-180.0, 0) - 0.0).abs() < 1e-9);
193 assert!((lng_to_tile_x(180.0, 0) - 1.0).abs() < 1e-9);
194 }
195
196 #[test]
197 fn greenwich_is_half_world() {
198 assert!((lng_to_tile_x(0.0, 1) - 1.0).abs() < 1e-9);
199 }
200
201 #[test]
202 fn inverse_roundtrip() {
203 for z in [2u8, 5, 10, 18] {
204 for &lat in &[-60.0, -30.0, 0.0, 30.0, 60.0] {
205 for &lng in &[-170.0, -90.0, 0.0, 90.0, 170.0] {
206 let x = lng_to_tile_x(lng, z);
207 let y = lat_to_tile_y(lat, z);
208 let lng2 = tile_x_to_lng(x, z);
209 let lat2 = tile_y_to_lat(y, z);
210
211 assert!((lng - lng2).abs() < 1e-6, "lng z={} {} -> {}", z, lng, lng2);
212 assert!((lat - lat2).abs() < 1e-6, "lat z={} {} -> {}", z, lat, lat2);
213 }
214 }
215 }
216 }
217
218 #[test]
219 fn viewport_center_projects_to_middle() {
220 let v = Viewport {
221 width: 800.0,
222 height: 600.0,
223 center: LatLng {
224 lat: 12.9716,
225 lng: 77.5946,
226 },
227 zoom: 10,
228 };
229
230 let (px, py) = latlng_to_viewport_pixel(&v, v.center);
231 assert!((px - 400.0).abs() < 1.0);
232 assert!((py - 300.0).abs() < 1.0);
233 }
234
235 #[test]
236 fn pixel_to_latlng_inverse() {
237 let v = Viewport {
238 width: 1000.0,
239 height: 800.0,
240 center: LatLng {
241 lat: 48.8566,
242 lng: 2.3522,
243 },
244 zoom: 12,
245 };
246
247 let ll = viewport_pixel_to_latlng(&v, 500.0, 400.0);
248 assert!((ll.lat - v.center.lat).abs() < 1e-6);
249 assert!((ll.lng - v.center.lng).abs() < 1e-6);
250 }
251
252 #[test]
253 fn visible_tiles_zoom2_paris() {
254 let v = Viewport {
255 width: 800.0,
256 height: 600.0,
257 center: LatLng {
258 lat: 48.85,
259 lng: 2.35,
260 },
261 zoom: 2,
262 };
263
264 let tiles = visible_tiles(&v);
265 assert!(!tiles.is_empty());
266 assert!(tiles.iter().all(|t| t.z == 2));
267 assert!(tiles.iter().all(|t| t.x < 4 && t.y < 4));
268 }
269
270 #[test]
271 fn fit_bounds_single_point_zooms_in() {
272 let pins = vec![LatLng {
273 lat: 12.97,
274 lng: 77.59,
275 }];
276 let (c, z) = fit_bounds(&pins, 1000.0, 800.0);
277 assert_eq!(z, 13);
278 assert!((c.lat - 12.97).abs() < 1e-6);
279 }
280
281 #[test]
282 fn fit_bounds_world_wide_returns_min_zoomish() {
283 let pins = vec![
284 LatLng {
285 lat: -60.0,
286 lng: -170.0,
287 },
288 LatLng {
289 lat: 60.0,
290 lng: 170.0,
291 },
292 ];
293 let (_c, z) = fit_bounds(&pins, 800.0, 600.0);
294 assert!(z <= 3);
295 }
296}