React-UI: settings (#1164)

* Image filters: brightness, contrast, saturation
* Auto saving
* Frame auto fit
* Player speed
* Leave confirmation for unsaved changes
main
Dmitry Kalinin 6 years ago committed by GitHub
parent 90d594d706
commit 5645cdf7c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -77,12 +77,6 @@
return result;
},
async hasUnsavedChanges() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.hasUnsavedChanges);
return result;
},
async merge(objectStates) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.annotations.merge, objectStates);
@ -107,6 +101,12 @@
.apiWrapper.call(this, prototype.annotations.exportDataset, format);
return result;
},
hasUnsavedChanges() {
const result = prototype.annotations
.hasUnsavedChanges.implementation.call(this);
return result;
},
},
writable: true,
}),
@ -381,14 +381,14 @@
* @async
*/
/**
* Indicate if there are any changes in
* Method indicates if there are any changes in
* annotations which haven't been saved on a server
* </br><b> This function cannot be wrapped with a plugin </b>
* @method hasUnsavedChanges
* @memberof Session.annotations
* @returns {boolean}
* @throws {module:API.cvat.exceptions.PluginError}
* @instance
* @async
*/
/**
* Export as a dataset.

@ -341,11 +341,11 @@ describe('Feature: save annotations', () => {
zOrder: 0,
});
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
await task.annotations.put([state]);
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
annotations = await task.annotations.get(0);
expect(annotations).toHaveLength(length + 1);
});
@ -354,23 +354,23 @@ describe('Feature: save annotations', () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].occluded = true;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 101 }))[0];
const annotations = await task.annotations.get(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.save();
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
});
test('create & save annotations for a job', async () => {
@ -387,11 +387,11 @@ describe('Feature: save annotations', () => {
zOrder: 0,
});
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
await job.annotations.put([state]);
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
annotations = await job.annotations.get(0);
expect(annotations).toHaveLength(length + 1);
});
@ -400,23 +400,23 @@ describe('Feature: save annotations', () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
annotations[0].points = [0, 100, 200, 300];
await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const annotations = await job.annotations.get(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
await annotations[0].delete();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.save();
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
});
test('delete & save annotations for a job when there are a track and a shape with the same id', async () => {
@ -658,11 +658,11 @@ describe('Feature: clear annotations', () => {
expect(annotations.length).not.toBe(0);
annotations[0].occluded = true;
await annotations[0].save();
expect(await task.annotations.hasUnsavedChanges()).toBe(true);
expect(task.annotations.hasUnsavedChanges()).toBe(true);
await task.annotations.clear(true);
annotations = await task.annotations.get(0);
expect(annotations.length).not.toBe(0);
expect(await task.annotations.hasUnsavedChanges()).toBe(false);
expect(task.annotations.hasUnsavedChanges()).toBe(false);
});
test('clear annotations with reload in a job', async () => {
@ -671,11 +671,11 @@ describe('Feature: clear annotations', () => {
expect(annotations.length).not.toBe(0);
annotations[0].occluded = true;
await annotations[0].save();
expect(await job.annotations.hasUnsavedChanges()).toBe(true);
expect(job.annotations.hasUnsavedChanges()).toBe(true);
await job.annotations.clear(true);
annotations = await job.annotations.get(0);
expect(annotations.length).not.toBe(0);
expect(await job.annotations.hasUnsavedChanges()).toBe(false);
expect(job.annotations.hasUnsavedChanges()).toBe(false);
});
test('clear annotations with bad reload parameter', async () => {

@ -12,6 +12,7 @@ import {
ShapeType,
ObjectType,
Task,
FrameSpeed,
} from 'reducers/interfaces';
import getCore from 'cvat-core';
@ -27,7 +28,8 @@ function getStore(): Store<CombinedState> {
return store;
}
function receiveAnnotationsParameters(): { filters: string[]; frame: number } {
function receiveAnnotationsParameters():
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
if (store === null) {
store = getCVATStore();
}
@ -35,10 +37,11 @@ function receiveAnnotationsParameters(): { filters: string[]; frame: number } {
const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
return {
filters,
frame,
showAllInterpolationTracks,
};
}
@ -57,6 +60,7 @@ export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
GET_JOB_FAILED = 'GET_JOB_FAILED',
CLOSE_JOB = 'CLOSE_JOB',
CHANGE_FRAME = 'CHANGE_FRAME',
CHANGE_FRAME_SUCCESS = 'CHANGE_FRAME_SUCCESS',
CHANGE_FRAME_FAILED = 'CHANGE_FRAME_FAILED',
@ -143,8 +147,9 @@ export function fetchAnnotationsAsync(sessionInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters);
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({
@ -179,12 +184,13 @@ export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({
@ -211,12 +217,13 @@ export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
dispatch({
@ -280,7 +287,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state: CombinedState = getStore().getState();
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
if (state.tasks.activities.loads[job.task.id]) {
throw Error('Annotations is being uploaded for the task');
@ -314,7 +321,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
await job.annotations.clear(true);
await job.actions.clear();
const history = await job.actions.get();
const states = await job.annotations.get(frame, false, filters);
const states = await job.annotations.get(frame, showAllInterpolationTracks, filters);
setTimeout(() => {
dispatch({
@ -422,6 +429,7 @@ export function propagateObjectAsync(
? objectState.objectType : ObjectType.SHAPE,
shapeType: objectState.shapeType,
label: objectState.label,
zOrder: objectState.zOrder,
frame: from,
};
@ -583,7 +591,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const state: CombinedState = getStore().getState();
const { instance: job } = state.annotation.job;
const { filters, frame } = receiveAnnotationsParameters();
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
try {
if (toFrame < job.startFrame || toFrame > job.stopFrame) {
@ -610,8 +618,25 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, false, filters);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime();
let frameSpeed;
switch (state.settings.player.frameSpeed) {
case (FrameSpeed.Fast): {
frameSpeed = (FrameSpeed.Fast as number) / 2;
break;
}
case (FrameSpeed.Fastest): {
frameSpeed = (FrameSpeed.Fastest as number) / 3;
break;
}
default: {
frameSpeed = state.settings.player.frameSpeed as number;
}
}
const delay = Math.max(0, Math.round(1000 / frameSpeed)
- currentTime + (state.annotation.player.frame.changeTime as number));
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
@ -620,6 +645,8 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
states,
minZ,
maxZ,
changeTime: currentTime + delay,
delay,
},
});
} catch (error) {
@ -681,8 +708,16 @@ export function getJobAsync(
try {
const state: CombinedState = getStore().getState();
const filters = initialFilters;
const { showAllInterpolationTracks } = state.settings.workspace;
// Check if already loaded job is different from asking one
if (state.annotation.job.instance && state.annotation.job.instance.id !== jid) {
dispatch({
type: AnnotationActionTypes.CLOSE_JOB,
});
}
// First check state if the task is already there
// Check state if the task is already there
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0];
@ -701,7 +736,8 @@ export function getJobAsync(
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber);
const states = await job.annotations.get(frameNumber, false, filters);
const states = await job.annotations
.get(frameNumber, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
@ -845,8 +881,9 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const { filters } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations.get(frame, false, filters);
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
payload: {
@ -862,9 +899,10 @@ export function createAnnotationsAsync(sessionInstance: any, frame: number, stat
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
await sessionInstance.annotations.put(statesToCreate);
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -889,9 +927,10 @@ export function mergeAnnotationsAsync(sessionInstance: any, frame: number, state
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
await sessionInstance.annotations.merge(statesToMerge);
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -916,9 +955,10 @@ export function groupAnnotationsAsync(sessionInstance: any, frame: number, state
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
await sessionInstance.annotations.group(statesToGroup);
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -942,10 +982,11 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function splitAnnotationsAsync(sessionInstance: any, frame: number, stateToSplit: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
try {
await sessionInstance.annotations.split(stateToSplit, frame);
const states = await sessionInstance.annotations.get(frame, false, filters);
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({
@ -974,10 +1015,11 @@ export function changeLabelColorAsync(
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters } = receiveAnnotationsParameters();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const updatedLabel = label;
updatedLabel.color = color;
const states = await sessionInstance.annotations.get(frameNumber, false, filters);
const states = await sessionInstance.annotations
.get(frameNumber, showAllInterpolationTracks, filters);
const history = await sessionInstance.actions.get();
dispatch({

@ -14,6 +14,16 @@ export enum SettingsActionTypes {
CHANGE_SELECTED_SHAPES_OPACITY = 'CHANGE_SELECTED_SHAPES_OPACITY',
CHANGE_SHAPES_COLOR_BY = 'CHANGE_SHAPES_COLOR_BY',
CHANGE_SHAPES_BLACK_BORDERS = 'CHANGE_SHAPES_BLACK_BORDERS',
CHANGE_FRAME_STEP = 'CHANGE_FRAME_STEP',
CHANGE_FRAME_SPEED = 'CHANGE_FRAME_SPEED',
SWITCH_RESET_ZOOM = 'SWITCH_RESET_ZOOM',
CHANGE_BRIGHTNESS_LEVEL = 'CHANGE_BRIGHTNESS_LEVEL',
CHANGE_CONTRAST_LEVEL = 'CHANGE_CONTRAST_LEVEL',
CHANGE_SATURATION_LEVEL = 'CHANGE_SATURATION_LEVEL',
SWITCH_AUTO_SAVE = 'SWITCH_AUTO_SAVE',
CHANGE_AUTO_SAVE_INTERVAL = 'CHANGE_AUTO_SAVE_INTERVAL',
CHANGE_AAM_ZOOM_MARGIN = 'CHANGE_AAM_ZOOM_MARGIN',
SWITCH_SHOWNIG_INTERPOLATED_TRACKS = 'SWITCH_SHOWNIG_INTERPOLATED_TRACKS',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -96,3 +106,93 @@ export function changeGridOpacity(gridOpacity: number): AnyAction {
},
};
}
export function changeFrameStep(frameStep: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_FRAME_STEP,
payload: {
frameStep,
},
};
}
export function changeFrameSpeed(frameSpeed: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_FRAME_SPEED,
payload: {
frameSpeed,
},
};
}
export function switchResetZoom(resetZoom: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_RESET_ZOOM,
payload: {
resetZoom,
},
};
}
export function changeBrightnessLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL,
payload: {
level,
},
};
}
export function changeContrastLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_CONTRAST_LEVEL,
payload: {
level,
},
};
}
export function changeSaturationLevel(level: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_SATURATION_LEVEL,
payload: {
level,
},
};
}
export function switchAutoSave(autoSave: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_AUTO_SAVE,
payload: {
autoSave,
},
};
}
export function changeAutoSaveInterval(autoSaveInterval: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_AUTO_SAVE_INTERVAL,
payload: {
autoSaveInterval,
},
};
}
export function changeAAMZoomMargin(aamZoomMargin: number): AnyAction {
return {
type: SettingsActionTypes.CHANGE_AAM_ZOOM_MARGIN,
payload: {
aamZoomMargin,
},
};
}
export function switchShowingInterpolatedTracks(showAllInterpolationTracks: boolean): AnyAction {
return {
type: SettingsActionTypes.SWITCH_SHOWNIG_INTERPOLATED_TRACKS,
payload: {
showAllInterpolationTracks,
},
};
}

@ -17,6 +17,7 @@ interface Props {
getJob(): void;
}
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job,
@ -24,6 +25,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
getJob,
} = props;
if (job === null) {
if (!fetching) {
getJob();

@ -47,6 +47,10 @@ interface Props {
curZLayer: number;
minZLayer: number;
maxZLayer: number;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
onSetupCanvas: () => void;
onDragCanvas: (enabled: boolean) => void;
onZoomCanvas: (enabled: boolean) => void;
@ -102,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
sidebarCollapsed,
activatedStateID,
curZLayer,
resetZoom,
} = this.props;
if (prevProps.sidebarCollapsed !== sidebarCollapsed) {
@ -156,6 +161,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateShapesView();
}
if (prevProps.frame !== frameData.number && resetZoom) {
canvasInstance.html().addEventListener('canvas.setup', () => {
canvasInstance.fit();
}, { once: true });
}
if (prevProps.curZLayer !== curZLayer) {
canvasInstance.setZLayer(curZLayer);
}
@ -339,6 +350,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onActivateObject,
onUpdateContextMenu,
onEditShape,
brightnessLevel,
contrastLevel,
saturationLevel,
} = this.props;
// Size
@ -357,6 +371,12 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
canvasInstance.grid(gridSize, gridSize);
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter = `brightness(${brightnessLevel / 100}) contrast(${contrastLevel / 100}) saturate(${saturationLevel / 100})`;
}
// Events
canvasInstance.html().addEventListener('mousedown', (e: MouseEvent): void => {
const {

@ -13,7 +13,7 @@ import {
notification,
} from 'antd';
import SettingsPageComponent from 'components/settings-page/settings-page';
import SettingsPageContainer from 'containers/settings-page/settings-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import TaskPageContainer from 'containers/task-page/task-page';
@ -204,7 +204,7 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Switch>
<Route exact path='/settings' component={SettingsPageComponent} />
<Route exact path='/settings' component={SettingsPageContainer} />
<Route exact path='/tasks' component={TasksPageContainer} />
<Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />

@ -61,11 +61,17 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
brightnessLevel,
contrastLevel,
saturationLevel,
onChangeFrameStep,
onChangeFrameSpeed,
onSwitchResetZoom,
onSwitchRotateAll,
onSwitchGrid,
onChangeGridSize,
onChangeGridColor,
onChangeGridOpacity,
onChangeBrightnessLevel,
onChangeContrastLevel,
onChangeSaturationLevel,
} = props;
return (
@ -73,7 +79,16 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Row type='flex' align='bottom' className='cvat-player-settings-step'>
<Col>
<Text className='cvat-text-color'> Player step </Text>
<InputNumber min={2} max={1000} value={frameStep} />
<InputNumber
min={2}
max={1000}
value={frameStep}
onChange={(value: number | undefined): void => {
if (value) {
onChangeFrameStep(value);
}
}}
/>
</Col>
<Col offset={1}>
<Text type='secondary'>
@ -87,7 +102,12 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Row type='flex' align='middle' className='cvat-player-settings-speed'>
<Col>
<Text className='cvat-text-color'> Player speed </Text>
<Select value={frameSpeed}>
<Select
value={frameSpeed}
onChange={(speed: FrameSpeed): void => {
onChangeFrameSpeed(speed);
}}
>
<Select.Option key='fastest' value={FrameSpeed.Fastest}>Fastest</Select.Option>
<Select.Option key='fast' value={FrameSpeed.Fast}>Fast</Select.Option>
<Select.Option key='usual' value={FrameSpeed.Usual}>Usual</Select.Option>
@ -118,6 +138,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
max={1000}
step={1}
value={gridSize}
disabled={!grid}
onChange={(value: number | undefined): void => {
if (value) {
onChangeGridSize(value);
@ -129,6 +150,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Text className='cvat-text-color'> Grid color </Text>
<Select
value={gridColor}
disabled={!grid}
onChange={(color: GridColor): void => {
onChangeGridColor(color);
}}
@ -146,6 +168,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
min={0}
max={100}
value={gridOpacity}
disabled={!grid}
onChange={(value: number | [number, number]): void => {
onChangeGridOpacity(value as number);
}}
@ -160,6 +183,9 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Checkbox
className='cvat-text-color'
checked={resetZoom}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchResetZoom(event.target.checked);
}}
>
Reset zoom
</Checkbox>
@ -193,7 +219,14 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
Brightness
</Col>
<Col>
<Slider min={0} max={100} value={brightnessLevel} />
<Slider
min={50}
max={200}
value={brightnessLevel}
onChange={(value: number | [number, number]): void => {
onChangeBrightnessLevel(value as number);
}}
/>
</Col>
</Row>
<Row className='cvat-player-settings-contrast'>
@ -201,7 +234,14 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
Contrast
</Col>
<Col>
<Slider min={0} max={100} value={contrastLevel} />
<Slider
min={50}
max={200}
value={contrastLevel}
onChange={(value: number | [number, number]): void => {
onChangeContrastLevel(value as number);
}}
/>
</Col>
</Row>
<Row className='cvat-player-settings-saturation'>
@ -209,7 +249,14 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
Saturation
</Col>
<Col>
<Slider min={0} max={100} value={saturationLevel} />
<Slider
min={0}
max={300}
value={saturationLevel}
onChange={(value: number | [number, number]): void => {
onChangeSaturationLevel(value as number);
}}
/>
</Col>
</Row>
</div>

@ -11,7 +11,6 @@ import {
import Text from 'antd/lib/typography/Text';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import WorkspaceSettingsContainer from 'containers/settings-page/workspace-settings';
import PlayerSettingsContainer from 'containers/settings-page/player-settings';
@ -76,4 +75,4 @@ function SettingsPage(props: RouteComponentProps): JSX.Element {
);
}
export default withRouter(SettingsPage);
export default SettingsPage;

@ -8,6 +8,7 @@ import {
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { CheckboxChangeEvent } from 'antd/lib/checkbox';
interface Props {
autoSave: boolean;
@ -26,6 +27,10 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
autoSaveInterval,
aamZoomMargin,
showAllInterpolationTracks,
onSwitchAutoSave,
onChangeAutoSaveInterval,
onChangeAAMZoomMargin,
onSwitchShowingInterpolatedTracks,
} = props;
return (
@ -35,6 +40,9 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Checkbox
className='cvat-text-color cvat-workspace-settings-auto-save'
checked={autoSave}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchAutoSave(event.target.checked);
}}
>
Enable auto save
</Checkbox>
@ -48,6 +56,11 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
max={60}
step={1}
value={Math.round(autoSaveInterval / (60 * 1000))}
onChange={(value: number | undefined): void => {
if (value) {
onChangeAutoSaveInterval(value * 60 * 1000);
}
}}
/>
<Text type='secondary'> minutes </Text>
</Col>
@ -57,6 +70,9 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Checkbox
className='cvat-text-color'
checked={showAllInterpolationTracks}
onChange={(event: CheckboxChangeEvent): void => {
onSwitchShowingInterpolatedTracks(event.target.checked);
}}
>
Show all interpolation tracks
</Checkbox>
@ -68,7 +84,16 @@ export default function WorkspaceSettingsComponent(props: Props): JSX.Element {
<Row className='cvat-workspace-settings-aam-zoom-margin'>
<Col>
<Text className='cvat-text-color'> Attribute annotation mode (AAM) zoom margin </Text>
<InputNumber min={0} max={1000} value={aamZoomMargin} />
<InputNumber
min={0}
max={1000}
value={aamZoomMargin}
onChange={(value: number | undefined): void => {
if (value) {
onChangeAAMZoomMargin(value);
}
}}
/>
</Col>
</Row>
</div>

@ -1,5 +1,8 @@
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import {
Row,
Col,
@ -27,19 +30,40 @@ interface Props {
onJobUpdate(jobInstance: any): void;
}
export default function JobListComponent(props: Props): JSX.Element {
function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
const {
taskInstance,
registeredUsers,
onJobUpdate,
history: {
push,
},
} = props;
const { jobs } = taskInstance;
const { jobs, id: taskId } = taskInstance;
const columns = [{
title: 'Job',
dataIndex: 'job',
key: 'job',
render: (id: number): JSX.Element => (<a href={`${baseURL}/?id=${id}`}>{ `Job #${id}` }</a>),
render: (id: number): JSX.Element => (
<div>
<Button type='link' href={`${baseURL}/?id=${id}`}>{`Job #${id}`}</Button>
|
<Tooltip title='Beta version of new UI written in React. It is to get
acquainted only, we do not recommend use it to annotations
process because it is lack of some features and can be unstable.'
>
<Button
type='link'
onClick={(): void => {
push(`/tasks/${taskId}/jobs/${id}`);
}}
>
Try new UI
</Button>
</Tooltip>
</div>
),
}, {
title: 'Frames',
dataIndex: 'frames',
@ -168,3 +192,5 @@ export default function JobListComponent(props: Props): JSX.Element {
</div>
);
}
export default withRouter(JobListComponent);

@ -24,7 +24,9 @@ interface DispatchToProps {
getJob(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { params } = own.match;
const jobID = +params.jid;
const {
annotation: {
job: {
@ -35,7 +37,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
} = state;
return {
job,
job: !job || jobID === job.id ? job : null,
fetching,
};
}

@ -52,6 +52,10 @@ interface StateToProps {
gridOpacity: number;
activeLabelID: number;
activeObjectType: ObjectType;
brightnessLevel: number;
contrastLevel: number;
saturationLevel: number;
resetZoom: boolean;
minZLayer: number;
maxZLayer: number;
curZLayer: number;
@ -116,6 +120,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridSize,
gridColor,
gridOpacity,
brightnessLevel,
contrastLevel,
saturationLevel,
resetZoom,
},
shapes: {
opacity,
@ -145,6 +153,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
gridOpacity,
activeLabelID,
activeObjectType,
brightnessLevel,
contrastLevel,
saturationLevel,
resetZoom,
curZLayer,
minZLayer,
maxZLayer,

@ -2,6 +2,9 @@ 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 { SliderValue } from 'antd/lib/slider';
import {
@ -15,18 +18,22 @@ import {
} from 'actions/annotation-actions';
import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-bar';
import { CombinedState } from 'reducers/interfaces';
import { CombinedState, FrameSpeed } 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;
}
interface DispatchToProps {
@ -45,6 +52,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
playing,
frame: {
number: frameNumber,
delay: frameDelay,
},
},
annotations: {
@ -63,13 +71,20 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
settings: {
player: {
frameSpeed,
frameStep,
},
workspace: {
autoSave,
autoSaveInterval,
},
},
} = state;
return {
frameStep,
frameSpeed,
frameDelay,
playing,
canvasIsReady,
saving,
@ -78,6 +93,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance,
undoAction: history.undo[history.undo.length - 1],
redoAction: history.redo[history.redo.length - 1],
autoSave,
autoSaveInterval,
};
}
@ -105,32 +122,80 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
type Props = StateToProps & DispatchToProps;
type Props = StateToProps & DispatchToProps & RouteComponentProps;
class AnnotationTopBarContainer extends React.PureComponent<Props> {
private autoSaveInterval: number | undefined;
private unblock: any;
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,
onChangeFrame,
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);
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,
@ -177,11 +242,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onFirstFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = jobInstance.startFrame;
@ -195,12 +260,12 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onBackward = (): void => {
const {
onChangeFrame,
frameNumber,
frameStep,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -215,11 +280,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onPrevFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -234,11 +299,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onNextFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -253,12 +318,12 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onForward = (): void => {
const {
onChangeFrame,
frameNumber,
frameStep,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = Math
@ -273,11 +338,11 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onLastFrame = (): void => {
const {
onChangeFrame,
frameNumber,
jobInstance,
playing,
onSwitchPlay,
onChangeFrame,
} = this.props;
const newFrame = jobInstance.stopFrame;
@ -314,8 +379,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
private onChangePlayerInputValue = (value: number | undefined): void => {
const {
onSwitchPlay,
playing,
onChangeFrame,
playing,
} = this.props;
if (typeof (value) !== 'undefined') {
@ -336,6 +401,17 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
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,
@ -379,7 +455,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props> {
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationTopBarContainer);
export default withRouter(
connect(
mapStateToProps,
mapDispatchToProps,
)(AnnotationTopBarContainer),
);

@ -4,11 +4,17 @@ import { connect } from 'react-redux';
import PlayerSettingsComponent from 'components/settings-page/player-settings';
import {
changeFrameStep,
changeFrameSpeed,
switchResetZoom,
switchRotateAll,
switchGrid,
changeGridSize,
changeGridColor,
changeGridOpacity,
changeBrightnessLevel,
changeContrastLevel,
changeSaturationLevel,
} from 'actions/settings-actions';
import {
@ -54,20 +60,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
// will be implemented
// eslint-disable-next-line
onChangeFrameStep(step: number): void {
dispatch(changeFrameStep(step));
},
// will be implemented
// eslint-disable-next-line
onChangeFrameSpeed(speed: FrameSpeed): void {
dispatch(changeFrameSpeed(speed));
},
// will be implemented
// eslint-disable-next-line
onSwitchResetZoom(enabled: boolean): void {
dispatch(switchResetZoom(enabled));
},
onSwitchRotateAll(rotateAll: boolean): void {
dispatch(switchRotateAll(rotateAll));
@ -84,20 +84,14 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
onChangeGridOpacity(gridOpacity: number): void {
dispatch(changeGridOpacity(gridOpacity));
},
// will be implemented
// eslint-disable-next-line
onChangeBrightnessLevel(level: number): void {
dispatch(changeBrightnessLevel(level));
},
// will be implemented
// eslint-disable-next-line
onChangeContrastLevel(level: number): void {
dispatch(changeContrastLevel(level));
},
// will be implemented
// eslint-disable-next-line
onChangeSaturationLevel(level: number): void {
dispatch(changeSaturationLevel(level));
},
};
}

@ -0,0 +1,79 @@
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import SettingsPageComponent from 'components/settings-page/settings-page';
import {
CombinedState,
} from 'reducers/interfaces';
interface StateToProps {
jobInstance: any;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
job: {
instance: jobInstance,
},
},
} = state;
return {
jobInstance,
};
}
type Props = StateToProps & RouteComponentProps;
class SettingsPageContainer extends React.PureComponent<Props> {
private unblock: any;
public componentDidMount(): void {
const {
history,
jobInstance,
} = this.props;
this.unblock = history.block((location: any) => {
if (jobInstance && jobInstance.annotations.hasUnsavedChanges()
&& 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 componentWillUnmount(): void {
window.removeEventListener('beforeunload', this.beforeUnloadCallback);
this.unblock();
}
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 {
return (
<SettingsPageComponent {...this.props} />
);
}
}
export default withRouter(
connect(
mapStateToProps,
)(SettingsPageContainer),
);

@ -1,6 +1,13 @@
import React from 'react';
import { connect } from 'react-redux';
import {
switchAutoSave,
changeAutoSaveInterval,
changeAAMZoomMargin,
switchShowingInterpolatedTracks,
} from 'actions/settings-actions';
import {
CombinedState,
} from 'reducers/interfaces';
@ -38,27 +45,19 @@ function mapStateToProps(state: CombinedState): StateToProps {
};
}
function mapDispatchToProps(): DispatchToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
// will be implemented
// eslint-disable-next-line
onSwitchAutoSave(enabled: boolean): void {
dispatch(switchAutoSave(enabled));
},
// will be implemented
// eslint-disable-next-line
onChangeAutoSaveInterval(interval: number): void {
dispatch(changeAutoSaveInterval(interval));
},
// will be implemented
// eslint-disable-next-line
onChangeAAMZoomMargin(margin: number): void {
dispatch(changeAAMZoomMargin(margin));
},
// will be implemented
// eslint-disable-next-line
onSwitchShowingInterpolatedTracks(enabled: boolean): void {
dispatch(switchShowingInterpolatedTracks(enabled));
},
};
}

@ -36,6 +36,8 @@ const defaultState: AnnotationState = {
number: 0,
data: null,
fetching: false,
delay: 0,
changeTime: null,
},
playing: false,
},
@ -151,6 +153,15 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
};
}
case AnnotationActionTypes.CLOSE_JOB: {
return {
...defaultState,
canvas: {
...defaultState.canvas,
instance: new Canvas(),
},
};
}
case AnnotationActionTypes.CHANGE_FRAME: {
return {
...state,
@ -174,6 +185,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
states,
minZ,
maxZ,
delay,
changeTime,
} = action.payload;
const activatedStateID = states
@ -188,6 +201,8 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
data,
number,
fetching: false,
changeTime,
delay,
},
},
annotations: {

@ -305,6 +305,8 @@ export interface AnnotationState {
number: number;
data: any | null;
fetching: boolean;
delay: number;
changeTime: number | null;
};
playing: boolean;
};

@ -30,9 +30,9 @@ const defaultState: SettingsState = {
gridSize: 100,
gridColor: GridColor.White,
gridOpacity: 0,
brightnessLevel: 50,
contrastLevel: 50,
saturationLevel: 50,
brightnessLevel: 100,
contrastLevel: 100,
saturationLevel: 100,
},
};
@ -119,6 +119,96 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.CHANGE_FRAME_STEP: {
return {
...state,
player: {
...state.player,
frameStep: action.payload.frameStep,
},
};
}
case SettingsActionTypes.CHANGE_FRAME_SPEED: {
return {
...state,
player: {
...state.player,
frameSpeed: action.payload.frameSpeed,
},
};
}
case SettingsActionTypes.SWITCH_RESET_ZOOM: {
return {
...state,
player: {
...state.player,
resetZoom: action.payload.resetZoom,
},
};
}
case SettingsActionTypes.CHANGE_BRIGHTNESS_LEVEL: {
return {
...state,
player: {
...state.player,
brightnessLevel: action.payload.level,
},
};
}
case SettingsActionTypes.CHANGE_CONTRAST_LEVEL: {
return {
...state,
player: {
...state.player,
contrastLevel: action.payload.level,
},
};
}
case SettingsActionTypes.CHANGE_SATURATION_LEVEL: {
return {
...state,
player: {
...state.player,
saturationLevel: action.payload.level,
},
};
}
case SettingsActionTypes.SWITCH_AUTO_SAVE: {
return {
...state,
workspace: {
...state.workspace,
autoSave: action.payload.autoSave,
},
};
}
case SettingsActionTypes.CHANGE_AUTO_SAVE_INTERVAL: {
return {
...state,
workspace: {
...state.workspace,
autoSaveInterval: action.payload.autoSaveInterval,
},
};
}
case SettingsActionTypes.CHANGE_AAM_ZOOM_MARGIN: {
return {
...state,
workspace: {
...state.workspace,
aamZoomMargin: action.payload.aamZoomMargin,
},
};
}
case SettingsActionTypes.SWITCH_SHOWNIG_INTERPOLATED_TRACKS: {
return {
...state,
workspace: {
...state.workspace,
showAllInterpolationTracks: action.payload.showAllInterpolationTracks,
},
};
}
default: {
return state;
}

Loading…
Cancel
Save