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
main
Boris Sekachev 6 years ago committed by GitHub
parent f329e14fe4
commit 9850094773
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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() | + | + | + | + | + | + | + | + |

@ -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);
}

@ -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;
}

@ -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;
}

@ -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,
});

@ -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 {

@ -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'];
}

@ -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,
};
}

@ -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;
})();

@ -290,7 +290,7 @@
* <li> clientID == 50 </li>
* <li> (label=="car" & attr["parked"]==true)
* | (label=="pedestrian" & width > 150) </li>
* <li> (( label==["car \\"mazda\\""]) &
* <li> (( label==["car \"mazda\""]) &
* (attr["sunglass ( help ) es"]==true |
* (width > 150 | height > 150 & (clientID == serverID))))) </li>
* </ul>

@ -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]))

@ -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);
});
});

@ -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');
});
});

@ -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<Promise<void>, {}, {}, 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<Promise<void>, {}, {}, 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<Promise<void>, {}, {}, 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<Promise<void>, {}, {}, 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<Promise<void>, {}, {}, AnyAction> {
.map((objectState: any): Promise<any> => 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) {

@ -0,0 +1,11 @@
<!--
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
<svg height="1em" width="1em" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g>
<rect rx="12" height="166" width="167" y="25" x="25" stroke-width="15" stroke="#000" fill="none" stroke-dasharray="35" />
<rect rx="12" height="166" width="167" y="70" x="70" stroke-width="15" stroke="#000" fill="none" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@ -0,0 +1,11 @@
<!--
Copyright (C) 2020 Intel Corporation
SPDX-License-Identifier: MIT
-->
<svg height="1em" width="1em" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
<g>
<rect rx="12" height="166" width="167" y="25" x="25" stroke-width="15" stroke="#000" fill="none" />
<rect rx="12" height="166" width="167" y="70" x="70" stroke-width="15" stroke="#000" fill="none" stroke-dasharray="35" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 434 B

@ -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;

@ -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<Props> {
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<Props> {
.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<Props> {
canvasInstance,
sidebarCollapsed,
activatedStateID,
curZLayer,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -143,6 +156,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView();
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
this.activateOnCanvas();
}
@ -462,13 +479,45 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
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
<Layout.Content
className='cvat-canvas-container'
/>
<Layout.Content style={{ position: 'relative' }}>
{/*
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
*/}
<div
className='cvat-canvas-container'
style={{
overflow: 'hidden',
width: '100%',
height: '100%',
}}
/>
<div className='cvat-canvas-z-axis-wrapper'>
<Slider
disabled={minZLayer === maxZLayer}
min={minZLayer}
max={maxZLayer}
value={curZLayer}
vertical
reverse
defaultValue={0}
onChange={(value: SliderValue): void => onSwitchZLayer(value as number)}
/>
<Tooltip title={`Add new layer ${maxZLayer + 1} and switch to it`}>
<Icon type='plus-circle' onClick={onAddZLayer} />
</Tooltip>
</div>
</Layout.Content>
);
}
}

@ -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 (
<Menu key='unique' className='cvat-object-item-menu'>
@ -57,6 +61,18 @@ function ItemMenu(
Propagate
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
<Menu.Item>
<Button
type='link'
@ -94,6 +110,8 @@ interface ItemTopComponentProps {
remove(): void;
propagate(): void;
createURL(): void;
toBackground(): void;
toForeground(): void;
}
function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
@ -109,6 +127,8 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
remove,
propagate,
createURL,
toBackground,
toForeground,
} = props;
return (
@ -130,7 +150,16 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
<Col span={2}>
<Dropdown
placement='bottomLeft'
overlay={ItemMenu(serverID, locked, copy, remove, propagate, createURL)}
overlay={ItemMenu(
serverID,
locked,
copy,
remove,
propagate,
createURL,
toBackground,
toForeground,
)}
>
<Icon type='more' />
</Dropdown>
@ -528,6 +557,8 @@ interface Props {
copy(): void;
propagate(): void;
createURL(): void;
toBackground(): void;
toForeground(): void;
remove(): void;
setOccluded(): void;
unsetOccluded(): void;
@ -595,6 +626,8 @@ function ObjectItemComponent(props: Props): JSX.Element {
copy,
propagate,
createURL,
toBackground,
toForeground,
remove,
setOccluded,
unsetOccluded,
@ -636,6 +669,8 @@ function ObjectItemComponent(props: Props): JSX.Element {
remove={remove}
propagate={propagate}
createURL={createURL}
toBackground={toBackground}
toForeground={toForeground}
/>
<ItemButtons
objectType={objectType}

@ -123,4 +123,50 @@
&:hover {
opacity: 1;
}
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}

@ -107,6 +107,7 @@
> .ant-slider-rail {
background-color: $player-slider-color;
}
}
.cvat-player-filename-wrapper {
@ -118,7 +119,7 @@
.cvat-player-frame-url-icon {
opacity: 0.7;
color: $info-icon-color;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;

@ -21,6 +21,8 @@ import {
activateObject,
selectObjects,
updateCanvasContextMenu,
addZLayer,
switchZLayer,
} from 'actions/annotation-actions';
import {
ColorBy,
@ -50,6 +52,9 @@ interface StateToProps {
gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
}
interface DispatchToProps {
@ -70,6 +75,8 @@ interface DispatchToProps {
onActivateObject: (activatedStateID: number | null) => void;
onSelectObjects: (selectedStatesID: number[]) => void;
onUpdateContextMenu(visible: boolean, left: number, top: number): void;
onAddZLayer(): void;
onSwitchZLayer(cur: number): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -95,6 +102,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
states: annotations,
activatedStateID,
selectedStatesID,
zLayer: {
cur: curZLayer,
min: minZLayer,
max: maxZLayer,
},
},
sidebarCollapsed,
},
@ -133,6 +145,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridOpacity,
activeLabelID,
activeObjectType,
curZLayer,
minZLayer,
maxZLayer,
};
}
@ -193,6 +208,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onUpdateContextMenu(visible: boolean, left: number, top: number): void {
dispatch(updateCanvasContextMenu(visible, left, top));
},
onAddZLayer(): void {
dispatch(addZLayer());
},
onSwitchZLayer(cur: number): void {
dispatch(switchZLayer(cur));
},
};
}

@ -33,6 +33,8 @@ interface StateToProps {
colorBy: ColorBy;
ready: boolean;
activeControl: ActiveControl;
minZLayer: number;
maxZLayer: number;
}
interface DispatchToProps {
@ -52,6 +54,10 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
states,
collapsed: statesCollapsed,
activatedStateID,
zLayer: {
min: minZLayer,
max: maxZLayer,
},
},
job: {
attributes: jobAttributes,
@ -93,6 +99,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
jobInstance,
frameNumber,
activated: activatedStateID === own.clientID,
minZLayer,
maxZLayer,
};
}
@ -220,6 +228,30 @@ class ObjectItemContainer extends React.PureComponent<Props> {
copy(url);
};
private toBackground = (): void => {
const {
objectState,
minZLayer,
} = this.props;
if (objectState.zOrder !== minZLayer) {
objectState.zOrder = minZLayer - 1;
this.commit();
}
};
private toForeground = (): void => {
const {
objectState,
maxZLayer,
} = this.props;
if (objectState.zOrder !== maxZLayer) {
objectState.zOrder = maxZLayer + 1;
this.commit();
}
};
private activate = (): void => {
const {
activateObject,
@ -404,6 +436,8 @@ class ObjectItemContainer extends React.PureComponent<Props> {
copy={this.copy}
propagate={this.propagate}
createURL={this.createURL}
toBackground={this.toBackground}
toForeground={this.toForeground}
setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside}

@ -33,6 +33,8 @@ import SVGInfoIcon from './assets/info-icon.svg';
import SVGFullscreenIcon from './assets/fullscreen-icon.svg';
import SVGObjectOutsideIcon from './assets/object-outside-icon.svg';
import SVGObjectInsideIcon from './assets/object-inside-icon.svg';
import SVGBackgroundIcon from './assets/background-icon.svg';
import SVGForegroundIcon from './assets/foreground-icon.svg';
export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />,
@ -133,3 +135,9 @@ export const ObjectOutsideIcon = React.memo(
export const ObjectInsideIcon = React.memo(
(): JSX.Element => <SVGObjectInsideIcon />,
);
export const BackgroundIcon = React.memo(
(): JSX.Element => <SVGBackgroundIcon />,
);
export const ForegroundIcon = React.memo(
(): JSX.Element => <SVGForegroundIcon />,
);

@ -58,6 +58,11 @@ const defaultState: AnnotationState = {
undo: [],
redo: [],
},
zLayer: {
min: 0,
max: 0,
cur: 0,
},
},
propagate: {
objectState: null,
@ -93,6 +98,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
colors,
filters,
frameData: data,
minZ,
maxZ,
} = action.payload;
return {
@ -112,6 +119,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations,
states,
filters,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
},
player: {
...state.player,
@ -160,6 +172,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
number,
data,
states,
minZ,
maxZ,
} = action.payload;
const activatedStateID = states
@ -180,6 +194,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations,
activatedStateID,
states,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
},
};
}
@ -431,6 +450,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const {
history,
states: updatedStates,
minZ,
maxZ,
} = action.payload;
const { states: prevStates } = state.annotations;
const nextStates = [...prevStates];
@ -443,10 +464,18 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}
}
const maxZLayer = Math.max(state.annotations.zLayer.max, maxZ);
const minZLayer = Math.min(state.annotations.zLayer.min, minZ);
return {
...state,
annotations: {
...state.annotations,
zLayer: {
min: minZLayer,
max: maxZLayer,
cur: maxZLayer,
},
states: nextStates,
history,
},
@ -841,6 +870,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const {
history,
states,
minZ,
maxZ,
} = action.payload;
const activatedStateID = states
@ -854,11 +885,16 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
activatedStateID,
states,
history,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
},
};
}
case AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
const { states, minZ, maxZ } = action.payload;
const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null;
@ -869,6 +905,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
...state.annotations,
activatedStateID,
states,
zLayer: {
min: minZ,
max: maxZ,
cur: maxZ,
},
},
};
}
@ -882,6 +923,49 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.SWITCH_Z_LAYER: {
const { cur } = action.payload;
const { max, min } = state.annotations.zLayer;
let { activatedStateID } = state.annotations;
if (activatedStateID !== null) {
const idx = state.annotations.states
.map((_state: any) => _state.clientID).indexOf(activatedStateID);
if (idx !== -1) {
if (state.annotations.states[idx].zOrder > cur) {
activatedStateID = null;
}
} else {
activatedStateID = null;
}
}
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
zLayer: {
...state.annotations.zLayer,
cur: Math.max(Math.min(cur, max), min),
},
},
};
}
case AnnotationActionTypes.ADD_Z_LAYER: {
const { max } = state.annotations.zLayer;
return {
...state,
annotations: {
...state.annotations,
zLayer: {
...state.annotations.zLayer,
max: max + 1,
cur: max + 1,
},
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,

@ -329,6 +329,11 @@ export interface AnnotationState {
uploading: boolean;
statuses: string[];
};
zLayer: {
min: number;
max: number;
cur: number;
};
};
propagate: {
objectState: any | null;

Loading…
Cancel
Save