@loaders.gl/tiles 5.0.0-alpha.2 → 5.0.0-alpha.3

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 (109) hide show
  1. package/LICENSE +1 -1
  2. package/dist/dist.dev.js +17325 -5650
  3. package/dist/dist.min.js +4 -1
  4. package/dist/index.cjs +1924 -178
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +9 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +6 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/point-cloud/point-cloud-tileset.d.ts +4 -0
  11. package/dist/point-cloud/point-cloud-tileset.d.ts.map +1 -1
  12. package/dist/point-cloud/point-cloud-tileset.js +51 -12
  13. package/dist/point-cloud/point-cloud-tileset.js.map +1 -1
  14. package/dist/point-cloud/types.d.ts +17 -0
  15. package/dist/point-cloud/types.d.ts.map +1 -1
  16. package/dist/raster-set.d.ts +10 -8
  17. package/dist/raster-set.d.ts.map +1 -1
  18. package/dist/raster-set.js +18 -1
  19. package/dist/raster-set.js.map +1 -1
  20. package/dist/spatial/format-spatial-reference.d.ts +67 -0
  21. package/dist/spatial/format-spatial-reference.d.ts.map +1 -0
  22. package/dist/spatial/format-spatial-reference.js +233 -0
  23. package/dist/spatial/format-spatial-reference.js.map +1 -0
  24. package/dist/spatial/get-spatial-coordinate-frame.d.ts +9 -0
  25. package/dist/spatial/get-spatial-coordinate-frame.d.ts.map +1 -0
  26. package/dist/spatial/get-spatial-coordinate-frame.js +58 -0
  27. package/dist/spatial/get-spatial-coordinate-frame.js.map +1 -0
  28. package/dist/spatial/i3s-elevation.d.ts +20 -0
  29. package/dist/spatial/i3s-elevation.d.ts.map +1 -0
  30. package/dist/spatial/i3s-elevation.js +90 -0
  31. package/dist/spatial/i3s-elevation.js.map +1 -0
  32. package/dist/spatial/i3s-spatial-transformer.d.ts +189 -0
  33. package/dist/spatial/i3s-spatial-transformer.d.ts.map +1 -0
  34. package/dist/spatial/i3s-spatial-transformer.js +686 -0
  35. package/dist/spatial/i3s-spatial-transformer.js.map +1 -0
  36. package/dist/spatial/spatial-coordinate-transformer.d.ts +48 -0
  37. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -0
  38. package/dist/spatial/spatial-coordinate-transformer.js +211 -0
  39. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -0
  40. package/dist/spatial/spatial-resource-registry.d.ts +44 -0
  41. package/dist/spatial/spatial-resource-registry.d.ts.map +1 -0
  42. package/dist/spatial/spatial-resource-registry.js +67 -0
  43. package/dist/spatial/spatial-resource-registry.js.map +1 -0
  44. package/dist/spatial/spatial-types.d.ts +190 -0
  45. package/dist/spatial/spatial-types.d.ts.map +1 -0
  46. package/dist/spatial/spatial-types.js +192 -0
  47. package/dist/spatial/spatial-types.js.map +1 -0
  48. package/dist/tileset-3d/common/tile-3d.d.ts +49 -1
  49. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  50. package/dist/tileset-3d/common/tile-3d.js +132 -64
  51. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  52. package/dist/tileset-3d/common/tileset-3d.d.ts +25 -0
  53. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  54. package/dist/tileset-3d/common/tileset-3d.js +49 -7
  55. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  56. package/dist/tileset-3d/common/tileset-source.d.ts +14 -0
  57. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  58. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  59. package/dist/tileset-3d/common/tileset-traverser.d.ts +6 -1
  60. package/dist/tileset-3d/common/tileset-traverser.d.ts.map +1 -1
  61. package/dist/tileset-3d/common/tileset-traverser.js +11 -6
  62. package/dist/tileset-3d/common/tileset-traverser.js.map +1 -1
  63. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts +26 -1
  64. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.d.ts.map +1 -1
  65. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js +62 -18
  66. package/dist/tileset-3d/format-3d-tiles/implicit-tiling.js.map +1 -1
  67. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +1 -7
  68. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  69. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +32 -55
  70. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  71. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +14 -1
  72. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  73. package/dist/tileset-3d/format-i3s/i3s-source.js +107 -17
  74. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  75. package/dist/tileset-3d/helpers/bounding-volume.d.ts.map +1 -1
  76. package/dist/tileset-3d/helpers/bounding-volume.js +3 -33
  77. package/dist/tileset-3d/helpers/bounding-volume.js.map +1 -1
  78. package/dist/tileset-3d/helpers/frame-state.d.ts +3 -1
  79. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  80. package/dist/tileset-3d/helpers/frame-state.js +1 -0
  81. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  82. package/dist/tileset-3d/helpers/i3s-lod.d.ts.map +1 -1
  83. package/dist/tileset-3d/helpers/i3s-lod.js +20 -8
  84. package/dist/tileset-3d/helpers/i3s-lod.js.map +1 -1
  85. package/dist/types.d.ts +3 -0
  86. package/dist/types.d.ts.map +1 -1
  87. package/package.json +12 -9
  88. package/src/index.ts +42 -1
  89. package/src/point-cloud/point-cloud-tileset.ts +66 -12
  90. package/src/point-cloud/types.ts +17 -0
  91. package/src/raster-set.ts +46 -11
  92. package/src/spatial/format-spatial-reference.ts +311 -0
  93. package/src/spatial/get-spatial-coordinate-frame.ts +74 -0
  94. package/src/spatial/i3s-elevation.ts +96 -0
  95. package/src/spatial/i3s-spatial-transformer.ts +957 -0
  96. package/src/spatial/spatial-coordinate-transformer.ts +276 -0
  97. package/src/spatial/spatial-resource-registry.ts +79 -0
  98. package/src/spatial/spatial-types.ts +422 -0
  99. package/src/tileset-3d/common/tile-3d.ts +156 -69
  100. package/src/tileset-3d/common/tileset-3d.ts +77 -6
  101. package/src/tileset-3d/common/tileset-source.ts +15 -0
  102. package/src/tileset-3d/common/tileset-traverser.ts +11 -6
  103. package/src/tileset-3d/format-3d-tiles/implicit-tiling.ts +100 -22
  104. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +36 -56
  105. package/src/tileset-3d/format-i3s/i3s-source.ts +119 -18
  106. package/src/tileset-3d/helpers/bounding-volume.ts +3 -55
  107. package/src/tileset-3d/helpers/frame-state.ts +3 -0
  108. package/src/tileset-3d/helpers/i3s-lod.ts +22 -8
  109. package/src/types.ts +3 -0
