React UI: Player updates (#1058)

* Move, zoom integration
* Moving, zooming, additional canvas handler
* Activating & changing for objects
* Improved colors
* Saving annotations on the server
* Fixed size
* Refactoring
* Added couple of notifications
* Basic shape drawing
* Cancel previous drawing
* Refactoring
* Minor draw improvings
* Merge, group, split
* Improved colors
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent 247d7f04ec
commit 2cc2e32ff1

@ -74,7 +74,6 @@ Canvas itself handles:
activate(clientID: number, attributeID?: number): void; activate(clientID: number, attributeID?: number): void;
rotate(rotation: Rotation, remember?: boolean): void; rotate(rotation: Rotation, remember?: boolean): void;
focus(clientID: number, padding?: number): void; focus(clientID: number, padding?: number): void;
fitCanvas(): void;
fit(): void; fit(): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
@ -84,6 +83,10 @@ Canvas itself handles:
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
cancel(): void; cancel(): void;
} }
``` ```
@ -118,6 +121,10 @@ Standard JS events are used.
- canvas.groupped => {states: ObjectState[]} - canvas.groupped => {states: ObjectState[]}
- canvas.merged => {states: ObjectState[]} - canvas.merged => {states: ObjectState[]}
- canvas.canceled - canvas.canceled
- canvas.dragstart
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
``` ```
### WEB ### WEB
@ -138,64 +145,26 @@ Standard JS events are used.
}); });
``` ```
### TypeScript
- Add to ```tsconfig.json```:
```json
"compilerOptions": {
"paths": {
"cvat-canvas.node": ["3rdparty/cvat-canvas.node"]
}
}
```
- ```3rdparty``` directory contains both ```cvat-canvas.node.js``` and ```cvat-canvas.node.d.ts```.
- Add alias to ```webpack.config.js```:
```js
module.exports = {
resolve: {
alias: {
'cvat-canvas.node': path.resolve(__dirname, '3rdparty/cvat-canvas.node.js'),
}
}
}
```
Than you can use it in TypeScript:
```ts
import * as CANVAS from 'cvat-canvas.node';
// Create an instance of a canvas
const canvas = new CANVAS.Canvas();
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());
// Next you can use its API methods. For example:
canvas.rotate(CANVAS.Rotation.CLOCKWISE90);
canvas.draw({
enabled: true,
shapeType: 'rectangle',
crosshair: true,
});
```
## States ## States
![](images/states.svg) ![](images/states.svg)
## API Reaction ## API Reaction
| | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | | | IDLE | GROUPING | SPLITTING | DRAWING | MERGING | EDITING | DRAG | ZOOM |
|-------------|------|----------|-----------|---------|---------|---------| |--------------|------|----------|-----------|---------|---------|---------|------|------|
| html() | + | + | + | + | + | + | | html() | + | + | + | + | + | + | + | + |
| setup() | + | + | + | + | + | - | | setup() | + | + | + | + | + | - | + | + |
| activate() | + | - | - | - | - | - | | activate() | + | - | - | - | - | - | - | - |
| rotate() | + | + | + | + | + | + | | rotate() | + | + | + | + | + | + | + | + |
| focus() | + | + | + | + | + | + | | focus() | + | + | + | + | + | + | + | + |
| fit() | + | + | + | + | + | + | | fit() | + | + | + | + | + | + | + | + |
| fitCanvas() | + | + | + | + | + | + | | grid() | + | + | + | + | + | + | + | + |
| grid() | + | + | + | + | + | + | | draw() | + | - | - | - | - | - | - | - |
| draw() | + | - | - | - | - | - | | split() | + | - | + | - | - | - | - | - |
| split() | + | - | + | - | - | - | | group() | + | + | - | - | - | - | - | - |
| group | + | + | - | - | - | - | | merge() | + | - | - | - | + | - | - | - |
| merge() | + | - | - | - | + | - | | fitCanvas() | + | + | + | + | + | + | + | + |
| cancel() | - | + | + | + | + | + | | dragCanvas() | + | - | - | - | - | - | + | - |
| zoomCanvas() | + | - | - | - | - | - | - | + |
| cancel() | - | + | + | + | + | + | + | + |

@ -3,7 +3,7 @@
} }
.cvat_canvas_shape { .cvat_canvas_shape {
fill-opacity: 0.05; fill-opacity: 0.03;
stroke-opacity: 1; stroke-opacity: 1;
} }
@ -68,6 +68,12 @@ polyline.cvat_canvas_shape_merging {
stroke: black; stroke: black;
} }
.cvat_canvas_zoom_selection {
stroke: #096dd9;
fill-opacity: 0;
stroke-dasharray: 4;
}
.cvat_canvas_shape_occluded { .cvat_canvas_shape_occluded {
stroke-dasharray: 5; stroke-dasharray: 5;
} }
@ -78,9 +84,9 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_wrapper { #cvat_canvas_wrapper {
width: 98%; width: calc(100% - 10px);
height: 98%; height: calc(100% - 10px);
margin: 10px; margin: 5px;
border-radius: 5px; border-radius: 5px;
background-color: white; background-color: white;
overflow: hidden; overflow: hidden;
@ -103,6 +109,7 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_text_content { #cvat_canvas_text_content {
text-rendering: optimizeSpeed;
position: absolute; position: absolute;
z-index: 3; z-index: 3;
pointer-events: none; pointer-events: none;
@ -135,6 +142,7 @@ polyline.cvat_canvas_shape_merging {
} }
#cvat_canvas_content { #cvat_canvas_content {
filter: contrast(120%) saturate(150%);
position: absolute; position: absolute;
z-index: 2; z-index: 2;
outline: 10px solid black; outline: 10px solid black;

@ -44,6 +44,10 @@ interface Canvas {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
cancel(): void; cancel(): void;
} }
@ -73,7 +77,15 @@ class CanvasImpl implements Canvas {
); );
} }
public activate(clientID: number, attributeID: number = null): void { public dragCanvas(enable: boolean): void {
this.model.dragCanvas(enable);
}
public zoomCanvas(enable: boolean): void {
this.model.zoomCanvas(enable);
}
public activate(clientID: number, attributeID: number | null = null): void {
this.model.activate(clientID, attributeID); this.model.activate(clientID, attributeID);
} }

