UNPKG

@giro3d/giro3d

Version:

A JS/WebGL framework for 3D geospatial data visualization

276 lines (227 loc) 9.38 kB
/* * Copyright (c) 2015-2018, IGN France. * Copyright (c) 2018-2026, Giro3D team. * SPDX-License-Identifier: MIT */ import type GUI from 'lil-gui'; import { Color } from 'three'; import type ColorMap from '../core/ColorMap'; import type { ColorMapMode } from '../core/ColorMap'; import type CoordinateSystem from '../core/geographic/CoordinateSystem'; import type Instance from '../core/Instance'; import type Layer from '../core/layer/Layer'; import type Entity3D from '../entities/Entity3D'; import type { BoundingBoxHelper } from '../helpers/Helpers'; import { isColorLayer } from '../core/layer/ColorLayer'; import { isElevationLayer } from '../core/layer/ElevationLayer'; import * as MemoryUsage from '../core/MemoryUsage'; import { isMap } from '../entities/Map'; import Helpers from '../helpers/Helpers'; import { isMaterial } from '../utils/predicates'; import ColorimetryPanel from './ColorimetryPanel'; import ColorMapInspector from './ColorMapInspector'; import Panel from './Panel'; import SourceInspector from './SourceInspector'; function getTitle(layer: Layer): string { return [layer.visible ? '👁️' : '❌', layer.type, `(${layer.name ?? layer.id})`].join(' '); } const blendingModes = ['None', 'Normal', 'Add', 'Multiply']; /** * Inspector for a {@link Layer}. */ class LayerInspector extends Panel { /** The inspected layer. */ public layer: Layer; public entity: Entity3D; public state: string; public sourceCrs: CoordinateSystem; public interpretation: string; public minmax: { min: number; max: number } | undefined; public extentColor: Color; public showExtent: boolean; public extentHelper: BoundingBoxHelper | null; public visible = true; /** The color map inspector */ public colorMapInspector: ColorMapInspector; /** The source inspector. */ public sourceInspector: SourceInspector | undefined; public colorimetryPanel: ColorimetryPanel | undefined; public composerImages = 0; public cpuMemoryUsage = 'unknown'; public gpuMemoryUsage = 'unknown'; public blendingMode = 'Normal'; /** * @param gui - The GUI. * @param instance - The Giro3D instance. * @param entity - The map. * @param layer - The layer to inspect */ public constructor(gui: GUI, instance: Instance, entity: Entity3D, layer: Layer) { super(gui, instance, getTitle(layer)); this.layer = layer; this.entity = entity; this.state = 'idle'; this.sourceCrs = layer.source.getCrs() ?? instance.coordinateSystem; this.updateValues(); this.addController(this.layer, 'id').name('Identifier'); this.addController(this, 'cpuMemoryUsage').name('Memory usage (CPU)'); this.addController(this, 'gpuMemoryUsage').name('Memory usage (GPU)'); if (layer.name != null) { this.addController(this.layer, 'name').name('Name'); } this.addController(this.sourceCrs, 'id').name('Source CRS'); this.addController(this, 'state').name('Status'); this.addController(this.layer, 'resolutionFactor').name('Resolution factor'); this.addController(this.layer, 'visible') .name('Visible') .onChange(() => { this.gui.title(getTitle(layer)); this.notify(entity); }); this.addController(this.layer, 'frozen') .name('Frozen') .onChange(() => { this.notify(entity); }); this.interpretation = layer.interpretation.toString(); this.addController(this, 'interpretation').name('Interpretation'); this.addController(this, 'repaint') .name('Repaint layer') .onChange(() => { this.notify(entity); }); this.addController(this, 'composerImages').name('Loaded images'); if (isElevationLayer(this.layer)) { this.minmax = { min: this.layer.minmax.min, max: this.layer.minmax.max }; this.addController(this.minmax, 'min').name('Minimum elevation'); this.addController(this.minmax, 'max').name('Maximum elevation'); } if (isColorLayer(this.layer)) { const colorLayer = this.layer; if (colorLayer.elevationRange) { this.addController(colorLayer.elevationRange, 'min') .name('Elevation range minimum') .onChange(() => this.notify(entity)); this.addController(colorLayer.elevationRange, 'max') .name('Elevation range maximum') .onChange(() => this.notify(entity)); } this.blendingMode = blendingModes[colorLayer.blendingMode]; this.addController(this, 'blendingMode', blendingModes) .name('Blending mode') .onChange(v => { colorLayer.blendingMode = blendingModes.indexOf(v); this.notify(colorLayer); }); this.colorimetryPanel = new ColorimetryPanel( colorLayer.colorimetry, this.gui, instance, ); } if ('opacity' in this.layer && this.layer.opacity !== undefined) { this.addController(this.layer, 'opacity') .name('Opacity') .min(0) .max(1) .onChange(() => this.notify(entity)); } this.extentColor = new Color('#52ff00'); this.showExtent = false; this.extentHelper = null; this.addController(this, 'showExtent') .name('Show extent') .onChange(() => this.toggleExtent()); this.addColorController(this, 'extentColor') .name('Extent color') .onChange(() => this.updateExtentColor()); this.colorMapInspector = new ColorMapInspector( this.gui, instance, () => layer.colorMap, () => this.notify(layer), ); if (this.layer.source != null) { this.sourceInspector = new SourceInspector(this.gui, instance, layer.source); } this.addController(this, 'disposeLayer').name('Dispose layer'); if (isMap(this.entity)) { this.addController(this, 'removeLayer').name('Remove layer from map'); } layer.addEventListener('visible-property-changed', () => this.gui.title(getTitle(layer))); } public repaint(): void { this.layer.clear(); } public get colorMap(): Pick<ColorMap, 'min' | 'max' | 'mode'> { if (this.layer.colorMap) { return this.layer.colorMap; } return { min: -1, max: -1, mode: 'N/A' as unknown as ColorMapMode }; } public removeLayer(): void { if (isMap(this.entity)) { this.entity.removeLayer(this.layer); } } public disposeLayer(): void { this.layer.dispose(); this.notify(this.layer); } public updateExtentColor(): void { if (this.extentHelper) { this.instance.threeObjects.remove(this.extentHelper); if (isMaterial(this.extentHelper.material)) { this.extentHelper.material.dispose(); } this.extentHelper.geometry.dispose(); this.extentHelper = null; } this.toggleExtent(); } public toggleExtent(): void { if (!this.extentHelper && this.showExtent && isMap(this.entity)) { const { min, max } = this.entity.getElevationMinMax(); const box = this.layer.getExtent()?.toBox3(min, max); if (box) { this.extentHelper = Helpers.createBoxHelper(box, this.extentColor); this.instance.threeObjects.add(this.extentHelper); this.extentHelper.updateMatrixWorld(true); } } if (this.extentHelper) { this.extentHelper.visible = this.showExtent; } this.notify(this.layer); } public override updateControllers(): void { super.updateControllers(); this.colorMapInspector?.updateControllers(); } public override updateValues(): void { this.state = this.layer.loading ? `loading (${Math.round(this.layer.progress * 100)}%)` : 'idle'; this.visible = this.layer.visible || true; this.composerImages = this.layer.composer?.images?.size ?? 0; if (isElevationLayer(this.layer)) { if (this.layer.minmax != null && this.minmax != null) { this.minmax.min = this.layer.minmax.min; this.minmax.max = this.layer.minmax.max; } } const ctx: MemoryUsage.GetMemoryUsageContext = { renderer: this.instance.renderer, objects: new Map(), }; this.layer.getMemoryUsage(ctx); const memUsage = MemoryUsage.aggregateMemoryUsage(ctx); this.cpuMemoryUsage = MemoryUsage.format(memUsage.cpuMemory); this.gpuMemoryUsage = MemoryUsage.format(memUsage.gpuMemory); this._controllers.forEach(c => c.updateDisplay()); if (this.sourceInspector) { this.sourceInspector.updateValues(); } } } export default LayerInspector;