feat: event api to delete cluster from graph

Handle deletion in graph.
Remove cluster from spatial on delete.
Unit tests.
This commit is contained in:
Oscar Lorentzon 2023-03-09 14:17:03 -08:00
parent 8afde1f186
commit 92f544dde5
31 changed files with 1823 additions and 364 deletions

View File

@ -585,7 +585,7 @@ export interface ImageTilesRequestContract {
/**
* @event
*/
export type ProviderEventType = "datacreate";
export type ProviderEventType = "datacreate" | "datadelete";
/**
* @interface IGeometryProvider
*
@ -775,6 +775,22 @@ export type ProviderCellEvent = {
type: "datacreate",
...
} & ProviderEvent;
/**
*
* Interface for data provider cluster events.
*/
export type ProviderClusterEvent = {
/**
* Cluster ids for clusters that have been deleted.
*/
clusterIds: string[],
/**
* Provider event type.
*/
type: "datadelete",
...
} & ProviderEvent;
/**
* @class DataProviderBase

View File

@ -56,6 +56,7 @@ module.exports = {
'react-hooks/rules-of-hooks': ERROR,
'react/prop-types': OFF, // PropTypes aren't used much these days.
'no-console': ['error', {allow: ['info', 'warn', 'error']}],
'no-continue': OFF,
'no-underscore-dangle': ['error', {allowAfterThis: true}],
'no-plusplus': ['error', {allowForLoopAfterthoughts: true}],
'no-restricted-syntax': [

View File

@ -0,0 +1,160 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {
DataProviderBase,
S2GeometryProvider,
} from '../../mapillary-js/dist/mapillary.module';
import {generateCells, generateImageBuffer} from './provider';
const IMAGE_TILE_SIZE = 10;
const IMAGE_TILES_Y = 10;
export const REFERENCE = {alt: 0, lat: 0, lng: 0};
export class ChunkDataProvider extends DataProviderBase {
constructor() {
super(new S2GeometryProvider());
this.chunks = new Map();
this.cells = new Map();
this.clusters = new Map();
this.images = new Map();
this.sequences = new Map();
}
addChunk(chunk) {
if (this.chunks.has(chunk.id)) {
throw new Error(`Chunk already exists ${chunk.id}`);
}
const {cluster, images, sequence} = chunk;
if (this.clusters.has(cluster.id)) {
throw new Error(`Cluster already exists ${cluster.id}`);
}
if (this.sequences.has(sequence.id)) {
throw new Error(`Sequence already exists ${sequence.id}`);
}
for (const imageId of images.keys()) {
if (this.images.has(imageId)) {
throw new Error(`Image already exists ${imageId}`);
}
}
this.chunks.set(chunk.id, chunk);
this.clusters.set(cluster.id, cluster);
this.sequences.set(sequence.id, sequence);
for (const image of images.values()) {
this.images.set(image.id, image);
}
const cells = generateCells(this.images.values(), this._geometry);
for (const [cellId, cellImages] of cells.entries()) {
if (!this.cells.has(cellId)) {
this.cells.set(cellId, new Map());
}
const cell = this.cells.get(cellId);
for (const cellImage of cellImages) {
cell.set(cellImage.id, cellImage);
}
}
const dataCreateEvent = {
cellIds: Array.from(cells.keys()),
target: this,
type: 'datacreate',
};
this.fire(dataCreateEvent.type, dataCreateEvent);
}
deleteChunks(chunkIds) {
for (const chunkId of chunkIds) {
if (!this.chunks.has(chunkId)) {
throw new Error(`Chunk does not exist ${chunkId}`);
}
const {cluster, images, sequence} = this.chunks.get(chunkId);
this.chunks.delete(chunkId);
this.clusters.delete(cluster.id);
this.sequences.delete(sequence.id);
for (const image of images.values()) {
this.images.delete(image.id);
const cellId = this._geometry.lngLatToCellId(image.geometry);
this.cells.get(cellId).delete(image.id);
}
}
const dataDeleteEvent = {
clusterIds: chunkIds,
target: this,
type: 'datadelete',
};
this.fire(dataDeleteEvent.type, dataDeleteEvent);
}
getCluster(url) {
if (this.clusters.has(url)) {
return Promise.resolve(this.clusters.get(url));
}
return Promise.reject(new Error(`Cluster does not exist ${url}`));
}
getCoreImages(cellId) {
const images = this.cells.has(cellId) ? this.cells.get(cellId) : new Map();
return Promise.resolve({
cell_id: cellId,
images: Array.from(images.values()),
});
}
getImages(imageIds) {
const images = [];
for (const imageId of imageIds) {
if (!this.images.has(imageId)) {
return Promise.reject(new Error(`Image does not exist ${imageId}`));
}
images.push({
node: this.images.get(imageId),
node_id: imageId,
});
}
return Promise.resolve(images);
}
// eslint-disable-next-line class-methods-use-this
getImageBuffer(url) {
return generateImageBuffer({
tileSize: IMAGE_TILE_SIZE,
tilesY: IMAGE_TILES_Y,
url,
});
}
// eslint-disable-next-line class-methods-use-this
getMesh(_url) {
return Promise.resolve({faces: [], vertices: []});
}
getSequence(sequenceId) {
if (this.sequences.has(sequenceId)) {
return Promise.resolve(this.sequences.get(sequenceId));
}
return Promise.reject(new Error(`Sequence does not exist ${sequenceId}`));
}
getSpatialImages(imageIds) {
return this.getImages(imageIds);
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {ProceduralDataProvider} from './ProceduralDataProvider';
export class DeletableProceduralDataProvider extends ProceduralDataProvider {
delete() {
if (!this.clusters.size) {
return;
}
const clusterId = this.clusters.keys().next().value;
this.clusters.delete(clusterId);
for (const [imageId, image] of this.images.entries()) {
if (image.cluster.id !== clusterId) {
continue;
}
this.images.delete(imageId);
this.sequences.delete(image.sequence.id);
for (const cellImages of this.cells.values()) {
const index = cellImages.indexOf(image);
if (index !== -1) {
cellImages.splice(index, 1);
}
}
}
this._fire([clusterId]);
}
_fire(clusterIds) {
const target = this;
const type = 'datadelete';
const event = {
clusterIds,
target,
type,
};
this.fire(type, event);
}
}

View File

@ -8,146 +8,23 @@
*/
import {
enuToGeodetic,
DataProviderBase,
S2GeometryProvider,
} from '../../mapillary-js/dist/mapillary.module';
import {generateImageBuffer} from './image';
const ASPECT_FISHEYE = 3 / 2;
const ASPECT_PERSPECTIVE = 4 / 3;
const ASPECT_SPHERICAL = 2;
const FISHEYE = 'fisheye';
const PERSPECTIVE = 'perspective';
const SPHERICAL = 'spherical';
import {
CAMERA_TYPE_FISHEYE,
CAMERA_TYPE_PERSPECTIVE,
CAMERA_TYPE_SPHERICAL,
cameraTypeToAspect,
generateCells,
generateCluster,
generateImageBuffer,
} from './provider';
export const DEFAULT_REFERENCE = {alt: 0, lat: 0, lng: 0};
export const DEFAULT_INTERVALS = 10;
function cameraTypeToAspect(cameraType) {
switch (cameraType) {
case FISHEYE:
return ASPECT_FISHEYE;
case PERSPECTIVE:
return ASPECT_PERSPECTIVE;
case SPHERICAL:
return ASPECT_SPHERICAL;
default:
throw new Error(`Camera type ${cameraType} not supported`);
}
}
function generateCells(images, geometryProvider) {
const cells = new Map();
for (const image of images) {
const cellId = geometryProvider.lngLatToCellId(image.geometry);
if (!cells.has(cellId)) {
cells.set(cellId, []);
}
cells.get(cellId).push(image);
}
return cells;
}
function generateCluster(options, intervals) {
const {cameraType, color, east, focal, height, k1, k2, width} = options;
let {idCounter} = options;
const {alt, lat, lng} = options.reference;
const distance = 5;
const images = [];
const thumbUrl = `${cameraType}`;
const clusterId = `cluster|${cameraType}`;
const sequenceId = `sequence|${cameraType}`;
const sequence = {id: sequenceId, image_ids: []};
const start = idCounter;
const end = idCounter + intervals;
while (idCounter <= end) {
const imageId = `image|${cameraType}|${idCounter}`;
const thumbId = `thumb|${cameraType}|${idCounter}`;
const meshId = `mesh|${cameraType}|${idCounter}`;
sequence.image_ids.push(imageId);
const index = idCounter - start;
const north = (-intervals * distance) / 2 + distance * index;
const up = 0;
const [computedLng, computedLat, computedAlt] = enuToGeodetic(
east,
north,
up,
lng,
lat,
alt,
);
const computedGeometry = {lat: computedLat, lng: computedLng};
const rotation = [Math.PI / 2, 0, 0];
const compassAngle = 0;
const cameraParameters = cameraType === SPHERICAL ? [] : [focal, k1, k2];
images.push({
altitude: computedAlt,
atomic_scale: 1,
camera_parameters: cameraParameters,
camera_type: cameraType,
captured_at: 0,
cluster: {id: clusterId, url: clusterId},
computed_rotation: rotation,
compass_angle: compassAngle,
computed_compass_angle: compassAngle,
computed_altitude: computedAlt,
computed_geometry: computedGeometry,
creator: {id: null, username: null},
geometry: computedGeometry,
height,
id: imageId,
merge_id: 'merge_id',
mesh: {id: meshId, url: meshId},
exif_orientation: 1,
private: null,
quality_score: 1,
sequence: {id: sequenceId},
thumb: {id: thumbId, url: thumbUrl},
owner: {id: null},
width,
});
idCounter += 1;
}
const cluster = {
colors: [],
coordinates: [],
id: clusterId,
pointIds: [],
reference: options.reference,
};
for (let i = 0; i <= intervals; i++) {
const easts = [-3, 3];
const north = (-intervals * distance) / 2 + distance * i;
const up = 0;
for (let y = 0; y < distance; y++) {
for (let z = 0; z < distance; z++) {
for (const x of easts) {
const pointId = `${i}-${z}-${y}-${x}`;
const cx = east + x;
const cy = north + y;
const cz = up + z;
cluster.pointIds.push(pointId);
cluster.coordinates.push(cx, cy, cz);
cluster.colors.push(...color);
}
}
}
}
return {cluster, images, sequence};
}
function generateClusters(options) {
const {height, intervals, reference} = options;
let {idCounter} = options;
@ -158,7 +35,7 @@ function generateClusters(options) {
const clusterConfigs = [
{
cameraType: PERSPECTIVE,
cameraType: CAMERA_TYPE_PERSPECTIVE,
color: [1, 0, 0],
east: -9,
focal: 0.8,
@ -167,7 +44,7 @@ function generateClusters(options) {
reference,
},
{
cameraType: FISHEYE,
cameraType: CAMERA_TYPE_FISHEYE,
color: [0, 1, 0],
east: 9,
focal: 0.45,
@ -176,7 +53,7 @@ function generateClusters(options) {
reference,
},
{
cameraType: SPHERICAL,
cameraType: CAMERA_TYPE_SPHERICAL,
color: [1, 1, 0],
east: 0,
reference,
@ -202,16 +79,6 @@ function generateClusters(options) {
};
}
function getImageBuffer(options) {
const {tileSize, tilesY, url} = options;
const aspect = cameraTypeToAspect(url);
return generateImageBuffer({
tileSize,
tilesX: aspect * tilesY,
tilesY,
});
}
export class ProceduralDataProvider extends DataProviderBase {
constructor(options) {
super(options.geometry ?? new S2GeometryProvider());
@ -249,7 +116,7 @@ export class ProceduralDataProvider extends DataProviderBase {
getImageBuffer(url) {
const {imageTileSize, imageTilesY} = this;
const options = {tileSize: imageTileSize, tilesY: imageTilesY, url};
return getImageBuffer(options);
return generateImageBuffer(options);
}
getMesh(url) {

View File

@ -0,0 +1,155 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import {enuToGeodetic} from '../../mapillary-js/dist/mapillary.module';
import {generateImageBuffer as genImageBuffer} from './image';
const ASPECT_FISHEYE = 3 / 2;
const ASPECT_PERSPECTIVE = 4 / 3;
const ASPECT_SPHERICAL = 2;
export const CAMERA_TYPE_FISHEYE = 'fisheye';
export const CAMERA_TYPE_PERSPECTIVE = 'perspective';
export const CAMERA_TYPE_SPHERICAL = 'spherical';
export function cameraTypeToAspect(cameraType) {
switch (cameraType) {
case CAMERA_TYPE_FISHEYE:
return ASPECT_FISHEYE;
case CAMERA_TYPE_PERSPECTIVE:
return ASPECT_PERSPECTIVE;
case CAMERA_TYPE_SPHERICAL:
return ASPECT_SPHERICAL;
default:
throw new Error(`Camera type ${cameraType} not supported`);
}
}
export function generateImageBuffer(options) {
const {tileSize, tilesY, url} = options;
const aspect = cameraTypeToAspect(url);
return genImageBuffer({
tileSize,
tilesX: aspect * tilesY,
tilesY,
});
}
export function generateCells(images, geometryProvider) {
const cells = new Map();
for (const image of images) {
const cellId = geometryProvider.lngLatToCellId(image.geometry);
if (!cells.has(cellId)) {
cells.set(cellId, []);
}
cells.get(cellId).push(image);
}
return cells;
}
export function generateCluster(options, intervals) {
const {cameraType, color, east, focal, height, id, k1, k2, width} = options;
let {idCounter} = options;
const {alt, lat, lng} = options.reference;
const distance = 5;
const images = [];
const thumbUrl = `${cameraType}`;
const clusterId = id ?? `cluster|${cameraType}`;
const sequenceId = id ?? `sequence|${cameraType}`;
const sequence = {id: sequenceId, image_ids: []};
const start = idCounter;
const end = idCounter + intervals;
while (idCounter <= end) {
const imageId = id
? `image|${id}|${cameraType}|${idCounter}`
: `image|${cameraType}|${idCounter}`;
const thumbId = `thumb|${cameraType}|${idCounter}`;
const meshId = `mesh|${cameraType}|${idCounter}`;
sequence.image_ids.push(imageId);
const index = idCounter - start;
const north = (-intervals * distance) / 2 + distance * index;
const up = 0;
const [computedLng, computedLat, computedAlt] = enuToGeodetic(
east,
north,
up,
lng,
lat,
alt,
);
const computedGeometry = {lat: computedLat, lng: computedLng};
const rotation = [Math.PI / 2, 0, 0];
const compassAngle = 0;
const cameraParameters =
cameraType === CAMERA_TYPE_SPHERICAL ? [] : [focal, k1, k2];
images.push({
altitude: computedAlt,
atomic_scale: 1,
camera_parameters: cameraParameters,
camera_type: cameraType,
captured_at: 0,
cluster: {id: clusterId, url: clusterId},
computed_rotation: rotation,
compass_angle: compassAngle,
computed_compass_angle: compassAngle,
computed_altitude: computedAlt,
computed_geometry: computedGeometry,
creator: {id: null, username: null},
geometry: computedGeometry,
height,
id: imageId,
merge_id: 'merge_id',
mesh: {id: meshId, url: meshId},
exif_orientation: 1,
private: null,
quality_score: 1,
sequence: {id: sequenceId},
thumb: {id: thumbId, url: thumbUrl},
owner: {id: null},
width,
});
idCounter += 1;
}
const cluster = {
colors: [],
coordinates: [],
id: clusterId,
pointIds: [],
reference: options.reference,
};
for (let i = 0; i <= intervals; i++) {
const easts = [-3, 3];
const north = (-intervals * distance) / 2 + distance * i;
const up = 0;
for (let y = 0; y < distance; y++) {
for (let z = 0; z < distance; z++) {
for (const x of easts) {
const pointId = `${i}-${z}-${y}-${x}`;
const cx = east + x;
const cy = north + y;
const cz = up + z;
cluster.pointIds.push(pointId);
cluster.coordinates.push(cx, cy, cz);
cluster.colors.push(...color);
}
}
}
}
return {cluster, images, sequence};
}

166
examples/debug/chunk.html Normal file
View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html>
<head>
<title>Chunk</title>
<link rel="icon" href="data:," />
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<link rel="stylesheet" href="/dist/mapillary.css" />
<style>
body {
margin: 0;
padding: 0;
}
html,
body,
.viewer {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script type="module">
import { accessToken } from "/doc-src/.access-token/token.js";
import {
CameraControls,
Viewer,
S2GeometryProvider,
} from "/dist/mapillary.module.js";
import { ChunkDataProvider } from "/doc-src/src/js/utils/ChunkDataProvider.js";
import {
CAMERA_TYPE_SPHERICAL,
cameraTypeToAspect,
generateCluster,
} from "/doc-src/src/js/utils/provider.js";
let viewer;
let dataProvider;
let chunks;
let chunkCounter = 0;
const INTERVALS = 1;
const REFERENCE = { alt: 0, lat: 0, lng: 0 };
(function main() {
const container = document.createElement("div");
container.className = "viewer";
document.body.append(container);
dataProvider = new ChunkDataProvider({
geometry: new S2GeometryProvider(18),
});
const options = {
dataProvider,
cameraControls: CameraControls.Earth,
component: {
cover: false,
image: false,
spatial: {
cameraSize: 0.3,
cellGridDepth: 2,
cellsVisible: true,
},
},
container,
imageTiling: false,
};
viewer = new Viewer(options);
chunks = [];
listen();
})();
function generateChunk() {
const cameraType = CAMERA_TYPE_SPHERICAL;
const aspect = cameraTypeToAspect(cameraType);
const height = 100;
const counter = chunks.length;
const shift = counter * 10;
const mod = 3;
const config = {
cameraType,
color: [1, (counter % mod) / (mod - 1), 0],
east: shift,
height,
id: counter.toString(),
idCounter: counter * (INTERVALS + 1),
reference: REFERENCE,
width: aspect * height,
};
const chunk = generateCluster(config, INTERVALS);
chunk.id = chunk.cluster.id;
return chunk;
}
function listen() {
window.document.addEventListener("keydown", (e) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
try {
switch (e.key) {
case "q": {
// Move to first
const imageId = dataProvider.images
.keys()
.next().value;
viewer
.moveTo(imageId)
.catch((error) => console.error(error));
break;
}
case "w": {
// Add a chunk
const added = generateChunk();
console.log(`Add chunk ${added.id}`);
chunks.push(added);
dataProvider.addChunk(added);
break;
}
case "e": {
// Delete last chunk
if (!chunks.length) {
console.log("No chunk to delete");
break;
}
const deleted = chunks.pop();
console.log(`Delete chunk ${deleted.id}`);
dataProvider.deleteChunks([deleted.id]);
break;
}
case "r": {
// Delete all except first chunk
if (!chunks.length) {
console.log("No chunks to delete");
break;
}
const deleted = chunks
.splice(1)
.map((chunk) => chunk.id);
console.log(`Delete chunks ${deleted}`);
dataProvider.deleteChunks(deleted);
break;
}
default:
break;
}
} catch (error) {
console.log(error);
}
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,95 @@
<!DOCTYPE html>
<html>
<head>
<title>Delete</title>
<link rel="icon" href="data:," />
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<link rel="stylesheet" href="/dist/mapillary.css" />
<style>
body {
margin: 0;
padding: 0;
}
html,
body,
.viewer {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script type="module">
import { accessToken } from "/doc-src/.access-token/token.js";
import {
CameraControls,
Viewer,
S2GeometryProvider,
} from "/dist/mapillary.module.js";
import { DeletableProceduralDataProvider } from "/doc-src/src/js/utils/DeletableProceduralDataProvider.js";
let viewer;
let dataProvider;
(function main() {
const container = document.createElement("div");
container.className = "viewer";
document.body.append(container);
dataProvider = new DeletableProceduralDataProvider({
geometry: new S2GeometryProvider(18),
});
const options = {
dataProvider,
cameraControls: CameraControls.Earth,
component: {
cover: false,
spatial: {
cameraSize: 0.3,
cellGridDepth: 2,
cellsVisible: true,
},
},
container,
imageTiling: false,
};
viewer = new Viewer(options);
viewer
.moveTo(dataProvider.images.keys().next().value)
.catch((error) => console.error(error));
listen();
})();
function listen() {
window.document.addEventListener("keydown", (e) => {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
try {
switch (e.key) {
case "q": {
// Delete a random cluster
dataProvider.delete();
break;
}
default:
break;
}
} catch (error) {
console.log(error);
}
});
}
</script>
</body>
</html>

View File

@ -69,10 +69,12 @@
};
viewer = new Viewer(options);
viewer.on("image", (event) =>
console.log(event.image ? event.image.id : null)
console.log("image", event.image ? event.image.id : null)
);
viewer.on("reference", (event) => console.log(event));
viewer.on("reset", (event) => console.log(event));
viewer.on("reference", (event) =>
console.log("reference", event)
);
viewer.on("reset", (event) => console.log("reset", event));
viewer
.moveTo(dataProvider.images.keys().next().value)
.catch((error) => console.error(error));

View File

@ -66,6 +66,7 @@
let hoveredImageId = null;
let paintedClusterId = null;
viewer.on("mousemove", async (event) => {
return;
function paintCluster(clusterId, color) {
spatial.setPointOverrideColor(clusterId, color);
spatial.setCameraOverrideColor(clusterId, color);

View File

@ -15,6 +15,7 @@ import { ProviderEvent } from "./events/ProviderEvent";
import { ProviderCellEvent } from "./events/ProviderCellEvent";
import { IDataProvider } from "./interfaces/IDataProvider";
import { IGeometryProvider } from "./interfaces/IGeometryProvider";
import { ProviderClusterEvent } from "./events/ProviderClusterEvent";
/**
* @class DataProviderBase
@ -85,6 +86,10 @@ export abstract class DataProviderBase extends EventEmitter implements IDataProv
type: "datacreate",
event: ProviderCellEvent)
: void;
public fire(
type: "datadelete",
event: ProviderClusterEvent)
: void;
/** @ignore */
public fire(
type: ProviderEventType,
@ -225,6 +230,10 @@ export abstract class DataProviderBase extends EventEmitter implements IDataProv
type: ProviderCellEvent["type"],
handler: (event: ProviderCellEvent) => void)
: void;
public off(
type: ProviderClusterEvent["type"],
handler: (event: ProviderClusterEvent) => void)
: void;
/** @ignore */
public off(
type: ProviderEventType,
@ -259,6 +268,10 @@ export abstract class DataProviderBase extends EventEmitter implements IDataProv
type: "datacreate",
handler: (event: ProviderCellEvent) => void)
: void;
public on(
type: "datadelete",
handler: (event: ProviderClusterEvent) => void)
: void;
/** @ignore */
public on(
type: ProviderEventType,

View File

@ -0,0 +1,17 @@
import { ProviderEvent } from "./ProviderEvent";
/**
*
* Interface for data provider cluster events.
*/
export interface ProviderClusterEvent extends ProviderEvent {
/**
* Cluster ids for clusters that have been deleted.
*/
clusterIds: string[];
/**
* Provider event type.
*/
type: "datadelete";
}

View File

@ -2,4 +2,5 @@
* @event
*/
export type ProviderEventType =
| "datacreate";
| "datacreate"
| "datadelete";

View File

@ -13,6 +13,7 @@ import { ProviderEventType } from "../events/ProviderEventType";
import { ProviderEvent } from "../events/ProviderEvent";
import { ProviderCellEvent } from "../events/ProviderCellEvent";
import { IGeometryProvider } from "./IGeometryProvider";
import { ProviderClusterEvent } from "../events/ProviderClusterEvent";
/**
* @interface IDataProvider
@ -63,6 +64,37 @@ export interface IDataProvider extends EventEmitter {
type: "datacreate",
event: ProviderCellEvent)
: void;
/**
* Fire when data has been created in the data provider
* after initial load.
*
* @param type datacreate
* @param event Provider cell event
*
* @example
* ```js
* // Initialize the data provider
* class MyDataProvider extends DataProviderBase {
* // Class implementation
* }
* var provider = new MyDataProvider();
* // Create the event
* var clusterIds = [ // Determine deleted clusters ];
* var target = provider;
* var type = "datadelete";
* var event = {
* clusterIds,
* target,
* type,
* };
* // Fire the event
* provider.fire(type, event);
* ```
*/
fire(
type: "datadelete",
event: ProviderClusterEvent)
: void;
/** @ignore */
fire(
type: ProviderEventType,
@ -183,6 +215,10 @@ export interface IDataProvider extends EventEmitter {
type: ProviderCellEvent["type"],
handler: (event: ProviderCellEvent) => void)
: void;
off(
type: ProviderClusterEvent["type"],
handler: (event: ProviderClusterEvent) => void)
: void;
/** @ignore */
off(
type: ProviderEventType,
@ -216,6 +252,28 @@ export interface IDataProvider extends EventEmitter {
type: "datacreate",
handler: (event: ProviderCellEvent) => void)
: void;
/**
* Fired when data has been deleted in the data provider
* after initial load.
*
* @event datacreate
* @example
* ```js
* // Initialize the data provider
* class MyDataProvider extends DataProviderBase {
* // implementation
* }
* var provider = new MyDataProvider();
* // Set an event listener
* provider.on("datadelete", function() {
* console.log("A datacreate event has occurred.");
* });
* ```
*/
on(
type: "datadelete",
handler: (event: ProviderClusterEvent) => void)
: void;
/** @ignore */
on(
type: ProviderEventType,

View File

@ -27,33 +27,45 @@ type ClusterData = {
url: string;
};
type Cluster = {
cellIds: Set<string>;
contract: ClusterContract;
};
type Cell = {
clusters: Map<string, string[]>;
images: Map<string, Image>;
};
type ClusterRequest = {
cancel: Function;
request: Observable<ClusterContract>;
};
export class SpatialCache {
private _graphService: GraphService;
private _api: APIWrapper;
private _cacheRequests: { [cellId: string]: Function[]; };
private _cells: { [cellId: string]: Image[]; };
private _clusters: { [key: string]: ClusterContract; };
private _clusterCells: { [key: string]: string[]; };
private _cells: Map<string, Cell>;
private _clusters: { [key: string]: Cluster; };
private _cellClusters: { [cellId: string]: ClusterData[]; };
private _cachingClusters$: { [cellId: string]: Observable<ClusterContract>; };
private _cachingCells$: { [cellId: string]: Observable<Image[]>; };
private _cellClusterRequests: { [cellId: string]: ClusterRequest; };
private _cellImageRequests: { [cellId: string]: Observable<Image[]>; };
private _clusterRequests: Set<string>;
constructor(graphService: GraphService, api: APIWrapper) {
this._graphService = graphService;
this._api = api;
this._cells = {};
this._cacheRequests = {};
this._cells = new Map();
this._clusters = {};
this._clusterCells = {};
this._cellClusters = {};
this._cachingCells$ = {};
this._cachingClusters$ = {};
this._cellImageRequests = {};
this._cellClusterRequests = {};
this._clusterRequests = new Set();
}
public cacheClusters$(cellId: string): Observable<ClusterContract> {
@ -66,7 +78,7 @@ export class SpatialCache {
}
if (this.isCachingClusters(cellId)) {
return this._cachingClusters$[cellId];
return this._cellClusterRequests[cellId].request;
}
const duplicatedClusters: ClusterData[] = this.getCell(cellId)
@ -89,31 +101,26 @@ export class SpatialCache {
.values());
this._cellClusters[cellId] = clusters;
this._cacheRequests[cellId] = [];
let aborter: Function;
const abort: Promise<void> = new Promise(
let cancel: Function;
const cancellationToken: Promise<void> = new Promise(
(_, reject): void => {
aborter = reject;
cancel = reject;
});
this._cacheRequests[cellId].push(aborter);
this._cellClusterRequests[cellId] = {
cancel, request:
this._cacheClusters$(clusters, cellId, cancellationToken).pipe(
finalize(
(): void => {
if (cellId in this._cellClusterRequests) {
delete this._cellClusterRequests[cellId];
}
}),
publish(),
refCount())
};
this._cachingClusters$[cellId] =
this._cacheClusters$(clusters, cellId, abort).pipe(
finalize(
(): void => {
if (cellId in this._cachingClusters$) {
delete this._cachingClusters$[cellId];
}
if (cellId in this._cacheRequests) {
delete this._cacheRequests[cellId];
}
}),
publish(),
refCount());
return this._cachingClusters$[cellId];
return this._cellClusterRequests[cellId].request;
}
public cacheCell$(cellId: string): Observable<Image[]> {
@ -122,10 +129,10 @@ export class SpatialCache {
}
if (this.isCachingCell(cellId)) {
return this._cachingCells$[cellId];
return this._cellImageRequests[cellId];
}
this._cachingCells$[cellId] = this._graphService.cacheCell$(cellId).pipe(
this._cellImageRequests[cellId] = this._graphService.cacheCell$(cellId).pipe(
catchError(
(error: Error): Observable<Image[]> => {
console.error(error);
@ -134,37 +141,50 @@ export class SpatialCache {
}),
filter(
(): boolean => {
return !(cellId in this._cells);
return !this._cells.has(cellId);
}),
tap(
(images: Image[]): void => {
this._cells[cellId] = [];
this._cells[cellId].push(...images);
const cell: Cell = {
clusters: new Map(),
images: new Map(),
};
this._cells.set(cellId, cell);
for (const image of images) {
cell.images.set(image.id, image);
const clusterId = image.clusterId;
if (!cell.clusters.has(clusterId)) {
cell.clusters.set(clusterId, []);
}
const clusterImageIds =
cell.clusters.get(clusterId);
clusterImageIds.push(image.id);
}
delete this._cachingCells$[cellId];
delete this._cellImageRequests[cellId];
}),
finalize(
(): void => {
if (cellId in this._cachingCells$) {
delete this._cachingCells$[cellId];
if (cellId in this._cellImageRequests) {
delete this._cellImageRequests[cellId];
}
}),
publish(),
refCount());
return this._cachingCells$[cellId];
return this._cellImageRequests[cellId];
}
public isCachingClusters(cellId: string): boolean {
return cellId in this._cachingClusters$;
return cellId in this._cellClusterRequests;
}
public isCachingCell(cellId: string): boolean {
return cellId in this._cachingCells$;
return cellId in this._cellImageRequests;
}
public hasClusters(cellId: string): boolean {
if (cellId in this._cachingClusters$ ||
if (cellId in this._cellClusterRequests ||
!(cellId in this._cellClusters)) {
return false;
}
@ -179,7 +199,7 @@ export class SpatialCache {
}
public hasCell(cellId: string): boolean {
return !(cellId in this._cachingCells$) && cellId in this._cells;
return !(cellId in this._cellImageRequests) && this._cells.has(cellId);
}
public getClusters(cellId: string): ClusterContract[] {
@ -187,7 +207,8 @@ export class SpatialCache {
this._cellClusters[cellId]
.map(
(cd: ClusterData): ClusterContract => {
return this._clusters[cd.key];
const cluster = this._clusters[cd.key];
return cluster ? cluster.contract : null;
})
.filter(
(reconstruction: ClusterContract): boolean => {
@ -197,20 +218,53 @@ export class SpatialCache {
}
public getCell(cellId: string): Image[] {
return cellId in this._cells ? this._cells[cellId] : [];
return this._cells.has(cellId) ?
Array.from(this._cells.get(cellId).images.values()) : [];
}
public removeCluster(clusterId: string): void {
this._clusterRequests.delete(clusterId);
if (clusterId in this._clusters) {
delete this._clusters[clusterId];
}
const cellIds: string[] = [];
for (const [cellId, cell] of this._cells.entries()) {
if (cell.clusters.has(clusterId)) {
cellIds.push(cellId);
}
}
for (const cellId of cellIds) {
if (!this._cells.has(cellId)) {
continue;
}
const cell = this._cells.get(cellId);
const clusterImages = cell.clusters.get(clusterId) ?? [];
for (const imageId of clusterImages) {
cell.images.delete(imageId);
}
cell.clusters.delete(clusterId);
if (cellId in this._cellClusters) {
const cellClusters = this._cellClusters[cellId];
const index = cellClusters.findIndex(cd => cd.key === clusterId);
if (index !== -1) {
cellClusters.splice(index, 1);
}
}
}
}
public uncache(keepCellIds?: string[]): void {
for (let cellId of Object.keys(this._cacheRequests)) {
for (const cellId of Object.keys(this._cellClusterRequests)) {
if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) {
continue;
}
for (const aborter of this._cacheRequests[cellId]) {
aborter();
}
delete this._cacheRequests[cellId];
this._cellClusterRequests[cellId].cancel();
delete this._cellClusterRequests[cellId];
}
for (let cellId of Object.keys(this._cellClusters)) {
@ -219,34 +273,28 @@ export class SpatialCache {
}
for (const cd of this._cellClusters[cellId]) {
if (!(cd.key in this._clusterCells)) {
if (!(cd.key in this._clusters)) {
continue;
}
const index: number = this._clusterCells[cd.key].indexOf(cellId);
if (index === -1) {
const { cellIds } = this._clusters[cd.key];
cellIds.delete(cellId);
if (cellIds.size > 0) {
continue;
}
this._clusterCells[cd.key].splice(index, 1);
if (this._clusterCells[cd.key].length > 0) {
continue;
}
delete this._clusterCells[cd.key];
delete this._clusters[cd.key];
}
delete this._cellClusters[cellId];
}
for (let cellId of Object.keys(this._cells)) {
for (let cellId of this._cells.keys()) {
if (!!keepCellIds && keepCellIds.indexOf(cellId) !== -1) {
continue;
}
delete this._cells[cellId];
this._cells.delete(cellId);
}
}
@ -264,12 +312,21 @@ export class SpatialCache {
}),
filter(
(): boolean => {
return cellId in this._cells;
return this._cells.has(cellId);
}),
tap(
(images: Image[]): void => {
this._cells[cellId] = [];
this._cells[cellId].push(...images);
const cell = this._cells.get(cellId);
for (const image of images) {
cell.images.set(image.id, image);
const clusterId = image.clusterId;
if (!cell.clusters.has(clusterId)) {
cell.clusters.set(clusterId, []);
}
const clusterImageIds =
cell.clusters.get(clusterId);
clusterImageIds.push(image.id);
}
}),
publish(),
refCount());
@ -324,6 +381,7 @@ export class SpatialCache {
this._getCluster(cd.key));
}
this._clusterRequests.add(cd.key);
return this._getCluster$(
cd.url,
cd.key,
@ -341,27 +399,28 @@ export class SpatialCache {
},
6),
filter(
(): boolean => {
return cellId in this._cellClusters;
(cluster: ClusterContract): boolean => {
return cellId in this._cellClusters &&
this._clusterRequests.has(cluster.id);
}),
tap(
(reconstruction: ClusterContract): void => {
if (!this._hasCluster(reconstruction.id)) {
this._clusters[reconstruction.id] = reconstruction;
(cluster: ClusterContract): void => {
if (!this._hasCluster(cluster.id)) {
this._clusters[cluster.id] = {
cellIds: new Set(),
contract: cluster,
};
}
if (!(reconstruction.id in this._clusterCells)) {
this._clusterCells[reconstruction.id] = [];
}
const { cellIds } = this._clusters[cluster.id];
cellIds.add(cellId);
if (this._clusterCells[reconstruction.id].indexOf(cellId) === -1) {
this._clusterCells[reconstruction.id].push(cellId);
}
this._clusterRequests.delete(cluster.id);
}));
}
private _getCluster(id: string): ClusterContract {
return this._clusters[id];
return this._clusters[id].contract;
}
private _getCluster$(url: string, clusterId: string, abort: Promise<void>): Observable<ClusterContract> {

View File

@ -597,6 +597,15 @@ export class SpatialComponent extends Component<SpatialConfiguration> {
}))
.subscribe(this._container.glRenderer.render$));
subs.push(this._navigator.graphService.dataDeleted$
.subscribe(
(clusterIds: string[]): void => {
for (const clusterId of clusterIds) {
this._cache.removeCluster(clusterId);
this._scene.uncacheCluster(clusterId);
}
}));
const updatedCell$ = this._navigator.graphService.dataAdded$
.pipe(
filter(

View File

@ -69,6 +69,7 @@ export class SpatialScene {
private _imageCellMap: Map<string, string>;
private _clusterCellMap: Map<string, Set<string>>;
private _clusterImageMap: Map<string, Set<string>>;
private _colors: { hover: string, select: string; };
private _cameraOverrideColors: Map<string, number | string>;
@ -79,6 +80,7 @@ export class SpatialScene {
scene?: Scene) {
this._imageCellMap = new Map();
this._clusterCellMap = new Map();
this._clusterImageMap = new Map();
this._scene = !!scene ? scene : new Scene();
this._scene.autoUpdate = false;
@ -145,8 +147,8 @@ export class SpatialScene {
this._scene.add(points);
this._clusters[clusterId] = {
points: points,
cellIds: [],
points: points,
};
}
@ -159,6 +161,9 @@ export class SpatialScene {
if (this._cellClusters[cellId].keys.indexOf(clusterId) === -1) {
this._cellClusters[cellId].keys.push(clusterId);
}
if (!this._clusterImageMap.has(clusterId)) {
this._clusterImageMap.set(clusterId, new Set());
}
this._needsRender = true;
}
@ -220,6 +225,11 @@ export class SpatialScene {
}
this._imageCellMap.set(imageId, cellId);
if (!this._clusterImageMap.has(idMap.clusterId)) {
this._clusterImageMap.set(idMap.clusterId, new Set());
}
this._clusterImageMap.get(idMap.clusterId).add(imageId);
if (imageId === this._selectedId) {
this._highlight(
imageId,
@ -560,6 +570,54 @@ export class SpatialScene {
this._needsRender = true;
}
public uncacheCluster(clusterId: string): void {
const cellIds = new Set<string>();
if (clusterId in this._clusters) {
const cluster = this._clusters[clusterId];
for (const cellId of cluster.cellIds) {
cellIds.add(cellId);
}
this._scene.remove(cluster.points);
cluster.points.dispose();
delete this._clusters[clusterId];
}
for (const cellId of this._clusterCellMap.get(clusterId) ?? []) {
cellIds.add(cellId);
}
this._clusterCellMap.delete(clusterId);
for (const cellId of cellIds.values()) {
if (!(cellId in this._cellClusters)) {
continue;
}
const cellClusters = this._cellClusters[cellId];
const index = cellClusters.keys.indexOf(clusterId);
if (index !== -1) {
cellClusters.keys.splice(index, 1);
}
}
for (const cellId of cellIds.values()) {
if (!(cellId in this._images)) {
continue;
}
const cell = this._images[cellId];
cell.disposeCluster(clusterId);
}
if (this._clusterImageMap.has(clusterId)) {
const imageCellMap = this._imageCellMap;
const imageIds = this._clusterImageMap.get(clusterId).values();
for (const imageId of imageIds) {
imageCellMap.delete(imageId);
}
this._clusterImageMap.delete(clusterId);
}
this._needsRender = true;
}
private _applyCameraColor(cellIds: string[]): void {
const mode = this._cameraVisualizationMode;

View File

@ -15,7 +15,7 @@ import { isSpherical } from "../../../geo/Geo";
import { SphericalCameraFrame } from "./SphericalCameraFrame";
import { PerspectiveCameraFrame } from "./PerspectiveCameraFrame";
import { LngLatAlt } from "../../../api/interfaces/LngLatAlt";
import { resetEnu, SPATIAL_DEFAULT_COLOR } from "../SpatialCommon";
import { resetEnu } from "../SpatialCommon";
type IDCamera = {
camera: CameraFrameBase,
@ -70,6 +70,8 @@ export class SpatialCell {
private _frameMaterial: LineBasicMaterial;
private _positionMaterial: LineBasicMaterial;
private readonly _clusterImages: Map<string, string[]>;
constructor(
public readonly id: string,
private _scene: Scene,
@ -97,6 +99,8 @@ export class SpatialCell {
color: 0xff0000,
});
this._clusterImages = new Map();
this._scene.add(
this.cameras,
this._positions);
@ -133,6 +137,11 @@ export class SpatialCell {
ids: { ccId, clusterId: cId, sequenceId: sId },
};
this.keys.push(id);
if (!this._clusterImages.has(cId)) {
this._clusterImages.set(cId, []);
}
this._clusterImages.get(cId).push(id);
}
public applyCameraColor(imageId: string, color: string | number): void {
@ -182,6 +191,49 @@ export class SpatialCell {
this._intersection = null;
}
public disposeCluster(clusterId: string): void {
if (!this._clusterImages.has(clusterId)) {
return;
}
const {
_cameraFrames,
_intersection,
_positionLines,
_positions,
_props,
cameras,
keys,
} = this;
const imageIds = this._clusterImages.get(clusterId);
for (const imageId of imageIds) {
this._disposeCamera(
_cameraFrames[imageId],
cameras,
_intersection);
this._disposePosition(
_positionLines[imageId],
_positions
);
delete _cameraFrames[imageId];
delete _positionLines[imageId];
const index = keys.indexOf(imageId);
if (index !== -1) {
keys.splice(index, 1);
}
delete _props[imageId];
}
this._clusterImages.delete(clusterId);
this._clusters.delete(clusterId);
delete this.clusterVisibles[clusterId];
}
public getCamerasByMode(mode: CameraVisualizationMode): ColorIdCamerasMap {
switch (mode) {
case CameraVisualizationMode.Cluster:
@ -295,22 +347,35 @@ export class SpatialCell {
this._positionLines[id] = position;
}
private _disposeCamera(
camera: CameraFrameBase,
cameras: Object3D,
intersection: SpatialIntersection): void {
camera.dispose();
intersection.remove(camera);
cameras.remove(camera);
}
private _disposeCameras(): void {
const intersection = this._intersection;
const cameras = this.cameras;
for (const camera of cameras.children.slice()) {
(<CameraFrameBase>camera).dispose();
intersection.remove(camera);
cameras.remove(camera);
this._disposeCamera(<CameraFrameBase>camera, cameras, intersection);
}
this._scene.remove(this.cameras);
}
private _disposePosition(
position: PositionLine,
positions: Object3D): void {
position.dispose();
positions.remove(position);
}
private _disposePositions(): void {
const positions = this._positions;
for (const position of positions.children.slice()) {
(<PositionLine>position).dispose();
positions.remove(position);
this._disposePosition(<PositionLine>position, positions);
}
this._scene.remove(this._positions);
}

1
src/external/api.ts vendored
View File

@ -35,6 +35,7 @@ export { S2GeometryProvider } from "../api/S2GeometryProvider";
// Event
export { ProviderCellEvent } from "../api/events/ProviderCellEvent";
export { ProviderClusterEvent } from "../api/events/ProviderClusterEvent";
export { ProviderEvent } from "../api/events/ProviderEvent";
export { ProviderEventType } from "../api/events/ProviderEventType";

View File

@ -11,6 +11,7 @@ import {
import {
catchError,
filter as filterObservable,
finalize,
map,
mergeAll,
@ -70,6 +71,8 @@ type SequenceAccess = {
};
export type NodeIndexItem = {
cellId: string,
id: string,
lat: number;
lng: number;
node: Image;
@ -153,6 +156,11 @@ export class Graph {
private _filter$: Observable<FilterFunction>;
private _filterSubscription: Subscription;
/**
* Nodes of clusters.
*/
private _clusterNodes: Map<string, Set<string>>;
/**
* All nodes in the graph.
*/
@ -163,11 +171,17 @@ export class Graph {
*/
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.
*/
@ -178,6 +192,11 @@ export class Graph {
*/
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.
*/
@ -251,12 +270,15 @@ export class Graph {
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 = {};
@ -566,7 +588,12 @@ export class Graph {
`Image has no sequence key (${key}).`);
}
const node = new Image(item.node);
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 cellId = this._api.data.geometry
@ -720,7 +747,13 @@ export class Graph {
console.warn(`Sequence missing, discarding node (${item.node_id})`);
}
const node = new Image(item.node);
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 cellId = this._api.data.geometry
@ -795,6 +828,10 @@ export class Graph {
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})`);
@ -1303,45 +1340,31 @@ export class Graph {
* Reset all spatial edges of the graph nodes.
*/
public resetSpatialEdges(): void {
let cachedKeys: string[] = Object.keys(this._cachedSpatialEdges);
for (let cachedKey of cachedKeys) {
let node: Image = this._cachedSpatialEdges[cachedKey];
for (const nodeId of Object.keys(this._cachedSpatialEdges)) {
const node = this._cachedSpatialEdges[nodeId];
node.resetSpatialEdges();
delete this._cachedSpatialEdges[cachedKey];
}
this._cachedSpatialEdges = {};
}
/**
* Reset the complete graph but keep the nodes corresponding
* to the supplied keys. All other nodes will be disposed.
*
* @param {Array<string>} keepKeys - Keys for nodes to keep
* in graph after reset.
* Reset all spatial areas of the graph nodes.
*/
public reset(keepKeys: string[]): void {
const nodes: Image[] = [];
for (const key of keepKeys) {
if (!this.hasNode(key)) {
throw new Error(`Image does not exist ${key}`);
}
const node: Image = this.getNode(key);
node.resetSequenceEdges();
node.resetSpatialEdges();
nodes.push(node);
}
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)) {
if (keepKeys.indexOf(cachedKey) !== -1) {
continue;
}
this._cachedNodes[cachedKey].node.dispose();
delete this._cachedNodes[cachedKey];
}
this._cachedNodes = {};
this._cachedNodeTiles = {};
this._cachedSpatialEdges = {};
this._cachedTiles = {};
@ -1352,23 +1375,19 @@ export class Graph {
this._cachingSpatialArea$ = {};
this._cachingTiles$ = {};
this._clusterNodes = new Map();
this._nodes = {};
this._nodeToTile = {};
this._preStored = {};
for (const node of nodes) {
this._nodes[node.id] = node;
const h: string = this._api.data.geometry.lngLatToCellId(node.originalLngLat);
this._preStore(h, node);
}
this._preDeletedNodes = new Map();
this._requiredNodeTiles = {};
this._requiredSpatialArea = {};
this._sequences = {};
this._nodeIndexNodes = new Map();
this._nodeIndexTiles = {};
this._nodeIndex.clear();
}
@ -1457,6 +1476,7 @@ export class Graph {
for (const id in idsInUse) {
if (!idsInUse.hasOwnProperty(id)) { continue; }
if (!this.hasNode(id)) { continue; }
const node = this._nodes[id];
const nodeCellId = geometry.lngLatToCellId(node.lngLat);
@ -1570,6 +1590,19 @@ export class Graph {
}
}
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) ||
@ -1613,7 +1646,7 @@ export class Graph {
* cached.
*
* @param {Array<string>} cellIds - Cell ids.
* @returns {Observable<Array<Image>>} Observable
* @returns {Observable<Array<string>>} Observable
* emitting the updated cells.
*/
public updateCells$(cellIds: string[]): Observable<string> {
@ -1639,6 +1672,80 @@ export class Graph {
));
}
/**
* 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.
*
@ -1719,6 +1826,7 @@ export class Graph {
};
const hCache = this._cachedTiles[cellId].nodes;
const preStored = this._removeFromPreStore(cellId);
const preDeleted = this._preDeletedNodes;
for (const core of cores) {
if (!core) { break; }
@ -1734,26 +1842,39 @@ export class Graph {
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;
}
const node = new Image(core);
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;
@ -1775,6 +1896,36 @@ export class Graph {
return this._cachingTiles$[cellId];
}
private _addClusterNode(node: Image): void {
const clusterId = node.clusterId;
if (clusterId == null) {
throw new GraphMapillaryError(`Image does not have cluster (${node.id}).`);
}
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 {
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;
@ -1785,6 +1936,12 @@ export class Graph {
}
node.makeComplete(fillNode);
this._addClusterNode(node);
}
private _disposeNode(node: Image): void {
this._removeClusterNode(node);
node.dispose();
}
private _preStore(h: string, node: Image): void {
@ -1818,7 +1975,7 @@ export class Graph {
private _uncacheTile(h: string, keepSequenceKey: string): void {
for (let node of this._cachedTiles[h].nodes) {
let key: string = node.id;
let key = node.id;
delete this._nodeToTile[key];
@ -1844,12 +2001,13 @@ export class Graph {
delete this._cachedSequenceNodes[node.sequenceId];
}
node.dispose();
this._disposeNode(node);
}
}
for (let nodeIndexItem of this._nodeIndexTiles[h]) {
this._nodeIndex.remove(nodeIndexItem);
this._nodeIndexNodes.delete(nodeIndexItem.id);
}
delete this._nodeIndexTiles[h];
@ -1867,15 +2025,13 @@ export class Graph {
delete this._cachedNodes[key];
}
let node: Image = this._preStored[h][key];
const node = this._preStored[h][key];
if (node.sequenceId in this._cachedSequenceNodes) {
delete this._cachedSequenceNodes[node.sequenceId];
}
delete this._preStored[h][key];
node.dispose();
this._disposeNode(node);
hs[h] = true;
}
@ -1913,6 +2069,7 @@ export class Graph {
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();
@ -1920,6 +2077,7 @@ export class Graph {
const cores = contract.images;
for (const core of cores) {
if (core == null) { break; }
if (this.hasNode(core.id)) { continue; }
@ -1929,15 +2087,24 @@ export class Graph {
continue;
}
const node = new Image(core);
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);
}
@ -1949,4 +2116,18 @@ export class Graph {
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;
}
}

View File

@ -35,6 +35,7 @@ import { ProviderCellEvent } from "../api/events/ProviderCellEvent";
import { GraphMapillaryError } from "../error/GraphMapillaryError";
import { ProjectionService } from "../viewer/ProjectionService";
import { ICameraFactory } from "../geometry/interfaces/ICameraFactory";
import { ProviderClusterEvent } from "../api/events/ProviderClusterEvent";
/**
* @class GraphService
@ -50,6 +51,7 @@ export class GraphService {
private _firstGraphSubjects$: Subject<Graph>[];
private _dataAdded$: Subject<string> = new Subject<string>();
private _dataDeleted$: Subject<string[]> = new Subject<string[]>();
private _dataReset$: Subject<void> = new Subject<void>();
private _initializeCacheSubscriptions: Subscription[];
@ -93,6 +95,7 @@ export class GraphService {
this._spatialSubscriptions = [];
graph.api.data.on("datacreate", this._onDataAdded);
graph.api.data.on("datadelete", this._onDataDeleted);
}
/**
@ -105,6 +108,16 @@ export class GraphService {
return this._dataAdded$;
}
/**
* Get dataDeleted$.
*
* @returns {Observable<string>} Observable emitting
* a cluster id every time a cluster has been deleted.
*/
public get dataDeleted$(): Observable<string[]> {
return this._dataDeleted$;
}
public get dataReset$(): Observable<void> {
return this._dataReset$;
}
@ -299,7 +312,12 @@ export class GraphService {
sequenceSubscription = graphSequence$.pipe(
tap(
(graph: Graph): void => {
if (!graph.getNode(id).sequenceEdges.cached) {
const node = graph.getNode(id);
if (!node.hasInitializedCache()) {
return;
}
if (!node.sequenceEdges.cached) {
graph.cacheSequenceEdges(id);
}
}),
@ -382,7 +400,12 @@ export class GraphService {
}),
tap(
(graph: Graph): void => {
if (!graph.getNode(id).spatialEdges.cached) {
const node = graph.getNode(id);
if (!node.hasInitializedCache()) {
return;
}
if (!node.spatialEdges.cached) {
graph.cacheSpatialEdges(id);
}
}),
@ -489,6 +512,23 @@ export class GraphService {
this._subscriptions.unsubscribe();
}
/**
* Check if an image exists in the graph.
*
* @description If a node has been deleted it will not exist.
*
* @return {Observable<boolean>} Observable emitting a single item,
* a value indicating if the image exists in the graph.
*/
public hasImage$(id: string): Observable<boolean> {
return this._graph$.pipe(
first(),
map(
(graph: Graph): boolean => {
return graph.hasNode(id);
}));
}
/**
* Set a spatial edge filter on the graph.
*
@ -546,11 +586,10 @@ export class GraphService {
* @description Resets the graph but keeps the images of the
* supplied ids.
*
* @param {Array<string>} keepIds - Ids of images to keep in graph.
* @return {Observable<Image>} Observable emitting a single item,
* the graph, when it has been reset.
*/
public reset$(keepIds: string[]): Observable<void> {
public reset$(): Observable<void> {
this._abortSubjects(this._firstGraphSubjects$);
this._resetSubscriptions(this._initializeCacheSubscriptions);
this._resetSubscriptions(this._sequenceSubscriptions);
@ -560,7 +599,7 @@ export class GraphService {
first(),
tap(
(graph: Graph): void => {
graph.reset(keepIds);
graph.reset();
this._dataReset$.next();
}),
map(
@ -617,12 +656,33 @@ export class GraphService {
first(),
mergeMap(
graph => {
return graph.updateCells$(event.cellIds).pipe(
tap(() => { graph.resetSpatialEdges(); }));
graph.resetSpatialArea();
graph.resetSpatialEdges();
return graph.updateCells$(event.cellIds);
}))
.subscribe(cellId => { this._dataAdded$.next(cellId); });
};
private _onDataDeleted = (event: ProviderClusterEvent): void => {
if (!event.clusterIds.length) {
return;
}
this._graph$
.pipe(
first(),
mergeMap(
graph => {
graph.resetSpatialArea();
graph.resetSpatialEdges();
return graph.deleteClusters$(event.clusterIds);
}))
.subscribe(
null,
null,
() => { this._dataDeleted$.next(event.clusterIds); });
};
private _removeFromArray<T>(object: T, objects: T[]): void {
const index: number = objects.indexOf(object);
if (index !== -1) {

View File

@ -331,6 +331,19 @@ export class Image {
return !this._core;
}
/**
* Get is complete.
*
* @returns {boolean} Value indicating that this image
* is complete.
*
* @ignore
*/
public get isComplete(): boolean {
return !!this._core && !!this._spatial;
}
/**
* Get lngLat.
*

View File

@ -198,7 +198,7 @@ export class ImageCache {
return this._cachingAssets$;
}
this._cachingAssets$ = observableCombineLatest(
const cachingAssets$ = observableCombineLatest(
this._cacheImage$(spatial),
this._cacheMesh$(spatial, merged)).pipe(
map(
@ -215,7 +215,9 @@ export class ImageCache {
publishReplay(1),
refCount());
this._cachingAssets$.pipe(
this._cachingAssets$ = cachingAssets$;
cachingAssets$.pipe(
first(
(imageCache: ImageCache): boolean => {
return !!imageCache._image;
@ -226,7 +228,7 @@ export class ImageCache {
},
(): void => { /*noop*/ });
return this._cachingAssets$;
return cachingAssets$;
}
/**

View File

@ -1,5 +1,6 @@
import {
empty as observableEmpty,
merge as observableMerge,
from as observableFrom,
Observable,
} from "rxjs";
@ -8,6 +9,7 @@ import {
bufferCount,
catchError,
distinctUntilChanged,
filter,
first,
map,
mergeMap,
@ -153,12 +155,27 @@ export class CacheService {
.subscribe(() => { /*noop*/ }));
subs.push(
this._graphService.dataAdded$
observableMerge(
this._graphService.dataAdded$,
this._graphService.dataDeleted$)
.pipe(
withLatestFrom(this._stateService.currentId$),
switchMap(
([_, imageId]: [string, string]): Observable<Image> => {
return this._graphService.cacheImage$(imageId);
return this._graphService.hasImage$(imageId).pipe(
filter((exists: boolean): boolean => {
return exists;
}),
mergeMap((): Observable<Image> => {
return this._graphService.cacheImage$(imageId)
.pipe(catchError(
(error): Observable<Image> => {
console.warn(
`Cache service data event caching failed ${imageId}`,
error);
return observableEmpty();
}));
}));
}))
.subscribe(() => { /*noop*/ }));

View File

@ -9,6 +9,7 @@ import {
} from "rxjs";
import {
filter as observableFilter,
finalize,
first,
last,
@ -274,7 +275,13 @@ export class Navigator {
const cacheImages$ = ids
.map(
(id: string): Observable<Image> => {
return this._graphService.cacheImage$(id);
return this._graphService.hasImage$(id).pipe(
observableFilter((exists: boolean): boolean => {
return exists;
}),
mergeMap((): Observable<Image> => {
return this._graphService.cacheImage$(id);
}));
});
return observableFrom(cacheImages$).pipe(
@ -356,7 +363,7 @@ export class Navigator {
tap((): void => { if (preCallback) { preCallback(); }; }),
mergeMap(
(): Observable<void> => {
return this._graphService.reset$([]);
return this._graphService.reset$();
}));
}

View File

@ -518,3 +518,111 @@ describe("SpatialCache.updateReconstructions$", () => {
resolver(cluster);
});
});
describe("SpatialCache.removeCluster", () => {
const createCluster = (id: string): ClusterContract => {
return {
colors: [],
coordinates: [],
id,
pointIds: [],
reference: { lat: 0, lng: 0, alt: 0 },
rotation: [0, 0, 0]
};
};
it("should have cell and cell clusters after remove", () => {
const image = new ImageHelper().createImage();
const cellId = "123";
let resolver: Function;
const promise: any = {
then: (resolve: (value: ClusterContract) => void): void => {
resolver = resolve;
},
};
const dataProvider = new DataProvider();
spyOn(dataProvider, "getCluster").and.returnValue(promise);
const graphService = new GraphServiceMockCreator().create();
const api = new APIWrapper(dataProvider);
const cache = new SpatialCache(graphService, api);
cacheTile(cellId, cache, graphService, [image]);
cache.cacheClusters$(cellId).subscribe();
const cluster = createCluster(image.clusterId);
resolver(cluster);
expect(cache.hasCell(cellId)).toBe(true);
expect(cache.hasClusters(cellId)).toBe(true);
cache.removeCluster(cluster.id);
expect(cache.hasCell(cellId)).toBe(true);
expect(cache.hasClusters(cellId)).toBe(true);
});
it("should remove cluster", () => {
const image = new ImageHelper().createImage();
const cellId = "123";
let resolver: Function;
const promise: any = {
then: (resolve: (value: ClusterContract) => void): void => {
resolver = resolve;
},
};
const dataProvider = new DataProvider();
spyOn(dataProvider, "getCluster").and.returnValue(promise);
const graphService = new GraphServiceMockCreator().create();
const api = new APIWrapper(dataProvider);
const cache = new SpatialCache(graphService, api);
cacheTile(cellId, cache, graphService, [image]);
cache.cacheClusters$(cellId).subscribe();
const cluster = createCluster(image.clusterId);
resolver(cluster);
expect(cache.getClusters(cellId).length).toBe(1);
cache.removeCluster(cluster.id);
expect(cache.getClusters(cellId).length).toBe(0);
});
it("should remove images", () => {
const image = new ImageHelper().createImage();
const cellId = "123";
let resolver: Function;
const promise: any = {
then: (resolve: (value: ClusterContract) => void): void => {
resolver = resolve;
},
};
const dataProvider = new DataProvider();
spyOn(dataProvider, "getCluster").and.returnValue(promise);
const graphService = new GraphServiceMockCreator().create();
const api = new APIWrapper(dataProvider);
const cache = new SpatialCache(graphService, api);
cacheTile(cellId, cache, graphService, [image]);
cache.cacheClusters$(cellId).subscribe();
const cluster = createCluster(image.clusterId);
resolver(cluster);
expect(cache.getCell(cellId).length).toBe(1);
cache.removeCluster(cluster.id);
expect(cache.getCell(cellId).length).toBe(0);
});
});

View File

@ -2682,7 +2682,7 @@ describe("Graph.reset", () => {
const nodeDisposeSpy = spyOn(node, "dispose");
nodeDisposeSpy.and.stub();
graph.reset([]);
graph.reset();
expect(nodeDisposeSpy.calls.count()).toBe(0);
expect(graph.hasNode(node.id)).toBe(false);
@ -2722,57 +2722,11 @@ describe("Graph.reset", () => {
const nodeDisposeSpy = spyOn(node, "dispose");
nodeDisposeSpy.and.stub();
graph.reset([]);
graph.reset();
expect(nodeDisposeSpy.calls.count()).toBe(1);
expect(graph.hasNode(node.id)).toBe(false);
});
it("should keep supplied node", () => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(
geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
const h = "h";
spyOn(geometryProvider, "lngLatToCellId").and.returnValue(h);
const getImages = new Subject<ImagesContract>();
spyOn(api, "getImages$").and.returnValue(getImages);
const graph = new Graph(api, undefined, calculator);
const fullNode = helper.createImageEnt();
const result: ImagesContract = [{
node: fullNode,
node_id: fullNode.id,
}];
graph.cacheFull$(fullNode.id).subscribe(() => { /*noop*/ });
getImages.next(result);
getImages.complete();
expect(graph.hasNode(fullNode.id)).toBe(true);
const node = graph.getNode(fullNode.id);
graph.initializeCache(node.id);
const nodeDisposeSpy = spyOn(node, "dispose");
nodeDisposeSpy.and.stub();
const nodeResetSequenceSpy = spyOn(node, "resetSequenceEdges");
nodeResetSequenceSpy.and.stub();
const nodeResetSpatialSpy = spyOn(node, "resetSpatialEdges");
nodeResetSpatialSpy.and.stub();
graph.reset([node.id]);
expect(nodeDisposeSpy.calls.count()).toBe(0);
expect(nodeResetSequenceSpy.calls.count()).toBe(1);
expect(nodeResetSpatialSpy.calls.count()).toBe(1);
expect(graph.hasNode(node.id)).toBe(true);
});
});
describe("Graph.uncache", () => {
@ -4293,9 +4247,7 @@ describe("Graph.updateCells$", () => {
it("should update currently caching cell", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(
geometryProvider);
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
@ -4353,9 +4305,7 @@ describe("Graph.updateCells$", () => {
it("should add new nodes to existing cell", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(
geometryProvider);
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
@ -4419,3 +4369,168 @@ describe("Graph.updateCells$", () => {
coreImagesUpdate.complete();
});
});
describe("Graph.deleteClusters$", () => {
it("should not delete when empty array", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
const cellIdSpy = spyOn(geometryProvider, "lngLatToCellId")
.and.returnValue("cell-id");
const graph = new Graph(api, undefined, calculator);
let count = 0;
graph.deleteClusters$([])
.subscribe(
(): void => { count++; },
undefined,
(): void => {
expect(count).toBe(0);
expect(cellIdSpy.calls.count()).toBe(0);
done();
});
});
it("should not emit when cluster does not exist", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
const cellIdSpy = spyOn(geometryProvider, "lngLatToCellId")
.and.returnValue("cell-id");
const graph = new Graph(api, undefined, calculator);
let count = 0;
graph.deleteClusters$(["non-existing-cluster-id"])
.subscribe(
(): void => { count++; },
undefined,
(): void => {
expect(count).toBe(0);
expect(cellIdSpy.calls.count()).toBe(0);
done();
});
});
it("should emit when deleting a cluster", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
const cellId = "cell-id";
const coreImages =
new Subject<CoreImagesContract>();
spyOn(api, "getCoreImages$").and.returnValue(coreImages);
const getSpatialImages = new Subject<SpatialImagesContract>();
spyOn(api, "getSpatialImages$").and.returnValue(getSpatialImages);
const id = "node-id";
const fullNode = new ImageHelper().createImageEnt();
fullNode.id = id;
const graph = new Graph(api, undefined, calculator);
graph.cacheCell$(cellId).subscribe();
const tileResult: CoreImagesContract = {
cell_id: cellId,
images: [fullNode],
};
coreImages.next(tileResult);
coreImages.complete();
const spatialImages: SpatialImagesContract = [{
node: fullNode,
node_id: fullNode.id,
}];
getSpatialImages.next(spatialImages);
getSpatialImages.complete();
expect(graph.hasNode(id)).toBe(true);
let count = 0;
graph.deleteClusters$([fullNode.cluster.id])
.subscribe(
(clusterId: string): void => {
count++;
expect(clusterId).toBe(fullNode.cluster.id);
},
undefined,
(): void => {
expect(count).toBe(1);
expect(graph.hasNode(id)).toBe(false);
done();
});
});
it("should not dispose deleted node", (done: Function) => {
const geometryProvider = new GeometryProvider();
const dataProvider = new DataProvider(geometryProvider);
const api = new APIWrapper(dataProvider);
const calculator = new GraphCalculator();
const cellId = "cell-id";
const coreImages =
new Subject<CoreImagesContract>();
spyOn(api, "getCoreImages$").and.returnValue(coreImages);
const getSpatialImages = new Subject<SpatialImagesContract>();
spyOn(api, "getSpatialImages$").and.returnValue(getSpatialImages);
const id = "node-id";
const fullNode = new ImageHelper().createImageEnt();
fullNode.id = id;
const graph = new Graph(api, undefined, calculator);
graph.cacheCell$(cellId).subscribe();
const tileResult: CoreImagesContract = {
cell_id: cellId,
images: [fullNode],
};
coreImages.next(tileResult);
coreImages.complete();
const spatialImages: SpatialImagesContract = [{
node: fullNode,
node_id: fullNode.id,
}];
getSpatialImages.next(spatialImages);
getSpatialImages.complete();
const node = graph.getNode(id);
expect(node.isDisposed).toBe(false);
let count = 0;
graph.deleteClusters$([fullNode.cluster.id])
.subscribe(
(clusterId: string): void => {
count++;
expect(clusterId).toBe(fullNode.cluster.id);
},
undefined,
(): void => {
expect(count).toBe(1);
expect(node.isDisposed).toBe(false);
done();
});
});
});

View File

@ -120,7 +120,7 @@ describe("GraphService.dataAdded$", () => {
});
});
it("should reset spatial edges for each updated cell", (done: Function) => {
it("should reset spatial edges one time for multiple updated cells", (done: Function) => {
const dataProvider = new DataProvider();
const api = new APIWrapper(dataProvider);
const graph = new Graph(api);
@ -142,7 +142,7 @@ describe("GraphService.dataAdded$", () => {
expect(cellIds.includes(cellId)).toBe(true);
expect(updateCellsSpy.calls.count()).toBe(1);
expect(resetSpatialEdgesSpy.calls.count()).toBe(count);
expect(resetSpatialEdgesSpy.calls.count()).toBe(1);
if (count === 2) { done(); }
});
@ -156,6 +156,98 @@ describe("GraphService.dataAdded$", () => {
});
});
describe("GraphService.dataDeleted$", () => {
it("should not do work if list empty", () => {
const dataProvider = new DataProvider();
const api = new APIWrapper(dataProvider);
const graph = new Graph(api);
const graphService = new GraphService(graph);
let count = 0;
graphService.dataDeleted$
.subscribe(
(): void => {
count++;
});
dataProvider.fire("datadelete", {
type: "datadelete",
target: dataProvider,
clusterIds: [],
});
expect(count).toBe(0);
});
it("should call graph", (done: Function) => {
const dataProvider = new DataProvider();
const api = new APIWrapper(dataProvider);
const graph = new Graph(api);
const deleteClustersSpy = spyOn(graph, "deleteClusters$");
deleteClustersSpy.and.returnValue(observableOf("cluster-id"));
const resetSpatialEdgesSpy =
spyOn(graph, "resetSpatialEdges").and.stub();
const graphService = new GraphService(graph);
graphService.dataDeleted$
.subscribe(
(clusterIds): void => {
expect(clusterIds.length).toBe(1);
expect(clusterIds[0]).toBe("cluster-id");
expect(deleteClustersSpy.calls.count()).toBe(1);
expect(resetSpatialEdgesSpy.calls.count()).toBe(1);
done();
});
dataProvider.fire("datadelete", {
type: "datadelete",
target: dataProvider,
clusterIds: ["cluster-id"],
});
});
it("should reset spatial edges once", (done: Function) => {
const dataProvider = new DataProvider();
const api = new APIWrapper(dataProvider);
const graph = new Graph(api);
const deleteClustersSpy = spyOn(graph, "deleteClusters$");
const clusterIds = ["cluster-id1", "cluster-id2"];
deleteClustersSpy.and.returnValue(observableFrom(clusterIds.slice()));
const resetSpatialEdgesSpy =
spyOn(graph, "resetSpatialEdges").and.stub();
const graphService = new GraphService(graph);
let count = 0;
graphService.dataDeleted$
.subscribe(
(deletedIds): void => {
count++;
expect(clusterIds.includes(deletedIds[0])).toBe(true);
expect(clusterIds.includes(deletedIds[1])).toBe(true);
expect(deleteClustersSpy.calls.count()).toBe(1);
expect(resetSpatialEdgesSpy.calls.count()).toBe(1);
if (count === 1) { done(); }
});
const type: ProviderEventType = "datadelete";
dataProvider.fire(type, {
type,
target: dataProvider,
clusterIds: clusterIds.slice(),
});
});
});
describe("GraphService.cacheSequence$", () => {
it("should cache sequence when graph does not have sequence", () => {
const api: APIWrapper = new APIWrapper(new DataProvider());
@ -488,6 +580,10 @@ class TestNode extends Image {
public get spatialEdges(): NavigationEdgeStatus {
return this._spatialEdges;
}
public hasInitializedCache(): boolean {
return true;
}
}
describe("GraphService.cacheNode$", () => {
@ -851,7 +947,7 @@ describe("GraphService.reset$", () => {
expect(e).toBeDefined();
});
graphService.reset$([]);
graphService.reset$();
cacheFull$.next(graph);
@ -914,7 +1010,7 @@ describe("GraphService.reset$", () => {
image.assetsCached = true;
cacheAssets$.next(image);
graphService.reset$([]);
graphService.reset$();
cacheNodeSequence$.next(graph);
@ -972,7 +1068,7 @@ describe("GraphService.reset$", () => {
image.assetsCached = true;
cacheAssets$.next(image);
graphService.reset$([]);
graphService.reset$();
cacheTiles$[0].next(graph);

View File

@ -12,6 +12,7 @@ export class GraphServiceMockCreator extends MockCreatorBase<GraphService> {
const mock: GraphService = new MockCreator().create(GraphService, "GraphService");
this._mockProperty(mock, "dataAdded$", new Subject<string>());
this._mockProperty(mock, "dataDeleted$", new Subject<string>());
this._mockProperty(mock, "dataReset$", new Subject<void>());
this._mockProperty(mock, "graphMode$", new Subject<GraphMode>());
this._mockProperty(mock, "filter$", new Subject<FilterFunction>());

View File

@ -1,7 +1,7 @@
import { bootstrap } from "../Bootstrap";
bootstrap();
import { of as observableOf, Subject } from "rxjs";
import { BehaviorSubject, of as observableOf, Subject } from "rxjs";
import { ImageHelper } from "../helper/ImageHelper";
@ -244,6 +244,68 @@ describe("CacheService.start", () => {
cacheService.stop();
});
it("should cache current image on data added event", () => {
const api = new APIWrapper(new DataProvider());
const graph = new Graph(api);
const graphService = new GraphService(graph);
spyOn(graphService, "uncache$").and.returnValue(observableOf<void>(null));
const stateService = new StateServiceMockCreator().create();
const cacheImageSpy = spyOn(graphService, "cacheImage$");
const cacheImageSubject = new Subject<Graph>();
cacheImageSpy.and.returnValue(cacheImageSubject);
spyOn(graphService, "hasImage$").and.returnValue(new BehaviorSubject(true));
const cacheService = new CacheService(graphService, stateService, api);
cacheService.start();
const coreImage1 = helper.createCoreImageEnt();
coreImage1.id = "image1";
(<Subject<string>>stateService.currentId$).next('image-id');
(<Subject<string>>graphService.dataAdded$).next('cell-id');
expect(cacheImageSpy.calls.count()).toBe(1);
expect(cacheImageSpy.calls.first().args.length).toBe(1);
expect(cacheImageSpy.calls.first().args[0]).toBe('image-id');
cacheService.stop();
});
it("should cache current image on data deleted event", () => {
const api = new APIWrapper(new DataProvider());
const graph = new Graph(api);
const graphService = new GraphService(graph);
spyOn(graphService, "uncache$").and.returnValue(observableOf<void>(null));
const stateService = new StateServiceMockCreator().create();
const cacheImageSpy = spyOn(graphService, "cacheImage$");
const cacheImageSubject = new Subject<Graph>();
cacheImageSpy.and.returnValue(cacheImageSubject);
spyOn(graphService, "hasImage$").and.returnValue(new BehaviorSubject(true));
const cacheService = new CacheService(graphService, stateService, api);
cacheService.start();
const coreImage1 = helper.createCoreImageEnt();
coreImage1.id = "image1";
(<Subject<string>>stateService.currentId$).next('image-id');
(<Subject<string[]>>graphService.dataDeleted$).next(['cluster-id']);
expect(cacheImageSpy.calls.count()).toBe(1);
expect(cacheImageSpy.calls.first().args.length).toBe(1);
expect(cacheImageSpy.calls.first().args[0]).toBe('image-id');
cacheService.stop();
});
it("should cache all trajectory images ahead if switching to spatial graph mode", () => {
const api = new APIWrapper(new DataProvider());
const graph = new Graph(api);

View File

@ -7,6 +7,7 @@ import {
throwError as observableThrowError,
Observable,
Subject,
BehaviorSubject,
} from "rxjs";
import { first } from "rxjs/operators";
@ -592,6 +593,9 @@ describe("Navigator.setFilter$", () => {
return cacheImageSubject2$;
});
const hasImageSubject = new Subject<boolean>();
const hasImageSpy = spyOn(graphService, "hasImage$").and.returnValue(hasImageSubject);
const navigator: Navigator =
new Navigator(
{ container: "co" },
@ -640,6 +644,8 @@ describe("Navigator.setFilter$", () => {
currentStateSubject$.complete();
setFilterSubject$.next(graph);
setFilterSubject$.complete();
hasImageSubject.next(true);
hasImageSubject.complete();
cacheImageSubject2$.next(image1);
cacheImageSubject2$.next(image2);
cacheImageSubject2$.complete();
@ -691,8 +697,7 @@ describe("Navigator.setToken$", () => {
expect(setTokenSpy.calls.first().args[0]).toBe("token");
expect(resetSpy.calls.count()).toBe(1);
expect(resetSpy.calls.first().args.length).toBe(1);
expect(resetSpy.calls.first().args[0]).toEqual([]);
expect(resetSpy.calls.first().args.length).toBe(0);
done();
});
@ -777,8 +782,7 @@ describe("Navigator.setToken$", () => {
expect(setTokenSpy.calls.first().args[0]).toBe("token");
expect(resetSpy.calls.count()).toBe(1);
expect(resetSpy.calls.first().args.length).toBe(1);
expect(resetSpy.calls.first().args[0].length).toBe(0);
expect(resetSpy.calls.first().args.length).toBe(0);
done();
});