import { concat as observableConcat, empty as observableEmpty, from as observableFrom, merge as observableMerge, of as observableOf, Observable, Subject, Subscription, } from "rxjs"; import { catchError, filter as filterObservable, 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 = { cellId: string, id: string, lat: number; lng: number; node: Image; }; export type GraphOptions = { computedGraph?: boolean; } /** * @class Graph * * @classdesc Represents a graph of nodes with edges. */ export class Graph { private static _spatialIndex: new (...args: any[]) => any; private _computedGraph: boolean; 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; }; /** * Nodes for which full properties are being retrieved. */ private _cachingFull$: { [key: string]: Observable; }; /** * Sequences for which the nodes are being retrieved. */ private _cachingSequenceNodes$: { [sequenceKey: string]: Observable; }; /** * Sequences that are being retrieved. */ private _cachingSequences$: { [sequenceKey: string]: Observable; }; /** * Nodes for which the spatial area fill properties are being retrieved. */ private _cachingSpatialArea$: { [key: string]: Observable[]; }; /** * Tiles that are being retrieved. */ private _cachingTiles$: { [h: string]: Observable; }; private _changed$: Subject; private _defaultAlt: number; private _edgeCalculator: EdgeCalculator; private _graphCalculator: GraphCalculator; private _configuration: GraphConfiguration; private _filter: FilterFunction; private _filterCreator: FilterCreator; private _filterSubject$: Subject; private _filter$: Observable; private _filterSubscription: Subscription; /** * Nodes of clusters. */ private _clusterNodes: Map>; /** * 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 _nodeIndexNodes: Map; /** * 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; }; }; /** * Nodes deleted through event, not yet possible to determine if they can be disposed. */ private _preDeletedNodes: Map; /** * 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} [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, options?: GraphOptions, nodeIndex?: any, graphCalculator?: GraphCalculator, edgeCalculator?: EdgeCalculator, filterCreator?: FilterCreator, configuration?: GraphConfiguration) { this._api = api; this._computedGraph = options?.computedGraph ?? false; 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(); this._filterCreator = filterCreator ?? new FilterCreator(); this._filter = this._filterCreator.createFilter(undefined); this._filterSubject$ = new Subject(); 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._clusterNodes = new Map(); this._nodes = {}; this._nodeIndex = nodeIndex ?? new Graph._spatialIndex(16); this._nodeIndexNodes = new Map(); this._nodeIndexTiles = {}; this._nodeToTile = {}; this._preStored = {}; this._preDeletedNodes = new Map(); 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} Observable emitting * the graph every time it has changed. */ public get changed$(): Observable { return this._changed$; } /** * Get filter$. * * @returns {Observable} Observable emitting * the filter every time it has changed. */ public get filter$(): Observable { 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>} Observable emitting * the full nodes in the bounding box. */ public cacheBoundingBox$(sw: LngLat, ne: LngLat): Observable { const cacheTiles$ = this._api.data.geometry.bboxToCellIds(sw, ne) .filter( (h: string): boolean => { return !(h in this._cachedTiles); }) .map( (h: string): Observable => { 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 => { const nodes = 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 => { 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>} Observable * emitting the full nodes of the cell. */ public cacheCell$(cellId: string): Observable { const cacheCell$ = cellId in this._cachedTiles ? observableOf(this) : cellId in this._cachingTiles$ ? this._cachingTiles$[cellId] : this._cacheTile$(cellId); return cacheCell$.pipe( mergeMap((): Observable => { 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 => { 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} 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 { 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} 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 { 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}).`); } let node: Image = null; if (this._preDeletedNodes.has(id)) { node = this._unDeleteNode(id); } else { node = new Image(item.node); } this._makeFull(node, item.node); const lngLat = this._getNodeLngLat(node); const cellId = this._api.data.geometry .lngLatToCellId(lngLat); 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} 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 { 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} 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 { 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} Observable emitting the graph * when the nodes of the sequence has been cached. */ public cacheSequenceNodes$(sequenceKey: string, referenceNodeKey?: string): Observable { 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 = observableFrom(batches).pipe( mergeMap( (batch: string[]): Observable => { 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})`); } let node: Image = null; if (this._preDeletedNodes.has(id)) { node = this._unDeleteNode(id); } else { node = new Image(item.node); } this._makeFull(node, item.node); const lngLat = this._getNodeLngLat(node); const cellId = this._api.data.geometry .lngLatToCellId(lngLat); 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} 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[] { 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[] = []; for (let batch of batches) { let spatialNodeBatch$: Observable = this._api.getSpatialImages$(batch).pipe( tap( (items: SpatialImagesContract): void => { if (!(key in this._cachingSpatialArea$)) { return; } 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 => { 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>} 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[] { 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[] = []; for (let h of nodeTiles.caching) { const cacheTile$: Observable = 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 => { 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 = 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 { for (const nodeId of Object.keys(this._cachedSpatialEdges)) { const node = this._cachedSpatialEdges[nodeId]; node.resetSpatialEdges(); } this._cachedSpatialEdges = {}; } /** * Reset all spatial areas of the graph nodes. */ public resetSpatialArea(): void { this._requiredSpatialArea = {}; this._cachingSpatialArea$ = {}; } /** * Reset the complete graph and disposed all nodes. */ public reset(): void { for (let cachedKey of Object.keys(this._cachedNodes)) { this._cachedNodes[cachedKey].node.dispose(); } this._cachedNodes = {}; this._cachedNodeTiles = {}; this._cachedSpatialEdges = {}; this._cachedTiles = {}; this._cachingFill$ = {}; this._cachingFull$ = {}; this._cachingSequences$ = {}; this._cachingSpatialArea$ = {}; this._cachingTiles$ = {}; this._clusterNodes = new Map(); this._nodes = {}; this._nodeToTile = {}; this._preStored = {}; this._preDeletedNodes = new Map(); this._requiredNodeTiles = {}; this._requiredSpatialArea = {}; this._sequences = {}; this._nodeIndexNodes = new Map(); 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} 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} 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 geometry = this._api.data.geometry; const keepCells = new Set(keepCellIds); const potentialCells: [string, TileAccess][] = []; for (const cellId in this._cachedTiles) { if (!this._cachedTiles.hasOwnProperty(cellId) || keepCells.has(cellId)) { continue; } potentialCells.push([cellId, this._cachedTiles[cellId]]); } const sortedPotentialCells = potentialCells .sort( (h1: [string, TileAccess], h2: [string, TileAccess]): number => { return h2[1].accessed - h1[1].accessed; }); const keepPotentialCells = sortedPotentialCells .slice(0, this._configuration.maxUnusedTiles) .map( (h: [string, TileAccess]): string => { return h[0]; }); for (const potentialCell of keepPotentialCells) { keepCells.add(potentialCell); } for (const id in idsInUse) { if (!idsInUse.hasOwnProperty(id)) { continue; } if (!this.hasNode(id)) { continue; } const node = this._nodes[id]; const lngLat = this._getNodeLngLat(node); const nodeCellId = geometry.lngLatToCellId(lngLat); if (!keepCells.has(nodeCellId)) { if (id in this._cachedNodeTiles) { delete this._cachedNodeTiles[id]; } if (id in this._nodeToTile) { delete this._nodeToTile[id]; } if (id in this._cachedNodeTiles) { delete this._cachedNodeTiles[id]; } if (id in this._cachedSpatialEdges) { delete this._cachedSpatialEdges[id]; } if (node.hasInitializedCache()) { node.resetSpatialEdges(); } if (nodeCellId in this._cachedTiles) { const index = this._cachedTiles[nodeCellId].nodes .findIndex(n => n.id === id); if (index !== -1) { this._cachedTiles[nodeCellId].nodes.splice(index, 1); } } this._preStore(nodeCellId, node); } } const uncacheCells = sortedPotentialCells .slice(this._configuration.maxUnusedTiles) .map( (h: [string, TileAccess]): string => { return h[0]; }); for (let uncacheCell of uncacheCells) { this._uncacheTile(uncacheCell, keepSequenceId); } const potentialPreStored: [NodeAccess, string][] = []; const nonCachedPreStored: [string, string][] = []; for (let cellId in this._preStored) { if (!this._preStored.hasOwnProperty(cellId) || cellId in this._cachingTiles$) { continue; } const prestoredNodes = this._preStored[cellId]; for (let id in prestoredNodes) { if (!prestoredNodes.hasOwnProperty(id) || id in idsInUse) { continue; } if (prestoredNodes[id].sequenceId === keepSequenceId) { continue; } if (id in this._cachedNodes) { potentialPreStored.push([this._cachedNodes[id], cellId]); } else { nonCachedPreStored.push([id, cellId]); } } } const uncachePreStored = potentialPreStored .sort( ([na1]: [NodeAccess, string], [na2]: [NodeAccess, string]): number => { return na2.accessed - na1.accessed; }) .slice(this._configuration.maxUnusedPreStoredImages) .map( ([na, h]: [NodeAccess, string]): [string, string] => { return [na.node.id, h]; }); this._uncachePreStored(nonCachedPreStored); this._uncachePreStored(uncachePreStored); const potentialNodes: NodeAccess[] = []; for (let id in this._cachedNodes) { if (!this._cachedNodes.hasOwnProperty(id) || id in idsInUse) { continue; } potentialNodes.push(this._cachedNodes[id]); } const uncacheNodes = potentialNodes .sort( (n1: NodeAccess, n2: NodeAccess): number => { return n2.accessed - n1.accessed; }) .slice(this._configuration.maxUnusedImages); for (const nodeAccess of uncacheNodes) { nodeAccess.node.uncache(); const id = nodeAccess.node.id; delete this._cachedNodes[id]; if (id in this._cachedNodeTiles) { delete this._cachedNodeTiles[id]; } if (id in this._cachedSpatialEdges) { delete this._cachedSpatialEdges[id]; } } for (const [nodeId, node] of this._preDeletedNodes.entries()) { if (nodeId in idsInUse) { continue; } if (nodeId in this._cachedNodes) { delete this._cachedNodes[nodeId]; } this._preDeletedNodes.delete(nodeId); node.dispose(); } const potentialSequences: SequenceAccess[] = []; for (let sequenceId in this._sequences) { if (!this._sequences.hasOwnProperty(sequenceId) || sequenceId in this._cachingSequences$ || sequenceId === keepSequenceId) { continue; } potentialSequences.push(this._sequences[sequenceId]); } const uncacheSequences = potentialSequences .sort( (s1: SequenceAccess, s2: SequenceAccess): number => { return s2.accessed - s1.accessed; }) .slice(this._configuration.maxSequences); for (const sequenceAccess of uncacheSequences) { const sequenceId = sequenceAccess.sequence.id; delete this._sequences[sequenceId]; if (sequenceId in this._cachedSequenceNodes) { delete this._cachedSequenceNodes[sequenceId]; } sequenceAccess.sequence.dispose(); } } /** * Updates existing cells with new core nodes. * * @description Non-existing cells are discarded * and not requested at all. * * Existing nodes are not changed. * * New nodes are not made full or getting assets * cached. * * @param {Array} cellIds - Cell ids. * @returns {Observable>} Observable * emitting the updated cells. */ public updateCells$(cellIds: string[]): Observable { const cachedCells = this._cachedTiles; const cachingCells = this._cachingTiles$; return observableFrom(cellIds) .pipe( mergeMap( (cellId: string): Observable => { if (cellId in cachedCells) { return this._updateCell$(cellId); } if (cellId in cachingCells) { return cachingCells[cellId] .pipe( catchError((): Observable => { return observableOf(this); }), mergeMap(() => this._updateCell$(cellId))); } return observableEmpty(); } )); } /** * Deletes clusters. * * @description Existing nodes for the clusters are deleted * and placed in a deleted store. The deleted store will be * purged during uncaching if the nodes are no longer in use. * * Nodes in the deleted store are always removed on reset. * * @param {Array} clusterIds - Cluster ids. * @returns {Observable>} Observable * emitting the IDs for the deleted clusters. */ public deleteClusters$(clusterIds: string[]): Observable { if (!clusterIds.length) { return observableEmpty(); } return observableFrom(clusterIds) .pipe( map( (clusterId: string): string | null => { if (!this._clusterNodes.has(clusterId)) { return null; } const clusterNodes = this._clusterNodes.get(clusterId); for (const nodeId of clusterNodes.values()) { const node = this._nodes[nodeId]; delete this._nodes[nodeId]; if (nodeId in this._cachedNodeTiles) { delete this._cachedNodeTiles[nodeId]; } if (nodeId in this._cachedNodeTiles) { delete this._cachedNodeTiles[nodeId]; } if (nodeId in this._nodeToTile) { const nodeCellId = this._nodeToTile[nodeId]; if (nodeCellId in this._cachedTiles) { const tileIndex = this._cachedTiles[nodeCellId].nodes .findIndex(n => n.id === nodeId); if (tileIndex !== -1) { this._cachedTiles[nodeCellId].nodes.splice(tileIndex, 1); } } delete this._nodeToTile[nodeId]; } const item = this._nodeIndexNodes.get(nodeId); this._nodeIndex.remove(item); this._nodeIndexNodes.delete(nodeId); const cell = this._nodeIndexTiles[item.cellId]; const nodeIndex = cell.indexOf(item); if (nodeIndex === -1) { throw new GraphMapillaryError(`Corrupt graph index cell (${nodeId})`); } cell.splice(nodeIndex, 1); this._preDeletedNodes.set(nodeId, node); } this._clusterNodes.delete(clusterId); return clusterId; }), filterObservable((clusterId: string | null): boolean => { return clusterId != null; })); } /** * Unsubscribes all subscriptions. * * @description Afterwards, you must not call any other methods * on the graph instance. */ public unsubscribe(): void { this._filterSubscription.unsubscribe(); } private _addNewKeys(keys: { [key: string]: boolean; }, dict: { [key: string]: T; }): void { for (let key in dict) { if (!dict.hasOwnProperty(key) || !this.hasNode(key)) { continue; } if (!(key in keys)) { keys[key] = true; } } } private _cacheSequence$(sequenceId: string): Observable { if (sequenceId in this._cachingSequences$) { return this._cachingSequences$[sequenceId]; } this._cachingSequences$[sequenceId] = this._api .getSequence$(sequenceId) .pipe( tap( (sequence: SequenceContract): void => { if (!sequence) { console.warn( `Sequence does not exist ` + `(${sequenceId})`); } else { if (!(sequence.id in this._sequences)) { this._sequences[sequence.id] = { accessed: new Date().getTime(), sequence: new Sequence(sequence), }; } delete this._cachingSequences$[sequenceId]; } }), map((): Graph => { return this; }), finalize( (): void => { if (sequenceId in this._cachingSequences$) { delete this._cachingSequences$[sequenceId]; } this._changed$.next(this); }), publish(), refCount()); return this._cachingSequences$[sequenceId]; } private _cacheTile$(cellId: string): Observable { this._cachingTiles$[cellId] = this._api .getCoreImages$(cellId) .pipe( tap((contract: CoreImagesContract): void => { if (cellId in this._cachedTiles) { return; } const cores = contract.images; this._nodeIndexTiles[cellId] = []; this._cachedTiles[cellId] = { accessed: new Date().getTime(), nodes: [], }; const hCache = this._cachedTiles[cellId].nodes; const preStored = this._removeFromPreStore(cellId); const preDeleted = this._preDeletedNodes; for (const core of cores) { if (!core) { break; } if (core.sequence.id == null) { console.warn(`Sequence missing, discarding ` + `node (${core.id})`); continue; } if (preStored != null && core.id in preStored) { const preStoredNode = preStored[core.id]; delete preStored[core.id]; hCache.push(preStoredNode); const preStoredNodeIndexItem: NodeIndexItem = { cellId, id: core.id, lat: preStoredNode.lngLat.lat, lng: preStoredNode.lngLat.lng, node: preStoredNode, }; this._nodeIndex.insert(preStoredNodeIndexItem); this._nodeIndexNodes.set(core.id, preStoredNodeIndexItem); this._nodeIndexTiles[cellId] .push(preStoredNodeIndexItem); this._nodeToTile[preStoredNode.id] = cellId; continue; } let node: Image = null; if (preDeleted.has(core.id)) { node = preDeleted.get(core.id); preDeleted.delete(core.id); } else { node = new Image(core); } hCache.push(node); const nodeIndexItem: NodeIndexItem = { cellId, id: node.id, lat: node.lngLat.lat, lng: node.lngLat.lng, node: node, }; this._nodeIndex.insert(nodeIndexItem); this._nodeIndexNodes.set(node.id, nodeIndexItem); this._nodeIndexTiles[cellId].push(nodeIndexItem); this._nodeToTile[node.id] = cellId; this._setNode(node); } delete this._cachingTiles$[cellId]; }), map((): Graph => this), catchError( (error: Error): Observable => { delete this._cachingTiles$[cellId]; throw error; }), publish(), refCount()); return this._cachingTiles$[cellId]; } private _addClusterNode(node: Image): void { const clusterId = node.clusterId; if (clusterId == null) { console.warn(`Cannot set cluster node, cluster ID is undefined for node ${node.id}.`); return; } if (!this._clusterNodes.has(clusterId)) { this._clusterNodes.set(clusterId, new Set()); } const clusterNodes = this._clusterNodes.get(clusterId); if (clusterNodes.has(node.id)) { throw new GraphMapillaryError(`Cluster has image (${clusterId}, ${node.id}).`); } clusterNodes.add(node.id); } private _removeClusterNode(node: Image): void { if (!node.isComplete) { return; } const clusterId = node.clusterId; if (clusterId == null || !this._clusterNodes.has(clusterId)) { return; } const clusterNodes = this._clusterNodes.get(clusterId); clusterNodes.delete(node.id); if (!clusterNodes.size) { this._clusterNodes.delete(clusterId); } } private _makeFull(node: Image, fillNode: SpatialImageEnt): void { if (fillNode.computed_altitude == null) { fillNode.computed_altitude = this._defaultAlt; } if (fillNode.computed_rotation == null) { fillNode.computed_rotation = this._graphCalculator.rotationFromCompass(fillNode.compass_angle, fillNode.exif_orientation); } node.makeComplete(fillNode); this._addClusterNode(node); } private _disposeNode(node: Image): void { this._removeClusterNode(node); node.dispose(); } private _getNodeLngLat(node: Image): LngLat { if (!this._computedGraph) { return node.originalLngLat; } return node.lngLat; } private _preStore(h: string, node: Image): void { if (!(h in this._preStored)) { this._preStored[h] = {}; } this._preStored[h][node.id] = node; } private _removeFromPreStore(h: string): { [key: string]: Image; } { let preStored: { [key: string]: Image; } = null; if (h in this._preStored) { preStored = this._preStored[h]; delete this._preStored[h]; } return preStored; } private _setNode(node: Image): void { let key: string = node.id; if (this.hasNode(key)) { throw new GraphMapillaryError(`Image already exist (${key}).`); } this._nodes[key] = node; } private _uncacheTile(h: string, keepSequenceKey: string): void { for (let node of this._cachedTiles[h].nodes) { let key = node.id; delete this._nodeToTile[key]; if (key in this._cachedNodes) { delete this._cachedNodes[key]; } if (key in this._cachedNodeTiles) { delete this._cachedNodeTiles[key]; } if (key in this._cachedSpatialEdges) { delete this._cachedSpatialEdges[key]; } if (node.sequenceId === keepSequenceKey) { this._preStore(h, node); node.uncache(); } else { delete this._nodes[key]; if (node.sequenceId in this._cachedSequenceNodes) { delete this._cachedSequenceNodes[node.sequenceId]; } this._disposeNode(node); } } for (let nodeIndexItem of this._nodeIndexTiles[h]) { this._nodeIndex.remove(nodeIndexItem); this._nodeIndexNodes.delete(nodeIndexItem.id); } delete this._nodeIndexTiles[h]; delete this._cachedTiles[h]; } private _uncachePreStored(preStored: [string, string][]): void { let hs: { [h: string]: boolean; } = {}; for (let [key, h] of preStored) { if (key in this._nodes) { delete this._nodes[key]; } if (key in this._cachedNodes) { delete this._cachedNodes[key]; } const node = this._preStored[h][key]; if (node.sequenceId in this._cachedSequenceNodes) { delete this._cachedSequenceNodes[node.sequenceId]; } delete this._preStored[h][key]; this._disposeNode(node); hs[h] = true; } for (let h in hs) { if (!hs.hasOwnProperty(h)) { continue; } if (Object.keys(this._preStored[h]).length === 0) { delete this._preStored[h]; } } } private _updateCachedTileAccess(key: string, accessed: number): void { if (key in this._nodeToTile) { this._cachedTiles[this._nodeToTile[key]].accessed = accessed; } } private _updateCachedNodeAccess(key: string, accessed: number): void { if (key in this._cachedNodes) { this._cachedNodes[key].accessed = accessed; } } private _updateCell$(cellId: string): Observable { return this._api.getCoreImages$(cellId).pipe( mergeMap( (contract: CoreImagesContract): Observable => { if (!(cellId in this._cachedTiles)) { return observableEmpty(); } const nodeIndex = this._nodeIndex; const nodeIndexCell = this._nodeIndexTiles[cellId]; const nodeIndexNodes = this._nodeIndexNodes; const nodeToCell = this._nodeToTile; const cell = this._cachedTiles[cellId]; cell.accessed = new Date().getTime(); const cellNodes = cell.nodes; const cores = contract.images; for (const core of cores) { if (core == null) { break; } if (this.hasNode(core.id)) { continue; } if (core.sequence.id == null) { console.warn(`Sequence missing, discarding ` + `node (${core.id})`); continue; } let node: Image = null; if (this._preDeletedNodes.has(core.id)) { node = this._unDeleteNode(core.id); } else { node = new Image(core); } cellNodes.push(node); const nodeIndexItem: NodeIndexItem = { cellId, id: node.id, lat: node.lngLat.lat, lng: node.lngLat.lng, node: node, }; nodeIndex.insert(nodeIndexItem); nodeIndexCell.push(nodeIndexItem); nodeIndexNodes.set(node.id, nodeIndexItem); nodeToCell[node.id] = cellId; this._setNode(node); } return observableOf(cellId); }), catchError( (error: Error): Observable => { console.error(error); return observableEmpty(); })); } private _unDeleteNode(id: string): Image { if (!this._preDeletedNodes.has(id)) { throw new GraphMapillaryError(`Pre-deleted node does not exist ${id}`); } const node = this._preDeletedNodes.get(id); this._preDeletedNodes.delete(id); if (node.isComplete) { this._addClusterNode(node); } return node; } }