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}