@loaders.gl/tiles 5.0.0-alpha.6 → 5.0.0-alpha.8

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 (114) hide show
  1. package/README.md +26 -0
  2. package/dist/dist.dev.js +10015 -8172
  3. package/dist/dist.min.js +3 -4
  4. package/dist/index.cjs +1676 -477
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +10 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +4 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/spatial/format-spatial-reference.d.ts +1 -0
  11. package/dist/spatial/format-spatial-reference.d.ts.map +1 -1
  12. package/dist/spatial/format-spatial-reference.js +104 -46
  13. package/dist/spatial/format-spatial-reference.js.map +1 -1
  14. package/dist/spatial/get-spatial-coordinate-frame.d.ts +12 -1
  15. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -1
  16. package/dist/spatial/get-spatial-coordinate-frame.js +105 -24
  17. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -1
  18. package/dist/spatial/normalize-crs-identifier.d.ts +15 -0
  19. package/dist/spatial/normalize-crs-identifier.d.ts.map +1 -0
  20. package/dist/spatial/normalize-crs-identifier.js +22 -0
  21. package/dist/spatial/normalize-crs-identifier.js.map +1 -0
  22. package/dist/spatial/spatial-coordinate-transformer.d.ts +4 -0
  23. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  24. package/dist/spatial/spatial-coordinate-transformer.js +102 -18
  25. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  26. package/dist/spatial/spatial-resource-registry.js +3 -3
  27. package/dist/spatial/spatial-resource-registry.js.map +1 -1
  28. package/dist/spatial/tiles-3d-spatial-transformer.d.ts +32 -0
  29. package/dist/spatial/tiles-3d-spatial-transformer.d.ts.map +1 -0
  30. package/dist/spatial/tiles-3d-spatial-transformer.js +175 -0
  31. package/dist/spatial/tiles-3d-spatial-transformer.js.map +1 -0
  32. package/dist/tileset-3d/common/tile-3d-contracts.d.ts +63 -0
  33. package/dist/tileset-3d/common/tile-3d-contracts.d.ts.map +1 -0
  34. package/dist/tileset-3d/common/tile-3d-contracts.js +51 -0
  35. package/dist/tileset-3d/common/tile-3d-contracts.js.map +1 -0
  36. package/dist/tileset-3d/common/tile-3d.d.ts +33 -5
  37. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  38. package/dist/tileset-3d/common/tile-3d.js +179 -29
  39. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  40. package/dist/tileset-3d/common/tileset-3d.d.ts +15 -25
  41. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  42. package/dist/tileset-3d/common/tileset-3d.js +35 -42
  43. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  44. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  45. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  46. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  47. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  48. package/dist/tileset-3d/common/tileset-observability.d.ts +60 -0
  49. package/dist/tileset-3d/common/tileset-observability.d.ts.map +1 -0
  50. package/dist/tileset-3d/common/tileset-observability.js +39 -0
  51. package/dist/tileset-3d/common/tileset-observability.js.map +1 -0
  52. package/dist/tileset-3d/common/tileset-source.d.ts +11 -1
  53. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  55. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  56. package/dist/tileset-3d/common/tileset-traverser.js +4 -3
  57. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  58. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +47 -1
  59. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  60. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +327 -30
  61. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  62. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +34 -0
  63. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  64. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +278 -14
  65. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  66. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts +5 -3
  67. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.d.ts.map +1 -1
  68. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js +7 -4
  69. package/dist/tileset-3d/format-3d-tiles/tileset-3d-traverser.js.map +1 -1
  70. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +4 -2
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.js +60 -9
  73. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  74. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  75. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +1 -1
  76. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  77. package/dist/tileset-3d/helpers/bounding-volume.d.ts +7 -3
  78. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  79. package/dist/tileset-3d/helpers/bounding-volume.js +19 -61
  80. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  81. package/dist/tileset-3d/helpers/frame-state.d.ts +5 -0
  82. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/frame-state.js +11 -5
  84. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  85. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts +17 -0
  86. package/dist/tileset-3d/helpers/s2-bounding-volume.d.ts.map +1 -0
  87. package/dist/tileset-3d/helpers/s2-bounding-volume.js +20 -0
  88. package/dist/tileset-3d/helpers/s2-bounding-volume.js.map +1 -0
  89. package/dist/types.d.ts +2 -0
  90. package/dist/types.d.ts.map +1 -1
  91. package/package.json +11 -12
  92. package/src/index.ts +18 -1
  93. package/src/spatial/format-spatial-reference.ts +127 -53
  94. package/src/spatial/get-spatial-coordinate-frame.ts +126 -31
  95. package/src/spatial/normalize-crs-identifier.ts +26 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +124 -24
  97. package/src/spatial/spatial-resource-registry.ts +3 -3
  98. package/src/spatial/tiles-3d-spatial-transformer.ts +206 -0
  99. package/src/tileset-3d/common/tile-3d-contracts.ts +115 -0
  100. package/src/tileset-3d/common/tile-3d.ts +208 -32
  101. package/src/tileset-3d/common/tileset-3d.ts +37 -61
  102. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  103. package/src/tileset-3d/common/tileset-observability.ts +100 -0
  104. package/src/tileset-3d/common/tileset-source.ts +19 -1
  105. package/src/tileset-3d/common/tileset-traverser.ts +4 -3
  106. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +481 -34
  107. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +405 -14
  108. package/src/tileset-3d/format-3d-tiles/tileset-3d-traverser.ts +7 -4
  109. package/src/tileset-3d/format-i3s/i3s-source.ts +84 -13
  110. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +2 -1
  111. package/src/tileset-3d/helpers/bounding-volume.ts +29 -62
  112. package/src/tileset-3d/helpers/frame-state.ts +15 -7
  113. package/src/tileset-3d/helpers/s2-bounding-volume.ts +35 -0
  114. package/src/types.ts +2 -0
