React UI: Attribute annotation mode (#1255)

* Done main work

* Fixed mount/unmount for canvas wrapper

* Refactoring, added filters

* Added missed file

* Removed unnecessary useEffect

* Removed extra code

* Max 9 attributes, inputNumber -> Input in aam

* Added blur

* Renamed component

* Fixed condition when validate number attribute

* Some minor fixes

* Fixed hotkeys config

* Fixed canvas zoom

* Improved behaviour of number & text

* Fixed attributes switching order

* Fix tags

* Fixed interval
main
Boris Sekachev 6 years ago committed by GitHub
parent 6a7bf6d267
commit 1bb582f7f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2,
]);
const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + canvasOffset.top,
];
const dragged = {
@ -725,7 +726,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) {
const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding,
bbox.width + padding, bbox.height + padding);
bbox.width + padding * 2, bbox.height + padding * 2);
}
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement);
@ -1014,7 +1015,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
}
private deactivate(): void {
private deactivateAttribute(): void {
const { clientID, attributeID } = this.activeElement;
if (clientID !== null && attributeID !== null) {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = '';
}
}
this.activeElement = {
...this.activeElement,
attributeID: null,
};
}
}
private deactivateShape(): void {
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID];
@ -1047,29 +1067,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects();
this.activeElement = {
...this.activeElement,
clientID: null,
attributeID: null,
};
}
}
private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement.clientID !== null) {
// Check if it is the same element
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
private deactivate(): void {
this.deactivateAttribute();
this.deactivateShape();
}
// Deactivate previous element
this.deactivate();
}
private activateAttribute(clientID: number, attributeID: number): void {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = 'red';
}
const { clientID } = activeElement;
if (clientID === null) {
return;
this.activeElement = {
...this.activeElement,
attributeID,
};
}
}
private activateShape(clientID: number): void {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);
@ -1082,7 +1107,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
return;
}
this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];
let text = this.svgTexts[clientID];
@ -1189,6 +1213,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
});
this.activeElement = {
...this.activeElement,
clientID,
};
this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
@ -1198,6 +1227,30 @@ export class CanvasViewImpl implements CanvasView, Listener {
}));
}
private activate(activeElement: ActiveElement): void {
// Check if another element have been already activated
if (this.activeElement.clientID !== null) {
if (this.activeElement.clientID !== activeElement.clientID) {
// Deactivate previous shape and attribute
this.deactivate();
} else if (this.activeElement.attributeID !== activeElement.attributeID) {
this.deactivateAttribute();
}
}
const { clientID, attributeID } = activeElement;
if (clientID !== null && this.activeElement.clientID !== clientID) {
this.activateShape(clientID);
}
if (clientID !== null
&& attributeID !== null
&& this.activeElement.attributeID !== attributeID
) {
this.activateAttribute(clientID, attributeID);
}
}
// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox();

@ -18,12 +18,20 @@ import {
Task,
FrameSpeed,
Rotation,
Workspace,
} from 'reducers/interfaces';
import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';
interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}
const cvat = getCore();
let store: null | Store<CombinedState> = null;
@ -34,19 +42,37 @@ function getStore(): Store<CombinedState> {
return store;
}
function receiveAnnotationsParameters():
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
function receiveAnnotationsParameters(): AnnotationsParameters {
if (store === null) {
store = getCVATStore();
}
const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
const {
annotation: {
annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;
return {
filters,
frame,
jobInstance,
showAllInterpolationTracks,
};
}
@ -138,11 +164,22 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
}
export function changeWorkspace(workspace: Workspace): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_WORKSPACE,
payload: {
workspace,
},
};
}
export function addZLayer(): AnyAction {
return {
type: AnnotationActionTypes.ADD_Z_LAYER,
payload: {},
};
}
@ -155,12 +192,17 @@ export function switchZLayer(cur: number): AnyAction {
};
}
export function fetchAnnotationsAsync(sessionInstance: any):
export function fetchAnnotationsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const {
filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
@ -559,11 +601,15 @@ export function selectObjects(selectedStatesID: number[]): AnyAction {
};
}
export function activateObject(activatedStateID: number | null): AnyAction {
export function activateObject(
activatedStateID: number | null,
activatedAttributeID: number | null,
): AnyAction {
return {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
activatedAttributeID,
},
};
}
@ -908,19 +954,26 @@ export function splitTrack(enabled: boolean): AnyAction {
};
}
export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]):
export function updateAnnotationsAsync(statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
dispatch(activateObject(null, null));
}
const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states);
dispatch({
@ -933,8 +986,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
@ -1112,8 +1164,6 @@ export function changeLabelColorAsync(
}
export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
@ -1123,9 +1173,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) {
groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates));
dispatch(updateAnnotationsAsync(groupStates));
} else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, []));
dispatch(updateAnnotationsAsync([]));
}
};
}

@ -11,14 +11,17 @@ import {
Result,
} from 'antd';
import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';
interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
workspace: Workspace;
}
@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job,
fetching,
getJob,
workspace,
} = props;
if (job === null) {
if (!fetching) {
getJob();
@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);

@ -0,0 +1,92 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import Select, { SelectValue, LabeledValue } from 'antd/lib/select';
import Icon from 'antd/lib/icon';
import {
changeAnnotationsFilters as changeAnnotationsFiltersAction,
fetchAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
interface StateToProps {
annotationsFilters: string[];
annotationsFiltersHistory: string[];
}
interface DispatchToProps {
changeAnnotationsFilters(value: SelectValue): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
filters: annotationsFilters,
filtersHistory: annotationsFiltersHistory,
},
},
} = state;
return {
annotationsFilters,
annotationsFiltersHistory,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
changeAnnotationsFilters(value: SelectValue) {
if (typeof (value) === 'string') {
dispatch(changeAnnotationsFiltersAction([value]));
dispatch(fetchAnnotationsAsync());
} else if (Array.isArray(value)
&& value.every((element: string | number | LabeledValue): boolean => (
typeof (element) === 'string'
))
) {
dispatch(changeAnnotationsFiltersAction(value as string[]));
dispatch(fetchAnnotationsAsync());
}
},
};
}
function AnnotationsFiltersInput(props: StateToProps & DispatchToProps): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
changeAnnotationsFilters,
} = props;
return (
<Select
className='cvat-annotations-filters-input'
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationsFiltersInput);

