gistools/geometry/s2/
coords.rs

1use core::f64::consts::PI;
2
3use libm::{atan, floor, round, sqrt, tan};
4
5use crate::{
6    s2::{S2Point, K_FACE_UVW_AXES, K_FACE_UVW_FACES},
7    Point,
8};
9
10// This file contains documentation of the various coordinate systems used
11// throughout the library.  Most importantly, S2 defines a framework for
12// decomposing the unit sphere into a hierarchy of "cells".  Each cell is a
13// quadrilateral bounded by four geodesics.  The top level of the hierarchy is
14// obtained by projecting the six faces of a cube onto the unit sphere, and
15// lower levels are obtained by subdividing each cell into four children
16// recursively.  Cells are numbered such that sequentially increasing cells
17// follow a continuous space-filling curve over the entire sphere.  The
18// transformation is designed to make the cells at each level fairly uniform
19// in size.
20//
21//
22////////////////////////// S2Cell Decomposition /////////////////////////
23//
24// The following methods define the cube-to-sphere projection used by
25// the S2Cell decomposition.
26//
27// In the process of converting a latitude-longitude pair to a 64-bit cell
28// id, the following coordinate systems are used:
29//
30//  (id)
31//    An S2CellId is a 64-bit encoding of a face and a Hilbert curve position
32//    on that face.  The Hilbert curve position implicitly encodes both the
33//    position of a cell and its subdivision level (see s2cell_id.h).
34//
35//  (face, i, j)
36//    Leaf-cell coordinates.  "i" and "j" are integers in the range
37//    [0,(2**30)-1] that identify a particular leaf cell on the given face.
38//    The (i, j) coordinate system is right-handed on each face, and the
39//    faces are oriented such that Hilbert curves connect continuously from
40//    one face to the next.
41//
42//  (face, s, t)
43//    Cell-space coordinates.  "s" and "t" are real numbers in the range
44//    [0,1] that identify a point on the given face.  For example, the point
45//    (s, t) = (0.5, 0.5) corresponds to the center of the top-level face
46//    cell.  This point is also a vertex of exactly four cells at each
47//    subdivision level greater than zero.
48//
49//  (face, si, ti)
50//    Discrete cell-space coordinates.  These are obtained by multiplying
51//    "s" and "t" by 2**31 and rounding to the nearest u32eger.
52//    Discrete coordinates lie in the range [0,2**31].  This coordinate
53//    system can represent the edge and center positions of all cells with
54//    no loss of precision (including non-leaf cells).  In binary, each
55//    coordinate of a level-k cell center ends with a 1 followed by
56//    (30 - k) 0s.  The coordinates of its edges end with (at least)
57//    (31 - k) 0s.
58//
59//  (face, u, v)
60//    Cube-space coordinates in the range [-1,1].  To make the cells at each
61//    level more uniform in size after they are projected onto the sphere,
62//    we apply a nonlinear transformation of the form u=f(s), v=f(t).
63//    The (u, v) coordinates after this transformation give the actual
64//    coordinates on the cube face (modulo some 90 degree rotations) before
65//    it is projected onto the unit sphere.
66//
67//  (face, u, v, w)
68//    Per-face coordinate frame.  This is an extension of the (face, u, v)
69//    cube-space coordinates that adds a third axis "w" in the direction of
70//    the face normal.  It is always a right-handed 3D coordinate system.
71//    Cube-space coordinates can be converted to this frame by setting w=1,
72//    while (u,v,w) coordinates can be projected onto the cube face by
73//    dividing by w, i.e. (face, u/w, v/w).
74//
75//  (x, y, z)
76//    Direction vector (S2Point).  Direction vectors are not necessarily unit
77//    length, and are often chosen to be points on the biunit cube
78//    [-1,+1]x[-1,+1]x[-1,+1].  They can be be normalized to obtain the
79//    corresponding point on the unit sphere.
80//
81//  (lon, lat)
82//    Latitude and longitude (lonlat).  Latitudes must be between -90 and
83//    90 degrees inclusive, and longitudes must be between -180 and 180
84//    degrees inclusive.
85//
86// Note that the (i, j), (s, t), (si, ti), and (u, v) coordinate systems are
87// right-handed on all six faces.
88
89// S2 is a namespace for constants and simple utility functions that are used
90// throughout the S2 library.  The name "S2" is derived from the mathematical
91// symbol for the two-dimensional unit sphere (note that the "2" refers to the
92// dimension of the surface, not the space it is embedded in).
93
94/// This is the number of levels needed to specify a leaf cell.  This
95/// constant is defined here so that the S2::Metric class and the conversion
96/// functions below can be implemented without including s2cell_id.h.  Please
97/// see s2cell_id.h for other useful constants and conversion functions.
98pub const K_MAX_CELL_LEVEL: u8 = 30;
99
100/// The maximum index of a valid leaf cell plus one.  The range of valid leaf
101/// cell indices is [0..kLimitIJ-1].
102pub const K_LIMIT_IJ: u32 = 1 << K_MAX_CELL_LEVEL; // == S2CellId::kMaxSize
103
104/// The maximum value of an si- or ti-coordinate.  The range of valid (si,ti)
105/// values is [0..kMaxSiTi].
106pub const K_MAX_SI_TI: u32 = 1 << (K_MAX_CELL_LEVEL + 1);
107
108/// We have implemented three different projections from cell-space (s,t) to
109/// cube-space (u,v): linear, quadratic, and tangent.  They have the following
110/// tradeoffs:
111///
112///   Linear - This is the fastest transformation, but also produces the least
113///   uniform cell sizes.  Cell areas vary by a factor of about 5.2, with the
114///   largest cells at the center of each face and the smallest cells in
115///   the corners.
116///
117///   Tangent - Transforming the coordinates via atan() makes the cell sizes
118///   more uniform.  The areas vary by a maximum ratio of 1.4 as opposed to a
119///   maximum ratio of 5.2.  However, each call to atan() is about as expensive
120///   as all of the other calculations combined when converting from points to
121///   cell ids, i.e. it reduces performance by a factor of 3.
122///
123///   Quadratic - This is an approximation of the tangent projection that
124///   is much faster and produces cells that are almost as uniform in size.
125///   It is about 3 times faster than the tangent projection for converting
126///   cell ids to points or vice versa.  Cell areas vary by a maximum ratio of
127///   about 2.1.
128///
129/// Here is a table comparing the cell uniformity using each projection.  "Area
130/// ratio" is the maximum ratio over all subdivision levels of the largest cell
131/// area to the smallest cell area at that level, "edge ratio" is the maximum
132/// ratio of the longest edge of any cell to the shortest edge of any cell at
133/// the same level, and "diag ratio" is the ratio of the longest diagonal of
134/// any cell to the shortest diagonal of any cell at the same level.  "ToPoint"
135/// and "FromPoint" are the times in microseconds required to convert cell ids
136/// to and from points (unit vectors) respectively.  "ToPointRaw" is the time
137/// to convert to a non-unit-length vector, which is all that is needed for
138/// some purposes.
139///              Area    Edge    Diag   ToPointRaw  ToPoint  FromPoint
140///              Ratio   Ratio   Ratio             (microseconds)
141/// -------------------------------------------------------------------
142/// Linear:      5.200   2.117   2.959      0.020     0.087     0.085
143/// Tangent:     1.414   1.414   1.704      0.237     0.299     0.258
144/// Quadratic:   2.082   1.802   1.932      0.033     0.096     0.108
145///
146/// The worst-case cell aspect ratios are about the same with all three
147/// projections.  The maximum ratio of the longest edge to the shortest edge
148/// within the same cell is about 1.4 and the maximum ratio of the diagonals
149/// within the same cell is about 1.7.
150///
151/// NOTE: Currently Tan only has 1e-12 accuracy while Quadratic is within 1e-15.
152#[derive(Default)]
153pub enum S2Projection {
154    /// Linear projection
155    S2LinearProjection,
156    /// Tangent projection
157    S2TanProjection,
158    /// Quadratic projection
159    #[default]
160    S2QuadraticProjection,
161}
162
163/// Convert an s- or t-value to the corresponding u- or v-value.  This is
164/// a non-linear transformation from [0,1] to [-1,1] that attempts to
165/// make the cell sizes more uniform.
166pub fn st_to_uvlinear(s: f64) -> f64 {
167    2. * s - 1.
168}
169/// Convert an s- or t-value to the corresponding u- or v-value.  This is
170/// a non-linear transformation from [0,1] to [-1,1] that attempts to
171/// make the cell sizes more uniform.
172pub fn st_to_uvquadratic(s: f64) -> f64 {
173    if s >= 0.5 {
174        (1.0 / 3.0) * (4.0 * s * s - 1.0)
175    } else {
176        (1.0 / 3.0) * (1.0 - 4.0 * (1.0 - s) * (1.0 - s))
177    }
178}
179/// Convert an s- or t-value to the corresponding u- or v-value.  This is
180/// a non-linear transformation from [0,1] to [-1,1] that attempts to
181/// make the cell sizes more uniform.
182pub fn st_to_uvtan(s_: f64) -> f64 {
183    use core::f64::consts::PI;
184    // Unfortunately, tan(M_PI_4) is slightly less than 1.0.  This isn't due to
185    // a flaw in the implementation of tan(), it's because the derivative of
186    // tan(x) at x=pi/4 is 2, and it happens that the two adjacent floating
187    // point numbers on either side of the infinite-precision value of pi/4 have
188    // tangents that are slightly below and slightly above 1.0 when rounded to
189    // the nearest double-precision result.
190    let s = tan(PI / 2.0 * s_ - PI / 4.0);
191    s + (1.0 / (1_u64 << 53) as f64) * s
192}
193
194/// The default projection is quadratic
195#[cfg(feature = "quadratic")]
196pub const ST_TO_UV: fn(f64) -> f64 = st_to_uvquadratic;
197/// If settings are updated you can use the tangent projection
198#[cfg(feature = "tan")]
199pub const ST_TO_UV: fn(f64) -> f64 = st_to_uvtan;
200/// If settings are updated you can use the linear projection
201#[cfg(feature = "linear")]
202pub const ST_TO_UV: fn(f64) -> f64 = st_to_uvlinear;
203
204/// The inverse of the STtoUV transformation.  Note that it is not always
205/// true that UV_TO_ST(STtoUV(x)) == x due to numerical errors.
206pub fn uv_to_stlinear(u: f64) -> f64 {
207    0.5 * (u + 1.0)
208}
209/// The inverse of the STtoUV transformation.  Note that it is not always
210/// true that UV_TO_ST(STtoUV(x)) == x due to numerical errors.
211pub fn uv_to_st_quadratic(u: f64) -> f64 {
212    if u >= 0. {
213        0.5 * sqrt(1.0 + 3.0 * u)
214    } else {
215        1.0 - 0.5 * sqrt(1.0 - 3.0 * u)
216    }
217}
218/// The inverse of the STtoUV transformation.  Note that it is not always
219/// true that UV_TO_ST(STtoUV(x)) == x due to numerical errors.
220pub fn uv_to_st_tan(u: f64) -> f64 {
221    let a: f64 = atan(u);
222    (2.0 * (1.0 / PI)) * (a + (PI / 4.0))
223}
224
225/// The default projection is quadratic
226#[cfg(feature = "quadratic")]
227pub const UV_TO_ST: fn(f64) -> f64 = uv_to_st_quadratic;
228/// If settings are updated you can use the tangent projection
229#[cfg(feature = "tan")]
230pub const UV_TO_ST: fn(f64) -> f64 = uv_to_st_tan;
231/// If settings are updated you can use the linear projection
232#[cfg(feature = "linear")]
233pub const UV_TO_ST: fn(f64) -> f64 = uv_to_stlinear;
234
235/// Convert the i- or j-index of a leaf cell to the minimum corresponding s-
236/// or t-value contained by that cell.  The argument must be in the range
237/// [0..2**30], i.e. up to one position beyond the normal range of valid leaf
238/// cell indices.
239pub fn ij_to_st(i: u32) -> f64 {
240    if !(0..=K_LIMIT_IJ).contains(&i) {
241        unreachable!();
242    }
243    i as f64 / K_LIMIT_IJ as f64
244}
245
246/// Return the i- or j-index of the leaf cell containing the given
247/// s- or t-value.  If the argument is outside the range spanned by valid
248/// leaf cell indices, return the index of the closest valid leaf cell (i.e.,
249/// return values are clamped to the range of valid leaf cell indices).
250pub fn st_to_ij(s: f64) -> u32 {
251    (round(K_LIMIT_IJ as f64 * s - 0.5) as u32).clamp(0, K_LIMIT_IJ - 1)
252}
253
254/// Convert an si- or ti-value to the corresponding s- or t-value.
255pub fn si_ti_to_st(si: u32) -> f64 {
256    if si > K_MAX_SI_TI {
257        unreachable!();
258    }
259    (1.0 / K_LIMIT_IJ as f64) * si as f64
260}
261
262/// Return the si- or ti-coordinate that is nearest to the given s- or
263/// t-value.  The result may be outside the range of valid (si,ti)-values.
264pub fn st_to_si_ti(s: f64) -> u32 {
265    round(s * K_MAX_SI_TI as f64) as u32
266}
267
268/// Convert a direction vector (not necessarily unit length) to an (s,t) point.
269pub struct ST {
270    /// the s coordinate
271    pub s: f64,
272    /// the t coordinate
273    pub t: f64,
274}
275
276/// Convert an S2Point to an (s,t) point.
277pub fn to_face_st(p: &S2Point, face: u8) -> ST {
278    let uv = to_face_uv(p, face);
279    ST { s: UV_TO_ST(uv.u), t: UV_TO_ST(uv.v) }
280}
281
282/// A U-V coordinate pair.
283pub struct UV {
284    /// the u coordinate
285    pub u: f64,
286    /// the v coordinate
287    pub v: f64,
288}
289
290/// Convert an S2Point to an (u,v) point.
291pub fn to_face_uv(p: &S2Point, face: u8) -> UV {
292    let (valid, u, v) = face_xyz_to_uv(face, p);
293    debug_assert!(valid, "face_xyz_to_uv failed");
294    UV { u, v }
295}
296
297/// Convert (face, u, v) coordinates to a direction vector (not
298/// necessarily unit length).
299pub fn face_uv_to_xyz(face: u8, u: f64, v: f64) -> S2Point {
300    match face {
301        0 => S2Point::new(1.0, u, v),
302        1 => S2Point::new(-u, 1.0, v),
303        2 => S2Point::new(-u, -v, 1.0),
304        3 => S2Point::new(-1.0, -v, -u),
305        4 => S2Point::new(v, -1.0, -u),
306        _ => S2Point::new(v, u, -1.0),
307    }
308}
309
310/// Given a *valid* face for the given point p (meaning that dot product
311/// of p with the face normal is positive), return the corresponding
312/// u and v values (which may lie outside the range [-1,1]).
313/// Returns (pu, pv).
314pub fn valid_face_xyz_to_uv(face: u8, p: &S2Point) -> (f64, f64) {
315    if p.dot(&get_norm(face)) <= 0.0 {
316        unreachable!();
317    }
318    match face {
319        0 => (p.y / p.x, p.z / p.x),
320        1 => (-p.x / p.y, p.z / p.y),
321        2 => (-p.x / p.z, -p.y / p.z),
322        3 => (p.z / p.x, p.y / p.x),
323        4 => (p.z / p.y, -p.x / p.y),
324        _ => (-p.y / p.z, -p.x / p.z),
325    }
326}
327
328/// Return the face containing the given direction vector.  (For points on
329/// the boundary between faces, the result is arbitrary but repeatable.)
330pub fn get_face(p: &S2Point) -> u8 {
331    let mut face: u8 = p.largest_abs_component();
332    if p.face(face) < 0.0 {
333        face += 3;
334    }
335    face
336}
337
338/// Convert a direction vector (not necessarily unit length) to
339/// (face, u, v) coordinates.
340/// Returns (face, pu, pv)
341pub fn xyz_to_face_uv(p: &S2Point) -> (u8, f64, f64) {
342    let face: u8 = get_face(p);
343    let (pu, pv) = valid_face_xyz_to_uv(face, p);
344    (face, pu, pv)
345}
346
347/// Convert a direction vector (not necessarily unit length) to
348/// (face, u, v) coordinates.
349/// Returns (face, ps, pt)
350pub fn xyz_to_face_st(p: &S2Point) -> (u8, f64, f64) {
351    let face: u8 = get_face(p);
352    let (pu, pv) = valid_face_xyz_to_uv(face, p);
353    (face, UV_TO_ST(pu), UV_TO_ST(pv))
354}
355
356/// If the dot product of p with the given face normal is positive,
357/// set the corresponding u and v values (which may lie outside the range
358/// [-1,1]) and return true.  Otherwise return false.
359pub fn face_xyz_to_uv(face: u8, p: &S2Point) -> (bool, f64, f64) {
360    if face < 3 {
361        if p.face(face) <= 0.0 {
362            return (false, 0.0, 0.0);
363        }
364    } else if p.face(face - 3) >= 0.0 {
365        return (false, 0.0, 0.0);
366    }
367    let (pu, pv) = valid_face_xyz_to_uv(face, p);
368    (true, pu, pv)
369}
370
371/// Transform the given point P to the (u,v,w) coordinate frame of the given
372/// face (where the w-axis represents the face normal).
373pub fn face_xyz_to_uvw(face: u8, p: &S2Point) -> S2Point {
374    // The result coordinates are simply the dot products of P with the (u,v,w)
375    // axes for the given face (see kFaceUVWAxes).
376    match face {
377        0 => S2Point::new(p.y, p.z, p.x),
378        1 => S2Point::new(-p.x, p.z, p.y),
379        2 => S2Point::new(-p.x, -p.y, p.z),
380        3 => S2Point::new(-p.z, -p.y, -p.x),
381        4 => S2Point::new(-p.z, p.x, -p.y),
382        _ => S2Point::new(p.y, p.x, -p.z),
383    }
384}
385
386/// Return the right-handed normal (not necessarily unit length) for an
387/// edge in the direction of the positive v-axis at the given u-value on
388/// the given face.  (This vector is perpendicular to the plane through
389/// the sphere origin that contains the given edge.)
390pub fn get_u_norm(face: u8, u: f64) -> S2Point {
391    match face {
392        0 => S2Point::new(u, -1.0, 0.0),
393        1 => S2Point::new(1.0, u, 0.0),
394        2 => S2Point::new(1.0, 0.0, u),
395        3 => S2Point::new(-u, 0.0, 1.0),
396        4 => S2Point::new(0.0, -u, 1.0),
397        _ => S2Point::new(0.0, -1., -u),
398    }
399}
400
401/// Return the right-handed normal (not necessarily unit length) for an
402/// edge in the direction of the positive u-axis at the given v-value on
403/// the given face.
404pub fn get_v_norm(face: u8, v: f64) -> S2Point {
405    match face {
406        0 => S2Point::new(-v, 0.0, 1.0),
407        1 => S2Point::new(0.0, -v, 1.0),
408        2 => S2Point::new(0.0, -1.0, -v),
409        3 => S2Point::new(v, -1.0, 0.0),
410        4 => S2Point::new(1.0, v, 0.0),
411        _ => S2Point::new(1.0, 0.0, v),
412    }
413}
414
415/// Return the unit-length normal for the given face.
416pub fn get_norm(face: u8) -> S2Point {
417    get_uvw_axis(face, 2)
418}
419/// Return the u-axis for the given face.
420pub fn get_u_axis(face: u8) -> S2Point {
421    get_uvw_axis(face, 0)
422}
423/// Return the v-axis for the given face.
424pub fn get_v_axis(face: u8) -> S2Point {
425    get_uvw_axis(face, 1)
426}
427
428/// Return the given axis of the given face (u=0, v=1, w=2).
429pub fn get_uvw_axis(face: u8, axis: usize) -> S2Point {
430    let p = K_FACE_UVW_AXES[face as usize][axis];
431    S2Point::new(p[0], p[1], p[2])
432}
433
434/// With respect to the (u,v,w) coordinate system of a given face, return the
435/// face that lies in the given direction (negative=0, positive=1) of the
436/// given axis (u=0, v=1, w=2).  For example, GetUVWFace(4, 0, 1) returns the
437/// face that is adjacent to face 4 in the positive u-axis direction.
438pub fn get_uvw_face(face: u8, axis: usize, direction: usize) -> i32 {
439    if !(0..=5).contains(&face) {
440        unreachable!();
441    }
442    if !(0..=2).contains(&axis) {
443        unreachable!();
444    }
445    if !(0..=1).contains(&direction) {
446        unreachable!();
447    }
448    K_FACE_UVW_FACES[face as usize][axis][direction]
449}
450
451/// Convert a direction vector (not necessarily unit length) to
452/// (face, si, ti) coordinates and, if p is exactly equal to the center of a
453/// cell, return the level of this cell (31 otherwise as its outside the bounds of levels).
454/// Return (face, zoom, si, ti).
455pub fn xyz_to_face_si_ti(p: &S2Point) -> (u8, u8, u32, u32) {
456    let (face, u, v) = xyz_to_face_uv(p);
457    let si = st_to_si_ti(UV_TO_ST(u));
458    let ti = st_to_si_ti(UV_TO_ST(v));
459    // If the levels corresponding to si,ti are not equal, then p is not a cell
460    // center.  The si,ti values 0 and kMaxSiTi need to be handled specially
461    // because they do not correspond to cell centers at any valid level; they
462    // are mapped to level -1 by the code below.
463    // let level = K_MAX_CELL_LEVEL - (si.* | K_MAX_SI_TI);
464    let level: i32 = K_MAX_CELL_LEVEL as i32 - ((si | K_MAX_SI_TI).trailing_zeros() as i32);
465    // if (level < 0 or level != K_MAX_CELL_LEVEL - @ctz(ti.* | K_MAX_SI_TI)) return 31;
466    if level < 0 || level != K_MAX_CELL_LEVEL as i32 - ((ti | K_MAX_SI_TI).trailing_zeros() as i32)
467    {
468        return (face, 31, si, ti);
469    }
470    // In infinite precision, this test could be changed to ST == SiTi. However,
471    // due to rounding errors, UV_TO_ST(xyz_to_face_uv(face_uv_to_xyz(STtoUV(...)))) is
472    // not idempotent. On the other hand, center_raw is computed exactly the same
473    // way p was originally computed (if it is indeed the center of an S2Cell):
474    // the comparison can be exact.
475    // let center: &S2Point = FaceSiTitoXYZ(face, si, ti).Normalize();
476    let mut center = face_si_ti_to_xyz(face, si, ti);
477    center.normalize();
478    if *p == center {
479        (face, level as u8, si, ti)
480    } else {
481        (face, 31, si, ti)
482    }
483}
484
485/// Convert (face, si, ti) coordinates to a direction vector (not necessarily
486/// unit length).
487pub fn face_si_ti_to_xyz(face: u8, si: u32, ti: u32) -> S2Point {
488    let u = ST_TO_UV(si_ti_to_st(si));
489    let v = ST_TO_UV(si_ti_to_st(ti));
490    face_uv_to_xyz(face, u, v)
491}
492
493/// Convert an u-v-zoom coordinate to a tile coordinate
494/// returns the tile X-Y coordinate
495pub fn tile_xy_from_uv_zoom(u: f64, v: f64, zoom: u8) -> Point {
496    tile_xy_from_st_zoom(UV_TO_ST(u), UV_TO_ST(v), zoom)
497}
498
499/// Convert an s-t-zoom coordinate to a tile coordinate
500/// returns the tile X-Y coordinate
501pub fn tile_xy_from_st_zoom(s: f64, t: f64, zoom: u8) -> Point {
502    let division_factor = (2 / (1 << zoom)) as f64 * 0.5;
503
504    (floor(s / division_factor), floor(t / division_factor))
505}
506
507// **** TEST FUNCTIONS ****
508
509/// swap the i and j axes
510pub fn swap_axes(ij: usize) -> usize {
511    ((ij >> 1) & 1) + ((ij & 1) << 1)
512}
513
514/// invert the i and j axes
515pub fn invert_bits(ij: usize) -> usize {
516    ij ^ 3
517}