React UI: Undo/redo (#1135)

main
Boris Sekachev 6 years ago committed by GitHub
parent fccccabe2b
commit 7e8fc2366a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -32,6 +32,7 @@
} = require('./exceptions');
const {
HistoryActions,
ObjectShape,
ObjectType,
colors,
@ -109,6 +110,7 @@
return labelAccumulator;
}, {});
this.history = data.history;
this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame
this.tracks = [];
@ -124,16 +126,25 @@
collectionZ: this.collectionZ,
groups: this.groups,
frameMeta: this.frameMeta,
history: this.history,
};
}
import(data) {
const result = {
tags: [],
shapes: [],
tracks: [],
};
for (const tag of data.tags) {
const clientID = ++this.count;
const tagModel = new Tag(tag, clientID, this.injection);
this.tags[tagModel.frame] = this.tags[tagModel.frame] || [];
this.tags[tagModel.frame].push(tagModel);
this.objects[clientID] = tagModel;
result.tags.push(tagModel);
}
for (const shape of data.shapes) {
@ -142,6 +153,8 @@
this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || [];
this.shapes[shapeModel.frame].push(shapeModel);
this.objects[clientID] = shapeModel;
result.shapes.push(shapeModel);
}
for (const track of data.tracks) {
@ -152,10 +165,12 @@
if (trackModel) {
this.tracks.push(trackModel);
this.objects[clientID] = trackModel;
result.tracks.push(trackModel);
}
}
return this;
return result;
}
export() {
@ -378,6 +393,18 @@
for (const object of objectsForMerge) {
object.removed = true;
}
this.history.do(HistoryActions.MERGED_OBJECTS, () => {
trackModel.removed = true;
for (const object of objectsForMerge) {
object.removed = false;
}
}, () => {
trackModel.removed = false;
for (const object of objectsForMerge) {
object.removed = true;
}
}, [...objectsForMerge.map((object) => object.clientID), trackModel.clientID]);
}
split(objectState, frame) {
@ -463,6 +490,16 @@
// Remove source object
object.removed = true;
this.history.do(HistoryActions.SPLITTED_TRACK, () => {
object.removed = false;
prevTrack.removed = true;
nextTrack.removed = true;
}, () => {
object.removed = true;
prevTrack.removed = false;
nextTrack.removed = false;
}, [object.clientID, prevTrack.clientID, nextTrack.clientID]);
}
group(objectStates, reset) {
@ -480,9 +517,21 @@
});
const groupIdx = reset ? 0 : ++this.groups.max;
const undoGroups = objectsForGroup.map((object) => object.group);
for (const object of objectsForGroup) {
object.group = groupIdx;
}
const redoGroups = objectsForGroup.map((object) => object.group);
this.history.do(HistoryActions.GROUPED_OBJECTS, () => {
objectsForGroup.forEach((object, idx) => {
object.group = undoGroups[idx];
});
}, () => {
objectsForGroup.forEach((object, idx) => {
object.group = redoGroups[idx];
});
}, objectsForGroup.map((object) => object.clientID));
return groupIdx;
}
@ -704,7 +753,20 @@
}
// Add constructed objects to a collection
this.import(constructed);
const imported = this.import(constructed);
const importedArray = imported.tags
.concat(imported.tracks)
.concat(imported.shapes);
this.history.do(HistoryActions.CREATED_OBJECTS, () => {
importedArray.forEach((object) => {
object.removed = true;
});
}, () => {
importedArray.forEach((object) => {
object.removed = false;
});
}, importedArray.map((object) => object.clientID));
}
select(objectStates, x, y) {

@ -0,0 +1,71 @@
/*
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
const MAX_HISTORY_LENGTH = 128;
class AnnotationHistory {
constructor() {
this.clear();
}
get() {
return {
undo: this._undo.map((undo) => undo.action),
redo: this._redo.map((redo) => redo.action),
};
}
do(action, undo, redo, clientIDs) {
const actionItem = {
clientIDs,
action,
undo,
redo,
};
this._undo = this._undo.slice(-MAX_HISTORY_LENGTH + 1);
this._undo.push(actionItem);
this._redo = [];
}
undo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._undo.pop();
if (action) {
action.undo();
this._redo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
redo(count) {
const affectedObjects = [];
for (let i = 0; i < count; i++) {
const action = this._redo.pop();
if (action) {
action.redo();
this._undo.push(action);
affectedObjects.push(...action.clientIDs);
} else {
break;
}
}
return affectedObjects;
}
clear() {
this._undo = [];
this._redo = [];
}
}
module.exports = AnnotationHistory;

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -17,6 +17,7 @@
ObjectShape,
ObjectType,
AttributeType,
HistoryActions,
} = require('./enums');
const {
@ -139,6 +140,7 @@
class Annotation {
constructor(data, clientID, injection) {
this.taskLabels = injection.labels;
this.history = injection.history;
this.clientID = clientID;
this.serverID = data.id;
this.group = data.group;
@ -156,6 +158,79 @@
injection.groups.max = Math.max(injection.groups.max, this.group);
}
_saveLock(lock) {
const undoLock = this.lock;
const redoLock = lock;
this.history.do(HistoryActions.CHANGED_LOCK, () => {
this.lock = undoLock;
}, () => {
this.lock = redoLock;
}, [this.clientID]);
this.lock = lock;
}
_saveColor(color) {
const undoColor = this.color;
const redoColor = color;
this.history.do(HistoryActions.CHANGED_COLOR, () => {
this.color = undoColor;
}, () => {
this.color = redoColor;
}, [this.clientID]);
this.color = color;
}
_saveHidden(hidden) {
const undoHidden = this.hidden;
const redoHidden = hidden;
this.history.do(HistoryActions.CHANGED_HIDDEN, () => {
this.hidden = undoHidden;
}, () => {
this.hidden = redoHidden;
}, [this.clientID]);
this.hidden = hidden;
}
_saveLabel(label) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = { ...this.attributes };
this.label = label;
this.attributes = {};
this.appendDefaultAttributes(label);
const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes;
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes;
}, [this.clientID]);
}
_saveAttributes(attributes) {
const undoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
this.attributes[attrID] = attributes[attrID];
}
const redoAttributes = { ...this.attributes };
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
this.attributes = undoAttributes;
}, () => {
this.attributes = redoAttributes;
}, [this.clientID]);
}
appendDefaultAttributes(label) {
const labelAttributes = label.attributes;
for (const attribute of labelAttributes) {
@ -178,9 +253,15 @@
delete(force) {
if (!this.lock || force) {
this.removed = true;
this.history.do(HistoryActions.REMOVED_OBJECT, () => {
this.removed = false;
}, () => {
this.removed = true;
}, [this.clientID]);
}
return true;
return this.removed;
}
}
@ -205,9 +286,8 @@
return this.collectionZ[frame];
}
validateStateBeforeSave(frame, data) {
_validateStateBeforeSave(frame, data, updated) {
let fittedPoints = [];
const updated = data.updateFlags;
if (updated.label) {
checkObjectType('label', data.label, null, Label);
@ -255,7 +335,7 @@
}
if (!checkShapeArea(this.shapeType, fittedPoints)) {
fittedPoints = false;
fittedPoints = [];
}
}
@ -393,6 +473,45 @@
};
}
_savePoints(points) {
const undoPoints = this.points;
const redoPoints = points;
this.history.do(HistoryActions.CHANGED_POINTS, () => {
this.points = undoPoints;
}, () => {
this.points = redoPoints;
}, [this.clientID]);
this.points = points;
}
_saveOccluded(occluded) {
const undoOccluded = this.occluded;
const redoOccluded = occluded;
this.history.do(HistoryActions.CHANGED_OCCLUDED, () => {
this.occluded = undoOccluded;
}, () => {
this.occluded = redoOccluded;
}, [this.clientID]);
this.occluded = occluded;
}
_saveZOrder(zOrder) {
const undoZOrder = this.zOrder;
const redoZOrder = zOrder;
this.history.do(HistoryActions.CHANGED_ZORDER, () => {
this.zOrder = undoZOrder;
}, () => {
this.zOrder = redoZOrder;
}, [this.clientID]);
this.zOrder = zOrder;
}
save(frame, data) {
if (frame !== this.frame) {
throw new ScriptingError(
@ -404,44 +523,40 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
const fittedPoints = this.validateStateBeforeSave(frame, data);
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
// Now when all fields are validated, we can apply them
if (updated.label) {
this.label = data.label;
this.attributes = {};
this.appendDefaultAttributes(data.label);
this._saveLabel(data.label);
}
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
this.attributes[attrID] = data.attributes[attrID];
}
this._saveAttributes(data.attributes);
}
if (updated.points && fittedPoints.length) {
this.points = [...fittedPoints];
this._savePoints(fittedPoints);
}
if (updated.occluded) {
this.occluded = data.occluded;
this._saveOccluded(data.occluded);
}
if (updated.zOrder) {
this.zOrder = data.zOrder;
this._saveZOrder(data.zOrder);
}
if (updated.lock) {
this.lock = data.lock;
this._saveLock(data.lock);
}
if (updated.color) {
this.color = data.color;
this._saveColor(data.color);
}
if (updated.hidden) {
this.hidden = data.hidden;
this._saveHidden(data.hidden);
}
this.updateTimestamp(updated);
@ -622,87 +737,304 @@
return result;
}
save(frame, data) {
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
_saveLabel(label) {
const undoLabel = this.label;
const redoLabel = label;
const undoAttributes = {
unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.label = label;
this.attributes = {};
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
}
this.appendDefaultAttributes(label);
const fittedPoints = this.validateStateBeforeSave(frame, data);
const updated = data.updateFlags;
const redoAttributes = {
unmutable: { ...this.attributes },
mutable: Object.keys(this.shapes).map((key) => ({
frame: +key,
attributes: { ...this.shapes[key].attributes },
})),
};
this.history.do(HistoryActions.CHANGED_LABEL, () => {
this.label = undoLabel;
this.attributes = undoAttributes.unmutable;
for (const mutable of undoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
}, () => {
this.label = redoLabel;
this.attributes = redoAttributes.unmutable;
for (const mutable of redoAttributes.mutable) {
this.shapes[mutable.frame].attributes = mutable.attributes;
}
}, [this.clientID]);
}
_saveAttributes(frame, attributes) {
const current = this.get(frame);
const labelAttributes = data.label.attributes
const labelAttributes = this.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
if (updated.label) {
this.label = data.label;
this.attributes = {};
for (const shape of Object.values(this.shapes)) {
shape.attributes = {};
const wasKeyframe = frame in this.shapes;
const undoAttributes = this.attributes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
let mutableAttributesUpdated = false;
const redoAttributes = { ...this.attributes };
for (const attrID of Object.keys(attributes)) {
if (!labelAttributes[attrID].mutable) {
redoAttributes[attrID] = attributes[attrID];
} else if (attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated
// not keyframe yet
|| !(frame in this.shapes)
// keyframe, but without this attrID
|| !(attrID in this.shapes[frame].attributes)
// keyframe with attrID, but with another value
|| (this.shapes[frame].attributes[attrID] !== attributes[attrID]);
}
}
let redoShape;
if (mutableAttributesUpdated) {
if (wasKeyframe) {
redoShape = {
...this.shapes[frame],
attributes: {
...this.shapes[frame].attributes,
},
};
} else {
redoShape = {
frame,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
}
this.appendDefaultAttributes(data.label);
}
let mutableAttributesUpdated = false;
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
if (!labelAttributes[attrID].mutable) {
this.attributes[attrID] = data.attributes[attrID];
this.attributes[attrID] = data.attributes[attrID];
} else if (data.attributes[attrID] !== current.attributes[attrID]) {
mutableAttributesUpdated = mutableAttributesUpdated
// not keyframe yet
|| !(frame in this.shapes)
// keyframe, but without this attrID
|| !(attrID in this.shapes[frame])
// keyframe with attrID, but with another value
|| (this.shapes[frame][attrID] !== data.attributes[attrID]);
}
for (const attrID of Object.keys(attributes)) {
if (labelAttributes[attrID].mutable
&& attributes[attrID] !== current.attributes[attrID]) {
redoShape.attributes[attrID] = attributes[attrID];
}
}
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
this.history.do(HistoryActions.CHANGED_ATTRIBUTES, () => {
this.attributes = undoAttributes;
if (undoShape) {
this.shapes[frame] = undoShape;
} else if (redoShape) {
delete this.shapes[frame];
}
}, () => {
this.attributes = redoAttributes;
if (redoShape) {
this.shapes[frame] = redoShape;
}
}, [this.clientID]);
}
_appendShapeActionToHistory(actionType, frame, undoShape, redoShape) {
this.history.do(actionType, () => {
if (!undoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = undoShape;
}
}, () => {
if (!redoShape) {
delete this.shapes[frame];
} else {
this.shapes[frame] = redoShape;
}
}, [this.clientID]);
}
_savePoints(frame, points) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], points } : {
frame,
points,
zOrder: current.zOrder,
outside: current.outside,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_POINTS,
frame,
undoShape,
redoShape,
);
}
_saveOutside(frame, outside) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], outside } : {
frame,
outside,
zOrder: current.zOrder,
points: current.points,
occluded: current.occluded,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OUTSIDE,
frame,
undoShape,
redoShape,
);
}
_saveOccluded(frame, occluded) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], occluded } : {
frame,
occluded,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_OCCLUDED,
frame,
undoShape,
redoShape,
);
}
_saveZOrder(frame, zOrder) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = wasKeyframe ? { ...this.shapes[frame], zOrder } : {
frame,
zOrder,
occluded: current.occluded,
points: current.points,
outside: current.outside,
attributes: {},
};
this.shapes[frame] = redoShape;
this._appendShapeActionToHistory(
HistoryActions.CHANGED_ZORDER,
frame,
undoShape,
redoShape,
);
}
_saveKeyframe(frame, keyframe) {
const current = this.get(frame);
const wasKeyframe = frame in this.shapes;
if ((keyframe && wasKeyframe) || (!keyframe && !wasKeyframe)) {
return;
}
const undoShape = wasKeyframe ? this.shapes[frame] : undefined;
const redoShape = keyframe ? {
frame,
zOrder: current.zOrder,
points: current.points,
outside: current.outside,
occluded: current.occluded,
attributes: {},
} : undefined;
if (redoShape) {
this.shapes[frame] = redoShape;
} else {
delete this.shapes[frame];
}
this._appendShapeActionToHistory(
HistoryActions.CHANGED_KEYFRAME,
frame,
undoShape,
redoShape,
);
}
save(frame, data) {
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
const fittedPoints = this._validateStateBeforeSave(frame, data, updated);
if (updated.label) {
this._saveLabel(data.label);
}
if (updated.lock) {
this.lock = data.lock;
this._saveLock(data.lock);
}
if (updated.color) {
this.color = data.color;
this._saveColor(data.color);
}
if (updated.hidden) {
this.hidden = data.hidden;
}
if (updated.points || updated.keyframe || updated.outside
|| updated.occluded || updated.zOrder || mutableAttributesUpdated) {
const mutableAttributes = frame in this.shapes ? this.shapes[frame].attributes : {};
this.shapes[frame] = {
frame,
zOrder: data.zOrder,
points: updated.points && fittedPoints.length ? fittedPoints : current.points,
outside: data.outside,
occluded: data.occluded,
attributes: mutableAttributes,
};
this._saveHidden(data.hidden);
}
for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes[attrID].mutable
&& data.attributes[attrID] !== current.attributes[attrID]) {
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
}
}
if (updated.points && fittedPoints.length) {
this._savePoints(frame, fittedPoints);
}
if (updated.keyframe && !data.keyframe) {
if (Object.keys(this.shapes).length === 1) {
throw new DataError('You are not able to remove the latest keyframe for a track. '
+ 'Consider removing a track instead');
} else {
delete this.shapes[frame];
}
}
if (updated.outside) {
this._saveOutside(frame, data.outside);
}
if (updated.occluded) {
this._saveOccluded(frame, data.occluded);
}
if (updated.zOrder) {
this._saveZOrder(frame, data.zOrder);
}
if (updated.attributes) {
this._saveAttributes(frame, data.attributes);
}
if (updated.keyframe) {
this._saveKeyframe(frame, data.keyframe);
}
this.updateTimestamp(updated);
@ -752,14 +1084,6 @@
+ `Interpolation impossible. Client ID: ${this.id}`,
);
}
delete(force) {
if (!this.lock || force) {
this.removed = true;
}
return this.removed;
}
}
class Tag extends Annotation {
@ -810,7 +1134,7 @@
save(frame, data) {
if (frame !== this.frame) {
throw new ScriptingError(
'Got frame is not equal to the frame of the shape',
'Got frame is not equal to the frame of the tag',
);
}
@ -818,59 +1142,20 @@
return objectStateFactory.call(this, frame, this.get(frame));
}
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
const updated = data.updateFlags;
// First validate all the fields
if (updated.label) {
checkObjectType('label', data.label, null, Label);
}
if (updated.attributes) {
const labelAttributes = data.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) {
const value = data.attributes[attrID];
if (attrID in labelAttributes) {
if (!validateAttributeValue(value, labelAttributes[attrID])) {
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}`,
);
}
}
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
}
this._validateStateBeforeSave(frame, data, updated);
// Now when all fields are validated, we can apply them
if (updated.label) {
this.label = data.label;
this.attributes = {};
this.appendDefaultAttributes(data.label);
this._saveLabel(data.label);
}
if (updated.attributes) {
for (const attrID of Object.keys(data.attributes)) {
this.attributes[attrID] = data.attributes[attrID];
}
this._saveAttributes(data.attributes);
}
if (updated.lock) {
this.lock = data.lock;
this._saveLock(data.lock);
}
this.updateTimestamp(updated);

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -11,6 +11,7 @@
const serverProxy = require('./server-proxy');
const Collection = require('./annotations-collection');
const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common');
const { Task } = require('./session');
const {
@ -56,27 +57,35 @@
frameMeta[i] = await session.frames.get(i);
}
const history = new AnnotationsHistory();
const collection = new Collection({
labels: session.labels || session.task.labels,
history,
startFrame,
stopFrame,
frameMeta,
}).import(rawAnnotations);
});
collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
cache.set(session, {
collection,
saver,
history,
});
}
}
async function getAnnotations(session, frame, filter) {
await getAnnotationsFromServer(session);
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.get(frame, filter);
}
await getAnnotationsFromServer(session);
return cache.get(session).collection.get(frame, filter);
}
@ -244,6 +253,58 @@
return result;
}
function undoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.undo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function redoActions(session, count) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.redo(count);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function clearActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.clear();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function getActions(session) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).history.get();
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
module.exports = {
getAnnotations,
putAnnotations,
@ -258,5 +319,9 @@
uploadAnnotations,
dumpAnnotations,
exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
};
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -28,6 +28,7 @@ function build() {
ObjectType,
ObjectShape,
LogType,
HistoryActions,
colors,
} = require('./enums');
@ -498,6 +499,7 @@ function build() {
ObjectType,
ObjectShape,
LogType,
HistoryActions,
colors,
},
/**

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -166,6 +166,45 @@
rotateImage: 26,
};
/**
* Types of actions with annotations
* @enum {string}
* @name HistoryActions
* @memberof module:API.cvat.enums
* @property {string} CHANGED_LABEL Changed label
* @property {string} CHANGED_ATTRIBUTES Changed attributes
* @property {string} CHANGED_POINTS Changed points
* @property {string} CHANGED_OUTSIDE Changed outside
* @property {string} CHANGED_OCCLUDED Changed occluded
* @property {string} CHANGED_ZORDER Changed z-order
* @property {string} CHANGED_LOCK Changed lock
* @property {string} CHANGED_COLOR Changed color
* @property {string} CHANGED_HIDDEN Changed hidden
* @property {string} MERGED_OBJECTS Merged objects
* @property {string} SPLITTED_TRACK Splitted track
* @property {string} GROUPED_OBJECTS Grouped objects
* @property {string} CREATED_OBJECTS Created objects
* @property {string} REMOVED_OBJECT Removed object
* @readonly
*/
const HistoryActions = Object.freeze({
CHANGED_LABEL: 'Changed label',
CHANGED_ATTRIBUTES: 'Changed attributes',
CHANGED_POINTS: 'Changed points',
CHANGED_OUTSIDE: 'Changed outside',
CHANGED_OCCLUDED: 'Changed occluded',
CHANGED_ZORDER: 'Changed z-order',
CHANGED_KEYFRAME: 'Changed keyframe',
CHANGED_LOCK: 'Changed lock',
CHANGED_COLOR: 'Changed color',
CHANGED_HIDDEN: 'Changed hidden',
MERGED_OBJECTS: 'Merged objects',
SPLITTED_TRACK: 'Splitted track',
GROUPED_OBJECTS: 'Grouped objects',
CREATED_OBJECTS: 'Created objects',
REMOVED_OBJECT: 'Removed object',
});
/**
* Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
@ -189,6 +228,7 @@
ObjectType,
ObjectShape,
LogType,
HistoryActions,
colors,
};
})();

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -141,12 +141,12 @@
}),
actions: Object.freeze({
value: {
async undo(count) {
async undo(count = 1) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.undo, count);
return result;
},
async redo(count) {
async redo(count = 1) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.redo, count);
return result;
@ -156,6 +156,11 @@
.apiWrapper.call(this, prototype.actions.clear);
return result;
},
async get() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.actions.get);
return result;
},
},
writable: true,
}),
@ -454,28 +459,48 @@
*/
/**
* Is a dictionary of pairs "id:action" where "id" is an identifier of an object
* which has been affected by undo/redo and "action" is what exactly has been
* done with the object. Action can be: "created", "deleted", "updated".
* Size of an output array equal the param "count".
* @typedef {Object} HistoryAction
* @typedef {Object} HistoryActions
* @property {string[]} [undo] - array of possible actions to undo
* @property {string[]} [redo] - array of possible actions to redo
* @global
*/
/**
* Undo actions
* Make undo
* @method undo
* @memberof Session.actions
* @returns {HistoryAction}
* @param {number} [count=1] number of actions to undo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Redo actions
* Make redo
* @method redo
* @memberof Session.actions
* @returns {HistoryAction}
* @param {number} [count=1] number of actions to redo
* @returns {number[]} Array of affected objects
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Remove all actions from history
* @method clear
* @memberof Session.actions
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Get actions
* @method get
* @memberof Session.actions
* @returns {HistoryActions}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
@ -653,6 +678,13 @@
.annotations.hasUnsavedChanges.bind(this),
};
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
@ -1155,6 +1187,13 @@
.annotations.exportDataset.bind(this),
};
this.actions = {
undo: Object.getPrototypeOf(this).actions.undo.bind(this),
redo: Object.getPrototypeOf(this).actions.redo.bind(this),
clear: Object.getPrototypeOf(this).actions.clear.bind(this),
get: Object.getPrototypeOf(this).actions.get.bind(this),
};
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
@ -1217,6 +1256,10 @@
uploadAnnotations,
dumpAnnotations,
exportDataset,
undoActions,
redoActions,
clearActions,
getActions,
} = require('./annotations');
buildDublicatedAPI(Job.prototype);
@ -1328,6 +1371,31 @@
return result;
};
Job.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this.task, format);
return result;
};
Job.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Job.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Job.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Job.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') {
@ -1484,4 +1552,24 @@
const result = await exportDataset(this, format);
return result;
};
Task.prototype.actions.undo.implementation = function (count) {
const result = undoActions(this, count);
return result;
};
Task.prototype.actions.redo.implementation = function (count) {
const result = redoActions(this, count);
return result;
};
Task.prototype.actions.clear.implementation = function () {
const result = clearActions(this);
return result;
};
Task.prototype.actions.get.implementation = function () {
const result = getActions(this);
return result;
};
})();

@ -266,7 +266,7 @@ describe('Feature: check unsaved changes', () => {
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
const annotations = await task.annotations.get(0);
annotations[0].keyframe = true;
annotations[0].keyframe = false;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);

@ -74,6 +74,64 @@ export enum AnnotationActionTypes {
REMOVE_JOB_ANNOTATIONS_SUCCESS = 'REMOVE_JOB_ANNOTATIONS_SUCCESS',
REMOVE_JOB_ANNOTATIONS_FAILED = 'REMOVE_JOB_ANNOTATIONS_FAILED',
UPDATE_CANVAS_CONTEXT_MENU = 'UPDATE_CANVAS_CONTEXT_MENU',
UNDO_ACTION_SUCCESS = 'UNDO_ACTION_SUCCESS',
UNDO_ACTION_FAILED = 'UNDO_ACTION_FAILED',
REDO_ACTION_SUCCESS = 'REDO_ACTION_SUCCESS',
REDO_ACTION_FAILED = 'REDO_ACTION_FAILED',
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
// TODO: use affected IDs as an optimization
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
payload: {
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
// TODO: use affected IDs as an optimization
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
payload: {
history,
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.REDO_ACTION_FAILED,
payload: {
error,
},
});
}
};
}
export function updateCanvasContextMenu(visible: boolean, left: number, top: number): AnyAction {
@ -91,11 +149,14 @@ export function removeAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
sessionInstance.annotations.clear();
await sessionInstance.annotations.clear();
await sessionInstance.actions.clear();
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS,
payload: {
sessionInstance,
history,
},
});
} catch (error) {
@ -109,7 +170,6 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
export function uploadJobAnnotationsAsync(job: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
@ -146,12 +206,15 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
await job.annotations.clear(true);
await job.actions.clear();
const history = await job.actions.get();
const states = await job.annotations.get(frame);
setTimeout(() => {
dispatch({
type: AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS,
payload: {
history,
job,
states,
},
@ -264,11 +327,13 @@ export function propagateObjectAsync(
}
await sessionInstance.annotations.put(states);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS,
payload: {
objectState,
history,
},
});
} catch (error) {
@ -300,16 +365,19 @@ export function changePropagateFrames(frames: number): AnyAction {
};
}
export function removeObjectAsync(objectState: any, force: boolean):
export function removeObjectAsync(sessionInstance: any, objectState: any, force: boolean):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const removed = await objectState.delete(force);
const history = await sessionInstance.actions.get();
if (removed) {
dispatch({
type: AnnotationActionTypes.REMOVE_OBJECT_SUCCESS,
payload: {
objectState,
history,
},
});
} else {
@ -647,11 +715,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
const promises = statesToUpdate.map((state: any): Promise<any> => state.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
},
});
} catch (error) {
@ -673,11 +743,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
await sessionInstance.annotations.put(statesToCreate);
const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
},
});
} catch (error) {
@ -697,11 +769,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
await sessionInstance.annotations.merge(statesToMerge);
const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
},
});
} catch (error) {
@ -721,11 +795,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
await sessionInstance.annotations.group(statesToGroup);
const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
},
});
} catch (error) {
@ -745,11 +821,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
try {
await sessionInstance.annotations.split(stateToSplit, frame);
const states = await sessionInstance.annotations.get(frame);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS,
payload: {
states,
history,
},
});
} catch (error) {
@ -774,11 +852,13 @@ export function changeLabelColorAsync(
const updatedLabel = label;
updatedLabel.color = color;
const states = await sessionInstance.annotations.get(frameNumber);
const history = await sessionInstance.actions.get();
dispatch({
type: AnnotationActionTypes.CHANGE_LABEL_COLOR_SUCCESS,
payload: {
label: updatedLabel,
history,
states,
},
});

@ -86,7 +86,7 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element {
isDrawing={activeControl === ActiveControl.DRAW_POINTS}
/>
<Tooltip overlay='Setup a tag' placement='right'>
<Tooltip title='Setup a tag' placement='right'>
<Icon component={TagIcon} style={{ pointerEvents: 'none', opacity: 0.5 }} />
</Tooltip>

@ -29,7 +29,7 @@ function CursorControl(props: Props): JSX.Element {
} = props;
return (
<Tooltip overlay='Cursor' placement='right'>
<Tooltip title='Cursor' placement='right'>
<Icon
component={CursorIcon}
className={activeControl === ActiveControl.CURSOR

@ -23,7 +23,7 @@ function FitControl(props: Props): JSX.Element {
} = props;
return (
<Tooltip overlay='Fit the image' placement='right'>
<Tooltip title='Fit the image' placement='right'>
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip>
);

@ -42,7 +42,7 @@ function GroupControl(props: Props): JSX.Element {
};
return (
<Tooltip overlay='Group shapes/tracks' placement='right'>
<Tooltip title='Group shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip>
);

@ -42,7 +42,7 @@ function MergeControl(props: Props): JSX.Element {
};
return (
<Tooltip overlay='Merge shapes/tracks' placement='right'>
<Tooltip title='Merge shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip>
);

@ -29,7 +29,7 @@ function MoveControl(props: Props): JSX.Element {
} = props;
return (
<Tooltip overlay='Move the image' placement='right'>
<Tooltip title='Move the image' placement='right'>
<Icon
component={MoveIcon}
className={activeControl === ActiveControl.DRAG_CANVAS

@ -29,7 +29,7 @@ function ResizeControl(props: Props): JSX.Element {
} = props;
return (
<Tooltip overlay='Select a region of interest' placement='right'>
<Tooltip title='Select a region of interest' placement='right'>
<Icon
component={ZoomIcon}
className={activeControl === ActiveControl.ZOOM_CANVAS

@ -32,7 +32,7 @@ function RotateControl(props: Props): JSX.Element {
placement='right'
content={(
<>
<Tooltip overlay='Rotate the image anticlockwise' placement='topRight'>
<Tooltip title='Rotate the image anticlockwise' placement='topRight'>
<Icon
className='cvat-rotate-canvas-controls-left'
onClick={(): void => canvasInstance
@ -40,7 +40,7 @@ function RotateControl(props: Props): JSX.Element {
component={RotateIcon}
/>
</Tooltip>
<Tooltip overlay='Rotate the image clockwise' placement='topRight'>
<Tooltip title='Rotate the image clockwise' placement='topRight'>
<Icon
className='cvat-rotate-canvas-controls-right'
onClick={(): void => canvasInstance

@ -42,7 +42,7 @@ function SplitControl(props: Props): JSX.Element {
};
return (
<Tooltip overlay='Split a track' placement='right'>
<Tooltip title='Split a track' placement='right'>
<Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip>
);

@ -21,14 +21,22 @@ import {
interface Props {
saving: boolean;
savingStatuses: string[];
undoAction?: string;
redoAction?: string;
onSaveAnnotation(): void;
onUndoClick(): void;
onRedoClick(): void;
}
function LeftGroup(props: Props): JSX.Element {
const {
saving,
savingStatuses,
undoAction,
redoAction,
onSaveAnnotation,
onUndoClick,
onRedoClick,
} = props;
return (
@ -67,11 +75,25 @@ function LeftGroup(props: Props): JSX.Element {
</Timeline>
</Modal>
</Button>
<Button disabled type='link' className='cvat-annotation-header-button'>
<Button
title={undoAction}
disabled={!undoAction}
style={{ pointerEvents: undoAction ? 'initial' : 'none', opacity: undoAction ? 1 : 0.5 }}
type='link'
className='cvat-annotation-header-button'
onClick={onUndoClick}
>
<Icon component={UndoIcon} />
Undo
<span>Undo</span>
</Button>
<Button disabled type='link' className='cvat-annotation-header-button'>
<Button
title={redoAction}
disabled={!redoAction}
style={{ pointerEvents: redoAction ? 'initial' : 'none', opacity: redoAction ? 1 : 0.5 }}
type='link'
className='cvat-annotation-header-button'
onClick={onRedoClick}
>
<Icon component={RedoIcon} />
Redo
</Button>

@ -42,19 +42,19 @@ function PlayerButtons(props: Props): JSX.Element {
return (
<Col className='cvat-player-buttons'>
<Tooltip overlay='Go to the first frame'>
<Tooltip title='Go to the first frame'>
<Icon component={FirstIcon} onClick={onFirstFrame} />
</Tooltip>
<Tooltip overlay='Go back with a step'>
<Tooltip title='Go back with a step'>
<Icon component={BackJumpIcon} onClick={onBackward} />
</Tooltip>
<Tooltip overlay='Go back'>
<Tooltip title='Go back'>
<Icon component={PreviousIcon} onClick={onPrevFrame} />
</Tooltip>
{!playing
? (
<Tooltip overlay='Play'>
<Tooltip title='Play'>
<Icon
component={PlayIcon}
onClick={onSwitchPlay}
@ -62,7 +62,7 @@ function PlayerButtons(props: Props): JSX.Element {
</Tooltip>
)
: (
<Tooltip overlay='Pause'>
<Tooltip title='Pause'>
<Icon
component={PauseIcon}
onClick={onSwitchPlay}
@ -71,13 +71,13 @@ function PlayerButtons(props: Props): JSX.Element {
)
}
<Tooltip overlay='Go next'>
<Tooltip title='Go next'>
<Icon component={NextIcon} onClick={onNextFrame} />
</Tooltip>
<Tooltip overlay='Go next with a step'>
<Tooltip title='Go next with a step'>
<Icon component={ForwardJumpIcon} onClick={onForward} />
</Tooltip>
<Tooltip overlay='Go to the last frame'>
<Tooltip title='Go to the last frame'>
<Icon component={LastIcon} onClick={onLastFrame} />
</Tooltip>
</Col>

@ -44,7 +44,7 @@ function PlayerNavigation(props: Props): JSX.Element {
</Row>
<Row type='flex' justify='space-around'>
<Col className='cvat-player-filename-wrapper'>
<Tooltip overlay='filename.png'>
<Tooltip title='filename.png'>
<Text type='secondary'>filename.png</Text>
</Tooltip>
</Col>

@ -90,7 +90,7 @@ export default function StatisticsModalComponent(props: Props): JSX.Element {
});
const makeShapesTracksTitle = (title: string): JSX.Element => (
<Tooltip overlay='Shapes / Tracks'>
<Tooltip title='Shapes / Tracks'>
<Text strong style={{ marginRight: 5 }}>{title}</Text>
<Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip>

@ -20,6 +20,8 @@ interface Props {
frameNumber: number;
startFrame: number;
stopFrame: number;
undoAction?: string;
redoAction?: string;
showStatistics(): void;
onSwitchPlay(): void;
onSaveAnnotation(): void;
@ -31,21 +33,16 @@ interface Props {
onLastFrame(): void;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
}
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;
onUndoClick(): void;
onRedoClick(): void;
}
function AnnotationTopBarComponent(props: Props): JSX.Element {
const {
saving,
savingStatuses,
undoAction,
redoAction,
playing,
frameNumber,
startFrame,
@ -61,6 +58,8 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
onLastFrame,
onSliderChange,
onInputChange,
onUndoClick,
onRedoClick,
} = props;
return (
@ -70,6 +69,10 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
saving={saving}
savingStatuses={savingStatuses}
onSaveAnnotation={onSaveAnnotation}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={onUndoClick}
onRedoClick={onRedoClick}
/>
<Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'>
@ -98,4 +101,4 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
);
}
export default React.memo(AnnotationTopBarComponent, propsAreEqual);
export default React.memo(AnnotationTopBarComponent);

@ -107,7 +107,7 @@ export default class CreateModelContent extends React.PureComponent<Props> {
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}>
<Tooltip overlay='Click to open guide'>
<Tooltip title='Click to open guide'>
<Icon
onClick={(): void => {
// false positive

@ -59,7 +59,7 @@ export class CreateModelForm extends React.PureComponent<Props> {
</Col>
<Col span={8} offset={2}>
<Form.Item>
<Tooltip overlay='Will this model be availabe for everyone?'>
<Tooltip title='Will this model be availabe for everyone?'>
{ getFieldDecorator('global', {
initialValue: false,
valuePropName: 'checked',

@ -85,7 +85,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return (
<Form.Item label={<span>Image quality</span>}>
<Tooltip overlay='Defines image compression level'>
<Tooltip title='Defines image compression level'>
{form.getFieldDecorator('imageQuality', {
initialValue: 70,
rules: [{
@ -111,7 +111,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return (
<Form.Item label={<span>Overlap size</span>}>
<Tooltip overlay='Defines a number of intersected frames between different segments'>
<Tooltip title='Defines a number of intersected frames between different segments'>
{form.getFieldDecorator('overlapSize')(
<Input size='large' type='number' />,
)}
@ -125,7 +125,7 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
return (
<Form.Item label={<span>Segment size</span>}>
<Tooltip overlay='Defines a number of frames in a segment'>
<Tooltip title='Defines a number of frames in a segment'>
{form.getFieldDecorator('segmentSize')(
<Input size='large' type='number' />,
)}

@ -138,7 +138,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return (
<Col span={4}>
<Form.Item>
<Tooltip overlay='An HTML element representing the attribute'>
<Tooltip title='An HTML element representing the attribute'>
{ form.getFieldDecorator(`type[${key}]`, {
initialValue: type,
})(
@ -188,7 +188,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
};
return (
<Tooltip overlay='Press enter to add a new value'>
<Tooltip title='Press enter to add a new value'>
<Form.Item>
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: existedValues,
@ -215,7 +215,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
const { form } = this.props;
return (
<Tooltip overlay='Specify a default value'>
<Tooltip title='Specify a default value'>
<Form.Item>
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
@ -299,7 +299,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return (
<Form.Item>
<Tooltip overlay='Can this attribute be changed frame to frame?'>
<Tooltip title='Can this attribute be changed frame to frame?'>
{ form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value,
valuePropName: 'checked',
@ -316,7 +316,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return (
<Form.Item>
<Tooltip overlay='Delete the attribute'>
<Tooltip title='Delete the attribute'>
<Button
type='link'
className='cvat-delete-attribute-button'
@ -417,7 +417,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
private renderDoneButton(): JSX.Element {
return (
<Col>
<Tooltip overlay='Save the label and return'>
<Tooltip title='Save the label and return'>
<Button
style={{ width: '150px' }}
type='primary'
@ -440,7 +440,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
label ? <div />
: (
<Col offset={1}>
<Tooltip overlay='Save the label and create one more'>
<Tooltip title='Save the label and create one more'>
<Button
style={{ width: '150px' }}
type='primary'
@ -462,7 +462,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
return (
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Tooltip title='Do not save the label and return'>
<Button
style={{ width: '150px' }}
type='danger'

@ -236,7 +236,7 @@ export default class LabelsEditor
tabBarStyle={{ marginBottom: '0px' }}
tabBarExtraContent={(
<>
<Tooltip overlay='Copied to clipboard!' trigger='click'>
<Tooltip title='Copied to clipboard!' trigger='click'>
<Button
type='link'
icon='copy'

@ -76,7 +76,7 @@ class RawViewer extends React.PureComponent<Props> {
</Form.Item>
<Row type='flex' justify='start' align='middle'>
<Col>
<Tooltip overlay='Save labels and return'>
<Tooltip title='Save labels and return'>
<Button
style={{ width: '150px' }}
type='primary'
@ -87,7 +87,7 @@ class RawViewer extends React.PureComponent<Props> {
</Tooltip>
</Col>
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Tooltip title='Do not save the label and return'>
<Button
style={{ width: '150px' }}
type='danger'

@ -186,7 +186,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
<Tag color={colors[modelLabel]}>{taskLabel}</Tag>
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Remove the mapped values'>
<Tooltip title='Remove the mapped values'>
<Icon
className='cvat-run-model-dialog-remove-mapping-icon'
type='close-circle'
@ -288,7 +288,7 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
)}
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'>
<Tooltip title='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-info-circle-icon' type='question-circle' />
</Tooltip>
</Col>

@ -39,7 +39,7 @@ interface DispatchToProps {
updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void;
removeObject: (objectState: any) => void;
removeObject: (sessionInstance: any, objectState: any) => void;
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
}
@ -109,8 +109,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID));
},
removeObject(objectState: any): void {
dispatch(removeObjectAsync(objectState, true));
removeObject(sessionInstance: any, objectState: any): void {
dispatch(removeObjectAsync(sessionInstance, objectState, true));
},
copyShape(objectState: any): void {
dispatch(copyShapeAction(objectState));
@ -197,9 +197,10 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const {
objectState,
removeObject,
jobInstance,
} = this.props;
removeObject(objectState);
removeObject(jobInstance, objectState);
};
private activate = (): void => {

@ -9,6 +9,8 @@ import {
saveAnnotationsAsync,
collectStatisticsAsync,
showStatistics as showStatisticsAction,
undoActionAsync,
redoActionAsync,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
@ -22,6 +24,8 @@ interface StateToProps {
saving: boolean;
canvasIsReady: boolean;
savingStatuses: string[];
undoAction?: string;
redoAction?: string;
}
interface DispatchToProps {
@ -29,6 +33,8 @@ interface DispatchToProps {
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -45,6 +51,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
uploading: saving,
statuses: savingStatuses,
},
history,
},
job: {
instance: jobInstance,
@ -68,6 +75,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
savingStatuses,
frameNumber,
jobInstance,
undoAction: history.undo[history.undo.length - 1],
redoAction: history.redo[history.redo.length - 1],
};
}
@ -86,6 +95,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true));
},
undo(sessionInstance: any, frameNumber: any): void {
dispatch(undoActionAsync(sessionInstance, frameNumber));
},
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
};
}
@ -115,6 +130,26 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
}
}
private undo = (): void => {
const {
undo,
jobInstance,
frameNumber,
} = this.props;
undo(jobInstance, frameNumber);
};
private redo = (): void => {
const {
redo,
jobInstance,
frameNumber,
} = this.props;
redo(jobInstance, frameNumber);
};
private showStatistics = (): void => {
const {
jobInstance,
@ -300,6 +335,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
stopFrame,
},
frameNumber,
undoAction,
redoAction,
} = this.props;
return (
@ -321,6 +358,10 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={this.undo}
onRedoClick={this.redo}
/>
);
}

@ -53,6 +53,10 @@ const defaultState: AnnotationState = {
},
collapsed: {},
states: [],
history: {
undo: [],
redo: [],
},
},
propagate: {
objectState: null,
@ -421,7 +425,10 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.UPDATE_ANNOTATIONS_SUCCESS: {
const { states: updatedStates } = action.payload;
const {
history,
states: updatedStates,
} = action.payload;
const { states: prevStates } = state.annotations;
const nextStates = [...prevStates];
@ -438,6 +445,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: {
...state.annotations,
states: nextStates,
history,
},
};
}
@ -452,46 +460,62 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
const {
states,
history,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
history,
},
};
}
case AnnotationActionTypes.MERGE_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
const {
states,
history,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
history,
},
};
}
case AnnotationActionTypes.GROUP_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
const {
states,
history,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
history,
},
};
}
case AnnotationActionTypes.SPLIT_ANNOTATIONS_SUCCESS: {
const { states } = action.payload;
const {
states,
history,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
states,
history,
},
};
}
@ -499,6 +523,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
const {
label,
states,
history,
} = action.payload;
const { instance: job } = state.job;
@ -515,6 +540,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: {
...state.annotations,
states,
history,
},
};
}
@ -547,12 +573,14 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
case AnnotationActionTypes.REMOVE_OBJECT_SUCCESS: {
const {
objectState,
history,
} = action.payload;
return {
...state,
annotations: {
...state.annotations,
history,
activatedStateID: null,
states: state.annotations.states
.filter((_objectState: any) => (
@ -617,8 +645,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.PROPAGATE_OBJECT_SUCCESS: {
const { history } = action.payload;
return {
...state,
annotations: {
...state.annotations,
history,
},
propagate: {
...state.propagate,
objectState: null,
@ -739,7 +772,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.UPLOAD_JOB_ANNOTATIONS_SUCCESS: {
const { states, job } = action.payload;
const {
states,
job,
history,
} = action.payload;
const { loads } = state.activities;
delete loads[job.id];
@ -754,6 +791,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
annotations: {
...state.annotations,
history,
states,
selectedStatesID: [],
activatedStateID: null,
@ -762,10 +800,12 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.REMOVE_JOB_ANNOTATIONS_SUCCESS: {
const { history } = action.payload;
return {
...state,
annotations: {
...state.annotations,
history,
selectedStatesID: [],
activatedStateID: null,
collapsed: {},
@ -793,6 +833,27 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.REDO_ACTION_SUCCESS:
case AnnotationActionTypes.UNDO_ACTION_SUCCESS: {
const {
history,
states,
} = action.payload;
const activatedStateID = states
.map((_state: any) => _state.clientID).includes(state.annotations.activatedStateID)
? state.annotations.activatedStateID : null;
return {
...state,
annotations: {
...state.annotations,
activatedStateID,
states,
history,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,

@ -312,6 +312,10 @@ export interface AnnotationState {
activatedStateID: number | null;
collapsed: Record<number, boolean>;
states: any[];
history: {
undo: string[];
redo: string[];
};
saving: {
uploading: boolean;
statuses: string[];

@ -8,7 +8,7 @@
},
"dependencies": {},
"devDependencies": {
"eslint": "^6.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",

Loading…
Cancel
Save