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 intervalmain
parent
6a7bf6d267
commit
1bb582f7f0
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue