UNPKG

mapillary-js

Version:

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

366 lines (305 loc) 12.1 kB
import { from as observableFrom, throwError as observableThrowError, BehaviorSubject, Observable, ReplaySubject, Subscription, } from "rxjs"; import { finalize, first, last, map, mergeAll, mergeMap, tap, } from "rxjs/operators"; import { FilterExpression, Graph, GraphService, IEdgeStatus, Node, } from "../Graph"; import { EdgeDirection } from "../Edge"; import { AbortMapillaryError } from "../Error"; import { StateService, IFrame, } from "../State"; import { CacheService, IViewerOptions, LoadingService, PlayService, } from "../Viewer"; import PanService from "./PanService"; import API from "../api/API"; import FalcorDataProvider from "../api/FalcorDataProvider"; import DataProviderBase from "../api/DataProviderBase"; export class Navigator { private _api: API; private _cacheService: CacheService; private _graphService: GraphService; private _loadingService: LoadingService; private _loadingName: string; private _panService: PanService; private _playService: PlayService; private _stateService: StateService; private _keyRequested$: BehaviorSubject<string>; private _movedToKey$: BehaviorSubject<string>; private _request$: ReplaySubject<Node>; private _requestSubscription: Subscription; private _nodeRequestSubscription: Subscription; constructor( options: IViewerOptions, api?: API, graphService?: GraphService, loadingService?: LoadingService, stateService?: StateService, cacheService?: CacheService, playService?: PlayService, panService?: PanService) { if (!!api) { this._api = api; } else if (typeof options.apiClient === 'string') { this._api = new API(new FalcorDataProvider({ clientToken: options.apiClient, userToken: options.userToken, })); } else if (options.apiClient instanceof DataProviderBase) { this._api = new API(options.apiClient); } else { throw new Error(`Invalid type: 'apiClient' must be a String or an object instance extending the DataProvderBase class.`); } this._graphService = graphService != null ? graphService : new GraphService(new Graph(this.api)); this._loadingService = loadingService != null ? loadingService : new LoadingService(); this._loadingName = "navigator"; this._stateService = stateService != null ? stateService : new StateService(options.transitionMode); this._cacheService = cacheService != null ? cacheService : new CacheService(this._graphService, this._stateService); this._playService = playService != null ? playService : new PlayService(this._graphService, this._stateService); this._panService = panService != null ? panService : new PanService(this._graphService, this._stateService, options.combinedPanning); this._keyRequested$ = new BehaviorSubject<string>(null); this._movedToKey$ = new BehaviorSubject<string>(null); this._request$ = null; this._requestSubscription = null; this._nodeRequestSubscription = null; } public get api(): API { return this._api; } public get cacheService(): CacheService { return this._cacheService; } public get graphService(): GraphService { return this._graphService; } public get loadingService(): LoadingService { return this._loadingService; } public get movedToKey$(): Observable<string> { return this._movedToKey$; } public get panService(): PanService { return this._panService; } public get playService(): PlayService { return this._playService; } public get stateService(): StateService { return this._stateService; } public dispose(): void { this._abortRequest("viewer removed"); this._cacheService.stop(); this._graphService.dispose(); this._panService.dispose(); this._playService.dispose(); this._stateService.dispose(); } public moveToKey$(key: string): Observable<Node> { this._abortRequest(`to key ${key}`); this._loadingService.startLoading(this._loadingName); const node$: Observable<Node> = this._moveToKey$(key); return this._makeRequest$(node$); } public moveDir$(direction: EdgeDirection): Observable<Node> { this._abortRequest(`in dir ${EdgeDirection[direction]}`); this._loadingService.startLoading(this._loadingName); const node$: Observable<Node> = this.stateService.currentNode$.pipe( first(), mergeMap( (node: Node): Observable<string> => { return ([EdgeDirection.Next, EdgeDirection.Prev].indexOf(direction) > -1 ? node.sequenceEdges$ : node.spatialEdges$).pipe( first(), map( (status: IEdgeStatus): string => { for (let edge of status.edges) { if (edge.data.direction === direction) { return edge.to; } } return null; })); }), mergeMap( (directionKey: string) => { if (directionKey == null) { this._loadingService.stopLoading(this._loadingName); return observableThrowError(new Error(`Direction (${direction}) does not exist for current node.`)); } return this._moveToKey$(directionKey); })); return this._makeRequest$(node$); } public setFilter$(filter: FilterExpression): Observable<void> { this._stateService.clearNodes(); return this._movedToKey$.pipe( first(), mergeMap( (key: string): Observable<Node> => { if (key != null) { return this._trajectoryKeys$().pipe( mergeMap( (keys: string[]): Observable<Node> => { return this._graphService.setFilter$(filter).pipe( mergeMap( (): Observable<Node> => { return this._cacheKeys$(keys); })); }), last()); } return this._keyRequested$.pipe( first(), mergeMap( (requestedKey: string): Observable<Node> => { if (requestedKey != null) { return this._graphService.setFilter$(filter).pipe( mergeMap( (): Observable<Node> => { return this._graphService.cacheNode$(requestedKey); })); } return this._graphService.setFilter$(filter).pipe( map( (): Node => { return undefined; })); })); }), map( (): void => { return undefined; })); } public setUserToken$(userToken?: string): Observable<void> { this._abortRequest("to set user token"); this._stateService.clearNodes(); return this._movedToKey$.pipe( first(), tap( (): void => { this._api.setUserToken(userToken); }), mergeMap( (key: string): Observable<void> => { return key == null ? this._graphService.reset$([]) : this._trajectoryKeys$().pipe( mergeMap( (keys: string[]): Observable<Node> => { return this._graphService.reset$(keys).pipe( mergeMap( (): Observable<Node> => { return this._cacheKeys$(keys); })); }), last(), map( (): void => { return undefined; })); })); } private _cacheKeys$(keys: string[]): Observable<Node> { let cacheNodes$: Observable<Node>[] = keys .map( (key: string): Observable<Node> => { return this._graphService.cacheNode$(key); }); return observableFrom(cacheNodes$).pipe( mergeAll()); } private _abortRequest(reason: string): void { if (this._requestSubscription != null) { this._requestSubscription.unsubscribe(); this._requestSubscription = null; } if (this._nodeRequestSubscription != null) { this._nodeRequestSubscription.unsubscribe(); this._nodeRequestSubscription = null; } if (this._request$ != null) { if (!(this._request$.isStopped || this._request$.hasError)) { this._request$.error(new AbortMapillaryError(`Request aborted by a subsequent request ${reason}.`)); } this._request$ = null; } } private _makeRequest$(node$: Observable<Node>): Observable<Node> { const request$: ReplaySubject<Node> = new ReplaySubject<Node>(1); this._requestSubscription = request$ .subscribe(undefined, (): void => { /*noop*/ }); this._request$ = request$; this._nodeRequestSubscription = node$ .subscribe( (node: Node): void => { this._request$ = null; request$.next(node); request$.complete(); }, (error: Error): void => { this._request$ = null; request$.error(error); }); return request$; } private _moveToKey$(key: string): Observable<Node> { this._keyRequested$.next(key); return this._graphService.cacheNode$(key).pipe( tap( (node: Node) => { this._stateService.setNodes([node]); this._movedToKey$.next(node.key); }), finalize( (): void => { this._loadingService.stopLoading(this._loadingName); })); } private _trajectoryKeys$(): Observable<string[]> { return this._stateService.currentState$.pipe( first(), map( (frame: IFrame): string[] => { return frame.state.trajectory .map( (node: Node): string => { return node.key; }); })); } } export default Navigator;