@loaders.gl/tile-converter 4.5.3 → 5.0.0-alpha.10
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/LICENSE +14 -3
- package/README.md +464 -0
- package/dist/converter.min.cjs +199 -260
- package/dist/i3s-server/bin/i3s-server.min.cjs +106 -341
- package/dist/index.cjs +38482 -0
- package/dist/index.js +38453 -0
- package/dist/v5/adapters.cjs +1992 -0
- package/dist/v5/adapters.d.ts +22 -0
- package/dist/v5/adapters.js +1972 -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 +64 -0
- package/dist/v5/browser.d.ts +34 -0
- package/dist/v5/browser.js +57 -0
- package/dist/v5/conversion-api.d.ts +122 -0
- package/dist/v5/conversion-archive.d.ts +32 -0
- package/dist/v5/core.cjs +863 -0
- package/dist/v5/core.d.ts +16 -0
- package/dist/v5/core.js +845 -0
- package/dist/v5/feature-arrow.d.ts +36 -0
- package/dist/v5/i3s-mesh-conversion.d.ts +26 -0
- package/dist/v5/index.cjs +63 -0
- package/dist/v5/index.d.ts +32 -0
- package/dist/v5/index.js +59 -0
- package/dist/v5/mesh-conversion.d.ts +62 -0
- package/dist/v5/mesh-draco.d.ts +8 -0
- package/dist/v5/mesh-source-features.d.ts +21 -0
- package/dist/v5/mesh-source-texture.d.ts +4 -0
- package/dist/v5/mesh-source.d.ts +34 -0
- package/dist/v5/mesh-tileset-archive.d.ts +18 -0
- package/dist/v5/mesh-tileset-sink.d.ts +51 -0
- package/dist/v5/mesh.d.ts +81 -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 +53 -27
- 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 +14 -3
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
- package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +4 -4
- package/src/{converter-cli.ts → v4/converter-cli.ts} +4 -4
- package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
- package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +1 -1
- package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +34 -59
- package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +7 -6
- package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
- package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +5 -6
- package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +16 -19
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +4 -4
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +6 -6
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +1 -1
- package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +1 -1
- package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
- package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +1 -1
- package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +2 -1
- package/src/v4/index.ts +2 -0
- package/src/{lib → v4/lib}/utils/compress-util.ts +8 -7
- package/src/{lib → v4/lib}/utils/conversion-dump.ts +4 -4
- package/src/{lib → v4/lib}/utils/file-utils.ts +2 -2
- package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +2 -4
- package/src/v5/adapters.ts +56 -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 +105 -0
- package/src/v5/conversion-api.ts +287 -0
- package/src/v5/conversion-archive.ts +85 -0
- package/src/v5/core.ts +61 -0
- package/src/v5/feature-arrow.ts +429 -0
- package/src/v5/i3s-mesh-conversion.ts +152 -0
- package/src/v5/index.ts +99 -0
- package/src/v5/mesh-conversion.ts +200 -0
- package/src/v5/mesh-draco.ts +138 -0
- package/src/v5/mesh-source-features.ts +232 -0
- package/src/v5/mesh-source-texture.ts +122 -0
- package/src/v5/mesh-source.ts +360 -0
- package/src/v5/mesh-tileset-archive.ts +50 -0
- package/src/v5/mesh-tileset-sink.ts +324 -0
- package/src/v5/mesh.ts +517 -0
- package/src/v5/point-cloud-conversion.ts +80 -0
- package/src/v5/point-cloud-source-encoder.ts +105 -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/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/{constants.ts → v4/constants.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/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/metadata.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}/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/geometry-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,105 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
PointCloudBoundingVolume,
|
|
7
|
+
PointCloudCoordinateSystem,
|
|
8
|
+
PointCloudTilesetSource,
|
|
9
|
+
TilesetSpatialReference
|
|
10
|
+
} from '@loaders.gl/tiles';
|
|
11
|
+
import {encodePointCloudTile} from './point-cloud.js';
|
|
12
|
+
import type {EncodePointCloudTileOptions} from './point-cloud.js';
|
|
13
|
+
import {traversePointCloudSource} from '@loaders.gl/tile-converter/v5/core';
|
|
14
|
+
import type {
|
|
15
|
+
PointCloudSourceTile,
|
|
16
|
+
TraversePointCloudSourceOptions
|
|
17
|
+
} from '@loaders.gl/tile-converter/v5/core';
|
|
18
|
+
|
|
19
|
+
/** Traversal and per-tile encoding options for a point-cloud source. */
|
|
20
|
+
export type EncodePointCloudSourceOptions = TraversePointCloudSourceOptions & {
|
|
21
|
+
/** Select encoder options for each source tile with content, using its header and decoded data. */
|
|
22
|
+
readonly getTileEncodingOptions?: (
|
|
23
|
+
sourceTile: PointCloudSourceTile
|
|
24
|
+
) => EncodePointCloudTileOptions;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A PNTS resource paired with the source placement metadata needed to package its tile. */
|
|
28
|
+
export type EncodedPointCloudSourceTile = {
|
|
29
|
+
/** Stable source tile identifier. */
|
|
30
|
+
readonly id: string;
|
|
31
|
+
/** Original source hierarchy and bounds. */
|
|
32
|
+
readonly header: PointCloudSourceTile['header'];
|
|
33
|
+
/** Encoded point-cloud tile content. */
|
|
34
|
+
readonly pnts: ArrayBuffer;
|
|
35
|
+
/** Number of point records encoded. */
|
|
36
|
+
readonly pointCount: number;
|
|
37
|
+
/** Coordinate system used by the returned point positions. */
|
|
38
|
+
readonly coordinateSystem: PointCloudCoordinateSystem;
|
|
39
|
+
/** Origin to add to source-relative point positions. */
|
|
40
|
+
readonly cartographicOrigin: readonly number[];
|
|
41
|
+
/** Optional placement transform supplied by the point-cloud source. */
|
|
42
|
+
readonly modelMatrix?: readonly number[];
|
|
43
|
+
/** Optional source or target CRS metadata. */
|
|
44
|
+
readonly spatialReference?: TilesetSpatialReference;
|
|
45
|
+
/** Optional output-frame tile bounds supplied by the source. */
|
|
46
|
+
readonly spatialBoundingVolume?: PointCloudBoundingVolume;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Encodes a point-cloud source as a stream of PNTS resources, one for each non-empty source tile.
|
|
51
|
+
*
|
|
52
|
+
* @param source - An I3S, COPC, or compatible point-cloud tileset source.
|
|
53
|
+
* @param options - Optional traversal depth limit, cancellation signal, and per-tile encoder options.
|
|
54
|
+
* @returns Encoded source tiles in deterministic depth-first order.
|
|
55
|
+
*/
|
|
56
|
+
export async function* encodePointCloudSource(
|
|
57
|
+
source: PointCloudTilesetSource,
|
|
58
|
+
options: EncodePointCloudSourceOptions = {}
|
|
59
|
+
): AsyncIterableIterator<EncodedPointCloudSourceTile> {
|
|
60
|
+
for await (const sourceTile of traversePointCloudSource(source, options)) {
|
|
61
|
+
const encodingOptions = sourceTile.content
|
|
62
|
+
? options.getTileEncodingOptions?.(sourceTile)
|
|
63
|
+
: undefined;
|
|
64
|
+
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
65
|
+
if (encodedTile) {
|
|
66
|
+
yield encodedTile;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Encodes one decoded source tile as PNTS and retains its hierarchy and placement metadata.
|
|
73
|
+
*
|
|
74
|
+
* Positions use the source coordinate system, with an optional RTC center subtracted before float32
|
|
75
|
+
* encoding and stored in the PNTS feature table. Source placement metadata is retained so callers
|
|
76
|
+
* can assemble the output tileset. This function does not transform the source CRS.
|
|
77
|
+
*
|
|
78
|
+
* @param sourceTile - Tile header and decoded Arrow point content from `traversePointCloudSource`.
|
|
79
|
+
* @param options - Optional RTC center, batch-table values, and constant color override.
|
|
80
|
+
* @returns An encoded source tile, or null when the source tile has no content.
|
|
81
|
+
*/
|
|
82
|
+
export function encodePointCloudSourceTile(
|
|
83
|
+
sourceTile: PointCloudSourceTile,
|
|
84
|
+
options: EncodePointCloudTileOptions = {}
|
|
85
|
+
): EncodedPointCloudSourceTile | null {
|
|
86
|
+
const {header, content} = sourceTile;
|
|
87
|
+
if (!content) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
id: header.id,
|
|
93
|
+
header,
|
|
94
|
+
pnts: encodePointCloudTile(content.data, {
|
|
95
|
+
...options,
|
|
96
|
+
constantRGBA: options.constantRGBA ?? content.constantRGBA
|
|
97
|
+
}),
|
|
98
|
+
pointCount: content.pointCount,
|
|
99
|
+
coordinateSystem: content.coordinateSystem,
|
|
100
|
+
cartographicOrigin: [...content.cartographicOrigin],
|
|
101
|
+
modelMatrix: content.modelMatrix ? Array.from(content.modelMatrix) : undefined,
|
|
102
|
+
spatialReference: content.spatialReference,
|
|
103
|
+
spatialBoundingVolume: content.spatialBoundingVolume || header.spatialBoundingVolume
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {
|
|
6
|
+
PointCloudTileContent,
|
|
7
|
+
PointCloudTileHeader,
|
|
8
|
+
PointCloudTilesetSource
|
|
9
|
+
} from '@loaders.gl/tiles';
|
|
10
|
+
|
|
11
|
+
/** A source tile and its decoded point content, including null content when no payload exists. */
|
|
12
|
+
export type PointCloudSourceTile = {
|
|
13
|
+
/** Header with the source tile's hierarchy, bounds, and LOD metadata. */
|
|
14
|
+
readonly header: PointCloudTileHeader;
|
|
15
|
+
/** Arrow point data and its coordinate metadata, or null for an empty tile. */
|
|
16
|
+
readonly content: PointCloudTileContent | null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Options for camera-independent point-cloud source traversal. */
|
|
20
|
+
export type TraversePointCloudSourceOptions = {
|
|
21
|
+
/** Stop descending after this source tile level. Defaults to the complete hierarchy. */
|
|
22
|
+
readonly maxDepth?: number;
|
|
23
|
+
/** Cancel traversal between source operations. */
|
|
24
|
+
readonly signal?: AbortSignal;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads a point-cloud source in deterministic depth-first order, independent of a render camera.
|
|
29
|
+
* Each yielded value retains its tile header and Arrow content metadata so callers can apply the
|
|
30
|
+
* source coordinate frame and placement when encoding output tiles.
|
|
31
|
+
*
|
|
32
|
+
* @param source - An I3S, COPC, or compatible point-cloud tileset source.
|
|
33
|
+
* @param options - Optional depth limit and cancellation signal.
|
|
34
|
+
* @returns One header and decoded payload for each reachable source tile.
|
|
35
|
+
*/
|
|
36
|
+
export async function* traversePointCloudSource(
|
|
37
|
+
source: PointCloudTilesetSource,
|
|
38
|
+
options: TraversePointCloudSourceOptions = {}
|
|
39
|
+
): AsyncIterableIterator<PointCloudSourceTile> {
|
|
40
|
+
const {maxDepth = Number.POSITIVE_INFINITY, signal} = options;
|
|
41
|
+
if (maxDepth !== Number.POSITIVE_INFINITY && (!Number.isSafeInteger(maxDepth) || maxDepth < 0)) {
|
|
42
|
+
throw new RangeError('Point-cloud source maxDepth must be a non-negative integer');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throwIfAborted(signal);
|
|
46
|
+
if (!source.isReady) {
|
|
47
|
+
await source.initialize();
|
|
48
|
+
}
|
|
49
|
+
throwIfAborted(signal);
|
|
50
|
+
|
|
51
|
+
const root = await source.getRootTile();
|
|
52
|
+
const pendingTiles = [root];
|
|
53
|
+
const visitedTileIds = new Set<string>();
|
|
54
|
+
|
|
55
|
+
while (pendingTiles.length > 0) {
|
|
56
|
+
throwIfAborted(signal);
|
|
57
|
+
const header = pendingTiles.pop()!;
|
|
58
|
+
if (visitedTileIds.has(header.id)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
visitedTileIds.add(header.id);
|
|
62
|
+
|
|
63
|
+
const content = await source.loadTileContent(header);
|
|
64
|
+
throwIfAborted(signal);
|
|
65
|
+
yield {header, content};
|
|
66
|
+
throwIfAborted(signal);
|
|
67
|
+
|
|
68
|
+
if (header.level >= maxDepth) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const children = await source.getChildren(header);
|
|
72
|
+
throwIfAborted(signal);
|
|
73
|
+
for (let index = children.length - 1; index >= 0; index--) {
|
|
74
|
+
pendingTiles.push(children[index]);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Throws the signal reason when traversal has been cancelled. */
|
|
80
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
81
|
+
if (signal?.aborted) {
|
|
82
|
+
throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {Tile3DWriter, TILE3D_TYPE} from '@loaders.gl/3d-tiles';
|
|
6
|
+
import type {Mesh, MeshArrowTable, MeshAttribute} from '@loaders.gl/schema';
|
|
7
|
+
import {convertTableToMesh} from '@loaders.gl/schema-utils';
|
|
8
|
+
import {TileConversionError} from '@loaders.gl/tile-converter/v5/core';
|
|
9
|
+
|
|
10
|
+
/** Options for encoding one Arrow or mesh point batch as a 3D Tiles point tile. */
|
|
11
|
+
export interface EncodePointCloudTileOptions {
|
|
12
|
+
/** Optional origin subtracted from absolute positions and stored as the PNTS RTC_CENTER. */
|
|
13
|
+
readonly rtcCenter?: readonly [number, number, number];
|
|
14
|
+
/** Optional source-wide RGBA color stored as the PNTS CONSTANT_RGBA property. */
|
|
15
|
+
readonly constantRGBA?: readonly number[];
|
|
16
|
+
/** Optional per-point values indexed by the tile's `BATCH_ID` attribute. */
|
|
17
|
+
readonly batchTableJson?: Readonly<Record<string, readonly (string | number)[]>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Encodes one mesh or Mesh Arrow point batch as a PNTS resource.
|
|
22
|
+
*
|
|
23
|
+
* The input position field must be named `POSITION` and contain xyz values. `COLOR_0` or `COLOR`
|
|
24
|
+
* is written when present; `constantRGBA` supplies a fallback PNTS `CONSTANT_RGBA` feature-table
|
|
25
|
+
* property. `NORMAL` and `BATCH_ID` are also written when present. Arrow batches should be passed
|
|
26
|
+
* one at a time so callers can bound memory and apply their own tiling and output policy.
|
|
27
|
+
* Unsupported attributes, including a second color attribute, fail instead of being discarded.
|
|
28
|
+
*
|
|
29
|
+
* @param pointBatch - Point data produced by a LAS, COPC, I3S, or other point source.
|
|
30
|
+
* @param options - Optional RTC center and batch-table values.
|
|
31
|
+
* @returns A 3D Tiles PNTS resource.
|
|
32
|
+
*/
|
|
33
|
+
export function encodePointCloudTile(
|
|
34
|
+
pointBatch: Mesh | MeshArrowTable,
|
|
35
|
+
options: EncodePointCloudTileOptions = {}
|
|
36
|
+
): ArrayBuffer {
|
|
37
|
+
const mesh = 'attributes' in pointBatch ? pointBatch : convertTableToMesh(pointBatch);
|
|
38
|
+
validatePointAttributes(mesh.attributes);
|
|
39
|
+
const positionAttribute = mesh.attributes.POSITION;
|
|
40
|
+
if (!positionAttribute || positionAttribute.size !== 3) {
|
|
41
|
+
throw new TileConversionError(
|
|
42
|
+
'POINT_CLOUD_POSITION_REQUIRED',
|
|
43
|
+
'Point cloud batches must contain a three-component POSITION attribute'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const positions = getPositionValues(positionAttribute, options.rtcCenter);
|
|
48
|
+
const pointCount = positions.length / 3;
|
|
49
|
+
if (pointCount === 0 || !Number.isInteger(pointCount)) {
|
|
50
|
+
throw new TileConversionError(
|
|
51
|
+
'POINT_CLOUD_POSITION_COUNT_INVALID',
|
|
52
|
+
'Point cloud POSITION must contain one or more xyz triples'
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
|
|
57
|
+
const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
|
|
58
|
+
const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
|
|
59
|
+
const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
|
|
60
|
+
const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
|
|
61
|
+
|
|
62
|
+
return Tile3DWriter.encodeSync(
|
|
63
|
+
{
|
|
64
|
+
type: TILE3D_TYPE.POINT_CLOUD,
|
|
65
|
+
featureTableJson:
|
|
66
|
+
options.rtcCenter || constantRGBA
|
|
67
|
+
? {
|
|
68
|
+
...(options.rtcCenter ? {RTC_CENTER: [...options.rtcCenter]} : {}),
|
|
69
|
+
...(constantRGBA ? {CONSTANT_RGBA: constantRGBA} : {})
|
|
70
|
+
}
|
|
71
|
+
: undefined,
|
|
72
|
+
batchTableJson,
|
|
73
|
+
attributes: {positions, colors, normals, batchIds}
|
|
74
|
+
},
|
|
75
|
+
{}
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Rejects attributes that this PNTS encoder would otherwise silently discard. */
|
|
80
|
+
function validatePointAttributes(attributes: Mesh['attributes']): void {
|
|
81
|
+
const supportedAttributes = new Set([
|
|
82
|
+
'POSITION',
|
|
83
|
+
'NORMAL',
|
|
84
|
+
'BATCH_ID',
|
|
85
|
+
attributes.COLOR_0 ? 'COLOR_0' : 'COLOR'
|
|
86
|
+
]);
|
|
87
|
+
const unsupportedAttributes = Object.keys(attributes).filter(
|
|
88
|
+
name => !supportedAttributes.has(name)
|
|
89
|
+
);
|
|
90
|
+
if (unsupportedAttributes.length > 0) {
|
|
91
|
+
throw new TileConversionError(
|
|
92
|
+
'POINT_CLOUD_ATTRIBUTE_UNSUPPORTED',
|
|
93
|
+
`PNTS encoding does not support these point attributes: ${unsupportedAttributes.join(', ')}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Validates an optional source-wide PNTS RGBA color. */
|
|
99
|
+
function getConstantRgba(
|
|
100
|
+
constantRGBA?: readonly number[]
|
|
101
|
+
): [number, number, number, number] | null {
|
|
102
|
+
if (!constantRGBA) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
constantRGBA.length !== 4 ||
|
|
107
|
+
constantRGBA.some(value => !Number.isInteger(value) || value < 0 || value > 255)
|
|
108
|
+
) {
|
|
109
|
+
throw new TileConversionError(
|
|
110
|
+
'POINT_CLOUD_CONSTANT_RGBA_INVALID',
|
|
111
|
+
'Point cloud constantRGBA must contain four unsigned byte values'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return [constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3]];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Decodes positions and offsets them relative to the optional RTC center. */
|
|
118
|
+
function getPositionValues(
|
|
119
|
+
attribute: MeshAttribute,
|
|
120
|
+
rtcCenter?: readonly [number, number, number]
|
|
121
|
+
): Float32Array {
|
|
122
|
+
if (rtcCenter && (rtcCenter.length !== 3 || rtcCenter.some(value => !Number.isFinite(value)))) {
|
|
123
|
+
throw new TileConversionError(
|
|
124
|
+
'POINT_CLOUD_RTC_CENTER_INVALID',
|
|
125
|
+
'Point cloud RTC_CENTER must contain three finite values'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const values = attribute.value;
|
|
129
|
+
const result = new Float32Array(values.length);
|
|
130
|
+
const transform = attribute.transform;
|
|
131
|
+
for (let index = 0; index < values.length; index++) {
|
|
132
|
+
const component = index % 3;
|
|
133
|
+
const value = values[index];
|
|
134
|
+
const absoluteValue =
|
|
135
|
+
transform?.type === 'quantization'
|
|
136
|
+
? transform.origin[component] + (value / (2 ** transform.bits - 1)) * transform.range
|
|
137
|
+
: value;
|
|
138
|
+
result[index] = absoluteValue - (rtcCenter?.[component] ?? 0);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Converts an optional mesh color attribute into the byte colors accepted by PNTS. */
|
|
144
|
+
function getColorAttribute(
|
|
145
|
+
attribute: MeshAttribute | undefined,
|
|
146
|
+
pointCount: number
|
|
147
|
+
): {type: number; value: Uint8Array; size: number; normalized: boolean} | null {
|
|
148
|
+
if (!attribute) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (attribute.size !== 3 && attribute.size !== 4) {
|
|
152
|
+
throw new TileConversionError(
|
|
153
|
+
'POINT_CLOUD_COLOR_SIZE_UNSUPPORTED',
|
|
154
|
+
'Point cloud color attributes must contain three or four components'
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (attribute.value.length !== pointCount * attribute.size) {
|
|
158
|
+
throw new TileConversionError(
|
|
159
|
+
'POINT_CLOUD_COLOR_COUNT_MISMATCH',
|
|
160
|
+
'Point cloud color count does not match the POSITION count'
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const colors = new Uint8Array(attribute.value.length);
|
|
165
|
+
const maximum = getIntegerColorMaximum(attribute.value);
|
|
166
|
+
for (let index = 0; index < attribute.value.length; index++) {
|
|
167
|
+
const value = attribute.value[index];
|
|
168
|
+
const scaledValue = maximum ? (value / maximum) * 255 : value <= 1 ? value * 255 : value;
|
|
169
|
+
if (!Number.isFinite(scaledValue) || scaledValue < 0 || scaledValue > 255) {
|
|
170
|
+
throw new TileConversionError(
|
|
171
|
+
'POINT_CLOUD_COLOR_VALUE_INVALID',
|
|
172
|
+
'Point cloud color components must be finite values from 0 through 255'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
colors[index] = Math.round(scaledValue);
|
|
176
|
+
}
|
|
177
|
+
return {type: 5121, value: colors, size: attribute.size, normalized: false};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Returns the maximum value represented by an integer color component array. */
|
|
181
|
+
function getIntegerColorMaximum(values: ArrayLike<number>): number | null {
|
|
182
|
+
if (values instanceof Uint8Array || values instanceof Uint8ClampedArray) {
|
|
183
|
+
return 255;
|
|
184
|
+
}
|
|
185
|
+
if (values instanceof Uint16Array) {
|
|
186
|
+
return 65_535;
|
|
187
|
+
}
|
|
188
|
+
if (values instanceof Uint32Array) {
|
|
189
|
+
return 4_294_967_295;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Returns float normals and rejects unsupported component shapes. */
|
|
195
|
+
function getNormalAttribute(
|
|
196
|
+
attribute: MeshAttribute | undefined,
|
|
197
|
+
pointCount: number
|
|
198
|
+
): {type: number; value: Float32Array; size: number} | null {
|
|
199
|
+
if (!attribute) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
if (attribute.size !== 3 || attribute.value.length !== pointCount * 3) {
|
|
203
|
+
throw new TileConversionError(
|
|
204
|
+
'POINT_CLOUD_NORMAL_COUNT_MISMATCH',
|
|
205
|
+
'Point cloud normals must contain one xyz triple per position'
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
return {type: 5126, value: Float32Array.from(attribute.value), size: 3};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Validates optional stable feature IDs for the PNTS uint16 batch-ID field. */
|
|
212
|
+
function getBatchIds(attribute: MeshAttribute | undefined, pointCount: number): Uint16Array | null {
|
|
213
|
+
if (!attribute) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
if (attribute.size !== 1 || attribute.value.length !== pointCount) {
|
|
217
|
+
throw new TileConversionError(
|
|
218
|
+
'POINT_CLOUD_BATCH_ID_COUNT_MISMATCH',
|
|
219
|
+
'Point cloud BATCH_ID must contain one value per position'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const batchIds = new Uint16Array(pointCount);
|
|
223
|
+
for (let index = 0; index < pointCount; index++) {
|
|
224
|
+
const batchId = attribute.value[index];
|
|
225
|
+
if (!Number.isInteger(batchId) || batchId < 0 || batchId > 65_535) {
|
|
226
|
+
throw new TileConversionError(
|
|
227
|
+
'POINT_CLOUD_BATCH_ID_OUT_OF_RANGE',
|
|
228
|
+
'Point cloud BATCH_ID values must be unsigned 16-bit integers'
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
batchIds[index] = batchId;
|
|
232
|
+
}
|
|
233
|
+
return batchIds;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Checks that each batch-table property is indexable by the batch IDs used by this tile. */
|
|
237
|
+
function validateBatchTable(
|
|
238
|
+
batchTableJson: EncodePointCloudTileOptions['batchTableJson'],
|
|
239
|
+
batchIds: Uint16Array | null
|
|
240
|
+
): Record<string, (string | number)[]> | undefined {
|
|
241
|
+
if (!batchTableJson) {
|
|
242
|
+
if (batchIds) {
|
|
243
|
+
throw new TileConversionError(
|
|
244
|
+
'POINT_CLOUD_BATCH_TABLE_REQUIRED',
|
|
245
|
+
'BATCH_ID values require matching batchTableJson properties'
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
if (!batchIds) {
|
|
251
|
+
throw new TileConversionError(
|
|
252
|
+
'POINT_CLOUD_BATCH_ID_REQUIRED',
|
|
253
|
+
'batchTableJson requires a BATCH_ID attribute'
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
let batchLength = 0;
|
|
257
|
+
for (const batchId of batchIds) {
|
|
258
|
+
batchLength = Math.max(batchLength, batchId + 1);
|
|
259
|
+
}
|
|
260
|
+
const normalizedBatchTable: Record<string, (string | number)[]> = {};
|
|
261
|
+
for (const [name, values] of Object.entries(batchTableJson)) {
|
|
262
|
+
if (values.length !== batchLength) {
|
|
263
|
+
throw new TileConversionError(
|
|
264
|
+
'POINT_CLOUD_BATCH_TABLE_LENGTH_MISMATCH',
|
|
265
|
+
`Batch table property "${name}" must contain ${batchLength} values`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
normalizedBatchTable[name] = [...values];
|
|
269
|
+
}
|
|
270
|
+
return normalizedBatchTable;
|
|
271
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
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 TILE_RESOURCE_MANIFEST_VERSION = 1;
|
|
9
|
+
|
|
10
|
+
/** Durable record for one successfully written, deterministic output resource. */
|
|
11
|
+
export interface TileResourceManifestEntry {
|
|
12
|
+
/** Stable resource identifier supplied by the output profile. */
|
|
13
|
+
readonly resourceId: string;
|
|
14
|
+
/** Byte length of the output resource. */
|
|
15
|
+
readonly byteLength: number;
|
|
16
|
+
/** Stable caller-computed content fingerprint, such as a SHA-256 digest. */
|
|
17
|
+
readonly fingerprint: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Versioned progress record used to resume deterministic output packaging. */
|
|
21
|
+
export interface TileResourceManifest {
|
|
22
|
+
/** Manifest format version. */
|
|
23
|
+
readonly version: 1;
|
|
24
|
+
/** True only after the destination and report have been finalized successfully. */
|
|
25
|
+
readonly complete: boolean;
|
|
26
|
+
/** Resources durably written before interruption or completion. */
|
|
27
|
+
readonly resources: readonly TileResourceManifestEntry[];
|
|
28
|
+
/** Conversion summary saved after successful destination finalization. */
|
|
29
|
+
readonly report?: TileConversionReport;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Application-owned persistence for a conversion manifest. */
|
|
33
|
+
export interface TileResourceManifestStore {
|
|
34
|
+
/** Loads the previous manifest, or returns null when starting a new conversion. */
|
|
35
|
+
load(): Promise<TileResourceManifest | null>;
|
|
36
|
+
/** Atomically replaces the saved manifest with the supplied snapshot. */
|
|
37
|
+
save(manifest: TileResourceManifest): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Options for decorating a resource sink with restart-safe manifest tracking. */
|
|
41
|
+
export interface ManifestBackedTileConversionSinkOptions<TResource> {
|
|
42
|
+
/** Output destination with deterministic, idempotent resource writes. */
|
|
43
|
+
readonly sink: TileConversionSink<TResource>;
|
|
44
|
+
/** Persistence adapter for snapshots written after each resource. */
|
|
45
|
+
readonly manifestStore: TileResourceManifestStore;
|
|
46
|
+
/** Stable profile-specific path or resource key. */
|
|
47
|
+
readonly getResourceId: (resource: TResource) => string;
|
|
48
|
+
/** Byte length used to detect changed content under an existing resource ID. */
|
|
49
|
+
readonly measureResourceBytes: (resource: TResource) => number;
|
|
50
|
+
/** Stable fingerprint used to detect changed content under an existing resource ID. */
|
|
51
|
+
readonly fingerprintResource: (resource: TResource) => string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Creates a sink that skips matching checkpointed resources when a process restarts.
|
|
56
|
+
*
|
|
57
|
+
* The destination must write resources idempotently by the ID returned from `getResourceId`.
|
|
58
|
+
* The manifest is persisted after every successful write; the store should replace snapshots
|
|
59
|
+
* atomically. Existing IDs must retain their byte length and fingerprint or the resumed job fails
|
|
60
|
+
* rather than silently mixing output from different conversion inputs. Writes are serialized even
|
|
61
|
+
* when callers issue them concurrently. Reports count resources processed by the current run,
|
|
62
|
+
* including writes skipped because the matching resource was already present. A graceful abort
|
|
63
|
+
* clears the manifest checkpoint before asking the destination to clean partial output, so the
|
|
64
|
+
* next run starts cleanly. A process interruption that does not call `abort` can resume from its
|
|
65
|
+
* last saved checkpoint.
|
|
66
|
+
*
|
|
67
|
+
* @param options - Destination, manifest store, stable IDs, byte measurement, and fingerprinting.
|
|
68
|
+
* @returns A `TileConversionSink` with resumable per-resource progress.
|
|
69
|
+
*/
|
|
70
|
+
export async function createManifestBackedTileConversionSink<TResource>(
|
|
71
|
+
options: ManifestBackedTileConversionSinkOptions<TResource>
|
|
72
|
+
): Promise<TileConversionSink<TResource>> {
|
|
73
|
+
const priorManifest = await options.manifestStore.load();
|
|
74
|
+
const resources = new Map<string, TileResourceManifestEntry>();
|
|
75
|
+
if (priorManifest) {
|
|
76
|
+
validateManifest(priorManifest);
|
|
77
|
+
for (const entry of priorManifest.resources) {
|
|
78
|
+
resources.set(entry.resourceId, entry);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let writeChain: Promise<void> = Promise.resolve();
|
|
83
|
+
let writeFailed = false;
|
|
84
|
+
let writeFailure: unknown;
|
|
85
|
+
let closed = false;
|
|
86
|
+
|
|
87
|
+
const persistManifest = async (complete: boolean, report?: TileConversionReport) => {
|
|
88
|
+
await options.manifestStore.save({
|
|
89
|
+
version: TILE_RESOURCE_MANIFEST_VERSION,
|
|
90
|
+
complete,
|
|
91
|
+
resources: [...resources.values()].sort((left, right) =>
|
|
92
|
+
left.resourceId < right.resourceId ? -1 : left.resourceId > right.resourceId ? 1 : 0
|
|
93
|
+
),
|
|
94
|
+
...(report ? {report} : {})
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const writeResource = async (resource: TResource, signal?: AbortSignal): Promise<void> => {
|
|
99
|
+
if (closed) {
|
|
100
|
+
throw new TileConversionError('RESOURCE_SINK_CLOSED', 'Cannot write after sink finalization');
|
|
101
|
+
}
|
|
102
|
+
if (writeFailed) {
|
|
103
|
+
throw writeFailure;
|
|
104
|
+
}
|
|
105
|
+
const entry = createManifestEntry(options, resource);
|
|
106
|
+
const priorEntry = resources.get(entry.resourceId);
|
|
107
|
+
if (priorEntry) {
|
|
108
|
+
if (
|
|
109
|
+
priorEntry.byteLength !== entry.byteLength ||
|
|
110
|
+
priorEntry.fingerprint !== entry.fingerprint
|
|
111
|
+
) {
|
|
112
|
+
throw new TileConversionError(
|
|
113
|
+
'RESOURCE_RESUME_MISMATCH',
|
|
114
|
+
`Resource "${entry.resourceId}" changed since the saved manifest`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await options.sink.write(resource, signal);
|
|
121
|
+
resources.set(entry.resourceId, entry);
|
|
122
|
+
await persistManifest(false);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
write(resource, signal) {
|
|
127
|
+
const operation = writeChain.then(() => writeResource(resource, signal));
|
|
128
|
+
writeChain = operation.catch(error => {
|
|
129
|
+
writeFailed = true;
|
|
130
|
+
writeFailure = error;
|
|
131
|
+
});
|
|
132
|
+
return operation;
|
|
133
|
+
},
|
|
134
|
+
async finalize(report) {
|
|
135
|
+
await writeChain;
|
|
136
|
+
if (writeFailed) {
|
|
137
|
+
throw writeFailure;
|
|
138
|
+
}
|
|
139
|
+
if (closed) {
|
|
140
|
+
throw new TileConversionError('RESOURCE_SINK_CLOSED', 'Sink has already been finalized');
|
|
141
|
+
}
|
|
142
|
+
await options.sink.finalize(report);
|
|
143
|
+
await persistManifest(true, report);
|
|
144
|
+
closed = true;
|
|
145
|
+
},
|
|
146
|
+
async abort(reason) {
|
|
147
|
+
await writeChain;
|
|
148
|
+
if (closed) return;
|
|
149
|
+
closed = true;
|
|
150
|
+
resources.clear();
|
|
151
|
+
await persistManifest(false);
|
|
152
|
+
await options.sink.abort(reason);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Creates and validates the deterministic manifest entry for an output resource. */
|
|
158
|
+
function createManifestEntry<TResource>(
|
|
159
|
+
options: ManifestBackedTileConversionSinkOptions<TResource>,
|
|
160
|
+
resource: TResource
|
|
161
|
+
): TileResourceManifestEntry {
|
|
162
|
+
const resourceId = options.getResourceId(resource);
|
|
163
|
+
const byteLength = options.measureResourceBytes(resource);
|
|
164
|
+
const fingerprint = options.fingerprintResource(resource);
|
|
165
|
+
if (
|
|
166
|
+
typeof resourceId !== 'string' ||
|
|
167
|
+
resourceId.length === 0 ||
|
|
168
|
+
typeof fingerprint !== 'string' ||
|
|
169
|
+
fingerprint.length === 0 ||
|
|
170
|
+
!Number.isSafeInteger(byteLength) ||
|
|
171
|
+
byteLength < 0
|
|
172
|
+
) {
|
|
173
|
+
throw new TileConversionError(
|
|
174
|
+
'INVALID_RESOURCE_MANIFEST_ENTRY',
|
|
175
|
+
'Resource manifest entries require an ID, a fingerprint, and a non-negative safe byte length'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return {resourceId, byteLength, fingerprint};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Checks a loaded manifest before trusting it for restart decisions. */
|
|
182
|
+
function validateManifest(manifest: TileResourceManifest): void {
|
|
183
|
+
if (
|
|
184
|
+
typeof manifest !== 'object' ||
|
|
185
|
+
manifest === null ||
|
|
186
|
+
manifest.version !== TILE_RESOURCE_MANIFEST_VERSION ||
|
|
187
|
+
typeof manifest.complete !== 'boolean' ||
|
|
188
|
+
!Array.isArray(manifest.resources)
|
|
189
|
+
) {
|
|
190
|
+
throw new TileConversionError(
|
|
191
|
+
'UNSUPPORTED_RESOURCE_MANIFEST',
|
|
192
|
+
`Unsupported or malformed resource manifest version: ${String(manifest.version)}`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
const resourceIds = new Set<string>();
|
|
196
|
+
for (const entry of manifest.resources) {
|
|
197
|
+
if (
|
|
198
|
+
typeof entry?.resourceId !== 'string' ||
|
|
199
|
+
entry.resourceId.length === 0 ||
|
|
200
|
+
typeof entry?.fingerprint !== 'string' ||
|
|
201
|
+
entry.fingerprint.length === 0 ||
|
|
202
|
+
!Number.isSafeInteger(entry.byteLength) ||
|
|
203
|
+
entry.byteLength < 0 ||
|
|
204
|
+
resourceIds.has(entry.resourceId)
|
|
205
|
+
) {
|
|
206
|
+
throw new TileConversionError(
|
|
207
|
+
'INVALID_RESOURCE_MANIFEST',
|
|
208
|
+
'Resource manifest contains an invalid or duplicate entry'
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
resourceIds.add(entry.resourceId);
|
|
212
|
+
}
|
|
213
|
+
}
|