UNPKG

mapillary-js

Version:

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

1,172 lines (1,034 loc) 48.9 kB
import { concat as observableConcat, of as observableOf, zip as observableZip, combineLatest as observableCombineLatest, empty as observableEmpty, Observable, Subscription, Subject, } from "rxjs"; import { publishReplay, pairwise, catchError, takeUntil, publish, skipWhile, scan, filter, debounceTime, startWith, map, switchMap, withLatestFrom, refCount, first, distinctUntilChanged, } from "rxjs/operators"; import * as THREE from "three"; import { ISliderNodes, ISliderCombination, IGLRendererOperation, PositionLookat, } from "./interfaces/interfaces"; import { Component, ComponentService, ISliderConfiguration, ISliderKeys, SliderDOMRenderer, SliderGLRenderer, SliderMode, } from "../../Component"; import { Spatial, Transform, ViewportCoords, } from "../../Geo"; import { Node } from "../../Graph"; import { ICurrentState, IFrame, State, } from "../../State"; import { Container, ImageSize, Navigator, } from "../../Viewer"; import { GLRenderStage, IGLRenderHash, ISize, IVNodeHash, RenderCamera, } from "../../Render"; import { IBoundingBox, ImageTileLoader, ImageTileStore, IRegionOfInterest, RegionOfInterestCalculator, TextureProvider, } from "../../Tiles"; import { Settings, Urls, } from "../../Utils"; /** * @class SliderComponent * * @classdesc Component for comparing pairs of images. Renders * a slider for adjusting the curtain of the first image. * * Deactivate the sequence, direction and image plane * components when activating the slider component to avoid * interfering UI elements. * * To retrive and use the slider component * * @example * ``` * var viewer = new Mapillary.Viewer( * "<element-id>", * "<client-id>", * "<my key>"); * * viewer.deactivateComponent("imagePlane"); * viewer.deactivateComponent("direction"); * viewer.deactivateComponent("sequence"); * * viewer.activateComponent("slider"); * * var sliderComponent = viewer.getComponent("slider"); * ``` */ export class SliderComponent extends Component<ISliderConfiguration> { public static componentName: string = "slider"; private _viewportCoords: ViewportCoords; private _domRenderer: SliderDOMRenderer; private _imageTileLoader: ImageTileLoader; private _roiCalculator: RegionOfInterestCalculator; private _spatial: Spatial; private _glRendererOperation$: Subject<IGLRendererOperation>; private _glRenderer$: Observable<SliderGLRenderer>; private _glRendererCreator$: Subject<void>; private _glRendererDisposer$: Subject<void>; private _setKeysSubscription: Subscription; private _modeSubcription: Subscription; private _stateSubscription: Subscription; private _glRenderSubscription: Subscription; private _domRenderSubscription: Subscription; private _moveSubscription: Subscription; private _updateCurtainSubscription: Subscription; private _waitSubscription: Subscription; private _textureProviderSubscription: Subscription; private _setTextureProviderSubscription: Subscription; private _setTileSizeSubscription: Subscription; private _abortTextureProviderSubscription: Subscription; private _setRegionOfInterestSubscription: Subscription; private _hasTextureSubscription: Subscription; private _updateBackgroundSubscription: Subscription; private _updateTextureImageSubscription: Subscription; private _textureProviderSubscriptionPrev: Subscription; private _setTextureProviderSubscriptionPrev: Subscription; private _setTileSizeSubscriptionPrev: Subscription; private _abortTextureProviderSubscriptionPrev: Subscription; private _setRegionOfInterestSubscriptionPrev: Subscription; private _hasTextureSubscriptionPrev: Subscription; private _updateBackgroundSubscriptionPrev: Subscription; private _updateTextureImageSubscriptionPrev: Subscription; /** @ignore */ constructor (name: string, container: Container, navigator: Navigator, viewportCoords?: ViewportCoords) { super(name, container, navigator); this._viewportCoords = !!viewportCoords ? viewportCoords : new ViewportCoords(); this._domRenderer = new SliderDOMRenderer(container); this._imageTileLoader = new ImageTileLoader(Urls.tileScheme, Urls.tileDomain, Urls.origin); this._roiCalculator = new RegionOfInterestCalculator(); this._spatial = new Spatial(); this._glRendererOperation$ = new Subject<IGLRendererOperation>(); this._glRendererCreator$ = new Subject<void>(); this._glRendererDisposer$ = new Subject<void>(); this._glRenderer$ = this._glRendererOperation$.pipe( scan( (glRenderer: SliderGLRenderer, operation: IGLRendererOperation): SliderGLRenderer => { return operation(glRenderer); }, null), filter( (glRenderer: SliderGLRenderer): boolean => { return glRenderer != null; }), distinctUntilChanged( undefined, (glRenderer: SliderGLRenderer): number => { return glRenderer.frameId; })); this._glRendererCreator$.pipe( map( (): IGLRendererOperation => { return (glRenderer: SliderGLRenderer): SliderGLRenderer => { if (glRenderer != null) { throw new Error("Multiple slider states can not be created at the same time"); } return new SliderGLRenderer(); }; })) .subscribe(this._glRendererOperation$); this._glRendererDisposer$.pipe( map( (): IGLRendererOperation => { return (glRenderer: SliderGLRenderer): SliderGLRenderer => { glRenderer.dispose(); return null; }; })) .subscribe(this._glRendererOperation$); } /** * Set the initial position. * * @description Configures the intial position of the slider. * The inital position value will be used when the component * is activated. * * @param {number} initialPosition - Initial slider position. */ public setInitialPosition(initialPosition: number): void { this.configure({ initialPosition: initialPosition }); } /** * Set the image keys. * * @description Configures the component to show the image * planes for the supplied image keys. * * @param {ISliderKeys} keys - Slider keys object specifying * the images to be shown in the foreground and the background. */ public setKeys(keys: ISliderKeys): void { this.configure({ keys: keys }); } /** * Set the slider mode. * * @description Configures the mode for transitions between * image pairs. * * @param {SliderMode} mode - Slider mode to be set. */ public setSliderMode(mode: SliderMode): void { this.configure({ mode: mode }); } /** * Set the value controlling if the slider is visible. * * @param {boolean} sliderVisible - Value indicating if * the slider should be visible or not. */ public setSliderVisible(sliderVisible: boolean): void { this.configure({ sliderVisible: sliderVisible }); } protected _activate(): void { this._modeSubcription = this._domRenderer.mode$ .subscribe( (mode: SliderMode): void => { this.setSliderMode(mode); }); this._glRenderSubscription = this._glRenderer$.pipe( map( (glRenderer: SliderGLRenderer): IGLRenderHash => { let renderHash: IGLRenderHash = { name: this._name, render: { frameId: glRenderer.frameId, needsRender: glRenderer.needsRender, render: glRenderer.render.bind(glRenderer), stage: GLRenderStage.Background, }, }; return renderHash; })) .subscribe(this._container.glRenderer.render$); const position$: Observable<number> = observableConcat( this.configuration$.pipe( map( (configuration: ISliderConfiguration): number => { return configuration.initialPosition != null ? configuration.initialPosition : 1; }), first()), this._domRenderer.position$); const mode$: Observable<SliderMode> = this.configuration$.pipe( map( (configuration: ISliderConfiguration): SliderMode => { return configuration.mode; }), distinctUntilChanged()); const motionless$: Observable<boolean> = this._navigator.stateService.currentState$.pipe( map( (frame: IFrame): boolean => { return frame.state.motionless; }), distinctUntilChanged()); const fullPano$: Observable<boolean> = this._navigator.stateService.currentState$.pipe( map( (frame: IFrame): boolean => { return frame.state.currentNode.fullPano; }), distinctUntilChanged()); const sliderVisible$: Observable<boolean> = observableCombineLatest( this._configuration$.pipe( map( (configuration: ISliderConfiguration): boolean => { return configuration.sliderVisible; })), this._navigator.stateService.currentState$.pipe( map( (frame: IFrame): boolean => { return !(frame.state.currentNode == null || frame.state.previousNode == null || (frame.state.currentNode.pano && !frame.state.currentNode.fullPano) || (frame.state.previousNode.pano && !frame.state.previousNode.fullPano) || (frame.state.currentNode.fullPano && !frame.state.previousNode.fullPano)); }), distinctUntilChanged())).pipe( map( ([sliderVisible, enabledState]: [boolean, boolean]): boolean => { return sliderVisible && enabledState; }), distinctUntilChanged()); this._waitSubscription = observableCombineLatest( mode$, motionless$, fullPano$, sliderVisible$).pipe( withLatestFrom(this._navigator.stateService.state$)) .subscribe( ([[mode, motionless, fullPano, sliderVisible], state]: [[SliderMode, boolean, boolean, boolean], State]): void => { const interactive: boolean = sliderVisible && (motionless || mode === SliderMode.Stationary || fullPano); if (interactive && state !== State.WaitingInteractively) { this._navigator.stateService.waitInteractively(); } else if (!interactive && state !== State.Waiting) { this._navigator.stateService.wait(); } }); this._moveSubscription = observableCombineLatest( position$, mode$, motionless$, fullPano$, sliderVisible$) .subscribe( ([position, mode, motionless, fullPano, sliderVisible]: [number, SliderMode, boolean, boolean, boolean]): void => { if (motionless || mode === SliderMode.Stationary || fullPano) { this._navigator.stateService.moveTo(1); } else { this._navigator.stateService.moveTo(position); } }); this._domRenderSubscription = observableCombineLatest( position$, mode$, motionless$, fullPano$, sliderVisible$, this._container.renderService.size$).pipe( map( ([position, mode, motionless, fullPano, sliderVisible, size]: [number, SliderMode, boolean, boolean, boolean, ISize]): IVNodeHash => { return { name: this._name, vnode: this._domRenderer.render(position, mode, motionless, fullPano, sliderVisible), }; })) .subscribe(this._container.domRenderer.render$); this._glRendererCreator$.next(null); this._updateCurtainSubscription = observableCombineLatest( position$, fullPano$, sliderVisible$, this._container.renderService.renderCamera$, this._navigator.stateService.currentTransform$).pipe( map( ([position, fullPano, visible, render, transform]: [number, boolean, boolean, RenderCamera, Transform]): number => { if (!fullPano) { return visible ? position : 1; } const basicMin: number[] = this._viewportCoords.viewportToBasic(-1.15, 0, transform, render.perspective); const basicMax: number[] = this._viewportCoords.viewportToBasic(1.15, 0, transform, render.perspective); const shiftedMax: number = basicMax[0] < basicMin[0] ? basicMax[0] + 1 : basicMax[0]; const basicPosition: number = basicMin[0] + position * (shiftedMax - basicMin[0]); return basicPosition > 1 ? basicPosition - 1 : basicPosition; }), map( (position: number): IGLRendererOperation => { return (glRenderer: SliderGLRenderer): SliderGLRenderer => { glRenderer.updateCurtain(position); return glRenderer; }; })) .subscribe(this._glRendererOperation$); this._stateSubscription = observableCombineLatest( this._navigator.stateService.currentState$, mode$).pipe( map( ([frame, mode]: [IFrame, SliderMode]): IGLRendererOperation => { return (glRenderer: SliderGLRenderer): SliderGLRenderer => { glRenderer.update(frame, mode); return glRenderer; }; })) .subscribe(this._glRendererOperation$); this._setKeysSubscription = this._configuration$.pipe( filter( (configuration: ISliderConfiguration): boolean => { return configuration.keys != null; }), switchMap( (configuration: ISliderConfiguration): Observable<ISliderCombination> => { return observableZip( observableZip( this._catchCacheNode$(configuration.keys.background), this._catchCacheNode$(configuration.keys.foreground)).pipe( map( (nodes: [Node, Node]): ISliderNodes => { return { background: nodes[0], foreground: nodes[1] }; })), this._navigator.stateService.currentState$.pipe(first())).pipe( map( (nf: [ISliderNodes, IFrame]): ISliderCombination => { return { nodes: nf[0], state: nf[1].state }; })); })) .subscribe( (co: ISliderCombination): void => { if (co.state.currentNode != null && co.state.previousNode != null && co.state.currentNode.key === co.nodes.foreground.key && co.state.previousNode.key === co.nodes.background.key) { return; } if (co.state.currentNode.key === co.nodes.background.key) { this._navigator.stateService.setNodes([co.nodes.foreground]); return; } if (co.state.currentNode.key === co.nodes.foreground.key && co.state.trajectory.length === 1) { this._navigator.stateService.prependNodes([co.nodes.background]); return; } this._navigator.stateService.setNodes([co.nodes.background]); this._navigator.stateService.setNodes([co.nodes.foreground]); }, (e: Error): void => { console.error(e); }); let previousNode$: Observable<Node> = this._navigator.stateService.currentState$.pipe( map( (frame: IFrame): Node => { return frame.state.previousNode; }), filter( (node: Node): boolean => { return node != null; }), distinctUntilChanged( undefined, (node: Node): string => { return node.key; })); const textureProvider$: Observable<TextureProvider> = this._navigator.stateService.currentState$.pipe( distinctUntilChanged( undefined, (frame: IFrame): string => { return frame.state.currentNode.key; }), withLatestFrom( this._container.glRenderer.webGLRenderer$, this._container.renderService.size$), map( ([frame, renderer, size]: [IFrame, THREE.WebGLRenderer, ISize]): TextureProvider => { const state: ICurrentState = frame.state; const viewportSize: number = Math.max(size.width, size.height); const currentNode: Node = state.currentNode; const currentTransform: Transform = state.currentTransform; const tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; return new TextureProvider( currentNode.key, currentTransform.basicWidth, currentTransform.basicHeight, tileSize, currentNode.image, this._imageTileLoader, new ImageTileStore(), renderer); }), publishReplay(1), refCount()); this._textureProviderSubscription = textureProvider$.subscribe(() => { /*noop*/ }); this._setTextureProviderSubscription = textureProvider$.pipe( map( (provider: TextureProvider): IGLRendererOperation => { return (renderer: SliderGLRenderer): SliderGLRenderer => { renderer.setTextureProvider(provider.key, provider); return renderer; }; })) .subscribe(this._glRendererOperation$); this._setTileSizeSubscription = this._container.renderService.size$.pipe( switchMap( (size: ISize): Observable<[TextureProvider, ISize]> => { return observableCombineLatest( textureProvider$, observableOf<ISize>(size)).pipe( first()); })) .subscribe( ([provider, size]: [TextureProvider, ISize]): void => { let viewportSize: number = Math.max(size.width, size.height); let tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; provider.setTileSize(tileSize); }); this._abortTextureProviderSubscription = textureProvider$.pipe( pairwise()) .subscribe( (pair: [TextureProvider, TextureProvider]): void => { let previous: TextureProvider = pair[0]; previous.abort(); }); let roiTrigger$: Observable<[RenderCamera, ISize, Transform]> = observableCombineLatest( this._container.renderService.renderCameraFrame$, this._container.renderService.size$.pipe(debounceTime(250))).pipe( map( ([camera, size]: [RenderCamera, ISize]): PositionLookat => { return [ camera.camera.position.clone(), camera.camera.lookat.clone(), camera.zoom.valueOf(), size.height.valueOf(), size.width.valueOf()]; }), pairwise(), skipWhile( (pls: [PositionLookat, PositionLookat]): boolean => { return pls[1][2] - pls[0][2] < 0 || pls[1][2] === 0; }), map( (pls: [PositionLookat, PositionLookat]): boolean => { let samePosition: boolean = pls[0][0].equals(pls[1][0]); let sameLookat: boolean = pls[0][1].equals(pls[1][1]); let sameZoom: boolean = pls[0][2] === pls[1][2]; let sameHeight: boolean = pls[0][3] === pls[1][3]; let sameWidth: boolean = pls[0][4] === pls[1][4]; return samePosition && sameLookat && sameZoom && sameHeight && sameWidth; }), distinctUntilChanged(), filter( (stalled: boolean): boolean => { return stalled; }), switchMap( (stalled: boolean): Observable<RenderCamera> => { return this._container.renderService.renderCameraFrame$.pipe( first()); }), withLatestFrom( this._container.renderService.size$, this._navigator.stateService.currentTransform$)); this._setRegionOfInterestSubscription = textureProvider$.pipe( switchMap( (provider: TextureProvider): Observable<[IRegionOfInterest, TextureProvider]> => { return roiTrigger$.pipe( map( ([camera, size, transform]: [RenderCamera, ISize, Transform]): [IRegionOfInterest, TextureProvider] => { return [ this._roiCalculator.computeRegionOfInterest(camera, size, transform), provider, ]; })); }), filter( (args: [IRegionOfInterest, TextureProvider]): boolean => { return !args[1].disposed; })) .subscribe( (args: [IRegionOfInterest, TextureProvider]): void => { let roi: IRegionOfInterest = args[0]; let provider: TextureProvider = args[1]; provider.setRegionOfInterest(roi); }); let hasTexture$: Observable<boolean> = textureProvider$.pipe( switchMap( (provider: TextureProvider): Observable<boolean> => { return provider.hasTexture$; }), startWith(false), publishReplay(1), refCount()); this._hasTextureSubscription = hasTexture$.subscribe(() => { /*noop*/ }); let nodeImage$: Observable<[HTMLImageElement, Node]> = this._navigator.stateService.currentState$.pipe( filter( (frame: IFrame): boolean => { return frame.state.nodesAhead === 0; }), map( (frame: IFrame): Node => { return frame.state.currentNode; }), distinctUntilChanged( undefined, (node: Node): string => { return node.key; }), debounceTime(1000), withLatestFrom(hasTexture$), filter( (args: [Node, boolean]): boolean => { return !args[1]; }), map( (args: [Node, boolean]): Node => { return args[0]; }), filter( (node: Node): boolean => { return node.pano ? Settings.maxImageSize > Settings.basePanoramaSize : Settings.maxImageSize > Settings.baseImageSize; }), switchMap( (node: Node): Observable<[HTMLImageElement, Node]> => { let baseImageSize: ImageSize = node.pano ? Settings.basePanoramaSize : Settings.baseImageSize; if (Math.max(node.image.width, node.image.height) > baseImageSize) { return observableEmpty(); } let image$: Observable<[HTMLImageElement, Node]> = node .cacheImage$(Settings.maxImageSize).pipe( map( (n: Node): [HTMLImageElement, Node] => { return [n.image, n]; })); return image$.pipe( takeUntil( hasTexture$.pipe( filter( (hasTexture: boolean): boolean => { return hasTexture; }))), catchError( (error: Error, caught: Observable<[HTMLImageElement, Node]>): Observable<[HTMLImageElement, Node]> => { console.error(`Failed to fetch high res image (${node.key})`, error); return observableEmpty(); })); })).pipe( publish(), refCount()); this._updateBackgroundSubscription = nodeImage$.pipe( withLatestFrom(textureProvider$)) .subscribe( (args: [[HTMLImageElement, Node], TextureProvider]): void => { if (args[0][1].key !== args[1].key || args[1].disposed) { return; } args[1].updateBackground(args[0][0]); }); this._updateTextureImageSubscription = nodeImage$.pipe( map( (imn: [HTMLImageElement, Node]): IGLRendererOperation => { return (renderer: SliderGLRenderer): SliderGLRenderer => { renderer.updateTextureImage(imn[0], imn[1]); return renderer; }; })) .subscribe(this._glRendererOperation$); const textureProviderPrev$: Observable<TextureProvider> = this._navigator.stateService.currentState$.pipe( filter( (frame: IFrame): boolean => { return !!frame.state.previousNode; }), distinctUntilChanged( undefined, (frame: IFrame): string => { return frame.state.previousNode.key; }), withLatestFrom( this._container.glRenderer.webGLRenderer$, this._container.renderService.size$), map( ([frame, renderer, size]: [IFrame, THREE.WebGLRenderer, ISize]): TextureProvider => { const state: ICurrentState = frame.state; const viewportSize: number = Math.max(size.width, size.height); const previousNode: Node = state.previousNode; const previousTransform: Transform = state.previousTransform; const tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; return new TextureProvider( previousNode.key, previousTransform.basicWidth, previousTransform.basicHeight, tileSize, previousNode.image, this._imageTileLoader, new ImageTileStore(), renderer); }), publishReplay(1), refCount()); this._textureProviderSubscriptionPrev = textureProviderPrev$.subscribe(() => { /*noop*/ }); this._setTextureProviderSubscriptionPrev = textureProviderPrev$.pipe( map( (provider: TextureProvider): IGLRendererOperation => { return (renderer: SliderGLRenderer): SliderGLRenderer => { renderer.setTextureProviderPrev(provider.key, provider); return renderer; }; })) .subscribe(this._glRendererOperation$); this._setTileSizeSubscriptionPrev = this._container.renderService.size$.pipe( switchMap( (size: ISize): Observable<[TextureProvider, ISize]> => { return observableCombineLatest( textureProviderPrev$, observableOf<ISize>(size)).pipe( first()); })) .subscribe( ([provider, size]: [TextureProvider, ISize]): void => { let viewportSize: number = Math.max(size.width, size.height); let tileSize: number = viewportSize > 2048 ? 2048 : viewportSize > 1024 ? 1024 : 512; provider.setTileSize(tileSize); }); this._abortTextureProviderSubscriptionPrev = textureProviderPrev$.pipe( pairwise()) .subscribe( (pair: [TextureProvider, TextureProvider]): void => { let previous: TextureProvider = pair[0]; previous.abort(); }); let roiTriggerPrev$: Observable<[RenderCamera, ISize, Transform]> = observableCombineLatest( this._container.renderService.renderCameraFrame$, this._container.renderService.size$.pipe(debounceTime(250))).pipe( map( ([camera, size]: [RenderCamera, ISize]): PositionLookat => { return [ camera.camera.position.clone(), camera.camera.lookat.clone(), camera.zoom.valueOf(), size.height.valueOf(), size.width.valueOf()]; }), pairwise(), skipWhile( (pls: [PositionLookat, PositionLookat]): boolean => { return pls[1][2] - pls[0][2] < 0 || pls[1][2] === 0; }), map( (pls: [PositionLookat, PositionLookat]): boolean => { let samePosition: boolean = pls[0][0].equals(pls[1][0]); let sameLookat: boolean = pls[0][1].equals(pls[1][1]); let sameZoom: boolean = pls[0][2] === pls[1][2]; let sameHeight: boolean = pls[0][3] === pls[1][3]; let sameWidth: boolean = pls[0][4] === pls[1][4]; return samePosition && sameLookat && sameZoom && sameHeight && sameWidth; }), distinctUntilChanged(), filter( (stalled: boolean): boolean => { return stalled; }), switchMap( (stalled: boolean): Observable<RenderCamera> => { return this._container.renderService.renderCameraFrame$.pipe( first()); }), withLatestFrom( this._container.renderService.size$, this._navigator.stateService.currentTransform$)); this._setRegionOfInterestSubscriptionPrev = textureProviderPrev$.pipe( switchMap( (provider: TextureProvider): Observable<[IRegionOfInterest, TextureProvider]> => { return roiTriggerPrev$.pipe( map( ([camera, size, transform]: [RenderCamera, ISize, Transform]): [IRegionOfInterest, TextureProvider] => { return [ this._roiCalculator.computeRegionOfInterest(camera, size, transform), provider, ]; })); }), filter( (args: [IRegionOfInterest, TextureProvider]): boolean => { return !args[1].disposed; }), withLatestFrom(this._navigator.stateService.currentState$)) .subscribe( ([[roi, provider], frame]: [[IRegionOfInterest, TextureProvider], IFrame]): void => { let shiftedRoi: IRegionOfInterest = null; if (frame.state.previousNode.fullPano) { if (frame.state.currentNode.fullPano) { const currentViewingDirection: THREE.Vector3 = this._spatial.viewingDirection(frame.state.currentNode.rotation); const previousViewingDirection: THREE.Vector3 = this._spatial.viewingDirection(frame.state.previousNode.rotation); const directionDiff: number = this._spatial.angleBetweenVector2( currentViewingDirection.x, currentViewingDirection.y, previousViewingDirection.x, previousViewingDirection.y); const shift: number = directionDiff / (2 * Math.PI); const bbox: IBoundingBox = { maxX: this._spatial.wrap(roi.bbox.maxX + shift, 0, 1), maxY: roi.bbox.maxY, minX: this._spatial.wrap(roi.bbox.minX + shift, 0, 1), minY: roi.bbox.minY, }; shiftedRoi = { bbox: bbox, pixelHeight: roi.pixelHeight, pixelWidth: roi.pixelWidth, }; } else { const currentViewingDirection: THREE.Vector3 = this._spatial.viewingDirection(frame.state.currentNode.rotation); const previousViewingDirection: THREE.Vector3 = this._spatial.viewingDirection(frame.state.previousNode.rotation); const directionDiff: number = this._spatial.angleBetweenVector2( currentViewingDirection.x, currentViewingDirection.y, previousViewingDirection.x, previousViewingDirection.y); const shiftX: number = directionDiff / (2 * Math.PI); const a1: number = this._spatial.angleToPlane(currentViewingDirection.toArray(), [0, 0, 1]); const a2: number = this._spatial.angleToPlane(previousViewingDirection.toArray(), [0, 0, 1]); const shiftY: number = (a2 - a1) / (2 * Math.PI); const currentTransform: Transform = frame.state.currentTransform; const size: number = Math.max(currentTransform.basicWidth, currentTransform.basicHeight); const hFov: number = size > 0 ? 2 * Math.atan(0.5 * currentTransform.basicWidth / (size * currentTransform.focal)) : Math.PI / 3; const vFov: number = size > 0 ? 2 * Math.atan(0.5 * currentTransform.basicHeight / (size * currentTransform.focal)) : Math.PI / 3; const spanningWidth: number = hFov / (2 * Math.PI); const spanningHeight: number = vFov / Math.PI; const basicWidth: number = (roi.bbox.maxX - roi.bbox.minX) * spanningWidth; const basicHeight: number = (roi.bbox.maxY - roi.bbox.minY) * spanningHeight; const pixelWidth: number = roi.pixelWidth * spanningWidth; const pixelHeight: number = roi.pixelHeight * spanningHeight; const zoomShiftX: number = (roi.bbox.minX + roi.bbox.maxX) / 2 - 0.5; const zoomShiftY: number = (roi.bbox.minY + roi.bbox.maxY) / 2 - 0.5; const minX: number = 0.5 + shiftX + spanningWidth * zoomShiftX - basicWidth / 2; const maxX: number = 0.5 + shiftX + spanningWidth * zoomShiftX + basicWidth / 2; const minY: number = 0.5 + shiftY + spanningHeight * zoomShiftY - basicHeight / 2; const maxY: number = 0.5 + shiftY + spanningHeight * zoomShiftY + basicHeight / 2; const bbox: IBoundingBox = { maxX: this._spatial.wrap(maxX, 0, 1), maxY: maxY, minX: this._spatial.wrap(minX, 0, 1), minY: minY, }; shiftedRoi = { bbox: bbox, pixelHeight: pixelHeight, pixelWidth: pixelWidth, }; } } else { const currentBasicAspect: number = frame.state.currentTransform.basicAspect; const previousBasicAspect: number = frame.state.previousTransform.basicAspect; const [[cornerMinX, cornerMinY], [cornerMaxX, cornerMaxY]]: number[][] = this._getBasicCorners(currentBasicAspect, previousBasicAspect); const basicWidth: number = cornerMaxX - cornerMinX; const basicHeight: number = cornerMaxY - cornerMinY; const pixelWidth: number = roi.pixelWidth / basicWidth; const pixelHeight: number = roi.pixelHeight / basicHeight; const minX: number = (basicWidth - 1) / (2 * basicWidth) + roi.bbox.minX / basicWidth; const maxX: number = (basicWidth - 1) / (2 * basicWidth) + roi.bbox.maxX / basicWidth; const minY: number = (basicHeight - 1) / (2 * basicHeight) + roi.bbox.minY / basicHeight; const maxY: number = (basicHeight - 1) / (2 * basicHeight) + roi.bbox.maxY / basicHeight; const bbox: IBoundingBox = { maxX: maxX, maxY: maxY, minX: minX, minY: minY, }; this._clipBoundingBox(bbox); shiftedRoi = { bbox: bbox, pixelHeight: pixelHeight, pixelWidth: pixelWidth, }; } provider.setRegionOfInterest(shiftedRoi); }); let hasTexturePrev$: Observable<boolean> = textureProviderPrev$.pipe( switchMap( (provider: TextureProvider): Observable<boolean> => { return provider.hasTexture$; }), startWith(false), publishReplay(1), refCount()); this._hasTextureSubscriptionPrev = hasTexturePrev$.subscribe(() => { /*noop*/ }); let nodeImagePrev$: Observable<[HTMLImageElement, Node]> = this._navigator.stateService.currentState$.pipe( filter( (frame: IFrame): boolean => { return frame.state.nodesAhead === 0 && !!frame.state.previousNode; }), map( (frame: IFrame): Node => { return frame.state.previousNode; }), distinctUntilChanged( undefined, (node: Node): string => { return node.key; }), debounceTime(1000), withLatestFrom(hasTexturePrev$), filter( (args: [Node, boolean]): boolean => { return !args[1]; }), map( (args: [Node, boolean]): Node => { return args[0]; }), filter( (node: Node): boolean => { return node.pano ? Settings.maxImageSize > Settings.basePanoramaSize : Settings.maxImageSize > Settings.baseImageSize; }), switchMap( (node: Node): Observable<[HTMLImageElement, Node]> => { let baseImageSize: ImageSize = node.pano ? Settings.basePanoramaSize : Settings.baseImageSize; if (Math.max(node.image.width, node.image.height) > baseImageSize) { return observableEmpty(); } let image$: Observable<[HTMLImageElement, Node]> = node .cacheImage$(Settings.maxImageSize).pipe( map( (n: Node): [HTMLImageElement, Node] => { return [n.image, n]; })); return image$.pipe( takeUntil( hasTexturePrev$.pipe( filter( (hasTexture: boolean): boolean => { return hasTexture; }))), catchError( (error: Error, caught: Observable<[HTMLImageElement, Node]>): Observable<[HTMLImageElement, Node]> => { console.error(`Failed to fetch high res image (${node.key})`, error); return observableEmpty(); })); })).pipe( publish(), refCount()); this._updateBackgroundSubscriptionPrev = nodeImagePrev$.pipe( withLatestFrom(textureProviderPrev$)) .subscribe( (args: [[HTMLImageElement, Node], TextureProvider]): void => { if (args[0][1].key !== args[1].key || args[1].disposed) { return; } args[1].updateBackground(args[0][0]); }); this._updateTextureImageSubscriptionPrev = nodeImagePrev$.pipe( map( (imn: [HTMLImageElement, Node]): IGLRendererOperation => { return (renderer: SliderGLRenderer): SliderGLRenderer => { renderer.updateTextureImage(imn[0], imn[1]); return renderer; }; })) .subscribe(this._glRendererOperation$); } protected _deactivate(): void { this._waitSubscription.unsubscribe(); this._navigator.stateService.state$.pipe( first()) .subscribe( (state: State): void => { if (state !== State.Traversing) { this._navigator.stateService.traverse(); } }); this._glRendererDisposer$.next(null); this._domRenderer.deactivate(); this._modeSubcription.unsubscribe(); this._setKeysSubscription.unsubscribe(); this._stateSubscription.unsubscribe(); this._glRenderSubscription.unsubscribe(); this._domRenderSubscription.unsubscribe(); this._moveSubscription.unsubscribe(); this._updateCurtainSubscription.unsubscribe(); this._textureProviderSubscription.unsubscribe(); this._setTextureProviderSubscription.unsubscribe(); this._setTileSizeSubscription.unsubscribe(); this._abortTextureProviderSubscription.unsubscribe(); this._setRegionOfInterestSubscription.unsubscribe(); this._hasTextureSubscription.unsubscribe(); this._updateBackgroundSubscription.unsubscribe(); this._updateTextureImageSubscription.unsubscribe(); this._textureProviderSubscriptionPrev.unsubscribe(); this._setTextureProviderSubscriptionPrev.unsubscribe(); this._setTileSizeSubscriptionPrev.unsubscribe(); this._abortTextureProviderSubscriptionPrev.unsubscribe(); this._setRegionOfInterestSubscriptionPrev.unsubscribe(); this._hasTextureSubscriptionPrev.unsubscribe(); this._updateBackgroundSubscriptionPrev.unsubscribe(); this._updateTextureImageSubscriptionPrev.unsubscribe(); this.configure({ keys: null }); } protected _getDefaultConfiguration(): ISliderConfiguration { return { initialPosition: 1, mode: SliderMode.Motion, sliderVisible: true, }; } private _catchCacheNode$(key: string): Observable<Node> { return this._navigator.graphService.cacheNode$(key).pipe( catchError( (error: Error, caught: Observable<Node>): Observable<Node> => { console.error(`Failed to cache slider node (${key})`, error); return observableEmpty(); })); } private _getBasicCorners(currentAspect: number, previousAspect: number): number[][] { let offsetX: number; let offsetY: number; if (currentAspect > previousAspect) { offsetX = 0.5; offsetY = 0.5 * currentAspect / previousAspect; } else { offsetX = 0.5 * previousAspect / currentAspect; offsetY = 0.5; } return [[0.5 - offsetX, 0.5 - offsetY], [0.5 + offsetX, 0.5 + offsetY]]; } private _clipBoundingBox(bbox: IBoundingBox): void { bbox.minX = Math.max(0, Math.min(1, bbox.minX)); bbox.maxX = Math.max(0, Math.min(1, bbox.maxX)); bbox.minY = Math.max(0, Math.min(1, bbox.minY)); bbox.maxY = Math.max(0, Math.min(1, bbox.maxY)); } } ComponentService.register(SliderComponent); export default SliderComponent;