React UI: Sidebar with objects and optimizations for annotation view (#1089)

* Basic layout for objects panel

* Objects header

* A little name refactoring

* Side panel base layout

* Firefox specific exceptions

* Some minor fixes

* React & canvas optimizations

* Icons refactoring

* Little style refactoring

* Some style fixes

* Improved side panel with objects

* Actual attribute values

* Actual icons

* Hidden > visible

* hidden -> __internal

* Fixed hidden in ui

* Fixed some issues in canvas

* Fixed list height

* Color picker for labels

* A bit fixed design

* Actual header icons

* Changing attributes and switchable buttons

* Removed react memo (will reoptimize better)

* Sorting methods, removed cache from cvat-core (a lot of bugs related with it)

* Label switchers

* Fixed bug with update timestamp for shapes

* Annotation state refactoring

* Removed old resetCache calls

* Optimized top & left panels. Number of renders significantly decreased

* Optimized some extra renders

* Accelerated performance

* Fixed two minor issues

* Canvas improvements

* Minor fixes

* Removed extra code
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent 604be63841
commit 285df9d408

@ -152,6 +152,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
angle: number;
canvasSize: Size;
image: string;
imageID: number | null;
imageOffset: number;
imageSize: Size;
focusData: FocusData;
@ -183,6 +184,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: 0,
},
image: '',
imageID: null,
imageOffset: 0,
imageSize: {
height: 0,
@ -300,9 +302,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: (frameData.width as number),
};
if (!this.data.rememberAngle) {
if (this.data.imageID !== frameData.number && !this.data.rememberAngle) {
this.data.angle = 0;
}
this.data.imageID = frameData.number;
this.data.image = data;
this.notify(UpdateReasons.IMAGE_CHANGED);

@ -44,14 +44,6 @@ export interface CanvasView {
html(): HTMLDivElement;
}
interface ShapeDict {
[index: number]: SVG.Shape;
}
interface TextDict {
[index: number]: SVG.Text;
}
function darker(color: string, percentage: number): string {
const R = Math.round(parseInt(color.slice(1, 3), 16) * (1 - percentage / 100));
const G = Math.round(parseInt(color.slice(3, 5), 16) * (1 - percentage / 100));
@ -78,8 +70,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
private gridPath: SVGPathElement;
private gridPattern: SVGPatternElement;
private controller: CanvasController;
private svgShapes: ShapeDict;
private svgTexts: TextDict;
private svgShapes: Record<number, SVG.Shape>;
private svgTexts: Record<number, SVG.Text>;
private drawnStates: Record<number, any>;
private geometry: Geometry;
private drawHandler: DrawHandler;
private editHandler: EditHandler;
@ -382,6 +375,38 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
}
private setupObjects(states: any[]): void {
this.deactivate();
const created = [];
const updated = [];
for (const state of states) {
if (!(state.clientID in this.drawnStates)) {
created.push(state);
} else {
const drawnState = this.drawnStates[state.clientID];
if (drawnState.updated !== state.updated || drawnState.frame !== state.frame) {
updated.push(state);
}
}
}
const newIDs = states.map((state: any): number => state.clientID);
const deleted = Object.keys(this.drawnStates).map((clientID: string): number => +clientID)
.filter((id: number): boolean => !newIDs.includes(id))
.map((id: number): any => this.drawnStates[id]);
for (const state of deleted) {
if (state.clientID in this.svgTexts) {
this.svgTexts[state.clientID].remove();
}
this.svgShapes[state.clientID].remove();
delete this.drawnStates[state.clientID];
}
this.addObjects(created);
this.updateObjects(updated);
}
private selectize(value: boolean, shape: SVG.Element): void {
const self = this;
@ -457,6 +482,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.geometry = controller.geometry;
this.svgShapes = {};
this.svgTexts = {};
this.drawnStates = {};
this.activeElement = null;
this.mode = Mode.IDLE;
@ -621,31 +647,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
function setupObjects(objects: any[]): void {
const ctm = this.content.getScreenCTM()
.inverse().multiply(this.background.getScreenCTM());
this.deactivate();
// TODO: Compute difference
// Instead of simple clearing let's remove all objects properly
for (const id of Object.keys(this.svgShapes)) {
if (id in this.svgTexts) {
this.svgTexts[id].remove();
}
this.svgShapes[id].remove();
}
this.svgTexts = {};
this.svgShapes = {};
this.addObjects(ctm, objects);
// TODO: Update objects
// TODO: Delete objects
}
this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.image.length) {
@ -658,6 +659,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.transformCanvas();
}
} else if (reason === UpdateReasons.FITTED_CANVAS) {
// Canvas geometry is going to be changed. Old object positions aren't valid any more
this.setupObjects([]);
this.moveCanvas();
this.resizeCanvas();
} else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) {
@ -669,7 +672,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
}
setupObjects.call(this, this.controller.objects);
this.setupObjects(this.controller.objects);
if (this.mode === Mode.MERGE) {
this.mergeHandler.repeatSelection();
}
@ -792,20 +795,94 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.canvas;
}
private addObjects(ctm: SVGMatrix, states: any[]): void {
private saveState(state: any): void {
this.drawnStates[state.clientID] = {
clientID: state.clientID,
outside: state.outside,
occluded: state.occluded,
hidden: state.hidden,
lock: state.lock,
points: [...state.points],
attributes: { ...state.attributes },
};
}
private updateObjects(states: any[]): void {
for (const state of states) {
const { clientID } = state;
const drawnState = this.drawnStates[clientID];
if (drawnState.hidden !== state.hidden || drawnState.outside !== state.outside) {
const none = state.hidden || state.outside;
this.svgShapes[clientID].style('display', none ? 'none' : '');
}
if (drawnState.occluded !== state.occluded) {
if (state.occluded) {
this.svgShapes[clientID].addClass('cvat_canvas_shape_occluded');
} else {
this.svgShapes[clientID].removeClass('cvat_canvas_shape_occluded');
}
}
if (drawnState.points
.some((p: number, id: number): boolean => p !== state.points[id])
) {
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, state.points,
);
if (state.shapeType === 'rectangle') {
const [xtl, ytl, xbr, ybr] = translatedPoints;
this.svgShapes[clientID].attr({
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
});
} else {
const stringified = translatedPoints.reduce(
(acc: string, val: number, idx: number): string => {
if (idx % 2) {
return `${acc}${val} `;
}
return `${acc}${val},`;
}, '',
);
this.svgShapes[clientID].attr('points', stringified);
}
}
for (const attrID of Object.keys(state.attributes)) {
if (state.attributes[attrID] !== drawnState.attributes[attrID]) {
const text = this.svgTexts[state.clientID];
if (text) {
const [span] = this.svgTexts[state.clientID].node
.querySelectorAll(`[attrID="${attrID}"]`) as any as SVGTSpanElement[];
if (span && span.textContent) {
const prefix = span.textContent.split(':').slice(0, -1).join(':');
span.textContent = `${prefix}: ${state.attributes[attrID]}`;
}
}
}
}
this.saveState(state);
}
}
private addObjects(states: any[]): void {
for (const state of states) {
if (state.objectType === 'tag') {
this.addTag(state);
} else {
const points: number[] = (state.points as number[]);
const translatedPoints: number[] = [];
for (let i = 0; i <= points.length - 1; i += 2) {
let point: SVGPoint = this.background.createSVGPoint();
point.x = points[i];
point.y = points[i + 1];
point = point.matrixTransform(ctm);
translatedPoints.push(point.x, point.y);
}
const translatedPoints: number[] = translateBetweenSVG(
this.background, this.content, points,
);
// TODO: Use enums after typification cvat-core
if (state.shapeType === 'rectangle') {
@ -833,16 +910,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
.addPoints(stringified, state);
}
}
// TODO: Use enums after typification cvat-core
if (state.visibility === 'all') {
this.svgTexts[state.clientID] = this.addText(state);
this.updateTextPosition(
this.svgTexts[state.clientID],
this.svgShapes[state.clientID],
);
}
}
this.saveState(state);
}
}
@ -852,17 +922,22 @@ export class CanvasViewImpl implements CanvasView, Listener {
const shape = this.svgShapes[this.activeElement.state.clientID];
shape.removeClass('cvat_canvas_shape_activated');
(shape as any).off('dragstart');
(shape as any).off('dragend');
(shape as any).draggable(false);
if (state.shapeType !== 'points') {
this.selectize(false, shape);
}
(shape as any).off('resizestart');
(shape as any).off('resizing');
(shape as any).off('resizedone');
(shape as any).resize(false);
// Hide text only if it is hidden by settings
// TODO: Hide text only if it is hidden by settings
const text = this.svgTexts[state.clientID];
if (text && state.visibility === 'shape') {
if (text) {
text.remove();
delete this.svgTexts[state.clientID];
}
@ -893,7 +968,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
shape.addClass('cvat_canvas_shape_activated');
let text = this.svgTexts[activeElement.clientID];
// Draw text if it's hidden by default
if (!text && state.visibility === 'shape') {
if (!text) {
text = this.addText(state);
this.svgTexts[state.clientID] = text;
this.updateTextPosition(
@ -1057,6 +1132,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
rect.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
rect.style('display', 'none');
}
return rect;
}
@ -1076,6 +1155,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
polygon.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
polygon.style('display', 'none');
}
return polygon;
}
@ -1095,6 +1178,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
polyline.addClass('cvat_canvas_shape_occluded');
}
if (state.hidden || state.outside) {
polyline.style('display', 'none');
}
return polyline;
}
@ -1120,6 +1207,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}).style({
'fill-opacity': 1,
});
if (state.hidden || state.outside) {
group.style('display', 'none');
}
group.bbox = shape.bbox.bind(shape);
group.clone = shape.clone.bind(shape);

@ -34,13 +34,9 @@
const {
ObjectShape,
ObjectType,
colors,
} = require('./enums');
const ObjectState = require('./object-state');
const colors = [
'#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380'];
function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData;
@ -381,9 +377,6 @@
// Remove other shapes
for (const object of objectsForMerge) {
object.removed = true;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}
}
@ -470,7 +463,6 @@
// Remove source object
object.removed = true;
object.resetCache();
}
group(objectStates, reset) {
@ -490,9 +482,6 @@
const groupIdx = reset ? 0 : ++this.groups.max;
for (const object of objectsForGroup) {
object.group = groupIdx;
if (typeof (object.resetCache) === 'function') {
object.resetCache();
}
}
return groupIdx;

@ -11,13 +11,11 @@
const ObjectState = require('./object-state');
const {
checkObjectType,
isEnum,
} = require('./common');
const {
ObjectShape,
ObjectType,
AttributeType,
VisibleState,
} = require('./enums');
const {
@ -32,7 +30,8 @@
function objectStateFactory(frame, data) {
const objectState = new ObjectState(data);
objectState.hidden = {
// eslint-disable-next-line no-underscore-dangle
objectState.__internal = {
save: this.save.bind(this, frame, objectState),
delete: this.delete.bind(this),
up: this.up.bind(this, frame, objectState),
@ -127,6 +126,10 @@
return ['true', 'false'].includes(value.toLowerCase());
}
if (type === AttributeType.TEXT) {
return true;
}
return values.includes(value);
}
@ -140,6 +143,7 @@
this.frame = data.frame;
this.removed = false;
this.lock = false;
this.updated = Date.now();
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
@ -158,6 +162,16 @@
}
}
updateTimestamp(updated) {
const anyChanges = updated.label || updated.attributes || updated.points
|| updated.outside || updated.occluded || updated.keyframe
|| updated.group || updated.zOrder;
if (anyChanges) {
this.updated = Date.now();
}
}
delete(force) {
if (!this.lock || force) {
this.removed = true;
@ -173,7 +187,7 @@
this.frameMeta = injection.frameMeta;
this.collectionZ = injection.collectionZ;
this.visibility = VisibleState.SHAPE;
this.hidden = false;
this.color = color;
this.shapeType = null;
@ -277,7 +291,8 @@
label: this.label,
group: this.group,
color: this.color,
visibility: this.visibility,
hidden: this.hidden,
updated: this.updated,
frame,
};
}
@ -313,9 +328,14 @@
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes
&& validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
if (attrID in labelAttributes) {
if (validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
@ -380,24 +400,22 @@
copy.color = data.color;
}
if (updated.visibility) {
if (!isEnum.call(VisibleState, data.visibility)) {
throw new ArgumentError(
`Got invalid visibility value: "${data.visibility}"`,
);
}
copy.visibility = data.visibility;
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
copy.hidden = data.hidden;
}
// Reset flags and commit all changes
updated.reset();
// Commit state
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
// Reset flags and commit all changes
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
}
@ -424,8 +442,6 @@
return shapeAccumulator;
}, {});
this.cache = {};
}
// Method is used to export data to the server
@ -480,27 +496,21 @@
// Method is used to construct ObjectState objects
get(frame) {
if (!(frame in this.cache)) {
const interpolation = {
...this.getPosition(frame),
attributes: this.getAttributes(frame),
group: this.group,
objectType: ObjectType.TRACK,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
color: this.color,
visibility: this.visibility,
frame,
};
this.cache[frame] = interpolation;
}
const result = JSON.parse(JSON.stringify(this.cache[frame]));
result.label = this.label;
return result;
return {
...this.getPosition(frame),
attributes: this.getAttributes(frame),
group: this.group,
objectType: ObjectType.TRACK,
shapeType: this.shapeType,
clientID: this.clientID,
serverID: this.serverID,
lock: this.lock,
color: this.color,
hidden: this.hidden,
updated: this.updated,
label: this.label,
frame,
};
}
neighborsFrames(targetFrame) {
@ -584,9 +594,14 @@
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes
&& validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
if (attrID in labelAttributes) {
if (validateAttributeValue(value, labelAttributes[attrID])) {
copy.attributes[attrID] = value;
} else {
throw new ArgumentError(
`Trying to save an attribute attribute with id ${attrID} and invalid value ${value}`,
);
}
} else {
throw new ArgumentError(
`Trying to save unknown attribute with id ${attrID} and value ${value}`,
@ -660,14 +675,9 @@
copy.color = data.color;
}
if (updated.visibility) {
if (!isEnum.call(VisibleState, data.visibility)) {
throw new ArgumentError(
`Got invalid visibility value: "${data.visibility}"`,
);
}
copy.visibility = data.visibility;
if (updated.hidden) {
checkObjectType('hidden', data.hidden, 'boolean', null);
copy.hidden = data.hidden;
}
if (updated.keyframe) {
@ -680,16 +690,14 @@
if (prop in this) {
this[prop] = copy[prop];
}
this.cache[frame][prop] = copy[prop];
}
if (updated.attributes) {
// Mutable attributes will be updated below
for (const attrID of Object.keys(copy.attributes)) {
if (!labelAttributes[attrID].mutable) {
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.attributes[attrID] = data.attributes[attrID];
this.attributes[attrID] = data.attributes[attrID];
}
}
}
@ -702,42 +710,21 @@
// Remove keyframe
if (updated.keyframe && !data.keyframe) {
// Remove all cache after this keyframe because it have just become outdated
for (const cacheFrame in this.cache) {
if (+cacheFrame > frame) {
delete this.cache[cacheFrame];
if (frame in this.shapes) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You cannot remove the latest keyframe of a track');
}
}
this.cache[frame].keyframe = false;
delete this.shapes[frame];
updated.reset();
delete this.shapes[frame];
this.updateTimestamp(updated);
updated.reset();
}
return objectStateFactory.call(this, frame, this.get(frame));
}
// Add/update keyframe
if (positionUpdated || (updated.keyframe && data.keyframe)) {
// Remove affected cached frames
const {
leftFrame,
rightFrame,
} = this.neighborsFrames(frame);
for (const cacheFrame of Object.keys(this.cache)) {
if (leftFrame === null && +cacheFrame < frame) {
delete this.cache[cacheFrame];
} else if (+cacheFrame < frame && +cacheFrame > leftFrame) {
delete this.cache[cacheFrame];
}
if (rightFrame === null && +cacheFrame > frame) {
delete this.cache[cacheFrame];
} else if (+cacheFrame > frame && +cacheFrame < rightFrame) {
delete this.cache[cacheFrame];
}
}
this.cache[frame].keyframe = true;
if (positionUpdated || updated.attributes || (updated.keyframe && data.keyframe)) {
data.keyframe = true;
this.shapes[frame] = {
@ -760,6 +747,7 @@
}
}
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
@ -823,15 +811,10 @@
delete(force) {
if (!this.lock || force) {
this.removed = true;
this.resetCache();
}
return true;
}
resetCache() {
this.cache = {};
}
}
class Tag extends Annotation {
@ -874,6 +857,7 @@
attributes: { ...this.attributes },
label: this.label,
group: this.group,
updated: this.updated,
frame,
};
}
@ -921,14 +905,17 @@
copy.lock = data.lock;
}
// Reset flags and commit all changes
updated.reset();
// Commit state
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
// Reset flags and commit all changes
this.updateTimestamp(updated);
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
}

@ -167,10 +167,6 @@
for (let i = 0; i < indexes[type].length; i++) {
const clientID = indexes[type][i];
this.collection.objects[clientID].serverID = saved[type][i].id;
if (type === 'tracks') {
// We have to reset cache because of old value of serverID was saved there
this.collection.objects[clientID].resetCache();
}
}
}
}

@ -27,8 +27,8 @@ function build() {
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
colors,
} = require('./enums');
const {
@ -497,8 +497,8 @@ function build() {
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
colors,
},
/**
* Namespace is used for access to exceptions

@ -102,22 +102,6 @@
POINTS: 'points',
});
/**
* Object visibility states
* @enum {string}
* @name ObjectShape
* @memberof module:API.cvat.enums
* @property {string} ALL 'all'
* @property {string} SHAPE 'shape'
* @property {string} NONE 'none'
* @readonly
*/
const VisibleState = Object.freeze({
ALL: 'all',
SHAPE: 'shape',
NONE: 'none',
});
/**
* Event types
* @enum {number}
@ -182,6 +166,21 @@
rotateImage: 26,
};
/**
* Array of hex color
* @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}
* @readonly
*/
const colors = [
'#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380',
];
module.exports = {
ShareFileType,
TaskStatus,
@ -189,7 +188,7 @@
AttributeType,
ObjectType,
ObjectShape,
VisibleState,
LogType,
colors,
};
})();

