@loaders.gl/tile-converter 4.2.0-alpha.1 → 4.2.0-alpha.3
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/dist/3d-tiles-converter/3d-tiles-converter.d.ts +19 -0
- package/dist/3d-tiles-converter/3d-tiles-converter.d.ts.map +1 -1
- package/dist/3d-tiles-converter/3d-tiles-converter.js +121 -30
- package/dist/3d-tiles-converter/3d-tiles-converter.js.map +1 -1
- package/dist/3d-tiles-converter/helpers/load-i3s.d.ts +28 -1
- package/dist/3d-tiles-converter/helpers/load-i3s.d.ts.map +1 -1
- package/dist/3d-tiles-converter/helpers/load-i3s.js +63 -4
- package/dist/3d-tiles-converter/helpers/load-i3s.js.map +1 -1
- package/dist/converter-cli.js +30 -21
- package/dist/converter-cli.js.map +1 -1
- package/dist/converter.min.cjs +137 -130
- package/dist/deps-installer/deps-installer.js +1 -1
- package/dist/i3s-converter/helpers/load-3d-tiles.d.ts.map +1 -1
- package/dist/i3s-converter/helpers/load-3d-tiles.js +22 -2
- package/dist/i3s-converter/helpers/load-3d-tiles.js.map +1 -1
- package/dist/i3s-converter/i3s-converter.js +1 -1
- package/dist/i3s-converter/i3s-converter.js.map +1 -1
- package/dist/i3s-server/bin/i3s-server.min.cjs +86 -86
- package/dist/index.cjs +590 -70
- package/dist/lib/json-schemas/conversion-dump-json-schema.d.ts +463 -0
- package/dist/lib/json-schemas/conversion-dump-json-schema.d.ts.map +1 -0
- package/dist/lib/json-schemas/conversion-dump-json-schema.js +463 -0
- package/dist/lib/json-schemas/conversion-dump-json-schema.js.map +1 -0
- package/dist/lib/utils/conversion-dump.d.ts +12 -5
- package/dist/lib/utils/conversion-dump.d.ts.map +1 -1
- package/dist/lib/utils/conversion-dump.js +44 -24
- package/dist/lib/utils/conversion-dump.js.map +1 -1
- package/dist/lib/utils/file-utils.d.ts +6 -0
- package/dist/lib/utils/file-utils.d.ts.map +1 -1
- package/dist/lib/utils/file-utils.js +7 -0
- package/dist/lib/utils/file-utils.js.map +1 -1
- package/dist/pgm-loader.js +1 -1
- package/package.json +15 -14
- package/src/3d-tiles-converter/3d-tiles-converter.ts +159 -34
- package/src/3d-tiles-converter/helpers/load-i3s.ts +106 -7
- package/src/converter-cli.ts +44 -29
- package/src/i3s-converter/helpers/load-3d-tiles.ts +52 -2
- package/src/i3s-converter/i3s-converter.ts +1 -1
- package/src/lib/json-schemas/conversion-dump-json-schema.ts +285 -0
- package/src/lib/utils/conversion-dump.ts +79 -27
- package/src/lib/utils/file-utils.ts +13 -0
|
@@ -4,7 +4,7 @@ import type {
|
|
|
4
4
|
NodeReference,
|
|
5
5
|
I3STilesetHeader
|
|
6
6
|
} from '@loaders.gl/i3s';
|
|
7
|
-
import type {Tiles3DTileJSON} from '@loaders.gl/3d-tiles';
|
|
7
|
+
import type {Tile3DBoundingVolume, Tiles3DTileJSON} from '@loaders.gl/3d-tiles';
|
|
8
8
|
|
|
9
9
|
import {join} from 'path';
|
|
10
10
|
import process from 'process';
|
|
@@ -24,8 +24,11 @@ import {WorkerFarm} from '@loaders.gl/worker-utils';
|
|
|
24
24
|
import {BROWSER_ERROR_MESSAGE} from '../constants';
|
|
25
25
|
import B3dmConverter, {I3SAttributesData} from './helpers/b3dm-converter';
|
|
26
26
|
import {I3STileHeader} from '@loaders.gl/i3s/src/types';
|
|
27
|
-
import {loadI3SContent} from './helpers/load-i3s';
|
|
27
|
+
import {getNodeCount, loadFromArchive, loadI3SContent, openSLPK} from './helpers/load-i3s';
|
|
28
28
|
import {I3SLoaderOptions} from '@loaders.gl/i3s/src/i3s-loader';
|
|
29
|
+
import {ZipFileSystem} from '../../../zip/src';
|
|
30
|
+
import {ConversionDump, ConversionDumpOptions} from '../lib/utils/conversion-dump';
|
|
31
|
+
import {Progress} from '../i3s-converter/helpers/progress';
|
|
29
32
|
|
|
30
33
|
const I3S = 'I3S';
|
|
31
34
|
|
|
@@ -41,6 +44,7 @@ export default class Tiles3DConverter {
|
|
|
41
44
|
sourceTileset: I3STilesetHeader | null;
|
|
42
45
|
attributeStorageInfo?: AttributeStorageInfo[] | null;
|
|
43
46
|
workerSource: {[key: string]: string} = {};
|
|
47
|
+
slpkFilesystem: ZipFileSystem | null = null;
|
|
44
48
|
loaderOptions: I3SLoaderOptions = {
|
|
45
49
|
_nodeWorkers: true,
|
|
46
50
|
reuseWorkers: true,
|
|
@@ -52,6 +56,8 @@ export default class Tiles3DConverter {
|
|
|
52
56
|
workerUrl: './modules/i3s/dist/i3s-content-worker-node.js'
|
|
53
57
|
}
|
|
54
58
|
};
|
|
59
|
+
conversionDump: ConversionDump;
|
|
60
|
+
progress: Progress;
|
|
55
61
|
|
|
56
62
|
constructor() {
|
|
57
63
|
this.options = {};
|
|
@@ -62,6 +68,8 @@ export default class Tiles3DConverter {
|
|
|
62
68
|
this.sourceTileset = null;
|
|
63
69
|
this.attributeStorageInfo = null;
|
|
64
70
|
this.workerSource = {};
|
|
71
|
+
this.conversionDump = new ConversionDump();
|
|
72
|
+
this.progress = new Progress();
|
|
65
73
|
}
|
|
66
74
|
|
|
67
75
|
/**
|
|
@@ -79,20 +87,43 @@ export default class Tiles3DConverter {
|
|
|
79
87
|
tilesetName: string;
|
|
80
88
|
maxDepth?: number;
|
|
81
89
|
egmFilePath: string;
|
|
90
|
+
inquirer?: Promise<unknown>;
|
|
91
|
+
analyze?: boolean;
|
|
82
92
|
}): Promise<any> {
|
|
83
93
|
if (isBrowser) {
|
|
84
94
|
console.log(BROWSER_ERROR_MESSAGE);
|
|
85
95
|
return BROWSER_ERROR_MESSAGE;
|
|
86
96
|
}
|
|
87
|
-
const {inputUrl, outputPath, tilesetName, maxDepth, egmFilePath} = options;
|
|
97
|
+
const {inputUrl, outputPath, tilesetName, maxDepth, egmFilePath, inquirer, analyze} = options;
|
|
88
98
|
this.conversionStartTime = process.hrtime();
|
|
89
|
-
this.options = {maxDepth};
|
|
99
|
+
this.options = {maxDepth, inquirer};
|
|
90
100
|
|
|
91
101
|
console.log('Loading egm file...'); // eslint-disable-line
|
|
92
102
|
this.geoidHeightModel = await load(egmFilePath, PGMLoader);
|
|
93
103
|
console.log('Loading egm file completed!'); // eslint-disable-line
|
|
94
104
|
|
|
95
|
-
this.
|
|
105
|
+
this.slpkFilesystem = await openSLPK(inputUrl);
|
|
106
|
+
|
|
107
|
+
let preprocessResult = true;
|
|
108
|
+
if (analyze || this.slpkFilesystem) {
|
|
109
|
+
preprocessResult = await this.preprocessConversion();
|
|
110
|
+
if (!preprocessResult || analyze) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.progress.startMonitoring();
|
|
116
|
+
|
|
117
|
+
this.sourceTileset = await loadFromArchive(
|
|
118
|
+
inputUrl,
|
|
119
|
+
I3SLoader,
|
|
120
|
+
{
|
|
121
|
+
...this.loaderOptions,
|
|
122
|
+
// @ts-expect-error `isTileset` can be boolean of 'auto' but TS expects a string
|
|
123
|
+
i3s: {...this.loaderOptions.i3s, isTileset: true}
|
|
124
|
+
},
|
|
125
|
+
this.slpkFilesystem
|
|
126
|
+
);
|
|
96
127
|
|
|
97
128
|
if (!this.sourceTileset) {
|
|
98
129
|
return;
|
|
@@ -105,11 +136,28 @@ export default class Tiles3DConverter {
|
|
|
105
136
|
|
|
106
137
|
this.tilesetPath = join(`${outputPath}`, `${tilesetName}`);
|
|
107
138
|
this.attributeStorageInfo = this.sourceTileset.attributeStorageInfo;
|
|
139
|
+
|
|
140
|
+
await this.conversionDump.createDump(options as ConversionDumpOptions);
|
|
141
|
+
if (this.conversionDump.restored && this.options.inquirer) {
|
|
142
|
+
const result = await this.options.inquirer.prompt([
|
|
143
|
+
{
|
|
144
|
+
name: 'resumeConversion',
|
|
145
|
+
type: 'confirm',
|
|
146
|
+
message:
|
|
147
|
+
'Dump file of the previous conversion exists, do you want to resume that conversion?'
|
|
148
|
+
}
|
|
149
|
+
]);
|
|
150
|
+
if (!result.resumeConversion) {
|
|
151
|
+
this.conversionDump.reset();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
108
154
|
// Removing the tilesetPath needed to exclude erroneous files after conversion
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
155
|
+
if (!this.conversionDump.restored) {
|
|
156
|
+
try {
|
|
157
|
+
await removeDir(this.tilesetPath);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// do nothing
|
|
160
|
+
}
|
|
113
161
|
}
|
|
114
162
|
|
|
115
163
|
const rootTile: Tiles3DTileJSON = {
|
|
@@ -125,14 +173,47 @@ export default class Tiles3DConverter {
|
|
|
125
173
|
|
|
126
174
|
const tileset = transform({root: rootTile}, tilesetTemplate());
|
|
127
175
|
await writeFile(this.tilesetPath, JSON.stringify(tileset), 'tileset.json');
|
|
176
|
+
await this.conversionDump.deleteDumpFile();
|
|
177
|
+
|
|
178
|
+
this.progress.stopMonitoring();
|
|
128
179
|
|
|
129
|
-
this._finishConversion({slpk: false, outputPath, tilesetName});
|
|
180
|
+
await this._finishConversion({slpk: false, outputPath, tilesetName});
|
|
181
|
+
|
|
182
|
+
if (this.slpkFilesystem) {
|
|
183
|
+
this.slpkFilesystem.destroy();
|
|
184
|
+
}
|
|
130
185
|
|
|
131
186
|
// Clean up worker pools
|
|
132
187
|
const workerFarm = WorkerFarm.getWorkerFarm({});
|
|
133
188
|
workerFarm.destroy();
|
|
134
189
|
}
|
|
135
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Preprocess stage of the tile converter. Calculate number of nodes
|
|
193
|
+
* @returns true - the conversion is possible, false - the tileset's content is not supported
|
|
194
|
+
*/
|
|
195
|
+
private async preprocessConversion(): Promise<boolean> {
|
|
196
|
+
console.log(`Analyze source layer`);
|
|
197
|
+
const nodesCount = await getNodeCount(this.slpkFilesystem);
|
|
198
|
+
this.progress.stepsTotal = nodesCount;
|
|
199
|
+
|
|
200
|
+
console.log(`------------------------------------------------`);
|
|
201
|
+
console.log(`Preprocess results:`);
|
|
202
|
+
if (this.slpkFilesystem) {
|
|
203
|
+
console.log(`Node count: ${nodesCount}`);
|
|
204
|
+
if (nodesCount === 0) {
|
|
205
|
+
console.log('Node count is 0. The conversion will be interrupted.');
|
|
206
|
+
console.log(`------------------------------------------------`);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
console.log(`Node count cannot be calculated for the remote dataset`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(`------------------------------------------------`);
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
136
217
|
/**
|
|
137
218
|
* Convert particular I3S Node
|
|
138
219
|
* @param parentSourceNode the parent node tile object (@loaders.gl/tiles/Tile3D)
|
|
@@ -146,9 +227,25 @@ export default class Tiles3DConverter {
|
|
|
146
227
|
level: number,
|
|
147
228
|
childNodeInfo: NodeReference
|
|
148
229
|
): Promise<void> {
|
|
230
|
+
let nextParentNode = parentNode;
|
|
149
231
|
const sourceChild = await this._loadChildNode(parentSourceNode, childNodeInfo);
|
|
150
232
|
if (sourceChild.contentUrl) {
|
|
151
|
-
|
|
233
|
+
if (
|
|
234
|
+
this.conversionDump.restored &&
|
|
235
|
+
this.conversionDump.isFileConversionComplete(`${sourceChild.id}.b3dm`) &&
|
|
236
|
+
(sourceChild.obb || sourceChild.mbs)
|
|
237
|
+
) {
|
|
238
|
+
const {child} = this._createChildAndBoundingVolume(sourceChild);
|
|
239
|
+
parentNode.children.push(child);
|
|
240
|
+
await this._addChildren(sourceChild, child, level + 1);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const content = await loadI3SContent(
|
|
244
|
+
this.sourceTileset,
|
|
245
|
+
sourceChild,
|
|
246
|
+
this.loaderOptions,
|
|
247
|
+
this.slpkFilesystem
|
|
248
|
+
);
|
|
152
249
|
|
|
153
250
|
if (!content) {
|
|
154
251
|
await this._addChildren(sourceChild, parentNode, level + 1);
|
|
@@ -162,39 +259,39 @@ export default class Tiles3DConverter {
|
|
|
162
259
|
featureAttributes = await this._loadChildAttributes(sourceChild, this.attributeStorageInfo);
|
|
163
260
|
}
|
|
164
261
|
|
|
165
|
-
|
|
166
|
-
sourceChild.obb = createObbFromMbs(sourceChild.mbs);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const boundingVolume = {
|
|
170
|
-
box: i3sObbTo3dTilesObb(sourceChild.obb, this.geoidHeightModel)
|
|
171
|
-
};
|
|
172
|
-
const child: Tiles3DTileJSON = {
|
|
173
|
-
boundingVolume,
|
|
174
|
-
geometricError: convertScreenThresholdToGeometricError(sourceChild),
|
|
175
|
-
children: []
|
|
176
|
-
};
|
|
262
|
+
const {child, boundingVolume} = this._createChildAndBoundingVolume(sourceChild);
|
|
177
263
|
|
|
178
264
|
const i3sAttributesData: I3SAttributesData = {
|
|
179
265
|
tileContent: content,
|
|
180
|
-
box: boundingVolume.box,
|
|
266
|
+
box: boundingVolume.box || [],
|
|
181
267
|
textureFormat: sourceChild.textureFormat
|
|
182
268
|
};
|
|
183
269
|
|
|
184
270
|
const b3dmConverter = new B3dmConverter();
|
|
185
271
|
const b3dm = await b3dmConverter.convert(i3sAttributesData, featureAttributes);
|
|
186
272
|
|
|
187
|
-
|
|
188
|
-
uri: `${sourceChild.id}.b3dm`,
|
|
189
|
-
boundingVolume
|
|
190
|
-
};
|
|
273
|
+
await this.conversionDump.addNode(`${sourceChild.id}.b3dm`, sourceChild.id);
|
|
191
274
|
await writeFile(this.tilesetPath, new Uint8Array(b3dm), `${sourceChild.id}.b3dm`);
|
|
275
|
+
await this.conversionDump.updateConvertedNodesDumpFile(
|
|
276
|
+
`${sourceChild.id}.b3dm`,
|
|
277
|
+
sourceChild.id,
|
|
278
|
+
true
|
|
279
|
+
);
|
|
192
280
|
parentNode.children.push(child);
|
|
281
|
+
nextParentNode = child;
|
|
282
|
+
}
|
|
193
283
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
284
|
+
this.progress.stepsDone += 1;
|
|
285
|
+
let timeRemainingString = 'Calculating time left...';
|
|
286
|
+
const timeRemaining = this.progress.getTimeRemainingString();
|
|
287
|
+
if (timeRemaining) {
|
|
288
|
+
timeRemainingString = `${timeRemaining} left`;
|
|
197
289
|
}
|
|
290
|
+
const percentString = this.progress.getPercentString();
|
|
291
|
+
const progressString = percentString ? ` ${percentString}%, ${timeRemainingString}` : '';
|
|
292
|
+
console.log(`[converted${progressString}]: ${childNodeInfo.id}`); // eslint-disable-line
|
|
293
|
+
|
|
294
|
+
await this._addChildren(sourceChild, nextParentNode, level + 1);
|
|
198
295
|
}
|
|
199
296
|
|
|
200
297
|
/**
|
|
@@ -235,20 +332,48 @@ export default class Tiles3DConverter {
|
|
|
235
332
|
} else {
|
|
236
333
|
const nodeUrl = this._relativeUrlToFullUrl(parentNode.url, childNodeInfo.href!);
|
|
237
334
|
// load metadata
|
|
238
|
-
const options = {
|
|
335
|
+
const options: I3SLoaderOptions = {
|
|
239
336
|
i3s: {
|
|
240
337
|
...this.loaderOptions,
|
|
338
|
+
// @ts-expect-error
|
|
241
339
|
isTileHeader: true,
|
|
242
340
|
loadContent: false
|
|
243
341
|
}
|
|
244
342
|
};
|
|
245
343
|
|
|
246
344
|
console.log(`Node conversion: ${nodeUrl}`); // eslint-disable-line no-console,no-undef
|
|
247
|
-
header = await
|
|
345
|
+
header = await loadFromArchive(nodeUrl, I3SLoader, options, this.slpkFilesystem);
|
|
248
346
|
}
|
|
249
347
|
return header;
|
|
250
348
|
}
|
|
251
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Create child and child's boundingVolume for the converted node
|
|
352
|
+
* @param sourceChild
|
|
353
|
+
* @returns child and child's boundingVolume
|
|
354
|
+
*/
|
|
355
|
+
private _createChildAndBoundingVolume(sourceChild: I3STileHeader): {
|
|
356
|
+
boundingVolume: Tile3DBoundingVolume;
|
|
357
|
+
child: Tiles3DTileJSON;
|
|
358
|
+
} {
|
|
359
|
+
if (!sourceChild.obb) {
|
|
360
|
+
sourceChild.obb = createObbFromMbs(sourceChild.mbs);
|
|
361
|
+
}
|
|
362
|
+
const boundingVolume: Tile3DBoundingVolume = {
|
|
363
|
+
box: i3sObbTo3dTilesObb(sourceChild.obb, this.geoidHeightModel)
|
|
364
|
+
};
|
|
365
|
+
const child: Tiles3DTileJSON = {
|
|
366
|
+
boundingVolume,
|
|
367
|
+
geometricError: convertScreenThresholdToGeometricError(sourceChild),
|
|
368
|
+
children: [],
|
|
369
|
+
content: {
|
|
370
|
+
uri: `${sourceChild.id}.b3dm`,
|
|
371
|
+
boundingVolume
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
return {boundingVolume, child};
|
|
375
|
+
}
|
|
376
|
+
|
|
252
377
|
/**
|
|
253
378
|
* Make an url of a resource from its relative url having the base url
|
|
254
379
|
* @param baseUrl the base url. A resulting url will be related from this url
|
|
@@ -292,7 +417,7 @@ export default class Tiles3DConverter {
|
|
|
292
417
|
attributeType: this._getAttributeType(attribute)
|
|
293
418
|
};
|
|
294
419
|
|
|
295
|
-
promises.push(
|
|
420
|
+
promises.push(loadFromArchive(inputUrl, I3SAttributeLoader, options, this.slpkFilesystem));
|
|
296
421
|
}
|
|
297
422
|
const attributesList = await Promise.all(promises);
|
|
298
423
|
this._replaceNestedArrays(attributesList);
|
|
@@ -1,36 +1,45 @@
|
|
|
1
|
-
import {load} from '@loaders.gl/core';
|
|
1
|
+
import {LoaderWithParser, load} from '@loaders.gl/core';
|
|
2
2
|
import {
|
|
3
3
|
I3STileContent,
|
|
4
4
|
I3STileHeader,
|
|
5
5
|
I3STilesetHeader,
|
|
6
6
|
I3SLoader,
|
|
7
|
-
I3SLoaderOptions
|
|
7
|
+
I3SLoaderOptions,
|
|
8
|
+
parseSLPKArchive
|
|
8
9
|
} from '@loaders.gl/i3s';
|
|
10
|
+
import {FileHandleFile} from '@loaders.gl/loader-utils';
|
|
11
|
+
import {ZipFileSystem, makeZipCDHeaderIterator} from '@loaders.gl/zip';
|
|
12
|
+
|
|
13
|
+
export type SLPKUrlParts = {slpkFileName: string; internalFileName: string};
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Load I3S node content
|
|
12
17
|
* @param sourceTileset - source layer JSON
|
|
13
18
|
* @param sourceTile - source I3S node metadata
|
|
14
19
|
* @param tilesetLoadOptions - load options for Tiles3DLoader
|
|
20
|
+
* @param slpkFilesystem - loaded instance of ZipFileSystem for local convertion from SLPK file
|
|
15
21
|
* @returns - 3DTiles tile content or null
|
|
16
22
|
*/
|
|
17
23
|
export const loadI3SContent = async (
|
|
18
24
|
sourceTileset: I3STilesetHeader | null,
|
|
19
25
|
sourceTile: I3STileHeader,
|
|
20
|
-
tilesetLoadOptions: I3SLoaderOptions
|
|
26
|
+
tilesetLoadOptions: I3SLoaderOptions,
|
|
27
|
+
slpkFilesystem: ZipFileSystem | null
|
|
21
28
|
): Promise<I3STileContent | null> => {
|
|
22
29
|
if (!sourceTileset || !sourceTile.contentUrl) {
|
|
23
30
|
return null;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
|
-
const loadOptions = {
|
|
33
|
+
const loadOptions: I3SLoaderOptions = {
|
|
27
34
|
...tilesetLoadOptions,
|
|
28
35
|
i3s: {
|
|
29
36
|
...tilesetLoadOptions.i3s,
|
|
37
|
+
// @ts-expect-error
|
|
30
38
|
isTileset: false,
|
|
39
|
+
// @ts-expect-error
|
|
31
40
|
isTileHeader: false,
|
|
32
41
|
_tileOptions: {
|
|
33
|
-
attributeUrls: sourceTile.attributeUrls,
|
|
42
|
+
attributeUrls: sourceTile.attributeUrls || [],
|
|
34
43
|
textureUrl: sourceTile.textureUrl,
|
|
35
44
|
textureFormat: sourceTile.textureFormat,
|
|
36
45
|
textureLoaderOptions: sourceTile.textureLoaderOptions,
|
|
@@ -40,13 +49,103 @@ export const loadI3SContent = async (
|
|
|
40
49
|
},
|
|
41
50
|
_tilesetOptions: {
|
|
42
51
|
store: sourceTileset.store,
|
|
52
|
+
// @ts-expect-error
|
|
43
53
|
attributeStorageInfo: sourceTileset.attributeStorageInfo,
|
|
54
|
+
// @ts-expect-error
|
|
44
55
|
fields: sourceTileset.fields
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
};
|
|
48
|
-
const tileContent = await
|
|
59
|
+
const tileContent = await loadFromArchive(
|
|
60
|
+
sourceTile.contentUrl,
|
|
61
|
+
I3SLoader,
|
|
62
|
+
loadOptions,
|
|
63
|
+
slpkFilesystem
|
|
64
|
+
);
|
|
49
65
|
|
|
50
|
-
// @ts-expect-error
|
|
51
66
|
return tileContent;
|
|
52
67
|
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Load local SLPK file to ZipFileSystem instance
|
|
71
|
+
* @param url - path to SLPK file
|
|
72
|
+
* @returns instance of ZipFileSystem or null if url is not an SLPK file
|
|
73
|
+
*/
|
|
74
|
+
export async function openSLPK(url: string): Promise<ZipFileSystem | null> {
|
|
75
|
+
const slpkUrlParts = url.split('.slpk');
|
|
76
|
+
const {slpkFileName} = getSlpkUrlParts(url) || {};
|
|
77
|
+
if (slpkFileName) {
|
|
78
|
+
const slpkFileName = `${slpkUrlParts[0]}.slpk`;
|
|
79
|
+
const fileProvider = new FileHandleFile(slpkFileName);
|
|
80
|
+
const archive = await parseSLPKArchive(fileProvider, undefined, slpkFileName);
|
|
81
|
+
const fileSystem = new ZipFileSystem(archive);
|
|
82
|
+
return fileSystem;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load a resource with load options and .3tz format support
|
|
89
|
+
* @param url - resource URL
|
|
90
|
+
* @param loader - loader to parse data (Tiles3DLoader / CesiumIonLoader)
|
|
91
|
+
* @param loadOptions - i3s loader options
|
|
92
|
+
* @returns i3s resource
|
|
93
|
+
*/
|
|
94
|
+
export async function loadFromArchive(
|
|
95
|
+
url: string,
|
|
96
|
+
loader: LoaderWithParser,
|
|
97
|
+
loadOptions: I3SLoaderOptions,
|
|
98
|
+
fileSystem: ZipFileSystem | null
|
|
99
|
+
) {
|
|
100
|
+
const slpkUrlParts = getSlpkUrlParts(url);
|
|
101
|
+
if (fileSystem !== null && slpkUrlParts !== null) {
|
|
102
|
+
const {internalFileName} = slpkUrlParts;
|
|
103
|
+
const content = await load(internalFileName, loader, {
|
|
104
|
+
...loadOptions,
|
|
105
|
+
fetch: fileSystem.fetch.bind(fileSystem)
|
|
106
|
+
});
|
|
107
|
+
return content;
|
|
108
|
+
}
|
|
109
|
+
return await load(url, loader, loadOptions);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Extract slpk file path and internal from the url
|
|
114
|
+
* For example, for `./path/to/file.slpk/nodes/0` it returns
|
|
115
|
+
* {"slpkFileName": "./path/to/file.slpk", "internalFileName": "/nodes/0" }
|
|
116
|
+
* @param url full internal file path
|
|
117
|
+
* @returns object with internal slpk file parts
|
|
118
|
+
*/
|
|
119
|
+
function getSlpkUrlParts(url: string): SLPKUrlParts | null {
|
|
120
|
+
const slpkUrlParts = url.split('.slpk');
|
|
121
|
+
let result: SLPKUrlParts | null;
|
|
122
|
+
// Not '.slpk'. The file will be loaded with global fetch function
|
|
123
|
+
if (slpkUrlParts.length === 1) {
|
|
124
|
+
result = null;
|
|
125
|
+
} else if (slpkUrlParts.length === 2) {
|
|
126
|
+
result = {slpkFileName: `${slpkUrlParts[0]}.slpk`, internalFileName: slpkUrlParts[1].slice(1)};
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error('Unexpected URL format');
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get nodes count inside SLPK
|
|
135
|
+
* @param fileSystem - file system of SLPK
|
|
136
|
+
* @returns number of nodes
|
|
137
|
+
*/
|
|
138
|
+
export async function getNodeCount(fileSystem: ZipFileSystem | null): Promise<number> {
|
|
139
|
+
if (!fileSystem?.fileProvider) {
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
let count = 0;
|
|
143
|
+
const filesIterator = makeZipCDHeaderIterator(fileSystem.fileProvider);
|
|
144
|
+
for await (const file of filesIterator) {
|
|
145
|
+
const filename = file.fileName;
|
|
146
|
+
if (filename.indexOf('3dNodeIndexDocument.json.gz') >= 0) {
|
|
147
|
+
count++;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return count;
|
|
151
|
+
}
|
package/src/converter-cli.ts
CHANGED
|
@@ -59,6 +59,8 @@ type TileConversionOptions = {
|
|
|
59
59
|
metadataClass?: string;
|
|
60
60
|
/** With this options the tileset content will be analyzed without conversion */
|
|
61
61
|
analyze?: boolean;
|
|
62
|
+
/** Skip all prompts that stop conversion and wait for a user input */
|
|
63
|
+
quiet?: boolean;
|
|
62
64
|
};
|
|
63
65
|
|
|
64
66
|
/* During validation we check that particular options are defined so they can't be undefined */
|
|
@@ -100,36 +102,40 @@ async function main() {
|
|
|
100
102
|
if (options.addHash) {
|
|
101
103
|
const validatedOptions = validateOptions(options, true);
|
|
102
104
|
let finalPath = validatedOptions.tileset;
|
|
103
|
-
if (validatedOptions.output === 'data') {
|
|
104
|
-
const nameWithoutExt = validatedOptions.tileset.substring(
|
|
105
|
-
0,
|
|
106
|
-
validatedOptions.tileset.length - 5
|
|
107
|
-
);
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
if (!options.quiet) {
|
|
107
|
+
if (validatedOptions.output === 'data') {
|
|
108
|
+
const nameWithoutExt = validatedOptions.tileset.substring(
|
|
109
|
+
0,
|
|
110
|
+
validatedOptions.tileset.length - 5
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await inquirer.prompt<{isNewFileRequired: boolean}>([
|
|
114
|
+
{
|
|
115
|
+
name: 'isNewFileRequired',
|
|
116
|
+
type: 'list',
|
|
117
|
+
message: 'What would you like to do?',
|
|
118
|
+
choices: [
|
|
119
|
+
{
|
|
120
|
+
name: 'Add hash file to the current SLPK file',
|
|
121
|
+
value: false
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: `Create a new file ${nameWithoutExt}-hash.slpk with hash file inside`,
|
|
125
|
+
value: true
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
]);
|
|
126
130
|
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
if (result.isNewFileRequired) {
|
|
132
|
+
finalPath = `${nameWithoutExt}-hash.slpk`;
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
finalPath = validatedOptions.output;
|
|
129
136
|
}
|
|
130
|
-
} else {
|
|
131
|
-
finalPath = validatedOptions.output;
|
|
132
137
|
}
|
|
138
|
+
|
|
133
139
|
if (finalPath !== validatedOptions.tileset) {
|
|
134
140
|
await copyFile(validatedOptions.tileset, finalPath);
|
|
135
141
|
}
|
|
@@ -190,6 +196,9 @@ function printHelp(): void {
|
|
|
190
196
|
'--metadata-class [One of the list of feature metadata classes, detected by converter on "analyze" stage, default: not set]'
|
|
191
197
|
);
|
|
192
198
|
console.log('--validate [Enable validation]');
|
|
199
|
+
console.log(
|
|
200
|
+
'--quiet [Skip all prompts that stop conversion and wait for a user input: default: false]'
|
|
201
|
+
);
|
|
193
202
|
process.exit(0); // eslint-disable-line
|
|
194
203
|
}
|
|
195
204
|
|
|
@@ -210,7 +219,9 @@ async function convert(options: ValidatedTileConversionOptions) {
|
|
|
210
219
|
outputPath: options.output,
|
|
211
220
|
tilesetName: options.name,
|
|
212
221
|
maxDepth: options.maxDepth,
|
|
213
|
-
egmFilePath: options.egm
|
|
222
|
+
egmFilePath: options.egm,
|
|
223
|
+
analyze: options.analyze,
|
|
224
|
+
inquirer: options.quiet ? undefined : inquirer
|
|
214
225
|
});
|
|
215
226
|
break;
|
|
216
227
|
case TILESET_TYPE._3DTILES:
|
|
@@ -232,7 +243,7 @@ async function convert(options: ValidatedTileConversionOptions) {
|
|
|
232
243
|
instantNodeWriting: options.instantNodeWriting,
|
|
233
244
|
metadataClass: options.metadataClass,
|
|
234
245
|
analyze: options.analyze,
|
|
235
|
-
inquirer
|
|
246
|
+
inquirer: options.quiet ? undefined : inquirer
|
|
236
247
|
});
|
|
237
248
|
break;
|
|
238
249
|
default:
|
|
@@ -307,7 +318,8 @@ function parseOptions(args: string[]): TileConversionOptions {
|
|
|
307
318
|
generateBoundingVolumes: false,
|
|
308
319
|
validate: false,
|
|
309
320
|
slpk: false,
|
|
310
|
-
addHash: false
|
|
321
|
+
addHash: false,
|
|
322
|
+
quiet: false
|
|
311
323
|
};
|
|
312
324
|
|
|
313
325
|
// eslint-disable-next-line complexity
|
|
@@ -368,6 +380,9 @@ function parseOptions(args: string[]): TileConversionOptions {
|
|
|
368
380
|
case '--analyze':
|
|
369
381
|
opts.analyze = getBooleanValue(index, args);
|
|
370
382
|
break;
|
|
383
|
+
case '--quiet':
|
|
384
|
+
opts.quiet = getBooleanValue(index, args);
|
|
385
|
+
break;
|
|
371
386
|
case '--metadata-class':
|
|
372
387
|
opts.metadataClass = getStringValue(index, args);
|
|
373
388
|
break;
|
|
@@ -4,8 +4,17 @@ import type {
|
|
|
4
4
|
Tiles3DTileJSONPostprocessed,
|
|
5
5
|
Tiles3DTilesetJSONPostprocessed
|
|
6
6
|
} from '@loaders.gl/3d-tiles';
|
|
7
|
-
import {
|
|
7
|
+
import {Tiles3DArchive} from '@loaders.gl/3d-tiles';
|
|
8
8
|
import {LoaderWithParser, load} from '@loaders.gl/core';
|
|
9
|
+
import {FileHandleFile, FileProvider} from '@loaders.gl/loader-utils';
|
|
10
|
+
import {
|
|
11
|
+
CD_HEADER_SIGNATURE,
|
|
12
|
+
ZipFileSystem,
|
|
13
|
+
parseHashTable,
|
|
14
|
+
parseZipCDFileHeader,
|
|
15
|
+
parseZipLocalFileHeader,
|
|
16
|
+
searchFromTheEnd
|
|
17
|
+
} from '@loaders.gl/zip';
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Load nested 3DTiles tileset. If the sourceTile is not nested tileset - do nothing
|
|
@@ -104,7 +113,10 @@ export async function loadFromArchive(
|
|
|
104
113
|
}
|
|
105
114
|
if (filename) {
|
|
106
115
|
const tz3Path = `${tz3UrlParts[0]}.3tz`;
|
|
107
|
-
const
|
|
116
|
+
const fileProvider = new FileHandleFile(tz3Path);
|
|
117
|
+
const hashTable = await loadHashTable(fileProvider);
|
|
118
|
+
const archive = new Tiles3DArchive(fileProvider, hashTable, tz3Path);
|
|
119
|
+
const fileSystem = new ZipFileSystem(archive);
|
|
108
120
|
const content = await load(filename, loader, {
|
|
109
121
|
...loadOptions,
|
|
110
122
|
fetch: fileSystem.fetch.bind(fileSystem)
|
|
@@ -123,3 +135,41 @@ export async function loadFromArchive(
|
|
|
123
135
|
export function isNestedTileset(tile: Tiles3DTileJSONPostprocessed) {
|
|
124
136
|
return tile?.type === 'json' || tile?.type === '3tz';
|
|
125
137
|
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Load hash file from 3TZ
|
|
141
|
+
* @param fileProvider - binary reader of 3TZ
|
|
142
|
+
* @returns hash table of the 3TZ file content or undefined if the hash file is not presented inside
|
|
143
|
+
*/
|
|
144
|
+
async function loadHashTable(
|
|
145
|
+
fileProvider: FileProvider
|
|
146
|
+
): Promise<undefined | Record<string, bigint>> {
|
|
147
|
+
let hashTable: undefined | Record<string, bigint>;
|
|
148
|
+
|
|
149
|
+
const hashCDOffset = await searchFromTheEnd(fileProvider, CD_HEADER_SIGNATURE);
|
|
150
|
+
|
|
151
|
+
const cdFileHeader = await parseZipCDFileHeader(hashCDOffset, fileProvider);
|
|
152
|
+
|
|
153
|
+
// '@3dtilesIndex1@' is index file that must be the last in the archive. It allows
|
|
154
|
+
// to improve load and read performance when the archive contains a very large number
|
|
155
|
+
// of files.
|
|
156
|
+
if (cdFileHeader?.fileName === '@3dtilesIndex1@') {
|
|
157
|
+
const localFileHeader = await parseZipLocalFileHeader(
|
|
158
|
+
cdFileHeader.localHeaderOffset,
|
|
159
|
+
fileProvider
|
|
160
|
+
);
|
|
161
|
+
if (!localFileHeader) {
|
|
162
|
+
throw new Error('corrupted 3tz');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const fileDataOffset = localFileHeader.fileDataOffset;
|
|
166
|
+
const hashFile = await fileProvider.slice(
|
|
167
|
+
fileDataOffset,
|
|
168
|
+
fileDataOffset + localFileHeader.compressedSize
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
hashTable = parseHashTable(hashFile);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return hashTable;
|
|
175
|
+
}
|
|
@@ -322,7 +322,6 @@ export default class I3SConverter {
|
|
|
322
322
|
console.log('Feature metadata classes have not been found');
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
console.log(`------------------------------------------------`);
|
|
326
325
|
if (
|
|
327
326
|
!meshTopologyTypes.has(GLTFPrimitiveModeString.TRIANGLES) &&
|
|
328
327
|
!meshTopologyTypes.has(GLTFPrimitiveModeString.TRIANGLE_STRIP)
|
|
@@ -334,6 +333,7 @@ export default class I3SConverter {
|
|
|
334
333
|
return false;
|
|
335
334
|
}
|
|
336
335
|
|
|
336
|
+
console.log(`------------------------------------------------`);
|
|
337
337
|
return true;
|
|
338
338
|
}
|
|
339
339
|
|