UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

609 lines (503 loc) 19.4 kB
import { Object3D, PerspectiveCamera, Scene, WebGLRenderer, } from "three"; import { ClusterContract } from "../../api/contracts/ClusterContract"; import { MapillaryError } from "../../error/MapillaryError"; import { Transform } from "../../geo/Transform"; import { FilterFunction } from "../../graph/FilterCreator"; import { Image } from "../../graph/Image"; import { SpatialConfiguration } from "../interfaces/SpatialConfiguration"; import { CameraVisualizationMode } from "./enums/CameraVisualizationMode"; import { OriginalPositionMode } from "./enums/OriginalPositionMode"; import { ClusterPoints } from "./scene/ClusterPoints"; import { CellLine } from "./scene/CellLine"; import { SpatialIntersection } from "./scene/SpatialIntersection"; import { SpatialCell } from "./scene/SpatialCell"; import { SpatialAssets } from "./scene/SpatialAssets"; import { isModeVisible } from "./Modes"; import { PointVisualizationMode } from "./enums/PointVisualizationMode"; import { LngLatAlt } from "../../api/interfaces/LngLatAlt"; import { resetEnu } from "./SpatialCommon"; const NO_CLUSTER_ID = "NO_CLUSTER_ID"; const NO_MERGE_ID = "NO_MERGE_ID"; const NO_SEQUENCE_ID = "NO_SEQUENCE_ID"; type Clusters = { [id: string]: { cellIds: string[]; points: Object3D; }; }; export class SpatialScene { private _scene: Scene; private _intersection: SpatialIntersection; private _assets: SpatialAssets; private _needsRender: boolean; private _cellClusters: { [cellId: string]: { keys: string[]; }; }; private _clusters: Clusters; private _images: { [cellId: string]: SpatialCell; }; private _cells: { [cellId: string]: Object3D; }; private _cameraVisualizationMode: CameraVisualizationMode; private _cameraSize: number; private _pointSize: number; private _pointVisualizationMode: PointVisualizationMode; private _positionMode: OriginalPositionMode; private _cellsVisible: boolean; private readonly _rayNearScale: number; private readonly _originalPointSize: number; private readonly _originalCameraSize: number; private _hoveredId: string; private _selectedId: string; private _filter: FilterFunction; private _imageCellMap: Map<string, string>; private _colors: { hover: string, select: string; }; constructor( configuration: SpatialConfiguration, scene?: Scene) { this._rayNearScale = 1.1; this._originalPointSize = 2; this._originalCameraSize = 2; this._imageCellMap = new Map(); this._scene = !!scene ? scene : new Scene(); this._scene.autoUpdate = false; this._intersection = new SpatialIntersection(); this._assets = new SpatialAssets(); this._needsRender = false; this._images = {}; this._cells = {}; this._cellClusters = {}; this._clusters = {}; this._cameraVisualizationMode = !!configuration.cameraVisualizationMode ? configuration.cameraVisualizationMode : CameraVisualizationMode.Homogeneous; this._cameraSize = configuration.cameraSize; this._pointSize = configuration.pointSize; this._pointVisualizationMode = !!configuration.pointVisualizationMode ? configuration.pointVisualizationMode : PointVisualizationMode.Original; this._positionMode = configuration.originalPositionMode; this._cellsVisible = configuration.cellsVisible; this._hoveredId = null; this._selectedId = null; this._colors = { hover: "#FF0000", select: "#FF8000" }; this._filter = () => true; } public get needsRender(): boolean { return this._needsRender; } public get intersection(): SpatialIntersection { return this._intersection; } public addCluster( reconstruction: ClusterContract, translation: number[], cellId: string): void { if (this.hasCluster(reconstruction.id, cellId)) { return; } const clusterId = reconstruction.id; if (!(clusterId in this._clusters)) { this._clusters[clusterId] = { points: new Object3D(), cellIds: [], }; const visible = this._getClusterVisible(clusterId); const cluster = this._clusters[clusterId]; const color = this._pointVisualizationMode === PointVisualizationMode.Cluster ? this._assets.getColor(clusterId) : null; const points = new ClusterPoints({ cluster: reconstruction, color, originalSize: this._originalPointSize, scale: this._pointSize, translation, }); cluster.points.visible = visible; cluster.points.add(points); this._scene.add(cluster.points); } if (this._clusters[clusterId].cellIds.indexOf(cellId) === -1) { this._clusters[clusterId].cellIds.push(cellId); } if (!(cellId in this._cellClusters)) { this._cellClusters[cellId] = { keys: [] }; } if (this._cellClusters[cellId].keys.indexOf(clusterId) === -1) { this._cellClusters[cellId].keys.push(clusterId); } this._needsRender = true; } public addImage( image: Image, transform: Transform, originalPosition: number[], cellId: string): void { const imageId = image.id; const idMap = { clusterId: image.clusterId ?? NO_CLUSTER_ID, sequenceId: image.sequenceId ?? NO_SEQUENCE_ID, ccId: image.mergeId ?? NO_MERGE_ID, }; if (!(cellId in this._images)) { const created = new SpatialCell( cellId, this._scene, this._intersection); created.cameras.visible = isModeVisible(this._cameraVisualizationMode); created.applyPositionMode(this._positionMode); this._images[cellId] = created; } const cell = this._images[cellId]; if (cell.hasImage(imageId)) { return; } cell.addImage({ idMap, image: image }); const colorId = cell.getColorId(imageId, this._cameraVisualizationMode); const color = this._assets.getColor(colorId); const visible = this._filter(image); cell.visualize({ id: imageId, color, positionMode: this._positionMode, scale: this._cameraSize, transform, visible, maxSize: this._originalCameraSize, originalPosition }); this._imageCellMap.set(imageId, cellId); if (imageId === this._selectedId) { this._highlight( imageId, this._colors.select, this._cameraVisualizationMode); } if (idMap.clusterId in this._clusters) { const clusterVisible = this._getClusterVisible(idMap.clusterId); this._clusters[idMap.clusterId].points.visible = clusterVisible; } this._needsRender = true; } public addCell(vertices: number[][], cellId: string): void { if (this.hasCell(cellId)) { return; } const cell = new CellLine(vertices); this._cells[cellId] = new Object3D(); this._cells[cellId].visible = this._cellsVisible; this._cells[cellId].add(cell); this._scene.add(this._cells[cellId]); this._needsRender = true; } public deactivate(): void { this._filter = () => true; this._selectedId = null; this._hoveredId = null; this.uncache(); } public hasCluster(clusterId: string, cellId: string): boolean { return clusterId in this._clusters && this._clusters[clusterId].cellIds.indexOf(cellId) !== -1; } public hasCell(cellId: string): boolean { return cellId in this._cells; } public hasImage(imageId: string, cellId: string): boolean { return cellId in this._images && this._images[cellId].hasImage(imageId); } public render( camera: PerspectiveCamera, renderer: WebGLRenderer): void { renderer.render(this._scene, camera); this._needsRender = false; } public resetReference( reference: LngLatAlt, prevReference: LngLatAlt) : void { const clusters = this._clusters; for (const clusterId in clusters) { if (!clusters.hasOwnProperty(clusterId)) { continue; } const cluster = clusters[clusterId]; cluster.points.position.fromArray(resetEnu( reference, cluster.points.position.toArray(), prevReference)); } const cells = this._cells; for (const cellId in cells) { if (!cells.hasOwnProperty(cellId)) { continue; } const cell = cells[cellId]; cell.position.fromArray(resetEnu( reference, cell.position.toArray(), prevReference)); } const images = this._images; for (const cellId in images) { if (!images.hasOwnProperty(cellId)) { continue; } const spatialCell = images[cellId]; spatialCell.resetReference(reference, prevReference); } } public setCameraSize(cameraSize: number): void { if (Math.abs(cameraSize - this._cameraSize) < 1e-3) { return; } const imageCells = this._images; for (const cellId of Object.keys(imageCells)) { imageCells[cellId].applyCameraSize(cameraSize); } this._intersection.raycaster.near = this._getNear(cameraSize); this._cameraSize = cameraSize; this._needsRender = true; } public setFilter(filter: FilterFunction): void { this._filter = filter; const clusterVisibles: { [key: string]: boolean; } = {}; for (const imageCell of Object.values(this._images)) { imageCell.applyFilter(filter); const imageCV = imageCell.clusterVisibles; for (const clusterId in imageCV) { if (!imageCV.hasOwnProperty(clusterId)) { continue; } if (!(clusterId in clusterVisibles)) { clusterVisibles[clusterId] = false; } clusterVisibles[clusterId] ||= imageCV[clusterId]; } } const pointsVisible = this._pointVisualizationMode !== PointVisualizationMode.Hidden; for (const clusterId in clusterVisibles) { if (!clusterVisibles.hasOwnProperty(clusterId)) { continue; } clusterVisibles[clusterId] &&= pointsVisible; const visible = clusterVisibles[clusterId]; if (clusterId in this._clusters) { this._clusters[clusterId].points.visible = visible; } } this._needsRender = true; } public setHoveredImage(imageId: string | null): void { if (imageId != null && !this._imageCellMap.has(imageId)) { throw new MapillaryError(`Image does not exist: ${imageId}`); } if (this._hoveredId === imageId) { return; } this._needsRender = true; if (this._hoveredId != null) { if (this._hoveredId === this._selectedId) { this._highlight( this._hoveredId, this._colors.select, this._cameraVisualizationMode); } else { this._resetCameraColor(this._hoveredId); } } this._highlight( imageId, this._colors.hover, this._cameraVisualizationMode); this._hoveredId = imageId; } public setNavigationState(isOverview: boolean): void { this._intersection.resetIntersectionThreshold(isOverview); } public setPointSize(pointSize: number): void { if (Math.abs(pointSize - this._pointSize) < 1e-3) { return; } const clusters = this._clusters; for (const key in clusters) { if (!clusters.hasOwnProperty(key)) { continue; } for (const points of clusters[key].points.children) { (<ClusterPoints>points).resize(pointSize); } } this._pointSize = pointSize; this._needsRender = true; } public setPointVisualizationMode(mode: PointVisualizationMode): void { if (mode === this._pointVisualizationMode) { return; } this._pointVisualizationMode = mode; for (const clusterId in this._clusters) { if (!this._clusters.hasOwnProperty(clusterId)) { continue; } const cluster = this._clusters[clusterId]; cluster.points.visible = this._getClusterVisible(clusterId); for (const points of cluster.points.children) { const color = mode === PointVisualizationMode.Cluster ? this._assets.getColor(clusterId) : null; (<ClusterPoints>points).setColor(color); } } this._needsRender = true; } public setPositionMode(mode: OriginalPositionMode): void { if (mode === this._positionMode) { return; } for (const cell of Object.values(this._images)) { cell.applyPositionMode(mode); } this._positionMode = mode; this._needsRender = true; } public setSelectedImage(id: string | null): void { if (this._selectedId === id) { return; } this._needsRender = true; if (this._selectedId != null) { this._resetCameraColor(this._selectedId); } this._highlight( id, this._colors.select, this._cameraVisualizationMode); this._selectedId = id; } public setCellVisibility(visible: boolean): void { if (visible === this._cellsVisible) { return; } for (const cellId in this._cells) { if (!this._cells.hasOwnProperty(cellId)) { continue; } this._cells[cellId].visible = visible; } this._cellsVisible = visible; this._needsRender = true; } public setCameraVisualizationMode(mode: CameraVisualizationMode): void { if (mode === this._cameraVisualizationMode) { return; } const visible = isModeVisible(mode); const assets = this._assets; for (const cell of Object.values(this._images)) { cell.cameras.visible = visible; const cameraMap = cell.getCamerasByMode(mode); cameraMap.forEach( (cameras, colorId) => { const color = assets.getColor(colorId); for (const camera of cameras) { camera.setColor(color); } }); } this._highlight(this._hoveredId, this._colors.hover, mode); this._highlight(this._selectedId, this._colors.select, mode); this._cameraVisualizationMode = mode; this._needsRender = true; } public uncache(keepCellIds?: string[]): void { for (const cellId of Object.keys(this._cellClusters)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } this._disposeReconstruction(cellId); } for (const cellId of Object.keys(this._images)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } const nceMap = this._imageCellMap; const keys = this._images[cellId].keys; for (const key of keys) { nceMap.delete(key); } this._images[cellId].dispose(); delete this._images[cellId]; } for (const cellId of Object.keys(this._cells)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } this._disposeCell(cellId); } this._needsRender = true; } private _getClusterVisible(clusterId: string): boolean { if (this._pointVisualizationMode === PointVisualizationMode.Hidden) { return false; } let visible = false; for (const imageCell of Object.values(this._images)) { const imageCV = imageCell.clusterVisibles; if (!(clusterId in imageCV)) { continue; } visible ||= imageCV[clusterId]; } return visible; } private _disposePoints(cellId: string): void { for (const clusterId of this._cellClusters[cellId].keys) { if (!(clusterId in this._clusters)) { continue; } const index: number = this._clusters[clusterId].cellIds.indexOf(cellId); if (index === -1) { continue; } this._clusters[clusterId].cellIds.splice(index, 1); if (this._clusters[clusterId].cellIds.length > 0) { continue; } for (const points of this._clusters[clusterId].points.children.slice()) { (<ClusterPoints>points).dispose(); } this._scene.remove(this._clusters[clusterId].points); delete this._clusters[clusterId]; } } private _disposeReconstruction(cellId: string): void { this._disposePoints(cellId); delete this._cellClusters[cellId]; } private _disposeCell(cellId: string): void { const cell = this._cells[cellId]; for (const line of cell.children.slice()) { (<CellLine>line).dispose(); cell.remove(line); } this._scene.remove(cell); delete this._cells[cellId]; } private _getNear(cameraSize: number): number { const near = this._rayNearScale * this._originalCameraSize * cameraSize; return Math.max(1, near); } private _resetCameraColor(imageId: string): void { const nceMap = this._imageCellMap; if (imageId == null || !nceMap.has(imageId)) { return; } const cellId = nceMap.get(imageId); const cell = this._images[cellId]; const colorId = cell.getColorId(imageId, this._cameraVisualizationMode); const color = this._assets.getColor(colorId); cell.applyCameraColor(imageId, color); } private _highlight( imageId: string, color: string, mode: CameraVisualizationMode): void { const nceMap = this._imageCellMap; if (imageId == null || !nceMap.has(imageId)) { return; } const cellId = nceMap.get(imageId); color = mode === CameraVisualizationMode.Homogeneous ? color : "#FFFFFF"; this._images[cellId].applyCameraColor(imageId, color); } }