• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

visgl / loaders.gl / 25285056678

03 May 2026 04:52PM UTC coverage: 60.005% (+0.3%) from 59.717%
25285056678

push

github

web-flow
feat: Add COPC and potree sources (#3413)

12967 of 23938 branches covered (54.17%)

Branch coverage included in aggregate %.

875 of 1287 new or added lines in 33 files covered. (67.99%)

5 existing lines in 5 files now uncovered.

26843 of 42406 relevant lines covered (63.3%)

14662.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.88
/modules/copc/src/copc-source-loader.ts
1
// loaders.gl
2
// SPDX-License-Identifier: MIT
3
// Copyright (c) vis.gl contributors
4

5
import type {Schema, Field, DataType, Mesh, MeshArrowTable} from '@loaders.gl/schema';
6
import {convertMeshToTable} from '@loaders.gl/schema-utils';
2✔
7
import type {
8
  CoreAPI,
9
  SourceLoader,
10
  DataSourceOptions,
11
  TileSource,
12
  TileSourceMetadata,
13
  GetTileParameters,
14
  GetTileDataParameters
15
} from '@loaders.gl/loader-utils';
16
import {DataSource} from '@loaders.gl/loader-utils';
17
import {Proj4Projection} from '@math.gl/proj4';
18

19
import {Copc, Hierarchy, Dimension, Getter, Bounds, Key} from 'copc';
20

21
const VERSION = '1.0.0';
4✔
22
const COORDINATE_SYSTEM = {
4✔
23
  CARTESIAN: 'cartesian',
24
  LNGLAT_OFFSETS: 'lnglat-offsets'
25
} as const;
26

27
type COPCViewState = {
28
  boundingVolume: {
29
    cartographicBounds: [number[], number[]];
30
    center: number[];
31
    radius: number;
32
  };
33
  cartographicCenter: number[];
34
  zoom: number;
35
};
36

37
type COPCMetadata = TileSourceMetadata & {
38
  formatSpecificMetadata: Copc;
39
  viewState: COPCViewState;
40
};
41

42
type GetNodeParameters = {
43
  nodeIndex: [depth: number, x: number, y: number, z: number];
44
  columns?: string[];
45
  offset?: number;
46
  limit?: number;
47
};
48

49
import {COPCFormat} from './copc-format';
50

51
export type COPCSourceLoaderOptions = DataSourceOptions & {
52
  copc?: {
53
    sourceCoordinateSystem?: string;
54
  };
55
};
56

57
/**
58
 * Creates point cloud tile source for COPC urls or blobs
59
 */
60
export const COPCSourceLoader = {
4✔
61
  ...COPCFormat,
62
  dataType: null as unknown as COPCTileSource,
63
  batchType: null as never,
64
  name: 'COPC',
65
  id: 'copc',
66
  module: 'copc',
67
  version: VERSION,
68
  encoding: 'binary',
69
  format: 'copc',
70
  extensions: ['laz'],
71
  mimeTypes: ['application/octet-stream'],
72
  type: 'copc',
73
  fromUrl: true,
74
  fromBlob: true,
75

76
  options: {
77
    copc: {}
78
  },
79

80
  defaultOptions: {
81
    copc: {}
82
  },
83

NEW
84
  testURL: (url: string) => /\.copc\.laz($|\?)/i.test(url),
×
85
  createDataSource: (url: string | Blob, options: COPCSourceLoaderOptions, coreApi?: CoreAPI) =>
86
    new COPCTileSource(url, options, coreApi)
8✔
87
} as const satisfies SourceLoader<COPCTileSource>;
88

89
/**
90
 * A COPC data source
91
 * @note Can be either a raster or vector tile source depending on the contents of the COPC file.
92
 */
93
export class COPCTileSource
94
  extends DataSource<string | Blob, COPCSourceLoaderOptions>
95
  implements TileSource
96
{
97
  mimeType: string | null = null;
8✔
98
  metadata: Promise<COPCMetadata>;
99
  isReady = false;
8✔
100

101
  protected _initPromise: Promise<{
102
    copc: Copc;
103
    hierarchy: Hierarchy.Subtree;
104
    rootNode: Hierarchy.Node;
105
  }>;
106
  protected _urlOrGetter: string | Getter;
107
  protected _copc: Copc | null = null;
8✔
108
  protected _projection: Proj4Projection | null = null;
8✔
109
  protected _hierarchy: Hierarchy.Subtree | null = null;
8✔
110
  protected _pageLoadPromises: Map<string, Promise<void>> = new Map();
8✔
111

112
  constructor(data: string | Blob, options: COPCSourceLoaderOptions, coreApi?: CoreAPI) {
113
    super(data, options, COPCSourceLoader.defaultOptions, coreApi);
8✔
114
    this._urlOrGetter = createCOPCGetter(data, this.url);
8✔
115
    this._initPromise = this._initCopc(this.url || 'Blob');
8✔
116
    this.metadata = this.getMetadata();
8✔
117
  }
118

119
  async initialize(): Promise<void> {
120
    await this._initPromise;
100✔
121
  }
122

123
  async getSchema(): Promise<Schema> {
124
    const {copc, rootNode} = await this._initPromise;
×
125
    const view = await Copc.loadPointDataView(this._urlOrGetter, copc, rootNode);
×
126

127
    const fields: Field[] = [];
×
128
    for (const [name, dimension] of Object.entries(view.dimensions)) {
×
129
      if (dimension) {
×
130
        const type = getDataTypeFromDimension(dimension);
×
131
        fields.push({name, type, nullable: false});
×
132
      }
133
    }
134

135
    return {fields, metadata: {}};
×
136
  }
137

138
  async getMetadata(): Promise<COPCMetadata> {
139
    const {copc} = await this._initPromise;
10✔
140
    const viewState = this.getInferredViewState();
10✔
141
    const [minBounds, maxBounds] = viewState.boundingVolume.cartographicBounds;
10✔
142
    const metadata: COPCMetadata = {
10✔
143
      format: 'copc',
144
      boundingBox: [
145
        [minBounds[0], minBounds[1]],
146
        [maxBounds[0], maxBounds[1]]
147
      ],
148
      formatSpecificMetadata: copc,
149
      viewState
150
    };
151
    return metadata;
10✔
152
  }
153

154
  async getRootTile(): Promise<{
155
    id: string;
156
    level: number;
157
    pointCount: number;
158
    geometricError: number;
159
    boundingVolume: {
160
      cartographicBounds: [number[], number[]];
161
      center: number[];
162
      radius: number;
163
    };
164
  }> {
165
    const {rootNode} = await this._initPromise;
4✔
166
    return {
4✔
167
      id: '0-0-0-0',
168
      level: 0,
169
      pointCount: rootNode.pointCount,
170
      geometricError: this.getGeometricError(0),
171
      boundingVolume: this.getDataBoundingVolume()
172
    };
173
  }
174

175
  async getChildren(tile: {id: string}): Promise<
176
    {
177
      id: string;
178
      level: number;
179
      pointCount: number;
180
      geometricError: number;
181
      boundingVolume: {
182
        cartographicBounds: [number[], number[]];
183
        center: number[];
184
        radius: number;
185
      };
186
    }[]
187
  > {
188
    await this.initialize();
5✔
189
    await this.ensureHierarchyLoaded(tile.id);
5✔
190

191
    const childKeys = this.getChildKeys(tile.id);
5✔
192
    const children = await Promise.all(
5✔
193
      childKeys.map(async childKey => {
194
        const node = await this.getNodeById(childKey);
40✔
195
        return node ? this.getTileHeader(childKey, node) : null;
40✔
196
      })
197
    );
198

199
    return children.filter(Boolean) as {
5✔
200
      id: string;
201
      level: number;
202
      pointCount: number;
203
      geometricError: number;
204
      boundingVolume: {
205
        cartographicBounds: [number[], number[]];
206
        center: number[];
207
        radius: number;
208
      };
209
    }[];
210
  }
211

212
  getViewState(): COPCViewState {
213
    return this.getInferredViewState();
2✔
214
  }
215

216
  async getTile(tileParams: GetTileParameters): Promise<number[] | null> {
NEW
217
    const nodeIndex: [number, number, number, number] = [
×
218
      0,
219
      tileParams.x,
220
      tileParams.y,
221
      tileParams.z
222
    ];
UNCOV
223
    return this.getPoints({nodeIndex});
×
224
  }
225

226
  async getTileData(parameters: GetTileDataParameters): Promise<unknown | null> {
227
    throw new Error('Not implemented');
×
228
  }
229

230
  async getPoints(parameters: GetNodeParameters) {
231
    const {copc} = await this._initPromise;
×
232
    const node = await this.getNode(parameters);
×
233
    const view = node && (await Copc.loadPointDataView(this._urlOrGetter, copc, node));
×
234
    if (!view) {
×
235
      return null;
×
236
    }
237

238
    // console.log('Dimensions:', view.dimensions);
239

240
    const schema = await this.getSchema();
×
241
    const columnNames = schema.fields.map(field => field.name);
×
242
    const columnGetters = columnNames.map(name => view.getter(name));
×
243

244
    // const offset = parameters.offset || 0;
245
    // const limit = Math.min(parameters.limit ?? view.pointCount, view.pointCount - offset);
246
    // const ArrayType = getArrayTypeFromDataType(limit);
247

248
    function getXyzi(index: number): number[] {
249
      return columnGetters.map(get => get(index));
×
250
    }
251
    const point = getXyzi(0);
×
252
    // console.log('Point:', point);
253
    return point;
×
254
  }
255

256
  async getNode(parameters: GetNodeParameters): Promise<Hierarchy.Node | undefined> {
NEW
257
    return await this.getNodeById(Key.toString(parameters.nodeIndex));
×
258
  }
259

260
  async loadTileContent(tile: {id: string}) {
261
    const {copc} = await this._initPromise;
2✔
262
    const node = await this.getNodeById(tile.id);
2✔
263
    if (!node) {
2!
NEW
264
      return null;
×
265
    }
266

267
    const view = await Copc.loadPointDataView(this._urlOrGetter, copc, node);
2✔
268
    const pointCount = view.pointCount;
2✔
269
    const positions = new Float32Array(pointCount * 3);
2✔
270
    const nativeOrigin = this.getNativeTileCenter(tile.id);
2✔
271
    const cartographicOrigin = this.projectPoint(nativeOrigin);
2✔
272
    const colors = this.createColorArray(view, pointCount);
2✔
273

274
    this.populateTileAttributes(view, positions, colors, nativeOrigin, cartographicOrigin);
2✔
275

276
    return this.createTileContentResult(pointCount, positions, colors, cartographicOrigin);
2✔
277
  }
278

279
  async _initCopc(url: string) {
280
    const copc = await Copc.create(this._urlOrGetter);
8✔
281
    const hierarchy = await Copc.loadHierarchyPage(this._urlOrGetter, copc.info.rootHierarchyPage);
8✔
282
    const {['0-0-0-0']: rootNode} = hierarchy.nodes;
8✔
283
    if (!rootNode) {
8!
284
      throw new Error(`Failed to load COPC hierarchy root node ${url}`);
×
285
    }
286
    this._copc = copc;
8✔
287
    this._hierarchy = hierarchy;
8✔
288
    this._projection = createProjection(copc.wkt || this.options.copc?.sourceCoordinateSystem);
8!
289
    this.isReady = true;
8✔
290
    return {copc, hierarchy, rootNode};
8✔
291
  }
292

293
  protected async getNodeById(tileId: string): Promise<Hierarchy.Node | undefined> {
294
    await this.initialize();
42✔
295

296
    if (!this._hierarchy) {
42!
NEW
297
      return undefined;
×
298
    }
299

300
    if (this._hierarchy.nodes[tileId]) {
42✔
301
      return this._hierarchy.nodes[tileId];
14✔
302
    }
303

304
    const parentKeys = this.getAncestorKeys(tileId);
28✔
305
    for (const parentKey of parentKeys) {
28✔
306
      await this.ensureHierarchyLoaded(parentKey);
44✔
307
      if (this._hierarchy.nodes[tileId]) {
44!
NEW
308
        return this._hierarchy.nodes[tileId];
×
309
      }
310
    }
311

312
    return this._hierarchy.nodes[tileId];
28✔
313
  }
314

315
  protected createColorArray(
316
    view: Awaited<ReturnType<typeof Copc.loadPointDataView>>,
317
    pointCount: number
318
  ) {
319
    const hasColors =
320
      Boolean(view.dimensions.Red) &&
2✔
321
      Boolean(view.dimensions.Green) &&
322
      Boolean(view.dimensions.Blue);
323
    return hasColors ? new Uint16Array(pointCount * 3) : null;
2!
324
  }
325

326
  protected populateTileAttributes(
327
    view: Awaited<ReturnType<typeof Copc.loadPointDataView>>,
328
    positions: Float32Array,
329
    colors: Uint16Array | null,
330
    nativeOrigin: number[],
331
    cartographicOrigin: number[]
332
  ): void {
333
    const getX = view.getter('X');
2✔
334
    const getY = view.getter('Y');
2✔
335
    const getZ = view.getter('Z');
2✔
336
    const getRed = colors ? view.getter('Red') : null;
2!
337
    const getGreen = colors ? view.getter('Green') : null;
2!
338
    const getBlue = colors ? view.getter('Blue') : null;
2!
339

340
    for (let index = 0; index < view.pointCount; index++) {
2✔
341
      const targetIndex = index * 3;
78,393✔
342
      this.writePositionValues(
78,393✔
343
        positions,
344
        targetIndex,
345
        [getX(index), getY(index), getZ(index)],
346
        nativeOrigin,
347
        cartographicOrigin
348
      );
349
      if (colors && getRed && getGreen && getBlue) {
78,393!
350
        this.writeColorValues(colors, targetIndex, getRed(index), getGreen(index), getBlue(index));
78,393✔
351
      }
352
    }
353
  }
354

355
  protected writePositionValues(
356
    positions: Float32Array,
357
    targetIndex: number,
358
    nativePosition: [number, number, number],
359
    nativeOrigin: number[],
360
    cartographicOrigin: number[]
361
  ): void {
362
    if (this._projection) {
78,393!
363
      const cartographicPosition = this.projectPoint(nativePosition);
78,393✔
364
      positions[targetIndex] = cartographicPosition[0] - cartographicOrigin[0];
78,393✔
365
      positions[targetIndex + 1] = cartographicPosition[1] - cartographicOrigin[1];
78,393✔
366
      positions[targetIndex + 2] = nativePosition[2] - nativeOrigin[2];
78,393✔
367
      return;
78,393✔
368
    }
369

NEW
370
    positions[targetIndex] = nativePosition[0] - nativeOrigin[0];
×
NEW
371
    positions[targetIndex + 1] = nativePosition[1] - nativeOrigin[1];
×
NEW
372
    positions[targetIndex + 2] = nativePosition[2] - nativeOrigin[2];
×
373
  }
374

375
  protected writeColorValues(
376
    colors: Uint16Array,
377
    targetIndex: number,
378
    red: number,
379
    green: number,
380
    blue: number
381
  ): void {
382
    colors[targetIndex] = red;
78,393✔
383
    colors[targetIndex + 1] = green;
78,393✔
384
    colors[targetIndex + 2] = blue;
78,393✔
385
  }
386

387
  protected createTileContentResult(
388
    pointCount: number,
389
    positions: Float32Array,
390
    colors: Uint16Array | null,
391
    origin: number[]
392
  ) {
393
    const positionsAttribute = {value: positions, size: 3};
2✔
394
    const colorsAttribute = colors ? {value: colors, size: 3, normalized: true} : undefined;
2!
395
    const data = this.createTileContentTable(pointCount, positionsAttribute, colorsAttribute);
2✔
396

397
    return {
2✔
398
      data,
399
      pointCount,
400
      cartographicOrigin: origin,
401
      coordinateSystem: this._projection
2!
402
        ? COORDINATE_SYSTEM.LNGLAT_OFFSETS
403
        : COORDINATE_SYSTEM.CARTESIAN
404
    };
405
  }
406

407
  protected createTileContentTable(
408
    pointCount: number,
409
    positions: {value: Float32Array; size: number},
410
    colors?: {value: Uint16Array; size: number; normalized: boolean}
411
  ): MeshArrowTable {
412
    const attributes: Mesh['attributes'] = {
2✔
413
      POSITION: positions
414
    };
415
    if (colors) {
2!
416
      attributes.COLOR_0 = colors;
2✔
417
    }
418

419
    return convertMeshToTable(
2✔
420
      {
421
        topology: 'point-list',
422
        mode: 0,
423
        header: {vertexCount: pointCount},
424
        schema: {
425
          fields: [],
426
          metadata: {}
427
        },
428
        attributes
429
      },
430
      'arrow-table'
431
    );
432
  }
433

434
  protected async ensureHierarchyLoaded(tileId: string): Promise<void> {
435
    await this.initialize();
49✔
436

437
    if (!this._hierarchy) {
49!
NEW
438
      return;
×
439
    }
440

441
    const page = this._hierarchy.pages[tileId];
49✔
442
    if (!page) {
49!
443
      return;
49✔
444
    }
445

NEW
446
    if (!this._pageLoadPromises.has(tileId)) {
×
NEW
447
      const loadPromise = this.loadHierarchyPage(tileId, page);
×
NEW
448
      this._pageLoadPromises.set(tileId, loadPromise);
×
449
    }
450

NEW
451
    await this._pageLoadPromises.get(tileId);
×
452
  }
453

454
  protected async loadHierarchyPage(tileId: string, page: Hierarchy.Page): Promise<void> {
NEW
455
    if (!this._hierarchy) {
×
NEW
456
      return;
×
457
    }
458

NEW
459
    const subtree = await Copc.loadHierarchyPage(this._urlOrGetter, page);
×
NEW
460
    this._hierarchy.nodes = {
×
461
      ...this._hierarchy.nodes,
462
      ...subtree.nodes
463
    };
NEW
464
    this._hierarchy.pages = {
×
465
      ...this._hierarchy.pages,
466
      ...subtree.pages
467
    };
NEW
468
    delete this._hierarchy.pages[tileId];
×
469
  }
470

471
  protected getTileHeader(
472
    tileId: string,
473
    node: Hierarchy.Node
474
  ): {
475
    id: string;
476
    level: number;
477
    pointCount: number;
478
    geometricError: number;
479
    boundingVolume: {
480
      cartographicBounds: [number[], number[]];
481
      center: number[];
482
      radius: number;
483
    };
484
  } {
485
    const [depth] = Key.parse(tileId);
12✔
486
    return {
12✔
487
      id: tileId,
488
      level: depth,
489
      pointCount: node.pointCount,
490
      geometricError: this.getGeometricError(depth),
491
      boundingVolume: this.getBoundingVolume(tileId)
492
    };
493
  }
494

495
  protected getBoundingVolume(tileId: string): {
496
    cartographicBounds: [number[], number[]];
497
    center: number[];
498
    radius: number;
499
  } {
500
    const [minBounds, maxBounds] = this.getCartographicBounds(tileId);
12✔
501
    const center = [
12✔
502
      (minBounds[0] + maxBounds[0]) / 2,
503
      (minBounds[1] + maxBounds[1]) / 2,
504
      (minBounds[2] + maxBounds[2]) / 2
505
    ];
506
    const radius = Math.sqrt(
12✔
507
      Math.pow(maxBounds[0] - center[0], 2) +
508
        Math.pow(maxBounds[1] - center[1], 2) +
509
        Math.pow(maxBounds[2] - center[2], 2)
510
    );
511

512
    return {
12✔
513
      cartographicBounds: [minBounds, maxBounds] as [number[], number[]],
514
      center,
515
      radius
516
    };
517
  }
518

519
  protected getTileCenter(tileId: string): number[] {
NEW
520
    return this.getBoundingVolume(tileId).center;
×
521
  }
522

523
  protected getInferredViewState(): COPCViewState {
524
    const boundingVolume = this.getDataBoundingVolume();
12✔
525
    return {
12✔
526
      boundingVolume,
527
      cartographicCenter: boundingVolume.center,
528
      zoom: this.estimateZoom(boundingVolume)
529
    };
530
  }
531

532
  protected getDataBoundingVolume(): {
533
    cartographicBounds: [number[], number[]];
534
    center: number[];
535
    radius: number;
536
  } {
537
    const {copc} = this.unwrapState();
16✔
538
    const [minBounds, maxBounds] = this.projectBounds([
16✔
539
      [copc.header.min[0], copc.header.min[1], copc.header.min[2] ?? 0],
16!
540
      [copc.header.max[0], copc.header.max[1], copc.header.max[2] ?? 0]
16!
541
    ]);
542

543
    const center = [
16✔
544
      (minBounds[0] + maxBounds[0]) / 2,
545
      (minBounds[1] + maxBounds[1]) / 2,
546
      (minBounds[2] + maxBounds[2]) / 2
547
    ];
548
    const radius = Math.sqrt(
16✔
549
      Math.pow(maxBounds[0] - center[0], 2) +
550
        Math.pow(maxBounds[1] - center[1], 2) +
551
        Math.pow(maxBounds[2] - center[2], 2)
552
    );
553

554
    return {
16✔
555
      cartographicBounds: [minBounds, maxBounds],
556
      center,
557
      radius
558
    };
559
  }
560

561
  protected getNativeTileCenter(tileId: string): number[] {
562
    const [minBounds, maxBounds] = this.getNativeTileBounds(tileId);
2✔
563
    return [
2✔
564
      (minBounds[0] + maxBounds[0]) / 2,
565
      (minBounds[1] + maxBounds[1]) / 2,
566
      (minBounds[2] + maxBounds[2]) / 2
567
    ];
568
  }
569

570
  protected getCartographicBounds(tileId: string): [number[], number[]] {
571
    return this.projectBounds(this.getNativeTileBounds(tileId));
12✔
572
  }
573

574
  protected getGeometricError(depth: number): number {
575
    const {copc} = this.unwrapState();
16✔
576
    return copc.info.spacing / Math.pow(2, depth);
16✔
577
  }
578

579
  protected estimateZoom(boundingVolume: {cartographicBounds: [number[], number[]]}): number {
580
    const [minBounds, maxBounds] = boundingVolume.cartographicBounds;
12✔
581
    const longitudeSpan = Math.max(Math.abs(maxBounds[0] - minBounds[0]), 0.000001);
12✔
582
    return Math.max(1, Math.round(Math.log2(360 / longitudeSpan)));
12✔
583
  }
584

585
  protected projectPoint(point: number[]): number[] {
586
    if (!this._projection) {
78,619!
NEW
587
      return [...point];
×
588
    }
589

590
    const projectedPoint = this._projection.project([...point]);
78,619✔
591
    return [projectedPoint[0], projectedPoint[1], projectedPoint[2] ?? point[2] ?? 0];
78,619!
592
  }
593

594
  protected projectBounds(bounds: [number[], number[]]): [number[], number[]] {
595
    const [minBounds, maxBounds] = bounds;
28✔
596
    const corners = [
28✔
597
      [minBounds[0], minBounds[1], minBounds[2] || 0],
28!
598
      [minBounds[0], minBounds[1], maxBounds[2] || 0],
28!
599
      [minBounds[0], maxBounds[1], minBounds[2] || 0],
28!
600
      [minBounds[0], maxBounds[1], maxBounds[2] || 0],
28!
601
      [maxBounds[0], minBounds[1], minBounds[2] || 0],
28!
602
      [maxBounds[0], minBounds[1], maxBounds[2] || 0],
28!
603
      [maxBounds[0], maxBounds[1], minBounds[2] || 0],
28!
604
      [maxBounds[0], maxBounds[1], maxBounds[2] || 0]
28!
605
    ].map(corner => this.projectPoint(corner));
224✔
606

607
    return [
28✔
608
      [
609
        Math.min(...corners.map(corner => corner[0])),
224✔
610
        Math.min(...corners.map(corner => corner[1])),
224✔
611
        Math.min(...corners.map(corner => corner[2] ?? 0))
224!
612
      ],
613
      [
614
        Math.max(...corners.map(corner => corner[0])),
224✔
615
        Math.max(...corners.map(corner => corner[1])),
224✔
616
        Math.max(...corners.map(corner => corner[2] ?? 0))
224!
617
      ]
618
    ];
619
  }
620

621
  protected getNativeTileBounds(tileId: string): [number[], number[]] {
622
    const {copc} = this.unwrapState();
14✔
623
    const nativeBounds = Bounds.stepTo(copc.info.cube, Key.parse(tileId));
14✔
624
    const dataMin = copc.header.min;
14✔
625
    const dataMax = copc.header.max;
14✔
626

627
    return [
14✔
628
      [
629
        Math.max(nativeBounds[0], dataMin[0]),
630
        Math.max(nativeBounds[1], dataMin[1]),
631
        Math.max(nativeBounds[2], dataMin[2] ?? nativeBounds[2])
14!
632
      ],
633
      [
634
        Math.min(nativeBounds[3], dataMax[0]),
635
        Math.min(nativeBounds[4], dataMax[1]),
636
        Math.min(nativeBounds[5], dataMax[2] ?? nativeBounds[5])
14!
637
      ]
638
    ];
639
  }
640

641
  protected getChildKeys(tileId: string): string[] {
642
    const key = Key.parse(tileId);
5✔
643
    const result: string[] = [];
5✔
644

645
    for (let childX = 0; childX < 2; childX++) {
5✔
646
      for (let childY = 0; childY < 2; childY++) {
10✔
647
        for (let childZ = 0; childZ < 2; childZ++) {
20✔
648
          result.push(
40✔
649
            Key.toString([
650
              key[0] + 1,
651
              key[1] * 2 + childX,
652
              key[2] * 2 + childY,
653
              key[3] * 2 + childZ
654
            ])
655
          );
656
        }
657
      }
658
    }
659

660
    return result;
5✔
661
  }
662

663
  protected getAncestorKeys(tileId: string): string[] {
664
    const key = Key.parse(tileId);
28✔
665
    const result: string[] = [];
28✔
666

667
    for (let depth = key[0] - 1; depth >= 0; depth--) {
28✔
668
      result.push(
44✔
669
        Key.toString([
670
          depth,
671
          key[1] >> (key[0] - depth),
672
          key[2] >> (key[0] - depth),
673
          key[3] >> (key[0] - depth)
674
        ])
675
      );
676
    }
677

678
    return result;
28✔
679
  }
680

681
  protected unwrapState(): {
682
    copc: Copc;
683
    hierarchy: Hierarchy.Subtree;
684
  } {
685
    if (!this._copc || !this._hierarchy) {
46!
NEW
686
      throw new Error('COPC source is not initialized');
×
687
    }
688

689
    return {
46✔
690
      copc: this._copc,
691
      hierarchy: this._hierarchy
692
    };
693
  }
694

695
  /*
696
  async getTile(tileParams: GetTileParameters): Promise<ArrayBuffer | null> {
697
    const {x, y, z} = tileParams;
698
    const rangeResponse = await this.pmtiles.getZxy(z, x, y);
699
    const arrayBuffer = rangeResponse?.data;
700
    if (!arrayBuffer) {
701
      // console.error('No arrayBuffer', tileParams);
702
      return null;
703
    }
704
    return arrayBuffer;
705
  }
706

707
  // Tile Source interface implementation: deck.gl compatible API
708
  // TODO - currently only handles image tiles, not vector tiles
709

710
  async getTileData(tileParams: GetTileDataParameters): Promise<any> {
711
    const {x, y, z} = tileParams.index;
712
    const metadata = await this.metadata;
713
    switch (metadata.tileMIMEType) {
714
      case 'application/vnd.mapbox-vector-tile':
715
        return await this.getVectorTile({x, y, z, layers: []});
716
      default:
717
        return await this.getImageTile({x, y, z, layers: []});
718
    }
719
  }
720
  */
721
}
722

723
function getDataTypeFromDimension(dimension: Dimension): DataType {
724
  const {type, size} = dimension;
×
725
  switch (type) {
×
726
    case 'unsigned':
727
      return size === 1 ? 'uint8' : size === 2 ? 'uint16' : size === 4 ? 'uint32' : 'uint64';
×
728
    case 'signed':
729
      return size === 1 ? 'int8' : size === 2 ? 'int16' : size === 4 ? 'int32' : 'int64';
×
730
    case 'float':
731
      return size === 4 ? 'float32' : 'float64';
×
732
    default:
733
      return 'null';
×
734
  }
735
}
736

737
function createProjection(projectionData?: string): Proj4Projection | null {
738
  if (!projectionData) {
8!
NEW
739
    return null;
×
740
  }
741

742
  try {
8✔
743
    return new Proj4Projection({
8✔
744
      from: normalizeProjectionDefinition(projectionData),
745
      to: 'WGS84'
746
    });
747
  } catch {
NEW
748
    return null;
×
749
  }
750
}
751

752
function normalizeProjectionDefinition(projectionData: string): string {
753
  const horizontalWktMatch =
754
    projectionData.match(/(PROJCS\[[\s\S]*\])(?:,VERT_CS\[[\s\S]*\])\]$/) ||
8✔
755
    projectionData.match(/(GEOGCS\[[\s\S]*\])(?:,VERT_CS\[[\s\S]*\])\]$/);
756

757
  return horizontalWktMatch?.[1] || projectionData;
8✔
758
}
759

760
/** Create the COPC package byte-range getter for URL/path and Blob inputs. */
761
function createCOPCGetter(data: string | Blob, url: string): string | Getter {
762
  if (typeof data === 'string') {
8✔
763
    return url;
4✔
764
  }
765

766
  return async (begin: number, end: number): Promise<Uint8Array> => {
4✔
767
    if (begin < 0 || end < 0 || begin > end) {
13!
NEW
768
      throw new Error('Invalid range');
×
769
    }
770

771
    const arrayBuffer = await data.slice(begin, end).arrayBuffer();
13✔
772
    return new Uint8Array(arrayBuffer);
13✔
773
  };
774
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc