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.
642 lines
19 KiB
TypeScript
642 lines
19 KiB
TypeScript
// Copyright (C) 2020 Intel Corporation
|
|
//
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
import React from 'react';
|
|
import copy from 'copy-to-clipboard';
|
|
import { connect } from 'react-redux';
|
|
|
|
import { withRouter } from 'react-router';
|
|
import { RouteComponentProps } from 'react-router-dom';
|
|
import { GlobalHotKeys, KeyMap } from 'react-hotkeys';
|
|
|
|
import { InputNumber } from 'antd';
|
|
import { SliderValue } from 'antd/lib/slider';
|
|
|
|
import {
|
|
changeFrameAsync,
|
|
switchPlay,
|
|
saveAnnotationsAsync,
|
|
collectStatisticsAsync,
|
|
showStatistics as showStatisticsAction,
|
|
undoActionAsync,
|
|
redoActionAsync,
|
|
searchAnnotationsAsync,
|
|
changeWorkspace as changeWorkspaceAction,
|
|
activateObject,
|
|
} from 'actions/annotation-actions';
|
|
|
|
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
|
|
import { CombinedState, FrameSpeed, Workspace } from 'reducers/interfaces';
|
|
|
|
interface StateToProps {
|
|
jobInstance: any;
|
|
frameNumber: number;
|
|
frameStep: number;
|
|
frameSpeed: FrameSpeed;
|
|
frameDelay: number;
|
|
playing: boolean;
|
|
saving: boolean;
|
|
canvasIsReady: boolean;
|
|
savingStatuses: string[];
|
|
undoAction?: string;
|
|
redoAction?: string;
|
|
autoSave: boolean;
|
|
autoSaveInterval: number;
|
|
workspace: Workspace;
|
|
}
|
|
|
|
interface DispatchToProps {
|
|
onChangeFrame(frame: number): void;
|
|
onSwitchPlay(playing: boolean): void;
|
|
onSaveAnnotation(sessionInstance: any): void;
|
|
showStatistics(sessionInstance: any): void;
|
|
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 {
|
|
const {
|
|
annotation: {
|
|
player: {
|
|
playing,
|
|
frame: {
|
|
number: frameNumber,
|
|
delay: frameDelay,
|
|
},
|
|
},
|
|
annotations: {
|
|
saving: {
|
|
uploading: saving,
|
|
statuses: savingStatuses,
|
|
},
|
|
history,
|
|
},
|
|
job: {
|
|
instance: jobInstance,
|
|
},
|
|
canvas: {
|
|
ready: canvasIsReady,
|
|
},
|
|
workspace,
|
|
},
|
|
settings: {
|
|
player: {
|
|
frameSpeed,
|
|
frameStep,
|
|
},
|
|
workspace: {
|
|
autoSave,
|
|
autoSaveInterval,
|
|
},
|
|
},
|
|
} = state;
|
|
|
|
return {
|
|
frameStep,
|
|
frameSpeed,
|
|
frameDelay,
|
|
playing,
|
|
canvasIsReady,
|
|
saving,
|
|
savingStatuses,
|
|
frameNumber,
|
|
jobInstance,
|
|
undoAction: history.undo[history.undo.length - 1],
|
|
redoAction: history.redo[history.redo.length - 1],
|
|
autoSave,
|
|
autoSaveInterval,
|
|
workspace,
|
|
};
|
|
}
|
|
|
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
|
return {
|
|
onChangeFrame(frame: number): void {
|
|
dispatch(changeFrameAsync(frame));
|
|
},
|
|
onSwitchPlay(playing: boolean): void {
|
|
dispatch(switchPlay(playing));
|
|
},
|
|
onSaveAnnotation(sessionInstance: any): void {
|
|
dispatch(saveAnnotationsAsync(sessionInstance));
|
|
},
|
|
showStatistics(sessionInstance: any): void {
|
|
dispatch(collectStatisticsAsync(sessionInstance));
|
|
dispatch(showStatisticsAction(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: any, frameTo: any): void {
|
|
dispatch(searchAnnotationsAsync(sessionInstance, frameFrom, frameTo));
|
|
},
|
|
changeWorkspace(workspace: Workspace): void {
|
|
dispatch(activateObject(null, null));
|
|
dispatch(changeWorkspaceAction(workspace));
|
|
},
|
|
};
|
|
}
|
|
|
|
type Props = StateToProps & DispatchToProps & RouteComponentProps;
|
|
class AnnotationTopBarContainer extends React.PureComponent<Props> {
|
|
private inputFrameRef: React.RefObject<InputNumber>;
|
|
private autoSaveInterval: number | undefined;
|
|
private unblock: any;
|
|
|
|
constructor(props: Props) {
|
|
super(props);
|
|
this.inputFrameRef = React.createRef<InputNumber>();
|
|
}
|
|
|
|
public componentDidMount(): void {
|
|
const {
|
|
autoSave,
|
|
autoSaveInterval,
|
|
saving,
|
|
history,
|
|
jobInstance,
|
|
} = this.props;
|
|
|
|
this.autoSaveInterval = window.setInterval((): void => {
|
|
if (autoSave && !saving) {
|
|
this.onSaveAnnotation();
|
|
}
|
|
}, autoSaveInterval);
|
|
|
|
this.unblock = history.block((location: any) => {
|
|
if (jobInstance.annotations.hasUnsavedChanges() && location.pathname !== '/settings'
|
|
&& location.pathname !== `/tasks/${jobInstance.task.id}/jobs/${jobInstance.id}`) {
|
|
return 'You have unsaved changes, please confirm leaving this page.';
|
|
}
|
|
return undefined;
|
|
});
|
|
this.beforeUnloadCallback = this.beforeUnloadCallback.bind(this);
|
|
window.addEventListener('beforeunload', this.beforeUnloadCallback);
|
|
}
|
|
|
|
public componentDidUpdate(): void {
|
|
const {
|
|
jobInstance,
|
|
frameSpeed,
|
|
frameNumber,
|
|
frameDelay,
|
|
playing,
|
|
canvasIsReady,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
|
|
if (playing && canvasIsReady) {
|
|
if (frameNumber < jobInstance.stopFrame) {
|
|
let framesSkiped = 0;
|
|
if (frameSpeed === FrameSpeed.Fast
|
|
&& (frameNumber + 1 < jobInstance.stopFrame)) {
|
|
framesSkiped = 1;
|
|
}
|
|
if (frameSpeed === FrameSpeed.Fastest
|
|
&& (frameNumber + 2 < jobInstance.stopFrame)) {
|
|
framesSkiped = 2;
|
|
}
|
|
|
|
setTimeout(() => {
|
|
const { playing: stillPlaying } = this.props;
|
|
if (stillPlaying) {
|
|
onChangeFrame(frameNumber + 1 + framesSkiped);
|
|
}
|
|
}, frameDelay);
|
|
} else {
|
|
onSwitchPlay(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
public componentWillUnmount(): void {
|
|
window.clearInterval(this.autoSaveInterval);
|
|
window.removeEventListener('beforeunload', this.beforeUnloadCallback);
|
|
this.unblock();
|
|
}
|
|
|
|
private undo = (): void => {
|
|
const {
|
|
undo,
|
|
jobInstance,
|
|
frameNumber,
|
|
} = this.props;
|
|
|
|
undo(jobInstance, frameNumber);
|
|
};
|
|
|
|
private redo = (): void => {
|
|
const {
|
|
redo,
|
|
jobInstance,
|
|
frameNumber,
|
|
} = this.props;
|
|
|
|
redo(jobInstance, frameNumber);
|
|
};
|
|
|
|
private showStatistics = (): void => {
|
|
const {
|
|
jobInstance,
|
|
showStatistics,
|
|
} = this.props;
|
|
|
|
showStatistics(jobInstance);
|
|
};
|
|
|
|
private onSwitchPlay = (): void => {
|
|
const {
|
|
frameNumber,
|
|
jobInstance,
|
|
onSwitchPlay,
|
|
playing,
|
|
} = this.props;
|
|
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
} else if (frameNumber < jobInstance.stopFrame) {
|
|
onSwitchPlay(true);
|
|
}
|
|
};
|
|
|
|
private onFirstFrame = (): void => {
|
|
const {
|
|
frameNumber,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = jobInstance.startFrame;
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onBackward = (): void => {
|
|
const {
|
|
frameNumber,
|
|
frameStep,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = Math
|
|
.max(jobInstance.startFrame, frameNumber - frameStep);
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onPrevFrame = (): void => {
|
|
const {
|
|
frameNumber,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = Math
|
|
.max(jobInstance.startFrame, frameNumber - 1);
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onNextFrame = (): void => {
|
|
const {
|
|
frameNumber,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = Math
|
|
.min(jobInstance.stopFrame, frameNumber + 1);
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onForward = (): void => {
|
|
const {
|
|
frameNumber,
|
|
frameStep,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = Math
|
|
.min(jobInstance.stopFrame, frameNumber + frameStep);
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onLastFrame = (): void => {
|
|
const {
|
|
frameNumber,
|
|
jobInstance,
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
const newFrame = jobInstance.stopFrame;
|
|
if (newFrame !== frameNumber) {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(newFrame);
|
|
}
|
|
};
|
|
|
|
private onSaveAnnotation = (): void => {
|
|
const {
|
|
onSaveAnnotation,
|
|
jobInstance,
|
|
} = this.props;
|
|
|
|
onSaveAnnotation(jobInstance);
|
|
};
|
|
|
|
private onChangePlayerSliderValue = (value: SliderValue): void => {
|
|
const {
|
|
playing,
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
} = this.props;
|
|
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(value as number);
|
|
};
|
|
|
|
private onChangePlayerInputValue = (value: number | undefined): void => {
|
|
const {
|
|
onSwitchPlay,
|
|
onChangeFrame,
|
|
playing,
|
|
} = this.props;
|
|
|
|
if (typeof (value) !== 'undefined') {
|
|
if (playing) {
|
|
onSwitchPlay(false);
|
|
}
|
|
onChangeFrame(value);
|
|
}
|
|
};
|
|
|
|
private onURLIconClick = (): void => {
|
|
const { frameNumber } = this.props;
|
|
const {
|
|
origin,
|
|
pathname,
|
|
} = window.location;
|
|
const url = `${origin}${pathname}?frame=${frameNumber}`;
|
|
copy(url);
|
|
};
|
|
|
|
private beforeUnloadCallback(event: BeforeUnloadEvent): any {
|
|
const { jobInstance } = this.props;
|
|
if (jobInstance.annotations.hasUnsavedChanges()) {
|
|
const confirmationMessage = 'You have unsaved changes, please confirm leaving this page.';
|
|
// eslint-disable-next-line no-param-reassign
|
|
event.returnValue = confirmationMessage;
|
|
return confirmationMessage;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
public render(): JSX.Element {
|
|
const {
|
|
playing,
|
|
saving,
|
|
savingStatuses,
|
|
jobInstance,
|
|
jobInstance: {
|
|
startFrame,
|
|
stopFrame,
|
|
},
|
|
frameNumber,
|
|
undoAction,
|
|
redoAction,
|
|
workspace,
|
|
canvasIsReady,
|
|
searchAnnotations,
|
|
changeWorkspace,
|
|
} = this.props;
|
|
|
|
const preventDefault = (event: KeyboardEvent | undefined): void => {
|
|
if (event) {
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
const keyMap = {
|
|
SAVE_JOB: {
|
|
name: 'Save the job',
|
|
description: 'Send all changes of annotations to the server',
|
|
sequence: 'ctrl+s',
|
|
action: 'keydown',
|
|
},
|
|
UNDO: {
|
|
name: 'Undo action',
|
|
description: 'Cancel the latest action related with objects',
|
|
sequence: 'ctrl+z',
|
|
action: 'keydown',
|
|
},
|
|
REDO: {
|
|
name: 'Redo action',
|
|
description: 'Cancel undo action',
|
|
sequences: ['ctrl+shift+z', 'ctrl+y'],
|
|
action: 'keydown',
|
|
},
|
|
NEXT_FRAME: {
|
|
name: 'Next frame',
|
|
description: 'Go to the next frame',
|
|
sequence: 'f',
|
|
action: 'keydown',
|
|
},
|
|
PREV_FRAME: {
|
|
name: 'Previous frame',
|
|
description: 'Go to the previous frame',
|
|
sequence: 'd',
|
|
action: 'keydown',
|
|
},
|
|
FORWARD_FRAME: {
|
|
name: 'Forward frame',
|
|
description: 'Go forward with a step',
|
|
sequence: 'v',
|
|
action: 'keydown',
|
|
},
|
|
BACKWARD_FRAME: {
|
|
name: 'Backward frame',
|
|
description: 'Go backward with a step',
|
|
sequence: 'c',
|
|
action: 'keydown',
|
|
},
|
|
SEARCH_FORWARD: {
|
|
name: 'Search forward',
|
|
description: 'Search the next frame that satisfies to the filters',
|
|
sequence: 'right',
|
|
action: 'keydown',
|
|
},
|
|
SEARCH_BACKWARD: {
|
|
name: 'Search backward',
|
|
description: 'Search the previous frame that satisfies to the filters',
|
|
sequence: 'left',
|
|
action: 'keydown',
|
|
},
|
|
PLAY_PAUSE: {
|
|
name: 'Play/pause',
|
|
description: 'Start/stop automatic changing frames',
|
|
sequence: 'space',
|
|
action: 'keydown',
|
|
},
|
|
FOCUS_INPUT_FRAME: {
|
|
name: 'Focus input frame',
|
|
description: 'Focus on the element to change the current frame',
|
|
sequences: ['`', '~'],
|
|
action: 'keydown',
|
|
},
|
|
};
|
|
|
|
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);
|
|
this.onSaveAnnotation();
|
|
},
|
|
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) {
|
|
searchAnnotations(jobInstance, frameNumber + 1, stopFrame);
|
|
}
|
|
},
|
|
SEARCH_BACKWARD: (event: KeyboardEvent | undefined) => {
|
|
preventDefault(event);
|
|
if (frameNumber - 1 >= startFrame && canvasIsReady) {
|
|
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={keyMap as any as KeyMap} handlers={handlers} allowChanges />
|
|
<AnnotationTopBarComponent
|
|
showStatistics={this.showStatistics}
|
|
onSwitchPlay={this.onSwitchPlay}
|
|
onSaveAnnotation={this.onSaveAnnotation}
|
|
onPrevFrame={this.onPrevFrame}
|
|
onNextFrame={this.onNextFrame}
|
|
onForward={this.onForward}
|
|
onBackward={this.onBackward}
|
|
onFirstFrame={this.onFirstFrame}
|
|
onLastFrame={this.onLastFrame}
|
|
onSliderChange={this.onChangePlayerSliderValue}
|
|
onInputChange={this.onChangePlayerInputValue}
|
|
onURLIconClick={this.onURLIconClick}
|
|
changeWorkspace={changeWorkspace}
|
|
workspace={workspace}
|
|
playing={playing}
|
|
saving={saving}
|
|
savingStatuses={savingStatuses}
|
|
startFrame={startFrame}
|
|
stopFrame={stopFrame}
|
|
frameNumber={frameNumber}
|
|
inputFrameRef={this.inputFrameRef}
|
|
undoAction={undoAction}
|
|
redoAction={redoAction}
|
|
onUndoClick={this.undo}
|
|
onRedoClick={this.redo}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withRouter(
|
|
connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps,
|
|
)(AnnotationTopBarContainer),
|
|
);
|