UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

362 lines (303 loc) 12.4 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 { CacheService } from "./CacheService"; import { LoadingService } from "./LoadingService"; import { PanService } from "./PanService"; import { PlayService } from "./PlayService"; import { ViewerOptions } from "./options/ViewerOptions"; import { APIWrapper } from "../api/APIWrapper"; import { CancelMapillaryError } from "../error/CancelMapillaryError"; import { FilterExpression } from "../graph/FilterExpression"; import { Graph } from "../graph/Graph"; import { GraphService } from "../graph/GraphService"; import { Image } from "../graph/Image"; import { NavigationDirection } from "../graph/edge/NavigationDirection"; import { NavigationEdgeStatus } from "../graph/interfaces/NavigationEdgeStatus"; import { StateService } from "../state/StateService"; import { AnimationFrame } from "../state/interfaces/AnimationFrame"; import { cameraControlsToState } from "./Modes"; import { CameraControls } from "./enums/CameraControls"; import { GraphDataProvider } from "../api/provider/GraphDataProvider"; export class Navigator { private _api: APIWrapper; private _cacheService: CacheService; private _graphService: GraphService; private _loadingService: LoadingService; private _loadingName: string; private _panService: PanService; private _playService: PlayService; private _stateService: StateService; private _idRequested$: BehaviorSubject<string>; private _movedToId$: BehaviorSubject<string>; private _request$: ReplaySubject<Image>; private _requestSubscription: Subscription; private _imageRequestSubscription: Subscription; constructor( options: ViewerOptions, api?: APIWrapper, graphService?: GraphService, loadingService?: LoadingService, stateService?: StateService, cacheService?: CacheService, playService?: PlayService, panService?: PanService) { if (api) { this._api = api; } else if (options.dataProvider) { this._api = new APIWrapper(options.dataProvider); } else { this._api = new APIWrapper(new GraphDataProvider({ accessToken: options.accessToken, })); } this._graphService = graphService ?? new GraphService(new Graph(this.api)); this._loadingName = "navigator"; this._loadingService = loadingService ?? new LoadingService(); const cameraControls = options.cameraControls ?? CameraControls.Street; this._stateService = stateService ?? new StateService( cameraControlsToState(cameraControls), options.transitionMode); this._cacheService = cacheService ?? new CacheService( this._graphService, this._stateService, this._api); this._playService = playService ?? new PlayService(this._graphService, this._stateService); this._panService = panService ?? new PanService( this._graphService, this._stateService, options.combinedPanning); this._idRequested$ = new BehaviorSubject<string>(null); this._movedToId$ = new BehaviorSubject<string>(null); this._request$ = null; this._requestSubscription = null; this._imageRequestSubscription = null; } public get api(): APIWrapper { 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 movedToId$(): Observable<string> { return this._movedToId$; } 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 moveTo$(id: string): Observable<Image> { this._abortRequest(`to id ${id}`); this._loadingService.startLoading(this._loadingName); const image$ = this._moveTo$(id); return this._makeRequest$(image$); } public moveDir$(direction: NavigationDirection): Observable<Image> { this._abortRequest(`in dir ${NavigationDirection[direction]}`); this._loadingService.startLoading(this._loadingName); const image$ = this.stateService.currentImage$.pipe( first(), mergeMap( (image: Image): Observable<string> => { return ([NavigationDirection.Next, NavigationDirection.Prev].indexOf(direction) > -1 ? image.sequenceEdges$ : image.spatialEdges$).pipe( first(), map( (status: NavigationEdgeStatus): string => { for (let edge of status.edges) { if (edge.data.direction === direction) { return edge.target; } } return null; })); }), mergeMap( (directionId: string) => { if (directionId == null) { this._loadingService.stopLoading(this._loadingName); return observableThrowError(new Error(`Direction (${direction}) does not exist for current image.`)); } return this._moveTo$(directionId); })); return this._makeRequest$(image$); } public setFilter$(filter: FilterExpression): Observable<void> { this._stateService.clearImages(); return this._movedToId$.pipe( first(), mergeMap( (id: string): Observable<Image> => { if (id != null) { return this._trajectoryIds$().pipe( mergeMap( (ids: string[]): Observable<Image> => { return this._graphService.setFilter$(filter).pipe( mergeMap( (): Observable<Image> => { return this._cacheIds$(ids); })); }), last()); } return this._idRequested$.pipe( first(), mergeMap( (requestedId: string): Observable<Image> => { if (requestedId != null) { return this._graphService.setFilter$(filter).pipe( mergeMap( (): Observable<Image> => { return this._graphService.cacheImage$(requestedId); })); } return this._graphService.setFilter$(filter).pipe( map( (): Image => { return undefined; })); })); }), map( (): void => { return undefined; })); } public setAccessToken$(accessToken?: string): Observable<void> { this._abortRequest("to set user token"); this._stateService.clearImages(); return this._movedToId$.pipe( first(), tap( (): void => { this._api.setAccessToken(accessToken); }), mergeMap( (id: string): Observable<void> => { return id == null ? this._graphService.reset$([]) : this._trajectoryIds$().pipe( mergeMap( (ids: string[]): Observable<Image> => { return this._graphService.reset$(ids).pipe( mergeMap( (): Observable<Image> => { return this._cacheIds$(ids); })); }), last(), map( (): void => { return undefined; })); })); } private _cacheIds$(ids: string[]): Observable<Image> { const cacheImages$ = ids .map( (id: string): Observable<Image> => { return this._graphService.cacheImage$(id); }); return observableFrom(cacheImages$).pipe( mergeAll()); } private _abortRequest(reason: string): void { if (this._requestSubscription != null) { this._requestSubscription.unsubscribe(); this._requestSubscription = null; } if (this._imageRequestSubscription != null) { this._imageRequestSubscription.unsubscribe(); this._imageRequestSubscription = null; } if (this._request$ != null) { if (!(this._request$.isStopped || this._request$.hasError)) { this._request$.error(new CancelMapillaryError(`Request aborted by a subsequent request ${reason}.`)); } this._request$ = null; } } private _makeRequest$(image$: Observable<Image>): Observable<Image> { const request$: ReplaySubject<Image> = new ReplaySubject<Image>(1); this._requestSubscription = request$ .subscribe(undefined, (): void => { /*noop*/ }); this._request$ = request$; this._imageRequestSubscription = image$ .subscribe( (image: Image): void => { this._request$ = null; request$.next(image); request$.complete(); }, (error: Error): void => { this._request$ = null; request$.error(error); }); return request$; } private _moveTo$(id: string): Observable<Image> { this._idRequested$.next(id); return this._graphService.cacheImage$(id).pipe( tap( (image: Image) => { this._stateService.setImages([image]); this._movedToId$.next(image.id); }), finalize( (): void => { this._loadingService.stopLoading(this._loadingName); })); } private _trajectoryIds$(): Observable<string[]> { return this._stateService.currentState$.pipe( first(), map( (frame: AnimationFrame): string[] => { return frame.state.trajectory .map( (image: Image): string => { return image.id; }); })); } }