@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.
Files changed (53) hide show
  1. package/README.md +15 -0
  2. package/build/index.cjs +43 -0
  3. package/build/index.js +932 -0
  4. package/build/r3f.cjs +1 -0
  5. package/build/r3f.js +38 -0
  6. package/build/shared.cjs +1 -0
  7. package/build/shared.js +198 -0
  8. package/package.json +42 -0
  9. package/src/ArrayBufferLoader.ts +35 -0
  10. package/src/DataLoader.ts +114 -0
  11. package/src/Ellipsoid.ts +128 -0
  12. package/src/EllipsoidGeometry.ts +107 -0
  13. package/src/Geodetic.ts +160 -0
  14. package/src/PointOfView.ts +169 -0
  15. package/src/Rectangle.ts +97 -0
  16. package/src/TileCoordinate.test.ts +38 -0
  17. package/src/TileCoordinate.ts +112 -0
  18. package/src/TilingScheme.test.ts +63 -0
  19. package/src/TilingScheme.ts +76 -0
  20. package/src/TypedArrayLoader.ts +53 -0
  21. package/src/assertions.ts +13 -0
  22. package/src/bufferGeometry.ts +62 -0
  23. package/src/helpers/projectOnEllipsoidSurface.ts +72 -0
  24. package/src/index.ts +25 -0
  25. package/src/math.ts +41 -0
  26. package/src/r3f/EastNorthUpFrame.tsx +52 -0
  27. package/src/r3f/EllipsoidMesh.tsx +36 -0
  28. package/src/r3f/index.ts +2 -0
  29. package/src/shaders/depth.glsl +15 -0
  30. package/src/shaders/packing.glsl +20 -0
  31. package/src/shaders/transform.glsl +12 -0
  32. package/src/typedArray.ts +76 -0
  33. package/src/types.ts +54 -0
  34. package/types/ArrayBufferLoader.d.ts +5 -0
  35. package/types/DataLoader.d.ts +67 -0
  36. package/types/Ellipsoid.d.ts +18 -0
  37. package/types/EllipsoidGeometry.d.ts +13 -0
  38. package/types/Geodetic.d.ts +37 -0
  39. package/types/PointOfView.d.ts +20 -0
  40. package/types/Rectangle.d.ts +27 -0
  41. package/types/TileCoordinate.d.ts +21 -0
  42. package/types/TilingScheme.d.ts +21 -0
  43. package/types/TypedArrayLoader.d.ts +16 -0
  44. package/types/assertions.d.ts +4 -0
  45. package/types/bufferGeometry.d.ts +5 -0
  46. package/types/helpers/projectOnEllipsoidSurface.d.ts +6 -0
  47. package/types/index.d.ts +18 -0
  48. package/types/math.d.ts +13 -0
  49. package/types/r3f/EastNorthUpFrame.d.ts +15 -0
  50. package/types/r3f/EllipsoidMesh.d.ts +13 -0
  51. package/types/r3f/index.d.ts +2 -0
  52. package/types/typedArray.d.ts +10 -0
  53. package/types/types.d.ts +22 -0
@@ -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
+ }
@@ -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
+ })