From 9850094773957c998690cb596fac302a6373ef32 Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Tue, 25 Feb 2020 15:27:04 +0300
Subject: [PATCH] React UI: ZOrder implementation (#1176)
* Drawn z-order switcher
* Z layer was added to state
* Added ZLayer API method cvat-canvas
* Added sorting by Z
* Displaying points in top
* Removed old code
* Improved sort function
* Drawn a couple of icons
* Send to foreground / send to background
* Updated unit tests
* Added unit tests for filter parser
* Removed extra code
* Updated README.md
---
cvat-canvas/README.md | 5 +-
cvat-canvas/src/typescript/canvas.ts | 5 +
.../src/typescript/canvasController.ts | 5 +
cvat-canvas/src/typescript/canvasModel.ts | 19 +++
cvat-canvas/src/typescript/canvasView.ts | 65 +++++++--
cvat-core/src/annotations-collection.js | 9 +-
cvat-core/src/annotations-filter.js | 4 +
cvat-core/src/annotations-objects.js | 38 +-----
cvat-core/src/object-state.js | 49 +------
cvat-core/src/session.js | 2 +-
cvat-core/tests/api/annotations.js | 47 ++++++-
cvat-core/tests/api/object-state.js | 42 ------
cvat-core/tests/internal/filter.js | 124 ++++++++++++++++++
cvat-ui/src/actions/annotation-actions.ts | 47 +++++++
cvat-ui/src/assets/background-icon.svg | 11 ++
cvat-ui/src/assets/foreground-icon.svg | 11 ++
cvat-ui/src/base.scss | 1 +
.../standard-workspace/canvas-wrapper.tsx | 61 ++++++++-
.../objects-side-bar/object-item.tsx | 37 +++++-
.../standard-workspace/styles.scss | 48 ++++++-
.../components/annotation-page/styles.scss | 3 +-
.../standard-workspace/canvas-wrapper.tsx | 21 +++
.../objects-side-bar/object-item.tsx | 34 +++++
cvat-ui/src/icons.tsx | 8 ++
cvat-ui/src/reducers/annotation-reducer.ts | 86 +++++++++++-
cvat-ui/src/reducers/interfaces.ts | 5 +
26 files changed, 633 insertions(+), 154 deletions(-)
create mode 100644 cvat-core/tests/internal/filter.js
create mode 100644 cvat-ui/src/assets/background-icon.svg
create mode 100644 cvat-ui/src/assets/foreground-icon.svg
diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md
index a45f1e043773..a52744332652 100644
--- a/cvat-canvas/README.md
+++ b/cvat-canvas/README.md
@@ -71,6 +71,7 @@ Canvas itself handles:
interface Canvas {
html(): HTMLDivElement;
+ setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
@@ -149,9 +150,6 @@ Standard JS events are used.
});
```
-## States
-
- ![](images/states.svg)
## API Reaction
@@ -172,3 +170,4 @@ Standard JS events are used.
| dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |
+| setZLayer() | + | + | + | + | + | + | + | + |
diff --git a/cvat-canvas/src/typescript/canvas.ts b/cvat-canvas/src/typescript/canvas.ts
index 560c46a28a49..94d5c226f0e7 100644
--- a/cvat-canvas/src/typescript/canvas.ts
+++ b/cvat-canvas/src/typescript/canvas.ts
@@ -34,6 +34,7 @@ const CanvasVersion = pjson.version;
interface Canvas {
html(): HTMLDivElement;
+ setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
activate(clientID: number | null, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void;
@@ -69,6 +70,10 @@ class CanvasImpl implements Canvas {
return this.view.html();
}
+ public setZLayer(zLayer: number | null): void {
+ this.model.setZLayer(zLayer);
+ }
+
public setup(frameData: any, objectStates: any[]): void {
this.model.setup(frameData, objectStates);
}
diff --git a/cvat-canvas/src/typescript/canvasController.ts b/cvat-canvas/src/typescript/canvasController.ts
index 30914a568b76..0f7ed36eee17 100644
--- a/cvat-canvas/src/typescript/canvasController.ts
+++ b/cvat-canvas/src/typescript/canvasController.ts
@@ -18,6 +18,7 @@ import {
export interface CanvasController {
readonly objects: any[];
+ readonly zLayer: number | null;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
readonly drawData: DrawData;
@@ -105,6 +106,10 @@ export class CanvasControllerImpl implements CanvasController {
this.model.geometry = geometry;
}
+ public get zLayer(): number | null {
+ return this.model.zLayer;
+ }
+
public get objects(): any[] {
return this.model.objects;
}
diff --git a/cvat-canvas/src/typescript/canvasModel.ts b/cvat-canvas/src/typescript/canvasModel.ts
index 8100f579d217..05e39139b2fd 100644
--- a/cvat-canvas/src/typescript/canvasModel.ts
+++ b/cvat-canvas/src/typescript/canvasModel.ts
@@ -80,6 +80,7 @@ export enum UpdateReasons {
IMAGE_FITTED = 'image_fitted',
IMAGE_MOVED = 'image_moved',
GRID_UPDATED = 'grid_updated',
+ SET_Z_LAYER = 'set_z_layer',
OBJECTS_UPDATED = 'objects_updated',
SHAPE_ACTIVATED = 'shape_activated',
@@ -113,6 +114,7 @@ export enum Mode {
export interface CanvasModel {
readonly image: HTMLImageElement | null;
readonly objects: any[];
+ readonly zLayer: number | null;
readonly gridSize: Size;
readonly focusData: FocusData;
readonly activeElement: ActiveElement;
@@ -124,6 +126,7 @@ export interface CanvasModel {
geometry: Geometry;
mode: Mode;
+ setZLayer(zLayer: number | null): void;
zoom(x: number, y: number, direction: number): void;
move(topOffset: number, leftOffset: number): void;
@@ -163,6 +166,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: boolean;
scale: number;
top: number;
+ zLayer: number | null;
drawData: DrawData;
mergeData: MergeData;
groupData: GroupData;
@@ -204,6 +208,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
rememberAngle: false,
scale: 1,
top: 0,
+ zLayer: null,
drawData: {
enabled: false,
initialState: null,
@@ -222,6 +227,11 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
};
}
+ public setZLayer(zLayer: number | null): void {
+ this.data.zLayer = zLayer;
+ this.notify(UpdateReasons.SET_Z_LAYER);
+ }
+
public zoom(x: number, y: number, direction: number): void {
const oldScale: number = this.data.scale;
const newScale: number = direction > 0 ? oldScale * 6 / 5 : oldScale * 5 / 6;
@@ -515,11 +525,20 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
));
}
+ public get zLayer(): number | null {
+ return this.data.zLayer;
+ }
+
public get image(): HTMLImageElement | null {
return this.data.image;
}
public get objects(): any[] {
+ if (this.data.zLayer !== null) {
+ return this.data.objects
+ .filter((object: any): boolean => object.zOrder <= this.data.zLayer);
+ }
+
return this.data.objects;
}
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index fdabf19936c1..ebda629c183b 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -77,12 +77,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
private onDrawDone(data: object, continueDraw?: boolean): void {
if (data) {
+ const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
bubbles: false,
cancelable: true,
detail: {
// eslint-disable-next-line new-cap
- state: data,
+ state: {
+ ...data,
+ zOrder: zLayer || 0,
+ },
continue: continueDraw,
},
});
@@ -364,6 +368,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
+
private setupObjects(states: any[]): void {
const { offset } = this.controller.geometry;
const translate = (points: number[]): number[] => points
@@ -403,6 +408,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.addObjects(created, translate);
this.updateObjects(updated, translate);
+ this.sortObjects();
if (this.controller.activeElement.clientID !== null) {
const { clientID } = this.controller.activeElement;
@@ -685,12 +691,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.setupObjects([]);
this.moveCanvas();
this.resizeCanvas();
- } else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) {
+ } else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
- } else if (reason === UpdateReasons.OBJECTS_UPDATED) {
+ } else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
}
@@ -833,6 +839,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shapeType: state.shapeType,
points: [...state.points],
attributes: { ...state.attributes },
+ zOrder: state.zOrder,
};
}
@@ -851,6 +858,15 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
+ if (drawnState.zOrder !== state.zOrder) {
+ if (state.shapeType === 'points') {
+ this.svgShapes[clientID].remember('_selectHandler').nested
+ .attr('data-z-order', state.zOrder);
+ } else {
+ this.svgShapes[clientID].attr('data-z-order', state.zOrder);
+ }
+ }
+
if (drawnState.occluded !== state.occluded) {
if (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
@@ -961,6 +977,27 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
+ private sortObjects(): void {
+ // TODO: Can be significantly optimized
+ const states = Array.from(
+ this.content.getElementsByClassName('cvat_canvas_shape'),
+ ).map((state: SVGElement): [SVGElement, number] => (
+ [state, +state.getAttribute('data-z-order')]
+ ));
+
+ const needSort = states.some((pair): boolean => pair[1] !== states[0][1]);
+ if (!states.length || !needSort) {
+ return;
+ }
+
+ const sorted = states.sort((a, b): number => a[1] - b[1]);
+ sorted.forEach((pair): void => {
+ this.content.appendChild(pair[0]);
+ });
+
+ this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
+ }
+
private deactivate(): void {
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
@@ -989,6 +1026,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
delete this.svgTexts[clientID];
}
+ this.sortObjects();
+
this.activeElement = {
clientID: null,
attributeID: null,
@@ -1016,6 +1055,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
+ if (!state) {
+ return;
+ }
+
if (state.shapeType === 'points') {
this.svgShapes[clientID].remember('_selectHandler').nested
.style('pointer-events', state.lock ? 'none' : '');
@@ -1040,7 +1083,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
const self = this;
- this.content.append(shape.node);
+ if (state.shapeType === 'points') {
+ this.content.append(this.svgShapes[clientID]
+ .remember('_selectHandler').nested.node);
+ } else {
+ this.content.append(shape.node);
+ }
+
(shape as any).draggable().on('dragstart', (): void => {
this.mode = Mode.DRAG;
if (text) {
@@ -1197,7 +1246,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
- zOrder: state.zOrder,
+ 'data-z-order': state.zOrder,
}).move(xtl, ytl)
.addClass('cvat_canvas_shape');
@@ -1221,7 +1270,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
- zOrder: state.zOrder,
+ 'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
@@ -1244,7 +1293,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
'shape-rendering': 'geometricprecision',
stroke: state.color,
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
- zOrder: state.zOrder,
+ 'data-z-order': state.zOrder,
}).addClass('cvat_canvas_shape');
if (state.occluded) {
@@ -1264,9 +1313,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
const group = basicPolyline.remember('_selectHandler').nested
.addClass('cvat_canvas_shape').attr({
clientID: state.clientID,
- zOrder: state.zOrder,
id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color,
+ 'data-z-order': state.zOrder,
}).style({
'fill-opacity': 1,
});
diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js
index dc312ea20b1e..3ab2910b990c 100644
--- a/cvat-core/src/annotations-collection.js
+++ b/cvat-core/src/annotations-collection.js
@@ -119,13 +119,11 @@
this.objects = {}; // key is a client id
this.count = 0;
this.flush = false;
- this.collectionZ = {}; // key is a frame, {max, min} are values
this.groups = {
max: 0,
}; // it is an object to we can pass it as an argument by a reference
this.injection = {
labels: this.labels,
- collectionZ: this.collectionZ,
groups: this.groups,
frameMeta: this.frameMeta,
history: this.history,
@@ -461,7 +459,7 @@
points: [...objectState.points],
occluded: objectState.occluded,
outside: objectState.outside,
- zOrder: 0,
+ zOrder: objectState.zOrder,
attributes: Object.keys(objectState.attributes)
.reduce((accumulator, attrID) => {
if (!labelAttributes[attrID].mutable) {
@@ -725,6 +723,7 @@
} else {
checkObjectType('state occluded', state.occluded, 'boolean', null);
checkObjectType('state points', state.points, null, Array);
+ checkObjectType('state zOrder', state.zOrder, 'integer', null);
for (const coord of state.points) {
checkObjectType('point coordinate', coord, 'number', null);
@@ -746,7 +745,7 @@
occluded: state.occluded || false,
points: [...state.points],
type: state.shapeType,
- z_order: 0,
+ z_order: state.zOrder,
});
} else if (state.objectType === 'track') {
constructed.tracks.push({
@@ -763,7 +762,7 @@
outside: false,
points: [...state.points],
type: state.shapeType,
- z_order: 0,
+ z_order: state.zOrder,
}],
});
} else {
diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js
index 0684ceb33420..625301675579 100644
--- a/cvat-core/src/annotations-filter.js
+++ b/cvat-core/src/annotations-filter.js
@@ -210,6 +210,10 @@ class AnnotationsFilter {
toJSONQuery(filters) {
try {
+ if (!Array.isArray(filters) || filters.some((value) => typeof (value) !== 'string')) {
+ throw Error('Argument must be an array of strings');
+ }
+
if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
diff --git a/cvat-core/src/annotations-objects.js b/cvat-core/src/annotations-objects.js
index cbd8c456220d..e5de9685e3a9 100644
--- a/cvat-core/src/annotations-objects.js
+++ b/cvat-core/src/annotations-objects.js
@@ -38,8 +38,6 @@
objectState.__internal = {
save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this),
- up: this.up.bind(this, frame, objectState),
- down: this.down.bind(this, frame, objectState),
};
return objectState;
@@ -270,22 +268,12 @@
super(data, clientID, injection);
this.frameMeta = injection.frameMeta;
- this.collectionZ = injection.collectionZ;
this.hidden = false;
this.color = color;
this.shapeType = null;
}
- _getZ(frame) {
- this.collectionZ[frame] = this.collectionZ[frame] || {
- max: 0,
- min: 0,
- };
-
- return this.collectionZ[frame];
- }
-
_validateStateBeforeSave(frame, data, updated) {
let fittedPoints = [];
@@ -392,20 +380,6 @@
'Is not implemented',
);
}
-
- // Increase ZOrder within frame
- up(frame, objectState) {
- const z = this._getZ(frame);
- z.max++;
- objectState.zOrder = z.max;
- }
-
- // Decrease ZOrder within frame
- down(frame, objectState) {
- const z = this._getZ(frame);
- z.min--;
- objectState.zOrder = z.min;
- }
}
class Shape extends Drawn {
@@ -414,10 +388,6 @@
this.points = data.points;
this.occluded = data.occluded;
this.zOrder = data.z_order;
-
- const z = this._getZ(this.frame);
- z.max = Math.max(z.max, this.zOrder || 0);
- z.min = Math.min(z.min, this.zOrder || 0);
}
// Method is used to export data to the server
@@ -582,10 +552,6 @@
}, {}),
};
- const z = this._getZ(value.frame);
- z.max = Math.max(z.max, value.z_order);
- z.min = Math.min(z.min, value.z_order);
-
return shapeAccumulator;
}, {});
}
@@ -1064,7 +1030,7 @@
points: [...leftPosition.points],
occluded: leftPosition.occluded,
outside: leftPosition.outside,
- zOrder: 0,
+ zOrder: leftPosition.zOrder,
keyframe: targetFrame in this.shapes,
};
}
@@ -1074,7 +1040,7 @@
points: [...rightPosition.points],
occluded: rightPosition.occluded,
outside: true,
- zOrder: 0,
+ zOrder: rightPosition.zOrder,
keyframe: targetFrame in this.shapes,
};
}
diff --git a/cvat-core/src/object-state.js b/cvat-core/src/object-state.js
index ff38205068d6..282ab344e619 100644
--- a/cvat-core/src/object-state.js
+++ b/cvat-core/src/object-state.js
@@ -34,7 +34,7 @@
occluded: null,
keyframe: null,
- zOrder: null,
+ zOrder: undefined,
lock: null,
color: null,
hidden: null,
@@ -372,36 +372,6 @@
.apiWrapper.call(this, ObjectState.prototype.delete, force);
return result;
}
-
- /**
- * Set the highest ZOrder within a frame
- * @method up
- * @memberof module:API.cvat.classes.ObjectState
- * @readonly
- * @instance
- * @async
- * @throws {module:API.cvat.exceptions.PluginError}
- */
- async up() {
- const result = await PluginRegistry
- .apiWrapper.call(this, ObjectState.prototype.up);
- return result;
- }
-
- /**
- * Set the lowest ZOrder within a frame
- * @method down
- * @memberof module:API.cvat.classes.ObjectState
- * @readonly
- * @instance
- * @async
- * @throws {module:API.cvat.exceptions.PluginError}
- */
- async down() {
- const result = await PluginRegistry
- .apiWrapper.call(this, ObjectState.prototype.down);
- return result;
- }
}
// Updates element in collection which contains it
@@ -422,22 +392,5 @@
return false;
};
- ObjectState.prototype.up.implementation = async function () {
- if (this.__internal && this.__internal.up) {
- return this.__internal.up();
- }
-
- return false;
- };
-
- ObjectState.prototype.down.implementation = async function () {
- if (this.__internal && this.__internal.down) {
- return this.__internal.down();
- }
-
- return false;
- };
-
-
module.exports = ObjectState;
})();
diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js
index 87b545157df6..c3c1ad6dd73b 100644
--- a/cvat-core/src/session.js
+++ b/cvat-core/src/session.js
@@ -290,7 +290,7 @@
*
clientID == 50
* (label=="car" & attr["parked"]==true)
* | (label=="pedestrian" & width > 150)
- * (( label==["car \\"mazda\\""]) &
+ * (( label==["car \"mazda\""]) &
* (attr["sunglass ( help ) es"]==true |
* (width > 150 | height > 150 & (clientID == serverID)))))
*
diff --git a/cvat-core/tests/api/annotations.js b/cvat-core/tests/api/annotations.js
index 3400ea4efc1a..bf91379f5b9a 100644
--- a/cvat-core/tests/api/annotations.js
+++ b/cvat-core/tests/api/annotations.js
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018 Intel Corporation
+ * Copyright (C) 2018-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@@ -85,6 +85,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
await task.annotations.put([state]);
@@ -104,6 +105,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
+ zOrder: 0,
});
await job.annotations.put([state]);
@@ -123,6 +125,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
await task.annotations.put([state]);
@@ -142,6 +145,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 100],
occluded: false,
label: job.task.labels[0],
+ zOrder: 0,
});
await job.annotations.put([state]);
@@ -158,6 +162,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(task.annotations.put([state]))
@@ -175,12 +180,45 @@ describe('Feature: put annotations', () => {
attributes: { 'bad key': 55 },
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(task.annotations.put([state]))
.rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
+ test('put shape with bad zOrder to a task', async () => {
+ const task = (await window.cvat.tasks.get({ id: 101 }))[0];
+ await task.annotations.clear(true);
+ const state = new window.cvat.classes.ObjectState({
+ frame: 1,
+ objectType: window.cvat.enums.ObjectType.SHAPE,
+ shapeType: window.cvat.enums.ObjectShape.POLYGON,
+ points: [0, 0, 100, 0, 100, 50],
+ attributes: { 'bad key': 55 },
+ occluded: true,
+ label: task.labels[0],
+ zOrder: 'bad value',
+ });
+
+ expect(task.annotations.put([state]))
+ .rejects.toThrow(window.cvat.exceptions.ArgumentError);
+
+ const state1 = new window.cvat.classes.ObjectState({
+ frame: 1,
+ objectType: window.cvat.enums.ObjectType.SHAPE,
+ shapeType: window.cvat.enums.ObjectShape.POLYGON,
+ points: [0, 0, 100, 0, 100, 50],
+ attributes: { 'bad key': 55 },
+ occluded: true,
+ label: task.labels[0],
+ zOrder: NaN,
+ });
+
+ expect(task.annotations.put([state1]))
+ .rejects.toThrow(window.cvat.exceptions.ArgumentError);
+ });
+
test('put shape without points and with invalud points to a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
await task.annotations.clear(true);
@@ -191,6 +229,7 @@ describe('Feature: put annotations', () => {
occluded: true,
points: [],
label: task.labels[0],
+ zOrder: 0,
});
await expect(task.annotations.put([state]))
@@ -214,6 +253,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(task.annotations.put([state]))
@@ -229,6 +269,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50],
occluded: true,
+ zOrder: 0,
});
await expect(task.annotations.put([state]))
@@ -253,6 +294,7 @@ describe('Feature: put annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(task.annotations.put([state]))
@@ -296,6 +338,7 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
@@ -341,6 +384,7 @@ describe('Feature: save annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: job.task.labels[0],
+ zOrder: 0,
});
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
@@ -574,6 +618,7 @@ describe('Feature: group annotations', () => {
points: [0, 0, 100, 0, 100, 50],
occluded: true,
label: task.labels[0],
+ zOrder: 0,
});
expect(task.annotations.group([state]))
diff --git a/cvat-core/tests/api/object-state.js b/cvat-core/tests/api/object-state.js
index 96988b3e0caf..1cf592ac4d43 100644
--- a/cvat-core/tests/api/object-state.js
+++ b/cvat-core/tests/api/object-state.js
@@ -303,45 +303,3 @@ describe('Feature: delete object', () => {
expect(annotationsAfter).toHaveLength(length - 1);
});
});
-
-describe('Feature: change z order of an object', () => {
- test('up z order for a shape', async () => {
- const task = (await window.cvat.tasks.get({ id: 100 }))[0];
- const annotations = await task.annotations.get(0);
- const state = annotations[0];
-
- const { zOrder } = state;
- await state.up();
- expect(state.zOrder).toBeGreaterThan(zOrder);
- });
-
- test('up z order for a track', async () => {
- const task = (await window.cvat.tasks.get({ id: 101 }))[0];
- const annotations = await task.annotations.get(0);
- const state = annotations[0];
-
- const { zOrder } = state;
- await state.up();
- expect(state.zOrder).toBeGreaterThan(zOrder);
- });
-
- test('down z order for a shape', async () => {
- const task = (await window.cvat.tasks.get({ id: 100 }))[0];
- const annotations = await task.annotations.get(0);
- const state = annotations[0];
-
- const { zOrder } = state;
- await state.down();
- expect(state.zOrder).toBeLessThan(zOrder);
- });
-
- test('down z order for a track', async () => {
- const task = (await window.cvat.tasks.get({ id: 101 }))[0];
- const annotations = await task.annotations.get(0);
- const state = annotations[0];
-
- const { zOrder } = state;
- await state.down();
- expect(state.zOrder).toBeLessThan(zOrder);
- });
-});
diff --git a/cvat-core/tests/internal/filter.js b/cvat-core/tests/internal/filter.js
new file mode 100644
index 000000000000..97336c43b0c6
--- /dev/null
+++ b/cvat-core/tests/internal/filter.js
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2018-2020 Intel Corporation
+ * SPDX-License-Identifier: MIT
+*/
+
+/* global
+ require:false
+ jest:false
+ describe:false
+*/
+
+// Setup mock for a server
+jest.mock('../../src/server-proxy', () => {
+ const mock = require('../mocks/server-proxy.mock');
+ return mock;
+});
+
+const AnnotationsFilter = require('../../src/annotations-filter');
+// Initialize api
+window.cvat = require('../../src/api');
+
+// Test cases
+describe('Feature: toJSONQuery', () => {
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter.toJSONQuery([]);
+ expect(Array.isArray(groups)).toBeTruthy();
+ expect(typeof (query)).toBe('string');
+ });
+
+ test('convert empty fitlers to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [, query] = annotationsFilter.toJSONQuery([]);
+ expect(query).toBe('$.objects[*].clientID');
+ });
+
+ test('convert wrong fitlers (empty string) to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ expect(() => {
+ annotationsFilter.toJSONQuery(['']);
+ }).toThrow(window.cvat.exceptions.ArgumentError);
+ });
+
+ test('convert wrong fitlers (wrong number argument) to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ expect(() => {
+ annotationsFilter.toJSONQuery(1);
+ }).toThrow(window.cvat.exceptions.ArgumentError);
+ });
+
+ test('convert wrong fitlers (wrong array argument) to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ expect(() => {
+ annotationsFilter.toJSONQuery(['clientID ==6', 1]);
+ }).toThrow(window.cvat.exceptions.ArgumentError);
+ });
+
+ test('convert wrong filters (wrong expression) to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ expect(() => {
+ annotationsFilter.toJSONQuery(['clientID=5']);
+ }).toThrow(window.cvat.exceptions.ArgumentError);
+ });
+
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter
+ .toJSONQuery(['clientID==5 & shape=="rectangle" & label==["car"]']);
+ expect(groups).toEqual([
+ ['clientID==5', '&', 'shape=="rectangle"', '&', 'label==["car"]'],
+ ]);
+ expect(query).toBe('$.objects[?((@.clientID==5&@.shape=="rectangle"&@.label==["car"]))].clientID');
+ });
+
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter
+ .toJSONQuery(['label=="car" | width >= height & type=="track"']);
+ expect(groups).toEqual([
+ ['label=="car"', '|', 'width >= height', '&', 'type=="track"'],
+ ]);
+ expect(query).toBe('$.objects[?((@.label=="car"|@.width>=@.height&@.type=="track"))].clientID');
+ });
+
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter
+ .toJSONQuery(['label=="person" & attr["Attribute 1"] ==attr["Attribute 2"]']);
+ expect(groups).toEqual([
+ ['label=="person"', '&', 'attr["Attribute 1"] ==attr["Attribute 2"]'],
+ ]);
+ expect(query).toBe('$.objects[?((@.label=="person"&@.attr["Attribute 1"]==@.attr["Attribute 2"]))].clientID');
+ });
+
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter
+ .toJSONQuery(['label=="car" & attr["parked"]==true', 'label=="pedestrian" & width > 150']);
+ expect(groups).toEqual([
+ ['label=="car"', '&', 'attr["parked"]==true'],
+ '|',
+ ['label=="pedestrian"', '&', 'width > 150'],
+ ]);
+ expect(query).toBe('$.objects[?((@.label=="car"&@.attr["parked"]==true)|(@.label=="pedestrian"&@.width>150))].clientID');
+ });
+
+ test('convert filters to a json query', () => {
+ const annotationsFilter = new AnnotationsFilter();
+ const [groups, query] = annotationsFilter
+ .toJSONQuery(['(( label==["car \\"mazda\\""]) & (attr["sunglass ( help ) es"]==true | (width > 150 | height > 150 & (clientID == serverID))))) ']);
+ expect(groups).toEqual([[[
+ ['label==["car `mazda`"]'],
+ '&',
+ ['attr["sunglass ( help ) es"]==true', '|',
+ ['width > 150', '|', 'height > 150', '&',
+ [
+ 'clientID == serverID',
+ ],
+ ],
+ ],
+ ]]]);
+ expect(query).toBe('$.objects[?((((@.label==["car `mazda`"])&(@.attr["sunglass ( help ) es"]==true|(@.width>150|@.height>150&(@.clientID==serverID))))))].clientID');
+ });
+});
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 08a0ad4204da..8459c0145902 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -42,6 +42,17 @@ function receiveAnnotationsParameters(): { filters: string[]; frame: number } {
};
}
+function computeZRange(states: any[]): number[] {
+ let minZ = states.length ? states[0].zOrder : 0;
+ let maxZ = states.length ? states[0].zOrder : 0;
+ states.forEach((state: any): void => {
+ minZ = Math.min(minZ, state.zOrder);
+ maxZ = Math.max(maxZ, state.zOrder);
+ });
+
+ return [minZ, maxZ];
+}
+
export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@@ -109,6 +120,23 @@ export enum AnnotationActionTypes {
CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS',
FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS',
FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED',
+ SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
+ ADD_Z_LAYER = 'ADD_Z_LAYER',
+}
+
+export function addZLayer(): AnyAction {
+ return {
+ type: AnnotationActionTypes.ADD_Z_LAYER,
+ };
+}
+
+export function switchZLayer(cur: number): AnyAction {
+ return {
+ type: AnnotationActionTypes.SWITCH_Z_LAYER,
+ payload: {
+ cur,
+ },
+ };
}
export function fetchAnnotationsAsync(sessionInstance: any):
@@ -117,10 +145,14 @@ ThunkAction, {}, {}, AnyAction> {
try {
const { filters, frame } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters);
+ const [minZ, maxZ] = computeZRange(states);
+
dispatch({
type: AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS,
payload: {
states,
+ minZ,
+ maxZ,
},
});
} catch (error) {
@@ -153,12 +185,15 @@ ThunkAction, {}, {}, AnyAction> {
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters);
+ const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: {
history,
states,
+ minZ,
+ maxZ,
},
});
} catch (error) {
@@ -182,12 +217,15 @@ ThunkAction, {}, {}, AnyAction> {
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters);
+ const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
history,
states,
+ minZ,
+ maxZ,
},
});
} catch (error) {
@@ -573,12 +611,15 @@ ThunkAction, {}, {}, AnyAction> {
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, false, filters);
+ const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
number: toFrame,
data,
states,
+ minZ,
+ maxZ,
},
});
} catch (error) {
@@ -661,6 +702,7 @@ export function getJobAsync(
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber);
const states = await job.annotations.get(frameNumber, false, filters);
+ const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
dispatch({
@@ -672,6 +714,8 @@ export function getJobAsync(
frameData,
colors,
filters,
+ minZ,
+ maxZ,
},
});
} catch (error) {
@@ -789,12 +833,15 @@ ThunkAction, {}, {}, AnyAction> {
.map((objectState: any): Promise => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
+ const [minZ, maxZ] = computeZRange(states);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
+ minZ,
+ maxZ,
},
});
} catch (error) {
diff --git a/cvat-ui/src/assets/background-icon.svg b/cvat-ui/src/assets/background-icon.svg
new file mode 100644
index 000000000000..8765d6d5cfa1
--- /dev/null
+++ b/cvat-ui/src/assets/background-icon.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/cvat-ui/src/assets/foreground-icon.svg b/cvat-ui/src/assets/foreground-icon.svg
new file mode 100644
index 000000000000..e54d5337f858
--- /dev/null
+++ b/cvat-ui/src/assets/foreground-icon.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss
index c4a83cd8dde5..23eb8bf4fe16 100644
--- a/cvat-ui/src/base.scss
+++ b/cvat-ui/src/base.scss
@@ -6,6 +6,7 @@ $inprogress-progress-color: #1890FF;
$pending-progress-color: #C1C1C1;
$border-color-1: #c3c3c3;
$border-color-2: #d9d9d9;
+$border-color-3: #242424;
$border-color-hover: #40a9ff;
$background-color-1: white;
$background-color-2: #F1F1F1;
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
index 6c9cc997d357..e7a08656f3e1 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
@@ -2,8 +2,13 @@ import React from 'react';
import {
Layout,
+ Slider,
+ Icon,
+ Tooltip,
} from 'antd';
+import { SliderValue } from 'antd/lib//slider';
+
import {
ColorBy,
GridColor,
@@ -39,6 +44,9 @@ interface Props {
gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
+ curZLayer: number;
+ minZLayer: number;
+ maxZLayer: number;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@@ -56,12 +64,15 @@ interface Props {
onActivateObject(activatedStateID: number | null): void;
onSelectObjects(selectedStatesID: number[]): void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
+ onAddZLayer(): void;
+ onSwitchZLayer(cur: number): void;
}
export default class CanvasWrapperComponent extends React.PureComponent {
public componentDidMount(): void {
const {
canvasInstance,
+ curZLayer,
} = this.props;
// It's awful approach from the point of view React
@@ -70,6 +81,7 @@ export default class CanvasWrapperComponent extends React.PureComponent {
.getElementsByClassName('cvat-canvas-container');
wrapper.appendChild(canvasInstance.html());
+ canvasInstance.setZLayer(curZLayer);
this.initialSetup();
this.updateCanvas();
}
@@ -89,6 +101,7 @@ export default class CanvasWrapperComponent extends React.PureComponent {
canvasInstance,
sidebarCollapsed,
activatedStateID,
+ curZLayer,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@@ -143,6 +156,10 @@ export default class CanvasWrapperComponent extends React.PureComponent {
this.updateShapesView();
}
+ if (prevProps.curZLayer !== curZLayer) {
+ canvasInstance.setZLayer(curZLayer);
+ }
+
this.activateOnCanvas();
}
@@ -462,13 +479,45 @@ export default class CanvasWrapperComponent extends React.PureComponent {
}
public render(): JSX.Element {
+ const {
+ maxZLayer,
+ curZLayer,
+ minZLayer,
+ onSwitchZLayer,
+ onAddZLayer,
+ } = this.props;
+
return (
- // This element doesn't have any props
- // So, React isn't going to rerender it
- // And it's a reason why cvat-canvas appended in mount function works
-
+
+ {/*
+ This element doesn't have any props
+ So, React isn't going to rerender it
+ And it's a reason why cvat-canvas appended in mount function works
+ */}
+
+
+ onSwitchZLayer(value as number)}
+ />
+
+
+
+
+
);
}
}
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index 725df0253989..5dc79c1ef2a0 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -26,6 +26,8 @@ import {
LastIcon,
PreviousIcon,
NextIcon,
+ BackgroundIcon,
+ ForegroundIcon,
} from 'icons';
import {
@@ -39,6 +41,8 @@ function ItemMenu(
remove: (() => void),
propagate: (() => void),
createURL: (() => void),
+ toBackground: (() => void),
+ toForeground: (() => void),
): JSX.Element {
return (