@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.
- package/README.md +155 -0
- package/dist/converter.min.cjs +194 -175
- package/dist/i3s-server/bin/i3s-server.min.cjs +84 -84
- package/dist/index.cjs +35865 -0
- package/dist/index.js +35837 -0
- package/dist/v5/browser-sink.d.ts +37 -0
- package/dist/v5/browser-source.d.ts +38 -0
- package/dist/v5/browser-tileset-source.d.ts +70 -0
- package/dist/v5/browser.cjs +1962 -0
- package/dist/v5/browser.d.ts +18 -0
- package/dist/v5/browser.js +1927 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/index.cjs +3975 -0
- package/dist/v5/index.d.ts +18 -0
- package/dist/v5/index.js +3928 -0
- package/dist/v5/point-cloud-conversion.d.ts +31 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +49 -0
- package/dist/v5/point-cloud-source.d.ts +25 -0
- package/dist/v5/point-cloud.d.ts +24 -0
- package/dist/v5/resource-manifest.d.ts +58 -0
- package/dist/v5/spatial-conversion.d.ts +59 -0
- package/dist/v5/tileset-source.d.ts +21 -0
- package/package.json +41 -26
- package/src/common/README.md +5 -0
- package/src/index.ts +1 -2
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
- package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
- package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +15 -42
- package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
- package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
- package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
- package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
- package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
- package/src/v4/index.ts +2 -0
- package/src/v5/browser-sink.ts +129 -0
- package/src/v5/browser-source.ts +155 -0
- package/src/v5/browser-tileset-source.ts +328 -0
- package/src/v5/browser.ts +54 -0
- package/src/v5/conversion-api.ts +287 -0
- package/src/v5/feature-arrow.ts +427 -0
- package/src/v5/index.ts +47 -0
- package/src/v5/point-cloud-conversion.ts +80 -0
- package/src/v5/point-cloud-source-encoder.ts +102 -0
- package/src/v5/point-cloud-source.ts +84 -0
- package/src/v5/point-cloud.ts +271 -0
- package/src/v5/resource-manifest.ts +213 -0
- package/src/v5/spatial-conversion.ts +305 -0
- package/src/v5/tileset-source.ts +54 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
- /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
- /package/src/{constants.ts → v4/constants.ts} +0 -0
- /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
- /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
- /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
- /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
- /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
- /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/compress-util.ts +0 -0
- /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
- /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
- /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
- /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
- /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
|
|
1032
|
-
const
|
|
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([
|
|
1037
|
-
|
|
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 =
|
|
1464
|
+
attributeBuffer = encodeStringAttribute(value);
|
|
1462
1465
|
break;
|
|
1463
1466
|
default:
|
|
1464
|
-
attributeBuffer =
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
|
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
|
|
40
|
-
width
|
|
41
|
-
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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.
|
|
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
|
-
|
|
1270
|
-
const
|
|
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
|
-
{...
|
|
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.
|
|
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 `
|
|
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 {
|
package/src/v4/index.ts
ADDED
|
@@ -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
|
+
}
|