@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
|
@@ -0,0 +1,328 @@
|
|
|
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
|
+
type TilesetContent = {uri?: string; url?: string};
|
|
9
|
+
type TilesetTile = {
|
|
10
|
+
content?: TilesetContent;
|
|
11
|
+
contents?: TilesetContent[];
|
|
12
|
+
children?: TilesetTile[];
|
|
13
|
+
transform?: number[];
|
|
14
|
+
implicitTiling?: unknown;
|
|
15
|
+
extensions?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
type TilesetDocument = {asset?: {version?: string}; root?: TilesetTile};
|
|
18
|
+
|
|
19
|
+
/** Input options for bounded traversal of one explicit 3D Tiles tileset. */
|
|
20
|
+
export interface BrowserTilesetConversionSourceOptions {
|
|
21
|
+
/** URL of the root tileset JSON document. */
|
|
22
|
+
readonly input: string;
|
|
23
|
+
/** Maximum bytes retained across the tileset JSON and all content resources. */
|
|
24
|
+
readonly maxInputBytes: number;
|
|
25
|
+
/** Maximum number of content resources to visit. */
|
|
26
|
+
readonly maxInputResources: number;
|
|
27
|
+
/** Fetch implementation, defaulting to the browser global. */
|
|
28
|
+
readonly fetcher?: typeof fetch;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Inspection result for an explicit 3D Tiles source. */
|
|
32
|
+
export interface BrowserTilesetConversionInspection {
|
|
33
|
+
/** Absolute URL of the root tileset document. */
|
|
34
|
+
readonly rootUrl: string;
|
|
35
|
+
/** Parsed root document used for deterministic content traversal. */
|
|
36
|
+
readonly tileset: TilesetDocument;
|
|
37
|
+
/** Ordered content descriptors. */
|
|
38
|
+
readonly resources: readonly BrowserTilesetResourceDescriptor[];
|
|
39
|
+
/** Root document bytes charged against the input limit. */
|
|
40
|
+
readonly tilesetBytes: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Placement metadata for one ordered content resource. */
|
|
44
|
+
export interface BrowserTilesetResourceDescriptor {
|
|
45
|
+
/** Unique, stable path for this content placement. */
|
|
46
|
+
readonly resourceId: string;
|
|
47
|
+
/** Original URI declared by the tileset. */
|
|
48
|
+
readonly uri: string;
|
|
49
|
+
/** Tile child indexes from the tileset root. */
|
|
50
|
+
readonly tilePath: readonly number[];
|
|
51
|
+
/** Combined root-to-tile transform in column-major order. */
|
|
52
|
+
readonly transform: readonly number[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One raw content resource from a browser-readable explicit tileset. */
|
|
56
|
+
export interface BrowserTilesetConversionInputResource extends BrowserTilesetResourceDescriptor {
|
|
57
|
+
/** Complete content bytes for an injected profile-specific codec. */
|
|
58
|
+
readonly data: Uint8Array;
|
|
59
|
+
/** Optional content type reported by the response. */
|
|
60
|
+
readonly contentType?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Creates a bounded browser source for explicit 3D Tiles 1.0 and 1.1 tilesets.
|
|
65
|
+
*
|
|
66
|
+
* Traversal visits each declared content placement once in depth-first order. Implicit tiling and
|
|
67
|
+
* nested external tilesets are rejected; content decoding and conversion remain codec work.
|
|
68
|
+
*
|
|
69
|
+
* @param options - Tileset URL and aggregate byte and resource limits.
|
|
70
|
+
* @returns A source yielding ordered raw tile resources with placement transforms.
|
|
71
|
+
*/
|
|
72
|
+
export function createBrowserTilesetConversionSource(
|
|
73
|
+
options: BrowserTilesetConversionSourceOptions
|
|
74
|
+
): TileConversionSource<BrowserTilesetConversionInspection, BrowserTilesetConversionInputResource> {
|
|
75
|
+
if (
|
|
76
|
+
!Number.isSafeInteger(options.maxInputBytes) ||
|
|
77
|
+
options.maxInputBytes < 1 ||
|
|
78
|
+
!Number.isSafeInteger(options.maxInputResources) ||
|
|
79
|
+
options.maxInputResources < 1
|
|
80
|
+
) {
|
|
81
|
+
throw new TileConversionError(
|
|
82
|
+
'INVALID_BROWSER_SOURCE_OPTIONS',
|
|
83
|
+
'Browser tileset sources require positive byte and resource limits'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fetcher = options.fetcher ?? fetch;
|
|
88
|
+
return {
|
|
89
|
+
async inspect(signal) {
|
|
90
|
+
throwIfAborted(signal);
|
|
91
|
+
const rootUrl = new URL(options.input).href;
|
|
92
|
+
const response = await fetcher(rootUrl, {signal});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new TileConversionError(
|
|
95
|
+
'BROWSER_SOURCE_FETCH_FAILED',
|
|
96
|
+
`Could not fetch browser tileset (${response.status} ${response.statusText})`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const data = await readResponseBytes(response, options.maxInputBytes, signal);
|
|
100
|
+
let tileset: TilesetDocument;
|
|
101
|
+
try {
|
|
102
|
+
tileset = JSON.parse(new TextDecoder().decode(data)) as TilesetDocument;
|
|
103
|
+
} catch {
|
|
104
|
+
throw new TileConversionError('INVALID_BROWSER_TILESET', 'Tileset input is not valid JSON');
|
|
105
|
+
}
|
|
106
|
+
if (
|
|
107
|
+
!isRecord(tileset) ||
|
|
108
|
+
!['1.0', '1.1'].includes(tileset.asset?.version || '') ||
|
|
109
|
+
!isRecord(tileset.root)
|
|
110
|
+
) {
|
|
111
|
+
throw new TileConversionError(
|
|
112
|
+
'UNSUPPORTED_BROWSER_TILESET',
|
|
113
|
+
'Browser tileset traversal supports explicit 3D Tiles 1.0 and 1.1 documents'
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
const resources = collectResources(tileset.root, options.maxInputResources);
|
|
117
|
+
return {rootUrl, tileset, resources, tilesetBytes: data.byteLength};
|
|
118
|
+
},
|
|
119
|
+
async *read(inspection, signal) {
|
|
120
|
+
let inputBytes = inspection.tilesetBytes;
|
|
121
|
+
for (const descriptor of inspection.resources) {
|
|
122
|
+
throwIfAborted(signal);
|
|
123
|
+
const resourceUrl = resolveResourceUrl(descriptor.uri, inspection.rootUrl);
|
|
124
|
+
const response = await fetcher(resourceUrl, {signal});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
throw new TileConversionError(
|
|
127
|
+
'BROWSER_SOURCE_FETCH_FAILED',
|
|
128
|
+
`Could not fetch browser tile content (${response.status} ${response.statusText})`,
|
|
129
|
+
[
|
|
130
|
+
{
|
|
131
|
+
code: 'BROWSER_SOURCE_FETCH_FAILED',
|
|
132
|
+
message: resourceUrl,
|
|
133
|
+
severity: 'error',
|
|
134
|
+
resourceId: descriptor.resourceId
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const remainingBytes = options.maxInputBytes - inputBytes;
|
|
140
|
+
const data = await readResponseBytes(response, remainingBytes, signal);
|
|
141
|
+
inputBytes += data.byteLength;
|
|
142
|
+
const contentType = response.headers.get('content-type') ?? undefined;
|
|
143
|
+
if (isJsonContent(data, contentType)) {
|
|
144
|
+
throw new TileConversionError(
|
|
145
|
+
'UNSUPPORTED_BROWSER_TILESET_CONTENT',
|
|
146
|
+
`Nested tilesets and JSON tile content are not supported: ${descriptor.uri}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
yield {...descriptor, data, ...(contentType ? {contentType} : {})};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Collects explicit tile contents in declaration order and fails on unbounded formats. */
|
|
156
|
+
function collectResources(
|
|
157
|
+
root: TilesetTile,
|
|
158
|
+
maxInputResources: number
|
|
159
|
+
): BrowserTilesetResourceDescriptor[] {
|
|
160
|
+
const resources: BrowserTilesetResourceDescriptor[] = [];
|
|
161
|
+
const stack: Array<{tile: TilesetTile; path: number[]; transform: number[]}> = [
|
|
162
|
+
{tile: root, path: [], transform: identityMatrix()}
|
|
163
|
+
];
|
|
164
|
+
while (stack.length > 0) {
|
|
165
|
+
const {tile, path, transform: parentTransform} = stack.pop()!;
|
|
166
|
+
if (!isRecord(tile)) {
|
|
167
|
+
throw new TileConversionError('INVALID_BROWSER_TILESET', 'Tile entries must be objects');
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
tile.implicitTiling !== undefined ||
|
|
171
|
+
tile.extensions?.['3DTILES_implicit_tiling'] !== undefined
|
|
172
|
+
) {
|
|
173
|
+
throw new TileConversionError(
|
|
174
|
+
'UNSUPPORTED_BROWSER_TILESET',
|
|
175
|
+
'Browser tileset traversal does not yet support implicit tiling'
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
if (tile.contents !== undefined && !Array.isArray(tile.contents)) {
|
|
179
|
+
throw new TileConversionError('INVALID_BROWSER_TILESET', 'Tile contents must be an array');
|
|
180
|
+
}
|
|
181
|
+
if (tile.children !== undefined && !Array.isArray(tile.children)) {
|
|
182
|
+
throw new TileConversionError('INVALID_BROWSER_TILESET', 'Tile children must be an array');
|
|
183
|
+
}
|
|
184
|
+
const transform = multiplyMatrices(parentTransform, normalizeMatrix(tile.transform));
|
|
185
|
+
const contents = tile.contents ?? (tile.content ? [tile.content] : []);
|
|
186
|
+
for (let contentIndex = 0; contentIndex < contents.length; contentIndex++) {
|
|
187
|
+
const content = contents[contentIndex];
|
|
188
|
+
if (!isRecord(content)) {
|
|
189
|
+
throw new TileConversionError(
|
|
190
|
+
'INVALID_BROWSER_TILESET',
|
|
191
|
+
'Tile content entries must be objects'
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const uri = content.uri ?? content.url;
|
|
195
|
+
if (typeof uri !== 'string' || !uri) {
|
|
196
|
+
throw new TileConversionError(
|
|
197
|
+
'INVALID_BROWSER_TILESET',
|
|
198
|
+
`Tile content at ${path.join('/')} has no URI`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (resources.length >= maxInputResources) {
|
|
202
|
+
throw new TileConversionError(
|
|
203
|
+
'INPUT_RESOURCE_COUNT_EXCEEDED',
|
|
204
|
+
`Browser tileset exceeds the configured ${maxInputResources} resource limit`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
resources.push({
|
|
208
|
+
resourceId: `tile-${path.length ? path.join('-') : 'root'}-content-${contentIndex}`,
|
|
209
|
+
uri,
|
|
210
|
+
tilePath: path,
|
|
211
|
+
transform
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const children = tile.children ?? [];
|
|
215
|
+
for (let childIndex = children.length - 1; childIndex >= 0; childIndex--) {
|
|
216
|
+
stack.push({
|
|
217
|
+
tile: children[childIndex],
|
|
218
|
+
path: [...path, childIndex],
|
|
219
|
+
transform
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return resources;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Reads one response into a single buffer without exceeding its remaining byte budget. */
|
|
227
|
+
async function readResponseBytes(
|
|
228
|
+
response: Response,
|
|
229
|
+
maximumBytes: number,
|
|
230
|
+
signal?: AbortSignal
|
|
231
|
+
): Promise<Uint8Array> {
|
|
232
|
+
const reader = response.body?.getReader();
|
|
233
|
+
if (!reader) {
|
|
234
|
+
throw new TileConversionError(
|
|
235
|
+
'BROWSER_SOURCE_STREAM_UNAVAILABLE',
|
|
236
|
+
'Browser inputs must provide a readable response stream'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const data = new Uint8Array(maximumBytes);
|
|
240
|
+
let byteLength = 0;
|
|
241
|
+
try {
|
|
242
|
+
while (true) {
|
|
243
|
+
throwIfAborted(signal);
|
|
244
|
+
const {done, value} = await reader.read();
|
|
245
|
+
if (done) break;
|
|
246
|
+
if (value.byteLength > maximumBytes - byteLength) {
|
|
247
|
+
await reader.cancel();
|
|
248
|
+
throw new TileConversionError(
|
|
249
|
+
'INPUT_RESOURCE_TOO_LARGE',
|
|
250
|
+
`Browser tileset input exceeds the configured ${maximumBytes} byte remaining limit`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
data.set(value, byteLength);
|
|
254
|
+
byteLength += value.byteLength;
|
|
255
|
+
}
|
|
256
|
+
} finally {
|
|
257
|
+
reader.releaseLock();
|
|
258
|
+
}
|
|
259
|
+
return data.subarray(0, byteLength);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Detects JSON payloads that may declare an unsupported external tileset. */
|
|
263
|
+
function isJsonContent(data: Uint8Array, contentType?: string): boolean {
|
|
264
|
+
if (contentType?.includes('json')) return true;
|
|
265
|
+
let byteOffset = data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf ? 3 : 0;
|
|
266
|
+
while ([0x09, 0x0a, 0x0d, 0x20].includes(data[byteOffset])) byteOffset++;
|
|
267
|
+
return data[byteOffset] === 0x7b;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Resolves a tile URI and carries root query parameters to same-origin relative resources. */
|
|
271
|
+
function resolveResourceUrl(uri: string, rootUrl: string): string {
|
|
272
|
+
const root = new URL(rootUrl);
|
|
273
|
+
const resource = new URL(uri, root);
|
|
274
|
+
if (resource.origin === root.origin) {
|
|
275
|
+
for (const [name, value] of root.searchParams) {
|
|
276
|
+
if (!resource.searchParams.has(name)) resource.searchParams.append(name, value);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return resource.href;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Returns whether a JSON value is a non-array object. */
|
|
283
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
284
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Returns an identity matrix in column-major order. */
|
|
288
|
+
function identityMatrix(): number[] {
|
|
289
|
+
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Validates and copies a tile's optional column-major transform. */
|
|
293
|
+
function normalizeMatrix(matrix?: number[]): number[] {
|
|
294
|
+
if (matrix === undefined) return identityMatrix();
|
|
295
|
+
if (
|
|
296
|
+
!Array.isArray(matrix) ||
|
|
297
|
+
matrix.length !== 16 ||
|
|
298
|
+
matrix.some(value => !Number.isFinite(value))
|
|
299
|
+
) {
|
|
300
|
+
throw new TileConversionError(
|
|
301
|
+
'INVALID_BROWSER_TILESET',
|
|
302
|
+
'Tile transform must contain 16 finite numbers'
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
return matrix;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Multiplies column-major 4x4 matrices. */
|
|
309
|
+
function multiplyMatrices(left: number[], right: number[]): number[] {
|
|
310
|
+
const result = new Array<number>(16);
|
|
311
|
+
for (let column = 0; column < 4; column++) {
|
|
312
|
+
for (let row = 0; row < 4; row++) {
|
|
313
|
+
result[column * 4 + row] =
|
|
314
|
+
left[row] * right[column * 4] +
|
|
315
|
+
left[4 + row] * right[column * 4 + 1] +
|
|
316
|
+
left[8 + row] * right[column * 4 + 2] +
|
|
317
|
+
left[12 + row] * right[column * 4 + 3];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return result;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Throws the signal reason when a browser source operation has been canceled. */
|
|
324
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
325
|
+
if (signal?.aborted) {
|
|
326
|
+
throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// loaders.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
convertTileset,
|
|
7
|
+
inspectTileset,
|
|
8
|
+
TileConversionError,
|
|
9
|
+
validateTileset
|
|
10
|
+
} from './conversion-api.js';
|
|
11
|
+
export {createTilesetConversionSource} from './tileset-source.js';
|
|
12
|
+
export type {TilesetConversionSourceOptions} from './tileset-source.js';
|
|
13
|
+
export type {
|
|
14
|
+
ConvertTilesetOptions,
|
|
15
|
+
TileConversionCodec,
|
|
16
|
+
TileConversionDiagnostic,
|
|
17
|
+
TileConversionProgress,
|
|
18
|
+
TileConversionReport,
|
|
19
|
+
TileConversionSink,
|
|
20
|
+
TileConversionSource,
|
|
21
|
+
TileValidationReport,
|
|
22
|
+
ValidateTilesetOptions
|
|
23
|
+
} from './conversion-api.js';
|
|
24
|
+
export {createBoundedMemoryTileConversionSink} from './browser-sink.js';
|
|
25
|
+
export {createBrowserTileConversionSource} from './browser-source.js';
|
|
26
|
+
export type {
|
|
27
|
+
BoundedMemoryTileConversionSink,
|
|
28
|
+
BoundedMemoryTileConversionSinkOptions,
|
|
29
|
+
BrowserTileConversionFile,
|
|
30
|
+
BrowserTileConversionResource
|
|
31
|
+
} from './browser-sink.js';
|
|
32
|
+
export type {
|
|
33
|
+
BrowserTileConversionInspection,
|
|
34
|
+
BrowserTileConversionInputResource,
|
|
35
|
+
BrowserTileConversionSourceOptions
|
|
36
|
+
} from './browser-source.js';
|
|
37
|
+
export {createBrowserTilesetConversionSource} from './browser-tileset-source.js';
|
|
38
|
+
export {encodePointCloudTile} from './point-cloud.js';
|
|
39
|
+
export type {EncodePointCloudTileOptions} from './point-cloud.js';
|
|
40
|
+
export {traversePointCloudSource} from './point-cloud-source.js';
|
|
41
|
+
export type {PointCloudSourceTile, TraversePointCloudSourceOptions} from './point-cloud-source.js';
|
|
42
|
+
export {encodePointCloudSource, encodePointCloudSourceTile} from './point-cloud-source-encoder.js';
|
|
43
|
+
export type {
|
|
44
|
+
EncodedPointCloudSourceTile,
|
|
45
|
+
EncodePointCloudSourceOptions
|
|
46
|
+
} from './point-cloud-source-encoder.js';
|
|
47
|
+
export {convertPointCloudSource} from './point-cloud-conversion.js';
|
|
48
|
+
export type {ConvertPointCloudSourceOptions} from './point-cloud-conversion.js';
|
|
49
|
+
export type {
|
|
50
|
+
BrowserTilesetConversionInputResource,
|
|
51
|
+
BrowserTilesetConversionInspection,
|
|
52
|
+
BrowserTilesetConversionSourceOptions,
|
|
53
|
+
BrowserTilesetResourceDescriptor
|
|
54
|
+
} from './browser-tileset-source.js';
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/** A diagnostic produced while inspecting, converting, or validating tiles. */
|
|
2
|
+
export interface TileConversionDiagnostic {
|
|
3
|
+
/** Stable machine-readable diagnostic code. */
|
|
4
|
+
readonly code: string;
|
|
5
|
+
/** Human-readable explanation of the diagnostic. */
|
|
6
|
+
readonly message: string;
|
|
7
|
+
/** Severity used by callers to decide whether the result is acceptable. */
|
|
8
|
+
readonly severity: 'info' | 'warning' | 'error';
|
|
9
|
+
/** Optional resource that produced the diagnostic. */
|
|
10
|
+
readonly resourceId?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Typed failure raised when a conversion policy or validation gate rejects a resource. */
|
|
14
|
+
export class TileConversionError extends Error {
|
|
15
|
+
/** Stable machine-readable failure code. */
|
|
16
|
+
readonly code: string;
|
|
17
|
+
/** Diagnostics associated with this failure. */
|
|
18
|
+
readonly diagnostics: readonly TileConversionDiagnostic[];
|
|
19
|
+
|
|
20
|
+
/** Creates a typed conversion failure. */
|
|
21
|
+
constructor(
|
|
22
|
+
code: string,
|
|
23
|
+
message: string,
|
|
24
|
+
diagnostics: readonly TileConversionDiagnostic[] = []
|
|
25
|
+
) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'TileConversionError';
|
|
28
|
+
this.code = code;
|
|
29
|
+
this.diagnostics = diagnostics;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Progress reported by the portable conversion core. */
|
|
34
|
+
export interface TileConversionProgress {
|
|
35
|
+
/** Current conversion phase. */
|
|
36
|
+
readonly phase: 'inspect' | 'convert' | 'validate' | 'finalize';
|
|
37
|
+
/** Number of input resources consumed so far. */
|
|
38
|
+
readonly inputResources: number;
|
|
39
|
+
/** Number of output resources written so far. */
|
|
40
|
+
readonly outputResources: number;
|
|
41
|
+
/** Number of input bytes consumed so far. */
|
|
42
|
+
readonly inputBytes: number;
|
|
43
|
+
/** Number of output bytes written so far. */
|
|
44
|
+
readonly outputBytes: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Summary returned after a tileset conversion completes. */
|
|
48
|
+
export interface TileConversionReport {
|
|
49
|
+
/** Completed conversion state. Failed conversions reject with their cause. */
|
|
50
|
+
readonly state: 'completed';
|
|
51
|
+
/** Number of input resources consumed. */
|
|
52
|
+
readonly inputResources: number;
|
|
53
|
+
/** Number of output resources written. */
|
|
54
|
+
readonly outputResources: number;
|
|
55
|
+
/** Number of input bytes consumed. */
|
|
56
|
+
readonly inputBytes: number;
|
|
57
|
+
/** Number of output bytes written. */
|
|
58
|
+
readonly outputBytes: number;
|
|
59
|
+
/** Largest output resource written. */
|
|
60
|
+
readonly largestOutputResourceBytes: number;
|
|
61
|
+
/** Diagnostics reported during conversion and validation. */
|
|
62
|
+
readonly diagnostics: readonly TileConversionDiagnostic[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Portable source adapter that resolves and reads a tileset. */
|
|
66
|
+
export interface TileConversionSource<TInspection, TInput> {
|
|
67
|
+
/** Inspect the source and return its profile-specific metadata. */
|
|
68
|
+
inspect(signal?: AbortSignal): Promise<TInspection>;
|
|
69
|
+
/** Stream input resources in deterministic traversal order. */
|
|
70
|
+
read(inspection: TInspection, signal?: AbortSignal): AsyncIterable<TInput>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Output adapter whose awaited writes apply backpressure to conversion. */
|
|
74
|
+
export interface TileConversionSink<TOutput> {
|
|
75
|
+
/** Write one completed output resource and resolve when it can be released. */
|
|
76
|
+
write(resource: TOutput, signal?: AbortSignal): Promise<void>;
|
|
77
|
+
/** Finalize the destination after every resource has been written. */
|
|
78
|
+
finalize(report: TileConversionReport): Promise<void>;
|
|
79
|
+
/** Abort and clean up a partial destination after a failure. */
|
|
80
|
+
abort(reason: unknown): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Codec adapter used to convert one input resource into streamed outputs. */
|
|
84
|
+
export interface TileConversionCodec<TInspection, TInput, TOutput> {
|
|
85
|
+
/** Convert one input resource into zero or more output resources. */
|
|
86
|
+
convert(
|
|
87
|
+
resource: TInput,
|
|
88
|
+
inspection: TInspection,
|
|
89
|
+
signal?: AbortSignal
|
|
90
|
+
): AsyncIterable<TOutput> | Promise<AsyncIterable<TOutput>>;
|
|
91
|
+
/** Optionally validate each output resource before it is written. */
|
|
92
|
+
validateOutput?(
|
|
93
|
+
resource: TOutput,
|
|
94
|
+
signal?: AbortSignal
|
|
95
|
+
): Promise<readonly TileConversionDiagnostic[]>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Options for the portable `convertTileset` operation. */
|
|
99
|
+
export interface ConvertTilesetOptions<TInspection, TInput, TOutput> {
|
|
100
|
+
/** Injected input resolver and resource reader. */
|
|
101
|
+
readonly source: TileConversionSource<TInspection, TInput>;
|
|
102
|
+
/** Injected output codec. */
|
|
103
|
+
readonly codec: TileConversionCodec<TInspection, TInput, TOutput>;
|
|
104
|
+
/** Injected destination writer. */
|
|
105
|
+
readonly sink: TileConversionSink<TOutput>;
|
|
106
|
+
/** Return the decoded input byte size as a nonnegative safe integer. */
|
|
107
|
+
readonly measureInputBytes: (resource: TInput) => number;
|
|
108
|
+
/** Return the byte size of an output resource. */
|
|
109
|
+
readonly measureOutputBytes: (resource: TOutput) => number;
|
|
110
|
+
/** Reject a decoded input resource above this byte limit before encoding. Defaults to no limit. */
|
|
111
|
+
readonly maxInputResourceBytes?: number;
|
|
112
|
+
/** Reject any output resource larger than this configured byte limit. */
|
|
113
|
+
readonly maxOutputResourceBytes?: number;
|
|
114
|
+
/** Cancel inspection, conversion, validation, or writes. */
|
|
115
|
+
readonly signal?: AbortSignal;
|
|
116
|
+
/** Receive progress updates after each resource transition. */
|
|
117
|
+
readonly onProgress?: (progress: TileConversionProgress) => void;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Options for a standalone tileset validation pass. */
|
|
121
|
+
export interface ValidateTilesetOptions<TTileset> {
|
|
122
|
+
/** Tileset or conversion result to validate. */
|
|
123
|
+
readonly tileset: TTileset;
|
|
124
|
+
/** Injected profile-specific validator. */
|
|
125
|
+
readonly validate: (
|
|
126
|
+
tileset: TTileset,
|
|
127
|
+
signal?: AbortSignal
|
|
128
|
+
) => Promise<readonly TileConversionDiagnostic[]>;
|
|
129
|
+
/** Cancel validation. */
|
|
130
|
+
readonly signal?: AbortSignal;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Result of validating a tileset. */
|
|
134
|
+
export interface TileValidationReport {
|
|
135
|
+
/** False when at least one error diagnostic was reported. */
|
|
136
|
+
readonly valid: boolean;
|
|
137
|
+
/** Diagnostics returned by the injected validator. */
|
|
138
|
+
readonly diagnostics: readonly TileConversionDiagnostic[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Inspects a tileset with an injected, platform-specific source adapter. */
|
|
142
|
+
export async function inspectTileset<TInspection, TInput>(
|
|
143
|
+
source: TileConversionSource<TInspection, TInput>,
|
|
144
|
+
signal?: AbortSignal
|
|
145
|
+
): Promise<TInspection> {
|
|
146
|
+
throwIfAborted(signal);
|
|
147
|
+
const inspection = await source.inspect(signal);
|
|
148
|
+
throwIfAborted(signal);
|
|
149
|
+
return inspection;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Converts a stream of tileset resources through injected source, codec, and sink adapters.
|
|
154
|
+
* Decoded inputs are measured and checked against `maxInputResourceBytes` before encoding.
|
|
155
|
+
* This does not limit source decoding allocations or content retained by a source.
|
|
156
|
+
* Writes are awaited one resource at a time, keeping queued output bytes bounded by the
|
|
157
|
+
* largest emitted resource and the configured `maxOutputResourceBytes` limit.
|
|
158
|
+
*/
|
|
159
|
+
export async function convertTileset<TInspection, TInput, TOutput>(
|
|
160
|
+
options: ConvertTilesetOptions<TInspection, TInput, TOutput>
|
|
161
|
+
): Promise<TileConversionReport> {
|
|
162
|
+
const {
|
|
163
|
+
source,
|
|
164
|
+
codec,
|
|
165
|
+
sink,
|
|
166
|
+
measureInputBytes,
|
|
167
|
+
measureOutputBytes,
|
|
168
|
+
maxInputResourceBytes = Number.POSITIVE_INFINITY,
|
|
169
|
+
maxOutputResourceBytes = Number.POSITIVE_INFINITY,
|
|
170
|
+
signal,
|
|
171
|
+
onProgress
|
|
172
|
+
} = options;
|
|
173
|
+
let inputResources = 0;
|
|
174
|
+
let outputResources = 0;
|
|
175
|
+
let inputBytes = 0;
|
|
176
|
+
let outputBytes = 0;
|
|
177
|
+
let largestOutputResourceBytes = 0;
|
|
178
|
+
const diagnostics: TileConversionDiagnostic[] = [];
|
|
179
|
+
const reportProgress = (phase: TileConversionProgress['phase']): void => {
|
|
180
|
+
onProgress?.({phase, inputResources, outputResources, inputBytes, outputBytes});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
if (
|
|
185
|
+
maxInputResourceBytes !== Number.POSITIVE_INFINITY &&
|
|
186
|
+
(!Number.isSafeInteger(maxInputResourceBytes) || maxInputResourceBytes < 0)
|
|
187
|
+
) {
|
|
188
|
+
throw new TileConversionError(
|
|
189
|
+
'INVALID_INPUT_RESOURCE_LIMIT',
|
|
190
|
+
'maxInputResourceBytes must be a nonnegative safe integer or Infinity'
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
reportProgress('inspect');
|
|
194
|
+
const inspection = await inspectTileset(source, signal);
|
|
195
|
+
for await (const inputResource of source.read(inspection, signal)) {
|
|
196
|
+
throwIfAborted(signal);
|
|
197
|
+
const inputResourceBytes = measureInputBytes(inputResource);
|
|
198
|
+
if (!Number.isSafeInteger(inputResourceBytes) || inputResourceBytes < 0) {
|
|
199
|
+
throw new TileConversionError(
|
|
200
|
+
'INVALID_INPUT_RESOURCE_SIZE',
|
|
201
|
+
'measureInputBytes must return a nonnegative safe integer'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (inputResourceBytes > maxInputResourceBytes) {
|
|
205
|
+
throw new TileConversionError(
|
|
206
|
+
'INPUT_RESOURCE_TOO_LARGE',
|
|
207
|
+
`Input resource is ${inputResourceBytes} bytes, exceeding the configured limit of ${maxInputResourceBytes} bytes`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
inputResources++;
|
|
211
|
+
inputBytes += inputResourceBytes;
|
|
212
|
+
reportProgress('convert');
|
|
213
|
+
const convertedResources = await codec.convert(inputResource, inspection, signal);
|
|
214
|
+
for await (const outputResource of convertedResources) {
|
|
215
|
+
throwIfAborted(signal);
|
|
216
|
+
reportProgress('validate');
|
|
217
|
+
const resourceDiagnostics = (await codec.validateOutput?.(outputResource, signal)) || [];
|
|
218
|
+
diagnostics.push(...resourceDiagnostics);
|
|
219
|
+
const outputResourceBytes = measureOutputBytes(outputResource);
|
|
220
|
+
largestOutputResourceBytes = Math.max(largestOutputResourceBytes, outputResourceBytes);
|
|
221
|
+
if (outputResourceBytes > maxOutputResourceBytes) {
|
|
222
|
+
throw new TileConversionError(
|
|
223
|
+
'OUTPUT_RESOURCE_TOO_LARGE',
|
|
224
|
+
`Output resource is ${outputResourceBytes} bytes, exceeding the configured limit of ${maxOutputResourceBytes} bytes`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (resourceDiagnostics.some(diagnostic => diagnostic.severity === 'error')) {
|
|
228
|
+
throw new TileConversionError(
|
|
229
|
+
'OUTPUT_VALIDATION_FAILED',
|
|
230
|
+
'Output resource validation failed',
|
|
231
|
+
resourceDiagnostics
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
throwIfAborted(signal);
|
|
235
|
+
await sink.write(outputResource, signal);
|
|
236
|
+
throwIfAborted(signal);
|
|
237
|
+
outputResources++;
|
|
238
|
+
outputBytes += outputResourceBytes;
|
|
239
|
+
reportProgress('convert');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const report: TileConversionReport = {
|
|
244
|
+
state: 'completed',
|
|
245
|
+
inputResources,
|
|
246
|
+
outputResources,
|
|
247
|
+
inputBytes,
|
|
248
|
+
outputBytes,
|
|
249
|
+
largestOutputResourceBytes,
|
|
250
|
+
diagnostics
|
|
251
|
+
};
|
|
252
|
+
throwIfAborted(signal);
|
|
253
|
+
reportProgress('finalize');
|
|
254
|
+
await sink.finalize(report);
|
|
255
|
+
return report;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
try {
|
|
258
|
+
await sink.abort(error);
|
|
259
|
+
} catch (abortError) {
|
|
260
|
+
throw new AggregateError(
|
|
261
|
+
[error, abortError],
|
|
262
|
+
'Conversion failed and the destination could not abort'
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** Validates a tileset through an injected profile-specific validator. */
|
|
270
|
+
export async function validateTileset<TTileset>(
|
|
271
|
+
options: ValidateTilesetOptions<TTileset>
|
|
272
|
+
): Promise<TileValidationReport> {
|
|
273
|
+
throwIfAborted(options.signal);
|
|
274
|
+
const diagnostics = await options.validate(options.tileset, options.signal);
|
|
275
|
+
throwIfAborted(options.signal);
|
|
276
|
+
return {
|
|
277
|
+
valid: !diagnostics.some(diagnostic => diagnostic.severity === 'error'),
|
|
278
|
+
diagnostics
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Throws the signal reason when a conversion operation has been cancelled. */
|
|
283
|
+
function throwIfAborted(signal?: AbortSignal): void {
|
|
284
|
+
if (signal?.aborted) {
|
|
285
|
+
throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
|
|
286
|
+
}
|
|
287
|
+
}
|