Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Saving of annotations on the server #561

Merged
merged 3 commits into from
Jul 10, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cvatjs/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,5 +49,6 @@
"no-useless-constructor": 0,
"func-names": [0],
"valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
},
};
23 changes: 17 additions & 6 deletions cvatjs/src/annotations-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,15 @@
return labelAccumulator;
}, {});

this.empty();
this.shapes = {}; // key is frame
this.tags = {}; // key is frame
this.tracks = [];
this.objects = {}; // key is client id
this.count = 0;
this.flush = false;
}

import(data) {
this.empty();
const injection = {
labels: this.labels,
};
Expand Down Expand Up @@ -142,16 +146,21 @@
this.objects[clientID] = trackModel;
}
}

return this;
}

export() {
const data = {
tracks: Object.values(this.tracks).reduce((accumulator, value) => {
tracks: this.tracks.map(track => track.toJSON()),
shapes: Object.values(this.shapes).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(track => track.toJSON()),
shapes: this.shapes.map(shape => shape.toJSON()),
tags: this.shapes.map(tag => tag.toJSON()),
}, []).map(shape => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).map(tag => tag.toJSON()),
};

return data;
Expand All @@ -163,6 +172,8 @@
this.tracks = [];
this.objects = {}; // by id
this.count = 0;

this.flush = true;
}

