Merge branch 'release-1.2.0'
commit
37d82f9005
@ -0,0 +1,10 @@
|
|||||||
|
.*/
|
||||||
|
3rdparty/
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
data/
|
||||||
|
datumaro/
|
||||||
|
keys/
|
||||||
|
logs/
|
||||||
|
static/
|
||||||
|
templates/
|
||||||
@ -1,53 +1,23 @@
|
|||||||
/*
|
// Copyright (C) 2018-2020 Intel Corporation
|
||||||
* Copyright (C) 2018 Intel Corporation
|
//
|
||||||
*
|
// SPDX-License-Identifier: MIT
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"env": {
|
env: {
|
||||||
"node": false,
|
node: true,
|
||||||
"browser": true,
|
browser: true,
|
||||||
"es6": true,
|
es6: true,
|
||||||
"jquery": true,
|
|
||||||
"qunit": true,
|
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
parserOptions: {
|
||||||
"sourceType": "script",
|
sourceType: 'module',
|
||||||
|
ecmaVersion: 2018,
|
||||||
},
|
},
|
||||||
"plugins": [
|
plugins: ['eslint-plugin-header'],
|
||||||
"security",
|
extends: ['eslint:recommended', 'prettier'],
|
||||||
"no-unsanitized",
|
rules: {
|
||||||
"no-unsafe-innerhtml",
|
'header/header': [2, 'line', [{
|
||||||
],
|
pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2020 Intel Corporation',
|
||||||
"extends": [
|
template: ' Copyright (C) 2020 Intel Corporation'
|
||||||
"eslint:recommended",
|
}, '', ' SPDX-License-Identifier: MIT']],
|
||||||
"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],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 +0,0 @@
|
|||||||
PYTHONPATH="datumaro/:$PYTHONPATH"
|
|
||||||
@ -1,3 +1,3 @@
|
|||||||
http.host: 0.0.0.0
|
http.host: 0.0.0.0
|
||||||
script.painless.regex.enabled: true
|
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
|
server.host: 0.0.0.0
|
||||||
elasticsearch.url: http://elasticsearch:9200
|
elasticsearch.url: http://elasticsearch:9200
|
||||||
elasticsearch.requestHeadersWhitelist: [ "cookie", "authorization", "x-forwarded-user" ]
|
elasticsearch.requestHeadersWhitelist: ['cookie', 'authorization', 'x-forwarded-user']
|
||||||
kibana.defaultAppId: "discover"
|
kibana.defaultAppId: 'discover'
|
||||||
server.basePath: /analytics
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
webpack.config.js
|
||||||
@ -1,55 +1,47 @@
|
|||||||
/*
|
// Copyright (C) 2018-2020 Intel Corporation
|
||||||
* Copyright (C) 2018 Intel Corporation
|
//
|
||||||
*
|
// SPDX-License-Identifier: MIT
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
"env": {
|
env: {
|
||||||
"node": false,
|
node: true,
|
||||||
"browser": true,
|
browser: true,
|
||||||
"es6": true,
|
es6: true,
|
||||||
"jquery": true,
|
'jest/globals': true,
|
||||||
"qunit": true,
|
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
parserOptions: {
|
||||||
"parser": "babel-eslint",
|
parser: 'babel-eslint',
|
||||||
"sourceType": "module",
|
sourceType: 'module',
|
||||||
"ecmaVersion": 2018,
|
ecmaVersion: 2018,
|
||||||
|
},
|
||||||
|
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',
|
||||||
},
|
},
|
||||||
"plugins": [
|
|
||||||
"security",
|
|
||||||
"no-unsanitized",
|
|
||||||
"no-unsafe-innerhtml",
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:security/recommended",
|
|
||||||
"plugin:no-unsanitized/DOM",
|
|
||||||
"airbnb-base",
|
|
||||||
],
|
],
|
||||||
"rules": {
|
'no-plusplus': [0],
|
||||||
"no-await-in-loop": [0],
|
'no-param-reassign': [0],
|
||||||
"global-require": [0],
|
'no-underscore-dangle': ['error', { allowAfterThis: true }],
|
||||||
"no-new": [0],
|
'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
|
||||||
"class-methods-use-this": [0],
|
'no-continue': [0],
|
||||||
"no-restricted-properties": [0, {
|
'no-unsafe-innerhtml/no-unsafe-innerhtml': 1,
|
||||||
"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.
|
// This rule actual for user input data on the node.js environment mainly.
|
||||||
"security/detect-object-injection": 0,
|
'security/detect-object-injection': 0,
|
||||||
"indent": ["warn", 4],
|
indent: ['warn', 4],
|
||||||
"no-useless-constructor": 0,
|
'no-useless-constructor': 0,
|
||||||
"func-names": [0],
|
'func-names': [0],
|
||||||
"valid-typeof": [0],
|
'valid-typeof': [0],
|
||||||
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
|
'no-console': [0],
|
||||||
"max-classes-per-file": [0],
|
'max-classes-per-file': [0],
|
||||||
|
'max-len': ['warn', { code: 120 }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,32 +1,15 @@
|
|||||||
/*
|
// Copyright (C) 2019-2020 Intel Corporation
|
||||||
* Copyright (C) 2019 Intel Corporation
|
//
|
||||||
* SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
*/
|
|
||||||
|
|
||||||
/* global
|
|
||||||
require:false
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { defaults } = require('jest-config');
|
const { defaults } = require('jest-config');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
coverageDirectory: 'reports/coverage',
|
coverageDirectory: 'reports/coverage',
|
||||||
coverageReporters: ['lcov'],
|
coverageReporters: ['lcov'],
|
||||||
moduleFileExtensions: [
|
moduleFileExtensions: [...defaults.moduleFileExtensions, 'ts', 'tsx'],
|
||||||
...defaults.moduleFileExtensions,
|
reporters: ['default', ['jest-junit', { outputDirectory: 'reports/junit' }]],
|
||||||
'ts',
|
testMatch: ['**/tests/**/*.js'],
|
||||||
'tsx',
|
testPathIgnorePatterns: ['/node_modules/', '/tests/mocks/*'],
|
||||||
],
|
|
||||||
reporters: [
|
|
||||||
'default',
|
|
||||||
['jest-junit', { outputDirectory: 'reports/junit' }],
|
|
||||||
],
|
|
||||||
testMatch: [
|
|
||||||
'**/tests/**/*.js',
|
|
||||||
],
|
|
||||||
testPathIgnorePatterns: [
|
|
||||||
'/node_modules/',
|
|
||||||
'/tests/mocks/*',
|
|
||||||
],
|
|
||||||
automock: false,
|
automock: false,
|
||||||
};
|
};
|
||||||
|
|||||||
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