UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

222 lines (180 loc) 7.67 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { MathUtils } from 'three'; import type CoordinateSystem from '../core/geographic/CoordinateSystem'; import type { GetMemoryUsageContext } from '../core/MemoryUsage'; import type { GetImageOptions, ImageResponse, ImageResult } from './ImageSource'; import Extent from '../core/geographic/Extent'; import { nonEmpty, nonNull } from '../utils/tsutils'; import ImageSource from './ImageSource'; interface SourceProperties { zIndex: number; id: string; visible: boolean; } /** * An image source that aggregates several sub-sources. * The extent of this source is the union of the extent of all sub-sources. * * Overlapping sources are stacked vertically with the sources toward the end of the array * being drawn on top of sources at the beginning of the array. * * Constraints: * - all sub-sources must have the same CRS. * - all sub-sources must have the same color space * - all sub-sources must have the same flip-Y parameter * - all sub-sources must produce textures that have the same datatype (e.g either 8-bit or 32-bit textures, but not both) */ export default class AggregateImageSource extends ImageSource { public readonly isAggregateImageSource = true as const; public override readonly type = 'AggregateImageSource' as const; private readonly _sources: Readonly<ImageSource[]>; private readonly _sourceProperties: Map<ImageSource, SourceProperties> = new Map(); private _cachedExtent: Extent | null = null; public constructor(options: { /** * The sub-sources. The order in which they appear in the array will set their z-index * (i.e sources at the end of the array will be displayed on top). */ sources: ImageSource[]; }) { super({ // Since we are stacking images, they must support transparency transparent: true, flipY: options.sources[0].flipY, colorSpace: options.sources[0].colorSpace, synchronous: options.sources.every(s => s.synchronous), }); this._sources = Object.freeze(nonEmpty(options.sources, 'at least one source is expected')); let zIndex = 0; for (const source of this._sources) { // Ensure that we bubble the events from the sub-sources source.addEventListener('updated', e => this.update(e.extent)); this._sourceProperties.set(source, { zIndex, // We must assign a unique ID to each sub-source to avoid duplicate images, id: MathUtils.generateUUID(), visible: true, }); zIndex++; } } /** * The sources in this source. */ public get sources(): Readonly<ImageSource[]> { return this._sources; } public override async initialize(options: { targetProjection: CoordinateSystem; }): Promise<void> { const promises = this._sources.map(source => source.initialize(options)); await Promise.allSettled(promises); } public override getCrs(): CoordinateSystem { return this._sources[0].getCrs(); } /** * Sets the visibility of a sub-source. This will trigger a repaint of the source. * @param source - The source to update. * @param visible - The new visibility. * @throws if the sub-source is not present in this source. */ public setSourceVisibility(source: ImageSource, visible: boolean): void { const props = nonNull(this._sourceProperties.get(source), 'this source is not present'); if (props.visible !== visible) { props.visible = visible; this.update(this.getExtent() ?? undefined); } } /** * Returns the union of the extent of all the sub-sources, if possible. * If at least one source does not have a known extent, then the entire aggregate extent is `undefined`. */ public override getExtent(): Extent | null { if (this._cachedExtent == null) { const allSources = [...this._sourceProperties.keys()]; const extents = allSources.map(source => source.getExtent()).filter(e => e != null); // To avoid advertising an incorrect extent, all sub-sources must have a valid extent, // otherwise we consider that we cannot compute the aggregate extent. // If we considered only the sub-sources with a valid extent, then the aggregate // extent would be partial and thus incorrect. if (extents.length === allSources.length) { const extent = Extent.unionMany(...extents); this._cachedExtent = extent; } } return this._cachedExtent ?? null; } public override getMemoryUsage(context: GetMemoryUsageContext): void { this._sources.forEach(source => source.getMemoryUsage(context)); } private patchRequest( source: ImageSource, request: ImageResponse['request'], ): ImageResponse['request'] { const { zIndex, id } = nonNull(this._sourceProperties.get(source)); const patched = (): Promise<ImageResult> | ImageResult => { const result = request(); if (result instanceof Promise) { return result.then(img => { img.zIndex = zIndex; img.id = `${img.id}-${id}`; return img; }); } else { result.zIndex = zIndex; result.id = `${result.id}-${id}`; return result; } }; // @ts-expect-error slightly different typing but it should be ok return patched; } /** * Returns true if the extent intersects with any sub-source's extent. */ public override contains(extent: Extent): boolean { const convertedExtent = extent.clone().as(this.getCrs()); return this._sources.some(source => source.contains(convertedExtent)); } /** * Disposes all sub-sources. */ public override dispose(): void { this._sources.forEach(source => source.dispose()); } /** * Patches the response provided by the sub-source with specific per-source properties. */ private patchResponse(source: ImageSource, response: ImageResponse): ImageResponse { const { id } = nonNull(this._sourceProperties.get(source)); const result: ImageResponse = { // Since each sub-source will possibly return the same ID, we have to deduplicate // it so that the layer does not think that those are duplicate responses that should be eliminated. id: `${response.id}-${id}`, request: this.patchRequest(source, response.request), }; return result; } public override getImages(options: GetImageOptions): ImageResponse[] { const result: ImageResponse[] = []; for (const source of this._sources) { const { visible } = nonNull(this._sourceProperties.get(source)); if (!visible) { continue; } const extent = source.getExtent(); if (extent == null || extent.intersectsExtent(options.extent) === true) { const images = source .getImages(options) .map(response => this.patchResponse(source, response)); result.push(...images); } } return result; } }