diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index 40b43e42ef1..fdabf19936c 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -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 = {
diff --git a/cvat-core/package.json b/cvat-core/package.json
index 8e2088f614c..558f437c1bc 100644
--- a/cvat-core/package.json
+++ b/cvat-core/package.json
@@ -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"
}
diff --git a/cvat-core/src/annotations-collection.js b/cvat-core/src/annotations-collection.js
index 560a79b9371..dc312ea20b1 100644
--- a/cvat-core/src/annotations-collection.js
+++ b/cvat-core/src/annotations-collection.js
@@ -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;
diff --git a/cvat-core/src/annotations-filter.js b/cvat-core/src/annotations-filter.js
new file mode 100644
index 00000000000..0684ceb3342
--- /dev/null
+++ b/cvat-core/src/annotations-filter.js
@@ -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;
diff --git a/cvat-core/src/annotations.js b/cvat-core/src/annotations.js
index 516a8280b84..3ee70d3cb48 100644
--- a/cvat-core/src/annotations.js
+++ b/cvat-core/src/annotations.js
@@ -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,
diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js
index 80290179276..4b7e2872719 100644
--- a/cvat-core/src/api-implementation.js
+++ b/cvat-core/src/api-implementation.js
@@ -9,7 +9,6 @@
require:false
*/
-
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js
index 2a19364c1c4..87b545157df 100644
--- a/cvat-core/src/session.js
+++ b/cvat-core/src/session.js
@@ -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
+ * Filter supports following operators:
+ * ==, !=, >, >=, <, <=, ~= and (), |, & for grouping.
+ * Filter supports properties:
+ * width, height, label, serverID, clientID, type, shape, occluded
+ * All prop values are case-sensitive. CVAT uses json queries for search.
+ * Examples:
+ *
+ * - label=="car" | label==["road sign"]
+ * - width >= height
+ * - attr["Attribute 1"] == attr["Attribute 2"]
+ * - type=="track" & shape="rectangle"
+ * - clientID == 50
+ * - (label=="car" & attr["parked"]==true)
+ * | (label=="pedestrian" & width > 150)
+ * - (( label==["car \\"mazda\\""]) &
+ * (attr["sunglass ( help ) es"]==true |
+ * (width > 150 | height > 150 & (clientID == serverID)))))
+ *
+ * If you have double quotes in your query string,
+ * please escape them using back slash: \"
* @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;
};
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index 8594fc7ee55..08a0ad4204d 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -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 = null;
+
+function getStore(): Store {
+ 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, {}, {}, AnyAction> {
+ return async (dispatch: ActionCreator): Promise => {
+ 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
- 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, {}, {}, 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, {}, {}, AnyAction> {
+export function getJobAsync(
+ tid: number,
+ jid: number,
+ initialFrame: number,
+ initialFilters: string[],
+): ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
@@ -567,8 +638,8 @@ ThunkAction, {}, {}, 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, {}, {}, 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, {}, {}, AnyAction> {
frameNumber,
frameData,
colors,
+ filters,
},
});
} catch (error) {
@@ -713,7 +785,8 @@ export function updateAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
- const promises = statesToUpdate.map((state: any): Promise => state.save());
+ const promises = statesToUpdate
+ .map((objectState: any): Promise => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
@@ -725,7 +798,8 @@ ThunkAction, {}, {}, 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any):
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
+ 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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({
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index 8abb71867a4..725df025398 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -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 (