UNPKG

mapillary-js

Version:

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

616 lines (533 loc) 22.4 kB
import * as geohash from "latlon-geohash"; import { combineLatest as observableCombineLatest, empty as observableEmpty, from as observableFrom, of as observableOf, Observable, Subscription, } from "rxjs"; import { catchError, withLatestFrom, map, distinctUntilChanged, concatMap, switchMap, tap, filter, last, mergeMap, first, refCount, publishReplay, publish, } from "rxjs/operators"; import { ComponentService, Component, ISpatialDataConfiguration, NodeData, SpatialDataCache, SpatialDataScene, } from "../../Component"; import { Geo, GeoCoords, ILatLonAlt, Transform, ViewportCoords, } from "../../Geo"; import { Node, } from "../../Graph"; import { IGLRenderHash, GLRenderStage, RenderCamera, } from "../../Render"; import { IFrame, } from "../../State"; import { Container, Navigator, } from "../../Viewer"; import PlayService from "../../viewer/PlayService"; import State from "../../state/State"; import IClusterReconstruction from "./interfaces/IClusterReconstruction"; import CameraVisualizationMode from "./CameraVisualizationMode"; export class SpatialDataComponent extends Component<ISpatialDataConfiguration> { public static componentName: string = "spatialData"; private _cache: SpatialDataCache; private _scene: SpatialDataScene; private _viewportCoords: ViewportCoords; private _geoCoords: GeoCoords; private _addNodeSubscription: Subscription; private _addReconstructionSubscription: Subscription; private _addTileSubscription: Subscription; private _cameraVisibilitySubscription: Subscription; private _earthControlsSubscription: Subscription; private _moveSubscription: Subscription; private _pointVisibilitySubscription: Subscription; private _positionVisibilitySubscription: Subscription; private _renderSubscription: Subscription; private _tileVisibilitySubscription: Subscription; private _uncacheSubscription: Subscription; private _cameraVisualizationModeSubscription: Subscription; private _ccToModeSubscription: Subscription; constructor(name: string, container: Container, navigator: Navigator) { super(name, container, navigator); this._cache = new SpatialDataCache(navigator.graphService); this._scene = new SpatialDataScene(this._getDefaultConfiguration()); this._viewportCoords = new ViewportCoords(); this._geoCoords = new GeoCoords(); } protected _activate(): void { this._earthControlsSubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): boolean => { return configuration.earthControls; }), distinctUntilChanged(), withLatestFrom(this._navigator.stateService.state$)) .subscribe( ([earth, state]: [boolean, State]): void => { if (earth && state !== State.Earth) { this._navigator.stateService.earth(); } else if (!earth && state === State.Earth) { this._navigator.stateService.traverse(); } }); const direction$: Observable<string> = this._container.renderService.bearing$.pipe( map( (bearing: number): string => { let direction: string = ""; if (bearing > 292.5 || bearing <= 67.5) { direction += "n"; } if (bearing > 112.5 && bearing <= 247.5) { direction += "s"; } if (bearing > 22.5 && bearing <= 157.5) { direction += "e"; } if (bearing > 202.5 && bearing <= 337.5) { direction += "w"; } return direction; }), distinctUntilChanged(), publishReplay(1), refCount()); const hash$: Observable<string> = this._navigator.stateService.reference$.pipe( tap( (): void => { this._scene.uncache(); }), switchMap( (): Observable<string> => { return this._navigator.stateService.currentNode$.pipe( map( (node: Node): string => { return geohash.encode(node.latLon.lat, node.latLon.lon, 8); }), distinctUntilChanged()); }), publishReplay(1), refCount()); const sequencePlay$: Observable<boolean> = observableCombineLatest( this._navigator.playService.playing$, this._navigator.playService.speed$).pipe( map( ([playing, speed]: [boolean, number]): boolean => { return playing && speed > PlayService.sequenceSpeed; }), distinctUntilChanged(), publishReplay(1), refCount()); const hashes$: Observable<string[]> = observableCombineLatest( this._navigator.stateService.state$.pipe( map( (state: State): boolean => { return state === State.Earth; }), distinctUntilChanged()), hash$, sequencePlay$, direction$).pipe( distinctUntilChanged( ( [e1, h1, s1, d1]: [boolean, string, boolean, string], [e2, h2, s2, d2]: [boolean, string, boolean, string]): boolean => { if (e1 !== e2) { return false; } if (e1) { return h1 === h2 && s1 === s2; } return h1 === h2 && s1 === s2 && d1 === d2; }), concatMap( ([earth, hash, sequencePlay, direction]: [boolean, string, boolean, string]): Observable<string[]> => { if (earth) { return sequencePlay ? observableOf([hash]) : observableOf(this._adjacentComponent(hash, 4)); } return sequencePlay ? observableOf([hash, geohash.neighbours(hash)[<keyof geohash.Neighbours>direction]]) : observableOf(this._computeTiles(hash, direction)); }), publish(), refCount()); const tile$: Observable<[string, NodeData[]]> = hashes$.pipe( switchMap( (hashes: string[]): Observable<[string, NodeData[]]> => { return observableFrom(hashes).pipe( mergeMap( (h: string): Observable<[string, NodeData[]]> => { const t$: Observable<NodeData[]> = this._cache.hasTile(h) ? observableOf(this._cache.getTile(h)) : this._cache.cacheTile$(h); return observableCombineLatest(observableOf(h), t$); }, 6)); }), publish(), refCount()); this._addTileSubscription = tile$.pipe( withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([[hash], reference]: [[string, NodeData[]], ILatLonAlt]): void => { if (this._scene.hasTile(hash)) { return; } this._scene.addTile(this._computeTileBBox(hash, reference), hash); }); this._addNodeSubscription = tile$.pipe( withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([[hash, datas], reference]: [[string, [NodeData]], ILatLonAlt]): void => { for (const data of datas) { if (this._scene.hasNode(data.key, hash)) { continue; } this._scene.addNode( data, this._createTransform(data, reference), this._computeOriginalPosition(data, reference), hash); } }); this._addReconstructionSubscription = tile$.pipe( concatMap( ([hash]: [string, NodeData[]]): Observable<[string, IClusterReconstruction]> => { let reconstructions$: Observable<IClusterReconstruction>; if (this._cache.hasClusterReconstructions(hash)) { reconstructions$ = observableFrom(this._cache.getClusterReconstructions(hash)); } else if (this._cache.isCachingClusterReconstructions(hash)) { reconstructions$ = this._cache.cacheClusterReconstructions$(hash).pipe( last(null, {}), switchMap( (): Observable<IClusterReconstruction> => { return observableFrom(this._cache.getClusterReconstructions(hash)); })); } else if (this._cache.hasTile(hash)) { reconstructions$ = this._cache.cacheClusterReconstructions$(hash); } else { reconstructions$ = observableEmpty(); } return observableCombineLatest(observableOf(hash), reconstructions$); }), withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([[hash, reconstruction], reference]: [[string, IClusterReconstruction], ILatLonAlt]): void => { if (this._scene.hasClusterReconstruction(reconstruction.key, hash)) { return; } this._scene.addClusterReconstruction( reconstruction, this._computeTranslation(reconstruction, reference), hash); }); this._cameraVisibilitySubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): boolean => { return configuration.camerasVisible; }), distinctUntilChanged()) .subscribe( (visible: boolean): void => { this._scene.setCameraVisibility(visible); }); this._pointVisibilitySubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): boolean => { return configuration.pointsVisible; }), distinctUntilChanged()) .subscribe( (visible: boolean): void => { this._scene.setPointVisibility(visible); }); this._positionVisibilitySubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): boolean => { return configuration.positionsVisible; }), distinctUntilChanged()) .subscribe( (visible: boolean): void => { this._scene.setPositionVisibility(visible); }); this._tileVisibilitySubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): boolean => { return configuration.tilesVisible; }), distinctUntilChanged()) .subscribe( (visible: boolean): void => { this._scene.setTileVisibility(visible); }); this._ccToModeSubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): CameraVisualizationMode => { return configuration.connectedComponents === true ? CameraVisualizationMode.ConnectedComponent : CameraVisualizationMode.Default; }), distinctUntilChanged()) .subscribe( (mode: CameraVisualizationMode): void => { this.configure({ cameraVisualizationMode: mode }); }); this._cameraVisualizationModeSubscription = this._configuration$.pipe( map( (configuration: ISpatialDataConfiguration): CameraVisualizationMode => { return configuration.cameraVisualizationMode; }), distinctUntilChanged()) .subscribe( (mode: CameraVisualizationMode): void => { this._scene.setCameraVisualizationMode(mode); }); this._uncacheSubscription = hash$ .subscribe( (hash: string): void => { const keepHashes: string[] = this._adjacentComponent(hash, 4); this._scene.uncache(keepHashes); this._cache.uncache(keepHashes); }); this._moveSubscription = this._navigator.playService.playing$.pipe( switchMap( (playing: boolean): Observable<MouseEvent> => { return playing ? observableEmpty() : this._container.mouseService.dblClick$; }), withLatestFrom(this._container.renderService.renderCamera$), switchMap( ([event, render]: [MouseEvent, RenderCamera]): Observable<Node> => { const element: HTMLElement = this._container.element; const [canvasX, canvasY]: number[] = this._viewportCoords.canvasPosition(event, element); const viewport: number[] = this._viewportCoords.canvasToViewport( canvasX, canvasY, element); const key: string = this._scene.intersectObjects(viewport, render.perspective); return !!key ? this._navigator.moveToKey$(key).pipe( catchError( (): Observable<Node> => { return observableEmpty(); })) : observableEmpty(); })) .subscribe(); this._renderSubscription = this._navigator.stateService.currentState$.pipe( map( (frame: IFrame): IGLRenderHash => { const scene: SpatialDataScene = this._scene; return { name: this._name, render: { frameId: frame.id, needsRender: scene.needsRender, render: scene.render.bind(scene), stage: GLRenderStage.Foreground, }, }; })) .subscribe(this._container.glRenderer.render$); } protected _deactivate(): void { this._cache.uncache(); this._scene.uncache(); this._addNodeSubscription.unsubscribe(); this._addReconstructionSubscription.unsubscribe(); this._addTileSubscription.unsubscribe(); this._cameraVisibilitySubscription.unsubscribe(); this._earthControlsSubscription.unsubscribe(); this._moveSubscription.unsubscribe(); this._pointVisibilitySubscription.unsubscribe(); this._positionVisibilitySubscription.unsubscribe(); this._renderSubscription.unsubscribe(); this._tileVisibilitySubscription.unsubscribe(); this._uncacheSubscription.unsubscribe(); this._cameraVisualizationModeSubscription.unsubscribe(); this._ccToModeSubscription.unsubscribe(); this._navigator.stateService.state$.pipe( first()) .subscribe( (state: State): void => { if (state === State.Earth) { this._navigator.stateService.traverse(); } }); } protected _getDefaultConfiguration(): ISpatialDataConfiguration { return { cameraVisualizationMode: CameraVisualizationMode.Default, camerasVisible: false, connectedComponents: false, pointsVisible: true, positionsVisible: false, tilesVisible: false, }; } private _adjacentComponent(hash: string, depth: number): string[] { const hashSet: Set<string> = new Set<string>(); hashSet.add(hash); this._adjacentComponentRecursive(hashSet, [hash], 0, depth); return this._setToArray(hashSet); } private _adjacentComponentRecursive( hashSet: Set<string>, currentHashes: string[], currentDepth: number, maxDepth: number): void { if (currentDepth === maxDepth) { return; } const neighbours: string[] = []; for (const hash of currentHashes) { const hashNeighbours: geohash.Neighbours = geohash.neighbours(hash); for (const direction in hashNeighbours) { if (!hashNeighbours.hasOwnProperty(direction)) { continue; } neighbours.push(hashNeighbours[<keyof geohash.Neighbours>direction]); } } const newHashes: string[] = []; for (const neighbour of neighbours) { if (!hashSet.has(neighbour)) { hashSet.add(neighbour); newHashes.push(neighbour); } } this._adjacentComponentRecursive(hashSet, newHashes, currentDepth + 1, maxDepth); } private _computeOriginalPosition(data: NodeData, reference: ILatLonAlt): number[] { return this._geoCoords.geodeticToEnu( data.originalLat, data.originalLon, data.alt, reference.lat, reference.lon, reference.alt); } private _computeTileBBox(hash: string, reference: ILatLonAlt): number[][] { const bounds: geohash.Bounds = geohash.bounds(hash); const sw: number[] = this._geoCoords.geodeticToEnu( bounds.sw.lat, bounds.sw.lon, 0, reference.lat, reference.lon, reference.alt); const ne: number[] = this._geoCoords.geodeticToEnu( bounds.ne.lat, bounds.ne.lon, 0, reference.lat, reference.lon, reference.alt); return [sw, ne]; } private _createTransform(data: NodeData, reference: ILatLonAlt): Transform { const translation: number[] = Geo.computeTranslation( { alt: data.alt, lat: data.lat, lon: data.lon }, data.rotation, reference); const transform: Transform = new Transform( data.orientation, data.width, data.height, data.focal, data.scale, data.gpano, data.rotation, translation, undefined, undefined, data.k1, data.k2, data.cameraProjection); return transform; } private _computeTiles(hash: string, direction: string): string[] { const hashSet: Set<string> = new Set<string>(); const directions: string[] = ["n", "ne", "e", "se", "s", "sw", "w", "nw"]; this._computeTilesRecursive(hashSet, hash, direction, directions, 0, 2); return this._setToArray(hashSet); } private _computeTilesRecursive( hashSet: Set<string>, currentHash: string, direction: string, directions: string[], currentDepth: number, maxDepth: number): void { hashSet.add(currentHash); if (currentDepth === maxDepth) { return; } const neighbours: geohash.Neighbours = geohash.neighbours(currentHash); const directionIndex: number = directions.indexOf(direction); const length: number = directions.length; const directionNeighbours: string[] = [ neighbours[<keyof geohash.Neighbours>directions[this._modulo((directionIndex - 1), length)]], neighbours[<keyof geohash.Neighbours>direction], neighbours[<keyof geohash.Neighbours>directions[this._modulo((directionIndex + 1), length)]], ]; for (let directionNeighbour of directionNeighbours) { this._computeTilesRecursive(hashSet, directionNeighbour, direction, directions, currentDepth + 1, maxDepth); } } private _computeTranslation(reconstruction: IClusterReconstruction, reference: ILatLonAlt): number[] { return this._geoCoords.geodeticToEnu( reconstruction.reference_lla.latitude, reconstruction.reference_lla.longitude, reconstruction.reference_lla.altitude, reference.lat, reference.lon, reference.alt); } private _modulo(a: number, n: number): number { return ((a % n) + n) % n; } private _setToArray<T>(s: Set<T>): T[] { const a: T[] = []; s.forEach( (value: T) => { a.push(value); }); return a; } } ComponentService.register(SpatialDataComponent); export default SpatialDataComponent;