gistools/geometry/s2/
cap.rs

1use crate::geometry::{K_MAX_EDGE, K_MAX_LENGTH_2, S1Angle, S1ChordAngle, S2CellId, S2Point};
2use alloc::{vec, vec::Vec};
3use core::f64::consts::TAU;
4use libm::fmax;
5
6/// S2Cap represents a disc-shaped region defined by a center and radius.
7/// Technically this shape is called a "spherical cap" (rather than disc)
8/// because it is not planar; the cap represents a portion of the sphere that
9/// has been cut off by a plane.  The boundary of the cap is the circle defined
10/// by the intersection of the sphere and the plane.  For containment purposes,
11/// the cap is a closed set, i.e. it contains its boundary.
12///
13/// For the most part, you can use a spherical cap wherever you would use a
14/// disc in planar geometry.  The radius of the cap is measured along the
15/// surface of the sphere (rather than the straight-line distance through the
16/// interior).  Thus a cap of radius Pi/2 is a hemisphere, and a cap of radius
17/// Pi covers the entire sphere.
18///
19/// A cap can also be defined by its center point and height.  The height is
20/// simply the distance from the center point to the cutoff plane.  There is
21/// also support for empty and full caps, which contain no points and all
22/// points respectively.
23///
24/// This class is intended to be copied by value as desired.  It uses the
25/// default copy constructor and assignment operator, however it is not a
26/// "plain old datatype" (POD) because it has virtual functions.
27///
28/// Here are some useful relationships between the cap height (h), the cap
29/// radius (r), the maximum chord length from the cap's center (d), and the
30/// radius of cap's base (a).
31///
32/// $$    h = 1 - cos(r) $$
33/// $$      = 2 * sin^2(r/2) $$
34/// $$  d^2 = 2 * h $$
35/// $$      = a^2 + h^2 $$
36///
37/// ## Usage
38///
39/// Methods that are available:
40/// - [`S2Cap::new`]: Create a new S2Cap
41/// - [`S2Cap::empty`]: Return an empty cap, i.e. a cap that contains no points.
42/// - [`S2Cap::full`]: Return a full cap, i.e. a cap that contains all points.
43/// - [`S2Cap::area`]: Return the area of the cap.
44/// - [`S2Cap::is_empty`]: Return true if the cap is empty, i.e. it contains no points.
45/// - [`S2Cap::is_full`]: Return true if the cap is full, i.e. it contains all points.
46/// - [`S2Cap::height`]: Return the cap height.
47/// - [`S2Cap::from_s1_angle`]: Convenience function that creates a cap where the angle is expressed as an S1Angle.
48/// - [`S2Cap::from_s1_chord_angle`]: Convenience function that creates a cap where the angle is expressed as an S1ChordAngle.
49/// - [`S2Cap::from_s2_point`]: Convenience function that creates a cap containing a single point.
50/// - [`S2Cap::radius`]: Return the cap radius as an S1Angle.
51/// - [`S2Cap::contains_s2_point`]: Returns true if the cap contains the given point.
52/// - [`S2Cap::complement`]: Return the complement of the interior of the cap.
53/// - [`S2Cap::contains_s2_cell_vertex_count`]: Return count of vertices the cap contains for the given cell.
54/// - [`S2Cap::contains_s2_cell`]: Return true if the cap contains the given cell.
55/// - [`S2Cap::intersects_s2_cell_fast`]: Return true if the cap intersects "cell", given that the cap does contain any of the cell vertices.
56/// - [`S2Cap::intersects_s2_cell`]: Return true if the cap intersects "cell", given that the cap does contain any of the cell vertices (supplied in "vertices", an array of length 4).
57/// - [`S2Cap::get_intersecting_cells`]: Return the cells that intersect the cap.
58#[derive(Debug, Copy, Clone)]
59pub struct S2Cap<T = ()> {
60    /// the center of the cap
61    pub center: S2Point,
62    /// the radius of the cap
63    pub radius: S1ChordAngle,
64    /// the data associated with the cap
65    pub data: T,
66}
67impl<T> S2Cap<T>
68where
69    T: Clone,
70{
71    /// Constructs a cap with the given center and radius.
72    pub fn new(center: S2Point, radius: S1ChordAngle, data: T) -> Self {
73        S2Cap { center, radius, data }
74    }
75
76    /// Return an empty cap, i.e. a cap that contains no points.
77    pub fn empty(data: T) -> Self {
78        S2Cap::new(S2Point::new(1.0, 0.0, 0.0), S1ChordAngle::negative_angle(), data)
79    }
80
81    /// Return a full cap, i.e. a cap that contains all points.
82    pub fn full(data: T) -> Self {
83        S2Cap::new(S2Point::new(1.0, 0.0, 0.0), S1ChordAngle::straight_angle(), data)
84    }
85
86    /// Return the area of the cap.
87    pub fn area(&self) -> f64 {
88        TAU * fmax(0., self.height())
89    }
90
91    /// Return true if the cap is empty, i.e. it contains no points.
92    pub fn is_empty(&self) -> bool {
93        self.radius < 0.
94    }
95
96    /// Return true if the cap is full, i.e. it contains all points.
97    pub fn is_full(&self) -> bool {
98        self.radius >= 4.
99    }
100
101    /// Returns the height of the cap, i.e. the distance from the center point to
102    /// the cutoff plane.
103    pub fn height(&self) -> f64 {
104        0.5 * self.radius.length2
105    }
106
107    /// Constructs a cap with the given center and radius.  A negative radius
108    /// yields an empty cap; a radius of 180 degrees or more yields a full cap
109    /// (containing the entire sphere).  "center" should be unit length.
110    pub fn from_s1_angle(center: S2Point, radius: S1Angle, data: T) -> Self {
111        S2Cap::new(center, S1ChordAngle::from_angle(radius), data)
112    }
113
114    /// Constructs a cap where the angle is expressed as an S1ChordAngle.  This
115    /// constructor is more efficient than the one above.
116    pub fn from_s1_chord_angle(center: S2Point, radius: S1ChordAngle, data: T) -> Self {
117        S2Cap::new(center, radius, data)
118    }
119
120    /// Convenience function that creates a cap containing a single point. This
121    /// method is more efficient that the S2Cap(center, radius) constructor.
122    pub fn from_s2_point(center: S2Point, data: T) -> Self {
123        S2Cap::new(center, S1ChordAngle::zero(), data)
124    }
125
126    /// Return the cap radius as an S1Angle. (Note that the cap angle is stored
127    /// internally as an S1ChordAngle, so this method requires a trigonometric
128    /// operation and may yield a slightly different result than the value passed
129    /// to the (S2Point, S1Angle) constructor.)
130    pub fn radius(&self) -> S1Angle {
131        self.radius.to_angle()
132    }
133
134    /// Returns true if the cap contains the given point.
135    ///
136    /// NOTE: The point "p" should be a unit-length vector.
137    pub fn contains_s2_point(&self, p: &S2Point) -> bool {
138        S1ChordAngle::from_s2_points(&self.center, p) <= self.radius
139    }
140
141    /// Return the complement of the interior of the cap. A cap and its
142    /// complement have the same boundary but do not share any interior points.
143    /// The complement operator is not a bijection because the complement of a
144    /// singleton cap (containing a single point) is the same as the complement
145    /// of an empty cap.
146    pub fn complement(&self) -> S2Cap<T> {
147        // The complement of a full cap is an empty cap, not a singleton.
148        // Also make sure that the complement of an empty cap is full.
149        if self.is_full() {
150            return S2Cap::empty(self.data.clone());
151        } else if self.is_empty() {
152            return S2Cap::full(self.data.clone());
153        }
154        S2Cap::new(
155            self.center.invert(),
156            S1ChordAngle::from_length2(K_MAX_LENGTH_2 - self.radius.length2),
157            self.data.clone(),
158        )
159    }
160
161    /// Return count of vertices the cap contains for the given cell.
162    pub fn contains_s2_cell_vertex_count(&self, cell: S2CellId) -> usize {
163        // If the cap does not contain all cell vertices, return false.
164        let mut count = 0;
165        for vertex in cell.get_vertices() {
166            if self.contains_s2_point(&vertex) {
167                count += 1;
168            }
169        }
170        count
171    }
172
173    /// Return true if the cap contains the given cell.
174    pub fn contains_s2_cell(&self, cell: S2CellId) -> bool {
175        // If the cap does not contain all cell vertices, return false.
176        // We check the vertices before taking the complement() because we can't
177        // accurately represent the complement of a very small cap (a height
178        // of 2-epsilon is rounded off to 2).
179        let vertices = cell.get_vertices();
180        for vertex in vertices {
181            if !self.contains_s2_point(&vertex) {
182                return false;
183            }
184        }
185        // Otherwise, return true if the complement of the cap does not intersect
186        // the cell.  (This test is slightly conservative, because technically we
187        !self.complement().intersects_s2_cell(cell, &vertices)
188    }
189
190    /// Return true if the cap intersects "cell", given that the cap does intersect
191    /// any of the cell vertices or edges.
192    pub fn intersects_s2_cell_fast(&self, cell: S2CellId) -> bool {
193        // If the cap contains any cell vertex, return true.
194        let vertices = cell.get_vertices();
195        for vertex in vertices {
196            if self.contains_s2_point(&vertex) {
197                return true;
198            }
199        }
200
201        self.intersects_s2_cell(cell, &vertices)
202    }
203
204    /// Return true if the cap intersects "cell", given that the cap does contain
205    /// any of the cell vertices (supplied in "vertices", an array of length 4).
206    /// Return true if this cap intersects any point of 'cell' excluding its
207    /// vertices (which are assumed to already have been checked).
208    pub fn intersects_s2_cell(&self, cell: S2CellId, vertices: &[S2Point; 4]) -> bool {
209        // If the cap is a hemisphere or larger, the cell and the complement of the
210        // cap are both convex.  Therefore since no vertex of the cell is contained,
211        // no other interior point of the cell is contained either.
212        if self.radius >= S1ChordAngle::right_angle() {
213            return false;
214        }
215        // We need to check for empty caps due to the center check just below.
216        if self.is_empty() {
217            return false;
218        }
219        // Optimization: return true if the cell contains the cap center.  (This
220        // allows half of the edge checks below to be skipped.)
221        if cell.contains_s2point(&self.center) {
222            return true;
223        }
224
225        // At this point we know that the cell does not contain the cap center,
226        // and the cap does not contain any cell vertex.  The only way that they
227        // can intersect is if the cap intersects the interior of some edge.
228        //   let sin2Angle = chordAngleSin2(self.radius);
229        let sin2_angle = self.radius.chord_angle_sin2();
230        let edges: [S2Point; 4] = cell.get_edges_raw();
231        //   for (let k = 0; k < 4; k += 1) {
232        for k in 0..4 {
233            let edge = edges[k];
234            let dot = self.center.dot(&edge);
235            if dot > 0. {
236                // The center is in the interior half-space defined by the edge.  We don't
237                // need to consider these edges, since if the cap intersects this edge
238                // then it also intersects the edge on the opposite side of the cell
239                // (because we know the center is not contained with the cell).
240                continue;
241            }
242            // The Norm2() factor is necessary because "edge" is not normalized.
243            if dot * dot > sin2_angle * edge.norm2() {
244                return false; // Entire cap is on the exterior side of this edge.
245            }
246            // Otherwise, the great circle containing this edge intersects
247            // the interior of the cap.  We just need to check whether the point
248            // of closest approach occurs between the two edge endpoints.
249            let dir = edge.cross(&self.center);
250            if dir.dot(&vertices[k]) < 0. && dir.dot(&vertices[(k + 1) & 3]) > 0. {
251                return true;
252            }
253        }
254
255        false
256    }
257
258    /// Return the cells that intersect the cap.
259    pub fn get_intersecting_cells(&self) -> Vec<S2CellId> {
260        let mut res: Vec<S2CellId> = vec![];
261        // Find appropriate max depth for radius
262        // while loop:
263        // - if cell corners are all in cap, store in res.
264        // - if cell corners are all outside cap, move on.
265        // - if even one cell corner is outside cap:
266        // - - if reached max depth, store in res
267        // - - if not reached max depth, store children in cache for another pass
268
269        if self.is_empty() {
270            return res;
271        }
272        let mut queue: Vec<S2CellId> = vec![
273            S2CellId::from_face(0),
274            S2CellId::from_face(1),
275            S2CellId::from_face(2),
276            S2CellId::from_face(3),
277            S2CellId::from_face(4),
278            S2CellId::from_face(5),
279        ];
280        if self.is_full() {
281            return queue;
282        }
283        let max_depth = K_MAX_EDGE.get_closest_level(self.radius.to_angle().radians) as u8;
284        loop {
285            let Some(cell) = queue.pop() else {
286                break;
287            };
288            let vertex_count = self.contains_s2_cell_vertex_count(cell);
289            let max_level = cell.level() >= max_depth;
290            if vertex_count == 4 || (vertex_count > 0 && max_level) {
291                res.push(cell);
292            } else if vertex_count == 0 && !max_level {
293                // if cap center is in the cell, then we check all children because the cell is larger than the cap
294                if cell.contains_s2point(&self.center) {
295                    for child in cell.children(None) {
296                        queue.push(child);
297                    }
298                } else {
299                    continue;
300                };
301            } else if max_level {
302                continue;
303            } else {
304                for child in cell.children(None) {
305                    queue.push(child);
306                }
307            }
308        }
309
310        res
311    }
312}