Skip to content

Commit 4d7a221

Browse files
committed
feat(s2): cellid
1 parent c71b8d0 commit 4d7a221

4 files changed

Lines changed: 839 additions & 137 deletions

File tree

s2/cellid.ts

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { faceSiTiToXYZ, faceUVToXYZ, siTiToST, stToUV, uvToST, xyzToFaceUV } fro
44
import { FACE_BITS, MAX_LEVEL, MAX_SIZE, NUM_FACES, POS_BITS, WRAP_OFFSET } from './cellid_constants'
55
import { LatLng } from './LatLng'
66
import { Point } from './Point'
7-
import { clampInt } from './util'
7+
import { abs, clampInt } from './util'
88
import { Vector } from '../r3/Vector'
99
import { Interval } from '../r1/Interval'
1010
import { Rect } from '../r2/Rect'
@@ -40,12 +40,12 @@ import { Rect as R2Rect } from '../r2/Rect'
4040
export type CellID = bigint
4141

4242
/**
43-
* An invalid cell ID guaranteed to be larger than any
44-
* valid cell ID. It is used primarily by ShapeIndex. The value is also used
45-
* by some S2 types when encoding data.
43+
* An invalid cell ID guaranteed to be larger than any valid cell ID.
44+
* It is used primarily by ShapeIndex.
45+
* The value is also used by some S2 types when encoding data.
4646
* Note that the sentinel's RangeMin == RangeMax == itself.
4747
*/
48-
export const SentinelCellID = 18446744073709551615n
48+
export const SentinelCellID = (1n << 64n) - 1n
4949

5050
/**
5151
* Returns the cube face for this cell id, in the range [0,5].
@@ -58,7 +58,7 @@ export const face = (ci: CellID): number => {
5858
* Returns the position along the Hilbert curve of this cell id, in the range [0,2^POS_BITS-1].
5959
*/
6060
export const pos = (ci: CellID): CellID => {
61-
return ci & (~0n >> BigInt(FACE_BITS))
61+
return ci & (SentinelCellID >> BigInt(FACE_BITS))
6262
}
6363

6464
/**
@@ -101,6 +101,15 @@ export const valid = (ci: CellID): boolean => {
101101
*/
102102
export const isLeaf = (ci: CellID): boolean => (ci & 1n) != 0n
103103

104+
/**
105+
* Returns the child position (0..3) of this cell's ancestor at the given level, relative to its parent.
106+
* The argument should be in the range 1..MaxLevel.
107+
* For example, childPosition(1) returns the position of this cell's level-1 ancestor within its top-level face cell.
108+
*/
109+
export const childPosition = (ci: CellID, level: number): number => {
110+
return Number(ci >> (2n * BigInt(MAX_LEVEL - level) + 1n)) & 0b11
111+
}
112+
104113
// Bitwise
105114

106115
/**
@@ -213,7 +222,7 @@ export const intersects = (ci: CellID, oci: CellID) => {
213222
* Returns a hex-encoded string of the uint64 cell id, with leading zeros included but trailing zeros stripped
214223
*/
215224
export const toToken = (ci: CellID): string => {
216-
const s = ci.toString(16).replace(/0+$/, '')
225+
const s = ci.toString(16).padStart(16, '0').replace(/0+$/, '')
217226
if (s.length === 0) return 'X'
218227
return s
219228
}
@@ -224,11 +233,22 @@ export const toToken = (ci: CellID): string => {
224233
*/
225234
export const fromToken = (t: string): CellID => {
226235
if (t.length > 16) return 0n
236+
if (!/^[A-F0-9]+$/i.test(t)) return 0n
227237
let ci = BigInt('0x' + t)
228238
if (t.length < 16) ci = ci << BigInt(4 * (16 - t.length))
229239
return ci
230240
}
231241

242+
/**
243+
* Returns the string representation of the cell ID in the form "1/3210".
244+
*/
245+
export const toString = (ci: CellID): string => {
246+
if (!valid(ci)) return `Invalid: ${ci.toString(16)}`
247+
let result = `${face(ci)}/`
248+
for (let l = 1; l <= level(ci); l++) result += childPosition(ci, l)
249+
return result
250+
}
251+
232252
/**
233253
* Converts a string in the form "1/3210" to a CellID.
234254
* @category Constructors
@@ -238,12 +258,12 @@ export const fromString = (s: string): CellID => {
238258
if (level < 0 || level > MAX_LEVEL) return 0n
239259

240260
const face = parseInt(s[0], 10)
241-
if (face < 0 || face > 5 || s[1] !== '/') return 0n
261+
if (isNaN(face) || face < 0 || face > 5 || s[1] !== '/') return 0n
242262

243263
let cid = fromFace(face)
244264
for (let i = 2; i < s.length; i++) {
245265
const childPos = parseInt(s[i], 10)
246-
if (childPos < 0 || childPos > 3) return 0n
266+
if (isNaN(childPos) || childPos < 0 || childPos > 3) return 0n
247267
cid = children(cid)[childPos]
248268
}
249269

@@ -303,7 +323,7 @@ export const stToIJ = (s: number): number => {
303323
}
304324

305325
export const sizeIJ = (level: number): number => {
306-
return 1 << (MAX_LEVEL - level)
326+
return 1 << clampInt(MAX_LEVEL - level, 0, MAX_LEVEL)
307327
}
308328

309329
/** Returns the edge length of this CellID in (s,t)-space at the given level. */
@@ -471,6 +491,16 @@ export const ijLevelToBoundUV = (i: number, j: number, level: number): Rect => {
471491
)
472492
}
473493