@ -0,0 +1,300 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { connect } from 'react-redux';
import Layout, { SiderProps } from 'antd/lib/layout';
import { SelectValue } from 'antd/lib/select';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
} from 'actions/annotation-actions';
import { CombinedState } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import ObjectSwitcher from './object-switcher';
import AttributeSwitcher from './attribute-switcher';
import ObjectBasicsEditor from './object-basics-edtior';
import AttributeEditor from './attribute-editor';
interface StateToProps {
activatedStateID: number | null;
activatedAttributeID: number | null;
states: any[];
labels: any[];
}
interface DispatchToProps {
activateObject(clientID: number | null, attrID: number | null): void;
updateAnnotations(statesToUpdate: any[]): void;
}
interface LabelAttrMap {
[index: number]: any;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
activatedStateID,
activatedAttributeID,
states,
},
job: {
labels,
},
},
} = state;
return {
labels,
activatedStateID,
activatedAttributeID,
states,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
activateObject(clientID: number, attrID: number): void {
dispatch(activateObjectAction(clientID, attrID));
},
updateAnnotations(states): void {
dispatch(updateAnnotationsAsync(states));
},
};
}
function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
const {
labels,
states,
activatedStateID,
activatedAttributeID,
updateAnnotations,
activateObject,
} = props;
const [labelAttrMap, setLabelAttrMap] = useState(
labels.reduce((acc, label): LabelAttrMap => {
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
return acc;
}, {}),
);
const [activeObjectState] = activatedStateID === null
? [null] : states.filter((objectState: any): boolean => (
objectState.clientID === activatedStateID
));
const activeAttribute = activeObjectState
? labelAttrMap[activeObjectState.label.id]
: null;
if (activeObjectState) {
const attribute = labelAttrMap[activeObjectState.label.id];
if (attribute && attribute.id !== activatedAttributeID) {
activateObject(activatedStateID, attribute ? attribute.id : null);
}
} else if (states.length) {
const attribute = labelAttrMap[states[0].label.id];
activateObject(states[0].clientID, attribute ? attribute.id : null);
}
const nextObject = (step: number): void => {
if (states.length) {
const index = states.indexOf(activeObjectState);
let nextIndex = index + step;
if (nextIndex > states.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = states.length - 1;
}
if (nextIndex !== index) {
const attribute = labelAttrMap[states[nextIndex].label.id];
activateObject(states[nextIndex].clientID, attribute ? attribute.id : null);
}
}
};
const nextAttribute = (step: number): void => {
if (activeObjectState) {
const { label } = activeObjectState;
const { attributes } = label;
if (attributes.length) {
const index = attributes.indexOf(activeAttribute);
let nextIndex = index + step;
if (nextIndex > attributes.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = attributes.length - 1;
}
if (index !== nextIndex) {
const updatedLabelAttrMap = { ...labelAttrMap };
updatedLabelAttrMap[label.id] = attributes[nextIndex];
setLabelAttrMap(updatedLabelAttrMap);
}
}
}
};
useEffect(() => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
}, []);
const siderProps: SiderProps = {
className: 'attribute-annotation-sidebar',
theme: 'light',
width: 300,
collapsedWidth: 0,
reverseArrow: true,
collapsible: true,
trigger: null,
};
const keyMap = {
NEXT_ATTRIBUTE: {
name: 'Next attribute',
description: 'Go to the next attribute',
sequence: 'ArrowDown',
action: 'keydown',
},
PREVIOUS_ATTRIBUTE: {
name: 'Previous attribute',
description: 'Go to the previous attribute',
sequence: 'ArrowUp',
action: 'keydown',
},
NEXT_OBJECT: {
name: 'Next object',
description: 'Go to the next object',
sequence: 'Tab',
action: 'keydown',
},
PREVIOUS_OBJECT: {
name: 'Previous object',
description: 'Go to the previous object',
sequence: 'Shift+Tab',
action: 'keydown',
},
};
const handlers = {
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(1);
},
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextAttribute(-1);
},
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(1);
},
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
nextObject(-1);
},
};
if (activeObjectState) {
return (
<Layout.Sider {...siderProps}>
<GlobalHotKeys keyMap={keyMap as any as KeyMap} handlers={handlers} allowChanges />
<Row>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={states.length}
currentIndex={states.indexOf(activeObjectState)}
nextObject={nextObject}
/>
<ObjectBasicsEditor
currentLabel={activeObjectState.label.name}
labels={labels}
occluded={activeObjectState.occluded}
changeLabel={(value: SelectValue): void => {
const labelName = value as string;
const [newLabel] = labels
.filter((_label): boolean => _label.name === labelName);
activeObjectState.label = newLabel;
updateAnnotations([activeObjectState]);
}}
setOccluded={(event: CheckboxChangeEvent): void => {
activeObjectState.occluded = event.target.checked;
updateAnnotations([activeObjectState]);
}}
/>
{
activeAttribute
? (
<>
<AttributeSwitcher
currentAttribute={activeAttribute.name}
currentIndex={activeObjectState.label.attributes
.indexOf(activeAttribute)}
attributesCount={activeObjectState.label.attributes.length}
nextAttribute={nextAttribute}
/>
<AttributeEditor
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
}}
/>
</>
) : (
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No attributes found</Text>
</div>
)
}
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>
</Layout.Sider>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AttributeAnnotationSidebar);