@ -33,8 +33,8 @@ export interface FocusData {
} }
export interface ActiveElement { export interface ActiveElement {
clientID: number; clientID: number | null;
attributeID: number; attributeID: number | null;
} }
export interface DrawData { export interface DrawData {
@ -74,21 +74,26 @@ export enum Rotation {
} }
export enum UpdateReasons { export enum UpdateReasons {
IMAGE = 'image', IMAGE_CHANGED = 'image_changed',
OBJECTS = 'objects', IMAGE_ZOOMED = 'image_zoomed',
ZOOM = 'zoom', IMAGE_FITTED = 'image_fitted',
FIT = 'fit', IMAGE_MOVED = 'image_moved',
FIT_CANVAS = 'fit_canvas', GRID_UPDATED = 'grid_updated',
MOVE = 'move',
GRID = 'grid', OBJECTS_UPDATED = 'objects_updated',
FOCUS = 'focus', SHAPE_ACTIVATED = 'shape_activated',
ACTIVATE = 'activate', SHAPE_FOCUSED = 'shape_focused',
FITTED_CANVAS = 'fitted_canvas',
DRAW = 'draw', DRAW = 'draw',
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
SELECT = 'select', SELECT = 'select',
CANCEL = 'cancel', CANCEL = 'cancel',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'ZOOM_CANVAS',
} }
export enum Mode { export enum Mode {
@ -100,6 +105,8 @@ export enum Mode {
MERGE = 'merge', MERGE = 'merge',
SPLIT = 'split', SPLIT = 'split',
GROUP = 'group', GROUP = 'group',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
} }
export interface CanvasModel { export interface CanvasModel {
@ -120,11 +127,10 @@ export interface CanvasModel {
move(topOffset: number, leftOffset: number): void; move(topOffset: number, leftOffset: number): void;
setup(frameData: any, objectStates: any[]): void; setup(frameData: any, objectStates: any[]): void;
activate(clientID: number, attributeID: number): void; activate(clientID: number, attributeID: number | null): void;
rotate(rotation: Rotation, remember: boolean): void; rotate(rotation: Rotation, remember: boolean): void;
focus(clientID: number, padding: number): void; focus(clientID: number, padding: number): void;
fit(): void; fit(): void;
fitCanvas(width: number, height: number): void;
grid(stepX: number, stepY: number): void; grid(stepX: number, stepY: number): void;
draw(drawData: DrawData): void; draw(drawData: DrawData): void;
@ -133,6 +139,10 @@ export interface CanvasModel {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(objectState: any): void; select(objectState: any): void;
fitCanvas(width: number, height: number): void;
dragCanvas(enable: boolean): void;
zoomCanvas(enable: boolean): void;
cancel(): void; cancel(): void;
} }
@ -193,8 +203,6 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
top: 0, top: 0,
drawData: { drawData: {
enabled: false, enabled: false,
shapeType: null,
numberOfPoints: null,
initialState: null, initialState: null,
}, },
mergeData: { mergeData: {
@ -207,7 +215,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
enabled: false, enabled: false,
}, },
selected: null, selected: null,
mode: null, mode: Mode.IDLE,
}; };
} }
@ -232,13 +240,13 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
* (oldScale / this.data.scale - 1)) * this.data.scale; * (oldScale / this.data.scale - 1)) * this.data.scale;
} }
this.notify(UpdateReasons.ZOOM); this.notify(UpdateReasons.IMAGE_ZOOMED);
} }
public move(topOffset: number, leftOffset: number): void { public move(topOffset: number, leftOffset: number): void {
this.data.top += topOffset; this.data.top += topOffset;
this.data.left += leftOffset; this.data.left += leftOffset;
this.notify(UpdateReasons.MOVE); this.notify(UpdateReasons.IMAGE_MOVED);
} }
public fitCanvas(width: number, height: number): void { public fitCanvas(width: number, height: number): void {
@ -250,15 +258,41 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.canvasSize.width / FrameZoom.MIN, this.data.canvasSize.width / FrameZoom.MIN,
)); ));
this.notify(UpdateReasons.FIT_CANVAS); this.notify(UpdateReasons.FITTED_CANVAS);
this.notify(UpdateReasons.OBJECTS); this.notify(UpdateReasons.OBJECTS_UPDATED);
}
public dragCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.DRAG_CANVAS) {
throw Error(`Canvas is not in the drag mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.DRAG_CANVAS : Mode.IDLE;
this.notify(UpdateReasons.DRAG_CANVAS);
}
public zoomCanvas(enable: boolean): void {
if (enable && this.data.mode !== Mode.IDLE) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (!enable && this.data.mode !== Mode.ZOOM_CANVAS) {
throw Error(`Canvas is not in the zoom mode. Action: ${this.data.mode}`);
}
this.data.mode = enable ? Mode.ZOOM_CANVAS : Mode.IDLE;
this.notify(UpdateReasons.ZOOM_CANVAS);
} }
public setup(frameData: any, objectStates: any[]): void { public setup(frameData: any, objectStates: any[]): void {
frameData.data( frameData.data(
(): void => { (): void => {
this.data.image = ''; this.data.image = '';
this.notify(UpdateReasons.IMAGE); this.notify(UpdateReasons.IMAGE_CHANGED);
}, },
).then((data: string): void => { ).then((data: string): void => {
this.data.imageSize = { this.data.imageSize = {
@ -271,15 +305,15 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
} }
this.data.image = data; this.data.image = data;
this.notify(UpdateReasons.IMAGE); this.notify(UpdateReasons.IMAGE_CHANGED);
this.data.objects = objectStates; this.data.objects = objectStates;
this.notify(UpdateReasons.OBJECTS); this.notify(UpdateReasons.OBJECTS_UPDATED);
}).catch((exception: any): void => { }).catch((exception: any): void => {
throw exception; throw exception;
}); });
} }
public activate(clientID: number, attributeID: number): void { public activate(clientID: number, attributeID: number | null): void {
if (this.data.mode !== Mode.IDLE) { if (this.data.mode !== Mode.IDLE) {
// Exception or just return? // Exception or just return?
throw Error(`Canvas is busy. Action: ${this.data.mode}`); throw Error(`Canvas is busy. Action: ${this.data.mode}`);
@ -290,7 +324,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
attributeID, attributeID,
}; };
this.notify(UpdateReasons.ACTIVATE); this.notify(UpdateReasons.SHAPE_ACTIVATED);
} }
public rotate(rotation: Rotation, remember: boolean = false): void { public rotate(rotation: Rotation, remember: boolean = false): void {
@ -311,7 +345,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
padding, padding,
}; };
this.notify(UpdateReasons.FOCUS); this.notify(UpdateReasons.SHAPE_FOCUSED);
} }
public fit(): void { public fit(): void {
@ -338,7 +372,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2); this.data.top = (this.data.canvasSize.height / 2 - this.data.imageSize.height / 2);
this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2); this.data.left = (this.data.canvasSize.width / 2 - this.data.imageSize.width / 2);
this.notify(UpdateReasons.FIT); this.notify(UpdateReasons.IMAGE_FITTED);
} }
public grid(stepX: number, stepY: number): void { public grid(stepX: number, stepY: number): void {
@ -347,7 +381,7 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
width: stepX, width: stepX,
}; };
this.notify(UpdateReasons.GRID); this.notify(UpdateReasons.GRID_UPDATED);
} }
public draw(drawData: DrawData): void { public draw(drawData: DrawData): void {

@ -16,6 +16,7 @@ import { EditHandler, EditHandlerImpl } from './editHandler';
import { MergeHandler, MergeHandlerImpl } from './mergeHandler'; import { MergeHandler, MergeHandlerImpl } from './mergeHandler';
import { SplitHandler, SplitHandlerImpl } from './splitHandler'; import { SplitHandler, SplitHandlerImpl } from './splitHandler';
import { GroupHandler, GroupHandlerImpl } from './groupHandler'; import { GroupHandler, GroupHandlerImpl } from './groupHandler';
import { ZoomHandler, ZoomHandlerImpl } from './zoomHandler';
import consts from './consts'; import consts from './consts';
import { import {
translateToSVG, translateToSVG,
@ -29,7 +30,6 @@ import {
CanvasModel, CanvasModel,
Geometry, Geometry,
UpdateReasons, UpdateReasons,
FocusData,
FrameZoom, FrameZoom,
ActiveElement, ActiveElement,
DrawData, DrawData,
@ -86,10 +86,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
private mergeHandler: MergeHandler; private mergeHandler: MergeHandler;
private splitHandler: SplitHandler; private splitHandler: SplitHandler;
private groupHandler: GroupHandler; private groupHandler: GroupHandler;
private zoomHandler: ZoomHandler;
private activeElement: { private activeElement: {
state: any; state: any;
attributeID: number; attributeID: number;
}; } | null;
private set mode(value: Mode) { private set mode(value: Mode) {
this.controller.mode = value; this.controller.mode = value;
@ -206,7 +207,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE; this.mode = Mode.IDLE;
} }
private onGroupDone(objects: any[]): void { private onGroupDone(objects?: any[]): void {
if (objects) { if (objects) {
const event: CustomEvent = new CustomEvent('canvas.groupped', { const event: CustomEvent = new CustomEvent('canvas.groupped', {
bubbles: false, bubbles: false,
@ -252,12 +253,141 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
} }
private onFocusRegion(x: number, y: number, width: number, height: number): void {
// First of all, compute and apply scale
let scale = null;
if ((this.geometry.angle / 90) % 2) {
// 90, 270, ..
scale = Math.min(Math.max(Math.min(
this.geometry.canvas.width / height,
this.geometry.canvas.height / width,
), FrameZoom.MIN), FrameZoom.MAX);
} else {
scale = Math.min(Math.max(Math.min(
this.geometry.canvas.width / width,
this.geometry.canvas.height / height,
), FrameZoom.MIN), FrameZoom.MAX);
}
this.geometry = { ...this.geometry, scale };
this.transformCanvas();
const [canvasX, canvasY] = translateFromSVG(this.content, [
x + width / 2,
y + height / 2,
]);
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
];
const dragged = {
...this.geometry,
top: this.geometry.top + cy - canvasY,
left: this.geometry.left + cx - canvasX,
scale,
};
this.controller.geometry = dragged;
this.geometry = dragged;
this.moveCanvas();
}
private moveCanvas(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.top = `${this.geometry.top}px`;
obj.style.left = `${this.geometry.left}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.top = `${this.geometry.top - this.geometry.offset}px`;
obj.style.left = `${this.geometry.left - this.geometry.offset}px`;
}
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
this.zoomHandler.transform(this.geometry);
}
private transformCanvas(): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
}
// Transform grid
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / (this.geometry.scale)}px`);
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
element.setAttribute(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`,
);
element.setAttribute(
'r',
`${consts.BASE_POINT_SIZE / this.geometry.scale}`,
);
}
for (const element of
window.document.getElementsByClassName('cvat_canvas_selected_point')) {
const previousWidth = element.getAttribute('stroke-width') as string;
element.setAttribute(
'stroke-width',
`${+previousWidth * 2}`,
);
}
// Transform all drawn shapes
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
}
}
// Transform all text
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)
&& Object.prototype.hasOwnProperty.call(this.svgTexts, key)) {
this.updateTextPosition(
this.svgTexts[key],
this.svgShapes[key],
);
}
}
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
}
private resizeCanvas(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.width = `${this.geometry.image.width}px`;
obj.style.height = `${this.geometry.image.height}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.width = `${this.geometry.image.width + this.geometry.offset * 2}px`;
obj.style.height = `${this.geometry.image.height + this.geometry.offset * 2}px`;
}
}
private selectize(value: boolean, shape: SVG.Element): void { private selectize(value: boolean, shape: SVG.Element): void {
const self = this; const self = this;
function dblClickHandler(e: MouseEvent): void { function dblClickHandler(e: MouseEvent): void {
const pointID = Array.prototype.indexOf const pointID = Array.prototype.indexOf
.call((e.target as HTMLElement).parentElement.children, e.target); .call(((e.target as HTMLElement).parentElement as HTMLElement).children, e.target);
if (self.activeElement) { if (self.activeElement) {
if (e.ctrlKey) { if (e.ctrlKey) {
@ -324,6 +454,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
public constructor(model: CanvasModel & Master, controller: CanvasController) { public constructor(model: CanvasModel & Master, controller: CanvasController) {
this.controller = controller; this.controller = controller;
this.geometry = controller.geometry;
this.svgShapes = {}; this.svgShapes = {};
this.svgTexts = {}; this.svgTexts = {};
this.activeElement = null; this.activeElement = null;
@ -426,7 +557,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.onFindObject.bind(this), this.onFindObject.bind(this),
this.adoptedContent, this.adoptedContent,
); );
this.zoomHandler = new ZoomHandlerImpl(
this.onFocusRegion.bind(this),
this.adoptedContent,
this.geometry,
);
// Setup event handlers // Setup event handlers
this.content.addEventListener('dblclick', (e: MouseEvent): void => { this.content.addEventListener('dblclick', (e: MouseEvent): void => {
@ -436,10 +571,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
}); });
this.content.addEventListener('mousedown', (event): void => { this.content.addEventListener('mousedown', (event): void => {
if ((event.which === 1 && this.mode === Mode.IDLE) || (event.which === 2)) { if ([1, 2].includes(event.which)) {
self.controller.enableDrag(event.clientX, event.clientY); if ([Mode.DRAG_CANVAS, Mode.IDLE].includes(this.mode)) {
self.controller.enableDrag(event.clientX, event.clientY);
event.preventDefault(); } else if (this.mode === Mode.ZOOM_CANVAS && event.which === 2) {
self.controller.enableDrag(event.clientX, event.clientY);
}
} }
}); });
@ -452,6 +589,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.addEventListener('wheel', (event): void => { this.content.addEventListener('wheel', (event): void => {
const point = translateToSVG(self.background, [event.clientX, event.clientY]); const point = translateToSVG(self.background, [event.clientX, event.clientY]);
self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1); self.controller.zoom(point[0], point[1], event.deltaY > 0 ? -1 : 1);
this.canvas.dispatchEvent(new CustomEvent('canvas.zoom', {
bubbles: false,
cancelable: true,
}));
event.preventDefault(); event.preventDefault();
}); });
@ -480,140 +621,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
public notify(model: CanvasModel & Master, reason: UpdateReasons): void { public notify(model: CanvasModel & Master, reason: UpdateReasons): void {
function transform(): void {
// Transform canvas
for (const obj of [this.background, this.grid, this.loadingAnimation, this.content]) {
obj.style.transform = `scale(${this.geometry.scale}) rotate(${this.geometry.angle}deg)`;
}
// Transform grid
this.gridPath.setAttribute('stroke-width', `${consts.BASE_GRID_WIDTH / (this.geometry.scale)}px`);
// Transform all shape points
for (const element of window.document.getElementsByClassName('svg_select_points')) {
element.setAttribute(
'stroke-width',
`${consts.POINTS_STROKE_WIDTH / this.geometry.scale}`,
);
element.setAttribute(
'r',
`${consts.BASE_POINT_SIZE / this.geometry.scale}`,
);
}
for (const element of
window.document.getElementsByClassName('cvat_canvas_selected_point')) {
element.setAttribute(
'stroke-width',
`${+element.getAttribute('stroke-width') * 2}`,
);
}
// Transform all drawn shapes
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)) {
const object = this.svgShapes[key];
if (object.attr('stroke-width')) {
object.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
}
}
}
// Transform all text
for (const key in this.svgShapes) {
if (Object.prototype.hasOwnProperty.call(this.svgShapes, key)
&& Object.prototype.hasOwnProperty.call(this.svgTexts, key)) {
this.updateTextPosition(
this.svgTexts[key],
this.svgShapes[key],
);
}
}
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
}
function resize(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.width = `${this.geometry.image.width}px`;
obj.style.height = `${this.geometry.image.height}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.width = `${this.geometry.image.width + this.geometry.offset * 2}px`;
obj.style.height = `${this.geometry.image.height + this.geometry.offset * 2}px`;
}
}
function move(): void {
for (const obj of [this.background, this.grid, this.loadingAnimation]) {
obj.style.top = `${this.geometry.top}px`;
obj.style.left = `${this.geometry.left}px`;
}
for (const obj of [this.content, this.text]) {
obj.style.top = `${this.geometry.top - this.geometry.offset}px`;
obj.style.left = `${this.geometry.left - this.geometry.offset}px`;
}
// Transform handlers
this.drawHandler.transform(this.geometry);
this.editHandler.transform(this.geometry);
}
function computeFocus(focusData: FocusData): void {
// This computation cann't be done in the model because of lack of data
const object = this.svgShapes[focusData.clientID];
if (!object) {
return;
}
// First of all, compute and apply scale
let scale = null;
const bbox: SVG.BBox = object.bbox();
if ((this.geometry.angle / 90) % 2) {
// 90, 270, ..
scale = Math.min(Math.max(Math.min(
this.geometry.canvas.width / bbox.height,
this.geometry.canvas.height / bbox.width,
), FrameZoom.MIN), FrameZoom.MAX);
} else {
scale = Math.min(Math.max(Math.min(
this.geometry.canvas.width / bbox.width,
this.geometry.canvas.height / bbox.height,
), FrameZoom.MIN), FrameZoom.MAX);
}
this.geometry = { ...this.geometry, scale };
transform.call(this);
const [x, y] = translateFromSVG(this.content, [
bbox.x + bbox.width / 2,
bbox.y + bbox.height / 2,
]);
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
];
const dragged = {
...this.geometry,
top: this.geometry.top + cy - y,
left: this.geometry.left + cx - x,
scale,
};
this.controller.geometry = dragged;
this.geometry = dragged;
move.call(this);
}
function setupObjects(objects: any[]): void { function setupObjects(objects: any[]): void {
const ctm = this.content.getScreenCTM() const ctm = this.content.getScreenCTM()
.inverse().multiply(this.background.getScreenCTM()); .inverse().multiply(this.background.getScreenCTM());
@ -640,36 +647,83 @@ export class CanvasViewImpl implements CanvasView, Listener {
} }
this.geometry = this.controller.geometry; this.geometry = this.controller.geometry;
if (reason === UpdateReasons.IMAGE) { if (reason === UpdateReasons.IMAGE_CHANGED) {
if (!model.image.length) { if (!model.image.length) {
this.loadingAnimation.classList.remove('cvat_canvas_hidden'); this.loadingAnimation.classList.remove('cvat_canvas_hidden');
} else { } else {
this.loadingAnimation.classList.add('cvat_canvas_hidden'); this.loadingAnimation.classList.add('cvat_canvas_hidden');
this.background.style.backgroundImage = `url("${model.image}")`; this.background.style.backgroundImage = `url("${model.image}")`;
move.call(this); this.moveCanvas();
resize.call(this); this.resizeCanvas();
transform.call(this); this.transformCanvas();
}
} else if (reason === UpdateReasons.FITTED_CANVAS) {
this.moveCanvas();
this.resizeCanvas();
} else if (reason === UpdateReasons.IMAGE_ZOOMED || reason === UpdateReasons.IMAGE_FITTED) {
this.moveCanvas();
this.transformCanvas();
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} else if (reason === UpdateReasons.OBJECTS_UPDATED) {
if (this.mode === Mode.GROUP) {
this.groupHandler.resetSelectedObjects();
} }
} else if (reason === UpdateReasons.FIT_CANVAS) {
move.call(this);
resize.call(this);
} else if (reason === UpdateReasons.ZOOM || reason === UpdateReasons.FIT) {
move.call(this);
transform.call(this);
} else if (reason === UpdateReasons.MOVE) {
move.call(this);
} else if (reason === UpdateReasons.OBJECTS) {
setupObjects.call(this, this.controller.objects); setupObjects.call(this, this.controller.objects);
if (this.mode === Mode.MERGE) {
this.mergeHandler.repeatSelection();
}
const event: CustomEvent = new CustomEvent('canvas.setup'); const event: CustomEvent = new CustomEvent('canvas.setup');
this.canvas.dispatchEvent(event); this.canvas.dispatchEvent(event);
} else if (reason === UpdateReasons.GRID) { } else if (reason === UpdateReasons.GRID_UPDATED) {
const size: Size = this.geometry.grid; const size: Size = this.geometry.grid;
this.gridPattern.setAttribute('width', `${size.width}`); this.gridPattern.setAttribute('width', `${size.width}`);
this.gridPattern.setAttribute('height', `${size.height}`); this.gridPattern.setAttribute('height', `${size.height}`);
} else if (reason === UpdateReasons.FOCUS) { } else if (reason === UpdateReasons.SHAPE_FOCUSED) {
computeFocus.call(this, this.controller.focusData); const {
} else if (reason === UpdateReasons.ACTIVATE) { padding,
clientID,
} = this.controller.focusData;
const object = this.svgShapes[clientID];
if (object) {
const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding,
bbox.width + padding, bbox.height + padding);
}
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement); this.activate(this.controller.activeElement);
} else if (reason === UpdateReasons.DRAG_CANVAS) {
this.deactivate();
if (this.mode === Mode.DRAG_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.dragstart', {
bubbles: false,
cancelable: true,
}));
this.canvas.style.cursor = 'move';
} else {
this.canvas.dispatchEvent(new CustomEvent('canvas.dragstop', {
bubbles: false,
cancelable: true,
}));
this.canvas.style.cursor = '';
}
} else if (reason === UpdateReasons.ZOOM_CANVAS) {
this.deactivate();
if (this.mode === Mode.ZOOM_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstart', {
bubbles: false,
cancelable: true,
}));
this.canvas.style.cursor = 'zoom-in';
this.zoomHandler.zoom();
} else {
this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstop', {
bubbles: false,
cancelable: true,
}));
this.canvas.style.cursor = '';
this.zoomHandler.cancel();
}
} else if (reason === UpdateReasons.DRAW) { } else if (reason === UpdateReasons.DRAW) {
const data: DrawData = this.controller.drawData; const data: DrawData = this.controller.drawData;
if (data.enabled) { if (data.enabled) {
@ -717,7 +771,20 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.groupHandler.cancel(); this.groupHandler.cancel();
} else if (this.mode === Mode.EDIT) { } else if (this.mode === Mode.EDIT) {
this.editHandler.cancel(); this.editHandler.cancel();
} else if (this.mode === Mode.DRAG_CANVAS) {
this.canvas.dispatchEvent(new CustomEvent('canvas.dragstop', {
bubbles: false,
cancelable: true,
}));
} else if (this.mode === Mode.ZOOM_CANVAS) {
this.zoomHandler.cancel();
this.canvas.dispatchEvent(new CustomEvent('canvas.zoomstop', {
bubbles: false,
cancelable: true,
}));
} }
this.mode = Mode.IDLE;
this.canvas.style.cursor = '';
} }
} }
@ -871,7 +938,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.selectize(true, shape); this.selectize(true, shape);
} }
let shapeSizeElement: ShapeSizeElement = null; let shapeSizeElement: ShapeSizeElement | null = null;
let resized = false; let resized = false;
(shape as any).resize().on('resizestart', (): void => { (shape as any).resize().on('resizestart', (): void => {
this.mode = Mode.RESIZE; this.mode = Mode.RESIZE;
@ -980,7 +1047,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50), stroke: darker(state.color, 20),
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).move(xtl, ytl) }).move(xtl, ytl)
@ -1000,7 +1067,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50), stroke: darker(state.color, 20),
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');
@ -1019,7 +1086,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
id: `cvat_canvas_shape_${state.clientID}`, id: `cvat_canvas_shape_${state.clientID}`,
fill: state.color, fill: state.color,
'shape-rendering': 'geometricprecision', 'shape-rendering': 'geometricprecision',
stroke: darker(state.color, 50), stroke: darker(state.color, 20),
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
zOrder: state.zOrder, zOrder: state.zOrder,
}).addClass('cvat_canvas_shape'); }).addClass('cvat_canvas_shape');

@ -3,7 +3,7 @@
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
const BASE_STROKE_WIDTH = 2; const BASE_STROKE_WIDTH = 1.75;
const BASE_GRID_WIDTH = 1; const BASE_GRID_WIDTH = 1;
const BASE_POINT_SIZE = 5; const BASE_POINT_SIZE = 5;
const TEXT_MARGIN = 10; const TEXT_MARGIN = 10;

@ -26,6 +26,7 @@ import {
export interface DrawHandler { export interface DrawHandler {
draw(drawData: DrawData, geometry: Geometry): void; draw(drawData: DrawData, geometry: Geometry): void;
transform(geometry: Geometry): void;
cancel(): void; cancel(): void;
} }

@ -10,16 +10,17 @@ export interface GroupHandler {
group(groupData: GroupData): void; group(groupData: GroupData): void;
select(state: any): void; select(state: any): void;
cancel(): void; cancel(): void;
resetSelectedObjects(): void;
} }
export class GroupHandlerImpl implements GroupHandler { export class GroupHandlerImpl implements GroupHandler {
// callback is used to notify about grouping end // callback is used to notify about grouping end
private onGroupDone: (objects: any[]) => void; private onGroupDone: (objects?: any[]) => void;
private getStates: () => any[]; private getStates: () => any[];
private onFindObject: (event: MouseEvent) => void; private onFindObject: (event: MouseEvent) => void;
private onSelectStart: (event: MouseEvent) => void; private bindedOnSelectStart: (event: MouseEvent) => void;
private onSelectUpdate: (event: MouseEvent) => void; private bindedOnSelectUpdate: (event: MouseEvent) => void;
private onSelectStop: (event: MouseEvent) => void; private bindedOnSelectStop: (event: MouseEvent) => void;
private selectionRect: SVG.Rect; private selectionRect: SVG.Rect;
private startSelectionPoint: { private startSelectionPoint: {
x: number; x: number;
@ -27,7 +28,7 @@ export class GroupHandlerImpl implements GroupHandler {
}; };
private canvas: SVG.Container; private canvas: SVG.Container;
private initialized: boolean; private initialized: boolean;
private states: any[]; private statesToBeGroupped: any[];
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private getSelectionBox(event: MouseEvent): { private getSelectionBox(event: MouseEvent): {
@ -53,19 +54,72 @@ export class GroupHandlerImpl implements GroupHandler {
}; };
} }
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect) {
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().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}
private onSelectUpdate(event: MouseEvent): void {
// called on mousemove
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 onSelectStop(event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.statesToBeGroupped.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}
private release(): void { private release(): void {
this.canvas.node.removeEventListener('click', this.onFindObject); this.canvas.node.removeEventListener('click', this.onFindObject);
this.canvas.node.removeEventListener('mousedown', this.onSelectStart); this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.removeEventListener('mousemove', this.onSelectUpdate); this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.removeEventListener('mouseup', this.onSelectStop); this.canvas.node.removeEventListener('mouseup', this.bindedOnSelectStop);
this.canvas.node.removeEventListener('mouseleave', this.onSelectStop);
for (const state of this.states) { this.resetSelectedObjects();
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.states = [];
this.highlightedShapes = {};
this.initialized = false; this.initialized = false;
this.selectionRect = null; this.selectionRect = null;
this.startSelectionPoint = { this.startSelectionPoint = {
@ -76,29 +130,28 @@ export class GroupHandlerImpl implements GroupHandler {
private initGrouping(): void { private initGrouping(): void {
this.canvas.node.addEventListener('click', this.onFindObject); this.canvas.node.addEventListener('click', this.onFindObject);
this.canvas.node.addEventListener('mousedown', this.onSelectStart); this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.addEventListener('mousemove', this.onSelectUpdate); this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.onSelectStop); this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop);
this.canvas.node.addEventListener('mouseleave', this.onSelectStop);
this.initialized = true; this.initialized = true;
} }
private closeGrouping(): void { private closeGrouping(): void {
if (this.initialized) { if (this.initialized) {
const { states } = this; const { statesToBeGroupped } = this;
this.release(); this.release();
if (states.length) { if (statesToBeGroupped.length) {
this.onGroupDone(states); this.onGroupDone(statesToBeGroupped);
} else { } else {
this.onGroupDone(null); this.onGroupDone();
} }
} }
} }
public constructor( public constructor(
onGroupDone: (objects: any[]) => void, onGroupDone: (objects?: any[]) => void,
getStates: () => any[], getStates: () => any[],
onFindObject: (event: MouseEvent) => void, onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container, canvas: SVG.Container,
@ -107,69 +160,18 @@ export class GroupHandlerImpl implements GroupHandler {
this.getStates = getStates; this.getStates = getStates;
this.onFindObject = onFindObject; this.onFindObject = onFindObject;
this.canvas = canvas; this.canvas = canvas;
this.states = []; this.statesToBeGroupped = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.selectionRect = null; this.selectionRect = null;
this.initialized = false;
this.startSelectionPoint = { this.startSelectionPoint = {
x: null, x: null,
y: null, y: null,
}; };
this.onSelectStart = function (event: MouseEvent): void { this.bindedOnSelectStart = this.onSelectStart.bind(this);
if (!this.selectionRect) { this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this);
const point = translateToSVG(this.canvas.node, [event.clientX, event.clientY]); this.bindedOnSelectStop = this.onSelectStop.bind(this);
this.startSelectionPoint = {
x: point[0],
y: point[1],
};
this.selectionRect = this.canvas.rect().addClass('cvat_canvas_shape_grouping');
this.selectionRect.attr({ ...this.startSelectionPoint });
}
}.bind(this);
this.onSelectUpdate = function (event: MouseEvent): void {
// called on mousemove
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,
});
}
}.bind(this);
this.onSelectStop = function (event: MouseEvent): void {
// called on mouseup, mouseleave
if (this.selectionRect) {
this.selectionRect.remove();
this.selectionRect = null;
const box = this.getSelectionBox(event);
const shapes = (this.canvas.select('.cvat_canvas_shape') as any).members;
for (const shape of shapes) {
// TODO: Doesn't work properly for groups
const bbox = shape.bbox();
const clientID = shape.attr('clientID');
if (bbox.x > box.xtl && bbox.y > box.ytl
&& bbox.x + bbox.width < box.xbr
&& bbox.y + bbox.height < box.ybr
&& !(clientID in this.highlightedShapes)) {
const objectState = this.getStates()
.filter((state: any): boolean => state.clientID === clientID)[0];
if (objectState) {
this.states.push(objectState);
this.highlightedShapes[clientID] = shape;
(shape as any).addClass('cvat_canvas_shape_grouping');
}
}
}
}
}.bind(this);
} }
/* eslint-disable-next-line */ /* eslint-disable-next-line */
@ -182,11 +184,11 @@ export class GroupHandlerImpl implements GroupHandler {
} }
public select(objectState: any): void { public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID); const stateIndexes = this.statesToBeGroupped.map((state): number => state.clientID);
const includes = stateIndexes.indexOf(objectState.clientID); const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) { if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID]; const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1); this.statesToBeGroupped.splice(includes, 1);
if (shape) { if (shape) {
delete this.highlightedShapes[objectState.clientID]; delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_grouping'); shape.removeClass('cvat_canvas_shape_grouping');
@ -194,15 +196,24 @@ export class GroupHandlerImpl implements GroupHandler {
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) { if (shape) {
this.states.push(objectState); this.statesToBeGroupped.push(objectState);
this.highlightedShapes[objectState.clientID] = shape; this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_grouping'); shape.addClass('cvat_canvas_shape_grouping');
} }
} }
} }
public resetSelectedObjects(): void {
for (const state of this.statesToBeGroupped) {
const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_grouping');
}
this.statesToBeGroupped = [];
this.highlightedShapes = {};
}
public cancel(): void { public cancel(): void {
this.release(); this.release();
this.onGroupDone(null); this.onGroupDone();
} }
} }

