UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

172 lines (164 loc) 5.7 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import { MathUtils } from 'three'; import Extent from '../core/geographic/Extent'; import { nonEmpty, nonNull } from '../utils/tsutils'; import ImageSource from './ImageSource'; /** * 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 { isAggregateImageSource = true; type = 'AggregateImageSource'; _sourceProperties = new Map(); _cachedExtent = null; constructor(options) { 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. */ get sources() { return this._sources; } async initialize(options) { const promises = this._sources.map(source => source.initialize(options)); await Promise.allSettled(promises); } getCrs() { 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. */ setSourceVisibility(source, visible) { 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`. */ getExtent() { 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; } getMemoryUsage(context) { this._sources.forEach(source => source.getMemoryUsage(context)); } patchRequest(source, request) { const { zIndex, id } = nonNull(this._sourceProperties.get(source)); // @ts-expect-error slightly different typing but it should be ok return () => { 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; } }; } /** * Returns true if the extent intersects with any sub-source's extent. */ contains(extent) { const convertedExtent = extent.clone().as(this.getCrs()); return this._sources.some(source => source.contains(convertedExtent)); } /** * Disposes all sub-sources. */ dispose() { this._sources.forEach(source => source.dispose()); } /** * Patches the response provided by the sub-source with specific per-source properties. */ patchResponse(source, response) { const { id } = nonNull(this._sourceProperties.get(source)); const result = { // 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; } getImages(options) { const result = []; 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; } }