@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
276 lines (227 loc) • 9.38 kB
text/typescript
/*
* 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;