UNPKG

mapillary-js

Version:

WebGL JavaScript library for displaying street level imagery from mapillary.com

751 lines (579 loc) 25.3 kB
import * as THREE from "three"; import { IReconstructionPoint, ISpatialDataConfiguration, } from "../../Component"; import { Transform, } from "../../Geo"; import IClusterReconstruction from "./interfaces/IClusterReconstruction"; import CameraVisualizationMode from "./CameraVisualizationMode"; import { NodeData } from "./SpatialDataCache"; export class SpatialDataScene { private _scene: THREE.Scene; private _raycaster: THREE.Raycaster; private _cameraColors: { [id: string]: string }; private _needsRender: boolean; private _interactiveObjects: THREE.Object3D[]; private _tileClusterReconstructions: { [hash: string]: { keys: string[]; }; }; private _clusterReconstructions: { [key: string]: { tiles: string[]; points: THREE.Object3D; }; }; private _nodes: { [hash: string]: { cameras: THREE.Object3D; cameraKeys: { [id: string]: string }; clusters: { [id: string]: THREE.Object3D[] }; connectedComponents: { [id: string]: THREE.Object3D[] }; keys: string[]; positions: THREE.Object3D; sequences: { [id: string]: THREE.Object3D[] }; }; }; private _tiles: { [hash: string]: THREE.Object3D }; private _cameraVisualizationMode: CameraVisualizationMode; private _camerasVisible: boolean; private _pointsVisible: boolean; private _positionsVisible: boolean; private _tilesVisible: boolean; constructor(configuration: ISpatialDataConfiguration, scene?: THREE.Scene, raycaster?: THREE.Raycaster) { this._scene = !!scene ? scene : new THREE.Scene(); this._raycaster = !!raycaster ? raycaster : new THREE.Raycaster(undefined, undefined, 0.8); this._cameraColors = {}; this._needsRender = false; this._interactiveObjects = []; this._nodes = {}; this._tiles = {}; this._tileClusterReconstructions = {}; this._clusterReconstructions = {}; this._cameraVisualizationMode = !!configuration.cameraVisualizationMode ? configuration.cameraVisualizationMode : CameraVisualizationMode.Default; if (this._cameraVisualizationMode === CameraVisualizationMode.Default && configuration.connectedComponents === true) { this._cameraVisualizationMode = CameraVisualizationMode.ConnectedComponent; } this._camerasVisible = configuration.camerasVisible; this._pointsVisible = configuration.pointsVisible; this._positionsVisible = configuration.positionsVisible; this._tilesVisible = configuration.tilesVisible; } public get needsRender(): boolean { return this._needsRender; } public addClusterReconstruction( reconstruction: IClusterReconstruction, translation: number[], hash: string): void { if (this.hasClusterReconstruction(reconstruction.key, hash)) { return; } const key: string = reconstruction.key; if (!(key in this._clusterReconstructions)) { this._clusterReconstructions[key] = { points: new THREE.Object3D(), tiles: [], }; this._clusterReconstructions[key].points.visible = this._pointsVisible; this._clusterReconstructions[key].points.add( this._createClusterPoints(reconstruction, translation)); this._scene.add( this._clusterReconstructions[key].points); } if (this._clusterReconstructions[key].tiles.indexOf(hash) === -1) { this._clusterReconstructions[key].tiles.push(hash); } if (!(hash in this._tileClusterReconstructions)) { this._tileClusterReconstructions[hash] = { keys: [], }; } if (this._tileClusterReconstructions[hash].keys.indexOf(key) === -1) { this._tileClusterReconstructions[hash].keys.push(key); } this._needsRender = true; } public addNode( data: NodeData, transform: Transform, originalPosition: number[], hash: string): void { const key: string = data.key; const clusterKey: string = data.clusterKey; const sequenceKey: string = data.sequenceKey; const connectedComponent: string = !!data.mergeCC ? data.mergeCC.toString() : ""; if (this.hasNode(key, hash)) { return; } if (!(hash in this._nodes)) { this._nodes[hash] = { cameraKeys: {}, cameras: new THREE.Object3D(), clusters: {}, connectedComponents: {}, keys: [], positions: new THREE.Object3D(), sequences: {}, }; this._nodes[hash].cameras.visible = this._camerasVisible; this._nodes[hash].positions.visible = this._positionsVisible; this._scene.add( this._nodes[hash].cameras, this._nodes[hash].positions); } if (!(connectedComponent in this._nodes[hash].connectedComponents)) { this._nodes[hash].connectedComponents[connectedComponent] = []; } if (!(clusterKey in this._nodes[hash].clusters)) { this._nodes[hash].clusters[clusterKey] = []; } if (!(sequenceKey in this._nodes[hash].sequences)) { this._nodes[hash].sequences[sequenceKey] = []; } const camera: THREE.Object3D = this._createCamera(transform); this._nodes[hash].cameras.add(camera); for (const child of camera.children) { this._nodes[hash].cameraKeys[child.uuid] = key; this._interactiveObjects.push(child); } this._nodes[hash].connectedComponents[connectedComponent].push(camera); this._nodes[hash].clusters[clusterKey].push(camera); this._nodes[hash].sequences[sequenceKey].push(camera); const id: string = this._getId( clusterKey, connectedComponent, sequenceKey, this._cameraVisualizationMode); const color: string = this._getColor(id, this._cameraVisualizationMode); this._setCameraColor(color, camera); this._nodes[hash].positions.add(this._createPosition(transform, originalPosition)); this._nodes[hash].keys.push(key); this._needsRender = true; } public addTile(tileBBox: number[][], hash: string): void { if (this.hasTile(hash)) { return; } const sw: number[] = tileBBox[0]; const ne: number[] = tileBBox[1]; const geometry: THREE.Geometry = new THREE.Geometry(); geometry.vertices.push( new THREE.Vector3().fromArray(sw), new THREE.Vector3(sw[0], ne[1], (sw[2] + ne[2]) / 2), new THREE.Vector3().fromArray(ne), new THREE.Vector3(ne[0], sw[1], (sw[2] + ne[2]) / 2), new THREE.Vector3().fromArray(sw)); const tile: THREE.Object3D = new THREE.Line(geometry, new THREE.LineBasicMaterial()); this._tiles[hash] = new THREE.Object3D(); this._tiles[hash].visible = this._tilesVisible; this._tiles[hash].add(tile); this._scene.add(this._tiles[hash]); this._needsRender = true; } public uncache(keepHashes?: string[]): void { for (const hash of Object.keys(this._tileClusterReconstructions)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } this._disposeReconstruction(hash); } for (const hash of Object.keys(this._nodes)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } this._disposeNodes(hash); } for (const hash of Object.keys(this._tiles)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } this._disposeTile(hash); } this._needsRender = true; } public hasClusterReconstruction(key: string, hash: string): boolean { return key in this._clusterReconstructions && this._clusterReconstructions[key].tiles.indexOf(hash) !== -1; } public hasTile(hash: string): boolean { return hash in this._tiles; } public hasNode(key: string, hash: string): boolean { return hash in this._nodes && this._nodes[hash].keys.indexOf(key) !== -1; } public intersectObjects([viewportX, viewportY]: number[], camera: THREE.Camera): string { if (!this._camerasVisible) { return null; } this._raycaster.setFromCamera(new THREE.Vector2(viewportX, viewportY), camera); const intersects: THREE.Intersection[] = this._raycaster.intersectObjects(this._interactiveObjects); for (const intersect of intersects) { for (const hash in this._nodes) { if (!this._nodes.hasOwnProperty(hash)) { continue; } if (intersect.object.uuid in this._nodes[hash].cameraKeys) { return this._nodes[hash].cameraKeys[intersect.object.uuid]; } } } return null; } public setCameraVisibility(visible: boolean): void { if (visible === this._camerasVisible) { return; } for (const hash in this._nodes) { if (!this._nodes.hasOwnProperty(hash)) { continue; } this._nodes[hash].cameras.visible = visible; } this._camerasVisible = visible; this._needsRender = true; } public setPointVisibility(visible: boolean): void { if (visible === this._pointsVisible) { return; } for (const key in this._clusterReconstructions) { if (!this._clusterReconstructions.hasOwnProperty(key)) { continue; } this._clusterReconstructions[key].points.visible = visible; } this._pointsVisible = visible; this._needsRender = true; } public setPositionVisibility(visible: boolean): void { if (visible === this._positionsVisible) { return; } for (const hash in this._nodes) { if (!this._nodes.hasOwnProperty(hash)) { continue; } this._nodes[hash].positions.visible = visible; } this._positionsVisible = visible; this._needsRender = true; } public setTileVisibility(visible: boolean): void { if (visible === this._tilesVisible) { return; } for (const hash in this._tiles) { if (!this._tiles.hasOwnProperty(hash)) { continue; } this._tiles[hash].visible = visible; } this._tilesVisible = visible; this._needsRender = true; } public setCameraVisualizationMode(mode: CameraVisualizationMode): void { if (mode === this._cameraVisualizationMode) { return; } for (const hash in this._nodes) { if (!this._nodes.hasOwnProperty(hash)) { continue; } let cameras: { [id: number]: THREE.Object3D[] } = undefined; if (mode === CameraVisualizationMode.Cluster) { cameras = this._nodes[hash].clusters; } else if (mode === CameraVisualizationMode.ConnectedComponent) { cameras = this._nodes[hash].connectedComponents; } else if (mode === CameraVisualizationMode.Sequence) { cameras = this._nodes[hash].sequences; } else { for (const child of this._nodes[hash].cameras.children) { const color: string = this._getColor("", mode); this._setCameraColor(color, child); } continue; } for (const id in cameras) { if (!cameras.hasOwnProperty(id)) { continue; } const color: string = this._getColor(id, mode); for (const camera of cameras[id]) { this._setCameraColor(color, camera); } } } this._cameraVisualizationMode = mode; this._needsRender = true; } public render( perspectiveCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer): void { renderer.render(this._scene, perspectiveCamera); this._needsRender = false; } private _arrayToFloatArray(a: number[][], columns: number): Float32Array { const n: number = a.length; const f: Float32Array = new Float32Array(n * columns); for (let i: number = 0; i < n; i++) { const item: number[] = a[i]; const index: number = 3 * i; f[index + 0] = item[0]; f[index + 1] = item[1]; f[index + 2] = item[2]; } return f; } private _createAxis(transform: Transform): THREE.Object3D { const north: number[] = transform.unprojectBasic([0.5, 0], 0.22); const south: number[] = transform.unprojectBasic([0.5, 1], 0.16); const axis: THREE.BufferGeometry = new THREE.BufferGeometry(); axis.setAttribute("position", new THREE.BufferAttribute(this._arrayToFloatArray([north, south], 3), 3)); return new THREE.Line(axis, new THREE.LineBasicMaterial()); } private _createCamera(transform: Transform): THREE.Object3D { return !!transform.gpano ? this._createPanoCamera(transform) : this._createPrespectiveCamera(transform); } private _createDiagonals(transform: Transform, depth: number): THREE.Object3D { const origin: number [] = transform.unprojectBasic([0, 0], 0, true); const topLeft: number[] = transform.unprojectBasic([0, 0], depth, true); const topRight: number[] = transform.unprojectBasic([1, 0], depth, true); const bottomRight: number[] = transform.unprojectBasic([1, 1], depth, true); const bottomLeft: number[] = transform.unprojectBasic([0, 1], depth, true); const vertices: number[][] = [ origin, topLeft, origin, topRight, origin, bottomRight, origin, bottomLeft, ]; const diagonals: THREE.BufferGeometry = new THREE.BufferGeometry(); diagonals.setAttribute("position", new THREE.BufferAttribute(this._arrayToFloatArray(vertices, 3), 3)); return new THREE.LineSegments(diagonals, new THREE.LineBasicMaterial()); } private _createFrame(transform: Transform, depth: number): THREE.Object3D { const vertices2d: number[][] = []; vertices2d.push(...this._subsample([0, 1], [0, 0], 20)); vertices2d.push(...this._subsample([0, 0], [1, 0], 20)); vertices2d.push(...this._subsample([1, 0], [1, 1], 20)); const vertices3d: number[][] = vertices2d .map( (basic: number[]): number[] => { return transform.unprojectBasic(basic, depth, true); }); const frame: THREE.BufferGeometry = new THREE.BufferGeometry(); frame.setAttribute("position", new THREE.BufferAttribute(this._arrayToFloatArray(vertices3d, 3), 3)); return new THREE.Line(frame, new THREE.LineBasicMaterial()); } private _createLatitude(basicY: number, numVertices: number, transform: Transform): THREE.Object3D { const positions: Float32Array = new Float32Array((numVertices + 1) * 3); for (let i: number = 0; i <= numVertices; i++) { const position: number[] = transform.unprojectBasic([i / numVertices, basicY], 0.16); const index: number = 3 * i; positions[index + 0] = position[0]; positions[index + 1] = position[1]; positions[index + 2] = position[2]; } const latitude: THREE.BufferGeometry = new THREE.BufferGeometry(); latitude.setAttribute("position", new THREE.BufferAttribute(positions, 3)); return new THREE.Line(latitude, new THREE.LineBasicMaterial()); } private _createLongitude(basicX: number, numVertices: number, transform: Transform): THREE.Object3D { const positions: Float32Array = new Float32Array((numVertices + 1) * 3); for (let i: number = 0; i <= numVertices; i++) { const position: number[] = transform.unprojectBasic([basicX, i / numVertices], 0.16); const index: number = 3 * i; positions[index + 0] = position[0]; positions[index + 1] = position[1]; positions[index + 2] = position[2]; } const latitude: THREE.BufferGeometry = new THREE.BufferGeometry(); latitude.setAttribute("position", new THREE.BufferAttribute(positions, 3)); return new THREE.Line(latitude, new THREE.LineBasicMaterial()); } private _createPanoCamera(transform: Transform): THREE.Object3D { const camera: THREE.Object3D = new THREE.Object3D(); camera.children.push(this._createAxis(transform)); camera.children.push(this._createLatitude(0.5, 10, transform)); camera.children.push(this._createLongitude(0, 6, transform)); camera.children.push(this._createLongitude(0.25, 6, transform)); camera.children.push(this._createLongitude(0.5, 6, transform)); camera.children.push(this._createLongitude(0.75, 6, transform)); return camera; } private _createClusterPoints(reconstruction: IClusterReconstruction, translation: number[]): THREE.Object3D { const points: IReconstructionPoint[] = Object .keys(reconstruction.points) .map( (key: string): IReconstructionPoint => { return reconstruction.points[key]; }); const numPoints: number = points.length; const positions: Float32Array = new Float32Array(numPoints * 3); const colors: Float32Array = new Float32Array(numPoints * 3); for (let i: number = 0; i < numPoints; i++) { const index: number = 3 * i; const coords: number[] = points[i].coordinates; const point: THREE.Vector3 = new THREE.Vector3(coords[0], coords[1], coords[2]) .add(new THREE.Vector3().fromArray(translation)); positions[index + 0] = point.x; positions[index + 1] = point.y; positions[index + 2] = point.z; const color: number[] = points[i].color; colors[index + 0] = color[0] / 255.0; colors[index + 1] = color[1] / 255.0; colors[index + 2] = color[2] / 255.0; } const geometry: THREE.BufferGeometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3)); const material: THREE.PointsMaterial = new THREE.PointsMaterial({ size: 0.1, vertexColors: true, }); return new THREE.Points(geometry, material); } private _createPosition(transform: Transform, originalPosition: number[]): THREE.Object3D { const computedPosition: number[] = transform.unprojectBasic([0, 0], 0); const vertices: number[][] = [originalPosition, computedPosition]; const geometry: THREE.BufferGeometry = new THREE.BufferGeometry(); geometry.setAttribute("position", new THREE.BufferAttribute(this._arrayToFloatArray(vertices, 3), 3)); return new THREE.Line( geometry, new THREE.LineBasicMaterial({ color: new THREE.Color(1, 0, 0) })); } private _createPrespectiveCamera(transform: Transform): THREE.Object3D { const depth: number = 0.2; const camera: THREE.Object3D = new THREE.Object3D(); camera.children.push(this._createDiagonals(transform, depth)); camera.children.push(this._createFrame(transform, depth)); return camera; } private _disposeCameras(hash: string): void { const tileCameras: THREE.Object3D = this._nodes[hash].cameras; for (const camera of tileCameras.children.slice()) { for (const child of camera.children) { (<THREE.Line>child).geometry.dispose(); (<THREE.Material>(<THREE.Line>child).material).dispose(); const index: number = this._interactiveObjects.indexOf(child); if (index !== -1) { this._interactiveObjects.splice(index, 1); } else { console.warn(`Object does not exist (${child.id}) for ${hash}`); } } tileCameras.remove(camera); } this._scene.remove(tileCameras); } private _disposePoints(hash: string): void { for (const key of this._tileClusterReconstructions[hash].keys) { if (!(key in this._clusterReconstructions)) { continue; } const index: number = this._clusterReconstructions[key].tiles.indexOf(hash); if (index === -1) { continue; } this._clusterReconstructions[key].tiles.splice(index, 1); if (this._clusterReconstructions[key].tiles.length > 0) { continue; } for (const points of this._clusterReconstructions[key].points.children.slice()) { (<THREE.Points>points).geometry.dispose(); (<THREE.Material>(<THREE.Points>points).material).dispose(); } this._scene.remove(this._clusterReconstructions[key].points); delete this._clusterReconstructions[key]; } } private _disposePositions(hash: string): void { const tilePositions: THREE.Object3D = this._nodes[hash].positions; for (const position of tilePositions.children.slice()) { (<THREE.Points>position).geometry.dispose(); (<THREE.Material>(<THREE.Points>position).material).dispose(); tilePositions.remove(position); } this._scene.remove(tilePositions); } private _disposeNodes(hash: string): void { this._disposeCameras(hash); this._disposePositions(hash); delete this._nodes[hash]; } private _disposeReconstruction(hash: string): void { this._disposePoints(hash); delete this._tileClusterReconstructions[hash]; } private _disposeTile(hash: string): void { const tile: THREE.Object3D = this._tiles[hash]; for (const line of tile.children.slice()) { (<THREE.Line>line).geometry.dispose(); (<THREE.Material>(<THREE.Line>line).material).dispose(); tile.remove(line); } this._scene.remove(tile); delete this._tiles[hash]; } private _getColor(id: string, mode: CameraVisualizationMode): string { return mode !== CameraVisualizationMode.Default && id.length > 0 ? this._getCameraColor(id) : "#FFFFFF"; } private _getCameraColor(id: string): string { if (!(id in this._cameraColors)) { this._cameraColors[id] = this._randomColor(); } return this._cameraColors[id]; } private _getId( clusterKey: string, connectedComponent: string, sequenceKey: string, mode: CameraVisualizationMode): string { switch (mode) { case CameraVisualizationMode.Cluster: return clusterKey; case CameraVisualizationMode.ConnectedComponent: return connectedComponent; case CameraVisualizationMode.Sequence: return sequenceKey; default: return ""; } } private _interpolate(a: number, b: number, alpha: number): number { return a + alpha * (b - a); } private _randomColor(): string { return `hsl(${Math.floor(360 * Math.random())}, 100%, 65%)`; } private _setCameraColor(color: string, camera: THREE.Object3D): void { for (const child of camera.children) { (<THREE.LineBasicMaterial>(<THREE.Line>child).material).color = new THREE.Color(color); } } private _subsample(p1: number[], p2: number[], subsamples: number): number[][] { if (subsamples < 1) { return [p1, p2]; } const samples: number[][] = []; for (let i: number = 0; i <= subsamples + 1; i++) { const p: number[] = []; for (let j: number = 0; j < 3; j++) { p.push(this._interpolate(p1[j], p2[j], i / (subsamples + 1))); } samples.push(p); } return samples; } } export default SpatialDataScene;