UNPKG

mapillary-js

Version:

A WebGL interactive street imagery library

1,453 lines (1,234 loc) 66.4 kB
import { concat as observableConcat, empty as observableEmpty, from as observableFrom, merge as observableMerge, of as observableOf, Observable, Subject, Subscription, } from "rxjs"; import { catchError, finalize, map, mergeAll, mergeMap, last, publish, publishReplay, reduce, refCount, tap, } from "rxjs/operators"; import { FilterCreator, FilterFunction } from "./FilterCreator"; import { FilterExpression } from "./FilterExpression"; import { GraphCalculator } from "./GraphCalculator"; import { Image } from "./Image"; import { ImageCache } from "./ImageCache"; import { Sequence } from "./Sequence"; import { GraphConfiguration } from "./interfaces/GraphConfiguration"; import { EdgeCalculator } from "./edge/EdgeCalculator"; import { NavigationEdge } from "./edge/interfaces/NavigationEdge"; import { PotentialEdge } from "./edge/interfaces/PotentialEdge"; import { APIWrapper } from "../api/APIWrapper"; import { SpatialImageEnt } from "../api/ents/SpatialImageEnt"; import { LngLat } from "../api/interfaces/LngLat"; import { GraphMapillaryError } from "../error/GraphMapillaryError"; import { SpatialImagesContract } from "../api/contracts/SpatialImagesContract"; import { ImagesContract } from "../api/contracts/ImagesContract"; import { SequenceContract } from "../api/contracts/SequenceContract"; import { CoreImagesContract } from "../api/contracts/CoreImagesContract"; type NodeTiles = { cache: string[]; caching: string[]; }; type SpatialArea = { all: { [key: string]: Image; }; cacheKeys: string[]; cacheNodes: { [key: string]: Image; }; }; type NodeAccess = { node: Image; accessed: number; }; type TileAccess = { nodes: Image[]; accessed: number; }; type SequenceAccess = { sequence: Sequence; accessed: number; }; export type NodeIndexItem = { lat: number; lng: number; node: Image; }; /** * @class Graph * * @classdesc Represents a graph of nodes with edges. */ export class Graph { private static _spatialIndex: new (...args: any[]) => any; private _api: APIWrapper; /** * Nodes that have initialized cache with a timestamp of last access. */ private _cachedNodes: { [key: string]: NodeAccess; }; /** * Nodes for which the required tiles are cached. */ private _cachedNodeTiles: { [key: string]: boolean; }; /** * Sequences for which the nodes are cached. */ private _cachedSequenceNodes: { [sequenceKey: string]: boolean; }; /** * Nodes for which the spatial edges are cached. */ private _cachedSpatialEdges: { [key: string]: Image; }; /** * Cached tiles with a timestamp of last access. */ private _cachedTiles: { [h: string]: TileAccess; }; /** * Nodes for which fill properties are being retreived. */ private _cachingFill$: { [key: string]: Observable<Graph>; }; /** * Nodes for which full properties are being retrieved. */ private _cachingFull$: { [key: string]: Observable<Graph>; }; /** * Sequences for which the nodes are being retrieved. */ private _cachingSequenceNodes$: { [sequenceKey: string]: Observable<Graph>; }; /** * Sequences that are being retrieved. */ private _cachingSequences$: { [sequenceKey: string]: Observable<Graph>; }; /** * Nodes for which the spatial area fill properties are being retrieved. */ private _cachingSpatialArea$: { [key: string]: Observable<Graph>[]; }; /** * Tiles that are being retrieved. */ private _cachingTiles$: { [h: string]: Observable<Graph>; }; private _changed$: Subject<Graph>; private _defaultAlt: number; private _edgeCalculator: EdgeCalculator; private _graphCalculator: GraphCalculator; private _configuration: GraphConfiguration; private _filter: FilterFunction; private _filterCreator: FilterCreator; private _filterSubject$: Subject<FilterFunction>; private _filter$: Observable<FilterFunction>; private _filterSubscription: Subscription; /** * All nodes in the graph. */ private _nodes: { [key: string]: Image; }; /** * Contains all nodes in the graph. Used for fast spatial lookups. */ private _nodeIndex: any; /** * All node index items sorted in tiles for easy uncache. */ private _nodeIndexTiles: { [h: string]: NodeIndexItem[]; }; /** * Node to tile dictionary for easy tile access updates. */ private _nodeToTile: { [key: string]: string; }; /** * Nodes retrieved before tiles, stored on tile level. */ private _preStored: { [h: string]: { [key: string]: Image; }; }; /** * Tiles required for a node to retrive spatial area. */ private _requiredNodeTiles: { [key: string]: NodeTiles; }; /** * Other nodes required for node to calculate spatial edges. */ private _requiredSpatialArea: { [key: string]: SpatialArea; }; /** * All sequences in graph with a timestamp of last access. */ private _sequences: { [skey: string]: SequenceAccess; }; private _tileThreshold: number; /** * Create a new graph instance. * * @param {APIWrapper} [api] - API instance for retrieving data. * @param {rbush.RBush<NodeIndexItem>} [nodeIndex] - Node index for fast spatial retreival. * @param {GraphCalculator} [graphCalculator] - Instance for graph calculations. * @param {EdgeCalculator} [edgeCalculator] - Instance for edge calculations. * @param {FilterCreator} [filterCreator] - Instance for filter creation. * @param {GraphConfiguration} [configuration] - Configuration struct. */ constructor( api: APIWrapper, nodeIndex?: any, graphCalculator?: GraphCalculator, edgeCalculator?: EdgeCalculator, filterCreator?: FilterCreator, configuration?: GraphConfiguration) { this._api = api; this._cachedNodes = {}; this._cachedNodeTiles = {}; this._cachedSequenceNodes = {}; this._cachedSpatialEdges = {}; this._cachedTiles = {}; this._cachingFill$ = {}; this._cachingFull$ = {}; this._cachingSequenceNodes$ = {}; this._cachingSequences$ = {}; this._cachingSpatialArea$ = {}; this._cachingTiles$ = {}; this._changed$ = new Subject<Graph>(); this._filterCreator = filterCreator ?? new FilterCreator(); this._filter = this._filterCreator.createFilter(undefined); this._filterSubject$ = new Subject<FilterFunction>(); this._filter$ = observableConcat( observableOf(this._filter), this._filterSubject$).pipe( publishReplay(1), refCount()); this._filterSubscription = this._filter$.subscribe(() => { /*noop*/ }); this._defaultAlt = 2; this._edgeCalculator = edgeCalculator ?? new EdgeCalculator(); this._graphCalculator = graphCalculator ?? new GraphCalculator(); this._configuration = configuration ?? { maxSequences: 50, maxUnusedImages: 100, maxUnusedPreStoredImages: 30, maxUnusedTiles: 20, }; this._nodes = {}; this._nodeIndex = nodeIndex ?? new Graph._spatialIndex(16); this._nodeIndexTiles = {}; this._nodeToTile = {}; this._preStored = {}; this._requiredNodeTiles = {}; this._requiredSpatialArea = {}; this._sequences = {}; this._tileThreshold = 20; } public static register(spatialIndex: new (...args: any[]) => any): void { Graph._spatialIndex = spatialIndex; } /** * Get api. * * @returns {APIWrapper} The API instance used by * the graph. */ public get api(): APIWrapper { return this._api; } /** * Get changed$. * * @returns {Observable<Graph>} Observable emitting * the graph every time it has changed. */ public get changed$(): Observable<Graph> { return this._changed$; } /** * Get filter$. * * @returns {Observable<FilterFunction>} Observable emitting * the filter every time it has changed. */ public get filter$(): Observable<FilterFunction> { return this._filter$; } /** * Caches the full node data for all images within a bounding * box. * * @description The node assets are not cached. * * @param {LngLat} sw - South west corner of bounding box. * @param {LngLat} ne - North east corner of bounding box. * @returns {Observable<Array<Image>>} Observable emitting * the full nodes in the bounding box. */ public cacheBoundingBox$(sw: LngLat, ne: LngLat): Observable<Image[]> { const cacheTiles$ = this._api.data.geometry.bboxToCellIds(sw, ne) .filter( (h: string): boolean => { return !(h in this._cachedTiles); }) .map( (h: string): Observable<Graph> => { return h in this._cachingTiles$ ? this._cachingTiles$[h] : this._cacheTile$(h); }); if (cacheTiles$.length === 0) { cacheTiles$.push(observableOf(this)); } return observableFrom(cacheTiles$).pipe( mergeAll(), last(), mergeMap( (): Observable<Image[]> => { const nodes = <Image[]>this._nodeIndex .search({ maxX: ne.lng, maxY: ne.lat, minX: sw.lng, minY: sw.lat, }) .map( (item: NodeIndexItem): Image => { return item.node; }); const fullNodes: Image[] = []; const coreNodes: string[] = []; for (const node of nodes) { if (node.complete) { fullNodes.push(node); } else { coreNodes.push(node.id); } } const coreNodeBatches: string[][] = []; const batchSize = 200; while (coreNodes.length > 0) { coreNodeBatches.push(coreNodes.splice(0, batchSize)); } const fullNodes$ = observableOf(fullNodes); const fillNodes$ = coreNodeBatches .map((batch: string[]): Observable<Image[]> => { return this._api .getSpatialImages$(batch) .pipe( map((items: SpatialImagesContract) : Image[] => { const result: Image[] = []; for (const item of items) { const exists = this .hasNode(item.node_id); if (!exists) { continue; } const node = this .getNode(item.node_id); if (!node.complete) { this._makeFull(node, item.node); } result.push(node); } return result; })); }); return observableMerge( fullNodes$, observableFrom(fillNodes$).pipe( mergeAll())); }), reduce( (acc: Image[], value: Image[]): Image[] => { return acc.concat(value); })); } /** * Caches the full node data for all images of a cell. * * @description The node assets are not cached. * * @param {string} cellId - Cell id. * @returns {Observable<Array<Image>>} Observable * emitting the full nodes of the cell. */ public cacheCell$(cellId: string): Observable<Image[]> { const cacheCell$ = cellId in this._cachedTiles ? observableOf(this) : cellId in this._cachingTiles$ ? this._cachingTiles$[cellId] : this._cacheTile$(cellId); return cacheCell$.pipe( mergeMap((): Observable<Image[]> => { const cachedCell = this._cachedTiles[cellId]; cachedCell.accessed = new Date().getTime(); const cellNodes = cachedCell.nodes; const fullNodes: Image[] = []; const coreNodes: string[] = []; for (const node of cellNodes) { if (node.complete) { fullNodes.push(node); } else { coreNodes.push(node.id); } } const coreNodeBatches: string[][] = []; const batchSize: number = 200; while (coreNodes.length > 0) { coreNodeBatches.push(coreNodes.splice(0, batchSize)); } const fullNodes$ = observableOf(fullNodes); const fillNodes$ = coreNodeBatches .map((batch: string[]): Observable<Image[]> => { return this._api.getSpatialImages$(batch).pipe( map((items: SpatialImagesContract): Image[] => { const filled: Image[] = []; for (const item of items) { if (!item.node) { console.warn( `Image is empty (${item.node})`); continue; } const id = item.node_id; if (!this.hasNode(id)) { continue; } const node = this.getNode(id); if (!node.complete) { this._makeFull(node, item.node); } filled.push(node); } return filled; })); }); return observableMerge( fullNodes$, observableFrom(fillNodes$).pipe( mergeAll())); }), reduce( (acc: Image[], value: Image[]): Image[] => { return acc.concat(value); })); } /** * Retrieve and cache node fill properties. * * @param {string} key - Key of node to fill. * @returns {Observable<Graph>} Observable emitting the graph * when the node has been updated. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheFill$(key: string): Observable<Graph> { if (key in this._cachingFull$) { throw new GraphMapillaryError(`Cannot fill node while caching full (${key}).`); } if (!this.hasNode(key)) { throw new GraphMapillaryError(`Cannot fill node that does not exist in graph (${key}).`); } if (key in this._cachingFill$) { return this._cachingFill$[key]; } const node = this.getNode(key); if (node.complete) { throw new GraphMapillaryError(`Cannot fill node that is already full (${key}).`); } this._cachingFill$[key] = this._api.getSpatialImages$([key]).pipe( tap( (items: SpatialImagesContract): void => { for (const item of items) { if (!item.node) { console.warn(`Image is empty ${item.node_id}`); } if (!node.complete) { this._makeFull(node, item.node); } delete this._cachingFill$[item.node_id]; } }), map((): Graph => { return this; }), finalize( (): void => { if (key in this._cachingFill$) { delete this._cachingFill$[key]; } this._changed$.next(this); }), publish(), refCount()); return this._cachingFill$[key]; } /** * Retrieve and cache full node properties. * * @param {string} key - Key of node to fill. * @returns {Observable<Graph>} Observable emitting the graph * when the node has been updated. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheFull$(key: string): Observable<Graph> { if (key in this._cachingFull$) { return this._cachingFull$[key]; } if (this.hasNode(key)) { throw new GraphMapillaryError(`Cannot cache full node that already exist in graph (${key}).`); } this._cachingFull$[key] = this._api.getImages$([key]).pipe( tap( (items: ImagesContract): void => { for (const item of items) { if (!item.node) { throw new GraphMapillaryError( `Image does not exist (${key}, ${item.node}).`); } const id = item.node_id; if (this.hasNode(id)) { const node = this.getNode(key); if (!node.complete) { this._makeFull(node, item.node); } } else { if (item.node.sequence.id == null) { throw new GraphMapillaryError( `Image has no sequence key (${key}).`); } const node = new Image(item.node); this._makeFull(node, item.node); const cellId = this._api.data.geometry .lngLatToCellId(node.originalLngLat); this._preStore(cellId, node); this._setNode(node); delete this._cachingFull$[id]; } } }), map((): Graph => this), finalize( (): void => { if (key in this._cachingFull$) { delete this._cachingFull$[key]; } this._changed$.next(this); }), publish(), refCount()); return this._cachingFull$[key]; } /** * Retrieve and cache a node sequence. * * @param {string} key - Key of node for which to retrieve sequence. * @returns {Observable<Graph>} Observable emitting the graph * when the sequence has been retrieved. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheNodeSequence$(key: string): Observable<Graph> { if (!this.hasNode(key)) { throw new GraphMapillaryError(`Cannot cache sequence edges of node that does not exist in graph (${key}).`); } let node: Image = this.getNode(key); if (node.sequenceId in this._sequences) { throw new GraphMapillaryError(`Sequence already cached (${key}), (${node.sequenceId}).`); } return this._cacheSequence$(node.sequenceId); } /** * Retrieve and cache a sequence. * * @param {string} sequenceKey - Key of sequence to cache. * @returns {Observable<Graph>} Observable emitting the graph * when the sequence has been retrieved. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheSequence$(sequenceKey: string): Observable<Graph> { if (sequenceKey in this._sequences) { throw new GraphMapillaryError(`Sequence already cached (${sequenceKey})`); } return this._cacheSequence$(sequenceKey); } /** * Cache sequence edges for a node. * * @param {string} key - Key of node. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheSequenceEdges(key: string): void { let node: Image = this.getNode(key); if (!(node.sequenceId in this._sequences)) { throw new GraphMapillaryError(`Sequence is not cached (${key}), (${node.sequenceId})`); } let sequence: Sequence = this._sequences[node.sequenceId].sequence; let edges: NavigationEdge[] = this._edgeCalculator.computeSequenceEdges(node, sequence); node.cacheSequenceEdges(edges); } /** * Retrieve and cache full nodes for all keys in a sequence. * * @param {string} sequenceKey - Key of sequence. * @param {string} referenceNodeKey - Key of node to use as reference * for optimized caching. * @returns {Observable<Graph>} Observable emitting the graph * when the nodes of the sequence has been cached. */ public cacheSequenceNodes$(sequenceKey: string, referenceNodeKey?: string): Observable<Graph> { if (!this.hasSequence(sequenceKey)) { throw new GraphMapillaryError( `Cannot cache sequence nodes of sequence that does not exist in graph (${sequenceKey}).`); } if (this.hasSequenceNodes(sequenceKey)) { throw new GraphMapillaryError(`Sequence nodes already cached (${sequenceKey}).`); } const sequence: Sequence = this.getSequence(sequenceKey); if (sequence.id in this._cachingSequenceNodes$) { return this._cachingSequenceNodes$[sequence.id]; } const batches: string[][] = []; const keys: string[] = sequence.imageIds.slice(); const referenceBatchSize: number = 50; if (!!referenceNodeKey && keys.length > referenceBatchSize) { const referenceIndex: number = keys.indexOf(referenceNodeKey); const startIndex: number = Math.max( 0, Math.min( referenceIndex - referenceBatchSize / 2, keys.length - referenceBatchSize)); batches.push(keys.splice(startIndex, referenceBatchSize)); } const batchSize: number = 200; while (keys.length > 0) { batches.push(keys.splice(0, batchSize)); } let batchesToCache: number = batches.length; const sequenceNodes$: Observable<Graph> = observableFrom(batches).pipe( mergeMap( (batch: string[]): Observable<Graph> => { return this._api.getImages$(batch).pipe( tap( (items: ImagesContract): void => { for (const item of items) { if (!item.node) { console.warn( `Image empty (${item.node_id})`); continue; } const id = item.node_id; if (this.hasNode(id)) { const node = this.getNode(id); if (!node.complete) { this._makeFull(node, item.node); } } else { if (item.node.sequence.id == null) { console.warn(`Sequence missing, discarding node (${item.node_id})`); } const node = new Image(item.node); this._makeFull(node, item.node); const cellId = this._api.data.geometry .lngLatToCellId(node.originalLngLat); this._preStore(cellId, node); this._setNode(node); } } batchesToCache--; }), map((): Graph => this)); }, 6), last(), finalize( (): void => { delete this._cachingSequenceNodes$[sequence.id]; if (batchesToCache === 0) { this._cachedSequenceNodes[sequence.id] = true; } }), publish(), refCount()); this._cachingSequenceNodes$[sequence.id] = sequenceNodes$; return sequenceNodes$; } /** * Retrieve and cache full nodes for a node spatial area. * * @param {string} key - Key of node for which to retrieve sequence. * @returns {Observable<Graph>} Observable emitting the graph * when the nodes in the spatial area has been made full. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheSpatialArea$(key: string): Observable<Graph>[] { if (!this.hasNode(key)) { throw new GraphMapillaryError(`Cannot cache spatial area of node that does not exist in graph (${key}).`); } if (key in this._cachedSpatialEdges) { throw new GraphMapillaryError(`Image already spatially cached (${key}).`); } if (!(key in this._requiredSpatialArea)) { throw new GraphMapillaryError(`Spatial area not determined (${key}).`); } let spatialArea: SpatialArea = this._requiredSpatialArea[key]; if (Object.keys(spatialArea.cacheNodes).length === 0) { throw new GraphMapillaryError(`Spatial nodes already cached (${key}).`); } if (key in this._cachingSpatialArea$) { return this._cachingSpatialArea$[key]; } let batches: string[][] = []; while (spatialArea.cacheKeys.length > 0) { batches.push(spatialArea.cacheKeys.splice(0, 200)); } let batchesToCache: number = batches.length; let spatialNodes$: Observable<Graph>[] = []; for (let batch of batches) { let spatialNodeBatch$: Observable<Graph> = this._api.getSpatialImages$(batch).pipe( tap( (items: SpatialImagesContract): void => { for (const item of items) { if (!item.node) { console.warn(`Image is empty (${item.node_id})`); continue; } const id = item.node_id; const spatialNode = spatialArea.cacheNodes[id]; if (spatialNode.complete) { delete spatialArea.cacheNodes[id]; continue; } this._makeFull(spatialNode, item.node); delete spatialArea.cacheNodes[id]; } if (--batchesToCache === 0) { delete this._cachingSpatialArea$[key]; } }), map((): Graph => { return this; }), catchError( (error: Error): Observable<Graph> => { for (let batchKey of batch) { if (batchKey in spatialArea.all) { delete spatialArea.all[batchKey]; } if (batchKey in spatialArea.cacheNodes) { delete spatialArea.cacheNodes[batchKey]; } } if (--batchesToCache === 0) { delete this._cachingSpatialArea$[key]; } throw error; }), finalize( (): void => { if (Object.keys(spatialArea.cacheNodes).length === 0) { this._changed$.next(this); } }), publish(), refCount()); spatialNodes$.push(spatialNodeBatch$); } this._cachingSpatialArea$[key] = spatialNodes$; return spatialNodes$; } /** * Cache spatial edges for a node. * * @param {string} key - Key of node. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheSpatialEdges(key: string): void { if (key in this._cachedSpatialEdges) { throw new GraphMapillaryError(`Spatial edges already cached (${key}).`); } let node: Image = this.getNode(key); let sequence: Sequence = this._sequences[node.sequenceId].sequence; let fallbackKeys: string[] = []; let prevKey: string = sequence.findPrev(node.id); if (prevKey != null) { fallbackKeys.push(prevKey); } let nextKey: string = sequence.findNext(node.id); if (nextKey != null) { fallbackKeys.push(nextKey); } let allSpatialNodes: { [key: string]: Image; } = this._requiredSpatialArea[key].all; let potentialNodes: Image[] = []; let filter: FilterFunction = this._filter; for (let spatialNodeKey in allSpatialNodes) { if (!allSpatialNodes.hasOwnProperty(spatialNodeKey)) { continue; } let spatialNode: Image = allSpatialNodes[spatialNodeKey]; if (filter(spatialNode)) { potentialNodes.push(spatialNode); } } let potentialEdges: PotentialEdge[] = this._edgeCalculator.getPotentialEdges(node, potentialNodes, fallbackKeys); let edges: NavigationEdge[] = this._edgeCalculator.computeStepEdges( node, potentialEdges, prevKey, nextKey); edges = edges.concat(this._edgeCalculator.computeTurnEdges(node, potentialEdges)); edges = edges.concat(this._edgeCalculator.computeSphericalEdges(node, potentialEdges)); edges = edges.concat(this._edgeCalculator.computePerspectiveToSphericalEdges(node, potentialEdges)); edges = edges.concat(this._edgeCalculator.computeSimilarEdges(node, potentialEdges)); node.cacheSpatialEdges(edges); this._cachedSpatialEdges[key] = node; delete this._requiredSpatialArea[key]; delete this._cachedNodeTiles[key]; } /** * Retrieve and cache tiles for a node. * * @param {string} key - Key of node for which to retrieve tiles. * @returns {Array<Observable<Graph>>} Array of observables emitting * the graph for each tile required for the node has been cached. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public cacheTiles$(key: string): Observable<Graph>[] { if (key in this._cachedNodeTiles) { throw new GraphMapillaryError(`Tiles already cached (${key}).`); } if (key in this._cachedSpatialEdges) { throw new GraphMapillaryError(`Spatial edges already cached so tiles considered cached (${key}).`); } if (!(key in this._requiredNodeTiles)) { throw new GraphMapillaryError(`Tiles have not been determined (${key}).`); } let nodeTiles: NodeTiles = this._requiredNodeTiles[key]; if (nodeTiles.cache.length === 0 && nodeTiles.caching.length === 0) { throw new GraphMapillaryError(`Tiles already cached (${key}).`); } if (!this.hasNode(key)) { throw new GraphMapillaryError(`Cannot cache tiles of node that does not exist in graph (${key}).`); } let hs: string[] = nodeTiles.cache.slice(); nodeTiles.caching = this._requiredNodeTiles[key].caching.concat(hs); nodeTiles.cache = []; let cacheTiles$: Observable<Graph>[] = []; for (let h of nodeTiles.caching) { const cacheTile$: Observable<Graph> = h in this._cachingTiles$ ? this._cachingTiles$[h] : this._cacheTile$(h); cacheTiles$.push( cacheTile$.pipe( tap( (graph: Graph): void => { let index: number = nodeTiles.caching.indexOf(h); if (index > -1) { nodeTiles.caching.splice(index, 1); } if (nodeTiles.caching.length === 0 && nodeTiles.cache.length === 0) { delete this._requiredNodeTiles[key]; this._cachedNodeTiles[key] = true; } }), catchError( (error: Error): Observable<Graph> => { let index: number = nodeTiles.caching.indexOf(h); if (index > -1) { nodeTiles.caching.splice(index, 1); } if (nodeTiles.caching.length === 0 && nodeTiles.cache.length === 0) { delete this._requiredNodeTiles[key]; this._cachedNodeTiles[key] = true; } throw error; }), finalize( (): void => { this._changed$.next(this); }), publish(), refCount())); } return cacheTiles$; } /** * Initialize the cache for a node. * * @param {string} key - Key of node. * @throws {GraphMapillaryError} When the operation is not valid on the * current graph. */ public initializeCache(key: string): void { if (key in this._cachedNodes) { throw new GraphMapillaryError(`Image already in cache (${key}).`); } const node: Image = this.getNode(key); const provider = this._api.data; node.initializeCache(new ImageCache(provider)); const accessed: number = new Date().getTime(); this._cachedNodes[key] = { accessed: accessed, node: node }; this._updateCachedTileAccess(key, accessed); } /** * Get a value indicating if the graph is fill caching a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the node is being fill cached. */ public isCachingFill(key: string): boolean { return key in this._cachingFill$; } /** * Get a value indicating if the graph is fully caching a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the node is being fully cached. */ public isCachingFull(key: string): boolean { return key in this._cachingFull$; } /** * Get a value indicating if the graph is caching a sequence of a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the sequence of a node is * being cached. */ public isCachingNodeSequence(key: string): boolean { let node: Image = this.getNode(key); return node.sequenceId in this._cachingSequences$; } /** * Get a value indicating if the graph is caching a sequence. * * @param {string} sequenceKey - Key of sequence. * @returns {boolean} Value indicating if the sequence is * being cached. */ public isCachingSequence(sequenceKey: string): boolean { return sequenceKey in this._cachingSequences$; } /** * Get a value indicating if the graph is caching sequence nodes. * * @param {string} sequenceKey - Key of sequence. * @returns {boolean} Value indicating if the sequence nodes are * being cached. */ public isCachingSequenceNodes(sequenceKey: string): boolean { return sequenceKey in this._cachingSequenceNodes$; } /** * Get a value indicating if the graph is caching the tiles * required for calculating spatial edges of a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the tiles of * a node are being cached. */ public isCachingTiles(key: string): boolean { return key in this._requiredNodeTiles && this._requiredNodeTiles[key].cache.length === 0 && this._requiredNodeTiles[key].caching.length > 0; } /** * Get a value indicating if the cache has been initialized * for a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the cache has been * initialized for a node. */ public hasInitializedCache(key: string): boolean { return key in this._cachedNodes; } /** * Get a value indicating if a node exist in the graph. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if a node exist in the graph. */ public hasNode(key: string): boolean { let accessed: number = new Date().getTime(); this._updateCachedNodeAccess(key, accessed); this._updateCachedTileAccess(key, accessed); return key in this._nodes; } /** * Get a value indicating if a node sequence exist in the graph. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if a node sequence exist * in the graph. */ public hasNodeSequence(key: string): boolean { let node: Image = this.getNode(key); let sequenceKey: string = node.sequenceId; let hasNodeSequence: boolean = sequenceKey in this._sequences; if (hasNodeSequence) { this._sequences[sequenceKey].accessed = new Date().getTime(); } return hasNodeSequence; } /** * Get a value indicating if a sequence exist in the graph. * * @param {string} sequenceKey - Key of sequence. * @returns {boolean} Value indicating if a sequence exist * in the graph. */ public hasSequence(sequenceKey: string): boolean { let hasSequence: boolean = sequenceKey in this._sequences; if (hasSequence) { this._sequences[sequenceKey].accessed = new Date().getTime(); } return hasSequence; } /** * Get a value indicating if sequence nodes has been cached in the graph. * * @param {string} sequenceKey - Key of sequence. * @returns {boolean} Value indicating if a sequence nodes has been * cached in the graph. */ public hasSequenceNodes(sequenceKey: string): boolean { return sequenceKey in this._cachedSequenceNodes; } /** * Get a value indicating if the graph has fully cached * all nodes in the spatial area of a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the spatial area * of a node has been cached. */ public hasSpatialArea(key: string): boolean { if (!this.hasNode(key)) { throw new GraphMapillaryError(`Spatial area nodes cannot be determined if node not in graph (${key}).`); } if (key in this._cachedSpatialEdges) { return true; } if (key in this._requiredSpatialArea) { return Object .keys(this._requiredSpatialArea[key].cacheNodes) .length === 0; } let node = this.getNode(key); let bbox = this._graphCalculator .boundingBoxCorners( node.lngLat, this._tileThreshold); let spatialItems = <NodeIndexItem[]>this._nodeIndex .search({ maxX: bbox[1].lng, maxY: bbox[1].lat, minX: bbox[0].lng, minY: bbox[0].lat, }); let spatialNodes: SpatialArea = { all: {}, cacheKeys: [], cacheNodes: {}, }; for (let spatialItem of spatialItems) { spatialNodes.all[spatialItem.node.id] = spatialItem.node; if (!spatialItem.node.complete) { spatialNodes.cacheKeys.push(spatialItem.node.id); spatialNodes.cacheNodes[spatialItem.node.id] = spatialItem.node; } } this._requiredSpatialArea[key] = spatialNodes; return spatialNodes.cacheKeys.length === 0; } /** * Get a value indicating if the graph has a tiles required * for a node. * * @param {string} key - Key of node. * @returns {boolean} Value indicating if the the tiles required * by a node has been cached. */ public hasTiles(key: string): boolean { if (key in this._cachedNodeTiles) { return true; } if (key in this._cachedSpatialEdges) { return true; } if (!this.hasNode(key)) { throw new GraphMapillaryError(`Image does not exist in graph (${key}).`); } let nodeTiles: NodeTiles = { cache: [], caching: [] }; if (!(key in this._requiredNodeTiles)) { const node = this.getNode(key); const [sw, ne] = this._graphCalculator .boundingBoxCorners( node.lngLat, this._tileThreshold); nodeTiles.cache = this._api.data.geometry .bboxToCellIds(sw, ne) .filter( (h: string): boolean => { return !(h in this._cachedTiles); }); if (nodeTiles.cache.length > 0) { this._requiredNodeTiles[key] = nodeTiles; } } else { nodeTiles = this._requiredNodeTiles[key]; } return nodeTiles.cache.length === 0 && nodeTiles.caching.length === 0; } /** * Get a node. * * @param {string} key - Key of node. * @returns {Image} Retrieved node. */ public getNode(key: string): Image { let accessed: number = new Date().getTime(); this._updateCachedNodeAccess(key, accessed); this._updateCachedTileAccess(key, accessed); return this._nodes[key]; } /** * Get a sequence. * * @param {string} sequenceKey - Key of sequence. * @returns {Image} Retrieved sequence. */ public getSequence(sequenceKey: string): Sequence { let sequenceAccess: SequenceAccess = this._sequences[sequenceKey]; sequenceAccess.accessed = new Date().getTime(); return sequenceAccess.sequence; } /** * Reset all spatial edges of the graph nodes. */ public resetSpatialEdges(): void { let cachedKeys: string[] = Object.keys(this._cachedSpatialEdges); for (let cachedKey of cachedKeys) { let node: Image = this._cachedSpatialEdges[cachedKey]; node.resetSpatialEdges(); delete this._cachedSpatialEdges[cachedKey]; } } /** * Reset the complete graph but keep the nodes corresponding * to the supplied keys. All other nodes will be disposed. * * @param {Array<string>} keepKeys - Keys for nodes to keep * in graph after reset. */ public reset(keepKeys: string[]): void { const nodes: Image[] = []; for (const key of keepKeys) { if (!this.hasNode(key)) { throw new Error(`Image does not exist ${key}`); } const node: Image = this.getNode(key); node.resetSequenceEdges(); node.resetSpatialEdges(); nodes.push(node); } for (let cachedKey of Object.keys(this._cachedNodes)) { if (keepKeys.indexOf(cachedKey) !== -1) { continue; } this._cachedNodes[cachedKey].node.dispose(); delete this._cachedNodes[cachedKey]; } this._cachedNodeTiles = {}; this._cachedSpatialEdges = {}; this._cachedTiles = {}; this._cachingFill$ = {}; this._cachingFull$ = {}; this._cachingSequences$ = {}; this._cachingSpatialArea$ = {}; this._cachingTiles$ = {}; this._nodes = {}; this._nodeToTile = {}; this._preStored = {}; for (const node of nodes) { this._nodes[node.id] = node; const h: string = this._api.data.geometry.lngLatToCellId(node.originalLngLat); this._preStore(h, node); } this._requiredNodeTiles = {}; this._requiredSpatialArea = {}; this._sequences = {}; this._nodeIndexTiles = {}; this._nodeIndex.clear(); } /** * Set the spatial node filter. * * @emits FilterFunction The filter function to the {@link Graph.filter$} * observable. * * @param {FilterExpression} filter - Filter expression to be applied * when calculating spatial edges. */ public setFilter(filter: FilterExpression): void { this._filter = this._filterCreator.createFilter(filter); this._filterSubject$.next(this._filter); } /** * Uncache the graph according to the graph configuration. * * @description Uncaches unused tiles, unused nodes and * sequences according to the numbers specified in the * graph configuration. Sequences does not have a direct * reference to either tiles or nodes and may be uncached * even if they are related to the nodes that should be kept. * * @param {Array<string>} keepIds - Ids of nodes to keep in * graph unrelated to last access. Tiles related to those keys * will also be kept in graph. * @param {Array<string>} keepCellIds - Ids of cells to keep in * graph unrelated to last access. The nodes of the cells may * still be uncached if not specified in the keep ids param * but are guaranteed to not be disposed. * @param {string} keepSequenceId - Optional id 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 ids param but are guaranteed to not * be disposed. */ public uncache( keepIds: string[], keepCellIds: string[], keepSequenceId?: string) : void { const idsInUse: { [id: string]: boolean; } = {}; this._addNewKeys(idsInUse, this._cachingFull$); this._addNewKeys(idsInUse, this._cachingFill$); this._addNewKeys(idsInUse, this._cachingSpatialArea$); this._addNewKeys(idsInUse, this._requiredNodeTiles); this._addNewKeys(idsInUse, this._requiredSpatialArea); for (const key of keepIds) { if (key in idsInUse) { continue; } idsInUse[key] = true; } const tileThreshold = this._tileThreshold; const calculator = this._graphCalculator; const geometry = this._api.data.geometry; const keepCells = new Set<string>(keepCellIds); for (let id in idsInUse) { if (!idsInUse.hasOwnProperty(id)) { continue; } const node = this._nodes[id]; const [sw, ne] = calculator .boundingBoxCorners( node.lngLat, tileThreshold, ); const nodeCellIds = geometry.bboxToCellIds(sw, ne); for (const nodeCellId of nodeCellIds) { if (!keepCells.has(nodeCellId)) { keepCells.add(nodeCellId); } } } const potentialCells: [string, TileAccess][]