@ -5,6 +5,7 @@ export interface MergeHandler {
merge(mergeData: MergeData): void; merge(mergeData: MergeData): void;
select(state: any): void; select(state: any): void;
cancel(): void; cancel(): void;
repeatSelection(): void;
} }
@ -14,7 +15,7 @@ export class MergeHandlerImpl implements MergeHandler {
private onFindObject: (event: MouseEvent) => void; private onFindObject: (event: MouseEvent) => void;
private canvas: SVG.Container; private canvas: SVG.Container;
private initialized: boolean; private initialized: boolean;
private states: any[]; // are being merged private statesToBeMerged: any[]; // are being merged
private highlightedShapes: Record<number, SVG.Shape>; private highlightedShapes: Record<number, SVG.Shape>;
private constraints: { private constraints: {
labelID: number; labelID: number;
@ -22,7 +23,7 @@ export class MergeHandlerImpl implements MergeHandler {
}; };
private addConstraints(): void { private addConstraints(): void {
const shape = this.states[0]; const shape = this.statesToBeMerged[0];
this.constraints = { this.constraints = {
labelID: shape.label.id, labelID: shape.label.id,
shapeType: shape.shapeType, shapeType: shape.shapeType,
@ -41,11 +42,11 @@ export class MergeHandlerImpl implements MergeHandler {
private release(): void { private release(): void {
this.removeConstraints(); this.removeConstraints();
this.canvas.node.removeEventListener('click', this.onFindObject); this.canvas.node.removeEventListener('click', this.onFindObject);
for (const state of this.states) { for (const state of this.statesToBeMerged) {
const shape = this.highlightedShapes[state.clientID]; const shape = this.highlightedShapes[state.clientID];
shape.removeClass('cvat_canvas_shape_merging'); shape.removeClass('cvat_canvas_shape_merging');
} }
this.states = []; this.statesToBeMerged = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.initialized = false; this.initialized = false;
} }
@ -57,11 +58,11 @@ export class MergeHandlerImpl implements MergeHandler {
private closeMerging(): void { private closeMerging(): void {
if (this.initialized) { if (this.initialized) {
const { states } = this; const { statesToBeMerged } = this;
this.release(); this.release();
if (states.length > 1) { if (statesToBeMerged.length > 1) {
this.onMergeDone(states); this.onMergeDone(statesToBeMerged);
} else { } else {
this.onMergeDone(null); this.onMergeDone(null);
// here is a cycle // here is a cycle
@ -79,7 +80,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.onMergeDone = onMergeDone; this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject; this.onFindObject = onFindObject;
this.canvas = canvas; this.canvas = canvas;
this.states = []; this.statesToBeMerged = [];
this.highlightedShapes = {}; this.highlightedShapes = {};
this.constraints = null; this.constraints = null;
this.initialized = false; this.initialized = false;
@ -94,35 +95,45 @@ export class MergeHandlerImpl implements MergeHandler {
} }
public select(objectState: any): void { public select(objectState: any): void {
const stateIndexes = this.states.map((state): number => state.clientID); const stateIndexes = this.statesToBeMerged.map((state): number => state.clientID);
const stateFrames = this.states.map((state): number => state.frame); const stateFrames = this.statesToBeMerged.map((state): number => state.frame);
const includes = stateIndexes.indexOf(objectState.clientID); const includes = stateIndexes.indexOf(objectState.clientID);
if (includes !== -1) { if (includes !== -1) {
const shape = this.highlightedShapes[objectState.clientID]; const shape = this.highlightedShapes[objectState.clientID];
this.states.splice(includes, 1); this.statesToBeMerged.splice(includes, 1);
if (shape) { if (shape) {
delete this.highlightedShapes[objectState.clientID]; delete this.highlightedShapes[objectState.clientID];
shape.removeClass('cvat_canvas_shape_merging'); shape.removeClass('cvat_canvas_shape_merging');
} }
if (!this.states.length) { if (!this.statesToBeMerged.length) {
this.removeConstraints(); this.removeConstraints();
} }
} else { } else {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first(); const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape && this.checkConstraints(objectState) if (shape && this.checkConstraints(objectState)
&& !stateFrames.includes(objectState.frame)) { && !stateFrames.includes(objectState.frame)) {
this.states.push(objectState); this.statesToBeMerged.push(objectState);
this.highlightedShapes[objectState.clientID] = shape; this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging'); shape.addClass('cvat_canvas_shape_merging');
if (this.states.length === 1) { if (this.statesToBeMerged.length === 1) {
this.addConstraints(); this.addConstraints();
} }
} }
} }
} }
public repeatSelection(): void {
for (const objectState of this.statesToBeMerged) {
const shape = this.canvas.select(`#cvat_canvas_shape_${objectState.clientID}`).first();
if (shape) {
this.highlightedShapes[objectState.clientID] = shape;
shape.addClass('cvat_canvas_shape_merging');
}
}
}
public cancel(): void { public cancel(): void {
this.release(); this.release();
this.onMergeDone(null); this.onMergeDone(null);

@ -26,11 +26,11 @@ export interface BBox {
y: number; y: number;
} }
// Translate point array from the client coordinate system // Translate point array from the canvas coordinate system
// to a coordinate system of a canvas // to the coordinate system of a client
export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] { export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = []; const output = [];
const transformationMatrix = svg.getScreenCTM(); const transformationMatrix = svg.getScreenCTM() as DOMMatrix;
let pt = svg.createSVGPoint(); let pt = svg.createSVGPoint();
for (let i = 0; i < points.length - 1; i += 2) { for (let i = 0; i < points.length - 1; i += 2) {
pt.x = points[i]; pt.x = points[i];
@ -42,11 +42,11 @@ export function translateFromSVG(svg: SVGSVGElement, points: number[]): number[]
return output; return output;
} }
// Translate point array from a coordinate system of a canvas // Translate point array from the coordinate system of a client
// to the client coordinate system // to the canvas coordinate system
export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] { export function translateToSVG(svg: SVGSVGElement, points: number[]): number[] {
const output = []; const output = [];
const transformationMatrix = svg.getScreenCTM().inverse(); const transformationMatrix = (svg.getScreenCTM() as DOMMatrix).inverse();
let pt = svg.createSVGPoint(); let pt = svg.createSVGPoint();
for (let i = 0; i < points.length; i += 2) { for (let i = 0; i < points.length; i += 2) {
pt.x = points[i]; pt.x = points[i];

@ -0,0 +1,141 @@
import * as SVG from 'svg.js';
import consts from './consts';
import {
translateToSVG,
} from './shared';
import {
Geometry,
} from './canvasModel';
export interface ZoomHandler {
zoom(): void;
cancel(): void;
transform(geometry: Geometry): void;
}
export class ZoomHandlerImpl implements ZoomHandler {
private onZoomRegion: (x: number, y: number, width: number, height: number) => void;
private bindedOnSelectStart: (event: MouseEvent) => void;
private bindedOnSelectUpdate: (event: MouseEvent) => void;
private bindedOnSelectStop: (event: MouseEvent) => void;
private geometry: Geometry;
private canvas: SVG.Container;
private selectionRect: SVG.Rect | null;
private startSelectionPoint: {
x: number;
y: number;
};
private onSelectStart(event: MouseEvent): void {
if (!this.selectionRect && event.which === 1) {
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().addClass('cvat_canvas_zoom_selection');
this.selectionRect.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
...this.startSelectionPoint,
});
}
}
private getSelectionBox(event: MouseEvent): {
x: number;
y: number;
width: number;
height: number;
} {
const point = translateToSVG(
(this.canvas.node as any as SVGSVGElement),
[event.clientX, event.clientY],
);
const stopSelectionPoint = {
x: point[0],
y: point[1],
};
const xtl = Math.min(this.startSelectionPoint.x, stopSelectionPoint.x);
const ytl = Math.min(this.startSelectionPoint.y, stopSelectionPoint.y);
const xbr = Math.max(this.startSelectionPoint.x, stopSelectionPoint.x);
const ybr = Math.max(this.startSelectionPoint.y, stopSelectionPoint.y);
return {
x: xtl,
y: ytl,
width: xbr - xtl,
height: ybr - ytl,
};
}
private onSelectUpdate(event: MouseEvent): void {
if (this.selectionRect) {
this.selectionRect.attr({
...this.getSelectionBox(event),
});
}
}
private onSelectStop(event: MouseEvent): void {
if (this.selectionRect) {
const box = this.getSelectionBox(event);
this.selectionRect.remove();
this.selectionRect = null;
this.startSelectionPoint = {
x: 0,
y: 0,
};
const threshold = 5;
if (box.width > threshold && box.height > threshold) {
this.onZoomRegion(box.x, box.y, box.width, box.height);
}
}
}
public constructor(
onZoomRegion: (x: number, y: number, width: number, height: number) => void,
canvas: SVG.Container,
geometry: Geometry,
) {
this.onZoomRegion = onZoomRegion;
this.canvas = canvas;
this.geometry = geometry;
this.selectionRect = null;
this.startSelectionPoint = {
x: 0,
y: 0,
};
this.bindedOnSelectStart = this.onSelectStart.bind(this);
this.bindedOnSelectUpdate = this.onSelectUpdate.bind(this);
this.bindedOnSelectStop = this.onSelectStop.bind(this);
}
public zoom(): void {
this.canvas.node.addEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.addEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.addEventListener('mouseup', this.bindedOnSelectStop);
}
public cancel(): void {
this.canvas.node.removeEventListener('mousedown', this.bindedOnSelectStart);
this.canvas.node.removeEventListener('mousemove', this.bindedOnSelectUpdate);
this.canvas.node.removeEventListener('mouseup ', this.bindedOnSelectStop);
}
public transform(geometry: Geometry): void {
this.geometry = geometry;
if (this.selectionRect) {
this.selectionRect.style({
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
});
}
}
}

@ -36,24 +36,11 @@
ObjectType, ObjectType,
} = require('./enums'); } = require('./enums');
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const colors = [ const colors = [
'#0066FF', '#AF593E', '#01A368', '#FF861F', '#ED0A3F', '#FF3F34', '#76D7EA', '#FF355E', '#E936A7', '#FD5B78', '#FF007C', '#FF00CC', '#66FF66',
'#8359A3', '#FBE870', '#C5E17A', '#03BB85', '#FFDF00', '#8B8680', '#0A6B0D', '#50BFE6', '#CCFF00', '#FFFF66', '#FF9966', '#FF6037', '#FFCC33',
'#8FD8D8', '#A36F40', '#F653A6', '#CA3435', '#FFCBA4', '#FF99CC', '#FA9D5A', '#AAF0D1', '#FF3855', '#FFF700', '#A7F432', '#FF5470', '#FAFA37',
'#FFAE42', '#A78B00', '#788193', '#514E49', '#1164B4', '#F4FA9F', '#FED8B1', '#FF7A00', '#FF9933', '#AFE313', '#00CC99', '#FF5050', '#733380'];
'#C32148', '#01796F', '#E90067', '#FF91A4', '#404E5A', '#6CDAE7', '#FFC1CC',
'#006A93', '#867200', '#E2B631', '#6EEB6E', '#FFC800', '#CC99BA', '#FF007C',
'#BC6CAC', '#DCCCD7', '#EBE1C2', '#A6AAAE', '#B99685', '#0086A7', '#5E4330',
'#C8A2C8', '#708EB3', '#BC8777', '#B2592D', '#497E48', '#6A2963', '#E6335F',
'#00755E', '#B5A895', '#0048ba', '#EED9C4', '#C88A65', '#FF6E4A', '#87421F',
'#B2BEB5', '#926F5B', '#00B9FB', '#6456B7', '#DB5079', '#C62D42', '#FA9C44',
'#DA8A67', '#FD7C6E', '#93CCEA', '#FCF686', '#503E32', '#FF5470', '#9DE093',
'#FF7A00', '#4F69C6', '#A50B5E', '#F0E68C', '#FDFF00', '#F091A9', '#FFFF66',
'#6F9940', '#FC74FD', '#652DC1', '#D6AEDD', '#EE34D2', '#BB3385', '#6B3FA0',
'#33CC99', '#FFDB00', '#87FF2A', '#6EEB6E', '#FFC800', '#CC99BA', '#7A89B8',
'#006A93', '#867200', '#E2B631', '#D9D6CF',
];
function shapeFactory(shapeData, clientID, injection) { function shapeFactory(shapeData, clientID, injection) {
const { type } = shapeData; const { type } = shapeData;
@ -416,7 +403,7 @@
} }
const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b); const keyframes = Object.keys(object.shapes).sort((a, b) => +a - +b);
if (frame <= +keyframes[0] || frame > keyframes[keyframes.length - 1]) { if (frame <= +keyframes[0]) {
return; return;
} }

@ -278,6 +278,7 @@
group: this.group, group: this.group,
color: this.color, color: this.color,
visibility: this.visibility, visibility: this.visibility,
frame,
}; };
} }
@ -491,6 +492,7 @@
lock: this.lock, lock: this.lock,
color: this.color, color: this.color,
visibility: this.visibility, visibility: this.visibility,
frame,
}; };
this.cache[frame] = interpolation; this.cache[frame] = interpolation;
@ -716,9 +718,21 @@
// Add/update keyframe // Add/update keyframe
if (positionUpdated || (updated.keyframe && data.keyframe)) { if (positionUpdated || (updated.keyframe && data.keyframe)) {
// Remove all cache after this keyframe because it have just become outdated // Remove affected cached frames
for (const cacheFrame in this.cache) { const {
if (+cacheFrame > frame) { leftFrame,
rightFrame,
} = this.neighborsFrames(frame);
for (const cacheFrame of Object.keys(this.cache)) {
if (leftFrame === null && +cacheFrame < frame) {
delete this.cache[cacheFrame];
} else if (+cacheFrame < frame && +cacheFrame > leftFrame) {
delete this.cache[cacheFrame];
}
if (rightFrame === null && +cacheFrame > frame) {
delete this.cache[cacheFrame];
} else if (+cacheFrame > frame && +cacheFrame < rightFrame) {
delete this.cache[cacheFrame]; delete this.cache[cacheFrame];
} }
} }
@ -860,6 +874,7 @@
attributes: { ...this.attributes }, attributes: { ...this.attributes },
label: this.label, label: this.label,
group: this.group, group: this.group,
frame,
}; };
} }

@ -200,80 +200,67 @@
}; };
} }
try { const exported = this.collection.export();
const exported = this.collection.export(); const { flush } = this.collection;
const { flush } = this.collection; if (flush) {
if (flush) { onUpdate('Created objects are being saved on the server');
onUpdate('New objects are being saved..'); const indexes = this._receiveIndexes(exported);
const indexes = this._receiveIndexes(exported); const savedData = await this._put({ ...exported, version: this.version });
const savedData = await this._put({ ...exported, version: this.version }); this.version = savedData.version;
this.version = savedData.version; this.collection.flush = false;
this.collection.flush = false;
this._updateCreatedObjects(savedData, indexes);
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(savedData, indexes); this._resetState();
for (const type of Object.keys(this.initialObjects)) {
onUpdate('Initial state is being updated'); for (const object of savedData[type]) {
this.initialObjects[type][object.id] = object;
this._resetState();
for (const type of Object.keys(this.initialObjects)) {
for (const object of savedData[type]) {
this.initialObjects[type][object.id] = object;
}
} }
} else { }
const { } else {
created, const {
updated, created,
deleted, updated,
} = this._split(exported); deleted,
} = this._split(exported);
onUpdate('New objects are being saved..');
const indexes = this._receiveIndexes(created); onUpdate('Created objects are being saved on the server');
const createdData = await this._create({ ...created, version: this.version }); const indexes = this._receiveIndexes(created);
this.version = createdData.version; const createdData = await this._create({ ...created, version: this.version });
this.version = createdData.version;
onUpdate('Saved objects are being updated in the client');
this._updateCreatedObjects(createdData, indexes); this._updateCreatedObjects(createdData, indexes);
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of createdData[type]) {
for (const object of createdData[type]) { this.initialObjects[type][object.id] = object;
this.initialObjects[type][object.id] = object;
}
} }
}
onUpdate('Changed objects are being saved..'); onUpdate('Updated objects are being saved on the server');
this._receiveIndexes(updated); this._receiveIndexes(updated);
const updatedData = await this._update({ ...updated, version: this.version }); const updatedData = await this._update({ ...updated, version: this.version });
this.version = updatedData.version; this.version = updatedData.version;
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of updatedData[type]) {
for (const object of updatedData[type]) { this.initialObjects[type][object.id] = object;
this.initialObjects[type][object.id] = object;
}
} }
}
onUpdate('Changed objects are being saved..'); onUpdate('Deleted objects are being deleted from the server');
this._receiveIndexes(deleted); this._receiveIndexes(deleted);
const deletedData = await this._delete({ ...deleted, version: this.version }); const deletedData = await this._delete({ ...deleted, version: this.version });
this._version = deletedData.version; this._version = deletedData.version;
onUpdate('Initial state is being updated'); for (const type of Object.keys(this.initialObjects)) {
for (const type of Object.keys(this.initialObjects)) { for (const object of deletedData[type]) {
for (const object of deletedData[type]) { delete this.initialObjects[type][object.id];
delete this.initialObjects[type][object.id];
}
} }
} }
this.hash = this._getHash();
onUpdate('Saving is done');
} catch (error) {
onUpdate(`Can not save annotations: ${error.message}`);
throw error;
} }
this.hash = this._getHash();
} }
hasUnsavedChanges() { hasUnsavedChanges() {

@ -26,9 +26,9 @@
return result; return result;
}, },
async save() { async save(onUpdate) {
const result = await PluginRegistry const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.save); .apiWrapper.call(this, prototype.annotations.save, onUpdate);
return result; return result;
}, },

@ -39,9 +39,9 @@ module.exports = {
}, },
'settings': { 'settings': {
'import/resolver': { 'import/resolver': {
'node': { 'typescript': {
'extensions': ['.tsx', '.ts', '.jsx', '.js', '.json'], 'directory': './tsconfig.json'
}, }
}, },
}, },
}; };

@ -1002,6 +1002,12 @@
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
"dev": true "dev": true
}, },
"@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -3849,6 +3855,19 @@
} }
} }
}, },
"eslint-import-resolver-typescript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.0.0.tgz",
"integrity": "sha512-bT5Frpl8UWoHBtY25vKUOMoVIMlJQOMefHLyQ4Tz3MQpIZ2N6yYKEEIHMo38bszBNUuMBW6M3+5JNYxeiGFH4w==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"is-glob": "^4.0.1",
"resolve": "^1.12.0",
"tiny-glob": "^0.2.6",
"tsconfig-paths": "^3.9.0"
}
},
"eslint-module-utils": { "eslint-module-utils": {
"version": "2.4.1", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz",
@ -5408,6 +5427,12 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
}, },
"globalyzer": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.4.tgz",
"integrity": "sha512-LeguVWaxgHN0MNbWC6YljNMzHkrCny9fzjmEUdnF1kQ7wATFD1RHFRqA1qxaX2tgxGENlcxjOflopBwj3YZiXA==",
"dev": true
},
"globby": { "globby": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
@ -5429,6 +5454,12 @@
} }
} }
}, },
"globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
"integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
"dev": true
},
"globule": { "globule": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
@ -11116,6 +11147,16 @@
"setimmediate": "^1.0.4" "setimmediate": "^1.0.4"
} }
}, },
"tiny-glob": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.6.tgz",
"integrity": "sha512-A7ewMqPu1B5PWwC3m7KVgAu96Ch5LA0w4SnEN/LbDREj/gAD0nPWboRbn8YoP9ISZXqeNAlMvKSKoEuhcfK3Pw==",
"dev": true,
"requires": {
"globalyzer": "^0.1.0",
"globrex": "^0.1.1"
}
},
"tiny-invariant": { "tiny-invariant": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
@ -11234,6 +11275,40 @@
"glob": "^7.1.2" "glob": "^7.1.2"
} }
}, },
"tsconfig-paths": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz",
"integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==",
"dev": true,
"requires": {
"@types/json5": "^0.0.29",
"json5": "^1.0.1",
"minimist": "^1.2.0",
"strip-bom": "^3.0.0"
},
"dependencies": {
"json5": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
}
}
}
},
"tsconfig-paths-webpack-plugin": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz",
"integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==",
"dev": true,
"requires": {
"chalk": "^2.3.0",
"enhanced-resolve": "^4.0.0",
"tsconfig-paths": "^3.4.0"
}
},
"tslib": { "tslib": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",

@ -20,6 +20,7 @@
"babel-plugin-import": "^1.12.2", "babel-plugin-import": "^1.12.2",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"eslint-config-airbnb-typescript": "^4.0.1", "eslint-config-airbnb-typescript": "^4.0.1",
"eslint-import-resolver-typescript": "^2.0.0",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.17.0", "eslint-plugin-react": "^7.17.0",
@ -33,6 +34,7 @@
"react-svg-loader": "^3.0.3", "react-svg-loader": "^3.0.3",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.7.3", "typescript": "^3.7.3",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-cli": "^3.3.8", "webpack-cli": "^3.3.8",

@ -3,11 +3,14 @@ import { ThunkAction } from 'redux-thunk';
import { import {
CombinedState, CombinedState,
ActiveControl,
ShapeType,
ObjectType,
Task, Task,
} from '../reducers/interfaces'; } from 'reducers/interfaces';
import getCore from '../core'; import getCore from 'cvat-core';
import { getCVATStore } from '../store'; import { getCVATStore } from 'cvat-store';
const cvat = getCore(); const cvat = getCore();
@ -18,8 +21,24 @@ export enum AnnotationActionTypes {
CHANGE_FRAME = 'CHANGE_FRAME', CHANGE_FRAME = 'CHANGE_FRAME',
CHANGE_FRAME_SUCCESS = 'CHANGE_FRAME_SUCCESS', CHANGE_FRAME_SUCCESS = 'CHANGE_FRAME_SUCCESS',
CHANGE_FRAME_FAILED = 'CHANGE_FRAME_FAILED', CHANGE_FRAME_FAILED = 'CHANGE_FRAME_FAILED',
SAVE_ANNOTATIONS = 'SAVE_ANNOTATIONS',
SAVE_ANNOTATIONS_SUCCESS = 'SAVE_ANNOTATIONS_SUCCESS',
SAVE_ANNOTATIONS_FAILED = 'SAVE_ANNOTATIONS_FAILED',
SAVE_ANNOTATIONS_UPDATED_STATUS = 'SAVE_ANNOTATIONS_UPDATED_STATUS',
SWITCH_PLAY = 'SWITCH_PLAY', SWITCH_PLAY = 'SWITCH_PLAY',
CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY', CONFIRM_CANVAS_READY = 'CONFIRM_CANVAS_READY',
DRAG_CANVAS = 'DRAG_CANVAS',
ZOOM_CANVAS = 'ZOOM_CANVAS',
DRAW_SHAPE = 'DRAW_SHAPE',
SHAPE_DRAWN = 'SHAPE_DRAWN',
MERGE_OBJECTS = 'MERGE_OBJECTS',
OBJECTS_MERGED = 'OBJECTS_MERGED',
GROUP_OBJECTS = 'GROUP_OBJECTS',
OBJECTS_GROUPPED = 'OBJECTS_GROUPPED',
SPLIT_TRACK = 'SPLIT_TRACK',
TRACK_SPLITTED = 'TRACK_SPLITTED',
RESET_CANVAS = 'RESET_CANVAS',
ANNOTATIONS_UPDATED = 'ANNOTATIONS_UPDATED',
} }
export function switchPlay(playing: boolean): AnyAction { export function switchPlay(playing: boolean): AnyAction {
@ -76,6 +95,31 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}; };
} }
export function dragCanvas(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.DRAG_CANVAS,
payload: {
enabled,
},
};
}
export function zoomCanvas(enabled: boolean): AnyAction {
return {
type: AnnotationActionTypes.ZOOM_CANVAS,
payload: {
enabled,
},
};
}
export function resetCanvas(): AnyAction {
return {
type: AnnotationActionTypes.RESET_CANVAS,
payload: {},
};
}
export function confirmCanvasReady(): AnyAction { export function confirmCanvasReady(): AnyAction {
return { return {
type: AnnotationActionTypes.CONFIRM_CANVAS_READY, type: AnnotationActionTypes.CONFIRM_CANVAS_READY,
@ -130,3 +174,121 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
} }
}; };
} }
export function saveAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS,
payload: {},
});
try {
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_UPDATED_STATUS,
payload: {
status,
},
});
});
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_FAILED,
payload: {
error,
},
});
}
};
}
export function drawShape(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
points?: number,
): AnyAction {
let activeControl = ActiveControl.DRAW_RECTANGLE;
if (shapeType === ShapeType.POLYGON) {
activeControl = ActiveControl.DRAW_POLYGON;
} else if (shapeType === ShapeType.POLYLINE) {
activeControl = ActiveControl.DRAW_POLYLINE;
} else if (shapeType === ShapeType.POINTS) {
activeControl = ActiveControl.DRAW_POINTS;
}
return {
type: AnnotationActionTypes.DRAW_SHAPE,
payload: {
shapeType,
labelID,
objectType,
points,
activeControl,
},
};
}
export function shapeDrawn(): AnyAction {
return {
type: AnnotationActionTypes.SHAPE_DRAWN,
payload: {},
};
}
export function mergeObjects(): AnyAction {
return {
type: AnnotationActionTypes.MERGE_OBJECTS,
payload: {},
};
}
export function objectsMerged(): AnyAction {
return {
type: AnnotationActionTypes.OBJECTS_MERGED,
payload: {},
};
}
export function groupObjects(): AnyAction {
return {
type: AnnotationActionTypes.GROUP_OBJECTS,
payload: {},
};
}
export function objectsGroupped(): AnyAction {
return {
type: AnnotationActionTypes.OBJECTS_GROUPPED,
payload: {},
};
}
export function splitTrack(): AnyAction {
return {
type: AnnotationActionTypes.SPLIT_TRACK,
payload: {},
};
}
export function trackSplitted(): AnyAction {
return {
type: AnnotationActionTypes.TRACK_SPLITTED,
payload: {},
};
}
export function annotationsUpdated(annotations: any[]): AnyAction {
return {
type: AnnotationActionTypes.ANNOTATIONS_UPDATED,
payload: {
annotations,
},
};
}

