UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

243 lines (193 loc) 6.88 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { Box3, Vector3 } from 'three'; import type { GetMemoryUsageContext } from '../core/MemoryUsage'; import { nonEmpty, nonNull } from '../utils/tsutils'; import { PointCloudSourceBase, type GetNodeDataOptions, type PointCloudAttribute, type PointCloudMetadata, type PointCloudNode, type PointCloudNodeData, type PointCloudSource, } from './PointCloudSource'; export interface AggregatePointCloudSourceOptions { /** * The sources to aggregate. Cannot be empty. */ sources: PointCloudSource[]; } function getAttributeKey(attr: PointCloudAttribute): string { return `${attr.name}-${attr.dimension}-${attr.type}-${attr.interpretation}-${attr.size}`; } function aggregateAttributes(attr: PointCloudAttribute[]): PointCloudAttribute { const { name, dimension, type, interpretation, size } = attr[0]; let min = +Infinity; let max = -Infinity; for (let i = 0; i < attr.length; i++) { const att = attr[i]; if (att.min != null) { min = Math.min(att.min, min); } if (att.max != null) { max = Math.max(att.max, max); } } return { name, dimension, type, size, interpretation, min: isFinite(min) ? min : undefined, max: isFinite(max) ? max : undefined, }; } /** * A {@link PointCloudSource} that combines multiple sources. * * All aspects of the underlying sources are combined in the following way: * - volumes are union'ed * - point counts are summed * - only attributes that are found in *all* sources are exposed. */ export default class AggregatePointCloudSource extends PointCloudSourceBase { public readonly isAggregatePointCloudSource = true as const; public readonly type = 'AggregatePointCloudSource' as const; private readonly _sourceMap: Map<string, PointCloudSource> = new Map(); private readonly _sources: PointCloudSource[]; /** * The sources in this source. */ public get sources(): Readonly<PointCloudSource[]> { return this._sources; } public constructor(params: AggregatePointCloudSourceOptions) { super(); const sources = nonEmpty(params.sources, 'sources is required'); this._sources = sources; sources.forEach(s => { this._sourceMap.set(s.id, s); s.addEventListener('progress', () => this.dispatchEvent({ type: 'progress' })); }); } protected async initializeOnce(): Promise<this> { const promises = this._sources.map(s => { const promise = s.initialize(); return promise; }); const results = await Promise.allSettled(promises); const actualSources: PointCloudSource[] = []; let notifyWarning = false; for (const result of results) { if (result.status === 'fulfilled') { actualSources.push(result.value); } else { notifyWarning = true; } } this._sources.length = 0; this._sources.push(...actualSources); Object.freeze(this._sources); if (notifyWarning) { console.warn('one or more sources could not be initialized.'); } return this; } public get loading(): boolean { return this._sources.some(s => s.loading); } public get progress(): number { let sum = 0; let count = 0; this._sources.forEach(s => { if (s.progress < 1) { sum += s.progress; count++; } }); if (count > 0) { return sum / count; } return 1; } public async getHierarchy(): Promise<PointCloudNode> { const allRootNodes = await Promise.all(this._sources.map(s => s.getHierarchy())); const volume = new Box3().makeEmpty(); for (const node of allRootNodes) { volume.union(node.volume); } const pseudoRoot: PointCloudNode = { hasData: false, volume, sourceId: this.id, center: volume.getCenter(new Vector3()), id: '__pseudoRoot', depth: -1, children: allRootNodes, geometricError: +Infinity, }; return pseudoRoot; } public async getMetadata(): Promise<PointCloudMetadata> { const sourceCount = this._sources.length; const promises = this._sources.map(s => s.getMetadata()); const all = await Promise.all(promises); let pointCount = 0; const volume: Box3 = new Box3().makeEmpty(); const attributeMap: Map<string, { count: number; attributes: PointCloudAttribute[] }> = new Map(); for (const metadata of all) { pointCount += metadata.pointCount ?? 0; // Create the union of all volumes if (metadata.volume) { volume.union(metadata.volume); } // Create the *intersection* of attributes. for (const attribute of metadata.attributes) { const key = getAttributeKey(attribute); const existing = attributeMap.get(key); if (!existing) { attributeMap.set(key, { count: 1, attributes: [attribute] }); } else { existing.count += 1; existing.attributes.push(attribute); } } } // FIXME when Set.prototype.intersection() becomes widely available, // use this instead. const attributes: PointCloudAttribute[] = []; attributeMap.forEach(attr => { if (attr.count === sourceCount) { attributes.push(aggregateAttributes(attr.attributes)); } }); return { pointCount, volume: volume.isEmpty() ? undefined : volume, attributes, }; } public getNodeData(params: GetNodeDataOptions): Promise<PointCloudNodeData> { const { node } = params; const targetSource = nonNull(this._sourceMap.get(node.sourceId)); return targetSource.getNodeData(params); } /** * Disposes this source and all underlying sources. */ public dispose(): void { this._sources.forEach(s => s.dispose()); } public getMemoryUsage(context: GetMemoryUsageContext): void { this._sources.forEach(s => s.getMemoryUsage(context)); } } export function isAggregatePointCloudSource(obj: unknown): obj is AggregatePointCloudSource { return (obj as AggregatePointCloudSource).isAggregatePointCloudSource === true; }