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}