@ -1,7 +1,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import getCore from '../core'; import getCore from 'cvat-core';
const cvat = getCore(); const cvat = getCore();

@ -1,7 +1,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import getCore from '../core'; import getCore from 'cvat-core';
const cvat = getCore(); const cvat = getCore();

@ -1,8 +1,8 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import getCore from '../core'; import getCore from 'cvat-core';
import { getCVATStore } from '../store'; import { getCVATStore } from 'cvat-store';
import { import {
Model, Model,
ModelFiles, ModelFiles,

@ -1,7 +1,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import { SupportedPlugins } from '../reducers/interfaces'; import { SupportedPlugins } from 'reducers/interfaces';
import PluginChecker from '../utils/plugin-checker'; import PluginChecker from 'utils/plugin-checker';
export enum PluginsActionTypes { export enum PluginsActionTypes {
CHECK_PLUGINS = 'CHECK_PLUGINS', CHECK_PLUGINS = 'CHECK_PLUGINS',

@ -1,7 +1,7 @@
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { import {
GridColor, GridColor,
} from '../reducers/interfaces'; } from 'reducers/interfaces';
export enum SettingsActionTypes { export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',

@ -1,8 +1,8 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import { ShareFileInfo } from '../reducers/interfaces'; import { ShareFileInfo } from 'reducers/interfaces';
import getCore from '../core'; import getCore from 'cvat-core';
const core = getCore(); const core = getCore();

@ -1,10 +1,9 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import { TasksQuery } from '../reducers/interfaces'; import { TasksQuery } from 'reducers/interfaces';
import getCore from 'cvat-core';
import { getInferenceStatusAsync } from './models-actions'; import { getInferenceStatusAsync } from './models-actions';
import getCore from '../core';
const cvat = getCore(); const cvat = getCore();
export enum TasksActionTypes { export enum TasksActionTypes {

@ -1,7 +1,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import getCore from '../core'; import getCore from 'cvat-core';
const core = getCore(); const core = getCore();

@ -7,8 +7,8 @@ import {
Result, Result,
} from 'antd'; } from 'antd';
import AnnotationTopBarContainer from '../../containers/annotation-page/top-bar/top-bar'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StandardWorkspaceContainer from '../../containers/annotation-page/standard-workspace/standard-workspace'; import StandardWorkspaceContainer from 'containers/annotation-page/standard-workspace/standard-workspace';
interface Props { interface Props {
jobInstance: any | null | undefined; jobInstance: any | null | undefined;

@ -6,20 +6,40 @@ import {
import { import {
GridColor, GridColor,
} from '../../../reducers/interfaces'; ObjectType,
} from 'reducers/interfaces';
import { Canvas } from '../../../canvas'; import {
Canvas,
} from 'cvat-canvas';
import getCore from 'cvat-core';
const cvat = getCore();
const MAX_DISTANCE_TO_OPEN_SHAPE = 50;
interface Props { interface Props {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frame: number;
grid: boolean; grid: boolean;
gridSize: number; gridSize: number;
gridColor: GridColor; gridColor: GridColor;
gridOpacity: number; gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
onSetupCanvas: () => void; onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
onShapeDrawn: () => void;
onObjectsMerged: () => void;
onObjectsGroupped: () => void;
onTrackSplitted: () => void;
onResetCanvas: () => void;
onAnnotationsUpdated: (annotations: any[]) => void;
} }
export default class CanvasWrapperComponent extends React.PureComponent<Props> { export default class CanvasWrapperComponent extends React.PureComponent<Props> {
@ -75,6 +95,118 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateCanvas(); this.updateCanvas();
} }
private async onShapeDrawn(event: any): Promise<void> {
const {
jobInstance,
activeLabelID,
activeObjectType,
frame,
onShapeDrawn,
onAnnotationsUpdated,
} = this.props;
onShapeDrawn();
const { state } = event.detail;
if (!state.objectType) {
state.objectType = activeObjectType;
}
if (!state.label) {
[state.label] = jobInstance.task.labels
.filter((label: any) => label.id === activeLabelID);
}
if (!state.occluded) {
state.occluded = false;
}
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
await jobInstance.annotations.put([objectState]);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
}
private async onShapeEdited(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
} = this.props;
const {
state,
points,
} = event.detail;
state.points = points;
state.save();
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
}
private async onObjectsMerged(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onObjectsMerged,
} = this.props;
onObjectsMerged();
const { states } = event.detail;
await jobInstance.annotations.merge(states);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
}
private async onObjectsGroupped(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onObjectsGroupped,
} = this.props;
onObjectsGroupped();
const { states } = event.detail;
await jobInstance.annotations.group(states);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
}
private async onTrackSplitted(event: any): Promise<void> {
const {
jobInstance,
frame,
onAnnotationsUpdated,
onTrackSplitted,
} = this.props;
onTrackSplitted();
const { state } = event.detail;
await jobInstance.annotations.split(state, frame);
const annotations = await jobInstance.annotations.get(frame);
onAnnotationsUpdated(annotations);
}
private updateCanvas(): void {
const {
annotations,
frameData,
canvasInstance,
} = this.props;
if (frameData !== null) {
canvasInstance.setup(frameData, annotations);
}
}
private initialSetup(): void { private initialSetup(): void {
const { const {
grid, grid,
@ -84,6 +216,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance, canvasInstance,
jobInstance, jobInstance,
onSetupCanvas, onSetupCanvas,
onDragCanvas,
onZoomCanvas,
onResetCanvas,
} = this.props; } = this.props;
// Size // Size
@ -112,18 +247,65 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.setup', () => { canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit(); canvasInstance.fit();
}, { once: true }); }, { once: true });
}
private updateCanvas(): void { canvasInstance.html().addEventListener('canvas.canceled', () => {
const { onResetCanvas();
annotations, });
frameData,
canvasInstance,
} = this.props;
if (frameData !== null) { canvasInstance.html().addEventListener('canvas.dragstart', () => {
canvasInstance.setup(frameData, annotations); onDragCanvas(true);
} });
canvasInstance.html().addEventListener('canvas.dragstop', () => {
onDragCanvas(false);
});
canvasInstance.html().addEventListener('canvas.zoomstart', () => {
onZoomCanvas(true);
});
canvasInstance.html().addEventListener('canvas.zoomstop', () => {
onZoomCanvas(false);
});
canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise<void> => {
const result = await jobInstance.annotations.select(
event.detail.states,
event.detail.x,
event.detail.y,
);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
canvasInstance.activate(result.state.clientID);
}
});
canvasInstance.html().addEventListener('canvas.find', async (e: any) => {
const result = await jobInstance.annotations
.select(e.detail.states, e.detail.x, e.detail.y);
if (result && result.state) {
if (result.state.shapeType === 'polyline' || result.state.shapeType === 'points') {
if (result.distance > MAX_DISTANCE_TO_OPEN_SHAPE) {
return;
}
}
canvasInstance.select(result.state);
}
});
canvasInstance.html().addEventListener('canvas.edited', this.onShapeEdited.bind(this));
canvasInstance.html().addEventListener('canvas.drawn', this.onShapeDrawn.bind(this));
canvasInstance.html().addEventListener('canvas.merged', this.onObjectsMerged.bind(this));
canvasInstance.html().addEventListener('canvas.groupped', this.onObjectsGroupped.bind(this));
canvasInstance.html().addEventListener('canvas.splitted', this.onTrackSplitted.bind(this));
} }
public render(): JSX.Element { public render(): JSX.Element {

@ -1,127 +0,0 @@
import React from 'react';
import {
Icon,
Layout,
Tooltip,
Popover,
} from 'antd';
import {
CursorIcon,
MoveIcon,
RotateIcon,
FitIcon,
ZoomIcon,
RectangleIcon,
PolygonIcon,
PointIcon,
PolylineIcon,
TagIcon,
MergeIcon,
GroupIcon,
SplitIcon,
} from '../../../icons';
import {
Canvas,
Rotation,
} from '../../../canvas';
interface Props {
canvasInstance: Canvas;
rotateAll: boolean;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
const {
rotateAll,
canvasInstance,
} = props;
return (
<Layout.Sider
className='cvat-annotation-page-controls-sidebar'
theme='light'
width={44}
>
<Tooltip overlay='Cursor' placement='right'>
<Icon component={CursorIcon} />
</Tooltip>
<Tooltip overlay='Move the image' placement='right'>
<Icon component={MoveIcon} />
</Tooltip>
<Popover
overlayClassName='cvat-annotation-page-controls-rotate'
placement='right'
content={(
<>
<Icon
className='cvat-annotation-page-controls-rotate-left'
onClick={(): void => canvasInstance
.rotate(Rotation.ANTICLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
<Icon
className='cvat-annotation-page-controls-rotate-right'
onClick={(): void => canvasInstance
.rotate(Rotation.CLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
</>
)}
trigger='hover'
>
<Tooltip overlay='Rotate the image' placement='topRight'>
<Icon component={RotateIcon} />
</Tooltip>
</Popover>
<hr />
<Tooltip overlay='Fit the image' placement='right'>
<Icon component={FitIcon} />
</Tooltip>
<Tooltip overlay='Zoom the image' placement='right'>
<Icon component={ZoomIcon} />
</Tooltip>
<hr />
<Tooltip overlay='Draw a rectangle' placement='right'>
<Icon component={RectangleIcon} />
</Tooltip>
<Tooltip overlay='Draw a polygon' placement='right'>
<Icon component={PolygonIcon} />
</Tooltip>
<Tooltip overlay='Draw a polyline' placement='right'>
<Icon component={PolylineIcon} />
</Tooltip>
<Tooltip overlay='Draw points' placement='right'>
<Icon component={PointIcon} />
</Tooltip>
<Tooltip overlay='Setup a tag' placement='right'>
<Icon component={TagIcon} />
</Tooltip>
<hr />
<Tooltip overlay='Merge shapes/tracks' placement='right'>
<Icon component={MergeIcon} />
</Tooltip>
<Tooltip overlay='Group shapes/tracks' placement='right'>
<Icon component={GroupIcon} />
</Tooltip>
<Tooltip overlay='Split a track' placement='right'>
<Icon component={SplitIcon} />
</Tooltip>
</Layout.Sider>
);
}

@ -0,0 +1,78 @@
import React from 'react';
import {
Icon,
Layout,
Tooltip,
} from 'antd';
import {
ActiveControl,
} from 'reducers/interfaces';
import {
TagIcon,
} from 'icons';
import {
Canvas,
} from 'cvat-canvas';
import CursorControl from './cursor-control';
import MoveControl from './move-control';
import RotateControl from './rotate-control';
import FitControl from './fit-control';
import ResizeControl from './resize-control';
import DrawRectangleControl from './draw-rectangle-control';
import DrawPolygonControl from './draw-polygon-control';
import DrawPolylineControl from './draw-polyline-control';
import DrawPointsControl from './draw-points-control';
import MergeControl from './merge-control';
import GroupControl from './group-control';
import SplitControl from './split-control';
interface Props {
canvasInstance: Canvas;
rotateAll: boolean;
activeControl: ActiveControl;
onMergeStart(): void;
onGroupStart(): void;
onSplitStart(): void;
}
export default function ControlsSideBarComponent(props: Props): JSX.Element {
return (
<Layout.Sider
className='cvat-annotation-page-controls-sidebar'
theme='light'
width={44}
>
<CursorControl {...props} />
<MoveControl {...props} />
<RotateControl {...props} />
<hr />
<FitControl {...props} />
<ResizeControl {...props} />
<hr />
<DrawRectangleControl {...props} />
<DrawPolygonControl {...props} />
<DrawPolylineControl {...props} />
<DrawPointsControl {...props} />
<Tooltip overlay='Setup a tag' placement='right'>
<Icon component={TagIcon} />
</Tooltip>
<hr />
<MergeControl {...props} />
<GroupControl {...props} />
<SplitControl {...props} />
</Layout.Sider>
);
}

@ -0,0 +1,46 @@
import React from 'react';
import {
Icon,
Tooltip,
} from 'antd';
import {
CursorIcon,
} from 'icons';
import {
ActiveControl,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function CursorControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
return (
<Tooltip overlay='Cursor' placement='right'>
<Icon
component={CursorIcon}
className={activeControl === ActiveControl.CURSOR
? 'cvat-annotation-page-active-control' : ''
}
onClick={(): void => {
if (activeControl !== ActiveControl.CURSOR) {
canvasInstance.cancel();
}
}}
/>
</Tooltip>
);
}

@ -0,0 +1,57 @@
import React from 'react';
import {
Popover,
Icon,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import { PointIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POINTS
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POINTS
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={(
<DrawShapePopoverContainer shapeType={ShapeType.POINTS} />
)}
>
<Icon
{...dynamicIconProps}
component={PointIcon}
/>
</Popover>
);
}

@ -0,0 +1,57 @@
import React from 'react';
import {
Popover,
Icon,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import { PolygonIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYGON
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYGON
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={(
<DrawShapePopoverContainer shapeType={ShapeType.POLYGON} />
)}
>
<Icon
{...dynamicIconProps}
component={PolygonIcon}
/>
</Popover>
);
}

@ -0,0 +1,57 @@
import React from 'react';
import {
Popover,
Icon,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import { PolylineIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_POLYLINE
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_POLYLINE
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={(
<DrawShapePopoverContainer shapeType={ShapeType.POLYLINE} />
)}
>
<Icon
{...dynamicIconProps}
component={PolylineIcon}
/>
</Popover>
);
}

@ -0,0 +1,57 @@
import React from 'react';
import {
Popover,
Icon,
} from 'antd';
import { Canvas } from 'cvat-canvas';
import { RectangleIcon } from 'icons';
import {
ShapeType,
ActiveControl,
} from 'reducers/interfaces';
import DrawShapePopoverContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function DrawRectangleControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
const dynamcPopoverPros = activeControl === ActiveControl.DRAW_RECTANGLE
? {
overlayStyle: {
display: 'none',
},
} : {};
const dynamicIconProps = activeControl === ActiveControl.DRAW_RECTANGLE
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.draw({ enabled: false });
},
} : {};
return (
<Popover
{...dynamcPopoverPros}
overlayClassName='cvat-draw-shape-popover'
placement='right'
content={(
<DrawShapePopoverContainer shapeType={ShapeType.RECTANGLE} />
)}
>
<Icon
{...dynamicIconProps}
component={RectangleIcon}
/>
</Popover>
);
}

@ -0,0 +1,186 @@
import React from 'react';
import {
Row,
Col,
Select,
Button,
InputNumber,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import {
ShapeType,
ObjectType,
StringObject,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
shapeType: ShapeType;
labels: StringObject;
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
points?: number,
): void;
}
interface State {
numberOfPoints?: number;
selectedLabeID: number;
}
function defineMinimumPoints(shapeType: ShapeType): number {
if (shapeType === ShapeType.POLYGON) {
return 3;
}
if (shapeType === ShapeType.POLYLINE) {
return 2;
}
if (shapeType === ShapeType.POINTS) {
return 1;
}
return 0;
}
export default class DrawShapePopoverComponent extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const defaultLabelID = +Object.keys(props.labels)[0];
this.state = {
selectedLabeID: defaultLabelID,
};
}
private onChangePoints = (value: number | undefined): void => {
this.setState({
numberOfPoints: value,
});
};
private onChangeLabel = (value: string): void => {
this.setState({
selectedLabeID: +value,
});
};
private onDrawTrackStart = (): void => {
this.onDrawStart(ObjectType.TRACK);
};
private onDrawShapeStart = (): void => {
this.onDrawStart(ObjectType.SHAPE);
};
private onDrawStart = (objectType: ObjectType): void => {
const {
numberOfPoints,
selectedLabeID,
} = this.state;
const {
shapeType,
onDrawStart,
canvasInstance,
} = this.props;
canvasInstance.cancel();
canvasInstance.draw({
enabled: true,
numberOfPoints,
shapeType,
crosshair: shapeType === ShapeType.RECTANGLE,
});
onDrawStart(shapeType, selectedLabeID,
objectType, numberOfPoints);
};
public render(): JSX.Element {
const {
selectedLabeID,
} = this.state;
const {
shapeType,
labels,
} = this.props;
const minimumPoints = defineMinimumPoints(shapeType);
return (
<div className='cvat-draw-shape-popover-content'>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color' strong>{`Draw new ${shapeType}`}</Text>
</Col>
</Row>
<Row type='flex' justify='start'>
<Col>
<Text className='cvat-text-color'>Label</Text>
</Col>
</Row>
<Row type='flex' justify='center'>
<Col span={24}>
<Select
value={labels[selectedLabeID]}
onChange={this.onChangeLabel}
>
{
Object.keys(labels).map((key: string) => (
<Select.Option
key={key}
value={key}
>
{labels[+key]}
</Select.Option>
))
}
</Select>
</Col>
</Row>
{
shapeType !== ShapeType.RECTANGLE && (
<Row type='flex' justify='space-around' align='middle'>
<Col span={14}>
<Text className='cvat-text-color'> Number of points: </Text>
</Col>
<Col span={10}>
<InputNumber
onChange={this.onChangePoints}
className='cvat-draw-shape-popover-points-selector'
min={minimumPoints}
step={1}
/>
</Col>
</Row>
)
}
<Row type='flex' justify='space-around'>
<Col span={12}>
<Button
onClick={this.onDrawShapeStart}
>
Shape
</Button>
</Col>
<Col span={12}>
<Button
onClick={this.onDrawTrackStart}
>
Track
</Button>
</Col>
</Row>
</div>
);
}
}

@ -0,0 +1,30 @@
import React from 'react';
import {
Icon,
Tooltip,
} from 'antd';
import {
FitIcon,
} from 'icons';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
}
export default function FitControl(props: Props): JSX.Element {
const {
canvasInstance,
} = props;
return (
<Tooltip overlay='Fit the image' placement='right'>
<Icon component={FitIcon} onClick={(): void => canvasInstance.fit()} />
</Tooltip>
);
}

