@loaders.gl/tiles 5.0.0-alpha.7 → 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 (56) hide show
  1. package/README.md +26 -0
  2. package/dist/dist.dev.js +6381 -6802
  3. package/dist/dist.min.js +3 -6
  4. package/dist/index.cjs +209 -65
  5. package/dist/index.cjs.map +3 -3
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/spatial/spatial-coordinate-transformer.d.ts.map +1 -1
  11. package/dist/spatial/spatial-coordinate-transformer.js +13 -6
  12. package/dist/spatial/spatial-coordinate-transformer.js.map +1 -1
  13. package/dist/spatial/spatial-resource-registry.js +3 -3
  14. package/dist/spatial/spatial-resource-registry.js.map +1 -1
  15. package/dist/tileset-3d/common/tile-3d.d.ts +4 -0
  16. package/dist/tileset-3d/common/tile-3d.d.ts.map +1 -1
  17. package/dist/tileset-3d/common/tile-3d.js +35 -14
  18. package/dist/tileset-3d/common/tile-3d.js.map +1 -1
  19. package/dist/tileset-3d/common/tileset-3d.d.ts +1 -25
  20. package/dist/tileset-3d/common/tileset-3d.d.ts.map +1 -1
  21. package/dist/tileset-3d/common/tileset-3d.js +8 -41
  22. package/dist/tileset-3d/common/tileset-3d.js.map +1 -1
  23. package/dist/tileset-3d/common/tileset-content-traversal.d.ts +37 -0
  24. package/dist/tileset-3d/common/tileset-content-traversal.d.ts.map +1 -0
  25. package/dist/tileset-3d/common/tileset-content-traversal.js +80 -0
  26. package/dist/tileset-3d/common/tileset-content-traversal.js.map +1 -0
  27. package/dist/tileset-3d/common/tileset-source.d.ts +8 -1
  28. package/dist/tileset-3d/common/tileset-source.d.ts.map +1 -1
  29. package/dist/tileset-3d/common/tileset-source.js.map +1 -1
  30. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts +2 -0
  31. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.d.ts.map +1 -1
  32. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js +13 -0
  33. package/dist/tileset-3d/format-3d-tiles/tiles-3d-source.js.map +1 -1
  34. package/dist/tileset-3d/format-i3s/i3s-source.d.ts +4 -2
  35. package/dist/tileset-3d/format-i3s/i3s-source.d.ts.map +1 -1
  36. package/dist/tileset-3d/format-i3s/i3s-source.js +60 -9
  37. package/dist/tileset-3d/format-i3s/i3s-source.js.map +1 -1
  38. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.d.ts.map +1 -1
  39. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js +1 -1
  40. package/dist/tileset-3d/format-i3s/i3s-tileset-traverser.js.map +1 -1
  41. package/dist/tileset-3d/helpers/frame-state.d.ts +1 -0
  42. package/dist/tileset-3d/helpers/frame-state.d.ts.map +1 -1
  43. package/dist/tileset-3d/helpers/frame-state.js +5 -4
  44. package/dist/tileset-3d/helpers/frame-state.js.map +1 -1
  45. package/package.json +11 -13
  46. package/src/index.ts +5 -0
  47. package/src/spatial/spatial-coordinate-transformer.ts +18 -9
  48. package/src/spatial/spatial-resource-registry.ts +3 -3
  49. package/src/tileset-3d/common/tile-3d.ts +39 -17
  50. package/src/tileset-3d/common/tileset-3d.ts +8 -60
  51. package/src/tileset-3d/common/tileset-content-traversal.ts +114 -0
  52. package/src/tileset-3d/common/tileset-source.ts +16 -1
  53. package/src/tileset-3d/format-3d-tiles/tiles-3d-source.ts +18 -0
  54. package/src/tileset-3d/format-i3s/i3s-source.ts +84 -13
  55. package/src/tileset-3d/format-i3s/i3s-tileset-traverser.ts +2 -1
  56. package/src/tileset-3d/helpers/frame-state.ts +9 -6
