gistools/geometry/s2/
coords.rs

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