UNPKG

mapillary-js

Version:

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

535 lines (476 loc) 19.3 kB
import { concat as observableConcat, empty as observableEmpty, from as observableFrom, of as observableOf, Observable, Subject, Subscription, } from "rxjs"; import { first, expand, map, last, mergeMap, startWith, publishReplay, refCount, catchError, finalize, tap, } from "rxjs/operators"; import {ILatLon} from "../API"; import { FilterExpression, Graph, GraphMode, ImageLoadingService, Node, Sequence, } from "../Graph"; /** * @class GraphService * * @classdesc Represents a service for graph operations. */ export class GraphService { private _graph$: Observable<Graph>; private _graphMode: GraphMode; private _graphMode$: Observable<GraphMode>; private _graphModeSubject$: Subject<GraphMode>; private _imageLoadingService: ImageLoadingService; private _firstGraphSubjects$: Subject<Graph>[]; private _initializeCacheSubscriptions: Subscription[]; private _sequenceSubscriptions: Subscription[]; private _spatialSubscriptions: Subscription[]; /** * Create a new graph service instance. * * @param {Graph} graph - Graph instance to be operated on. */ constructor(graph: Graph, imageLoadingService: ImageLoadingService) { this._graph$ = observableConcat( observableOf(graph), graph.changed$).pipe( publishReplay(1), refCount()); this._graph$.subscribe(() => { /*noop*/ }); this._graphMode = GraphMode.Spatial; this._graphModeSubject$ = new Subject<GraphMode>(); this._graphMode$ = this._graphModeSubject$.pipe( startWith(this._graphMode), publishReplay(1), refCount()); this._graphMode$.subscribe(() => { /*noop*/ }); this._imageLoadingService = imageLoadingService; this._firstGraphSubjects$ = []; this._initializeCacheSubscriptions = []; this._sequenceSubscriptions = []; this._spatialSubscriptions = []; } /** * Get graph mode observable. * * @description Emits the current graph mode. * * @returns {Observable<GraphMode>} Observable * emitting the current graph mode when it changes. */ public get graphMode$(): Observable<GraphMode> { return this._graphMode$; } /** * Cache full nodes in a bounding box. * * @description When called, the full properties of * the node are retrieved. The node cache is not initialized * for any new nodes retrieved and the node assets are not * retrieved, {@link cacheNode$} needs to be called for caching * assets. * * @param {ILatLon} sw - South west corner of bounding box. * @param {ILatLon} ne - North east corner of bounding box. * @return {Observable<Array<Node>>} Observable emitting a single item, * the nodes of the bounding box, when they have all been retrieved. * @throws {Error} Propagates any IO node caching errors to the caller. */ public cacheBoundingBox$(sw: ILatLon, ne: ILatLon): Observable<Node[]> { return this._graph$.pipe( first(), mergeMap( (graph: Graph): Observable<Node[]> => { return graph.cacheBoundingBox$(sw, ne); })); } /** * Cache a node in the graph and retrieve it. * * @description When called, the full properties of * the node are retrieved and the node cache is initialized. * After that the node assets are cached and the node * is emitted to the observable when. * In parallel to caching the node assets, the sequence and * spatial edges of the node are cached. For this, the sequence * of the node and the required tiles and spatial nodes are * retrieved. The sequence and spatial edges may be set before * or after the node is returned. * * @param {string} key - Key of the node to cache. * @return {Observable<Node>} Observable emitting a single item, * the node, when it has been retrieved and its assets are cached. * @throws {Error} Propagates any IO node caching errors to the caller. */ public cacheNode$(key: string): Observable<Node> { const firstGraphSubject$: Subject<Graph> = new Subject<Graph>(); this._firstGraphSubjects$.push(firstGraphSubject$); const firstGraph$: Observable<Graph> = firstGraphSubject$.pipe( publishReplay(1), refCount()); const node$: Observable<Node> = firstGraph$.pipe( map( (graph: Graph): Node => { return graph.getNode(key); }), mergeMap( (node: Node): Observable<Node> => { return node.assetsCached ? observableOf(node) : node.cacheAssets$(); }), publishReplay(1), refCount()); node$.subscribe( (node: Node): void => { this._imageLoadingService.loadnode$.next(node); }, (error: Error): void => { console.error(`Failed to cache node (${key})`, error); }); const initializeCacheSubscription: Subscription = this._graph$.pipe( first(), mergeMap( (graph: Graph): Observable<Graph> => { if (graph.isCachingFull(key) || !graph.hasNode(key)) { return graph.cacheFull$(key); } if (graph.isCachingFill(key) || !graph.getNode(key).full) { return graph.cacheFill$(key); } return observableOf<Graph>(graph); }), tap( (graph: Graph): void => { if (!graph.hasInitializedCache(key)) { graph.initializeCache(key); } }), finalize( (): void => { if (initializeCacheSubscription == null) { return; } this._removeFromArray(initializeCacheSubscription, this._initializeCacheSubscriptions); this._removeFromArray(firstGraphSubject$, this._firstGraphSubjects$); })) .subscribe( (graph: Graph): void => { firstGraphSubject$.next(graph); firstGraphSubject$.complete(); }, (error: Error): void => { firstGraphSubject$.error(error); }); if (!initializeCacheSubscription.closed) { this._initializeCacheSubscriptions.push(initializeCacheSubscription); } const graphSequence$: Observable<Graph> = firstGraph$.pipe( mergeMap( (graph: Graph): Observable<Graph> => { if (graph.isCachingNodeSequence(key) || !graph.hasNodeSequence(key)) { return graph.cacheNodeSequence$(key); } return observableOf<Graph>(graph); }), publishReplay(1), refCount()); const sequenceSubscription: Subscription = graphSequence$.pipe( tap( (graph: Graph): void => { if (!graph.getNode(key).sequenceEdges.cached) { graph.cacheSequenceEdges(key); } }), finalize( (): void => { if (sequenceSubscription == null) { return; } this._removeFromArray(sequenceSubscription, this._sequenceSubscriptions); })) .subscribe( (graph: Graph): void => { return; }, (error: Error): void => { console.error(`Failed to cache sequence edges (${key}).`, error); }); if (!sequenceSubscription.closed) { this._sequenceSubscriptions.push(sequenceSubscription); } if (this._graphMode === GraphMode.Spatial) { const spatialSubscription: Subscription = firstGraph$.pipe( expand( (graph: Graph): Observable<Graph> => { if (graph.hasTiles(key)) { return observableEmpty(); } return observableFrom(graph.cacheTiles$(key)).pipe( mergeMap( (graph$: Observable<Graph>): Observable<Graph> => { return graph$.pipe( mergeMap( (g: Graph): Observable<Graph> => { if (g.isCachingTiles(key)) { return observableEmpty(); } return observableOf<Graph>(g); }), catchError( (error: Error, caught$: Observable<Graph>): Observable<Graph> => { console.error(`Failed to cache tile data (${key}).`, error); return observableEmpty(); })); })); }), last(), mergeMap( (graph: Graph): Observable<Graph> => { if (graph.hasSpatialArea(key)) { return observableOf<Graph>(graph); } return observableFrom(graph.cacheSpatialArea$(key)).pipe( mergeMap( (graph$: Observable<Graph>): Observable<Graph> => { return graph$.pipe( catchError( (error: Error, caught$: Observable<Graph>): Observable<Graph> => { console.error(`Failed to cache spatial nodes (${key}).`, error); return observableEmpty(); })); })); }), last(), mergeMap( (graph: Graph): Observable<Graph> => { return graph.hasNodeSequence(key) ? observableOf<Graph>(graph) : graph.cacheNodeSequence$(key); }), tap( (graph: Graph): void => { if (!graph.getNode(key).spatialEdges.cached) { graph.cacheSpatialEdges(key); } }), finalize( (): void => { if (spatialSubscription == null) { return; } this._removeFromArray(spatialSubscription, this._spatialSubscriptions); })) .subscribe( (graph: Graph): void => { return; }, (error: Error): void => { console.error(`Failed to cache spatial edges (${key}).`, error); }); if (!spatialSubscription.closed) { this._spatialSubscriptions.push(spatialSubscription); } } return node$.pipe( first( (node: Node): boolean => { return node.assetsCached; })); } /** * Cache a sequence in the graph and retrieve it. * * @param {string} sequenceKey - Sequence key. * @returns {Observable<Sequence>} Observable emitting a single item, * the sequence, when it has been retrieved and its assets are cached. * @throws {Error} Propagates any IO node caching errors to the caller. */ public cacheSequence$(sequenceKey: string): Observable<Sequence> { return this._graph$.pipe( first(), mergeMap( (graph: Graph): Observable<Graph> => { if (graph.isCachingSequence(sequenceKey) || !graph.hasSequence(sequenceKey)) { return graph.cacheSequence$(sequenceKey); } return observableOf<Graph>(graph); }), map( (graph: Graph): Sequence => { return graph.getSequence(sequenceKey); })); } /** * Cache a sequence and its nodes in the graph and retrieve the sequence. * * @description Caches a sequence and its assets are cached and * retrieves all nodes belonging to the sequence. The node assets * or edges will not be cached. * * @param {string} sequenceKey - Sequence key. * @param {string} referenceNodeKey - Key of node to use as reference * for optimized caching. * @returns {Observable<Sequence>} Observable emitting a single item, * the sequence, when it has been retrieved, its assets are cached and * all nodes belonging to the sequence has been retrieved. * @throws {Error} Propagates any IO node caching errors to the caller. */ public cacheSequenceNodes$(sequenceKey: string, referenceNodeKey?: string): Observable<Sequence> { return this._graph$.pipe( first(), mergeMap( (graph: Graph): Observable<Graph> => { if (graph.isCachingSequence(sequenceKey) || !graph.hasSequence(sequenceKey)) { return graph.cacheSequence$(sequenceKey); } return observableOf<Graph>(graph); }), mergeMap( (graph: Graph): Observable<Graph> => { if (graph.isCachingSequenceNodes(sequenceKey) || !graph.hasSequenceNodes(sequenceKey)) { return graph.cacheSequenceNodes$(sequenceKey, referenceNodeKey); } return observableOf<Graph>(graph); }), map( (graph: Graph): Sequence => { return graph.getSequence(sequenceKey); })); } /** * Set a spatial edge filter on the graph. * * @description Resets the spatial edges of all cached nodes. * * @param {FilterExpression} filter - Filter expression to be applied. * @return {Observable<Graph>} Observable emitting a single item, * the graph, when the spatial edges have been reset. */ public setFilter$(filter: FilterExpression): Observable<void> { this._resetSubscriptions(this._spatialSubscriptions); return this._graph$.pipe( first(), tap( (graph: Graph): void => { graph.resetSpatialEdges(); graph.setFilter(filter); }), map( (graph: Graph): void => { return undefined; })); } /** * Set the graph mode. * * @description If graph mode is set to spatial, caching * is performed with emphasis on spatial edges. If graph * mode is set to sequence no tile data is requested and * no spatial edges are computed. * * When setting graph mode to sequence all spatial * subscriptions are aborted. * * @param {GraphMode} mode - Graph mode to set. */ public setGraphMode(mode: GraphMode): void { if (this._graphMode === mode) { return; } if (mode === GraphMode.Sequence) { this._resetSubscriptions(this._spatialSubscriptions); } this._graphMode = mode; this._graphModeSubject$.next(this._graphMode); } /** * Reset the graph. * * @description Resets the graph but keeps the nodes of the * supplied keys. * * @param {Array<string>} keepKeys - Keys of nodes to keep in graph. * @return {Observable<Node>} Observable emitting a single item, * the graph, when it has been reset. */ public reset$(keepKeys: string[]): Observable<void> { this._abortSubjects(this._firstGraphSubjects$); this._resetSubscriptions(this._initializeCacheSubscriptions); this._resetSubscriptions(this._sequenceSubscriptions); this._resetSubscriptions(this._spatialSubscriptions); return this._graph$.pipe( first(), tap( (graph: Graph): void => { graph.reset(keepKeys); }), map( (graph: Graph): void => { return undefined; })); } /** * Uncache the graph. * * @description Uncaches the graph by removing tiles, nodes and * sequences. Keeps the nodes of the supplied keys and the tiles * related to those nodes. * * @param {Array<string>} keepKeys - Keys of nodes to keep in graph. * @param {string} keepSequenceKey - Optional key of sequence * for which the belonging nodes should not be disposed or * removed from the graph. These nodes may still be uncached if * not specified in keep keys param. * @return {Observable<Graph>} Observable emitting a single item, * the graph, when the graph has been uncached. */ public uncache$(keepKeys: string[], keepSequenceKey?: string): Observable<void> { return this._graph$.pipe( first(), tap( (graph: Graph): void => { graph.uncache(keepKeys, keepSequenceKey); }), map( (graph: Graph): void => { return undefined; })); } private _abortSubjects<T>(subjects: Subject<T>[]): void { for (const subject of subjects.slice()) { this._removeFromArray(subject, subjects); subject.error(new Error("Cache node request was aborted.")); } } private _removeFromArray<T>(object: T, objects: T[]): void { const index: number = objects.indexOf(object); if (index !== -1) { objects.splice(index, 1); } } private _resetSubscriptions(subscriptions: Subscription[]): void { for (const subscription of subscriptions.slice()) { this._removeFromArray(subscription, subscriptions); if (!subscription.closed) { subscription.unsubscribe(); } } } } export default GraphService;