UNPKG

mapillary-js

Version:

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

378 lines (335 loc) 14.1 kB
import { merge as observableMerge, empty as observableEmpty, combineLatest as observableCombineLatest, Observable, Subject, Subscription, } from "rxjs"; import { distinctUntilChanged, withLatestFrom, switchMap, auditTime, first, map, } from "rxjs/operators"; import {ILatLon} from "../API"; import { ILatLonAlt, Transform, } from "../Geo"; import { IEdgeStatus, Node, } from "../Graph"; import {RenderCamera} from "../Render"; import {EventEmitter} from "../Utils"; import { Container, IUnprojection, IViewerMouseEvent, Navigator, Projection, Viewer, } from "../Viewer"; export class Observer { private _started: boolean; private _navigable$: Subject<boolean>; private _bearingSubscription: Subscription; private _currentNodeSubscription: Subscription; private _fovSubscription: Subscription; private _moveSubscription: Subscription; private _positionSubscription: Subscription; private _povSubscription: Subscription; private _sequenceEdgesSubscription: Subscription; private _spatialEdgesSubscription: Subscription; private _viewerMouseEventSubscription: Subscription; private _container: Container; private _eventEmitter: EventEmitter; private _navigator: Navigator; private _projection: Projection; constructor(eventEmitter: EventEmitter, navigator: Navigator, container: Container) { this._container = container; this._eventEmitter = eventEmitter; this._navigator = navigator; this._projection = new Projection(); this._started = false; this._navigable$ = new Subject<boolean>(); // navigable and loading should always emit, also when cover is activated. this._navigable$ .subscribe( (navigable: boolean): void => { this._eventEmitter.fire(Viewer.navigablechanged, navigable); }); this._navigator.loadingService.loading$ .subscribe( (loading: boolean): void => { this._eventEmitter.fire(Viewer.loadingchanged, loading); }); } public get started(): boolean { return this._started; } public get navigable$(): Subject<boolean> { return this._navigable$; } public get projection(): Projection { return this._projection; } public project$(latLon: ILatLon): Observable<number[]> { return observableCombineLatest( this._container.renderService.renderCamera$, this._navigator.stateService.currentNode$, this._navigator.stateService.reference$).pipe( first(), map( ([render, node, reference]: [RenderCamera, Node, ILatLonAlt]): number[] => { if (this._projection.distanceBetweenLatLons(latLon, node.latLon) > 1000) { return null; } const canvasPoint: number[] = this._projection.latLonToCanvas( latLon, this._container.element, render, reference); return !!canvasPoint ? [Math.round(canvasPoint[0]), Math.round(canvasPoint[1])] : null; })); } public projectBasic$(basicPoint: number[]): Observable<number[]> { return observableCombineLatest( this._container.renderService.renderCamera$, this._navigator.stateService.currentTransform$).pipe( first(), map( ([render, transform]: [RenderCamera, Transform]): number[] => { const canvasPoint: number[] = this._projection.basicToCanvas( basicPoint, this._container.element, render, transform); return !!canvasPoint ? [Math.round(canvasPoint[0]), Math.round(canvasPoint[1])] : null; })); } public startEmit(): void { if (this._started) { return; } this._started = true; this._currentNodeSubscription = this._navigator.stateService.currentNodeExternal$ .subscribe((node: Node): void => { this._eventEmitter.fire(Viewer.nodechanged, node); }); this._sequenceEdgesSubscription = this._navigator.stateService.currentNodeExternal$.pipe( switchMap( (node: Node): Observable<IEdgeStatus> => { return node.sequenceEdges$; })) .subscribe( (status: IEdgeStatus): void => { this._eventEmitter.fire(Viewer.sequenceedgeschanged, status); }); this._spatialEdgesSubscription = this._navigator.stateService.currentNodeExternal$.pipe( switchMap( (node: Node): Observable<IEdgeStatus> => { return node.spatialEdges$; })) .subscribe( (status: IEdgeStatus): void => { this._eventEmitter.fire(Viewer.spatialedgeschanged, status); }); this._moveSubscription = observableCombineLatest( this._navigator.stateService.inMotion$, this._container.mouseService.active$, this._container.touchService.active$).pipe( map( (values: boolean[]): boolean => { return values[0] || values[1] || values[2]; }), distinctUntilChanged()) .subscribe( (started: boolean) => { if (started) { this._eventEmitter.fire(Viewer.movestart, null); } else { this._eventEmitter.fire(Viewer.moveend, null); } }); this._bearingSubscription = this._container.renderService.bearing$.pipe( auditTime(100), distinctUntilChanged( (b1: number, b2: number): boolean => { return Math.abs(b2 - b1) < 1; })) .subscribe( (bearing): void => { this._eventEmitter.fire(Viewer.bearingchanged, bearing); }); const mouseMove$: Observable<MouseEvent> = this._container.mouseService.active$.pipe( switchMap( (active: boolean): Observable<MouseEvent> => { return active ? observableEmpty() : this._container.mouseService.mouseMove$; })); this._viewerMouseEventSubscription = observableMerge( this._mapMouseEvent$(Viewer.click, this._container.mouseService.staticClick$), this._mapMouseEvent$(Viewer.contextmenu, this._container.mouseService.contextMenu$), this._mapMouseEvent$(Viewer.dblclick, this._container.mouseService.dblClick$), this._mapMouseEvent$(Viewer.mousedown, this._container.mouseService.mouseDown$), this._mapMouseEvent$(Viewer.mousemove, mouseMove$), this._mapMouseEvent$(Viewer.mouseout, this._container.mouseService.mouseOut$), this._mapMouseEvent$(Viewer.mouseover, this._container.mouseService.mouseOver$), this._mapMouseEvent$(Viewer.mouseup, this._container.mouseService.mouseUp$)).pipe( withLatestFrom( this._container.renderService.renderCamera$, this._navigator.stateService.reference$, this._navigator.stateService.currentTransform$), map( ([[type, event], render, reference, transform]: [[string, MouseEvent], RenderCamera, ILatLonAlt, Transform]): IViewerMouseEvent => { const unprojection: IUnprojection = this._projection.eventToUnprojection( event, this._container.element, render, reference, transform); return { basicPoint: unprojection.basicPoint, latLon: unprojection.latLon, originalEvent: event, pixelPoint: unprojection.pixelPoint, target: <Viewer>this._eventEmitter, type: type, }; })) .subscribe( (event: IViewerMouseEvent): void => { this._eventEmitter.fire(event.type, event); }); this._positionSubscription = this._container.renderService.renderCamera$.pipe( distinctUntilChanged( ([x1, y1], [x2, y2]): boolean => { return this._closeTo(x1, x2, 1e-2) && this._closeTo(y1, y2, 1e-2); }, (rc: RenderCamera): number[] => { return rc.camera.position.toArray(); })) .subscribe( (): void => { this._eventEmitter.fire( Viewer.positionchanged, { target: this._eventEmitter, type: Viewer.positionchanged, }); }); this._povSubscription = this._container.renderService.renderCamera$.pipe( distinctUntilChanged( ([phi1, theta1], [phi2, theta2]): boolean => { return this._closeTo(phi1, phi2, 1e-3) && this._closeTo(theta1, theta2, 1e-3); }, (rc: RenderCamera): [number, number] => { return [rc.rotation.phi, rc.rotation.theta]; })) .subscribe( (): void => { this._eventEmitter.fire( Viewer.povchanged, { target: this._eventEmitter, type: Viewer.povchanged, }); }); this._fovSubscription = this._container.renderService.renderCamera$.pipe( distinctUntilChanged( (fov1, fov2): boolean => { return this._closeTo(fov1, fov2, 1e-2); }, (rc: RenderCamera): number => { return rc.perspective.fov; })) .subscribe( (): void => { this._eventEmitter.fire( Viewer.fovchanged, { target: this._eventEmitter, type: Viewer.fovchanged, }); }); } public stopEmit(): void { if (!this.started) { return; } this._started = false; this._bearingSubscription.unsubscribe(); this._currentNodeSubscription.unsubscribe(); this._fovSubscription.unsubscribe(); this._moveSubscription.unsubscribe(); this._positionSubscription.unsubscribe(); this._povSubscription.unsubscribe(); this._sequenceEdgesSubscription.unsubscribe(); this._spatialEdgesSubscription.unsubscribe(); this._viewerMouseEventSubscription.unsubscribe(); this._bearingSubscription = null; this._currentNodeSubscription = null; this._fovSubscription = null; this._moveSubscription = null; this._positionSubscription = null; this._povSubscription = null; this._sequenceEdgesSubscription = null; this._spatialEdgesSubscription = null; this._viewerMouseEventSubscription = null; } public unproject$(canvasPoint: number[]): Observable<ILatLon> { return observableCombineLatest( this._container.renderService.renderCamera$, this._navigator.stateService.reference$, this._navigator.stateService.currentTransform$).pipe( first(), map( ([render, reference, transform]: [RenderCamera, ILatLonAlt, Transform]): ILatLon => { const unprojection: IUnprojection = this._projection.canvasToUnprojection( canvasPoint, this._container.element, render, reference, transform); return unprojection.latLon; })); } public unprojectBasic$(canvasPoint: number[]): Observable<number[]> { return observableCombineLatest( this._container.renderService.renderCamera$, this._navigator.stateService.currentTransform$).pipe( first(), map( ([render, transform]: [RenderCamera, Transform]): number[] => { return this._projection.canvasToBasic( canvasPoint, this._container.element, render, transform); })); } private _closeTo(v1: number, v2: number, absoluteTolerance: number): boolean { return Math.abs(v1 - v2) <= absoluteTolerance; } private _mapMouseEvent$(type: string, mouseEvent$: Observable<MouseEvent>): Observable<[string, MouseEvent]> { return mouseEvent$.pipe(map( (event: MouseEvent): [string, MouseEvent] => { return [type, event]; })); } } export default Observer;