mapillary-js
Version:
WebGL JavaScript library for displaying street level imagery from mapillary.com
362 lines (295 loc) • 12.4 kB
text/typescript
import {from as observableFrom, throwError as observableThrowError, BehaviorSubject, Observable, ReplaySubject, Subscription} from "rxjs";
import {last, tap, map, mergeAll, finalize, mergeMap, first} from "rxjs/operators";
import {
APIv3,
IFullNode,
} from "../API";
import {
FilterExpression,
Graph,
GraphService,
IEdgeStatus,
ImageLoadingService,
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";
export class Navigator {
private _apiV3: APIv3;
private _cacheService: CacheService;
private _graphService: GraphService;
private _imageLoadingService: ImageLoadingService;
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 (
clientId: string,
options: IViewerOptions,
token?: string,
apiV3?: APIv3,
graphService?: GraphService,
imageLoadingService?: ImageLoadingService,
loadingService?: LoadingService,
stateService?: StateService,
cacheService?: CacheService,
playService?: PlayService,
panService?: PanService) {
this._apiV3 = apiV3 != null ? apiV3 : new APIv3(clientId, token);
this._imageLoadingService = imageLoadingService != null ? imageLoadingService : new ImageLoadingService();
this._graphService = graphService != null ?
graphService :
new GraphService(new Graph(this.apiV3), this._imageLoadingService);
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 apiV3(): APIv3 {
return this._apiV3;
}
public get cacheService(): CacheService {
return this._cacheService;
}
public get graphService(): GraphService {
return this._graphService;
}
public get imageLoadingService(): ImageLoadingService {
return this._imageLoadingService;
}
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 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 moveCloseTo$(lat: number, lon: number): Observable<Node> {
this._abortRequest(`to lat ${lat}, lon ${lon}`);
this._loadingService.startLoading(this._loadingName);
const node$: Observable<Node> = this.apiV3.imageCloseTo$(lat, lon).pipe(
mergeMap(
(fullNode: IFullNode): Observable<Node> => {
if (fullNode == null) {
this._loadingService.stopLoading(this._loadingName);
return observableThrowError(new Error(`No image found close to lat ${lat}, lon ${lon}.`));
}
return this._moveToKey$(fullNode.key);
}));
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(
(node: Node): void => {
return undefined;
}));
}
public setToken$(token?: string): Observable<void> {
this._abortRequest("to set token");
this._stateService.clearNodes();
return this._movedToKey$.pipe(
first(),
tap(
(key: string): void => {
this._apiV3.setToken(token);
}),
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(
(node: Node): 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;