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 ( @@ -57,6 +61,18 @@ function ItemMenu( Propagate + + + + + +