@@ -6,8 +6,13 @@ import type {Geoid} from '@math.gl/geoid';
6
6
  import {Vector3} from '@math.gl/core';
7
7
  import {Ellipsoid} from '@math.gl/geospatial';
8
8
  import type {ReadonlyCRSDefinition} from '@math.gl/crs';
9
- import {Proj4Projection, toProj4CRSDefinition, type Proj4CRSDefinition} from '@math.gl/proj4';
9
+ import {Projection, type Proj4CRSDefinition} from '@math.gl/proj4';
10
10
  import {getGeoidModel} from './spatial-resource-registry';
11
+ import {
12
+ normalizeCrsIdentifier,
13
+ WGS84_GEOCENTRIC_CRS,
14
+ WGS84_GEOGRAPHIC_CRS
15
+ } from './normalize-crs-identifier';
11
16
  import type {
12
17
  TilesetHeightReference,
13
18
  TilesetSpatialOptions,
@@ -16,9 +21,6 @@ import type {
16
21
  } from './spatial-types';
17
22
  export {getSpatialCoordinateFrame} from './get-spatial-coordinate-frame';
18
23
 
19
- const GEOGRAPHIC_CRS = 'EPSG:4326';
20
- const GEOCENTRIC_CRS = 'EPSG:4978';
21
-
22
24
  /**
23
25
  * Deterministic coordinate transformer used by 3D tile format adapters.
24
26
  *
@@ -31,13 +33,13 @@ export class SpatialCoordinateTransformer {
31
33
  readonly spatialReference: TilesetSpatialReference;
32
34
 
33
35
  /** Projection from the source CRS directly to the requested horizontal target CRS. */
34
- private readonly horizontalProjection?: Proj4Projection;
36
+ private readonly horizontalProjection?: Projection;
35
37
 
36
38
  /** Projection from the source CRS to geographic longitude, latitude, and ellipsoidal height. */
37
- private readonly geographicProjection?: Proj4Projection;
39
+ private readonly geographicProjection?: Projection;
38
40
 
39
41
  /** Projection from adjusted geographic coordinates to the requested output CRS. */
40
- private readonly heightOutputProjection?: Proj4Projection;
42
+ private readonly heightOutputProjection?: Projection;
41
43
 
42
44
  /** Whether source coordinates use the WGS84 geocentric frame handled by math.gl. */
43
45
  private readonly sourceIsGeocentric: boolean;
@@ -75,7 +77,7 @@ export class SpatialCoordinateTransformer {
75
77
  !this.sourceIsGeocentric &&
76
78
  !this.outputIsGeocentric
77
79
  ) {
78
- this.horizontalProjection = new Proj4Projection({
80
+ this.horizontalProjection = new Projection({
79
81
  from: getHorizontalProj4Definition(spatialReference.sourceCrs),
80
82
  to: getHorizontalProj4Definition(spatialReference.targetCrs),
81
83
  enforceAxis: false
@@ -88,15 +90,15 @@ export class SpatialCoordinateTransformer {
88
90
  this.outputIsGeocentric
89
91
  ) {
90
92
  if (!this.sourceIsGeographic && !this.sourceIsGeocentric) {
91
- this.geographicProjection = new Proj4Projection({
93
+ this.geographicProjection = new Projection({
92
94
  from: getHorizontalProj4Definition(spatialReference.sourceCrs),
93
- to: GEOGRAPHIC_CRS,
95
+ to: WGS84_GEOGRAPHIC_CRS,
94
96
  enforceAxis: false
95
97
  });
96
98
  }
97
99
  if (!this.outputIsGeographic && !this.outputIsGeocentric) {
98
- this.heightOutputProjection = new Proj4Projection({
99
- from: GEOGRAPHIC_CRS,
100
+ this.heightOutputProjection = new Projection({
101
+ from: WGS84_GEOGRAPHIC_CRS,
100
102
  to: getHorizontalProj4Definition(outputCrs),
101
103
  enforceAxis: false
102
104
  });
@@ -157,6 +159,59 @@ export class SpatialCoordinateTransformer {
157
159
  return result;
158
160
  }
159
161
 
162
+ /** Transform a packed sequence of xyz positions, preserving the input array type only when safe. */
163
+ transformPositions(positions: ArrayLike<number>): Float64Array {
164
+ if (positions.length % 3 !== 0) {
165
+ throw new Error('Packed spatial positions must contain a multiple of three components');
166
+ }
167
+ const transformedPositions = new Float64Array(positions.length);
168
+ for (let index = 0; index < positions.length; index += 3) {
169
+ const transformed = this.transformPosition([
170
+ Number(positions[index]),
171
+ Number(positions[index + 1]),
172
+ Number(positions[index + 2])
173
+ ]);
174
+ transformedPositions[index] = transformed[0];
175
+ transformedPositions[index + 1] = transformed[1];
176
+ transformedPositions[index + 2] = transformed[2];
177
+ }
178
+ return transformedPositions;
179
+ }
180
+
181
+ /** Transform packed xyz normals using a local finite-difference tangent approximation. */
182
+ transformNormals(normals: ArrayLike<number>, positions: ArrayLike<number>): Float32Array {
183
+ if (normals.length !== positions.length || normals.length % 3 !== 0) {
184
+ throw new Error('Spatial normals and positions must have matching xyz component counts');
185
+ }
186
+ const transformedNormals = new Float32Array(normals.length);
187
+ const epsilon = 1e-5;
188
+ for (let index = 0; index < normals.length; index += 3) {
189
+ const position = [
190
+ Number(positions[index]),
191
+ Number(positions[index + 1]),
192
+ Number(positions[index + 2])
193
+ ];
194
+ const normal = [
195
+ Number(normals[index]),
196
+ Number(normals[index + 1]),
197
+ Number(normals[index + 2])
198
+ ];
199
+ const jacobian = getFiniteDifferenceJacobian(this, position, epsilon);
200
+ const transformedNormal = inverseTransposeMultiply(jacobian, normal);
201
+ const length = Math.hypot(...transformedNormal);
202
+ if (length > 0 && Number.isFinite(length)) {
203
+ transformedNormals[index] = transformedNormal[0] / length;
204
+ transformedNormals[index + 1] = transformedNormal[1] / length;
205
+ transformedNormals[index + 2] = transformedNormal[2] / length;
206
+ } else {
207
+ transformedNormals[index] = normal[0];
208
+ transformedNormals[index + 1] = normal[1];
209
+ transformedNormals[index + 2] = normal[2];
210
+ }
211
+ }
212
+ return transformedNormals;
213
+ }
214
+
160
215
  /** Convert one source coordinate to conventional WGS84 longitude, latitude, and height. */
161
216
  private toGeographic(coordinate: number[]): number[] {
162
217
  if (this.sourceIsGeocentric) {
@@ -180,14 +235,64 @@ export class SpatialCoordinateTransformer {
180
235
  }
181
236
  }
182
237
 
183
- /** Select the horizontal component that the proj4js runtime can execute. */
238
+ function getFiniteDifferenceJacobian(
239
+ transformer: SpatialCoordinateTransformer,
240
+ position: number[],
241
+ epsilon: number
242
+ ): number[][] {
243
+ const columns: number[][] = [];
244
+ for (let axis = 0; axis < 3; axis++) {
245
+ const plus = position.slice();
246
+ const minus = position.slice();
247
+ plus[axis] += epsilon;
248
+ minus[axis] -= epsilon;
249
+ const plusPosition = transformer.transformPosition(plus);
250
+ const minusPosition = transformer.transformPosition(minus);
251
+ columns.push([
252
+ (plusPosition[0] - minusPosition[0]) / (2 * epsilon),
253
+ (plusPosition[1] - minusPosition[1]) / (2 * epsilon),
254
+ (plusPosition[2] - minusPosition[2]) / (2 * epsilon)
255
+ ]);
256
+ }
257
+ return columns;
258
+ }
259
+
260
+ function inverseTransposeMultiply(jacobianColumns: number[][], vector: number[]): number[] {
261
+ const [a, b, c] = jacobianColumns;
262
+ const determinant =
263
+ a[0] * (b[1] * c[2] - b[2] * c[1]) -
264
+ b[0] * (a[1] * c[2] - a[2] * c[1]) +
265
+ c[0] * (a[1] * b[2] - a[2] * b[1]);
266
+ if (!Number.isFinite(determinant) || Math.abs(determinant) < 1e-15) {
267
+ return vector;
268
+ }
269
+ const inverseTranspose = [
270
+ [b[1] * c[2] - b[2] * c[1], a[2] * c[1] - a[1] * c[2], a[1] * b[2] - a[2] * b[1]],
271
+ [b[2] * c[0] - b[0] * c[2], a[0] * c[2] - a[2] * c[0], a[2] * b[0] - a[0] * b[2]],
272
+ [b[0] * c[1] - b[1] * c[0], a[1] * c[0] - a[0] * c[1], a[0] * b[1] - a[1] * b[0]]
273
+ ];
274
+ return inverseTranspose.map(
275
+ row => (row[0] * vector[0] + row[1] * vector[1] + row[2] * vector[2]) / determinant
276
+ );
277
+ }
278
+
279
+ /** Select the horizontal CRS while height conversion is handled separately. */
184
280
  function getHorizontalProj4Definition(
185
281
  definition: ReadonlyCRSDefinition | undefined
186
282
  ): Proj4CRSDefinition {
187
283
  if (!definition) {
188
284
  throw new Error('Cannot construct a projection because the CRS is unknown');
189
285
  }
190
- return toProj4CRSDefinition(definition, {mode: 'horizontal'});
286
+ if (typeof definition !== 'string' && definition.type === 'CompoundCRS') {
287
+ const horizontalComponents = definition.components.filter(
288
+ component => component.type !== 'VerticalCRS'
289
+ );
290
+ if (horizontalComponents.length !== 1) {
291
+ throw new Error('CompoundCRS requires exactly one horizontal component');
292
+ }
293
+ return getHorizontalProj4Definition(horizontalComponents[0]);
294
+ }
295
+ return definition as Proj4CRSDefinition;
191
296
  }
192
297
 
193
298
  /** Validate that all requested operations can be represented by the current runtime. */
@@ -224,7 +329,9 @@ function validateTransformRequest(spatialReference: TilesetSpatialReference): vo
224
329
 
225
330
  /** Return whether a CRS definition identifies the WGS84 geocentric frame. */
226
331
  function isWgs84Geocentric(definition: unknown): boolean {
227
- return typeof definition === 'string' && normalizeCrsIdentifier(definition) === GEOCENTRIC_CRS;
332
+ return (
333
+ typeof definition === 'string' && normalizeCrsIdentifier(definition) === WGS84_GEOCENTRIC_CRS
334
+ );
228
335
  }
229
336
 
230
337
  /** Return whether a CRS definition uses conventional WGS84 longitude/latitude coordinates. */
@@ -233,16 +340,9 @@ function isWgs84Geographic(definition: unknown): boolean {
233
340
  return false;
234
341
  }
235
342
  const identifier = normalizeCrsIdentifier(definition);
236
- return identifier === GEOGRAPHIC_CRS || identifier === 'EPSG:4979' || identifier === 'OGC:CRS84';
237
- }
238
-
239
- /** Normalize common OGC URL and URN CRS spellings to authority identifiers. */
240
- function normalizeCrsIdentifier(identifier: string): string {
241
- const normalizedIdentifier = identifier.trim().toUpperCase();
242
- const ogcMatch = normalizedIdentifier.match(
243
- /(?:\/DEF\/CRS\/|URN:OGC:DEF:CRS:)([A-Z0-9_-]+)(?:\/|::)(?:[^/:]*[/:])?([A-Z0-9_.-]+)$/
343
+ return (
344
+ identifier === WGS84_GEOGRAPHIC_CRS || identifier === 'EPSG:4979' || identifier === 'OGC:CRS84'
244
345
  );
245
- return ogcMatch ? `${ogcMatch[1]}:${ogcMatch[2]}` : normalizedIdentifier;
246
346
  }
247
347
 
248
348
  /** Return whether source and target height interpretations differ. */
@@ -3,7 +3,7 @@
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
5
  import {Geoid, parsePGM} from '@math.gl/geoid';
6
- import {Proj4Projection} from '@math.gl/proj4';
6
+ import {Projection} from '@math.gl/proj4';
7
7
  import type {Proj4CRSDefinition} from '@math.gl/proj4';
8
8
 
9
9
  const geoidModels = new Map<string, Geoid>();
@@ -18,7 +18,7 @@ const geoidModels = new Map<string, Geoid>();
18
18
  */
19
19
  export function registerSpatialCrs(name: string, definition: Proj4CRSDefinition): void {
20
20
  validateResourceName(name, 'CRS');
21
- Proj4Projection.defineProjectionAliases({[name]: definition});
21
+ Projection.defineProjectionAliases({[name]: definition});
22
22
  }
23
23
 
24
24
  /**
@@ -29,7 +29,7 @@ export function registerSpatialCrs(name: string, definition: Proj4CRSDefinition)
29
29
  */
30
30
  export function registerSpatialDatumGrid(name: string, data: ArrayBuffer): void {
31
31
  validateResourceName(name, 'datum grid');
32
- Proj4Projection.registerDatumGrid(name, data);
32
+ Projection.registerDatumGrid(name, data);
33
33
  }
34
34
 
35
35
  /**
@@ -0,0 +1,206 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {SpatialCoordinateTransformer} from './spatial-coordinate-transformer';
6
+ import {getSpatialCoordinateFrame} from './get-spatial-coordinate-frame';
7
+ import {Matrix4} from '@math.gl/core';
8
+ import {Ellipsoid} from '@math.gl/geospatial';
9
+ import type {TilesetSpatialOptions, TilesetSpatialReference} from './spatial-types';
10
+
11
+ /** Structural 3D Tiles volume shape used by the source adapter. */
12
+ export type Tiles3DSpatialBoundingVolume = {
13
+ box?: number[];
14
+ sphere?: number[];
15
+ region?: number[];
16
+ [key: string]: unknown;
17
+ };
18
+
19
+ /**
20
+ * Applies nonlinear CRS operations to 3D Tiles geometry bounds.
21
+ *
22
+ * Bounds are rebuilt from conservative samples. This intentionally produces an axis-aligned box
23
+ * in the target frame; it is safe for culling and screen-space-error calculations even when the
24
+ * source volume is rotated or the projection is nonlinear.
25
+ */
26
+ export class Tiles3DSpatialTransformer {
27
+ /** Normalized reference describing the completed operation. */
28
+ readonly spatialReference: TilesetSpatialReference;
29
+ private readonly coordinateTransformer: SpatialCoordinateTransformer;
30
+ private readonly geographicCoordinateTransformer: SpatialCoordinateTransformer;
31
+
32
+ /** Creates a 3D Tiles transformer for a requested target CRS. */
33
+ constructor(spatialReference: TilesetSpatialReference, options: TilesetSpatialOptions = {}) {
34
+ this.coordinateTransformer = new SpatialCoordinateTransformer(spatialReference, options);
35
+ // 3D Tiles regions are always encoded as WGS84 longitude/latitude radians, independently
36
+ // of the tileset's local CRS. Build a second pipeline for that fixed source frame.
37
+ const geographicSpatialReference = {
38
+ ...spatialReference,
39
+ sourceCrs: 'EPSG:4326' as const,
40
+ targetCrs: spatialReference.targetCrs || spatialReference.sourceCrs,
41
+ coordinateFrame: 'geographic' as const,
42
+ axisOrder: 'xy' as const
43
+ };
44
+ this.geographicCoordinateTransformer = new SpatialCoordinateTransformer(
45
+ geographicSpatialReference,
46
+ options
47
+ );
48
+ this.spatialReference = spatialReference;
49
+ }
50
+
51
+ /** Transform packed POSITION values into the target CRS. */
52
+ transformPositions(positions: ArrayLike<number>): Float64Array {
53
+ return this.coordinateTransformer.transformPositions(positions);
54
+ }
55
+
56
+ /** Transform packed NORMAL values using the corresponding source positions. */
57
+ transformNormals(normals: ArrayLike<number>, positions: ArrayLike<number>): Float32Array {
58
+ return this.coordinateTransformer.transformNormals(normals, positions);
59
+ }
60
+
61
+ /** Rebuild a conservative target-frame volume from a source volume. */
62
+ transformBoundingVolume(
63
+ volume: Tiles3DSpatialBoundingVolume,
64
+ sourceTransform?: ArrayLike<number>
65
+ ): Tiles3DSpatialBoundingVolume {
66
+ const transform = sourceTransform ? new Matrix4(Array.from(sourceTransform)) : undefined;
67
+ const samples = volume.region
68
+ ? getRegionSamples(volume.region)
69
+ : getVolumeSamples(volume).map(sample =>
70
+ transform ? Array.from(transform.transformAsPoint(sample)) : sample
71
+ );
72
+ if (!samples.length) {
73
+ return volume;
74
+ }
75
+ const transformed = volume.region
76
+ ? samples.map(sample => this.transformRegionSample(sample))
77
+ : samples.map(sample => this.coordinateTransformer.transformPosition(sample));
78
+ return {box: createAxisAlignedBox(transformed)};
79
+ }
80
+
81
+ /** Transforms one WGS84 region sample into the selected output frame. */
82
+ private transformRegionSample(sample: number[]): number[] {
83
+ const [longitudeDegrees, latitudeDegrees, height] = sample;
84
+ const targetCrs = this.spatialReference.targetCrs || this.spatialReference.sourceCrs;
85
+ if (targetCrs && getSpatialCoordinateFrame(targetCrs) === 'geocentric') {
86
+ const cartesian = Ellipsoid.WGS84.cartographicToCartesian([
87
+ longitudeDegrees,
88
+ latitudeDegrees,
89
+ height
90
+ ]);
91
+ return Array.from(cartesian);
92
+ }
93
+ return this.geographicCoordinateTransformer.transformPosition(sample);
94
+ }
95
+ }
96
+
97
+ /** Samples region corners, center lines, and both height planes in degrees/meters. */
98
+ function getRegionSamples(region: number[]): number[][] {
99
+ if (region.length < 6 || region.some(value => !Number.isFinite(value))) {
100
+ return [];
101
+ }
102
+ const [west, south, east, north, minimumHeight, maximumHeight] = region;
103
+ const longitudeEnd = east < west ? east + Math.PI * 2 : east;
104
+ const longitudes = getIntervalSamples(west, longitudeEnd);
105
+ const latitudes = getIntervalSamples(south, north);
106
+ const samples: number[][] = [];
107
+ for (const longitude of longitudes) {
108
+ for (const latitude of latitudes) {
109
+ for (const height of [minimumHeight, maximumHeight]) {
110
+ samples.push([(longitude * 180) / Math.PI, (latitude * 180) / Math.PI, height]);
111
+ }
112
+ }
113
+ }
114
+ return samples;
115
+ }
116
+
117
+ /** Includes interval endpoints and every quarter-turn where ECEF axes reach an extremum. */
118
+ function getIntervalSamples(start: number, end: number): number[] {
119
+ const samples = [start, (start + end) / 2, end];
120
+ const angularStep = Math.PI / 2;
121
+ const firstCriticalIndex = Math.ceil(start / angularStep);
122
+ const lastCriticalIndex = Math.floor(end / angularStep);
123
+ for (
124
+ let criticalIndex = firstCriticalIndex;
125
+ criticalIndex <= lastCriticalIndex;
126
+ criticalIndex++
127
+ ) {
128
+ samples.push(criticalIndex * angularStep);
129
+ }
130
+ return samples;
131
+ }
132
+
133
+ function getVolumeSamples(volume: Tiles3DSpatialBoundingVolume): number[][] {
134
+ if (volume.box && volume.box.length >= 12) {
135
+ const center = volume.box.slice(0, 3);
136
+ const axes = [volume.box.slice(3, 6), volume.box.slice(6, 9), volume.box.slice(9, 12)];
137
+ const samples: number[][] = [];
138
+ for (let firstIndex = 0; firstIndex <= 4; firstIndex++) {
139
+ for (let secondIndex = 0; secondIndex <= 4; secondIndex++) {
140
+ for (let thirdIndex = 0; thirdIndex <= 4; thirdIndex++) {
141
+ const first = firstIndex / 2 - 1;
142
+ const second = secondIndex / 2 - 1;
143
+ const third = thirdIndex / 2 - 1;
144
+ samples.push([
145
+ center[0] + first * axes[0][0] + second * axes[1][0] + third * axes[2][0],
146
+ center[1] + first * axes[0][1] + second * axes[1][1] + third * axes[2][1],
147
+ center[2] + first * axes[0][2] + second * axes[1][2] + third * axes[2][2]
148
+ ]);
149
+ }
150
+ }
151
+ }
152
+ return samples;
153
+ }
154
+ if (volume.sphere && volume.sphere.length >= 4) {
155
+ const [x, y, z, radius] = volume.sphere;
156
+ const samples = [
157
+ [x - radius, y, z],
158
+ [x + radius, y, z],
159
+ [x, y - radius, z],
160
+ [x, y + radius, z],
161
+ [x, y, z - radius],
162
+ [x, y, z + radius]
163
+ ];
164
+ for (let longitudeIndex = 0; longitudeIndex < 16; longitudeIndex++) {
165
+ const longitude = (longitudeIndex * Math.PI * 2) / 16;
166
+ samples.push([x + radius * Math.cos(longitude), y + radius * Math.sin(longitude), z]);
167
+ }
168
+ return samples;
169
+ }
170
+ // Regions are already WGS84 geographic volumes. Keep them unchanged until a dedicated
171
+ // geographic-region adapter can account for longitude wrapping and datum semantics.
172
+ return [];
173
+ }
174
+
175
+ function createAxisAlignedBox(samples: number[][]): number[] {
176
+ const minimum = [Infinity, Infinity, Infinity];
177
+ const maximum = [-Infinity, -Infinity, -Infinity];
178
+ for (const sample of samples) {
179
+ for (let axis = 0; axis < 3; axis++) {
180
+ minimum[axis] = Math.min(minimum[axis], sample[axis]);
181
+ maximum[axis] = Math.max(maximum[axis], sample[axis]);
182
+ }
183
+ }
184
+ const padding = [
185
+ Math.max((maximum[0] - minimum[0]) * 0.01, 1e-9),
186
+ Math.max((maximum[1] - minimum[1]) * 0.01, 1e-9),
187
+ Math.max((maximum[2] - minimum[2]) * 0.01, 1e-9)
188
+ ];
189
+ const center = [
190
+ (minimum[0] + maximum[0]) / 2,
191
+ (minimum[1] + maximum[1]) / 2,
192
+ (minimum[2] + maximum[2]) / 2
193
+ ];
194
+ return [
195
+ ...center,
196
+ (maximum[0] - minimum[0]) / 2 + padding[0],
197
+ 0,
198
+ 0,
199
+ 0,
200
+ (maximum[1] - minimum[1]) / 2 + padding[1],
201
+ 0,
202
+ 0,
203
+ 0,
204
+ (maximum[2] - minimum[2]) / 2 + padding[2]
205
+ ];
206
+ }
@@ -0,0 +1,115 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ /** Structural subset implemented by math.gl bounding volumes used by tile renderers. */
6
+ export type Tile3DBoundingVolume = {
7
+ distanceSquaredTo?(point: unknown): number;
8
+ intersectPlane?(plane: unknown): number;
9
+ };
10
+
11
+ /** Renderer-neutral description of one ordered tile content entry. */
12
+ export type Tile3DContent = {
13
+ /** Zero-based source order, stable across loading and unloading. */
14
+ index: number;
15
+ /** Resolved content URI when declared by the tileset. */
16
+ uri?: string;
17
+ /** Tileset group index declared by this content entry. */
18
+ group?: number;
19
+ /** Normalized tile content kind, when detected. */
20
+ type?: string;
21
+ /** Decoded payload, or null while unloaded/failed. */
22
+ payload: unknown;
23
+ /** Raw content metadata reference. */
24
+ metadata: Record<string, unknown> | null;
25
+ /** Transformed content volume, with tile-volume fallback. */
26
+ boundingVolume: Tile3DBoundingVolume | null;
27
+ /** Normalized feature-id declarations for this content. */
28
+ featureIds: Tile3DFeatureIdSet[];
29
+ /** Whether the payload can currently be rendered. */
30
+ renderable: boolean;
31
+ };
32
+
33
+ /** Renderer-neutral feature-id declaration from a tile content payload. */
34
+ export type Tile3DFeatureIdSet = {
35
+ /** Source of the feature identifiers. */
36
+ source: 'attribute' | 'property-table' | 'constant' | 'texture' | 'implicit';
37
+ /** glTF attribute name for attribute-backed identifiers. */
38
+ attribute?: string;
39
+ /** Structural-metadata property-table index. */
40
+ propertyTable?: number;
41
+ /** Constant feature identifier. */
42
+ constant?: number;
43
+ /** Texture-backed identifier declaration, when present. */
44
+ texture?: Record<string, unknown>;
45
+ /** Implicit identifier declaration, when present. */
46
+ implicit?: Record<string, unknown>;
47
+ /** Decoded identifier values, when supplied by a content loader. */
48
+ values?: Uint32Array | Uint16Array | Uint8Array;
49
+ /** Unrecognized source fields preserved for forward compatibility. */
50
+ details?: Record<string, unknown>;
51
+ };
52
+
53
+ /** Raw metadata references inherited by a tile and its contents. */
54
+ export type Tile3DMetadataContext = {
55
+ /** Tileset-level metadata entity. */
56
+ tileset?: Record<string, unknown> | null;
57
+ /** Group metadata entity selected for the tile's primary content. */
58
+ group?: Record<string, unknown> | null;
59
+ /** All tileset groups, used to resolve a selected content entry's group. */
60
+ groups?: readonly (Record<string, unknown> | null)[];
61
+ /** Tile metadata entity. */
62
+ tile?: Record<string, unknown> | null;
63
+ /** Content metadata entity. */
64
+ content?: Record<string, unknown> | null;
65
+ /** Implicit-subtree metadata entity. */
66
+ subtree?: Record<string, unknown> | null;
67
+ };
68
+
69
+ /** Extracts mesh-feature declarations from a decoded glTF-like payload. */
70
+ export function getTile3DFeatureIdSets(payload: unknown): Tile3DFeatureIdSet[] {
71
+ const featureIdSets: Tile3DFeatureIdSet[] = [];
72
+ const visited = new Set<object>();
73
+ const visit = (value: unknown): void => {
74
+ if (!value || typeof value !== 'object') {
75
+ return;
76
+ }
77
+ if (visited.has(value as object)) {
78
+ return;
79
+ }
80
+ visited.add(value as object);
81
+ const record = value as Record<string, any>;
82
+ const declarations = record.extensions?.EXT_mesh_features?.featureIds;
83
+ if (Array.isArray(declarations)) {
84
+ for (const declaration of declarations) {
85
+ if (!declaration || typeof declaration !== 'object') {
86
+ continue;
87
+ }
88
+ const source = declaration.texture
89
+ ? 'texture'
90
+ : declaration.implicit
91
+ ? 'implicit'
92
+ : declaration.attribute !== undefined
93
+ ? 'attribute'
94
+ : declaration.propertyTable !== undefined
95
+ ? 'property-table'
96
+ : 'constant';
97
+ featureIdSets.push({
98
+ source,
99
+ attribute: declaration.attribute,
100
+ propertyTable: declaration.propertyTable,
101
+ constant: declaration.constant,
102
+ texture: declaration.texture,
103
+ implicit: declaration.implicit,
104
+ values: declaration.values,
105
+ details: declaration
106
+ });
107
+ }
108
+ }
109
+ for (const child of Object.values(record)) {
110
+ visit(child);
111
+ }
112
+ };
113
+ visit(payload);
114
+ return featureIdSets;
115
+ }