@ -52,6 +52,13 @@
value: tid,
writable: false,
},
/**
* @name number
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
number: {
value: number,
writable: false,

@ -8,7 +8,10 @@
*/
(() => {
const { AttributeType } = require('./enums');
const {
AttributeType,
colors,
} = require('./enums');
const { ArgumentError } = require('./exceptions');
/**
@ -136,6 +139,7 @@
const data = {
id: undefined,
name: undefined,
color: undefined,
};
for (const key in data) {
@ -146,6 +150,9 @@
}
}
if (typeof (data.id) !== 'undefined') {
data.color = colors[data.id % colors.length];
}
data.attributes = [];
if (Object.prototype.hasOwnProperty.call(initialData, 'attributes')
@ -176,6 +183,23 @@
name: {
get: () => data.name,
},
/**
* @name color
* @type {string}
* @memberof module:API.cvat.classes.Label
* @readonly
* @instance
*/
color: {
get: () => data.color,
set: (color) => {
if (colors.includes(color)) {
data.color = color;
} else {
throw new ArgumentError('Trying to set unknown color');
}
},
},
/**
* @name attributes
* @type {module:API.cvat.classes.Attribute[]}

@ -19,11 +19,8 @@
/**
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* Necessary fields: objectType, shapeType
* (don't have setters)
* Necessary fields for objects which haven't been added to collection yet: frame
* (doesn't have setters)
* Optional fields: points, group, zOrder, outside, occluded,
* Necessary fields: objectType, shapeType, frame, updated
* Optional fields: points, group, zOrder, outside, occluded, hidden,
* attributes, lock, label, mode, color, keyframe, clientID, serverID
* These fields can be set later via setters
*/
@ -41,7 +38,8 @@
zOrder: null,
lock: null,
color: null,
visibility: null,
hidden: null,
updated: serialized.updated,
clientID: serialized.clientID,
serverID: serialized.serverID,
@ -67,7 +65,9 @@
this.zOrder = false;
this.lock = false;
this.color = false;
this.visibility = false;
this.hidden = false;
return reset;
},
writable: false,
});
@ -153,17 +153,17 @@
data.color = color;
},
},
visibility: {
hidden: {
/**
* @name visibility
* @type {module:API.cvat.enums.VisibleState}
* @name hidden
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.visibility,
set: (visibility) => {
data.updateFlags.visibility = true;
data.visibility = visibility;
get: () => data.hidden,
set: (hidden) => {
data.updateFlags.hidden = true;
data.hidden = hidden;
},
},
points: {
@ -266,6 +266,17 @@
data.lock = lock;
},
},
updated: {
/**
* Timestamp of the latest updated of the object
* @name updated
* @type {number}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @readonly
*/
get: () => data.updated,
},
attributes: {
/**
* Object is id:value pairs where "id" is an integer
@ -302,7 +313,7 @@
this.occluded = serialized.occluded;
this.color = serialized.color;
this.lock = serialized.lock;
this.visibility = serialized.visibility;
this.hidden = serialized.hidden;
// It can be undefined in a constructor and it can be defined later
if (typeof (serialized.points) !== 'undefined') {
@ -382,8 +393,8 @@
// Updates element in collection which contains it
ObjectState.prototype.save.implementation = async function () {
if (this.hidden && this.hidden.save) {
return this.hidden.save();
if (this.__internal && this.__internal.save) {
return this.__internal.save();
}
return this;
@ -391,24 +402,24 @@
// Delete element from a collection which contains it
ObjectState.prototype.delete.implementation = async function (force) {
if (this.hidden && this.hidden.delete) {
return this.hidden.delete(force);
if (this.__internal && this.__internal.delete) {
return this.__internal.delete(force);
}
return false;
};
ObjectState.prototype.up.implementation = async function () {
if (this.hidden && this.hidden.up) {
return this.hidden.up();
if (this.__internal && this.__internal.up) {
return this.__internal.up();
}
return false;
};
ObjectState.prototype.down.implementation = async function () {
if (this.hidden && this.hidden.down) {
return this.hidden.down();
if (this.__internal && this.__internal.down) {
return this.__internal.down();
}
return false;

@ -1094,6 +1094,27 @@
"@types/react": "*"
}
},
"@types/redux-logger": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.7.tgz",
"integrity": "sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A==",
"requires": {
"redux": "^3.6.0"
},
"dependencies": {
"redux": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
"integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
"requires": {
"lodash": "^4.2.1",
"lodash-es": "^4.2.1",
"loose-envify": "^1.1.0",
"symbol-observable": "^1.0.3"
}
}
}
},
"@typescript-eslint/eslint-plugin": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
@ -3217,6 +3238,11 @@
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
"dev": true
},
"deep-diff": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
"integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
},
"deep-equal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.0.tgz",
@ -6571,6 +6597,11 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"lodash._reinterpolate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
@ -9510,6 +9541,14 @@
"symbol-observable": "^1.2.0"
}
},
"redux-logger": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
"integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
"requires": {
"deep-diff": "^0.3.5"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",

@ -48,6 +48,7 @@
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.0",
"@types/react-share": "^3.0.1",
"@types/redux-logger": "^3.0.7",
"antd": "^3.25.2",
"copy-to-clipboard": "^3.2.0",
"dotenv-webpack": "^1.7.0",
@ -60,6 +61,7 @@
"react-router-dom": "^5.1.0",
"react-share": "^3.0.1",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
}
}

@ -20,10 +20,10 @@ function getAbout(): AnyAction {
return action;
}
function getAboutSuccess(about: any): AnyAction {
function getAboutSuccess(server: any): AnyAction {
const action = {
type: AboutActionTypes.GET_ABOUT_SUCCESS,
payload: { about },
payload: { server },
};
return action;

@ -24,21 +24,66 @@ export enum AnnotationActionTypes {
SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS',
SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS',
SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED',
SAVE_ANNOTATIONS_UPDATED_STATUS = 'SAVE_ANNOTATIONS_UPDATED_STATUS',
SAVE_UPDATE_ANNOTATIONS_STATUS = 'SAVE_UPDATE_ANNOTATIONS_STATUS',
SWITCH_PLAY = 'SWITCH_PLAY',
CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY',
DRAG_CANVAS = 'DRAG_CANVAS',
ZOOM_CANVAS = 'ZOOM_CANVAS',
DRAW_SHAPE = 'DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
MERGE_OBJECTS = 'MERGE_OBJECTS',
OBJECTS_MERGED = 'OBJECTS_MERGED',
GROUP_OBJECTS = 'GROUP_OBJECTS',
OBJECTS_GROUPPED = 'OBJECTS_GROUPPED',
SPLIT_TRACK = 'SPLIT_TRACK',
TRACK_SPLITTED = 'TRACK_SPLITTED',
DRAW_SHAPE = 'DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
RESET_CANVAS = 'RESET_CANVAS',
ANNOTATIONS_UPDATED = 'ANNOTATIONS_UPDATED',
UPDATE_ANNOTATIONS_SUCCESS = 'UPDATE_ANNOTATIONS_SUCCESS',
UPDATE_ANNOTATIONS_FAILED = 'UPDATE_ANNOTATIONS_FAILED',
CREATE_ANNOTATIONS_SUCCESS = 'CREATE_ANNOTATIONS_SUCCESS',
CREATE_ANNOTATIONS_FAILED = 'CREATE_ANNOTATIONS_FAILED',
MERGE_ANNOTATIONS_SUCCESS = 'MERGE_ANNOTATIONS_SUCCESS',
MERGE_ANNOTATIONS_FAILED = 'MERGE_ANNOTATIONS_FAILED',
GROUP_ANNOTATIONS_SUCCESS = 'GROUP_ANNOTATIONS_SUCCESS',
GROUP_ANNOTATIONS_FAILED = 'GROUP_ANNOTATIONS_FAILED',
SPLIT_ANNOTATIONS_SUCCESS = 'SPLIT_ANNOTATIONS_SUCCESS',
SPLIT_ANNOTATIONS_FAILED = 'SPLIT_ANNOTATIONS_FAILED',
CHANGE_LABEL_COLOR_SUCCESS = 'CHANGE_LABEL_COLOR_SUCCESS',
CHANGE_LABEL_COLOR_FAILED = 'CHANGE_LABEL_COLOR_FAILED',
UPDATE_TAB_CONTENT_HEIGHT = 'UPDATE_TAB_CONTENT_HEIGHT',
COLLAPSE_SIDEBAR = 'COLLAPSE_SIDEBAR',
COLLAPSE_APPEARANCE = 'COLLAPSE_APPEARANCE',
COLLAPSE_OBJECT_ITEMS = 'COLLAPSE_OBJECT_ITEMS'
}
export function updateTabContentHeight(tabContentHeight: number): AnyAction {
return {
type: AnnotationActionTypes.UPDATE_TAB_CONTENT_HEIGHT,
payload: {
tabContentHeight,
},
};
}
export function collapseSidebar(): AnyAction {
return {
type: AnnotationActionTypes.COLLAPSE_SIDEBAR,
payload: {},
};
}
export function collapseAppearance(): AnyAction {
return {
type: AnnotationActionTypes.COLLAPSE_APPEARANCE,
payload: {},
};
}
export function collapseObjectItems(states: any[], collapsed: boolean): AnyAction {
return {
type: AnnotationActionTypes.COLLAPSE_OBJECT_ITEMS,
payload: {
states,
collapsed,
},
};
}
export function switchPlay(playing: boolean): AnyAction {
@ -50,47 +95,56 @@ export function switchPlay(playing: boolean): AnyAction {
};
}
export function changeFrameAsync(toFrame: number, playing: boolean):
export function changeFrameAsync(toFrame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const store = getCVATStore();
const state: CombinedState = store.getState();
const { jobInstance } = state.annotation;
const currentFrame = state.annotation.frame;
const { instance: job } = state.annotation.job;
const { number: frame } = state.annotation.player.frame;
const frame = Math.max(
Math.min(toFrame, jobInstance.stopFrame),
jobInstance.startFrame,
);
// !playing || state.annotation.playing prevents changing frame on the latest setTimeout
// after playing had become false
if (frame !== currentFrame && (!playing || state.annotation.playing)) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME,
payload: {},
});
try {
if (toFrame < job.startFrame || toFrame > job.stopFrame) {
throw Error(`Required frame ${toFrame} is out of the current job`);
}
try {
const frameData = await jobInstance.frames.get(frame);
const annotations = await jobInstance.annotations.get(frame);
if (toFrame === frame) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
frame,
frameData,
annotations,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_FAILED,
payload: {
frame,
error,
number: state.annotation.player.frame.number,
data: state.annotation.player.frame.data,
states: state.annotation.annotations.states,
},
});
return;
}
// Start async requests
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME,
payload: {},
});
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame);
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
number: toFrame,
data,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_FAILED,
payload: {
number: toFrame,
error,
},
});
}
};
}
@ -138,30 +192,37 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
// First check state if the task is already there
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0];
// If there aren't the task, get it from the server
if (!task) {
[task] = await cvat.tasks.get({ id: tid });
}
// Finally get the job from the task
const job = task.jobs
.filter((_job: any) => _job.id === jid)[0];
if (!job) {
throw new Error('Job with specified id does not exist');
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
}
const frame = Math.min(0, job.startFrame);
const frameData = await job.frames.get(frame);
const annotations = await job.annotations.get(frame);
const frameNumber = Math.max(0, job.startFrame);
const frameData = await job.frames.get(frameNumber);
const states = await job.annotations.get(frameNumber);
const colors = [...cvat.enums.colors];
dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: {
jobInstance: job,
job,
states,
frameNumber,
frameData,
annotations,
frame,
colors,
},
});
} catch (error) {
@ -186,7 +247,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS,
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
payload: {
status,
},
@ -242,53 +303,172 @@ export function shapeDrawn(): AnyAction {
};
}
export function mergeObjects(): AnyAction {
export function mergeObjects(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.MERGE_OBJECTS,
payload: {},
payload: {
enabled,
},
};
}
export function objectsMerged(): AnyAction {
export function groupObjects(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.OBJECTS_MERGED,
payload: {},
type: AnnotationActionTypes.GROUP_OBJECTS,
payload: {
enabled,
},
};
}
export function groupObjects(): AnyAction {
export function splitTrack(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.GROUP_OBJECTS,
payload: {},
type: AnnotationActionTypes.SPLIT_TRACK,
payload: {
enabled,
},
};
}
export function objectsGroupped(): AnyAction {
return {
type: AnnotationActionTypes.OBJECTS_GROUPPED,
payload: {},
export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const promises = statesToUpdate.map((state: any): Promise<any> => state.save());
const states = await Promise.all(promises);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
payload: {
error,
states,
},
});
}
};
}
export function splitTrack(): AnyAction {
return {
type: AnnotationActionTypes.SPLIT_TRACK,
payload: {},
export function createAnnotationsAsync(sessionInstance: any, frame: number, statesToCreate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.annotations.put(statesToCreate);
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.CREATE_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function trackSplitted(): AnyAction {
return {
type: AnnotationActionTypes.TRACK_SPLITTED,
payload: {},
export function mergeAnnotationsAsync(sessionInstance: any, frame: number, statesToMerge: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.annotations.merge(statesToMerge);
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.MERGE_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function annotationsUpdated(annotations: any[]): AnyAction {
return {
type: AnnotationActionTypes.ANNOTATIONS_UPDATED,
payload: {
annotations,
},
export function groupAnnotationsAsync(sessionInstance: any, frame: number, statesToGroup: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.annotations.group(statesToGroup);
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.annotations.split(stateToSplit, frame);
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.SPLIT_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function changeLabelColor(label: any, color: string): AnyAction {
try {
const updatedLabel = label;
updatedLabel.color = color;
return {
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
},
};
} catch (error) {
return {
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED,
payload: {
error,
},
};
}
}

@ -76,9 +76,9 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const store = getCVATStore();
const state: CombinedState = store.getState();
const OpenVINO = state.plugins.plugins.AUTO_ANNOTATION;
const RCNN = state.plugins.plugins.TF_ANNOTATION;
const MaskRCNN = state.plugins.plugins.TF_SEGMENTATION;
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
dispatch(getModels());
const models: Model[] = [];
@ -468,9 +468,9 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const store = getCVATStore();
const state: CombinedState = store.getState();
const OpenVINO = state.plugins.plugins.AUTO_ANNOTATION;
const RCNN = state.plugins.plugins.TF_ANNOTATION;
const MaskRCNN = state.plugins.plugins.TF_SEGMENTATION;
const OpenVINO = state.plugins.list.AUTO_ANNOTATION;
const RCNN = state.plugins.list.TF_ANNOTATION;
const MaskRCNN = state.plugins.list.TF_SEGMENTATION;
try {
if (OpenVINO) {

@ -21,11 +21,11 @@ function checkPlugins(): AnyAction {
return action;
}
function checkedAllPlugins(plugins: PluginObjects): AnyAction {
function checkedAllPlugins(list: PluginObjects): AnyAction {
const action = {
type: PluginsActionTypes.CHECKED_ALL_PLUGINS,
payload: {
plugins,
list,
},
};

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M33.632 2.58a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L25.42 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L17.24 21.4a1.947 1.947 0 0 1 0-2.798zm-15.647 0a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L9.773 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L1.593 21.4a1.947 1.947 0 0 1 0-2.798z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 631 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M36.889 5.188H29.5L27.389 3H22.11C20.945 3 20 3.98 20 5.188v10.937c0 1.208.945 2.188 2.111 2.188H36.89c1.166 0 2.111-.98 2.111-2.188v-8.75c0-1.208-.945-2.188-2.111-2.188zm0 19.687H29.5l-2.111-2.188H22.11c-1.166 0-2.111.98-2.111 2.188v10.938C20 37.02 20.945 38 22.111 38H36.89C38.055 38 39 37.02 39 35.812v-8.75c0-1.208-.945-2.187-2.111-2.187zM5.222 4.094C5.222 3.49 4.75 3 4.167 3H2.056C1.473 3 1 3.49 1 4.094v27.343c0 1.209.945 2.188 2.111 2.188H17.89V29.25H5.222V13.937H17.89V9.564H5.222v-5.47z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 609 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M13.762 2v14.774L32 2v36L13.762 23.225V38H7V2h6.762z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 275 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M6.368 2.58a2.058 2.058 0 0 0-2.863 0L1.593 4.448a1.947 1.947 0 0 0-.003 2.795L14.58 20 1.59 32.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L22.76 21.4c.79-.773.79-2.025 0-2.798zm15.647 0a2.058 2.058 0 0 0-2.863 0L17.24 4.448a1.947 1.947 0 0 0-.003 2.795L30.227 20l-12.99 12.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L38.407 21.4c.79-.773.79-2.025 0-2.798z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 633 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3 7)" fill="#000" fill-rule="evenodd"><rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/><path stroke="#F1F1F1" stroke-width="2.25" d="M-1.39 2.72l2.451-3.773 33.967 22.057-2.451 3.774z"/></g></svg>

Before

Width:  |  Height:  |  Size: 296 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M25.238 2v14.774L7 2v36l18.238-14.775V38H32V2h-6.762z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 276 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M30.76 21.399L14.368 37.42a2.058 2.058 0 0 1-2.863 0l-1.912-1.868a1.947 1.947 0 0 1-.003-2.795L22.58 20 9.59 7.243a1.947 1.947 0 0 1 .003-2.795l1.912-1.868a2.058 2.058 0 0 1 2.863 0L30.76 18.6c.79.773.79 2.025 0 2.798z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 441 B

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896" style="transform: scale(1.5)">
<g style="transform: scale(25)">
<g transform="translate(3 7)" fill-rule="evenodd">
<rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 359 B

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896" style="transform: scale(1.5)">
<g style="transform: scale(25)">
<g transform="translate(3 7)" fill-rule="evenodd">
<rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/>
<path stroke="#F1F1F1" stroke-width="1" d="M-1.39 2.72l2.451-3.773 33.967 22.057-2.451 3.774z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 468 B

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<g transform="scale(0.07812)" transform-origin="bottom">
<path d="m201.350937,473.371796l0,-434.86451c0,-8.10006 -6.528412,-14.628468 -14.749344,-14.628468l-86.561874,0c-8.220955,0 -14.749359,6.528408 -14.749359,14.628468l0,434.86454c0,8.100037 6.528404,14.749359 14.749359,14.749359l86.561874,0c8.220932,0 14.749344,-6.528412 14.749344,-14.74939z" />
<path d="m423.967224,473.371796l0,-434.86451c0,-8.10006 -6.528381,-14.628468 -14.749329,-14.628468l-86.56189,0c-8.220947,0 -14.749329,6.528408 -14.749329,14.628468l0,434.86454c0,8.100037 6.528381,14.749359 14.749329,14.749359l86.56189,0c8.220947,0 14.749329,-6.528412 14.749329,-14.74939z" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 872 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path fill-rule="nonzero" d="M35.5 20l-30 19.5V.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 244 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M33.632 2.58a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L25.42 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L17.24 21.4a1.947 1.947 0 0 1 0-2.798zm-15.647 0a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L9.773 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L1.593 21.4a1.947 1.947 0 0 1 0-2.798z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 521 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M13.762 2v14.774L32 2v36L13.762 23.225V38H7V2h6.762z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 165 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M6.368 2.58a2.058 2.058 0 0 0-2.863 0L1.593 4.448a1.947 1.947 0 0 0-.003 2.795L14.58 20 1.59 32.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L22.76 21.4c.79-.773.79-2.025 0-2.798zm15.647 0a2.058 2.058 0 0 0-2.863 0L17.24 4.448a1.947 1.947 0 0 0-.003 2.795L30.227 20l-12.99 12.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L38.407 21.4c.79-.773.79-2.025 0-2.798z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 523 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.238 2v14.774L7 2v36l18.238-14.775V38H32V2h-6.762z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 166 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M30.76 21.399L14.368 37.42a2.058 2.058 0 0 1-2.863 0l-1.912-1.868a1.947 1.947 0 0 1-.003-2.795L22.58 20 9.59 7.243a1.947 1.947 0 0 1 .003-2.795l1.912-1.868a2.058 2.058 0 0 1 2.863 0L30.76 18.6c.79.773.79 2.025 0 2.798z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 331 B

@ -1,7 +0,0 @@
<!-- Drawn in https://www.iconfinder.com/editor/ -->
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg">
<g transform="scale(0.07812)" transform-origin="bottom">
<path d="m201.350937,473.371796l0,-434.86451c0,-8.10006 -6.528412,-14.628468 -14.749344,-14.628468l-86.561874,0c-8.220955,0 -14.749359,6.528408 -14.749359,14.628468l0,434.86454c0,8.100037 6.528404,14.749359 14.749359,14.749359l86.561874,0c8.220932,0 14.749344,-6.528412 14.749344,-14.74939z" />
<path d="m423.967224,473.371796l0,-434.86451c0,-8.10006 -6.528381,-14.628468 -14.749329,-14.628468l-86.56189,0c-8.220947,0 -14.749329,6.528408 -14.749329,14.628468l0,434.86454c0,8.100037 6.528381,14.749359 14.749329,14.749359l86.56189,0c8.220947,0 14.749329,-6.528412 14.749329,-14.74939z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 798 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path fill="#000" fill-rule="nonzero" d="M35.5 20l-30 19.5V.5z"/></svg>

Before

Width:  |  Height:  |  Size: 134 B

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M9.593 21.399L25.985 37.42c.79.773 2.073.773 2.863 0l1.912-1.868c.79-.772.79-2.022.003-2.795L17.773 20l12.99-12.757a1.947 1.947 0 0 0-.003-2.795L28.848 2.58a2.058 2.058 0 0 0-2.863 0L9.593 18.6a1.947 1.947 0 0 0 0 2.798z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 333 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" height="1em" width="1em" viewBox="64 64 896 896">
<g style="transform: scale(25)">
<path d="M9.593 21.399L25.985 37.42c.79.773 2.073.773 2.863 0l1.912-1.868c.79-.772.79-2.022.003-2.795L17.773 20l12.99-12.757a1.947 1.947 0 0 0-.003-2.795L28.848 2.58a2.058 2.058 0 0 0-2.863 0L9.593 18.6a1.947 1.947 0 0 0 0 2.798z" fill-rule="nonzero"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 443 B

@ -11,7 +11,11 @@ $background-color-1: white;
$background-color-2: #F1F1F1;
$transparent-color: rgba(0, 0, 0, 0);
$player-slider-color: #979797;
$player-buttons-color: #242424;
$danger-icon-color: #FF4136;
$info-icon-color: #0074D9;
$objects-bar-tabs-color: #BEBEBE;
$objects-bar-icons-color: #242424; // #6E6E6E
$active-object-item-background-color: #D8ECFF;
$monospaced-fonts-stack: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;

@ -8,22 +8,22 @@ import {
} from 'antd';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StandardWorkspaceContainer from 'containers/annotation-page/standard-workspace/standard-workspace';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
interface Props {
jobInstance: any | null | undefined;
job: any | null | undefined;
fetching: boolean;
getJob(): void;
}
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
jobInstance,
job,
fetching,
getJob,
} = props;
if (jobInstance === null) {
if (job === null) {
if (!fetching) {
getJob();
}
@ -31,7 +31,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return <Spin size='large' className='cvat-spinner' />;
}
if (typeof (jobInstance) === 'undefined') {
if (typeof (job) === 'undefined') {
return (
<Result
className='cvat-not-found'
@ -45,7 +45,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceContainer />
<StandardWorkspaceComponent />
</Layout>
);
}

@ -20,6 +20,7 @@ const cvat = getCore();
const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props {
sidebarCollapsed: boolean;
canvasInstance: Canvas;
jobInstance: any;
annotations: any[];
@ -34,12 +35,16 @@ interface Props {
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onShapeDrawn: () => void;
onObjectsMerged: () => void;
onObjectsGroupped: () => void;
onTrackSplitted: () => void;
onResetCanvas: () => void;
onAnnotationsUpdated: (annotations: any[]) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
}
export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -51,7 +56,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// It's awful approach from the point of view React
// But we do not have another way because cvat-canvas returns regular DOM element
const [wrapper] = window.document
.getElementsByClassName('cvat-annotation-page-canvas-container');
.getElementsByClassName('cvat-canvas-container');
wrapper.appendChild(canvasInstance.html());
this.initialSetup();
@ -65,8 +70,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridColor,
gridOpacity,
canvasInstance,
sidebarCollapsed,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
if (sidebar) {
sidebar.addEventListener('transitionend', () => {
canvasInstance.fitCanvas();
}, { once: true });
}
}
if (prevProps.grid !== grid) {
const gridElement = window.document.getElementById('cvat_canvas_grid');
if (gridElement) {
@ -102,7 +117,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
activeObjectType,
frame,
onShapeDrawn,
onAnnotationsUpdated,
onCreateAnnotations,
} = this.props;
onShapeDrawn();
@ -123,17 +138,14 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
await jobInstance.annotations.put([objectState]);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
onCreateAnnotations(jobInstance, frame, [objectState]);
}
private async onShapeEdited(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onUpdateAnnotations,
} = this.props;
const {
@ -141,58 +153,49 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
points,
} = event.detail;
state.points = points;
state.save();
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
onUpdateAnnotations(jobInstance, frame, [state]);
}
private async onObjectsMerged(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onObjectsMerged,
onMergeAnnotations,
onMergeObjects,
} = this.props;
onObjectsMerged();
onMergeObjects(false);
const { states } = event.detail;
await jobInstance.annotations.merge(states);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
onMergeAnnotations(jobInstance, frame, states);
}
private async onObjectsGroupped(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onObjectsGroupped,
onGroupAnnotations,
onGroupObjects,
} = this.props;
onObjectsGroupped();
onGroupObjects(false);
const { states } = event.detail;
await jobInstance.annotations.group(states);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
onGroupAnnotations(jobInstance, frame, states);
}
private async onTrackSplitted(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onTrackSplitted,
onSplitAnnotations,
onSplitTrack,
} = this.props;
onTrackSplitted();
onSplitTrack(false);
const { state } = event.detail;
await jobInstance.annotations.split(state, frame);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
onSplitAnnotations(jobInstance, frame, state);
}
private updateCanvas(): void {
@ -239,9 +242,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
// Events
canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas();
if (jobInstance.task.mode === 'annotation') {
canvasInstance.fit();
}
});
canvasInstance.html().addEventListener('canvas.setup', () => {
@ -314,7 +314,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<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-annotation-page-canvas-container'
className='cvat-canvas-container'
/>
);
}

@ -36,33 +36,55 @@ interface Props {
rotateAll: boolean;
activeControl: ActiveControl;
onMergeStart(): void;
onGroupStart(): void;
onSplitStart(): void;
mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
rotateAll,
mergeObjects,
groupObjects,
splitTrack,
} = props;
return (
<Layout.Sider
className='cvat-annotation-page-controls-sidebar'
className='cvat-canvas-controls-sidebar'
theme='light'
width={44}
>
<CursorControl {...props} />
<MoveControl {...props} />
<RotateControl {...props} />
<CursorControl canvasInstance={canvasInstance} activeControl={activeControl} />
<MoveControl canvasInstance={canvasInstance} activeControl={activeControl} />
<RotateControl canvasInstance={canvasInstance} rotateAll={rotateAll} />
<hr />
<FitControl {...props} />
<ResizeControl {...props} />
<FitControl canvasInstance={canvasInstance} />
<ResizeControl canvasInstance={canvasInstance} activeControl={activeControl} />
<hr />
<DrawRectangleControl {...props} />
<DrawPolygonControl {...props} />
<DrawPolylineControl {...props} />
<DrawPointsControl {...props} />
<DrawRectangleControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_RECTANGLE}
/>
<DrawPolygonControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYGON}
/>
<DrawPolylineControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POLYLINE}
/>
<DrawPointsControl
canvasInstance={canvasInstance}
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/>
<Tooltip overlay='Setup a tag' placement='right'>
<Icon component={TagIcon} />
@ -70,9 +92,21 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
<hr />
<MergeControl {...props} />
<GroupControl {...props} />
<SplitControl {...props} />
<MergeControl
canvasInstance={canvasInstance}
activeControl={activeControl}
mergeObjects={mergeObjects}
/>
<GroupControl
canvasInstance={canvasInstance}
activeControl={activeControl}
groupObjects={groupObjects}
/>
<SplitControl
canvasInstance={canvasInstance}
activeControl={activeControl}
splitTrack={splitTrack}
/>
</Layout.Sider>
);
}

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
export default function CursorControl(props: Props): JSX.Element {
const CursorControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
@ -33,14 +33,16 @@ export default function CursorControl(props: Props): JSX.Element {
<Icon
component={CursorIcon}
className={activeControl === ActiveControl.CURSOR
? 'cvat-annotation-page-active-control' : ''
? 'cvat-active-canvas-control' : ''
}
onClick={
activeControl !== ActiveControl.CURSOR
? (): void => canvasInstance.cancel()
: undefined
}
onClick={(): void => {
if (activeControl !== ActiveControl.CURSOR) {
canvasInstance.cancel();
}
}}
/>
</Tooltip>
);
}
});
export default CursorControl;

@ -6,38 +6,33 @@ import {
import { Canvas } from 'cvat-canvas';
import { PointIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import { ShapeType } from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
isDrawing: boolean;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const DrawPointsControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
isDrawing,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POINTS
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POINTS
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
@ -54,4 +49,6 @@ export default function DrawRectangleControl(props: Props): JSX.Element {
/>
</Popover>
);
}
});
export default DrawPointsControl;

@ -6,38 +6,33 @@ import {
import { Canvas } from 'cvat-canvas';
import { PolygonIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import { ShapeType } from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
isDrawing: boolean;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const DrawPolygonControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
isDrawing,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYGON
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYGON
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
@ -54,4 +49,6 @@ export default function DrawRectangleControl(props: Props): JSX.Element {
/>
</Popover>
);
}
});
export default DrawPolygonControl;

@ -6,38 +6,33 @@ import {
import { Canvas } from 'cvat-canvas';
import { PolylineIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import { ShapeType } from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
isDrawing: boolean;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const DrawPolylineControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
isDrawing,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYLINE
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYLINE
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
@ -54,4 +49,6 @@ export default function DrawRectangleControl(props: Props): JSX.Element {
/>
</Popover>
);
}
});
export default DrawPolylineControl;

@ -6,38 +6,33 @@ import {
import { Canvas } from 'cvat-canvas';
import { RectangleIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import { ShapeType } from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
isDrawing: boolean;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const DrawRectangleControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
isDrawing,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_RECTANGLE
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_RECTANGLE
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
const dynamcPopoverPros = isDrawing ? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = isDrawing ? {
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
@ -54,4 +49,6 @@ export default function DrawRectangleControl(props: Props): JSX.Element {
/>
</Popover>
);
}
});
export default DrawRectangleControl;

@ -12,175 +12,100 @@ import Text from 'antd/lib/typography/Text';
import {
ShapeType,
ObjectType,
StringObject,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
shapeType: ShapeType;
labels: StringObject;
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
points?: number,
): void;
}
interface State {
labels: any[];
minimumPoints: number;
numberOfPoints?: number;
selectedLabeID: number;
onChangeLabel(value: string): void;
onChangePoints(value: number | undefined): void;
onDrawTrack(): void;
onDrawShape(): void;
}
function defineMinimumPoints(shapeType: ShapeType): number {
if (shapeType === ShapeType.POLYGON) {
return 3;
}
if (shapeType === ShapeType.POLYLINE) {
return 2;
}
if (shapeType === ShapeType.POINTS) {
return 1;
}
return 0;
}
export default class DrawShapePopoverComponent extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const defaultLabelID = +Object.keys(props.labels)[0];
this.state = {
selectedLabeID: defaultLabelID,
};
}
private onChangePoints = (value: number | undefined): void => {
this.setState({
numberOfPoints: value,
});
};
private onChangeLabel = (value: string): void => {
this.setState({
selectedLabeID: +value,
});
};
private onDrawTrackStart = (): void => {
this.onDrawStart(ObjectType.TRACK);
};
private onDrawShapeStart = (): void => {
this.onDrawStart(ObjectType.SHAPE);
};
private onDrawStart = (objectType: ObjectType): void => {
const {
numberOfPoints,
selectedLabeID,
} = this.state;
const {
shapeType,
onDrawStart,
canvasInstance,
} = this.props;
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
numberOfPoints,
shapeType,
crosshair: shapeType === ShapeType.RECTANGLE,
});
onDrawStart(shapeType, selectedLabeID,
objectType, numberOfPoints);
};
public render(): JSX.Element {
const {
selectedLabeID,
} = this.state;
const {
shapeType,
labels,
} = this.props;
const minimumPoints = defineMinimumPoints(shapeType);
return (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>{`Draw new ${shapeType}`}</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
value={labels[selectedLabeID]}
onChange={this.onChangeLabel}
>
{
Object.keys(labels).map((key: string) => (
<Select.Option
key={key}
value={key}
>
{labels[+key]}
</Select.Option>
))
}
</Select>
</Col>
</Row>
{
shapeType !== ShapeType.RECTANGLE && (
<Row type='flex' justify='space-around' align='middle'>
<Col span={14}>
<Text className='cvat-text-color'> Number of points: </Text>
</Col>
<Col span={10}>
<InputNumber
onChange={this.onChangePoints}
className='cvat-draw-shape-popover-points-selector'
min={minimumPoints}
step={1}
/>
</Col>
</Row>
)
}
<Row type='flex' justify='space-around'>
<Col span={12}>
<Button
onClick={this.onDrawShapeStart}
>
Shape
</Button>
</Col>
<Col span={12}>
<Button
onClick={this.onDrawTrackStart}
>
Track
</Button>
</Col>
</Row>
</div>
);
}
}
const DrawShapePopoverComponent = React.memo((props: Props): JSX.Element => {
const {
labels,
shapeType,
minimumPoints,
selectedLabeID,
numberOfPoints,
onDrawTrack,
onDrawShape,
onChangeLabel,
onChangePoints,
} = props;
return (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>{`Draw new ${shapeType}`}</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
value={`${selectedLabeID}`}
onChange={onChangeLabel}
>
{
labels.map((label: any) => (
<Select.Option
key={label.id}
value={`${label.id}`}
>
{label.name}
</Select.Option>
))
}
</Select>
</Col>
</Row>
{
shapeType !== ShapeType.RECTANGLE && (
<Row type='flex' justify='space-around' align='middle'>
<Col span={14}>
<Text className='cvat-text-color'> Number of points: </Text>
</Col>
<Col span={10}>
<InputNumber
onChange={onChangePoints}
className='cvat-draw-shape-popover-points-selector'
min={minimumPoints}
value={numberOfPoints}
step={1}
/>
</Col>
</Row>
)
}
<Row type='flex' justify='space-around'>
<Col span={12}>
<Button
onClick={onDrawShape}
>
Shape
</Button>
</Col>
<Col span={12}>
<Button
onClick={onDrawTrack}
>
Track
</Button>
</Col>
</Row>
</div>
);
});
export default DrawShapePopoverComponent;

@ -17,7 +17,7 @@ interface Props {
canvasInstance: Canvas;
}
export default function FitControl(props: Props): JSX.Element {
const FitControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
} = props;
@ -27,4 +27,6 @@ export default function FitControl(props: Props): JSX.Element {
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip>
);
}
});
export default FitControl;

@ -16,27 +16,28 @@ interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onGroupStart(): void;
groupObjects(enabled: boolean): void;
}
export default function GroupControl(props: Props): JSX.Element {
const GroupControl = React.memo((props: Props): JSX.Element => {
const {
activeControl,
canvasInstance,
onGroupStart,
groupObjects,
} = props;
const dynamicIconProps = activeControl === ActiveControl.GROUP
? {
className: 'cvat-annotation-page-active-control',
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.group({ enabled: false });
groupObjects(false);
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.group({ enabled: true });
onGroupStart();
groupObjects(true);
},
};
@ -45,4 +46,6 @@ export default function GroupControl(props: Props): JSX.Element {
<Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip>
);
}
});
export default GroupControl;

@ -16,27 +16,28 @@ interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onMergeStart(): void;
mergeObjects(enabled: boolean): void;
}
export default function MergeControl(props: Props): JSX.Element {
const MergeControl = React.memo((props: Props): JSX.Element => {
const {
activeControl,
canvasInstance,
onMergeStart,
mergeObjects,
} = props;
const dynamicIconProps = activeControl === ActiveControl.MERGE
? {
className: 'cvat-annotation-page-active-control',
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.merge({ enabled: false });
mergeObjects(false);
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.merge({ enabled: true });
onMergeStart();
mergeObjects(true);
},
};
@ -45,4 +46,6 @@ export default function MergeControl(props: Props): JSX.Element {
<Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip>
);
}
});
export default MergeControl;

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
export default function MoveControl(props: Props): JSX.Element {
const MoveControl = React.memo((props: Props): JSX.Element => {
const {
canvasInstance,
activeControl,
@ -33,7 +33,7 @@ export default function MoveControl(props: Props): JSX.Element {
<Icon
component={MoveIcon}
className={activeControl === ActiveControl.DRAG_CANVAS
? 'cvat-annotation-page-active-control' : ''
? 'cvat-active-canvas-control' : ''
}
onClick={(): void => {
if (activeControl === ActiveControl.DRAG_CANVAS) {
@ -46,4 +46,6 @@ export default function MoveControl(props: Props): JSX.Element {
/>
</Tooltip>
);
}
});
export default MoveControl;

@ -22,7 +22,7 @@ interface Props {
activeControl: ActiveControl;
}
export default function ResizeControl(props: Props): JSX.Element {
const ResizeControl = React.memo((props: Props): JSX.Element => {
const {
activeControl,
canvasInstance,
@ -33,7 +33,7 @@ export default function ResizeControl(props: Props): JSX.Element {
<Icon
component={ZoomIcon}
className={activeControl === ActiveControl.ZOOM_CANVAS
? 'cvat-annotation-page-active-control' : ''
? 'cvat-active-canvas-control' : ''
}
onClick={(): void => {
if (activeControl === ActiveControl.ZOOM_CANVAS) {
@ -46,4 +46,6 @@ export default function ResizeControl(props: Props): JSX.Element {
/>
</Tooltip>
);
}
});
export default ResizeControl;

@ -20,7 +20,7 @@ interface Props {
rotateAll: boolean;
}
export default function RotateControl(props: Props): JSX.Element {
const RotateControl = React.memo((props: Props): JSX.Element => {
const {
rotateAll,
canvasInstance,
@ -28,13 +28,13 @@ export default function RotateControl(props: Props): JSX.Element {
return (
<Popover
overlayClassName='cvat-annotation-page-controls-rotate'
overlayClassName='cvat-rotate-canvas-controls'
placement='right'
content={(
<>
<Tooltip overlay='Rotate the image anticlockwise' placement='topRight'>
<Icon
className='cvat-annotation-page-controls-rotate-left'
className='cvat-rotate-canvas-controls-left'
onClick={(): void => canvasInstance
.rotate(Rotation.ANTICLOCKWISE90, rotateAll)}
component={RotateIcon}
@ -42,7 +42,7 @@ export default function RotateControl(props: Props): JSX.Element {
</Tooltip>
<Tooltip overlay='Rotate the image clockwise' placement='topRight'>
<Icon
className='cvat-annotation-page-controls-rotate-right'
className='cvat-rotate-canvas-controls-right'
onClick={(): void => canvasInstance
.rotate(Rotation.CLOCKWISE90, rotateAll)}
component={RotateIcon}
@ -55,4 +55,6 @@ export default function RotateControl(props: Props): JSX.Element {
<Icon component={RotateIcon} />
</Popover>
);
}
});
export default RotateControl;

@ -16,27 +16,28 @@ interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onSplitStart(): void;
splitTrack(enabled: boolean): void;
}
export default function SplitControl(props: Props): JSX.Element {
const SplitControl = React.memo((props: Props): JSX.Element => {
const {
activeControl,
canvasInstance,
onSplitStart,
splitTrack,
} = props;
const dynamicIconProps = activeControl === ActiveControl.SPLIT
? {
className: 'cvat-annotation-page-active-control',
className: 'cvat-active-canvas-control',
onClick: (): void => {
canvasInstance.split({ enabled: false });
splitTrack(false);
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.split({ enabled: true });
onSplitStart();
splitTrack(true);
},
};
@ -45,4 +46,6 @@ export default function SplitControl(props: Props): JSX.Element {
<Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip>
);
}
});
export default SplitControl;

@ -0,0 +1,130 @@
import React from 'react';
import {
Row,
Col,
Icon,
Popover,
Button,
} from 'antd';
import Text from 'antd/lib/typography/Text';
interface PopoverContentProps {
colors: string[];
changeColor(color: string): void;
}
function PopoverContent(props: PopoverContentProps): JSX.Element {
const {
colors,
changeColor,
} = props;
const cols = 6;
const rows = Math.ceil(colors.length / cols);
const antdRows = [];
for (let row = 0; row < rows; row++) {
const antdCols = [];
for (let col = 0; col < cols; col++) {
const idx = row * cols + col;
if (idx >= colors.length) {
break;
}
const color = colors[idx];
antdCols.push(
<Col key={col} span={4}>
<Button
onClick={(): void => changeColor(color)}
style={{ background: color }}
className='cvat-label-item-color-button'
/>
</Col>,
);
}
antdRows.push(
// eslint-disable-next-line react/no-children-prop
<Row key={row} children={antdCols} />,
);
}
return (
<>
{antdRows}
</>
);
}
interface Props {
labelName: string;
labelColor: string;
labelColors: string[];
visible: boolean;
statesHidden: boolean;
statesLocked: boolean;
hideStates(): void;
showStates(): void;
lockStates(): void;
unlockStates(): void;
changeColor(color: string): void;
}
const LabelItemComponent = React.memo((props: Props): JSX.Element => {
const {
labelName,
labelColor,
labelColors,
visible,
statesHidden,
statesLocked,
hideStates,
showStates,
lockStates,
unlockStates,
changeColor,
} = props;
return (
<Row
type='flex'
align='middle'
justify='space-around'
className='cvat-objects-sidebar-label-item'
style={{ display: visible ? 'flex' : 'none' }}
>
<Col span={4}>
<Popover
placement='left'
trigger='click'
content={(
<PopoverContent
changeColor={changeColor}
colors={labelColors}
/>
)}
>
<Button style={{ background: labelColor }} className='cvat-label-item-color-button' />
</Popover>
</Col>
<Col span={14}>
<Text strong className='cvat-text'>{labelName}</Text>
</Col>
<Col span={3}>
{ statesLocked
? <Icon type='lock' onClick={unlockStates} />
: <Icon type='unlock' onClick={lockStates} />
}
</Col>
<Col span={3}>
{ statesHidden
? <Icon type='eye-invisible' onClick={showStates} />
: <Icon type='eye' onClick={hideStates} />
}
</Col>
</Row>
);
});
export default LabelItemComponent;

@ -0,0 +1,25 @@
import React from 'react';
import LabelItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/label-item';
interface Props {
labelIDs: number[];
listHeight: number;
}
export default function LabelsListComponent(props: Props): JSX.Element {
const {
listHeight,
labelIDs,
} = props;
return (
<div style={{ height: listHeight }} className='cvat-objects-sidebar-labels-list'>
{
labelIDs.map((labelID: number): JSX.Element => (
<LabelItemContainer key={labelID} labelID={labelID} />
))
}
</div>
);
}

@ -0,0 +1,538 @@
import React from 'react';
import {
Row,
Col,
Icon,
Select,
Radio,
Input,
Collapse,
Checkbox,
InputNumber,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { RadioChangeEvent } from 'antd/lib/radio';
import {
ObjectOutsideIcon,
FirstIcon,
LastIcon,
PreviousIcon,
NextIcon,
} from 'icons';
import {
ObjectType, ShapeType,
} from 'reducers/interfaces';
interface ItemTopProps {
clientID: number;
labelID: number;
labels: any[];
type: string;
changeLabel(labelID: string): void;
}
const ItemTop = React.memo((props: ItemTopProps): JSX.Element => {
const {
clientID,
labelID,
labels,
type,
changeLabel,
} = props;
return (
<Row type='flex' align='middle'>
<Col span={10}>
<Text style={{ fontSize: 16 }}>{clientID}</Text>
<br />
<Text style={{ fontSize: 10 }}>{type}</Text>
</Col>
<Col span={12}>
<Select value={`${labelID}`} onChange={changeLabel}>
{ labels.map((label: any): JSX.Element => (
<Select.Option key={label.id} value={`${label.id}`}>
{label.name}
</Select.Option>
))}
</Select>
</Col>
<Col span={2}>
<Icon type='more' />
</Col>
</Row>
);
});
interface ItemButtonsProps {
objectType: ObjectType;
occluded: boolean;
outside: boolean | undefined;
locked: boolean;
hidden: boolean;
keyframe: boolean | undefined;
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
unsetOutside(): void;
setKeyframe(): void;
unsetKeyframe(): void;
lock(): void;
unlock(): void;
hide(): void;
show(): void;
}
const ItemButtons = React.memo((props: ItemButtonsProps): JSX.Element => {
const {
objectType,
occluded,
outside,
locked,
hidden,
keyframe,
setOccluded,
unsetOccluded,
setOutside,
unsetOutside,
setKeyframe,
unsetKeyframe,
lock,
unlock,
hide,
show,
} = props;
if (objectType === ObjectType.TRACK) {
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col span={6}>
<Icon component={FirstIcon} />
</Col>
<Col span={6}>
<Icon component={PreviousIcon} />
</Col>
<Col span={6}>
<Icon component={NextIcon} />
</Col>
<Col span={6}>
<Icon component={LastIcon} />
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col span={4}>
{ outside
? <Icon component={ObjectOutsideIcon} onClick={unsetOutside} />
: <Icon type='select' onClick={setOutside} />
}
</Col>
<Col span={4}>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />
}
</Col>
<Col span={4}>
{ occluded
? <Icon type='team' onClick={unsetOccluded} />
: <Icon type='user' onClick={setOccluded} />
}
</Col>
<Col span={4}>
{ hidden
? <Icon type='eye-invisible' onClick={show} />
: <Icon type='eye' onClick={hide} />
}
</Col>
<Col span={4}>
{ keyframe
? <Icon type='star' theme='filled' onClick={unsetKeyframe} />
: <Icon type='star' onClick={setKeyframe} />
}
</Col>
</Row>
</Col>
</Row>
);
}
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col span={8}>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />
}
</Col>
<Col span={8}>
{ occluded
? <Icon type='team' onClick={unsetOccluded} />
: <Icon type='user' onClick={setOccluded} />
}
</Col>
<Col span={8}>
{ hidden
? <Icon type='eye-invisible' onClick={show} />
: <Icon type='eye' onClick={hide} />
}
</Col>
</Row>
</Col>
</Row>
);
});
interface ItemAttributeProps {
attrInputType: string;
attrValues: string[];
attrValue: string;
attrName: string;
attrID: number;
changeAttribute(attrID: number, value: string): void;
}
function attrIsTheSame(prevProps: ItemAttributeProps, nextProps: ItemAttributeProps): boolean {
return nextProps.attrID === prevProps.attrID
&& nextProps.attrValue === prevProps.attrValue
&& nextProps.attrName === prevProps.attrName
&& nextProps.attrInputType === prevProps.attrInputType
&& nextProps.attrValues
.map((value: string, id: number): boolean => prevProps.attrValues[id] === value)
.every((value: boolean): boolean => value);
}
const ItemAttribute = React.memo((props: ItemAttributeProps): JSX.Element => {
const {
attrInputType,
attrValues,
attrValue,
attrName,
attrID,
changeAttribute,
} = props;
if (attrInputType === 'checkbox') {
return (
<Col span={24}>
<Checkbox
className='cvat-object-item-checkbox-attribute'
checked={attrValue === 'true'}
onChange={(event: CheckboxChangeEvent): void => {
const value = event.target.checked ? 'true' : 'false';
changeAttribute(attrID, value);
}}
>
<Text strong className='cvat-text' style={{ fontSize: '1.2em' }}>
{attrName}
</Text>
</Checkbox>
</Col>
);
}
if (attrInputType === 'radio') {
return (
<Col span={24}>
<fieldset className='cvat-object-item-radio-attribute'>
<legend>
<Text strong className='cvat-text'>{attrName}</Text>
</legend>
<Radio.Group
value={attrValue}
onChange={(event: RadioChangeEvent): void => {
changeAttribute(attrID, event.target.value);
}}
>
{ attrValues.map((value: string): JSX.Element => (
<Radio key={value} value={value}>{value}</Radio>
)) }
</Radio.Group>
</fieldset>
</Col>
);
}
if (attrInputType === 'select') {
return (
<>
<Col span={24}>
<Text strong className='cvat-text' style={{ fontSize: '1.2em' }}>
{attrName}
</Text>
</Col>
<Col span={24}>
<Select
onChange={(value: string): void => {
changeAttribute(attrID, value);
}}
value={attrValue}
className='cvat-object-item-select-attribute'
>
{ attrValues.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>{value}</Select.Option>
)) }
</Select>
</Col>
</>
);
}
if (attrInputType === 'number') {
const [min, max, step] = attrValues;
return (
<>
<Col span={24}>
<Text strong className='cvat-text' style={{ fontSize: '1.2em' }}>
{attrName}
</Text>
</Col>
<Col span={24}>
<InputNumber
onChange={(value: number | undefined): void => {
if (typeof (value) !== 'undefined') {
changeAttribute(attrID, `${value}`);
}
}}
value={+attrValue}
className='cvat-object-item-number-attribute'
min={+min}
max={+max}
step={+step}
/>
</Col>
</>
);
}
return (
<>
<Col span={24}>
<Text strong className='cvat-text' style={{ fontSize: '1.2em' }}>
{attrName}
</Text>
</Col>
<Col span={24}>
<Input
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => {
changeAttribute(attrID, event.target.value);
}}
value={attrValue}
className='cvat-object-item-text-attribute'
/>
</Col>
</>
);
}, attrIsTheSame);
interface ItemAttributesProps {
collapsed: boolean;
attributes: any[];
values: Record<number, string>;
changeAttribute(attrID: number, value: string): void;
collapse(): void;
}
function attrValuesAreEqual(next: Record<number, string>, prev: Record<number, string>): boolean {
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(next);
return nextKeys.length === prevKeys.length
&& nextKeys.map((key: string): boolean => prev[+key] === next[+key])
.every((value: boolean) => value);
}
function attrAreTheSame(prevProps: ItemAttributesProps, nextProps: ItemAttributesProps): boolean {
return nextProps.collapsed === prevProps.collapsed
&& nextProps.attributes === prevProps.attributes
&& attrValuesAreEqual(nextProps.values, prevProps.values);
}
const ItemAttributes = React.memo((props: ItemAttributesProps): JSX.Element => {
const {
collapsed,
attributes,
values,
changeAttribute,
collapse,
} = props;
const sorted = [...attributes]
.sort((a: any, b: any): number => a.inputType.localeCompare(b.inputType));
return (
<Row>
<Collapse
className='cvat-objects-sidebar-state-item-collapse'
activeKey={collapsed ? [] : ['details']}
onChange={collapse}
>
<Collapse.Panel
header='Details'
key='details'
>
{ sorted.map((attribute: any): JSX.Element => (
<Row
key={attribute.id}
type='flex'
align='middle'
justify='start'
className='cvat-object-item-attribute-wrapper'
>
<ItemAttribute
attrValue={values[attribute.id]}
attrInputType={attribute.inputType}
attrName={attribute.name}
attrID={attribute.id}
attrValues={attribute.values}
changeAttribute={changeAttribute}
/>
</Row>
))}
</Collapse.Panel>
</Collapse>
</Row>
);
}, attrAreTheSame);
interface Props {
objectType: ObjectType;
shapeType: ShapeType;
clientID: number;
labelID: number;
occluded: boolean;
outside: boolean | undefined;
locked: boolean;
hidden: boolean;
keyframe: boolean | undefined;
attrValues: Record<number, string>;
color: string;
labels: any[];
attributes: any[];
collapsed: boolean;
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
unsetOutside(): void;
setKeyframe(): void;
unsetKeyframe(): void;
lock(): void;
unlock(): void;
hide(): void;
show(): void;
changeLabel(labelID: string): void;
changeAttribute(attrID: number, value: string): void;
collapse(): void;
}
function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
return nextProps.locked === prevProps.locked
&& nextProps.occluded === prevProps.occluded
&& nextProps.outside === prevProps.outside
&& nextProps.hidden === prevProps.hidden
&& nextProps.keyframe === prevProps.keyframe
&& nextProps.label === prevProps.label
&& nextProps.color === prevProps.color
&& nextProps.clientID === prevProps.clientID
&& nextProps.objectType === prevProps.objectType
&& nextProps.shapeType === prevProps.shapeType
&& nextProps.collapsed === prevProps.collapsed
&& nextProps.labels === prevProps.labels
&& nextProps.attributes === prevProps.attributes
&& attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues);
}
const ObjectItem = React.memo((props: Props): JSX.Element => {
const {
objectType,
shapeType,
clientID,
occluded,
outside,
locked,
hidden,
keyframe,
attrValues,
labelID,
color,
attributes,
labels,
collapsed,
setOccluded,
unsetOccluded,
setOutside,
unsetOutside,
setKeyframe,
unsetKeyframe,
lock,
unlock,
hide,
show,
changeLabel,
changeAttribute,
collapse,
} = props;
const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase()
: `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`;
return (
<div
className='cvat-objects-sidebar-state-item'
style={{ borderLeftStyle: 'solid', borderColor: ` ${color}` }}
>
<ItemTop
clientID={clientID}
labelID={labelID}
labels={labels}
type={type}
changeLabel={changeLabel}
/>
<ItemButtons
objectType={objectType}
occluded={occluded}
outside={outside}
locked={locked}
hidden={hidden}
keyframe={keyframe}
setOccluded={setOccluded}
unsetOccluded={unsetOccluded}
setOutside={setOutside}
unsetOutside={unsetOutside}
setKeyframe={setKeyframe}
unsetKeyframe={unsetKeyframe}
lock={lock}
unlock={unlock}
hide={hide}
show={show}
/>
{ !!attributes.length
&& (
<ItemAttributes
collapsed={collapsed}
attributes={attributes}
values={attrValues}
collapse={collapse}
changeAttribute={changeAttribute}
/>
)
}
</div>
);
}, objectItemsAreEqual);
export default ObjectItem;

@ -0,0 +1,121 @@
import React from 'react';
import {
Row,
Col,
Icon,
Input,
Select,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorProps {
statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void;
}
const StatesOrderingSelector = React.memo((props: StatesOrderingSelectorProps): JSX.Element => {
const {
statesOrdering,
changeStatesOrdering,
} = props;
return (
<Col span={16}>
<Text strong>Sort by</Text>
<Select value={statesOrdering} onChange={changeStatesOrdering}>
<Select.Option
key={StatesOrdering.ID_DESCENT}
value={StatesOrdering.ID_DESCENT}
>
{StatesOrdering.ID_DESCENT}
</Select.Option>
<Select.Option
key={StatesOrdering.ID_ASCENT}
value={StatesOrdering.ID_ASCENT}
>
{StatesOrdering.ID_ASCENT}
</Select.Option>
<Select.Option
key={StatesOrdering.UPDATED}
value={StatesOrdering.UPDATED}
>
{StatesOrdering.UPDATED}
</Select.Option>
</Select>
</Col>
);
});
interface Props {
statesHidden: boolean;
statesLocked: boolean;
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
expandAllStates(): void;
hideAllStates(): void;
showAllStates(): void;
}
const Header = React.memo((props: Props): JSX.Element => {
const {
statesHidden,
statesLocked,
statesCollapsed,
statesOrdering,
changeStatesOrdering,
lockAllStates,
unlockAllStates,
collapseAllStates,
expandAllStates,
hideAllStates,
showAllStates,
} = props;
return (
<div className='cvat-objects-sidebar-states-header'>
<Row>
<Col>
<Input
placeholder='Filter e.g. car[attr/model="mazda"]'
prefix={<Icon type='filter' />}
/>
</Col>
</Row>
<Row type='flex' justify='space-between' align='middle'>
<Col span={2}>
{ statesLocked
? <Icon type='lock' onClick={unlockAllStates} />
: <Icon type='unlock' onClick={lockAllStates} />
}
</Col>
<Col span={2}>
{ statesHidden
? <Icon type='eye-invisible' onClick={showAllStates} />
: <Icon type='eye' onClick={hideAllStates} />
}
</Col>
<Col span={2}>
{ statesCollapsed
? <Icon type='caret-down' onClick={expandAllStates} />
: <Icon type='caret-up' onClick={collapseAllStates} />
}
</Col>
<StatesOrderingSelector
statesOrdering={statesOrdering}
changeStatesOrdering={changeStatesOrdering}
/>
</Row>
</div>
);
});
export default Header;

@ -0,0 +1,66 @@
import React from 'react';
import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import Header from './objects-list-header';
interface Props {
listHeight: number;
statesHidden: boolean;
statesLocked: boolean;
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
sortedStatesID: number[];
changeStatesOrdering(value: StatesOrdering): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
expandAllStates(): void;
hideAllStates(): void;
showAllStates(): void;
}
const ObjectListComponent = React.memo((props: Props): JSX.Element => {
const {
listHeight,
statesHidden,
statesLocked,
statesCollapsed,
statesOrdering,
sortedStatesID,
changeStatesOrdering,
lockAllStates,
unlockAllStates,
collapseAllStates,
expandAllStates,
hideAllStates,
showAllStates,
} = props;
return (
<div style={{ height: listHeight }}>
<Header
statesHidden={statesHidden}
statesLocked={statesLocked}
statesCollapsed={statesCollapsed}
statesOrdering={statesOrdering}
changeStatesOrdering={changeStatesOrdering}
lockAllStates={lockAllStates}
unlockAllStates={unlockAllStates}
collapseAllStates={collapseAllStates}
expandAllStates={expandAllStates}
hideAllStates={hideAllStates}
showAllStates={showAllStates}
/>
<div className='cvat-objects-sidebar-states-list'>
{ sortedStatesID.map((id: number): JSX.Element => (
<ObjectItemContainer key={id} clientID={id} />
))}
</div>
</div>
);
});
export default ObjectListComponent;

@ -1,66 +1,86 @@
import './styles.scss';
import React from 'react';
import {
Icon,
Tabs,
Layout,
Collapse,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import ObjectsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-list';
import LabelsListContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/labels-list';
interface Props {
onSidebarFoldUnfold(): void;
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
collapseSidebar(): void;
collapseAppearance(): void;
}
interface State {
collapsed: boolean;
}
const ObjectsSideBar = React.memo((props: Props): JSX.Element => {
const {
sidebarCollapsed,
appearanceCollapsed,
collapseSidebar,
collapseAppearance,
} = props;
export default class StandardWorkspaceComponent extends React.PureComponent<Props, State> {
public constructor(props: any) {
super(props);
this.state = {
collapsed: true,
};
}
return (
<Layout.Sider
className='cvat-objects-sidebar'
theme='light'
width={300}
collapsedWidth={0}
reverseArrow
collapsible
trigger={null}
collapsed={sidebarCollapsed}
>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={collapseSidebar}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
public render(): JSX.Element {
const { collapsed } = this.state;
const { onSidebarFoldUnfold } = this.props;
<Tabs type='card' defaultActiveKey='objects' className='cvat-objects-sidebar-tabs'>
<Tabs.TabPane
tab={<Text strong>Objects</Text>}
key='objects'
>
<ObjectsListContainer />
</Tabs.TabPane>
<Tabs.TabPane
tab={<Text strong>Labels</Text>}
key='labels'
>
<LabelsListContainer />
</Tabs.TabPane>
</Tabs>
return (
<Layout.Sider
className='cvat-annotation-page-objects-sidebar'
theme='light'
width={300}
collapsedWidth={0}
reverseArrow
collapsible
trigger={null}
collapsed={collapsed}
<Collapse
onChange={collapseAppearance}
activeKey={appearanceCollapsed ? [] : ['appearance']}
className='cvat-objects-appearance-collapse'
>
{/* eslint-disable-next-line */}
<span
className={`cvat-annotation-page-objects-sidebar
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={(): void => {
this.setState(
(prevState: State): State => ({
collapsed: !prevState.collapsed,
}),
);
const [sidebar] = window.document
.getElementsByClassName('cvat-annotation-page-objects-sidebar');
sidebar.addEventListener('transitionend', () => {
onSidebarFoldUnfold();
}, { once: true });
}}
<Collapse.Panel
header={
<Text strong>Appearance</Text>
}
key='appearance'
>
{collapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
Right sidebar
</Layout.Sider>
);
}
}
</Collapse.Panel>
</Collapse>
</Layout.Sider>
);
});
export default ObjectsSideBar;

@ -0,0 +1,248 @@
@import 'base.scss';
.cvat-objects-appearance-collapse.ant-collapse {
width: 100%;
bottom: 0px;
position: absolute;
border-radius: 0px;
> .ant-collapse-item {
border: none;
> .ant-collapse-header {
padding-top: 2.5px;
padding-bottom: 2.5px;
background: $header-color;
border-radius: 0px;
height: 25px;
}
> .ant-collapse-content {
background: $background-color-2;
border-bottom: none;
height: 150px;
}
}
}
.cvat-object-sidebar-icon {
fill: $objects-bar-icons-color;
color: $objects-bar-icons-color;
font-size: 21px;
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(1);
}
}
.cvat-objects-sidebar-tabs.ant-tabs.ant-tabs-card {
background: $header-color;
box-sizing: border-box;
border: none;
.ant-tabs-card-bar {
border: none;
margin-bottom: 0px;
padding-top: 25px;
.ant-tabs-tab {
background: $transparent-color;
border: none;
&:nth-child(1) {
margin-left: 5px;
}
}
.ant-tabs-tab.ant-tabs-tab-active {
background: $objects-bar-tabs-color;
}
}
}
.cvat-objects-sidebar-states-header {
background: $objects-bar-tabs-color;
padding: 5px;
> div:nth-child(1) > div > span {
> input {
text-indent: 10px;
}
i {
@extend .cvat-object-sidebar-icon;
}
}
> div:nth-child(2) {
margin-top: 5px;
> div {
text-align: center;
margin: 0px 2px;
> i {
@extend .cvat-object-sidebar-icon;
}
&:nth-child(4) {
text-align: right;
> .ant-select {
margin-left: 5px;
width: 60%;
}
}
}
}
}
.cvat-objects-sidebar-states-header {
height: 80px;
}
.cvat-objects-sidebar-states-list {
background-color: $background-color-2;
height: calc(100% - 80px);
overflow-y: auto;
}
.cvat-objects-sidebar-state-active-item {
background: $active-object-item-background-color;
}
.cvat-objects-sidebar-state-item {
width: 100%;
padding: 5px 5px 5px 5px;
border-left-width: 5px;
border-bottom: 1px dashed;
> div:nth-child(1) {
> div:nth-child(3) > i {
@extend .cvat-object-sidebar-icon;
font-size: 25px;
}
> div:nth-child(2) > .ant-select {
width: 100%;
}
}
> div:nth-child(2) {
> div > div {
margin-top: 10px;
}
i {
@extend .cvat-object-sidebar-icon;
}
}
> div:nth-child(3) {
margin-top: 10px;
}
&:hover {
@extend .cvat-objects-sidebar-state-active-item;
}
}
.cvat-objects-sidebar-state-item-collapse {
border: 0px;
background: inherit;
> .ant-collapse-item {
background: inherit;
border: none;
> .ant-collapse-header {
background: inherit;
padding-top: 2px;
padding-bottom: 2px;
}
> .ant-collapse-content {
background: inherit;
}
}
}
.cvat-object-item-attribute-wrapper {
margin-top: 5px;
}
.cvat-object-item-select-attribute {
width: 100%;
}
.cvat-object-item-number-attribute {
width: 100%;
}
.cvat-object-item-text-attribute {
width: 100%;
}
.cvat-object-item-radio-attribute {
border: 1px double $border-color-hover;
border-radius: 7px 7px 7px 7px;
> legend {
text-align: center;
width: unset;
text-overflow: ellipsis;
max-width: 80%;
overflow: hidden;
max-height: 1.2em;
font-size: 1.2em;
> span {
padding: 0 10px;
}
}
> .ant-radio-group {
display: grid;
padding: 5px
}
}
.cvat-objects-sidebar-labels-list {
background-color: $background-color-2;
height: 100%;
overflow-y: auto;
}
.cvat-objects-sidebar-label-active-item {
background: $active-object-item-background-color;
}
.cvat-objects-sidebar-label-item {
height: 2.5em;
border-bottom: 1px solid $border-color-2;
padding: 5px;
i {
@extend .cvat-object-sidebar-icon;
}
> div:nth-child(2) {
text-overflow: ellipsis;
overflow: hidden;
max-height: 1.5em;
font-size: 1em;
}
&:hover {
@extend .cvat-objects-sidebar-label-active-item;
}
}
.cvat-label-item-color-button {
width: 30px;
height: 20px;
border-radius: 5px;
}

@ -5,30 +5,17 @@ import {
Layout,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarComponent from './objects-side-bar/objects-side-bar';
interface Props {
canvasInstance: Canvas;
}
import ObjectSideBarContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
export default function StandardWorkspaceComponent(props: Props): JSX.Element {
const {
canvasInstance,
} = props;
export default function StandardWorkspaceComponent(): JSX.Element {
return (
<Layout hasSider>
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<ObjectSideBarComponent
onSidebarFoldUnfold={(): void => {
canvasInstance.fitCanvas();
}}
/>
<ObjectSideBarContainer />
</Layout>
);
}

@ -1,10 +1,10 @@
@import '../../../base.scss';
@import 'base.scss';
.cvat-annotation-page-canvas-container {
.cvat-canvas-container {
background-color: $background-color-1;
}
.cvat-annotation-page-objects-sidebar {
.cvat-objects-sidebar-sider {
top: 0px;
right: 0px;
left: auto;
@ -13,7 +13,11 @@
z-index: 2;
}
.cvat-annotation-page-controls-sidebar {
.cvat-objects-sidebar {
height: 100%;
}
.cvat-canvas-controls-sidebar {
background-color: $background-color-2;
border-right: 1px solid $border-color-1;
@ -39,13 +43,13 @@
}
}
.cvat-annotation-page-active-control {
.cvat-active-canvas-control {
background: $header-color;
transform: scale(0.75);
}
.cvat-annotation-page-controls-rotate-left,
.cvat-annotation-page-controls-rotate-right {
.cvat-rotate-canvas-controls-left,
.cvat-rotate-canvas-controls-right {
transform: scale(0.65);
border-radius: 5px;
@ -57,14 +61,14 @@
}
}
.cvat-annotation-page-controls-rotate >
.cvat-rotate-canvas-controls >
.ant-popover-content >
.ant-popover-inner > div >
.ant-popover-inner-content {
padding: 0px;
}
.cvat-annotation-page-controls-rotate-right > svg {
.cvat-rotate-canvas-controls-right > svg {
transform: scaleX(-1);
}
@ -99,4 +103,4 @@
border-radius: 0px 3px 3px 0px;
}
}
}
}

@ -4,7 +4,7 @@
height: 100%
}
.ant-layout-header.cvat-annotation-page-header {
.ant-layout-header.cvat-annotation-header {
background-color: $background-color-2;
border-bottom: 1px solid $border-color-1;
height: 54px;
@ -64,7 +64,7 @@
line-height: 0px;
}
.cvat-annotation-header-player-buttons {
.cvat-player-buttons {
display: flex;
align-items: center;
position: relative;
@ -72,19 +72,21 @@
margin-right: 10px;
> i {
transform: scale(0.6);
font-size: 25px;
margin: 0px 7px;
color: $player-buttons-color;
&:hover {
transform: scale(0.7);
transform: scale(1.1);
}
&:active {
transform: scale(0.6);
transform: scale(1);
}
}
}
.cvat-annotation-header-player-controls {
.cvat-player-controls {
position: relative;
height: 100%;
line-height: 27px;
@ -95,7 +97,7 @@
}
}
.cvat-annotation-header-player-slider {
.cvat-player-slider {
width: 350px;
> .ant-slider-rail {
@ -103,14 +105,14 @@
}
}
.cvat-annotation-header-filename-wrapper {
.cvat-player-filename-wrapper {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
user-select: none;
}
.cvat-annotation-header-frame-selector {
.cvat-player-frame-selector {
width: 5em;
padding-right: 5px;
margin-left: 5px;
@ -149,6 +151,6 @@
}
}
.cvat-annotation-header-workspace-selector {
.cvat-workspace-selector {
width: 150px;
}

@ -0,0 +1,76 @@
import React from 'react';
import {
Col,
Icon,
Modal,
Timeline,
} from 'antd';
import {
MainMenuIcon,
SaveIcon,
UndoIcon,
RedoIcon,
} from '../../../icons';
interface Props {
saving: boolean;
savingStatuses: string[];
onSaveAnnotation(): void;
}
const LeftGroup = React.memo((props: Props): JSX.Element => {
const {
saving,
savingStatuses,
onSaveAnnotation,
} = props;
return (
<Col className='cvat-annotation-header-left-group'>
<div className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} />
<span>Menu</span>
</div>
<div
className={saving
? 'cvat-annotation-disabled-header-button'
: 'cvat-annotation-header-button'
}
>
<Icon component={SaveIcon} onClick={onSaveAnnotation} />
<span>
{ saving ? 'Saving...' : 'Save' }
</span>
<Modal
title='Saving changes on the server'
visible={saving}
footer={[]}
closable={false}
>
<Timeline pending={savingStatuses[savingStatuses.length - 1] || 'Pending..'}>
{
savingStatuses.slice(0, -1)
.map((
status: string,
id: number,
// eslint-disable-next-line react/no-array-index-key
) => <Timeline.Item key={id}>{status}</Timeline.Item>)
}
</Timeline>
</Modal>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={UndoIcon} />
<span>Undo</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={RedoIcon} />
<span>Redo</span>
</div>
</Col>
);
});
export default LeftGroup;

@ -0,0 +1,87 @@
import React from 'react';
import {
Col,
Icon,
Tooltip,
} from 'antd';
import {
FirstIcon,
BackJumpIcon,
PreviousIcon,
PlayIcon,
PauseIcon,
NextIcon,
ForwardJumpIcon,
LastIcon,
} from '../../../icons';
interface Props {
playing: boolean;
onSwitchPlay(): void;
onPrevFrame(): void;
onNextFrame(): void;
onForward(): void;
onBackward(): void;
onFirstFrame(): void;
onLastFrame(): void;
}
const PlayerButtons = React.memo((props: Props): JSX.Element => {
const {
playing,
onSwitchPlay,
onPrevFrame,
onNextFrame,
onForward,
onBackward,
onFirstFrame,
onLastFrame,
} = props;
return (
<Col className='cvat-player-buttons'>
<Tooltip overlay='Go to the first frame'>
<Icon component={FirstIcon} onClick={onFirstFrame} />
</Tooltip>
<Tooltip overlay='Go back with a step'>
<Icon component={BackJumpIcon} onClick={onBackward} />
</Tooltip>
<Tooltip overlay='Go back'>
<Icon component={PreviousIcon} onClick={onPrevFrame} />
</Tooltip>
{!playing
? (
<Tooltip overlay='Play'>
<Icon
component={PlayIcon}
onClick={onSwitchPlay}
/>
</Tooltip>
)
: (
<Tooltip overlay='Pause'>
<Icon
component={PauseIcon}
onClick={onSwitchPlay}
/>
</Tooltip>
)
}
<Tooltip overlay='Go next'>
<Icon component={NextIcon} onClick={onNextFrame} />
</Tooltip>
<Tooltip overlay='Go next with a step'>
<Icon component={ForwardJumpIcon} onClick={onForward} />
</Tooltip>
<Tooltip overlay='Go to the last frame'>
<Icon component={LastIcon} onClick={onLastFrame} />
</Tooltip>
</Col>
);
});
export default PlayerButtons;

@ -0,0 +1,66 @@
import React from 'react';
import {
Row,
Col,
Slider,
Tooltip,
InputNumber,
} from 'antd';
import { SliderValue } from 'antd/lib/slider';
import Text from 'antd/lib/typography/Text';
interface Props {
startFrame: number;
stopFrame: number;
frameNumber: number;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
}
const PlayerNavigation = React.memo((props: Props): JSX.Element => {
const {
startFrame,
stopFrame,
frameNumber,
onSliderChange,
onInputChange,
} = props;
return (
<>
<Col className='cvat-player-controls'>
<Row type='flex'>
<Col>
<Slider
className='cvat-player-slider'
min={startFrame}
max={stopFrame}
value={frameNumber || 0}
onChange={onSliderChange}
/>
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col className='cvat-player-filename-wrapper'>
<Tooltip overlay='filename.png'>
<Text type='secondary'>filename.png</Text>
</Tooltip>
</Col>
</Row>
</Col>
<Col>
<InputNumber
className='cvat-player-frame-selector'
type='number'
value={frameNumber || 0}
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
onChange={onInputChange}
/>
</Col>
</>
);
});
export default PlayerNavigation;

@ -0,0 +1,33 @@
import React from 'react';
import {
Col,
Icon,
Select,
} from 'antd';
import {
InfoIcon,
FullscreenIcon,
} from '../../../icons';
const RightGroup = React.memo((): JSX.Element => (
<Col className='cvat-annotation-header-right-group'>
<div className='cvat-annotation-header-button'>
<Icon component={FullscreenIcon} />
<span>Fullscreen</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={InfoIcon} />
<span>Info</span>
</div>
<div>
<Select className='cvat-workspace-selector' defaultValue='standard'>
<Select.Option key='standard' value='standard'>Standard</Select.Option>
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option>
</Select>
</div>
</Col>
));
export default RightGroup;

@ -3,284 +3,97 @@ import React from 'react';
import {
Row,
Col,
Icon,
Slider,
Layout,
Input,
Tooltip,
Select,
Modal,
Timeline,
} from 'antd';
import { SliderValue } from 'antd/lib/slider';
import Text from 'antd/lib/typography/Text';
import {
MainMenuIcon,
SaveIcon,
UndoIcon,
RedoIcon,
PlaycontrolFirstIcon,
PlaycontrolBackJumpIcon,
PlaycontrolPreviousIcon,
PlaycontrolPlayIcon,
PlaycontrolPauseIcon,
PlaycontrolNextIcon,
PlaycontrolForwardJumpIcon,
PlaycontrolLastIcon,
InfoIcon,
FullscreenIcon,
} from '../../../icons';
import LeftGroup from './left-group';
import RightGroup from './right-group';
import PlayerNavigation from './player-navigation';
import PlayerButtons from './player-buttons';
interface Props {
jobInstance: any;
frame: number;
frameStep: number;
playing: boolean;
saving: boolean;
savingStatuses: string[];
canvasIsReady: boolean;
onChangeFrame(frame: number, playing: boolean): void;
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
frameNumber: number;
startFrame: number;
stopFrame: number;
onSwitchPlay(): void;
onSaveAnnotation(): void;
onPrevFrame(): void;
onNextFrame(): void;
onForward(): void;
onBackward(): void;
onFirstFrame(): void;
onLastFrame(): void;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
}
function SavingOverlay(saving: boolean, statuses: string[]): JSX.Element {
return (
<Modal
title='Saving changes on the server'
visible={saving}
footer={[]}
closable={false}
>
<Timeline pending={statuses[statuses.length - 1] || 'Pending..'}>
{
statuses.slice(0, -1)
.map((
status: string,
id: number,
// eslint-disable-next-line react/no-array-index-key
) => <Timeline.Item key={id}>{status}</Timeline.Item>)
}
</Timeline>
</Modal>
);
function propsAreEqual(curProps: Props, prevProps: Props): boolean {
return curProps.playing === prevProps.playing
&& curProps.saving === prevProps.saving
&& curProps.frameNumber === prevProps.frameNumber
&& curProps.startFrame === prevProps.startFrame
&& curProps.stopFrame === prevProps.stopFrame
&& curProps.savingStatuses.length === prevProps.savingStatuses.length;
}
export default function AnnotationTopBarComponent(props: Props): JSX.Element {
const AnnotationTopBarComponent = React.memo((props: Props): JSX.Element => {
const {
jobInstance,
frame,
frameStep,
playing,
saving,
savingStatuses,
canvasIsReady,
onChangeFrame,
playing,
frameNumber,
startFrame,
stopFrame,
onSwitchPlay,
onSaveAnnotation,
onPrevFrame,
onNextFrame,
onForward,
onBackward,
onFirstFrame,
onLastFrame,
onSliderChange,
onInputChange,
} = props;
if (playing && canvasIsReady) {
if (frame < jobInstance.stopFrame) {
setTimeout(() => {
onChangeFrame(frame + 1, true);
}, 30);
} else {
onSwitchPlay(false);
}
}
const savingOverlay = SavingOverlay(saving, savingStatuses);
return (
<Layout.Header className='cvat-annotation-page-header'>
<Layout.Header className='cvat-annotation-header'>
<Row type='flex' justify='space-between'>
<Col className='cvat-annotation-header-left-group'>
<div className='cvat-annotation-header-button'>
<Icon component={MainMenuIcon} />
<span>Menu</span>
</div>
<div className={saving ? 'cvat-annotation-disabled-header-button' : 'cvat-annotation-header-button'}>
<Icon
component={SaveIcon}
onClick={async (): Promise<void> => {
onSaveAnnotation(jobInstance);
}}
/>
<span>
{ saving ? 'Saving...' : 'Save' }
</span>
{ savingOverlay }
</div>
<div className='cvat-annotation-header-button'>
<Icon component={UndoIcon} />
<span>Undo</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={RedoIcon} />
<span>Redo</span>
</div>
</Col>
<LeftGroup
saving={saving}
savingStatuses={savingStatuses}
onSaveAnnotation={onSaveAnnotation}
/>
<Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'>
<Col className='cvat-annotation-header-player-buttons'>
<Tooltip overlay='Go to the first frame'>
<Icon
component={PlaycontrolFirstIcon}
onClick={(): void => {
if (jobInstance.startFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(jobInstance.startFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go back with a step'>
<Icon
component={PlaycontrolBackJumpIcon}
onClick={(): void => {
const newFrame = Math
.max(jobInstance.startFrame, frame - frameStep);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go back'>
<Icon
component={PlaycontrolPreviousIcon}
onClick={(): void => {
const newFrame = Math
.max(jobInstance.startFrame, frame - 1);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
{!playing
? (
<Tooltip overlay='Play'>
<Icon
component={PlaycontrolPlayIcon}
onClick={(): void => {
if (frame < jobInstance.stopFrame) {
onSwitchPlay(true);
}
}}
/>
</Tooltip>
)
: (
<Tooltip overlay='Pause'>
<Icon
component={PlaycontrolPauseIcon}
onClick={(): void => {
onSwitchPlay(false);
}}
/>
</Tooltip>
)
}
<Tooltip overlay='Go next'>
<Icon
component={PlaycontrolNextIcon}
onClick={(): void => {
const newFrame = Math
.min(jobInstance.stopFrame, frame + 1);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go next with a step'>
<Icon
component={PlaycontrolForwardJumpIcon}
onClick={(): void => {
const newFrame = Math
.min(jobInstance.stopFrame, frame + frameStep);
if (newFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(newFrame, false);
}
}}
/>
</Tooltip>
<Tooltip overlay='Go to the last frame'>
<Icon
component={PlaycontrolLastIcon}
onClick={(): void => {
if (jobInstance.stopFrame !== frame) {
onSwitchPlay(false);
onChangeFrame(jobInstance.stopFrame, false);
}
}}
/>
</Tooltip>
</Col>
<Col className='cvat-annotation-header-player-controls'>
<Row type='flex'>
<Col>
<Slider
className='cvat-annotation-header-player-slider'
min={jobInstance.startFrame}
max={jobInstance.stopFrame}
value={frame || 0}
onChange={(value: SliderValue): void => {
onSwitchPlay(false);
onChangeFrame(value as number, false);
}}
/>
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col className='cvat-annotation-header-filename-wrapper'>
<Tooltip overlay='filename.png'>
<Text type='secondary'>filename.png</Text>
</Tooltip>
</Col>
</Row>
</Col>
<Col>
<Input
className='cvat-annotation-header-frame-selector'
type='number'
value={frame || 0}
// https://stackoverflow.com/questions/38256332/in-react-whats-the-difference-between-onchange-and-oninput
onChange={(e: React.ChangeEvent<HTMLInputElement>): void => {
onSwitchPlay(false);
onChangeFrame(+e.target.value, false);
}}
/>
</Col>
<PlayerButtons
playing={playing}
onPrevFrame={onPrevFrame}
onNextFrame={onNextFrame}
onForward={onForward}
onBackward={onBackward}
onFirstFrame={onFirstFrame}
onLastFrame={onLastFrame}
onSwitchPlay={onSwitchPlay}
/>
<PlayerNavigation
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
/>
</Row>
</Col>
<Col className='cvat-annotation-header-right-group'>
<div className='cvat-annotation-header-button'>
<Icon component={FullscreenIcon} />
<span>Fullscreen</span>
</div>
<div className='cvat-annotation-header-button'>
<Icon component={InfoIcon} />
<span>Info</span>
</div>
<div>
<Select className='cvat-annotation-header-workspace-selector' defaultValue='standard'>
<Select.Option key='standard' value='standard'>Standard</Select.Option>
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option>
</Select>
</div>
</Col>
<RightGroup />
</Row>
</Layout.Header>
);
}
}, propsAreEqual);
export default AnnotationTopBarComponent;

@ -119,15 +119,15 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
resetMessages,
} = this.props;
const { tasks } = notifications.messages;
const { models } = notifications.messages;
const shown = !!tasks.loadingDone || !!models.inferenceDone;
if (tasks.loadingDone) {
showMessage(tasks.loadingDone);
}
if (models.inferenceDone) {
showMessage(models.inferenceDone);
let shown = false;
for (const where of Object.keys(notifications.messages)) {
for (const what of Object.keys(notifications.messages[where])) {
const message = notifications.messages[where][what];
shown = shown || !!message;
if (message) {
showMessage(message);
}
}
}
if (shown) {
@ -159,97 +159,15 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
resetErrors,
} = this.props;
const { auth } = notifications.errors;
const { tasks } = notifications.errors;
const { formats } = notifications.errors;
const { users } = notifications.errors;
const { about } = notifications.errors;
const { share } = notifications.errors;
const { models } = notifications.errors;
const { annotation } = notifications.errors;
const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
|| !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading
|| !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching
|| !!users.fetching || !!about.fetching || !!share.fetching || !!models.creating || !!models.starting
|| !!models.fetching || !!models.deleting || !!models.inferenceStatusFetching
|| !!models.metaFetching || !!annotation.frameFetching || !!annotation.saving
|| !!annotation.jobFetching;
if (auth.authorized) {
showError(auth.authorized.message, auth.authorized.reason);
}
if (auth.login) {
showError(auth.login.message, auth.login.reason);
}
if (auth.register) {
showError(auth.register.message, auth.register.reason);
}
if (auth.logout) {
showError(auth.logout.message, auth.logout.reason);
}
if (tasks.fetching) {
showError(tasks.fetching.message, tasks.fetching.reason);
}
if (tasks.updating) {
showError(tasks.updating.message, tasks.updating.reason);
}
if (tasks.dumping) {
showError(tasks.dumping.message, tasks.dumping.reason);
}
if (tasks.loading) {
showError(tasks.loading.message, tasks.loading.reason);
}
if (tasks.exporting) {
showError(tasks.exporting.message, tasks.exporting.reason);
}
if (tasks.deleting) {
showError(tasks.deleting.message, tasks.deleting.reason);
}
if (tasks.creating) {
showError(tasks.creating.message, tasks.creating.reason);
}
if (formats.fetching) {
showError(formats.fetching.message, formats.fetching.reason);
}
if (users.fetching) {
showError(users.fetching.message, users.fetching.reason);
}
if (about.fetching) {
showError(about.fetching.message, about.fetching.reason);
}
if (share.fetching) {
showError(share.fetching.message, share.fetching.reason);
}
if (models.creating) {
showError(models.creating.message, models.creating.reason);
}
if (models.starting) {
showError(models.starting.message, models.starting.reason);
}
if (models.fetching) {
showError(models.fetching.message, models.fetching.reason);
}
if (models.deleting) {
showError(models.deleting.message, models.deleting.reason);
}
if (models.metaFetching) {
showError(models.metaFetching.message, models.metaFetching.reason);
}
if (models.inferenceStatusFetching) {
showError(
models.inferenceStatusFetching.message,
models.inferenceStatusFetching.reason,
);
}
if (annotation.jobFetching) {
showError(annotation.jobFetching.message, annotation.jobFetching.reason);
}
if (annotation.frameFetching) {
showError(annotation.frameFetching.message, annotation.frameFetching.reason);
}
if (annotation.saving) {
showError(annotation.saving.message, annotation.saving.reason);
let shown = false;
for (const where of Object.keys(notifications.errors)) {
for (const what of Object.keys(notifications.errors[where])) {
const error = notifications.errors[where][what];
shown = shown || !!error;
if (error) {
showError(error.message, error.reason);
}
}
}
if (shown) {

@ -33,7 +33,7 @@ interface HeaderContainerProps {
installedTFAnnotation: boolean;
installedTFSegmentation: boolean;
username: string;
about: any;
serverAbout: any;
}
type Props = HeaderContainerProps & RouteComponentProps;
@ -45,7 +45,7 @@ function HeaderContainer(props: Props): JSX.Element {
installedTFAnnotation,
installedAnalytics,
username,
about,
serverAbout,
onLogout,
logoutFetching,
} = props;
@ -54,20 +54,25 @@ function HeaderContainer(props: Props): JSX.Element {
|| installedTFAnnotation
|| installedTFSegmentation;
function aboutModal() {
function aboutModal(): void {
const CHANGELOG = 'https://github.com/opencv/cvat/blob/develop/CHANGELOG.md';
const LICENSE = 'https://github.com/opencv/cvat/blob/develop/LICENSE';
const GITTER = 'https://gitter.im/opencv-cvat';
const FORUM = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit';
Modal.info({
title: `${about.name}`,
title: `${serverAbout.name}`,
content: (
<div>
<p>
{`${about.description}`}
{`${serverAbout.description}`}
</p>
<p>
<Text strong>
Server version:
</Text>
<Text type='secondary'>
{` ${about.version}`}
{` ${serverAbout.version}`}
</Text>
</p>
<p>
@ -79,20 +84,20 @@ function HeaderContainer(props: Props): JSX.Element {
</Text>
</p>
<Row type='flex' justify='space-around'>
<Col><a href='https://github.com/opencv/cvat/blob/develop/CHANGELOG.md' target='_blank' rel='noopener noreferrer' >What's new?</a></Col>
<Col><a href='https://github.com/opencv/cvat/blob/develop/LICENSE' target='_blank' rel='noopener noreferrer' >License</a></Col>
<Col><a href='https://gitter.im/opencv-cvat' target='_blank' rel='noopener noreferrer' >Need help?</a></Col>
<Col><a href='https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit' target='_blank' rel='noopener noreferrer' >Forum on Intel Developer Zone</a></Col>
</Row>
<Col><a href={CHANGELOG} target='_blank' rel='noopener noreferrer'>{'What\'s new?'}</a></Col>
<Col><a href={LICENSE} target='_blank' rel='noopener noreferrer'>License</a></Col>
<Col><a href={GITTER} target='_blank' rel='noopener noreferrer'>Need help?</a></Col>
<Col><a href={FORUM} target='_blank' rel='noopener noreferrer'>Forum on Intel Developer Zone</a></Col>
</Row>
</div>
),
width : 800,
width: 800,
okButtonProps: {
style: {
width: '100px',
},
},
})
});
}
const menu = (

@ -232,7 +232,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
private renderNumberRangeInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.values[0] : '';
const value = attr ? attr.values.join(';') : '';
const { form } = this.props;
const validator = (_: any, strNumbers: string, callback: any): void => {

@ -14,8 +14,8 @@ import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import {
PlaycontrolBackJumpIcon,
PlaycontrolForwardJumpIcon,
BackJumpIcon,
ForwardJumpIcon,
} from 'icons';
import {
@ -78,9 +78,9 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col offset={1}>
<Text type='secondary'>
Number of frames skipped when selecting
<Icon component={PlaycontrolBackJumpIcon} />
<Icon component={BackJumpIcon} />
or
<Icon component={PlaycontrolForwardJumpIcon} />
<Icon component={ForwardJumpIcon} />
</Text>
</Col>
</Row>

@ -45,13 +45,13 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { dumps } = activities;
const { loads } = activities;
const activeExports = activities.exports;
const { plugins } = state.plugins;
const { list } = state.plugins;
const { id } = own.taskInstance;
return {
installedTFAnnotation: plugins.TF_ANNOTATION,
installedTFSegmentation: plugins.TF_SEGMENTATION,
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
installedTFAnnotation: list.TF_ANNOTATION,
installedTFSegmentation: list.TF_SEGMENTATION,
installedAutoAnnotation: list.AUTO_ANNOTATION,
dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null,
exportActivities: activeExports.byTask[id] ? activeExports.byTask[id] : null,
loadActivity: loads.byTask[id] ? loads.byTask[id] : null,

@ -16,7 +16,7 @@ type OwnProps = RouteComponentProps<{
}>;
interface StateToProps {
jobInstance: any | null | undefined;
job: any | null | undefined;
fetching: boolean;
}
@ -25,11 +25,18 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
const {
annotation: {
job: {
instance: job,
fetching,
},
},
} = state;
return {
jobInstance: annotation.jobInstance,
fetching: annotation.jobFetching,
job,
fetching,
};
}

@ -9,10 +9,14 @@ import {
zoomCanvas,
resetCanvas,
shapeDrawn,
objectsMerged,
objectsGroupped,
trackSplitted,
annotationsUpdated,
mergeObjects,
groupObjects,
splitTrack,
updateAnnotationsAsync,
createAnnotationsAsync,
mergeAnnotationsAsync,
groupAnnotationsAsync,
splitAnnotationsAsync,
} from 'actions/annotation-actions';
import {
GridColor,
@ -23,6 +27,7 @@ import {
import { Canvas } from 'cvat-canvas';
interface StateToProps {
sidebarCollapsed: boolean;
canvasInstance: Canvas;
jobInstance: any;
annotations: any[];
@ -42,35 +47,52 @@ interface DispatchToProps {
onZoomCanvas: (enabled: boolean) => void;
onResetCanvas: () => void;
onShapeDrawn: () => void;
onObjectsMerged: () => void;
onObjectsGroupped: () => void;
onTrackSplitted: () => void;
onAnnotationsUpdated: (annotations: any[]) => void;
onMergeObjects: (enabled: boolean) => void;
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
canvasInstance,
jobInstance,
frameData,
frame,
annotations,
drawing,
} = state.annotation;
const {
activeLabelID,
activeObjectType,
} = drawing;
const {
grid,
gridSize,
gridColor,
gridOpacity,
} = state.settings.player;
annotation: {
canvas: {
instance: canvasInstance,
},
drawing: {
activeLabelID,
activeObjectType,
},
job: {
instance: jobInstance,
},
player: {
frame: {
data: frameData,
number: frame,
},
},
annotations: {
states: annotations,
},
sidebarCollapsed,
},
settings: {
player: {
grid,
gridSize,
gridColor,
gridOpacity,
},
},
} = state;
return {
sidebarCollapsed,
canvasInstance,
jobInstance,
frameData,
@ -102,17 +124,29 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onObjectsMerged(): void {
dispatch(objectsMerged());
onMergeObjects(enabled: boolean): void {
dispatch(mergeObjects(enabled));
},
onGroupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onSplitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled));
},
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
},
onObjectsGroupped(): void {
dispatch(objectsGroupped());
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(mergeAnnotationsAsync(sessionInstance, frame, states));
},
onTrackSplitted(): void {
dispatch(trackSplitted());
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(groupAnnotationsAsync(sessionInstance, frame, states));
},
onAnnotationsUpdated(annotations: any[]): void {
dispatch(annotationsUpdated(annotations));
onSplitAnnotations(sessionInstance: any, frame: number, state: any): void {
dispatch(splitAnnotationsAsync(sessionInstance, frame, state));
},
};
}

@ -12,62 +12,57 @@ import ControlsSideBarComponent from 'components/annotation-page/standard-worksp
import {
ActiveControl,
CombinedState,
StringObject,
} from 'reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
rotateAll: boolean;
activeControl: ActiveControl;
labels: StringObject;
}
interface DispatchToProps {
onMergeStart(): void;
onGroupStart(): void;
onSplitStart(): void;
mergeObjects(enabled: boolean): void;
groupObjects(enabled: boolean): void;
splitTrack(enabled: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
annotation: {
canvas: {
instance: canvasInstance,
activeControl,
},
},
settings: {
player: {
rotateAll,
},
},
} = state;
const {
canvasInstance,
activeControl,
} = annotation;
const labels = annotation.jobInstance.task.labels
.reduce((acc: StringObject, label: any): StringObject => {
acc[label.id as number] = label.name;
return acc;
}, {});
return {
rotateAll: settings.player.rotateAll,
rotateAll,
canvasInstance,
activeControl,
labels,
};
}
function dispatchToProps(dispatch: any): DispatchToProps {
return {
onMergeStart(): void {
dispatch(mergeObjects());
mergeObjects(enabled: boolean): void {
dispatch(mergeObjects(enabled));
},
onGroupStart(): void {
dispatch(groupObjects());
groupObjects(enabled: boolean): void {
dispatch(groupObjects(enabled));
},
onSplitStart(): void {
dispatch(splitTrack());
splitTrack(enabled: boolean): void {
dispatch(splitTrack(enabled));
},
};
}
function StandardWorkspaceContainer(props: StateToProps & DispatchToProps): JSX.Element {
function ControlsSideBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
@ -76,4 +71,4 @@ function StandardWorkspaceContainer(props: StateToProps & DispatchToProps): JSX.
export default connect(
mapStateToProps,
dispatchToProps,
)(StandardWorkspaceContainer);
)(ControlsSideBarContainer);

@ -5,7 +5,6 @@ import {
CombinedState,
ShapeType,
ObjectType,
StringObject,
} from 'reducers/interfaces';
import {
@ -30,7 +29,7 @@ interface DispatchToProps {
interface StateToProps {
canvasInstance: Canvas;
shapeType: ShapeType;
labels: StringObject;
labels: any[];
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
@ -48,19 +47,16 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const {
annotation,
annotation: {
canvas: {
instance: canvasInstance,
},
job: {
labels,
},
},
} = state;
const {
canvasInstance,
} = annotation;
const labels = annotation.jobInstance.task.labels
.reduce((acc: StringObject, label: any): StringObject => {
acc[label.id as number] = label.name;
return acc;
}, {});
return {
...own,
canvasInstance,
@ -68,10 +64,110 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
};
}
function DrawShapePopoverContainer(props: DispatchToProps & StateToProps): JSX.Element {
return (
<DrawShapePopoverComponent {...props} />
);
type Props = StateToProps & DispatchToProps;
interface State {
numberOfPoints?: number;
selectedLabelID: number;
}
class DrawShapePopoverContainer extends React.PureComponent<Props, State> {
private minimumPoints = 3;
constructor(props: Props) {
super(props);
const defaultLabelID = props.labels[0].id;
this.state = {
selectedLabelID: defaultLabelID,
};
const { shapeType } = props;
if (shapeType === ShapeType.POLYGON) {
this.minimumPoints = 3;
}
if (shapeType === ShapeType.POLYLINE) {
this.minimumPoints = 2;
}
if (shapeType === ShapeType.POINTS) {
this.minimumPoints = 1;
}
}
private onDraw(objectType: ObjectType): void {
const {
canvasInstance,
shapeType,
onDrawStart,
} = this.props;
const {
numberOfPoints,
selectedLabelID,
} = this.state;
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
numberOfPoints,
shapeType,
crosshair: shapeType === ShapeType.RECTANGLE,
});
onDrawStart(shapeType, selectedLabelID,
objectType, numberOfPoints);
}
private onDrawShape = (): void => {
this.onDraw(ObjectType.SHAPE);
};
private onDrawTrack = (): void => {
this.onDraw(ObjectType.TRACK);
};
private onChangePoints = (value: number | undefined): void => {
if (typeof (value) === 'undefined') {
this.setState({
numberOfPoints: value,
});
} else if (typeof (value) === 'number') {
this.setState({
numberOfPoints: Math.max(value, this.minimumPoints),
});
}
};
private onChangeLabel = (value: string): void => {
this.setState({
selectedLabelID: +value,
});
};
public render(): JSX.Element {
const {
selectedLabelID,
numberOfPoints,
} = this.state;
const {
labels,
shapeType,
} = this.props;
return (
<DrawShapePopoverComponent
labels={labels}
shapeType={shapeType}
minimumPoints={this.minimumPoints}
selectedLabeID={selectedLabelID}
numberOfPoints={numberOfPoints}
onChangeLabel={this.onChangeLabel}
onChangePoints={this.onChangePoints}
onDrawTrack={this.onDrawTrack}
onDrawShape={this.onDrawShape}
/>
);
}
}
export default connect(

@ -0,0 +1,211 @@
import React from 'react';
import { connect } from 'react-redux';
import {
changeLabelColor as changeLabelColorAction,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import LabelItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/label-item';
import { CombinedState } from 'reducers/interfaces';
interface OwnProps {
labelID: number;
}
interface StateToProps {
label: any;
labelName: string;
labelColor: string;
labelColors: string[];
objectStates: any[];
jobInstance: any;
frameNumber: any;
}
interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeLabelColor(label: any, color: string): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const {
annotation: {
annotations: {
states: objectStates,
},
job: {
instance: jobInstance,
labels,
},
player: {
frame: {
number: frameNumber,
},
},
colors: labelColors,
},
} = state;
const [label] = labels.filter((_label: any) => _label.id === own.labelID);
return {
label,
labelColor: label.color,
labelName: label.name,
labelColors,
objectStates,
jobInstance,
frameNumber,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
},
changeLabelColor(label: any, color: string): void {
dispatch(changeLabelColorAction(label, color));
},
};
}
type Props = StateToProps & DispatchToProps & OwnProps;
interface State {
objectStates: any[];
ownObjectStates: any[];
visible: boolean;
statesHidden: boolean;
statesLocked: boolean;
}
class LabelItemContainer extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
objectStates: [],
ownObjectStates: [],
visible: true,
statesHidden: false,
statesLocked: false,
};
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (props.objectStates === state.objectStates) {
return null;
}
const ownObjectStates = props.objectStates
.filter((ownObjectState: any): boolean => ownObjectState.label.id === props.labelID);
const visible = !!ownObjectStates.length;
let statesHidden = true;
let statesLocked = true;
ownObjectStates.forEach((objectState: any) => {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
});
return {
...state,
objectStates: props.objectStates,
ownObjectStates,
statesHidden,
statesLocked,
visible,
};
}
private hideStates = (): void => {
this.switchHidden(true);
};
private showStates = (): void => {
this.switchHidden(false);
};
private lockStates = (): void => {
this.switchLock(true);
};
private unlockStates = (): void => {
this.switchLock(false);
};
private changeColor = (color: string): void => {
const {
changeLabelColor,
label,
} = this.props;
changeLabelColor(label, color);
};
private switchHidden(value: boolean): void {
const {
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
const { ownObjectStates } = this.state;
for (const state of ownObjectStates) {
state.hidden = value;
}
updateAnnotations(jobInstance, frameNumber, ownObjectStates);
}
private switchLock(value: boolean): void {
const {
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
const { ownObjectStates } = this.state;
for (const state of ownObjectStates) {
state.lock = value;
}
updateAnnotations(jobInstance, frameNumber, ownObjectStates);
}
public render(): JSX.Element {
const {
visible,
statesHidden,
statesLocked,
} = this.state;
const {
labelName,
labelColor,
labelColors,
} = this.props;
return (
<LabelItemComponent
labelName={labelName}
labelColor={labelColor}
labelColors={labelColors}
visible={visible}
statesHidden={statesHidden}
statesLocked={statesLocked}
hideStates={this.hideStates}
showStates={this.showStates}
lockStates={this.lockStates}
unlockStates={this.unlockStates}
changeColor={this.changeColor}
/>
);
}
}
export default connect<StateToProps, DispatchToProps, OwnProps, CombinedState>(
mapStateToProps,
mapDispatchToProps,
)(LabelItemContainer);

@ -0,0 +1,36 @@
import React from 'react';
import { connect } from 'react-redux';
import LabelsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/labels-list';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
labelIDs: number[];
listHeight: number;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
job: {
labels,
},
tabContentHeight: listHeight,
},
} = state;
return {
labelIDs: labels.map((label: any): number => label.id),
listHeight,
};
}
function LabelsListContainer(props: StateToProps): JSX.Element {
return (
<LabelsListComponent {...props} />
);
}
export default connect(
mapStateToProps,
)(LabelsListContainer);

@ -0,0 +1,226 @@
import React from 'react';
import { connect } from 'react-redux';
import {
CombinedState,
} from 'reducers/interfaces';
import {
collapseObjectItems,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import ObjectStateItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/object-item';
interface OwnProps {
clientID: number;
}
interface StateToProps {
objectState: any;
collapsed: boolean;
labels: any[];
attributes: any[];
jobInstance: any;
frameNumber: number;
}
interface DispatchToProps {
updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const {
annotation: {
annotations: {
states,
collapsed: statesCollapsed,
},
job: {
labels,
attributes: jobAttributes,
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
},
} = state;
const index = states
.map((_state: any): number => _state.clientID)
.indexOf(own.clientID);
const collapsedState = typeof (statesCollapsed[own.clientID]) === 'undefined'
? true : statesCollapsed[own.clientID];
return {
objectState: states[index],
collapsed: collapsedState,
attributes: jobAttributes[states[index].label.id],
labels,
jobInstance,
frameNumber,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
updateState(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state]));
},
collapseOrExpand(objectStates: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(objectStates, collapsed));
},
};
}
type Props = StateToProps & DispatchToProps;
class ObjectItemContainer extends React.PureComponent<Props> {
private lock = (): void => {
const { objectState } = this.props;
objectState.lock = true;
this.commit();
};
private unlock = (): void => {
const { objectState } = this.props;
objectState.lock = false;
this.commit();
};
private show = (): void => {
const { objectState } = this.props;
objectState.hidden = false;
this.commit();
};
private hide = (): void => {
const { objectState } = this.props;
objectState.hidden = true;
this.commit();
};
private setOccluded = (): void => {
const { objectState } = this.props;
objectState.occluded = true;
this.commit();
};
private unsetOccluded = (): void => {
const { objectState } = this.props;
objectState.occluded = false;
this.commit();
};
private setOutside = (): void => {
const { objectState } = this.props;
objectState.outside = true;
this.commit();
};
private unsetOutside = (): void => {
const { objectState } = this.props;
objectState.outside = false;
this.commit();
};
private setKeyframe = (): void => {
const { objectState } = this.props;
objectState.keyframe = true;
this.commit();
};
private unsetKeyframe = (): void => {
const { objectState } = this.props;
objectState.keyframe = false;
this.commit();
};
private collapse = (): void => {
const {
collapseOrExpand,
objectState,
collapsed,
} = this.props;
collapseOrExpand([objectState], !collapsed);
};
private changeLabel = (labelID: string): void => {
const {
objectState,
labels,
} = this.props;
const [label] = labels.filter((_label: any): boolean => _label.id === +labelID);
objectState.label = label;
this.commit();
};
private changeAttribute = (id: number, value: string): void => {
const { objectState } = this.props;
const attr: Record<number, string> = {};
attr[id] = value;
objectState.attributes = attr;
this.commit();
};
private commit(): void {
const {
objectState,
updateState,
jobInstance,
frameNumber,
} = this.props;
updateState(jobInstance, frameNumber, objectState);
}
public render(): JSX.Element {
const {
objectState,
collapsed,
labels,
attributes,
} = this.props;
return (
<ObjectStateItemComponent
objectType={objectState.objectType}
shapeType={objectState.shapeType}
clientID={objectState.clientID}
occluded={objectState.occluded}
outside={objectState.outside}
locked={objectState.lock}
hidden={objectState.hidden}
keyframe={objectState.keyframe}
attrValues={{ ...objectState.attributes }}
labelID={objectState.label.id}
color={objectState.color}
attributes={attributes}
labels={labels}
collapsed={collapsed}
setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside}
unsetOutside={this.unsetOutside}
setKeyframe={this.setKeyframe}
unsetKeyframe={this.unsetKeyframe}
lock={this.lock}
unlock={this.unlock}
hide={this.hide}
show={this.show}
changeLabel={this.changeLabel}
changeAttribute={this.changeAttribute}
collapse={this.collapse}
/>
);
}
}
export default connect<StateToProps, DispatchToProps, OwnProps, CombinedState>(
mapStateToProps,
mapDispatchToProps,
)(ObjectItemContainer);

@ -0,0 +1,246 @@
import React from 'react';
import { connect } from 'react-redux';
import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list';
import {
updateAnnotationsAsync,
collapseObjectItems,
} from 'actions/annotation-actions';
import {
CombinedState,
StatesOrdering,
} from 'reducers/interfaces';
interface StateToProps {
jobInstance: any;
frameNumber: any;
listHeight: number;
statesHidden: boolean;
statesLocked: boolean;
statesCollapsed: boolean;
objectStates: any[];
}
interface DispatchToProps {
onUpdateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
onCollapseStates(states: any[], value: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
states: objectStates,
collapsed,
},
job: {
instance: jobInstance,
},
player: {
frame: {
number: frameNumber,
},
},
tabContentHeight: listHeight,
},
} = state;
let statesHidden = true;
let statesLocked = true;
let statesCollapsed = true;
objectStates.forEach((objectState: any) => {
const { clientID } = objectState;
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
const stateCollapsed = clientID in collapsed ? collapsed[clientID] : true;
statesCollapsed = statesCollapsed && stateCollapsed;
});
return {
listHeight,
statesHidden,
statesLocked,
statesCollapsed,
objectStates,
frameNumber,
jobInstance,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onUpdateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
},
onCollapseStates(states: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(states, collapsed));
},
};
}
function sortAndMap(objectStates: any[], ordering: StatesOrdering): number[] {
let sorted = [];
if (ordering === StatesOrdering.ID_ASCENT) {
sorted = [...objectStates].sort((a: any, b: any): number => a.clientID - b.clientID);
} else if (ordering === StatesOrdering.ID_DESCENT) {
sorted = [...objectStates].sort((a: any, b: any): number => b.clientID - a.clientID);
} else {
sorted = [...objectStates].sort((a: any, b: any): number => b.updated - a.updated);
}
return sorted.map((state: any) => state.clientID);
}
type Props = StateToProps & DispatchToProps;
interface State {
statesOrdering: StatesOrdering;
objectStates: any[];
sortedStatesID: number[];
}
class ObjectsListContainer extends React.Component<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
statesOrdering: StatesOrdering.ID_ASCENT,
objectStates: [],
sortedStatesID: [],
};
}
static getDerivedStateFromProps(props: Props, state: State): State | null {
if (props.objectStates === state.objectStates) {
return null;
}
return {
...state,
objectStates: props.objectStates,
sortedStatesID: sortAndMap(props.objectStates, state.statesOrdering),
};
}
public shouldComponentUpdate(nextProps: Props, nextState: State): boolean {
const {
objectStates,
listHeight,
statesHidden,
statesLocked,
statesCollapsed,
} = this.props;
const { statesOrdering } = this.state;
return nextProps.objectStates.length !== objectStates.length
|| nextProps.listHeight !== listHeight
|| nextProps.statesHidden !== statesHidden
|| nextProps.statesLocked !== statesLocked
|| nextProps.statesCollapsed !== statesCollapsed
|| nextState.statesOrdering !== statesOrdering
|| (statesOrdering === StatesOrdering.UPDATED
? nextProps.objectStates !== objectStates
: nextProps.objectStates.map((nextObjectState: any, id: number): boolean => (
nextObjectState.clientID !== objectStates[id].clientID
)).some((value: boolean) => value)
);
}
private onChangeStatesOrdering = (statesOrdering: StatesOrdering): void => {
const { objectStates } = this.props;
this.setState({
statesOrdering,
sortedStatesID: sortAndMap(objectStates, statesOrdering),
});
};
private onLockAllStates = (): void => {
this.lockAllStates(true);
};
private onUnlockAllStates = (): void => {
this.lockAllStates(false);
};
private onCollapseAllStates = (): void => {
this.collapseAllStates(true);
};
private onExpandAllStates = (): void => {
this.collapseAllStates(false);
};
private onHideAllStates = (): void => {
this.hideAllStates(true);
};
private onShowAllStates = (): void => {
this.hideAllStates(false);
};
private lockAllStates(locked: boolean): void {
const {
objectStates,
onUpdateAnnotations,
jobInstance,
frameNumber,
} = this.props;
for (const objectState of objectStates) {
objectState.lock = locked;
}
onUpdateAnnotations(jobInstance, frameNumber, objectStates);
}
private hideAllStates(hidden: boolean): void {
const {
objectStates,
onUpdateAnnotations,
jobInstance,
frameNumber,
} = this.props;
for (const objectState of objectStates) {
objectState.hidden = hidden;
}
onUpdateAnnotations(jobInstance, frameNumber, objectStates);
}
private collapseAllStates(collapsed: boolean): void {
const {
objectStates,
onCollapseStates,
} = this.props;
onCollapseStates(objectStates, collapsed);
}
public render(): JSX.Element {
const {
sortedStatesID,
statesOrdering,
} = this.state;
return (
<ObjectsListComponent
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
changeStatesOrdering={this.onChangeStatesOrdering}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}
expandAllStates={this.onExpandAllStates}
hideAllStates={this.onHideAllStates}
showAllStates={this.onShowAllStates}
/>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ObjectsListContainer);

@ -0,0 +1,103 @@
import React from 'react';
import { connect } from 'react-redux';
import ObjectsSidebarComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-side-bar';
import { CombinedState } from 'reducers/interfaces';
import {
collapseSidebar as collapseSidebarAction,
collapseAppearance as collapseAppearanceAction,
updateTabContentHeight as updateTabContentHeightAction,
} from 'actions/annotation-actions';
interface StateToProps {
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
}
interface DispatchToProps {
collapseSidebar(): void;
collapseAppearance(): void;
updateTabContentHeight(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
sidebarCollapsed,
appearanceCollapsed,
},
} = state;
return {
sidebarCollapsed,
appearanceCollapsed,
};
}
function computeHeight(): number {
const [sidebar] = window.document.getElementsByClassName('cvat-objects-sidebar');
const [appearance] = window.document.getElementsByClassName('cvat-objects-appearance-collapse');
const [tabs] = Array.from(
window.document.querySelectorAll('.cvat-objects-sidebar-tabs > .ant-tabs-card-bar'),
);
if (sidebar && appearance && tabs) {
const maxHeight = sidebar ? sidebar.clientHeight : 0;
const appearanceHeight = appearance ? appearance.clientHeight : 0;
const tabsHeight = tabs ? tabs.clientHeight : 0;
return maxHeight - appearanceHeight - tabsHeight;
}
return 0;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
collapseSidebar(): void {
dispatch(collapseSidebarAction());
},
collapseAppearance(): void {
dispatch(collapseAppearanceAction());
const [collapser] = window.document
.getElementsByClassName('cvat-objects-appearance-collapse');
if (collapser) {
collapser.addEventListener('transitionend', () => {
dispatch(
updateTabContentHeightAction(
computeHeight(),
),
);
}, { once: true });
}
},
updateTabContentHeight(): void {
dispatch(
updateTabContentHeightAction(
computeHeight(),
),
);
},
};
}
type Props = StateToProps & DispatchToProps;
class ObjectsSideBarContainer extends React.PureComponent<Props> {
public componentDidMount(): void {
const { updateTabContentHeight } = this.props;
updateTabContentHeight();
}
public render(): JSX.Element {
return (
<ObjectsSidebarComponent {...this.props} />
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ObjectsSideBarContainer);

@ -1,35 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from 'cvat-canvas';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { annotation } = state;
return {
canvasInstance: annotation.canvasInstance,
};
}
function StandardWorkspaceContainer(props: StateToProps): JSX.Element {
const {
canvasInstance,
} = props;
return (
<StandardWorkspaceComponent
canvasInstance={canvasInstance}
/>
);
}
export default connect(
mapStateToProps,
)(StandardWorkspaceContainer);

@ -1,9 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
import { SliderValue } from 'antd/lib/slider';
import {
changeFrameAsync,
switchPlay as switchPlayAction,
switchPlay,
saveAnnotationsAsync,
} from 'actions/annotation-actions';
@ -12,53 +14,67 @@ import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
jobInstance: any;
frame: number;
frameNumber: number;
frameStep: number;
playing: boolean;
canvasIsReady: boolean;
saving: boolean;
canvasIsReady: boolean;
savingStatuses: string[];
}
interface DispatchToProps {
onChangeFrame(frame: number, playing: boolean): void;
onChangeFrame(frame: number): void;
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
annotation: {
player: {
playing,
frame: {
number: frameNumber,
},
},
annotations: {
saving: {
uploading: saving,
statuses: savingStatuses,
},
},
job: {
instance: jobInstance,
},
canvas: {
ready: canvasIsReady,
},
},
settings: {
player: {
frameStep,
},
},
} = state;
const {
playing,
saving,
savingStatuses,
canvasIsReady,
frame,
jobInstance,
} = annotation;
return {
frameStep: settings.player.frameStep,
frameStep,
playing,
canvasIsReady,
saving,
savingStatuses,
canvasIsReady,
frame,
frameNumber,
jobInstance,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onChangeFrame(frame: number, playing: boolean): void {
dispatch(changeFrameAsync(frame, playing));
onChangeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
onSwitchPlay(playing: boolean): void {
dispatch(switchPlayAction(playing));
dispatch(switchPlay(playing));
},
onSaveAnnotation(sessionInstance: any): void {
dispatch(saveAnnotationsAsync(sessionInstance));
@ -66,10 +82,231 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
function AnnotationTopBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<AnnotationTopBarComponent {...props} />
);
type Props = StateToProps & DispatchToProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> {
public componentDidUpdate(): void {
const {
jobInstance,
frameNumber,
playing,
canvasIsReady,
onChangeFrame,
onSwitchPlay,
} = this.props;
if (playing && canvasIsReady) {
if (frameNumber < jobInstance.stopFrame) {
setTimeout(() => {
const { playing: stillPlaying } = this.props;
if (stillPlaying) {
onChangeFrame(frameNumber + 1);
}
});
} else {
onSwitchPlay(false);
}
}
}
private onSwitchPlay = (): void => {
const {
frameNumber,
jobInstance,
onSwitchPlay,
playing,
} = this.props;
if (playing) {
onSwitchPlay(false);
} else if (frameNumber < jobInstance.stopFrame) {
onSwitchPlay(true);
}
};
private onFirstFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = jobInstance.startFrame;
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onBackward = (): void => {
const {
onChangeFrame,
frameNumber,
frameStep,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = Math
.max(jobInstance.startFrame, frameNumber - frameStep);
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onPrevFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = Math
.max(jobInstance.startFrame, frameNumber - 1);
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onNextFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = Math
.min(jobInstance.stopFrame, frameNumber + 1);
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onForward = (): void => {
const {
onChangeFrame,
frameNumber,
frameStep,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = Math
.min(jobInstance.stopFrame, frameNumber + frameStep);
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onLastFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
} = this.props;
const newFrame = jobInstance.stopFrame;
if (newFrame !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(newFrame);
}
};
private onSaveAnnotation = (): void => {
const {
onSaveAnnotation,
jobInstance,
} = this.props;
onSaveAnnotation(jobInstance);
};
private onChangePlayerSliderValue = (value: SliderValue): void => {
const {
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(value as number);
};
private onChangePlayerInputValue = (value: number | undefined): void => {
const {
onSwitchPlay,
playing,
onChangeFrame,
} = this.props;
if (typeof (value) !== 'undefined') {
if (playing) {
onSwitchPlay(false);
}
onChangeFrame(value);
}
};
public render(): JSX.Element {
const {
playing,
saving,
savingStatuses,
jobInstance: {
startFrame,
stopFrame,
},
frameNumber,
} = this.props;
return (
<AnnotationTopBarComponent
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
/>
);
}
}
export default connect(

@ -25,7 +25,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { creates } = state.tasks.activities;
return {
...creates,
installedGit: state.plugins.plugins.GIT_INTEGRATION,
installedGit: state.plugins.list.GIT_INTEGRATION,
};
}

@ -16,7 +16,7 @@ interface StateToProps {
installedTFSegmentation: boolean;
installedTFAnnotation: boolean;
username: string;
about: any;
serverAbout: any;
}
interface DispatchToProps {
@ -25,15 +25,17 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState): StateToProps {
const { auth } = state;
const { plugins } = state.plugins;
const { list } = state.plugins;
const { about } = state;
return {
logoutFetching: state.auth.fetching,
installedAnalytics: plugins[SupportedPlugins.ANALYTICS],
installedAutoAnnotation: plugins[SupportedPlugins.AUTO_ANNOTATION],
installedTFSegmentation: plugins[SupportedPlugins.TF_SEGMENTATION],
installedTFAnnotation: plugins[SupportedPlugins.TF_ANNOTATION],
installedAnalytics: list[SupportedPlugins.ANALYTICS],
installedAutoAnnotation: list[SupportedPlugins.AUTO_ANNOTATION],
installedTFSegmentation: list[SupportedPlugins.TF_SEGMENTATION],
installedTFAnnotation: list[SupportedPlugins.TF_ANNOTATION],
username: auth.user.username,
about: state.about.about,
serverAbout: about.server,
};
}

@ -27,13 +27,13 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state.plugins;
const { list } = state.plugins;
const { models } = state;
return {
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
installedTFAnnotation: plugins.TF_ANNOTATION,
installedTFSegmentation: plugins.TF_SEGMENTATION,
installedAutoAnnotation: list.AUTO_ANNOTATION,
installedTFAnnotation: list.TF_ANNOTATION,
installedTFSegmentation: list.TF_SEGMENTATION,
modelsInitialized: models.initialized,
modelsFetching: models.fetching,
models: models.models,

@ -22,11 +22,11 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state.plugins;
const { list } = state.plugins;
return {
registeredUsers: state.users.users,
installedGit: plugins.GIT_INTEGRATION,
installedGit: list.GIT_INTEGRATION,
};
}

@ -25,7 +25,7 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState, own: Props): StateToProps {
const { plugins } = state.plugins;
const { list } = state.plugins;
const { tasks } = state;
const { gettingQuery } = tasks;
const { deletes } = tasks.activities;
@ -47,7 +47,7 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps {
task,
deleteActivity,
fetching: state.tasks.fetching,
installedGit: plugins.GIT_INTEGRATION,
installedGit: list.GIT_INTEGRATION,
};
}

@ -5,9 +5,17 @@ import {
Store,
Reducer,
} from 'redux';
import { createLogger } from 'redux-logger';
const logger = createLogger({
predicate: () => process.env.NODE_ENV === 'development',
collapsed: true,
});
const middlewares = [
thunk,
logger,
];
let store: Store | null = null;

@ -21,45 +21,115 @@ import SVGMainMenuIcon from './assets/main-menu-icon.svg';
import SVGSaveIcon from './assets/save-icon.svg';
import SVGUndoIcon from './assets/undo-icon.svg';
import SVGRedoIcon from './assets/redo-icon.svg';
import SVGPlaycontrolFirstIcon from './assets/playcontrol-first-icon.svg';
import SVGPlaycontrolBackJumpIcon from './assets/playcontrol-back-jump-icon.svg';
import SVGPlaycontrolPreviousIcon from './assets/playcontrol-previous-icon.svg';
import SVGPlaycontrolPlayIcon from './assets/playcontrol-play-icon.svg';
import SVGPlaycontrolPauseIcon from './assets/playcontrol-pause-icon.svg';
import SVGPlaycontrolNextIcon from './assets/playcontrol-next-icon.svg';
import SVGPlaycontrolForwardJumpIcon from './assets/playcontrol-forward-jump-icon.svg';
import SVGPlaycontrolLastIcon from './assets/playcontrol-last-icon.svg';
import SVGFirstIcon from './assets/first-icon.svg';
import SVGBackJumpIcon from './assets/back-jump-icon.svg';
import SVGPreviousIcon from './assets/previous-icon.svg';
import SVGPlayIcon from './assets/play-icon.svg';
import SVGPauseIcon from './assets/pause-icon.svg';
import SVGNextIcon from './assets/next-icon.svg';
import SVGForwardJumpIcon from './assets/forward-jump-icon.svg';
import SVGLastIcon from './assets/last-icon.svg';
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';
export const CVATLogo = (): JSX.Element => <SVGCVATLogo />;
export const AccountIcon = (): JSX.Element => <SVGAccountIcon />;
export const EmptyTasksIcon = (): JSX.Element => <SVGEmptyTasksIcon />;
export const MenuIcon = (): JSX.Element => <SVGMenuIcon />;
export const CursorIcon = (): JSX.Element => <SVGCursorIcon />;
export const MoveIcon = (): JSX.Element => <SVGMoveIcon />;
export const RotateIcon = (): JSX.Element => <SVGRotateIcon />;
export const FitIcon = (): JSX.Element => <SVGFitIcon />;
export const ZoomIcon = (): JSX.Element => <SVGZoomIcon />;
export const RectangleIcon = (): JSX.Element => <SVGRectangleIcon />;
export const PolygonIcon = (): JSX.Element => <SVGPolygonIcon />;
export const PointIcon = (): JSX.Element => <SVGPointIcon />;
export const PolylineIcon = (): JSX.Element => <SVGPolylineIcon />;
export const TagIcon = (): JSX.Element => <SVGTagIcon />;
export const MergeIcon = (): JSX.Element => <SVGMergeIcon />;
export const GroupIcon = (): JSX.Element => <SVGGroupIcon />;
export const SplitIcon = (): JSX.Element => <SVGSplitIcon />;
export const MainMenuIcon = (): JSX.Element => <SVGMainMenuIcon />;
export const SaveIcon = (): JSX.Element => <SVGSaveIcon />;
export const UndoIcon = (): JSX.Element => <SVGUndoIcon />;
export const RedoIcon = (): JSX.Element => <SVGRedoIcon />;
export const PlaycontrolFirstIcon = (): JSX.Element => <SVGPlaycontrolFirstIcon />;
export const PlaycontrolBackJumpIcon = (): JSX.Element => <SVGPlaycontrolBackJumpIcon />;
export const PlaycontrolPreviousIcon = (): JSX.Element => <SVGPlaycontrolPreviousIcon />;
export const PlaycontrolPauseIcon = (): JSX.Element => <SVGPlaycontrolPauseIcon />;
export const PlaycontrolPlayIcon = (): JSX.Element => <SVGPlaycontrolPlayIcon />;
export const PlaycontrolNextIcon = (): JSX.Element => <SVGPlaycontrolNextIcon />;
export const PlaycontrolForwardJumpIcon = (): JSX.Element => <SVGPlaycontrolForwardJumpIcon />;
export const PlaycontrolLastIcon = (): JSX.Element => <SVGPlaycontrolLastIcon />;
export const InfoIcon = (): JSX.Element => <SVGInfoIcon />;
export const FullscreenIcon = (): JSX.Element => <SVGFullscreenIcon />;
export const CVATLogo = React.memo(
(): JSX.Element => <SVGCVATLogo />,
);
export const AccountIcon = React.memo(
(): JSX.Element => <SVGAccountIcon />,
);
export const EmptyTasksIcon = React.memo(
(): JSX.Element => <SVGEmptyTasksIcon />,
);
export const MenuIcon = React.memo(
(): JSX.Element => <SVGMenuIcon />,
);
export const CursorIcon = React.memo(
(): JSX.Element => <SVGCursorIcon />,
);
export const MoveIcon = React.memo(
(): JSX.Element => <SVGMoveIcon />,
);
export const RotateIcon = React.memo(
(): JSX.Element => <SVGRotateIcon />,
);
export const FitIcon = React.memo(
(): JSX.Element => <SVGFitIcon />,
);
export const ZoomIcon = React.memo(
(): JSX.Element => <SVGZoomIcon />,
);
export const RectangleIcon = React.memo(
(): JSX.Element => <SVGRectangleIcon />,
);
export const PolygonIcon = React.memo(
(): JSX.Element => <SVGPolygonIcon />,
);
export const PointIcon = React.memo(
(): JSX.Element => <SVGPointIcon />,
);
export const PolylineIcon = React.memo(
(): JSX.Element => <SVGPolylineIcon />,
);
export const TagIcon = React.memo(
(): JSX.Element => <SVGTagIcon />,
);
export const MergeIcon = React.memo(
(): JSX.Element => <SVGMergeIcon />,
);
export const GroupIcon = React.memo(
(): JSX.Element => <SVGGroupIcon />,
);
export const SplitIcon = React.memo(
(): JSX.Element => <SVGSplitIcon />,
);
export const MainMenuIcon = React.memo(
(): JSX.Element => <SVGMainMenuIcon />,
);
export const SaveIcon = React.memo(
(): JSX.Element => <SVGSaveIcon />,
);
export const UndoIcon = React.memo(
(): JSX.Element => <SVGUndoIcon />,
);
export const RedoIcon = React.memo(
(): JSX.Element => <SVGRedoIcon />,
);
export const FirstIcon = React.memo(
(): JSX.Element => <SVGFirstIcon />,
);
export const BackJumpIcon = React.memo(
(): JSX.Element => <SVGBackJumpIcon />,
);
export const PreviousIcon = React.memo(
(): JSX.Element => <SVGPreviousIcon />,
);
export const PauseIcon = React.memo(
(): JSX.Element => <SVGPauseIcon />,
);
export const PlayIcon = React.memo(
(): JSX.Element => <SVGPlayIcon />,
);
export const NextIcon = React.memo(
(): JSX.Element => <SVGNextIcon />,
);
export const ForwardJumpIcon = React.memo(
(): JSX.Element => <SVGForwardJumpIcon />,
);
export const LastIcon = React.memo(
(): JSX.Element => <SVGLastIcon />,
);
export const InfoIcon = React.memo(
(): JSX.Element => <SVGInfoIcon />,
);
export const FullscreenIcon = React.memo(
(): JSX.Element => <SVGFullscreenIcon />,
);
export const ObjectOutsideIcon = React.memo(
(): JSX.Element => <SVGObjectOutsideIcon />,
);
export const ObjectInsideIcon = React.memo(
(): JSX.Element => <SVGObjectInsideIcon />,
);

@ -69,12 +69,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
aboutFetching: about.fetching,
formatsInitialized: formats.initialized,
formatsFetching: formats.fetching,
installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION,
installedTFSegmentation: plugins.plugins.TF_SEGMENTATION,
installedTFAnnotation: plugins.plugins.TF_ANNOTATION,
notifications: { ...state.notifications },
installedAutoAnnotation: plugins.list.AUTO_ANNOTATION,
installedTFSegmentation: plugins.list.TF_SEGMENTATION,
installedTFAnnotation: plugins.list.TF_ANNOTATION,
notifications: state.notifications,
user: auth.user,
about: state.about,
};
}

@ -5,7 +5,7 @@ import { AuthActionTypes } from '../actions/auth-actions';
import { AboutActionTypes } from '../actions/about-actions';
const defaultState: AboutState = {
about: {},
server: {},
fetching: false,
initialized: false,
};
@ -24,7 +24,7 @@ export default function (state: AboutState = defaultState, action: AnyAction): A
...state,
fetching: false,
initialized: true,
about: action.payload.about,
server: action.payload.server,
};
case AboutActionTypes.GET_ABOUT_FAILED:
return {
@ -38,8 +38,6 @@ export default function (state: AboutState = defaultState, action: AnyAction): A
};
}
default:
return {
...state,
};
return state;
}
}

@ -2,6 +2,7 @@ import { AnyAction } from 'redux';
import { Canvas } from 'cvat-canvas';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import {
AnnotationState,
ActiveControl,
@ -9,137 +10,292 @@ import {
ObjectType,
} from './interfaces';
const defaultState: AnnotationState = {
canvasInstance: new Canvas(),
canvasIsReady: false,
activeControl: ActiveControl.CURSOR,
jobInstance: null,
frame: 0,
playing: false,
annotations: [],
frameData: null,
saving: false,
savingStatuses: [],
dataFetching: false,
jobFetching: false,
canvas: {
instance: new Canvas(),
ready: false,
activeControl: ActiveControl.CURSOR,
},
job: {
instance: null,
labels: [],
attributes: {},
fetching: false,
},
player: {
frame: {
number: 0,
data: null,
fetching: false,
},
playing: false,
},
drawing: {
activeShapeType: ShapeType.RECTANGLE,
activeLabelID: 0,
activeObjectType: ObjectType.SHAPE,
},
annotations: {
saving: {
uploading: false,
statuses: [],
},
collapsed: {},
states: [],
},
colors: [],
sidebarCollapsed: false,
appearanceCollapsed: false,
tabContentHeight: 0,
};
export default (state = defaultState, action: AnyAction): AnnotationState => {
switch (action.type) {
case AnnotationActionTypes.GET_JOB: {
return {
...defaultState,
jobFetching: true,
...state,
job: {
...state.job,
fetching: true,
},
};
}
case AnnotationActionTypes.GET_JOB_SUCCESS: {
const {
jobInstance,
frame,
frameData,
annotations,
job,
states,
frameNumber: number,
colors,
frameData: data,
} = action.payload;
return {
...defaultState,
jobFetching: false,
jobInstance,
frame,
frameData,
annotations,
...state,
job: {
...state.job,
fetching: false,
instance: job,
labels: job.task.labels,
attributes: job.task.labels
.reduce((acc: Record<number, any[]>, label: any): Record<number, any[]> => {
acc[label.id] = label.attributes;
return acc;
}, {}),
},
annotations: {
...state.annotations,
states,
},
player: {
...state.player,
frame: {
...state.player.frame,
number,
data,
},
},
drawing: {
...defaultState.drawing,
activeLabelID: jobInstance.task.labels[0].id,
activeObjectType: jobInstance.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
...state.drawing,
activeLabelID: job.task.labels[0].id,
activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
},
colors,
};
}
case AnnotationActionTypes.GET_JOB_FAILED: {
return {
...state,
jobInstance: undefined,
jobFetching: false,
job: {
...state.job,
instance: undefined,
fetching: false,
},
};
}
case AnnotationActionTypes.CHANGE_FRAME: {
return {
...state,
frameData: null,
annotations: [],
dataFetching: true,
canvasIsReady: false,
player: {
...state.player,
frame: {
...state.player.frame,
fetching: true,
},
},
canvas: {
...state.canvas,
ready: false,
},
};
}
case AnnotationActionTypes.CHANGE_FRAME_SUCCESS: {
const {
number,
data,
states,
} = action.payload;
return {
...state,
frame: action.payload.frame,
annotations: action.payload.annotations,
frameData: action.payload.frameData,
dataFetching: false,
player: {
...state.player,
frame: {
data,
number,
fetching: false,
},
},
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.CHANGE_FRAME_FAILED: {
return {
...state,
dataFetching: false,
}; // add notification if failed
player: {
...state.player,
frame: {
...state.player.frame,
fetching: false,
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS: {
return {
...state,
saving: true,
savingStatuses: [],
annotations: {
...state.annotations,
saving: {
...state.annotations.saving,
uploading: true,
statuses: [],
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS: {
return {
...state,
saving: false,
annotations: {
...state.annotations,
saving: {
...state.annotations.saving,
uploading: false,
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED: {
return {
...state,
saving: false,
}; // add notification if failed
annotations: {
...state.annotations,
saving: {
...state.annotations.saving,
uploading: false,
},
},
};
}
case AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS: {
case AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS: {
const { status } = action.payload;
return {
...state,
savingStatuses: [...state.savingStatuses, action.payload.status],
annotations: {
...state.annotations,
saving: {
...state.annotations.saving,
statuses: [...state.annotations.saving.statuses, status],
},
},
};
}
case AnnotationActionTypes.SWITCH_PLAY: {
const { playing } = action.payload;
return {
...state,
playing: action.payload.playing,
player: {
...state.player,
playing,
},
};
}
case AnnotationActionTypes.COLLAPSE_SIDEBAR: {
return {
...state,
sidebarCollapsed: !state.sidebarCollapsed,
};
}
case AnnotationActionTypes.COLLAPSE_APPEARANCE: {
return {
...state,
appearanceCollapsed: !state.appearanceCollapsed,
};
}
case AnnotationActionTypes.UPDATE_TAB_CONTENT_HEIGHT: {
const { tabContentHeight } = action.payload;
return {
...state,
tabContentHeight,
};
}
case AnnotationActionTypes.COLLAPSE_OBJECT_ITEMS: {
const {
states,
collapsed,
} = action.payload;
const updatedCollapsedStates = { ...state.annotations.collapsed };
for (const objectState of states) {
updatedCollapsedStates[objectState.clientID] = collapsed;
}
return {
...state,
annotations: {
...state.annotations,
collapsed: updatedCollapsedStates,
},
};
}
case AnnotationActionTypes.CONFIRM_CANVAS_READY: {
return {
...state,
canvasIsReady: true,
canvas: {
...state.canvas,
ready: true,
},
};
}
case AnnotationActionTypes.DRAG_CANVAS: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.DRAG_CANVAS : ActiveControl.CURSOR;
return {
...state,
activeControl: enabled ? ActiveControl.DRAG_CANVAS : ActiveControl.CURSOR,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.ZOOM_CANVAS: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.ZOOM_CANVAS : ActiveControl.CURSOR;
return {
...state,
activeControl: enabled ? ActiveControl.ZOOM_CANVAS : ActiveControl.CURSOR,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.DRAW_SHAPE: {
@ -153,7 +309,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
return {
...state,
activeControl,
canvas: {
...state.canvas,
activeControl,
},
drawing: {
activeLabelID: labelID,
activeNumOfPoints: points,
@ -163,48 +322,163 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.MERGE_OBJECTS: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.MERGE : ActiveControl.CURSOR;
return {
...state,
activeControl: ActiveControl.MERGE,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.GROUP_OBJECTS: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.GROUP : ActiveControl.CURSOR;
return {
...state,
activeControl: ActiveControl.GROUP,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.SPLIT_TRACK: {
const { enabled } = action.payload;
const activeControl = enabled
? ActiveControl.SPLIT : ActiveControl.CURSOR;
return {
...state,
activeControl: ActiveControl.SPLIT,
canvas: {
...state.canvas,
activeControl,
},
};
}
case AnnotationActionTypes.OBJECTS_MERGED:
case AnnotationActionTypes.OBJECTS_GROUPPED:
case AnnotationActionTypes.TRACK_SPLITTED:
case AnnotationActionTypes.SHAPE_DRAWN: {
return {
...state,
activeControl: ActiveControl.CURSOR,
canvas: {
...state.canvas,
activeControl: ActiveControl.CURSOR,
},
};
}
case AnnotationActionTypes.ANNOTATIONS_UPDATED: {
case AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS: {
const { states: updatedStates } = action.payload;
const { states: prevStates } = state.annotations;
const nextStates = [...prevStates];
const clientIDs = prevStates.map((prevState: any): number => prevState.clientID);
for (const updatedState of updatedStates) {
const index = clientIDs.indexOf(updatedState.clientID);
if (index !== -1) {
nextStates[index] = updatedState;
}
}
return {
...state,
annotations: action.payload.annotations,
annotations: {
...state.annotations,
states: nextStates,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
case AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED: {
const { states } = action.payload;
return {
...state,
activeControl: ActiveControl.CURSOR,
annotations: {
...state.annotations,
states,
},
};
}
default: {
case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
},
};
}
case AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS: {
const {
label,
} = action.payload;
const { instance: job } = state.job;
const labels = [...job.task.labels];
const index = labels.indexOf(label);
labels[index] = label;
return {
...state,
job: {
...state.job,
labels,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,
canvas: {
...state.canvas,
activeControl: ActiveControl.CURSOR,
},
};
}
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
};
}
default: {
return state;
}
}
};

@ -83,7 +83,7 @@ export enum SupportedPlugins {
export interface PluginsState {
fetching: boolean;
initialized: boolean;
plugins: {
list: {
[name in SupportedPlugins]: boolean;
};
}
@ -95,7 +95,7 @@ export interface UsersState {
}
export interface AboutState {
about: any;
server: any;
fetching: boolean;
initialized: boolean;
}
@ -205,7 +205,15 @@ export interface NotificationsState {
saving: null | ErrorState;
jobFetching: null | ErrorState;
frameFetching: null | ErrorState;
changingLabelColor: null | ErrorState;
updating: null | ErrorState;
creating: null | ErrorState;
merging: null | ErrorState;
grouping: null | ErrorState;
splitting: null | ErrorState;
};
[index: string]: any;
};
messages: {
tasks: {
@ -214,6 +222,8 @@ export interface NotificationsState {
models: {
inferenceDone: string;
};
[index: string]: any;
};
}
@ -243,25 +253,50 @@ export enum ObjectType {
TAG = 'tag',
}
export enum StatesOrdering {
ID_DESCENT = 'ID - descent',
ID_ASCENT = 'ID - ascent',
UPDATED = 'Updated time',
}
export interface AnnotationState {
canvasInstance: Canvas;
canvasIsReady: boolean;
activeControl: ActiveControl;
jobInstance: any | null | undefined;
frameData: any | null;
frame: number;
playing: boolean;
annotations: any[];
saving: boolean;
savingStatuses: string[];
jobFetching: boolean;
dataFetching: boolean;
canvas: {
instance: Canvas;
ready: boolean;
activeControl: ActiveControl;
};
job: {
instance: any | null | undefined;
labels: any[];
attributes: Record<number, any[]>;
fetching: boolean;
};
player: {
frame: {
number: number;
data: any | null;
fetching: boolean;
};
playing: boolean;
};
drawing: {
activeShapeType: ShapeType;
activeNumOfPoints?: number;
activeLabelID: number;
activeObjectType: ObjectType;
};
annotations: {
collapsed: Record<number, boolean>;
states: any[];
saving: {
uploading: boolean;
statuses: string[];
};
};
colors: any[];
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
tabContentHeight: number;
}
export enum GridColor {

@ -113,9 +113,7 @@ export default function (state = defaultState, action: AnyAction): ModelsState {
};
}
default: {
return {
...state,
};
return state;
}
}
}

@ -6,7 +6,7 @@ import { ModelsActionTypes } from 'actions/models-actions';
import { ShareActionTypes } from 'actions/share-actions';
import { TasksActionTypes } from 'actions/tasks-actions';
import { UsersActionTypes } from 'actions/users-actions';
import { AboutActionTypes } from '../actions/about-actions';
import { AboutActionTypes } from 'actions/about-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { NotificationsActionType } from 'actions/notification-actions';
@ -53,6 +53,12 @@ const defaultState: NotificationsState = {
saving: null,
jobFetching: null,
frameFetching: null,
changingLabelColor: null,
updating: null,
creating: null,
merging: null,
grouping: null,
splitting: null,
},
},
messages: {
@ -294,7 +300,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
about: {
...state.errors.about,
fetching: {
message: 'Could not get data from the server',
message: 'Could not get info about the server',
reason: action.payload.error.toString(),
},
},
@ -453,7 +459,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
annotation: {
...state.errors.annotation,
frameFetching: {
message: `Could not receive frame ${action.payload.frame}`,
message: `Could not receive frame ${action.payload.number}`,
reason: action.payload.error.toString(),
},
},
@ -475,6 +481,96 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.CHANGE_LABEL_COLOR_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
changingLabelColor: {
message: 'Could not change label color',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
updating: {
message: 'Could not update annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.CREATE_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
creating: {
message: 'Could not create annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.MERGE_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
merging: {
message: 'Could not merge annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
grouping: {
message: 'Could not group annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.SPLIT_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
splitting: {
message: 'Could not split a track',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,
@ -497,9 +593,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
};
}
default: {
return {
...state,
};
return state;
}
}
}

@ -11,7 +11,7 @@ import {
const defaultState: PluginsState = {
fetching: false,
initialized: false,
plugins: {
list: {
GIT_INTEGRATION: false,
AUTO_ANNOTATION: false,
TF_ANNOTATION: false,
@ -29,9 +29,9 @@ export default function (state = defaultState, action: AnyAction): PluginsState
};
}
case PluginsActionTypes.CHECKED_ALL_PLUGINS: {
const { plugins } = action.payload;
const { list } = action.payload;
if (!state.plugins.GIT_INTEGRATION && plugins.GIT_INTEGRATION) {
if (!state.list.GIT_INTEGRATION && list.GIT_INTEGRATION) {
registerGitPlugin();
}
@ -39,7 +39,7 @@ export default function (state = defaultState, action: AnyAction): PluginsState
...state,
initialized: true,
fetching: false,
plugins,
list,
};
}
case AuthActionTypes.LOGOUT_SUCCESS: {
@ -48,6 +48,6 @@ export default function (state = defaultState, action: AnyAction): PluginsState
};
}
default:
return { ...state };
return state;
}
}

@ -77,9 +77,7 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
};
}
default: {
return {
...state,
};
return state;
}
}
};

@ -49,8 +49,6 @@ export default function (state = defaultState, action: AnyAction): ShareState {
};
}
default:
return {
...state,
};
return state;
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save