@loaders.gl/tile-converter 5.0.0-alpha.7 → 5.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +155 -0
  2. package/dist/converter.min.cjs +194 -175
  3. package/dist/i3s-server/bin/i3s-server.min.cjs +84 -84
  4. package/dist/index.cjs +35865 -0
  5. package/dist/index.js +35837 -0
  6. package/dist/v5/browser-sink.d.ts +37 -0
  7. package/dist/v5/browser-source.d.ts +38 -0
  8. package/dist/v5/browser-tileset-source.d.ts +70 -0
  9. package/dist/v5/browser.cjs +1962 -0
  10. package/dist/v5/browser.d.ts +18 -0
  11. package/dist/v5/browser.js +1927 -0
  12. package/dist/v5/conversion-api.d.ts +122 -0
  13. package/dist/v5/feature-arrow.d.ts +36 -0
  14. package/dist/v5/index.cjs +3975 -0
  15. package/dist/v5/index.d.ts +18 -0
  16. package/dist/v5/index.js +3928 -0
  17. package/dist/v5/point-cloud-conversion.d.ts +31 -0
  18. package/dist/v5/point-cloud-source-encoder.d.ts +49 -0
  19. package/dist/v5/point-cloud-source.d.ts +25 -0
  20. package/dist/v5/point-cloud.d.ts +24 -0
  21. package/dist/v5/resource-manifest.d.ts +58 -0
  22. package/dist/v5/spatial-conversion.d.ts +59 -0
  23. package/dist/v5/tileset-source.d.ts +21 -0
  24. package/package.json +41 -26
  25. package/src/common/README.md +5 -0
  26. package/src/index.ts +1 -2
  27. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
  28. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
  29. package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
  30. package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
  31. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +15 -42
  32. package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
  33. package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
  34. package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
  35. package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
  36. package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
  37. package/src/v4/index.ts +2 -0
  38. package/src/v5/browser-sink.ts +129 -0
  39. package/src/v5/browser-source.ts +155 -0
  40. package/src/v5/browser-tileset-source.ts +328 -0
  41. package/src/v5/browser.ts +54 -0
  42. package/src/v5/conversion-api.ts +287 -0
  43. package/src/v5/feature-arrow.ts +427 -0
  44. package/src/v5/index.ts +47 -0
  45. package/src/v5/point-cloud-conversion.ts +80 -0
  46. package/src/v5/point-cloud-source-encoder.ts +102 -0
  47. package/src/v5/point-cloud-source.ts +84 -0
  48. package/src/v5/point-cloud.ts +271 -0
  49. package/src/v5/resource-manifest.ts +213 -0
  50. package/src/v5/spatial-conversion.ts +305 -0
  51. package/src/v5/tileset-source.ts +54 -0
  52. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
  53. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
  54. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
  55. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
  56. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
  57. /package/src/{constants.ts → v4/constants.ts} +0 -0
  58. /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
  59. /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
  60. /package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +0 -0
  61. /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
  62. /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
  63. /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
  64. /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
  65. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
  66. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
  67. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
  68. /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
  69. /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
  70. /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
  71. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
  72. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
  73. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
  74. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
  75. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
  76. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
  77. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
  78. /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
  79. /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
  80. /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
  81. /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
  82. /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
  83. /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
  84. /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
  85. /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
  86. /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
  87. /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
  88. /package/src/{lib → v4/lib}/utils/compress-util.ts +0 -0
  89. /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
  90. /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
  91. /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
  92. /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
  93. /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
  94. /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
  95. /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
  96. /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
@@ -21,7 +21,6 @@ import {Ellipsoid} from '@math.gl/geospatial';
21
21
  /* @ts-expect-error TS2732: Cannot find module 'node:module' or its corresponding type declarations. */
22
22
  import {createRequire} from 'node:module';
23
23
  /* @ts-expect-error TS2732: Cannot find module 'node:module' or its corresponding type declarations. */
24
- import {dirname, join} from 'node:path';
25
24
 
26
25
  import {DracoWriterWorker} from '@loaders.gl/draco';
27
26
  import {assert, encode} from '@loaders.gl/core';
