Merge branch 'release-1.2.0'
commit
37d82f9005
@ -1,5 +1,5 @@
|
||||
exclude_paths:
|
||||
- '**/3rdparty/**'
|
||||
- '**/engine/js/cvat-core.min.js'
|
||||
- '**/engine/js/unzip_imgs.js'
|
||||
- CHANGELOG.md
|
||||
- '**/3rdparty/**'
|
||||
- '**/engine/js/cvat-core.min.js'
|
||||
- '**/engine/js/unzip_imgs.js'
|
||||
- CHANGELOG.md
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
.*/
|
||||
3rdparty/
|
||||
node_modules/
|
||||
dist/
|
||||
data/
|
||||
datumaro/
|
||||
keys/
|
||||
logs/
|
||||
static/
|
||||
templates/
|
||||
@ -1,53 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// Copyright (C) 2018-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": false,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true,
|
||||
"qunit": true,
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es6: true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "script",
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
"plugins": [
|
||||
"security",
|
||||
"no-unsanitized",
|
||||
"no-unsafe-innerhtml",
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:security/recommended",
|
||||
"plugin:no-unsanitized/DOM",
|
||||
"airbnb-base",
|
||||
],
|
||||
"rules": {
|
||||
"no-await-in-loop": [0],
|
||||
"global-require": [0],
|
||||
"no-new": [0],
|
||||
"class-methods-use-this": [0],
|
||||
"no-restricted-properties": [0, {
|
||||
"object": "Math",
|
||||
"property": "pow",
|
||||
}],
|
||||
"no-param-reassign": [0],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
|
||||
"no-continue": [0],
|
||||
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
|
||||
// This rule actual for user input data on the node.js environment mainly.
|
||||
"security/detect-object-injection": 0,
|
||||
"indent": ["warn", 4],
|
||||
// recently added to airbnb
|
||||
"max-classes-per-file": [0],
|
||||
// it was opposite before and our code has been written according to previous rule
|
||||
"arrow-parens": [0],
|
||||
// object spread is a modern ECMA standard. Let's do not use it without babel
|
||||
"prefer-object-spread": [0],
|
||||
plugins: ['eslint-plugin-header'],
|
||||
extends: ['eslint:recommended', 'prettier'],
|
||||
rules: {
|
||||
'header/header': [2, 'line', [{
|
||||
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2020 Intel Corporation',
|
||||
template: ' Copyright (C) 2020 Intel Corporation'
|
||||
}, '', ' SPDX-License-Identifier: MIT']],
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
{
|
||||
"all": true,
|
||||
"compact": false,
|
||||
"extension": [
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"**/3rdparty/*",
|
||||
"**/tests/*"
|
||||
],
|
||||
"parser-plugins": [
|
||||
"typescript"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
.*/
|
||||
3rdparty/
|
||||
node_modules/
|
||||
dist/
|
||||
data/
|
||||
datumaro/
|
||||
keys/
|
||||
logs/
|
||||
static/
|
||||
templates/
|
||||
@ -0,0 +1,27 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.json", "*.yml", "*.yaml", "*.md"],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,16 +1,16 @@
|
||||
exports.settings = {bullet: '*', paddedTable: false}
|
||||
exports.settings = { bullet: '*', paddedTable: false };
|
||||
|
||||
exports.plugins = [
|
||||
'remark-preset-lint-recommended',
|
||||
'remark-preset-lint-consistent',
|
||||
['remark-preset-lint-markdown-style-guide', 'mixed'],
|
||||
['remark-lint-no-dead-urls', { skipOffline: true }],
|
||||
['remark-lint-maximum-line-length', 120],
|
||||
['remark-lint-maximum-heading-length', 120],
|
||||
['remark-lint-strong-marker', "*"],
|
||||
['remark-lint-emphasis-marker', "_"],
|
||||
['remark-lint-unordered-list-marker-style', "-"],
|
||||
['remark-lint-ordered-list-marker-style', "."],
|
||||
['remark-lint-no-file-name-irregular-characters', false],
|
||||
['remark-lint-list-item-spacing', false],
|
||||
]
|
||||
'remark-preset-lint-recommended',
|
||||
'remark-preset-lint-consistent',
|
||||
['remark-preset-lint-markdown-style-guide', 'mixed'],
|
||||
['remark-lint-no-dead-urls', { skipOffline: true }],
|
||||
['remark-lint-maximum-line-length', 120],
|
||||
['remark-lint-maximum-heading-length', 120],
|
||||
['remark-lint-strong-marker', '*'],
|
||||
['remark-lint-emphasis-marker', '_'],
|
||||
['remark-lint-unordered-list-marker-style', '-'],
|
||||
['remark-lint-ordered-list-marker-style', '.'],
|
||||
['remark-lint-no-file-name-irregular-characters', false],
|
||||
['remark-lint-list-item-spacing', false],
|
||||
];
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"value-keyword-case": null,
|
||||
"selector-combinator-space-after": null,
|
||||
"no-descending-specificity": null,
|
||||
"at-rule-no-unknown": [true, {
|
||||
"ignoreAtRules": ["extend"]
|
||||
}],
|
||||
"selector-type-no-unknown": [true, {
|
||||
"ignoreTypes": ["first-child"]
|
||||
}]
|
||||
},
|
||||
"ignoreFiles": [
|
||||
"**/*.js",
|
||||
"**/*.ts",
|
||||
"**/*.py"
|
||||
"extends": "stylelint-config-standard",
|
||||
"rules": {
|
||||
"indentation": 4,
|
||||
"value-keyword-case": null,
|
||||
"selector-combinator-space-after": null,
|
||||
"no-descending-specificity": null,
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreAtRules": ["extend"]
|
||||
}
|
||||
],
|
||||
"selector-type-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreTypes": ["first-child"]
|
||||
}
|
||||
]
|
||||
},
|
||||
"ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"]
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
PYTHONPATH="datumaro/:$PYTHONPATH"
|
||||
@ -1,3 +1,3 @@
|
||||
http.host: 0.0.0.0
|
||||
script.painless.regex.enabled: true
|
||||
path.repo: ["/usr/share/elasticsearch/data/backup"]
|
||||
path.repo: ['/usr/share/elasticsearch/data/backup']
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
server.host: 0.0.0.0
|
||||
elasticsearch.url: http://elasticsearch:9200
|
||||
elasticsearch.requestHeadersWhitelist: [ "cookie", "authorization", "x-forwarded-user" ]
|
||||
kibana.defaultAppId: "discover"
|
||||
elasticsearch.requestHeadersWhitelist: ['cookie', 'authorization', 'x-forwarded-user']
|
||||
kibana.defaultAppId: 'discover'
|
||||
server.basePath: /analytics
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
## Serverless for Computer Vision Annotation Tool (CVAT)
|
||||
|
||||
### Run docker container
|
||||
|
||||
```bash
|
||||
# From project root directory
|
||||
docker-compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
|
||||
```
|
||||
@ -0,0 +1,28 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
serverless:
|
||||
container_name: nuclio
|
||||
image: quay.io/nuclio/dashboard:1.5.8-amd64
|
||||
restart: always
|
||||
networks:
|
||||
default:
|
||||
aliases:
|
||||
- nuclio
|
||||
volumes:
|
||||
- /tmp:/tmp
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
http_proxy:
|
||||
https_proxy:
|
||||
no_proxy: 172.28.0.1,${no_proxy}
|
||||
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
|
||||
ports:
|
||||
- '8070:8070'
|
||||
|
||||
cvat:
|
||||
environment:
|
||||
CVAT_SERVERLESS: 1
|
||||
no_proxy: kibana,logstash,nuclio,${no_proxy}
|
||||
|
||||
volumes:
|
||||
cvat_events:
|
||||
@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import consts from './consts';
|
||||
|
||||
export default class Crosshair {
|
||||
private x: SVG.Line | null;
|
||||
private y: SVG.Line | null;
|
||||
private canvas: SVG.Container | null;
|
||||
|
||||
public constructor() {
|
||||
this.x = null;
|
||||
this.y = null;
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
|
||||
if (this.canvas && this.canvas !== canvas) {
|
||||
if (this.x) this.x.remove();
|
||||
if (this.y) this.y.remove();
|
||||
this.x = null;
|
||||
this.y = null;
|
||||
}
|
||||
|
||||
this.canvas = canvas;
|
||||
this.x = this.canvas
|
||||
.line(0, y, this.canvas.node.clientWidth, y)
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
|
||||
})
|
||||
.addClass('cvat_canvas_crosshair');
|
||||
|
||||
this.y = this.canvas
|
||||
.line(x, 0, x, this.canvas.node.clientHeight)
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
|
||||
})
|
||||
.addClass('cvat_canvas_crosshair');
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this.x) {
|
||||
this.x.remove();
|
||||
this.x = null;
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.remove();
|
||||
this.y = null;
|
||||
}
|
||||
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
public move(x: number, y: number): void {
|
||||
if (this.x) {
|
||||
this.x.attr({ y1: y, y2: y });
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.attr({ x1: x, x2: x });
|
||||
}
|
||||
}
|
||||
|
||||
public scale(scale: number): void {
|
||||
if (this.x) {
|
||||
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,268 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import consts from './consts';
|
||||
import Crosshair from './crosshair';
|
||||
import { translateToSVG } from './shared';
|
||||
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
|
||||
|
||||
export interface InteractionHandler {
|
||||
transform(geometry: Geometry): void;
|
||||
interact(interactData: InteractionData): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class InteractionHandlerImpl implements InteractionHandler {
|
||||
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
|
||||
private geometry: Geometry;
|
||||
private canvas: SVG.Container;
|
||||
private interactionData: InteractionData;
|
||||
private cursorPosition: { x: number; y: number };
|
||||
private shapesWereUpdated: boolean;
|
||||
private interactionShapes: SVG.Shape[];
|
||||
private currentInteractionShape: SVG.Shape | null;
|
||||
private crosshair: Crosshair;
|
||||
|
||||
private prepareResult(): InteractionResult[] {
|
||||
return this.interactionShapes.map(
|
||||
(shape: SVG.Shape): InteractionResult => {
|
||||
if (shape.type === 'circle') {
|
||||
const points = [(shape as SVG.Circle).cx(), (shape as SVG.Circle).cy()];
|
||||
return {
|
||||
points: points.map((coord: number): number => coord - this.geometry.offset),
|
||||
shapeType: 'points',
|
||||
button: shape.attr('stroke') === 'green' ? 0 : 2,
|
||||
};
|
||||
}
|
||||
|
||||
const bbox = ((shape.node as any) as SVGRectElement).getBBox();
|
||||
const points = [bbox.x, bbox.y, bbox.x + bbox.width, bbox.y + bbox.height];
|
||||
return {
|
||||
points: points.map((coord: number): number => coord - this.geometry.offset),
|
||||
shapeType: 'rectangle',
|
||||
button: 0,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private shouldRaiseEvent(ctrlKey: boolean): boolean {
|
||||
const { interactionData, interactionShapes, shapesWereUpdated } = this;
|
||||
const { minPosVertices, minNegVertices, enabled } = interactionData;
|
||||
|
||||
const positiveShapes = interactionShapes.filter(
|
||||
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') === 'green',
|
||||
);
|
||||
const negativeShapes = interactionShapes.filter(
|
||||
(shape: SVG.Shape): boolean => (shape as any).attr('stroke') !== 'green',
|
||||
);
|
||||
|
||||
if (interactionData.shapeType === 'rectangle') {
|
||||
return enabled && !ctrlKey && !!interactionShapes.length;
|
||||
}
|
||||
|
||||
const minimumVerticesAchieved =
|
||||
(typeof minPosVertices === 'undefined' || minPosVertices <= positiveShapes.length) &&
|
||||
(typeof minNegVertices === 'undefined' || minPosVertices <= negativeShapes.length);
|
||||
return enabled && !ctrlKey && minimumVerticesAchieved && shapesWereUpdated;
|
||||
}
|
||||
|
||||
private addCrosshair(): void {
|
||||
const { x, y } = this.cursorPosition;
|
||||
this.crosshair.show(this.canvas, x, y, this.geometry.scale);
|
||||
}
|
||||
|
||||
private removeCrosshair(): void {
|
||||
this.crosshair.hide();
|
||||
}
|
||||
|
||||
private interactPoints(): void {
|
||||
const eventListener = (e: MouseEvent): void => {
|
||||
if ((e.button === 0 || e.button === 2) && !e.altKey) {
|
||||
e.preventDefault();
|
||||
const [cx, cy] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
|
||||
this.currentInteractionShape = this.canvas
|
||||
.circle((consts.BASE_POINT_SIZE * 2) / this.geometry.scale)
|
||||
.center(cx, cy)
|
||||
.fill('white')
|
||||
.stroke(e.button === 0 ? 'green' : 'red')
|
||||
.addClass('cvat_interaction_point')
|
||||
.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
|
||||
this.interactionShapes.push(this.currentInteractionShape);
|
||||
this.shapesWereUpdated = true;
|
||||
if (this.shouldRaiseEvent(e.ctrlKey)) {
|
||||
this.onInteraction(this.prepareResult(), true, false);
|
||||
}
|
||||
|
||||
const self = this.currentInteractionShape;
|
||||
self.on('mouseenter', (): void => {
|
||||
self.attr({
|
||||
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
|
||||
self.on('mousedown', (_e: MouseEvent): void => {
|
||||
_e.preventDefault();
|
||||
_e.stopPropagation();
|
||||
self.remove();
|
||||
this.interactionShapes = this.interactionShapes.filter(
|
||||
(shape: SVG.Shape): boolean => shape !== self,
|
||||
);
|
||||
this.shapesWereUpdated = true;
|
||||
if (this.shouldRaiseEvent(_e.ctrlKey)) {
|
||||
this.onInteraction(this.prepareResult(), true, false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.on('mouseleave', (): void => {
|
||||
self.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
|
||||
self.off('mousedown');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// clear this listener in relese()
|
||||
this.canvas.on('mousedown.interaction', eventListener);
|
||||
}
|
||||
|
||||
private interactRectangle(): void {
|
||||
let initialized = false;
|
||||
const eventListener = (e: MouseEvent): void => {
|
||||
if (e.button === 0 && !e.altKey) {
|
||||
if (!initialized) {
|
||||
(this.currentInteractionShape as any).draw(e, { snapToGrid: 0.1 });
|
||||
initialized = true;
|
||||
} else {
|
||||
(this.currentInteractionShape as any).draw(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.currentInteractionShape = this.canvas.rect();
|
||||
this.canvas.on('mousedown.interaction', eventListener);
|
||||
this.currentInteractionShape
|
||||
.on('drawstop', (): void => {
|
||||
this.interactionShapes.push(this.currentInteractionShape);
|
||||
this.shapesWereUpdated = true;
|
||||
|
||||
this.canvas.off('mousedown.interaction', eventListener);
|
||||
this.interact({ enabled: false });
|
||||
})
|
||||
.addClass('cvat_canvas_shape_drawing')
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
}
|
||||
|
||||
private initInteraction(): void {
|
||||
if (this.interactionData.crosshair) {
|
||||
this.addCrosshair();
|
||||
}
|
||||
}
|
||||
|
||||
private startInteraction(): void {
|
||||
if (this.interactionData.shapeType === 'rectangle') {
|
||||
this.interactRectangle();
|
||||
} else if (this.interactionData.shapeType === 'points') {
|
||||
this.interactPoints();
|
||||
} else {
|
||||
throw new Error('Interactor implementation supports only rectangle and points');
|
||||
}
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
if (this.crosshair) {
|
||||
this.removeCrosshair();
|
||||
}
|
||||
|
||||
this.canvas.off('mousedown.interaction');
|
||||
this.interactionShapes.forEach((shape: SVG.Shape): SVG.Shape => shape.remove());
|
||||
this.interactionShapes = [];
|
||||
if (this.currentInteractionShape) {
|
||||
this.currentInteractionShape.remove();
|
||||
this.currentInteractionShape = null;
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void,
|
||||
canvas: SVG.Container,
|
||||
geometry: Geometry,
|
||||
) {
|
||||
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
|
||||
this.shapesWereUpdated = false;
|
||||
onInteraction(shapes, shapesUpdated, isDone);
|
||||
};
|
||||
this.canvas = canvas;
|
||||
this.geometry = geometry;
|
||||
this.shapesWereUpdated = false;
|
||||
this.interactionShapes = [];
|
||||
this.interactionData = { enabled: false };
|
||||
this.currentInteractionShape = null;
|
||||
this.crosshair = new Crosshair();
|
||||
this.cursorPosition = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
};
|
||||
|
||||
this.canvas.on('mousemove.interaction', (e: MouseEvent): void => {
|
||||
const [x, y] = translateToSVG((this.canvas.node as any) as SVGSVGElement, [e.clientX, e.clientY]);
|
||||
this.cursorPosition = { x, y };
|
||||
if (this.crosshair) {
|
||||
this.crosshair.move(x, y);
|
||||
}
|
||||
});
|
||||
|
||||
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
|
||||
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) {
|
||||
// 17 is ctrl
|
||||
this.onInteraction(this.prepareResult(), true, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
|
||||
if (this.crosshair) {
|
||||
this.crosshair.scale(this.geometry.scale);
|
||||
}
|
||||
|
||||
const shapesToBeScaled = this.currentInteractionShape
|
||||
? [...this.interactionShapes, this.currentInteractionShape]
|
||||
: [...this.interactionShapes];
|
||||
for (const shape of shapesToBeScaled) {
|
||||
if (shape.type === 'circle') {
|
||||
(shape as SVG.Circle).radius(consts.BASE_POINT_SIZE / this.geometry.scale);
|
||||
shape.attr('stroke-width', consts.POINTS_STROKE_WIDTH / this.geometry.scale);
|
||||
} else {
|
||||
shape.attr('stroke-width', consts.BASE_STROKE_WIDTH / this.geometry.scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interact(interactionData: InteractionData): void {
|
||||
if (interactionData.enabled) {
|
||||
this.interactionData = interactionData;
|
||||
this.initInteraction();
|
||||
this.startInteraction();
|
||||
} else {
|
||||
this.onInteraction(this.prepareResult(), this.shouldRaiseEvent(false), true);
|
||||
this.release();
|
||||
this.interactionData = interactionData;
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onInteraction(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
|
||||
import consts from './consts';
|
||||
import { translateToSVG } from './shared';
|
||||
import { Geometry } from './canvasModel';
|
||||
|
||||
export interface RegionSelector {
|
||||
select(enabled: boolean): void;
|
||||
cancel(): void;
|
||||
transform(geometry: Geometry): void;
|
||||
}
|
||||
|
||||
export class RegionSelectorImpl implements RegionSelector {
|
||||
private onRegionSelected: (points?: number[]) => void;
|
||||
private geometry: Geometry;
|
||||
private canvas: SVG.Container;
|
||||
private selectionRect: SVG.Rect | null;
|
||||
private startSelectionPoint: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
private getSelectionBox(event: MouseEvent): { xtl: number; ytl: number; xbr: number; ybr: number } {
|
||||
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
|
||||
const stopSelectionPoint = {
|
||||
x: point[0],
|
||||
y: point[1],
|
||||
};
|
||||
|
||||
return {
|
||||
xtl: Math.min(this.startSelectionPoint.x, stopSelectionPoint.x),
|
||||
ytl: Math.min(this.startSelectionPoint.y, stopSelectionPoint.y),
|
||||
xbr: Math.max(this.startSelectionPoint.x, stopSelectionPoint.x),
|
||||
ybr: Math.max(this.startSelectionPoint.y, stopSelectionPoint.y),
|
||||
};
|
||||
}
|
||||
|
||||
private onMouseMove = (event: MouseEvent): void => {
|
||||
if (this.selectionRect) {
|
||||
const box = this.getSelectionBox(event);
|
||||
|
||||
this.selectionRect.attr({
|
||||
x: box.xtl,
|
||||
y: box.ytl,
|
||||
width: box.xbr - box.xtl,
|
||||
height: box.ybr - box.ytl,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private onMouseDown = (event: MouseEvent): void => {
|
||||
if (!this.selectionRect && !event.altKey) {
|
||||
const point = translateToSVG((this.canvas.node as any) as SVGSVGElement, [event.clientX, event.clientY]);
|
||||
this.startSelectionPoint = {
|
||||
x: point[0],
|
||||
y: point[1],
|
||||
};
|
||||
|
||||
this.selectionRect = this.canvas
|
||||
.rect()
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
|
||||
})
|
||||
.addClass('cvat_canvas_shape_region_selection');
|
||||
this.selectionRect.attr({ ...this.startSelectionPoint });
|
||||
}
|
||||
};
|
||||
|
||||
private onMouseUp = (): void => {
|
||||
const { offset } = this.geometry;
|
||||
if (this.selectionRect) {
|
||||
const {
|
||||
w, h, x, y, x2, y2,
|
||||
} = this.selectionRect.bbox();
|
||||
this.selectionRect.remove();
|
||||
this.selectionRect = null;
|
||||
if (w === 0 && h === 0) {
|
||||
this.onRegionSelected([x - offset, y - offset]);
|
||||
} else {
|
||||
this.onRegionSelected([x - offset, y - offset, x2 - offset, y2 - offset]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private startSelection(): void {
|
||||
this.canvas.node.addEventListener('mousemove', this.onMouseMove);
|
||||
this.canvas.node.addEventListener('mousedown', this.onMouseDown);
|
||||
this.canvas.node.addEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
private stopSelection(): void {
|
||||
this.canvas.node.removeEventListener('mousemove', this.onMouseMove);
|
||||
this.canvas.node.removeEventListener('mousedown', this.onMouseDown);
|
||||
this.canvas.node.removeEventListener('mouseup', this.onMouseUp);
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.stopSelection();
|
||||
}
|
||||
|
||||
public constructor(onRegionSelected: (points?: number[]) => void, canvas: SVG.Container, geometry: Geometry) {
|
||||
this.onRegionSelected = onRegionSelected;
|
||||
this.geometry = geometry;
|
||||
this.canvas = canvas;
|
||||
this.selectionRect = null;
|
||||
}
|
||||
|
||||
public select(enabled: boolean): void {
|
||||
if (enabled) {
|
||||
this.startSelection();
|
||||
} else {
|
||||
this.release();
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onRegionSelected();
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
if (this.selectionRect) {
|
||||
this.selectionRect.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"preserveConstEnums": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"declarationDir": "dist/declaration",
|
||||
"paths": {
|
||||
"cvat-canvas.node": ["dist/cvat-canvas.node"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/typescript/*.ts"
|
||||
]
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"emitDeclarationOnly": true,
|
||||
"module": "es6",
|
||||
"target": "es6",
|
||||
"noImplicitAny": true,
|
||||
"preserveConstEnums": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"declarationDir": "dist/declaration",
|
||||
"paths": {
|
||||
"cvat-canvas.node": ["dist/cvat-canvas.node"]
|
||||
}
|
||||
},
|
||||
"include": ["src/typescript/*.ts"]
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
webpack.config.js
|
||||
@ -1,55 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Intel Corporation
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
// Copyright (C) 2018-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
"env": {
|
||||
"node": false,
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true,
|
||||
"qunit": true,
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es6: true,
|
||||
'jest/globals': true,
|
||||
},
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint",
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2018,
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2018,
|
||||
},
|
||||
"plugins": [
|
||||
"security",
|
||||
"no-unsanitized",
|
||||
"no-unsafe-innerhtml",
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:security/recommended",
|
||||
"plugin:no-unsanitized/DOM",
|
||||
"airbnb-base",
|
||||
],
|
||||
"rules": {
|
||||
"no-await-in-loop": [0],
|
||||
"global-require": [0],
|
||||
"no-new": [0],
|
||||
"class-methods-use-this": [0],
|
||||
"no-restricted-properties": [0, {
|
||||
"object": "Math",
|
||||
"property": "pow",
|
||||
}],
|
||||
"no-plusplus": [0],
|
||||
"no-param-reassign": [0],
|
||||
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
|
||||
"no-restricted-syntax": [0, {"selector": "ForOfStatement"}],
|
||||
"no-continue": [0],
|
||||
"no-unsafe-innerhtml/no-unsafe-innerhtml": 1,
|
||||
plugins: ['security', 'jest', 'no-unsafe-innerhtml', 'no-unsanitized'],
|
||||
extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'],
|
||||
rules: {
|
||||
'no-await-in-loop': [0],
|
||||
'global-require': [0],
|
||||
'no-new': [0],
|
||||
'class-methods-use-this': [0],
|
||||
'no-restricted-properties': [
|
||||
0,
|
||||
{
|
||||
object: 'Math',
|
||||
property: 'pow',
|
||||
},
|
||||
],
|
||||
'no-plusplus': [0],
|
||||
'no-param-reassign': [0],
|
||||
'no-underscore-dangle': ['error', { allowAfterThis: true }],
|
||||
'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
|
||||
'no-continue': [0],
|
||||
'no-unsafe-innerhtml/no-unsafe-innerhtml': 1,
|
||||
// This rule actual for user input data on the node.js environment mainly.
|
||||
"security/detect-object-injection": 0,
|
||||
"indent": ["warn", 4],
|
||||
"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"
|
||||
"max-classes-per-file": [0],
|
||||
'security/detect-object-injection': 0,
|
||||
indent: ['warn', 4],
|
||||
'no-useless-constructor': 0,
|
||||
'func-names': [0],
|
||||
'valid-typeof': [0],
|
||||
'no-console': [0],
|
||||
'max-classes-per-file': [0],
|
||||
'max-len': ['warn', { code: 120 }],
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,32 +1,15 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Intel Corporation
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
/* global
|
||||
require:false
|
||||
*/
|
||||
// Copyright (C) 2019-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const { defaults } = require('jest-config');
|
||||
|
||||
module.exports = {
|
||||
coverageDirectory: 'reports/coverage',
|
||||
coverageReporters: ['lcov'],
|
||||
moduleFileExtensions: [
|
||||
...defaults.moduleFileExtensions,
|
||||
'ts',
|
||||
'tsx',
|
||||
],
|
||||
reporters: [
|
||||
'default',
|
||||
['jest-junit', { outputDirectory: 'reports/junit' }],
|
||||
],
|
||||
testMatch: [
|
||||
'**/tests/**/*.js',
|
||||
],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/tests/mocks/*',
|
||||
],
|
||||
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
|
||||
reporters: ['default', ['jest-junit', { outputDirectory: 'reports/junit' }]],
|
||||
testMatch: ['**/tests/**/*.js'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/tests/mocks/*'],
|
||||
automock: false,
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,153 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const User = require('./user');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { negativeIDGenerator } = require('./common');
|
||||
|
||||
/**
|
||||
* Class representing a single comment
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Comment {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
message: undefined,
|
||||
created_date: undefined,
|
||||
updated_date: undefined,
|
||||
removed: false,
|
||||
author: undefined,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.author && !(data.author instanceof User)) data.author = new User(data.author);
|
||||
|
||||
if (typeof id === 'undefined') {
|
||||
data.id = negativeIDGenerator();
|
||||
}
|
||||
if (typeof data.created_date === 'undefined') {
|
||||
data.created_date = new Date().toISOString();
|
||||
}
|
||||
|
||||
Object.defineProperties(
|
||||
this,
|
||||
Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* @name message
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
message: {
|
||||
get: () => data.message,
|
||||
set: (value) => {
|
||||
if (!value.trim().length) {
|
||||
throw new ArgumentError('Value must not be empty');
|
||||
}
|
||||
data.message = value;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @name createdDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
createdDate: {
|
||||
get: () => data.created_date,
|
||||
},
|
||||
/**
|
||||
* @name updatedDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
updatedDate: {
|
||||
get: () => data.updated_date,
|
||||
},
|
||||
/**
|
||||
* Instance of a user who has created the comment
|
||||
* @name author
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
author: {
|
||||
get: () => data.author,
|
||||
},
|
||||
/**
|
||||
* @name removed
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @instance
|
||||
*/
|
||||
removed: {
|
||||
get: () => data.removed,
|
||||
set: (value) => {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new ArgumentError('Value must be a boolean value');
|
||||
}
|
||||
data.removed = value;
|
||||
},
|
||||
},
|
||||
__internal: {
|
||||
get: () => data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const data = {
|
||||
message: this.message,
|
||||
};
|
||||
|
||||
if (this.id > 0) {
|
||||
data.id = this.id;
|
||||
}
|
||||
if (this.createdDate) {
|
||||
data.created_date = this.createdDate;
|
||||
}
|
||||
if (this.updatedDate) {
|
||||
data.updated_date = this.updatedDate;
|
||||
}
|
||||
if (this.author) {
|
||||
data.author = this.author.serialize();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const data = this.serialize();
|
||||
const { author, ...updated } = data;
|
||||
return {
|
||||
...updated,
|
||||
author_id: author ? author.id : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Comment;
|
||||
@ -0,0 +1,335 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const quickhull = require('quickhull');
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const Comment = require('./comment');
|
||||
const User = require('./user');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { negativeIDGenerator } = require('./common');
|
||||
const serverProxy = require('./server-proxy');
|
||||
|
||||
/**
|
||||
* Class representing a single issue
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Issue {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
position: undefined,
|
||||
comment_set: [],
|
||||
frame: undefined,
|
||||
created_date: undefined,
|
||||
resolved_date: undefined,
|
||||
owner: undefined,
|
||||
resolver: undefined,
|
||||
removed: false,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
|
||||
if (data.resolver && !(data.resolver instanceof User)) data.resolver = new User(data.resolver);
|
||||
|
||||
if (data.comment_set) {
|
||||
data.comment_set = data.comment_set.map((comment) => new Comment(comment));
|
||||
}
|
||||
|
||||
if (typeof data.id === 'undefined') {
|
||||
data.id = negativeIDGenerator();
|
||||
}
|
||||
if (typeof data.created_date === 'undefined') {
|
||||
data.created_date = new Date().toISOString();
|
||||
}
|
||||
|
||||
Object.defineProperties(
|
||||
this,
|
||||
Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* Region of interests of the issue
|
||||
* @name position
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
position: {
|
||||
get: () => data.position,
|
||||
set: (value) => {
|
||||
if (Array.isArray(value) || value.some((coord) => typeof coord !== 'number')) {
|
||||
throw new ArgumentError(`Array of numbers is expected. Got ${value}`);
|
||||
}
|
||||
data.position = value;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* List of comments attached to the issue
|
||||
* @name comments
|
||||
* @type {module:API.cvat.classes.Comment[]}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
comments: {
|
||||
get: () => data.comment_set.filter((comment) => !comment.removed),
|
||||
},
|
||||
/**
|
||||
* @name frame
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
frame: {
|
||||
get: () => data.frame,
|
||||
},
|
||||
/**
|
||||
* @name createdDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
createdDate: {
|
||||
get: () => data.created_date,
|
||||
},
|
||||
/**
|
||||
* @name resolvedDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
resolvedDate: {
|
||||
get: () => data.resolved_date,
|
||||
},
|
||||
/**
|
||||
* An instance of a user who has raised the issue
|
||||
* @name owner
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
owner: {
|
||||
get: () => data.owner,
|
||||
},
|
||||
/**
|
||||
* An instance of a user who has resolved the issue
|
||||
* @name resolver
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
resolver: {
|
||||
get: () => data.resolver,
|
||||
},
|
||||
/**
|
||||
* @name removed
|
||||
* @type {boolean}
|
||||
* @memberof module:API.cvat.classes.Comment
|
||||
* @instance
|
||||
*/
|
||||
removed: {
|
||||
get: () => data.removed,
|
||||
set: (value) => {
|
||||
if (typeof value !== 'boolean') {
|
||||
throw new ArgumentError('Value must be a boolean value');
|
||||
}
|
||||
data.removed = value;
|
||||
},
|
||||
},
|
||||
__internal: {
|
||||
get: () => data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
static hull(coordinates) {
|
||||
if (coordinates.length > 4) {
|
||||
const points = coordinates.reduce((acc, coord, index, arr) => {
|
||||
if (index % 2) acc.push({ x: arr[index - 1], y: coord });
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return quickhull(points)
|
||||
.map((point) => [point.x, point.y])
|
||||
.flat();
|
||||
}
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CommentData
|
||||
* @property {number} [author] an ID of a user who has created the comment
|
||||
* @property {string} message a comment message
|
||||
* @global
|
||||
*/
|
||||
/**
|
||||
* Method appends a comment to the issue
|
||||
* For a new issue it saves comment locally, for a saved issue it saves comment on the server
|
||||
* @method comment
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @param {CommentData} data
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async comment(data) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.comment, data);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method resolves the issue
|
||||
* New issues are resolved locally, server-saved issues are resolved on the server
|
||||
* @method resolve
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @param {module:API.cvat.classes.User} user
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async resolve(user) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.resolve, user);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method resolves the issue
|
||||
* New issues are reopened locally, server-saved issues are reopened on the server
|
||||
* @method reopen
|
||||
* @memberof module:API.cvat.classes.Issue
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
async reopen() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Issue.prototype.reopen);
|
||||
return result;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const { comments } = this;
|
||||
const data = {
|
||||
position: this.position,
|
||||
frame: this.frame,
|
||||
comment_set: comments.map((comment) => comment.serialize()),
|
||||
};
|
||||
|
||||
if (this.id > 0) {
|
||||
data.id = this.id;
|
||||
}
|
||||
if (this.createdDate) {
|
||||
data.created_date = this.createdDate;
|
||||
}
|
||||
if (this.resolvedDate) {
|
||||
data.resolved_date = this.resolvedDate;
|
||||
}
|
||||
if (this.owner) {
|
||||
data.owner = this.owner.toJSON();
|
||||
}
|
||||
if (this.resolver) {
|
||||
data.resolver = this.resolver.toJSON();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const data = this.serialize();
|
||||
const { owner, resolver, ...updated } = data;
|
||||
return {
|
||||
...updated,
|
||||
comment_set: this.comments.map((comment) => comment.toJSON()),
|
||||
owner_id: owner ? owner.id : undefined,
|
||||
resolver_id: resolver ? resolver.id : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Issue.prototype.comment.implementation = async function (data) {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
|
||||
}
|
||||
if (typeof data.message !== 'string' || data.message.length < 1) {
|
||||
throw new ArgumentError(`Comment message must be a not empty string. Got ${data.message}`);
|
||||
}
|
||||
if (!(data.author instanceof User)) {
|
||||
throw new ArgumentError(`Author of the comment must a User instance. Got ${data.author}`);
|
||||
}
|
||||
|
||||
const comment = new Comment(data);
|
||||
const { id } = this;
|
||||
if (id >= 0) {
|
||||
const jsonified = comment.toJSON();
|
||||
jsonified.issue = id;
|
||||
const response = await serverProxy.comments.create(jsonified);
|
||||
const savedComment = new Comment(response);
|
||||
this.__internal.comment_set.push(savedComment);
|
||||
} else {
|
||||
this.__internal.comment_set.push(comment);
|
||||
}
|
||||
};
|
||||
|
||||
Issue.prototype.resolve.implementation = async function (user) {
|
||||
if (!(user instanceof User)) {
|
||||
throw new ArgumentError(`The argument "user" must be an instance of a User class. Got ${typeof user}`);
|
||||
}
|
||||
|
||||
const { id } = this;
|
||||
if (id >= 0) {
|
||||
const response = await serverProxy.issues.update(id, { resolver_id: user.id });
|
||||
this.__internal.resolved_date = response.resolved_date;
|
||||
this.__internal.resolver = new User(response.resolver);
|
||||
} else {
|
||||
this.__internal.resolved_date = new Date().toISOString();
|
||||
this.__internal.resolver = user;
|
||||
}
|
||||
};
|
||||
|
||||
Issue.prototype.reopen.implementation = async function () {
|
||||
const { id } = this;
|
||||
if (id >= 0) {
|
||||
const response = await serverProxy.issues.update(id, { resolver_id: null });
|
||||
this.__internal.resolved_date = response.resolved_date;
|
||||
this.__internal.resolver = response.resolver;
|
||||
} else {
|
||||
this.__internal.resolved_date = null;
|
||||
this.__internal.resolver = null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Issue;
|
||||
@ -0,0 +1,265 @@
|
||||
// Copyright (C) 2019-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
(() => {
|
||||
const PluginRegistry = require('./plugins');
|
||||
const serverProxy = require('./server-proxy');
|
||||
const { ArgumentError } = require('./exceptions');
|
||||
const { Task } = require('./session');
|
||||
const { Label } = require('./labels');
|
||||
const User = require('./user');
|
||||
|
||||
/**
|
||||
* Class representing a project
|
||||
* @memberof module:API.cvat.classes
|
||||
*/
|
||||
class Project {
|
||||
/**
|
||||
* In a fact you need use the constructor only if you want to create a project
|
||||
* @param {object} initialData - Object which is used for initalization
|
||||
* <br> It can contain keys:
|
||||
* <br> <li style="margin-left: 10px;"> name
|
||||
* <br> <li style="margin-left: 10px;"> labels
|
||||
*/
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
assignee: undefined,
|
||||
owner: undefined,
|
||||
bug_tracker: undefined,
|
||||
created_date: undefined,
|
||||
updated_date: undefined,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
data.labels = [];
|
||||
data.tasks = [];
|
||||
|
||||
if (Array.isArray(initialData.labels)) {
|
||||
for (const label of initialData.labels) {
|
||||
const classInstance = new Label(label);
|
||||
data.labels.push(classInstance);
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(initialData.tasks)) {
|
||||
for (const task of initialData.tasks) {
|
||||
const taskInstance = new Task(task);
|
||||
data.tasks.push(taskInstance);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperties(
|
||||
this,
|
||||
Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* @name name
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
name: {
|
||||
get: () => data.name,
|
||||
set: (value) => {
|
||||
if (!value.trim().length) {
|
||||
throw new ArgumentError('Value must not be empty');
|
||||
}
|
||||
data.name = value;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @name status
|
||||
* @type {module:API.cvat.enums.TaskStatus}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
status: {
|
||||
get: () => data.status,
|
||||
},
|
||||
/**
|
||||
* Instance of a user who was assigned for the project
|
||||
* @name assignee
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
assignee: {
|
||||
get: () => data.assignee,
|
||||
set: (assignee) => {
|
||||
if (assignee !== null && !(assignee instanceof User)) {
|
||||
throw new ArgumentError('Value must be a user instance');
|
||||
}
|
||||
data.assignee = assignee;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Instance of a user who has created the project
|
||||
* @name owner
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
owner: {
|
||||
get: () => data.owner,
|
||||
},
|
||||
/**
|
||||
* @name bugTracker
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
bugTracker: {
|
||||
get: () => data.bug_tracker,
|
||||
set: (tracker) => {
|
||||
data.bug_tracker = tracker;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @name createdDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Task
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
createdDate: {
|
||||
get: () => data.created_date,
|
||||
},
|
||||
/**
|
||||
* @name updatedDate
|
||||
* @type {string}
|
||||
* @memberof module:API.cvat.classes.Task
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
updatedDate: {
|
||||
get: () => data.updated_date,
|
||||
},
|
||||
/**
|
||||
* After project has been created value can be appended only.
|
||||
* @name labels
|
||||
* @type {module:API.cvat.classes.Label[]}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
labels: {
|
||||
get: () => [...data.labels],
|
||||
set: (labels) => {
|
||||
if (!Array.isArray(labels)) {
|
||||
throw new ArgumentError('Value must be an array of Labels');
|
||||
}
|
||||
|
||||
if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) {
|
||||
throw new ArgumentError(
|
||||
`Each array value must be an instance of Label. ${typeof label} was found`,
|
||||
);
|
||||
}
|
||||
|
||||
data.labels = [...labels];
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Tasks linked with the project
|
||||
* @name tasks
|
||||
* @type {module:API.cvat.classes.Task[]}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
tasks: {
|
||||
get: () => [...data.tasks],
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method updates data of a created project or creates new project from scratch
|
||||
* @method save
|
||||
* @returns {module:API.cvat.classes.Project}
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async save() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method deletes a task from a server
|
||||
* @method delete
|
||||
* @memberof module:API.cvat.classes.Project
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ServerError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async delete() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Project,
|
||||
};
|
||||
|
||||
Project.prototype.save.implementation = async function () {
|
||||
if (typeof this.id !== 'undefined') {
|
||||
const projectData = {
|
||||
name: this.name,
|
||||
assignee_id: this.assignee ? this.assignee.id : null,
|
||||
bug_tracker: this.bugTracker,
|
||||
labels: [...this.labels.map((el) => el.toJSON())],
|
||||
};
|
||||
|
||||
await serverProxy.projects.save(this.id, projectData);
|
||||
return this;
|
||||
}
|
||||
|
||||
const projectSpec = {
|
||||
name: this.name,
|
||||
labels: [...this.labels.map((el) => el.toJSON())],
|
||||
};
|
||||
|
||||
if (this.bugTracker) {
|
||||
projectSpec.bug_tracker = this.bugTracker;
|
||||
}
|
||||
|
||||
const project = await serverProxy.projects.create(projectSpec);
|
||||
return new Project(project);
|
||||
};
|
||||
|
||||
Project.prototype.delete.implementation = async function () {
|
||||
const result = await serverProxy.projects.delete(this.id);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
@ -0,0 +1,397 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const store = require('store');
|
||||
|
||||
const PluginRegistry = require('./plugins');
|
||||
const Issue = require('./issue');
|
||||
const User = require('./user');
|
||||
const { ArgumentError, DataError } = require('./exceptions');
|
||||
const { ReviewStatus } = require('./enums');
|
||||
const { negativeIDGenerator } = require('./common');
|
||||
const serverProxy = require('./server-proxy');
|
||||
|
||||
/**
|
||||
* Class representing a single review
|
||||
* @memberof module:API.cvat.classes
|
||||
* @hideconstructor
|
||||
*/
|
||||
class Review {
|
||||
constructor(initialData) {
|
||||
const data = {
|
||||
id: undefined,
|
||||
job: undefined,
|
||||
issue_set: [],
|
||||
estimated_quality: undefined,
|
||||
status: undefined,
|
||||
reviewer: undefined,
|
||||
assignee: undefined,
|
||||
reviewed_frames: undefined,
|
||||
reviewed_states: undefined,
|
||||
};
|
||||
|
||||
for (const property in data) {
|
||||
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
|
||||
data[property] = initialData[property];
|
||||
}
|
||||
}
|
||||
|
||||
if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
|
||||
if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);
|
||||
|
||||
data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
|
||||
data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
|
||||
if (data.issue_set) {
|
||||
data.issue_set = data.issue_set.map((issue) => new Issue(issue));
|
||||
}
|
||||
|
||||
if (typeof data.id === 'undefined') {
|
||||
data.id = negativeIDGenerator();
|
||||
}
|
||||
|
||||
Object.defineProperties(
|
||||
this,
|
||||
Object.freeze({
|
||||
/**
|
||||
* @name id
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
id: {
|
||||
get: () => data.id,
|
||||
},
|
||||
/**
|
||||
* An identifier of a job the review is attached to
|
||||
* @name job
|
||||
* @type {integer}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
job: {
|
||||
get: () => data.job,
|
||||
},
|
||||
/**
|
||||
* List of attached issues
|
||||
* @name issues
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
*/
|
||||
issues: {
|
||||
get: () => data.issue_set.filter((issue) => !issue.removed),
|
||||
},
|
||||
/**
|
||||
* Estimated quality of the review
|
||||
* @name estimatedQuality
|
||||
* @type {number}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
estimatedQuality: {
|
||||
get: () => data.estimated_quality,
|
||||
set: (value) => {
|
||||
if (typeof value !== 'number' || value < 0 || value > 5) {
|
||||
throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
|
||||
}
|
||||
data.estimated_quality = value;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* @name status
|
||||
* @type {module:API.cvat.enums.ReviewStatus}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @instance
|
||||
* @readonly
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
status: {
|
||||
get: () => data.status,
|
||||
set: (status) => {
|
||||
const type = ReviewStatus;
|
||||
let valueInEnum = false;
|
||||
for (const value in type) {
|
||||
if (type[value] === status) {
|
||||
valueInEnum = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!valueInEnum) {
|
||||
throw new ArgumentError(
|
||||
'Value must be a value from the enumeration cvat.enums.ReviewStatus',
|
||||
);
|
||||
}
|
||||
|
||||
data.status = status;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* An instance of a user who has done the review
|
||||
* @name reviewer
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
*/
|
||||
reviewer: {
|
||||
get: () => data.reviewer,
|
||||
set: (reviewer) => {
|
||||
if (!(reviewer instanceof User)) {
|
||||
throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
|
||||
}
|
||||
|
||||
data.reviewer = reviewer;
|
||||
},
|
||||
},
|
||||
/**
|
||||
* An instance of a user who was assigned for annotation before the review
|
||||
* @name assignee
|
||||
* @type {module:API.cvat.classes.User}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
assignee: {
|
||||
get: () => data.assignee,
|
||||
},
|
||||
/**
|
||||
* A set of frames that have been visited during review
|
||||
* @name reviewedFrames
|
||||
* @type {number[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
reviewedFrames: {
|
||||
get: () => Array.from(data.reviewed_frames),
|
||||
},
|
||||
/**
|
||||
* A set of reviewed states (server IDs combined with frames)
|
||||
* @name reviewedFrames
|
||||
* @type {string[]}
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
*/
|
||||
reviewedStates: {
|
||||
get: () => Array.from(data.reviewed_states),
|
||||
},
|
||||
__internal: {
|
||||
get: () => data,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method appends a frame to a set of reviewed frames
|
||||
* Reviewed frames are saved only in local storage
|
||||
* @method reviewFrame
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {number} frame
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async reviewFrame(frame) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method appends a frame to a set of reviewed frames
|
||||
* Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
|
||||
* @method reviewStates
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {string[]} stateIDs
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async reviewStates(stateIDs) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} IssueData
|
||||
* @property {number} frame
|
||||
* @property {number[]} position
|
||||
* @property {number} owner
|
||||
* @property {CommentData[]} comment_set
|
||||
* @global
|
||||
*/
|
||||
/**
|
||||
* Method adds a new issue to the review
|
||||
* @method openIssue
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @param {IssueData} data
|
||||
* @returns {module:API.cvat.classes.Issue}
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.ArgumentError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async openIssue(data) {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method submits local review to the server
|
||||
* @method submit
|
||||
* @memberof module:API.cvat.classes.Review
|
||||
* @readonly
|
||||
* @instance
|
||||
* @async
|
||||
* @throws {module:API.cvat.exceptions.DataError}
|
||||
* @throws {module:API.cvat.exceptions.PluginError}
|
||||
*/
|
||||
async submit() {
|
||||
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
|
||||
return result;
|
||||
}
|
||||
|
||||
serialize() {
|
||||
const { issues, reviewedFrames, reviewedStates } = this;
|
||||
const data = {
|
||||
job: this.job,
|
||||
issue_set: issues.map((issue) => issue.serialize()),
|
||||
reviewed_frames: Array.from(reviewedFrames),
|
||||
reviewed_states: Array.from(reviewedStates),
|
||||
};
|
||||
|
||||
if (this.id > 0) {
|
||||
data.id = this.id;
|
||||
}
|
||||
if (typeof this.estimatedQuality !== 'undefined') {
|
||||
data.estimated_quality = this.estimatedQuality;
|
||||
}
|
||||
if (typeof this.status !== 'undefined') {
|
||||
data.status = this.status;
|
||||
}
|
||||
if (this.reviewer) {
|
||||
data.reviewer = this.reviewer.toJSON();
|
||||
}
|
||||
if (this.assignee) {
|
||||
data.reviewer = this.assignee.toJSON();
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
const data = this.serialize();
|
||||
const {
|
||||
reviewer,
|
||||
assignee,
|
||||
reviewed_frames: reviewedFrames,
|
||||
reviewed_states: reviewedStates,
|
||||
...updated
|
||||
} = data;
|
||||
|
||||
return {
|
||||
...updated,
|
||||
issue_set: this.issues.map((issue) => issue.toJSON()),
|
||||
reviewer_id: reviewer ? reviewer.id : undefined,
|
||||
assignee_id: assignee ? assignee.id : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async toLocalStorage() {
|
||||
const data = this.serialize();
|
||||
store.set(`job-${this.job}-review`, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
|
||||
Review.prototype.reviewFrame.implementation = function (frame) {
|
||||
if (!Number.isInteger(frame)) {
|
||||
throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
|
||||
}
|
||||
this.__internal.reviewed_frames.add(frame);
|
||||
};
|
||||
|
||||
Review.prototype.reviewStates.implementation = function (stateIDs) {
|
||||
if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
|
||||
throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
|
||||
}
|
||||
|
||||
stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
|
||||
};
|
||||
|
||||
Review.prototype.openIssue.implementation = async function (data) {
|
||||
if (typeof data !== 'object' || data === null) {
|
||||
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
|
||||
}
|
||||
|
||||
if (typeof data.frame !== 'number') {
|
||||
throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
|
||||
}
|
||||
|
||||
if (!(data.owner instanceof User)) {
|
||||
throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
|
||||
throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(data.comment_set)) {
|
||||
throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
|
||||
}
|
||||
|
||||
const copied = {
|
||||
frame: data.frame,
|
||||
position: Issue.hull(data.position),
|
||||
owner: data.owner,
|
||||
comment_set: [],
|
||||
};
|
||||
|
||||
const issue = new Issue(copied);
|
||||
|
||||
for (const comment of data.comment_set) {
|
||||
await issue.comment.implementation.call(issue, comment);
|
||||
}
|
||||
|
||||
this.__internal.issue_set.push(issue);
|
||||
return issue;
|
||||
};
|
||||
|
||||
Review.prototype.submit.implementation = async function () {
|
||||
if (typeof this.estimatedQuality === 'undefined') {
|
||||
throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
|
||||
}
|
||||
|
||||
if (typeof this.status === 'undefined') {
|
||||
throw new DataError('Review status is expected to be a string. Got "undefined"');
|
||||
}
|
||||
|
||||
if (this.id < 0) {
|
||||
const data = this.toJSON();
|
||||
|
||||
const response = await serverProxy.jobs.reviews.create(data);
|
||||
store.remove(`job-${this.job}-review`);
|
||||
this.__internal.id = response.id;
|
||||
this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
|
||||
this.__internal.estimated_quality = response.estimated_quality;
|
||||
this.__internal.status = response.status;
|
||||
|
||||
if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
|
||||
if (response.assignee) this.__internal.assignee = new User(response.assignee);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Review;
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,170 @@
|
||||
// Copyright (C) 2019-2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Setup mock for a server
|
||||
jest.mock('../../src/server-proxy', () => {
|
||||
const mock = require('../mocks/server-proxy.mock');
|
||||
return mock;
|
||||
});
|
||||
|
||||
// Initialize api
|
||||
window.cvat = require('../../src/api');
|
||||
|
||||
const { Task } = require('../../src/session');
|
||||
const { Project } = require('../../src/project');
|
||||
|
||||
describe('Feature: get projects', () => {
|
||||
test('get all projects', async () => {
|
||||
const result = await window.cvat.projects.get();
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(2);
|
||||
for (const el of result) {
|
||||
expect(el).toBeInstanceOf(Project);
|
||||
}
|
||||
});
|
||||
|
||||
test('get project by id', async () => {
|
||||
const result = await window.cvat.projects.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(Project);
|
||||
expect(result[0].id).toBe(2);
|
||||
expect(result[0].tasks).toHaveLength(1);
|
||||
expect(result[0].tasks[0]).toBeInstanceOf(Task);
|
||||
});
|
||||
|
||||
test('get a project by an unknown id', async () => {
|
||||
const result = await window.cvat.projects.get({
|
||||
id: 1,
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('get a project by an invalid id', async () => {
|
||||
expect(
|
||||
window.cvat.projects.get({
|
||||
id: '1',
|
||||
}),
|
||||
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
|
||||
test('get projects by filters', async () => {
|
||||
const result = await window.cvat.projects.get({
|
||||
status: 'completed',
|
||||
});
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toBeInstanceOf(Project);
|
||||
expect(result[0].id).toBe(2);
|
||||
expect(result[0].status).toBe('completed');
|
||||
});
|
||||
|
||||
test('get projects by invalid filters', async () => {
|
||||
expect(
|
||||
window.cvat.projects.get({
|
||||
unknown: '5',
|
||||
}),
|
||||
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: save a project', () => {
|
||||
test('save some changed fields in a project', async () => {
|
||||
let result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
result[0].bugTracker = 'newBugTracker';
|
||||
result[0].name = 'New Project Name';
|
||||
|
||||
result[0].save();
|
||||
|
||||
result = await window.cvat.tasks.get({
|
||||
id: 2,
|
||||
});
|
||||
|
||||
expect(result[0].bugTracker).toBe('newBugTracker');
|
||||
expect(result[0].name).toBe('New Project Name');
|
||||
});
|
||||
|
||||
test('save some new labels in a project', async () => {
|
||||
let result = await window.cvat.projects.get({
|
||||
id: 6,
|
||||
});
|
||||
|
||||
const labelsLength = result[0].labels.length;
|
||||
const newLabel = new window.cvat.classes.Label({
|
||||
name: "My boss's car",
|
||||
attributes: [
|
||||
{
|
||||
default_value: 'false',
|
||||
input_type: 'checkbox',
|
||||
mutable: true,
|
||||
name: 'parked',
|
||||
values: ['false'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
result[0].labels = [...result[0].labels, newLabel];
|
||||
result[0].save();
|
||||
|
||||
result = await window.cvat.projects.get({
|
||||
id: 6,
|
||||
});
|
||||
|
||||
expect(result[0].labels).toHaveLength(labelsLength + 1);
|
||||
const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car");
|
||||
expect(appendedLabel).toHaveLength(1);
|
||||
expect(appendedLabel[0].attributes).toHaveLength(1);
|
||||
expect(appendedLabel[0].attributes[0].name).toBe('parked');
|
||||
expect(appendedLabel[0].attributes[0].defaultValue).toBe('false');
|
||||
expect(appendedLabel[0].attributes[0].mutable).toBe(true);
|
||||
expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox');
|
||||
});
|
||||
|
||||
test('save new project without an id', async () => {
|
||||
const project = new window.cvat.classes.Project({
|
||||
name: 'New Empty Project',
|
||||
labels: [
|
||||
{
|
||||
name: 'car',
|
||||
attributes: [
|
||||
{
|
||||
default_value: 'false',
|
||||
input_type: 'checkbox',
|
||||
mutable: true,
|
||||
name: 'parked',
|
||||
values: ['false'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bug_tracker: 'bug tracker value',
|
||||
});
|
||||
|
||||
const result = await project.save();
|
||||
expect(typeof result.id).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Feature: delete a project', () => {
|
||||
test('delete a project', async () => {
|
||||
let result = await window.cvat.projects.get({
|
||||
id: 6,
|
||||
});
|
||||
|
||||
await result[0].delete();
|
||||
result = await window.cvat.projects.get({
|
||||
id: 6,
|
||||
});
|
||||
|
||||
expect(Array.isArray(result)).toBeTruthy();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue