You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

415 lines
15 KiB
TypeScript

// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { GlobalHotKeys, ExtendedKeyMapOptions } from 'react-hotkeys';
import { connect } from 'react-redux';
import Layout, { SiderProps } from 'antd/lib/layout';
import { SelectValue } from 'antd/lib/select';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Icon from 'antd/lib/icon';
import { ThunkDispatch } from 'utils/redux';
import { Canvas } from 'cvat-canvas-wrapper';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
changeFrameAsync,
} from 'actions/annotation-actions';
import { CombinedState, ObjectType } from 'reducers/interfaces';
import AnnotationsFiltersInput from 'components/annotation-page/annotations-filters-input';
import AppearanceBlock from 'components/annotation-page/appearance-block';
import ObjectButtonsContainer from 'containers/annotation-page/standard-workspace/objects-side-bar/object-buttons';
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[];
jobInstance: any;
keyMap: Record<string, ExtendedKeyMapOptions>;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas;
canvasIsReady: boolean;
curZLayer: number;
}
interface DispatchToProps {
activateObject(clientID: number | null, attrID: number | null): void;
updateAnnotations(statesToUpdate: any[]): void;
changeFrame(frame: number): void;
}
interface LabelAttrMap {
[index: number]: any;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
annotations: {
activatedStateID,
activatedAttributeID,
states,
zLayer: {
cur,
},
},
job: {
instance: jobInstance,
labels,
},
canvas: {
instance: canvasInstance,
ready: canvasIsReady,
},
},
shortcuts: {
keyMap,
normalizedKeyMap,
},
} = state;
return {
jobInstance,
labels,
activatedStateID,
activatedAttributeID,
states,
keyMap,
normalizedKeyMap,
canvasInstance,
canvasIsReady,
curZLayer: cur,
};
}
function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
return {
activateObject(clientID: number, attrID: number): void {
dispatch(activateObjectAction(clientID, attrID));
},
updateAnnotations(states): void {
dispatch(updateAnnotationsAsync(states));
},
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
};
}
function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.Element {
const {
labels,
states,
activatedStateID,
activatedAttributeID,
jobInstance,
updateAnnotations,
changeFrame,
activateObject,
keyMap,
normalizedKeyMap,
canvasInstance,
canvasIsReady,
curZLayer,
} = props;
const filteredStates = states.filter((state) => !state.outside
&& !state.hidden
&& state.zOrder <= curZLayer);
const [labelAttrMap, setLabelAttrMap] = useState(
labels.reduce((acc, label): LabelAttrMap => {
acc[label.id] = label.attributes.length ? label.attributes[0] : null;
return acc;
}, {}),
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const collapse = (): void => {
const [collapser] = window.document
.getElementsByClassName('attribute-annotation-sidebar');
if (collapser) {
collapser.addEventListener('transitionend', () => {
canvasInstance.fitCanvas();
}, { once: true });
}
setSidebarCollapsed(!sidebarCollapsed);
};
const indexes = filteredStates.map((state) => state.clientID);
const activatedIndex = indexes.indexOf(activatedStateID);
const activeObjectState = activatedStateID === null || activatedIndex === -1
? null : filteredStates[activatedIndex];
const activeAttribute = activeObjectState
? labelAttrMap[activeObjectState.label.id]
: null;
if (canvasIsReady) {
if (activeObjectState) {
const attribute = labelAttrMap[activeObjectState.label.id];
if (attribute && attribute.id !== activatedAttributeID) {
activateObject(activatedStateID, attribute ? attribute.id : null);
}
} else if (filteredStates.length) {
const attribute = labelAttrMap[filteredStates[0].label.id];
activateObject(filteredStates[0].clientID, attribute ? attribute.id : null);
}
}
const nextObject = (step: number): void => {
if (filteredStates.length) {
const index = filteredStates.indexOf(activeObjectState);
let nextIndex = index + step;
if (nextIndex > filteredStates.length - 1) {
nextIndex = 0;
} else if (nextIndex < 0) {
nextIndex = filteredStates.length - 1;
}
if (nextIndex !== index) {
const attribute = labelAttrMap[filteredStates[nextIndex].label.id];
activateObject(filteredStates[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,
collapsed: sidebarCollapsed,
};
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const subKeyMap = {
NEXT_ATTRIBUTE: keyMap.NEXT_ATTRIBUTE,
PREVIOUS_ATTRIBUTE: keyMap.PREVIOUS_ATTRIBUTE,
NEXT_OBJECT: keyMap.NEXT_OBJECT,
PREVIOUS_OBJECT: keyMap.PREVIOUS_OBJECT,
SWITCH_LOCK: keyMap.SWITCH_LOCK,
SWITCH_OCCLUDED: keyMap.SWITCH_OCCLUDED,
NEXT_KEY_FRAME: keyMap.NEXT_KEY_FRAME,
PREV_KEY_FRAME: keyMap.PREV_KEY_FRAME,
};
const handlers = {
NEXT_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
nextAttribute(1);
},
PREVIOUS_ATTRIBUTE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
nextAttribute(-1);
},
NEXT_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
nextObject(1);
},
PREVIOUS_OBJECT: (event: KeyboardEvent | undefined) => {
preventDefault(event);
nextObject(-1);
},
SWITCH_LOCK: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState) {
activeObjectState.lock = !activeObjectState.lock;
updateAnnotations([activeObjectState]);
}
},
SWITCH_OCCLUDED: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType !== ObjectType.TAG) {
activeObjectState.occluded = !activeObjectState.occluded;
updateAnnotations([activeObjectState]);
}
},
NEXT_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) {
const frame = typeof (activeObjectState.keyframes.next) === 'number'
? activeObjectState.keyframes.next : null;
if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
changeFrame(frame);
}
}
},
PREV_KEY_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (activeObjectState && activeObjectState.objectType === ObjectType.TRACK) {
const frame = typeof (activeObjectState.keyframes.prev) === 'number'
? activeObjectState.keyframes.prev : null;
if (frame !== null && canvasInstance.isAbleToChangeFrame()) {
changeFrame(frame);
}
}
},
};
if (activeObjectState) {
return (
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={collapse}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} allowChanges />
<Row className='cvat-objects-sidebar-filter-input'>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<ObjectSwitcher
currentLabel={activeObjectState.label.name}
clientID={activeObjectState.clientID}
occluded={activeObjectState.occluded}
objectsCount={filteredStates.length}
currentIndex={filteredStates.indexOf(activeObjectState)}
normalizedKeyMap={normalizedKeyMap}
nextObject={nextObject}
/>
<ObjectBasicsEditor
currentLabel={activeObjectState.label.name}
labels={labels}
changeLabel={(value: SelectValue): void => {
const labelName = value as string;
const [newLabel] = labels
.filter((_label): boolean => _label.name === labelName);
activeObjectState.label = newLabel;
updateAnnotations([activeObjectState]);
}}
/>
<ObjectButtonsContainer
clientID={activeObjectState.clientID}
outsideDisabled
hiddenDisabled
keyframeDisabled
/>
{
activeAttribute
? (
<>
<AttributeSwitcher
currentAttribute={activeAttribute.name}
currentIndex={activeObjectState.label.attributes
.indexOf(activeAttribute)}
attributesCount={activeObjectState.label.attributes.length}
normalizedKeyMap={normalizedKeyMap}
nextAttribute={nextAttribute}
/>
<AttributeEditor
clientID={activeObjectState.clientID}
attribute={activeAttribute}
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
jobInstance.logger.log(
LogType.changeAttribute, {
id: activeAttribute.id,
object_id: activeObjectState.clientID,
value,
},
);
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);
}}
/>
</>
) : (
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No attributes found</Text>
</div>
)
}
{ !sidebarCollapsed && <AppearanceBlock /> }
</Layout.Sider>
);
}
return (
<Layout.Sider {...siderProps}>
{/* eslint-disable-next-line */}
<span
className={`cvat-objects-sidebar-sider
ant-layout-sider-zero-width-trigger
ant-layout-sider-zero-width-trigger-left`}
onClick={collapse}
>
{sidebarCollapsed ? <Icon type='menu-fold' title='Show' />
: <Icon type='menu-unfold' title='Hide' />}
</span>
<Row className='cvat-objects-sidebar-filter-input'>
<Col>
<AnnotationsFiltersInput />
</Col>
</Row>
<div className='attribute-annotations-sidebar-not-found-wrapper'>
<Text strong>No objects found</Text>
</div>
</Layout.Sider>
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AttributeAnnotationSidebar);