Skip to content

Commit 57b2c39

Browse files
committed
feat(s2): Region
1 parent cfbb732 commit 57b2c39

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

s2/Cap.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { Interval as S1Interval } from '../s1/Interval'
1212
import { MinWidthMetric } from './Metric_constants'
1313
import * as cellid from './cellid'
1414
import type { CellID } from './cellid'
15+
import { Region } from './Region'
16+
import { Cell } from './Cell'
1517

1618
export const CENTER_POINT = Point.fromCoords(1.0, 0, 0)
1719

@@ -49,7 +51,7 @@ export const CENTER_POINT = Point.fromCoords(1.0, 0, 0)
4951
*
5052
* @beta incomplete
5153
*/
52-
export class Cap {
54+
export class Cap implements Region {
5355
center: Point
5456
rad: ChordAngle
5557

@@ -204,13 +206,86 @@ export class Cap {
204206
return chordangle.add(this.rad, other.rad) > Point.chordAngleBetweenPoints(this.center, other.center)
205207
}
206208

209+
/**
210+
* Reports whether the cap intersects the cell.
211+
*/
212+
intersectsCell(cell: Cell): boolean {
213+
// If the cap contains any cell vertex, return true.
214+
const vertices: Point[] = []
215+
for (let k = 0; k < 4; k++) {
216+
vertices[k] = cell.vertex(k)
217+
if (this.containsPoint(vertices[k])) return true
218+
}
219+
return this._intersects(cell, vertices)
220+
}
221+
222+
/**
223+
* Reports whether the cap intersects any point of the cell excluding
224+
* its vertices (which are assumed to already have been checked).
225+
*/
226+
private _intersects(cell: Cell, vertices: Point[]): boolean {
227+
// If the cap is a hemisphere or larger, the cell and the complement of the cap
228+
// are both convex. Therefore since no vertex of the cell is contained, no other
229+
// interior point of the cell is contained either.
230+
if (this.rad >= RIGHT_CHORDANGLE) return false
231+
232+
// We need to check for empty caps due to the center check just below.
233+
if (this.isEmpty()) return false
234+
235+
// Optimization: return true if the cell contains the cap center. This allows half
236+
// of the edge checks below to be skipped.
237+
if (cell.containsPoint(this.center)) return true
238+
239+
// At this point we know that the cell does not contain the cap center, and the cap
240+
// does not contain any cell vertex. The only way that they can intersect is if the
241+
// cap intersects the interior of some edge.
242+
const sin2Angle = chordangle.sin2(this.rad)
243+
for (let k = 0; k < 4; k++) {
244+
const edge = cell.edge(k).vector
245+
const dot = this.center.vector.dot(edge)
246+
if (dot > 0) {
247+
// The center is in the interior half-space defined by the edge. We do not need
248+
// to consider these edges, since if the cap intersects this edge then it also
249+
// intersects the edge on the opposite side of the cell, because the center is
250+
// not contained with the cell.
251+
continue
252+
}
253+
254+
// The Norm2() factor is necessary because "edge" is not normalized.
255+
if (dot * dot > sin2Angle * edge.norm2()) return false
256+
257+
// Otherwise, the great circle containing this edge intersects the interior of the cap. We just
258+
// need to check whether the point of closest approach occurs between the two edge endpoints.
259+
const dir = edge.cross(this.center.vector)
260+
if (dir.dot(vertices[k].vector) < 0 && dir.dot(vertices[(k + 1) & 3].vector) > 0) {
261+
return true
262+
}
263+
}
264+
265+
return false
266+
}
267+
207268
/**
208269
* Reports whether this cap contains the point.
209270
*/
210271
containsPoint(p: Point): boolean {
211272
return Point.chordAngleBetweenPoints(this.center, p) <= this.rad
212273
}
213274

275+
/** Reports whether the cap contains the given cell. */
276+
containsCell(cell: Cell): boolean {
277+
// If the cap does not contain all cell vertices, return false.
278+
const vertices: Point[] = []
279+
for (let k = 0; k < 4; k++) {
280+
vertices[k] = cell.vertex(k)
281+
if (!this.containsPoint(vertices[k])) {
282+
return false
283+
}
284+
}
285+
// Otherwise, return true if the complement of the cap does not intersect the cell.
286+
return !this.complement()._intersects(cell, vertices)
287+
}
288+
214289
/**
215290
* Reports whether the point is within the interior of this cap.
216291
*/

s2/Cell.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { NEGATIVE_CHORDANGLE, RIGHT_CHORDANGLE, STRAIGHT_CHORDANGLE } from '../s
2020
import { pointArea } from './point_measures'
2121
import { updateMaxDistance, updateMinDistance } from './edge_distances'
2222
import { maxChordAngle, minChordAngle } from './util'
23+
import { Region } from './Region'
2324

2425
const POLE_MIN_LAT = Math.asin(Math.sqrt(1.0 / 3)) - 0.5 * DBL_EPSILON
2526

@@ -28,7 +29,7 @@ const POLE_MIN_LAT = Math.asin(Math.sqrt(1.0 / 3)) - 0.5 * DBL_EPSILON
2829
* Unlike CellIDs, it supports efficient containment and intersection tests.
2930
* However, it is also a more expensive representation.
3031
*/
31-
export class Cell {
32+
export class Cell implements Region {
3233
face: number
3334
level: number
3435
orientation: number

s2/Region.d.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Cap } from './Cap'
2+
import { Rect } from './Rect'
3+
import { Cell } from './Cell'
4+
import { Point } from './Point'
5+
import type { CellID } from './cellid'
6+
7+
export interface Region {
8+
// Returns a bounding spherical cap. This is not guaranteed to be exact.
9+
capBound(): Cap
10+
11+
// Returns a bounding latitude-longitude rectangle that contains
12+
// the region. The bounds are not guaranteed to be tight.
13+
rectBound(): Rect
14+
15+
// Reports whether the region completely contains the given region.
16+
// It returns false if containment could not be determined.
17+
containsCell(c: Cell): boolean
18+
19+
// Reports whether the region intersects the given cell or
20+
// if intersection could not be determined. It returns false if the region
21+
// does not intersect.
22+
intersectsCell(c: Cell): boolean
23+
24+
// Reports whether the region contains the given point or not.
25+
// The point should be unit length, although some implementations may relax
26+
// this restriction.
27+
containsPoint(p: Point): boolean
28+
29+
// Returns a small collection of CellIDs whose union covers
30+
// the region. The cells are not sorted, may have redundancies (such as cells
31+
// that contain other cells), and may cover much more area than necessary.
32+
//
33+
// This method is not intended for direct use by client code. Clients
34+
// should typically use Covering, which has options to control the size and
35+
// accuracy of the covering. Alternatively, if you want a fast covering and
36+
// don't care about accuracy, consider calling FastCovering (which returns a
37+
// cleaned-up version of the covering computed by this method).
38+
//
39+
// CellUnionBound implementations should attempt to return a small
40+
// covering (ideally 4 cells or fewer) that covers the region and can be
41+
// computed quickly. The result is used by RegionCoverer as a starting
42+
// point for further refinement.
43+
cellUnionBound(): CellID[]
44+
}

0 commit comments

Comments
 (0)