494+
/**
495+
* Returns the number of steps along the Hilbert curve that this cell is from the first node
496+
* in the S2 hierarchy at our level. (i.e., FromFace(0).ChildBeginAtLevel(ci.Level())).
497+
* This is analogous to Pos(), but for this cell's level.
498+
* The return value is always non-negative.
499+
*/
500+
export const distanceFromBegin = (ci: CellID): bigint => {
501+
return ci >> (2n * BigInt(MAX_LEVEL - level(ci)) + 1n)
502+
}
503+
474504
/**
475505
* Returns an unnormalized r3 vector from the origin through the center
476506
* of the s2 cell on the sphere.
@@ -603,12 +633,12 @@ export const childEndAtLevel = (ci: CellID, level: number): CellID => {
603633
* or ChildBeginAtLevel and ChildEndAtLevel.
604634
*/
605635
export const next = (ci: CellID): CellID => {
606-
return ci + (lsb(ci) << 1n)
636+
return (ci + (lsb(ci) << 1n)) & SentinelCellID
607637
}
608638

609639
/** Returns the previous cell along the Hilbert curve. */
610640
export const prev = (ci: CellID): CellID => {
611-
return ci - (lsb(ci) << 1n)
641+
return (ci - (lsb(ci) << 1n)) & SentinelCellID
612642
}
613643

614644
/**
@@ -618,7 +648,7 @@ export const prev = (ci: CellID): CellID => {
618648
export const nextWrap = (ci: CellID): CellID => {
619649
const n = next(ci)
620650
if (n < WRAP_OFFSET) return n
621-
return n - WRAP_OFFSET
651+
return (n - WRAP_OFFSET) & SentinelCellID
622652
}
623653

624654
/**
@@ -628,7 +658,44 @@ export const nextWrap = (ci: CellID): CellID => {
628658
export const prevWrap = (ci: CellID): CellID => {
629659
const p = prev(ci)
630660
if (p < WRAP_OFFSET) return p
631-
return p + WRAP_OFFSET
661+
return (p + WRAP_OFFSET) & SentinelCellID
662+
}
663+
664+
/**
665+
* Advances or retreats the indicated number of steps along the
666+
* Hilbert curve at the current level and returns the new position. The
667+
* position wraps between the first and last faces as necessary.
668+
*/
669+
export const advanceWrap = (ci: CellID, steps: bigint): CellID => {
670+
if (steps === 0n) return ci
671+
672+
// We clamp the number of steps if necessary to ensure that we do not
673+
// advance past the End() or before the Begin() of this level.
674+
const shift = BigInt(2 * (MAX_LEVEL - level(ci)) + 1)
675+
if (steps < 0n) {
676+
const min = -(ci >> shift)
677+
if (steps < min) {
678+
const wrap = WRAP_OFFSET >> shift
679+
steps %= wrap
680+
if (steps < min) {
681+
steps += wrap
682+
}
683+
}
684+
} else {
685+
// Unlike Advance(), we don't want to return End(level).
686+
const max = (WRAP_OFFSET - ci) >> shift
687+
if (steps > max) {
688+
const wrap = WRAP_OFFSET >> shift
689+
steps %= wrap
690+
if (steps > max) {
691+
steps -= wrap
692+
}
693+
}
694+
}
695+
696+
// If steps is negative, then shifting it left has undefined behavior.
697+
// Cast to uint64 for a 2's complement answer.
698+
return ci + (steps << abs(shift))
632699
}
633700

