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: + * + * 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 ( + + +