UNPKG

mapillary-js

Version:

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

533 lines (452 loc) 19.4 kB
import { combineLatest as observableCombineLatest, empty as observableEmpty, from as observableFrom, of as observableOf, zip as observableZip, Observable, Subject, Subscription, } from "rxjs"; import { publish, finalize, startWith, publishReplay, refCount, map, distinctUntilChanged, switchMap, retry, catchError, scan, filter, withLatestFrom, first, timeout, bufferCount, mergeMap, } from "rxjs/operators"; import {ILatLon} from "../API"; import {EdgeDirection} from "../Edge"; import { Graph, GraphCalculator, GraphMode, GraphService, IEdgeStatus, Node, Sequence, } from "../Graph"; import { ICurrentState, IFrame, StateService, } from "../State"; export class PlayService { public static readonly sequenceSpeed: number = 0.54; private _graphService: GraphService; private _stateService: StateService; private _graphCalculator: GraphCalculator; private _nodesAhead: number; private _playing: boolean; private _speed: number; private _direction$: Observable<EdgeDirection>; private _directionSubject$: Subject<EdgeDirection>; private _playing$: Observable<boolean>; private _playingSubject$: Subject<boolean>; private _speed$: Observable<number>; private _speedSubject$: Subject<number>; private _playingSubscription: Subscription; private _cacheSubscription: Subscription; private _clearSubscription: Subscription; private _graphModeSubscription: Subscription; private _stopSubscription: Subscription; private _bridging$: Observable<Node>; constructor(graphService: GraphService, stateService: StateService, graphCalculator?: GraphCalculator) { this._graphService = graphService; this._stateService = stateService; this._graphCalculator = !!graphCalculator ? graphCalculator : new GraphCalculator(); this._directionSubject$ = new Subject<EdgeDirection>(); this._direction$ = this._directionSubject$.pipe( startWith(EdgeDirection.Next), publishReplay(1), refCount()); this._direction$.subscribe(); this._playing = false; this._playingSubject$ = new Subject<boolean>(); this._playing$ = this._playingSubject$.pipe( startWith(this._playing), publishReplay(1), refCount()); this._playing$.subscribe(); this._speed = 0.5; this._speedSubject$ = new Subject<number>(); this._speed$ = this._speedSubject$.pipe( startWith(this._speed), publishReplay(1), refCount()); this._speed$.subscribe(); this._nodesAhead = this._mapNodesAhead(this._mapSpeed(this._speed)); this._bridging$ = null; } public get playing(): boolean { return this._playing; } public get direction$(): Observable<EdgeDirection> { return this._direction$; } public get playing$(): Observable<boolean> { return this._playing$; } public get speed$(): Observable<number> { return this._speed$; } public play(): void { if (this._playing) { return; } this._stateService.cutNodes(); const stateSpeed: number = this._setSpeed(this._speed); this._stateService.setSpeed(stateSpeed); this._graphModeSubscription = this._speed$.pipe( map( (speed: number): GraphMode => { return speed > PlayService.sequenceSpeed ? GraphMode.Sequence : GraphMode.Spatial; }), distinctUntilChanged()) .subscribe( (mode: GraphMode): void => { this._graphService.setGraphMode(mode); }); this._cacheSubscription = observableCombineLatest( this._stateService.currentNode$.pipe( map( (node: Node): [string, string] => { return [node.sequenceKey, node.key]; }), distinctUntilChanged( undefined, ([sequenceKey, nodeKey]: [string, string]): string => { return sequenceKey; })), this._graphService.graphMode$, this._direction$).pipe( switchMap( ([[sequenceKey, nodeKey], mode, direction]: [[string, string], GraphMode, EdgeDirection]): Observable<[Sequence, EdgeDirection]> => { if (direction !== EdgeDirection.Next && direction !== EdgeDirection.Prev) { return observableOf<[Sequence, EdgeDirection]>([undefined, direction]); } const sequence$: Observable<Sequence> = (mode === GraphMode.Sequence ? this._graphService.cacheSequenceNodes$(sequenceKey, nodeKey) : this._graphService.cacheSequence$(sequenceKey)).pipe( retry(3), catchError( (error: Error): Observable<Sequence> => { console.error(error); return observableOf(undefined); })); return observableCombineLatest( sequence$, observableOf(direction)); }), switchMap( ([sequence, direction]: [Sequence, EdgeDirection]): Observable<string> => { if (sequence === undefined) { return observableEmpty(); } const sequenceKeys: string[] = sequence.keys.slice(); if (direction === EdgeDirection.Prev) { sequenceKeys.reverse(); } return this._stateService.currentState$.pipe( map( (frame: IFrame): [string, number] => { return [frame.state.trajectory[frame.state.trajectory.length - 1].key, frame.state.nodesAhead]; }), scan( ( [lastRequestKey, previousRequestKeys]: [string, string[]], [lastTrajectoryKey, nodesAhead]: [string, number]): [string, string[]] => { if (lastRequestKey === undefined) { lastRequestKey = lastTrajectoryKey; } const lastIndex: number = sequenceKeys.length - 1; if (nodesAhead >= this._nodesAhead || sequenceKeys[lastIndex] === lastRequestKey) { return [lastRequestKey, []]; } const current: number = sequenceKeys.indexOf(lastTrajectoryKey); const start: number = sequenceKeys.indexOf(lastRequestKey) + 1; const end: number = Math.min(lastIndex, current + this._nodesAhead - nodesAhead) + 1; if (end <= start) { return [lastRequestKey, []]; } return [sequenceKeys[end - 1], sequenceKeys.slice(start, end)]; }, [undefined, []]), mergeMap( ([lastRequestKey, newRequestKeys]: [string, string[]]): Observable<string> => { return observableFrom(newRequestKeys); })); }), mergeMap( (key: string): Observable<Node> => { return this._graphService.cacheNode$(key).pipe( catchError( (): Observable<Node> => { return observableEmpty(); })); }, 6)) .subscribe(); this._playingSubscription = this._stateService.currentState$.pipe( filter( (frame: IFrame): boolean => { return frame.state.nodesAhead < this._nodesAhead; }), distinctUntilChanged( undefined, (frame: IFrame): string => { return frame.state.lastNode.key; }), map( (frame: IFrame): [Node, boolean] => { const lastNode: Node = frame.state.lastNode; const trajectory: Node[] = frame.state.trajectory; let increasingTime: boolean = undefined; for (let i: number = trajectory.length - 2; i >= 0; i--) { const node: Node = trajectory[i]; if (node.sequenceKey !== lastNode.sequenceKey) { break; } if (node.capturedAt !== lastNode.capturedAt) { increasingTime = node.capturedAt < lastNode.capturedAt; break; } } return [frame.state.lastNode, increasingTime]; }), withLatestFrom(this._direction$), switchMap( ([[node, increasingTime], direction]: [[Node, boolean], EdgeDirection]): Observable<Node> => { return observableZip( ([EdgeDirection.Next, EdgeDirection.Prev].indexOf(direction) > -1 ? node.sequenceEdges$ : node.spatialEdges$).pipe( first( (status: IEdgeStatus): boolean => { return status.cached; }), timeout(15000)), observableOf<EdgeDirection>(direction)).pipe( map( ([s, d]: [IEdgeStatus, EdgeDirection]): string => { for (let edge of s.edges) { if (edge.data.direction === d) { return edge.to; } } return null; }), switchMap( (key: string): Observable<Node> => { return key != null ? this._graphService.cacheNode$(key) : this._bridge$(node, increasingTime).pipe( filter( (n: Node): boolean => { return !!n; })); })); })) .subscribe( (node: Node): void => { this._stateService.appendNodes([node]); }, (error: Error): void => { console.error(error); this.stop(); }); this._clearSubscription = this._stateService.currentNode$.pipe( bufferCount(1, 10)) .subscribe( (nodes: Node[]): void => { this._stateService.clearPriorNodes(); }); this._setPlaying(true); const currentLastNodes$: Observable<Node> = this._stateService.currentState$.pipe( map( (frame: IFrame): ICurrentState => { return frame.state; }), distinctUntilChanged( ([kc1, kl1]: [string, string], [kc2, kl2]: [string, string]): boolean => { return kc1 === kc2 && kl1 === kl2; }, (state: ICurrentState): [string, string] => { return [state.currentNode.key, state.lastNode.key]; }), filter( (state: ICurrentState): boolean => { return state.currentNode.key === state.lastNode.key && state.currentIndex === state.trajectory.length - 1; }), map( (state: ICurrentState): Node => { return state.currentNode; })); this._stopSubscription = observableCombineLatest( currentLastNodes$, this._direction$).pipe( switchMap( ([node, direction]: [Node, EdgeDirection]): Observable<boolean> => { const edgeStatus$: Observable<IEdgeStatus> = ( [EdgeDirection.Next, EdgeDirection.Prev].indexOf(direction) > -1 ? node.sequenceEdges$ : node.spatialEdges$).pipe( first( (status: IEdgeStatus): boolean => { return status.cached; }), timeout(15000), catchError( (error: Error): Observable<IEdgeStatus> => { console.error(error); return observableOf<IEdgeStatus>({ cached: false, edges: [] }); })); return observableCombineLatest( observableOf(direction), edgeStatus$).pipe( map( ([d, es]: [EdgeDirection, IEdgeStatus]): boolean => { for (const edge of es.edges) { if (edge.data.direction === d) { return true; } } return false; })); }), mergeMap( (hasEdge: boolean): Observable<boolean> => { if (hasEdge || !this._bridging$) { return observableOf(hasEdge); } return this._bridging$.pipe( map( (node: Node): boolean => { return node != null; }), catchError( (error: Error): Observable<boolean> => { console.error(error); return observableOf<boolean>(false); })); }), first( (hasEdge: boolean): boolean => { return !hasEdge; })) .subscribe( undefined, undefined, (): void => { this.stop(); }); if (this._stopSubscription.closed) { this._stopSubscription = null; } } public setDirection(direction: EdgeDirection): void { this._directionSubject$.next(direction); } public setSpeed(speed: number): void { speed = Math.max(0, Math.min(1, speed)); if (speed === this._speed) { return; } const stateSpeed: number = this._setSpeed(speed); if (this._playing) { this._stateService.setSpeed(stateSpeed); } this._speedSubject$.next(this._speed); } public stop(): void { if (!this._playing) { return; } if (!!this._stopSubscription) { if (!this._stopSubscription.closed) { this._stopSubscription.unsubscribe(); } this._stopSubscription = null; } this._graphModeSubscription.unsubscribe(); this._graphModeSubscription = null; this._cacheSubscription.unsubscribe(); this._cacheSubscription = null; this._playingSubscription.unsubscribe(); this._playingSubscription = null; this._clearSubscription.unsubscribe(); this._clearSubscription = null; this._stateService.setSpeed(1); this._stateService.cutNodes(); this._graphService.setGraphMode(GraphMode.Spatial); this._setPlaying(false); } private _bridge$(node: Node, increasingTime: boolean): Observable<Node> { if (increasingTime === undefined) { return observableOf(null); } const boundingBox: ILatLon[] = this._graphCalculator.boundingBoxCorners(node.latLon, 25); this._bridging$ = this._graphService.cacheBoundingBox$(boundingBox[0], boundingBox[1]).pipe( mergeMap( (nodes: Node[]): Observable<Node> => { let nextNode: Node = null; for (const n of nodes) { if (n.sequenceKey === node.sequenceKey || !n.cameraUuid || n.cameraUuid !== node.cameraUuid || n.capturedAt === node.capturedAt || n.capturedAt > node.capturedAt !== increasingTime) { continue; } const delta: number = Math.abs(n.capturedAt - node.capturedAt); if (delta > 15000) { continue; } if (!nextNode || delta < Math.abs(nextNode.capturedAt - node.capturedAt)) { nextNode = n; } } return !!nextNode ? this._graphService.cacheNode$(nextNode.key) : observableOf(null); }), finalize( (): void => { this._bridging$ = null; }), publish(), refCount()); return this._bridging$; } private _mapSpeed(speed: number): number { const x: number = 2 * speed - 1; return Math.pow(10, x) - 0.2 * x; } private _mapNodesAhead(stateSpeed: number): number { return Math.round(Math.max(10, Math.min(50, 8 + 6 * stateSpeed))); } private _setPlaying(playing: boolean): void { this._playing = playing; this._playingSubject$.next(playing); } private _setSpeed(speed: number): number { this._speed = speed; const stateSpeed: number = this._mapSpeed(this._speed); this._nodesAhead = this._mapNodesAhead(stateSpeed); return stateSpeed; } } export default PlayService;