gistools/geometry/wm/
coords.rs

1use crate::space::EARTH_CIRCUMFERENCE;
2use core::f64::consts::{PI, TAU};
3use libm::{atan, cos, exp, floor, fmax, fmin, log, pow, sin, tan};
4
5/// 900913 (Web Mercator) constant
6pub const A: f64 = 6_378_137.0;
7/// 900913 (Web Mercator) max extent
8pub const MAXEXTENT: f64 = 20_037_508.342789244;
9/// 900913 (Web Mercator) maximum latitude
10pub const MAXLAT: f64 = 85.0511287798;
11
12/// The source of the coordinate inputs
13#[derive(Debug, Clone, PartialEq)]
14pub enum Source {
15    /// The WGS84 projection
16    WGS84,
17    /// The Google (900913) projection
18    Google,
19}
20
21/// Given a zoom and tilesize, build mercator positional attributes
22fn 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
27/// Convert Longitude and Latitude to a mercator pixel coordinate
28/// Return the mercator pixel coordinate
29pub 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
54/// Convert mercator pixel coordinates to Longitude and Latitude
55/// Return the Longitude and Latitude
56pub 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
66/// Convert Longitude and Latitude to a mercator x-y coordinates
67/// Return the mercator x-y coordinates
68pub 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    // if xy value is beyond maxextent (e.g. poles), return maxextent.
72    x = x.clamp(-MAXEXTENT, MAXEXTENT);
73    y = y.clamp(-MAXEXTENT, MAXEXTENT);
74
75    (x, y)
76}
77
78/// Convert mercator x-y coordinates to Longitude and Latitude
79/// Return the Longitude and Latitude
80pub 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
86/// Convert a pixel coordinate to a tile x-y coordinate
87/// Return the tile x-y
88pub 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
93/// Convert a tile x-y-z to a bbox of the form `[w, s, e, n]`
94/// Return the bbox
95pub 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
106/// Convert a lat-lon and zoom to the tile's x-y coordinates
107/// Return the tile x-y
108pub 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
113/// given a lon-lat and tile, find the offset in pixels
114/// return the tile xy pixel
115pub 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
130/// Convert a bbox of the form `[w, s, e, n]` to a bbox of the form `[w, s, e, n]`
131/// The result can be in lon-lat (WGS84) or WebMercator (900913)
132pub 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
148/// Convert a tile x-y-z to a bbox of the form `[w, s, e, n]`
149/// The result can be in lon-lat (WGS84) or WebMercator (900913)
150/// The default result is in WebMercator (900913)
151pub 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    // Convert xyz into bbox with srs WGS84
166    // if tmsStyle, the y is inverted
167    if tms_style {
168        y = pow(2., zoom as f64) - 1. - y;
169    }
170    // Use +y to make sure it's a number to avoid inadvertent concatenation.
171    let bl: (f64, f64) = (x * tile_size_f, (y + 1.) * tile_size_f);
172    // Use +x to make sure it's a number to avoid inadvertent concatenation.
173    let tr: (f64, f64) = ((x + 1.) * tile_size_f, y * tile_size_f);
174    // to pixel-coordinates
175    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
188/// Convert a bbox of the form `[w, s, e, n]` to a tile's bounding box
189/// in the form of { minX, maxX, minY, maxY }
190/// The bbox can be in lon-lat (WGS84) or WebMercator (900913)
191/// The default expectation is in WebMercator (900913)
192/// returns the tile's bounding box
193pub 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); // bottom left
206    let mut tr = (bbox.2, bbox.3); // top right
207
208    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
233/// The circumference at a line of latitude in meters.
234pub 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
239/// Convert longitude to mercator projection X-Value
240/// returns the X-Value
241pub fn lng_to_mercator_x(lng: f64) -> f64 {
242    (180.0 + lng) / 360.0
243}
244
245/// Convert latitude to mercator projection Y-Value
246/// returns the Y-Value
247pub fn lat_to_mercator_y(lat: f64) -> f64 {
248    (180. - (180. / PI) * log(tan(PI / 4. + (lat * PI) / 360.))) / 360.
249}
250
251/// Convert altitude to mercator projection Z-Value
252/// returns the Z-Value
253pub fn altitude_to_mercator_z(altitude: f64, lat: f64, circumference: Option<f64>) -> f64 {
254    altitude / circumference_at_latitude(lat, circumference)
255}
256
257/// Convert mercator projection's X-Value to longitude
258/// returns the longitude
259pub fn lng_from_mercator_x(x: f64) -> f64 {
260    x * 360. - 180.
261}
262
263/// Convert mercator projection's Y-Value to latitude
264/// returns the latitude
265pub 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
270/// Convert mercator projection's Z-Value to altitude
271/// returns the altitude
272pub 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
276/// Determine the Mercator scale factor for a given latitude, [See more](https://en.wikipedia.org/wiki/Mercator_projection#Scale_factor)
277///
278/// At the equator the scale factor will be 1, which increases at higher latitudes.
279/// returns the scale factor
280pub fn mercator_lat_scale(lat: f64) -> f64 {
281    1. / cos((lat * PI) / 180.)
282}