src/component/spatial/SpatialComponent.ts (625 lines of code) (raw):

import { combineLatest as observableCombineLatest, concat as observableConcat, empty as observableEmpty, from as observableFrom, merge as observableMerge, of as observableOf, Observable, } from "rxjs"; import { catchError, concatMap, distinctUntilChanged, first, last, map, mergeMap, publishReplay, publish, refCount, switchMap, take, withLatestFrom, filter, pairwise, } from "rxjs/operators"; import { Image } from "../../graph/Image"; import { Container } from "../../viewer/Container"; import { Navigator } from "../../viewer/Navigator"; import { ClusterContract } from "../../api/contracts/ClusterContract"; import { LngLatAlt } from "../../api/interfaces/LngLatAlt"; import { Spatial } from "../../geo/Spatial"; import { Transform } from "../../geo/Transform"; import { ViewportCoords } from "../../geo/ViewportCoords"; import { FilterFunction } from "../../graph/FilterCreator"; import * as Geo from "../../geo/Geo"; import { RenderPass } from "../../render/RenderPass"; import { GLRenderHash } from "../../render/interfaces/IGLRenderHash"; import { RenderCamera } from "../../render/RenderCamera"; import { AnimationFrame } from "../../state/interfaces/AnimationFrame"; import { PlayService } from "../../viewer/PlayService"; import { Component } from "../Component"; import { SpatialConfiguration } from "../interfaces/SpatialConfiguration"; import { CameraVisualizationMode } from "./enums/CameraVisualizationMode"; import { OriginalPositionMode } from "./enums/OriginalPositionMode"; import { SpatialScene } from "./SpatialScene"; import { SpatialCache } from "./SpatialCache"; import { CameraType } from "../../geo/interfaces/CameraType"; import { geodeticToEnu } from "../../geo/GeoCoords"; import { LngLat } from "../../api/interfaces/LngLat"; import { ComponentName } from "../ComponentName"; import { isModeVisible, isOverviewState } from "./Modes"; import { State } from "../../state/State"; import { connectedComponent } from "../../api/CellMath"; import { PointVisualizationMode } from "./enums/PointVisualizationMode"; type IntersectEvent = MouseEvent | FocusEvent; type Cell = { id: string; images: Image[]; }; type AdjancentParams = [boolean, boolean, number, number, Image]; interface IntersectConfiguration { size: number; visible: boolean; state: State; } export class SpatialComponent extends Component<SpatialConfiguration> { public static componentName: ComponentName = "spatial"; private _cache: SpatialCache; private _scene: SpatialScene; private _viewportCoords: ViewportCoords; private _spatial: Spatial; /** @ignore */ constructor(name: string, container: Container, navigator: Navigator) { super(name, container, navigator); this._cache = new SpatialCache( navigator.graphService, navigator.api.data); this._scene = new SpatialScene(this._getDefaultConfiguration()); this._viewportCoords = new ViewportCoords(); this._spatial = new Spatial(); } /** * Returns the image id of the camera frame closest to the current * render camera position at the specified point. * * @description Notice that the pixelPoint argument requires x, y * coordinates from pixel space. * * With this function, you can use the coordinates provided by mouse * events to get information out of the spatial component. * * If no camera frame exist at the pixel * point, `null` will be returned. * * @param {Array<number>} pixelPoint - Pixel coordinates on * the viewer element. * @returns {string} Image id of the camera frame closest to * the camera. If no camera frame is intersected at the * pixel point, `null` will be returned. * * @example * ```js * spatialComponent.getFrameIdAt([100, 125]) * .then((imageId) => { console.log(imageId); }); * ``` */ public getFrameIdAt(pixelPoint: number[]): Promise<string> { return new Promise<string>((resolve: (value: string) => void, reject: (reason: Error) => void): void => { this._container.renderService.renderCamera$.pipe( first(), map( (render: RenderCamera): string => { const viewport = this._viewportCoords .canvasToViewport( pixelPoint[0], pixelPoint[1], this._container.container); const id = this._scene.intersection .intersectObjects(viewport, render.perspective); return id; })) .subscribe( (id: string): void => { resolve(id); }, (error: Error): void => { reject(error); }); }); } protected _activate(): void { this._navigator.cacheService.configure({ cellDepth: 3 }); const subs = this._subscriptions; subs.push(this._navigator.stateService.reference$ .pipe( pairwise()) .subscribe( ([prevReference, reference]: [LngLatAlt, LngLatAlt]): void => { this._scene.resetReference(reference, prevReference); } )); subs.push(this._navigator.graphService.filter$ .subscribe(imageFilter => { this._scene.setFilter(imageFilter); })); const bearing$ = this._container.renderService.bearing$.pipe( map( (bearing: number): number => { const interval = 6; const discrete = interval * Math.floor(bearing / interval); return discrete; }), distinctUntilChanged(), publishReplay(1), refCount()); const cellId$ = this._navigator.stateService.currentImage$ .pipe( map( (image: Image): string => { return this._navigator.api.data.geometry .lngLatToCellId(image.originalLngLat); }), distinctUntilChanged(), publishReplay(1), refCount()); const cellGridDepth$ = this._configuration$ .pipe( map( (c: SpatialConfiguration): number => { return this._spatial.clamp(c.cellGridDepth, 1, 3); }), distinctUntilChanged(), publishReplay(1), refCount()); const sequencePlay$ = 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 isOverview$ = this._navigator.stateService.state$.pipe( map( (state: State): boolean => { return isOverviewState(state); }), distinctUntilChanged(), publishReplay(1), refCount()); subs.push(isOverview$.subscribe( (isOverview: boolean): void => { this._scene.setNavigationState(isOverview); })); const cell$ = observableCombineLatest( isOverview$, sequencePlay$, bearing$, cellGridDepth$, this._navigator.stateService.currentImage$) .pipe( distinctUntilChanged(( [o1, s1, b1, d1, i1]: AdjancentParams, [o2, s2, b2, d2, i2]: AdjancentParams) : boolean => { if (o1 !== o2) { return false; } const isd = i1.id === i2.id && s1 === s2 && d1 === d2; if (o1) { return isd; } return isd && b1 === b2; }), concatMap( ([isOverview, sequencePlay, bearing, depth, image] : AdjancentParams) : Observable<string[]> => { if (isOverview) { const geometry = this._navigator.api.data.geometry; const cellId = geometry .lngLatToCellId(image.originalLngLat); const cells = sequencePlay ? [cellId] : connectedComponent(cellId, depth, geometry); return observableOf(cells); } const fov = sequencePlay ? 30 : 90; return observableOf( this._cellsInFov( image, bearing, fov)); }), switchMap( (cellIds: string[]): Observable<Cell> => { return observableFrom(cellIds).pipe( mergeMap( (cellId: string): Observable<Cell> => { const t$ = this._cache.hasCell(cellId) ? observableOf( this._cache.getCell(cellId)) : this._cache.cacheCell$(cellId); return t$.pipe( map((images: Image[]) => ({ id: cellId, images }))); }, 6)); })); subs.push(cell$.pipe( withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([cell, reference]: [Cell, LngLatAlt]): void => { if (this._scene.hasCell(cell.id)) { return; } this._scene.addCell( this._cellToTopocentric(cell.id, reference), cell.id); })); subs.push(cell$.pipe( withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([cell, reference]: [Cell, LngLatAlt]): void => { this._addSceneImages(cell, reference); })); subs.push(cell$.pipe( concatMap( (cell: Cell): Observable<[string, ClusterContract]> => { const cellId = cell.id; let reconstructions$: Observable<ClusterContract>; if (this._cache.hasClusters(cellId)) { reconstructions$ = observableFrom(this._cache.getClusters(cellId)); } else if (this._cache.isCachingClusters(cellId)) { reconstructions$ = this._cache.cacheClusters$(cellId).pipe( last(null, {}), switchMap( (): Observable<ClusterContract> => { return observableFrom(this._cache.getClusters(cellId)); })); } else if (this._cache.hasCell(cellId)) { reconstructions$ = this._cache.cacheClusters$(cellId); } else { reconstructions$ = observableEmpty(); } return observableCombineLatest(observableOf(cellId), reconstructions$); }), withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([[cellId, reconstruction], reference]: [[string, ClusterContract], LngLatAlt]): void => { if (this._scene .hasCluster( reconstruction.id, cellId)) { return; } this._scene.addCluster( reconstruction, this._computeTranslation( reconstruction, reference), cellId); })); subs.push(this._configuration$.pipe( map( (c: SpatialConfiguration): SpatialConfiguration => { c.cameraSize = this._spatial.clamp(c.cameraSize, 0.01, 1); c.pointSize = this._spatial.clamp(c.pointSize, 0.01, 1); const pointVisualizationMode = c.pointsVisible ? c.pointVisualizationMode ?? PointVisualizationMode.Original : PointVisualizationMode.Hidden; return { cameraSize: c.cameraSize, cameraVisualizationMode: c.cameraVisualizationMode, cellsVisible: c.cellsVisible, originalPositionMode: c.originalPositionMode, pointSize: c.pointSize, pointVisualizationMode, }; }), distinctUntilChanged( (c1: SpatialConfiguration, c2: SpatialConfiguration) : boolean => { return c1.cameraSize === c2.cameraSize && c1.cameraVisualizationMode === c2.cameraVisualizationMode && c1.cellsVisible === c2.cellsVisible && c1.originalPositionMode === c2.originalPositionMode && c1.pointSize === c2.pointSize && c1.pointVisualizationMode === c2.pointVisualizationMode; })) .subscribe( (c: SpatialConfiguration): void => { this._scene.setCameraSize(c.cameraSize); const cvm = c.cameraVisualizationMode; this._scene.setCameraVisualizationMode(cvm); this._scene.setCellVisibility(c.cellsVisible); this._scene.setPointSize(c.pointSize); const pvm = c.pointVisualizationMode; this._scene.setPointVisualizationMode(pvm); const opm = c.originalPositionMode; this._scene.setPositionMode(opm); })); subs.push(observableCombineLatest(cellId$, cellGridDepth$) .subscribe( ([cellId, depth]: [string, number]): void => { const keepCells = connectedComponent( cellId, depth, this._navigator.api.data.geometry); this._scene.uncache(keepCells); this._cache.uncache(keepCells); })); subs.push(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<Image> => { const element = this._container.container; const [canvasX, canvasY] = this._viewportCoords .canvasPosition(event, element); const viewport = this._viewportCoords.canvasToViewport( canvasX, canvasY, element); const id = this._scene.intersection .intersectObjects(viewport, render.perspective); return !!id ? this._navigator.moveTo$(id).pipe( catchError( (): Observable<Image> => { return observableEmpty(); })) : observableEmpty(); })) .subscribe()); const intersectChange$ = observableCombineLatest( this._configuration$, this._navigator.stateService.state$).pipe( map( ([c, state]: [SpatialConfiguration, State]) : IntersectConfiguration => { c.cameraSize = this._spatial.clamp( c.cameraSize, 0.01, 1); return { size: c.cameraSize, visible: isModeVisible(c.cameraVisualizationMode), state, }; }), distinctUntilChanged( (c1: IntersectConfiguration, c2: IntersectConfiguration): boolean => { return c1.size === c2.size && c1.visible === c2.visible && c1.state === c2.state; })); const mouseMove$ = this._container.mouseService.mouseMove$.pipe( publishReplay(1), refCount()); subs.push(mouseMove$.subscribe()); const mouseHover$ = observableMerge( this._container.mouseService.mouseEnter$, this._container.mouseService.mouseLeave$, this._container.mouseService.windowBlur$); subs.push(observableCombineLatest( this._navigator.playService.playing$, mouseHover$, isOverview$, this._navigator.graphService.filter$) .pipe( switchMap( ([playing, mouseHover]: [boolean, IntersectEvent, boolean, FilterFunction]) : Observable<[IntersectEvent, RenderCamera, IntersectConfiguration]> => { return !playing && mouseHover.type === "pointerenter" ? observableCombineLatest( observableConcat( mouseMove$.pipe(take(1)), this._container.mouseService.mouseMove$), this._container.renderService.renderCamera$, intersectChange$) : observableCombineLatest( observableOf(mouseHover), observableOf(null), observableOf(null)); })) .subscribe( ([event, render] : [IntersectEvent, RenderCamera, IntersectConfiguration]): void => { if (event.type !== "pointermove") { this._scene.setHoveredImage(null); return; } const element = this._container.container; const [canvasX, canvasY] = this._viewportCoords.canvasPosition(<MouseEvent>event, element); const viewport = this._viewportCoords.canvasToViewport( canvasX, canvasY, element); const key = this._scene.intersection .intersectObjects(viewport, render.perspective); this._scene.setHoveredImage(key); })); subs.push(this._navigator.stateService.currentId$ .subscribe( (id: string): void => { this._scene.setSelectedImage(id); })); subs.push(this._navigator.stateService.currentState$ .pipe( map((frame: AnimationFrame): GLRenderHash => { const scene = this._scene; return { name: this._name, renderer: { frameId: frame.id, needsRender: scene.needsRender, render: scene.render.bind(scene), pass: RenderPass.Opaque, }, }; })) .subscribe(this._container.glRenderer.render$)); const updatedCell$ = this._navigator.graphService.dataAdded$ .pipe( filter( (cellId: string) => { return this._cache.hasCell(cellId); }), mergeMap( (cellId: string): Observable<[Cell, LngLatAlt]> => { return this._cache.updateCell$(cellId).pipe( map((images: Image[]) => ({ id: cellId, images })), withLatestFrom( this._navigator.stateService.reference$ ) ); }), publish<[Cell, LngLatAlt]>(), refCount()); subs.push(updatedCell$ .subscribe( ([cell, reference]: [Cell, LngLatAlt]): void => { this._addSceneImages(cell, reference); })); subs.push(updatedCell$ .pipe( concatMap( ([cell]: [Cell, LngLatAlt]): Observable<[string, ClusterContract]> => { const cellId = cell.id; const cache = this._cache; let reconstructions$: Observable<ClusterContract>; if (cache.hasClusters(cellId)) { reconstructions$ = cache.updateClusters$(cellId); } else if (cache.isCachingClusters(cellId)) { reconstructions$ = this._cache.cacheClusters$(cellId).pipe( last(null, {}), switchMap( (): Observable<ClusterContract> => { return observableFrom( cache.updateClusters$(cellId)); })); } else { reconstructions$ = observableEmpty(); } return observableCombineLatest( observableOf(cellId), reconstructions$); }), withLatestFrom(this._navigator.stateService.reference$)) .subscribe( ([[cellId, reconstruction], reference]: [[string, ClusterContract], LngLatAlt]): void => { if (this._scene.hasCluster(reconstruction.id, cellId)) { return; } this._scene.addCluster( reconstruction, this._computeTranslation(reconstruction, reference), cellId); })); } protected _deactivate(): void { this._subscriptions.unsubscribe(); this._cache.uncache(); this._scene.deactivate(); this._navigator.cacheService.configure(); } protected _getDefaultConfiguration(): SpatialConfiguration { return { cameraSize: 0.1, cameraVisualizationMode: CameraVisualizationMode.Homogeneous, cellGridDepth: 1, originalPositionMode: OriginalPositionMode.Hidden, pointSize: 0.1, pointsVisible: true, pointVisualizationMode: PointVisualizationMode.Original, cellsVisible: false, }; } private _addSceneImages(cell: Cell, reference: LngLatAlt): void { const cellId = cell.id; const images = cell.images; for (const image of images) { if (this._scene.hasImage(image.id, cellId)) { continue; } this._scene.addImage( image, this._createTransform(image, reference), this._computeOriginalPosition(image, reference), cellId); } } private _cellsInFov( image: Image, bearing: number, fov: number) : string[] { const spatial = this._spatial; const geometry = this._navigator.api.data.geometry; const cell = geometry.lngLatToCellId(image.originalLngLat); const cells = [cell]; const threshold = fov / 2; const adjacent = geometry.getAdjacent(cell); for (const a of adjacent) { const vertices = geometry.getVertices(a); for (const vertex of vertices) { const [x, y] = geodeticToEnu( vertex.lng, vertex.lat, 0, image.lngLat.lng, image.lngLat.lat, 0); const azimuthal = Math.atan2(y, x); const vertexBearing = spatial.radToDeg( spatial.azimuthalToBearing(azimuthal)); if (Math.abs(vertexBearing - bearing) < threshold) { cells.push(a); } } } return cells; } private _computeOriginalPosition(image: Image, reference: LngLatAlt): number[] { return geodeticToEnu( image.originalLngLat.lng, image.originalLngLat.lat, image.originalAltitude != null ? image.originalAltitude : image.computedAltitude, reference.lng, reference.lat, reference.alt); } private _cellToTopocentric( cellId: string, reference: LngLatAlt): number[][] { const vertices = this._navigator.api.data.geometry .getVertices(cellId) .map( (vertex: LngLat): number[] => { return geodeticToEnu( vertex.lng, vertex.lat, -2, reference.lng, reference.lat, reference.alt); }); return vertices; } private _computeTranslation( reconstruction: ClusterContract, reference: LngLatAlt) : number[] { return geodeticToEnu( reconstruction.reference.lng, reconstruction.reference.lat, reconstruction.reference.alt, reference.lng, reference.lat, reference.alt); } private _createTransform(image: Image, reference: LngLatAlt): Transform { const translation = Geo.computeTranslation( { alt: image.computedAltitude, lat: image.lngLat.lat, lng: image.lngLat.lng }, image.rotation, reference); const transform = new Transform( image.exifOrientation, image.width, image.height, image.scale, image.rotation, translation, undefined, undefined, image.cameraParameters, <CameraType>image.cameraType); return transform; } }