2023-11-15 15:32:10 +01:00

2158 lines
73 KiB
TypeScript

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<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;
/**
* Nodes of clusters.
*/
private _clusterNodes: Map<string, Set<string>>;
/**
* 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<string, NodeIndexItem>;
/**
* 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<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,
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<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._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<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}).`);
}
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<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})`);
}
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<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 => {
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<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 {
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<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 geometry = this._api.data.geometry;
const keepCells = new Set<string>(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<string>} cellIds - Cell ids.
* @returns {Observable<Array<string>>} Observable
* emitting the updated cells.
*/
public updateCells$(cellIds: string[]): Observable<string> {
const cachedCells = this._cachedTiles;
const cachingCells = this._cachingTiles$;
return observableFrom(cellIds)
.pipe(
mergeMap(
(cellId: string): Observable<string> => {
if (cellId in cachedCells) {
return this._updateCell$(cellId);
}
if (cellId in cachingCells) {
return cachingCells[cellId]
.pipe(
catchError((): Observable<Graph> => {
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<string>} clusterIds - Cluster ids.
* @returns {Observable<Array<string>>} Observable
* emitting the IDs for the deleted clusters.
*/
public deleteClusters$(clusterIds: string[]): Observable<string> {
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<T>(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<Graph> {
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<Graph> {
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<Graph> => {
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<string> {
return this._api.getCoreImages$(cellId).pipe(
mergeMap(
(contract: CoreImagesContract): Observable<string> => {
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<string> => {
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;
}
}