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

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
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  I3SSource: () => I3SSource,
24
+ I3SSpatialTransformer: () => I3SSpatialTransformer,
24
25
  ImageSet: () => ImageSet,
25
26
  IndexedArchiveTilesetSource: () => IndexedArchiveTilesetSource,
26
27
  LOD_METRIC_TYPE: () => LOD_METRIC_TYPE,
@@ -31,6 +32,7 @@ __export(index_exports, {
31
32
  STRATEGY_NEVER: () => STRATEGY_NEVER,
32
33
  STRATEGY_REPLACE: () => STRATEGY_REPLACE,
33
34
  SharedTile2DHeader: () => SharedTile2DHeader,
35
+ SpatialCoordinateTransformer: () => SpatialCoordinateTransformer,
34
36
  TILESET_TYPE: () => TILESET_TYPE,
35
37
  TILE_CONTENT_STATE: () => TILE_CONTENT_STATE,
36
38
  TILE_REFINEMENT: () => TILE_REFINEMENT,
@@ -41,13 +43,24 @@ __export(index_exports, {
41
43
  Tileset3D: () => Tileset3D,
42
44
  TilesetCache: () => TilesetCache,
43
45
  TilesetTraverser: () => TilesetTraverser,
46
+ applyTilesetSpatialOptions: () => applyTilesetSpatialOptions,
44
47
  calculateTransformProps: () => calculateTransformProps,
45
48
  createBoundingVolume: () => createBoundingVolume,
46
49
  createImplicitSubtreeReference: () => createImplicitSubtreeReference,
50
+ createTilesetSpatialReference: () => createTilesetSpatialReference,
51
+ get3DTilesSpatialReference: () => get3DTilesSpatialReference,
47
52
  getFrameState: () => getFrameState,
53
+ getGeoidModel: () => getGeoidModel,
54
+ getI3SSpatialReference: () => getI3SSpatialReference,
48
55
  getLodStatus: () => getLodStatus,
56
+ getSpatialCoordinateFrame: () => getSpatialCoordinateFrame,
49
57
  isTileset3DSource: () => isTileset3DSource,
58
+ markTilesetSpatialReferenceTransformed: () => markTilesetSpatialReferenceTransformed,
50
59
  materializeImplicitSubtree: () => materializeImplicitSubtree,
60
+ registerGeoidModel: () => registerGeoidModel,
61
+ registerGeoidModelFromPgm: () => registerGeoidModelFromPgm,
62
+ registerSpatialCrs: () => registerSpatialCrs,
63
+ registerSpatialDatumGrid: () => registerSpatialDatumGrid,
51
64
  replaceImplicitUrlTemplate: () => replaceImplicitUrlTemplate
52
65
  });
53
66
  module.exports = __toCommonJS(index_exports);
@@ -212,6 +225,232 @@ var RequestScheduler = class {
212
225
  }
213
226
  };
214
227
 
228
+ // ../loader-utils/src/lib/request-utils/request-cache.ts
229
+ var DEFAULT_MAX_ENTRIES = Number.POSITIVE_INFINITY;
230
+ var DEFAULT_MAX_BYTES = Number.POSITIVE_INFINITY;
231
+ var RequestCache = class {
232
+ /** Maximum number of settled entries retained by this cache. */
233
+ maxEntries;
234
+ /** Maximum estimated bytes retained by this cache. */
235
+ maxBytes;
236
+ getByteLength;
237
+ onRemove;
238
+ entries = /* @__PURE__ */ new Map();
239
+ retainedByteLength = 0;
240
+ /** Creates a request cache with optional entry and byte limits. */
241
+ constructor(props = {}) {
242
+ this.maxEntries = validateLimit(props.maxEntries, DEFAULT_MAX_ENTRIES, "maxEntries");
243
+ this.maxBytes = validateLimit(props.maxBytes, DEFAULT_MAX_BYTES, "maxBytes");
244
+ this.getByteLength = props.getByteLength || (() => 0);
245
+ this.onRemove = props.onRemove;
246
+ }
247
+ /** Number of pending and settled entries currently tracked. */
248
+ get size() {
249
+ return this.entries.size;
250
+ }
251
+ /** Number of requests that have not settled. */
252
+ get pendingSize() {
253
+ let pendingSize = 0;
254
+ for (const entry of this.entries.values()) {
255
+ pendingSize += entry.pending ? 1 : 0;
256
+ }
257
+ return pendingSize;
258
+ }
259
+ /** Estimated bytes retained by settled entries. */
260
+ get byteLength() {
261
+ return this.retainedByteLength;
262
+ }
263
+ /** Returns a cached request and updates its LRU position. */
264
+ get(key, signal) {
265
+ const entry = this.entries.get(key);
266
+ if (!entry) {
267
+ return void 0;
268
+ }
269
+ this.touch(key, entry);
270
+ return this.waitForEntry(key, entry, signal);
271
+ }
272
+ /** Returns whether a pending or settled entry exists without updating its LRU position. */
273
+ has(key) {
274
+ return this.entries.has(key);
275
+ }
276
+ /** Stores an already available value and returns it. */
277
+ set(key, value) {
278
+ this.removeEntry(key, "replace");
279
+ const byteLength = getValidByteLength(this.getByteLength(value));
280
+ const entry = {
281
+ promise: Promise.resolve(value),
282
+ pending: false,
283
+ waiterCount: 0,
284
+ byteLength
285
+ };
286
+ this.entries.set(key, entry);
287
+ this.retainedByteLength += byteLength;
288
+ this.trim();
289
+ return value;
290
+ }
291
+ /**
292
+ * Returns a cached value or starts one shared request.
293
+ *
294
+ * The loader receives a cache-owned abort signal. One caller aborting only rejects that caller;
295
+ * the shared request is cancelled when no waiters remain.
296
+ */
297
+ getOrLoad(key, load, signal) {
298
+ const cached = this.get(key, signal);
299
+ if (cached) {
300
+ return cached;
301
+ }
302
+ if (signal == null ? void 0 : signal.aborted) {
303
+ return Promise.reject(createAbortError());
304
+ }
305
+ const controller = new AbortController();
306
+ let resolveRequest;
307
+ let rejectRequest;
308
+ const promise = new Promise((resolve2, reject) => {
309
+ resolveRequest = resolve2;
310
+ rejectRequest = reject;
311
+ });
312
+ const entry = {
313
+ promise,
314
+ controller,
315
+ pending: true,
316
+ waiterCount: 0,
317
+ byteLength: 0
318
+ };
319
+ this.entries.set(key, entry);
320
+ void entry.promise.then(
321
+ (value) => this.handleLoadedValue(key, entry, value),
322
+ () => this.handleLoadError(key, entry)
323
+ );
324
+ try {
325
+ void Promise.resolve(load(controller.signal)).then(resolveRequest, rejectRequest);
326
+ } catch (error) {
327
+ rejectRequest(error);
328
+ }
329
+ return this.waitForEntry(key, entry, signal);
330
+ }
331
+ /** Removes one cached or pending request. */
332
+ delete(key) {
333
+ return this.removeEntry(key, "delete");
334
+ }
335
+ /** Aborts pending requests and removes every entry. */
336
+ clear() {
337
+ for (const key of Array.from(this.entries.keys())) {
338
+ this.removeEntry(key, "clear");
339
+ }
340
+ }
341
+ handleLoadedValue(key, entry, value) {
342
+ if (this.entries.get(key) !== entry) {
343
+ return;
344
+ }
345
+ entry.pending = false;
346
+ entry.controller = void 0;
347
+ try {
348
+ entry.byteLength = getValidByteLength(this.getByteLength(value));
349
+ } catch {
350
+ this.removeEntry(key, "error");
351
+ return;
352
+ }
353
+ this.retainedByteLength += entry.byteLength;
354
+ this.trim();
355
+ }
356
+ handleLoadError(key, entry) {
357
+ var _a;
358
+ if (this.entries.get(key) === entry) {
359
+ this.removeEntry(key, ((_a = entry.controller) == null ? void 0 : _a.signal.aborted) ? "abort" : "error");
360
+ }
361
+ }
362
+ waitForEntry(key, entry, signal) {
363
+ if (signal == null ? void 0 : signal.aborted) {
364
+ return Promise.reject(createAbortError());
365
+ }
366
+ if (!entry.pending) {
367
+ return entry.promise;
368
+ }
369
+ entry.waiterCount++;
370
+ return new Promise((resolve2, reject) => {
371
+ let waiting = true;
372
+ const finishWaiting = () => {
373
+ if (!waiting) return;
374
+ waiting = false;
375
+ signal == null ? void 0 : signal.removeEventListener("abort", handleAbort);
376
+ entry.waiterCount--;
377
+ };
378
+ const handleAbort = () => {
379
+ if (!waiting) return;
380
+ finishWaiting();
381
+ reject(createAbortError());
382
+ if (entry.pending && entry.waiterCount === 0 && this.entries.get(key) === entry) {
383
+ this.removeEntry(key, "abort");
384
+ }
385
+ };
386
+ signal == null ? void 0 : signal.addEventListener("abort", handleAbort, { once: true });
387
+ void entry.promise.then(
388
+ (value) => {
389
+ if (!waiting) return;
390
+ finishWaiting();
391
+ resolve2(value);
392
+ },
393
+ (error) => {
394
+ if (!waiting) return;
395
+ finishWaiting();
396
+ reject(error);
397
+ }
398
+ );
399
+ });
400
+ }
401
+ touch(key, entry) {
402
+ this.entries.delete(key);
403
+ this.entries.set(key, entry);
404
+ }
405
+ trim() {
406
+ let settledEntryCount = this.entries.size - this.pendingSize;
407
+ if (settledEntryCount <= this.maxEntries && this.retainedByteLength <= this.maxBytes) {
408
+ return;
409
+ }
410
+ for (const [key, entry] of this.entries) {
411
+ if (!entry.pending) {
412
+ this.removeEntry(key, "evict");
413
+ settledEntryCount--;
414
+ }
415
+ if (settledEntryCount <= this.maxEntries && this.retainedByteLength <= this.maxBytes) {
416
+ break;
417
+ }
418
+ }
419
+ }
420
+ removeEntry(key, reason) {
421
+ var _a, _b;
422
+ const entry = this.entries.get(key);
423
+ if (!entry) {
424
+ return false;
425
+ }
426
+ this.entries.delete(key);
427
+ this.retainedByteLength -= entry.byteLength;
428
+ if (entry.pending) {
429
+ (_a = entry.controller) == null ? void 0 : _a.abort();
430
+ }
431
+ (_b = this.onRemove) == null ? void 0 : _b.call(this, key, reason);
432
+ return true;
433
+ }
434
+ };
435
+ function createAbortError() {
436
+ const error = new Error("Request aborted");
437
+ error.name = "AbortError";
438
+ return error;
439
+ }
440
+ function validateLimit(value, fallback, name) {
441
+ const limit = value ?? fallback;
442
+ if (limit < 0 || Number.isNaN(limit)) {
443
+ throw new Error(`${name} must be a non-negative number`);
444
+ }
445
+ return limit;
446
+ }
447
+ function getValidByteLength(value) {
448
+ if (!Number.isFinite(value) || value < 0) {
449
+ throw new Error("Request cache entry byte length must be a non-negative finite number");
450
+ }
451
+ return value;
452
+ }
453
+
215
454
  // ../loader-utils/src/lib/request-utils/range-request-scheduler.ts
216
455
  var import_stats2 = require("@probe.gl/stats");
217
456
  var RANGE_STATS_KEYS = {
@@ -269,7 +508,7 @@ var RangeRequestScheduler = class {
269
508
  return Promise.reject(new Error("Byte range length cannot be negative"));
270
509
  }
271
510
  if ((_a = request.signal) == null ? void 0 : _a.aborted) {
272
- return Promise.reject(createAbortError());
511
+ return Promise.reject(createAbortError2());
273
512
  }
274
513
  if (request.length === 0) {
275
514
  return Promise.resolve(new ArrayBuffer(0));
@@ -484,7 +723,7 @@ var RangeRequestScheduler = class {
484
723
  return;
485
724
  }
486
725
  request.settled = true;
487
- request.reject(createAbortError());
726
+ request.reject(createAbortError2());
488
727
  this.trackEvent({
489
728
  type: "abort",
490
729
  sourceId: request.sourceId,
@@ -714,7 +953,7 @@ function getTimestamp() {
714
953
  var _a;
715
954
  return ((_a = globalThis.performance) == null ? void 0 : _a.now()) ?? Date.now();
716
955
  }
717
- function createAbortError() {
956
+ function createAbortError2() {
718
957
  if (typeof DOMException !== "undefined") {
719
958
  return new DOMException("Request aborted", "AbortError");
720
959
  }
@@ -1216,7 +1455,7 @@ var HttpFile = class _HttpFile {
1216
1455
  async open(signal) {
1217
1456
  this.assertOpen();
1218
1457
  if (signal == null ? void 0 : signal.aborted) {
1219
- const error = createAbortError2();
1458
+ const error = createAbortError3();
1220
1459
  this.transport.trackFailure(error);
1221
1460
  throw error;
1222
1461
  }
@@ -1258,7 +1497,7 @@ var HttpFile = class _HttpFile {
1258
1497
  async read(offset = 0, length = 0, signal) {
1259
1498
  this.assertOpen();
1260
1499
  if (signal == null ? void 0 : signal.aborted) {
1261
- const error = createAbortError2();
1500
+ const error = createAbortError3();
1262
1501
  this.transport.trackFailure(error);
1263
1502
  throw error;
1264
1503
  }
@@ -1366,7 +1605,7 @@ function validateRange(offset, length, byteLength) {
1366
1605
  throw new Error("HTTP byte range extends beyond the end of the file");
1367
1606
  }
1368
1607
  }
1369
- function createAbortError2() {
1608
+ function createAbortError3() {
1370
1609
  if (typeof DOMException !== "undefined") {
1371
1610
  return new DOMException("Request aborted", "AbortError");
1372
1611
  }
@@ -1379,12 +1618,12 @@ function waitForPromiseWithSignal(promise, signal) {
1379
1618
  return promise;
1380
1619
  }
1381
1620
  if (signal.aborted) {
1382
- return Promise.reject(createAbortError2());
1621
+ return Promise.reject(createAbortError3());
1383
1622
  }
1384
1623
  return new Promise((resolve2, reject) => {
1385
1624
  const abortListener = () => {
1386
1625
  signal.removeEventListener("abort", abortListener);
1387
- reject(createAbortError2());
1626
+ reject(createAbortError3());
1388
1627
  };
1389
1628
  signal.addEventListener("abort", abortListener, { once: true });
1390
1629
  promise.then(
@@ -1684,6 +1923,7 @@ function getFrameState(viewport, frameNumber, options = {}) {
1684
1923
  topDownViewport,
1685
1924
  height,
1686
1925
  cullingVolume,
1926
+ clippingPlanes: viewport.clippingPlanes,
1687
1927
  frameNumber,
1688
1928
  // TODO: This can be the same between updates, what number is unique for between updates?
1689
1929
  sseDenominator: 1.15,
@@ -2024,6 +2264,177 @@ function calculateTileRequestPriority(parameters) {
2024
2264
  return progressiveResolutionDigits + foveatedSortingDigits + preferredSortingDigits;
2025
2265
  }
2026
2266
 
2267
+ // dist/spatial/spatial-types.js
2268
+ var import_crs = require("@math.gl/crs");
2269
+
2270
+ // dist/spatial/get-spatial-coordinate-frame.js
2271
+ var GEOGRAPHIC_CRS = "EPSG:4326";
2272
+ var GEOCENTRIC_CRS = "EPSG:4978";
2273
+ function normalizeCrsIdentifier(identifier) {
2274
+ const normalizedIdentifier = identifier.trim().toUpperCase();
2275
+ const ogcMatch = normalizedIdentifier.match(/(?:\/DEF\/CRS\/|URN:OGC:DEF:CRS:)([A-Z0-9_-]+)(?:\/|::)(?:[^/:]*[/:])?([A-Z0-9_.-]+)$/);
2276
+ return ogcMatch ? `${ogcMatch[1]}:${ogcMatch[2]}` : normalizedIdentifier;
2277
+ }
2278
+ function getSpatialCoordinateFrame(definition) {
2279
+ var _a, _b;
2280
+ if (typeof definition === "string") {
2281
+ const normalized = normalizeCrsIdentifier(definition);
2282
+ if (normalized === GEOCENTRIC_CRS || /^GEOCCS\s*[\[(]/.test(normalized)) {
2283
+ return "geocentric";
2284
+ }
2285
+ if (normalized === GEOGRAPHIC_CRS || normalized === "EPSG:4490" || normalized === "EPSG:4979" || normalized === "OGC:CRS84" || normalized.includes("+PROJ=LONGLAT") || normalized.includes("+PROJ=LATLONG") || /^(?:GEOGCS|GEOGCRS|GEOGRAPHICCRS|GEOGRAPHIC2DCRS|GEOGRAPHIC3DCRS)\s*[\[(]/.test(normalized)) {
2286
+ return "geographic";
2287
+ }
2288
+ if (/^(?:GEODCRS|GEODETICCRS)\s*[\[(]/.test(normalized)) {
2289
+ return /CS\s*[\[(]\s*CARTESIAN/.test(normalized) ? "geocentric" : "geographic";
2290
+ }
2291
+ return "projected";
2292
+ }
2293
+ const projJsonDefinition = definition;
2294
+ if (projJsonDefinition.type === "BoundCRS" && projJsonDefinition.source_crs) {
2295
+ return getSpatialCoordinateFrame(projJsonDefinition.source_crs);
2296
+ }
2297
+ if (projJsonDefinition.type === "CompoundCRS" && ((_a = projJsonDefinition.components) == null ? void 0 : _a[0])) {
2298
+ return getSpatialCoordinateFrame(projJsonDefinition.components[0]);
2299
+ }
2300
+ if (String(projJsonDefinition.type).includes("Projected")) {
2301
+ return "projected";
2302
+ }
2303
+ if (String(projJsonDefinition.type).includes("Geographic")) {
2304
+ return "geographic";
2305
+ }
2306
+ if (String(projJsonDefinition.type).includes("Geodetic")) {
2307
+ return ((_b = projJsonDefinition.coordinate_system) == null ? void 0 : _b.subtype) === "Cartesian" ? "geocentric" : "geographic";
2308
+ }
2309
+ return "projected";
2310
+ }
2311
+
2312
+ // dist/spatial/spatial-types.js
2313
+ function createTilesetSpatialReference(discovered, options = {}) {
2314
+ const hasSourceCrsOverride = options.sourceCrs !== void 0;
2315
+ const sourceCrs = options.sourceCrs || discovered.sourceCrs;
2316
+ const outputCoordinates = options.outputCoordinates || "auto";
2317
+ const targetCrs = options.targetCrs || (outputCoordinates === "ecef" ? "EPSG:4978" : void 0);
2318
+ const targetHeightReference = options.targetHeightReference || "native";
2319
+ const verticalUnitScale = discovered.verticalUnitScale ?? 1;
2320
+ const elevationUnitScale = discovered.elevationUnitScale ?? 1;
2321
+ const elevationOffset = discovered.elevationOffset || 0;
2322
+ const requiresSurface = discovered.elevationMode === "onTheGround" || discovered.elevationMode === "relativeToGround" || discovered.elevationMode === "relativeToScene";
2323
+ const hasElevationProvider = discovered.elevationMode === "relativeToScene" ? Boolean(options.sceneElevationProvider) : !requiresSurface || Boolean(options.terrainElevationProvider);
2324
+ const hasFormatVerticalOperation = verticalUnitScale !== 1 || elevationUnitScale !== 1 || discovered.elevationMode !== void 0 && (requiresSurface || discovered.elevationMode === "absoluteHeight" && elevationOffset !== 0);
2325
+ const needsHeightTransform = targetHeightReference !== "native" && targetHeightReference !== discovered.heightReference;
2326
+ const hasRequestedTransform = Boolean(targetCrs) || targetHeightReference !== "native" || outputCoordinates !== "auto" || hasFormatVerticalOperation;
2327
+ const hasRequiredTarget = outputCoordinates !== "target-crs" || Boolean(targetCrs);
2328
+ const hasHeightMetadata = !needsHeightTransform || (discovered.heightReference || "unknown") !== "unknown";
2329
+ const hasVerticalUnits = Number.isFinite(verticalUnitScale) && verticalUnitScale > 0 && Number.isFinite(elevationUnitScale) && elevationUnitScale > 0;
2330
+ const canTransform = Boolean(sourceCrs) && hasRequestedTransform && hasRequiredTarget && hasHeightMetadata && hasVerticalUnits && hasElevationProvider && outputCoordinates !== "local-enu";
2331
+ const provenance = hasSourceCrsOverride ? "caller-override" : discovered.provenance || "unknown";
2332
+ const coordinateFrame = hasSourceCrsOverride && sourceCrs ? getSpatialCoordinateFrame(sourceCrs) : discovered.coordinateFrame;
2333
+ const sourceCrsState = hasSourceCrsOverride ? "explicit" : discovered.sourceCrsState || (sourceCrs ? provenance === "format-default" ? "default" : "explicit" : "absent");
2334
+ const spatialReference = (0, import_crs.createSpatialReference)({
2335
+ crs: sourceCrs ? {
2336
+ state: sourceCrsState === "default" ? "default" : "explicit",
2337
+ definition: sourceCrs,
2338
+ representation: !hasSourceCrsOverride && discovered.sourceCrsRepresentation || (0, import_crs.inferCRSRepresentation)(sourceCrs),
2339
+ provenance,
2340
+ alternatives: hasSourceCrsOverride ? void 0 : discovered.sourceCrsAlternatives
2341
+ } : {
2342
+ state: sourceCrsState === "unknown" ? "unknown" : "absent",
2343
+ provenance
2344
+ },
2345
+ vertical: discovered.verticalCrs ? {
2346
+ state: "explicit",
2347
+ definition: discovered.verticalCrs,
2348
+ representation: (0, import_crs.inferCRSRepresentation)(discovered.verticalCrs),
2349
+ provenance: discovered.provenance || "metadata"
2350
+ } : void 0,
2351
+ coordinateEpoch: options.coordinateEpoch ?? discovered.coordinateEpoch,
2352
+ coordinateFrame,
2353
+ coordinateOrder: getCoordinateOrder(discovered.axisOrder),
2354
+ units: hasSourceCrsOverride ? void 0 : discovered.units
2355
+ });
2356
+ const normalizedSourceCrs = getKnownCRSDefinition(spatialReference.crs);
2357
+ const normalizedVerticalCrs = spatialReference.vertical ? getKnownCRSDefinition(spatialReference.vertical) : void 0;
2358
+ const normalizedTargetCrs = targetCrs ? cloneReadonlyCRSDefinition(targetCrs) : void 0;
2359
+ return Object.freeze({
2360
+ ...spatialReference,
2361
+ sourceCrs: normalizedSourceCrs,
2362
+ verticalCrs: normalizedVerticalCrs,
2363
+ verticalUnitScale,
2364
+ coordinateEpoch: options.coordinateEpoch ?? discovered.coordinateEpoch,
2365
+ heightReference: discovered.heightReference || "unknown",
2366
+ elevationMode: discovered.elevationMode,
2367
+ elevationOffset: discovered.elevationOffset,
2368
+ elevationUnit: discovered.elevationUnit,
2369
+ elevationUnitScale,
2370
+ coordinateFrame: coordinateFrame || "unknown",
2371
+ axisOrder: discovered.axisOrder || "unknown",
2372
+ provenance,
2373
+ targetCrs: normalizedTargetCrs,
2374
+ targetHeightReference,
2375
+ outputCoordinates,
2376
+ status: hasRequestedTransform ? canTransform ? "transformable" : "unresolved" : "native",
2377
+ warnings: Object.freeze([...discovered.warnings || []])
2378
+ });
2379
+ }
2380
+ function applyTilesetSpatialOptions(discovered, options = {}) {
2381
+ return createTilesetSpatialReference({
2382
+ sourceCrs: discovered == null ? void 0 : discovered.sourceCrs,
2383
+ sourceCrsState: discovered == null ? void 0 : discovered.crs.state,
2384
+ sourceCrsRepresentation: (discovered == null ? void 0 : discovered.crs.state) === "explicit" || (discovered == null ? void 0 : discovered.crs.state) === "default" ? discovered.crs.representation : void 0,
2385
+ sourceCrsAlternatives: (discovered == null ? void 0 : discovered.crs.state) === "explicit" || (discovered == null ? void 0 : discovered.crs.state) === "default" ? discovered.crs.alternatives : void 0,
2386
+ verticalCrs: discovered == null ? void 0 : discovered.verticalCrs,
2387
+ units: discovered == null ? void 0 : discovered.units,
2388
+ verticalUnitScale: discovered == null ? void 0 : discovered.verticalUnitScale,
2389
+ coordinateEpoch: discovered == null ? void 0 : discovered.coordinateEpoch,
2390
+ heightReference: discovered == null ? void 0 : discovered.heightReference,
2391
+ elevationMode: discovered == null ? void 0 : discovered.elevationMode,
2392
+ elevationOffset: discovered == null ? void 0 : discovered.elevationOffset,
2393
+ elevationUnit: discovered == null ? void 0 : discovered.elevationUnit,
2394
+ elevationUnitScale: discovered == null ? void 0 : discovered.elevationUnitScale,
2395
+ coordinateFrame: discovered == null ? void 0 : discovered.coordinateFrame,
2396
+ axisOrder: discovered == null ? void 0 : discovered.axisOrder,
2397
+ provenance: discovered == null ? void 0 : discovered.provenance,
2398
+ warnings: discovered == null ? void 0 : discovered.warnings
2399
+ }, options);
2400
+ }
2401
+ function getKnownCRSDefinition(reference) {
2402
+ return reference.state === "explicit" || reference.state === "default" ? reference.definition : void 0;
2403
+ }
2404
+ function cloneReadonlyCRSDefinition(definition) {
2405
+ const spatialReference = (0, import_crs.createSpatialReference)({
2406
+ crs: {
2407
+ state: "explicit",
2408
+ definition,
2409
+ representation: (0, import_crs.inferCRSRepresentation)(definition),
2410
+ provenance: "caller-override"
2411
+ }
2412
+ });
2413
+ const clonedDefinition = getKnownCRSDefinition(spatialReference.crs);
2414
+ if (!clonedDefinition) {
2415
+ throw new Error("Failed to normalize a known target CRS definition");
2416
+ }
2417
+ return clonedDefinition;
2418
+ }
2419
+ function getCoordinateOrder(axisOrder) {
2420
+ switch (axisOrder) {
2421
+ case "xy":
2422
+ return ["x", "y"];
2423
+ case "yx":
2424
+ return ["y", "x"];
2425
+ case "xyz":
2426
+ return ["x", "y", "z"];
2427
+ default:
2428
+ return [];
2429
+ }
2430
+ }
2431
+ function markTilesetSpatialReferenceTransformed(spatialReference) {
2432
+ if (spatialReference.status !== "transformable" && spatialReference.status !== "transformed") {
2433
+ throw new Error("Only a transformable spatial reference can be marked as transformed");
2434
+ }
2435
+ return Object.freeze({ ...spatialReference, status: "transformed" });
2436
+ }
2437
+
2027
2438
  // dist/tileset-3d/common/tileset-source.js
2028
2439
  function isTileset3DSource(value) {
2029
2440
  return Boolean(value && typeof value === "object" && "initialize" in value && "getRootTileset" in value && "initializeTileHeaders" in value && "loadTileContent" in value);
@@ -2083,6 +2494,7 @@ var DEFAULT_PROPS3 = {
2083
2494
  foveatedInterpolationCallback: interpolateLinearly,
2084
2495
  foveatedTimeDelay: 0.2,
2085
2496
  maximumScreenSpaceError: 8,
2497
+ skipLevelOfDetail: false,
2086
2498
  dynamicScreenSpaceError: true,
2087
2499
  dynamicScreenSpaceErrorDensity: 2e-4,
2088
2500
  dynamicScreenSpaceErrorFactor: 24,
@@ -2094,7 +2506,8 @@ var DEFAULT_PROPS3 = {
2094
2506
  loadOptions: { fetch: {} },
2095
2507
  attributions: [],
2096
2508
  basePath: "",
2097
- i3s: {}
2509
+ i3s: {},
2510
+ spatial: {}
2098
2511
  };
2099
2512
  var TILES_TOTAL = "Tiles In Tileset(s)";
2100
2513
  var TILES_IN_MEMORY = "Tiles In Memory";
@@ -2125,6 +2538,18 @@ var Tileset3D = class {
2125
2538
  root = null;
2126
2539
  roots = {};
2127
2540
  asset = {};
2541
+ /** Inline metadata schema declared by the source tileset, when present. */
2542
+ schema = null;
2543
+ /** External metadata schema URI declared by the source tileset, when present. */
2544
+ schemaUri = null;
2545
+ /** Metadata groups declared by the source tileset, in source order. */
2546
+ groups = [];
2547
+ /** Tileset-wide metadata entity, preserved without property-table decoding. */
2548
+ metadata = null;
2549
+ /** Tileset statistics metadata, preserved for application-level inspection. */
2550
+ statistics = null;
2551
+ /** Normalized source and target coordinate reference system metadata. */
2552
+ spatialReference = createTilesetSpatialReference({});
2128
2553
  description = "";
2129
2554
  properties;
2130
2555
  extras = null;
@@ -2209,6 +2634,10 @@ var Tileset3D = class {
2209
2634
  this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;
2210
2635
  this._cacheBytes = cacheBytes;
2211
2636
  this._maximumCacheOverflowBytes = maximumCacheOverflowBytes;
2637
+ const metadata = this._getSourceMetadata();
2638
+ if (metadata) {
2639
+ this.spatialReference = applyTilesetSpatialOptions(metadata.spatialReference, this.options.spatial);
2640
+ }
2212
2641
  this.stats = new import_stats3.Stats({ id: this.url });
2213
2642
  this._initializeStats();
2214
2643
  this.tilesetInitializationPromise = this._initializeTileSet();
@@ -2527,10 +2956,13 @@ var Tileset3D = class {
2527
2956
  for (const tile of this.selectedTiles) {
2528
2957
  if (tile.contentAvailable && tile.content) {
2529
2958
  tilesRenderable++;
2530
- if (tile.content.pointCount) {
2531
- pointsRenderable += tile.content.pointCount;
2532
- } else {
2533
- pointsRenderable += tile.content.vertexCount;
2959
+ const contents = tile.contents.length ? tile.contents : [tile.content];
2960
+ for (const content of contents) {
2961
+ if (content.pointCount) {
2962
+ pointsRenderable += content.pointCount;
2963
+ } else {
2964
+ pointsRenderable += content.vertexCount || 0;
2965
+ }
2534
2966
  }
2535
2967
  }
2536
2968
  }
@@ -2540,20 +2972,21 @@ var Tileset3D = class {
2540
2972
  this.stats.get(MAXIMUM_SSE).count = this.memoryAdjustedScreenSpaceError;
2541
2973
  }
2542
2974
  async _initializeTileSet() {
2543
- var _a;
2975
+ var _a, _b, _c;
2544
2976
  try {
2545
2977
  const initializePromise = this.source.initialize();
2546
2978
  const initialMetadata = this._getSourceMetadata();
2547
2979
  if (initialMetadata) {
2548
2980
  this._syncSourceState(initialMetadata);
2549
2981
  }
2550
- if (((_a = initialMetadata == null ? void 0 : initialMetadata.tileset) == null ? void 0 : _a.root) && typeof initialMetadata.tileset.root.then !== "function") {
2982
+ if (((_a = initialMetadata == null ? void 0 : initialMetadata.tileset) == null ? void 0 : _a.root) && typeof initialMetadata.tileset.root.then !== "function" && !this.source.prepareTileset) {
2551
2983
  this.root = this._initializeTileHeaders(initialMetadata.tileset, null);
2552
2984
  this._applyViewState();
2553
2985
  }
2554
2986
  await initializePromise;
2555
2987
  const metadata = this.source.getMetadata();
2556
2988
  this._syncSourceState(metadata);
2989
+ await ((_c = (_b = this.source).prepareTileset) == null ? void 0 : _c.call(_b, this));
2557
2990
  if (!this.root) {
2558
2991
  this.root = this._initializeTileHeaders(metadata.tileset, null);
2559
2992
  }
@@ -2729,6 +3162,14 @@ var Tileset3D = class {
2729
3162
  this.refine = metadata.refine;
2730
3163
  this.contentFormats = this.source.contentFormats;
2731
3164
  this.asset = this.source.asset || this.asset;
3165
+ const tileset = metadata.tileset;
3166
+ this.schema = tileset.schema || null;
3167
+ this.schemaUri = tileset.schemaUri || null;
3168
+ this.groups = tileset.groups || [];
3169
+ this.metadata = tileset.metadata || null;
3170
+ this.statistics = tileset.statistics ?? null;
3171
+ const spatialReference = applyTilesetSpatialOptions(metadata.spatialReference, this.options.spatial);
3172
+ this.spatialReference = preserveTransformedSpatialReference(this.spatialReference, spatialReference);
2732
3173
  this.properties = this.source.properties ?? this.properties;
2733
3174
  this.extras = this.source.extras ?? this.extras;
2734
3175
  if ((_a = this.options.attributions) == null ? void 0 : _a.length) {
@@ -2762,6 +3203,9 @@ var Tileset3D = class {
2762
3203
  }
2763
3204
  }
2764
3205
  };
3206
+ function preserveTransformedSpatialReference(current, next) {
3207
+ return (current == null ? void 0 : current.status) === "transformed" && current.sourceCrs === next.sourceCrs && current.targetCrs === next.targetCrs && current.targetHeightReference === next.targetHeightReference && current.outputCoordinates === next.outputCoordinates ? current : next;
3208
+ }
2765
3209
 
2766
3210
  // dist/tileset-3d/common/indexed-archive-tileset-source.js
2767
3211
  var IndexedArchiveTilesetSource = class {
@@ -2888,19 +3332,13 @@ function defined(x) {
2888
3332
  }
2889
3333
  var scratchPoint = new import_core6.Vector3();
2890
3334
  var scratchScale = new import_core6.Vector3();
2891
- var scratchNorthWest = new import_core6.Vector3();
2892
- var scratchSouthEast = new import_core6.Vector3();
2893
- var scratchCenter2 = new import_core6.Vector3();
2894
- var scratchXAxis = new import_core6.Vector3();
2895
- var scratchYAxis = new import_core6.Vector3();
2896
- var scratchZAxis = new import_core6.Vector3();
2897
3335
  function createBoundingVolume(boundingVolumeHeader, transform, result) {
2898
3336
  assert(boundingVolumeHeader, "3D Tile: boundingVolume must be defined");
2899
3337
  if (boundingVolumeHeader.box) {
2900
3338
  return createBox(boundingVolumeHeader.box, transform, result);
2901
3339
  }
2902
3340
  if (boundingVolumeHeader.region) {
2903
- return createObbFromRegion(boundingVolumeHeader.region);
3341
+ return (0, import_geospatial4.makeOBBFromRegion)(boundingVolumeHeader.region);
2904
3342
  }
2905
3343
  if (boundingVolumeHeader.sphere) {
2906
3344
  return createSphere(boundingVolumeHeader.sphere, transform, result);
@@ -2978,22 +3416,6 @@ function createSphere(sphere, transform, result) {
2978
3416
  }
2979
3417
  return new import_culling3.BoundingSphere(center, radius);
2980
3418
  }
2981
- function createObbFromRegion(region) {
2982
- const [west, south, east, north, minHeight, maxHeight] = region;
2983
- const northWest = import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core6.degrees)(west), (0, import_core6.degrees)(north), minHeight], scratchNorthWest);
2984
- const southEast = import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core6.degrees)(east), (0, import_core6.degrees)(south), maxHeight], scratchSouthEast);
2985
- const centerInCartesian = new import_core6.Vector3().addVectors(northWest, southEast).multiplyByScalar(0.5);
2986
- import_geospatial4.Ellipsoid.WGS84.cartesianToCartographic(centerInCartesian, scratchCenter2);
2987
- import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core6.degrees)(east), scratchCenter2[1], scratchCenter2[2]], scratchXAxis);
2988
- import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([scratchCenter2[0], (0, import_core6.degrees)(north), scratchCenter2[2]], scratchYAxis);
2989
- import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([scratchCenter2[0], scratchCenter2[1], maxHeight], scratchZAxis);
2990
- return createBox([
2991
- ...centerInCartesian,
2992
- ...scratchXAxis.subtract(centerInCartesian),
2993
- ...scratchYAxis.subtract(centerInCartesian),
2994
- ...scratchZAxis.subtract(centerInCartesian)
2995
- ], new import_core6.Matrix4());
2996
- }
2997
3419
  function orientedBoundingBoxToCartographicBounds(boundingVolume) {
2998
3420
  const result = emptyCartographicBounds();
2999
3421
  const { halfAxes } = boundingVolume;
@@ -3078,19 +3500,32 @@ function getLodStatus(tile, frameState) {
3078
3500
  if (screenSize < 2) {
3079
3501
  return "OUT";
3080
3502
  }
3081
- if (!tile.header.children || screenSize <= tile.lodMetricValue) {
3503
+ if (!tile.header.children) {
3082
3504
  return "DRAW";
3083
- } else if (tile.header.children) {
3084
- return "DIG";
3085
3505
  }
3086
- return "OUT";
3506
+ switch (tile.lodMetricType) {
3507
+ case "screenSpaceRelative": {
3508
+ const viewportHeight = frameState.topDownViewport.height || 1;
3509
+ return screenSize / viewportHeight <= tile.lodMetricValue ? "DRAW" : "DIG";
3510
+ }
3511
+ case "distanceRangeFromDefaultCamera":
3512
+ return tile.distanceToTile(frameState) >= tile.lodMetricValue ? "DRAW" : "DIG";
3513
+ case "maxScreenThresholdSQ": {
3514
+ const diameterThreshold = Math.sqrt(tile.lodMetricValue / (Math.PI * 0.25));
3515
+ return screenSize <= diameterThreshold ? "DRAW" : "DIG";
3516
+ }
3517
+ case "maxScreenThreshold":
3518
+ default:
3519
+ return screenSize <= tile.lodMetricValue ? "DRAW" : "DIG";
3520
+ }
3087
3521
  }
3088
3522
  function getProjectedRadius(tile, frameState) {
3089
3523
  const { topDownViewport: viewport } = frameState;
3090
- const mbsLat = tile.header.mbs[1];
3091
- const mbsLon = tile.header.mbs[0];
3092
- const mbsZ = tile.header.mbs[2];
3093
- const mbsR = tile.header.mbs[3];
3524
+ const mbs = tile.header.i3sLodMbs || tile.header.mbs;
3525
+ const mbsLat = mbs[1];
3526
+ const mbsLon = mbs[0];
3527
+ const mbsZ = mbs[2];
3528
+ const mbsR = mbs[3];
3094
3529
  const mbsCenterCartesian = [...tile.boundingVolume.center];
3095
3530
  const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition);
3096
3531
  import_geospatial5.Ellipsoid.WGS84.cartographicToCartesian(cameraPositionCartographic, cameraPositionCartesian);
@@ -3341,7 +3776,7 @@ var TilesetTraverser = class {
3341
3776
  this.selectTile(tile, frameState);
3342
3777
  } else if (tile.refine === TILE_REFINEMENT.REPLACE) {
3343
3778
  this.loadTile(tile, frameState);
3344
- if (stoppedRefining) {
3779
+ if (stoppedRefining || this.options.skipLevelOfDetail) {
3345
3780
  this.selectTile(tile, frameState);
3346
3781
  }
3347
3782
  }
@@ -3406,7 +3841,7 @@ var TilesetTraverser = class {
3406
3841
  }
3407
3842
  // tile to render in the browser
3408
3843
  selectTile(tile, frameState) {
3409
- if (this.shouldSelectTile(tile)) {
3844
+ if (this.shouldSelectTile(tile, frameState)) {
3410
3845
  tile._selectedFrame = frameState.frameNumber;
3411
3846
  this.selectedTiles[tile.id] = tile;
3412
3847
  }
@@ -3456,8 +3891,13 @@ var TilesetTraverser = class {
3456
3891
  shouldLoadTile(tile) {
3457
3892
  return tile.hasUnloadedContent || tile.contentExpired;
3458
3893
  }
3459
- shouldSelectTile(tile) {
3460
- return tile.contentAvailable && !this.options.skipLevelOfDetail;
3894
+ /**
3895
+ * Returns whether a tile's content is available and visible for rendering.
3896
+ * @param tile Tile considered for rendering.
3897
+ * @param frameState Current culling state.
3898
+ */
3899
+ shouldSelectTile(tile, frameState) {
3900
+ return tile.contentAvailable && tile.contentVisibility(frameState) !== "outside";
3461
3901
  }
3462
3902
  /** Decide if tile LoD (level of detail) is not sufficient under current viewport */
3463
3903
  shouldRefine(tile, frameState, useParentMetric = false) {
@@ -3529,6 +3969,35 @@ var TilesetTraverser = class {
3529
3969
 
3530
3970
  // dist/tileset-3d/common/tile-3d.js
3531
3971
  var scratchVector2 = new import_core8.Vector3();
3972
+ function getContentVisibility(contentVolumes, fallbackVolume, cullingVolume2, visibilityPlaneMask, clippingPlanes = []) {
3973
+ const volumes = contentVolumes.length ? contentVolumes : [fallbackVolume];
3974
+ let visibleVolume = false;
3975
+ let intersectingVolume = false;
3976
+ for (const contentVolume of volumes) {
3977
+ let visibility = "inside";
3978
+ if (visibilityPlaneMask !== import_culling4.CullingVolume.MASK_INSIDE) {
3979
+ visibility = cullingVolume2.computeVisibility(contentVolume);
3980
+ if (visibility === "outside") {
3981
+ continue;
3982
+ }
3983
+ }
3984
+ let clipped = false;
3985
+ for (const clippingPlane of clippingPlanes) {
3986
+ if (contentVolume.intersectPlane(clippingPlane) === "outside") {
3987
+ clipped = true;
3988
+ break;
3989
+ }
3990
+ }
3991
+ if (!clipped) {
3992
+ visibleVolume = true;
3993
+ intersectingVolume = intersectingVolume || visibility === "intersecting";
3994
+ }
3995
+ }
3996
+ if (!visibleVolume) {
3997
+ return "outside";
3998
+ }
3999
+ return intersectingVolume ? "intersecting" : "inside";
4000
+ }
3532
4001
  function defined2(x) {
3533
4002
  return x !== void 0 && x !== null;
3534
4003
  }
@@ -3542,6 +4011,8 @@ var Tile3D = class {
3542
4011
  refine;
3543
4012
  type;
3544
4013
  contentUrl;
4014
+ /** Resolved render-content URLs in source order; empty for contentless tiles. */
4015
+ contentUrls = [];
3545
4016
  /** Different refinement algorithms used by I3S and 3D tiles */
3546
4017
  lodMetricType = "geometricError";
3547
4018
  /**
@@ -3561,6 +4032,17 @@ var Tile3D = class {
3561
4032
  * not the content's metadata in the tileset JSON file.
3562
4033
  */
3563
4034
  content = null;
4035
+ /** Loaded payloads in the same order as {@link contentUrls}; `content` remains the primary payload. */
4036
+ contents = [];
4037
+ /**
4038
+ * Metadata entity attached to this tile in the source tileset, if present.
4039
+ *
4040
+ * This is the raw 3D Tiles metadata reference. The generic tiles runtime preserves it without
4041
+ * decoding schema classes or binary property tables.
4042
+ */
4043
+ metadata = null;
4044
+ /** Raw metadata entities attached to tile contents, in content source order. */
4045
+ contentMetadata = [];
3564
4046
  contentState = TILE_CONTENT_STATE.UNLOADED;
3565
4047
  gpuMemoryUsageInBytes = 0;
3566
4048
  /** The tile's children - an array of Tile3D objects. */
@@ -3602,7 +4084,22 @@ var Tile3D = class {
3602
4084
  _visibilityPlaneMask;
3603
4085
  _visible = void 0;
3604
4086
  _contentBoundingVolume;
4087
+ _contentBoundingVolumes = [];
4088
+ /** Source content-volume headers retained after render content is unloaded. */
4089
+ _contentBoundingVolumeHeaders = [];
3605
4090
  _viewerRequestVolume;
4091
+ /** Bounding volume used to cull the tile's first renderable content entry. */
4092
+ get contentBoundingVolume() {
4093
+ return this._contentBoundingVolume;
4094
+ }
4095
+ /** All transformed render-content bounding volumes, including tile-volume fallbacks. */
4096
+ get contentBoundingVolumes() {
4097
+ return this._contentBoundingVolumes;
4098
+ }
4099
+ /** Bounding volume that limits when this tile may be requested, when declared. */
4100
+ get viewerRequestVolume() {
4101
+ return this._viewerRequestVolume;
4102
+ }
3606
4103
  _initialTransform = new import_core8.Matrix4();
3607
4104
  // Used by traverser, cannot be marked private
3608
4105
  _priority = 0;
@@ -3643,6 +4140,10 @@ var Tile3D = class {
3643
4140
  this.refine = this._getRefine(header.refine);
3644
4141
  this.type = header.type;
3645
4142
  this.contentUrl = header.contentUrl;
4143
+ this.contentUrls = header.contentUrls || (header.contentUrl ? [header.contentUrl] : []);
4144
+ this.metadata = header.metadata || null;
4145
+ const contentHeaders = Array.isArray(header.content) ? header.content : [header.content];
4146
+ this.contentMetadata = contentHeaders.filter(Boolean).map((contentHeader) => contentHeader.metadata || null);
3646
4147
  this.childrenState = header.implicitSubtree ? "unloaded" : "ready";
3647
4148
  this._initializeLodMetric(header);
3648
4149
  this._initializeTransforms(header);
@@ -3774,7 +4275,8 @@ var Tile3D = class {
3774
4275
  * Memory usage of tile on GPU
3775
4276
  */
3776
4277
  _getGpuMemoryUsageInBytes() {
3777
- return this.content.gpuMemoryUsageInBytes || this.content.byteLength || 0;
4278
+ const contents = this.contents.length ? this.contents : [this.content];
4279
+ return contents.reduce((total, content) => total + ((content == null ? void 0 : content.gpuMemoryUsageInBytes) || (content == null ? void 0 : content.byteLength) || 0), 0);
3778
4280
  }
3779
4281
  /**
3780
4282
  * Returns scheduler priority for render-content requests.
@@ -3855,6 +4357,10 @@ var Tile3D = class {
3855
4357
  }
3856
4358
  try {
3857
4359
  const loadResult = await this.tileset.source.loadTileContent(this);
4360
+ if (loadResult.contents) {
4361
+ this.contents = loadResult.contents;
4362
+ this.content = this.contents[0] || null;
4363
+ }
3858
4364
  if (this.tileset.options.contentLoader) {
3859
4365
  await this.tileset.options.contentLoader(this);
3860
4366
  }
@@ -3941,6 +4447,7 @@ var Tile3D = class {
3941
4447
  this.refine = this._getRefine(materializedHeader.refine);
3942
4448
  this.type = materializedHeader.type;
3943
4449
  this.contentUrl = materializedHeader.contentUrl;
4450
+ this.contentUrls = materializedHeader.contentUrls || (materializedHeader.contentUrl ? [materializedHeader.contentUrl] : []);
3944
4451
  this._initializeLodMetric(materializedHeader);
3945
4452
  this._updateLodMetricScale();
3946
4453
  this._initializeBoundingVolumes(this.header);
@@ -3948,9 +4455,12 @@ var Tile3D = class {
3948
4455
  }
3949
4456
  // Unloads the tile's content.
3950
4457
  unloadContent() {
3951
- if (this.content && this.content.destroy) {
3952
- this.content.destroy();
4458
+ for (const content of this.contents.length ? this.contents : [this.content]) {
4459
+ if (content && content.destroy) {
4460
+ content.destroy();
4461
+ }
3953
4462
  }
4463
+ this.contents = [];
3954
4464
  this.content = null;
3955
4465
  if (this.header.content && this.header.content.destroy) {
3956
4466
  this.header.content.destroy();
@@ -4038,12 +4548,16 @@ var Tile3D = class {
4038
4548
  const { boundingVolume } = this;
4039
4549
  return cullingVolume2.computeVisibilityWithPlaneMask(boundingVolume, parentVisibilityPlaneMask);
4040
4550
  }
4041
- // Assuming the tile's bounding volume intersects the culling volume, determines
4042
- // whether the tile's content's bounding volume intersects the culling volume.
4043
- // @param {FrameState} frameState The frame state.
4044
- // @returns {Intersect} The result of the intersection: the tile's content is completely outside, completely inside, or intersecting the culling volume.
4045
- contentVisibility() {
4046
- return true;
4551
+ /**
4552
+ * Determines whether renderable content intersects the view and configured clipping planes.
4553
+ *
4554
+ * Clipping planes are render-only constraints; traversal continues to use the tile volume.
4555
+ *
4556
+ * @param frameState Current camera, culling, and optional clipping-plane state.
4557
+ * @returns The content visibility classification.
4558
+ */
4559
+ contentVisibility(frameState) {
4560
+ return getContentVisibility(this._contentBoundingVolumes, this.boundingVolume, frameState.cullingVolume, this._visibilityPlaneMask, frameState.clippingPlanes);
4047
4561
  }
4048
4562
  /**
4049
4563
  * Computes the (potentially approximate) distance from the closest point of the tile's bounding volume to the camera.
@@ -4135,15 +4649,18 @@ var Tile3D = class {
4135
4649
  }
4136
4650
  _initializeBoundingVolumes(tileHeader) {
4137
4651
  this._contentBoundingVolume = null;
4652
+ this._contentBoundingVolumeHeaders = [];
4138
4653
  this._viewerRequestVolume = null;
4139
4654
  this._updateBoundingVolume(tileHeader);
4140
4655
  }
4141
4656
  _initializeContent(tileHeader) {
4657
+ var _a;
4142
4658
  this.content = { _tileset: this.tileset, _tile: this };
4659
+ this.contents = [];
4143
4660
  this.hasEmptyContent = true;
4144
4661
  this.contentState = TILE_CONTENT_STATE.UNLOADED;
4145
4662
  this.hasTilesetContent = false;
4146
- if (tileHeader.contentUrl) {
4663
+ if (tileHeader.contentUrl || ((_a = tileHeader.contentUrls) == null ? void 0 : _a.length)) {
4147
4664
  this.content = null;
4148
4665
  this.hasEmptyContent = false;
4149
4666
  }
@@ -4182,16 +4699,18 @@ var Tile3D = class {
4182
4699
  * `content`.
4183
4700
  */
4184
4701
  _isTileset() {
4185
- var _a;
4186
- return ((_a = this.content) == null ? void 0 : _a.shape) === "tileset3d";
4702
+ return (this.contents.length ? this.contents : [this.content]).some((content) => (content == null ? void 0 : content.shape) === "tileset3d");
4187
4703
  }
4188
4704
  _onContentLoaded() {
4189
- switch (this.content && this.content.type) {
4190
- case "vctr":
4191
- case "geom":
4192
- this.tileset._traverser.disableSkipLevelOfDetail = true;
4193
- break;
4194
- default:
4705
+ const contents = this.contents.length ? this.contents : [this.content];
4706
+ for (const content of contents) {
4707
+ switch (content && content.type) {
4708
+ case "vctr":
4709
+ case "geom":
4710
+ this.tileset._traverser.disableSkipLevelOfDetail = true;
4711
+ break;
4712
+ default:
4713
+ }
4195
4714
  }
4196
4715
  if (this._isTileset()) {
4197
4716
  this.hasTilesetContent = true;
@@ -4205,12 +4724,12 @@ var Tile3D = class {
4205
4724
  this._viewerRequestVolume = createBoundingVolume(header.viewerRequestVolume, this.computedTransform, this._viewerRequestVolume);
4206
4725
  }
4207
4726
  const content = header.content;
4208
- if (!content) {
4209
- return;
4210
- }
4211
- if (content.boundingVolume) {
4212
- this._contentBoundingVolume = createBoundingVolume(content.boundingVolume, this.computedTransform, this._contentBoundingVolume);
4727
+ if (content) {
4728
+ this._contentBoundingVolumeHeaders = Array.isArray(content) ? content : [content];
4213
4729
  }
4730
+ const contentHeaders = content ? Array.isArray(content) ? content : [content] : this._contentBoundingVolumeHeaders;
4731
+ this._contentBoundingVolumes = contentHeaders.length ? contentHeaders.map((headerEntry) => (headerEntry == null ? void 0 : headerEntry.boundingVolume) ? createBoundingVolume(headerEntry.boundingVolume, this.computedTransform) : this.boundingVolume) : [this.boundingVolume];
4732
+ this._contentBoundingVolume = this._contentBoundingVolumes[0] || null;
4214
4733
  }
4215
4734
  /**
4216
4735
  * Updates the tile's computed transform, world-space geometric error, and bounding volumes.
@@ -4376,6 +4895,259 @@ function getObbSize(halfAxes) {
4376
4895
  return size;
4377
4896
  }
4378
4897
 
4898
+ // dist/spatial/i3s-elevation.js
4899
+ var LINEAR_UNIT_TO_METERS = Object.freeze({
4900
+ meter: 1,
4901
+ millimeter: 1e-3,
4902
+ centimeter: 0.01,
4903
+ decimeter: 0.1,
4904
+ kilometer: 1e3,
4905
+ foot: 0.3048,
4906
+ yard: 0.9144,
4907
+ mile: 1609.344,
4908
+ "us-foot": 1200 / 3937,
4909
+ "us-inch": 0.0254000508001016,
4910
+ "us-yard": 3600 / 3937,
4911
+ "us-mile": 6336e3 / 3937,
4912
+ "clarke-foot": 0.3047972654,
4913
+ "clarke-yard": 0.9143917962,
4914
+ "clarke-link": 0.201166195164,
4915
+ "sears-yard": 0.914398414616,
4916
+ "sears-foot": 0.304799471539,
4917
+ "sears-chain": 20.1167651216,
4918
+ "sears-1922-truncated-chain": 20.116756,
4919
+ "benoit-1895-b-chain": 20.1167824944,
4920
+ "indian-yard": 0.914398530744,
4921
+ "indian-1937-yard": 0.91439523,
4922
+ "gold-coast-foot": 0.304799710181
4923
+ });
4924
+ var LINEAR_UNIT_ALIASES = Object.freeze({
4925
+ m: "meter",
4926
+ metre: "meter",
4927
+ meters: "meter",
4928
+ metres: "meter",
4929
+ mm: "millimeter",
4930
+ millimetre: "millimeter",
4931
+ millimeters: "millimeter",
4932
+ millimetres: "millimeter",
4933
+ cm: "centimeter",
4934
+ centimetre: "centimeter",
4935
+ centimeters: "centimeter",
4936
+ centimetres: "centimeter",
4937
+ dm: "decimeter",
4938
+ decimetre: "decimeter",
4939
+ decimeters: "decimeter",
4940
+ decimetres: "decimeter",
4941
+ km: "kilometer",
4942
+ kilometre: "kilometer",
4943
+ kilometers: "kilometer",
4944
+ kilometres: "kilometer",
4945
+ ft: "foot",
4946
+ feet: "foot",
4947
+ "us-survey-foot": "us-foot",
4948
+ "us-survey-feet": "us-foot",
4949
+ yd: "yard",
4950
+ yards: "yard",
4951
+ mi: "mile",
4952
+ miles: "mile"
4953
+ });
4954
+ function getI3SLinearUnitScale(unit) {
4955
+ if (!unit) {
4956
+ return void 0;
4957
+ }
4958
+ const normalized = unit.trim().toLowerCase().replaceAll("_", "-");
4959
+ const canonical = LINEAR_UNIT_ALIASES[normalized] || normalized;
4960
+ return LINEAR_UNIT_TO_METERS[canonical];
4961
+ }
4962
+ function getI3SVerticalUnitScale(heightUnit, zFactor) {
4963
+ if (zFactor !== void 0) {
4964
+ return Number.isFinite(zFactor) && zFactor > 0 ? zFactor : void 0;
4965
+ }
4966
+ return heightUnit ? getI3SLinearUnitScale(heightUnit) : 1;
4967
+ }
4968
+
4969
+ // dist/spatial/format-spatial-reference.js
4970
+ function getI3SSpatialReference(layer) {
4971
+ var _a, _b, _c, _d, _e, _f, _g, _h;
4972
+ const spatialReference = layer.spatialReference || ((_a = layer.fullExtent) == null ? void 0 : _a.spatialReference);
4973
+ const sourceCrs = getI3SCrsDefinition(spatialReference);
4974
+ const verticalCrs = ((_b = layer.heightModelInfo) == null ? void 0 : _b.vertCRS) || getI3SVerticalCrsDefinition(spatialReference);
4975
+ const heightModel = (_c = layer.heightModelInfo) == null ? void 0 : _c.heightModel;
4976
+ const verticalUnit = (_d = layer.heightModelInfo) == null ? void 0 : _d.heightUnit;
4977
+ const verticalUnitScale = getI3SVerticalUnitScale(verticalUnit, layer.ZFactor);
4978
+ const heightReference = heightModel === "ellipsoidal" ? "ellipsoidal" : heightModel === "gravity_related_height" ? "orthometric" : "unknown";
4979
+ const sourceIdentifier = getCrsIdentifier(spatialReference);
4980
+ const coordinateFrame = sourceIdentifier === void 0 ? getWktCoordinateFrame(spatialReference == null ? void 0 : spatialReference.wkt) : getIdentifierCoordinateFrame(sourceIdentifier);
4981
+ const units = getI3SCoordinateUnits(coordinateFrame, verticalUnit, layer.ZFactor);
4982
+ const warnings = [];
4983
+ const elevationMode = getI3SElevationMode((_e = layer.elevationInfo) == null ? void 0 : _e.mode);
4984
+ const elevationUnit = layer.elevationInfo ? layer.elevationInfo.unit || "meter" : void 0;
4985
+ const elevationUnitScale = getI3SLinearUnitScale(elevationUnit);
4986
+ if (verticalUnitScale === void 0) {
4987
+ warnings.push(layer.ZFactor !== void 0 ? `I3S ZFactor ${layer.ZFactor} is not a positive finite conversion factor` : `Unsupported I3S vertical unit ${verticalUnit}`);
4988
+ }
4989
+ if (layer.elevationInfo && elevationUnitScale === void 0) {
4990
+ warnings.push(`Unsupported I3S elevation unit ${elevationUnit}`);
4991
+ }
4992
+ if (((_f = layer.elevationInfo) == null ? void 0 : _f.mode) && !elevationMode) {
4993
+ warnings.push(`Unsupported I3S elevation mode ${layer.elevationInfo.mode}`);
4994
+ }
4995
+ if (elevationMode && elevationMode !== "absoluteHeight") {
4996
+ warnings.push(`I3S elevation mode ${elevationMode} requires a terrain or scene elevation provider`);
4997
+ }
4998
+ return createTilesetSpatialReference({
4999
+ sourceCrs,
5000
+ sourceCrsRepresentation: sourceIdentifier !== void 0 ? "identifier" : sourceCrs ? "wkt" : void 0,
5001
+ sourceCrsAlternatives: sourceIdentifier !== void 0 && (spatialReference == null ? void 0 : spatialReference.wkt) ? [{ definition: spatialReference.wkt, representation: "wkt" }] : void 0,
5002
+ verticalCrs,
5003
+ units,
5004
+ verticalUnitScale: verticalUnitScale ?? Number.NaN,
5005
+ heightReference,
5006
+ elevationMode,
5007
+ elevationOffset: (_g = layer.elevationInfo) == null ? void 0 : _g.offset,
5008
+ elevationUnit,
5009
+ elevationUnitScale: ((_h = layer.elevationInfo) == null ? void 0 : _h.mode) && !elevationMode ? Number.NaN : elevationUnitScale ?? (layer.elevationInfo ? Number.NaN : 1),
5010
+ coordinateFrame,
5011
+ axisOrder: sourceCrs ? "xyz" : "unknown",
5012
+ provenance: sourceCrs ? "metadata" : "unknown",
5013
+ warnings
5014
+ });
5015
+ }
5016
+ function getI3SCoordinateUnits(coordinateFrame, verticalUnit, zFactor) {
5017
+ if (coordinateFrame === "geocentric") {
5018
+ return ["meter", "meter", "meter"];
5019
+ }
5020
+ if (coordinateFrame === "geographic" && (verticalUnit || zFactor === void 0)) {
5021
+ return ["degree", "degree", verticalUnit || "meter"];
5022
+ }
5023
+ return void 0;
5024
+ }
5025
+ function getI3SElevationMode(mode) {
5026
+ switch (mode) {
5027
+ case "absoluteHeight":
5028
+ case "onTheGround":
5029
+ case "relativeToGround":
5030
+ case "relativeToScene":
5031
+ return mode;
5032
+ default:
5033
+ return void 0;
5034
+ }
5035
+ }
5036
+ function get3DTilesSpatialReference(tileset) {
5037
+ var _a, _b;
5038
+ const geocentricCrs = getTilesetSemanticValue(tileset, "TILESET_CRS_GEOCENTRIC");
5039
+ const coordinateEpoch = getTilesetSemanticValue(tileset, "TILESET_CRS_COORDINATE_EPOCH");
5040
+ const warnings = [];
5041
+ let sourceCrs;
5042
+ let provenance = "unknown";
5043
+ let sourceCrsState;
5044
+ if (typeof geocentricCrs === "string" && geocentricCrs.toUpperCase() !== "UNKNOWN") {
5045
+ sourceCrs = geocentricCrs;
5046
+ provenance = "metadata";
5047
+ sourceCrsState = "explicit";
5048
+ } else if (geocentricCrs !== void 0) {
5049
+ warnings.push("3D Tiles geocentric CRS is explicitly unknown");
5050
+ provenance = "metadata";
5051
+ sourceCrsState = "unknown";
5052
+ } else if ((_b = (_a = tileset.root) == null ? void 0 : _a.boundingVolume) == null ? void 0 : _b.region) {
5053
+ sourceCrs = "EPSG:4978";
5054
+ provenance = "format-default";
5055
+ sourceCrsState = "default";
5056
+ } else if (tileset.schemaUri && !tileset.schema) {
5057
+ warnings.push("External 3D Tiles metadata schema must be loaded before CRS semantics can resolve");
5058
+ }
5059
+ const epoch = parseCoordinateEpoch(coordinateEpoch);
5060
+ if (coordinateEpoch !== void 0 && epoch === void 0) {
5061
+ warnings.push("3D Tiles coordinate epoch is not a finite decimal year");
5062
+ }
5063
+ return createTilesetSpatialReference({
5064
+ sourceCrs,
5065
+ sourceCrsState,
5066
+ sourceCrsRepresentation: sourceCrs ? "identifier" : void 0,
5067
+ coordinateEpoch: epoch,
5068
+ heightReference: sourceCrs ? "ellipsoidal" : "unknown",
5069
+ coordinateFrame: sourceCrs ? "geocentric" : "unknown",
5070
+ axisOrder: sourceCrs ? "xyz" : "unknown",
5071
+ provenance,
5072
+ warnings
5073
+ });
5074
+ }
5075
+ function parseCoordinateEpoch(value) {
5076
+ if (typeof value !== "string" && typeof value !== "number") {
5077
+ return void 0;
5078
+ }
5079
+ if (typeof value === "string" && !value.trim()) {
5080
+ return void 0;
5081
+ }
5082
+ const epoch = Number(value);
5083
+ return Number.isFinite(epoch) ? epoch : void 0;
5084
+ }
5085
+ function getI3SCrsDefinition(spatialReference) {
5086
+ const identifier = getCrsIdentifier(spatialReference);
5087
+ if (identifier !== void 0) {
5088
+ return `EPSG:${identifier}`;
5089
+ }
5090
+ return spatialReference == null ? void 0 : spatialReference.wkt;
5091
+ }
5092
+ function getI3SVerticalCrsDefinition(spatialReference) {
5093
+ const identifier = (spatialReference == null ? void 0 : spatialReference.latestVcsWkid) ?? (spatialReference == null ? void 0 : spatialReference.vcsWkid);
5094
+ return identifier === void 0 ? void 0 : `EPSG:${identifier}`;
5095
+ }
5096
+ function getCrsIdentifier(spatialReference) {
5097
+ return (spatialReference == null ? void 0 : spatialReference.latestWkid) ?? (spatialReference == null ? void 0 : spatialReference.wkid);
5098
+ }
5099
+ function getIdentifierCoordinateFrame(identifier) {
5100
+ if (identifier === 4978) {
5101
+ return "geocentric";
5102
+ }
5103
+ if (identifier === 4326 || identifier === 4490 || identifier === 4979) {
5104
+ return "geographic";
5105
+ }
5106
+ return "projected";
5107
+ }
5108
+ function getWktCoordinateFrame(wkt) {
5109
+ var _a;
5110
+ if (!wkt) {
5111
+ return "unknown";
5112
+ }
5113
+ const normalizedWkt = wkt.trim().toUpperCase();
5114
+ const rootKeyword = (_a = normalizedWkt.match(/^([A-Z][A-Z0-9_]*)\s*[\[(]/)) == null ? void 0 : _a[1];
5115
+ if (rootKeyword === "GEOCCS") {
5116
+ return "geocentric";
5117
+ }
5118
+ if (rootKeyword === "GEOGCS" || rootKeyword === "GEOGRAPHICCRS" || rootKeyword === "GEOGRAPHIC2DCRS" || rootKeyword === "GEOGRAPHIC3DCRS") {
5119
+ return "geographic";
5120
+ }
5121
+ if (rootKeyword === "PROJCS" || rootKeyword === "PROJCRS" || rootKeyword === "PROJECTEDCRS") {
5122
+ return "projected";
5123
+ }
5124
+ if (rootKeyword === "GEODCRS" || rootKeyword === "GEODETICCRS") {
5125
+ if (/\bCS\s*[\[(]\s*CARTESIAN\s*,\s*3\b/.test(normalizedWkt)) {
5126
+ return "geocentric";
5127
+ }
5128
+ if (/\bCS\s*[\[(]\s*ELLIPSOIDAL\s*,\s*[23]\b/.test(normalizedWkt)) {
5129
+ return "geographic";
5130
+ }
5131
+ }
5132
+ return "unknown";
5133
+ }
5134
+ function getTilesetSemanticValue(tileset, semantic) {
5135
+ var _a, _b, _c, _d;
5136
+ const classIdentifier = (_a = tileset.metadata) == null ? void 0 : _a.class;
5137
+ if (!classIdentifier) {
5138
+ return void 0;
5139
+ }
5140
+ const classDefinition = (_c = (_b = tileset.schema) == null ? void 0 : _b.classes) == null ? void 0 : _c[classIdentifier];
5141
+ const propertyDefinitions = classDefinition == null ? void 0 : classDefinition.properties;
5142
+ const propertyValues = (_d = tileset.metadata) == null ? void 0 : _d.properties;
5143
+ for (const [propertyIdentifier, propertyDefinition] of Object.entries(propertyDefinitions || {})) {
5144
+ if (propertyDefinition.semantic === semantic) {
5145
+ return propertyValues == null ? void 0 : propertyValues[propertyIdentifier];
5146
+ }
5147
+ }
5148
+ return void 0;
5149
+ }
5150
+
4379
5151
  // dist/tileset-3d/format-3d-tiles/implicit-tiling.js
4380
5152
  var QUADTREE_CHILD_COUNT = 4;
4381
5153
  var OCTREE_CHILD_COUNT = 8;
@@ -4413,7 +5185,7 @@ function materializeAvailableTile(subtree, reference, localCoordinates, globalCo
4413
5185
  return null;
4414
5186
  }
4415
5187
  counters.tileCount++;
4416
- const contentAvailable = getAvailabilityValue(getPrimaryContentAvailability(subtree.contentAvailability), availabilityIndex);
5188
+ const contentAvailability = getContentAvailability(subtree.contentAvailability, availabilityIndex);
4417
5189
  const children = [];
4418
5190
  if (globalCoordinates.level < descriptor.maximumLevel) {
4419
5191
  if (localCoordinates.localLevel + 1 < descriptor.subtreeLevels) {
@@ -4432,15 +5204,15 @@ function materializeAvailableTile(subtree, reference, localCoordinates, globalCo
4432
5204
  const childSubtreeIndex = localCoordinates.mortonIndex * childCount + childIndex;
4433
5205
  if (getAvailabilityValue(subtree.childSubtreeAvailability, childSubtreeIndex)) {
4434
5206
  const childCoordinates = getChildCoordinates(globalCoordinates, childIndex, descriptor.subdivisionScheme);
4435
- children.push(createLazyImplicitTileHeader(descriptor, childCoordinates));
5207
+ children.push(createLazyImplicitTileHeader(subtree, descriptor, childCoordinates));
4436
5208
  counters.childSubtreeCount++;
4437
5209
  }
4438
5210
  }
4439
5211
  }
4440
5212
  }
4441
- return formatImplicitTileHeader(descriptor, reference, globalCoordinates, contentAvailable, children);
5213
+ return formatImplicitTileHeader(subtree, descriptor, reference, globalCoordinates, contentAvailability, children);
4442
5214
  }
4443
- function createLazyImplicitTileHeader(descriptor, coordinates) {
5215
+ function createLazyImplicitTileHeader(subtree, descriptor, coordinates) {
4444
5216
  const reference = createImplicitSubtreeReference(descriptor, coordinates);
4445
5217
  return {
4446
5218
  id: getImplicitTileId(reference, coordinates),
@@ -4451,23 +5223,56 @@ function createLazyImplicitTileHeader(descriptor, coordinates) {
4451
5223
  lodMetricType: descriptor.lodMetricType,
4452
5224
  lodMetricValue: descriptor.rootLodMetricValue / 2 ** coordinates.level,
4453
5225
  refine: descriptor.refine,
4454
- type: TILE_TYPE.EMPTY
5226
+ type: TILE_TYPE.EMPTY,
5227
+ implicitMetadata: getImplicitSubtreeMetadata(subtree)
4455
5228
  };
4456
5229
  }
4457
- function formatImplicitTileHeader(descriptor, reference, coordinates, contentAvailable, children) {
4458
- const contentUrl = contentAvailable && descriptor.contentUrlTemplate ? replaceImplicitUrlTemplate(descriptor.contentUrlTemplate, coordinates) : void 0;
5230
+ function formatImplicitTileHeader(subtree, descriptor, reference, coordinates, contentAvailability, children) {
5231
+ var _a;
5232
+ const contentUrlTemplates = ((_a = descriptor.contentUrlTemplates) == null ? void 0 : _a.length) ? descriptor.contentUrlTemplates : [descriptor.contentUrlTemplate];
5233
+ const contentEntries = contentUrlTemplates.map((templateUrl, contentIndex) => {
5234
+ if (!contentAvailability[contentIndex] || !templateUrl) {
5235
+ return null;
5236
+ }
5237
+ return {
5238
+ contentIndex,
5239
+ contentUrl: replaceImplicitUrlTemplate(templateUrl, coordinates)
5240
+ };
5241
+ }).filter((entry) => Boolean(entry));
5242
+ const availableContentUrls = contentEntries.map((entry) => entry.contentUrl);
5243
+ const content = contentEntries.map(({ contentIndex, contentUrl: contentUrl2 }) => {
5244
+ var _a2;
5245
+ return {
5246
+ ...((_a2 = descriptor.contentHeaders) == null ? void 0 : _a2[contentIndex]) || (contentIndex === 0 ? descriptor.contentHeader : {}) || {},
5247
+ uri: contentUrl2
5248
+ };
5249
+ });
5250
+ const contentUrl = availableContentUrls[0];
4459
5251
  const lodMetricValue = descriptor.rootLodMetricValue / 2 ** coordinates.level;
4460
5252
  return {
4461
5253
  id: getImplicitTileId(reference, coordinates),
4462
5254
  children,
4463
5255
  contentUrl,
4464
- content: contentUrl ? { ...descriptor.contentHeader, uri: contentUrl } : void 0,
5256
+ content: content.length > 1 ? content : content[0],
5257
+ contentUrls: availableContentUrls,
4465
5258
  refine: descriptor.refine,
4466
5259
  type: getImplicitTileType(contentUrl),
4467
5260
  lodMetricType: descriptor.lodMetricType,
4468
5261
  lodMetricValue,
4469
5262
  geometricError: lodMetricValue,
4470
- boundingVolume: calculateImplicitBoundingVolume(descriptor.rootBoundingVolume, coordinates, descriptor.subdivisionScheme)
5263
+ boundingVolume: calculateImplicitBoundingVolume(descriptor.rootBoundingVolume, coordinates, descriptor.subdivisionScheme),
5264
+ implicitMetadata: getImplicitSubtreeMetadata(subtree)
5265
+ };
5266
+ }
5267
+ function getImplicitSubtreeMetadata(subtree) {
5268
+ if (!subtree.propertyTables && subtree.tileMetadata === void 0 && !subtree.contentMetadata && subtree.subtreeMetadata === void 0) {
5269
+ return void 0;
5270
+ }
5271
+ return {
5272
+ propertyTables: subtree.propertyTables,
5273
+ tileMetadata: subtree.tileMetadata,
5274
+ contentMetadata: subtree.contentMetadata,
5275
+ subtreeMetadata: subtree.subtreeMetadata
4471
5276
  };
4472
5277
  }
4473
5278
  function getImplicitTileId(reference, coordinates) {
@@ -4485,8 +5290,9 @@ function getAvailabilityValue(availability, index) {
4485
5290
  const bitIndex = index % 8;
4486
5291
  return Boolean(bitstream[byteIndex] >> bitIndex & 1);
4487
5292
  }
4488
- function getPrimaryContentAvailability(availability) {
4489
- return Array.isArray(availability) ? availability[0] || { constant: 0 } : availability || { constant: 0 };
5293
+ function getContentAvailability(availability, index) {
5294
+ const streams = Array.isArray(availability) ? availability : [availability];
5295
+ return streams.map((stream) => getAvailabilityValue(stream || { constant: 0 }, index));
4490
5296
  }
4491
5297
  function getLevelOffset(childCount, localLevel) {
4492
5298
  return (childCount ** localLevel - 1) / (childCount - 1);
@@ -4628,12 +5434,8 @@ var Tiles3DSource = class {
4628
5434
  queryParams = {};
4629
5435
  /** Final request URLs cached by unmodified tile content URL. */
4630
5436
  tileUrlCache = /* @__PURE__ */ new Map();
4631
- /** Parsed subtree promises keyed by final source URL for request deduplication and LRU reuse. */
4632
- implicitSubtreeCache = /* @__PURE__ */ new Map();
4633
- /** URLs whose subtree resource requests have not settled. */
4634
- pendingImplicitSubtreeUrls = /* @__PURE__ */ new Set();
4635
- /** Maximum number of settled parsed subtrees retained by this source. */
4636
- maximumCachedSubtrees;
5437
+ /** Parsed subtree requests keyed by final source URL for deduplication and LRU reuse. */
5438
+ implicitSubtreeCache;
4637
5439
  /** Mutable counters exposed as a defensive snapshot through {@link getImplicitTilingStats}. */
4638
5440
  implicitTilingStats = {
4639
5441
  requestedSubtrees: 0,
@@ -4663,7 +5465,8 @@ var Tiles3DSource = class {
4663
5465
  this.coreApi = request.coreApi;
4664
5466
  this.loadOptions = loadOptions;
4665
5467
  const maximumCachedSubtrees = Number((_a = loadOptions["3d-tiles"]) == null ? void 0 : _a.maximumCachedSubtrees);
4666
- this.maximumCachedSubtrees = Number.isFinite(maximumCachedSubtrees) ? Math.max(0, Math.floor(maximumCachedSubtrees)) : DEFAULT_MAXIMUM_CACHED_SUBTREES;
5468
+ const maximumCacheEntries = Number.isFinite(maximumCachedSubtrees) ? Math.max(0, Math.floor(maximumCachedSubtrees)) : DEFAULT_MAXIMUM_CACHED_SUBTREES;
5469
+ this.implicitSubtreeCache = new RequestCache({ maxEntries: maximumCacheEntries });
4667
5470
  }
4668
5471
  /**
4669
5472
  * Releases URL and parsed-subtree caches and blocks late subtree installation.
@@ -4675,7 +5478,6 @@ var Tiles3DSource = class {
4675
5478
  this.destroyed = true;
4676
5479
  this.tileUrlCache.clear();
4677
5480
  this.implicitSubtreeCache.clear();
4678
- this.pendingImplicitSubtreeUrls.clear();
4679
5481
  }
4680
5482
  /**
4681
5483
  * Fetches and validates top-level metadata, then normalizes common fields used by {@link Tileset3D}.
@@ -4721,7 +5523,8 @@ var Tiles3DSource = class {
4721
5523
  tileset: this.rootTileset,
4722
5524
  lodMetricType: this.rootTileset.lodMetricType,
4723
5525
  lodMetricValue: this.rootTileset.lodMetricValue,
4724
- refine: (_a = this.rootTileset.root) == null ? void 0 : _a.refine
5526
+ refine: (_a = this.rootTileset.root) == null ? void 0 : _a.refine,
5527
+ spatialReference: get3DTilesSpatialReference(this.rootTileset)
4725
5528
  };
4726
5529
  }
4727
5530
  /**
@@ -4780,7 +5583,7 @@ var Tiles3DSource = class {
4780
5583
  * Loads binary content or nested tileset JSON for a runtime tile.
4781
5584
  */
4782
5585
  async loadTileContent(tile) {
4783
- const contentUrl = this.getTileUrl(tile.contentUrl);
5586
+ const contentUrls = (tile.contentUrls || [tile.contentUrl]).filter(Boolean);
4784
5587
  const tilesetLoaderOptions = this.loadOptions[this.loader.id] || {};
4785
5588
  const options = {
4786
5589
  ...this.loadOptions,
@@ -4792,11 +5595,15 @@ var Tiles3DSource = class {
4792
5595
  assetGltfUpAxis: this.asset && this.asset.gltfUpAxis || "Y"
4793
5596
  }
4794
5597
  };
4795
- const content = await this.loadResourceData(contentUrl, options);
4796
- tile.content = content;
5598
+ const contents = await Promise.all(contentUrls.map((contentUrl) => this.loadResourceData(this.getTileUrl(contentUrl), options)));
5599
+ tile.contents = contents;
5600
+ tile.content = contents[0] || null;
5601
+ const nestedTilesets = contents.filter((content) => (content == null ? void 0 : content.shape) === "tileset3d");
4797
5602
  return {
4798
5603
  loaded: true,
4799
- nestedTileset: (content == null ? void 0 : content.shape) === "tileset3d" ? content : void 0
5604
+ contents,
5605
+ nestedTileset: nestedTilesets[0],
5606
+ nestedTilesets
4800
5607
  };
4801
5608
  }
4802
5609
  /**
@@ -4843,8 +5650,8 @@ var Tiles3DSource = class {
4843
5650
  getImplicitTilingStats() {
4844
5651
  return {
4845
5652
  ...this.implicitTilingStats,
4846
- cachedSubtrees: this.implicitSubtreeCache.size - this.pendingImplicitSubtreeUrls.size,
4847
- pendingSubtrees: this.pendingImplicitSubtreeUrls.size
5653
+ cachedSubtrees: this.implicitSubtreeCache.size - this.implicitSubtreeCache.pendingSize,
5654
+ pendingSubtrees: this.implicitSubtreeCache.pendingSize
4848
5655
  };
4849
5656
  }
4850
5657
  /**
@@ -4939,8 +5746,11 @@ var Tiles3DSource = class {
4939
5746
  * Updates content-format flags and installs nested tileset subtrees.
4940
5747
  */
4941
5748
  onTileLoaded(tileset, tile, loadResult) {
4942
- var _a;
4943
- const { extensionsRemoved = [] } = ((_a = tile.content) == null ? void 0 : _a.gltf) || {};
5749
+ const contents = tile.contents.length ? tile.contents : [tile.content];
5750
+ const extensionsRemoved = contents.flatMap((content) => {
5751
+ var _a;
5752
+ return ((_a = content == null ? void 0 : content.gltf) == null ? void 0 : _a.extensionsRemoved) || [];
5753
+ });
4944
5754
  if (extensionsRemoved.includes("KHR_draco_mesh_compression")) {
4945
5755
  this.contentFormats.draco = true;
4946
5756
  }
@@ -4950,8 +5760,9 @@ var Tiles3DSource = class {
4950
5760
  if (extensionsRemoved.includes("KHR_texture_basisu")) {
4951
5761
  this.contentFormats.ktx2 = true;
4952
5762
  }
4953
- if (loadResult.nestedTileset) {
4954
- tileset._initializeTileHeaders(loadResult.nestedTileset, tile);
5763
+ const nestedTilesets = loadResult.nestedTilesets || (loadResult.nestedTileset ? [loadResult.nestedTileset] : []);
5764
+ for (const nestedTileset of nestedTilesets) {
5765
+ tileset._initializeTileHeaders(nestedTileset, tile);
4955
5766
  }
4956
5767
  }
4957
5768
  /**
@@ -4991,31 +5802,20 @@ var Tiles3DSource = class {
4991
5802
  const cachedSubtree = this.implicitSubtreeCache.get(subtreeUrl);
4992
5803
  if (cachedSubtree) {
4993
5804
  this.implicitTilingStats.cacheHits++;
4994
- this.implicitSubtreeCache.delete(subtreeUrl);
4995
- this.implicitSubtreeCache.set(subtreeUrl, cachedSubtree);
4996
5805
  return await cachedSubtree;
4997
5806
  }
4998
5807
  this.implicitTilingStats.requestedSubtrees++;
4999
- this.pendingImplicitSubtreeUrls.add(subtreeUrl);
5000
5808
  const loaderOptions = this.loadOptions[this.loader.id] || {};
5001
- const subtreePromise = this.loadResourceData(subtreeUrl, {
5002
- ...this.loadOptions,
5003
- [this.loader.id]: {
5004
- ...loaderOptions,
5005
- isTileset: false,
5006
- isSubtree: true
5007
- }
5809
+ return await this.implicitSubtreeCache.getOrLoad(subtreeUrl, async () => {
5810
+ return await this.loadResourceData(subtreeUrl, {
5811
+ ...this.loadOptions,
5812
+ [this.loader.id]: {
5813
+ ...loaderOptions,
5814
+ isTileset: false,
5815
+ isSubtree: true
5816
+ }
5817
+ });
5008
5818
  });
5009
- this.implicitSubtreeCache.set(subtreeUrl, subtreePromise);
5010
- try {
5011
- return await subtreePromise;
5012
- } catch (error) {
5013
- this.implicitSubtreeCache.delete(subtreeUrl);
5014
- throw error;
5015
- } finally {
5016
- this.pendingImplicitSubtreeUrls.delete(subtreeUrl);
5017
- this.trimImplicitSubtreeCache();
5018
- }
5019
5819
  }
5020
5820
  /**
5021
5821
  * Instantiates every header represented by one materialized subtree.
@@ -5046,20 +5846,6 @@ var Tiles3DSource = class {
5046
5846
  }
5047
5847
  return materializedTileCount;
5048
5848
  }
5049
- /** Evicts least-recently-used settled subtree entries until the configured bound is met. */
5050
- trimImplicitSubtreeCache() {
5051
- if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
5052
- return;
5053
- }
5054
- for (const subtreeUrl of this.implicitSubtreeCache.keys()) {
5055
- if (!this.pendingImplicitSubtreeUrls.has(subtreeUrl)) {
5056
- this.implicitSubtreeCache.delete(subtreeUrl);
5057
- }
5058
- if (this.implicitSubtreeCache.size <= this.maximumCachedSubtrees) {
5059
- break;
5060
- }
5061
- }
5062
- }
5063
5849
  };
5064
5850
  function isTilesetRequest(input) {
5065
5851
  return Boolean(input && typeof input === "object" && "url" in input && "loader" in input && !("type" in input));
@@ -5081,8 +5867,8 @@ function normalizeTiles3DRequest(input) {
5081
5867
  }
5082
5868
 
5083
5869
  // dist/tileset-3d/format-i3s/i3s-source.js
5084
- var import_geospatial8 = require("@math.gl/geospatial");
5085
- var import_core11 = require("@math.gl/core");
5870
+ var import_geospatial10 = require("@math.gl/geospatial");
5871
+ var import_core13 = require("@math.gl/core");
5086
5872
 
5087
5873
  // dist/tileset-3d/format-i3s/i3s-pending-tiles-register.js
5088
5874
  var I3SPendingTilesRegister = class {
@@ -5262,6 +6048,836 @@ var I3STilesetTraverser = class extends TilesetTraverser {
5262
6048
  }
5263
6049
  };
5264
6050
 
6051
+ // dist/spatial/i3s-spatial-transformer.js
6052
+ var import_core12 = require("@math.gl/core");
6053
+ var import_geospatial9 = require("@math.gl/geospatial");
6054
+
6055
+ // dist/spatial/spatial-coordinate-transformer.js
6056
+ var import_core11 = require("@math.gl/core");
6057
+ var import_geospatial8 = require("@math.gl/geospatial");
6058
+ var import_proj42 = require("@math.gl/proj4");
6059
+
6060
+ // dist/spatial/spatial-resource-registry.js
6061
+ var import_geoid = require("@math.gl/geoid");
6062
+ var import_proj4 = require("@math.gl/proj4");
6063
+ var geoidModels = /* @__PURE__ */ new Map();
6064
+ function registerSpatialCrs(name, definition) {
6065
+ validateResourceName(name, "CRS");
6066
+ import_proj4.Proj4Projection.defineProjectionAliases({ [name]: definition });
6067
+ }
6068
+ function registerSpatialDatumGrid(name, data) {
6069
+ validateResourceName(name, "datum grid");
6070
+ import_proj4.Proj4Projection.registerDatumGrid(name, data);
6071
+ }
6072
+ function registerGeoidModel(name, geoid) {
6073
+ validateResourceName(name, "geoid model");
6074
+ geoidModels.set(name, geoid);
6075
+ }
6076
+ function registerGeoidModelFromPgm(name, data, options = {}) {
6077
+ const geoid = (0, import_geoid.parsePGM)(data, options);
6078
+ registerGeoidModel(name, geoid);
6079
+ return geoid;
6080
+ }
6081
+ function getGeoidModel(name) {
6082
+ return geoidModels.get(name);
6083
+ }
6084
+ function validateResourceName(name, resourceType) {
6085
+ if (!name.trim()) {
6086
+ throw new Error(`${resourceType} name must not be empty`);
6087
+ }
6088
+ }
6089
+
6090
+ // dist/spatial/spatial-coordinate-transformer.js
6091
+ var GEOGRAPHIC_CRS2 = "EPSG:4326";
6092
+ var GEOCENTRIC_CRS2 = "EPSG:4978";
6093
+ var SpatialCoordinateTransformer = class {
6094
+ /** Normalized spatial metadata describing this transformation. */
6095
+ spatialReference;
6096
+ /** Projection from the source CRS directly to the requested horizontal target CRS. */
6097
+ horizontalProjection;
6098
+ /** Projection from the source CRS to geographic longitude, latitude, and ellipsoidal height. */
6099
+ geographicProjection;
6100
+ /** Projection from adjusted geographic coordinates to the requested output CRS. */
6101
+ heightOutputProjection;
6102
+ /** Whether source coordinates use the WGS84 geocentric frame handled by math.gl. */
6103
+ sourceIsGeocentric;
6104
+ /** Whether output coordinates use the WGS84 geocentric frame handled by math.gl. */
6105
+ outputIsGeocentric;
6106
+ /** Whether source coordinates already use conventional WGS84 longitude/latitude order. */
6107
+ sourceIsGeographic;
6108
+ /** Whether output coordinates use conventional WGS84 longitude/latitude order. */
6109
+ outputIsGeographic;
6110
+ /** Geoid model used to convert between ellipsoidal and orthometric heights. */
6111
+ geoid;
6112
+ /**
6113
+ * Creates a coordinate transformer.
6114
+ *
6115
+ * @param spatialReference - Format-discovered and option-normalized spatial metadata.
6116
+ * @param options - Application options containing an optional geoid model.
6117
+ */
6118
+ constructor(spatialReference, options = {}) {
6119
+ this.spatialReference = spatialReference;
6120
+ validateTransformRequest(spatialReference);
6121
+ const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
6122
+ this.sourceIsGeocentric = isWgs84Geocentric(spatialReference.sourceCrs);
6123
+ this.outputIsGeocentric = isWgs84Geocentric(outputCrs);
6124
+ this.sourceIsGeographic = isWgs84Geographic(spatialReference.sourceCrs);
6125
+ this.outputIsGeographic = isWgs84Geographic(outputCrs);
6126
+ if (spatialReference.sourceCrs && spatialReference.targetCrs && !this.sourceIsGeocentric && !this.outputIsGeocentric) {
6127
+ this.horizontalProjection = new import_proj42.Proj4Projection({
6128
+ from: getHorizontalProj4Definition(spatialReference.sourceCrs),
6129
+ to: getHorizontalProj4Definition(spatialReference.targetCrs),
6130
+ enforceAxis: false
6131
+ });
6132
+ }
6133
+ if (requiresHeightTransformation(spatialReference) || this.sourceIsGeocentric || this.outputIsGeocentric) {
6134
+ if (!this.sourceIsGeographic && !this.sourceIsGeocentric) {
6135
+ this.geographicProjection = new import_proj42.Proj4Projection({
6136
+ from: getHorizontalProj4Definition(spatialReference.sourceCrs),
6137
+ to: GEOGRAPHIC_CRS2,
6138
+ enforceAxis: false
6139
+ });
6140
+ }
6141
+ if (!this.outputIsGeographic && !this.outputIsGeocentric) {
6142
+ this.heightOutputProjection = new import_proj42.Proj4Projection({
6143
+ from: GEOGRAPHIC_CRS2,
6144
+ to: getHorizontalProj4Definition(outputCrs),
6145
+ enforceAxis: false
6146
+ });
6147
+ }
6148
+ }
6149
+ if (requiresHeightTransformation(spatialReference)) {
6150
+ this.geoid = resolveGeoid(options.geoidModel);
6151
+ if (!this.geoid) {
6152
+ const modelName = typeof options.geoidModel === "string" ? ` "${options.geoidModel}"` : "";
6153
+ throw new Error(`Height conversion requires a registered geoid model${modelName}; call registerGeoidModel() or pass a parsed Geoid instance`);
6154
+ }
6155
+ }
6156
+ }
6157
+ /**
6158
+ * Transforms one `[x, y, z?]` coordinate while retaining additional components.
6159
+ *
6160
+ * @param coordinate - Source coordinate in the format adapter's normalized `x/y/z` order.
6161
+ * @returns A new transformed coordinate array.
6162
+ */
6163
+ transformPosition(coordinate) {
6164
+ if (coordinate.length < 2) {
6165
+ throw new Error("A spatial coordinate must contain at least x and y components");
6166
+ }
6167
+ const result = [...coordinate];
6168
+ if (requiresHeightTransformation(this.spatialReference) || this.sourceIsGeocentric || this.outputIsGeocentric) {
6169
+ if (result.length < 3 || !Number.isFinite(result[2])) {
6170
+ throw new Error("Three-dimensional CRS conversion requires a finite z coordinate");
6171
+ }
6172
+ const geographic = this.toGeographic(result);
6173
+ if (requiresHeightTransformation(this.spatialReference)) {
6174
+ const geoidUndulation = this.geoid.getHeight(geographic[1], geographic[0]);
6175
+ geographic[2] = transformHeight(geographic[2], geoidUndulation, this.spatialReference.heightReference, this.spatialReference.targetHeightReference);
6176
+ }
6177
+ const projected = this.fromGeographic(geographic);
6178
+ result.splice(0, projected.length, ...projected);
6179
+ return result;
6180
+ }
6181
+ if (this.horizontalProjection) {
6182
+ const projected = this.horizontalProjection.project(result.slice(0, 3));
6183
+ result.splice(0, projected.length, ...projected);
6184
+ }
6185
+ return result;
6186
+ }
6187
+ /** Convert one source coordinate to conventional WGS84 longitude, latitude, and height. */
6188
+ toGeographic(coordinate) {
6189
+ if (this.sourceIsGeocentric) {
6190
+ return Array.from(import_geospatial8.Ellipsoid.WGS84.cartesianToCartographic(new import_core11.Vector3(coordinate)));
6191
+ }
6192
+ if (this.sourceIsGeographic) {
6193
+ return coordinate.slice(0, 3);
6194
+ }
6195
+ return this.geographicProjection.project(coordinate.slice(0, 3));
6196
+ }
6197
+ /** Convert one conventional WGS84 geographic coordinate to the selected output CRS. */
6198
+ fromGeographic(coordinate) {
6199
+ if (this.outputIsGeocentric) {
6200
+ return Array.from(import_geospatial8.Ellipsoid.WGS84.cartographicToCartesian(new import_core11.Vector3(coordinate)));
6201
+ }
6202
+ if (this.outputIsGeographic) {
6203
+ return coordinate.slice(0, 3);
6204
+ }
6205
+ return this.heightOutputProjection.project(coordinate.slice(0, 3));
6206
+ }
6207
+ };
6208
+ function getHorizontalProj4Definition(definition) {
6209
+ if (!definition) {
6210
+ throw new Error("Cannot construct a projection because the CRS is unknown");
6211
+ }
6212
+ return (0, import_proj42.toProj4CRSDefinition)(definition, { mode: "horizontal" });
6213
+ }
6214
+ function validateTransformRequest(spatialReference) {
6215
+ if (spatialReference.outputCoordinates === "local-enu") {
6216
+ throw new Error("local-enu output requires a dataset-derived local origin and must be created by a tileset source");
6217
+ }
6218
+ if (spatialReference.outputCoordinates === "target-crs" && !spatialReference.targetCrs) {
6219
+ throw new Error("target-crs output requires targetCrs");
6220
+ }
6221
+ if (!spatialReference.sourceCrs && spatialReference.status === "unresolved") {
6222
+ throw new Error("Cannot transform coordinates because the source CRS is unknown");
6223
+ }
6224
+ if (requiresHeightTransformation(spatialReference) && spatialReference.heightReference === "unknown") {
6225
+ throw new Error("Cannot convert heights because the source height reference is unknown");
6226
+ }
6227
+ if (spatialReference.coordinateFrame === "geocentric" && !isWgs84Geocentric(spatialReference.sourceCrs)) {
6228
+ throw new Error("Only EPSG:4978 geocentric coordinates are supported by the current spatial transformer");
6229
+ }
6230
+ if (spatialReference.status === "unresolved") {
6231
+ throw new Error("The requested spatial output cannot be resolved from the available metadata");
6232
+ }
6233
+ }
6234
+ function isWgs84Geocentric(definition) {
6235
+ return typeof definition === "string" && normalizeCrsIdentifier2(definition) === GEOCENTRIC_CRS2;
6236
+ }
6237
+ function isWgs84Geographic(definition) {
6238
+ if (typeof definition !== "string") {
6239
+ return false;
6240
+ }
6241
+ const identifier = normalizeCrsIdentifier2(definition);
6242
+ return identifier === GEOGRAPHIC_CRS2 || identifier === "EPSG:4979" || identifier === "OGC:CRS84";
6243
+ }
6244
+ function normalizeCrsIdentifier2(identifier) {
6245
+ const normalizedIdentifier = identifier.trim().toUpperCase();
6246
+ const ogcMatch = normalizedIdentifier.match(/(?:\/DEF\/CRS\/|URN:OGC:DEF:CRS:)([A-Z0-9_-]+)(?:\/|::)(?:[^/:]*[/:])?([A-Z0-9_.-]+)$/);
6247
+ return ogcMatch ? `${ogcMatch[1]}:${ogcMatch[2]}` : normalizedIdentifier;
6248
+ }
6249
+ function requiresHeightTransformation(spatialReference) {
6250
+ const target = spatialReference.targetHeightReference;
6251
+ return target !== "native" && target !== spatialReference.heightReference;
6252
+ }
6253
+ function resolveGeoid(model) {
6254
+ return typeof model === "string" ? getGeoidModel(model) : model;
6255
+ }
6256
+ function transformHeight(height, geoidUndulation, source, target) {
6257
+ if (source === target || target === "native") {
6258
+ return height;
6259
+ }
6260
+ if (source === "orthometric" && target === "ellipsoidal") {
6261
+ return height + geoidUndulation;
6262
+ }
6263
+ if (source === "ellipsoidal" && target === "orthometric") {
6264
+ return height - geoidUndulation;
6265
+ }
6266
+ throw new Error(`Unsupported height conversion from ${source} to ${target}`);
6267
+ }
6268
+
6269
+ // dist/spatial/i3s-spatial-transformer.js
6270
+ var I3SSpatialTransformer = class {
6271
+ /** Descriptor attached to output after all adapter-owned spatial values have been transformed. */
6272
+ spatialReference;
6273
+ /** Broad coordinate frame of the selected output CRS. */
6274
+ targetCoordinateFrame;
6275
+ /** Source-to-target position operation. */
6276
+ positionTransformer;
6277
+ /** Source-to-WGS84 geographic operation used for vector-frame conversion. */
6278
+ sourceToGeographicTransformer;
6279
+ /** WGS84-geographic-to-target operation used for normal finite differences. */
6280
+ geographicToTargetTransformer;
6281
+ /** Source frame recorded by I3S metadata. */
6282
+ sourceCoordinateFrame;
6283
+ /** Source Z multiplier that normalizes stored elevation values to meters. */
6284
+ sourceVerticalUnitScale;
6285
+ /** I3S elevation placement mode. */
6286
+ elevationMode;
6287
+ /** I3S elevation offset normalized to meters. */
6288
+ elevationOffset;
6289
+ /** Terrain or scene provider selected for the declared placement mode. */
6290
+ elevationProvider;
6291
+ /** Provider height multiplier that normalizes returned values to meters. */
6292
+ providerUnitScale;
6293
+ /** Optional provider-height to layer-height datum operation. */
6294
+ providerHeightTransformer;
6295
+ /**
6296
+ * Creates an I3S spatial adapter.
6297
+ *
6298
+ * @param spatialReference - Transformable I3S spatial descriptor.
6299
+ * @param options - Registered geoid and custom resource options used by the shared transformer.
6300
+ */
6301
+ constructor(spatialReference, options = {}) {
6302
+ var _a;
6303
+ if (spatialReference.status !== "transformable" && spatialReference.status !== "transformed") {
6304
+ throw new Error("I3SSpatialTransformer requires a requested, transformable spatial output");
6305
+ }
6306
+ const targetCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
6307
+ if (!targetCrs) {
6308
+ throw new Error("I3S spatial transformation requires a target CRS");
6309
+ }
6310
+ this.positionTransformer = new SpatialCoordinateTransformer(spatialReference, options);
6311
+ this.sourceCoordinateFrame = spatialReference.coordinateFrame;
6312
+ this.sourceVerticalUnitScale = spatialReference.verticalUnitScale;
6313
+ this.elevationMode = spatialReference.elevationMode;
6314
+ this.elevationOffset = (spatialReference.elevationOffset || 0) * spatialReference.elevationUnitScale;
6315
+ this.elevationProvider = getElevationProvider(this.elevationMode, options);
6316
+ if (this.elevationProvider && (typeof this.elevationProvider.sampleElevations !== "function" || typeof this.elevationProvider.getElevationRange !== "function")) {
6317
+ throw new Error("An I3S elevation provider must implement sampleElevations() and getElevationRange()");
6318
+ }
6319
+ const providerUnitScale = getI3SLinearUnitScale(((_a = this.elevationProvider) == null ? void 0 : _a.unit) || "meter");
6320
+ if (this.elevationProvider && providerUnitScale === void 0) {
6321
+ throw new Error(`Unsupported elevation-provider unit ${this.elevationProvider.unit}`);
6322
+ }
6323
+ this.providerUnitScale = providerUnitScale || 1;
6324
+ this.targetCoordinateFrame = getSpatialCoordinateFrame(targetCrs);
6325
+ this.spatialReference = markTilesetSpatialReferenceTransformed(spatialReference);
6326
+ const sourceToGeographicReference = createTilesetSpatialReference({
6327
+ sourceCrs: spatialReference.sourceCrs,
6328
+ heightReference: spatialReference.heightReference,
6329
+ coordinateFrame: spatialReference.coordinateFrame,
6330
+ axisOrder: spatialReference.axisOrder,
6331
+ provenance: spatialReference.provenance
6332
+ }, { targetCrs: "EPSG:4326" });
6333
+ this.sourceToGeographicTransformer = new SpatialCoordinateTransformer(sourceToGeographicReference);
6334
+ if (requiresElevationSurface(this.elevationMode)) {
6335
+ if (!this.elevationProvider) {
6336
+ const providerName = this.elevationMode === "relativeToScene" ? "scene" : "terrain";
6337
+ throw new Error(`I3S elevation mode ${this.elevationMode} requires a ${providerName} elevation provider`);
6338
+ }
6339
+ if (spatialReference.heightReference === "unknown") {
6340
+ throw new Error(`I3S elevation mode ${this.elevationMode} requires a known source height reference`);
6341
+ }
6342
+ const providerHeightReference = this.elevationProvider.heightReference || spatialReference.heightReference;
6343
+ if (providerHeightReference !== spatialReference.heightReference) {
6344
+ if (spatialReference.heightReference !== "ellipsoidal" && spatialReference.heightReference !== "orthometric") {
6345
+ throw new Error("Converting elevation-provider heights requires an ellipsoidal or orthometric source height reference");
6346
+ }
6347
+ const providerHeightSpatialReference = createTilesetSpatialReference({
6348
+ sourceCrs: "EPSG:4326",
6349
+ heightReference: providerHeightReference,
6350
+ coordinateFrame: "geographic",
6351
+ axisOrder: "xyz",
6352
+ provenance: "caller-override"
6353
+ }, {
6354
+ targetCrs: "EPSG:4326",
6355
+ targetHeightReference: spatialReference.heightReference,
6356
+ geoidModel: options.geoidModel
6357
+ });
6358
+ this.providerHeightTransformer = new SpatialCoordinateTransformer(providerHeightSpatialReference, options);
6359
+ }
6360
+ }
6361
+ const geographicToTargetReference = createTilesetSpatialReference({
6362
+ sourceCrs: "EPSG:4326",
6363
+ heightReference: spatialReference.heightReference,
6364
+ coordinateFrame: "geographic",
6365
+ axisOrder: "xyz",
6366
+ provenance: "format-default"
6367
+ }, {
6368
+ targetCrs,
6369
+ targetHeightReference: spatialReference.targetHeightReference,
6370
+ geoidModel: options.geoidModel
6371
+ });
6372
+ this.geographicToTargetTransformer = new SpatialCoordinateTransformer(geographicToTargetReference, options);
6373
+ }
6374
+ /** Transform one absolute I3S source position. */
6375
+ transformPosition(position) {
6376
+ const placedPosition = this.placeSourcePositionsSync(position);
6377
+ const transformed = this.positionTransformer.transformPosition([
6378
+ placedPosition[0],
6379
+ placedPosition[1],
6380
+ placedPosition[2]
6381
+ ]);
6382
+ return [transformed[0], transformed[1], transformed[2]];
6383
+ }
6384
+ /** Transform one absolute I3S source position after asynchronous surface placement. */
6385
+ async transformPositionAsync(position) {
6386
+ const placedPosition = await this.placeSourcePositions(position);
6387
+ const transformed = this.positionTransformer.transformPosition([
6388
+ placedPosition[0],
6389
+ placedPosition[1],
6390
+ placedPosition[2]
6391
+ ]);
6392
+ return [transformed[0], transformed[1], transformed[2]];
6393
+ }
6394
+ /** Transform an already placed, meter-normalized position to the requested target. */
6395
+ transformPlacedPosition(position) {
6396
+ const transformed = this.positionTransformer.transformPosition([
6397
+ position[0],
6398
+ position[1],
6399
+ position[2]
6400
+ ]);
6401
+ return [transformed[0], transformed[1], transformed[2]];
6402
+ }
6403
+ /** Transform one absolute I3S source position to WGS84 longitude, latitude, and height. */
6404
+ transformSourcePositionToGeographic(position) {
6405
+ const placedPosition = this.placeSourcePositionsSync(position);
6406
+ const transformed = this.sourceToGeographicTransformer.transformPosition([
6407
+ placedPosition[0],
6408
+ placedPosition[1],
6409
+ placedPosition[2]
6410
+ ]);
6411
+ return [transformed[0], transformed[1], transformed[2]];
6412
+ }
6413
+ /** Transform one source position to WGS84 after asynchronous elevation placement. */
6414
+ async transformSourcePositionToGeographicAsync(position) {
6415
+ const placedPosition = await this.placeSourcePositions(position);
6416
+ const transformed = this.sourceToGeographicTransformer.transformPosition([
6417
+ placedPosition[0],
6418
+ placedPosition[1],
6419
+ placedPosition[2]
6420
+ ]);
6421
+ return [transformed[0], transformed[1], transformed[2]];
6422
+ }
6423
+ /**
6424
+ * Transform absolute Float64 positions and return stable Float32 offsets around a target origin.
6425
+ *
6426
+ * @param sourcePositions - Absolute positions in source CRS order.
6427
+ * @param sourceOrigin - Stable source origin, normally the node MBS or OBB center.
6428
+ */
6429
+ transformPositions(sourcePositions, sourceOrigin) {
6430
+ const placedSourcePositions = this.placeSourcePositionsSync(sourcePositions);
6431
+ const placedSourceOrigin = this.placeSourcePositionsSync(sourceOrigin);
6432
+ return this.transformPlacedPositions(placedSourcePositions, placedSourceOrigin);
6433
+ }
6434
+ /**
6435
+ * Transform positions after asynchronously sampling any requested terrain or scene surface.
6436
+ *
6437
+ * @param sourcePositions - Absolute positions in source CRS order.
6438
+ * @param sourceOrigin - Stable source origin, normally the node MBS or OBB center.
6439
+ * @returns Renderer-ready target-frame offsets and stable origin metadata.
6440
+ */
6441
+ async transformPositionsAsync(sourcePositions, sourceOrigin) {
6442
+ const combinedPositions = new Float64Array(sourcePositions.length + 3);
6443
+ combinedPositions.set(Array.from(sourcePositions), 0);
6444
+ combinedPositions.set(Array.from(sourceOrigin).slice(0, 3), sourcePositions.length);
6445
+ const placedPositions = await this.placeSourcePositions(combinedPositions);
6446
+ return this.transformPlacedPositions(placedPositions.subarray(0, sourcePositions.length), placedPositions.subarray(sourcePositions.length));
6447
+ }
6448
+ /** Transform already placed positions into renderer-relative target coordinates. */
6449
+ transformPlacedPositions(sourcePositions, sourceOrigin) {
6450
+ const origin = this.transformPlacedPosition(sourceOrigin);
6451
+ const positions = new Float32Array(sourcePositions.length);
6452
+ const absoluteSourcePositions = Float64Array.from(sourcePositions);
6453
+ for (let index = 0; index < sourcePositions.length; index += 3) {
6454
+ const transformed = this.transformPlacedPosition([
6455
+ sourcePositions[index],
6456
+ sourcePositions[index + 1],
6457
+ sourcePositions[index + 2]
6458
+ ]);
6459
+ positions[index] = transformed[0] - origin[0];
6460
+ positions[index + 1] = transformed[1] - origin[1];
6461
+ positions[index + 2] = transformed[2] - origin[2];
6462
+ }
6463
+ const isGeographic = this.targetCoordinateFrame === "geographic";
6464
+ const modelMatrix = isGeographic ? new import_core12.Matrix4() : new import_core12.Matrix4().translate(origin);
6465
+ return {
6466
+ positions,
6467
+ sourcePositions: absoluteSourcePositions,
6468
+ origin,
6469
+ cartographicOrigin: isGeographic ? origin : [0, 0, 0],
6470
+ coordinateSystem: isGeographic ? "lnglat-offsets" : "cartesian",
6471
+ modelMatrix
6472
+ };
6473
+ }
6474
+ /**
6475
+ * Transform I3S normals into the selected target basis.
6476
+ *
6477
+ * Earth-centered normals are rotated directly. Vertex-reference-frame normals are first mapped
6478
+ * through the local east/north/up basis at each source position.
6479
+ *
6480
+ * @param normals - Three-component normal vectors.
6481
+ * @param sourcePositions - Absolute source position paired with each normal.
6482
+ * @param normalReferenceFrame - I3S normal frame declaration.
6483
+ */
6484
+ transformNormals(normals, sourcePositions, normalReferenceFrame = "earth-centered") {
6485
+ const transformedNormals = new Float32Array(normals.length);
6486
+ for (let index = 0; index < normals.length; index += 3) {
6487
+ const geographic = this.sourceToGeographicTransformer.transformPosition([
6488
+ sourcePositions[index],
6489
+ sourcePositions[index + 1],
6490
+ sourcePositions[index + 2]
6491
+ ]);
6492
+ const inputNormal = new import_core12.Vector3([
6493
+ normals[index],
6494
+ normals[index + 1],
6495
+ normals[index + 2]
6496
+ ]).normalize();
6497
+ const ecefNormal = normalReferenceFrame === "earth-centered" ? inputNormal : localEnuVectorToEcef(inputNormal, geographic);
6498
+ const outputNormal = this.transformEcefNormal(ecefNormal, geographic);
6499
+ transformedNormals.set(outputNormal, index);
6500
+ }
6501
+ return transformedNormals;
6502
+ }
6503
+ /**
6504
+ * Rebuild an I3S node bound from transformed corner or sphere-axis samples.
6505
+ *
6506
+ * @param bounds - Source MBS/OBB fields.
6507
+ * @returns A generic tile `region` for geographic output or axis-aligned `box` otherwise.
6508
+ */
6509
+ transformBoundingVolume(bounds) {
6510
+ const geographicSamples = this.getGeographicBoundSamples(bounds);
6511
+ const targetSamples = geographicSamples.map((position) => this.geographicToTargetTransformer.transformPosition(position));
6512
+ return this.targetCoordinateFrame === "geographic" ? { region: getGeographicRegion(targetSamples) } : { box: getAxisAlignedBox(targetSamples) };
6513
+ }
6514
+ /** Rebuild an I3S bound in the target frame after asynchronous elevation placement. */
6515
+ async transformBoundingVolumeAsync(bounds) {
6516
+ const geographicSamples = await this.getGeographicBoundSamplesAsync(bounds);
6517
+ const targetSamples = geographicSamples.map((position) => this.geographicToTargetTransformer.transformPosition(position));
6518
+ return this.targetCoordinateFrame === "geographic" ? { region: getGeographicRegion(targetSamples) } : { box: getAxisAlignedBox(targetSamples) };
6519
+ }
6520
+ /** Rebuild an I3S bound as a WGS84 geographic region for geographic traversal algorithms. */
6521
+ transformBoundingVolumeToGeographic(bounds) {
6522
+ return { region: getGeographicRegion(this.getGeographicBoundSamples(bounds)) };
6523
+ }
6524
+ /** Rebuild an I3S bound as WGS84 geographic after asynchronous elevation placement. */
6525
+ async transformBoundingVolumeToGeographicAsync(bounds) {
6526
+ return { region: getGeographicRegion(await this.getGeographicBoundSamplesAsync(bounds)) };
6527
+ }
6528
+ /** Rebuild an I3S bound as a WGS84 ECEF box for generic `Tileset3D` traversal. */
6529
+ transformBoundingVolumeToGeocentric(bounds) {
6530
+ const cartesianSamples = this.getGeographicBoundSamples(bounds).map((position) => Array.from(import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(position))));
6531
+ return { box: getAxisAlignedBox(cartesianSamples) };
6532
+ }
6533
+ /** Rebuild an I3S bound as WGS84 ECEF after asynchronous elevation placement. */
6534
+ async transformBoundingVolumeToGeocentricAsync(bounds) {
6535
+ const cartesianSamples = (await this.getGeographicBoundSamplesAsync(bounds)).map((position) => Array.from(import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(position))));
6536
+ return { box: getAxisAlignedBox(cartesianSamples) };
6537
+ }
6538
+ /** Return a WGS84 geographic MBS used by the I3S screen-threshold calculation. */
6539
+ transformBoundingSphereToGeographic(bounds) {
6540
+ var _a, _b;
6541
+ const sourceCenter = ((_a = bounds.mbs) == null ? void 0 : _a.length) ? Array.from(bounds.mbs).slice(0, 3) : bounds.obb ? Array.from(bounds.obb.center).slice(0, 3) : null;
6542
+ if (!sourceCenter) {
6543
+ throw new Error("I3S spatial transformation requires an MBS or OBB bound");
6544
+ }
6545
+ const placedCenter = this.placeSourcePositionsSync(sourceCenter);
6546
+ const geographicCenter = this.sourceToGeographicTransformer.transformPosition(Array.from(placedCenter));
6547
+ const radius = ((_b = bounds.mbs) == null ? void 0 : _b.length) ? bounds.mbs[3] : Math.hypot(...Array.from(bounds.obb.halfSize).slice(0, 3));
6548
+ return [geographicCenter[0], geographicCenter[1], geographicCenter[2], radius];
6549
+ }
6550
+ /** Return a placed WGS84 MBS used by the I3S screen-threshold calculation. */
6551
+ async transformBoundingSphereToGeographicAsync(bounds) {
6552
+ return getGeographicBoundingSphere(await this.getGeographicBoundSamplesAsync(bounds));
6553
+ }
6554
+ /**
6555
+ * Rebuild traversal, target-output, and I3S LOD bounds from one elevation-sampling batch.
6556
+ *
6557
+ * @param bounds - Source MBS/OBB fields.
6558
+ * @returns Parallel bounds in WGS84 ECEF, the selected output frame, and WGS84 geographic.
6559
+ */
6560
+ async transformBoundsAsync(bounds) {
6561
+ const geographicSamples = await this.getGeographicBoundSamplesAsync(bounds);
6562
+ const cartesianSamples = geographicSamples.map((position) => Array.from(import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(position))));
6563
+ const targetSamples = geographicSamples.map((position) => this.geographicToTargetTransformer.transformPosition(position));
6564
+ return {
6565
+ boundingVolume: { box: getAxisAlignedBox(cartesianSamples) },
6566
+ spatialBoundingVolume: this.targetCoordinateFrame === "geographic" ? { region: getGeographicRegion(targetSamples) } : { box: getAxisAlignedBox(targetSamples) },
6567
+ i3sLodMbs: requiresElevationSurface(this.elevationMode) ? getGeographicBoundingSphere(geographicSamples) : this.transformBoundingSphereToGeographic(bounds)
6568
+ };
6569
+ }
6570
+ /**
6571
+ * Rebuild Point Cloud traversal and target-output bounds from one elevation-sampling batch.
6572
+ *
6573
+ * @param bounds - Source OBB fields.
6574
+ * @returns WGS84 geographic traversal bound plus the selected output-frame bound.
6575
+ */
6576
+ async transformPointCloudBoundsAsync(bounds) {
6577
+ const geographicSamples = await this.getGeographicBoundSamplesAsync(bounds);
6578
+ const targetSamples = geographicSamples.map((position) => this.geographicToTargetTransformer.transformPosition(position));
6579
+ return {
6580
+ boundingVolume: { region: getGeographicRegion(geographicSamples) },
6581
+ spatialBoundingVolume: this.targetCoordinateFrame === "geographic" ? { region: getGeographicRegion(targetSamples) } : { box: getAxisAlignedBox(targetSamples) }
6582
+ };
6583
+ }
6584
+ /** Sample a source I3S bound and normalize every sample to WGS84 geographic coordinates. */
6585
+ getGeographicBoundSamples(bounds) {
6586
+ const sourceSamples = getSourceBoundSamples(bounds, this.sourceCoordinateFrame, this.sourceToGeographicTransformer, this.sourceVerticalUnitScale);
6587
+ if (!sourceSamples.length) {
6588
+ throw new Error("I3S spatial transformation requires an MBS or OBB bound");
6589
+ }
6590
+ const placedSourceSamples = this.placeSourcePositionsSync(sourceSamples.flat(), true);
6591
+ const placedSamples = toPositionArrays(placedSourceSamples);
6592
+ return this.sourceCoordinateFrame === "geographic" ? placedSamples : placedSamples.map((position) => this.sourceToGeographicTransformer.transformPosition(position));
6593
+ }
6594
+ /** Sample, place, and normalize one source bound to WGS84 geographic coordinates. */
6595
+ async getGeographicBoundSamplesAsync(bounds) {
6596
+ const sourceSamples = getSourceBoundSamples(bounds, this.sourceCoordinateFrame, this.sourceToGeographicTransformer, this.sourceVerticalUnitScale);
6597
+ if (!sourceSamples.length) {
6598
+ throw new Error("I3S spatial transformation requires an MBS or OBB bound");
6599
+ }
6600
+ if (requiresElevationSurface(this.elevationMode)) {
6601
+ const geographicSamples = this.sourceCoordinateFrame === "geographic" ? sourceSamples : sourceSamples.map((position) => this.sourceToGeographicTransformer.transformPosition(position));
6602
+ return await this.placeGeographicBoundSamples(geographicSamples);
6603
+ }
6604
+ const placedSamples = toPositionArrays(await this.placeSourcePositions(sourceSamples.flat(), true));
6605
+ return this.sourceCoordinateFrame === "geographic" ? placedSamples : placedSamples.map((position) => this.sourceToGeographicTransformer.transformPosition(position));
6606
+ }
6607
+ /** Apply a provider's conservative surface range to an entire geographic bound footprint. */
6608
+ async placeGeographicBoundSamples(geographicSamples) {
6609
+ const elevationProvider = this.elevationProvider;
6610
+ const elevationRange = await elevationProvider.getElevationRange(getElevationBounds(geographicSamples));
6611
+ const minimumProviderHeight = elevationRange.minimum * this.providerUnitScale;
6612
+ const maximumProviderHeight = elevationRange.maximum * this.providerUnitScale;
6613
+ if (!Number.isFinite(minimumProviderHeight) || !Number.isFinite(maximumProviderHeight) || minimumProviderHeight > maximumProviderHeight) {
6614
+ throw new Error("Elevation provider returned an invalid range; minimum and maximum must be finite and ordered");
6615
+ }
6616
+ const sourceHeights = geographicSamples.map((position) => position[2]);
6617
+ const minimumSourceHeight = Math.min(...sourceHeights);
6618
+ const maximumSourceHeight = Math.max(...sourceHeights);
6619
+ const placedSamples = [];
6620
+ for (const [longitude, latitude] of geographicSamples) {
6621
+ const convertedMinimum = this.providerHeightTransformer ? this.providerHeightTransformer.transformPosition([
6622
+ longitude,
6623
+ latitude,
6624
+ minimumProviderHeight
6625
+ ])[2] : minimumProviderHeight;
6626
+ const convertedMaximum = this.providerHeightTransformer ? this.providerHeightTransformer.transformPosition([
6627
+ longitude,
6628
+ latitude,
6629
+ maximumProviderHeight
6630
+ ])[2] : maximumProviderHeight;
6631
+ const minimumSurfaceHeight = Math.min(convertedMinimum, convertedMaximum);
6632
+ const maximumSurfaceHeight = Math.max(convertedMinimum, convertedMaximum);
6633
+ const minimumHeight = this.elevationMode === "onTheGround" ? minimumSurfaceHeight : minimumSurfaceHeight + minimumSourceHeight + this.elevationOffset;
6634
+ const maximumHeight = this.elevationMode === "onTheGround" ? maximumSurfaceHeight : maximumSurfaceHeight + maximumSourceHeight + this.elevationOffset;
6635
+ placedSamples.push([longitude, latitude, minimumHeight], [longitude, latitude, maximumHeight]);
6636
+ }
6637
+ return placedSamples;
6638
+ }
6639
+ /** Apply source units and non-surface placement without awaiting external resources. */
6640
+ placeSourcePositionsSync(sourcePositions, sourceHeightsAreMeters = false) {
6641
+ if (requiresElevationSurface(this.elevationMode)) {
6642
+ throw new Error(`I3S elevation mode ${this.elevationMode} requires asynchronous surface placement`);
6643
+ }
6644
+ const positions = Float64Array.from(sourcePositions);
6645
+ const offset = this.elevationMode === "absoluteHeight" ? this.elevationOffset : 0;
6646
+ for (let index = 2; index < positions.length; index += 3) {
6647
+ positions[index] = positions[index] * (sourceHeightsAreMeters ? 1 : this.sourceVerticalUnitScale) + offset;
6648
+ }
6649
+ return positions;
6650
+ }
6651
+ /** Apply source units and all I3S elevation-placement modes. */
6652
+ async placeSourcePositions(sourcePositions, sourceHeightsAreMeters = false) {
6653
+ if (!requiresElevationSurface(this.elevationMode)) {
6654
+ return this.placeSourcePositionsSync(sourcePositions, sourceHeightsAreMeters);
6655
+ }
6656
+ const positions = Float64Array.from(sourcePositions);
6657
+ const geographicLocations = toPositionArrays(positions).map((position) => {
6658
+ const geographic = this.sourceToGeographicTransformer.transformPosition(position);
6659
+ return [geographic[0], geographic[1]];
6660
+ });
6661
+ const sampledHeights = await this.elevationProvider.sampleElevations(geographicLocations);
6662
+ if (sampledHeights.length !== geographicLocations.length) {
6663
+ throw new Error(`Elevation provider returned ${sampledHeights.length} heights for ${geographicLocations.length} positions`);
6664
+ }
6665
+ for (let positionIndex = 0; positionIndex < geographicLocations.length; positionIndex++) {
6666
+ const sampledHeight = sampledHeights[positionIndex] * this.providerUnitScale;
6667
+ if (!Number.isFinite(sampledHeight)) {
6668
+ throw new Error(`Elevation provider returned a non-finite height at index ${positionIndex}`);
6669
+ }
6670
+ const [longitude, latitude] = geographicLocations[positionIndex];
6671
+ const surfaceHeight = this.providerHeightTransformer ? this.providerHeightTransformer.transformPosition([longitude, latitude, sampledHeight])[2] : sampledHeight;
6672
+ const sourceHeight = positions[positionIndex * 3 + 2] * (sourceHeightsAreMeters ? 1 : this.sourceVerticalUnitScale);
6673
+ positions[positionIndex * 3 + 2] = this.elevationMode === "onTheGround" ? surfaceHeight : surfaceHeight + sourceHeight + this.elevationOffset;
6674
+ }
6675
+ return positions;
6676
+ }
6677
+ /** Transform one earth-centered normal to the selected target basis. */
6678
+ transformEcefNormal(ecefNormal, geographic) {
6679
+ if (this.targetCoordinateFrame === "geocentric") {
6680
+ return ecefNormal.normalize();
6681
+ }
6682
+ if (this.targetCoordinateFrame === "geographic") {
6683
+ return ecefVectorToLocalEnu(ecefNormal, geographic).normalize();
6684
+ }
6685
+ const cartesian = import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(geographic));
6686
+ const jacobianColumns = [];
6687
+ for (const direction of getPositiveAxisDirections()) {
6688
+ const positiveGeographic = import_geospatial9.Ellipsoid.WGS84.cartesianToCartographic(new import_core12.Vector3(cartesian).add(direction));
6689
+ const negativeGeographic = import_geospatial9.Ellipsoid.WGS84.cartesianToCartographic(new import_core12.Vector3(cartesian).subtract(direction));
6690
+ const positiveTarget = this.geographicToTargetTransformer.transformPosition(positiveGeographic);
6691
+ const negativeTarget = this.geographicToTargetTransformer.transformPosition(negativeGeographic);
6692
+ jacobianColumns.push((positiveTarget[0] - negativeTarget[0]) / 2, (positiveTarget[1] - negativeTarget[1]) / 2, (positiveTarget[2] - negativeTarget[2]) / 2);
6693
+ }
6694
+ const normalMatrix = new import_core12.Matrix3(jacobianColumns);
6695
+ if (Math.abs(normalMatrix.determinant()) < 1e-12) {
6696
+ throw new Error("Cannot transform an I3S normal through a singular local projection");
6697
+ }
6698
+ normalMatrix.invert().transpose();
6699
+ return new import_core12.Vector3(ecefNormal).transformByMatrix3(normalMatrix).normalize();
6700
+ }
6701
+ };
6702
+ function getElevationProvider(mode, options) {
6703
+ return mode === "relativeToScene" ? options.sceneElevationProvider : requiresElevationSurface(mode) ? options.terrainElevationProvider : void 0;
6704
+ }
6705
+ function requiresElevationSurface(mode) {
6706
+ return mode === "onTheGround" || mode === "relativeToGround" || mode === "relativeToScene";
6707
+ }
6708
+ function toPositionArrays(positions) {
6709
+ if (positions.length % 3 !== 0) {
6710
+ throw new Error("I3S position arrays must contain complete XYZ tuples");
6711
+ }
6712
+ const result = [];
6713
+ for (let index = 0; index < positions.length; index += 3) {
6714
+ result.push([positions[index], positions[index + 1], positions[index + 2]]);
6715
+ }
6716
+ return result;
6717
+ }
6718
+ function getElevationBounds(geographicSamples) {
6719
+ const region = getGeographicRegion(geographicSamples);
6720
+ return {
6721
+ west: region[0] * 180 / Math.PI,
6722
+ south: region[1] * 180 / Math.PI,
6723
+ east: region[2] * 180 / Math.PI,
6724
+ north: region[3] * 180 / Math.PI
6725
+ };
6726
+ }
6727
+ function getGeographicBoundingSphere(geographicSamples) {
6728
+ if (!geographicSamples.length) {
6729
+ throw new Error("I3S spatial transformation requires at least one bound sample");
6730
+ }
6731
+ const cartesianSamples = geographicSamples.map((position) => import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(position)));
6732
+ const cartesianCenter = new import_core12.Vector3();
6733
+ for (const sample of cartesianSamples) {
6734
+ cartesianCenter.add(sample);
6735
+ }
6736
+ cartesianCenter.scale(1 / cartesianSamples.length);
6737
+ let radius = 0;
6738
+ for (const sample of cartesianSamples) {
6739
+ radius = Math.max(radius, new import_core12.Vector3(sample).subtract(cartesianCenter).magnitude());
6740
+ }
6741
+ const geographicCenter = import_geospatial9.Ellipsoid.WGS84.cartesianToCartographic(cartesianCenter);
6742
+ return [geographicCenter[0], geographicCenter[1], geographicCenter[2], radius];
6743
+ }
6744
+ function getSourceBoundSamples(bounds, sourceFrame, sourceToGeographicTransformer, sourceVerticalUnitScale) {
6745
+ const obb = bounds.obb;
6746
+ if (obb) {
6747
+ const center2 = Array.from(obb.center).slice(0, 3);
6748
+ center2[2] *= sourceVerticalUnitScale;
6749
+ const halfSize = Array.from(obb.halfSize).slice(0, 3);
6750
+ const quaternion = new import_core12.Quaternion().fromArray(Array.from(obb.quaternion));
6751
+ const axes = [
6752
+ new import_core12.Vector3([halfSize[0], 0, 0]).transformByQuaternion(quaternion),
6753
+ new import_core12.Vector3([0, halfSize[1], 0]).transformByQuaternion(quaternion),
6754
+ new import_core12.Vector3([0, 0, halfSize[2]]).transformByQuaternion(quaternion)
6755
+ ];
6756
+ if (sourceFrame === "geographic") {
6757
+ const geographicCenter = sourceToGeographicTransformer.transformPosition(center2);
6758
+ const cartesianCenter = import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(geographicCenter));
6759
+ const cartesianAxes = bounds.normalReferenceFrame === "vertex-reference-frame" ? axes.map((axis) => localEnuVectorToEcef(axis, geographicCenter)) : axes;
6760
+ return getBoxSigns().map((signs) => {
6761
+ const corner = new import_core12.Vector3(cartesianCenter);
6762
+ for (let axisIndex = 0; axisIndex < 3; axisIndex++) {
6763
+ corner.add(new import_core12.Vector3(cartesianAxes[axisIndex]).scale(signs[axisIndex]));
6764
+ }
6765
+ return Array.from(import_geospatial9.Ellipsoid.WGS84.cartesianToCartographic(corner));
6766
+ });
6767
+ }
6768
+ return getBoxSigns().map((signs) => {
6769
+ const corner = new import_core12.Vector3(center2);
6770
+ for (let axisIndex = 0; axisIndex < 3; axisIndex++) {
6771
+ corner.add(new import_core12.Vector3(axes[axisIndex]).scale(signs[axisIndex]));
6772
+ }
6773
+ return Array.from(corner);
6774
+ });
6775
+ }
6776
+ const mbs = bounds.mbs ? Array.from(bounds.mbs) : [];
6777
+ if (mbs.length < 4) {
6778
+ return [];
6779
+ }
6780
+ const center = mbs.slice(0, 3);
6781
+ center[2] *= sourceVerticalUnitScale;
6782
+ const radius = mbs[3];
6783
+ if (sourceFrame === "geographic") {
6784
+ const geographicCenter = sourceToGeographicTransformer.transformPosition(center);
6785
+ const cartesianCenter = import_geospatial9.Ellipsoid.WGS84.cartographicToCartesian(new import_core12.Vector3(geographicCenter));
6786
+ return getAxisDirections().map((direction) => Array.from(import_geospatial9.Ellipsoid.WGS84.cartesianToCartographic(new import_core12.Vector3(cartesianCenter).add(new import_core12.Vector3(direction).scale(radius)))));
6787
+ }
6788
+ return getAxisDirections().map((direction) => Array.from(new import_core12.Vector3(center).add(new import_core12.Vector3(direction).scale(radius))));
6789
+ }
6790
+ function getBoxSigns() {
6791
+ return [-1, 1].flatMap((x) => [-1, 1].flatMap((y) => [-1, 1].map((z) => [x, y, z])));
6792
+ }
6793
+ function getAxisDirections() {
6794
+ return [
6795
+ [1, 0, 0],
6796
+ [-1, 0, 0],
6797
+ [0, 1, 0],
6798
+ [0, -1, 0],
6799
+ [0, 0, 1],
6800
+ [0, 0, -1]
6801
+ ];
6802
+ }
6803
+ function getPositiveAxisDirections() {
6804
+ return [new import_core12.Vector3([1, 0, 0]), new import_core12.Vector3([0, 1, 0]), new import_core12.Vector3([0, 0, 1])];
6805
+ }
6806
+ function localEnuVectorToEcef(vector, geographic) {
6807
+ const longitude = geographic[0] * Math.PI / 180;
6808
+ const latitude = geographic[1] * Math.PI / 180;
6809
+ const east = new import_core12.Vector3([-Math.sin(longitude), Math.cos(longitude), 0]);
6810
+ const north = new import_core12.Vector3([
6811
+ -Math.sin(latitude) * Math.cos(longitude),
6812
+ -Math.sin(latitude) * Math.sin(longitude),
6813
+ Math.cos(latitude)
6814
+ ]);
6815
+ const up = new import_core12.Vector3([
6816
+ Math.cos(latitude) * Math.cos(longitude),
6817
+ Math.cos(latitude) * Math.sin(longitude),
6818
+ Math.sin(latitude)
6819
+ ]);
6820
+ return east.scale(vector[0]).add(north.scale(vector[1])).add(up.scale(vector[2]));
6821
+ }
6822
+ function ecefVectorToLocalEnu(vector, geographic) {
6823
+ const longitude = geographic[0] * Math.PI / 180;
6824
+ const latitude = geographic[1] * Math.PI / 180;
6825
+ const east = new import_core12.Vector3([-Math.sin(longitude), Math.cos(longitude), 0]);
6826
+ const north = new import_core12.Vector3([
6827
+ -Math.sin(latitude) * Math.cos(longitude),
6828
+ -Math.sin(latitude) * Math.sin(longitude),
6829
+ Math.cos(latitude)
6830
+ ]);
6831
+ const up = new import_core12.Vector3([
6832
+ Math.cos(latitude) * Math.cos(longitude),
6833
+ Math.cos(latitude) * Math.sin(longitude),
6834
+ Math.sin(latitude)
6835
+ ]);
6836
+ return new import_core12.Vector3([east.dot(vector), north.dot(vector), up.dot(vector)]);
6837
+ }
6838
+ function getAxisAlignedBox(positions) {
6839
+ const minimum = [Infinity, Infinity, Infinity];
6840
+ const maximum = [-Infinity, -Infinity, -Infinity];
6841
+ for (const position of positions) {
6842
+ for (let component = 0; component < 3; component++) {
6843
+ minimum[component] = Math.min(minimum[component], position[component]);
6844
+ maximum[component] = Math.max(maximum[component], position[component]);
6845
+ }
6846
+ }
6847
+ const center = minimum.map((value, index) => (value + maximum[index]) / 2);
6848
+ const halfSize = minimum.map((value, index) => (maximum[index] - value) / 2);
6849
+ return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
6850
+ }
6851
+ function getGeographicRegion(positions) {
6852
+ const referenceLongitude = positions[0][0];
6853
+ let minimumLongitude = Infinity;
6854
+ let maximumLongitude = -Infinity;
6855
+ let minimumLatitude = Infinity;
6856
+ let maximumLatitude = -Infinity;
6857
+ let minimumHeight = Infinity;
6858
+ let maximumHeight = -Infinity;
6859
+ for (const position of positions) {
6860
+ const longitude = referenceLongitude + normalizeLongitude(position[0] - referenceLongitude);
6861
+ minimumLongitude = Math.min(minimumLongitude, longitude);
6862
+ maximumLongitude = Math.max(maximumLongitude, longitude);
6863
+ minimumLatitude = Math.min(minimumLatitude, position[1]);
6864
+ maximumLatitude = Math.max(maximumLatitude, position[1]);
6865
+ minimumHeight = Math.min(minimumHeight, position[2]);
6866
+ maximumHeight = Math.max(maximumHeight, position[2]);
6867
+ }
6868
+ return [
6869
+ normalizeLongitude(minimumLongitude) * Math.PI / 180,
6870
+ minimumLatitude * Math.PI / 180,
6871
+ normalizeLongitude(maximumLongitude) * Math.PI / 180,
6872
+ maximumLatitude * Math.PI / 180,
6873
+ minimumHeight,
6874
+ maximumHeight
6875
+ ];
6876
+ }
6877
+ function normalizeLongitude(longitude) {
6878
+ return ((longitude + 180) % 360 + 360) % 360 - 180;
6879
+ }
6880
+
5265
6881
  // dist/tileset-3d/format-i3s/i3s-source.js
5266
6882
  var EMPTY_CONTENT_FORMATS2 = {
5267
6883
  draco: false,
@@ -5294,6 +6910,10 @@ var I3SSource = class {
5294
6910
  queryParams = {};
5295
6911
  resolver;
5296
6912
  rootTileset;
6913
+ /** Spatial adapter created lazily after Tileset3D applies target options. */
6914
+ spatialTransformer;
6915
+ /** Root header prepared after asynchronous elevation sampling. */
6916
+ preparedRootHeader;
5297
6917
  /**
5298
6918
  * Creates an I3S source.
5299
6919
  * @param input Root metadata request, or preloaded root metadata for tests and internal callers.
@@ -5309,6 +6929,7 @@ var I3SSource = class {
5309
6929
  this.resolver = request.resolver;
5310
6930
  this.coreApi = request.coreApi;
5311
6931
  this.loadOptions = loadOptions;
6932
+ this.initializeQueryParams(loadOptions);
5312
6933
  }
5313
6934
  /**
5314
6935
  * Fetches and normalizes root I3S metadata for runtime consumption.
@@ -5316,19 +6937,12 @@ var I3SSource = class {
5316
6937
  async initialize() {
5317
6938
  var _a;
5318
6939
  if (!this.rootTileset) {
5319
- this.rootTileset = await this.loadRootData(this.url, this.loadOptions);
6940
+ this.rootTileset = await this.loadRootData(this.getTileUrl(this.url), this.loadOptions);
5320
6941
  }
5321
6942
  this.tileset = this.rootTileset;
5322
6943
  if (this.rootTileset.root && typeof this.rootTileset.root.then === "function") {
5323
6944
  this.rootTileset.root = await this.rootTileset.root;
5324
6945
  }
5325
- const i3sOptions = this.loadOptions.i3s;
5326
- if (i3sOptions && typeof i3sOptions === "object" && "token" in i3sOptions) {
5327
- const token = i3sOptions.token;
5328
- if (typeof token === "string") {
5329
- this.queryParams.token = token;
5330
- }
5331
- }
5332
6946
  this.metadata = {
5333
6947
  type: this.type,
5334
6948
  loader: this.loader,
@@ -5337,7 +6951,8 @@ var I3SSource = class {
5337
6951
  tileset: this.rootTileset,
5338
6952
  lodMetricType: this.rootTileset.lodMetricType,
5339
6953
  lodMetricValue: this.rootTileset.lodMetricValue,
5340
- refine: (_a = this.rootTileset.root) == null ? void 0 : _a.refine
6954
+ refine: (_a = this.rootTileset.root) == null ? void 0 : _a.refine,
6955
+ spatialReference: getI3SSpatialReference(this.rootTileset)
5341
6956
  };
5342
6957
  }
5343
6958
  /**
@@ -5355,11 +6970,28 @@ var I3SSource = class {
5355
6970
  async getRootTileset() {
5356
6971
  return this.getMetadata().tileset;
5357
6972
  }
6973
+ /** Prepare option-dependent spatial state and the asynchronously placed root bound. */
6974
+ async prepareTileset(tileset) {
6975
+ const spatialReference = tileset.spatialReference;
6976
+ if (spatialReference.status === "unresolved") {
6977
+ throw new Error(spatialReference.warnings[0] || "I3S spatial operations cannot be resolved from the supplied metadata and options");
6978
+ }
6979
+ if (spatialReference.status !== "transformable" && spatialReference.status !== "transformed") {
6980
+ return;
6981
+ }
6982
+ this.spatialTransformer ||= new I3SSpatialTransformer(spatialReference, tileset.options.spatial);
6983
+ tileset.spatialReference = this.spatialTransformer.spatialReference;
6984
+ this.preparedRootHeader = await this.transformTileHeader(this.getMetadata().tileset.root);
6985
+ }
5358
6986
  /**
5359
6987
  * Creates the runtime root tile for an I3S subtree.
5360
6988
  */
5361
6989
  initializeTileHeaders(tileset, tilesetJson, parentTile) {
5362
- const rootTile = new Tile3D(tileset, tilesetJson.root, parentTile || void 0);
6990
+ const rootHeader = parentTile ? tilesetJson.root : this.preparedRootHeader || tilesetJson.root;
6991
+ if (this.spatialTransformer && !this.preparedRootHeader) {
6992
+ throw new Error("I3S spatial root header was not prepared before tile initialization");
6993
+ }
6994
+ const rootTile = new Tile3D(tileset, rootHeader, parentTile || void 0);
5363
6995
  if (parentTile) {
5364
6996
  parentTile.children.push(rootTile);
5365
6997
  rootTile.depth = parentTile.depth + 1;
@@ -5388,16 +7020,22 @@ var I3SSource = class {
5388
7020
  _tileOptions: {
5389
7021
  attributeUrls: tile.header.attributeUrls,
5390
7022
  textureUrl: tile.header.textureUrl,
7023
+ textureUrls: tile.header.textureUrls,
5391
7024
  textureFormat: tile.header.textureFormat,
5392
7025
  textureLoaderOptions: tile.header.textureLoaderOptions,
5393
7026
  materialDefinition: tile.header.materialDefinition,
7027
+ layerType: tile.header.layerType,
7028
+ pointRenderer: tile.header.pointRenderer,
7029
+ pointSymbol: tile.header.pointSymbol,
5394
7030
  isDracoGeometry: tile.header.isDracoGeometry,
5395
7031
  mbs: tile.header.mbs
5396
7032
  },
5397
7033
  _tilesetOptions: {
5398
7034
  store: metadata.tileset.store,
5399
7035
  attributeStorageInfo: metadata.tileset.attributeStorageInfo,
5400
- fields: metadata.tileset.fields
7036
+ fields: metadata.tileset.fields,
7037
+ spatialReference: tile.tileset.spatialReference,
7038
+ spatialOptions: tile.tileset.options.spatial
5401
7039
  },
5402
7040
  isTileHeader: false
5403
7041
  }
@@ -5410,8 +7048,9 @@ var I3SSource = class {
5410
7048
  */
5411
7049
  async loadChildTileHeader(_parentTile, childId, _frameState) {
5412
7050
  const metadata = this.getMetadata();
5413
- if (metadata.tileset.nodePages) {
5414
- return await metadata.tileset.nodePagesTile.formTileFromNodePages(childId);
7051
+ if (metadata.tileset.nodePages || metadata.tileset.pointNodePages) {
7052
+ const header2 = await metadata.tileset.nodePagesTile.formTileFromNodePages(childId);
7053
+ return await this.transformTileHeader(header2);
5415
7054
  }
5416
7055
  const nodeUrl = this.getTileUrl(`${this.url}/nodes/${childId}`);
5417
7056
  const i3sLoaderOptions = this.loadOptions.i3s && typeof this.loadOptions.i3s === "object" ? this.loadOptions.i3s : {};
@@ -5422,7 +7061,8 @@ var I3SSource = class {
5422
7061
  isTileHeader: true
5423
7062
  }
5424
7063
  };
5425
- return await this.loadResourceData(nodeUrl, options);
7064
+ const header = await this.loadResourceData(nodeUrl, options);
7065
+ return await this.transformTileHeader(header);
5426
7066
  }
5427
7067
  /**
5428
7068
  * Resolves an I3S request URL with source-managed query parameters.
@@ -5434,21 +7074,51 @@ var I3SSource = class {
5434
7074
  if (!Object.keys(this.queryParams).length) {
5435
7075
  return tilePath;
5436
7076
  }
5437
- const queryParams = new URLSearchParams(this.queryParams).toString();
5438
- return `${tilePath}${tilePath.includes("?") ? "&" : "?"}${queryParams}`;
7077
+ const queryDelimiterIndex = tilePath.indexOf("?");
7078
+ if (queryDelimiterIndex === -1) {
7079
+ return `${tilePath}?${new URLSearchParams(this.queryParams).toString()}`;
7080
+ }
7081
+ const existingQuery = tilePath.slice(queryDelimiterIndex + 1);
7082
+ const existingQueryKeys = new Set(existingQuery.split("&").map((parameter) => parameter.split("=", 1)[0]));
7083
+ const queryParams = new URLSearchParams();
7084
+ for (const [key, value] of Object.entries(this.queryParams)) {
7085
+ if (!existingQueryKeys.has(key)) {
7086
+ queryParams.set(key, value);
7087
+ }
7088
+ }
7089
+ const queryString = queryParams.toString();
7090
+ return queryString ? `${tilePath}${existingQuery ? "&" : ""}${queryString}` : tilePath;
5439
7091
  }
5440
7092
  /**
5441
7093
  * Derives the default view state from full extent or store extent metadata.
5442
7094
  */
5443
- getViewState(_rootTile) {
7095
+ getViewState(rootTile) {
5444
7096
  var _a;
7097
+ if (this.spatialTransformer && rootTile) {
7098
+ const center = new import_core13.Vector3(rootTile.boundingVolume.center);
7099
+ const cartographicCenter = new import_core13.Vector3(rootTile.header.i3sLodMbs.slice(0, 3));
7100
+ if (this.spatialTransformer.targetCoordinateFrame === "geographic") {
7101
+ return {
7102
+ boundingVolume: rootTile.boundingVolume,
7103
+ cartographicCenter,
7104
+ cartesianCenter: center,
7105
+ zoom: 1
7106
+ };
7107
+ }
7108
+ return {
7109
+ boundingVolume: rootTile.boundingVolume,
7110
+ cartographicCenter,
7111
+ cartesianCenter: center,
7112
+ zoom: 1
7113
+ };
7114
+ }
5445
7115
  const metadata = this.getMetadata();
5446
7116
  const fullExtent = metadata.tileset.fullExtent;
5447
7117
  if (fullExtent) {
5448
7118
  const { xmin, xmax, ymin, ymax, zmin, zmax } = fullExtent;
5449
- const cartographicCenter = new import_core11.Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, zmin + (zmax - zmin) / 2);
5450
- const cartesianCenter = new import_core11.Vector3();
5451
- import_geospatial8.Ellipsoid.WGS84.cartographicToCartesian(cartographicCenter, cartesianCenter);
7119
+ const cartographicCenter = new import_core13.Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, zmin + (zmax - zmin) / 2);
7120
+ const cartesianCenter = new import_core13.Vector3();
7121
+ import_geospatial10.Ellipsoid.WGS84.cartographicToCartesian(cartographicCenter, cartesianCenter);
5452
7122
  return {
5453
7123
  cartographicCenter,
5454
7124
  cartesianCenter,
@@ -5458,9 +7128,9 @@ var I3SSource = class {
5458
7128
  const extent = (_a = metadata.tileset.store) == null ? void 0 : _a.extent;
5459
7129
  if (extent) {
5460
7130
  const [xmin, ymin, xmax, ymax] = extent;
5461
- const cartographicCenter = new import_core11.Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, 0);
5462
- const cartesianCenter = new import_core11.Vector3();
5463
- import_geospatial8.Ellipsoid.WGS84.cartographicToCartesian(cartographicCenter, cartesianCenter);
7131
+ const cartographicCenter = new import_core13.Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, 0);
7132
+ const cartesianCenter = new import_core13.Vector3();
7133
+ import_geospatial10.Ellipsoid.WGS84.cartographicToCartesian(cartographicCenter, cartesianCenter);
5464
7134
  return {
5465
7135
  cartographicCenter,
5466
7136
  cartesianCenter,
@@ -5469,7 +7139,7 @@ var I3SSource = class {
5469
7139
  }
5470
7140
  console.warn("Extent is not defined in the tileset header");
5471
7141
  return {
5472
- cartographicCenter: new import_core11.Vector3(),
7142
+ cartographicCenter: new import_core13.Vector3(),
5473
7143
  zoom: 1
5474
7144
  };
5475
7145
  }
@@ -5516,6 +7186,19 @@ var I3SSource = class {
5516
7186
  }
5517
7187
  return await this.loadWithCoreApi(url, options);
5518
7188
  }
7189
+ /**
7190
+ * Initializes query parameters before any URL-backed resource is requested.
7191
+ * @param loadOptions Loader options that may contain an ArcGIS token.
7192
+ */
7193
+ initializeQueryParams(loadOptions) {
7194
+ const i3sOptions = loadOptions.i3s;
7195
+ if (i3sOptions && typeof i3sOptions === "object" && "token" in i3sOptions) {
7196
+ const token = i3sOptions.token;
7197
+ if (typeof token === "string") {
7198
+ this.queryParams.token = token;
7199
+ }
7200
+ }
7201
+ }
5519
7202
  /**
5520
7203
  * Loads tile metadata or content through an injected resolver when present, otherwise through the injected core API.
5521
7204
  */
@@ -5525,6 +7208,27 @@ var I3SSource = class {
5525
7208
  }
5526
7209
  return await this.loadWithCoreApi(url, options);
5527
7210
  }
7211
+ /** Transform one I3S header bound after target options and elevation providers are available. */
7212
+ async transformTileHeader(header) {
7213
+ var _a;
7214
+ if (!this.spatialTransformer) {
7215
+ return header;
7216
+ }
7217
+ const sourceBounds = {
7218
+ mbs: header.mbs,
7219
+ obb: header.obb,
7220
+ normalReferenceFrame: (_a = this.getMetadata().tileset.store) == null ? void 0 : _a.normalReferenceFrame
7221
+ };
7222
+ const transformedBounds = await this.spatialTransformer.transformBoundsAsync(sourceBounds);
7223
+ return {
7224
+ ...header,
7225
+ // Tileset3D traversal and culling operate in WGS84 ECEF independently of content output.
7226
+ boundingVolume: transformedBounds.boundingVolume,
7227
+ // Preserve the renderer/output-frame bound for applications and content coordination.
7228
+ spatialBoundingVolume: transformedBounds.spatialBoundingVolume,
7229
+ i3sLodMbs: transformedBounds.i3sLodMbs
7230
+ };
7231
+ }
5528
7232
  };
5529
7233
  function isTilesetRequest2(input) {
5530
7234
  return Boolean(input && typeof input === "object" && "url" in input && "loader" in input && !("type" in input));
@@ -6520,6 +8224,10 @@ var RasterSet = class _RasterSet {
6520
8224
  static fromRasterSource(rasterSource, opts = {}) {
6521
8225
  return new _RasterSet({ ...opts, rasterSource });
6522
8226
  }
8227
+ /** Convenience factory for sources whose requests are not viewport-shaped. */
8228
+ static fromCallbacks(opts) {
8229
+ return new _RasterSet(opts);
8230
+ }
6523
8231
  /** Whether the manager has no requests in flight and a current raster. */
6524
8232
  get isLoaded() {
6525
8233
  return this._loadCounter === 0 && Boolean(this._currentRequest);
@@ -6630,7 +8338,7 @@ var RasterSet = class _RasterSet {
6630
8338
  try {
6631
8339
  const abortController = new AbortController();
6632
8340
  this._abortController = abortController;
6633
- const raster = await this._opts.getRaster({ ...parameters, signal: abortController.signal });
8341
+ const raster = await this._opts.getRaster(withAbortSignal(parameters, abortController.signal));
6634
8342
  if (this._finalized || abortController.signal.aborted || requestId <= this._lastAcceptedRequestId) {
6635
8343
  return;
6636
8344
  }
@@ -6724,6 +8432,16 @@ var RasterSet = class _RasterSet {
6724
8432
  this._abortController = null;
6725
8433
  }
6726
8434
  };
8435
+ function withAbortSignal(parameters, signal) {
8436
+ const request = Array.isArray(parameters) ? parameters.slice() : Object.create(Object.getPrototypeOf(parameters), Object.getOwnPropertyDescriptors(parameters));
8437
+ Object.defineProperty(request, "signal", {
8438
+ configurable: true,
8439
+ enumerable: true,
8440
+ value: signal,
8441
+ writable: true
8442
+ });
8443
+ return request;
8444
+ }
6727
8445
  function normalizeRasterSetError(error, message) {
6728
8446
  if (error instanceof Error) {
6729
8447
  return error;
@@ -6738,7 +8456,7 @@ function isAbortError3(error) {
6738
8456
  }
6739
8457
 
6740
8458
  // dist/point-cloud/point-cloud-tileset.js
6741
- var import_core12 = require("@math.gl/core");
8459
+ var import_core14 = require("@math.gl/core");
6742
8460
  var import_culling6 = require("@math.gl/culling");
6743
8461
 
6744
8462
  // dist/point-cloud/point-cloud-tile.js
@@ -6811,6 +8529,8 @@ var DEFAULT_PROPS5 = {
6811
8529
  debounceTime: 0,
6812
8530
  minimumNodePixelSize: 150,
6813
8531
  maximumScreenSpaceError: 1,
8532
+ lodSelectionMetricType: "maxScreenThresholdSQ",
8533
+ densityThreshold: 1,
6814
8534
  pointBudget: 2e6,
6815
8535
  maxDepth: Number.POSITIVE_INFINITY,
6816
8536
  onTileLoad: () => {
@@ -6900,7 +8620,7 @@ var PointCloudTileset = class {
6900
8620
  const viewState = await ((_b = (_a = this.dataSource).getViewState) == null ? void 0 : _b.call(_a));
6901
8621
  this.boundingVolume = (viewState == null ? void 0 : viewState.boundingVolume) || this.root.boundingVolume;
6902
8622
  const center = (viewState == null ? void 0 : viewState.cartographicCenter) || this.root.boundingVolume.center;
6903
- this.cartographicCenter = new import_core12.Vector3(center);
8623
+ this.cartographicCenter = new import_core14.Vector3(center);
6904
8624
  this.zoom = (viewState == null ? void 0 : viewState.zoom) || this.estimateZoom(this.boundingVolume);
6905
8625
  }
6906
8626
  /**
@@ -6994,6 +8714,12 @@ var PointCloudTileset = class {
6994
8714
  }
6995
8715
  /** Whether a tile should refine to children based on its projected screen radius. */
6996
8716
  shouldRefine(tile, traversalWeight) {
8717
+ if (tile.level < this.options.maxDepth && Number.isFinite(traversalWeight) && (tile.header.lodSelectionMetricType || this.options.lodSelectionMetricType) === "density-threshold") {
8718
+ const projectedArea = Math.max(1, Math.PI * traversalWeight * traversalWeight);
8719
+ const density = tile.pointCount / projectedArea;
8720
+ const threshold = tile.header.lodThreshold || this.options.densityThreshold;
8721
+ return density < threshold;
8722
+ }
6997
8723
  return tile.level < this.options.maxDepth && Number.isFinite(traversalWeight) && traversalWeight >= this.options.minimumNodePixelSize;
6998
8724
  }
6999
8725
  /**
@@ -7191,9 +8917,13 @@ var PointCloudTileset = class {
7191
8917
  if (!boundingVolume) {
7192
8918
  return 1;
7193
8919
  }
8920
+ if (boundingVolume.coordinateFrame === "cartesian") {
8921
+ return 1;
8922
+ }
7194
8923
  const [minBounds, maxBounds] = boundingVolume.cartographicBounds;
7195
- const longitudeSpan = Math.max(Math.abs(maxBounds[0] - minBounds[0]), 1e-6);
7196
- return Math.max(1, Math.round(Math.log2(360 / longitudeSpan)));
8924
+ const longitudeSpan = boundingVolume.coversFullLongitude ? 360 : boundingVolume.wrapsDateline ? 360 - Math.abs(maxBounds[0] - minBounds[0]) : Math.abs(maxBounds[0] - minBounds[0]);
8925
+ const normalizedLongitudeSpan = Math.max(longitudeSpan, 1e-6);
8926
+ return Math.max(1, Math.round(Math.log2(360 / normalizedLongitudeSpan)));
7197
8927
  }
7198
8928
  haveSameIds(idsA, idsB) {
7199
8929
  if (idsA.size !== idsB.size) {
@@ -7211,16 +8941,32 @@ var PointCloudTileset = class {
7211
8941
  */
7212
8942
  getBoundingVolumeCorners(boundingVolume) {
7213
8943
  const [minBounds, maxBounds] = boundingVolume.cartographicBounds;
7214
- return [
7215
- [minBounds[0], minBounds[1], minBounds[2] || 0],
7216
- [minBounds[0], minBounds[1], maxBounds[2] || 0],
7217
- [minBounds[0], maxBounds[1], minBounds[2] || 0],
7218
- [minBounds[0], maxBounds[1], maxBounds[2] || 0],
7219
- [maxBounds[0], minBounds[1], minBounds[2] || 0],
7220
- [maxBounds[0], minBounds[1], maxBounds[2] || 0],
7221
- [maxBounds[0], maxBounds[1], minBounds[2] || 0],
7222
- [maxBounds[0], maxBounds[1], maxBounds[2] || 0]
7223
- ];
8944
+ const longitudeIntervals = boundingVolume.coversFullLongitude ? [[-180, 180]] : boundingVolume.wrapsDateline ? [
8945
+ [minBounds[0], 180],
8946
+ [-180, maxBounds[0]]
8947
+ ] : [[minBounds[0], maxBounds[0]]];
8948
+ const corners = [];
8949
+ const wrappedLongitudeSpan = 360 - Math.abs(maxBounds[0] - minBounds[0]);
8950
+ const longitudeReference = minBounds[0] + wrappedLongitudeSpan / 2;
8951
+ for (const [minimumLongitude, maximumLongitude] of longitudeIntervals) {
8952
+ const normalizeLongitude2 = (longitude) => {
8953
+ if (!boundingVolume.wrapsDateline) {
8954
+ return longitude;
8955
+ }
8956
+ let normalizedLongitude = longitude;
8957
+ while (normalizedLongitude - longitudeReference > 180) {
8958
+ normalizedLongitude -= 360;
8959
+ }
8960
+ while (normalizedLongitude - longitudeReference < -180) {
8961
+ normalizedLongitude += 360;
8962
+ }
8963
+ return normalizedLongitude;
8964
+ };
8965
+ const unwrappedMinimumLongitude = normalizeLongitude2(minimumLongitude);
8966
+ const unwrappedMaximumLongitude = normalizeLongitude2(maximumLongitude);
8967
+ corners.push([unwrappedMinimumLongitude, minBounds[1], minBounds[2] || 0], [unwrappedMinimumLongitude, minBounds[1], maxBounds[2] || 0], [unwrappedMinimumLongitude, maxBounds[1], minBounds[2] || 0], [unwrappedMinimumLongitude, maxBounds[1], maxBounds[2] || 0], [unwrappedMaximumLongitude, minBounds[1], minBounds[2] || 0], [unwrappedMaximumLongitude, minBounds[1], maxBounds[2] || 0], [unwrappedMaximumLongitude, maxBounds[1], minBounds[2] || 0], [unwrappedMaximumLongitude, maxBounds[1], maxBounds[2] || 0]);
8968
+ }
8969
+ return corners;
7224
8970
  }
7225
8971
  /**
7226
8972
  * Build a culling volume directly from the viewport frustum in common space.