React UI: Objects filtering & search (#1155)

* Initial filter function

* Updated method for filtering

* Updated documentation

* Added annotations filter file

* Updated some comments

* Added filter to UI

* Implemented search alorithm

* Removed extra code

* Fixed typos

* Added frame URL

* Object URL

* Removed extra encoding/decoding
main
Boris Sekachev 6 years ago committed by GitHub
parent 538da9fe0f
commit 228b813160
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -830,6 +830,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
occluded: state.occluded,
hidden: state.hidden,
lock: state.lock,
shapeType: state.shapeType,
points: [...state.points],
attributes: { ...state.attributes },
};
@ -963,16 +964,16 @@ export class CanvasViewImpl implements CanvasView, Listener {
private deactivate(): void {
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
const shape = this.svgShapes[state.clientID];
const drawnState = this.drawnStates[clientID];
const shape = this.svgShapes[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') {
if (drawnState.shapeType !== 'points') {
this.selectize(false, shape);
}
@ -982,10 +983,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
(shape as any).resize(false);
// TODO: Hide text only if it is hidden by settings
const text = this.svgTexts[state.clientID];
const text = this.svgTexts[clientID];
if (text) {
text.remove();
delete this.svgTexts[state.clientID];
delete this.svgTexts[clientID];
}
this.activeElement = {

@ -38,6 +38,7 @@
"form-data": "^2.5.0",
"jest-config": "^24.8.0",
"js-cookie": "^2.2.0",
"jsonpath": "^1.0.2",
"platform": "^1.3.5",
"store": "^2.0.12"
}

@ -1,5 +1,5 @@
/*
* Copyright (C) 2019 Intel Corporation
* Copyright (C) 2019-2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
@ -22,6 +22,7 @@
Tag,
objectStateFactory,
} = require('./annotations-objects');
const AnnotationsFilter = require('./annotations-filter');
const { checkObjectType } = require('./common');
const Statistics = require('./statistics');
const { Label } = require('./labels');
@ -110,6 +111,7 @@
return labelAccumulator;
}, {});
this.annotationsFilter = new AnnotationsFilter();
this.history = data.history;
this.shapes = {}; // key is a frame
this.tags = {}; // key is a frame
@ -193,25 +195,46 @@
return data;
}
get(frame) {
get(frame, allTracks, filters) {
const { tracks } = this;
const shapes = this.shapes[frame] || [];
const tags = this.tags[frame] || [];
const objects = tracks.concat(shapes).concat(tags).filter((object) => !object.removed);
// filtering here
const objects = [].concat(tracks, shapes, tags);
const visible = {
models: [],
data: [],
};
const objectStates = [];
for (const object of objects) {
if (object.removed) {
continue;
}
const stateData = object.get(frame);
if (stateData.outside && !stateData.keyframe) {
if (!allTracks && stateData.outside && !stateData.keyframe) {
continue;
}
const objectState = objectStateFactory.call(object, frame, stateData);
objectStates.push(objectState);
visible.models.push(object);
visible.data.push(stateData);
}
const [, query] = this.annotationsFilter.toJSONQuery(filters);
let filtered = [];
if (filters.length) {
filtered = this.annotationsFilter.filter(visible.data, query);
}
const objectStates = [];
visible.data.forEach((stateData, idx) => {
if (!filters.length || filtered.includes(stateData.clientID)) {
const model = visible.models[idx];
const objectState = objectStateFactory.call(model, frame, stateData);
objectStates.push(objectState);
}
});
return objectStates;
}
@ -799,6 +822,106 @@
distance: minimumDistance,
};
}
search(filters, frameFrom, frameTo) {
const [groups, query] = this.annotationsFilter.toJSONQuery(filters);
const sign = Math.sign(frameTo - frameFrom);
const flattenedQuery = groups.flat(Number.MAX_SAFE_INTEGER);
const containsDifficultProperties = flattenedQuery
.some((fragment) => fragment
.match(/^width/) || fragment.match(/^height/));
const deepSearch = (deepSearchFrom, deepSearchTo) => {
// deepSearchFrom is expected to be a frame that doesn't satisfy a filter
// deepSearchTo is expected to be a frame that satifies a filter
let [prev, next] = [deepSearchFrom, deepSearchTo];
// half division method instead of linear search
while (!(Math.abs(prev - next) === 1)) {
const middle = next + Math.floor((prev - next) / 2);
const shapesData = this.tracks.map((track) => track.get(middle));
const filtered = this.annotationsFilter.filter(shapesData, query);
if (filtered.length) {
next = middle;
} else {
prev = middle;
}
}
return next;
};
const keyframesMemory = {};
const predicate = sign > 0
? (frame) => frame <= frameTo
: (frame) => frame >= frameTo;
const update = sign > 0
? (frame) => frame + 1
: (frame) => frame - 1;
for (let frame = frameFrom; predicate(frame); frame = update(frame)) {
// First prepare all data for the frame
// Consider all shapes, tags, and tracks that have keyframe here
// In particular consider first and last frame as keyframes for all frames
const statesData = [].concat(
(frame in this.shapes ? this.shapes[frame] : [])
.map((shape) => shape.get(frame)),
(frame in this.tags ? this.tags[frame] : [])
.map((tag) => tag.get(frame)),
);
const tracks = Object.values(this.tracks)
.filter((track) => (
frame in track.shapes
|| frame === frameFrom
|| frame === frameTo
));
statesData.push(...tracks.map((track) => track.get(frame)));
// Nothing to filtering, go to the next iteration
if (!statesData.length) {
continue;
}
// Filtering
const filtered = this.annotationsFilter.filter(statesData, query);
// Now we are checking whether we need deep search or not
// Deep search is needed in some difficult cases
// For example when filter contains fields which
// can be changed between keyframes (like: height and width of a shape)
// It's expected, that a track doesn't satisfy a filter on the previous keyframe
// At the same time it sutisfies the filter on the next keyframe
let withDeepSearch = false;
if (containsDifficultProperties) {
for (const track of tracks) {
const trackIsSatisfy = filtered.includes(track.clientID);
if (!trackIsSatisfy) {
keyframesMemory[track.clientID] = [
filtered.includes(track.clientID),
frame,
];
} else if (keyframesMemory[track.clientID]
&& keyframesMemory[track.clientID][0] === false) {
withDeepSearch = true;
}
}
}
if (withDeepSearch) {
const reducer = sign > 0 ? Math.min : Math.max;
const deepSearchFrom = reducer(
...Object.values(keyframesMemory).map((value) => value[1]),
);
return deepSearch(deepSearchFrom, frame);
}
if (filtered.length) {
return frame;
}
}
return null;
}
}
module.exports = Collection;

@ -0,0 +1,236 @@
/*
* Copyright (C) 2020 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
const jsonpath = require('jsonpath');
const { AttributeType } = require('./enums');
const { ArgumentError } = require('./exceptions');
class AnnotationsFilter {
constructor() {
// eslint-disable-next-line security/detect-unsafe-regex
this.operatorRegex = /(==|!=|<=|>=|>|<|~=)(?=(?:[^"]*(["])[^"]*\2)*[^"]*$)/g;
}
// Method splits expression by operators that are outside of any brackets
_splitWithOperator(container, expression) {
const operators = ['|', '&'];
const splitted = [];
let nestedCounter = 0;
let isQuotes = false;
let start = -1;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
// We don't split with operator inside brackets
// It will be done later in recursive call
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
}
if (operators.includes(expression[i])) {
if (!nestedCounter) {
const subexpression = expression
.substr(start + 1, i - start - 1).trim();
splitted.push(subexpression);
splitted.push(expression[i]);
start = i;
}
}
}
const subexpression = expression
.substr(start + 1).trim();
splitted.push(subexpression);
splitted.forEach((internalExpression) => {
if (internalExpression === '|' || internalExpression === '&') {
container.push(internalExpression);
} else {
this._groupByBrackets(
container,
internalExpression,
);
}
});
}
// Method groups bracket containings to nested arrays of container
_groupByBrackets(container, expression) {
if (!(expression.startsWith('(') && expression.endsWith(')'))) {
container.push(expression);
}
let nestedCounter = 0;
let startBracket = null;
let endBracket = null;
let isQuotes = false;
for (let i = 0; i < expression.length; i++) {
if (expression[i] === '"') {
// all quotes inside other quotes must
// be escaped by a user and changed to ` above
isQuotes = !isQuotes;
}
if (!isQuotes && expression[i] === '(') {
nestedCounter++;
if (startBracket === null) {
startBracket = i;
}
}
if (!isQuotes && expression[i] === ')') {
nestedCounter--;
if (!nestedCounter) {
endBracket = i;
const subcontainer = [];
const subexpression = expression
.substr(startBracket + 1, endBracket - 1 - startBracket);
this._splitWithOperator(
subcontainer,
subexpression,
);
container.push(subcontainer);
startBracket = null;
endBracket = null;
}
}
}
if (startBracket !== null) {
throw Error('Extra opening bracket found');
}
if (endBracket !== null) {
throw Error('Extra closing bracket found');
}
}
_parse(expression) {
const groups = [];
this._splitWithOperator(groups, expression);
}
_join(groups) {
let expression = '';
for (const group of groups) {
if (Array.isArray(group)) {
expression += `(${this._join(group)})`;
} else if (typeof (group) === 'string') {
// it can be operator or expression
if (group === '|' || group === '&') {
expression += group;
} else {
let [field, operator, , value] = group.split(this.operatorRegex);
field = `@.${field.trim()}`;
operator = operator.trim();
value = value.trim();
if (value === 'width' || value === 'height' || value.startsWith('attr')) {
value = `@.${value}`;
}
expression += [field, operator, value].join('');
}
}
}
return expression;
}
_convertObjects(statesData) {
const objects = statesData.map((state) => {
const labelAttributes = state.label.attributes
.reduce((acc, attr) => {
acc[attr.id] = attr;
return acc;
}, {});
let xtl = Number.MAX_SAFE_INTEGER;
let xbr = Number.MIN_SAFE_INTEGER;
let ytl = Number.MAX_SAFE_INTEGER;
let ybr = Number.MIN_SAFE_INTEGER;
state.points.forEach((coord, idx) => {
if (idx % 2) { // y
ytl = Math.min(ytl, coord);
ybr = Math.max(ybr, coord);
} else { // x
xtl = Math.min(xtl, coord);
xbr = Math.max(xbr, coord);
}
});
const [width, height] = [xbr - xtl, ybr - ytl];
const attributes = {};
Object.keys(state.attributes).reduce((acc, key) => {
const attr = labelAttributes[key];
let value = state.attributes[key].replace(/\\"/g, '`');
if (attr.inputType === AttributeType.NUMBER) {
value = +value;
} else if (attr.inputType === AttributeType.CHECKBOX) {
value = value === 'true';
}
acc[attr.name] = value;
return acc;
}, attributes);
return {
width,
height,
attr: attributes,
label: state.label.name.replace(/\\"/g, '`'),
serverID: state.serverID,
clientID: state.clientID,
type: state.objectType,
shape: state.objectShape,
occluded: state.occluded,
};
});
return {
objects,
};
}
toJSONQuery(filters) {
try {
if (!filters.length) {
return [[], '$.objects[*].clientID'];
}
const groups = [];
const expression = filters.map((filter) => `(${filter})`).join('|').replace(/\\"/g, '`');
this._splitWithOperator(groups, expression);
return [groups, `$.objects[?(${this._join(groups)})].clientID`];
} catch (error) {
throw new ArgumentError(`Wrong filter expression. ${error.toString()}`);
}
}
filter(statesData, query) {
try {
const objects = this._convertObjects(statesData);
return jsonpath.query(objects, query);
} catch (error) {
throw new ArgumentError(`Could not apply the filter. ${error.toString()}`);
}
}
}
module.exports = AnnotationsFilter;

@ -77,16 +77,16 @@
}
}
async function getAnnotations(session, frame, filter) {
async function getAnnotations(session, frame, allTracks, filters) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.get(frame, filter);
return cache.get(session).collection.get(frame, allTracks, filters);
}
await getAnnotationsFromServer(session);
return cache.get(session).collection.get(frame, filter);
return cache.get(session).collection.get(frame, allTracks, filters);
}
async function saveAnnotations(session, onUpdate) {
@ -100,6 +100,19 @@
// If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it
}
function searchAnnotations(session, filters, frameFrom, frameTo) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
return cache.get(session).collection.search(filters, frameFrom, frameTo);
}
throw new DataError(
'Collection has not been initialized yet. Call annotations.get() or annotations.clear(true) before',
);
}
function mergeAnnotations(session, objectStates) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
@ -311,6 +324,7 @@
saveAnnotations,
hasUnsavedChanges,
mergeAnnotations,
searchAnnotations,
splitAnnotations,
groupAnnotations,
clearAnnotations,

@ -9,7 +9,6 @@
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');

@ -56,16 +56,17 @@
return result;
},
async get(frame, filter = {}) {
async get(frame, allTracks = false, filters = []) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.get, frame, filter);
.apiWrapper.call(this, prototype.annotations.get,
frame, allTracks, filters);
return result;
},
async search(filter, frameFrom, frameTo) {
async search(filters, frameFrom, frameTo) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.search,
filter, frameFrom, frameTo);
filters, frameFrom, frameTo);
return result;
},
@ -273,24 +274,34 @@
* @instance
* @async
*/
/**
* @typedef {Object} ObjectFilter
* @property {string} [label] a name of a label
* @property {module:API.cvat.enums.ObjectType} [type]
* @property {module:API.cvat.enums.ObjectShape} [shape]
* @property {boolean} [occluded] a value of occluded property
* @property {boolean} [lock] a value of lock property
* @property {number} [width] a width of a shape
* @property {number} [height] a height of a shape
* @property {Object[]} [attributes] dictionary with "name: value" pairs
* @global
*/
/**
* Get annotations for a specific frame
* </br> Filter supports following operators:
* ==, !=, >, >=, <, <=, ~= and (), |, & for grouping.
* </br> Filter supports properties:
* width, height, label, serverID, clientID, type, shape, occluded
* </br> All prop values are case-sensitive. CVAT uses json queries for search.
* </br> Examples:
* <ul>
* <li> label=="car" | label==["road sign"] </li>
* <li> width >= height </li>
* <li> attr["Attribute 1"] == attr["Attribute 2"] </li>
* <li> type=="track" & shape="rectangle" </li>
* <li> clientID == 50 </li>
* <li> (label=="car" & attr["parked"]==true)
* | (label=="pedestrian" & width > 150) </li>
* <li> (( label==["car \\"mazda\\""]) &
* (attr["sunglass ( help ) es"]==true |
* (width > 150 | height > 150 & (clientID == serverID))))) </li>
* </ul>
* <b> If you have double quotes in your query string,
* please escape them using back slash: \" </b>
* @method get
* @param {integer} frame get objects from the frame
* @param {ObjectFilter[]} [filter = []]
* get only objects are satisfied to specific filter
* @param {boolean} allTracks show all tracks
* even if they are outside and not keyframe
* @param {string[]} [filters = []]
* get only objects that satisfied to specific filters
* @returns {module:API.cvat.classes.ObjectState[]}
* @memberof Session.annotations
* @throws {module:API.cvat.exceptions.PluginError}
@ -299,13 +310,14 @@
* @async
*/
/**
* Find frame which contains at least one object satisfied to a filter
* Find a frame in the range [from, to]
* that contains at least one object satisfied to a filter
* @method search
* @memberof Session.annotations
* @param {ObjectFilter} [filter = []] filter
* @param {integer} from lower bound of a search
* @param {integer} to upper bound of a search
* @returns {integer} the nearest frame which contains filtered objects
* @returns {integer|null} a frame that contains objects according to the filter
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
@ -671,6 +683,7 @@
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
@ -1178,6 +1191,7 @@
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
clear: Object.getPrototypeOf(this).annotations.clear.bind(this),
search: Object.getPrototypeOf(this).annotations.search.bind(this),
upload: Object.getPrototypeOf(this).annotations.upload.bind(this),
select: Object.getPrototypeOf(this).annotations.select.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
@ -1247,6 +1261,7 @@
putAnnotations,
saveAnnotations,
hasUnsavedChanges,
searchAnnotations,
mergeAnnotations,
splitAnnotations,
groupAnnotations,
@ -1305,17 +1320,58 @@
};
// TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, filter) {
Job.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frame)) {
throw new ArgumentError(
'The frame argument must be an integer',
);
}
if (frame < this.startFrame || frame > this.stopFrame) {
throw new ArgumentError(
`Frame ${frame} does not exist in the job`,
);
}
const annotationsData = await getAnnotations(this, frame, filter);
const annotationsData = await getAnnotations(this, frame, allTracks, filters);
return annotationsData;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < this.startFrame || frameFrom > this.stopFrame) {
throw new ArgumentError(
'The start frame is out of the job',
);
}
if (frameTo < this.startFrame || frameTo > this.stopFrame) {
throw new ArgumentError(
'The stop frame is out of the job',
);
}
const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result;
};
Job.prototype.annotations.save.implementation = async function (onUpdate) {
const result = await saveAnnotations(this, onUpdate);
return result;
@ -1476,7 +1532,13 @@
};
// TODO: Check filter for annotations
Task.prototype.annotations.get.implementation = async function (frame, filter) {
Task.prototype.annotations.get.implementation = async function (frame, allTracks, filters) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(
`Frame must be a positive integer. Got: "${frame}"`,
@ -1489,7 +1551,36 @@
);
}
const result = await getAnnotations(this, frame, filter);
const result = await getAnnotations(this, frame, allTracks, filters);
return result;
};
Job.prototype.annotations.search.implementation = async function (filters, frameFrom, frameTo) {
if (!Array.isArray(filters) || filters.some((filter) => typeof (filter) !== 'string')) {
throw new ArgumentError(
'The filters argument must be an array of strings',
);
}
if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) {
throw new ArgumentError(
'The start and end frames both must be an integer',
);
}
if (frameFrom < 0 || frameFrom >= this.size) {
throw new ArgumentError(
'The start frame is out of the task',
);
}
if (frameTo < 0 || frameTo >= this.size) {
throw new ArgumentError(
'The stop frame is out of the task',
);
}
const result = searchAnnotations(this, filters, frameFrom, frameTo);
return result;
};

@ -1,4 +1,9 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import {
AnyAction,
Dispatch,
ActionCreator,
Store,
} from 'redux';
import { ThunkAction } from 'redux-thunk';
import {
@ -13,6 +18,29 @@ import getCore from 'cvat-core';
import { getCVATStore } from 'cvat-store';
const cvat = getCore();
let store: null | Store<CombinedState> = null;
function getStore(): Store<CombinedState> {
if (store === null) {
store = getCVATStore();
}
return store;
}
function receiveAnnotationsParameters(): { filters: string[]; frame: number } {
if (store === null) {
store = getCVATStore();
}
const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
return {
filters,
frame,
};
}
export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
@ -78,16 +106,53 @@ export enum AnnotationActionTypes {
UNDO_ACTION_FAILED = 'UNDO_ACTION_FAILED',
REDO_ACTION_SUCCESS = 'REDO_ACTION_SUCCESS',
REDO_ACTION_FAILED = 'REDO_ACTION_FAILED',
CHANGE_ANNOTATIONS_FILTERS = 'CHANGE_ANNOTATIONS_FILTERS',
FETCH_ANNOTATIONS_SUCCESS = 'FETCH_ANNOTATIONS_SUCCESS',
FETCH_ANNOTATIONS_FAILED = 'FETCH_ANNOTATIONS_FAILED',
}
export function fetchAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters);
dispatch({
type: AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS,
payload: {
states,
},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.FETCH_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function changeAnnotationsFilters(filters: string[]): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS,
payload: {
filters,
},
};
}
export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
@ -111,10 +176,12 @@ export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
@ -174,8 +241,9 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
const state: CombinedState = getStore().getState();
const { filters } = receiveAnnotationsParameters();
if (state.tasks.activities.loads[job.task.id]) {
throw Error('Annotations is being uploaded for the task');
}
@ -208,7 +276,7 @@ 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);
const states = await job.annotations.get(frame, false, filters);
setTimeout(() => {
dispatch({
@ -475,10 +543,9 @@ export function switchPlay(playing: boolean): AnyAction {
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 state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job;
const { number: frame } = state.annotation.player.frame;
const { filters, frame } = receiveAnnotationsParameters();
try {
if (toFrame < job.startFrame || toFrame > job.stopFrame) {
@ -505,7 +572,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame);
const states = await job.annotations.get(toFrame, false, filters);
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
@ -558,8 +625,12 @@ export function confirmCanvasReady(): AnyAction {
};
}
export function getJobAsync(tid: number, jid: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function getJobAsync(
tid: number,
jid: number,
initialFrame: number,
initialFilters: string[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
@ -567,8 +638,8 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
try {
const store = getCVATStore();
const state: CombinedState = store.getState();
const state: CombinedState = getStore().getState();
const filters = initialFilters;
// First check state if the task is already there
let task = state.tasks.current
@ -587,9 +658,9 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
}
const frameNumber = Math.max(0, job.startFrame);
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber);
const states = await job.annotations.get(frameNumber);
const states = await job.annotations.get(frameNumber, false, filters);
const colors = [...cvat.enums.colors];
dispatch({
@ -600,6 +671,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
frameNumber,
frameData,
colors,
filters,
},
});
} catch (error) {
@ -713,7 +785,8 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const promises = statesToUpdate.map((state: any): Promise<any> => state.save());
const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
@ -725,7 +798,8 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const states = await sessionInstance.annotations.get(frame);
const { filters } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
payload: {
@ -741,8 +815,9 @@ export function createAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
await sessionInstance.annotations.put(statesToCreate);
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -767,8 +842,9 @@ export function mergeAnnotationsAsync(sessionInstance: any, frame: number, state
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
await sessionInstance.annotations.merge(statesToMerge);
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -793,8 +869,9 @@ export function groupAnnotationsAsync(sessionInstance: any, frame: number, state
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
await sessionInstance.annotations.group(statesToGroup);
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -818,9 +895,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { filters } = receiveAnnotationsParameters();
try {
await sessionInstance.annotations.split(stateToSplit, frame);
const states = await sessionInstance.annotations.get(frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -849,9 +927,10 @@ export function changeLabelColorAsync(
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const updatedLabel = label;
updatedLabel.color = color;
const states = await sessionInstance.annotations.get(frameNumber);
const states = await sessionInstance.annotations.get(frameNumber, false, filters);
const history = await sessionInstance.actions.get();
dispatch({

@ -33,13 +33,20 @@ import {
} from 'reducers/interfaces';
function ItemMenu(
serverID: number | undefined,
locked: boolean,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
createURL: (() => void),
): JSX.Element {
return (
<Menu key='unique' className='cvat-object-item-menu'>
<Menu.Item>
<Button disabled={serverID === undefined} type='link' icon='link' onClick={createURL}>
Create object URL
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' icon='copy' onClick={copy}>
Make a copy
@ -77,6 +84,7 @@ function ItemMenu(
interface ItemTopComponentProps {
clientID: number;
serverID: number | undefined;
labelID: number;
labels: any[];
type: string;
@ -85,11 +93,13 @@ interface ItemTopComponentProps {
copy(): void;
remove(): void;
propagate(): void;
createURL(): void;
}
function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
const {
clientID,
serverID,
labelID,
labels,
type,
@ -98,6 +108,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
copy,
remove,
propagate,
createURL,
} = props;
return (
@ -119,7 +130,7 @@ function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
<Col span={2}>
<Dropdown
placement='bottomLeft'
overlay={ItemMenu(locked, copy, remove, propagate)}
overlay={ItemMenu(serverID, locked, copy, remove, propagate, createURL)}
>
<Icon type='more' />
</Dropdown>
@ -495,6 +506,7 @@ interface Props {
objectType: ObjectType;
shapeType: ShapeType;
clientID: number;
serverID: number | undefined;
labelID: number;
occluded: boolean;
outside: boolean | undefined;
@ -515,6 +527,7 @@ interface Props {
activate(): void;
copy(): void;
propagate(): void;
createURL(): void;
remove(): void;
setOccluded(): void;
unsetOccluded(): void;
@ -541,6 +554,7 @@ function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
&& nextProps.labelID === prevProps.labelID
&& nextProps.color === prevProps.color
&& nextProps.clientID === prevProps.clientID
&& nextProps.serverID === prevProps.serverID
&& nextProps.objectType === prevProps.objectType
&& nextProps.shapeType === prevProps.shapeType
&& nextProps.collapsed === prevProps.collapsed
@ -559,6 +573,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
objectType,
shapeType,
clientID,
serverID,
occluded,
outside,
locked,
@ -579,6 +594,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
activate,
copy,
propagate,
createURL,
remove,
setOccluded,
unsetOccluded,
@ -609,6 +625,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
style={{ borderLeftStyle: 'solid', borderColor: ` ${color}` }}
>
<ItemTop
serverID={serverID}
clientID={clientID}
labelID={labelID}
labels={labels}
@ -618,6 +635,7 @@ function ObjectItemComponent(props: Props): JSX.Element {
copy={copy}
remove={remove}
propagate={propagate}
createURL={createURL}
/>
<ItemButtons
objectType={objectType}

@ -4,11 +4,11 @@ import {
Row,
Col,
Icon,
Input,
Select,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { SelectValue } from 'antd/lib/select';
import { StatesOrdering } from 'reducers/interfaces';
@ -58,7 +58,9 @@ interface Props {
statesLocked: boolean;
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
annotationsFilters: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
@ -69,6 +71,7 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element {
const {
annotationsFilters,
statesHidden,
statesLocked,
statesCollapsed,
@ -80,15 +83,26 @@ function ObjectListHeader(props: Props): JSX.Element {
expandAllStates,
hideAllStates,
showAllStates,
changeAnnotationsFilters,
} = props;
return (
<div className='cvat-objects-sidebar-states-header'>
<Row>
<Col>
<Input
placeholder='Filter e.g. car[attr/model="mazda"]'
prefix={<Icon type='filter' />}
<Select
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
dropdownStyle={{ display: 'none' }}
onChange={changeAnnotationsFilters}
/>
</Col>
</Row>

@ -1,7 +1,7 @@
import React from 'react';
import { SelectValue } from 'antd/lib/select';
import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import ObjectListHeader from './objects-list-header';
@ -13,7 +13,9 @@ interface Props {
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
sortedStatesID: number[];
annotationsFilters: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
@ -30,7 +32,9 @@ function ObjectListComponent(props: Props): JSX.Element {
statesCollapsed,
statesOrdering,
sortedStatesID,
annotationsFilters,
changeStatesOrdering,
changeAnnotationsFilters,
lockAllStates,
unlockAllStates,
collapseAllStates,
@ -46,7 +50,9 @@ function ObjectListComponent(props: Props): JSX.Element {
statesLocked={statesLocked}
statesCollapsed={statesCollapsed}
statesOrdering={statesOrdering}
annotationsFilters={annotationsFilters}
changeStatesOrdering={changeStatesOrdering}
changeAnnotationsFilters={changeAnnotationsFilters}
lockAllStates={lockAllStates}
unlockAllStates={unlockAllStates}
collapseAllStates={collapseAllStates}

@ -63,13 +63,17 @@
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(1) > div:nth-child(1) {
height: 32px;
> .ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}
}

@ -116,6 +116,19 @@
user-select: none;
}
.cvat-player-frame-url-icon {
opacity: 0.7;
color: $info-icon-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
.cvat-player-frame-selector {
width: 5em;
padding-right: 5px;

@ -3,6 +3,7 @@ import React from 'react';
import {
Row,
Col,
Icon,
Slider,
Tooltip,
InputNumber,
@ -17,6 +18,7 @@ interface Props {
frameNumber: number;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
onURLIconClick(): void;
}
function PlayerNavigation(props: Props): JSX.Element {
@ -26,6 +28,7 @@ function PlayerNavigation(props: Props): JSX.Element {
frameNumber,
onSliderChange,
onInputChange,
onURLIconClick,
} = props;
return (
@ -42,12 +45,17 @@ function PlayerNavigation(props: Props): JSX.Element {
/>
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Row type='flex' justify='center'>
<Col className='cvat-player-filename-wrapper'>
<Tooltip title='filename.png'>
<Text type='secondary'>filename.png</Text>
</Tooltip>
</Col>
<Col offset={1}>
<Tooltip title='Create frame URL'>
<Icon className='cvat-player-frame-url-icon' type='link' onClick={onURLIconClick} />
</Tooltip>
</Col>
</Row>
</Col>
<Col>

@ -33,6 +33,7 @@ interface Props {
onLastFrame(): void;
onSliderChange(value: SliderValue): void;
onInputChange(value: number | undefined): void;
onURLIconClick(): void;
onUndoClick(): void;
onRedoClick(): void;
}
@ -58,6 +59,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
onLastFrame,
onSliderChange,
onInputChange,
onURLIconClick,
onUndoClick,
onRedoClick,
} = props;
@ -92,6 +94,7 @@ function AnnotationTopBarComponent(props: Props): JSX.Element {
frameNumber={frameNumber}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
/>
</Row>
</Col>

@ -44,10 +44,32 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
const { params } = own.match;
const taskID = +params.tid;
const jobID = +params.jid;
const searchParams = new URLSearchParams(window.location.search);
const initialFilters: string[] = [];
let initialFrame = 0;
if (searchParams.has('frame')) {
const searchFrame = +(searchParams.get('frame') as string);
if (!Number.isNaN(searchFrame)) {
initialFrame = searchFrame;
}
}
if (searchParams.has('object')) {
const searchObject = +(searchParams.get('object') as string);
if (!Number.isNaN(searchObject)) {
initialFilters.push(`serverID==${searchObject}`);
}
}
if (searchParams.has('frame') || searchParams.has('object')) {
own.history.replace(own.history.location.state);
}
return {
getJob(): void {
dispatch(getJobAsync(taskID, jobID));
dispatch(getJobAsync(taskID, jobID, initialFrame, initialFilters));
},
};
}

@ -1,4 +1,5 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import {
ActiveControl,
@ -203,6 +204,22 @@ class ObjectItemContainer extends React.PureComponent<Props> {
removeObject(jobInstance, objectState);
};
private createURL = (): void => {
const {
objectState,
frameNumber,
} = this.props;
const {
origin,
pathname,
} = window.location;
const search = `frame=${frameNumber}&object=${objectState.serverID}`;
const url = `${origin}${pathname}?${search}`;
copy(url);
};
private activate = (): void => {
const {
activateObject,
@ -354,6 +371,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
objectType={objectState.objectType}
shapeType={objectState.shapeType}
clientID={objectState.clientID}
serverID={objectState.serverID}
occluded={objectState.occluded}
outside={objectState.outside}
locked={objectState.lock}
@ -385,6 +403,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
remove={this.remove}
copy={this.copy}
propagate={this.propagate}
createURL={this.createURL}
setOccluded={this.setOccluded}
unsetOccluded={this.unsetOccluded}
setOutside={this.setOutside}

@ -1,9 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import { SelectValue } from 'antd/lib/select';
import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list';
import {
updateAnnotationsAsync,
fetchAnnotationsAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems,
} from 'actions/annotation-actions';
@ -20,11 +24,13 @@ interface StateToProps {
statesLocked: boolean;
statesCollapsed: boolean;
objectStates: any[];
annotationsFilters: string[];
}
interface DispatchToProps {
onUpdateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
onCollapseStates(states: any[], value: boolean): void;
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
collapseStates(states: any[], value: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -32,6 +38,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotation: {
annotations: {
states: objectStates,
filters: annotationsFilters,
collapsed,
},
job: {
@ -66,17 +73,25 @@ function mapStateToProps(state: CombinedState): StateToProps {
objectStates,
frameNumber,
jobInstance,
annotationsFilters,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onUpdateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
},
onCollapseStates(states: any[], collapsed: boolean): void {
collapseStates(states: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(states, collapsed));
},
changeAnnotationsFilters(
sessionInstance: any,
filters: string[],
): void {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
};
}
@ -101,7 +116,7 @@ interface State {
sortedStatesID: number[];
}
class ObjectsListContainer extends React.Component<Props, State> {
class ObjectsListContainer extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
@ -123,31 +138,6 @@ class ObjectsListContainer extends React.Component<Props, State> {
};
}
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({
@ -156,6 +146,15 @@ class ObjectsListContainer extends React.Component<Props, State> {
});
};
private onChangeAnnotationsFilters = (value: SelectValue): void => {
const {
jobInstance,
changeAnnotationsFilters,
} = this.props;
const filters = value as string[];
changeAnnotationsFilters(jobInstance, filters);
};
private onLockAllStates = (): void => {
this.lockAllStates(true);
};
@ -183,7 +182,7 @@ class ObjectsListContainer extends React.Component<Props, State> {
private lockAllStates(locked: boolean): void {
const {
objectStates,
onUpdateAnnotations,
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
@ -191,13 +190,13 @@ class ObjectsListContainer extends React.Component<Props, State> {
objectState.lock = locked;
}
onUpdateAnnotations(jobInstance, frameNumber, objectStates);
updateAnnotations(jobInstance, frameNumber, objectStates);
}
private hideAllStates(hidden: boolean): void {
const {
objectStates,
onUpdateAnnotations,
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
@ -205,19 +204,20 @@ class ObjectsListContainer extends React.Component<Props, State> {
objectState.hidden = hidden;
}
onUpdateAnnotations(jobInstance, frameNumber, objectStates);
updateAnnotations(jobInstance, frameNumber, objectStates);
}
private collapseAllStates(collapsed: boolean): void {
const {
objectStates,
onCollapseStates,
collapseStates,
} = this.props;
onCollapseStates(objectStates, collapsed);
collapseStates(objectStates, collapsed);
}
public render(): JSX.Element {
const { annotationsFilters } = this.props;
const {
sortedStatesID,
statesOrdering,
@ -228,7 +228,9 @@ class ObjectsListContainer extends React.Component<Props, State> {
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}

@ -1,4 +1,5 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import { SliderValue } from 'antd/lib/slider';
@ -325,6 +326,16 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
}
};
private onURLIconClick = (): void => {
const { frameNumber } = this.props;
const {
origin,
pathname,
} = window.location;
const url = `${origin}${pathname}?frame=${frameNumber}`;
copy(url);
};
public render(): JSX.Element {
const {
playing,
@ -352,6 +363,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
onLastFrame={this.onLastFrame}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}

@ -53,6 +53,7 @@ const defaultState: AnnotationState = {
},
collapsed: {},
states: [],
filters: [],
history: {
undo: [],
redo: [],
@ -90,6 +91,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
states,
frameNumber: number,
colors,
filters,
frameData: data,
} = action.payload;
@ -109,6 +111,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: {
...state.annotations,
states,
filters,
},
player: {
...state.player,
@ -854,6 +857,31 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.FETCH_ANNOTATIONS_SUCCESS: {
const { 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,
},
};
}
case AnnotationActionTypes.CHANGE_ANNOTATIONS_FILTERS: {
const { filters } = action.payload;
return {
...state,
annotations: {
...state.annotations,
filters,
},
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,

@ -209,6 +209,9 @@ export interface NotificationsState {
savingJob: null | ErrorState;
uploadAnnotations: null | ErrorState;
removeAnnotations: null | ErrorState;
fetchingAnnotations: null | ErrorState;
undo: null | ErrorState;
redo: null | ErrorState;
};
[index: string]: any;
@ -312,6 +315,7 @@ export interface AnnotationState {
activatedStateID: number | null;
collapsed: Record<number, boolean>;
states: any[];
filters: string[];
history: {
undo: string[];
redo: string[];

@ -65,6 +65,9 @@ const defaultState: NotificationsState = {
savingJob: null,
uploadAnnotations: null,
removeAnnotations: null,
fetchingAnnotations: null,
undo: null,
redo: null,
},
},
messages: {
@ -680,6 +683,51 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.FETCH_ANNOTATIONS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
fetchingAnnotations: {
message: 'Could not fetch annotations',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.REDO_ACTION_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
redo: {
message: 'Could not redo',
reason: action.payload.error.toString(),
},
},
},
};
}
case AnnotationActionTypes.UNDO_ACTION_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
undo: {
message: 'Could not undo',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

Loading…
Cancel
Save