@ -0,0 +1,48 @@
import React from 'react';
import {
Tooltip,
Icon,
} from 'antd';
import {
GroupIcon,
} from 'icons';
import { Canvas } from 'cvat-canvas';
import { ActiveControl } from 'reducers/interfaces';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onGroupStart(): void;
}
export default function GroupControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
onGroupStart,
} = props;
const dynamicIconProps = activeControl === ActiveControl.GROUP
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.group({ enabled: false });
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.group({ enabled: true });
onGroupStart();
},
};
return (
<Tooltip overlay='Group shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={GroupIcon} />
</Tooltip>
);
}

@ -0,0 +1,48 @@
import React from 'react';
import {
Tooltip,
Icon,
} from 'antd';
import {
MergeIcon,
} from 'icons';
import { Canvas } from 'cvat-canvas';
import { ActiveControl } from 'reducers/interfaces';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onMergeStart(): void;
}
export default function MergeControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
onMergeStart,
} = props;
const dynamicIconProps = activeControl === ActiveControl.MERGE
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.merge({ enabled: false });
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.merge({ enabled: true });
onMergeStart();
},
};
return (
<Tooltip overlay='Merge shapes/tracks' placement='right'>
<Icon {...dynamicIconProps} component={MergeIcon} />
</Tooltip>
);
}

@ -0,0 +1,49 @@
import React from 'react';
import {
Icon,
Tooltip,
} from 'antd';
import {
MoveIcon,
} from 'icons';
import {
ActiveControl,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function MoveControl(props: Props): JSX.Element {
const {
canvasInstance,
activeControl,
} = props;
return (
<Tooltip overlay='Move the image' placement='right'>
<Icon
component={MoveIcon}
className={activeControl === ActiveControl.DRAG_CANVAS
? 'cvat-annotation-page-active-control' : ''
}
onClick={(): void => {
if (activeControl === ActiveControl.DRAG_CANVAS) {
canvasInstance.dragCanvas(false);
} else {
canvasInstance.cancel();
canvasInstance.dragCanvas(true);
}
}}
/>
</Tooltip>
);
}

@ -0,0 +1,49 @@
import React from 'react';
import {
Icon,
Tooltip,
} from 'antd';
import {
ZoomIcon,
} from 'icons';
import {
ActiveControl,
} from 'reducers/interfaces';
import {
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
}
export default function ResizeControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
} = props;
return (
<Tooltip overlay='Select a region of interest' placement='right'>
<Icon
component={ZoomIcon}
className={activeControl === ActiveControl.ZOOM_CANVAS
? 'cvat-annotation-page-active-control' : ''
}
onClick={(): void => {
if (activeControl === ActiveControl.ZOOM_CANVAS) {
canvasInstance.zoomCanvas(false);
} else {
canvasInstance.cancel();
canvasInstance.zoomCanvas(true);
}
}}
/>
</Tooltip>
);
}