@ -0,0 +1,281 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import Text from 'antd/lib/typography/Text';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import Select, { SelectValue } from 'antd/lib/select';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import Input from 'antd/lib/input';
import InputNumber from 'antd/lib/input-number';
interface InputElementParameters {
attrID: number;
inputType: string;
values: string[];
currentValue: string;
onChange(value: string): void;
ref: React.RefObject<Input | InputNumber>;
}
function renderInputElement(parameters: InputElementParameters): JSX.Element {
const {
inputType,
attrID,
values,
currentValue,
onChange,
ref,
} = parameters;
const renderCheckbox = (): JSX.Element => (
<>
<Text strong>Checkbox: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Checkbox
onChange={(event: CheckboxChangeEvent): void => (
onChange(event.target.checked ? 'true' : 'false')
)}
checked={currentValue === 'true'}
/>
</div>
</>
);
const renderSelect = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Select
value={currentValue}
style={{ width: '80%' }}
onChange={(value: SelectValue) => (
onChange(value as string)
)}
>
{values.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>{value}</Select.Option>
))}
</Select>
</div>
</>
);
const renderRadio = (): JSX.Element => (
<>
<Text strong>Values: </Text>
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Radio.Group
value={currentValue}
onChange={(event: RadioChangeEvent) => (
onChange(event.target.value)
)}
>
{values.map((value: string): JSX.Element => (
<Radio style={{ display: 'block' }} key={value} value={value}>{value}</Radio>
))}
</Radio.Group>
</div>
</>
);
const handleKeydown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
if (['ArrowDown', 'ArrowUp', 'ArrowLeft',
'ArrowRight', 'Tab', 'Shift', 'Control']
.includes(event.key)
) {
event.preventDefault();
const copyEvent = new KeyboardEvent('keydown', event);
window.document.dispatchEvent(copyEvent);
}
};
const renderText = (): JSX.Element => (
<>
{inputType === 'number' ? <Text strong>Number: </Text> : <Text strong>Text: </Text>}
<div className='attribute-annotation-sidebar-attr-elem-wrapper'>
<Input
autoFocus
key={attrID}
defaultValue={currentValue}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
if (inputType === 'number') {
if (value !== '') {
const numberValue = +value;
if (!Number.isNaN(numberValue)) {
onChange(`${numberValue}`);
}
}
} else {
onChange(value);
}
}}
onKeyDown={handleKeydown}
ref={ref as React.RefObject<Input>}
/>
</div>
</>
);
let element = null;
if (inputType === 'checkbox') {
element = renderCheckbox();
} else if (inputType === 'select') {
element = renderSelect();
} else if (inputType === 'radio') {
element = renderRadio();
} else {
element = renderText();
}
return (
<div className='attribute-annotation-sidebar-attr-editor'>
{element}
</div>
);
}
interface ListParameters {
inputType: string;
values: string[];
onChange(value: string): void;
}
function renderList(parameters: ListParameters): JSX.Element | null {
const { inputType, values, onChange } = parameters;
if (inputType === 'checkbox') {
const sortedValues = ['true', 'false'];
if (values[0].toLowerCase() !== 'true') {
sortedValues.reverse();
}
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
sortedValues.forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
<div>
<Text strong>0:</Text>
<Text>{` ${sortedValues[0]}`}</Text>
</div>
<div>
<Text strong>1:</Text>
<Text>{` ${sortedValues[1]}`}</Text>
</div>
</div>
);
}
if (inputType === 'radio' || inputType === 'select') {
const keyMap: KeyMap = {};
const handlers: {
[key: string]: (keyEvent?: KeyboardEvent) => void;
} = {};
values.slice(0, 10).forEach((value: string, index: number): void => {
const key = `SET_${index}_VALUE`;
keyMap[key] = {
name: `Set value "${value}"`,
description: `Change current value for the attribute to "${value}"`,
sequence: `${index}`,
action: 'keydown',
};
handlers[key] = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault();
}
onChange(value);
};
});
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<GlobalHotKeys keyMap={keyMap as KeyMap} handlers={handlers} allowChanges />
{values.map((value: string, index: number): JSX.Element => (
<div key={value}>
<Text strong>{`${index}:`}</Text>
<Text>{` ${value}`}</Text>
</div>
))}
</div>
);
}
if (inputType === 'number') {
return (
<div className='attribute-annotation-sidebar-attr-list-wrapper'>
<div>
<Text strong>From:</Text>
<Text>{` ${values[0]}`}</Text>
</div>
<div>
<Text strong>To:</Text>
<Text>{` ${values[1]}`}</Text>
</div>
<div>
<Text strong>Step:</Text>
<Text>{` ${values[2]}`}</Text>
</div>
</div>
);
}
return null;
}
interface Props {
attribute: any;
currentValue: string;
onChange(value: string): void;
}
function AttributeEditor(props: Props): JSX.Element {
const { attribute, currentValue, onChange } = props;
const { inputType, values, id: attrID } = attribute;
const ref = inputType === 'number' ? React.createRef<InputNumber>()
: React.createRef<Input>();
return (
<div>
{renderList({ values, inputType, onChange })}
<hr />
{renderInputElement({
attrID,
ref,
inputType,
currentValue,
values,
onChange,
})}
</div>
);
}
export default React.memo(AttributeEditor);

