@loaders.gl/tile-converter 5.0.0-alpha.7 → 5.0.0-alpha.9
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 +389 -0
- package/dist/converter.min.cjs +194 -175
- package/dist/i3s-server/bin/i3s-server.min.cjs +84 -84
- package/dist/index.cjs +37747 -0
- package/dist/index.js +37718 -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 +10165 -0
- package/dist/v5/browser.d.ts +30 -0
- package/dist/v5/browser.js +10140 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/conversion-archive.d.ts +18 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/index.cjs +11446 -0
- package/dist/v5/index.d.ts +28 -0
- package/dist/v5/index.js +11410 -0
- package/dist/v5/mesh-conversion.d.ts +55 -0
- package/dist/v5/mesh-tileset-archive.d.ts +16 -0
- package/dist/v5/mesh-tileset-sink.d.ts +31 -0
- package/dist/v5/mesh.d.ts +73 -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 +130 -0
- package/src/v5/browser-source.ts +155 -0
- package/src/v5/browser-tileset-source.ts +328 -0
- package/src/v5/browser.ts +77 -0
- package/src/v5/conversion-api.ts +287 -0
- package/src/v5/conversion-archive.ts +51 -0
- package/src/v5/feature-arrow.ts +427 -0
- package/src/v5/index.ts +68 -0
- package/src/v5/mesh-conversion.ts +177 -0
- package/src/v5/mesh-tileset-archive.ts +48 -0
- package/src/v5/mesh-tileset-sink.ts +178 -0
- package/src/v5/mesh.ts +503 -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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {parseWKTCRS, type WKTCRSNode} from '@math.gl/crs';
|
|
6
|
+
import {getSpatialCoordinateFrame} from '@loaders.gl/tiles';
|
|
7
|
+
|
|
8
|
+
/** Horizontal CRS declarations consumed by the legacy I3S converter. */
|
|
9
|
+
export type I3SConversionSpatialReference = {
|
|
10
|
+
/** Layer-level horizontal coordinate reference system. */
|
|
11
|
+
spatialReference?: {
|
|
12
|
+
/** Original spatial reference identifier. */
|
|
13
|
+
wkid?: number;
|
|
14
|
+
/** Current spatial reference identifier. */
|
|
15
|
+
latestWkid?: number;
|
|
16
|
+
/** Custom spatial reference definition. */
|
|
17
|
+
wkt?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Fallback CRS declaration on the layer extent. */
|
|
20
|
+
fullExtent?: {
|
|
21
|
+
/** Coordinate system shared by the extent and layer. */
|
|
22
|
+
spatialReference?: I3SConversionSpatialReference['spatialReference'];
|
|
23
|
+
};
|
|
24
|
+
/** Coordinate systems declared for node bounds and geometry. */
|
|
25
|
+
store?: {
|
|
26
|
+
/** Coordinate system used by node bounds. */
|
|
27
|
+
indexCRS?: string;
|
|
28
|
+
/** Coordinate system used by geometry positions. */
|
|
29
|
+
vertexCRS?: string;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reject explicit horizontal CRS declarations that the legacy converter cannot transform.
|
|
35
|
+
* Undeclared CRS metadata retains the historical WGS-84 assumption.
|
|
36
|
+
* @param layer - Source layer metadata, before interpreting any coordinates as longitude/latitude.
|
|
37
|
+
*/
|
|
38
|
+
export function validateI3SConversionSpatialReference(layer: I3SConversionSpatialReference): void {
|
|
39
|
+
const spatialReferences = [layer.spatialReference, layer.fullExtent?.spatialReference];
|
|
40
|
+
const unsupportedSpatialReference = spatialReferences.find(spatialReference => {
|
|
41
|
+
if (!spatialReference) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const identifier = spatialReference.latestWkid ?? spatialReference.wkid;
|
|
45
|
+
return (
|
|
46
|
+
(identifier !== undefined && identifier !== 4326 && identifier !== 4979) ||
|
|
47
|
+
(spatialReference.wkt !== undefined && !isWgs84GeographicWkt(spatialReference.wkt))
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
const declarations = [layer.store?.indexCRS, layer.store?.vertexCRS].filter(
|
|
51
|
+
(declaration): declaration is string => Boolean(declaration)
|
|
52
|
+
);
|
|
53
|
+
const unsupportedDeclaration = declarations.find(
|
|
54
|
+
declaration =>
|
|
55
|
+
!/^(?:EPSG:(?:4326|4979)|urn:ogc:def:crs:EPSG::(?:4326|4979)|https?:\/\/www\.opengis\.net\/def\/crs\/EPSG\/0\/(?:4326|4979)\/?)$/i.test(
|
|
56
|
+
declaration
|
|
57
|
+
)
|
|
58
|
+
);
|
|
59
|
+
if (unsupportedSpatialReference || unsupportedDeclaration) {
|
|
60
|
+
const identifier = unsupportedSpatialReference?.latestWkid ?? unsupportedSpatialReference?.wkid;
|
|
61
|
+
const description =
|
|
62
|
+
unsupportedDeclaration || (identifier !== undefined ? `EPSG:${identifier}` : 'custom WKT');
|
|
63
|
+
throw new Error(
|
|
64
|
+
`The legacy I3S-to-3D Tiles converter requires WGS-84 (EPSG:4326) input; received ${description}. Reproject the source I3S dataset to EPSG:4326 before conversion.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Recognize geographic WGS-84 WKT with degree units and the Greenwich prime meridian. */
|
|
70
|
+
function isWgs84GeographicWkt(wellKnownText: string): boolean {
|
|
71
|
+
try {
|
|
72
|
+
if (getSpatialCoordinateFrame(wellKnownText) !== 'geographic') {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const {root} = parseWKTCRS(wellKnownText);
|
|
76
|
+
const nodes = collectWktNodes(root);
|
|
77
|
+
const datum = root.values.find(
|
|
78
|
+
value =>
|
|
79
|
+
value.type === 'node' &&
|
|
80
|
+
['DATUM', 'ENSEMBLE', 'GEODETICDATUM'].includes(value.keyword.toUpperCase())
|
|
81
|
+
);
|
|
82
|
+
if (datum?.type !== 'node' || datum.values[0]?.type !== 'string') {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const datumName = datum.values[0].value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
86
|
+
if (
|
|
87
|
+
![
|
|
88
|
+
'wgs84',
|
|
89
|
+
'wgs1984',
|
|
90
|
+
'dwgs1984',
|
|
91
|
+
'worldgeodeticsystem1984',
|
|
92
|
+
'worldgeodeticsystem1984ensemble'
|
|
93
|
+
].includes(datumName)
|
|
94
|
+
) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
const angularUnits = nodes.filter(
|
|
98
|
+
node =>
|
|
99
|
+
['UNIT', 'ANGLEUNIT'].includes(node.keyword.toUpperCase()) &&
|
|
100
|
+
(node === root || root.values.includes(node) || node.keyword.toUpperCase() === 'ANGLEUNIT')
|
|
101
|
+
);
|
|
102
|
+
const primeMeridian = nodes.find(node => node.keyword.toUpperCase() === 'PRIMEM');
|
|
103
|
+
return (
|
|
104
|
+
angularUnits.length > 0 &&
|
|
105
|
+
angularUnits.every(
|
|
106
|
+
node =>
|
|
107
|
+
node.values[1]?.type === 'number' &&
|
|
108
|
+
Math.abs(node.values[1].value - Math.PI / 180) < 1e-12
|
|
109
|
+
) &&
|
|
110
|
+
(!primeMeridian ||
|
|
111
|
+
(primeMeridian.values[1]?.type === 'number' && primeMeridian.values[1].value === 0))
|
|
112
|
+
);
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Collect WKT nodes for semantic checks without inspecting quoted text as syntax. */
|
|
119
|
+
function collectWktNodes(node: WKTCRSNode): WKTCRSNode[] {
|
|
120
|
+
return [
|
|
121
|
+
node,
|
|
122
|
+
...node.values.flatMap(value => (value.type === 'node' ? collectWktNodes(value) : []))
|
|
123
|
+
];
|
|
124
|
+
}
|
|
@@ -48,13 +48,13 @@ export class DepsInstaller {
|
|
|
48
48
|
await this.installFromNpm('draco', 'draco-worker-node.js');
|
|
49
49
|
|
|
50
50
|
console.log('Installing "Draco Writer" worker'); // eslint-disable-line no-console
|
|
51
|
-
await this.installFromNpm('draco', 'draco-writer-worker-node.
|
|
51
|
+
await this.installFromNpm('draco', 'draco-writer-worker-node.cjs');
|
|
52
52
|
|
|
53
53
|
console.log('Installing "Basis Loader" worker'); // eslint-disable-line no-console
|
|
54
|
-
await this.installFromNpm('textures', 'basis-worker-node.
|
|
54
|
+
await this.installFromNpm('textures', 'basis-worker-node.cjs');
|
|
55
55
|
|
|
56
56
|
console.log('Installing "KTX2 Basis Writer" worker'); // eslint-disable-line no-console
|
|
57
|
-
await this.installFromNpm('textures', 'ktx2-basis-writer-worker-node.
|
|
57
|
+
await this.installFromNpm('textures', 'ktx2-basis-writer-worker-node.cjs');
|
|
58
58
|
|
|
59
59
|
console.log('Installing "Draco decoder" library'); // eslint-disable-line no-console
|
|
60
60
|
await this.installFromUrl(
|
|
@@ -101,13 +101,16 @@ export class DepsInstaller {
|
|
|
101
101
|
console.log('All dependencies were installed succesfully.'); // eslint-disable-line no-console
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/** Download a published worker or library into the converter's local resource directory. */
|
|
104
105
|
private async installFromNpm(module: string, name: string, extraPath: string = '') {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
);
|
|
106
|
+
const resourcePath = ['dist', extraPath, name].filter(Boolean).join('/');
|
|
107
|
+
const resourceUrl = `https://unpkg.com/@loaders.gl/${module}@${VERSION}/${resourcePath}`;
|
|
108
|
+
const fileResponse = await fetchFile(resourceUrl);
|
|
108
109
|
|
|
109
110
|
if (fileResponse.status < 200 || fileResponse.status >= 300) {
|
|
110
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
`Failed to load resource ${name} from ${resourceUrl}: HTTP ${fileResponse.status}`
|
|
113
|
+
);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
const fileData = await fileResponse.arrayBuffer();
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes I3S string attributes as little-endian counts followed by UTF-8 strings.
|
|
3
|
+
* Each string's byte count includes its trailing null byte. Values retain the
|
|
4
|
+
* converter's existing JavaScript string coercion, including null and undefined.
|
|
5
|
+
*/
|
|
6
|
+
export function encodeStringAttribute(values: readonly unknown[]): ArrayBuffer {
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
const encodedValues = Array.from(values, value => textEncoder.encode(`${String(value)}\0`));
|
|
9
|
+
const stringByteLength = encodedValues.reduce((total, value) => total + value.byteLength, 0);
|
|
10
|
+
const headerByteLength = (2 + values.length) * Uint32Array.BYTES_PER_ELEMENT;
|
|
11
|
+
const output = new Uint8Array(headerByteLength + stringByteLength);
|
|
12
|
+
const header = new DataView(output.buffer);
|
|
13
|
+
header.setUint32(0, values.length, true);
|
|
14
|
+
header.setUint32(4, stringByteLength, true);
|
|
15
|
+
|
|
16
|
+
let byteOffset = headerByteLength;
|
|
17
|
+
for (const [index, encodedValue] of encodedValues.entries()) {
|
|
18
|
+
header.setUint32(8 + index * Uint32Array.BYTES_PER_ELEMENT, encodedValue.byteLength, true);
|
|
19
|
+
output.set(encodedValue, byteOffset);
|
|
20
|
+
byteOffset += encodedValue.byteLength;
|
|
21
|
+
}
|
|
22
|
+
return output.buffer;
|
|
23
|
+
}
|
|
@@ -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,130 @@
|
|
|
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, captured at creation, 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
|
+
const {maxTotalBytes} = options;
|
|
54
|
+
if (!Number.isSafeInteger(maxTotalBytes) || maxTotalBytes < 0) {
|
|
55
|
+
throw new TileConversionError(
|
|
56
|
+
'INVALID_OUTPUT_MEMORY_LIMIT',
|
|
57
|
+
'The browser output memory limit must be a non-negative safe integer'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const files = new Map<string, BrowserTileConversionFile>();
|
|
62
|
+
let totalBytes = 0;
|
|
63
|
+
let closed = false;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
getFiles() {
|
|
67
|
+
return [...files.values()].sort((left, right) =>
|
|
68
|
+
left.resourceId < right.resourceId ? -1 : left.resourceId > right.resourceId ? 1 : 0
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
async write(resource, signal) {
|
|
72
|
+
if (signal?.aborted) {
|
|
73
|
+
throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
|
|
74
|
+
}
|
|
75
|
+
if (closed) {
|
|
76
|
+
throw new TileConversionError(
|
|
77
|
+
'RESOURCE_SINK_CLOSED',
|
|
78
|
+
'Cannot write after sink finalization'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (!resource.resourceId || files.has(resource.resourceId)) {
|
|
82
|
+
throw new TileConversionError(
|
|
83
|
+
'INVALID_BROWSER_RESOURCE_ID',
|
|
84
|
+
'Browser output resource IDs must be non-empty and unique'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const byteLength = resource.parts.reduce((total, part) => total + measureBlobPart(part), 0);
|
|
89
|
+
if (!Number.isSafeInteger(byteLength) || totalBytes + byteLength > maxTotalBytes) {
|
|
90
|
+
throw new TileConversionError(
|
|
91
|
+
'OUTPUT_MEMORY_LIMIT_EXCEEDED',
|
|
92
|
+
`Browser output exceeds the configured ${maxTotalBytes} byte memory limit`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const blob = new Blob([...resource.parts], {type: resource.contentType});
|
|
97
|
+
files.set(resource.resourceId, {resourceId: resource.resourceId, blob});
|
|
98
|
+
totalBytes += blob.size;
|
|
99
|
+
},
|
|
100
|
+
async finalize(_report: TileConversionReport) {
|
|
101
|
+
if (closed) {
|
|
102
|
+
throw new TileConversionError('RESOURCE_SINK_CLOSED', 'Sink has already been finalized');
|
|
103
|
+
}
|
|
104
|
+
closed = true;
|
|
105
|
+
},
|
|
106
|
+
async abort() {
|
|
107
|
+
files.clear();
|
|
108
|
+
totalBytes = 0;
|
|
109
|
+
closed = true;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Measures supported Blob parts without copying them into a Blob first. */
|
|
115
|
+
function measureBlobPart(part: BlobPart): number {
|
|
116
|
+
if (typeof part === 'string') {
|
|
117
|
+
return new TextEncoder().encode(part).byteLength;
|
|
118
|
+
}
|
|
119
|
+
if (BLOB_SIZE_GETTER) {
|
|
120
|
+
try {
|
|
121
|
+
return BLOB_SIZE_GETTER.call(part) as number;
|
|
122
|
+
} catch {
|
|
123
|
+
// Keep checking the remaining supported Blob part types.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) {
|
|
127
|
+
return part.byteLength;
|
|
128
|
+
}
|
|
129
|
+
throw new TileConversionError('UNSUPPORTED_BROWSER_RESOURCE', 'Unsupported browser Blob part');
|
|
130
|
+
}
|