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.

805 lines
26 KiB
TypeScript

// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import {
Row,
Col,
Icon,
Select,
Radio,
Input,
Collapse,
Checkbox,
InputNumber,
Dropdown,
Menu,
Button,
Modal,
Popover,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { RadioChangeEvent } from 'antd/lib/radio';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import ColorChanger from 'components/annotation-page/standard-workspace/objects-side-bar/color-changer';
import {
ObjectOutsideIcon,
FirstIcon,
LastIcon,
PreviousIcon,
NextIcon,
BackgroundIcon,
ForegroundIcon,
} from 'icons';
import {
ObjectType, ShapeType,
} from 'reducers/interfaces';
function ItemMenu(
serverID: number | undefined,
locked: boolean,
objectType: ObjectType,
copy: (() => void),
remove: (() => void),
propagate: (() => void),
createURL: (() => void),
toBackground: (() => void),
toForeground: (() => void),
): JSX.Element {
return (
<Menu key='unique' className='cvat-object-item-menu'>
<Menu.Item>
<Button disabled={serverID === undefined} type='link' icon='link' onClick={createURL}>
Create object URL
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' icon='copy' onClick={copy}>
Make a copy
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' icon='block' onClick={propagate}>
Propagate
</Button>
</Menu.Item>
{ objectType !== ObjectType.TAG && (
<>
<Menu.Item>
<Button type='link' onClick={toBackground}>
<Icon component={BackgroundIcon} />
To background
</Button>
</Menu.Item>
<Menu.Item>
<Button type='link' onClick={toForeground}>
<Icon component={ForegroundIcon} />
To foreground
</Button>
</Menu.Item>
</>
)}
<Menu.Item>
<Button
type='link'
icon='delete'
onClick={(): void => {
if (locked) {
Modal.confirm({
title: 'Object is locked',
content: 'Are you sure you want to remove it?',
onOk() {
remove();
},
});
} else {
remove();
}
}}
>
Remove
</Button>
</Menu.Item>
</Menu>
);
}
interface ItemTopComponentProps {
clientID: number;
serverID: number | undefined;
labelID: number;
labels: any[];
objectType: ObjectType;
type: string;
locked: boolean;
changeLabel(labelID: string): void;
copy(): void;
remove(): void;
propagate(): void;
createURL(): void;
toBackground(): void;
toForeground(): void;
}
function ItemTopComponent(props: ItemTopComponentProps): JSX.Element {
const {
clientID,
serverID,
labelID,
labels,
objectType,
type,
locked,
changeLabel,
copy,
remove,
propagate,
createURL,
toBackground,
toForeground,
} = props;
return (
<Row type='flex' align='middle'>
<Col span={10}>
<Text style={{ fontSize: 12 }}>{clientID}</Text>
<br />
<Text type='secondary' style={{ fontSize: 10 }}>{type}</Text>
</Col>
<Col span={12}>
<Select size='small' value={`${labelID}`} onChange={changeLabel}>
{ labels.map((label: any): JSX.Element => (
<Select.Option key={label.id} value={`${label.id}`}>
{label.name}
</Select.Option>
))}
</Select>
</Col>
<Col span={2}>
<Dropdown
placement='bottomLeft'
overlay={ItemMenu(
serverID,
locked,
objectType,
copy,
remove,
propagate,
createURL,
toBackground,
toForeground,
)}
>
<Icon type='more' />
</Dropdown>
</Col>
</Row>
);
}
const ItemTop = React.memo(ItemTopComponent);
interface ItemButtonsComponentProps {
objectType: ObjectType;
shapeType: ShapeType;
occluded: boolean;
outside: boolean | undefined;
locked: boolean;
pinned: boolean;
hidden: boolean;
keyframe: boolean | undefined;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
unsetOutside(): void;
setKeyframe(): void;
unsetKeyframe(): void;
lock(): void;
unlock(): void;
pin(): void;
unpin(): void;
hide(): void;
show(): void;
}
function ItemButtonsComponent(props: ItemButtonsComponentProps): JSX.Element {
const {
objectType,
shapeType,
occluded,
outside,
locked,
pinned,
hidden,
keyframe,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
setOccluded,
unsetOccluded,
setOutside,
unsetOutside,
setKeyframe,
unsetKeyframe,
lock,
unlock,
pin,
unpin,
hide,
show,
} = props;
if (objectType === ObjectType.TRACK) {
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col>
{ navigateFirstKeyframe
? <Icon component={FirstIcon} onClick={navigateFirstKeyframe} />
: <Icon component={FirstIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />}
</Col>
<Col>
{ navigatePrevKeyframe
? <Icon component={PreviousIcon} onClick={navigatePrevKeyframe} />
: <Icon component={PreviousIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />}
</Col>
<Col>
{ navigateNextKeyframe
? <Icon component={NextIcon} onClick={navigateNextKeyframe} />
: <Icon component={NextIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />}
</Col>
<Col>
{ navigateLastKeyframe
? <Icon component={LastIcon} onClick={navigateLastKeyframe} />
: <Icon component={LastIcon} style={{ opacity: 0.5, pointerEvents: 'none' }} />}
</Col>
</Row>
<Row type='flex' justify='space-around'>
<Col>
{ outside
? <Icon component={ObjectOutsideIcon} onClick={unsetOutside} />
: <Icon type='select' onClick={setOutside} />}
</Col>
<Col>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />}
</Col>
<Col>
{ occluded
? <Icon type='team' onClick={unsetOccluded} />
: <Icon type='user' onClick={setOccluded} />}
</Col>
<Col>
{ hidden
? <Icon type='eye-invisible' onClick={show} />
: <Icon type='eye' onClick={hide} />}
</Col>
<Col>
{ keyframe
? <Icon type='star' theme='filled' onClick={unsetKeyframe} />
: <Icon type='star' onClick={setKeyframe} />}
</Col>
{
shapeType !== ShapeType.POINTS && (
<Col>
{ pinned
? <Icon type='pushpin' theme='filled' onClick={unpin} />
: <Icon type='pushpin' onClick={pin} />}
</Col>
)
}
</Row>
</Col>
</Row>
);
}
if (objectType === ObjectType.TAG) {
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />}
</Col>
</Row>
</Col>
</Row>
);
}
return (
<Row type='flex' align='middle' justify='space-around'>
<Col span={20} style={{ textAlign: 'center' }}>
<Row type='flex' justify='space-around'>
<Col>
{ locked
? <Icon type='lock' onClick={unlock} />
: <Icon type='unlock' onClick={lock} />}
</Col>
<Col>
{ occluded
? <Icon type='team' onClick={unsetOccluded} />
: <Icon type='user' onClick={setOccluded} />}
</Col>
<Col>
{ hidden
? <Icon type='eye-invisible' onClick={show} />
: <Icon type='eye' onClick={hide} />}
</Col>
{
shapeType !== ShapeType.POINTS && (
<Col>
{ pinned
? <Icon type='pushpin' theme='filled' onClick={unpin} />
: <Icon type='pushpin' onClick={pin} />}
</Col>
)
}
</Row>
</Col>
</Row>
);
}
const ItemButtons = React.memo(ItemButtonsComponent);
interface ItemAttributeComponentProps {
attrInputType: string;
attrValues: string[];
attrValue: string;
attrName: string;
attrID: number;
changeAttribute(attrID: number, value: string): void;
}
function attrIsTheSame(
prevProps: ItemAttributeComponentProps,
nextProps: ItemAttributeComponentProps,
): boolean {
return nextProps.attrID === prevProps.attrID
&& nextProps.attrValue === prevProps.attrValue
&& nextProps.attrName === prevProps.attrName
&& nextProps.attrInputType === prevProps.attrInputType
&& nextProps.attrValues
.map((value: string, id: number): boolean => prevProps.attrValues[id] === value)
.every((value: boolean): boolean => value);
}
function ItemAttributeComponent(props: ItemAttributeComponentProps): JSX.Element {
const {
attrInputType,
attrValues,
attrValue,
attrName,
attrID,
changeAttribute,
} = props;
if (attrInputType === 'checkbox') {
return (
<Col span={24}>
<Checkbox
className='cvat-object-item-checkbox-attribute'
checked={attrValue === 'true'}
onChange={(event: CheckboxChangeEvent): void => {
const value = event.target.checked ? 'true' : 'false';
changeAttribute(attrID, value);
}}
>
<Text strong className='cvat-text'>
{attrName}
</Text>
</Checkbox>
</Col>
);
}
if (attrInputType === 'radio') {
return (
<Col span={24}>
<fieldset className='cvat-object-item-radio-attribute'>
<legend>
<Text strong className='cvat-text'>{attrName}</Text>
</legend>
<Radio.Group
size='small'
value={attrValue}
onChange={(event: RadioChangeEvent): void => {
changeAttribute(attrID, event.target.value);
}}
>
{ attrValues.map((value: string): JSX.Element => (
<Radio key={value} value={value}>{value}</Radio>
)) }
</Radio.Group>
</fieldset>
</Col>
);
}
if (attrInputType === 'select') {
return (
<>
<Col span={24}>
<Text strong className='cvat-text'>
{attrName}
</Text>
</Col>
<Col span={24}>
<Select
size='small'
onChange={(value: string): void => {
changeAttribute(attrID, value);
}}
value={attrValue}
className='cvat-object-item-select-attribute'
>
{ attrValues.map((value: string): JSX.Element => (
<Select.Option key={value} value={value}>{value}</Select.Option>
)) }
</Select>
</Col>
</>
);
}
if (attrInputType === 'number') {
const [min, max, step] = attrValues;
return (
<>
<Col span={24}>
<Text strong className='cvat-text'>
{attrName}
</Text>
</Col>
<Col span={24}>
<InputNumber
size='small'
onChange={(value: number | undefined): void => {
if (typeof (value) !== 'undefined') {
changeAttribute(attrID, `${value}`);
}
}}
value={+attrValue}
className='cvat-object-item-number-attribute'
min={+min}
max={+max}
step={+step}
/>
</Col>
</>
);
}
return (
<>
<Col span={24}>
<Text strong className='cvat-text'>
{attrName}
</Text>
</Col>
<Col span={24}>
<Input
size='small'
onChange={(event: React.ChangeEvent<HTMLInputElement>): void => {
changeAttribute(attrID, event.target.value);
}}
value={attrValue}
className='cvat-object-item-text-attribute'
/>
</Col>
</>
);
}
const ItemAttribute = React.memo(ItemAttributeComponent, attrIsTheSame);
interface ItemAttributesComponentProps {
collapsed: boolean;
attributes: any[];
values: Record<number, string>;
changeAttribute(attrID: number, value: string): void;
collapse(): void;
}
function attrValuesAreEqual(next: Record<number, string>, prev: Record<number, string>): boolean {
const prevKeys = Object.keys(prev);
const nextKeys = Object.keys(next);
return nextKeys.length === prevKeys.length
&& nextKeys.map((key: string): boolean => prev[+key] === next[+key])
.every((value: boolean) => value);
}
function attrAreTheSame(
prevProps: ItemAttributesComponentProps,
nextProps: ItemAttributesComponentProps,
): boolean {
return nextProps.collapsed === prevProps.collapsed
&& nextProps.attributes === prevProps.attributes
&& attrValuesAreEqual(nextProps.values, prevProps.values);
}
function ItemAttributesComponent(props: ItemAttributesComponentProps): JSX.Element {
const {
collapsed,
attributes,
values,
changeAttribute,
collapse,
} = props;
const sorted = [...attributes]
.sort((a: any, b: any): number => a.inputType.localeCompare(b.inputType));
return (
<Row>
<Collapse
className='cvat-objects-sidebar-state-item-collapse'
activeKey={collapsed ? [] : ['details']}
onChange={collapse}
>
<Collapse.Panel
header={<span style={{ fontSize: '11px' }}>Details</span>}
key='details'
>
{ sorted.map((attribute: any): JSX.Element => (
<Row
key={attribute.id}
type='flex'
align='middle'
justify='start'
className='cvat-object-item-attribute-wrapper'
>
<ItemAttribute
attrValue={values[attribute.id]}
attrInputType={attribute.inputType}
attrName={attribute.name}
attrID={attribute.id}
attrValues={attribute.values}
changeAttribute={changeAttribute}
/>
</Row>
))}
</Collapse.Panel>
</Collapse>
</Row>
);
}
const ItemAttributes = React.memo(ItemAttributesComponent, attrAreTheSame);
interface Props {
activated: boolean;
objectType: ObjectType;
shapeType: ShapeType;
clientID: number;
serverID: number | undefined;
labelID: number;
occluded: boolean;
outside: boolean | undefined;
locked: boolean;
pinned: boolean;
hidden: boolean;
keyframe: boolean | undefined;
attrValues: Record<number, string>;
color: string;
colors: string[];
labels: any[];
attributes: any[];
collapsed: boolean;
navigateFirstKeyframe: null | (() => void);
navigatePrevKeyframe: null | (() => void);
navigateNextKeyframe: null | (() => void);
navigateLastKeyframe: null | (() => void);
activate(): void;
copy(): void;
propagate(): void;
createURL(): void;
toBackground(): void;
toForeground(): void;
remove(): void;
setOccluded(): void;
unsetOccluded(): void;
setOutside(): void;
unsetOutside(): void;
setKeyframe(): void;
unsetKeyframe(): void;
lock(): void;
unlock(): void;
pin(): void;
unpin(): void;
hide(): void;
show(): void;
changeLabel(labelID: string): void;
changeAttribute(attrID: number, value: string): void;
changeColor(color: string): void;
collapse(): void;
}
function objectItemsAreEqual(prevProps: Props, nextProps: Props): boolean {
return nextProps.activated === prevProps.activated
&& nextProps.locked === prevProps.locked
&& nextProps.pinned === prevProps.pinned
&& nextProps.occluded === prevProps.occluded
&& nextProps.outside === prevProps.outside
&& nextProps.hidden === prevProps.hidden
&& nextProps.keyframe === prevProps.keyframe
&& nextProps.labelID === prevProps.labelID
&& nextProps.color === prevProps.color
&& nextProps.clientID === prevProps.clientID
&& nextProps.serverID === prevProps.serverID
&& nextProps.objectType === prevProps.objectType
&& nextProps.shapeType === prevProps.shapeType
&& nextProps.collapsed === prevProps.collapsed
&& nextProps.labels === prevProps.labels
&& nextProps.attributes === prevProps.attributes
&& nextProps.navigateFirstKeyframe === prevProps.navigateFirstKeyframe
&& nextProps.navigatePrevKeyframe === prevProps.navigatePrevKeyframe
&& nextProps.navigateNextKeyframe === prevProps.navigateNextKeyframe
&& nextProps.navigateLastKeyframe === prevProps.navigateLastKeyframe
&& attrValuesAreEqual(nextProps.attrValues, prevProps.attrValues);
}
function ObjectItemComponent(props: Props): JSX.Element {
const {
activated,
objectType,
shapeType,
clientID,
serverID,
occluded,
outside,
locked,
pinned,
hidden,
keyframe,
attrValues,
labelID,
color,
colors,
attributes,
labels,
collapsed,
navigateFirstKeyframe,
navigatePrevKeyframe,
navigateNextKeyframe,
navigateLastKeyframe,
activate,
copy,
propagate,
createURL,
toBackground,
toForeground,
remove,
setOccluded,
unsetOccluded,
setOutside,
unsetOutside,
setKeyframe,
unsetKeyframe,
lock,
unlock,
pin,
unpin,
hide,
show,
changeLabel,
changeAttribute,
changeColor,
collapse,
} = props;
const type = objectType === ObjectType.TAG ? ObjectType.TAG.toUpperCase()
: `${shapeType.toUpperCase()} ${objectType.toUpperCase()}`;
const className = !activated ? 'cvat-objects-sidebar-state-item'
: 'cvat-objects-sidebar-state-item cvat-objects-sidebar-state-active-item';
return (
<div style={{ display: 'flex' }}>
<Popover
placement='left'
trigger='click'
content={(
<ColorChanger
onChange={changeColor}
colors={colors}
/>
)}
>
<div
className='cvat-objects-sidebar-state-item-color'
style={{ background: ` ${color}` }}
/>
</Popover>
<div
onMouseEnter={activate}
id={`cvat-objects-sidebar-state-item-${clientID}`}
className={className}
style={{ borderColor: ` ${color}` }}
>
<ItemTop
serverID={serverID}
clientID={clientID}
labelID={labelID}
labels={labels}
objectType={objectType}
type={type}
locked={locked}
changeLabel={changeLabel}
copy={copy}
remove={remove}
propagate={propagate}
createURL={createURL}
toBackground={toBackground}
toForeground={toForeground}
/>
<ItemButtons
shapeType={shapeType}
objectType={objectType}
occluded={occluded}
outside={outside}
locked={locked}
pinned={pinned}
hidden={hidden}
keyframe={keyframe}
navigateFirstKeyframe={navigateFirstKeyframe}
navigatePrevKeyframe={navigatePrevKeyframe}
navigateNextKeyframe={navigateNextKeyframe}
navigateLastKeyframe={navigateLastKeyframe}
setOccluded={setOccluded}
unsetOccluded={unsetOccluded}
setOutside={setOutside}
unsetOutside={unsetOutside}
setKeyframe={setKeyframe}
unsetKeyframe={unsetKeyframe}
lock={lock}
unlock={unlock}
pin={pin}
unpin={unpin}
hide={hide}
show={show}
/>
{ !!attributes.length
&& (
<ItemAttributes
collapsed={collapsed}
attributes={attributes}
values={attrValues}
collapse={collapse}
changeAttribute={changeAttribute}
/>
)}
</div>
</div>
);
}
export default React.memo(ObjectItemComponent, objectItemsAreEqual);