@@ -34,6 +33,7 @@ import {
34
33
  import md5 from 'md5';
35
34
  import {v4 as uuidv4} from 'uuid';
36
35
  import {generateAttributes} from './geometry-attributes';
36
+ import {encodeStringAttribute} from './encode-string-attribute';
37
37
  import {createBoundingVolumesFromGeometry} from './coordinate-converter';
38
38
  import {
39
39
  ConvertedAttributes,
@@ -1028,15 +1028,18 @@ async function mergeMaterials(
1028
1028
  material1.mergedMaterials &&
1029
1029
  material2.mergedMaterials
1030
1030
  ) {
1031
- const buffer1 = Buffer.from(material1.texture.bufferView.data);
1032
- const buffer2 = Buffer.from(material2.texture.bufferView.data);
1031
+ const firstImageBytes = new Uint8Array(material1.texture.bufferView.data);
1032
+ const secondImageBytes = new Uint8Array(material2.texture.bufferView.data);
1033
1033
  try {
1034
1034
  // @ts-ignore
1035
1035
  const {joinImages} = await import('join-images');
1036
- const sharpData = await joinImages([buffer1, buffer2], {direction: 'horizontal'});
1037
- material1.texture.bufferView.data = await sharpData
1036
+ const sharpData = await joinImages([firstImageBytes, secondImageBytes], {
1037
+ direction: 'horizontal'
1038
+ });
1039
+ const encodedImage = await sharpData
1038
1040
  .toFormat(material1.texture.mimeType === 'image/png' ? 'png' : 'jpeg')
1039
1041
  .toBuffer();
1042
+ material1.texture.bufferView.data = new Uint8Array(encodedImage);
1040
1043
  } catch (error) {
1041
1044
  // eslint-disable-next-line no-console
1042
1045
  console.log(
@@ -1458,10 +1461,10 @@ function generateAttributeBuffer(type: string, value: any): ArrayBuffer {
1458
1461
  attributeBuffer = generateDoubleAttributeBuffer(value);
1459
1462
  break;
1460
1463
  case STRING_TYPE:
1461
- attributeBuffer = generateStringAttributeBuffer(value);
1464
+ attributeBuffer = encodeStringAttribute(value);
1462
1465
  break;
1463
1466
  default:
1464
- attributeBuffer = generateStringAttributeBuffer(value);
1467
+ attributeBuffer = encodeStringAttribute(value);
1465
1468
  }
1466
1469
 
1467
1470
  return attributeBuffer;
@@ -1497,7 +1500,7 @@ function getAttributeType(key: string, attributeStorageInfo: any[]): string {
1497
1500
  /**
1498
1501
  * Convert short integer to attribute arrayBuffer.
1499
1502
  * @param featureIds
1500
- * @returns - Buffer with objectId data.
1503
+ * @returns ArrayBuffer with objectId data.
1501
1504
  */
1502
1505
  function generateShortIntegerAttributeBuffer(featureIds: any[]): ArrayBuffer {
1503
1506
  const count = new Uint32Array([featureIds.length]);
@@ -1508,7 +1511,7 @@ function generateShortIntegerAttributeBuffer(featureIds: any[]): ArrayBuffer {
1508
1511
  /**
1509
1512
  * Convert double to attribute arrayBuffer.
1510
1513
  * @param featureIds
1511
- * @returns - Buffer with objectId data.
1514
+ * @returns ArrayBuffer with objectId data.
1512
1515
  */
1513
1516
  function generateDoubleAttributeBuffer(featureIds: any[]): ArrayBuffer {
1514
1517
  const count = new Uint32Array([featureIds.length]);
@@ -1518,36 +1521,6 @@ function generateDoubleAttributeBuffer(featureIds: any[]): ArrayBuffer {
1518
1521
  return concatenateArrayBuffers(count.buffer, padding.buffer, valuesArray.buffer);
1519
1522
  }
1520
1523
 
1521
- /**
1522
- * Convert batch table attributes to array buffer with batch table data.
1523
- * @param batchAttributes
1524
- * @returns - Buffer with batch table data.
1525
- */
1526
- function generateStringAttributeBuffer(batchAttributes: any[]): ArrayBuffer {
1527
- const stringCountArray = new Uint32Array([batchAttributes.length]);
1528
- let totalNumberOfBytes = 0;
1529
- const stringSizesArray = new Uint32Array(batchAttributes.length);
1530
- const stringBufferArray: ArrayBuffer[] = [];
1531
-
1532
- for (let index = 0; index < batchAttributes.length; index++) {
1533
- const currentString = `${String(batchAttributes[index])}\0`;
1534
- const currentStringBuffer = Buffer.from(currentString);
1535
- const currentStringSize = currentStringBuffer.length;
1536
- totalNumberOfBytes += currentStringSize;
1537
- stringSizesArray[index] = currentStringSize;
1538
- stringBufferArray.push(currentStringBuffer);
1539
- }
1540
-
1541
- const totalBytes = new Uint32Array([totalNumberOfBytes]);
1542
-
1543
- return concatenateArrayBuffers(
1544
- stringCountArray.buffer,
1545
- totalBytes.buffer,
1546
- stringSizesArray.buffer,
1547
- ...stringBufferArray
1548
- );
1549
- }
1550
-
1551
1524
  /**
1552
1525
  * Convert featureIds to BigUint64Array.
1553
1526
  * @param featureIds
@@ -1642,14 +1615,14 @@ async function generateCompressedGeometry(
1642
1615
  );
1643
1616
  }
1644
1617
 
1618
+ /** Resolve the published Node Draco writer, falling back to the dependency installer destination. */
1645
1619
  function getLocalDracoWriterWorkerUrl(): string {
1646
1620
  try {
1647
1621
  const require = createRequire(import.meta.url);
1648
- const packageJsonPath = require.resolve('@loaders.gl/draco/package.json');
1649
- return join(dirname(packageJsonPath), 'src/workers/draco-writer-worker-node.cjs');
1622
+ return require.resolve('@loaders.gl/draco/draco-writer-worker-node.cjs');
1650
1623
  } catch {
1651
1624
  // Fall back to monorepo-local path
1652
- return './modules/draco/src/workers/draco-writer-worker-node.cjs';
1625
+ return './modules/draco/dist/draco-writer-worker-node.cjs';
1653
1626
  }
1654
1627
  }
1655
1628
 
@@ -2,6 +2,7 @@ import type {Tiles3DTileContent} from '@loaders.gl/3d-tiles';
2
2
  import type {GLTFAccessorPostprocessed, GLTFNodePostprocessed} from '@loaders.gl/gltf';
3
3
  import {Matrix4, TypedArray, Vector3} from '@math.gl/core';
4
4
  import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling';
5
+ import {getImageData} from '@loaders.gl/images';
5
6
  import {Ellipsoid} from '@math.gl/geospatial';
6
7
  import {GLTFAttributesData} from '../types';
7
8
 
@@ -30,15 +31,15 @@ export function prepareDataForAttributesConversion(
30
31
  if (imageObject?.image?.compressed) {
31
32
  return null;
32
33
  }
33
- const data = imageObject?.image?.data;
34
+ const {data, width, height} = getImageData(imageObject.image);
34
35
  const dataCopy = new Uint8Array(data.length);
35
36
  dataCopy.set(data);
36
37
  return {
37
38
  data: dataCopy,
38
39
  compressed: false,
39
- height: imageObject.image.height,
40
- width: imageObject.image.width,
41
- components: imageObject.image.components,
40
+ height,
41
+ width,
42
+ components: data.length / (width * height),
42
43
  mimeType: imageObject.mimeType
43
44
  };
44
45
  }) || [];
@@ -119,15 +119,11 @@ export async function loadFromArchive(
119
119
  const hashTable = await loadHashTable(fileProvider);
120
120
  const archive = new Tiles3DArchive(fileProvider, hashTable, tz3Path);
121
121
  const fileSystem = new ZipFileSystem(archive);
122
- // const loadOptionsWithoutDeprecatedFetch = {...loadOptions} as Tiles3DLoaderOptions & {
123
- // fetch?: unknown;
124
- // };
125
- // delete loadOptionsWithoutDeprecatedFetch.fetch;
126
- // const mergedOptions = {
127
- // ...loadOptionsWithoutDeprecatedFetch,
128
- // core: {...loadOptions?.core, fetch: fileSystem.fetch.bind(fileSystem)}
129
- // }
130
- const content = await load(filename, loader, loadOptions);
122
+ const archiveLoadOptions = {
123
+ ...loadOptions,
124
+ core: {...loadOptions?.core, fetch: fileSystem.fetch.bind(fileSystem)}
125
+ };
126
+ const content = await load(filename, loader, archiveLoadOptions);
131
127
  await fileSystem.destroy();
132
128
  return content;
133
129
  }
@@ -49,7 +49,7 @@ import {validateNodeBoundingVolumes} from './helpers/node-debug';
49
49
  import {KTX2BasisWriterWorker} from '@loaders.gl/textures';
50
50
  import {LoaderWithParser} from '@loaders.gl/loader-utils';
51
51
  import {I3SMaterialDefinition, TextureSetDefinitionFormats} from '@loaders.gl/i3s';
52
- import {ImageWriter} from '@loaders.gl/images';
52
+ import {getImageData, ImageWriter} from '@loaders.gl/images';
53
53
  import {GLTFImagePostprocessed} from '@loaders.gl/gltf';
54
54
  import {
55
55
  GLTFPrimitiveModeString,
@@ -143,7 +143,7 @@ export default class I3SConverter {
143
143
  basis: {
144
144
  format: 'rgba32',
145
145
  // We need to load local fs workers because nodejs can't load workers from the Internet
146
- workerUrl: './modules/textures/dist/basis-worker-node.js'
146
+ workerUrl: './modules/textures/dist/basis-worker-node.cjs'
147
147
  },
148
148
  // We need to load local fs workers because nodejs can't load workers from the Internet
149
149
  draco: {workerUrl: './modules/draco/dist/draco-worker-node.js'},
@@ -1266,18 +1266,17 @@ export default class I3SConverter {
1266
1266
 
1267
1267
  if (this.generateTextures) {
1268
1268
  formats.push({name: '1', format: 'ktx2'});
1269
- // For Node.js texture.image.data is type of Buffer
1270
- const copyArrayBuffer = texture.image.data.subarray();
1271
- const arrayToEncode = new Uint8Array(copyArrayBuffer);
1269
+ const imageData = getImageData(texture.image);
1270
+ const arrayToEncode = new Uint8Array(imageData.data);
1272
1271
  const ktx2TextureData = encode(
1273
- {...texture.image, data: arrayToEncode},
1272
+ {...imageData, data: arrayToEncode},
1274
1273
  // @ts-expect-error - Worker encoder typing is still WIP
1275
1274
  KTX2BasisWriterWorker,
1276
1275
  {
1277
1276
  ...KTX2BasisWriterWorker.options,
1278
1277
  ['ktx2-basis-writer']: {
1279
1278
  // We need to load local fs workers because nodejs can't load workers from the Internet
1280
- workerUrl: './modules/textures/dist/ktx2-basis-writer-worker-node.js'
1279
+ workerUrl: './modules/textures/dist/ktx2-basis-writer-worker-node.cjs'
1281
1280
  },
1282
1281
  reuseWorkers: true,
1283
1282
  _nodeWorkers: true,
@@ -6,8 +6,8 @@ The server provides I3S Rest endpoints per specification https://github.com/Esri
6
6
  ## Development
7
7
 
8
8
  - install tsx globally `yarn global add tsx`
9
- - uncomment "local debug" lines in `modules/tile-converter/src/i3s-server/app.ts` and `modules/tile-converter/src/i3s-server/bin/www.ts` (see comments);
10
- - run `PORT=8080 HTTPS_PORT=4443 I3sLayerPath="./path/to/dataset" DEBUG=i3s-server:* npx tsx ./src/i3s-server/bin/www.ts`
9
+ - uncomment "local debug" lines in `src/v4/i3s-server/app.ts` and `src/v4/i3s-server/bin/www.ts` (see comments);
10
+ - run `PORT=8080 HTTPS_PORT=4443 I3sLayerPath="./path/to/dataset" DEBUG=i3s-server:* npx tsx ./src/v4/i3s-server/bin/www.ts`
11
11
 
12
12
  ## Usage
13
13
 
@@ -29,6 +29,7 @@ router.get('*', (req, res, next) => {
29
29
  const json = JSON.parse(textDecoder.decode(file));
30
30
  res.send(json);
31
31
  } catch (_e) {
32
+ // Express 4 sends typed arrays as JSON; adapt only at this Node.js HTTP boundary.
32
33
  res.send(Buffer.from(file));
33
34
  }
34
35
  } else {
@@ -0,0 +1,2 @@
1
+ export {default as I3SConverter} from './i3s-converter/i3s-converter';
2
+ export {default as Tiles3DConverter} from './3d-tiles-converter/3d-tiles-converter';
@@ -0,0 +1,129 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {TileConversionError} from './conversion-api.js';
6
+ import type {TileConversionReport, TileConversionSink} from './conversion-api.js';
7
+
8
+ const BLOB_SIZE_GETTER = Object.getOwnPropertyDescriptor(Blob.prototype, 'size')?.get;
9
+
10
+ /** One named browser output resource. */
11
+ export interface BrowserTileConversionResource {
12
+ /** Stable relative output path supplied by the target profile. */
13
+ readonly resourceId: string;
14
+ /** Parts combined into a Blob after the total output budget is checked. */
15
+ readonly parts: readonly BlobPart[];
16
+ /** MIME type recorded on the resulting Blob. */
17
+ readonly contentType?: string;
18
+ }
19
+
20
+ /** A named output resource retained by the bounded browser sink. */
21
+ export interface BrowserTileConversionFile {
22
+ /** Stable relative output path supplied by the target profile. */
23
+ readonly resourceId: string;
24
+ /** Browser-native data for download or application-provided packaging. */
25
+ readonly blob: Blob;
26
+ }
27
+
28
+ /** Memory limit for a browser conversion sink. */
29
+ export interface BoundedMemoryTileConversionSinkOptions {
30
+ /** Maximum combined Blob size retained by this sink, in bytes. */
31
+ readonly maxTotalBytes: number;
32
+ }
33
+
34
+ /** Browser sink with read access to its bounded output resources. */
35
+ export interface BoundedMemoryTileConversionSink
36
+ extends TileConversionSink<BrowserTileConversionResource> {
37
+ /** Returns the retained files in stable resource-ID order. */
38
+ getFiles(): readonly BrowserTileConversionFile[];
39
+ }
40
+
41
+ /**
42
+ * Creates a browser-only sink that retains output as Blobs under a total byte limit.
43
+ *
44
+ * The sink does not download or archive files. Applications can use `getFiles()` after
45
+ * `convertTileset` completes and package or download the returned Blobs as needed.
46
+ *
47
+ * @param options - Maximum in-memory output size.
48
+ * @returns A sink that stores browser-native Blobs and clears them on abort.
49
+ */
50
+ export function createBoundedMemoryTileConversionSink(
51
+ options: BoundedMemoryTileConversionSinkOptions
52
+ ): BoundedMemoryTileConversionSink {
53
+ if (!Number.isSafeInteger(options.maxTotalBytes) || options.maxTotalBytes < 0) {
54
+ throw new TileConversionError(
55
+ 'INVALID_OUTPUT_MEMORY_LIMIT',
56
+ 'The browser output memory limit must be a non-negative safe integer'
57
+ );
58
+ }
59
+
60
+ const files = new Map<string, BrowserTileConversionFile>();
61
+ let totalBytes = 0;
62
+ let closed = false;
63
+
64
+ return {
65
+ getFiles() {
66
+ return [...files.values()].sort((left, right) =>
67
+ left.resourceId < right.resourceId ? -1 : left.resourceId > right.resourceId ? 1 : 0
68
+ );
69
+ },
70
+ async write(resource, signal) {
71
+ if (signal?.aborted) {
72
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
73
+ }
74
+ if (closed) {
75
+ throw new TileConversionError(
76
+ 'RESOURCE_SINK_CLOSED',
77
+ 'Cannot write after sink finalization'
78
+ );
79
+ }
80
+ if (!resource.resourceId || files.has(resource.resourceId)) {
81
+ throw new TileConversionError(
82
+ 'INVALID_BROWSER_RESOURCE_ID',
83
+ 'Browser output resource IDs must be non-empty and unique'
84
+ );
85
+ }
86
+
87
+ const byteLength = resource.parts.reduce((total, part) => total + measureBlobPart(part), 0);
88
+ if (!Number.isSafeInteger(byteLength) || totalBytes + byteLength > options.maxTotalBytes) {
89
+ throw new TileConversionError(
90
+ 'OUTPUT_MEMORY_LIMIT_EXCEEDED',
91
+ `Browser output exceeds the configured ${options.maxTotalBytes} byte memory limit`
92
+ );
93
+ }
94
+
95
+ const blob = new Blob([...resource.parts], {type: resource.contentType});
96
+ files.set(resource.resourceId, {resourceId: resource.resourceId, blob});
97
+ totalBytes += blob.size;
98
+ },
99
+ async finalize(_report: TileConversionReport) {
100
+ if (closed) {
101
+ throw new TileConversionError('RESOURCE_SINK_CLOSED', 'Sink has already been finalized');
102
+ }
103
+ closed = true;
104
+ },
105
+ async abort() {
106
+ files.clear();
107
+ totalBytes = 0;
108
+ closed = true;
109
+ }
110
+ };
111
+ }
112
+
113
+ /** Measures supported Blob parts without copying them into a Blob first. */
114
+ function measureBlobPart(part: BlobPart): number {
115
+ if (typeof part === 'string') {
116
+ return new TextEncoder().encode(part).byteLength;
117
+ }
118
+ if (BLOB_SIZE_GETTER) {
119
+ try {
120
+ return BLOB_SIZE_GETTER.call(part) as number;
121
+ } catch {
122
+ // Keep checking the remaining supported Blob part types.
123
+ }
124
+ }
125
+ if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) {
126
+ return part.byteLength;
127
+ }
128
+ throw new TileConversionError('UNSUPPORTED_BROWSER_RESOURCE', 'Unsupported browser Blob part');
129
+ }
@@ -0,0 +1,155 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {TileConversionError} from './conversion-api.js';
6
+ import type {TileConversionSource} from './conversion-api.js';
7
+
8
+ /** Metadata available before a browser resource is read. */
9
+ export interface BrowserTileConversionInspection {
10
+ /** Stable resource path assigned to the input tile. */
11
+ readonly resourceId: string;
12
+ /** Optional MIME type supplied by the caller or source Blob. */
13
+ readonly contentType?: string;
14
+ }
15
+
16
+ /** Raw bytes read from one browser tile resource. */
17
+ export interface BrowserTileConversionInputResource {
18
+ /** Stable resource path assigned to the input tile. */
19
+ readonly resourceId: string;
20
+ /** Complete resource bytes for a profile-specific codec. */
21
+ readonly data: Uint8Array;
22
+ /** Optional MIME type supplied by the caller, Blob, or URL response. */
23
+ readonly contentType?: string;
24
+ }
25
+
26
+ /** Browser input and size limit for a single-resource conversion. */
27
+ export interface BrowserTileConversionSourceOptions {
28
+ /** Input URL or already available browser Blob. */
29
+ readonly input: string | Blob;
30
+ /** Stable path to associate with the input resource. */
31
+ readonly resourceId: string;
32
+ /** Maximum number of input bytes held in memory. */
33
+ readonly maxInputBytes: number;
34
+ /** Optional MIME type override. */
35
+ readonly contentType?: string;
36
+ }
37
+
38
+ /**
39
+ * Creates a browser source for one mesh resource provided as a URL or Blob.
40
+ *
41
+ * URL responses are read incrementally and canceled if they exceed `maxInputBytes`.
42
+ * Tileset JSON traversal and format decoding remain profile-specific source and codec work.
43
+ *
44
+ * @param options - Input, stable resource ID, and maximum input size.
45
+ * @returns A source that yields one complete resource to a v5 codec.
46
+ */
47
+ export function createBrowserTileConversionSource(
48
+ options: BrowserTileConversionSourceOptions
49
+ ): TileConversionSource<BrowserTileConversionInspection, BrowserTileConversionInputResource> {
50
+ if (
51
+ !options.resourceId ||
52
+ !Number.isSafeInteger(options.maxInputBytes) ||
53
+ options.maxInputBytes < 0
54
+ ) {
55
+ throw new TileConversionError(
56
+ 'INVALID_BROWSER_SOURCE_OPTIONS',
57
+ 'Browser sources require a resource ID and a non-negative safe input byte limit'
58
+ );
59
+ }
60
+
61
+ return {
62
+ async inspect(signal) {
63
+ throwIfAborted(signal);
64
+ const contentType = options.contentType ?? getBlobContentType(options.input);
65
+ return {
66
+ resourceId: options.resourceId,
67
+ ...(contentType ? {contentType} : {})
68
+ };
69
+ },
70
+ async *read(inspection, signal) {
71
+ throwIfAborted(signal);
72
+ const input = options.input;
73
+ let data: Uint8Array;
74
+ let contentType = inspection.contentType;
75
+
76
+ if (typeof input === 'string') {
77
+ const response = await fetch(input, {signal});
78
+ if (!response.ok) {
79
+ throw new TileConversionError(
80
+ 'BROWSER_SOURCE_FETCH_FAILED',
81
+ `Could not fetch browser input (${response.status} ${response.statusText})`
82
+ );
83
+ }
84
+ contentType = contentType ?? response.headers.get('content-type') ?? undefined;
85
+ data = await readResponseBytes(response, options.maxInputBytes, signal);
86
+ } else {
87
+ if (input.size > options.maxInputBytes) {
88
+ throw createInputLimitError(options.maxInputBytes);
89
+ }
90
+ data = new Uint8Array(await input.arrayBuffer());
91
+ }
92
+
93
+ throwIfAborted(signal);
94
+ yield {
95
+ resourceId: inspection.resourceId,
96
+ data,
97
+ ...(contentType ? {contentType} : {})
98
+ };
99
+ }
100
+ };
101
+ }
102
+
103
+ /** Reads a response stream while enforcing the input byte limit. */
104
+ async function readResponseBytes(
105
+ response: Response,
106
+ maxInputBytes: number,
107
+ signal?: AbortSignal
108
+ ): Promise<Uint8Array> {
109
+ const reader = response.body?.getReader();
110
+ if (!reader) {
111
+ throw new TileConversionError(
112
+ 'BROWSER_SOURCE_STREAM_UNAVAILABLE',
113
+ 'Browser URL inputs must provide a readable response stream'
114
+ );
115
+ }
116
+
117
+ const data = new Uint8Array(maxInputBytes);
118
+ let totalBytes = 0;
119
+ try {
120
+ while (true) {
121
+ throwIfAborted(signal);
122
+ const {done, value} = await reader.read();
123
+ if (done) break;
124
+ if (value.byteLength > maxInputBytes - totalBytes) {
125
+ await reader.cancel();
126
+ throw createInputLimitError(maxInputBytes);
127
+ }
128
+ data.set(value, totalBytes);
129
+ totalBytes += value.byteLength;
130
+ }
131
+ } finally {
132
+ reader.releaseLock();
133
+ }
134
+ return data.subarray(0, totalBytes);
135
+ }
136
+
137
+ /** Returns a Blob's MIME type without requiring a same-realm Blob constructor. */
138
+ function getBlobContentType(input: string | Blob): string | undefined {
139
+ return typeof input === 'string' ? undefined : input.type || undefined;
140
+ }
141
+
142
+ /** Creates the typed error used when browser input exceeds its configured limit. */
143
+ function createInputLimitError(maxInputBytes: number): TileConversionError {
144
+ return new TileConversionError(
145
+ 'INPUT_RESOURCE_TOO_LARGE',
146
+ `Browser input exceeds the configured ${maxInputBytes} byte memory limit`
147
+ );
148
+ }
149
+
150
+ /** Throws the signal reason when a browser source operation has been canceled. */
151
+ function throwIfAborted(signal?: AbortSignal): void {
152
+ if (signal?.aborted) {
153
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
154
+ }
155
+ }