@giro3d/giro3d
Version:
A JS/WebGL framework for 3D geospatial data visualization
188 lines (155 loc) • 6.14 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, Group, Vector2, Vector3 } from 'three';
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js';
import type Instance from '../core/Instance';
import type Map from '../entities/Map';
import type TileMesh from '../entities/tiles/TileMesh';
import Ellipsoid from '../core/geographic/Ellipsoid';
import Panel from './Panel';
const tmpVec2 = new Vector2();
function createTileLabel(): HTMLDivElement {
const text = document.createElement('div');
text.style.color = '#FFFFFF';
text.style.padding = '0.2em 1em';
text.style.textShadow = '2px 2px 2px black';
text.style.textAlign = 'center';
text.style.fontSize = '12px';
text.style.backgroundColor = 'rgba(0,0,0,0.5)';
return text;
}
const tmpLines: string[] = [];
class TileInfoPanel extends Panel {
private readonly _labels: globalThis.Map<number, CSS2DObject> = new globalThis.Map();
private readonly _map: Map;
private readonly _root = new Group();
public readonly params: {
enabled: boolean;
nodeInfo: boolean;
imageSize: boolean;
minMax: boolean;
layerInfo: boolean;
color: Color;
} = {
enabled: false,
nodeInfo: true,
imageSize: true,
layerInfo: false,
minMax: false,
color: new Color('yellow'),
};
/**
* @param map - The map.
* @param parentGui - Parent GUI
* @param instance - The instance
*/
public constructor(map: Map, parentGui: GUI, instance: Instance) {
super(parentGui, instance, 'Tile info');
this._map = map;
this.addController(this.params, 'enabled')
.name('Show tile info')
.onChange(() => this.updateValues());
this.addController(this.params, 'imageSize').onChange(() => this.updateValues());
this.addController(this.params, 'minMax')
.name('Elevation range')
.onChange(() => this.updateValues());
this.addController(this.params, 'layerInfo').onChange(() => this.updateValues());
}
public getOrCreateLabel(obj: TileMesh): CSS2DObject {
if (!this._labels.has(obj.id)) {
const label = new CSS2DObject(createTileLabel());
label.name = 'MapInspector label';
obj.addEventListener('dispose', () => {
this.removeLabel(obj.id, label);
});
this._root.add(label);
this._root.updateMatrixWorld(true);
if (this._root.parent == null) {
this._root.name = 'TileInfoPanel';
this.instance.threeObjects.add(this._root);
}
this._labels.set(obj.id, label);
}
return this._labels.get(obj.id) as CSS2DObject;
}
public getInfo(tile: TileMesh): string {
tmpLines.length = 0;
if (this.params.nodeInfo) {
tmpLines.push(
`Node #${tile.id} LOD=${tile.lod} (${tile.coordinate.x}, ${tile.coordinate.y})`,
);
}
if (this.params.minMax) {
tmpLines.push(`min: ${tile.minmax.min.toFixed(1)}, max: ${tile.minmax.max.toFixed(1)}`);
}
if (this.params.imageSize) {
const size = tile.getScreenPixelSize(this.instance.view, tmpVec2);
tmpLines.push(
`${tile.textureSize.width} * ${tile.textureSize.height} / ${size != null ? Math.max(size.width, size.height) : 'NULL'}`,
);
}
if (this.params.layerInfo) {
this._map.forEachLayer(layer => {
const info = layer.getInfo(tile);
tmpLines.push(
`${layer.name ?? layer.id}: ${info.imageCount} img, ${info.state}, ${info.paintCount} paints)`,
);
});
}
return tmpLines.join('\n');
}
private getLabelVisibility(tile: TileMesh, position: Vector3): boolean {
let result = tile.visible && tile.material.visible;
if (this.instance.coordinateSystem.isEpsg(4978)) {
const camPos = this.instance.view.camera.position;
result = result && Ellipsoid.WGS84.isHorizonVisible(camPos, position);
}
return result;
}
private getLabelPosition(tile: TileMesh): Vector3 {
const center = tile.extent.center();
const elev = this._map.getElevation({ coordinates: center });
const sample = elev.samples?.sort((a, b) => a.resolution - b.resolution)[0];
const z = sample?.elevation ?? 0;
if (this.instance.coordinateSystem.isEpsg(4978)) {
return Ellipsoid.WGS84.toCartesian(center.latitude, center.longitude, z);
} else {
return new Vector3(center.x, center.y, z);
}
}
private removeLabel(id: number, label: CSS2DObject): void {
label.element.remove();
label.removeFromParent();
this._labels.delete(id);
}
public updateLabel(tile: TileMesh): void {
const color = this.params.color;
const visible = tile.visible && tile.material.visible && this.params.enabled;
if (!visible) {
const label = this._labels.get(tile.id);
if (label) {
this.removeLabel(tile.id, label);
}
} else {
const label = this.getOrCreateLabel(tile);
const element = label.element;
element.innerText = this.getInfo(tile);
element.style.color = `#${color.getHexString()}`;
label.position.copy(this.getLabelPosition(tile));
label.updateMatrixWorld(true);
const isVisible = this.getLabelVisibility(tile, label.position);
element.style.opacity = isVisible ? '100%' : '0%';
label.visible = isVisible;
}
}
public override updateValues(): void {
this._map.traverseTiles(tile => {
this.updateLabel(tile);
});
}
}
export default TileInfoPanel;