@@ -23,8 +23,12 @@ export type ImplicitTileCoordinates = {
23
23
  export type ImplicitTilingDescriptor = {
24
24
  /** Absolute template URL for render content, or an empty string for a contentless hierarchy. */
25
25
  contentUrlTemplate: string;
26
+ /** Absolute templates for every implicit content stream, in source order. */
27
+ contentUrlTemplates?: string[];
26
28
  /** Non-URI content metadata inherited by each available implicit content resource. */
27
29
  contentHeader?: Record<string, any>;
30
+ /** Metadata inherited by every implicit content stream, in source order. */
31
+ contentHeaders?: Array<Record<string, any>>;
28
32
  /** Absolute template URL for subtree availability files. */
29
33
  subtreesUrlTemplate: string;
30
34
  /** Spatial subdivision used by the hierarchy. */
@@ -66,12 +70,20 @@ export type ParsedImplicitSubtree = {
66
70
  /** Availability of tiles inside this subtree. */
67
71
  tileAvailability: ImplicitAvailability;
68
72
  /**
69
- * Availability of tile content; only the first multiple-content stream is currently used.
73
+ * Availability of tile content. Array entries correspond to content templates in source order.
70
74
  * Omitted availability denotes a metadata-only subtree with no render content.
71
75
  */
72
76
  contentAvailability?: ImplicitAvailability | ImplicitAvailability[];
73
77
  /** Availability of subtree roots immediately below this subtree. */
74
78
  childSubtreeAvailability: ImplicitAvailability;
79
+ /** Property-table payloads referenced by subtree metadata declarations. */
80
+ propertyTables?: unknown;
81
+ /** Property-table reference for metadata attached to each available tile. */
82
+ tileMetadata?: unknown;
83
+ /** Property-table references for metadata attached to each content stream. */
84
+ contentMetadata?: unknown;
85
+ /** Metadata entity attached to the subtree itself. */
86
+ subtreeMetadata?: unknown;
75
87
  };
76
88
 
77
89
  /** Runtime header produced for an available implicit tile. */
@@ -82,6 +94,20 @@ export type ImplicitTileHeader = Record<string, any> & {
82
94
  children: ImplicitTileHeader[];
83
95
  /** Optional lazy reference when this tile begins a child subtree. */
84
96
  implicitSubtree?: ImplicitSubtreeReference;
97
+ /** Raw subtree metadata references preserved for application-level interpretation. */
98
+ implicitMetadata?: ImplicitSubtreeMetadata;
99
+ };
100
+
101
+ /** Metadata references inherited by generated implicit tile headers. */
102
+ export type ImplicitSubtreeMetadata = {
103
+ /** Property tables declared by the subtree. */
104
+ propertyTables?: unknown;
105
+ /** Tile-level metadata property-table reference. */
106
+ tileMetadata?: unknown;
107
+ /** Content-level metadata property-table references in content order. */
108
+ contentMetadata?: unknown;
109
+ /** Subtree-level metadata entity. */
110
+ subtreeMetadata?: unknown;
85
111
  };
86
112
 
87
113
  /** Result of materializing exactly one subtree resource. */
@@ -198,8 +224,8 @@ function materializeAvailableTile(
198
224
  }
199
225
 
200
226
  counters.tileCount++;
201
- const contentAvailable = getAvailabilityValue(
202
- getPrimaryContentAvailability(subtree.contentAvailability),
227
+ const contentAvailability = getContentAvailability(
228
+ subtree.contentAvailability,
203
229
  availabilityIndex
204
230
  );
205
231
  const children: ImplicitTileHeader[] = [];
@@ -235,7 +261,7 @@ function materializeAvailableTile(
235
261
  childIndex,
236
262
  descriptor.subdivisionScheme
237
263
  );
238
- children.push(createLazyImplicitTileHeader(descriptor, childCoordinates));
264
+ children.push(createLazyImplicitTileHeader(subtree, descriptor, childCoordinates));
239
265
  counters.childSubtreeCount++;
240
266
  }
241
267
  }
@@ -243,10 +269,11 @@ function materializeAvailableTile(
243
269
  }
244
270
 
245
271
  return formatImplicitTileHeader(
272
+ subtree,
246
273
  descriptor,
247
274
  reference,
248
275
  globalCoordinates,
249
- contentAvailable,
276
+ contentAvailability,
250
277
  children
251
278
  );
252
279
  }