634701
/**
@@ -640,9 +707,7 @@ export const commonAncestorLevel = (ci: CellID, other: CellID): [number, boolean
640707
if (bits < lsb(other)) bits = lsb(other)
641708

642709
const msbPos = findMSBSetNonZero64(bits)
643-
if (msbPos > 60) {
644-
return [0, false]
645-
}
710+
if (msbPos > 60) return [0, false]
646711
return [(60 - msbPos) >> 1, true]
647712
}
648713

@@ -691,20 +756,20 @@ export const maxTile = (ci: CellID, limit: CellID): CellID => {
691756
* Hilbert curve at the current level, and returns the new position. The
692757
* position is never advanced past End() or before Begin().
693758
*/
694-
export const advance = (ci: CellID, steps: CellID): CellID => {
759+
export const advance = (ci: CellID, steps: bigint): CellID => {
695760
if (steps === 0n) return ci
696761

697762
// We clamp the number of steps if necessary to ensure that we do not
698763
// advance past the End() or before the Begin() of this level.
699764
const stepShift = BigInt(2 * (MAX_LEVEL - level(ci)) + 1)
700765

701766
if (steps < 0n) {
702-
const minSteps = -((ci >> stepShift) as bigint)
767+
const minSteps = -(ci >> stepShift)
703768
if (steps < minSteps) {
704769
steps = minSteps
705770
}
706771
} else {
707-
const maxSteps = ((WRAP_OFFSET + lsb(ci) - ci) >> stepShift) as bigint
772+
const maxSteps = (WRAP_OFFSET + lsb(ci) - ci) >> stepShift
708773
if (steps > maxSteps) {
709774
steps = maxSteps
710775
}

s2/cellid_extra_test.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { test, describe } from 'node:test'
2+
import { equal, ok } from 'node:assert/strict'
3+
import * as cellid from './cellid'
4+
5+
describe('s2.cellid', () => {
6+
test('face', (t) => {
7+
equal(cellid.face(0b0001111111111111111111111111111111111111111111111111111111111111n), 0)
8+
equal(cellid.face(0b0011111111111111111111111111111111111111111111111111111111111111n), 1)
9+
equal(cellid.face(0b0101111111111111111111111111111111111111111111111111111111111111n), 2)
10+
equal(cellid.face(0b0111111111111111111111111111111111111111111111111111111111111111n), 3)
11+
equal(cellid.face(0b1001111111111111111111111111111111111111111111111111111111111111n), 4)
12+
equal(cellid.face(0b1011111111111111111111111111111111111111111111111111111111111111n), 5)
13+
})
14+
15+
test('level', (t) => {
16+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000000001n), 30)
17+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000000100n), 29)
18+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000010000n), 28)
19+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000001000000n), 27)
20+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000100000000n), 26)
21+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000000010000000000n), 25)
22+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000001000000000000n), 24)
23+
equal(cellid.level(0b0000000000000000000000000000000000000000000000000100000000000000n), 23)
24+
equal(cellid.level(0b0000000000000000000000000000000000000000000000010000000000000000n), 22)
25+
equal(cellid.level(0b0000000000000000000000000000000000000000000001000000000000000000n), 21)
26+
equal(cellid.level(0b0000000000000000000000000000000000000000000100000000000000000000n), 20)
27+
equal(cellid.level(0b0000000000000000000000000000000000000000010000000000000000000000n), 19)
28+
equal(cellid.level(0b0000000000000000000000000000000000000001000000000000000000000000n), 18)
29+
equal(cellid.level(0b0000000000000000000000000000000000000100000000000000000000000000n), 17)
30+
equal(cellid.level(0b0000000000000000000000000000000000010000000000000000000000000000n), 16)
31+
equal(cellid.level(0b0000000000000000000000000000000001000000000000000000000000000000n), 15)
32+
equal(cellid.level(0b0000000000000000000000000000000100000000000000000000000000000000n), 14)
33+
equal(cellid.level(0b0000000000000000000000000000010000000000000000000000000000000000n), 13)
34+
equal(cellid.level(0b0000000000000000000000000001000000000000000000000000000000000000n), 12)
35+
equal(cellid.level(0b0000000000000000000000000100000000000000000000000000000000000000n), 11)
36+
equal(cellid.level(0b0000000000000000000000010000000000000000000000000000000000000000n), 10)
37+
equal(cellid.level(0b0000000000000000000001000000000000000000000000000000000000000000n), 9)
38+
equal(cellid.level(0b0000000000000000000100000000000000000000000000000000000000000000n), 8)
39+
equal(cellid.level(0b0000000000000000010000000000000000000000000000000000000000000000n), 7)
40+
equal(cellid.level(0b0000000000000001000000000000000000000000000000000000000000000000n), 6)
41+
equal(cellid.level(0b0000000000000100000000000000000000000000000000000000000000000000n), 5)
42+
equal(cellid.level(0b0000000000010000000000000000000000000000000000000000000000000000n), 4)
43+
equal(cellid.level(0b0000000001000000000000000000000000000000000000000000000000000000n), 3)
44+
equal(cellid.level(0b0000000100000000000000000000000000000000000000000000000000000000n), 2)
45+
equal(cellid.level(0b0000010000000000000000000000000000000000000000000000000000000000n), 1)
46+
equal(cellid.level(0b0001000000000000000000000000000000000000000000000000000000000000n), 0)
47+
})
48+
49+
test('parent', (t) => {
50+
const c1 = /* */ 0b0011110000111100001111000011110000000000000000000000000000000000n
51+
equal(cellid.parent(c1, 9), 0b0011110000111100001111000000000000000000000000000000000000000000n)
52+
equal(cellid.parent(c1, 5), 0b0011110000111100000000000000000000000000000000000000000000000000n)
53+
equal(cellid.parent(c1, 1), 0b0011110000000000000000000000000000000000000000000000000000000000n)
54+
equal(c1, /* */ 0b0011110000111100001111000011110000000000000000000000000000000000n)
55+
56+
const c2 = /* */ 0b0011110000111100001111000011110000111100001111000011110000111101n
57+
equal(cellid.parent(c2, 30), 0b0011110000111100001111000011110000111100001111000011110000111101n)
58+
equal(cellid.parent(c2, 29), 0b0011110000111100001111000011110000111100001111000011110000111100n)
59+
equal(cellid.parent(c2, 15), 0b0011110000111100001111000011110001000000000000000000000000000000n)
60+
equal(cellid.parent(c2, 14), 0b0011110000111100001111000011110100000000000000000000000000000000n)
61+
equal(cellid.parent(c2, 1), 0b0011110000000000000000000000000000000000000000000000000000000000n)
62+
equal(cellid.parent(c2, 0), 0b0011000000000000000000000000000000000000000000000000000000000000n)
63+
})
64+
65+
test('range', (t) => {
66+
const c1 = /* */ 0b0011110000111100000001000000000000000000000000000000000000000000n
67+
equal(cellid.rangeMin(c1), 0b0011110000111100000000000000000000000000000000000000000000000001n)
68+
equal(cellid.rangeMax(c1), 0b0011110000111100000001111111111111111111111111111111111111111111n)
69+
70+
const c2 = /* */ 0b0011110000111100001111000011110001000000000000000000000000000000n
71+
equal(cellid.rangeMin(c2), 0b0011110000111100001111000011110000000000000000000000000000000001n)
72+
equal(cellid.rangeMax(c2), 0b0011110000111100001111000011110001111111111111111111111111111111n)
73+
})
74+
75+
test('contains', (t) => {
76+
const c1 = 0b0011110000111100001111000011110000111100001111000011110000111101n
77+
ok(!cellid.contains(c1, cellid.parent(c1, 10)))
78+
ok(cellid.contains(cellid.parent(c1, 10), c1))
79+
80+
const c2 = 0b1011111111111111111111111111111111111111111111111111111111111111n
81+
ok(!cellid.contains(c2, cellid.parent(c2, 10)))
82+
ok(cellid.contains(cellid.parent(c2, 10), c2))
83+
84+
ok(!cellid.contains(c1, c2))
85+
ok(!cellid.contains(c2, c1))
86+
})
87+
88+
test('intersects', (t) => {
89+
const c1 = 0b0011110000111100001111000011110000111100001111000011110000111101n
90+
ok(cellid.intersects(c1, cellid.parent(c1, 10)))
91+
ok(cellid.intersects(cellid.parent(c1, 10), c1))
92+
93+
const c2 = 0b1011111111111111111111111111111111111111111111111111111111111111n
94+
ok(cellid.intersects(c2, cellid.parent(c2, 10)))
95+
ok(cellid.intersects(cellid.parent(c2, 10), c2))
96+
97+
ok(cellid.intersects(c1, c1))
98+
ok(!cellid.intersects(c1, c2))
99+
ok(!cellid.intersects(c1, c2))
100+
})
101+
102+
test('valid', (t) => {
103+
ok(cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000001n))
104+
ok(!cellid.valid(0b1110000000000000000000000000000000000000000000000000000000000001n), 'face')
105+
ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000010n), 'level')
106+
107+
ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000010n))
108+
ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000001000n))
109+
})
110+
111+
test('fromFace', (t) => {
112+
equal(cellid.fromFace(0), 0b0001000000000000000000000000000000000000000000000000000000000000n)
113+
equal(cellid.fromFace(1), 0b0011000000000000000000000000000000000000000000000000000000000000n)
114+
equal(cellid.fromFace(2), 0b0101000000000000000000000000000000000000000000000000000000000000n)
115+
equal(cellid.fromFace(3), 0b0111000000000000000000000000000000000000000000000000000000000000n)
116+
equal(cellid.fromFace(4), 0b1001000000000000000000000000000000000000000000000000000000000000n)
117+
equal(cellid.fromFace(5), 0b1011000000000000000000000000000000000000000000000000000000000000n)
118+
})
119+
120+
test('fromFaceIJWrap', (t) => {
121+
equal(cellid.fromFaceIJWrap(0, 1073741824, -1), 13066443718877599061n)
122+
})
123+
})

0 commit comments

Comments
 (0)