itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
192 lines (184 loc) • 6.58 kB
JavaScript
import * as THREE from 'three';
import GlobeLayer from "../Core/Prefab/Globe/GlobeLayer.js";
function isIntersectedOrOverlaped(a, b) {
return !(a.left > b.right || a.right < b.left || a.top > b.bottom || a.bottom < b.top);
}
const frustum = new THREE.Frustum();
// A grid to manage labels on the screen.
export class ScreenGrid {
constructor() {
let x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 12;
let y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10;
let width = arguments.length > 2 ? arguments[2] : undefined;
let height = arguments.length > 3 ? arguments[3] : undefined;
this.x = x;
this.y = y;
this.grid = [];
this.visible = [];
this.resize();
this.reset();
this.width = width;
this.height = height;
}
// Reset each cell and hidden and visible.
reset() {
for (let i = 0; i < this.x; i++) {
for (let j = 0; j < this.y; j++) {
// Splice is prefered to creating a new array, in term of memory
this.grid[i][j].splice(0, this.grid[i][j].length);
}
}
this.visible = [];
}
// Add rows if needed — but don't delete anything else. Columns are taken
// care in reset().
resize() {
for (let i = 0; i < this.x; i++) {
if (!this.grid[i]) {
this.grid[i] = [];
}
for (let j = 0; j < this.y; j++) {
if (!this.grid[i][j]) {
this.grid[i][j] = [];
}
}
}
}
// Insert a label using its boundaries. It is either added to hidden or
// visible, given the result. The grid is populated with true for every
// filled cell.
insert(obj) {
const minx = Math.max(0, Math.floor(obj.boundaries.left / this.width * this.x));
const maxx = Math.min(this.x - 1, Math.floor(obj.boundaries.right / this.width * this.x));
const miny = Math.max(0, Math.floor(obj.boundaries.top / this.height * this.y));
const maxy = Math.min(this.y - 1, Math.floor(obj.boundaries.bottom / this.height * this.y));
for (let i = minx; i <= maxx; i++) {
for (let j = miny; j <= maxy; j++) {
if (this.grid[i][j].length > 0) {
if (this.grid[i][j].some(l => isIntersectedOrOverlaped(l.boundaries, obj.boundaries))) {
obj.visible = false;
return false;
}
}
}
}
for (let i = minx; i <= maxx; i++) {
for (let j = miny; j <= maxy; j++) {
this.grid[i][j].push(obj);
}
}
return true;
}
}
const worldPosition = new THREE.Vector3();
/**
* This renderer is inspired by the
* [`THREE.CSS2DRenderer`](https://threejs.org/docs/#examples/en/renderers/CSS2DRenderer).
* It is instanciated in `c3DEngine`, as another renderer to handles Labels.
*/
class Label2DRenderer {
constructor() {
this.domElement = document.createElement('div');
this.domElement.style.overflow = 'hidden';
this.domElement.style.position = 'absolute';
this.domElement.style.top = '0';
this.domElement.style.height = '100%';
this.domElement.style.width = '100%';
this.domElement.style.zIndex = 1;
// Used to destroy labels that are not added to the DOM
this.garbage = document.createElement('div');
this.garbage.style.display = 'none';
this.domElement.appendChild(this.garbage);
this.halfWidth = 0;
this.halfHeight = 0;
this.grid = new ScreenGrid();
this.infoTileLayer = undefined;
}
setSize(width, height) {
this.domElement.style.width = `${width}`;
this.domElement.style.height = `${height}`;
this.halfWidth = width / 2;
this.halfHeight = height / 2;
this.grid.width = width;
this.grid.height = height;
this.grid.x = Math.ceil(width / 20);
this.grid.y = Math.ceil(height / 20);
this.grid.resize();
}
registerLayer(layer) {
this.domElement.appendChild(layer.domElement.dom);
}
render(scene, camera) {
const labelLayers = this.infoTileLayer && this.infoTileLayer.layer.attachedLayers.filter(l => l.isLabelLayer && l.visible);
if (labelLayers.length == 0) {
return;
}
this.grid.reset();
// set camera frustum
frustum.setFromProjectionMatrix(camera.projectionMatrix);
labelLayers.forEach(labelLayer => {
labelLayer.submittedLabelNodes.forEach(labelsNode => {
labelsNode.labels.forEach(label => {
labelsNode.updatePosition(label);
this.culling(label, camera);
});
labelsNode.domElements.labels.show();
labelsNode.needsUpdate = false;
});
});
// sort by order, then by visibility inside those subsets
// https://docs.mapbox.com/help/troubleshooting/optimize-map-label-placement/#label-hierarchy
this.grid.visible.sort((a, b) => {
const r = b.order - a.order;
if (r == 0) {
if (!a.visible && b.visible) {
return 1;
} else {
return -1;
}
} else {
return r;
}
});
this.grid.visible.forEach(l => {
if (this.grid.insert(l)) {
l.visible = true;
l.updateCSSPosition();
} else {
l.visible = false;
}
});
labelLayers.forEach(labelLayer => {
labelLayer.toHide.children.forEach(labelsNode => labelsNode.domElements?.labels.hide());
labelLayer.toHide.clear();
});
}
culling(label, camera) {
label.getWorldPosition(worldPosition);
// Check if the frustum contains tle label
if (!frustum.containsPoint(worldPosition.applyMatrix4(camera.matrixWorldInverse)) ||
// Check if globe horizon culls the label
// Do some horizon culling (if possible) if the tiles level is small enough.
label.horizonCullingPoint && GlobeLayer.horizonCulling(label.horizonCullingPoint)
// Why do we might need this part ?
// || // Check if content isn't present in visible labels
// this.grid.visible.some((l) => {
// // TODO for icon without text filter by position
// const textContent = label.content.textContent;
// return textContent !== '' && l.content.textContent.toLowerCase() == textContent.toLowerCase();
// })
) {
label.visible = false;
} else {
// projecting world position label
worldPosition.applyMatrix4(camera.projectionMatrix);
label.updateProjectedPosition(worldPosition.x * this.halfWidth + this.halfWidth, -worldPosition.y * this.halfHeight + this.halfHeight);
this.grid.visible.push(label);
}
}
removeLabelDOM(label) {
this.garbage.appendChild(label.content);
this.garbage.innerHTML = '';
}
}
export default Label2DRenderer;