@@ -259,6 +286,7 @@ function materializeAvailableTile(
259
286
  * @returns Header that participates in culling and SSE before its subtree is requested.
260
287
  */
261
288
  function createLazyImplicitTileHeader(
289
+ subtree: ParsedImplicitSubtree,
262
290
  descriptor: ImplicitTilingDescriptor,
263
291
  coordinates: ImplicitTileCoordinates
264
292
  ): ImplicitTileHeader {
@@ -276,7 +304,8 @@ function createLazyImplicitTileHeader(
276
304
  lodMetricType: descriptor.lodMetricType,
277
305
  lodMetricValue: descriptor.rootLodMetricValue / 2 ** coordinates.level,
278
306
  refine: descriptor.refine,
279
- type: TILE_TYPE.EMPTY
307
+ type: TILE_TYPE.EMPTY,
308
+ implicitMetadata: getImplicitSubtreeMetadata(subtree)
280
309
  };
281
310
  }
282
311
 
@@ -286,28 +315,48 @@ function createLazyImplicitTileHeader(
286
315
  * @param descriptor - Shared implicit hierarchy description.
287
316
  * @param reference - Current subtree reference used to build stable IDs.
288
317
  * @param coordinates - Global tile coordinates.
289
- * @param contentAvailable - Whether the subtree declares render content for this tile.
318
+ * @param contentAvailability - Availability for each content stream in source order.
290
319
  * @param children - Materialized or lazy child headers.
291
320
  * @returns Runtime tile header.
292
321
  */
293
322
  function formatImplicitTileHeader(
323
+ subtree: ParsedImplicitSubtree,
294
324
  descriptor: ImplicitTilingDescriptor,
295
325
  reference: ImplicitSubtreeReference,
296
326
  coordinates: ImplicitTileCoordinates,
297
- contentAvailable: boolean,
327
+ contentAvailability: boolean[],
298
328
  children: ImplicitTileHeader[]
299
329
  ): ImplicitTileHeader {
300
- const contentUrl =
301
- contentAvailable && descriptor.contentUrlTemplate
302
- ? replaceImplicitUrlTemplate(descriptor.contentUrlTemplate, coordinates)
303
- : undefined;
330
+ const contentUrlTemplates = descriptor.contentUrlTemplates?.length
331
+ ? descriptor.contentUrlTemplates
332
+ : [descriptor.contentUrlTemplate];
333
+ const contentEntries = contentUrlTemplates
334
+ .map((templateUrl, contentIndex) => {
335
+ if (!contentAvailability[contentIndex] || !templateUrl) {
336
+ return null;
337
+ }
338
+ return {
339
+ contentIndex,
340
+ contentUrl: replaceImplicitUrlTemplate(templateUrl, coordinates)
341
+ };
342
+ })
343
+ .filter((entry): entry is {contentIndex: number; contentUrl: string} => Boolean(entry));
344
+ const availableContentUrls = contentEntries.map(entry => entry.contentUrl);
345
+ const content = contentEntries.map(({contentIndex, contentUrl}) => ({
346
+ ...(descriptor.contentHeaders?.[contentIndex] ||
347
+ (contentIndex === 0 ? descriptor.contentHeader : {}) ||
348
+ {}),
349
+ uri: contentUrl
350
+ }));
351
+ const contentUrl = availableContentUrls[0];
304
352
  const lodMetricValue = descriptor.rootLodMetricValue / 2 ** coordinates.level;
305
353
 
306
354
  return {
307
355
  id: getImplicitTileId(reference, coordinates),
308
356
  children,
309
357
  contentUrl,
310
- content: contentUrl ? {...descriptor.contentHeader, uri: contentUrl} : undefined,
358
+ content: content.length > 1 ? content : content[0],
359
+ contentUrls: availableContentUrls,
311
360
  refine: descriptor.refine,
312
361
  type: getImplicitTileType(contentUrl),
313
362
  lodMetricType: descriptor.lodMetricType,
@@ -317,7 +366,36 @@ function formatImplicitTileHeader(
317
366
  descriptor.rootBoundingVolume,
318
367
  coordinates,
319
368
  descriptor.subdivisionScheme
320
- )
369
+ ),
370
+ implicitMetadata: getImplicitSubtreeMetadata(subtree)
371
+ };
372
+ }
373
+
374
+ /**
375
+ * Copies subtree metadata references without interpreting property-table schemas.
376
+ *
377
+ * Keeping the references on generated headers makes metadata available to applications while
378
+ * avoiding a false promise that the runtime has already decoded classes, enums, or values.
379
+ *
380
+ * @param subtree - Parsed subtree containing optional metadata declarations.
381
+ * @returns Metadata references, or `undefined` when the subtree declares none.
382
+ */
383
+ function getImplicitSubtreeMetadata(
384
+ subtree: ParsedImplicitSubtree
385
+ ): ImplicitSubtreeMetadata | undefined {
386
+ if (
387
+ !subtree.propertyTables &&
388
+ subtree.tileMetadata === undefined &&
389
+ !subtree.contentMetadata &&
390
+ subtree.subtreeMetadata === undefined
391
+ ) {
392
+ return undefined;
393
+ }
394
+ return {
395
+ propertyTables: subtree.propertyTables,
396
+ tileMetadata: subtree.tileMetadata,
397
+ contentMetadata: subtree.contentMetadata,
398
+ subtreeMetadata: subtree.subtreeMetadata
321
399
  };
322
400
  }
323
401
 
@@ -356,17 +434,17 @@ function getAvailabilityValue(availability: ImplicitAvailability, index: number)
356
434
  }
