@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.
Files changed (41) hide show
  1. package/dist/3d-tiles-converter/3d-tiles-converter.d.ts +19 -0
  2. package/dist/3d-tiles-converter/3d-tiles-converter.d.ts.map +1 -1
  3. package/dist/3d-tiles-converter/3d-tiles-converter.js +121 -30
  4. package/dist/3d-tiles-converter/3d-tiles-converter.js.map +1 -1
  5. package/dist/3d-tiles-converter/helpers/load-i3s.d.ts +28 -1
  6. package/dist/3d-tiles-converter/helpers/load-i3s.d.ts.map +1 -1
  7. package/dist/3d-tiles-converter/helpers/load-i3s.js +63 -4
  8. package/dist/3d-tiles-converter/helpers/load-i3s.js.map +1 -1
  9. package/dist/converter-cli.js +30 -21
  10. package/dist/converter-cli.js.map +1 -1
  11. package/dist/converter.min.cjs +137 -130
  12. package/dist/deps-installer/deps-installer.js +1 -1
  13. package/dist/i3s-converter/helpers/load-3d-tiles.d.ts.map +1 -1
  14. package/dist/i3s-converter/helpers/load-3d-tiles.js +22 -2
  15. package/dist/i3s-converter/helpers/load-3d-tiles.js.map +1 -1
  16. package/dist/i3s-converter/i3s-converter.js +1 -1
  17. package/dist/i3s-converter/i3s-converter.js.map +1 -1
  18. package/dist/i3s-server/bin/i3s-server.min.cjs +86 -86
  19. package/dist/index.cjs +590 -70
  20. package/dist/lib/json-schemas/conversion-dump-json-schema.d.ts +463 -0
  21. package/dist/lib/json-schemas/conversion-dump-json-schema.d.ts.map +1 -0
  22. package/dist/lib/json-schemas/conversion-dump-json-schema.js +463 -0
  23. package/dist/lib/json-schemas/conversion-dump-json-schema.js.map +1 -0
  24. package/dist/lib/utils/conversion-dump.d.ts +12 -5
  25. package/dist/lib/utils/conversion-dump.d.ts.map +1 -1
  26. package/dist/lib/utils/conversion-dump.js +44 -24
  27. package/dist/lib/utils/conversion-dump.js.map +1 -1
  28. package/dist/lib/utils/file-utils.d.ts +6 -0
  29. package/dist/lib/utils/file-utils.d.ts.map +1 -1
  30. package/dist/lib/utils/file-utils.js +7 -0
  31. package/dist/lib/utils/file-utils.js.map +1 -1
  32. package/dist/pgm-loader.js +1 -1
  33. package/package.json +15 -14
  34. package/src/3d-tiles-converter/3d-tiles-converter.ts +159 -34
  35. package/src/3d-tiles-converter/helpers/load-i3s.ts +106 -7
  36. package/src/converter-cli.ts +44 -29
  37. package/src/i3s-converter/helpers/load-3d-tiles.ts +52 -2
  38. package/src/i3s-converter/i3s-converter.ts +1 -1
  39. package/src/lib/json-schemas/conversion-dump-json-schema.ts +285 -0
  40. package/src/lib/utils/conversion-dump.ts +79 -27
  41. 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.sourceTileset = await load(inputUrl, I3SLoader, this.loaderOptions);
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
- try {
110
- await removeDir(this.tilesetPath);
111
- } catch (e) {
112
- // do nothing
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
- const content = await loadI3SContent(this.sourceTileset, sourceChild, this.loaderOptions);
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
- if (!sourceChild.obb) {
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
- child.content = {
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
- await this._addChildren(sourceChild, child, level + 1);
195
- } else {
196
- await this._addChildren(sourceChild, parentNode, level + 1);
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 load(nodeUrl, I3SLoader, options);
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(load(inputUrl, I3SAttributeLoader, options));
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 load(sourceTile.contentUrl, I3SLoader, loadOptions);
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
+ }
@@ -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
- const result = await inquirer.prompt<{isNewFileRequired: boolean}>([
110
- {
111
- name: 'isNewFileRequired',
112
- type: 'list',
113
- message: 'What would you like to do?',
114
- choices: [
115
- {
116
- name: 'Add hash file to the current SLPK file',
117
- value: false
118
- },
119
- {
120
- name: `Create a new file ${nameWithoutExt}-hash.slpk with hash file inside`,
121
- value: true
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
- if (result.isNewFileRequired) {
128
- finalPath = `${nameWithoutExt}-hash.slpk`;
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 {Tiles3DArchiveFileSystem} from '@loaders.gl/3d-tiles';
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 fileSystem = new Tiles3DArchiveFileSystem(tz3Path);
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