@ -0,0 +1,43 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
interface Props {
currentAttribute: string;
currentIndex: number;
attributesCount: number;
nextAttribute(step: number): void;
}
function AttributeSwitcher(props: Props): JSX.Element {
const {
currentAttribute,
currentIndex,
attributesCount,
nextAttribute,
} = props;
const title = `${currentAttribute} [${currentIndex + 1}/${attributesCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentAttribute}</Text>
<Text strong>{` [${currentIndex + 1}/${attributesCount}]`}</Text>
</Tooltip>
<Button disabled={attributesCount <= 1} onClick={() => nextAttribute(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(AttributeSwitcher);

@ -0,0 +1,43 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Select, { SelectValue } from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
interface Props {
currentLabel: string;
labels: any[];
occluded: boolean;
setOccluded(event: CheckboxChangeEvent): void;
changeLabel(value: SelectValue): void;
}
function ObjectBasicsEditor(props: Props): JSX.Element {
const {
currentLabel,
occluded,
labels,
setOccluded,
changeLabel,
} = props;
return (
<div className='attribute-annotation-sidebar-basics-editor'>
<Select value={currentLabel} onChange={changeLabel} style={{ width: '50%' }}>
{labels.map((label: any): JSX.Element => (
<Select.Option
value={label.name}
key={label.name}
>
{label.name}
</Select.Option>
))}
</Select>
<Checkbox checked={occluded} onChange={setOccluded}>Occluded</Checkbox>
</div>
);
}
export default React.memo(ObjectBasicsEditor);

@ -0,0 +1,48 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import Tooltip from 'antd/lib/tooltip';
import Button from 'antd/lib/button';
interface Props {
currentLabel: string;
clientID: number;
occluded: boolean;
objectsCount: number;
currentIndex: number;
nextObject(step: number): void;
}
function ObjectSwitcher(props: Props): JSX.Element {
const {
currentLabel,
clientID,
objectsCount,
currentIndex,
nextObject,
} = props;
const title = `${currentLabel} ${clientID} [${currentIndex + 1}/${objectsCount}]`;
return (
<div className='attribute-annotation-sidebar-switcher'>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(-1)}>
<Icon type='left' />
</Button>
<Tooltip title={title}>
<Text className='cvat-text'>{currentLabel}</Text>
<Text className='cvat-text'>{` ${clientID} `}</Text>
<Text strong>{`[${currentIndex + 1}/${objectsCount}]`}</Text>
</Tooltip>
<Button disabled={objectsCount <= 1} onClick={() => nextObject(1)}>
<Icon type='right' />
</Button>
</div>
);
}
export default React.memo(ObjectSwitcher);

@ -0,0 +1,19 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import Layout from 'antd/lib/layout';
import CanvasWrapperContainer from 'containers/annotation-page/standard-workspace/canvas-wrapper';
import AttributeAnnotationSidebar from './attribute-annotation-sidebar/attribute-annotation-sidebar';
export default function AttributeAnnotationWorkspace(): JSX.Element {
return (
<Layout hasSider className='attribute-annotation-workspace'>
<CanvasWrapperContainer />
<AttributeAnnotationSidebar />
</Layout>
);
}

@ -0,0 +1,66 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.attribute-annotation-workspace.ant-layout {
height: 100%;
}
.attribute-annotation-sidebar {
background: $background-color-2;
padding: 5px;
}
.attribute-annotation-sidebar-switcher {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 18px;
margin-top: 10px;
> span {
max-width: 60%;
text-overflow: ellipsis;
overflow: hidden;
}
> button > i {
color: $objects-bar-icons-color;
}
}
.attribute-annotation-sidebar-basics-editor {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 18px;
margin: 10px 0px;
}
.attribute-annotations-sidebar-not-found-wrapper {
margin-top: 20px;
text-align: center;
}
.attribute-annotation-sidebar-attr-list-wrapper {
margin: 10px 0px 10px 10px;
}
.attribute-annotation-sidebar-attr-elem-wrapper {
display: inline-block;
width: 60%;
}
.attribute-annotation-sidebar-number-list {
display: flex;
justify-content: space-around;
}
.attribute-annotation-sidebar-attr-editor {
display: flex;
align-items: center;
justify-content: space-around;
}

@ -4,18 +4,19 @@
import React from 'react';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import Slider, { SliderValue } from 'antd/lib/slider';
import Layout from 'antd/lib/layout';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import {
Layout,
Slider,
Icon,
Tooltip,
} from 'antd';
import { SliderValue } from 'antd/lib//slider';
import { ColorBy, GridColor, ObjectType } from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
import {
ColorBy,
GridColor,
ObjectType,
Workspace,
} from 'reducers/interfaces';
const cvat = getCore();
@ -26,6 +27,7 @@ interface Props {
canvasInstance: Canvas;
jobInstance: any;
activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[];
annotations: any[];
frameData: any;
@ -48,6 +50,8 @@ interface Props {
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
aamZoomMargin: number;
workspace: Workspace;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@ -57,7 +61,7 @@ interface Props {
onEditShape: (enabled: boolean) => void;
onShapeDrawn: () => void;
onResetCanvas: () => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
@ -113,6 +117,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
brightnessLevel,
contrastLevel,
saturationLevel,
workspace,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -161,11 +166,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.annotations !== annotations || prevProps.frameData !== frameData) {
this.updateCanvas();
}
if (prevProps.frame !== frameData.number && resetZoom) {
if (prevProps.frame !== frameData.number
&& resetZoom
&& workspace !== Workspace.ATTRIBUTE_ANNOTATION
) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
}, { once: true });
@ -176,10 +188,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView();
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
if (prevProps.frameAngle !== frameAngle) {
canvasInstance.rotate(frameAngle);
}
@ -188,10 +196,34 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
public componentWillUnmount(): void {
const { canvasInstance } = this.props;
canvasInstance.html().removeEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().removeEventListener('click', this.onCanvasClicked);
canvasInstance.html().removeEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().removeEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().removeEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().removeEventListener('canvas.dragstart', this.onCanvasDragStart);
canvasInstance.html().removeEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().removeEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().removeEventListener('canvas.zoomstop', this.onCanvasZoomDone);
canvasInstance.html().removeEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().removeEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().removeEventListener('canvas.find', this.onCanvasFindObject);
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().removeEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
window.removeEventListener('resize', this.fitCanvas);
}
private onShapeDrawn(event: any): void {
private onCanvasShapeDrawn = (event: any): void => {
const {
jobInstance,
activeLabelID,
@ -222,27 +254,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
}
private onShapeEdited(event: any): void {
const {
jobInstance,
frame,
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
} = event.detail;
state.points = points;
onUpdateAnnotations(jobInstance, frame, [state]);
}
};
private onObjectsMerged(event: any): void {
private onCanvasObjectsMerged = (event: any): void => {
const {
jobInstance,
frame,
@ -254,9 +268,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail;
onMergeAnnotations(jobInstance, frame, states);
}
};
private onObjectsGroupped(event: any): void {
private onCanvasObjectsGroupped = (event: any): void => {
const {
jobInstance,
frame,
@ -268,9 +282,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { states } = event.detail;
onGroupAnnotations(jobInstance, frame, states);
}
};
private onTrackSplitted(event: any): void {
private onCanvasTrackSplitted = (event: any): void => {
const {
jobInstance,
frame,
@ -282,22 +296,179 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
const { state } = event.detail;
onSplitAnnotations(jobInstance, frame, state);
}
};
private fitCanvas = (): void => {
const { canvasInstance } = this.props;
canvasInstance.fitCanvas();
};
private onCanvasMouseDown = (e: MouseEvent): void => {
const { workspace, activatedStateID, onActivateObject } = this.props;
if ((e.target as HTMLElement).tagName === 'svg') {
if (activatedStateID !== null && workspace !== Workspace.ATTRIBUTE_ANNOTATION) {
onActivateObject(null);
}
}
};
private onCanvasClicked = (): void => {
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
private onCanvasContextMenu = (e: MouseEvent): void => {
const { activatedStateID, onUpdateContextMenu } = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
};
private onCanvasShapeClicked = (e: any): void => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
};
private onCanvasShapeDeactivated = (e: any): void => {
const { onActivateObject, activatedStateID } = this.props;
const { state } = e.detail;
// when we activate element, canvas deactivates the previous
// and triggers this event
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
}
};
private onCanvasCursorMoved = async (event: any): Promise<void> => {
const {
jobInstance,
activatedStateID,
workspace,
onActivateObject,
} = this.props;
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
return;
}
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;
}
}
if (activatedStateID !== result.state.clientID) {
onActivateObject(result.state.clientID);
}
}
};
private onCanvasEditStart = (): void => {
const { onActivateObject, onEditShape } = this.props;
onActivateObject(null);
onEditShape(true);
};
private onCanvasEditDone = (event: any): void => {
const {
onEditShape,
onUpdateAnnotations,
} = this.props;
onEditShape(false);
const {
state,
points,
} = event.detail;
state.points = points;
onUpdateAnnotations([state]);
};
private onCanvasDragStart = (): void => {
const { onDragCanvas } = this.props;
onDragCanvas(true);
};
private onCanvasDragDone = (): void => {
const { onDragCanvas } = this.props;
onDragCanvas(false);
};
private onCanvasZoomStart = (): void => {
const { onZoomCanvas } = this.props;
onZoomCanvas(true);
};
private onCanvasZoomDone = (): void => {
const { onZoomCanvas } = this.props;
onZoomCanvas(false);
};
private onCanvasSetup = (): void => {
const { onSetupCanvas } = this.props;
onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
};
private onCanvasCancel = (): void => {
const { onResetCanvas } = this.props;
onResetCanvas();
};
private onCanvasFindObject = async (e: any): Promise<void> => {
const { jobInstance, canvasInstance } = this.props;
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);
}
};
private activateOnCanvas(): void {
const {
activatedStateID,
activatedAttributeID,
canvasInstance,
selectedOpacity,
aamZoomMargin,
workspace,
annotations,
} = this.props;
if (activatedStateID !== null) {
canvasInstance.activate(activatedStateID);
if (workspace === Workspace.ATTRIBUTE_ANNOTATION) {
const [activatedState] = annotations
.filter((state: any): boolean => state.clientID === activatedStateID);
if (activatedState.objectType !== ObjectType.TAG) {
canvasInstance.focus(activatedStateID, aamZoomMargin);
} else {
canvasInstance.fit();
}
}
canvasInstance.activate(activatedStateID, activatedAttributeID);
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
(el as any as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
@ -358,14 +529,6 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
gridColor,
gridOpacity,
canvasInstance,
jobInstance,
onSetupCanvas,
onDragCanvas,
onZoomCanvas,
onResetCanvas,
onActivateObject,
onUpdateContextMenu,
onEditShape,
brightnessLevel,
contrastLevel,
saturationLevel,
@ -396,127 +559,33 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
// Events
canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
if ((e.target as HTMLElement).tagName === 'svg' && activatedStateID !== null) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('click', (): void => {
if (document.activeElement) {
(document.activeElement as HTMLElement).blur();
}
});
canvasInstance.html().addEventListener('contextmenu', (e: MouseEvent): void => {
const {
activatedStateID,
} = this.props;
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
});
canvasInstance.html().addEventListener('canvas.editstart', (): void => {
onActivateObject(null);
onEditShape(true);
});
canvasInstance.html().addEventListener('canvas.setup', (): void => {
onSetupCanvas();
this.updateShapesView();
this.activateOnCanvas();
});
canvasInstance.html().addEventListener('canvas.setup', () => {
const { activatedStateID, activatedAttributeID } = this.props;
canvasInstance.fit();
canvasInstance.activate(activatedStateID, activatedAttributeID);
}, { once: true });
canvasInstance.html().addEventListener('canvas.canceled', () => {
onResetCanvas();
});
canvasInstance.html().addEventListener('canvas.dragstart', () => {
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.clicked', (e: any) => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
.getElementById(`cvat-objects-sidebar-state-item-${clientID}`);
if (sidebarItem) {
sidebarItem.scrollIntoView();
}
});
canvasInstance.html().addEventListener('canvas.deactivated', (e: any): void => {
const { activatedStateID } = this.props;
const { state } = e.detail;
// when we activate element, canvas deactivates the previous
// and triggers this event
// in this case we do not need to update our state
if (state.clientID === activatedStateID) {
onActivateObject(null);
}
});
canvasInstance.html().addEventListener('canvas.moved', async (event: any): Promise<void> => {
const { activatedStateID } = this.props;
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;
}
}
if (activatedStateID !== result.state.clientID) {
onActivateObject(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));
canvasInstance.html().addEventListener('mousedown', this.onCanvasMouseDown);
canvasInstance.html().addEventListener('click', this.onCanvasClicked);
canvasInstance.html().addEventListener('contextmenu', this.onCanvasContextMenu);
canvasInstance.html().addEventListener('canvas.editstart', this.onCanvasEditStart);
canvasInstance.html().addEventListener('canvas.edited', this.onCanvasEditDone);
canvasInstance.html().addEventListener('canvas.dragstart', this.onCanvasDragStart);
canvasInstance.html().addEventListener('canvas.dragstop', this.onCanvasDragDone);
canvasInstance.html().addEventListener('canvas.zoomstart', this.onCanvasZoomStart);
canvasInstance.html().addEventListener('canvas.zoomstop', this.onCanvasZoomDone);
canvasInstance.html().addEventListener('canvas.setup', this.onCanvasSetup);
canvasInstance.html().addEventListener('canvas.canceled', this.onCanvasCancel);
canvasInstance.html().addEventListener('canvas.find', this.onCanvasFindObject);
canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);
canvasInstance.html().addEventListener('canvas.groupped', this.onCanvasObjectsGroupped);
canvasInstance.html().addEventListener('canvas.splitted', this.onCanvasTrackSplitted);
}
public render(): JSX.Element {

@ -3,20 +3,14 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Row,
Col,
Icon,
Select,
} from 'antd';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import Select from 'antd/lib/select';
import Text from 'antd/lib/typography/Text';
import { SelectValue } from 'antd/lib/select';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import { StatesOrdering } from 'reducers/interfaces';
interface StatesOrderingSelectorComponentProps {
statesOrdering: StatesOrdering;
changeStatesOrdering(value: StatesOrdering): void;
@ -62,10 +56,7 @@ interface Props {
statesLocked: boolean;
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
@ -76,8 +67,6 @@ interface Props {
function ObjectListHeader(props: Props): JSX.Element {
const {
annotationsFilters,
annotationsFiltersHistory,
statesHidden,
statesLocked,
statesCollapsed,
@ -89,30 +78,13 @@ function ObjectListHeader(props: Props): JSX.Element {
expandAllStates,
hideAllStates,
showAllStates,
changeAnnotationsFilters,
} = props;
return (
<div className='cvat-objects-sidebar-states-header'>
<Row>
<Col>
<Select
allowClear
value={annotationsFilters}
mode='tags'
style={{ width: '100%' }}
placeholder={(
<>
<Icon type='filter' />
<span style={{ marginLeft: 5 }}>Annotations filter</span>
</>
)}
onChange={changeAnnotationsFilters}
>
{annotationsFiltersHistory.map((element: string): JSX.Element => (
<Select.Option key={element} value={element}>{element}</Select.Option>
))}
</Select>
<AnnotationsFiltersInput />
</Col>
</Row>
<Row type='flex' justify='space-between' align='middle'>

@ -4,7 +4,6 @@
import React from 'react';
import { SelectValue } from 'antd/lib/select';
import { StatesOrdering } from 'reducers/interfaces';
import ObjectItemContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-item';
import ObjectListHeader from './objects-list-header';
@ -17,10 +16,7 @@ interface Props {
statesCollapsed: boolean;
statesOrdering: StatesOrdering;
sortedStatesID: number[];
annotationsFilters: string[];
annotationsFiltersHistory: string[];
changeStatesOrdering(value: StatesOrdering): void;
changeAnnotationsFilters(value: SelectValue): void;
lockAllStates(): void;
unlockAllStates(): void;
collapseAllStates(): void;
@ -37,10 +33,7 @@ function ObjectListComponent(props: Props): JSX.Element {
statesCollapsed,
statesOrdering,
sortedStatesID,
annotationsFilters,
annotationsFiltersHistory,
changeStatesOrdering,
changeAnnotationsFilters,
lockAllStates,
unlockAllStates,
collapseAllStates,
@ -56,16 +49,13 @@ function ObjectListComponent(props: Props): JSX.Element {
statesLocked={statesLocked}
statesCollapsed={statesCollapsed}
statesOrdering={statesOrdering}
annotationsFilters={annotationsFilters}
changeStatesOrdering={changeStatesOrdering}
changeAnnotationsFilters={changeAnnotationsFilters}
lockAllStates={lockAllStates}
unlockAllStates={unlockAllStates}
collapseAllStates={collapseAllStates}
expandAllStates={expandAllStates}
hideAllStates={hideAllStates}
showAllStates={showAllStates}
annotationsFiltersHistory={annotationsFiltersHistory}
/>
<div className='cvat-objects-sidebar-states-list'>
{ sortedStatesID.map((id: number): JSX.Element => (

@ -69,16 +69,6 @@
> div:nth-child(1) > div:nth-child(1) {
height: 32px;
> .ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}
}
> div:nth-child(2) {

@ -17,7 +17,7 @@ import CanvasContextMenuContainer from 'containers/annotation-page/standard-work
export default function StandardWorkspaceComponent(): JSX.Element {
return (
<Layout hasSider>
<Layout hasSider className='cvat-standard-workspace'>
<ControlsSideBarContainer />
<CanvasWrapperContainer />
<ObjectSideBarContainer />

@ -4,6 +4,10 @@
@import 'base.scss';
.cvat-standard-workspace.ant-layout {
height: 100%
}
.cvat-canvas-container {
background-color: $background-color-1;
}
@ -115,62 +119,3 @@
margin: 0px 5px;
}
}
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}

@ -213,3 +213,75 @@
width: 15em;
}
}
// TODO: Move canvas from standard workspace and create its own .scss
.cvat-canvas-context-menu {
opacity: 0.6;
position: fixed;
width: 300px;
z-index: 10;
max-height: 50%;
overflow-y: auto;
&:hover {
opacity: 1;
}
}
.cvat-canvas-z-axis-wrapper {
position: absolute;
background: $background-color-2;
bottom: 10px;
right: 10px;
height: 150px;
z-index: 100;
border-radius: 6px;
opacity: 0.5;
border: 1px solid $border-color-3;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3px;
&:hover {
opacity: 1;
}
> .ant-slider {
height: 75%;
margin: 5px 3px;
> .ant-slider-rail {
background-color: #979797;
}
> .ant-slider-handle {
transform: none !important;
}
}
> i {
opacity: 0.7;
color: $objects-bar-icons-color;
&:hover {
opacity: 1;
}
&:active {
opacity: 0.7;
}
}
}
.cvat-annotations-filters-input.ant-select > div {
height: 32px;
> div {
height: 32px;
ul {
display: flex;
}
}
}

@ -11,17 +11,17 @@ import {
Button,
} from 'antd';
import {
InfoIcon,
FullscreenIcon,
} from '../../../icons';
import { Workspace } from 'reducers/interfaces';
import { InfoIcon, FullscreenIcon } from '../../../icons';
interface Props {
workspace: Workspace;
showStatistics(): void;
changeWorkspace(workspace: Workspace): void;
}
function RightGroup(props: Props): JSX.Element {
const { showStatistics } = props;
const { showStatistics, changeWorkspace, workspace } = props;
return (
<Col className='cvat-annotation-header-right-group'>
@ -46,9 +46,23 @@ function RightGroup(props: Props): JSX.Element {
Info
</Button>
<div>
<Select disabled className='cvat-workspace-selector' defaultValue='standard'>
<Select.Option key='standard' value='standard'>Standard</Select.Option>
<Select.Option key='aam' value='aam'>Attribute annotation</Select.Option>
<Select
className='cvat-workspace-selector'
onChange={changeWorkspace}
value={workspace}
>
<Select.Option
key={Workspace.STANDARD}
value={Workspace.STANDARD}
>
{Workspace.STANDARD}
</Select.Option>
<Select.Option
key={Workspace.ATTRIBUTE_ANNOTATION}
value={Workspace.ATTRIBUTE_ANNOTATION}
>
{Workspace.ATTRIBUTE_ANNOTATION}
</Select.Option>
</Select>
</div>
</Col>

@ -7,12 +7,12 @@ import React from 'react';
import {
Row,
Col,
Layout,
InputNumber,
} from 'antd';
import { SliderValue } from 'antd/lib/slider';
import { Workspace } from 'reducers/interfaces';
import LeftGroup from './left-group';
import RightGroup from './right-group';
import PlayerNavigation from './player-navigation';
@ -28,6 +28,8 @@ interface Props {
stopFrame: number;
undoAction?: string;
redoAction?: string;
workspace: Workspace;
changeWorkspace(workspace: Workspace): void;
showStatistics(): void;
onSwitchPlay(): void;
onSaveAnnotation(): void;
@ -55,7 +57,9 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
inputFrameRef,
startFrame,
stopFrame,
workspace,
showStatistics,
changeWorkspace,
onSwitchPlay,
onSaveAnnotation,
onPrevFrame,
@ -72,42 +76,44 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
} = props;
return (
<Layout.Header className='cvat-annotation-header'>
<Row type='flex' justify='space-between'>
<LeftGroup
saving={saving}
savingStatuses={savingStatuses}
onSaveAnnotation={onSaveAnnotation}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={onUndoClick}
onRedoClick={onRedoClick}
/>
<Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'>
<PlayerButtons
playing={playing}
onPrevFrame={onPrevFrame}
onNextFrame={onNextFrame}
onForward={onForward}
onBackward={onBackward}
onFirstFrame={onFirstFrame}
onLastFrame={onLastFrame}
onSwitchPlay={onSwitchPlay}
/>
<PlayerNavigation
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={inputFrameRef}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
/>
</Row>
</Col>
<RightGroup showStatistics={showStatistics} />
</Row>
</Layout.Header>
<Row type='flex' justify='space-between'>
<LeftGroup
saving={saving}
savingStatuses={savingStatuses}
onSaveAnnotation={onSaveAnnotation}
undoAction={undoAction}
redoAction={redoAction}
onUndoClick={onUndoClick}
onRedoClick={onRedoClick}
/>
<Col className='cvat-annotation-header-player-group'>
<Row type='flex' align='middle'>
<PlayerButtons
playing={playing}
onPrevFrame={onPrevFrame}
onNextFrame={onNextFrame}
onForward={onForward}
onBackward={onBackward}
onFirstFrame={onFirstFrame}
onLastFrame={onLastFrame}
onSwitchPlay={onSwitchPlay}
/>
<PlayerNavigation
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
inputFrameRef={inputFrameRef}
onSliderChange={onSliderChange}
onInputChange={onInputChange}
onURLIconClick={onURLIconClick}
/>
</Row>
</Col>
<RightGroup
workspace={workspace}
changeWorkspace={changeWorkspace}
showStatistics={showStatistics}
/>
</Row>
);
}

@ -93,7 +93,7 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
max={1000}
value={aamZoomMargin}
onChange={(value: number | undefined): void => {
if (value) {
if (typeof (value) === 'number') {
onChangeAAMZoomMargin(value);
}
}}

@ -9,9 +9,7 @@ import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from 'actions/annotation-actions';
import {
CombinedState,
} from 'reducers/interfaces';
import { CombinedState, Workspace } from 'reducers/interfaces';
type OwnProps = RouteComponentProps<{
tid: string;
@ -21,6 +19,7 @@ type OwnProps = RouteComponentProps<{
interface StateToProps {
job: any | null | undefined;
fetching: boolean;
workspace: Workspace;
}
interface DispatchToProps {
@ -36,12 +35,14 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
instance: job,
fetching,
},
workspace,
},
} = state;
return {
job: !job || jobID === job.id ? job : null,
fetching,
workspace,
};
}

@ -39,6 +39,7 @@ import {
GridColor,
ObjectType,
CombinedState,
Workspace,
} from 'reducers/interfaces';
import { Canvas } from 'cvat-canvas';
@ -48,6 +49,7 @@ interface StateToProps {
canvasInstance: Canvas;
jobInstance: any;
activatedStateID: number | null;
activatedAttributeID: number | null;
selectedStatesID: number[];
annotations: any[];
frameData: any;
@ -67,6 +69,8 @@ interface StateToProps {
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
aamZoomMargin: number;
workspace: Workspace;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
@ -82,7 +86,7 @@ interface DispatchToProps {
onGroupObjects: (enabled: boolean) => void;
onSplitTrack: (enabled: boolean) => void;
onEditShape: (enabled: boolean) => void;
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onUpdateAnnotations(states: any[]): void;
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onMergeAnnotations(sessionInstance: any, frame: number, states: any[]): void;
onGroupAnnotations(sessionInstance: any, frame: number, states: any[]): void;
@ -123,6 +127,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotations: {
states: annotations,
activatedStateID,
activatedAttributeID,
selectedStatesID,
zLayer: {
cur: curZLayer,
@ -131,6 +136,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
},
sidebarCollapsed,
workspace,
},
settings: {
player: {
@ -143,6 +149,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
saturationLevel,
resetZoom,
},
workspace: {
aamZoomMargin,
},
shapes: {
opacity,
colorBy,
@ -160,6 +169,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
frameAngle: frameAngles[frame - jobInstance.startFrame],
frame,
activatedStateID,
activatedAttributeID,
selectedStatesID,
annotations,
opacity,
@ -176,9 +186,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
contrastLevel,
saturationLevel,
resetZoom,
aamZoomMargin,
curZLayer,
minZLayer,
maxZLayer,
workspace,
};
}
@ -211,8 +223,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onEditShape(enabled: boolean): void {
dispatch(editShape(enabled));
},
onUpdateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frame, states));
onUpdateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
onCreateAnnotations(sessionInstance: any, frame: number, states: any[]): void {
dispatch(createAnnotationsAsync(sessionInstance, frame, states));
@ -231,7 +243,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(updateCanvasContextMenu(false, 0, 0));
}
dispatch(activateObject(activatedStateID));
dispatch(activateObject(activatedStateID, null));
},
onSelectObjects(selectedStatesID: number[]): void {
dispatch(selectObjects(selectedStatesID));

@ -29,7 +29,7 @@ interface StateToProps {
}
interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
updateAnnotations(states: any[]): void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
}
@ -68,8 +68,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
changeLabelColor(
sessionInstance: any,
@ -162,8 +162,6 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
private switchHidden(value: boolean): void {
const {
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
const { ownObjectStates } = this.state;
@ -171,14 +169,12 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.hidden = value;
}
updateAnnotations(jobInstance, frameNumber, ownObjectStates);
updateAnnotations(ownObjectStates);
}
private switchLock(value: boolean): void {
const {
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
const { ownObjectStates } = this.state;
@ -186,7 +182,7 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
state.lock = value;
}
updateAnnotations(jobInstance, frameNumber, ownObjectStates);
updateAnnotations(ownObjectStates);
}
public render(): JSX.Element {

@ -49,7 +49,7 @@ interface StateToProps {
interface DispatchToProps {
changeFrame(frame: number): void;
updateState(sessionInstance: any, frameNumber: number, objectState: any): void;
updateState(objectState: any): void;
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void;
collapseOrExpand(objectStates: any[], collapsed: boolean): void;
activateObject: (activatedStateID: number | null) => void;
@ -57,7 +57,7 @@ interface DispatchToProps {
copyShape: (objectState: any) => void;
propagateObject: (objectState: any) => void;
changeLabelColor(sessionInstance: any, frameNumber: number, label: any, color: string): void;
changeGroupColor(sessionInstance: any, frameNumber: number, group: number, color: string): void;
changeGroupColor(group: number, color: string): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -124,8 +124,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
updateState(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, [state]));
updateState(state: any): void {
dispatch(updateAnnotationsAsync([state]));
},
createAnnotations(sessionInstance: any, frameNumber: number, state: any): void {
dispatch(createAnnotationsAsync(sessionInstance, frameNumber, state));
@ -134,7 +134,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(collapseObjectItems(objectStates, collapsed));
},
activateObject(activatedStateID: number | null): void {
dispatch(activateObjectAction(activatedStateID));
dispatch(activateObjectAction(activatedStateID, null));
},
removeObject(sessionInstance: any, objectState: any): void {
dispatch(removeObjectAsync(sessionInstance, objectState, true));
@ -154,13 +154,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
): void {
dispatch(changeLabelColorAsync(sessionInstance, frameNumber, label, color));
},
changeGroupColor(
sessionInstance: any,
frameNumber: number,
group: number,
color: string,
): void {
dispatch(changeGroupColorAsync(sessionInstance, frameNumber, group, color));
changeGroupColor(group: number, color: string): void {
dispatch(changeGroupColorAsync(group, color));
},
};
}
@ -392,7 +387,7 @@ class ObjectItemContainer extends React.PureComponent<Props> {
objectState.color = color;
this.commit();
} else if (colorBy === ColorBy.GROUP) {
changeGroupColor(jobInstance, frameNumber, objectState.group.id, color);
changeGroupColor(objectState.group.id, color);
} else if (colorBy === ColorBy.LABEL) {
changeLabelColor(jobInstance, frameNumber, objectState.label, color);
}
@ -421,11 +416,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
const {
objectState,
updateState,
jobInstance,
frameNumber,
} = this.props;
updateState(jobInstance, frameNumber, objectState);
updateState(objectState);
}
public render(): JSX.Element {

@ -6,15 +6,11 @@ import React from 'react';
import { connect } from 'react-redux';
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
import { SelectValue } from 'antd/lib/select';
import ObjectsListComponent from 'components/annotation-page/standard-workspace/objects-side-bar/objects-list';
import {
updateAnnotationsAsync,
fetchAnnotationsAsync,
removeObjectAsync,
changeFrameAsync,
changeAnnotationsFilters as changeAnnotationsFiltersAction,
collapseObjectItems,
copyShape as copyShapeAction,
propagateObject as propagateObjectAction,
@ -42,8 +38,7 @@ interface StateToProps {
}
interface DispatchToProps {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void;
changeAnnotationsFilters(sessionInstance: any, filters: string[]): void;
updateAnnotations(states: any[]): void;
collapseStates(states: any[], value: boolean): void;
removeObject: (sessionInstance: any, objectState: any, force: boolean) => void;
copyShape: (objectState: any) => void;
@ -111,19 +106,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
updateAnnotations(sessionInstance: any, frameNumber: number, states: any[]): void {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, states));
updateAnnotations(states: any[]): void {
dispatch(updateAnnotationsAsync(states));
},
collapseStates(states: any[], collapsed: boolean): void {
dispatch(collapseObjectItems(states, collapsed));
},
changeAnnotationsFilters(
sessionInstance: any,
filters: string[],
): void {
dispatch(changeAnnotationsFiltersAction(filters));
dispatch(fetchAnnotationsAsync(sessionInstance));
},
removeObject(sessionInstance: any, objectState: any, force: boolean): void {
dispatch(removeObjectAsync(sessionInstance, objectState, force));
},
@ -190,15 +178,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
});
};
private onChangeAnnotationsFilters = (value: SelectValue): void => {
const {
jobInstance,
changeAnnotationsFilters,
} = this.props;
const filters = value as string[];
changeAnnotationsFilters(jobInstance, filters);
};
private onLockAllStates = (): void => {
this.lockAllStates(true);
};
@ -227,28 +206,24 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const {
objectStates,
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
for (const objectState of objectStates) {
objectState.lock = locked;
}
updateAnnotations(jobInstance, frameNumber, objectStates);
updateAnnotations(objectStates);
}
private hideAllStates(hidden: boolean): void {
const {
objectStates,
updateAnnotations,
jobInstance,
frameNumber,
} = this.props;
for (const objectState of objectStates) {
objectState.hidden = hidden;
}
updateAnnotations(jobInstance, frameNumber, objectStates);
updateAnnotations(objectStates);
}
private collapseAllStates(collapsed: boolean): void {
@ -262,12 +237,10 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element {
const {
annotationsFilters,
statesHidden,
statesLocked,
activatedStateID,
objectStates,
frameNumber,
jobInstance,
updateAnnotations,
removeObject,
@ -276,7 +249,6 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
changeFrame,
maxZLayer,
minZLayer,
annotationsFiltersHistory,
} = this.props;
const {
sortedStatesID,
@ -399,7 +371,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state) {
state.lock = !state.lock;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
SWITCH_ALL_HIDDEN: (event: KeyboardEvent | undefined) => {
@ -411,7 +383,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state) {
state.hidden = !state.hidden;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
@ -419,7 +391,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.occluded = !state.occluded;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
SWITCH_KEYFRAME: (event: KeyboardEvent | undefined) => {
@ -427,7 +399,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.keyframe = !state.keyframe;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
SWITCH_OUTSIDE: (event: KeyboardEvent | undefined) => {
@ -435,7 +407,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state && state.objectType === ObjectType.TRACK) {
state.outside = !state.outside;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
DELETE_OBJECT: (event: KeyboardEvent | undefined) => {
@ -450,7 +422,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = minZLayer - 1;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
TO_FOREGROUND: (event: KeyboardEvent | undefined) => {
@ -458,7 +430,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
const state = activatedStated();
if (state && state.objectType !== ObjectType.TAG) {
state.zOrder = maxZLayer + 1;
updateAnnotations(jobInstance, frameNumber, [state]);
updateAnnotations([state]);
}
},
COPY_SHAPE: (event: KeyboardEvent | undefined) => {
@ -506,10 +478,7 @@ class ObjectsListContainer extends React.PureComponent<Props, State> {
{...this.props}
statesOrdering={statesOrdering}
sortedStatesID={sortedStatesID}
annotationsFilters={annotationsFilters}
changeStatesOrdering={this.onChangeStatesOrdering}
changeAnnotationsFilters={this.onChangeAnnotationsFilters}
annotationsFiltersHistory={annotationsFiltersHistory}
lockAllStates={this.onLockAllStates}
unlockAllStates={this.onUnlockAllStates}
collapseAllStates={this.onCollapseAllStates}

@ -22,10 +22,12 @@ import {
undoActionAsync,
redoActionAsync,
searchAnnotationsAsync,
changeWorkspace as changeWorkspaceAction,
activateObject,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { CombinedState, FrameSpeed } from 'reducers/interfaces';
import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
interface StateToProps {
jobInstance: any;
@ -41,6 +43,7 @@ interface StateToProps {
redoAction?: string;
autoSave: boolean;
autoSaveInterval: number;
workspace: Workspace;
}
interface DispatchToProps {
@ -51,6 +54,7 @@ interface DispatchToProps {
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void;
changeWorkspace(workspace: Workspace): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -76,6 +80,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
canvas: {
ready: canvasIsReady,
},
workspace,
},
settings: {
player: {
@ -103,6 +108,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
redoAction: history.redo[history.redo.length - 1],
autoSave,
autoSaveInterval,
workspace,
};
}
@ -130,6 +136,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
searchAnnotations(sessionInstance: any, frameFrom: any, frameTo: any): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
changeWorkspace(workspace: Workspace): void {
dispatch(activateObject(null, null));
dispatch(changeWorkspaceAction(workspace));
},
};
}
@ -442,8 +452,10 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
frameNumber,
undoAction,
redoAction,
searchAnnotations,
workspace,
canvasIsReady,
searchAnnotations,
changeWorkspace,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -602,6 +614,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
changeWorkspace={changeWorkspace}
workspace={workspace}
playing={playing}
saving={saving}
savingStatuses={savingStatuses}

@ -12,6 +12,7 @@ import {
ActiveControl,
ShapeType,
ObjectType,
Workspace,
} from './interfaces';
const defaultState: AnnotationState = {
@ -54,6 +55,7 @@ const defaultState: AnnotationState = {
annotations: {
selectedStatesID: [],
activatedStateID: null,
activatedAttributeID: null,
saving: {
uploading: false,
statuses: [],
@ -88,6 +90,7 @@ const defaultState: AnnotationState = {
sidebarCollapsed: false,
appearanceCollapsed: false,
tabContentHeight: 0,
workspace: Workspace.STANDARD,
};
export default (state = defaultState, action: AnyAction): AnnotationState => {
@ -646,7 +649,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
};
}
case AnnotationActionTypes.ACTIVATE_OBJECT: {
const { activatedStateID } = action.payload;
const {
activatedStateID,
activatedAttributeID,
} = action.payload;
const {
canvas: {
activeControl,
@ -663,6 +670,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
annotations: {
...state.annotations,
activatedStateID,
activatedAttributeID,
},
};
}
@ -1041,6 +1049,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.CHANGE_WORKSPACE: {
const { workspace } = action.payload;
return {
...state,
workspace,
};
}
case AnnotationActionTypes.RESET_CANVAS: {
return {
...state,

@ -335,6 +335,7 @@ export interface AnnotationState {
annotations: {
selectedStatesID: number[];
activatedStateID: number | null;
activatedAttributeID: number | null;
collapsed: Record<number, boolean>;
states: any[];
filters: string[];
@ -367,6 +368,12 @@ export interface AnnotationState {
sidebarCollapsed: boolean;
appearanceCollapsed: boolean;
tabContentHeight: number;
workspace: Workspace;
}
export enum Workspace {
STANDARD = 'Standard',
ATTRIBUTE_ANNOTATION = 'Attribute annotation',
}
export enum GridColor {

Loading…
Cancel
Save