From fd1d0376a2b6ecdd25d12575a90ecc5c915e2b28 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 10:48:15 +0300 Subject: [PATCH 01/10] Increase/decrease ZOrder --- cvatjs/src/annotations-collection.js | 8 +-- cvatjs/src/annotations-objects.js | 76 ++++++++++++++++++++++++---- cvatjs/src/api.js | 10 ++++ cvatjs/src/object-state.js | 48 +++++++++++++++++- 4 files changed, 129 insertions(+), 13 deletions(-) diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 4251214c18b0..4dae92e2ebfe 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -47,17 +47,19 @@ return labelAccumulator; }, {}); - this.shapes = {}; // key is frame - this.tags = {}; // key is frame + this.shapes = {}; // key is a frame + this.tags = {}; // key is a frame this.tracks = []; - this.objects = {}; // key is client id + this.objects = {}; // key is a client id this.count = 0; this.flush = false; + this.collectionZ = {}; // key is a frame } import(data) { const injection = { labels: this.labels, + collectionZ: this.collectionZ, }; function shapeFactory(shapeData, clientID) { diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index e145fb73a797..6d5c8569b41f 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -14,8 +14,11 @@ const objectState = new ObjectState(data); // Rewrite default implementations of save/delete - objectState.updateInCollection = this.save.bind(this, frame, objectState); - objectState.deleteFromCollection = this.delete.bind(this); + const proto = Object.getPrototypeOf(objectState); + proto.updateInCollection = this.save.bind(this, frame, objectState); + proto.deleteFromCollection = this.delete.bind(this); + proto.upZOrder = this.up.bind(this, frame, objectState); + proto.downZOrder = this.down.bind(this, frame, objectState); return objectState; } @@ -92,14 +95,67 @@ } } - class Shape extends Annotation { + class Drawn extends Annotation { constructor(data, clientID, color, injection) { super(data, clientID, injection); + + this.collectionZ = injection.collectionZ; + const z = this._getZ(this.frame); + z.max = Math.max(z.max, this.zOrder || 0); + z.min = Math.min(z.min, this.zOrder || 0); + + this.color = color; + this.shape = null; + } + + _getZ(frame) { + this.collectionZ[frame] = this.collectionZ[frame] || { + max: 0, + min: 0, + }; + + return this.collectionZ[frame]; + } + + save() { + throw window.cvat.exceptions.ScriptingError( + 'Is not implemented', + ); + } + + get() { + throw window.cvat.exceptions.ScriptingError( + 'Is not implemented', + ); + } + + toJSON() { + throw window.cvat.exceptions.ScriptingError( + 'Is not implemented', + ); + } + + // Increase ZOrder within frame + up(frame, objectState) { + const z = this._getZ(frame); + z.max++; + objectState.zOrder = z.max; + } + + // Decrease ZOrder within frame + down(frame, objectState) { + const z = this._getZ(frame); + z.min--; + objectState.zOrder = z.min; + } + } + + class Shape extends Drawn { + constructor(data, clientID, color, injection) { + super(data, clientID, color, injection); this.points = data.points; this.occluded = data.occluded; this.zOrder = data.z_order; - this.color = color; - this.shape = null; } // Method is used to export data to the server @@ -232,9 +288,9 @@ } } - class Track extends Annotation { + class Track extends Drawn { constructor(data, clientID, color, injection) { - super(data, clientID, injection); + super(data, clientID, color, injection); this.shapes = data.shapes.reduce((shapeAccumulator, value) => { shapeAccumulator[value.frame] = { serverID: value.id, @@ -249,6 +305,10 @@ }, {}), }; + const z = this._getZ(value.frame); + z.max = Math.max(z.max, value.z_order); + z.min = Math.min(z.min, value.z_order); + return shapeAccumulator; }, {}); @@ -258,8 +318,6 @@ }, {}); this.cache = {}; - this.color = color; - this.shape = null; } // Method is used to export data to the server diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index 4f88de8cea7a..ff0550573480 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,3 +453,13 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); + +async function tmp() { + await window.cvat.server.login('admin', 'nimda760'); + const task = (await window.cvat.tasks.get({id : 2}))[0]; + const annotations = await task.annotations.get(0); + await annotations[0].up(); + await annotations[0].down(); +} + +tmp(); \ No newline at end of file diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 44acce924425..ab7c0959f2d2 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -287,7 +287,7 @@ * @instance * @param {boolean} [force=false] delete object even if it is locked * @async - * @returns {boolean} wheter object was deleted + * @returns {boolean} true if object has been deleted * @throws {module:API.cvat.exceptions.PluginError} */ async delete(force = false) { @@ -295,6 +295,36 @@ .apiWrapper.call(this, ObjectState.prototype.delete, force); return result; } + + /** + * Set the highest ZOrder within a frame + * @method up + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + */ + async up() { + const result = await PluginRegistry + .apiWrapper.call(this, ObjectState.prototype.up); + return result; + } + + /** + * Set the lowest ZOrder within a frame + * @method down + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + */ + async down() { + const result = await PluginRegistry + .apiWrapper.call(this, ObjectState.prototype.down); + return result; + } } // Default implementation saves element in collection @@ -315,6 +345,22 @@ return false; }; + ObjectState.prototype.up.implementation = async function () { + if (this.upZOrder) { + return this.upZOrder(); + } + + return false; + }; + + ObjectState.prototype.down.implementation = async function () { + if (this.downZOrder) { + return this.downZOrder(); + } + + return false; + }; + module.exports = ObjectState; })(); From ae2eb89e705451840371d4976305aa534a1c694c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 11:25:20 +0300 Subject: [PATCH 02/10] ZOrder up/down --- cvatjs/src/annotations-objects.js | 12 +++--- cvatjs/src/annotations.js | 65 ++++++++++++++++++------------- cvatjs/src/api.js | 10 ----- cvatjs/src/object-state.js | 16 ++++---- cvatjs/src/server-proxy.js | 29 +++----------- cvatjs/src/session.js | 38 ++++++++++++++---- 6 files changed, 86 insertions(+), 84 deletions(-) diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index 6d5c8569b41f..317a9c0544b8 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -13,12 +13,12 @@ function objectStateFactory(frame, data) { const objectState = new ObjectState(data); - // Rewrite default implementations of save/delete - const proto = Object.getPrototypeOf(objectState); - proto.updateInCollection = this.save.bind(this, frame, objectState); - proto.deleteFromCollection = this.delete.bind(this); - proto.upZOrder = this.up.bind(this, frame, objectState); - proto.downZOrder = this.down.bind(this, frame, objectState); + objectState.callbacks = { + updateInCollection: this.save.bind(this, frame, objectState), + deleteFromCollection: this.delete.bind(this), + upZOrder: this.up.bind(this, frame, objectState), + downZOrder: this.down.bind(this, frame, objectState), + }; return objectState; } diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 181eabdf8c57..c58a7a3024d1 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -15,56 +15,65 @@ const jobCache = {}; const taskCache = {}; - async function getJobAnnotations(job, frame, filter) { - if (!(job.id in jobCache)) { - const rawAnnotations = await serverProxy.annotations.getJobAnnotations(job.id); - const collection = new Collection(job.task.labels).import(rawAnnotations); - const saver = new AnnotationsSaver(rawAnnotations.version, collection, job); + function getCache(sessionType) { + if (sessionType === 'task') { + return taskCache; + } - jobCache[job.id] = { - collection, - saver, - }; + if (sessionType === 'job') { + return jobCache; } - return jobCache[job.id].collection.get(frame, filter); + throw window.cvat.exceptions.ScriptingError( + `Unknown session type was received ${sessionType}`, + ); } - async function getTaskAnnotations(task, frame, filter) { - if (!(task.id in jobCache)) { - const rawAnnotations = await serverProxy.annotations.getTaskAnnotations(task.id); - const collection = new Collection(task.labels).import(rawAnnotations); - const saver = new AnnotationsSaver(rawAnnotations.version, collection, task); + async function getAnnotations(session, frame, filter) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); - taskCache[task.id] = { + if (!(session.id in cache)) { + const rawAnnotations = await serverProxy.annotations + .getAnnotations(sessionType, session.id); + const collection = new Collection(session.labels || session.task.labels) + .import(rawAnnotations); + const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); + + cache[session.id] = { collection, saver, }; } - return taskCache[task.id].collection.get(frame, filter); + return cache[session.id].collection.get(frame, filter); } - async function saveJobAnnotations(job, onUpdate) { - if (job.id in jobCache) { - await jobCache[job.id].saver.save(onUpdate); + async function saveAnnotations(session, onUpdate) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); + + if (session.id in cache) { + await cache[session.id].saver.save(onUpdate); } // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it } - async function saveTaskAnnotations(task, onUpdate) { - if (task.id in taskCache) { - await taskCache[task.id].saver.save(onUpdate); + async function hasUnsavedChanges(session) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); + + if (!(session.id in cache)) { + return cache[session.id].saver.hasUnsavedChanges(); } - // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it + return false; } module.exports = { - getJobAnnotations, - getTaskAnnotations, - saveJobAnnotations, - saveTaskAnnotations, + getAnnotations, + saveAnnotations, + hasUnsavedChanges, }; })(); diff --git a/cvatjs/src/api.js b/cvatjs/src/api.js index ff0550573480..4f88de8cea7a 100644 --- a/cvatjs/src/api.js +++ b/cvatjs/src/api.js @@ -453,13 +453,3 @@ window.cvat = Object.freeze(implementAPI(cvat)); })(); - -async function tmp() { - await window.cvat.server.login('admin', 'nimda760'); - const task = (await window.cvat.tasks.get({id : 2}))[0]; - const annotations = await task.annotations.get(0); - await annotations[0].up(); - await annotations[0].down(); -} - -tmp(); \ No newline at end of file diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index ab7c0959f2d2..97e22aa62941 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -329,8 +329,8 @@ // Default implementation saves element in collection ObjectState.prototype.save.implementation = async function () { - if (this.updateInCollection) { - return this.updateInCollection(); + if (this.callbacks && this.callbacks.updateInCollection) { + return this.callbacks.updateInCollection(); } return this; @@ -338,24 +338,24 @@ // Default implementation do nothing ObjectState.prototype.delete.implementation = async function (force) { - if (this.deleteFromCollection) { - return this.deleteFromCollection(force); + if (this.callbacks && this.callbacks.deleteFromCollection) { + return this.callbacks.deleteFromCollection(force); } return false; }; ObjectState.prototype.up.implementation = async function () { - if (this.upZOrder) { - return this.upZOrder(); + if (this.callbacks && this.callbacks.upZOrder) { + return this.callbacks.upZOrder(); } return false; }; ObjectState.prototype.down.implementation = async function () { - if (this.downZOrder) { - return this.downZOrder(); + if (this.callbacks && this.callbacks.downZOrder) { + return this.callbacks.downZOrder(); } return false; diff --git a/cvatjs/src/server-proxy.js b/cvatjs/src/server-proxy.js index b74aa1ee2963..7b7238f80e38 100644 --- a/cvatjs/src/server-proxy.js +++ b/cvatjs/src/server-proxy.js @@ -460,37 +460,19 @@ return response.data; } - async function getTaskAnnotations(tid) { - const { backendAPI } = window.cvat.config; - - let response = null; - try { - response = await Axios.get(`${backendAPI}/tasks/${tid}/annotations`, { - proxy: window.cvat.config.proxy, - }); - } catch (errorData) { - const code = errorData.response ? errorData.response.status : errorData.code; - throw new window.cvat.exceptions.ServerError( - `Could not get annotations for the task ${tid} from the server`, - code, - ); - } - - return response.data; - } - - async function getJobAnnotations(jid) { + // Session is 'task' or 'job' + async function getAnnotations(session, id) { const { backendAPI } = window.cvat.config; let response = null; try { - response = await Axios.get(`${backendAPI}/jobs/${jid}/annotations`, { + response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, { proxy: window.cvat.config.proxy, }); } catch (errorData) { const code = errorData.response ? errorData.response.status : errorData.code; throw new window.cvat.exceptions.ServerError( - `Could not get annotations for the job ${jid} from the server`, + `Could not get annotations for the ${session} ${id} from the server`, code, ); } @@ -586,9 +568,8 @@ annotations: { value: Object.freeze({ - getTaskAnnotations, - getJobAnnotations, updateAnnotations, + getAnnotations, }), writable: false, }, diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index c4003634dd3b..298a9dba90d0 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -12,10 +12,8 @@ const serverProxy = require('./server-proxy'); const { getFrame } = require('./frames'); const { - getJobAnnotations, - getTaskAnnotations, - saveJobAnnotations, - saveTaskAnnotations, + getAnnotations, + saveAnnotations, } = require('./annotations'); function buildDublicatedAPI() { @@ -75,6 +73,12 @@ .apiWrapper.call(this, annotations.value.select, frame, x, y); return result; }, + + async hasUnsavedChanges() { + const result = await PluginRegistry + .apiWrapper.call(this, annotations.value.hasUnsavedChanges); + return result; + }, }, }); @@ -274,6 +278,16 @@ * @instance * @async */ + /** + * Indicate if there are any changes in + * annotations which haven't been saved on a server + * @method hasUnsavedChanges + * @memberof Session.annotations + * @returns {boolean} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ /** @@ -578,12 +592,16 @@ ); } - const annotationsData = await getJobAnnotations(this, frame, filter); + const annotationsData = await getAnnotations(this, frame, filter); return annotationsData; }; Job.prototype.annotations.save.implementation = async function (onUpdate) { - await saveJobAnnotations(this, onUpdate); + await saveAnnotations(this, onUpdate); + }; + + Job.prototype.annotations.hasUnsavedChanges.implementation = async function() { + }; /** @@ -1104,12 +1122,16 @@ ); } - const annotationsData = await getTaskAnnotations(this, frame, filter); + const annotationsData = await getAnnotations(this, frame, filter); return annotationsData; }; Task.prototype.annotations.save.implementation = async function (onUpdate) { - await saveTaskAnnotations(this, onUpdate); + await saveAnnotations(this, onUpdate); + }; + + Task.prototype.annotations.hasUnsavedChanges.implementation = async function() { + }; module.exports = { From 5f1d67b783cf791510901c2c404cbf8f21488e1a Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 13:45:52 +0300 Subject: [PATCH 03/10] Some crutial bugs fixed --- cvatjs/src/annotations-objects.js | 10 +- cvatjs/src/annotations-saver.js | 5 +- cvatjs/src/annotations.js | 6 +- cvatjs/src/object-state.js | 16 +- cvatjs/src/session.js | 276 ++++++++++++++++-------------- 5 files changed, 166 insertions(+), 147 deletions(-) diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index 317a9c0544b8..5f9acc54a3e9 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -13,11 +13,11 @@ function objectStateFactory(frame, data) { const objectState = new ObjectState(data); - objectState.callbacks = { - updateInCollection: this.save.bind(this, frame, objectState), - deleteFromCollection: this.delete.bind(this), - upZOrder: this.up.bind(this, frame, objectState), - downZOrder: this.down.bind(this, frame, objectState), + objectState.hidden = { + save: this.save.bind(this, frame, objectState), + delete: this.delete.bind(this), + up: this.up.bind(this, frame, objectState), + down: this.down.bind(this, frame, objectState), }; return objectState; diff --git a/cvatjs/src/annotations-saver.js b/cvatjs/src/annotations-saver.js index e8db605f9082..5778f1a716c6 100644 --- a/cvatjs/src/annotations-saver.js +++ b/cvatjs/src/annotations-saver.js @@ -249,6 +249,9 @@ delete this.initialObjects[object.id]; } } + + this.hash = this._getHash(); + onUpdate('Saving is done'); } catch (error) { onUpdate(`Can not save annotations: ${error.message}`); throw error; @@ -256,7 +259,7 @@ } hasUnsavedChanges() { - return this._getHash() !== this._hash; + return this._getHash() !== this.hash; } } diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index c58a7a3024d1..8f4d0dc16449 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -24,7 +24,7 @@ return jobCache; } - throw window.cvat.exceptions.ScriptingError( + throw new window.cvat.exceptions.ScriptingError( `Unknown session type was received ${sessionType}`, ); } @@ -60,11 +60,11 @@ // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it } - async function hasUnsavedChanges(session) { + function hasUnsavedChanges(session) { const sessionType = session.constructor.name.toLowerCase(); const cache = getCache(sessionType); - if (!(session.id in cache)) { + if (session.id in cache) { return cache[session.id].saver.hasUnsavedChanges(); } diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 97e22aa62941..5668f2d6c948 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -329,8 +329,8 @@ // Default implementation saves element in collection ObjectState.prototype.save.implementation = async function () { - if (this.callbacks && this.callbacks.updateInCollection) { - return this.callbacks.updateInCollection(); + if (this.hidden && this.hidden.save) { + return this.hidden.save(); } return this; @@ -338,24 +338,24 @@ // Default implementation do nothing ObjectState.prototype.delete.implementation = async function (force) { - if (this.callbacks && this.callbacks.deleteFromCollection) { - return this.callbacks.deleteFromCollection(force); + if (this.hidden && this.hidden.delete) { + return this.hidden.delete(force); } return false; }; ObjectState.prototype.up.implementation = async function () { - if (this.callbacks && this.callbacks.upZOrder) { - return this.callbacks.upZOrder(); + if (this.hidden && this.hidden.up) { + return this.hidden.up(); } return false; }; ObjectState.prototype.down.implementation = async function () { - if (this.callbacks && this.callbacks.downZOrder) { - return this.callbacks.downZOrder(); + if (this.hidden && this.hidden.down) { + return this.hidden.down(); } return false; diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index 298a9dba90d0..7958f5a15bc5 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -14,140 +14,136 @@ const { getAnnotations, saveAnnotations, + hasUnsavedChanges, } = require('./annotations'); - function buildDublicatedAPI() { - const annotations = Object.freeze({ - value: { - async upload(file) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.upload, file); - return result; - }, - - async save() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.save); - return result; - }, - - async clear() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.clear); - return result; - }, - - async dump() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.dump); - return result; - }, + function buildDublicatedAPI(prototype) { + Object.defineProperties(prototype, { + annotations: Object.freeze({ + value: { + async upload(file) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.upload, file); + return result; + }, - async statistics() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.statistics); - return result; - }, + async save() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.save); + return result; + }, - async put(arrayOfObjects = []) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.put, arrayOfObjects); - return result; - }, + async clear() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.clear); + return result; + }, - async get(frame, filter = {}) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.get, frame, filter); - return result; - }, + async dump() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.dump); + return result; + }, - async search(filter, frameFrom, frameTo) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.search, - filter, frameFrom, frameTo); - return result; - }, + async statistics() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.statistics); + return result; + }, - async select(frame, x, y) { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.select, frame, x, y); - return result; - }, + async put(arrayOfObjects = []) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.put, arrayOfObjects); + return result; + }, - async hasUnsavedChanges() { - const result = await PluginRegistry - .apiWrapper.call(this, annotations.value.hasUnsavedChanges); - return result; - }, - }, - }); + async get(frame, filter = {}) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.get, frame, filter); + return result; + }, - const frames = Object.freeze({ - value: { - async get(frame) { - const result = await PluginRegistry - .apiWrapper.call(this, frames.value.get, frame); - return result; - }, - }, - }); + async search(filter, frameFrom, frameTo) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.search, + filter, frameFrom, frameTo); + return result; + }, - const logs = Object.freeze({ - value: { - async put(logType, details) { - const result = await PluginRegistry - .apiWrapper.call(this, logs.value.put, logType, details); - return result; - }, - async save(onUpdate) { - const result = await PluginRegistry - .apiWrapper.call(this, logs.value.save, onUpdate); - return result; - }, - }, - }); + async select(frame, x, y) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.select, frame, x, y); + return result; + }, - const actions = Object.freeze({ - value: { - async undo(count) { - const result = await PluginRegistry - .apiWrapper.call(this, actions.value.undo, count); - return result; + async hasUnsavedChanges() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.hasUnsavedChanges); + return result; + }, }, - async redo(count) { - const result = await PluginRegistry - .apiWrapper.call(this, actions.value.redo, count); - return result; + writable: true, + }), + frames: Object.freeze({ + value: { + async get(frame) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.frames.get, frame); + return result; + }, }, - async clear() { - const result = await PluginRegistry - .apiWrapper.call(this, actions.value.clear); - return result; + writable: true, + }), + logs: Object.freeze({ + value: { + async put(logType, details) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.logs.put, logType, details); + return result; + }, + async save(onUpdate) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.logs.save, onUpdate); + return result; + }, }, - }, - }); - - const events = Object.freeze({ - value: { - async subscribe(eventType, callback) { - const result = await PluginRegistry - .apiWrapper.call(this, events.value.subscribe, eventType, callback); - return result; + writable: true, + }), + actions: Object.freeze({ + value: { + async undo(count) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.actions.undo, count); + return result; + }, + async redo(count) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.actions.redo, count); + return result; + }, + async clear() { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.actions.clear); + return result; + }, }, - async unsubscribe(eventType, callback = null) { - const result = await PluginRegistry - .apiWrapper.call(this, events.value.unsubscribe, eventType, callback); - return result; + writable: true, + }), + events: Object.freeze({ + value: { + async subscribe(evType, callback) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.events.subscribe, evType, callback); + return result; + }, + async unsubscribe(evType, callback = null) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.events.unsubscribe, evType, callback); + return result; + }, }, - }, - }); - - return Object.freeze({ - annotations, - frames, - logs, - actions, - events, + writable: true, + }), }); } @@ -525,9 +521,19 @@ }, })); - this.frames.get.implementation = this.frames.get.implementation.bind(this); - this.annotations.get.implementation = this.annotations.get.implementation.bind(this); - this.annotations.save.implementation = this.annotations.save.implementation.bind(this); + // When we call a function, for example: task.annotations.get() + // In the method get we lose the task context + // So, we need return it + this.annotations = { + get: Object.getPrototypeOf(this).annotations.get.bind(this), + save: Object.getPrototypeOf(this).annotations.save.bind(this), + hasUnsavedChanges: Object.getPrototypeOf(this) + .annotations.hasUnsavedChanges.bind(this), + }; + + this.frames = { + get: Object.getPrototypeOf(this).frames.get.bind(this), + }; } /** @@ -549,7 +555,7 @@ // Fill up the prototype by properties. Class syntax doesn't allow do it // So, we do it seperately - Object.defineProperties(Job.prototype, buildDublicatedAPI()); + buildDublicatedAPI(Job.prototype); Job.prototype.save.implementation = async function () { // TODO: Add ability to change an assignee @@ -600,8 +606,8 @@ await saveAnnotations(this, onUpdate); }; - Job.prototype.annotations.hasUnsavedChanges.implementation = async function() { - + Job.prototype.annotations.hasUnsavedChanges.implementation = async function () { + return hasUnsavedChanges(this); }; /** @@ -1005,9 +1011,19 @@ }, })); - this.frames.get.implementation = this.frames.get.implementation.bind(this); - this.annotations.get.implementation = this.annotations.get.implementation.bind(this); - this.annotations.save.implementation = this.annotations.save.implementation.bind(this); + // When we call a function, for example: task.annotations.get() + // In the method get we lose the task context + // So, we need return it + this.annotations = { + get: Object.getPrototypeOf(this).annotations.get.bind(this), + save: Object.getPrototypeOf(this).annotations.save.bind(this), + hasUnsavedChanges: Object.getPrototypeOf(this) + .annotations.hasUnsavedChanges.bind(this), + }; + + this.frames = { + get: Object.getPrototypeOf(this).frames.get.bind(this), + }; } /** @@ -1049,7 +1065,7 @@ // Fill up the prototype by properties. Class syntax doesn't allow do it // So, we do it seperately - Object.defineProperties(Task.prototype, buildDublicatedAPI()); + buildDublicatedAPI(Task.prototype); Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { // TODO: Add ability to change an owner and an assignee @@ -1130,8 +1146,8 @@ await saveAnnotations(this, onUpdate); }; - Task.prototype.annotations.hasUnsavedChanges.implementation = async function() { - + Task.prototype.annotations.hasUnsavedChanges.implementation = async function () { + return hasUnsavedChanges(this); }; module.exports = { From eeecd28bc50c85b0272935cd634063481ae91b73 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 15:27:09 +0300 Subject: [PATCH 04/10] hasUnsavedChanges and merge --- cvatjs/src/annotations-collection.js | 284 ++++++++++++++++++++------- cvatjs/src/annotations-objects.js | 48 +---- cvatjs/src/api-implementation.js | 48 +---- cvatjs/src/object-state.js | 25 ++- 4 files changed, 253 insertions(+), 152 deletions(-) diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 4dae92e2ebfe..1e3af315c8c7 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -17,9 +17,12 @@ PolygonTrack, PolylineTrack, PointsTrack, + Track, + Shape, Tag, objectStateFactory, } = require('./annotations-objects'); + const { checkObjectType } = require('./common'); const colors = [ '#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA', @@ -39,6 +42,65 @@ '#006A93', '#867200', '#E2B631', '#D9D6CF', ]; + function shapeFactory(shapeData, clientID, injection) { + const { type } = shapeData; + const color = colors[clientID % colors.length]; + + let shapeModel = null; + switch (type) { + case 'rectangle': + shapeModel = new RectangleShape(shapeData, clientID, color, injection); + break; + case 'polygon': + shapeModel = new PolygonShape(shapeData, clientID, color, injection); + break; + case 'polyline': + shapeModel = new PolylineShape(shapeData, clientID, color, injection); + break; + case 'points': + shapeModel = new PointsShape(shapeData, clientID, color, injection); + break; + default: + throw new window.cvat.exceptions.DataError( + `An unexpected type of shape "${type}"`, + ); + } + + return shapeModel; + } + + + function trackFactory(trackData, clientID, injection) { + if (trackData.shapes.length) { + const { type } = trackData.shapes[0]; + const color = colors[clientID % colors.length]; + + let trackModel = null; + switch (type) { + case 'rectangle': + trackModel = new RectangleTrack(trackData, clientID, color, injection); + break; + case 'polygon': + trackModel = new PolygonTrack(trackData, clientID, color, injection); + break; + case 'polyline': + trackModel = new PolylineTrack(trackData, clientID, color, injection); + break; + case 'points': + trackModel = new PointsTrack(trackData, clientID, color, injection); + break; + default: + throw new window.cvat.exceptions.DataError( + `An unexpected type of track "${type}"`, + ); + } + + return trackModel; + } + + console.warn('The track without any shapes had been found. It was ignored.'); + return null; + } class Collection { constructor(labels) { @@ -54,77 +116,16 @@ this.count = 0; this.flush = false; this.collectionZ = {}; // key is a frame - } - - import(data) { - const injection = { + this.injection = { labels: this.labels, collectionZ: this.collectionZ, }; + } - function shapeFactory(shapeData, clientID) { - const { type } = shapeData; - const color = colors[clientID % colors.length]; - let shapeModel = null; - switch (type) { - case 'rectangle': - shapeModel = new RectangleShape(shapeData, clientID, color, injection); - break; - case 'polygon': - shapeModel = new PolygonShape(shapeData, clientID, color, injection); - break; - case 'polyline': - shapeModel = new PolylineShape(shapeData, clientID, color, injection); - break; - case 'points': - shapeModel = new PointsShape(shapeData, clientID, color, injection); - break; - default: - throw new window.cvat.exceptions.DataError( - `An unexpected type of shape "${type}"`, - ); - } - - return shapeModel; - } - - - function trackFactory(trackData, clientID) { - if (trackData.shapes.length) { - const { type } = trackData.shapes[0]; - const color = colors[clientID % colors.length]; - - - let trackModel = null; - switch (type) { - case 'rectangle': - trackModel = new RectangleTrack(trackData, clientID, color, injection); - break; - case 'polygon': - trackModel = new PolygonTrack(trackData, clientID, color, injection); - break; - case 'polyline': - trackModel = new PolylineTrack(trackData, clientID, color, injection); - break; - case 'points': - trackModel = new PointsTrack(trackData, clientID, color, injection); - break; - default: - throw new window.cvat.exceptions.DataError( - `An unexpected type of track "${type}"`, - ); - } - - return trackModel; - } - - console.warn('The track without any shapes had been found. It was ignored.'); - return null; - } - + import(data) { for (const tag of data.tags) { const clientID = ++this.count; - const tagModel = new Tag(tag, clientID, injection); + const tagModel = new Tag(tag, clientID, this.injection); this.tags[tagModel.frame] = this.tags[tagModel.frame] || []; this.tags[tagModel.frame].push(tagModel); this.objects[clientID] = tagModel; @@ -132,7 +133,7 @@ for (const shape of data.shapes) { const clientID = ++this.count; - const shapeModel = shapeFactory(shape, clientID); + const shapeModel = shapeFactory(shape, clientID, this.injection); this.shapes[shapeModel.frame] = this.shapes[shapeModel.frame] || []; this.shapes[shapeModel.frame].push(shapeModel); this.objects[clientID] = shapeModel; @@ -140,7 +141,7 @@ for (const track of data.tracks) { const clientID = ++this.count; - const trackModel = trackFactory(track, clientID); + const trackModel = trackFactory(track, clientID, this.injection); // The function can return null if track doesn't have any shapes. // In this case a corresponded message will be sent to the console if (trackModel) { @@ -194,6 +195,155 @@ return objectStates; } + + merge(objectStates) { + checkObjectType('merged shapes', objectStates, Array, null); + if (!objectStates.length) return; + const objectsForMerge = objectStates.map((state) => { + checkObjectType('object state', state, window.cvat.classes.ObjectState, null); + return this.objects[state.clientID]; + }); + + const keyframes = {}; // frame: position + const { label, shapeType } = objectStates[0]; + const labelAttributes = label.attributes.reduce((accumulator, attribute) => { + accumulator[attribute.id] = attribute; + return accumulator; + }, {}); + + for (let i = 0; i < objectsForMerge.length; i++) { + // For each state get corresponding object + const object = objectsForMerge[i]; + const state = objectStates[i]; + if (state.label !== label) { + // error + } + + if (state.shape !== shapeType) { + // error + } + + // If this object is shape, get it position and save as a keyframe + if (object instanceof Shape) { + // Frame already saved and it is not outside + if (object.frame in keyframes && !keyframes[object.frame].outside) { + // error + } + + keyframes[object.frame] = { + points: [...object.points], + occluded: object.occluded, + zOrder: object.zOrder, + outside: false, + attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => { + // We save only mutable attributes inside a keyframe + if (attrID in labelAttributes && labelAttributes[attrID].mutable) { + accumulator[attrID] = object.attributes[attrID]; + } + return accumulator; + }, {}), + }; + + // Push outside shape after each annotation shape + // Any not outside shape rewrites it + if (!((object.frame + 1) in keyframes)) { + keyframes[object.frame + 1] = JSON + .parse(JSON.stringify(keyframes[object.frame])); + } + } else if (object instanceof Track) { + // If this object is track, iterate through all its + // keyframes and push copies to new keyframes + const attributes = {}; // id:value + for (const keyframe of Object.keys(object.shapes)) { + const shape = object.shapes[keyframe]; + // Frame already saved and it is not outside + if (keyframe in keyframes && !keyframes[keyframe].outside) { + // This shape is outside and non-outside shape already exists + if (shape.outside) { + continue; + } + + // error + } + + // We do not save an attribute if it has the same value + // We save only updates + let updatedAttributes = false; + for (const attrID in shape.attributes) { + if (!(attrID in attributes) + || attributes[attrID] !== shape.attributes[attrID]) { + updatedAttributes = true; + attributes[attrID] = shape.attributes[attrID]; + } + } + + keyframes[keyframe] = { + points: [...shape.points], + occluded: shape.occuded, + outside: shape.outside, + zOrder: shape.zOrder, + attributes: updatedAttributes ? Object.assign({}, attributes) : {}, + }; + } + } else { + // error + } + } + + let firstNonOutside = false; + for (const frame of Object.keys(keyframes).sort((a, b) => +a - +b)) { + // Remove all outside frames at the begin + firstNonOutside = firstNonOutside || keyframes[frame].outside; + if (!firstNonOutside) { + delete keyframes[frame]; + } else { + break; + } + } + + const clientID = ++this.count; + const track = { + group: 0, + label_id: label.id, + attributes: Object.keys(objectStates[0].attributes) + .reduce((accumulator, attrID) => { + if (!labelAttributes[attrID].mutable) { + accumulator.push({ + spec_id: +attrID, + value: objectStates[0].attributes[attrID], + }); + } + + return accumulator; + }, []), + }; + + const trackModel = trackFactory(track, clientID, this.injection); + if (trackModel) { + this.tracks.push(trackModel); + this.objects[clientID] = trackModel; + } + + // Remove other shapes + for (const object of objectsForMerge) { + object.removed = true; + } + } + + split(objectState) { + checkObjectType('object state', objectState, window.cvat.classes.ObjectState, null); + + // TODO: split + } + + group(array) { + checkObjectType('merged shapes', array, Array, null); + for (const shape of array) { + checkObjectType('object state', shape, window.cvat.classes.ObjectState, null); + } + + // TODO: + } } module.exports = Collection; diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index 5f9acc54a3e9..a9b6fcfca0bd 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -9,7 +9,9 @@ (() => { const ObjectState = require('./object-state'); + const { checkObjectType } = require('./common'); + // Called with the Annotation context function objectStateFactory(frame, data) { const objectState = new ObjectState(data); @@ -23,43 +25,6 @@ return objectState; } - function checkObjectType(name, value, type, instance) { - if (type) { - if (typeof (value) !== type) { - // specific case for integers which aren't native type in JS - if (type === 'integer' && Number.isInteger(value)) { - return; - } - - if (value !== undefined) { - throw new window.cvat.exceptions.ArgumentError( - `Got ${typeof (value)} value for ${name}. ` - + `Expected ${type}`, - ); - } - - throw new window.cvat.exceptions.ArgumentError( - `Got undefined value for ${name}. ` - + `Expected ${type}`, - ); - } - } else if (instance) { - if (!(value instanceof instance)) { - if (value !== undefined) { - throw new window.cvat.exceptions.ArgumentError( - `Got ${value.constructor.name} value for ${name}. ` - + `Expected instance of ${instance.name}`, - ); - } - - throw new window.cvat.exceptions.ArgumentError( - `Got undefined value for ${name}. ` - + `Expected instance of ${instance.name}`, - ); - } - } - } - class Annotation { constructor(data, clientID, injection) { this.taskLabels = injection.labels; @@ -192,6 +157,7 @@ type: window.cvat.enums.ObjectType.SHAPE, shape: this.shape, clientID: this.clientID, + serverID: this.serverID, occluded: this.occluded, lock: this.lock, zOrder: this.zOrder, @@ -297,7 +263,6 @@ occluded: value.occluded, zOrder: value.z_order, points: value.points, - frame: value.frame, outside: value.outside, attributes: value.attributes.reduce((attributeAccumulator, attr) => { attributeAccumulator[attr.spec_id] = attr.value; @@ -312,11 +277,6 @@ return shapeAccumulator; }, {}); - this.attributes = data.attributes.reduce((attributeAccumulator, attr) => { - attributeAccumulator[attr.spec_id] = attr.value; - return attributeAccumulator; - }, {}); - this.cache = {}; } @@ -373,6 +333,7 @@ type: window.cvat.enums.ObjectType.TRACK, shape: this.shape, clientID: this.clientID, + serverID: this.serverID, lock: this.lock, color: this.color, }, @@ -690,6 +651,7 @@ return { type: window.cvat.enums.ObjectType.TAG, clientID: this.clientID, + serverID: this.serverID, lock: this.lock, attributes: Object.assign({}, this.attributes), label: this.label, diff --git a/cvatjs/src/api-implementation.js b/cvatjs/src/api-implementation.js index e2d7505e8bc8..f2103a62de3a 100644 --- a/cvatjs/src/api-implementation.js +++ b/cvatjs/src/api-implementation.js @@ -13,47 +13,13 @@ (() => { const PluginRegistry = require('./plugins'); const serverProxy = require('./server-proxy'); - - function isBoolean(value) { - return typeof (value) === 'boolean'; - } - - function isInteger(value) { - return typeof (value) === 'number' && Number.isInteger(value); - } - - function isEnum(value) { - // Called with specific Enum context - for (const key in this) { - if (Object.prototype.hasOwnProperty.call(this, key)) { - if (this[key] === value) { - return true; - } - } - } - - return false; - } - - function isString(value) { - return typeof (value) === 'string'; - } - - function checkFilter(filter, fields) { - for (const prop in filter) { - if (Object.prototype.hasOwnProperty.call(filter, prop)) { - if (!(prop in fields)) { - throw new window.cvat.exceptions.ArgumentError( - `Unsupported filter property has been recieved: "${prop}"`, - ); - } else if (!fields[prop](filter[prop])) { - throw new window.cvat.exceptions.ArgumentError( - `Received filter property ${prop} is not satisfied for checker`, - ); - } - } - } - } + const { + isBoolean, + isInteger, + isEnum, + isString, + checkFilter, + } = require('./common'); function implementAPI(cvat) { cvat.plugins.list.implementation = PluginRegistry.list; diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index 5668f2d6c948..e0401b58fe11 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -21,7 +21,7 @@ * Necessary fields: type, shape * Necessary fields for objects which haven't been added to collection yet: frame * Optional fields: points, group, zOrder, outside, occluded, - * attributes, lock, label, mode, color, keyframe + * attributes, lock, label, mode, color, keyframe, clientID, serverID * These fields can be set later via setters */ constructor(serialized) { @@ -39,6 +39,9 @@ lock: null, color: null, + clientID: serialized.clientID, + serverID: serialized.serverID, + frame: serialized.frame, type: serialized.type, shape: serialized.shape, @@ -99,6 +102,26 @@ */ get: () => data.shape, }, + clientID: { + /** + * @name clientID + * @type {integer} + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + */ + get: () => data.clientID, + }, + serverID: { + /** + * @name serverID + * @type {integer} + * @memberof module:API.cvat.classes.ObjectState + * @readonly + * @instance + */ + get: () => data.serverID, + }, label: { /** * @name shape From 8b170e11ff63c4182e334456bd8ba40350b0d01f Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 15:52:01 +0300 Subject: [PATCH 05/10] Added API description --- cvatjs/src/annotations-collection.js | 17 ++++++-- cvatjs/src/session.js | 64 +++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 1e3af315c8c7..25ed39cb6861 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -216,11 +216,15 @@ const object = objectsForMerge[i]; const state = objectStates[i]; if (state.label !== label) { - // error + throw window.cvat.exceptions.ArgumentError( + `All shape labels are expected to be ${label.name}, but got ${state.label.name}`, + ); } if (state.shape !== shapeType) { - // error + throw window.cvat.exceptions.ArgumentError( + `All shapes are expected to be ${shapeType}, but got ${state.shape}`, + ); } // If this object is shape, get it position and save as a keyframe @@ -263,7 +267,9 @@ continue; } - // error + throw window.cvat.exceptions.ArgumentError( + 'Expected only one visible shape per frame', + ); } // We do not save an attribute if it has the same value @@ -286,7 +292,10 @@ }; } } else { - // error + throw window.cvat.exceptions.ArgumentError( + `Trying to merge unknown object type: ${object.constructor.name}. ` + + 'Only shapes and tracks are expected.', + ); } } diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index 7958f5a15bc5..6cd0d08bea9f 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -81,6 +81,24 @@ .apiWrapper.call(this, prototype.annotations.hasUnsavedChanges); return result; }, + + async merge(objectStates) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.merge, objectStates); + return result; + }, + + async split(objectState, frame) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.split, objectState, frame); + return result; + }, + + async group(objectStates) { + const result = await PluginRegistry + .apiWrapper.call(this, prototype.annotations.group, objectStates); + return result; + }, }, writable: true, }), @@ -268,12 +286,50 @@ * @param {float} x horizontal coordinate * @param {float} y vertical coordinate * @returns {(integer|null)} - * identifier of a selected object or null if no one of objects is on position + * an ID of a selected object or null if no one of objects is on position * @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.ArgumentError} * @instance * @async */ + /** + * Method unites several shapes and tracks into the one + * All shapes must be the same (rectangle, polygon, etc) + * All labels must be the same + * After successful merge you need to update object states on a frame + * @method merge + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} objectStates + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ArgumentError} + * @instance + * @async + */ + /** + * Method splits a track into two parts + * (start frame: previous frame), (frame, last frame) + * After successful split you need to update object states on a frame + * @method split + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState} objectState + * @param {integer} frame + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ + /** + * Method creates a new group and put all passed objects into it + * After successful split you need to update object states on a frame + * @method group + * @memberof Session.annotations + * @param {module:API.cvat.classes.ObjectState[]} objectStates + * @returns {integer} an ID of created group + * @throws {module:API.cvat.exceptions.ArgumentError} + * @throws {module:API.cvat.exceptions.PluginError} + * @instance + * @async + */ /** * Indicate if there are any changes in * annotations which haven't been saved on a server @@ -527,6 +583,9 @@ this.annotations = { get: Object.getPrototypeOf(this).annotations.get.bind(this), save: Object.getPrototypeOf(this).annotations.save.bind(this), + merge: Object.getPrototypeOf(this).annotations.merge.bind(this), + split: Object.getPrototypeOf(this).annotations.split.bind(this), + group: Object.getPrototypeOf(this).annotations.group.bind(this), hasUnsavedChanges: Object.getPrototypeOf(this) .annotations.hasUnsavedChanges.bind(this), }; @@ -1017,6 +1076,9 @@ this.annotations = { get: Object.getPrototypeOf(this).annotations.get.bind(this), save: Object.getPrototypeOf(this).annotations.save.bind(this), + merge: Object.getPrototypeOf(this).annotations.merge.bind(this), + split: Object.getPrototypeOf(this).annotations.split.bind(this), + group: Object.getPrototypeOf(this).annotations.group.bind(this), hasUnsavedChanges: Object.getPrototypeOf(this) .annotations.hasUnsavedChanges.bind(this), }; From db827abff2fa6f51dfd3bf6f457992f98c701e50 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 16:52:58 +0300 Subject: [PATCH 06/10] Fixed bugs --- cvatjs/src/annotations-collection.js | 81 ++++++++++++++++++++-------- cvatjs/src/annotations-objects.js | 3 ++ cvatjs/src/annotations.js | 42 +++++++++++++++ cvatjs/src/session.js | 27 ++++++++++ 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 25ed39cb6861..81f99964961d 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -155,15 +155,19 @@ export() { const data = { - tracks: this.tracks.map(track => track.toJSON()), - shapes: Object.values(this.shapes).reduce((accumulator, value) => { - accumulator.push(...value); - return accumulator; - }, []).map(shape => shape.toJSON()), + tracks: this.tracks.filter(track => !track.removed) + .map(track => track.toJSON()), + shapes: Object.values(this.shapes) + .reduce((accumulator, value) => { + accumulator.push(...value); + return accumulator; + }, []).filter(shape => !shape.removed) + .map(shape => shape.toJSON()), tags: Object.values(this.tags).reduce((accumulator, value) => { accumulator.push(...value); return accumulator; - }, []).map(tag => tag.toJSON()), + }, []).filter(tag => !tag.removed) + .map(tag => tag.toJSON()), }; return data; @@ -189,7 +193,12 @@ const objectStates = []; for (const object of objects) { - const objectState = objectStateFactory.call(object, frame, object.get(frame)); + const stateData = object.get(frame); + if (stateData.outside && !stateData.keyframe) { + continue; + } + + const objectState = objectStateFactory.call(object, frame, stateData); objectStates.push(objectState); } @@ -197,15 +206,22 @@ } merge(objectStates) { - checkObjectType('merged shapes', objectStates, Array, null); + checkObjectType('merged shapes', objectStates, null, Array); if (!objectStates.length) return; const objectsForMerge = objectStates.map((state) => { - checkObjectType('object state', state, window.cvat.classes.ObjectState, null); - return this.objects[state.clientID]; + checkObjectType('object state', state, null, window.cvat.classes.ObjectState); + const object = this.objects[state.clientID]; + if (typeof (object) === 'undefined') { + throw new window.cvat.exceptions.ArgumentError( + 'The object has not been saved yet. Call ObjectState.save() before you can merge it', + ); + } + return object; }); const keyframes = {}; // frame: position - const { label, shapeType } = objectStates[0]; + const { label } = objectStates[0]; + const shapeType = objectStates[0].shape; const labelAttributes = label.attributes.reduce((accumulator, attribute) => { accumulator[attribute.id] = attribute; return accumulator; @@ -215,14 +231,14 @@ // For each state get corresponding object const object = objectsForMerge[i]; const state = objectStates[i]; - if (state.label !== label) { - throw window.cvat.exceptions.ArgumentError( + if (state.label.id !== label.id) { + throw new window.cvat.exceptions.ArgumentError( `All shape labels are expected to be ${label.name}, but got ${state.label.name}`, ); } if (state.shape !== shapeType) { - throw window.cvat.exceptions.ArgumentError( + throw new window.cvat.exceptions.ArgumentError( `All shapes are expected to be ${shapeType}, but got ${state.shape}`, ); } @@ -231,10 +247,14 @@ if (object instanceof Shape) { // Frame already saved and it is not outside if (object.frame in keyframes && !keyframes[object.frame].outside) { - // error + throw new window.cvat.exceptions.ArgumentError( + 'Expected only one visible shape per frame', + ); } keyframes[object.frame] = { + type: shapeType, + frame: object.frame, points: [...object.points], occluded: object.occluded, zOrder: object.zOrder, @@ -242,10 +262,13 @@ attributes: Object.keys(object.attributes).reduce((accumulator, attrID) => { // We save only mutable attributes inside a keyframe if (attrID in labelAttributes && labelAttributes[attrID].mutable) { - accumulator[attrID] = object.attributes[attrID]; + accumulator.push({ + spec_id: +attrID, + value: object.attributes[attrID], + }); } return accumulator; - }, {}), + }, []), }; // Push outside shape after each annotation shape @@ -253,6 +276,8 @@ if (!((object.frame + 1) in keyframes)) { keyframes[object.frame + 1] = JSON .parse(JSON.stringify(keyframes[object.frame])); + keyframes[object.frame + 1].outside = true; + keyframes[object.frame + 1].frame++; } } else if (object instanceof Track) { // If this object is track, iterate through all its @@ -267,7 +292,7 @@ continue; } - throw window.cvat.exceptions.ArgumentError( + throw new window.cvat.exceptions.ArgumentError( 'Expected only one visible shape per frame', ); } @@ -284,15 +309,25 @@ } keyframes[keyframe] = { + type: shapeType, + frame: +keyframe, points: [...shape.points], - occluded: shape.occuded, + occluded: shape.occluded, outside: shape.outside, zOrder: shape.zOrder, - attributes: updatedAttributes ? Object.assign({}, attributes) : {}, + attributes: updatedAttributes ? Object.keys(attributes) + .reduce((accumulator, attrID) => { + accumulator.push({ + spec_id: +attrID, + value: attributes[attrID], + }); + + return accumulator; + }, []) : [], }; } } else { - throw window.cvat.exceptions.ArgumentError( + throw new window.cvat.exceptions.ArgumentError( `Trying to merge unknown object type: ${object.constructor.name}. ` + 'Only shapes and tracks are expected.', ); @@ -303,7 +338,7 @@ for (const frame of Object.keys(keyframes).sort((a, b) => +a - +b)) { // Remove all outside frames at the begin firstNonOutside = firstNonOutside || keyframes[frame].outside; - if (!firstNonOutside) { + if (!firstNonOutside && keyframes[frame].outside) { delete keyframes[frame]; } else { break; @@ -312,6 +347,8 @@ const clientID = ++this.count; const track = { + frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)), + shapes: Object.values(keyframes), group: 0, label_id: label.id, attributes: Object.keys(objectStates[0].attributes) diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index a9b6fcfca0bd..20aa48375d37 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -126,6 +126,7 @@ // Method is used to export data to the server toJSON() { return { + type: this.shape, clientID: this.clientID, occluded: this.occluded, z_order: this.zOrder, @@ -1193,6 +1194,8 @@ PolygonTrack, PolylineTrack, PointsTrack, + Track, + Shape, Tag, objectStateFactory, }; diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 8f4d0dc16449..8c27929a125a 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -60,6 +60,45 @@ // If a collection wasn't uploaded, than it wasn't changed, finally we shouldn't save it } + function mergeAnnotations(session, objectStates) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); + + if (session.id in cache) { + return cache[session.id].collection.merge(objectStates); + } + + throw window.cvat.exceptions.DataError( + 'Collection has not been initialized yet. Call annotations.get() before', + ); + } + + function splitAnnotations(session, objectState, frame) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); + + if (session.id in cache) { + return cache[session.id].collection.split(objectState, frame); + } + + throw window.cvat.exceptions.DataError( + 'Collection has not been initialized yet. Call annotations.get() before', + ); + } + + function groupAnnotations(session, objectStates) { + const sessionType = session.constructor.name.toLowerCase(); + const cache = getCache(sessionType); + + if (session.id in cache) { + return cache[session.id].collection.group(objectStates); + } + + throw window.cvat.exceptions.DataError( + 'Collection has not been initialized yet. Call annotations.get() before', + ); + } + function hasUnsavedChanges(session) { const sessionType = session.constructor.name.toLowerCase(); const cache = getCache(sessionType); @@ -75,5 +114,8 @@ getAnnotations, saveAnnotations, hasUnsavedChanges, + mergeAnnotations, + splitAnnotations, + groupAnnotations, }; })(); diff --git a/cvatjs/src/session.js b/cvatjs/src/session.js index 6cd0d08bea9f..939d2f552bb6 100644 --- a/cvatjs/src/session.js +++ b/cvatjs/src/session.js @@ -15,6 +15,9 @@ getAnnotations, saveAnnotations, hasUnsavedChanges, + mergeAnnotations, + splitAnnotations, + groupAnnotations, } = require('./annotations'); function buildDublicatedAPI(prototype) { @@ -665,6 +668,18 @@ await saveAnnotations(this, onUpdate); }; + Job.prototype.annotations.merge.implementation = async function (objectStates) { + await mergeAnnotations(this, objectStates); + }; + + Job.prototype.annotations.split.implementation = async function (objectState, frame) { + await splitAnnotations(this, objectState, frame); + }; + + Job.prototype.annotations.group.implementation = async function (objectStates) { + await groupAnnotations(this, objectStates); + }; + Job.prototype.annotations.hasUnsavedChanges.implementation = async function () { return hasUnsavedChanges(this); }; @@ -1208,6 +1223,18 @@ await saveAnnotations(this, onUpdate); }; + Task.prototype.annotations.merge.implementation = async function (objectStates) { + await mergeAnnotations(this, objectStates); + }; + + Task.prototype.annotations.split.implementation = async function (objectState, frame) { + await splitAnnotations(this, objectState, frame); + }; + + Task.prototype.annotations.group.implementation = async function (objectStates) { + await groupAnnotations(this, objectStates); + }; + Task.prototype.annotations.hasUnsavedChanges.implementation = async function () { return hasUnsavedChanges(this); }; From 820ca3de5c99ba05f23b4752bd591ec7e7b5f20a Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 17:07:18 +0300 Subject: [PATCH 07/10] Merge was tested --- cvatjs/src/annotations-collection.js | 7 +++--- cvatjs/src/annotations-objects.js | 36 +++++++++++++--------------- cvatjs/src/object-state.js | 16 ++++++------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/cvatjs/src/annotations-collection.js b/cvatjs/src/annotations-collection.js index 81f99964961d..9715d8fbf5ce 100644 --- a/cvatjs/src/annotations-collection.js +++ b/cvatjs/src/annotations-collection.js @@ -220,8 +220,7 @@ }); const keyframes = {}; // frame: position - const { label } = objectStates[0]; - const shapeType = objectStates[0].shape; + const { label, shapeType } = objectStates[0]; const labelAttributes = label.attributes.reduce((accumulator, attribute) => { accumulator[attribute.id] = attribute; return accumulator; @@ -237,9 +236,9 @@ ); } - if (state.shape !== shapeType) { + if (state.shapeType !== shapeType) { throw new window.cvat.exceptions.ArgumentError( - `All shapes are expected to be ${shapeType}, but got ${state.shape}`, + `All shapes are expected to be ${shapeType}, but got ${state.shapeType}`, ); } diff --git a/cvatjs/src/annotations-objects.js b/cvatjs/src/annotations-objects.js index 20aa48375d37..23d7e383d9ce 100644 --- a/cvatjs/src/annotations-objects.js +++ b/cvatjs/src/annotations-objects.js @@ -70,7 +70,7 @@ z.min = Math.min(z.min, this.zOrder || 0); this.color = color; - this.shape = null; + this.shapeType = null; } _getZ(frame) { @@ -126,7 +126,7 @@ // Method is used to export data to the server toJSON() { return { - type: this.shape, + type: this.shapeType, clientID: this.clientID, occluded: this.occluded, z_order: this.zOrder, @@ -155,8 +155,8 @@ } return { - type: window.cvat.enums.ObjectType.SHAPE, - shape: this.shape, + objectType: window.cvat.enums.ObjectType.SHAPE, + shapeType: this.shapeType, clientID: this.clientID, serverID: this.serverID, occluded: this.occluded, @@ -299,7 +299,7 @@ }, []), shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => { shapesAccumulator.push({ - type: this.shape, + type: this.shapeType, occluded: this.shapes[frame].occluded, z_order: this.shapes[frame].zOrder, points: [...this.shapes[frame].points], @@ -331,8 +331,8 @@ attributes: this.getAttributes(frame), label: this.label, group: this.group, - type: window.cvat.enums.ObjectType.TRACK, - shape: this.shape, + objectType: window.cvat.enums.ObjectType.TRACK, + shapeType: this.shapeType, clientID: this.clientID, serverID: this.serverID, lock: this.lock, @@ -650,7 +650,7 @@ } return { - type: window.cvat.enums.ObjectType.TAG, + objectType: window.cvat.enums.ObjectType.TAG, clientID: this.clientID, serverID: this.serverID, lock: this.lock, @@ -718,7 +718,7 @@ class RectangleShape extends Shape { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.RECTANGLE; + this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE; } } @@ -731,28 +731,28 @@ class PolygonShape extends PolyShape { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POLYGON; + this.shapeType = window.cvat.enums.ObjectShape.POLYGON; } } class PolylineShape extends PolyShape { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POLYLINE; + this.shapeType = window.cvat.enums.ObjectShape.POLYLINE; } } class PointsShape extends PolyShape { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POINTS; + this.shapeType = window.cvat.enums.ObjectShape.POINTS; } } class RectangleTrack extends Track { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.RECTANGLE; + this.shapeType = window.cvat.enums.ObjectShape.RECTANGLE; } interpolatePosition(leftPosition, rightPosition, targetFrame) { @@ -1163,25 +1163,21 @@ class PolygonTrack extends PolyTrack { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POLYGON; + this.shapeType = window.cvat.enums.ObjectShape.POLYGON; } } class PolylineTrack extends PolyTrack { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POLYLINE; - } - - appendMapping() { - // TODO after checking how it works with polygons + this.shapeType = window.cvat.enums.ObjectShape.POLYLINE; } } class PointsTrack extends PolyTrack { constructor(data, clientID, color, injection) { super(data, clientID, color, injection); - this.shape = window.cvat.enums.ObjectShape.POINTS; + this.shapeType = window.cvat.enums.ObjectShape.POINTS; } } diff --git a/cvatjs/src/object-state.js b/cvatjs/src/object-state.js index e0401b58fe11..203874b08157 100644 --- a/cvatjs/src/object-state.js +++ b/cvatjs/src/object-state.js @@ -43,8 +43,8 @@ serverID: serialized.serverID, frame: serialized.frame, - type: serialized.type, - shape: serialized.shape, + objectType: serialized.objectType, + shapeType: serialized.shapeType, updateFlags: {}, }; @@ -82,25 +82,25 @@ */ get: () => data.frame, }, - type: { + objectType: { /** - * @name type + * @name objectType * @type {module:API.cvat.enums.ObjectType} * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance */ - get: () => data.type, + get: () => data.objectType, }, - shape: { + shapeType: { /** - * @name shape + * @name shapeType * @type {module:API.cvat.enums.ObjectShape} * @memberof module:API.cvat.classes.ObjectState * @readonly * @instance */ - get: () => data.shape, + get: () => data.shapeType, }, clientID: { /** From 63b7a8e9e12f43e7e84efada6bfe92669e3ce3a0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 17:10:29 +0300 Subject: [PATCH 08/10] New file --- cvatjs/src/common.js | 86 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 cvatjs/src/common.js diff --git a/cvatjs/src/common.js b/cvatjs/src/common.js new file mode 100644 index 000000000000..bd5b2e884684 --- /dev/null +++ b/cvatjs/src/common.js @@ -0,0 +1,86 @@ +/* +* Copyright (C) 2018 Intel Corporation +* SPDX-License-Identifier: MIT +*/ + +(() => { + function isBoolean(value) { + return typeof (value) === 'boolean'; + } + + function isInteger(value) { + return typeof (value) === 'number' && Number.isInteger(value); + } + + // Called with specific Enum context + function isEnum(value) { + for (const key in this) { + if (Object.prototype.hasOwnProperty.call(this, key)) { + if (this[key] === value) { + return true; + } + } + } + + return false; + } + + function isString(value) { + return typeof (value) === 'string'; + } + + function checkFilter(filter, fields) { + for (const prop in filter) { + if (Object.prototype.hasOwnProperty.call(filter, prop)) { + if (!(prop in fields)) { + throw new window.cvat.exceptions.ArgumentError( + `Unsupported filter property has been recieved: "${prop}"`, + ); + } else if (!fields[prop](filter[prop])) { + throw new window.cvat.exceptions.ArgumentError( + `Received filter property ${prop} is not satisfied for checker`, + ); + } + } + } + } + + function checkObjectType(name, value, type, instance) { + if (type) { + if (typeof (value) !== type) { + // specific case for integers which aren't native type in JS + if (type === 'integer' && Number.isInteger(value)) { + return; + } + + throw new window.cvat.exceptions.ArgumentError( + `Got "${name}" value of type: "${typeof (value)}". ` + + `Expected "${type}"`, + ); + } + } else if (instance) { + if (!(value instanceof instance)) { + if (value !== undefined) { + throw new window.cvat.exceptions.ArgumentError( + `Got ${value.constructor.name} value for ${name}. ` + + `Expected instance of ${instance.name}`, + ); + } + + throw new window.cvat.exceptions.ArgumentError( + `Got undefined value for ${name}. ` + + `Expected instance of ${instance.name}`, + ); + } + } + } + + module.exports = { + isBoolean, + isInteger, + isEnum, + isString, + checkFilter, + checkObjectType, + }; +})(); From 80855903dd45001586f0a99e5b313e1a312d0e61 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 17:45:11 +0300 Subject: [PATCH 09/10] Fixed unit tests --- cvatjs/src/api-implementation.js | 12 ++++++----- cvatjs/tests/mocks/server-proxy.mock.js | 27 ++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/cvatjs/src/api-implementation.js b/cvatjs/src/api-implementation.js index f2103a62de3a..0a7d8bb72141 100644 --- a/cvatjs/src/api-implementation.js +++ b/cvatjs/src/api-implementation.js @@ -78,17 +78,19 @@ ); } - let task = null; + let tasks = null; if ('taskID' in filter) { - task = await serverProxy.tasks.getTasks(`id=${filter.taskID}`); + tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`); } else { const job = await serverProxy.jobs.getJob(filter.jobID); - task = await serverProxy.tasks.getTasks(`id=${job.task_id}`); + if (typeof (job.task_id) !== 'undefined') { + tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`); + } } // If task was found by its id, then create task instance and get Job instance from it - if (task.length) { - task = new window.cvat.classes.Task(task[0]); + if (tasks !== null && tasks.length) { + const task = new window.cvat.classes.Task(tasks[0]); return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs; } diff --git a/cvatjs/tests/mocks/server-proxy.mock.js b/cvatjs/tests/mocks/server-proxy.mock.js index 41e4c308cf45..5b5f0e5e8f10 100644 --- a/cvatjs/tests/mocks/server-proxy.mock.js +++ b/cvatjs/tests/mocks/server-proxy.mock.js @@ -54,6 +54,10 @@ class ServerProxy { return null; } + async function logout() { + return null; + } + async function getTasks(filter = '') { function QueryStringToJSON(query) { const pairs = [...new URLSearchParams(query).entries()]; @@ -134,7 +138,7 @@ class ServerProxy { } async function getJob(jobID) { - return tasksDummyData.results.reduce((acc, task) => { + const jobs = tasksDummyData.results.reduce((acc, task) => { for (const segment of task.segments) { for (const job of segment.jobs) { const copy = JSON.parse(JSON.stringify(job)); @@ -148,6 +152,10 @@ class ServerProxy { return acc; }, []).filter(job => job.id === jobID); + + return jobs[0] || { + detail: 'Not found.', + }; } async function saveJob(id, jobData) { @@ -185,6 +193,14 @@ class ServerProxy { return null; } + async function getAnnotations() { + return null; + } + + async function updateAnnotations() { + return null; + } + Object.defineProperties(this, Object.freeze({ server: { value: Object.freeze({ @@ -192,6 +208,7 @@ class ServerProxy { share, exception, login, + logout, }), writable: false, }, @@ -229,6 +246,14 @@ class ServerProxy { }), writable: false, }, + + annotations: { + value: Object.freeze({ + updateAnnotations, + getAnnotations, + }), + writable: false, + }, })); } } From 5c9d9bfd32cf10d449f3e5439bc2b77849b62d9c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Thu, 11 Jul 2019 18:07:29 +0300 Subject: [PATCH 10/10] Fixed small bug which reproduced only after build --- cvatjs/src/annotations-saver.js | 4 ++-- cvatjs/src/annotations.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cvatjs/src/annotations-saver.js b/cvatjs/src/annotations-saver.js index 5778f1a716c6..5da4d753b62a 100644 --- a/cvatjs/src/annotations-saver.js +++ b/cvatjs/src/annotations-saver.js @@ -12,7 +12,7 @@ class AnnotationsSaver { constructor(version, collection, session) { - this.session = session.constructor.name.toLowerCase(); + this.sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; this.id = session.id; this.version = version; this.collection = collection; @@ -42,7 +42,7 @@ async _request(data, action) { const result = await serverProxy.annotations.updateAnnotations( - this.session, + this.sessionType, this.id, data, action, diff --git a/cvatjs/src/annotations.js b/cvatjs/src/annotations.js index 8c27929a125a..36160a5374c5 100644 --- a/cvatjs/src/annotations.js +++ b/cvatjs/src/annotations.js @@ -30,7 +30,7 @@ } async function getAnnotations(session, frame, filter) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (!(session.id in cache)) { @@ -50,7 +50,7 @@ } async function saveAnnotations(session, onUpdate) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (session.id in cache) { @@ -61,7 +61,7 @@ } function mergeAnnotations(session, objectStates) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (session.id in cache) { @@ -74,7 +74,7 @@ } function splitAnnotations(session, objectState, frame) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (session.id in cache) { @@ -87,7 +87,7 @@ } function groupAnnotations(session, objectStates) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (session.id in cache) { @@ -100,7 +100,7 @@ } function hasUnsavedChanges(session) { - const sessionType = session.constructor.name.toLowerCase(); + const sessionType = session instanceof window.cvat.classes.Task ? 'task' : 'job'; const cache = getCache(sessionType); if (session.id in cache) {