1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 Torgeir Børresen <tb@starkad.no>
// Rust port of Google's S2 Geometry library — a derivative work, modified from
// the upstream Apache-2.0 source(s) below (Copyright Google Inc.). See LICENSE.
// - C++: google/s2geometry
// - Go: golang/geo
// - Java: google/s2-geometry-library-java
//! Convex hull computation on the sphere.
//!
//! [`ConvexHullQuery`] builds the convex hull of any collection of points,
//! polylines, loops, and polygons, returning a single convex [`Loop`].
//!
//! The convex hull is the smallest convex region on the sphere containing
//! all of the input geometry. Uses Andrew's monotone chain algorithm,
//! a variant of the Graham scan, in O(n log n) time.
//!
//! Corresponds to C++ `s2convex_hull_query.h`, Go `s2/convex_hull_query.go`.
use crate::s2::edge_distances;
use crate::s2::point::ortho;
use crate::s2::predicates::{self, Direction};
use crate::s2::{Cap, LatLng, Loop, Point, Rect};
/// Builds the convex hull of points, polylines, loops, and polygons.
///
/// Call the `add_*` methods to add geometry, then call [`convex_hull`](Self::convex_hull) to
/// compute the result. The query state is not cleared between calls; you can
/// keep adding geometry and recompute.
///
/// Not safe for concurrent use.
///
/// # Examples
///
/// ```
/// use s2rst::s2::convex_hull_query::ConvexHullQuery;
/// use s2rst::s2::LatLng;
///
/// let mut q = ConvexHullQuery::new();
/// q.add_point(LatLng::from_degrees(0.0, 0.0).to_point());
/// q.add_point(LatLng::from_degrees(0.0, 10.0).to_point());
/// q.add_point(LatLng::from_degrees(10.0, 5.0).to_point());
///
/// let hull = q.convex_hull();
/// assert!(hull.num_vertices() >= 3);
/// assert!(hull.area() > 0.0);
/// ```
#[derive(Debug)]
pub struct ConvexHullQuery {
bound: Rect,
points: Vec<Point>,
}
impl ConvexHullQuery {
/// Creates a new empty query.
pub fn new() -> Self {
ConvexHullQuery {
bound: Rect::empty(),
points: Vec::new(),
}
}
/// Adds a single point to the input geometry.
///
/// Points with a non-finite (NaN or infinite) coordinate are silently
/// ignored: they are not valid locations on the sphere and would otherwise
/// reach the exact-predicate path, where converting a non-finite value to
/// `ExactFloat` panics. Dropping them leaves a defined hull of the
/// remaining valid points.
pub fn add_point(&mut self, p: Point) {
if !is_finite(p) {
return;
}
self.bound = self.bound.add_point(LatLng::from_point(p));
self.points.push(p);
}
/// Adds a slice of points to the input geometry.
pub fn add_points(&mut self, points: &[Point]) {
for &p in points {
self.add_point(p);
}
}
/// Adds the vertices of a polyline. Matches C++ `AddPolyline`.
pub fn add_polyline(&mut self, polyline: &crate::s2::polyline::Polyline) {
self.add_points(polyline.vertices_vec());
}
/// Adds the vertices of a loop. Matches C++ `AddLoop`.
pub fn add_loop(&mut self, loop_: &Loop) {
self.add_points(loop_.vertices());
}
/// Adds all loop vertices of a polygon. Matches C++ `AddPolygon`.
pub fn add_polygon(&mut self, polygon: &crate::s2::polygon::Polygon) {
for i in 0..polygon.num_loops() {
self.add_loop(polygon.loop_at(i));
}
}
/// Returns a bounding cap for the input geometry.
pub fn cap_bound(&self) -> Cap {
self.bound.cap_bound()
}
/// Returns a [`Loop`] representing the convex hull of the input geometry.
///
/// Returns an empty loop if there is no geometry, a full loop if the
/// geometry spans more than half the sphere, or a small 3-vertex loop
/// for 1 or 2 input points.
pub fn convex_hull(&mut self) -> Loop {
let c = self.cap_bound();
if c.height() >= 1.0 {
// Bounding cap is not convex (> hemisphere). Return the full loop.
return Loop::full();
}
// Remove duplicates.
self.points.sort_unstable_by(|a, b| {
a.0.x
.total_cmp(&b.0.x)
.then(a.0.y.total_cmp(&b.0.y))
.then(a.0.z.total_cmp(&b.0.z))
});
self.points.dedup();
// Sort points in CCW order around an origin outside the convex hull.
let origin = ortho(c.center());
self.points.sort_unstable_by(|a, b| {
let sign = predicates::robust_sign(origin, *a, *b);
if sign == Direction::CounterClockwise {
std::cmp::Ordering::Less
} else if sign == Direction::Clockwise {
std::cmp::Ordering::Greater
} else {
std::cmp::Ordering::Equal
}
});
// Special cases for fewer than 3 points.
match self.points.len() {
0 => return Loop::empty(),
1 => return single_point_loop(self.points[0]),
2 => return single_edge_loop(self.points[0], self.points[1]),
_ => {}
}
// Generate the lower and upper halves of the convex hull.
let lower = monotone_chain(&self.points);
let mut reversed = self.points.clone();
reversed.reverse();
let upper = monotone_chain(&reversed);
// Combine chains, removing duplicate endpoints.
let mut hull: Vec<Point> = lower[..lower.len() - 1].to_vec();
hull.extend_from_slice(&upper[..upper.len() - 1]);
Loop::new(hull)
}
}
impl Default for ConvexHullQuery {
fn default() -> Self {
Self::new()
}
}
/// Returns `true` if every coordinate of `p` is finite (no NaN or infinity).
///
/// `Point` wraps a public `Vector`, so a caller can construct a non-finite
/// point directly (e.g. `Point::from_coords(f64::INFINITY, 0.0, 0.0)`, whose
/// normalization yields NaN). Such points are filtered out before they reach
/// the exact geometric predicates, which panic on non-finite input.
fn is_finite(p: Point) -> bool {
p.0.x.is_finite() && p.0.y.is_finite() && p.0.z.is_finite()
}
/// Andrew's monotone chain: selects the maximal subset of points such that
/// the edge chain makes only left (CCW) turns.
fn monotone_chain(points: &[Point]) -> Vec<Point> {
let mut output: Vec<Point> = Vec::new();
for &p in points {
while output.len() >= 2 {
let n = output.len();
if predicates::robust_sign(output[n - 2], output[n - 1], p)
== Direction::CounterClockwise
{
break;
}
output.pop();
}
output.push(p);
}
output
}
/// Constructs a 3-vertex polygon consisting of `p` and two nearby vertices.
fn single_point_loop(p: Point) -> Loop {
const OFFSET: f64 = 1e-15;
let d0 = ortho(p);
let d1 = Point(p.0.cross(d0.0));
let vertices = vec![
p,
Point((p.0 + d0.0 * OFFSET).normalize()),
Point((p.0 + d1.0 * OFFSET).normalize()),
];
Loop::new(vertices)
}
/// Constructs a loop consisting of two vertices and their midpoint.
fn single_edge_loop(a: Point, b: Point) -> Loop {
// If the points are exactly antipodal, return the full loop.
if (a.0 + b.0).norm2() == 0.0 {
return Loop::full();
}
let mid = edge_distances::interpolate(0.5, a, b);
let mut l = Loop::new(vec![a, b, mid]);
l.normalize();
l
}
// ─── Tests ──────────────────────────────────────────────────────────────
#[cfg(test)]
#[path = "convex_hull_query_tests.rs"]
mod convex_hull_query_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::s2::LatLng;
use crate::s2::region::Region;
fn p(lat: f64, lng: f64) -> Point {
LatLng::from_degrees(lat, lng).to_point()
}
#[test]
fn test_empty() {
let mut q = ConvexHullQuery::new();
let hull = q.convex_hull();
assert!(hull.is_empty_loop());
}
#[test]
fn test_single_point() {
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
let hull = q.convex_hull();
assert_eq!(hull.num_vertices(), 3);
}
#[test]
fn test_two_points() {
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 10.0));
let hull = q.convex_hull();
assert_eq!(hull.num_vertices(), 3);
}
#[test]
fn test_triangle() {
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 10.0));
q.add_point(p(10.0, 0.0));
let hull = q.convex_hull();
// Should return at least 3 vertices
assert!(hull.num_vertices() >= 3);
// Area should be positive
assert!(hull.area() > 0.0);
}
#[test]
fn test_square() {
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 10.0));
q.add_point(p(10.0, 0.0));
q.add_point(p(10.0, 10.0));
let hull = q.convex_hull();
assert!(hull.num_vertices() >= 3);
assert!(hull.area() > 0.0);
}
#[test]
fn test_duplicate_points() {
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 10.0));
q.add_point(p(0.0, 10.0));
let hull = q.convex_hull();
assert_eq!(hull.num_vertices(), 3);
}
#[test]
fn test_convex_hull_contains_points() {
let mut q = ConvexHullQuery::new();
let pts = vec![p(0.0, 0.0), p(0.0, 10.0), p(10.0, 5.0)];
for &pt in &pts {
q.add_point(pt);
}
let hull = q.convex_hull();
// Hull should contain a point well inside it
let inside = p(3.0, 5.0);
assert!(
hull.contains_point(&inside),
"convex hull should contain interior point"
);
}
#[test]
fn test_add_polyline() {
use crate::s2::polyline::Polyline;
let polyline = Polyline::new(vec![p(0.0, 0.0), p(0.0, 10.0), p(10.0, 5.0)]);
let mut q = ConvexHullQuery::new();
q.add_polyline(&polyline);
let hull = q.convex_hull();
assert!(hull.num_vertices() >= 3);
assert!(hull.area() > 0.0);
}
#[test]
fn test_add_loop() {
let loop_ = Loop::new(vec![p(0.0, 0.0), p(0.0, 10.0), p(10.0, 10.0), p(10.0, 0.0)]);
let mut q = ConvexHullQuery::new();
q.add_loop(&loop_);
let hull = q.convex_hull();
assert!(hull.num_vertices() >= 3);
assert!(hull.area() > 0.0);
}
#[test]
fn test_add_polygon() {
use crate::s2::polygon::Polygon;
let loop_ = Loop::new(vec![p(0.0, 0.0), p(0.0, 10.0), p(10.0, 10.0), p(10.0, 0.0)]);
let polygon = Polygon::from_loops(vec![loop_]);
let mut q = ConvexHullQuery::new();
q.add_polygon(&polygon);
let hull = q.convex_hull();
assert!(hull.num_vertices() >= 3);
}
#[test]
fn test_interior_point_removed() {
// Add 4 points where one is inside the triangle formed by the other 3.
let mut q = ConvexHullQuery::new();
q.add_point(p(0.0, 0.0));
q.add_point(p(0.0, 20.0));
q.add_point(p(20.0, 0.0));
q.add_point(p(5.0, 5.0)); // Inside the triangle
let hull = q.convex_hull();
// The hull should have exactly 3 vertices (the interior point removed).
assert_eq!(hull.num_vertices(), 3);
}
}