mirror of
https://github.com/mapillary/mapillary-js.git
synced 2026-02-01 14:33:45 +00:00
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:
parent
8afde1f186
commit
92f544dde5
@ -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
|
||||
|
||||
@ -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': [
|
||||
|
||||
160
doc/src/js/utils/ChunkDataProvider.js
Normal file
160
doc/src/js/utils/ChunkDataProvider.js
Normal 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);
|
||||
}
|
||||
}
|
||||
49
doc/src/js/utils/DeletableProceduralDataProvider.js
Normal file
49
doc/src/js/utils/DeletableProceduralDataProvider.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
155
doc/src/js/utils/provider.js
Normal file
155
doc/src/js/utils/provider.js
Normal 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
166
examples/debug/chunk.html
Normal 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>
|
||||
95
examples/debug/delete.html
Normal file
95
examples/debug/delete.html
Normal 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>
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
17
src/api/events/ProviderClusterEvent.ts
Normal file
17
src/api/events/ProviderClusterEvent.ts
Normal 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";
|
||||
}
|
||||
@ -2,4 +2,5 @@
|
||||
* @event
|
||||
*/
|
||||
export type ProviderEventType =
|
||||
| "datacreate";
|
||||
| "datacreate"
|
||||
| "datadelete";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
1
src/external/api.ts
vendored
@ -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";
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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$;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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*/ }));
|
||||
|
||||
|
||||
@ -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$();
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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>());
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user