357
435
 
358
436
  /**
359
- * Selects the first content availability stream until multiple contents are supported.
437
+ * Reads every content availability stream for one tile in descriptor/source order.
360
438
  *
361
439
  * @param availability - Single or multiple-content availability declaration.
362
- * @returns Primary content availability declaration.
440
+ * @returns Availability flags aligned with the content URL templates.
363
441
  */
364
- function getPrimaryContentAvailability(
365
- availability: ImplicitAvailability | ImplicitAvailability[] | undefined
366
- ): ImplicitAvailability {
367
- return Array.isArray(availability)
368
- ? availability[0] || {constant: 0}
369
- : availability || {constant: 0};
442
+ function getContentAvailability(
443
+ availability: ImplicitAvailability | ImplicitAvailability[] | undefined,
444
+ index: number
445
+ ): boolean[] {
446
+ const streams = Array.isArray(availability) ? availability : [availability];
447
+ return streams.map(stream => getAvailabilityValue(stream || {constant: 0}, index));
370
448
  }
371
449
 
372
450
  /**
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {path} from '@loaders.gl/loader-utils';
5
+ import {path, RequestCache} from '@loaders.gl/loader-utils';
6
6
  import {Ellipsoid} from '@math.gl/geospatial';
7
7
  import {Vector3} from '@math.gl/core';
8
8
  import type {CoreAPI, LoaderOptions, LoaderWithParser} from '@loaders.gl/loader-utils';
@@ -26,6 +26,7 @@ import {getZoomFromBoundingVolume} from '../helpers/zoom';
26
26
  import {TILESET_TYPE} from '../../constants';
27
27
  import type {TilesetTraverser, TilesetTraverserProps} from '../common/tileset-traverser';
28
28
  import type {FrameState} from '../helpers/frame-state';
29
+ import {get3DTilesSpatialReference} from '../../spatial/format-spatial-reference';
29
30
  import {
30
31
  materializeImplicitSubtree,
31
32
  type ImplicitSubtreeReference,
@@ -92,12 +93,8 @@ export class Tiles3DSource implements Tileset3DSource {
92
93
  private readonly queryParams: Record<string, string> = {};
93
94
  /** Final request URLs cached by unmodified tile content URL. */
94
95
  private readonly tileUrlCache: Map<string, string> = new Map();
95
- /** Parsed subtree promises keyed by final source URL for request deduplication and LRU reuse. */
96
- private readonly implicitSubtreeCache: Map<string, Promise<ParsedImplicitSubtree>> = new Map();
97
- /** URLs whose subtree resource requests have not settled. */
98
- private readonly pendingImplicitSubtreeUrls: Set<string> = new Set();
99
- /** Maximum number of settled parsed subtrees retained by this source. */
100
- private readonly maximumCachedSubtrees: number;
96
+ /** Parsed subtree requests keyed by final source URL for deduplication and LRU reuse. */
97
+ private readonly implicitSubtreeCache: RequestCache<ParsedImplicitSubtree>;
101
98
  /** Mutable counters exposed as a defensive snapshot through {@link getImplicitTilingStats}. */
102
99
  private readonly implicitTilingStats: Omit<
103
100
  ImplicitTilingStats,
@@ -132,9 +129,10 @@ export class Tiles3DSource implements Tileset3DSource {
132
129
  const maximumCachedSubtrees = Number(
133
130
  (loadOptions['3d-tiles'] as Record<string, unknown> | undefined)?.maximumCachedSubtrees
134
131
  );
135
- this.maximumCachedSubtrees = Number.isFinite(maximumCachedSubtrees)
132
+ const maximumCacheEntries = Number.isFinite(maximumCachedSubtrees)
136
133
  ? Math.max(0, Math.floor(maximumCachedSubtrees))
137
134
  : DEFAULT_MAXIMUM_CACHED_SUBTREES;
135
+ this.implicitSubtreeCache = new RequestCache({maxEntries: maximumCacheEntries});
138
136
  }
139
137
 
140
138
  /**
@@ -147,7 +145,6 @@ export class Tiles3DSource implements Tileset3DSource {
147
145
  this.destroyed = true;
148
146
  this.tileUrlCache.clear();
149
147
  this.implicitSubtreeCache.clear();
150
- this.pendingImplicitSubtreeUrls.clear();
151
148
  }
152
149
 
153
150
  /**
@@ -205,7 +202,8 @@ export class Tiles3DSource implements Tileset3DSource {
205
202
  tileset: this.rootTileset,
206
203
  lodMetricType: this.rootTileset.lodMetricType,
207
204
  lodMetricValue: this.rootTileset.lodMetricValue,
208
- refine: this.rootTileset.root?.refine
205
+ refine: this.rootTileset.root?.refine,
206
+ spatialReference: get3DTilesSpatialReference(this.rootTileset)
209
207
  };
210
208
  }
211
209
 
@@ -275,7 +273,7 @@ export class Tiles3DSource implements Tileset3DSource {
275
273
  * Loads binary content or nested tileset JSON for a runtime tile.
276
274
  */
277
275
  async loadTileContent(tile: Tile3D): Promise<TileContentLoadResult> {
278
- const contentUrl = this.getTileUrl(tile.contentUrl);
276
+ const contentUrls = (tile.contentUrls || [tile.contentUrl]).filter(Boolean);
279
277
  const tilesetLoaderOptions =
280
278
  (this.loadOptions[this.loader.id] as Record<string, unknown>) || {};
281
279
  const options = {
@@ -289,12 +287,18 @@ export class Tiles3DSource implements Tileset3DSource {
289
287
  }
290
288
  };
291
289
 
292
- const content = await this.loadResourceData(contentUrl, options);
293
- tile.content = content;
290
+ const contents = await Promise.all(
291
+ contentUrls.map(contentUrl => this.loadResourceData(this.getTileUrl(contentUrl), options))
292
+ );
293
+ tile.contents = contents;
294
+ tile.content = contents[0] || null;
295
+ const nestedTilesets = contents.filter(content => content?.shape === 'tileset3d');
294
296
 
295
297
  return {
296
298
  loaded: true,
297
- nestedTileset: content?.shape === 'tileset3d' ? content : undefined
299
+ contents,
300
+ nestedTileset: nestedTilesets[0],
301
+ nestedTilesets
298
302
  };
299
303
  }
300
304
 
@@ -350,8 +354,8 @@ export class Tiles3DSource implements Tileset3DSource {
350
354
  getImplicitTilingStats(): ImplicitTilingStats {
351
355
  return {
352
356
  ...this.implicitTilingStats,
353
- cachedSubtrees: this.implicitSubtreeCache.size - this.pendingImplicitSubtreeUrls.size,
354
- pendingSubtrees: this.pendingImplicitSubtreeUrls.size
357
+ cachedSubtrees: this.implicitSubtreeCache.size - this.implicitSubtreeCache.pendingSize,
358
+ pendingSubtrees: this.implicitSubtreeCache.pendingSize
355
359
  };
356
360
  }
357
361
 
@@ -458,7 +462,8 @@ export class Tiles3DSource implements Tileset3DSource {
458
462
  * Updates content-format flags and installs nested tileset subtrees.
459
463
  */
460
464
  onTileLoaded(tileset: Tileset3D, tile: Tile3D, loadResult: TileContentLoadResult): void {
461
- const {extensionsRemoved = []} = tile.content?.gltf || {};
465
+ const contents = tile.contents.length ? tile.contents : [tile.content];
466
+ const extensionsRemoved = contents.flatMap(content => content?.gltf?.extensionsRemoved || []);
462
467
  if (extensionsRemoved.includes('KHR_draco_mesh_compression')) {
463
468
  this.contentFormats.draco = true;
464
469
  }
@@ -469,8 +474,10 @@ export class Tiles3DSource implements Tileset3DSource {
469
474
  this.contentFormats.ktx2 = true;
470
475
  }
471
476
 
472
- if (loadResult.nestedTileset) {
473
- tileset._initializeTileHeaders(loadResult.nestedTileset, tile);
477
+ const nestedTilesets =
478
+ loadResult.nestedTilesets || (loadResult.nestedTileset ? [loadResult.nestedTileset] : []);
479
+ for (const nestedTileset of nestedTilesets) {
480
+ tileset._initializeTileHeaders(nestedTileset, tile);
474
481
  }
475
482
  }
476
483
 
@@ -525,33 +532,21 @@ export class Tiles3DSource implements Tileset3DSource {
525
532
  const cachedSubtree = this.implicitSubtreeCache.get(subtreeUrl);
526
533
  if (cachedSubtree) {
527
534
  this.implicitTilingStats.cacheHits++;
528
- this.implicitSubtreeCache.delete(subtreeUrl);
529
- this.implicitSubtreeCache.set(subtreeUrl, cachedSubtree);
530
535
  return await cachedSubtree;
531
536
  }
532
537
 
533
538
  this.implicitTilingStats.requestedSubtrees++;
534
- this.pendingImplicitSubtreeUrls.add(subtreeUrl);
535
539
  const loaderOptions = (this.loadOptions[this.loader.id] as Record<string, unknown>) || {};
536
- const subtreePromise = this.loadResourceData(subtreeUrl, {
537
- ...this.loadOptions,
538
- [this.loader.id]: {
539
- ...loaderOptions,
540
- isTileset: false,
541
- isSubtree: true
542
- }
543
- }) as Promise<ParsedImplicitSubtree>;
544
- this.implicitSubtreeCache.set(subtreeUrl, subtreePromise);
545
-
546
- try {
547
- return await subtreePromise;
548
- } catch (error) {
549
- this.implicitSubtreeCache.delete(subtreeUrl);
550
- throw error;
551
- } finally {
552
- this.pendingImplicitSubtreeUrls.delete(subtreeUrl);
553
- this.trimImplicitSubtreeCache();
554
- }
540
+ return await this.implicitSubtreeCache.getOrLoad(subtreeUrl, async () => {
541
+ return (await this.loadResourceData(subtreeUrl, {
542
+ ...this.loadOptions,
543
+ [this.loader.id]: {
544
+ ...loaderOptions,
545
+ isTileset: false,
546
+ isSubtree: true
547
+ }
548
+ })) as ParsedImplicitSubtree;
549
+ });
555
550
  }
556
551
 
557
552
  /**
@@ -586,21 +581,6 @@ export class Tiles3DSource implements Tileset3DSource {
586
581
  }
587
582
  return materializedTileCount;
588
583
  }
589
-
590
- /** Evicts least-recently-used settled subtree entries until the configured bound is met. */
591
- private trimImplicitSubtreeCache(): void {
592
- if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
593
- return;
594
- }
595
- for (const subtreeUrl of this.implicitSubtreeCache.keys()) {
596
- if (!this.pendingImplicitSubtreeUrls.has(subtreeUrl)) {
597
- this.implicitSubtreeCache.delete(subtreeUrl);
598
- }
599
- if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
600
- break;
601
- }
602
- }
603
- }
604
584
  }
605
585
 
606
586
  function isTilesetRequest(input: TilesetSourceInput): input is TilesetSourceRequest {
@@ -25,6 +25,8 @@ import type {
25
25
  import {getZoomFromExtent, getZoomFromFullExtent} from '../helpers/zoom';
26
26
  import {TILESET_TYPE} from '../../constants';
27
27
  import type {TilesetTraverser, TilesetTraverserProps} from '../common/tileset-traverser';
28
+ import {getI3SSpatialReference} from '../../spatial/format-spatial-reference';
29
+ import {I3SSpatialTransformer} from '../../spatial/i3s-spatial-transformer';
28
30
 
29
31
  const EMPTY_CONTENT_FORMATS: TilesetContentFormats = {
30
32
  draco: false,
@@ -63,6 +65,10 @@ export class I3SSource implements Tileset3DSource {
63
65
  private readonly queryParams: Record<string, string> = {};
64
66
  private readonly resolver?: TilesetSourceResolver;
65
67
  private rootTileset: TilesetJSON;
68
+ /** Spatial adapter created lazily after Tileset3D applies target options. */
69
+ private spatialTransformer?: I3SSpatialTransformer;
70
+ /** Root header prepared after asynchronous elevation sampling. */
71
+ private preparedRootHeader?: any;
66
72
 
67
73
  /**
68
74
  * Creates an I3S source.
@@ -79,6 +85,7 @@ export class I3SSource implements Tileset3DSource {
79
85
  this.resolver = request.resolver;
80
86
  this.coreApi = request.coreApi;
81
87
  this.loadOptions = loadOptions;
88
+ this.initializeQueryParams(loadOptions);
82
89
  }
83
90
 
84
91
  /**
@@ -86,7 +93,7 @@ export class I3SSource implements Tileset3DSource {
86
93
  */
87
94
  async initialize(): Promise<void> {
88
95
  if (!this.rootTileset) {
89
- this.rootTileset = await this.loadRootData(this.url, this.loadOptions);
96
+ this.rootTileset = await this.loadRootData(this.getTileUrl(this.url), this.loadOptions);
90
97
  }
91
98
  this.tileset = this.rootTileset;
92
99
 
@@ -94,14 +101,6 @@ export class I3SSource implements Tileset3DSource {
94
101
  this.rootTileset.root = await this.rootTileset.root;
95
102
  }
96
103
 
97
- const i3sOptions = this.loadOptions.i3s;
98
- if (i3sOptions && typeof i3sOptions === 'object' && 'token' in i3sOptions) {
99
- const token = (i3sOptions as Record<string, unknown>).token;
100
- if (typeof token === 'string') {
101
- this.queryParams.token = token;
102
- }
103
- }
104
-
105
104
  this.metadata = {
106
105
  type: this.type,
107
106
  loader: this.loader,
@@ -110,7 +109,8 @@ export class I3SSource implements Tileset3DSource {
110
109
  tileset: this.rootTileset,
111
110
  lodMetricType: this.rootTileset.lodMetricType,
112
111
  lodMetricValue: this.rootTileset.lodMetricValue,
113
- refine: this.rootTileset.root?.refine
112
+ refine: this.rootTileset.root?.refine,
113
+ spatialReference: getI3SSpatialReference(this.rootTileset)
114
114
  };
115
115
  }
116
116
 
@@ -131,6 +131,26 @@ export class I3SSource implements Tileset3DSource {
131
131
  return this.getMetadata().tileset;
132
132
  }
133
133
 
134
+ /** Prepare option-dependent spatial state and the asynchronously placed root bound. */
135
+ async prepareTileset(tileset: Tileset3D): Promise<void> {
136
+ const spatialReference = tileset.spatialReference;
137
+ if (spatialReference.status === 'unresolved') {
138
+ throw new Error(
139
+ spatialReference.warnings[0] ||
140
+ 'I3S spatial operations cannot be resolved from the supplied metadata and options'
141
+ );
142
+ }
143
+ if (spatialReference.status !== 'transformable' && spatialReference.status !== 'transformed') {
144
+ return;
145
+ }
146
+ this.spatialTransformer ||= new I3SSpatialTransformer(
147
+ spatialReference,
148
+ tileset.options.spatial
149
+ );
150
+ tileset.spatialReference = this.spatialTransformer.spatialReference;
151
+ this.preparedRootHeader = await this.transformTileHeader(this.getMetadata().tileset.root);
152
+ }
153
+
134
154
  /**
135
155
  * Creates the runtime root tile for an I3S subtree.
136
156
  */
@@ -139,7 +159,11 @@ export class I3SSource implements Tileset3DSource {
139
159
  tilesetJson: TilesetJSON,
140
160
  parentTile?: Tile3D | null
141
161
  ): Tile3D {
142
- const rootTile = new Tile3DNode(tileset, tilesetJson.root, parentTile || undefined);
162
+ const rootHeader = parentTile ? tilesetJson.root : this.preparedRootHeader || tilesetJson.root;
163
+ if (this.spatialTransformer && !this.preparedRootHeader) {
164
+ throw new Error('I3S spatial root header was not prepared before tile initialization');
165
+ }
166
+ const rootTile = new Tile3DNode(tileset, rootHeader, parentTile || undefined);
143
167
  if (parentTile) {
144
168
  parentTile.children.push(rootTile);
145
169
  rootTile.depth = parentTile.depth + 1;
@@ -170,16 +194,22 @@ export class I3SSource implements Tileset3DSource {
170
194
  _tileOptions: {
171
195
  attributeUrls: tile.header.attributeUrls,
172
196
  textureUrl: tile.header.textureUrl,
197
+ textureUrls: tile.header.textureUrls,
173
198
  textureFormat: tile.header.textureFormat,
174
199
  textureLoaderOptions: tile.header.textureLoaderOptions,
175
200
  materialDefinition: tile.header.materialDefinition,
201
+ layerType: tile.header.layerType,
202
+ pointRenderer: tile.header.pointRenderer,
203
+ pointSymbol: tile.header.pointSymbol,
176
204
  isDracoGeometry: tile.header.isDracoGeometry,
177
205
  mbs: tile.header.mbs
178
206
  },
179
207
  _tilesetOptions: {
180
208
  store: metadata.tileset.store,
181
209
  attributeStorageInfo: metadata.tileset.attributeStorageInfo,
182
- fields: metadata.tileset.fields
210
+ fields: metadata.tileset.fields,
211
+ spatialReference: tile.tileset.spatialReference,
212
+ spatialOptions: tile.tileset.options.spatial
183
213
  },
184
214
  isTileHeader: false
185
215
  }
@@ -198,8 +228,9 @@ export class I3SSource implements Tileset3DSource {
198
228
  _frameState: FrameState
199
229
  ): Promise<any> {
200
230
  const metadata = this.getMetadata();
201
- if (metadata.tileset.nodePages) {
202
- return await metadata.tileset.nodePagesTile.formTileFromNodePages(childId);
231
+ if (metadata.tileset.nodePages || metadata.tileset.pointNodePages) {
232
+ const header = await metadata.tileset.nodePagesTile.formTileFromNodePages(childId);
233
+ return await this.transformTileHeader(header);
203
234
  }
204
235
 
205
236
  const nodeUrl = this.getTileUrl(`${this.url}/nodes/${childId}`);
@@ -215,7 +246,8 @@ export class I3SSource implements Tileset3DSource {
215
246
  }
216
247
  };
217
248
 
218
- return await this.loadResourceData(nodeUrl, options);
249
+ const header = await this.loadResourceData(nodeUrl, options);
250
+ return await this.transformTileHeader(header);
219
251
  }
220
252
 
221
253
  /**
@@ -230,14 +262,48 @@ export class I3SSource implements Tileset3DSource {
230
262
  return tilePath;
231
263
  }
232
264
 
233
- const queryParams = new URLSearchParams(this.queryParams).toString();
234
- return `${tilePath}${tilePath.includes('?') ? '&' : '?'}${queryParams}`;
265
+ const queryDelimiterIndex = tilePath.indexOf('?');
266
+ if (queryDelimiterIndex === -1) {
267
+ return `${tilePath}?${new URLSearchParams(this.queryParams).toString()}`;
268
+ }
269
+
270
+ const existingQuery = tilePath.slice(queryDelimiterIndex + 1);
271
+ const existingQueryKeys = new Set(
272
+ existingQuery.split('&').map(parameter => parameter.split('=', 1)[0])
273
+ );
274
+ const queryParams = new URLSearchParams();
275
+ for (const [key, value] of Object.entries(this.queryParams)) {
276
+ if (!existingQueryKeys.has(key)) {
277
+ queryParams.set(key, value);
278
+ }
279
+ }
280
+
281
+ const queryString = queryParams.toString();
282
+ return queryString ? `${tilePath}${existingQuery ? '&' : ''}${queryString}` : tilePath;
235
283
  }
236
284
 
237
285
  /**
238
286
  * Derives the default view state from full extent or store extent metadata.
239
287
  */
240
- getViewState(_rootTile: Tile3D | null): TilesetSourceViewState {
288
+ getViewState(rootTile: Tile3D | null): TilesetSourceViewState {
289
+ if (this.spatialTransformer && rootTile) {
290
+ const center = new Vector3(rootTile.boundingVolume.center);
291
+ const cartographicCenter = new Vector3(rootTile.header.i3sLodMbs.slice(0, 3));
292
+ if (this.spatialTransformer.targetCoordinateFrame === 'geographic') {
293
+ return {
294
+ boundingVolume: rootTile.boundingVolume,
295
+ cartographicCenter,
296
+ cartesianCenter: center,
297
+ zoom: 1
298
+ };
299
+ }
300
+ return {
301
+ boundingVolume: rootTile.boundingVolume,
302
+ cartographicCenter,
303
+ cartesianCenter: center,
304
+ zoom: 1
305
+ };
306
+ }
241
307
  const metadata = this.getMetadata();
242
308
  const fullExtent = metadata.tileset.fullExtent;
243
309
  if (fullExtent) {
@@ -325,6 +391,20 @@ export class I3SSource implements Tileset3DSource {
325
391
  return await this.loadWithCoreApi(url, options);
326
392
  }
327
393
 
394
+ /**
395
+ * Initializes query parameters before any URL-backed resource is requested.
396
+ * @param loadOptions Loader options that may contain an ArcGIS token.
397
+ */
398
+ private initializeQueryParams(loadOptions: LoaderOptions): void {
399
+ const i3sOptions = loadOptions.i3s;
400
+ if (i3sOptions && typeof i3sOptions === 'object' && 'token' in i3sOptions) {
401
+ const token = (i3sOptions as Record<string, unknown>).token;
402
+ if (typeof token === 'string') {
403
+ this.queryParams.token = token;
404
+ }
405
+ }
406
+ }
407
+
328
408
  /**
329
409
  * Loads tile metadata or content through an injected resolver when present, otherwise through the injected core API.
330
410
  */
@@ -335,6 +415,27 @@ export class I3SSource implements Tileset3DSource {
335
415
 
336
416
  return await this.loadWithCoreApi(url, options);
337
417
  }
418
+
419
+ /** Transform one I3S header bound after target options and elevation providers are available. */
420
+ private async transformTileHeader(header: any): Promise<any> {
421
+ if (!this.spatialTransformer) {
422
+ return header;
423
+ }
424
+ const sourceBounds = {
425
+ mbs: header.mbs,
426
+ obb: header.obb,
427
+ normalReferenceFrame: this.getMetadata().tileset.store?.normalReferenceFrame
428
+ };
429
+ const transformedBounds = await this.spatialTransformer.transformBoundsAsync(sourceBounds);
430
+ return {
431
+ ...header,
432
+ // Tileset3D traversal and culling operate in WGS84 ECEF independently of content output.
433
+ boundingVolume: transformedBounds.boundingVolume,
434
+ // Preserve the renderer/output-frame bound for applications and content coordination.
435
+ spatialBoundingVolume: transformedBounds.spatialBoundingVolume,
436
+ i3sLodMbs: transformedBounds.i3sLodMbs
437
+ };
438
+ }
338
439
  }
339
440
 
340
441
  function isTilesetRequest(input: TilesetSourceInput): input is TilesetSourceRequest {