@ -0,0 +1,58 @@
import React from 'react';
import {
Icon,
Tooltip,
Popover,
} from 'antd';
import {
RotateIcon,
} from 'icons';
import {
Rotation,
Canvas,
} from 'cvat-canvas';
interface Props {
canvasInstance: Canvas;
rotateAll: boolean;
}
export default function RotateControl(props: Props): JSX.Element {
const {
rotateAll,
canvasInstance,
} = props;
return (
<Popover
overlayClassName='cvat-annotation-page-controls-rotate'
placement='right'
content={(
<>
<Tooltip overlay='Rotate the image anticlockwise' placement='topRight'>
<Icon
className='cvat-annotation-page-controls-rotate-left'
onClick={(): void => canvasInstance
.rotate(Rotation.ANTICLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
</Tooltip>
<Tooltip overlay='Rotate the image clockwise' placement='topRight'>
<Icon
className='cvat-annotation-page-controls-rotate-right'
onClick={(): void => canvasInstance
.rotate(Rotation.CLOCKWISE90, rotateAll)}
component={RotateIcon}
/>
</Tooltip>
</>
)}
trigger='hover'
>
<Icon component={RotateIcon} />
</Popover>
);
}

@ -0,0 +1,48 @@
import React from 'react';
import {
Tooltip,
Icon,
} from 'antd';
import {
SplitIcon,
} from 'icons';
import { Canvas } from 'cvat-canvas';
import { ActiveControl } from 'reducers/interfaces';
interface Props {
canvasInstance: Canvas;
activeControl: ActiveControl;
onSplitStart(): void;
}
export default function SplitControl(props: Props): JSX.Element {
const {
activeControl,
canvasInstance,
onSplitStart,
} = props;
const dynamicIconProps = activeControl === ActiveControl.SPLIT
? {
className: 'cvat-annotation-page-active-control',
onClick: (): void => {
canvasInstance.split({ enabled: false });
},
} : {
onClick: (): void => {
canvasInstance.cancel();
canvasInstance.split({ enabled: true });
onSplitStart();
},
};
return (
<Tooltip overlay='Split a track' placement='right'>
<Icon {...dynamicIconProps} component={SplitIcon} />
</Tooltip>
);
}

@ -5,10 +5,10 @@ import {
Layout, Layout,
} from 'antd'; } from 'antd';
import { Canvas } from '../../../canvas'; import { Canvas } from 'cvat-canvas';
import CanvasWrapperContainer from '../../../containers/annotation-page/standard-workspace/canvas-wrapper'; import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import ControlsSideBarContainer from '../../../containers/annotation-page/standard-workspace/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import ObjectSideBarComponent from './objects-side-bar/objects-side-bar'; import ObjectSideBarComponent from './objects-side-bar/objects-side-bar';
interface Props { interface Props {

@ -39,13 +39,17 @@
} }
} }
.cvat-annotation-page-active-control {
background: $header-color;
transform: scale(0.75);
}
.cvat-annotation-page-controls-rotate-left, .cvat-annotation-page-controls-rotate-left,
.cvat-annotation-page-controls-rotate-right { .cvat-annotation-page-controls-rotate-right {
transform: scale(0.65); transform: scale(0.65);
border-radius: 5px; border-radius: 5px;
&:hover { &:hover {
background: $header-color;
transform: scale(0.75); transform: scale(0.75);
} }
&:active { &:active {
@ -62,4 +66,37 @@
.cvat-annotation-page-controls-rotate-right > svg { .cvat-annotation-page-controls-rotate-right > svg {
transform: scaleX(-1); transform: scaleX(-1);
}
.cvat-draw-shape-popover >
.ant-popover-content >
.ant-popover-inner > div >
.ant-popover-inner-content {
padding: 0px;
}
.cvat-draw-shape-popover-points-selector {
width: 100%;
}
.cvat-draw-shape-popover-content {
padding: 10px;
border-radius: 5px;
background: $background-color-2;
width: 250px;
> div {
margin-top: 5px;
}
> div:nth-child(3) > div > div {
width: 100%;
}
div:last-child > div > button {
width: 100%;
&:nth-child(1) {
border-radius: 3px 0px 0px 3px;
}
&:nth-child(2) {
border-radius: 0px 3px 3px 0px;
}
}
} }

@ -53,6 +53,12 @@
} }
} }
.cvat-annotation-disabled-header-button {
@extend .cvat-annotation-header-button;
opacity: 0.5;
pointer-events: none;
}
.cvat-annotation-header-player-group > div { .cvat-annotation-header-player-group > div {
height: 54px; height: 54px;
line-height: 0px; line-height: 0px;

@ -9,6 +9,8 @@ import {
Input, Input,
Tooltip, Tooltip,
Select, Select,
Modal,
Timeline,
} from 'antd'; } from 'antd';
import { SliderValue } from 'antd/lib/slider'; import { SliderValue } from 'antd/lib/slider';
@ -36,9 +38,34 @@ interface Props {
frame: number; frame: number;
frameStep: number; frameStep: number;
playing: boolean; playing: boolean;
saving: boolean;
savingStatuses: string[];
canvasIsReady: boolean; canvasIsReady: boolean;
onChangeFrame(frame: number, playing: boolean): void; onChangeFrame(frame: number, playing: boolean): void;
onSwitchPlay(playing: boolean): void; onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
}
function SavingOverlay(saving: boolean, statuses: string[]): JSX.Element {
return (
<Modal
title='Saving changes on the server'
visible={saving}
footer={[]}
closable={false}
>
<Timeline pending={statuses[statuses.length - 1] || 'Pending..'}>
{
statuses.slice(0, -1)
.map((
status: string,
id: number,
// eslint-disable-next-line react/no-array-index-key
) => <Timeline.Item key={id}>{status}</Timeline.Item>)
}
</Timeline>
</Modal>
);
} }
export default function AnnotationTopBarComponent(props: Props): JSX.Element { export default function AnnotationTopBarComponent(props: Props): JSX.Element {
@ -47,9 +74,12 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
frame, frame,
frameStep, frameStep,
playing, playing,
saving,
savingStatuses,
canvasIsReady, canvasIsReady,
onChangeFrame, onChangeFrame,
onSwitchPlay, onSwitchPlay,
onSaveAnnotation,
} = props; } = props;
if (playing && canvasIsReady) { if (playing && canvasIsReady) {
@ -62,6 +92,8 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
} }
} }
const savingOverlay = SavingOverlay(saving, savingStatuses);
return ( return (
<Layout.Header className='cvat-annotation-page-header'> <Layout.Header className='cvat-annotation-page-header'>
<Row type='flex' justify='space-between'> <Row type='flex' justify='space-between'>
@ -70,9 +102,17 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
<Icon component={MainMenuIcon} /> <Icon component={MainMenuIcon} />
<span>Menu</span> <span>Menu</span>
</div> </div>
<div className='cvat-annotation-header-button'> <div className={saving ? 'cvat-annotation-disabled-header-button' : 'cvat-annotation-header-button'}>
<Icon component={SaveIcon} /> <Icon
<span>Save</span> component={SaveIcon}
onClick={async (): Promise<void> => {
onSaveAnnotation(jobInstance);
}}
/>
<span>
{ saving ? 'Saving...' : 'Save' }
</span>
{ savingOverlay }
</div> </div>
<div className='cvat-annotation-header-button'> <div className='cvat-annotation-header-button'>
<Icon component={UndoIcon} /> <Icon component={UndoIcon} />

@ -13,13 +13,14 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import ConnectedFileManager, {
FileManagerContainer,
} from 'containers/file-manager/file-manager';
import { ModelFiles } from 'reducers/interfaces';
import CreateModelForm, { import CreateModelForm, {
CreateModelForm as WrappedCreateModelForm, CreateModelForm as WrappedCreateModelForm,
} from './create-model-form'; } from './create-model-form';
import ConnectedFileManager, {
FileManagerContainer,
} from '../../containers/file-manager/file-manager';
import { ModelFiles } from '../../reducers/interfaces';
interface Props { interface Props {
createModel(name: string, files: ModelFiles, global: boolean): void; createModel(name: string, files: ModelFiles, global: boolean): void;

@ -7,9 +7,8 @@ import {
} from 'antd'; } from 'antd';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { ModelFiles } from 'reducers/interfaces';
import CreateModelContent from './create-model-content'; import CreateModelContent from './create-model-content';
import { ModelFiles } from '../../reducers/interfaces';
interface Props { interface Props {
createModel(name: string, files: ModelFiles, global: boolean): void; createModel(name: string, files: ModelFiles, global: boolean): void;

@ -12,7 +12,7 @@ import {
import Form, { FormComponentProps } from 'antd/lib/form/Form'; import Form, { FormComponentProps } from 'antd/lib/form/Form';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import patterns from '../../utils/validation-patterns'; import patterns from 'utils/validation-patterns';
export interface AdvancedConfiguration { export interface AdvancedConfiguration {
bugTracker?: string; bugTracker?: string;

@ -11,10 +11,10 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import FileManagerContainer from 'containers/file-manager/file-manager';
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form'; import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
import LabelsEditor from '../labels-editor/labels-editor'; import LabelsEditor from '../labels-editor/labels-editor';
import FileManagerContainer from '../../containers/file-manager/file-manager';
import { Files } from '../file-manager/file-manager'; import { Files } from '../file-manager/file-manager';
export interface CreateTaskData { export interface CreateTaskData {

@ -13,18 +13,18 @@ import {
notification, notification,
} from 'antd'; } from 'antd';
import SettingsPageComponent from './settings-page/settings-page'; import SettingsPageComponent from 'components/settings-page/settings-page';
import TasksPageContainer from '../containers/tasks-page/tasks-page'; import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from '../containers/create-task-page/create-task-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import TaskPageContainer from '../containers/task-page/task-page'; import TaskPageContainer from 'containers/task-page/task-page';
import ModelsPageContainer from '../containers/models-page/models-page'; import ModelsPageContainer from 'containers/models-page/models-page';
import CreateModelPageContainer from '../containers/create-model-page/create-model-page'; import CreateModelPageContainer from 'containers/create-model-page/create-model-page';
import AnnotationPageContainer from '../containers/annotation-page/annotation-page'; import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import LoginPageContainer from '../containers/login-page/login-page'; import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from '../containers/register-page/register-page'; import RegisterPageContainer from 'containers/register-page/register-page';
import HeaderContainer from '../containers/header/header'; import HeaderContainer from 'containers/header/header';
import { NotificationsState } from '../reducers/interfaces'; import { NotificationsState } from 'reducers/interfaces';
type CVATAppProps = { type CVATAppProps = {
loadFormats: () => void; loadFormats: () => void;
@ -155,13 +155,15 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
const { users } = notifications.errors; const { users } = notifications.errors;
const { share } = notifications.errors; const { share } = notifications.errors;
const { models } = notifications.errors; const { models } = notifications.errors;
const { annotation } = notifications.errors;
const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
|| !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading || !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading
|| !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching || !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching
|| !!users.fetching || !!share.fetching || !!models.creating || !!models.starting || !!users.fetching || !!share.fetching || !!models.creating || !!models.starting
|| !!models.fetching || !!models.deleting || !!models.inferenceStatusFetching || !!models.fetching || !!models.deleting || !!models.inferenceStatusFetching
|| !!models.metaFetching; || !!models.metaFetching || !!annotation.frameFetching || !!annotation.saving
|| !!annotation.jobFetching;
if (auth.authorized) { if (auth.authorized) {
showError(auth.authorized.message, auth.authorized.reason); showError(auth.authorized.message, auth.authorized.reason);
@ -226,6 +228,15 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
models.inferenceStatusFetching.reason, models.inferenceStatusFetching.reason,
); );
} }
if (annotation.jobFetching) {
showError(annotation.jobFetching.message, annotation.jobFetching.reason);
}
if (annotation.frameFetching) {
showError(annotation.frameFetching.message, annotation.frameFetching.reason);
}
if (annotation.saving) {
showError(annotation.saving.message, annotation.saving.reason);
}
if (shown) { if (shown) {
resetErrors(); resetErrors();

@ -14,11 +14,11 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import getCore from '../../core'; import getCore from 'cvat-core';
import { import {
CVATLogo, CVATLogo,
AccountIcon, AccountIcon,
} from '../../icons'; } from 'icons';
const core = getCore(); const core = getCore();
const serverHost = core.config.backendAPI.slice(0, -7); const serverHost = core.config.backendAPI.slice(0, -7);

@ -13,6 +13,7 @@ import {
import Form, { FormComponentProps } from 'antd/lib/form/Form'; import Form, { FormComponentProps } from 'antd/lib/form/Form';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import patterns from 'utils/validation-patterns';
import { import {
equalArrayHead, equalArrayHead,
@ -20,7 +21,7 @@ import {
Label, Label,
Attribute, Attribute,
} from './common'; } from './common';
import patterns from '../../utils/validation-patterns';
export enum AttributeType { export enum AttributeType {
SELECT = 'SELECT', SELECT = 'SELECT',

@ -14,11 +14,10 @@ import {
notification, notification,
} from 'antd'; } from 'antd';
import { Model } from '../../reducers/interfaces'; import {
Model,
interface StringObject { StringObject,
[index: string]: string; } from 'reducers/interfaces';
}
interface Props { interface Props {
modelsFetching: boolean; modelsFetching: boolean;

@ -9,7 +9,7 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Model } from '../../reducers/interfaces'; import { Model } from 'reducers/interfaces';
interface Props { interface Props {
model: Model; model: Model;

@ -7,8 +7,8 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Model } from 'reducers/interfaces';
import BuiltModelItemComponent from './built-model-item'; import BuiltModelItemComponent from './built-model-item';
import { Model } from '../../reducers/interfaces';
interface Props { interface Props {
models: Model[]; models: Model[];

@ -10,7 +10,7 @@ import {
import { import {
EmptyTasksIcon as EmptyModelsIcon, EmptyTasksIcon as EmptyModelsIcon,
} from '../../icons'; } from 'icons';
export default function EmptyListComponent(): JSX.Element { export default function EmptyListComponent(): JSX.Element {
return ( return (

@ -13,8 +13,8 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import moment from 'moment'; import moment from 'moment';
import { MenuIcon } from '../../icons'; import { MenuIcon } from 'icons';
import { Model } from '../../reducers/interfaces'; import { Model } from 'reducers/interfaces';
interface Props { interface Props {
model: Model; model: Model;

@ -7,8 +7,9 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Model } from 'reducers/interfaces';
import UploadedModelItem from './uploaded-model-item'; import UploadedModelItem from './uploaded-model-item';
import { Model } from '../../reducers/interfaces';
interface Props { interface Props {
registeredUsers: any[]; registeredUsers: any[];

@ -7,7 +7,7 @@ import {
Form, Form,
} from 'antd'; } from 'antd';
import patterns from '../../utils/validation-patterns'; import patterns from 'utils/validation-patterns';
export interface RegisterData { export interface RegisterData {
username: string; username: string;

@ -16,12 +16,12 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { import {
PlaycontrolBackJumpIcon, PlaycontrolBackJumpIcon,
PlaycontrolForwardJumpIcon, PlaycontrolForwardJumpIcon,
} from '../../icons'; } from 'icons';
import { import {
FrameSpeed, FrameSpeed,
GridColor, GridColor,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface Props { interface Props {
frameStep: number; frameStep: number;

@ -13,8 +13,8 @@ import Text from 'antd/lib/typography/Text';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import WorkspaceSettingsContainer from '../../containers/settings-page/workspace-settings'; import WorkspaceSettingsContainer from 'containers/settings-page/workspace-settings';
import PlayerSettingsContainer from '../../containers/settings-page/player-settings'; import PlayerSettingsContainer from 'containers/settings-page/player-settings';
function SettingsPage(props: RouteComponentProps): JSX.Element { function SettingsPage(props: RouteComponentProps): JSX.Element {
return ( return (

@ -15,11 +15,11 @@ import Title from 'antd/lib/typography/Title';
import moment from 'moment'; import moment from 'moment';
import getCore from 'cvat-core';
import patterns from 'utils/validation-patterns';
import { getReposData, syncRepos } from 'utils/git-utils';
import UserSelector from './user-selector'; import UserSelector from './user-selector';
import LabelsEditorComponent from '../labels-editor/labels-editor'; import LabelsEditorComponent from '../labels-editor/labels-editor';
import getCore from '../../core';
import patterns from '../../utils/validation-patterns';
import { getReposData, syncRepos } from '../../utils/git-utils';
const core = getCore(); const core = getCore();

@ -14,9 +14,8 @@ import Text from 'antd/lib/typography/Text';
import moment from 'moment'; import moment from 'moment';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import getCore from 'cvat-core';
import UserSelector from './user-selector'; import UserSelector from './user-selector';
import getCore from '../../core';
const core = getCore(); const core = getCore();

@ -10,11 +10,11 @@ import {
Result, Result,
} from 'antd'; } from 'antd';
import DetailsContainer from 'containers/task-page/details';
import JobListContainer from 'containers/task-page/job-list';
import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog';
import { Task } from 'reducers/interfaces';
import TopBarComponent from './top-bar'; import TopBarComponent from './top-bar';
import DetailsContainer from '../../containers/task-page/details';
import JobListContainer from '../../containers/task-page/job-list';
import ModelRunnerModalContainer from '../../containers/model-runner-dialog/model-runner-dialog';
import { Task } from '../../reducers/interfaces';
interface TaskPageComponentProps { interface TaskPageComponentProps {
task: Task | null | undefined; task: Task | null | undefined;

@ -10,8 +10,8 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; import ActionsMenuContainer from 'containers/actions-menu/actions-menu';
import { MenuIcon } from '../../icons'; import { MenuIcon } from 'icons';
interface DetailsComponentProps { interface DetailsComponentProps {
taskInstance: any; taskInstance: any;

@ -8,7 +8,7 @@ import {
Icon, Icon,
} from 'antd'; } from 'antd';
import { EmptyTasksIcon } from '../../icons'; import { EmptyTasksIcon } from 'icons';
export default function EmptyListComponent(): JSX.Element { export default function EmptyListComponent(): JSX.Element {
return ( return (

@ -14,9 +14,9 @@ import {
import moment from 'moment'; import moment from 'moment';
import ActionsMenuContainer from '../../containers/actions-menu/actions-menu'; import ActionsMenuContainer from 'containers/actions-menu/actions-menu';
import { ActiveInference } from '../../reducers/interfaces'; import { ActiveInference } from 'reducers/interfaces';
import { MenuIcon } from '../../icons'; import { MenuIcon } from 'icons';
export interface TaskItemProps { export interface TaskItemProps {
taskInstance: any; taskInstance: any;

@ -6,8 +6,8 @@ import {
Pagination, Pagination,
} from 'antd'; } from 'antd';
import ModelRunnerModalContainer from '../../containers/model-runner-dialog/model-runner-dialog'; import ModelRunnerModalContainer from 'containers/model-runner-dialog/model-runner-dialog';
import TaskItem from '../../containers/tasks-page/task-item'; import TaskItem from 'containers/tasks-page/task-item';
export interface ContentListProps { export interface ContentListProps {
onSwitchPage(page: number): void; onSwitchPage(page: number): void;

@ -13,12 +13,13 @@ import Text from 'antd/lib/typography/Text';
import { import {
TasksQuery, TasksQuery,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import FeedbackComponent from 'components/feedback/feedback';
import TaskListContainer from 'containers/tasks-page/tasks-list';
import TopBar from './top-bar'; import TopBar from './top-bar';
import FeedbackComponent from '../feedback/feedback';
import EmptyListComponent from './empty-list'; import EmptyListComponent from './empty-list';
import TaskListContainer from '../../containers/tasks-page/tasks-list';
interface TasksPageProps { interface TasksPageProps {
tasksFetching: boolean; tasksFetching: boolean;

@ -1,18 +1,18 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ActionsMenuComponent from '../../components/actions-menu/actions-menu'; import ActionsMenuComponent from 'components/actions-menu/actions-menu';
import { import {
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import { showRunModelDialog } from '../../actions/models-actions'; import { showRunModelDialog } from 'actions/models-actions';
import { import {
dumpAnnotationsAsync, dumpAnnotationsAsync,
loadAnnotationsAsync, loadAnnotationsAsync,
exportDatasetAsync, exportDatasetAsync,
deleteTaskAsync, deleteTaskAsync,
} from '../../actions/tasks-actions'; } from 'actions/tasks-actions';
interface OwnProps { interface OwnProps {
taskInstance: any; taskInstance: any;

@ -3,12 +3,12 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from '../../components/annotation-page/annotation-page'; import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from '../../actions/annotation-actions'; import { getJobAsync } from 'actions/annotation-actions';
import { import {
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
type OwnProps = RouteComponentProps<{ type OwnProps = RouteComponentProps<{
tid: string; tid: string;

@ -1,31 +1,51 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CanvasWrapperComponent from '../../../components/annotation-page/standard-workspace/canvas-wrapper'; import CanvasWrapperComponent from 'components/annotation-page/standard-workspace/canvas-wrapper';
import { import {
confirmCanvasReady, confirmCanvasReady,
} from '../../../actions/annotation-actions'; dragCanvas,
zoomCanvas,
resetCanvas,
shapeDrawn,
objectsMerged,
objectsGroupped,
trackSplitted,
annotationsUpdated,
} from 'actions/annotation-actions';
import { import {
GridColor, GridColor,
ObjectType,
CombinedState, CombinedState,
} from '../../../reducers/interfaces'; } from 'reducers/interfaces';
import { Canvas } from '../../../canvas'; import { Canvas } from 'cvat-canvas';
interface StateToProps { interface StateToProps {
canvasInstance: Canvas; canvasInstance: Canvas;
jobInstance: any; jobInstance: any;
annotations: any[]; annotations: any[];
frameData: any; frameData: any;
frame: number;
grid: boolean; grid: boolean;
gridSize: number; gridSize: number;
gridColor: GridColor; gridColor: GridColor;
gridOpacity: number; gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
} }
interface DispatchToProps { interface DispatchToProps {
onSetupCanvas(): void; onSetupCanvas(): void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
onResetCanvas: () => void;
onShapeDrawn: () => void;
onObjectsMerged: () => void;
onObjectsGroupped: () => void;
onTrackSplitted: () => void;
onAnnotationsUpdated: (annotations: any[]) => void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -33,9 +53,16 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvasInstance, canvasInstance,
jobInstance, jobInstance,
frameData, frameData,
frame,
annotations, annotations,
drawing,
} = state.annotation; } = state.annotation;
const {
activeLabelID,
activeObjectType,
} = drawing;
const { const {
grid, grid,
gridSize, gridSize,
@ -47,11 +74,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvasInstance, canvasInstance,
jobInstance, jobInstance,
frameData, frameData,
frame,
annotations, annotations,
grid, grid,
gridSize, gridSize,
gridColor, gridColor,
gridOpacity, gridOpacity,
activeLabelID,
activeObjectType,
}; };
} }
@ -60,6 +90,30 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSetupCanvas(): void { onSetupCanvas(): void {
dispatch(confirmCanvasReady()); dispatch(confirmCanvasReady());
}, },
onDragCanvas(enabled: boolean): void {
dispatch(dragCanvas(enabled));
},
onZoomCanvas(enabled: boolean): void {
dispatch(zoomCanvas(enabled));
},
onResetCanvas(): void {
dispatch(resetCanvas());
},
onShapeDrawn(): void {
dispatch(shapeDrawn());
},
onObjectsMerged(): void {
dispatch(objectsMerged());
},
onObjectsGroupped(): void {
dispatch(objectsGroupped());
},
onTrackSplitted(): void {
dispatch(trackSplitted());
},
onAnnotationsUpdated(annotations: any[]): void {
dispatch(annotationsUpdated(annotations));
},
}; };
} }

@ -1,35 +0,0 @@
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from '../../../canvas';
import ControlsSideBarComponent from '../../../components/annotation-page/standard-workspace/controls-side-bar';
import { CombinedState } from '../../../reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
rotateAll: boolean;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
} = state;
return {
rotateAll: settings.player.rotateAll,
canvasInstance: annotation.canvasInstance,
};
}
function StandardWorkspaceContainer(props: StateToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
}
export default connect(
mapStateToProps,
)(StandardWorkspaceContainer);

@ -0,0 +1,79 @@
import React from 'react';
import { connect } from 'react-redux';
import { Canvas } from 'cvat-canvas';
import {
mergeObjects,
groupObjects,
splitTrack,
} from 'actions/annotation-actions';
import ControlsSideBarComponent from 'components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar';
import {
ActiveControl,
CombinedState,
StringObject,
} from 'reducers/interfaces';
interface StateToProps {
canvasInstance: Canvas;
rotateAll: boolean;
activeControl: ActiveControl;
labels: StringObject;
}
interface DispatchToProps {
onMergeStart(): void;
onGroupStart(): void;
onSplitStart(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation,
settings,
} = state;
const {
canvasInstance,
activeControl,
} = annotation;
const labels = annotation.jobInstance.task.labels
.reduce((acc: StringObject, label: any): StringObject => {
acc[label.id as number] = label.name;
return acc;
}, {});
return {
rotateAll: settings.player.rotateAll,
canvasInstance,
activeControl,
labels,
};
}
function dispatchToProps(dispatch: any): DispatchToProps {
return {
onMergeStart(): void {
dispatch(mergeObjects());
},
onGroupStart(): void {
dispatch(groupObjects());
},
onSplitStart(): void {
dispatch(splitTrack());
},
};
}
function StandardWorkspaceContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<ControlsSideBarComponent {...props} />
);
}
export default connect(
mapStateToProps,
dispatchToProps,
)(StandardWorkspaceContainer);

@ -0,0 +1,80 @@
import React from 'react';
import { connect } from 'react-redux';
import {
CombinedState,
ShapeType,
ObjectType,
StringObject,
} from 'reducers/interfaces';
import {
drawShape,
} from 'actions/annotation-actions';
import { Canvas } from 'cvat-canvas';
import DrawShapePopoverComponent from 'components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover';
interface OwnProps {
shapeType: ShapeType;
}
interface DispatchToProps {
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
points?: number,
): void;
}
interface StateToProps {
canvasInstance: Canvas;
shapeType: ShapeType;
labels: StringObject;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onDrawStart(
shapeType: ShapeType,
labelID: number,
objectType: ObjectType,
points?: number,
): void {
dispatch(drawShape(shapeType, labelID, objectType, points));
},
};
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const {
annotation,
} = state;
const {
canvasInstance,
} = annotation;
const labels = annotation.jobInstance.task.labels
.reduce((acc: StringObject, label: any): StringObject => {
acc[label.id as number] = label.name;
return acc;
}, {});
return {
...own,
canvasInstance,
labels,
};
}
function DrawShapePopoverContainer(props: DispatchToProps & StateToProps): JSX.Element {
return (
<DrawShapePopoverComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(DrawShapePopoverContainer);

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Canvas } from '../../../canvas'; import { Canvas } from 'cvat-canvas';
import StandardWorkspaceComponent from '../../../components/annotation-page/standard-workspace/standard-workspace'; import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import { CombinedState } from '../../../reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {

@ -4,10 +4,11 @@ import { connect } from 'react-redux';
import { import {
changeFrameAsync, changeFrameAsync,
switchPlay as switchPlayAction, switchPlay as switchPlayAction,
} from '../../../actions/annotation-actions'; saveAnnotationsAsync,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from '../../../components/annotation-page/top-bar/top-bar'; import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { CombinedState } from '../../../reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
jobInstance: any; jobInstance: any;
@ -15,11 +16,14 @@ interface StateToProps {
frameStep: number; frameStep: number;
playing: boolean; playing: boolean;
canvasIsReady: boolean; canvasIsReady: boolean;
saving: boolean;
savingStatuses: string[];
} }
interface DispatchToProps { interface DispatchToProps {
onChangeFrame(frame: number, playing: boolean): void; onChangeFrame(frame: number, playing: boolean): void;
onSwitchPlay(playing: boolean): void; onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -28,12 +32,23 @@ function mapStateToProps(state: CombinedState): StateToProps {
settings, settings,
} = state; } = state;
const {
playing,
saving,
savingStatuses,
canvasIsReady,
frame,
jobInstance,
} = annotation;
return { return {
jobInstance: annotation.jobInstance,
frame: annotation.frame as number, // is number when jobInstance specified
frameStep: settings.player.frameStep, frameStep: settings.player.frameStep,
playing: annotation.playing, playing,
canvasIsReady: annotation.canvasIsReady, saving,
savingStatuses,
canvasIsReady,
frame,
jobInstance,
}; };
} }
@ -45,30 +60,15 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onSwitchPlay(playing: boolean): void { onSwitchPlay(playing: boolean): void {
dispatch(switchPlayAction(playing)); dispatch(switchPlayAction(playing));
}, },
onSaveAnnotation(sessionInstance: any): void {
dispatch(saveAnnotationsAsync(sessionInstance));
},
}; };
} }
function AnnotationTopBarContainer(props: StateToProps & DispatchToProps): JSX.Element { function AnnotationTopBarContainer(props: StateToProps & DispatchToProps): JSX.Element {
const {
jobInstance,
frame,
frameStep,
playing,
canvasIsReady,
onChangeFrame,
onSwitchPlay,
} = props;
return ( return (
<AnnotationTopBarComponent <AnnotationTopBarComponent {...props} />
jobInstance={jobInstance}
frame={frame}
frameStep={frameStep}
playing={playing}
canvasIsReady={canvasIsReady}
onChangeFrame={onChangeFrame}
onSwitchPlay={onSwitchPlay}
/>
); );
} }

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import CreateModelPageComponent from '../../components/create-model-page/create-model-page'; import CreateModelPageComponent from 'components/create-model-page/create-model-page';
import { createModelAsync } from '../../actions/models-actions'; import { createModelAsync } from 'actions/models-actions';
import { import {
ModelFiles, ModelFiles,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
isAdmin: boolean; isAdmin: boolean;

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { CombinedState } from '../../reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
import CreateTaskComponent from '../../components/create-task-page/create-task-page'; import CreateTaskComponent from 'components/create-task-page/create-task-page';
import { CreateTaskData } from '../../components/create-task-page/create-task-content'; import { CreateTaskData } from 'components/create-task-page/create-task-content';
import { createTaskAsync } from '../../actions/tasks-actions'; import { createTaskAsync } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
status: string; status: string;

@ -2,13 +2,13 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { TreeNodeNormal } from 'antd/lib/tree/Tree'; import { TreeNodeNormal } from 'antd/lib/tree/Tree';
import FileManagerComponent, { Files } from '../../components/file-manager/file-manager'; import FileManagerComponent, { Files } from 'components/file-manager/file-manager';
import { loadShareDataAsync } from '../../actions/share-actions'; import { loadShareDataAsync } from 'actions/share-actions';
import { import {
ShareItem, ShareItem,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface OwnProps { interface OwnProps {
ref: any; ref: any;

@ -1,13 +1,13 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { logoutAsync } from '../../actions/auth-actions'; import { logoutAsync } from 'actions/auth-actions';
import { import {
SupportedPlugins, SupportedPlugins,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import HeaderComponent from '../../components/header/header'; import HeaderComponent from 'components/header/header';
interface StateToProps { interface StateToProps {
logoutFetching: boolean; logoutFetching: boolean;

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loginAsync } from '../../actions/auth-actions'; import { loginAsync } from 'actions/auth-actions';
import LoginPageComponent from '../../components/login-page/login-page'; import LoginPageComponent from 'components/login-page/login-page';
import { CombinedState } from '../../reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
fetching: boolean; fetching: boolean;

@ -1,16 +1,16 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ModelRunnerModalComponent from '../../components/model-runner-modal/model-runner-modal'; import ModelRunnerModalComponent from 'components/model-runner-modal/model-runner-modal';
import { import {
Model, Model,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import { import {
getModelsAsync, getModelsAsync,
inferModelAsync, inferModelAsync,
closeRunModelDialog, closeRunModelDialog,
} from '../../actions/models-actions'; } from 'actions/models-actions';
interface StateToProps { interface StateToProps {

@ -1,15 +1,15 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import ModelsPageComponent from '../../components/models-page/models-page'; import ModelsPageComponent from 'components/models-page/models-page';
import { import {
Model, Model,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import { import {
getModelsAsync, getModelsAsync,
deleteModelAsync, deleteModelAsync,
} from '../../actions/models-actions'; } from 'actions/models-actions';
interface StateToProps { interface StateToProps {
installedAutoAnnotation: boolean; installedAutoAnnotation: boolean;

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { registerAsync } from '../../actions/auth-actions'; import { registerAsync } from 'actions/auth-actions';
import RegisterPageComponent from '../../components/register-page/register-page'; import RegisterPageComponent from 'components/register-page/register-page';
import { CombinedState } from '../../reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
fetching: boolean; fetching: boolean;

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PlayerSettingsComponent from '../../components/settings-page/player-settings'; import PlayerSettingsComponent from 'components/settings-page/player-settings';
import { import {
switchRotateAll, switchRotateAll,
@ -9,13 +9,13 @@ import {
changeGridSize, changeGridSize,
changeGridColor, changeGridColor,
changeGridOpacity, changeGridOpacity,
} from '../../actions/settings-actions'; } from 'actions/settings-actions';
import { import {
CombinedState, CombinedState,
FrameSpeed, FrameSpeed,
GridColor, GridColor,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface StateToProps { interface StateToProps {
frameStep: number; frameStep: number;

@ -3,9 +3,9 @@ import { connect } from 'react-redux';
import { import {
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import WorkspaceSettingsComponent from '../../components/settings-page/workspace-settings'; import WorkspaceSettingsComponent from 'components/settings-page/workspace-settings';
interface StateToProps { interface StateToProps {
autoSave: boolean; autoSave: boolean;

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import DetailsComponent from '../../components/task-page/details'; import DetailsComponent from 'components/task-page/details';
import { updateTaskAsync } from '../../actions/tasks-actions'; import { updateTaskAsync } from 'actions/tasks-actions';
import { import {
Task, Task,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface OwnProps { interface OwnProps {
task: Task; task: Task;

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import JobListComponent from '../../components/task-page/job-list'; import JobListComponent from 'components/task-page/job-list';
import { updateJobAsync } from '../../actions/tasks-actions'; import { updateJobAsync } from 'actions/tasks-actions';
import { import {
Task, Task,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
interface OwnProps { interface OwnProps {
task: Task; task: Task;

@ -3,13 +3,13 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import { getTasksAsync } from '../../actions/tasks-actions'; import { getTasksAsync } from 'actions/tasks-actions';
import TaskPageComponent from '../../components/task-page/task-page'; import TaskPageComponent from 'components/task-page/task-page';
import { import {
Task, Task,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
type Props = RouteComponentProps<{id: string}>; type Props = RouteComponentProps<{id: string}>;

@ -5,13 +5,13 @@ import {
TasksQuery, TasksQuery,
CombinedState, CombinedState,
ActiveInference, ActiveInference,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import TaskItemComponent from '../../components/tasks-page/task-item'; import TaskItemComponent from 'components/tasks-page/task-item';
import { import {
getTasksAsync, getTasksAsync,
} from '../../actions/tasks-actions'; } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
deleted: boolean; deleted: boolean;

@ -5,13 +5,13 @@ import {
TasksState, TasksState,
TasksQuery, TasksQuery,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import TasksListComponent from '../../components/tasks-page/task-list'; import TasksListComponent from 'components/tasks-page/task-list';
import { import {
getTasksAsync, getTasksAsync,
} from '../../actions/tasks-actions'; } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
tasks: TasksState; tasks: TasksState;

@ -5,14 +5,14 @@ import {
Task, Task,
TasksQuery, TasksQuery,
CombinedState, CombinedState,
} from '../../reducers/interfaces'; } from 'reducers/interfaces';
import TasksPageComponent from '../../components/tasks-page/tasks-page'; import TasksPageComponent from 'components/tasks-page/tasks-page';
import { import {
getTasksAsync, getTasksAsync,
hideEmptyTasks, hideEmptyTasks,
} from '../../actions/tasks-actions'; } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
tasksFetching: boolean; tasksFetching: boolean;

@ -5,7 +5,7 @@ import { connect, Provider } from 'react-redux';
import CVATApplication from './components/cvat-app'; import CVATApplication from './components/cvat-app';
import createRootReducer from './reducers/root-reducer'; import createRootReducer from './reducers/root-reducer';
import createCVATStore, { getCVATStore } from './store'; import createCVATStore, { getCVATStore } from './cvat-store';
import { authorizedAsync } from './actions/auth-actions'; import { authorizedAsync } from './actions/auth-actions';
import { getFormatsAsync } from './actions/formats-actions'; import { getFormatsAsync } from './actions/formats-actions';

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save