@takram/three-geospatial 0.0.1-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/build/index.cjs +43 -0
- package/build/index.js +932 -0
- package/build/r3f.cjs +1 -0
- package/build/r3f.js +38 -0
- package/build/shared.cjs +1 -0
- package/build/shared.js +198 -0
- package/package.json +42 -0
- package/src/ArrayBufferLoader.ts +35 -0
- package/src/DataLoader.ts +114 -0
- package/src/Ellipsoid.ts +128 -0
- package/src/EllipsoidGeometry.ts +107 -0
- package/src/Geodetic.ts +160 -0
- package/src/PointOfView.ts +169 -0
- package/src/Rectangle.ts +97 -0
- package/src/TileCoordinate.test.ts +38 -0
- package/src/TileCoordinate.ts +112 -0
- package/src/TilingScheme.test.ts +63 -0
- package/src/TilingScheme.ts +76 -0
- package/src/TypedArrayLoader.ts +53 -0
- package/src/assertions.ts +13 -0
- package/src/bufferGeometry.ts +62 -0
- package/src/helpers/projectOnEllipsoidSurface.ts +72 -0
- package/src/index.ts +25 -0
- package/src/math.ts +41 -0
- package/src/r3f/EastNorthUpFrame.tsx +52 -0
- package/src/r3f/EllipsoidMesh.tsx +36 -0
- package/src/r3f/index.ts +2 -0
- package/src/shaders/depth.glsl +15 -0
- package/src/shaders/packing.glsl +20 -0
- package/src/shaders/transform.glsl +12 -0
- package/src/typedArray.ts +76 -0
- package/src/types.ts +54 -0
- package/types/ArrayBufferLoader.d.ts +5 -0
- package/types/DataLoader.d.ts +67 -0
- package/types/Ellipsoid.d.ts +18 -0
- package/types/EllipsoidGeometry.d.ts +13 -0
- package/types/Geodetic.d.ts +37 -0
- package/types/PointOfView.d.ts +20 -0
- package/types/Rectangle.d.ts +27 -0
- package/types/TileCoordinate.d.ts +21 -0
- package/types/TilingScheme.d.ts +21 -0
- package/types/TypedArrayLoader.d.ts +16 -0
- package/types/assertions.d.ts +4 -0
- package/types/bufferGeometry.d.ts +5 -0
- package/types/helpers/projectOnEllipsoidSurface.d.ts +6 -0
- package/types/index.d.ts +18 -0
- package/types/math.d.ts +13 -0
- package/types/r3f/EastNorthUpFrame.d.ts +15 -0
- package/types/r3f/EllipsoidMesh.d.ts +13 -0
- package/types/r3f/index.d.ts +2 -0
- package/types/typedArray.d.ts +10 -0
- package/types/types.d.ts +22 -0
package/src/Geodetic.ts
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
import { Vector3 } from 'three'
|
2
|
+
|
3
|
+
import { Ellipsoid } from './Ellipsoid'
|
4
|
+
import {
|
5
|
+
projectOnEllipsoidSurface,
|
6
|
+
type ProjectOnEllipsoidSurfaceOptions
|
7
|
+
} from './helpers/projectOnEllipsoidSurface'
|
8
|
+
|
9
|
+
export type GeodeticTuple = [number, number, number]
|
10
|
+
|
11
|
+
export interface GeodeticLike {
|
12
|
+
readonly longitude: number
|
13
|
+
readonly latitude: number
|
14
|
+
readonly height: number
|
15
|
+
}
|
16
|
+
|
17
|
+
const vectorScratch1 = /*#__PURE__*/ new Vector3()
|
18
|
+
const vectorScratch2 = /*#__PURE__*/ new Vector3()
|
19
|
+
|
20
|
+
export class Geodetic {
|
21
|
+
static readonly MIN_LONGITUDE = -Math.PI
|
22
|
+
static readonly MAX_LONGITUDE = Math.PI
|
23
|
+
static readonly MIN_LATITUDE = -Math.PI / 2
|
24
|
+
static readonly MAX_LATITUDE = Math.PI / 2
|
25
|
+
|
26
|
+
constructor(
|
27
|
+
public longitude = 0,
|
28
|
+
public latitude = 0,
|
29
|
+
public height = 0
|
30
|
+
) {}
|
31
|
+
|
32
|
+
set(longitude: number, latitude: number, height?: number): this {
|
33
|
+
this.longitude = longitude
|
34
|
+
this.latitude = latitude
|
35
|
+
if (height != null) {
|
36
|
+
this.height = height
|
37
|
+
}
|
38
|
+
return this
|
39
|
+
}
|
40
|
+
|
41
|
+
clone(): Geodetic {
|
42
|
+
return new Geodetic(this.longitude, this.latitude, this.height)
|
43
|
+
}
|
44
|
+
|
45
|
+
copy(other: GeodeticLike): this {
|
46
|
+
this.longitude = other.longitude
|
47
|
+
this.latitude = other.latitude
|
48
|
+
this.height = other.height
|
49
|
+
return this
|
50
|
+
}
|
51
|
+
|
52
|
+
equals(other: GeodeticLike): boolean {
|
53
|
+
return (
|
54
|
+
other.longitude === this.longitude &&
|
55
|
+
other.latitude === this.latitude &&
|
56
|
+
other.height === this.height
|
57
|
+
)
|
58
|
+
}
|
59
|
+
|
60
|
+
setLongitude(value: number): this {
|
61
|
+
this.longitude = value
|
62
|
+
return this
|
63
|
+
}
|
64
|
+
|
65
|
+
setLatitude(value: number): this {
|
66
|
+
this.latitude = value
|
67
|
+
return this
|
68
|
+
}
|
69
|
+
|
70
|
+
setHeight(value: number): this {
|
71
|
+
this.height = value
|
72
|
+
return this
|
73
|
+
}
|
74
|
+
|
75
|
+
normalize(): this {
|
76
|
+
if (this.longitude < Geodetic.MIN_LONGITUDE) {
|
77
|
+
this.longitude += Math.PI * 2
|
78
|
+
}
|
79
|
+
return this
|
80
|
+
}
|
81
|
+
|
82
|
+
// See: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
|
83
|
+
// Reference: https://github.com/CesiumGS/cesium/blob/1.122/packages/engine/Source/Core/Geodetic.js#L119
|
84
|
+
setFromECEF(
|
85
|
+
position: Vector3,
|
86
|
+
options?: ProjectOnEllipsoidSurfaceOptions & {
|
87
|
+
ellipsoid?: Ellipsoid
|
88
|
+
}
|
89
|
+
): this {
|
90
|
+
const ellipsoid = options?.ellipsoid ?? Ellipsoid.WGS84
|
91
|
+
const reciprocalRadiiSquared =
|
92
|
+
ellipsoid.reciprocalRadiiSquared(vectorScratch1)
|
93
|
+
const projection = projectOnEllipsoidSurface(
|
94
|
+
position,
|
95
|
+
reciprocalRadiiSquared,
|
96
|
+
vectorScratch2,
|
97
|
+
options
|
98
|
+
)
|
99
|
+
if (projection == null) {
|
100
|
+
throw new Error(
|
101
|
+
`Could not project position to ellipsoid surface: ${position.toArray()}`
|
102
|
+
)
|
103
|
+
}
|
104
|
+
const normal = vectorScratch1
|
105
|
+
.multiplyVectors(projection, reciprocalRadiiSquared)
|
106
|
+
.normalize()
|
107
|
+
this.longitude = Math.atan2(normal.y, normal.x)
|
108
|
+
this.latitude = Math.asin(normal.z)
|
109
|
+
const height = vectorScratch1.subVectors(position, projection)
|
110
|
+
this.height = Math.sign(height.dot(position)) * height.length()
|
111
|
+
return this
|
112
|
+
}
|
113
|
+
|
114
|
+
// See: https://en.wikipedia.org/wiki/Geographic_coordinate_conversion
|
115
|
+
// Reference: https://github.com/CesiumGS/cesium/blob/1.122/packages/engine/Source/Core/Cartesian3.js#L916
|
116
|
+
toECEF(
|
117
|
+
result = new Vector3(),
|
118
|
+
options?: {
|
119
|
+
ellipsoid?: Ellipsoid
|
120
|
+
}
|
121
|
+
): Vector3 {
|
122
|
+
const ellipsoid = options?.ellipsoid ?? Ellipsoid.WGS84
|
123
|
+
const radiiSquared = vectorScratch1.multiplyVectors(
|
124
|
+
ellipsoid.radii,
|
125
|
+
ellipsoid.radii
|
126
|
+
)
|
127
|
+
const cosLatitude = Math.cos(this.latitude)
|
128
|
+
const normal = vectorScratch2
|
129
|
+
.set(
|
130
|
+
cosLatitude * Math.cos(this.longitude),
|
131
|
+
cosLatitude * Math.sin(this.longitude),
|
132
|
+
Math.sin(this.latitude)
|
133
|
+
)
|
134
|
+
.normalize()
|
135
|
+
result.multiplyVectors(radiiSquared, normal)
|
136
|
+
return result
|
137
|
+
.divideScalar(Math.sqrt(normal.dot(result)))
|
138
|
+
.add(normal.multiplyScalar(this.height))
|
139
|
+
}
|
140
|
+
|
141
|
+
fromArray(array: readonly number[], offset = 0): this {
|
142
|
+
this.longitude = array[offset]
|
143
|
+
this.latitude = array[offset + 1]
|
144
|
+
this.height = array[offset + 2]
|
145
|
+
return this
|
146
|
+
}
|
147
|
+
|
148
|
+
toArray(array: number[] = [], offset = 0): number[] {
|
149
|
+
array[offset] = this.longitude
|
150
|
+
array[offset + 1] = this.latitude
|
151
|
+
array[offset + 2] = this.height
|
152
|
+
return array
|
153
|
+
}
|
154
|
+
|
155
|
+
*[Symbol.iterator](): Generator<number> {
|
156
|
+
yield this.longitude
|
157
|
+
yield this.latitude
|
158
|
+
yield this.height
|
159
|
+
}
|
160
|
+
}
|
@@ -0,0 +1,169 @@
|
|
1
|
+
import { Matrix4, Quaternion, Ray, Vector3, type Camera } from 'three'
|
2
|
+
|
3
|
+
import { Ellipsoid } from './Ellipsoid'
|
4
|
+
import { clamp } from './math'
|
5
|
+
|
6
|
+
const EPSILON = 0.000001
|
7
|
+
|
8
|
+
const eastScratch = /*#__PURE__*/ new Vector3()
|
9
|
+
const northScratch = /*#__PURE__*/ new Vector3()
|
10
|
+
const upScratch = /*#__PURE__*/ new Vector3()
|
11
|
+
const vectorScratch1 = /*#__PURE__*/ new Vector3()
|
12
|
+
const vectorScratch2 = /*#__PURE__*/ new Vector3()
|
13
|
+
const vectorScratch3 = /*#__PURE__*/ new Vector3()
|
14
|
+
const matrixScratch = /*#__PURE__*/ new Matrix4()
|
15
|
+
const quaternionScratch = /*#__PURE__*/ new Quaternion()
|
16
|
+
const rayScratch = /*#__PURE__*/ new Ray()
|
17
|
+
|
18
|
+
export class PointOfView {
|
19
|
+
// Distance from the target.
|
20
|
+
private _distance!: number
|
21
|
+
|
22
|
+
// Radians from the local east direction relative from true north, measured
|
23
|
+
// clockwise (90 degrees is true north, and -90 is true south).
|
24
|
+
heading: number
|
25
|
+
|
26
|
+
// Radians from the local horizon plane, measured with positive values looking
|
27
|
+
// up (90 degrees is straight up, -90 is straight down).
|
28
|
+
private _pitch!: number
|
29
|
+
|
30
|
+
roll: number
|
31
|
+
|
32
|
+
constructor(distance = 0, heading = 0, pitch = 0, roll = 0) {
|
33
|
+
this.distance = distance
|
34
|
+
this.heading = heading
|
35
|
+
this.pitch = pitch
|
36
|
+
this.roll = roll
|
37
|
+
}
|
38
|
+
|
39
|
+
get distance(): number {
|
40
|
+
return this._distance
|
41
|
+
}
|
42
|
+
|
43
|
+
set distance(value: number) {
|
44
|
+
this._distance = Math.max(value, EPSILON)
|
45
|
+
}
|
46
|
+
|
47
|
+
get pitch(): number {
|
48
|
+
return this._pitch
|
49
|
+
}
|
50
|
+
|
51
|
+
set pitch(value: number) {
|
52
|
+
this._pitch = clamp(value, -Math.PI / 2 + EPSILON, Math.PI / 2 - EPSILON)
|
53
|
+
}
|
54
|
+
|
55
|
+
set(distance: number, heading: number, pitch: number, roll?: number): this {
|
56
|
+
this.distance = distance
|
57
|
+
this.heading = heading
|
58
|
+
this.pitch = pitch
|
59
|
+
if (roll != null) {
|
60
|
+
this.roll = roll
|
61
|
+
}
|
62
|
+
return this
|
63
|
+
}
|
64
|
+
|
65
|
+
clone(): PointOfView {
|
66
|
+
return new PointOfView(this.distance, this.heading, this.pitch, this.roll)
|
67
|
+
}
|
68
|
+
|
69
|
+
copy(other: PointOfView): this {
|
70
|
+
this.distance = other.distance
|
71
|
+
this.heading = other.heading
|
72
|
+
this.pitch = other.pitch
|
73
|
+
this.roll = other.roll
|
74
|
+
return this
|
75
|
+
}
|
76
|
+
|
77
|
+
equals(other: PointOfView): boolean {
|
78
|
+
return (
|
79
|
+
other.distance === this.distance &&
|
80
|
+
other.heading === this.heading &&
|
81
|
+
other.pitch === this.pitch &&
|
82
|
+
other.roll === this.roll
|
83
|
+
)
|
84
|
+
}
|
85
|
+
|
86
|
+
decompose(
|
87
|
+
target: Vector3,
|
88
|
+
eye: Vector3,
|
89
|
+
quaternion: Quaternion,
|
90
|
+
up?: Vector3,
|
91
|
+
ellipsoid = Ellipsoid.WGS84
|
92
|
+
): void {
|
93
|
+
ellipsoid.getEastNorthUpVectors(
|
94
|
+
target,
|
95
|
+
eastScratch,
|
96
|
+
northScratch,
|
97
|
+
upScratch
|
98
|
+
)
|
99
|
+
up?.copy(upScratch)
|
100
|
+
|
101
|
+
// h = east * cos(heading) + north * sin(heading)
|
102
|
+
// v = h * cos(pitch) + up * sin(pitch)
|
103
|
+
const offset = vectorScratch1
|
104
|
+
.copy(eastScratch)
|
105
|
+
.multiplyScalar(Math.cos(this.heading))
|
106
|
+
.add(
|
107
|
+
vectorScratch2.copy(northScratch).multiplyScalar(Math.sin(this.heading))
|
108
|
+
)
|
109
|
+
.multiplyScalar(Math.cos(this.pitch))
|
110
|
+
.add(vectorScratch2.copy(upScratch).multiplyScalar(Math.sin(this.pitch)))
|
111
|
+
.normalize()
|
112
|
+
.multiplyScalar(this.distance)
|
113
|
+
eye.copy(target).sub(offset)
|
114
|
+
|
115
|
+
if (this.roll !== 0) {
|
116
|
+
const rollAxis = vectorScratch1.copy(target).sub(eye).normalize()
|
117
|
+
upScratch.applyQuaternion(
|
118
|
+
quaternionScratch.setFromAxisAngle(rollAxis, this.roll)
|
119
|
+
)
|
120
|
+
}
|
121
|
+
quaternion.setFromRotationMatrix(
|
122
|
+
matrixScratch.lookAt(eye, target, upScratch)
|
123
|
+
)
|
124
|
+
}
|
125
|
+
|
126
|
+
setFromCamera(camera: Camera, ellipsoid = Ellipsoid.WGS84): this | undefined {
|
127
|
+
const eye = vectorScratch1.setFromMatrixPosition(camera.matrixWorld)
|
128
|
+
const direction = vectorScratch2
|
129
|
+
.set(0, 0, 0.5)
|
130
|
+
.unproject(camera)
|
131
|
+
.sub(eye)
|
132
|
+
.normalize()
|
133
|
+
const target = ellipsoid.getIntersection(rayScratch.set(eye, direction))
|
134
|
+
if (target == null) {
|
135
|
+
return
|
136
|
+
}
|
137
|
+
|
138
|
+
this.distance = eye.distanceTo(target)
|
139
|
+
ellipsoid.getEastNorthUpVectors(
|
140
|
+
target,
|
141
|
+
eastScratch,
|
142
|
+
northScratch,
|
143
|
+
upScratch
|
144
|
+
)
|
145
|
+
this.heading = Math.atan2(
|
146
|
+
northScratch.dot(direction),
|
147
|
+
eastScratch.dot(direction)
|
148
|
+
)
|
149
|
+
this.pitch = Math.asin(upScratch.dot(direction))
|
150
|
+
|
151
|
+
// Need to rotate camera's up to evaluate it in world space.
|
152
|
+
const up = vectorScratch1.copy(camera.up).applyQuaternion(camera.quaternion)
|
153
|
+
const s = vectorScratch3
|
154
|
+
.copy(direction)
|
155
|
+
.multiplyScalar(-up.dot(direction))
|
156
|
+
.add(up)
|
157
|
+
.normalize()
|
158
|
+
const t = vectorScratch1
|
159
|
+
.copy(direction)
|
160
|
+
.multiplyScalar(-upScratch.dot(direction))
|
161
|
+
.add(upScratch)
|
162
|
+
.normalize()
|
163
|
+
const x = t.dot(s)
|
164
|
+
const y = direction.dot(t.cross(s))
|
165
|
+
this.roll = Math.atan2(y, x)
|
166
|
+
|
167
|
+
return this
|
168
|
+
}
|
169
|
+
}
|
package/src/Rectangle.ts
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
import { Geodetic } from './Geodetic'
|
2
|
+
|
3
|
+
export type RectangleTuple = [number, number, number, number]
|
4
|
+
|
5
|
+
export interface RectangleLike {
|
6
|
+
readonly west: number
|
7
|
+
readonly south: number
|
8
|
+
readonly east: number
|
9
|
+
readonly north: number
|
10
|
+
}
|
11
|
+
|
12
|
+
export class Rectangle {
|
13
|
+
static readonly MAX = /*#__PURE__*/ new Rectangle(
|
14
|
+
Geodetic.MIN_LONGITUDE,
|
15
|
+
Geodetic.MIN_LATITUDE,
|
16
|
+
Geodetic.MAX_LONGITUDE,
|
17
|
+
Geodetic.MAX_LATITUDE
|
18
|
+
)
|
19
|
+
|
20
|
+
constructor(
|
21
|
+
public west = 0,
|
22
|
+
public south = 0,
|
23
|
+
public east = 0,
|
24
|
+
public north = 0
|
25
|
+
) {}
|
26
|
+
|
27
|
+
get width(): number {
|
28
|
+
let east = this.east
|
29
|
+
if (east < this.west) {
|
30
|
+
east += Math.PI * 2
|
31
|
+
}
|
32
|
+
return east - this.west
|
33
|
+
}
|
34
|
+
|
35
|
+
get height(): number {
|
36
|
+
return this.north - this.south
|
37
|
+
}
|
38
|
+
|
39
|
+
set(west: number, south: number, east: number, north: number): this {
|
40
|
+
this.west = west
|
41
|
+
this.south = south
|
42
|
+
this.east = east
|
43
|
+
this.north = north
|
44
|
+
return this
|
45
|
+
}
|
46
|
+
|
47
|
+
clone(): Rectangle {
|
48
|
+
return new Rectangle(this.west, this.south, this.east, this.north)
|
49
|
+
}
|
50
|
+
|
51
|
+
copy(other: RectangleLike): this {
|
52
|
+
this.west = other.west
|
53
|
+
this.south = other.south
|
54
|
+
this.east = other.east
|
55
|
+
this.north = other.north
|
56
|
+
return this
|
57
|
+
}
|
58
|
+
|
59
|
+
equals(other: RectangleLike): boolean {
|
60
|
+
return (
|
61
|
+
other.west === this.west &&
|
62
|
+
other.south === this.south &&
|
63
|
+
other.east === this.east &&
|
64
|
+
other.north === this.north
|
65
|
+
)
|
66
|
+
}
|
67
|
+
|
68
|
+
at(x: number, y: number, result = new Geodetic()): Geodetic {
|
69
|
+
return result.set(
|
70
|
+
this.west + (this.east - this.west) * x,
|
71
|
+
this.north + (this.south - this.north) * y
|
72
|
+
)
|
73
|
+
}
|
74
|
+
|
75
|
+
fromArray(array: readonly number[], offset = 0): this {
|
76
|
+
this.west = array[offset]
|
77
|
+
this.south = array[offset + 1]
|
78
|
+
this.east = array[offset + 2]
|
79
|
+
this.north = array[offset + 3]
|
80
|
+
return this
|
81
|
+
}
|
82
|
+
|
83
|
+
toArray(array: number[] = [], offset = 0): number[] {
|
84
|
+
array[offset] = this.west
|
85
|
+
array[offset + 1] = this.south
|
86
|
+
array[offset + 2] = this.east
|
87
|
+
array[offset + 3] = this.north
|
88
|
+
return array
|
89
|
+
}
|
90
|
+
|
91
|
+
*[Symbol.iterator](): Generator<number> {
|
92
|
+
yield this.west
|
93
|
+
yield this.south
|
94
|
+
yield this.east
|
95
|
+
yield this.north
|
96
|
+
}
|
97
|
+
}
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import { TileCoordinate } from './TileCoordinate'
|
2
|
+
|
3
|
+
describe('TileCoordinate', () => {
|
4
|
+
test('getParent', () => {
|
5
|
+
const tile = new TileCoordinate()
|
6
|
+
tile.set(0, 0, 1)
|
7
|
+
expect(tile.getParent()).toMatchObject({ x: 0, y: 0, z: 0 })
|
8
|
+
tile.set(1, 0, 1)
|
9
|
+
expect(tile.getParent()).toMatchObject({ x: 0, y: 0, z: 0 })
|
10
|
+
tile.set(0, 1, 1)
|
11
|
+
expect(tile.getParent()).toMatchObject({ x: 0, y: 0, z: 0 })
|
12
|
+
tile.set(1, 1, 1)
|
13
|
+
expect(tile.getParent()).toMatchObject({ x: 0, y: 0, z: 0 })
|
14
|
+
tile.set(2, 0, 1)
|
15
|
+
expect(tile.getParent()).toMatchObject({ x: 1, y: 0, z: 0 })
|
16
|
+
tile.set(3, 0, 1)
|
17
|
+
expect(tile.getParent()).toMatchObject({ x: 1, y: 0, z: 0 })
|
18
|
+
tile.set(2, 1, 1)
|
19
|
+
expect(tile.getParent()).toMatchObject({ x: 1, y: 0, z: 0 })
|
20
|
+
tile.set(3, 1, 1)
|
21
|
+
expect(tile.getParent()).toMatchObject({ x: 1, y: 0, z: 0 })
|
22
|
+
})
|
23
|
+
|
24
|
+
test('traverseChildren', () => {
|
25
|
+
const tile = new TileCoordinate()
|
26
|
+
const child = new TileCoordinate()
|
27
|
+
const iterator = tile.traverseChildren(1, child)
|
28
|
+
expect(iterator.next().done).toBe(false)
|
29
|
+
expect(child).toMatchObject({ x: 0, y: 0, z: 1 })
|
30
|
+
expect(iterator.next().done).toBe(false)
|
31
|
+
expect(child).toMatchObject({ x: 1, y: 0, z: 1 })
|
32
|
+
expect(iterator.next().done).toBe(false)
|
33
|
+
expect(child).toMatchObject({ x: 0, y: 1, z: 1 })
|
34
|
+
expect(iterator.next().done).toBe(false)
|
35
|
+
expect(child).toMatchObject({ x: 1, y: 1, z: 1 })
|
36
|
+
expect(iterator.next().done).toBe(true)
|
37
|
+
})
|
38
|
+
})
|
@@ -0,0 +1,112 @@
|
|
1
|
+
export type TileCoordinateTuple = [number, number, number]
|
2
|
+
|
3
|
+
export interface TileCoordinateLike {
|
4
|
+
readonly x: number
|
5
|
+
readonly y: number
|
6
|
+
readonly z: number
|
7
|
+
}
|
8
|
+
|
9
|
+
function* traverseChildren(
|
10
|
+
x: number,
|
11
|
+
y: number,
|
12
|
+
z: number,
|
13
|
+
maxZ: number,
|
14
|
+
result?: TileCoordinate
|
15
|
+
): Generator<TileCoordinate> {
|
16
|
+
if (z >= maxZ) {
|
17
|
+
return
|
18
|
+
}
|
19
|
+
const divisor = 2 ** z
|
20
|
+
const nextZ = z + 1
|
21
|
+
const scale = 2 ** nextZ
|
22
|
+
const nextX = Math.floor((x / divisor) * scale)
|
23
|
+
const nextY = Math.floor((y / divisor) * scale)
|
24
|
+
const children = [
|
25
|
+
[nextX, nextY, nextZ],
|
26
|
+
[nextX + 1, nextY, nextZ],
|
27
|
+
[nextX, nextY + 1, nextZ],
|
28
|
+
[nextX + 1, nextY + 1, nextZ]
|
29
|
+
] as const
|
30
|
+
if (nextZ < maxZ) {
|
31
|
+
for (const child of children) {
|
32
|
+
for (const coord of traverseChildren(...child, maxZ, result)) {
|
33
|
+
yield coord
|
34
|
+
}
|
35
|
+
}
|
36
|
+
} else {
|
37
|
+
for (const child of children) {
|
38
|
+
yield (result ?? new TileCoordinate()).set(...child)
|
39
|
+
}
|
40
|
+
}
|
41
|
+
}
|
42
|
+
|
43
|
+
export class TileCoordinate {
|
44
|
+
constructor(
|
45
|
+
public x = 0,
|
46
|
+
public y = 0,
|
47
|
+
public z = 0
|
48
|
+
) {}
|
49
|
+
|
50
|
+
set(x: number, y: number, z?: number): this {
|
51
|
+
this.x = x
|
52
|
+
this.y = y
|
53
|
+
if (z != null) {
|
54
|
+
this.z = z
|
55
|
+
}
|
56
|
+
return this
|
57
|
+
}
|
58
|
+
|
59
|
+
clone(): TileCoordinate {
|
60
|
+
return new TileCoordinate(this.x, this.y, this.z)
|
61
|
+
}
|
62
|
+
|
63
|
+
copy(other: TileCoordinateLike): this {
|
64
|
+
this.x = other.x
|
65
|
+
this.y = other.y
|
66
|
+
this.z = other.z
|
67
|
+
return this
|
68
|
+
}
|
69
|
+
|
70
|
+
equals(other: TileCoordinateLike): boolean {
|
71
|
+
return other.x === this.x && other.y === this.y && other.z === this.z
|
72
|
+
}
|
73
|
+
|
74
|
+
getParent(result = new TileCoordinate()): TileCoordinate {
|
75
|
+
const divisor = 2 ** this.z
|
76
|
+
const x = this.x / divisor
|
77
|
+
const y = this.y / divisor
|
78
|
+
const z = this.z - 1
|
79
|
+
const scale = 2 ** z
|
80
|
+
return result.set(Math.floor(x * scale), Math.floor(y * scale), z)
|
81
|
+
}
|
82
|
+
|
83
|
+
*traverseChildren(
|
84
|
+
depth: number,
|
85
|
+
result?: TileCoordinate
|
86
|
+
): Generator<TileCoordinate> {
|
87
|
+
const { x, y, z } = this
|
88
|
+
for (const coord of traverseChildren(x, y, z, z + depth, result)) {
|
89
|
+
yield coord
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
fromArray(array: readonly number[], offset = 0): this {
|
94
|
+
this.x = array[offset]
|
95
|
+
this.y = array[offset + 1]
|
96
|
+
this.z = array[offset + 2]
|
97
|
+
return this
|
98
|
+
}
|
99
|
+
|
100
|
+
toArray(array: number[] = [], offset = 0): number[] {
|
101
|
+
array[offset] = this.x
|
102
|
+
array[offset + 1] = this.y
|
103
|
+
array[offset + 2] = this.z
|
104
|
+
return array
|
105
|
+
}
|
106
|
+
|
107
|
+
*[Symbol.iterator](): Generator<number> {
|
108
|
+
yield this.x
|
109
|
+
yield this.y
|
110
|
+
yield this.z
|
111
|
+
}
|
112
|
+
}
|
@@ -0,0 +1,63 @@
|
|
1
|
+
import { Rectangle } from './Rectangle'
|
2
|
+
import { TileCoordinate } from './TileCoordinate'
|
3
|
+
import { TilingScheme } from './TilingScheme'
|
4
|
+
|
5
|
+
describe('TilingScheme', () => {
|
6
|
+
test('getTile', () => {
|
7
|
+
{
|
8
|
+
const bounds = Rectangle.MAX
|
9
|
+
const tilingScheme = new TilingScheme(2, 1, bounds)
|
10
|
+
const tile = new TileCoordinate()
|
11
|
+
|
12
|
+
tilingScheme.getTile(bounds.at(0, 0), 0, tile)
|
13
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
14
|
+
tilingScheme.getTile(bounds.at(0.5 - Number.EPSILON, 0), 0, tile)
|
15
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
16
|
+
tilingScheme.getTile(bounds.at(0, 1), 0, tile)
|
17
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
18
|
+
tilingScheme.getTile(bounds.at(0.5 - Number.EPSILON, 1), 0, tile)
|
19
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
20
|
+
|
21
|
+
tilingScheme.getTile(bounds.at(0.5 + Number.EPSILON, 0), 0, tile)
|
22
|
+
expect(tile).toMatchObject({ x: 1, y: 0, z: 0 })
|
23
|
+
tilingScheme.getTile(bounds.at(1, 0), 0, tile)
|
24
|
+
expect(tile).toMatchObject({ x: 1, y: 0, z: 0 })
|
25
|
+
tilingScheme.getTile(bounds.at(0.5 + Number.EPSILON, 1), 0, tile)
|
26
|
+
expect(tile).toMatchObject({ x: 1, y: 0, z: 0 })
|
27
|
+
tilingScheme.getTile(bounds.at(1, 1), 0, tile)
|
28
|
+
expect(tile).toMatchObject({ x: 1, y: 0, z: 0 })
|
29
|
+
}
|
30
|
+
{
|
31
|
+
const rect = Rectangle.MAX
|
32
|
+
const tilingScheme = new TilingScheme(1, 1, rect)
|
33
|
+
const tile = new TileCoordinate()
|
34
|
+
|
35
|
+
tilingScheme.getTile(rect.at(0, 0), 0, tile)
|
36
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
37
|
+
tilingScheme.getTile(rect.at(0, 1), 0, tile)
|
38
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
39
|
+
tilingScheme.getTile(rect.at(1, 0), 0, tile)
|
40
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
41
|
+
tilingScheme.getTile(rect.at(1, 1), 0, tile)
|
42
|
+
expect(tile).toMatchObject({ x: 0, y: 0, z: 0 })
|
43
|
+
}
|
44
|
+
})
|
45
|
+
|
46
|
+
test('getRectangle', () => {
|
47
|
+
const bounds = Rectangle.MAX
|
48
|
+
const tilingScheme = new TilingScheme(2, 1, bounds)
|
49
|
+
const rect = new Rectangle()
|
50
|
+
|
51
|
+
tilingScheme.getRectangle({ x: 0, y: 0, z: 0 }, rect)
|
52
|
+
expect(rect.west).toBeCloseTo(-Math.PI)
|
53
|
+
expect(rect.south).toBeCloseTo(-Math.PI / 2)
|
54
|
+
expect(rect.east).toBeCloseTo(0)
|
55
|
+
expect(rect.north).toBeCloseTo(Math.PI / 2)
|
56
|
+
|
57
|
+
tilingScheme.getRectangle({ x: 1, y: 0, z: 0 }, rect)
|
58
|
+
expect(rect.west).toBeCloseTo(0)
|
59
|
+
expect(rect.south).toBeCloseTo(-Math.PI / 2)
|
60
|
+
expect(rect.east).toBeCloseTo(Math.PI)
|
61
|
+
expect(rect.north).toBeCloseTo(Math.PI / 2)
|
62
|
+
})
|
63
|
+
})
|