@@ -58,17 +58,6 @@ export type Tileset3DProps = {
58
58
  * @default 536870912
59
59
  */
60
60
  maximumCacheOverflowBytes?: number;
61
- /**
62
- * Soft cache target in mebibytes.
63
- * @deprecated Use {@link Tileset3DProps.cacheBytes}; when both are supplied, `cacheBytes` wins.
64
- */
65
- maximumMemoryUsage?: number;
66
- /**
67
- * Cache overflow headroom in mebibytes.
68
- * @deprecated Use {@link Tileset3DProps.maximumCacheOverflowBytes}; when both are supplied, the
69
- * byte-native option wins.
70
- */
71
- memoryCacheOverflow?: number;
72
61
  maximumTilesSelected?: number;
73
62
  debounceTime?: number;
74
63
 
@@ -158,10 +147,6 @@ type Props = {
158
147
  cacheBytes: number;
159
148
  /** Current-frame headroom, in bytes, before memory-adjusted SSE rises. */
160
149
  maximumCacheOverflowBytes: number;
161
- /** @deprecated Byte-native code should use `cacheBytes`. */
162
- maximumMemoryUsage: number;
163
- /** @deprecated Byte-native code should use `maximumCacheOverflowBytes`. */
164
- memoryCacheOverflow: number;
165
150
  maximumTilesSelected: number;
166
151
  debounceTime: number;
167
152
  onTileLoad: (tile: Tile3D) => void;
@@ -241,31 +226,21 @@ function validateCacheByteLength(value: number, optionName: string): number {
241
226
  }
242
227
 
243
228
  /**
244
- * Resolves the base cache target from byte-native or deprecated mebibyte options.
229
+ * Resolves the base cache target from the byte-native option or its fallback.
245
230
  *
246
- * The byte-native option takes precedence so mixed migration configurations are deterministic.
247
- * The fallback is already expressed in bytes and is used for both construction and runtime updates.
248
- *
249
- * @param options - Options that may contain byte-native or deprecated cache values.
231
+ * @param options - Options that may contain a byte-native cache value.
250
232
  * @param fallbackCacheBytes - Existing or default cache target in bytes.
251
233
  * @returns The resolved and validated cache target in bytes.
252
234
  */
253
235
  function resolveCacheBytes(options: Tileset3DProps, fallbackCacheBytes: number): number {
254
- const cacheBytes =
255
- options.cacheBytes ??
256
- (options.maximumMemoryUsage !== undefined
257
- ? options.maximumMemoryUsage * BYTES_PER_MEBIBYTE
258
- : fallbackCacheBytes);
236
+ const cacheBytes = options.cacheBytes ?? fallbackCacheBytes;
259
237
  return validateCacheByteLength(cacheBytes, 'cacheBytes');
260
238
  }
261
239
 
262
240
  /**
263
- * Resolves cache overflow headroom from byte-native or deprecated mebibyte options.
264
- *
265
- * The byte-native option takes precedence so callers can migrate the two cache controls
266
- * independently without unit ambiguity.
241
+ * Resolves cache overflow headroom from the byte-native option or its fallback.
267
242
  *
268
- * @param options - Options that may contain byte-native or deprecated overflow values.
243
+ * @param options - Options that may contain a byte-native overflow value.
269
244
  * @param fallbackOverflowBytes - Existing or default overflow headroom in bytes.
270
245
  * @returns The resolved and validated overflow headroom in bytes.
271
246
  */
@@ -273,11 +248,7 @@ function resolveMaximumCacheOverflowBytes(
273
248
  options: Tileset3DProps,
274
249
  fallbackOverflowBytes: number
275
250
  ): number {
276
- const maximumCacheOverflowBytes =
277
- options.maximumCacheOverflowBytes ??
278
- (options.memoryCacheOverflow !== undefined
279
- ? options.memoryCacheOverflow * BYTES_PER_MEBIBYTE
280
- : fallbackOverflowBytes);
251
+ const maximumCacheOverflowBytes = options.maximumCacheOverflowBytes ?? fallbackOverflowBytes;
281
252
  return validateCacheByteLength(maximumCacheOverflowBytes, 'maximumCacheOverflowBytes');
282
253
  }
283
254
 
@@ -289,8 +260,6 @@ const DEFAULT_PROPS: Props = {
289
260
  maxRequests: 64,
290
261
  cacheBytes: DEFAULT_CACHE_BYTES,
291
262
  maximumCacheOverflowBytes: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES,
292
- maximumMemoryUsage: DEFAULT_CACHE_BYTES / BYTES_PER_MEBIBYTE,
293
- memoryCacheOverflow: DEFAULT_MAXIMUM_CACHE_OVERFLOW_BYTES / BYTES_PER_MEBIBYTE,
294
263
  maximumTilesSelected: 0,
295
264
  debounceTime: 0,
296
265
  onTileLoad: () => {},
@@ -456,8 +425,6 @@ export class Tileset3D {
456
425
  ...suppliedOptions,
457
426
  cacheBytes,
458
427
  maximumCacheOverflowBytes,
459
- maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
460
- memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE,
461
428
  memoryAdjustedScreenSpaceError:
462
429
  suppliedOptions.memoryAdjustedScreenSpaceError ?? usesTiles3DCacheDefaults
463
430
  };
@@ -552,7 +519,6 @@ export class Tileset3D {
552
519
  set cacheBytes(value: number) {
553
520
  this._cacheBytes = validateCacheByteLength(value, 'cacheBytes');
554
521
  this.options.cacheBytes = this._cacheBytes;
555
- this.options.maximumMemoryUsage = this._cacheBytes / BYTES_PER_MEBIBYTE;
556
522
  }
557
523
 
558
524
  /**
@@ -569,19 +535,6 @@ export class Tileset3D {
569
535
  set maximumCacheOverflowBytes(value: number) {
570
536
  this._maximumCacheOverflowBytes = validateCacheByteLength(value, 'maximumCacheOverflowBytes');
571
537
  this.options.maximumCacheOverflowBytes = this._maximumCacheOverflowBytes;
572
- this.options.memoryCacheOverflow = this._maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE;
573
- }
574
-
575
- /**
576
- * Gets or sets the soft cache target in mebibytes.
577
- * @deprecated Use {@link Tileset3D.cacheBytes}. Assignments remain synchronized for compatibility.
578
- */
579
- get maximumMemoryUsage(): number {
580
- return this.cacheBytes / BYTES_PER_MEBIBYTE;
581
- }
582
-
583
- set maximumMemoryUsage(value: number) {
584
- this.cacheBytes = validateCacheByteLength(value * BYTES_PER_MEBIBYTE, 'maximumMemoryUsage');
585
538
  }
586
539
 
587
540
  get queryParams(): string {
@@ -591,10 +544,7 @@ export class Tileset3D {
591
544
  }
592
545
 
593
546
  /**
594
- * Updates traversal and runtime options, synchronizing byte-native and deprecated cache values.
595
- *
596
- * For each budget, the byte-native option takes precedence when both unit forms are supplied.
597
- * Unspecified budgets retain their current byte values, avoiding repeated MiB conversion.
547
+ * Updates traversal and runtime options while retaining unspecified byte budgets.
598
548
  *
599
549
  * @param props - Partial runtime options to apply.
600
550
  */
@@ -608,9 +558,7 @@ export class Tileset3D {
608
558
  ...this.options,
609
559
  ...props,
610
560
  cacheBytes,
611
- maximumCacheOverflowBytes,
612
- maximumMemoryUsage: cacheBytes / BYTES_PER_MEBIBYTE,
613
- memoryCacheOverflow: maximumCacheOverflowBytes / BYTES_PER_MEBIBYTE
561
+ maximumCacheOverflowBytes
614
562
  };
615
563
  this._cacheBytes = cacheBytes;
616
564
  this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
@@ -0,0 +1,114 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {Tile3D} from './tile-3d';
6
+ import type {Tileset3D} from './tileset-3d';
7
+ import type {TileContentLoadResult} from './tileset-source';
8
+ import type {Tile3DContent} from './tile-3d-contracts';
9
+ import {TILESET_TYPE} from '../../constants';
10
+
11
+ /** Options for complete camera-independent tileset content traversal. */
12
+ export interface TraverseTilesetContentsOptions {
13
+ /** Cancel traversal and stop issuing further source requests. */
14
+ readonly signal?: AbortSignal;
15
+ /**
16
+ * Unload content loaded by this traversal when iteration advances or closes, including on error.
17
+ * Defaults to false. Preloaded content is retained. Use a runtime dedicated to this traversal;
18
+ * consume payloads before advancing and do not retain them or update the runtime concurrently.
19
+ */
20
+ readonly unloadContent?: boolean;
21
+ }
22
+
23
+ /** One tile placement and its ordered content entries from a completed source read. */
24
+ export interface TilesetContentTraversalItem {
25
+ /** Runtime tile placement. Shared content resources are yielded once per placement. */
26
+ readonly tile: Tile3D;
27
+ /** Ordered content entries, including multiple contents declared by a single tile. */
28
+ readonly contents: readonly Tile3DContent[];
29
+ }
30
+
31
+ /**
32
+ * Walks every declared tile placement and its loaded content without a camera or visibility test.
33
+ *
34
+ * Source implementations remain responsible for lazy subtree and node-page resolution. Children
35
+ * are visited in declaration order, nested tilesets are installed by the source before descent,
36
+ * and repeated resource URLs are not deduplicated because each placement can have a distinct
37
+ * transform. Content stays attached unless `unloadContent` is enabled. In that mode, newly loaded
38
+ * payloads are valid until iteration advances or closes. Tile placements and children remain
39
+ * available; callers still own the tileset, source, and archive lifetime.
40
+ *
41
+ * @param tileset - Initialized or initializing source-backed tileset runtime.
42
+ * @param options - Cancellation and decoded-content lifetime options.
43
+ * @returns One item for each visited tile placement in deterministic depth-first order.
44
+ */
45
+ export async function* traverseTilesetContents(
46
+ tileset: Tileset3D,
47
+ options: TraverseTilesetContentsOptions = {}
48
+ ): AsyncGenerator<TilesetContentTraversalItem> {
49
+ await tileset.tilesetInitializationPromise;
50
+ throwIfAborted(options.signal);
51
+ if (!tileset.root) {
52
+ throw new Error('Tileset initialization completed without a root tile');
53
+ }
54
+
55
+ const stack: Tile3D[] = [tileset.root];
56
+ const visitedTiles = new Set<Tile3D>();
57
+ while (stack.length > 0) {
58
+ throwIfAborted(options.signal);
59
+ const tile = stack.pop() as Tile3D;
60
+ if (visitedTiles.has(tile)) {
61
+ continue;
62
+ }
63
+ visitedTiles.add(tile);
64
+
65
+ if (tile.header.implicitSubtree && tile.childrenState !== 'ready') {
66
+ const loadChildren = tileset.source.loadTileChildrenForTraversal;
67
+ if (!loadChildren) {
68
+ throw new Error('Tileset source does not support camera-independent subtree loading');
69
+ }
70
+ const result = await loadChildren.call(tileset.source, tile, options.signal);
71
+ if (!result.loaded) {
72
+ throw new Error(`Unable to load implicit subtree for tile ${tile.id}`);
73
+ }
74
+ tile.childrenState = 'ready';
75
+ }
76
+
77
+ if (tileset.type === TILESET_TYPE.I3S && tileset.source.loadTileChildrenForTraversal) {
78
+ const result = await tileset.source.loadTileChildrenForTraversal(tile, options.signal);
79
+ if (!result.loaded) {
80
+ throw new Error(`Unable to load I3S child nodes for tile ${tile.id}`);
81
+ }
82
+ }
83
+
84
+ throwIfAborted(options.signal);
85
+ const loadsContent = tile.contentUrls.length > 0 && !tile.content;
86
+ try {
87
+ if (loadsContent) {
88
+ const loadResult: TileContentLoadResult = await tile.loadContentForTraversal();
89
+ throwIfAborted(options.signal);
90
+ if (!loadResult.loaded) {
91
+ throw new Error(`Unable to load tile content for tile ${tile.id}`);
92
+ }
93
+ tileset.source.onTileLoaded?.(tileset, tile, loadResult);
94
+ }
95
+
96
+ yield {tile, contents: tile.contentEntries};
97
+ } finally {
98
+ if (options.unloadContent && loadsContent) {
99
+ tile.unloadContent();
100
+ }
101
+ }
102
+
103
+ for (let childIndex = tile.children.length - 1; childIndex >= 0; childIndex--) {
104
+ stack.push(tile.children[childIndex]);
105
+ }
106
+ }
107
+ }
108
+
109
+ /** Throws the signal reason when traversal is cancelled. */
110
+ function throwIfAborted(signal?: AbortSignal): void {
111
+ if (signal?.aborted) {
112
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
113
+ }
114
+ }
@@ -221,6 +221,17 @@ export interface Tileset3DSource {
221
221
  */
222
222
  loadTileChildren?(tile: Tile3D, frameState: FrameState): Promise<TileChildrenLoadResult>;
223
223
 
224
+ /**
225
+ * Loads all source-managed child headers without requiring camera or visibility state.
226
+ *
227
+ * This hook is used by complete dataset operations such as conversion. Implementations retain
228
+ * ownership of implicit subtrees, node pages, archive resolution, and query inheritance.
229
+ */
230
+ loadTileChildrenForTraversal?(
231
+ tile: Tile3D,
232
+ signal?: AbortSignal
233
+ ): Promise<TileChildrenLoadResult>;
234
+
224
235
  /**
225
236
  * Resolves a tile-relative path to the final request URL.
226
237
  */
@@ -239,7 +250,11 @@ export interface Tileset3DSource {
239
250
  /**
240
251
  * Loads child tile metadata on demand for formats that do not expose the full tree up front.
241
252
  */
242
- loadChildTileHeader?(parentTile: Tile3D, childId: string, frameState: FrameState): Promise<any>;
253
+ loadChildTileHeader?(
254
+ parentTile: Tile3D,
255
+ childId: string,
256
+ frameState: FrameState | null
257
+ ): Promise<any>;
243
258
 
244
259
  /**
245
260
  * Updates source-managed bookkeeping after a tile finishes loading.
@@ -521,6 +521,17 @@ export class Tiles3DSource implements Tileset3DSource {
521
521
  };
522
522
  }
523
523
 
524
+ /** Loads an implicit subtree for dataset traversal without camera state. */
525
+ async loadTileChildrenForTraversal(
526
+ tile: Tile3D,
527
+ signal?: AbortSignal
528
+ ): Promise<TileChildrenLoadResult> {
529
+ throwIfTraversalAborted(signal);
530
+ const result = await this.loadTileChildren(tile, {} as FrameState);
531
+ throwIfTraversalAborted(signal);
532
+ return result;
533
+ }
534
+
524
535
  /**
525
536
  * Returns a snapshot of implicit-subtree request, cache, and materialization counters.
526
537
  *
@@ -939,6 +950,13 @@ export class Tiles3DSource implements Tileset3DSource {
939
950
  }
940
951
  }
941
952
 
953
+ /** Throws a cancellation reason before or after source-managed traversal work. */
954
+ function throwIfTraversalAborted(signal?: AbortSignal): void {
955
+ if (signal?.aborted) {
956
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
957
+ }
958
+ }
959
+
942
960
  function isTilesetRequest(input: TilesetSourceInput): input is TilesetSourceRequest {
943
961
  return Boolean(
944
962
  input && typeof input === 'object' && 'url' in input && 'loader' in input && !('type' in input)
@@ -8,10 +8,12 @@ import {Vector3} from '@math.gl/core';
8
8
  import type {CoreAPI, Loader, LoaderOptions} from '@loaders.gl/loader-utils';
9
9
  import type {Tile3D} from '../common/tile-3d';
10
10
  import {Tile3D as Tile3DNode} from '../common/tile-3d';
11
+ import type {Tile3DContent} from '../common/tile-3d-contracts';
11
12
  import {I3STilesetTraverser} from './i3s-tileset-traverser';
12
13
  import type {Tileset3D} from '../common/tileset-3d';
13
14
  import type {FrameState} from '../helpers/frame-state';
14
15
  import type {
16
+ TileChildrenLoadResult,
15
17
  TileContentLoadResult,
16
18
  TilesetContentFormats,
17
19
  TilesetJSON,
@@ -219,7 +221,17 @@ export class I3SSource implements Tileset3DSource {
219
221
  };
220
222
 
221
223
  tile.content = await this.loadResourceData(contentUrl, options, this.contentLoader);
222
- return {loaded: true};
224
+ const contentEntry: Tile3DContent = {
225
+ index: 0,
226
+ uri: tile.contentUrl,
227
+ type: tile.header.layerType,
228
+ payload: tile.content,
229
+ metadata: tile.header.metadata || null,
230
+ boundingVolume: tile.boundingVolume,
231
+ featureIds: tile.featureIdSets,
232
+ renderable: Boolean(tile.content)
233
+ };
234
+ return {loaded: true, contents: [tile.content], contentEntries: [contentEntry]};
223
235
  }
224
236
 
225
237
  /**
@@ -228,7 +240,7 @@ export class I3SSource implements Tileset3DSource {
228
240
  async loadChildTileHeader(
229
241
  _parentTile: Tile3D,
230
242
  childId: string,
231
- _frameState: FrameState
243
+ _frameState: FrameState | null
232
244
  ): Promise<any> {
233
245
  const metadata = this.getMetadata();
234
246
  if (metadata.tileset.nodePages || metadata.tileset.pointNodePages) {
@@ -253,6 +265,50 @@ export class I3SSource implements Tileset3DSource {
253
265
  return await this.transformTileHeader(header);
254
266
  }
255
267
 
268
+ /** Loads all declared I3S child headers without camera or visibility state. */
269
+ async loadTileChildrenForTraversal(
270
+ parentTile: Tile3D,
271
+ signal?: AbortSignal
272
+ ): Promise<TileChildrenLoadResult> {
273
+ const childHeaders = parentTile.header.children || [];
274
+ let loadedChildren = 0;
275
+ const orderedChildren: Tile3D[] = [];
276
+
277
+ for (const childHeader of childHeaders) {
278
+ throwIfTraversalAborted(signal);
279
+ const childId = childHeader.id;
280
+ if (typeof childId !== 'string' && typeof childId !== 'number') {
281
+ throw new Error('I3S child header is missing its node ID');
282
+ }
283
+ const existingChild = parentTile.children.find(
284
+ tile => tile.id === String(childId) || tile.header?.id === childId
285
+ );
286
+ if (existingChild) {
287
+ orderedChildren.push(existingChild);
288
+ continue;
289
+ }
290
+
291
+ const childTileHeader = await this.loadChildTileHeader(parentTile, String(childId), null);
292
+ throwIfTraversalAborted(signal);
293
+ const childTile = new Tile3DNode(
294
+ parentTile.tileset,
295
+ childTileHeader,
296
+ parentTile,
297
+ String(childId)
298
+ );
299
+ orderedChildren.push(childTile);
300
+ loadedChildren++;
301
+ }
302
+
303
+ const orderedChildSet = new Set(orderedChildren);
304
+ parentTile.children = [
305
+ ...orderedChildren,
306
+ ...parentTile.children.filter(childTile => !orderedChildSet.has(childTile))
307
+ ];
308
+
309
+ return {loaded: true, tileCount: loadedChildren, childSubtreeCount: 0};
310
+ }
311
+
256
312
  /**
257
313
  * Resolves an I3S request URL with source-managed query parameters.
258
314
  */
@@ -265,24 +321,28 @@ export class I3SSource implements Tileset3DSource {
265
321
  return tilePath;
266
322
  }
267
323
 
268
- const queryDelimiterIndex = tilePath.indexOf('?');
269
- if (queryDelimiterIndex === -1) {
270
- return `${tilePath}?${new URLSearchParams(this.queryParams).toString()}`;
271
- }
272
-
273
- const existingQuery = tilePath.slice(queryDelimiterIndex + 1);
274
- const existingQueryKeys = new Set(
275
- existingQuery.split('&').map(parameter => parameter.split('=', 1)[0])
276
- );
324
+ const hashIndex = tilePath.indexOf('#');
325
+ const hash = hashIndex >= 0 ? tilePath.slice(hashIndex) : '';
326
+ const tilePathWithoutHash = hashIndex >= 0 ? tilePath.slice(0, hashIndex) : tilePath;
327
+ const queryDelimiterIndex = tilePathWithoutHash.indexOf('?');
328
+ const path =
329
+ queryDelimiterIndex === -1
330
+ ? tilePathWithoutHash
331
+ : tilePathWithoutHash.slice(0, queryDelimiterIndex);
332
+ const existingQuery =
333
+ queryDelimiterIndex === -1 ? '' : tilePathWithoutHash.slice(queryDelimiterIndex + 1);
334
+ const existingQueryParams = new URLSearchParams(existingQuery);
277
335
  const queryParams = new URLSearchParams();
278
336
  for (const [key, value] of Object.entries(this.queryParams)) {
279
- if (!existingQueryKeys.has(key)) {
337
+ if (!existingQueryParams.has(key)) {
280
338
  queryParams.set(key, value);
281
339
  }
282
340
  }
283
341
 
284
342
  const queryString = queryParams.toString();
285
- return queryString ? `${tilePath}${existingQuery ? '&' : ''}${queryString}` : tilePath;
343
+ return `${path}${existingQuery ? `?${existingQuery}` : ''}${
344
+ queryString ? `${existingQuery ? '&' : '?'}${queryString}` : ''
345
+ }${hash}`;
286
346
  }
287
347
 
288
348
  /**
@@ -403,6 +463,10 @@ export class I3SSource implements Tileset3DSource {
403
463
  * @param loadOptions Loader options that may contain an ArcGIS token.
404
464
  */
405
465
  private initializeQueryParams(loadOptions: LoaderOptions): void {
466
+ for (const [key, value] of Object.entries(loadOptions.searchParams || {})) {
467
+ this.queryParams[key] = String(value);
468
+ }
469
+
406
470
  const i3sOptions = loadOptions.i3s;
407
471
  if (i3sOptions && typeof i3sOptions === 'object' && 'token' in i3sOptions) {
408
472
  const token = (i3sOptions as Record<string, unknown>).token;
@@ -449,6 +513,13 @@ export class I3SSource implements Tileset3DSource {
449
513
  }
450
514
  }
451
515
 
516
+ /** Throws a cancellation reason before or after source-managed traversal work. */
517
+ function throwIfTraversalAborted(signal?: AbortSignal): void {
518
+ if (signal?.aborted) {
519
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
520
+ }
521
+ }
522
+
452
523
  /** Returns the format-provided binary content loader, falling back for custom legacy loaders. */
453
524
  function getI3SContentLoader(loader: Loader): Loader {
454
525
  return (loader as Loader & {contentLoader?: Loader}).contentLoader || loader;
@@ -38,7 +38,8 @@ export class I3STilesetTraverser extends TilesetTraverser {
38
38
  for (const child of children) {
39
39
  const extendedId = `${child.id}-${frameState.viewport.id}`;
40
40
  // if child tile is not fetched
41
- const childTile = childTiles && childTiles.find(t => t.id === extendedId);
41
+ const childTile =
42
+ childTiles && childTiles.find(t => t.id === extendedId || t.header?.id === child.id);
42
43
  if (!childTile) {
43
44
  const request = () => this._loadTile(tile, child.id, frameState);
44
45
  const cachedRequest = this._tileManager.find(extendedId);
@@ -30,7 +30,8 @@ export type FrameState = {
30
30
  /** Optional world-space clipping planes applied to render-content culling. */
31
31
  clippingPlanes?: Plane[];
32
32
  frameNumber: number; // TODO: This can be the same between updates, what number is unique for between updates?
33
- sseDenominator: number; // Assumes fovy = 60 degrees
33
+ /** Perspective projection factor: twice the tangent of half the vertical field of view. */
34
+ sseDenominator: number;
34
35
  /** View-dependent density used by perspective dynamic screen-space error for this traversal. */
35
36
  dynamicScreenSpaceErrorDensity: number;
36
37
  };
@@ -151,6 +152,11 @@ export function getFrameState(
151
152
  pitch: 0
152
153
  });
153
154
 
155
+ const verticalFieldOfView =
156
+ Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0 && Number(viewport.fovy) < 180
157
+ ? (Number(viewport.fovy) * Math.PI) / 180
158
+ : Math.PI / 3;
159
+
154
160
  // TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function
155
161
  return {
156
162
  camera: {
@@ -158,10 +164,7 @@ export function getFrameState(
158
164
  direction: cameraDirectionCartesian,
159
165
  up: cameraUpCartesian,
160
166
  cartographicPosition: cameraPositionCartographic,
161
- verticalFieldOfView:
162
- Number.isFinite(viewport.fovy) && Number(viewport.fovy) > 0
163
- ? (Number(viewport.fovy) * Math.PI) / 180
164
- : Math.PI / 3,
167
+ verticalFieldOfView,
165
168
  timeSinceMovement: options.timeSinceCameraMovement ?? Number.POSITIVE_INFINITY
166
169
  },
167
170
  viewport,
@@ -170,7 +173,7 @@ export function getFrameState(
170
173
  cullingVolume,
171
174
  clippingPlanes: viewport.clippingPlanes,
172
175
  frameNumber, // TODO: This can be the same between updates, what number is unique for between updates?
173
- sseDenominator: 1.15, // Assumes fovy = 60 degrees
176
+ sseDenominator: 2 * Math.tan(verticalFieldOfView / 2),
174
177
  // Tileset3D fills this immediately before traversal because it depends on the root volume.
175
178
  dynamicScreenSpaceErrorDensity: 0
176
179
  };