UNPKG

mapillary-js

Version:

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

1,360 lines (1,093 loc) 42.5 kB
import * as THREE from "three"; import CameraVisualizationMode from "./CameraVisualizationMode"; import IClusterReconstruction, { IReconstructionPoint } from "../../api/interfaces/IClusterReconstruction"; import ISpatialDataConfiguration from "../interfaces/ISpatialDataConfiguration"; import MapillaryError from "../../error/MapillaryError"; import Node from "../../graph/Node"; import { FilterFunction } from "../../graph/FilterCreator"; import { Transform } from "../../geo/Transform"; import OriginalPositionMode from "./OriginalPositionMode"; type ClusterReconstructions = { [key: string]: { tiles: string[]; points: THREE.Object3D; }; } class CameraFrameLine extends THREE.Line { constructor( readonly geometry: THREE.BufferGeometry, readonly material: THREE.LineBasicMaterial, readonly frameOrigin: number[], readonly relativeFramePositions: number[][]) { super(geometry, material); } } class CameraFrameLineSegments extends THREE.LineSegments { constructor( readonly geometry: THREE.BufferGeometry, readonly material: THREE.LineBasicMaterial, readonly frameOrigin: number[], readonly relativeFramePositions: number[][]) { super(geometry, material); } } abstract class CameraFrameBase extends THREE.Object3D { constructor(protected readonly _originalSize: number) { super(); } public dispose(): void { for (const child of this.children) { const frameLine = <CameraFrameLine | CameraFrameLineSegments>child; frameLine.geometry.dispose(); frameLine.material.dispose(); } } public setColor(color: string): void { for (const child of this.children) { const frameLine = <CameraFrameLine | CameraFrameLineSegments>child; this._updateColorAttribute(frameLine, color); } } public resize(scale: number): void { for (const child of this.children) { const frameLine = <CameraFrameLine | CameraFrameLineSegments>child; this._updatePositionAttribute(frameLine, scale); } } protected _createBufferGeometry( positions: number[][]): THREE.BufferGeometry { const positionAttribute = new THREE.BufferAttribute( new Float32Array(3 * positions.length), 3) const colorAttribute = new THREE.BufferAttribute( new Float32Array(3 * positions.length), 3) const geometry = new THREE.BufferGeometry(); geometry.setAttribute("position", positionAttribute); geometry.setAttribute("color", colorAttribute); return geometry; } protected _createCameraFrame( origin: number[], relativePositions: number[][], scale: number, color: string): CameraFrameLine { const geometry = this._createBufferGeometry(relativePositions); const material = new THREE.LineBasicMaterial({ vertexColors: true, }); const frame = new CameraFrameLine( geometry, material, origin, relativePositions); this._updatePositionAttribute(frame, scale); this._updateColorAttribute(frame, color); return frame; } protected _updateColorAttribute( frame: CameraFrameLine | CameraFrameLineSegments, color: string): void { const [r, g, b] = new THREE.Color(color).toArray(); const colorAttribute = <THREE.BufferAttribute>frame.geometry.attributes.color; const colors = <Float32Array>colorAttribute.array; const length = colors.length; let index = 0; for (let i = 0; i < length; i++) { colors[index++] = r; colors[index++] = g; colors[index++] = b; } colorAttribute.needsUpdate = true; } protected _updatePositionAttribute( frame: CameraFrameLine | CameraFrameLineSegments, scale: number): void { const positionAttribute = <THREE.BufferAttribute>frame.geometry.attributes.position; const positions = <Float32Array>positionAttribute.array; const originX = frame.frameOrigin[0]; const originY = frame.frameOrigin[1]; const originZ = frame.frameOrigin[2]; const relativePositions = frame.relativeFramePositions; const length = relativePositions.length; let index = 0; for (let i = 0; i < length; i++) { const [deltaX, deltaY, deltaZ] = relativePositions[i]; positions[index++] = originX + scale * deltaX; positions[index++] = originY + scale * deltaY; positions[index++] = originZ + scale * deltaZ; } positionAttribute.needsUpdate = true; frame.geometry.computeBoundingSphere(); } protected _makeRelative( positions: number[][], origin: number[]): number[][] { for (const position of positions) { position[0] = position[0] - origin[0]; position[1] = position[1] - origin[1]; position[2] = position[2] - origin[2]; } return positions; } } class PerspectiveCameraFrame extends CameraFrameBase { private readonly _horizontalFrameSamples: number; private readonly _verticalFrameSamples: number; constructor( originalSize: number, transform: Transform, scale: number, color: string) { super(originalSize); this._horizontalFrameSamples = 8; this._verticalFrameSamples = 6; const origin = transform.unprojectBasic([0, 0], 0, true); const frame = this._createFrame(transform, scale, origin, color); const diagonals = this._createDiagonals(transform, scale, origin, color); this.add(frame, diagonals); } private _calculateRelativeDiagonals( transform: Transform, origin: number[]): number[][] { const depth = this._originalSize; const [topLeft, topRight, bottomRight, bottomLeft] = this._makeRelative( [ transform.unprojectBasic([0, 0], depth, true), transform.unprojectBasic([1, 0], depth, true), transform.unprojectBasic([1, 1], depth, true), transform.unprojectBasic([0, 1], depth, true), ], origin); const cameraCenter = [0, 0, 0]; const vertices: number[][] = [ cameraCenter, topLeft, cameraCenter, topRight, cameraCenter, bottomRight, cameraCenter, bottomLeft, ]; return vertices; } private _calculateRelativeFrame(transform: Transform, origin: number[]): number[][] { const vertices2d: number[][] = []; const vertical = this._verticalFrameSamples; const horizontal = this._horizontalFrameSamples; const cameraSize = this._originalSize; vertices2d.push(...this._subsample([0, 1], [0, 0], vertical)); vertices2d.push(...this._subsample([0, 0], [1, 0], horizontal)); vertices2d.push(...this._subsample([1, 0], [1, 1], vertical)); const vertices3d = vertices2d .map( (basic: number[]): number[] => { return transform.unprojectBasic(basic, cameraSize, true); }); return this._makeRelative(vertices3d, origin); } private _createDiagonals( transform: Transform, scale: number, origin: number[], color: string): CameraFrameLineSegments { const positions = this._calculateRelativeDiagonals(transform, origin); const geometry = this._createBufferGeometry(positions); const material = new THREE.LineBasicMaterial({ vertexColors: true, }); const diagonals = new CameraFrameLineSegments(geometry, material, origin, positions); this._updatePositionAttribute(diagonals, scale); this._updateColorAttribute(diagonals, color); return diagonals; } private _createFrame( transform: Transform, scale: number, origin: number[], color: string): CameraFrameLine { const positions = this._calculateRelativeFrame(transform, origin); return this._createCameraFrame(origin, positions, scale, color); } private _interpolate(a: number, b: number, alpha: number): number { return a + alpha * (b - a); } 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; } } class PanoCameraFrame extends CameraFrameBase { private readonly _latitudeVertices: number; private readonly _longitudeVertices: number; constructor( originalSize: number, transform: Transform, scale: number, color: string) { super(originalSize); this._latitudeVertices = 10; this._longitudeVertices = 6; const latV = this._latitudeVertices; const lonV = this._longitudeVertices; const origin = transform.unprojectBasic([0, 0], 0, true); const axis = this._createAxis(transform, scale, origin, color); const lat = this._createLatitude(0.5, latV, transform, scale, origin, color); const lon1 = this._createLongitude(0, lonV, transform, scale, origin, color); const lon2 = this._createLongitude(0.25, lonV, transform, scale, origin, color); const lon3 = this._createLongitude(0.5, lonV, transform, scale, origin, color); const lon4 = this._createLongitude(0.75, lonV, transform, scale, origin, color); this.add(axis, lat, lon1, lon2, lon3, lon4); } private _calculateRelativeAxis(transform: Transform, origin: number[]): number[][] { const depth = this._originalSize; const north: number[] = transform.unprojectBasic([0.5, 0], depth * 1.1); const south: number[] = transform.unprojectBasic([0.5, 1], depth * 0.8); return this._makeRelative([north, south], origin); } private _calculateRelativeLatitude( basicY: number, numVertices: number, transform: Transform, origin: number[]): number[][] { const depth = 0.8 * this._originalSize; const positions: number[][] = []; for (let i: number = 0; i <= numVertices; i++) { const position: number[] = transform.unprojectBasic( [i / numVertices, basicY], depth); positions.push(position); } return this._makeRelative(positions, origin); } private _calculateRelativeLongitude( basicX: number, numVertices: number, transform: Transform, origin: number[]): number[][] { const scaledDepth = 0.8 * this._originalSize; const positions: number[][] = []; for (let i: number = 0; i <= numVertices; i++) { const position: number[] = transform.unprojectBasic( [basicX, i / numVertices], scaledDepth); positions.push(position); } return this._makeRelative(positions, origin); } private _createAxis( transform: Transform, scale: number, origin: number[], color: string): CameraFrameLine { const positions = this._calculateRelativeAxis(transform, origin); return this._createCameraFrame(origin, positions, scale, color); } private _createLatitude( basicY: number, numVertices: number, transform: Transform, scale: number, origin: number[], color: string): CameraFrameLine { const positions = this._calculateRelativeLatitude( basicY, numVertices, transform, origin); return this._createCameraFrame(origin, positions, scale, color); } private _createLongitude( basicX: number, numVertices: number, transform: Transform, scale: number, origin: number[], color: string): CameraFrameLine { const positions = this._calculateRelativeLongitude( basicX, numVertices, transform, origin); return this._createCameraFrame(origin, positions, scale, color); } } class ClusterPoints extends THREE.Points { constructor( private readonly _originalSize: number, reconstruction: IClusterReconstruction, translation: number[], scale: number) { super(); const [positions, colors] = this._getArrays(reconstruction, translation); 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: scale * this._originalSize, vertexColors: true, }); this.geometry = geometry; this.material = material; } public dispose(): void { this.geometry.dispose(); (<THREE.PointsMaterial>this.material).dispose(); } public resize(scale: number): void { const material = <THREE.PointsMaterial>this.material; material.size = scale * this._originalSize; material.needsUpdate = true; } private _getArrays( reconstruction: IClusterReconstruction, translation: number[]): [Float32Array, Float32Array] { const points = Object .keys(reconstruction.points) .map( (key: string): IReconstructionPoint => { return reconstruction.points[key]; }); const numPoints = points.length; const positions = new Float32Array(numPoints * 3); const colors = new Float32Array(numPoints * 3); const [translationX, translationY, translationZ] = translation; for (let i = 0; i < numPoints; i++) { const index = 3 * i; const [coordsX, coordsY, coordsZ] = points[i].coordinates; positions[index + 0] = coordsX + translationX; positions[index + 1] = coordsY + translationY; positions[index + 2] = coordsZ + translationZ; const color = points[i].color; colors[index + 0] = color[0] / 255.0; colors[index + 1] = color[1] / 255.0; colors[index + 2] = color[2] / 255.0; } return [positions, colors]; } } class TileLine extends THREE.Line { constructor(bbox: number[][]) { super(); this.geometry = this._createGeometry(bbox); this.material = new THREE.LineBasicMaterial(); } public dispose(): void { this.geometry.dispose(); (<THREE.Material>this.material).dispose(); } private _createGeometry(bbox: number[][]): THREE.Geometry { const sw: number[] = bbox[0]; const ne: number[] = bbox[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)); return geometry; } } class PositionLine extends THREE.Line { public geometry: THREE.BufferGeometry; public material: THREE.LineBasicMaterial; private _adjustedAltitude: number; private _originalAltitude: number; constructor( transform: Transform, originalPosition: number[], mode: OriginalPositionMode) { super(); this._adjustedAltitude = transform.unprojectSfM([0, 0], 0)[2]; this._originalAltitude = originalPosition[2]; const altitude = this._getAltitude(mode); this.geometry = this._createGeometry( transform, originalPosition, altitude); this.material = new THREE.LineBasicMaterial({ color: new THREE.Color(1, 0, 0) }); } public dispose(): void { this.geometry.dispose(); this.material.dispose(); } public setMode(mode: OriginalPositionMode): void { const positionAttribute = <THREE.BufferAttribute>this.geometry.attributes.position; const positions = <Float32Array>positionAttribute.array; positions[2] = this._getAltitude(mode); positionAttribute.needsUpdate = true; this.geometry.computeBoundingSphere(); } private _createGeometry( transform: Transform, originalPosition: number[], altitude: number): THREE.BufferGeometry { const vertices = [ [ originalPosition[0], originalPosition[1], altitude, ], transform.unprojectBasic([0, 0], 0)]; const positions = new Float32Array(3 * vertices.length); let index = 0; for (const vertex of vertices) { positions[index++] = vertex[0]; positions[index++] = vertex[1]; positions[index++] = vertex[2]; } const geometry: THREE.BufferGeometry = new THREE.BufferGeometry(); geometry.setAttribute( "position", new THREE.BufferAttribute(positions, 3)); return geometry; } private _getAltitude(mode: OriginalPositionMode): number { return mode === OriginalPositionMode.Altitude ? this._originalAltitude : this._adjustedAltitude; } } 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: { [cellId: string]: { keys: string[]; }; }; private _clusterReconstructions: ClusterReconstructions; private _nodes: { [cellId: string]: { cameraFrames: { [key: string]: CameraFrameBase }; cameraIds: { [key: string]: { clusterKey: string; connectedComponent: string; sequenceKey: string; }; }; cameraKeys: { [id: string]: string }; cameras: THREE.Object3D; clusters: { [id: string]: CameraFrameBase[] }; connectedComponents: { [id: string]: CameraFrameBase[] }; keys: string[]; positions: THREE.Object3D; sequences: { [id: string]: CameraFrameBase[] }; props: { [id: string]: Node }; }; }; private _keyToCellId: { [key: string]: string }; private _tiles: { [cellId: string]: THREE.Object3D }; private _cameraVisualizationMode: CameraVisualizationMode; private _cameraSize: number; private _camerasVisible: boolean; private _pointSize: number; private _pointsVisible: boolean; private _positionMode: OriginalPositionMode; private _tilesVisible: boolean; private readonly _rayNearScale: number; private readonly _originalPointSize: number; private readonly _originalCameraSize: number; private readonly _lineThreshold: number; private readonly _largeLineThreshold: number; private _hoveredKey: string; private _selectedKey: string; private _interactiveLayer: number; private _filter: FilterFunction; constructor(configuration: ISpatialDataConfiguration, scene?: THREE.Scene, raycaster?: THREE.Raycaster) { this._rayNearScale = 1.1; this._originalPointSize = 2; this._originalCameraSize = 2; this._lineThreshold = 0.1; this._largeLineThreshold = 0.4; this._scene = !!scene ? scene : new THREE.Scene(); this._interactiveLayer = 1; const near = this._getNear(configuration.cameraSize); const far = 3000; this._raycaster = !!raycaster ? raycaster : new THREE.Raycaster( undefined, undefined, near, far); this._raycaster.params.Line.threshold = this._lineThreshold; this._raycaster.layers.set(this._interactiveLayer); this._cameraColors = {}; this._needsRender = false; this._interactiveObjects = []; this._nodes = {}; this._tiles = {}; this._tileClusterReconstructions = {}; this._clusterReconstructions = {}; this._keyToCellId = {}; this._cameraVisualizationMode = !!configuration.cameraVisualizationMode ? configuration.cameraVisualizationMode : CameraVisualizationMode.Default; if (this._cameraVisualizationMode === CameraVisualizationMode.Default && configuration.connectedComponents === true) { this._cameraVisualizationMode = CameraVisualizationMode.ConnectedComponent; } this._cameraSize = configuration.cameraSize; this._camerasVisible = configuration.camerasVisible; this._pointSize = configuration.pointSize; this._pointsVisible = configuration.pointsVisible; this._positionMode = configuration.originalPositionMode; this._tilesVisible = configuration.tilesVisible; this._hoveredKey = null; this._selectedKey = null; this._filter = () => true; } public get needsRender(): boolean { return this._needsRender; } public addClusterReconstruction( reconstruction: IClusterReconstruction, translation: number[], cellId: string): void { if (this.hasClusterReconstruction(reconstruction.key, cellId)) { 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( new ClusterPoints( this._originalPointSize, reconstruction, translation, this._pointSize)); this._scene.add( this._clusterReconstructions[key].points); } if (this._clusterReconstructions[key].tiles.indexOf(cellId) === -1) { this._clusterReconstructions[key].tiles.push(cellId); } if (!(cellId in this._tileClusterReconstructions)) { this._tileClusterReconstructions[cellId] = { keys: [], }; } if (this._tileClusterReconstructions[cellId].keys.indexOf(key) === -1) { this._tileClusterReconstructions[cellId].keys.push(key); } this._needsRender = true; } public addNode( node: Node, transform: Transform, originalPosition: number[], cellId: string): void { const key: string = node.key; const clusterKey: string = !!node.clusterKey ? node.clusterKey : "default_cluster_key"; const sequenceKey: string = !!node.sequenceKey ? node.sequenceKey : "default_sequence_key"; const connectedComponent: string = !!node.mergeCC ? node.mergeCC.toString() : "default_mergecc_key"; if (this.hasNode(key, cellId)) { return; } if (!(cellId in this._nodes)) { this._nodes[cellId] = { cameraFrames: {}, cameraKeys: {}, cameras: new THREE.Object3D(), clusters: {}, connectedComponents: {}, keys: [], positions: new THREE.Object3D(), sequences: {}, cameraIds: {}, props: {}, }; this._nodes[cellId].cameras.visible = this._camerasVisible; this._nodes[cellId].positions.visible = this._positionMode !== OriginalPositionMode.Hidden; this._scene.add( this._nodes[cellId].cameras, this._nodes[cellId].positions); } const nodeCell = this._nodes[cellId]; if (!(connectedComponent in nodeCell.connectedComponents)) { nodeCell.connectedComponents[connectedComponent] = []; } if (!(clusterKey in nodeCell.clusters)) { nodeCell.clusters[clusterKey] = []; } if (!(sequenceKey in nodeCell.sequences)) { nodeCell.sequences[sequenceKey] = []; } const scale = this._cameraSize; const maxSize = this._originalCameraSize; const id = this._getId( clusterKey, connectedComponent, sequenceKey, this._cameraVisualizationMode); const color = this._getColor(id, this._cameraVisualizationMode); const camera = !!transform.gpano ? new PanoCameraFrame(maxSize, transform, scale, color) : new PerspectiveCameraFrame(maxSize, transform, scale, color); this._applyFilter(camera, node, this._filter); nodeCell.cameras.add(camera); for (const child of camera.children) { nodeCell.cameraKeys[child.uuid] = key; this._interactiveObjects.push(child); } nodeCell.props[key] = node; nodeCell.connectedComponents[connectedComponent].push(camera); nodeCell.clusters[clusterKey].push(camera); nodeCell.sequences[sequenceKey].push(camera); nodeCell.positions.add( new PositionLine(transform, originalPosition, this._positionMode)); nodeCell.keys.push(key); nodeCell.cameraFrames[key] = camera; nodeCell.cameraIds[key] = { clusterKey, connectedComponent, sequenceKey }; this._keyToCellId[key] = cellId; if (key === this._selectedKey) { this._setSelectedKeyColor(key, this._cameraVisualizationMode); } this._needsRender = true; } public addTile(bbox: number[][], cellId: string): void { if (this.hasTile(cellId)) { return; } const tile = new TileLine(bbox); this._tiles[cellId] = new THREE.Object3D(); this._tiles[cellId].visible = this._tilesVisible; this._tiles[cellId].add(tile); this._scene.add(this._tiles[cellId]); this._needsRender = true; } public hasClusterReconstruction(key: string, cellId: string): boolean { return key in this._clusterReconstructions && this._clusterReconstructions[key].tiles.indexOf(cellId) !== -1; } public hasTile(cellId: string): boolean { return cellId in this._tiles; } public hasNode(key: string, cellId: string): boolean { return cellId in this._nodes && this._nodes[cellId].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 cellId in this._nodes) { if (!this._nodes.hasOwnProperty(cellId)) { continue; } if (intersect.object.uuid in this._nodes[cellId].cameraKeys) { return this._nodes[cellId].cameraKeys[intersect.object.uuid]; } } } return null; } public setCameraSize(cameraSize: number): void { if (Math.abs(cameraSize - this._cameraSize) < 1e-3) { return; } const nodes = this._nodes; for (const cellId in this._nodes) { if (!nodes.hasOwnProperty(cellId)) { continue; } for (const camera of nodes[cellId].cameras.children) { (<CameraFrameBase>camera).resize(cameraSize); } } this._raycaster.near = this._getNear(cameraSize); this._cameraSize = cameraSize; this._needsRender = true; } public setCameraVisibility(visible: boolean): void { if (visible === this._camerasVisible) { return; } for (const cellId in this._nodes) { if (!this._nodes.hasOwnProperty(cellId)) { continue; } this._nodes[cellId].cameras.visible = visible; } this._camerasVisible = visible; this._needsRender = true; } public setFilter(filter: FilterFunction): void { this._filter = filter; const nodes = this._nodes; for (const cellId in nodes) { if (!(nodes.hasOwnProperty(cellId))) { continue; } const cell = nodes[cellId]; for (const key in cell.props) { if (!(cell.props.hasOwnProperty(key))) { continue; } const node = cell.props[key]; const camera = cell.cameraFrames[key]; this._applyFilter(camera, node, filter); } } this._needsRender = true; } public setLargeIntersectionThreshold(large: boolean): void { this._raycaster.params.Line.threshold = large ? this._largeLineThreshold : this._lineThreshold; } public setHoveredKey(key?: string): void { key = key != null ? key : null; if (key != null && !(key in this._keyToCellId)) { throw new MapillaryError(`Node does not exist: ${key}`); } if (this._hoveredKey === key) { return; } this._needsRender = true; if (this._hoveredKey != null) { if (this._hoveredKey === this._selectedKey) { this._setSelectedKeyColor( this._hoveredKey, this._cameraVisualizationMode); } else { this._resetFrameColor(this._hoveredKey); } } this._setHoveredKeyColor( key, this._cameraVisualizationMode); this._hoveredKey = key; } public setPointSize(pointSize: number): void { if (Math.abs(pointSize - this._pointSize) < 1e-3) { return; } const scale = this._originalPointSize; const clusters = this._clusterReconstructions; 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 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 setPositionMode(mode: OriginalPositionMode): void { if (mode === this._positionMode) { return; } const nodes = this._nodes; for (const cellId in nodes) { if (!nodes.hasOwnProperty(cellId)) { continue; } const cell = nodes[cellId]; cell.positions.visible = mode !== OriginalPositionMode.Hidden; for (const position of cell.positions.children) { (<PositionLine>position).setMode(mode); } } this._positionMode = mode; this._needsRender = true; } public setSelectedKey(key?: string): void { key = key != null ? key : null; if (this._selectedKey === key) { return; } this._needsRender = true; if (this._selectedKey != null) { this._resetFrameColor(this._selectedKey); } this._setSelectedKeyColor( key, this._cameraVisualizationMode); this._selectedKey = key; } public setTileVisibility(visible: boolean): void { if (visible === this._tilesVisible) { return; } for (const cellId in this._tiles) { if (!this._tiles.hasOwnProperty(cellId)) { continue; } this._tiles[cellId].visible = visible; } this._tilesVisible = visible; this._needsRender = true; } public setCameraVisualizationMode(mode: CameraVisualizationMode): void { if (mode === this._cameraVisualizationMode) { return; } const nodes = this._nodes; for (const cellId in nodes) { if (!this._nodes.hasOwnProperty(cellId)) { continue; } let cameras: { [id: number]: CameraFrameBase[] } = undefined; if (mode === CameraVisualizationMode.Cluster) { cameras = nodes[cellId].clusters; } else if (mode === CameraVisualizationMode.ConnectedComponent) { cameras = nodes[cellId].connectedComponents; } else if (mode === CameraVisualizationMode.Sequence) { cameras = nodes[cellId].sequences; } else { const color: string = this._getColor("", mode); for (const child of nodes[cellId].cameras.children) { (<CameraFrameBase>child).setColor(color); } continue; } for (const id in cameras) { if (!cameras.hasOwnProperty(id)) { continue; } const color: string = this._getColor(id, mode); for (const camera of cameras[id]) { camera.setColor(color); } } } this._setHoveredKeyColor(this._hoveredKey, mode); this._setSelectedKeyColor(this._selectedKey, mode); this._cameraVisualizationMode = mode; this._needsRender = true; } public render( perspectiveCamera: THREE.PerspectiveCamera, renderer: THREE.WebGLRenderer): void { renderer.render(this._scene, perspectiveCamera); this._needsRender = false; } public uncache(keepCellIds?: string[]): void { for (const cellId of Object.keys(this._tileClusterReconstructions)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } this._disposeReconstruction(cellId); } for (const cellId of Object.keys(this._nodes)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } this._disposeNodes(cellId); } for (const cellId of Object.keys(this._tiles)) { if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) { continue; } this._disposeTile(cellId); } this._needsRender = true; } private _applyFilter( camera: THREE.Object3D, node: Node, filter: FilterFunction): void { const interactiveLayer = this._interactiveLayer; camera.visible = filter(node); if (camera.visible) { for (const child of camera.children) { child.layers.enable(interactiveLayer); } } else { for (const child of camera.children) { child.layers.disable(interactiveLayer); } } } private _disposeCameras(cellId: string): void { const tileCameras = this._nodes[cellId].cameras; for (const camera of tileCameras.children.slice()) { (<CameraFrameBase>camera).dispose(); for (const child of camera.children) { const index = this._interactiveObjects.indexOf(child); if (index !== -1) { this._interactiveObjects.splice(index, 1); } else { console.warn(`Object does not exist (${child.id}) for ${cellId}`); } } tileCameras.remove(camera); } const keyToCellId = this._keyToCellId; const keys = this._nodes[cellId].keys; for (const key of keys) { delete keyToCellId[key]; } this._scene.remove(tileCameras); } private _disposePoints(cellId: string): void { for (const key of this._tileClusterReconstructions[cellId].keys) { if (!(key in this._clusterReconstructions)) { continue; } const index: number = this._clusterReconstructions[key].tiles.indexOf(cellId); 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()) { (<ClusterPoints>points).dispose(); } this._scene.remove(this._clusterReconstructions[key].points); delete this._clusterReconstructions[key]; } } private _disposePositions(cellId: string): void { const tilePositions: THREE.Object3D = this._nodes[cellId].positions; for (const position of tilePositions.children.slice()) { (<PositionLine>position).dispose(); tilePositions.remove(position); } this._scene.remove(tilePositions); } private _disposeNodes(cellId: string): void { this._disposeCameras(cellId); this._disposePositions(cellId); delete this._nodes[cellId]; } private _disposeReconstruction(cellId: string): void { this._disposePoints(cellId); delete this._tileClusterReconstructions[cellId]; } private _disposeTile(cellId: string): void { const tile: THREE.Object3D = this._tiles[cellId]; for (const line of tile.children.slice()) { (<TileLine>line).dispose(); tile.remove(line); } this._scene.remove(tile); delete this._tiles[cellId]; } 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 _getNear(cameraSize: number): number { const near = this._rayNearScale * this._originalCameraSize * cameraSize; return Math.max(1, near); } private _randomColor(): string { return `hsl(${Math.floor(360 * Math.random())}, 100%, 50%)`; } private _resetFrameColor(key: string): void { if (key == null || !(key in this._keyToCellId)) { return; } const cellId = this._keyToCellId[key]; const cameraIds = this._nodes[cellId].cameraIds[key]; const id = this._getId( cameraIds.clusterKey, cameraIds.connectedComponent, cameraIds.sequenceKey, this._cameraVisualizationMode); const color = this._getColor(id, this._cameraVisualizationMode); this._nodes[cellId].cameraFrames[key].setColor(color); } private _setHoveredKeyColor(key: string, mode: CameraVisualizationMode) : void { if (key == null) { return; } const cellId = this._keyToCellId[key]; const color = mode === CameraVisualizationMode.Default ? "#FF0000" : "#FFFFFF"; this._nodes[cellId].cameraFrames[key].setColor(color); } private _setSelectedKeyColor(key: string, mode: CameraVisualizationMode) : void { if (key == null || !(key in this._keyToCellId)) { return; } const cellId = this._keyToCellId[key]; const color = mode === CameraVisualizationMode.Default ? "#FF8000" : "#FFFFFF"; this._nodes[cellId].cameraFrames[key].setColor(color); } } export default SpatialDataScene;