get(frame) {
Expand Down
21 changes: 10 additions & 11 deletions cvatjs/src/annotations-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
// Method is used to export data to the server
toJSON() {
return {
clientID: this.clientID,
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
Expand Down Expand Up @@ -264,9 +265,11 @@
// Method is used to export data to the server
toJSON() {
return {
occluded: this.occluded,
z_order: this.zOrder,
points: [...this.points],
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
Expand All @@ -275,19 +278,14 @@

return attributeAccumulator;
}, []),

id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => {
shapesAccumulator.push({
type: this.type,
type: this.shape,
occluded: this.shapes[frame].occluded,
z_order: this.shapes[frame].zOrder,
points: [...this.shapes[frame].points],
outside: [...this.shapes[frame].outside],
attributes: Object.keys(...this.shapes[frame].attributes)
outside: this.shapes[frame].outside,
attributes: Object.keys(this.shapes[frame].attributes)
.reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
Expand Down Expand Up @@ -607,6 +605,7 @@
// Method is used to export data to the server
toJSON() {
return {
clientID: this.clientID,
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
Expand Down
264 changes: 264 additions & 0 deletions cvatjs/src/annotations-saver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
/*
* Copyright (C) 2018 Intel Corporation
* SPDX-License-Identifier: MIT
*/

/* global
require:false
*/

(() => {
const serverProxy = require('./server-proxy');

class AnnotationsSaver {
constructor(version, collection, session) {
this.session = session.constructor.name.toLowerCase();
this.id = session.id;
this.version = version;
this.collection = collection;
this.initialObjects = [];
this.hash = this._getHash();

// We need use data from export instead of initialData
// Otherwise we have differ keys order and JSON comparison code incorrect
const exported = this.collection.export();
for (const shape of exported.shapes) {
this.initialObjects[shape.id] = shape;
}

for (const track of exported.tracks) {
this.initialObjects[track.id] = track;
}

for (const tag of exported.tags) {
this.initialObjects[tag.id] = tag;
}
}

_getHash() {
const exported = this.collection.export();
return JSON.stringify(exported);
}

async _request(data, action) {
const result = await serverProxy.annotations.updateAnnotations(
this.session,
this.id,
data,
action,
);

return result;
}

async _put(data) {
const result = await this._request(data, 'put');
return result;
}

async _create(created) {
const result = await this._request(created, 'create');
return result;
}

async _update(updated) {
const result = await this._request(updated, 'update');
return result;
}

async _delete(deleted) {
const result = await this._request(deleted, 'delete');
return result;
}

_split(exported) {
const splitted = {
created: {
shapes: [],
tracks: [],
tags: [],
},
updated: {
shapes: [],
tracks: [],
tags: [],
},
deleted: {
shapes: [],
tracks: [],
tags: [],
},
};

// Find created and updated objects
for (const type of Object.keys(exported)) {
for (const object of exported[type]) {
if (object.id in this.initialObjects) {
const exportedHash = JSON.stringify(object);
const initialHash = JSON.stringify(this.initialObjects[object.id]);
if (exportedHash !== initialHash) {
splitted.updated[type].push(object);
}
} else if (typeof (object.id) === 'undefined') {
splitted.created[type].push(object);
} else {
throw window.cvat.exceptions.ScriptingError(
`Id of object is defined "${object.id}"`
+ 'but it absents in initial state',
);
}
}
}

// Now find deleted objects
const indexes = exported.tracks.concat(exported.shapes)
.concat(exported.tags).map(object => object.id);

for (const id of Object.keys(this.initialObjects)) {
if (!indexes.includes(+id)) {
const object = this.initialObjects[id];
let type = null;
if ('shapes' in object) {
type = 'tracks';
} else if ('points' in object) {
type = 'shapes';
} else {
type = 'tags';
}
splitted.deleted[type].push(object);
}
}

return splitted;
}

_updateCreatedObjects(saved, indexes) {
const savedLength = saved.tracks.length
+ saved.shapes.length + saved.tags.length;

const indexesLength = indexes.tracks.length
+ indexes.shapes.length + indexes.tags.length;

if (indexesLength !== savedLength) {
throw window.cvat.exception.ScriptingError(
'Number of indexes is differed by number of saved objects'
+ `${indexesLength} vs ${savedLength}`,
);
}

// Updated IDs of created objects
for (const type of Object.keys(indexes)) {
for (let i = 0; i < indexes[type].length; i++) {
const clientID = indexes[type][i];
this.collection.objects[clientID].serverID = saved[type][i].id;
}
}
}

_receiveIndexes(exported) {
// Receive client indexes before saving
const indexes = {
tracks: exported.tracks.map(track => track.clientID),
shapes: exported.shapes.map(shape => shape.clientID),
tags: exported.tags.map(tag => tag.clientID),
};

// Remove them from the request body
exported.tracks.concat(exported.shapes).concat(exported.tags)
.map((value) => {
delete value.clientID;
return value;
});

return indexes;
}

async save(onUpdate) {
if (typeof onUpdate !== 'function') {
onUpdate = (message) => {
console.log(message);
};
}

try {
const exported = this.collection.export();
const { flush } = this.collection;
if (flush) {
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(exported);
const savedData = await this._put(Object.assign({}, exported, {
version: this.version,
}));
this.version = savedData.version;
this.collection.flush = false;

onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(savedData, indexes);

onUpdate('Initial state is being updated');
for (const object of savedData.shapes
.concat(savedData.tracks).concat(savedData.tags)) {
this.initialObjects[object.id] = object;
}
} else {
const {
created,
updated,
deleted,
} = this._split(exported);

onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(created);
const createdData = await this._create(Object.assign({}, created, {
version: this.version,
}));
this.version = createdData.version;

onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(createdData, indexes);

onUpdate('Initial state is being updated');
for (const object of createdData.shapes
.concat(createdData.tracks).concat(createdData.tags)) {
this.initialObjects[object.id] = object;
}

onUpdate('Changed objects are being saved..');
this._receiveIndexes(updated);
const updatedData = await this._update(Object.assign({}, updated, {
version: this.version,
}));
this.version = createdData.version;

onUpdate('Initial state is being updated');
for (const object of updatedData.shapes
.concat(updatedData.tracks).concat(updatedData.tags)) {
this.initialObjects[object.id] = object;
}

onUpdate('Changed objects are being saved..');
this._receiveIndexes(deleted);
const deletedData = await this._delete(Object.assign({}, deleted, {
version: this.version,
}));
this._version = deletedData.version;

onUpdate('Initial state is being updated');
for (const object of deletedData.shapes
.concat(deletedData.tracks).concat(deletedData.tags)) {
delete this.initialObjects[object.id];
}
}
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
}
}

hasUnsavedChanges() {
return this._getHash() !== this._hash;
}
}

module.exports = AnnotationsSaver;
})();
Loading