UNPKG

mapillary-js

Version:

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

562 lines (472 loc) 18.8 kB
import { BehaviorSubject, Observable, Subject, } from "rxjs"; import { first, tap, filter, withLatestFrom, startWith, pairwise, distinctUntilChanged, publishReplay, refCount, bufferCount, share, switchMap, map, scan, } from "rxjs/operators"; import {ILatLon} from "../API"; import {Node} from "../Graph"; import { Camera, ILatLonAlt, Transform, } from "../Geo"; import { FrameGenerator, IStateContext, IFrame, IRotation, StateContext, State, TransitionMode, } from "../State"; interface IContextOperation { (context: IStateContext): IStateContext; } export class StateService { private _start$: Subject<void>; private _frame$: Subject<number>; private _contextOperation$: BehaviorSubject<IContextOperation>; private _context$: Observable<IStateContext>; private _fps$: Observable<number>; private _state$: Observable<State>; private _currentState$: Observable<IFrame>; private _lastState$: Observable<IFrame>; private _currentNode$: Observable<Node>; private _currentNodeExternal$: Observable<Node>; private _currentCamera$: Observable<Camera>; private _currentKey$: BehaviorSubject<string>; private _currentTransform$: Observable<Transform>; private _reference$: Observable<ILatLonAlt>; private _inMotionOperation$: Subject<boolean>; private _inMotion$: Observable<boolean>; private _inTranslationOperation$: Subject<boolean>; private _inTranslation$: Observable<boolean>; private _appendNode$: Subject<Node> = new Subject<Node>(); private _frameGenerator: FrameGenerator; private _frameId: number; private _fpsSampleRate: number; constructor(transitionMode?: TransitionMode) { this._start$ = new Subject<void>(); this._frame$ = new Subject<number>(); this._fpsSampleRate = 30; this._contextOperation$ = new BehaviorSubject<IContextOperation>( (context: IStateContext): IStateContext => { return context; }); this._context$ = this._contextOperation$.pipe( scan( (context: IStateContext, operation: IContextOperation): IStateContext => { return operation(context); }, new StateContext(transitionMode)), publishReplay(1), refCount()); this._state$ = this._context$.pipe( map( (context: IStateContext): State => { return context.state; }), distinctUntilChanged(), publishReplay(1), refCount()); this._fps$ = this._start$.pipe( switchMap( (): Observable<number> => { return this._frame$.pipe( bufferCount(1, this._fpsSampleRate), map( (frameIds: number[]): number => { return new Date().getTime(); }), pairwise(), map( (times: [number, number]): number => { return Math.max(20, 1000 * this._fpsSampleRate / (times[1] - times[0])); }), startWith(60)); }), share()); this._currentState$ = this._frame$.pipe( withLatestFrom( this._fps$, this._context$, (frameId: number, fps: number, context: IStateContext): [number, number, IStateContext] => { return [frameId, fps, context]; }), filter( (fc: [number, number, IStateContext]): boolean => { return fc[2].currentNode != null; }), tap( (fc: [number, number, IStateContext]): void => { fc[2].update(fc[1]); }), map( (fc: [number, number, IStateContext]): IFrame => { return { fps: fc[1], id: fc[0], state: fc[2] }; }), share()); this._lastState$ = this._currentState$.pipe( publishReplay(1), refCount()); let nodeChanged$: Observable<IFrame> = this._currentState$.pipe( distinctUntilChanged( undefined, (f: IFrame): string => { return f.state.currentNode.key; }), publishReplay(1), refCount()); let nodeChangedSubject$: Subject<IFrame> = new Subject<IFrame>(); nodeChanged$ .subscribe(nodeChangedSubject$); this._currentKey$ = new BehaviorSubject<string>(null); nodeChangedSubject$.pipe( map( (f: IFrame): string => { return f.state.currentNode.key; })) .subscribe(this._currentKey$); this._currentNode$ = nodeChangedSubject$.pipe( map( (f: IFrame): Node => { return f.state.currentNode; }), publishReplay(1), refCount()); this._currentCamera$ = nodeChangedSubject$.pipe( map( (f: IFrame): Camera => { return f.state.currentCamera; }), publishReplay(1), refCount()); this._currentTransform$ = nodeChangedSubject$.pipe( map( (f: IFrame): Transform => { return f.state.currentTransform; }), publishReplay(1), refCount()); this._reference$ = nodeChangedSubject$.pipe( map( (f: IFrame): ILatLonAlt => { return f.state.reference; }), distinctUntilChanged( (r1: ILatLon, r2: ILatLon): boolean => { return r1.lat === r2.lat && r1.lon === r2.lon; }, (reference: ILatLonAlt): ILatLon => { return { lat: reference.lat, lon: reference.lon }; }), publishReplay(1), refCount()); this._currentNodeExternal$ = nodeChanged$.pipe( map( (f: IFrame): Node => { return f.state.currentNode; }), publishReplay(1), refCount()); this._appendNode$.pipe( map( (node: Node) => { return (context: IStateContext): IStateContext => { context.append([node]); return context; }; })) .subscribe(this._contextOperation$); this._inMotionOperation$ = new Subject<boolean>(); nodeChanged$.pipe( map( (frame: IFrame): boolean => { return true; })) .subscribe(this._inMotionOperation$); this._inMotionOperation$.pipe( distinctUntilChanged(), filter( (moving: boolean): boolean => { return moving; }), switchMap( (moving: boolean): Observable<boolean> => { return this._currentState$.pipe( filter( (frame: IFrame): boolean => { return frame.state.nodesAhead === 0; }), map( (frame: IFrame): [Camera, number] => { return [frame.state.camera.clone(), frame.state.zoom]; }), pairwise(), map( (pair: [[Camera, number], [Camera, number]]): boolean => { let c1: Camera = pair[0][0]; let c2: Camera = pair[1][0]; let z1: number = pair[0][1]; let z2: number = pair[1][1]; return c1.diff(c2) > 1e-5 || Math.abs(z1 - z2) > 1e-5; }), first( (changed: boolean): boolean => { return !changed; })); })) .subscribe(this._inMotionOperation$); this._inMotion$ = this._inMotionOperation$.pipe( distinctUntilChanged(), publishReplay(1), refCount()); this._inTranslationOperation$ = new Subject<boolean>(); nodeChanged$.pipe( map( (frame: IFrame): boolean => { return true; })) .subscribe(this._inTranslationOperation$); this._inTranslationOperation$.pipe( distinctUntilChanged(), filter( (inTranslation: boolean): boolean => { return inTranslation; }), switchMap( (inTranslation: boolean): Observable<boolean> => { return this._currentState$.pipe( filter( (frame: IFrame): boolean => { return frame.state.nodesAhead === 0; }), map( (frame: IFrame): THREE.Vector3 => { return frame.state.camera.position.clone(); }), pairwise(), map( (pair: [THREE.Vector3, THREE.Vector3]): boolean => { return pair[0].distanceToSquared(pair[1]) !== 0; }), first( (changed: boolean): boolean => { return !changed; })); })) .subscribe(this._inTranslationOperation$); this._inTranslation$ = this._inTranslationOperation$.pipe( distinctUntilChanged(), publishReplay(1), refCount()); this._state$.subscribe(() => { /*noop*/ }); this._currentNode$.subscribe(() => { /*noop*/ }); this._currentCamera$.subscribe(() => { /*noop*/ }); this._currentTransform$.subscribe(() => { /*noop*/ }); this._reference$.subscribe(() => { /*noop*/ }); this._currentNodeExternal$.subscribe(() => { /*noop*/ }); this._lastState$.subscribe(() => { /*noop*/ }); this._inMotion$.subscribe(() => { /*noop*/ }); this._inTranslation$.subscribe(() => { /*noop*/ }); this._frameId = null; this._frameGenerator = new FrameGenerator(window); } public get currentState$(): Observable<IFrame> { return this._currentState$; } public get currentNode$(): Observable<Node> { return this._currentNode$; } public get currentKey$(): Observable<string> { return this._currentKey$; } public get currentNodeExternal$(): Observable<Node> { return this._currentNodeExternal$; } public get currentCamera$(): Observable<Camera> { return this._currentCamera$; } public get currentTransform$(): Observable<Transform> { return this._currentTransform$; } public get state$(): Observable<State> { return this._state$; } public get reference$(): Observable<ILatLonAlt> { return this._reference$; } public get inMotion$(): Observable<boolean> { return this._inMotion$; } public get inTranslation$(): Observable<boolean> { return this._inTranslation$; } public get appendNode$(): Subject<Node> { return this._appendNode$; } public earth(): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.earth(); }); } public traverse(): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.traverse(); }); } public wait(): void { this._invokeContextOperation((context: IStateContext) => { context.wait(); }); } public waitInteractively(): void { this._invokeContextOperation((context: IStateContext) => { context.waitInteractively(); }); } public appendNodes(nodes: Node[]): void { this._invokeContextOperation((context: IStateContext) => { context.append(nodes); }); } public prependNodes(nodes: Node[]): void { this._invokeContextOperation((context: IStateContext) => { context.prepend(nodes); }); } public removeNodes(n: number): void { this._invokeContextOperation((context: IStateContext) => { context.remove(n); }); } public clearNodes(): void { this._invokeContextOperation((context: IStateContext) => { context.clear(); }); } public clearPriorNodes(): void { this._invokeContextOperation((context: IStateContext) => { context.clearPrior(); }); } public cutNodes(): void { this._invokeContextOperation((context: IStateContext) => { context.cut(); }); } public setNodes(nodes: Node[]): void { this._invokeContextOperation((context: IStateContext) => { context.set(nodes); }); } public rotate(delta: IRotation): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotate(delta); }); } public rotateUnbounded(delta: IRotation): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateUnbounded(delta); }); } public rotateWithoutInertia(delta: IRotation): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateWithoutInertia(delta); }); } public rotateBasic(basicRotation: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateBasic(basicRotation); }); } public rotateBasicUnbounded(basicRotation: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateBasicUnbounded(basicRotation); }); } public rotateBasicWithoutInertia(basicRotation: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateBasicWithoutInertia(basicRotation); }); } public rotateToBasic(basic: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.rotateToBasic(basic); }); } public move(delta: number): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.move(delta); }); } public moveTo(position: number): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.moveTo(position); }); } public dolly(delta: number): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.dolly(delta); }); } public orbit(rotation: IRotation): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.orbit(rotation); }); } public truck(direction: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.truck(direction); }); } /** * Change zoom level while keeping the reference point position approximately static. * * @parameter {number} delta - Change in zoom level. * @parameter {Array<number>} reference - Reference point in basic coordinates. */ public zoomIn(delta: number, reference: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.zoomIn(delta, reference); }); } public getCenter(): Observable<number[]> { return this._lastState$.pipe( first(), map( (frame: IFrame): number[] => { return (<IStateContext>frame.state).getCenter(); })); } public getZoom(): Observable<number> { return this._lastState$.pipe( first(), map( (frame: IFrame): number => { return frame.state.zoom; })); } public setCenter(center: number[]): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.setCenter(center); }); } public setSpeed(speed: number): void { this._invokeContextOperation((context: IStateContext) => { context.setSpeed(speed); }); } public setTransitionMode(mode: TransitionMode): void { this._invokeContextOperation((context: IStateContext) => { context.setTransitionMode(mode); }); } public setZoom(zoom: number): void { this._inMotionOperation$.next(true); this._invokeContextOperation((context: IStateContext) => { context.setZoom(zoom); }); } public start(): void { if (this._frameId == null) { this._start$.next(null); this._frameId = this._frameGenerator.requestAnimationFrame(this._frame.bind(this)); this._frame$.next(this._frameId); } } public stop(): void { if (this._frameId != null) { this._frameGenerator.cancelAnimationFrame(this._frameId); this._frameId = null; } } private _invokeContextOperation(action: (context: IStateContext) => void): void { this._contextOperation$ .next( (context: IStateContext): IStateContext => { action(context); return context; }); } private _frame(time: number): void { this._frameId = this._frameGenerator.requestAnimationFrame(this._frame.bind(this)); this._frame$.next(this._frameId); } }