1use crate::space::EARTH_CIRCUMFERENCE;
2use core::f64::consts::{PI, TAU};
3use libm::{atan, cos, exp, floor, fmax, fmin, log, pow, sin, tan};
4
5pub const A: f64 = 6_378_137.0;
7pub const MAXEXTENT: f64 = 20_037_508.342789244;
9pub const MAXLAT: f64 = 85.0511287798;
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum Source {
15 WGS84,
17 Google,
19}
20
21fn get_zoom_size(zoom: u8, tile_size: f64) -> (f64, f64, f64, f64) {
23 let size = tile_size * pow(2., zoom as f64);
24 (size / 360., size / TAU, size / 2., size)
25}
26
27pub fn ll_to_px(
30 lonlat: (f64, f64),
31 zoom: u8,
32 anti_meridian: Option<bool>,
33 tile_size: Option<u16>,
34) -> (f64, f64) {
35 let anti_meridian = anti_meridian.unwrap_or(false);
36 let tile_size = tile_size.unwrap_or(512) as f64;
37
38 let (bc, cc, zc, ac) = get_zoom_size(zoom, tile_size);
39 let expansion = if anti_meridian { 2. } else { 1. };
40 let d = zc;
41 let f = sin((lonlat.1).to_radians()).clamp(-0.999999999999, 0.999999999999);
42 let mut x = d + lonlat.0 * bc;
43 let mut y = d + 0.5 * log((1. + f) / (1. - f)) * -cc;
44 if x > ac * expansion {
45 x = ac * expansion;
46 }
47 if y > ac {
48 y = ac;
49 }
50
51 (x, y)
52}
53
54pub fn px_to_ll(xy: (f64, f64), zoom: u8, tile_size: Option<u16>) -> (f64, f64) {
57 let tile_size = tile_size.unwrap_or(512) as f64;
58 let (bc, cc, zc, _) = get_zoom_size(zoom, tile_size);
59 let g = (xy.1 - zc) / -cc;
60 let lon = (xy.0 - zc) / bc;
61 let lat = (2. * atan(exp(g)) - 0.5 * PI).to_degrees();
62
63 (lon, lat)
64}
65
66pub fn ll_to_merc(lonlan: (f64, f64)) -> (f64, f64) {
69 let mut x = (A * lonlan.0).to_radians();
70 let mut y = A * log(tan(PI * 0.25 + (0.5 * lonlan.1).to_radians()));
71 x = x.clamp(-MAXEXTENT, MAXEXTENT);
73 y = y.clamp(-MAXEXTENT, MAXEXTENT);
74
75 (x, y)
76}
77
78pub fn merc_to_ll(merc: (f64, f64)) -> (f64, f64) {
81 let x = (merc.0 / A).to_degrees();
82 let y = (0.5 * PI - 2. * atan(exp(-merc.1 / A))).to_degrees();
83 (x, y)
84}
85
86pub fn px_to_tile(px: (f64, f64), tile_size: Option<u16>) -> (u32, u32) {
89 let tile_size = tile_size.unwrap_or(512) as f64;
90 (floor(px.0 / tile_size) as u32, floor(px.1 / tile_size) as u32)
91}
92
93pub fn tile_to_bbox(tile: (u8, u32, u32), tile_size: Option<u16>) -> (u32, u32, u32, u32) {
96 let tile_size = tile_size.unwrap_or(512) as u32;
97 let (_zoom, x, y) = tile;
98 let min_x = x * tile_size;
99 let min_y = y * tile_size;
100 let max_x = min_x + tile_size;
101 let max_y = min_y + tile_size;
102
103 (min_x, min_y, max_x, max_y)
104}
105
106pub fn ll_to_tile(lonlat: (f64, f64), zoom: u8, tile_size: Option<u16>) -> (u32, u32) {
109 let px = ll_to_px(lonlat, zoom, Some(false), tile_size);
110 px_to_tile(px, tile_size)
111}
112
113pub fn ll_to_tile_px(
116 lonlat: (f64, f64),
117 tile: (u8, u32, u32),
118 tile_size: Option<u16>,
119) -> (f64, f64) {
120 let (zoom, x, y) = tile;
121 let tile_size = tile_size.unwrap_or(512);
122 let tile_size_f = tile_size as f64;
123 let px = ll_to_px(lonlat, zoom, Some(false), Some(tile_size));
124 let tile_x_start = x as f64 * tile_size_f;
125 let tile_y_start = y as f64 * tile_size_f;
126
127 ((px.0 - tile_x_start) / tile_size_f, (px.1 - tile_y_start) / tile_size_f)
128}
129
130pub fn convert_bbox(bbox: (f64, f64, f64, f64), source: Source) -> (f64, f64, f64, f64) {
133 let low: (f64, f64);
134 let high: (f64, f64);
135 match source {
136 Source::WGS84 => {
137 low = merc_to_ll((bbox.0, bbox.1));
138 high = merc_to_ll((bbox.2, bbox.3));
139 }
140 Source::Google => {
141 low = ll_to_merc((bbox.0, bbox.1));
142 high = ll_to_merc((bbox.2, bbox.3));
143 }
144 };
145 (low.0, low.1, high.0, high.1)
146}
147
148pub fn xyz_to_bbox(
152 x: u32,
153 y: u32,
154 zoom: u8,
155 tms_style: Option<bool>,
156 source: Option<Source>,
157 tile_size: Option<u16>,
158) -> (f64, f64, f64, f64) {
159 let x = x as f64;
160 let mut y = y as f64;
161 let tms_style = tms_style.unwrap_or(true);
162 let source = source.unwrap_or(Source::Google);
163 let tile_size = tile_size.unwrap_or(512);
164 let tile_size_f = tile_size as f64;
165 if tms_style {
168 y = pow(2., zoom as f64) - 1. - y;
169 }
170 let bl: (f64, f64) = (x * tile_size_f, (y + 1.) * tile_size_f);
172 let tr: (f64, f64) = ((x + 1.) * tile_size_f, y * tile_size_f);
174 let px_bl = px_to_ll(bl, zoom, Some(tile_size));
176 let px_tr = px_to_ll(tr, zoom, Some(tile_size));
177
178 match source {
179 Source::Google => {
180 let ll_bl = ll_to_merc(px_bl);
181 let ll_tr = ll_to_merc(px_tr);
182 (ll_bl.0, ll_bl.1, ll_tr.0, ll_tr.1)
183 }
184 _ => (px_bl.0, px_bl.1, px_tr.0, px_tr.1),
185 }
186}
187
188pub fn bbox_to_xyz_bounds(
194 bbox: (f64, f64, f64, f64),
195 zoom: u8,
196 tms_style: Option<bool>,
197 source: Option<Source>,
198 tile_size: Option<u16>,
199) -> (u32, u32, u32, u32) {
200 let tms_style = tms_style.unwrap_or(true);
201 let source = source.unwrap_or(Source::WGS84);
202 let tile_size = tile_size.unwrap_or(512);
203 let tile_size_f: f64 = tile_size as f64;
204
205 let mut bl = (bbox.0, bbox.1); let mut tr = (bbox.2, bbox.3); if source == Source::Google {
209 bl = ll_to_merc(bl);
210 tr = ll_to_merc(tr);
211 }
212 let px_bl = ll_to_px(bl, zoom, Some(false), Some(tile_size));
213 let px_tr = ll_to_px(tr, zoom, Some(false), Some(tile_size));
214 let x = (floor(px_bl.0 / tile_size_f), floor((px_tr.0 - 1.0) / tile_size_f));
215 let y = (floor(px_tr.1 / tile_size_f), floor((px_bl.1 - 1.0) / tile_size_f));
216
217 let mut bounds = (fmin(x.0, x.1), fmin(y.0, y.1), fmax(x.0, x.1), fmax(y.0, y.1));
218
219 if tms_style {
220 let zoom_diff = pow(2., zoom as f64) - 1.;
221 bounds.1 = zoom_diff - bounds.3;
222 bounds.3 = zoom_diff - bounds.1;
223 }
224
225 let min_x = fmax(bounds.0, 0.) as u32;
226 let min_y = fmax(bounds.1, 0.) as u32;
227 let max_x = fmin(bounds.2, pow(2., zoom as f64) - 1.) as u32;
228 let max_y = fmin(bounds.3, pow(2., zoom as f64) - 1.) as u32;
229
230 (min_x, min_y, max_x, max_y)
231}
232
233pub fn circumference_at_latitude(latitude: f64, circumference: Option<f64>) -> f64 {
235 let circumference = circumference.unwrap_or(EARTH_CIRCUMFERENCE);
236 circumference * cos(latitude.to_radians())
237}
238
239pub fn lng_to_mercator_x(lng: f64) -> f64 {
242 (180.0 + lng) / 360.0
243}
244
245pub fn lat_to_mercator_y(lat: f64) -> f64 {
248 (180. - (180. / PI) * log(tan(PI / 4. + (lat * PI) / 360.))) / 360.
249}
250
251pub fn altitude_to_mercator_z(altitude: f64, lat: f64, circumference: Option<f64>) -> f64 {
254 altitude / circumference_at_latitude(lat, circumference)
255}
256
257pub fn lng_from_mercator_x(x: f64) -> f64 {
260 x * 360. - 180.
261}
262
263pub fn lat_from_mercator_y(y: f64) -> f64 {
266 let y2 = 180. - y * 360.;
267 (360. / PI) * atan(exp((y2 * PI) / 180.)) - 90.
268}
269
270pub fn altitude_from_mercator_z(z: f64, y: f64, circumference: Option<f64>) -> f64 {
273 z * circumference_at_latitude(lat_from_mercator_y(y), circumference)
274}
275
276pub fn mercator_lat_scale(lat: f64) -> f64 {
281 1. / cos((lat * PI) / 180.)
282}