UNPKG

mapillary-js

Version:

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

321 lines (263 loc) 10.9 kB
import { empty as observableEmpty, from as observableFrom, of as observableOf, Observable, Subscriber, } from "rxjs"; import { mergeMap, catchError, tap, publish, refCount, finalize, map, filter, } from "rxjs/operators"; import AbortMapillaryError from "../../error/AbortMapillaryError"; import GraphService from "../../graph/GraphService"; import IDataProvider from "../../api/interfaces/IDataProvider"; import IClusterReconstruction from "../../api/interfaces/IClusterReconstruction"; import ICellCorners from "../../api/interfaces/ICellCorners"; import Node from "../../graph/Node"; type ClusterData = { key: string; url: string; } export class SpatialDataCache { private _graphService: GraphService; private _data: IDataProvider; private _cacheRequests: { [hash: string]: Function[] }; private _tiles: { [hash: string]: Node[] }; private _clusterReconstructions: { [key: string]: IClusterReconstruction }; private _clusterReconstructionTiles: { [key: string]: string[] }; private _tileClusters: { [hash: string]: ClusterData[] }; private _cachingClusterReconstructions$: { [hash: string]: Observable<IClusterReconstruction> }; private _cachingTiles$: { [hash: string]: Observable<Node[]> }; constructor(graphService: GraphService, provider: IDataProvider) { this._graphService = graphService; this._data = provider; this._tiles = {}; this._cacheRequests = {}; this._clusterReconstructions = {}; this._clusterReconstructionTiles = {}; this._tileClusters = {}; this._cachingTiles$ = {}; this._cachingClusterReconstructions$ = {}; } public cacheClusterReconstructions$(hash: string): Observable<IClusterReconstruction> { if (!this.hasTile(hash)) { throw new Error("Cannot cache reconstructions of a non-existing tile."); } if (this.hasClusterReconstructions(hash)) { throw new Error("Cannot cache reconstructions that already exists."); } if (this.isCachingClusterReconstructions(hash)) { return this._cachingClusterReconstructions$[hash]; } const duplicatedClusters: ClusterData[] = this.getTile(hash) .filter( (n: Node): boolean => { return !!n.clusterKey && !!n.clusterUrl; }) .map( (n: Node): ClusterData => { return { key: n.clusterKey, url: n.clusterUrl }; }); const clusters: ClusterData[] = Array .from<ClusterData>( new Map( duplicatedClusters.map( (cd: ClusterData): [string, ClusterData] => { return [cd.key, cd]; })) .values()); this._tileClusters[hash] = clusters; this._cacheRequests[hash] = []; this._cachingClusterReconstructions$[hash] = observableFrom(clusters).pipe( mergeMap( (cd: ClusterData): Observable<IClusterReconstruction> => { if (this._hasClusterReconstruction(cd.key)) { return observableOf(this._getClusterReconstruction(cd.key)); } let aborter: Function; const abort: Promise<void> = new Promise( (_, reject): void => { aborter = reject; }); this._cacheRequests[hash].push(aborter); return this._getClusterReconstruction$(cd.url, cd.key, abort) .pipe( catchError( (error: Error): Observable<IClusterReconstruction> => { if (error instanceof AbortMapillaryError) { return observableEmpty(); } console.error(error); return observableEmpty(); })); }, 6), filter( (): boolean => { return hash in this._tileClusters; }), tap( (reconstruction: IClusterReconstruction): void => { if (!this._hasClusterReconstruction(reconstruction.key)) { this._clusterReconstructions[reconstruction.key] = reconstruction; } if (!(reconstruction.key in this._clusterReconstructionTiles)) { this._clusterReconstructionTiles[reconstruction.key] = []; } if (this._clusterReconstructionTiles[reconstruction.key].indexOf(hash) === -1) { this._clusterReconstructionTiles[reconstruction.key].push(hash); } }), finalize( (): void => { if (hash in this._cachingClusterReconstructions$) { delete this._cachingClusterReconstructions$[hash]; } if (hash in this._cacheRequests) { delete this._cacheRequests[hash]; } }), publish(), refCount()); return this._cachingClusterReconstructions$[hash]; } public cacheTile$(hash: string): Observable<Node[]> { if (this.hasTile(hash)) { throw new Error("Cannot cache tile that already exists."); } if (this.isCachingTile(hash)) { return this._cachingTiles$[hash]; } const corners: ICellCorners = this._data.geometry.getCorners(hash); this._cachingTiles$[hash] = this._graphService.cacheBoundingBox$(corners.sw, corners.ne).pipe( catchError( (error: Error): Observable<Node[]> => { console.error(error); return observableEmpty(); }), filter( (): boolean => { return !(hash in this._tiles); }), tap( (node: Node[]): void => { this._tiles[hash] = []; this._tiles[hash].push(...node); delete this._cachingTiles$[hash]; }), finalize( (): void => { if (hash in this._cachingTiles$) { delete this._cachingTiles$[hash]; } }), publish(), refCount()); return this._cachingTiles$[hash]; } public isCachingClusterReconstructions(hash: string): boolean { return hash in this._cachingClusterReconstructions$; } public isCachingTile(hash: string): boolean { return hash in this._cachingTiles$; } public hasClusterReconstructions(hash: string): boolean { if (hash in this._cachingClusterReconstructions$ || !(hash in this._tileClusters)) { return false; } for (const cd of this._tileClusters[hash]) { if (!(cd.key in this._clusterReconstructions)) { return false; } } return true; } public hasTile(hash: string): boolean { return !(hash in this._cachingTiles$) && hash in this._tiles; } public getClusterReconstructions(hash: string): IClusterReconstruction[] { return hash in this._tileClusters ? this._tileClusters[hash] .map( (cd: ClusterData): IClusterReconstruction => { return this._clusterReconstructions[cd.key]; }) .filter( (reconstruction: IClusterReconstruction): boolean => { return !!reconstruction; }) : []; } public getTile(hash: string): Node[] { return hash in this._tiles ? this._tiles[hash] : []; } public uncache(keepHashes?: string[]): void { for (let hash of Object.keys(this._cacheRequests)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } for (const aborter of this._cacheRequests[hash]) { aborter(); } delete this._cacheRequests[hash]; } for (let hash of Object.keys(this._tileClusters)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } for (const cd of this._tileClusters[hash]) { if (!(cd.key in this._clusterReconstructionTiles)) { continue; } const index: number = this._clusterReconstructionTiles[cd.key].indexOf(hash); if (index === -1) { continue; } this._clusterReconstructionTiles[cd.key].splice(index, 1); if (this._clusterReconstructionTiles[cd.key].length > 0) { continue; } delete this._clusterReconstructionTiles[cd.key]; delete this._clusterReconstructions[cd.key]; } delete this._tileClusters[hash]; } for (let hash of Object.keys(this._tiles)) { if (!!keepHashes && keepHashes.indexOf(hash) !== -1) { continue; } delete this._tiles[hash]; } } private _getClusterReconstruction(key: string): IClusterReconstruction { return this._clusterReconstructions[key]; } private _getClusterReconstruction$(url: string, clusterKey: string, abort: Promise<void>): Observable<IClusterReconstruction> { return Observable.create( (subscriber: Subscriber<IClusterReconstruction>): void => { this._data.getClusterReconstruction(url, abort) .then( (reconstruction: IClusterReconstruction): void => { reconstruction.key = clusterKey; subscriber.next(reconstruction); subscriber.complete(); }, (error: Error): void => { subscriber.error(error); }); }); } private _hasClusterReconstruction(key: string): boolean { return key in this._clusterReconstructions; } } export default SpatialDataCache;