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.

790 lines
28 KiB
TypeScript

// Copyright (C) 2021-2022 Intel Corporation
// Copyright (C) 2023 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { RouteComponentProps } from 'react-router-dom';
import Input from 'antd/lib/input';
import copy from 'copy-to-clipboard';
import {
changeFrameAsync,
changeWorkspace as changeWorkspaceAction,
collectStatisticsAsync,
redoActionAsync,
saveAnnotationsAsync,
searchAnnotationsAsync,
searchEmptyFrameAsync,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
switchPredictor as switchPredictorAction,
getPredictionsAsync,
showFilters as showFiltersAction,
showStatistics as showStatisticsAction,
switchPlay,
undoActionAsync,
deleteFrameAsync,
restoreFrameAsync,
switchNavigationBlocked as switchNavigationBlockedAction,
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import {
CombinedState,
FrameSpeed,
Workspace,
PredictorState,
DimensionType,
ActiveControl,
ToolsBlockerState,
} from 'reducers';
import isAbleToChangeFrame from 'utils/is-able-to-change-frame';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { switchToolsBlockerState } from 'actions/settings-actions';
interface StateToProps {
jobInstance: any;
frameIsDeleted: boolean;
frameNumber: number;
frameFilename: string;
frameStep: number;
frameSpeed: FrameSpeed;
frameDelay: number;
frameFetching: boolean;
playing: boolean;
saving: boolean;
canvasIsReady: boolean;
undoAction?: string;
redoAction?: string;
autoSave: boolean;
autoSaveInterval: number;
toolsBlockerState: ToolsBlockerState;
showDeletedFrames: boolean;
workspace: Workspace;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
canvasInstance: Canvas | Canvas3d;
forceExit: boolean;
predictor: PredictorState;
activeControl: ActiveControl;
isTrainingActive: boolean;
}
interface DispatchToProps {
onChangeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void;
onSwitchPlay(playing: boolean): void;
onSaveAnnotation(sessionInstance: any): void;
showStatistics(sessionInstance: any): void;
showFilters(sessionInstance: any): void;
undo(sessionInstance: any, frameNumber: any): void;
redo(sessionInstance: any, frameNumber: any): void;
searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void;
searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void;
setForceExitAnnotationFlag(forceExit: boolean): void;
changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void;
deleteFrame(frame: number): void;
restoreFrame(frame: number): void;
switchNavigationBlocked(blocked: boolean): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
player: {
playing,
frame: {
data: { deleted: frameIsDeleted },
filename: frameFilename,
number: frameNumber,
delay: frameDelay,
fetching: frameFetching,
},
},
annotations: {
saving: { uploading: saving, forceExit },
history,
},
job: { instance: jobInstance },
canvas: { ready: canvasIsReady, instance: canvasInstance, activeControl },
workspace,
predictor,
},
settings: {
player: { frameSpeed, frameStep, showDeletedFrames },
workspace: {
autoSave,
autoSaveInterval,
toolsBlockerState,
},
},
shortcuts: { keyMap, normalizedKeyMap },
plugins: { list },
} = state;
return {
frameIsDeleted,
frameStep,
frameSpeed,
frameDelay,
frameFetching,
playing,
canvasIsReady,
saving,
frameNumber,
frameFilename,
jobInstance,
undoAction: history.undo.length ? history.undo[history.undo.length - 1][0] : undefined,
redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined,
autoSave,
autoSaveInterval,
toolsBlockerState,
showDeletedFrames,
workspace,
keyMap,
normalizedKeyMap,
canvasInstance,
forceExit,
predictor,
activeControl,
isTrainingActive: list.PREDICT,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onChangeFrame(frame: number, fillBuffer?: boolean, frameStep?: number): void {
dispatch(changeFrameAsync(frame, fillBuffer, frameStep));
},
onSwitchPlay(playing: boolean): void {
dispatch(switchPlay(playing));
},
onSaveAnnotation(sessionInstance: any): void {
dispatch(saveAnnotationsAsync(sessionInstance));
},
showStatistics(sessionInstance: any): void {
dispatch(collectStatisticsAsync(sessionInstance));
dispatch(showStatisticsAction(true));
},
showFilters(): void {
dispatch(showFiltersAction(true));
},
undo(sessionInstance: any, frameNumber: any): void {
dispatch(undoActionAsync(sessionInstance, frameNumber));
},
redo(sessionInstance: any, frameNumber: any): void {
dispatch(redoActionAsync(sessionInstance, frameNumber));
},
searchAnnotations(sessionInstance: any, frameFrom: number, frameTo: number): void {
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
},
searchEmptyFrame(sessionInstance: any, frameFrom: number, frameTo: number): void {
dispatch(searchEmptyFrameAsync(sessionInstance, frameFrom, frameTo));
},
changeWorkspace(workspace: Workspace): void {
dispatch(changeWorkspaceAction(workspace));
},
setForceExitAnnotationFlag(forceExit: boolean): void {
dispatch(setForceExitAnnotationFlagAction(forceExit));
},
switchPredictor(predictorEnabled: boolean): void {
dispatch(switchPredictorAction(predictorEnabled));
if (predictorEnabled) {
dispatch(getPredictionsAsync());
}
},
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void {
dispatch(switchToolsBlockerState(toolsBlockerState));
},
deleteFrame(frame: number): void {
dispatch(deleteFrameAsync(frame));
},
restoreFrame(frame: number): void {
dispatch(restoreFrameAsync(frame));
},
switchNavigationBlocked(blocked: boolean): void {
dispatch(switchNavigationBlockedAction(blocked));
},
};
}
interface State {
prevButtonType: 'regular' | 'filtered' | 'empty';
nextButtonType: 'regular' | 'filtered' | 'empty';
}
type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
private inputFrameRef: React.RefObject<Input>;
private autoSaveInterval: number | undefined;
private unblock: any;
constructor(props: Props) {
super(props);
this.inputFrameRef = React.createRef<Input>();
this.state = {
prevButtonType: 'regular',
nextButtonType: 'regular',
};
}
public componentDidMount(): void {
const {
autoSaveInterval, history, jobInstance, setForceExitAnnotationFlag,
} = this.props;
this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const self = this;
this.unblock = history.block((location: any) => {
const { forceExit } = self.props;
const { id: jobID, taskId: taskID } = jobInstance;
if (
jobInstance.annotations.hasUnsavedChanges() &&
location.pathname !== `/tasks/${taskID}/jobs/${jobID}` &&
!forceExit
) {
return 'You have unsaved changes, please confirm leaving this page.';
}
if (forceExit) {
setForceExitAnnotationFlag(false);
}
return undefined;
});
window.addEventListener('beforeunload', this.beforeUnloadCallback);
}
public componentDidUpdate(prevProps: Props): void {
const { autoSaveInterval } = this.props;
if (autoSaveInterval !== prevProps.autoSaveInterval) {
if (this.autoSaveInterval) window.clearInterval(this.autoSaveInterval);
this.autoSaveInterval = window.setInterval(this.autoSave.bind(this), autoSaveInterval);
}
this.play();
}
public componentWillUnmount(): void {
window.clearInterval(this.autoSaveInterval);
window.removeEventListener('beforeunload', this.beforeUnloadCallback);
this.unblock();
}
private undo = (): void => {
const { undo, jobInstance, frameNumber } = this.props;
if (isAbleToChangeFrame()) {
undo(jobInstance, frameNumber);
}
};
private redo = (): void => {
const { redo, jobInstance, frameNumber } = this.props;
if (isAbleToChangeFrame()) {
redo(jobInstance, frameNumber);
}
};
private showStatistics = (): void => {
const { jobInstance, showStatistics } = this.props;
showStatistics(jobInstance);
};
private showFilters = (): void => {
const { jobInstance, showFilters } = this.props;
showFilters(jobInstance);
};
private onSwitchPlay = (): void => {
const {
frameNumber, jobInstance, onSwitchPlay, playing,
} = this.props;
if (playing) {
onSwitchPlay(false);
} else if (frameNumber < jobInstance.stopFrame) {
onSwitchPlay(true);
}
};
private onFirstFrame = async (): Promise<void> => {
const {
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const newFrame = showDeletedFrames ? jobInstance.startFrame :
await jobInstance.frames.search({ notDeleted: true }, jobInstance.startFrame, frameNumber);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
}
};
private onBackward = async (): Promise<void> => {
const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
let newFrame = Math.max(jobInstance.startFrame, frameNumber - frameStep);
if (!showDeletedFrames) {
newFrame = await jobInstance.frames.search(
{ notDeleted: true, offset: frameStep }, frameNumber - 1, jobInstance.startFrame,
);
}
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
}
};
private onPrevFrame = async (): Promise<void> => {
const { prevButtonType } = this.state;
const {
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const { startFrame } = jobInstance;
const frameFrom = Math.max(jobInstance.startFrame, frameNumber - 1);
const newFrame = showDeletedFrames ? frameFrom :
await jobInstance.frames.search({ notDeleted: true }, frameFrom, jobInstance.startFrame);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
if (prevButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (prevButtonType === 'filtered') {
this.searchAnnotations(newFrame, startFrame);
} else {
this.searchEmptyFrame(newFrame, startFrame);
}
}
};
private onNextFrame = async (): Promise<void> => {
const { nextButtonType } = this.state;
const {
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const { stopFrame } = jobInstance;
const frameFrom = Math.min(jobInstance.stopFrame, frameNumber + 1);
const newFrame = showDeletedFrames ? frameFrom :
await jobInstance.frames.search({ notDeleted: true }, frameFrom, jobInstance.stopFrame);
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
if (nextButtonType === 'regular') {
this.changeFrame(newFrame);
} else if (nextButtonType === 'filtered') {
this.searchAnnotations(newFrame, stopFrame);
} else {
this.searchEmptyFrame(newFrame, stopFrame);
}
}
};
private onForward = async (): Promise<void> => {
const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
let newFrame = Math.min(jobInstance.stopFrame, frameNumber + frameStep);
if (!showDeletedFrames) {
newFrame = await jobInstance.frames.search(
{ notDeleted: true, offset: frameStep }, frameNumber + 1, jobInstance.stopFrame,
);
}
if (newFrame !== frameNumber && newFrame !== null) {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
}
};
private onLastFrame = async (): Promise<void> => {
const {
frameNumber, jobInstance, playing, onSwitchPlay, showDeletedFrames,
} = this.props;
const newFrame = showDeletedFrames ? jobInstance.stopFrame :
await jobInstance.frames.search({ notDeleted: true }, jobInstance.stopFrame, frameNumber);
if (newFrame !== frameNumber && frameNumber !== null) {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(newFrame);
}
};
private onSetPreviousButtonType = (type: 'regular' | 'filtered' | 'empty'): void => {
this.setState({
prevButtonType: type,
});
};
private onSetNextButtonType = (type: 'regular' | 'filtered' | 'empty'): void => {
this.setState({
nextButtonType: type,
});
};
private onSaveAnnotation = (): void => {
const { onSaveAnnotation, jobInstance } = this.props;
onSaveAnnotation(jobInstance);
};
private onChangePlayerSliderValue = (value: number): void => {
const { playing, onSwitchPlay } = this.props;
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(value);
};
private onChangePlayerInputValue = (value: number): void => {
const { frameNumber, onSwitchPlay, playing } = this.props;
if (value !== frameNumber) {
if (playing) {
onSwitchPlay(false);
}
this.changeFrame(value);
}
};
private onFinishDraw = (): void => {
const { activeControl, canvasInstance } = this.props;
if (
[ActiveControl.AI_TOOLS, ActiveControl.OPENCV_TOOLS].includes(activeControl) &&
canvasInstance instanceof Canvas
) {
canvasInstance.interact({ enabled: false });
return;
}
canvasInstance.draw({ enabled: false });
};
private onSwitchToolsBlockerState = (): void => {
const {
toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, activeControl,
} = this.props;
if (canvasInstance instanceof Canvas) {
if (activeControl.includes(ActiveControl.OPENCV_TOOLS)) {
canvasInstance.interact({
enabled: true,
crosshair: toolsBlockerState.algorithmsLocked,
enableThreshold: toolsBlockerState.algorithmsLocked,
});
}
}
onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked });
};
private onURLIconClick = (): void => {
const { frameNumber } = this.props;
const { origin, pathname } = window.location;
const url = `${origin}${pathname}?frame=${frameNumber}`;
copy(url);
};
private onDeleteFrame = (): void => {
const { deleteFrame, frameNumber } = this.props;
deleteFrame(frameNumber);
};
private onRestoreFrame = (): void => {
const { restoreFrame, frameNumber } = this.props;
restoreFrame(frameNumber);
};
private beforeUnloadCallback = (event: BeforeUnloadEvent): string | undefined => {
const { jobInstance, forceExit, setForceExitAnnotationFlag } = this.props;
if (jobInstance.annotations.hasUnsavedChanges() && !forceExit) {
const confirmationMessage = 'You have unsaved changes, please confirm leaving this page.';
// eslint-disable-next-line no-param-reassign
event.returnValue = confirmationMessage;
return confirmationMessage;
}
if (forceExit) {
setForceExitAnnotationFlag(false);
}
return undefined;
};
private play(): void {
const {
jobInstance,
frameSpeed,
frameNumber,
frameDelay,
frameFetching,
playing,
canvasIsReady,
onSwitchPlay,
onChangeFrame,
} = this.props;
if (playing && canvasIsReady && !frameFetching) {
if (frameNumber < jobInstance.stopFrame) {
let framesSkipped = 0;
if (frameSpeed === FrameSpeed.Fast && frameNumber + 1 < jobInstance.stopFrame) {
framesSkipped = 1;
}
if (frameSpeed === FrameSpeed.Fastest && frameNumber + 2 < jobInstance.stopFrame) {
framesSkipped = 2;
}
setTimeout(() => {
const { playing: stillPlaying } = this.props;
if (stillPlaying) {
if (isAbleToChangeFrame()) {
onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1);
} else if (jobInstance.dimension === DimensionType.DIM_2D) {
onSwitchPlay(false);
} else {
setTimeout(() => this.play(), frameDelay);
}
}
}, frameDelay);
} else {
onSwitchPlay(false);
}
}
}
private autoSave(): void {
const { autoSave, saving } = this.props;
if (autoSave && !saving) {
this.onSaveAnnotation();
}
}
private changeFrame(frame: number): void {
const { onChangeFrame } = this.props;
if (isAbleToChangeFrame()) {
onChangeFrame(frame);
}
}
private searchAnnotations(start: number, stop: number): void {
const { jobInstance, searchAnnotations } = this.props;
if (isAbleToChangeFrame()) {
searchAnnotations(jobInstance, start, stop);
}
}
private searchEmptyFrame(start: number, stop: number): void {
const { jobInstance, searchEmptyFrame } = this.props;
if (isAbleToChangeFrame()) {
searchEmptyFrame(jobInstance, start, stop);
}
}
public render(): JSX.Element {
const { nextButtonType, prevButtonType } = this.state;
const {
playing,
saving,
jobInstance,
jobInstance: { startFrame, stopFrame },
frameNumber,
frameFilename,
frameIsDeleted,
undoAction,
redoAction,
workspace,
canvasIsReady,
keyMap,
normalizedKeyMap,
predictor,
isTrainingActive,
activeControl,
searchAnnotations,
changeWorkspace,
switchPredictor,
switchNavigationBlocked,
toolsBlockerState,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
if (event) {
event.preventDefault();
}
};
const subKeyMap = {
SAVE_JOB: keyMap.SAVE_JOB,
UNDO: keyMap.UNDO,
REDO: keyMap.REDO,
DELETE_FRAME: keyMap.DELETE_FRAME,
NEXT_FRAME: keyMap.NEXT_FRAME,
PREV_FRAME: keyMap.PREV_FRAME,
FORWARD_FRAME: keyMap.FORWARD_FRAME,
BACKWARD_FRAME: keyMap.BACKWARD_FRAME,
SEARCH_FORWARD: keyMap.SEARCH_FORWARD,
SEARCH_BACKWARD: keyMap.SEARCH_BACKWARD,
PLAY_PAUSE: keyMap.PLAY_PAUSE,
FOCUS_INPUT_FRAME: keyMap.FOCUS_INPUT_FRAME,
};
const handlers = {
UNDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (undoAction) {
this.undo();
}
},
REDO: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (redoAction) {
this.redo();
}
},
SAVE_JOB: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (!saving) {
this.onSaveAnnotation();
}
},
DELETE_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onDeleteFrame();
}
},
NEXT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onNextFrame();
}
},
PREV_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onPrevFrame();
}
},
FORWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onForward();
}
},
BACKWARD_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (canvasIsReady) {
this.onBackward();
}
},
SEARCH_FORWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber + 1 <= stopFrame && canvasIsReady && isAbleToChangeFrame()) {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
}
},
SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (frameNumber - 1 >= startFrame && canvasIsReady && isAbleToChangeFrame()) {
searchAnnotations(jobInstance, frameNumber - 1, startFrame);
}
},
PLAY_PAUSE: (event: KeyboardEvent | undefined) => {
preventDefault(event);
this.onSwitchPlay();
},
FOCUS_INPUT_FRAME: (event: KeyboardEvent | undefined) => {
preventDefault(event);
if (this.inputFrameRef.current) {
this.inputFrameRef.current.focus();
}
},
};
return (
<>
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers} />
<AnnotationTopBarComponent
showStatistics={this.showStatistics}
showFilters={this.showFilters}
onSwitchPlay={this.onSwitchPlay}
onSaveAnnotation={this.onSaveAnnotation}
onPrevFrame={this.onPrevFrame}
onNextFrame={this.onNextFrame}
onForward={this.onForward}
onBackward={this.onBackward}
onFirstFrame={this.onFirstFrame}
onLastFrame={this.onLastFrame}
setNextButtonType={this.onSetNextButtonType}
setPrevButtonType={this.onSetPreviousButtonType}
onSliderChange={this.onChangePlayerSliderValue}
onInputChange={this.onChangePlayerInputValue}
onURLIconClick={this.onURLIconClick}
onDeleteFrame={this.onDeleteFrame}
onRestoreFrame={this.onRestoreFrame}
changeWorkspace={changeWorkspace}
switchPredictor={switchPredictor}
switchNavigationBlocked={switchNavigationBlocked}
predictor={predictor}
workspace={workspace}
playing={playing}
saving={saving}
startFrame={startFrame}
stopFrame={stopFrame}
frameNumber={frameNumber}
frameFilename={frameFilename}
frameDeleted={frameIsDeleted}
inputFrameRef={this.inputFrameRef}
undoAction={undoAction}
redoAction={redoAction}
saveShortcut={normalizedKeyMap.SAVE_JOB}
undoShortcut={normalizedKeyMap.UNDO}
redoShortcut={normalizedKeyMap.REDO}
drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE}
// this shortcut is handled in interactionHandler.ts separatelly
switchToolsBlockerShortcut={normalizedKeyMap.SWITCH_TOOLS_BLOCKER_STATE}
playPauseShortcut={normalizedKeyMap.PLAY_PAUSE}
deleteFrameShortcut={normalizedKeyMap.DELETE_FRAME}
nextFrameShortcut={normalizedKeyMap.NEXT_FRAME}
previousFrameShortcut={normalizedKeyMap.PREV_FRAME}
forwardShortcut={normalizedKeyMap.FORWARD_FRAME}
backwardShortcut={normalizedKeyMap.BACKWARD_FRAME}
nextButtonType={nextButtonType}
prevButtonType={prevButtonType}
focusFrameInputShortcut={normalizedKeyMap.FOCUS_INPUT_FRAME}
onUndoClick={this.undo}
onRedoClick={this.redo}
onFinishDraw={this.onFinishDraw}
onSwitchToolsBlockerState={this.onSwitchToolsBlockerState}
toolsBlockerState={toolsBlockerState}
jobInstance={jobInstance}
isTrainingActive={isTrainingActive}
activeControl={activeControl}
/>
</>
);
}
}
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(AnnotationTopBarContainer));