Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React UI: Attribute annotation mode #1255

Merged
merged 18 commits into from
Mar 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 72 additions & 19 deletions cvat-canvas/src/typescript/canvasView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,9 +264,10 @@ export class CanvasViewImpl implements CanvasView, Listener {
y + height / 2,
]);

const canvasOffset = this.canvas.getBoundingClientRect();
const [cx, cy] = [
this.canvas.clientWidth / 2 + this.canvas.offsetLeft,
this.canvas.clientHeight / 2 + this.canvas.offsetTop,
this.canvas.clientWidth / 2 + canvasOffset.left,
this.canvas.clientHeight / 2 + canvasOffset.top,
];

const dragged = {
Expand Down Expand Up @@ -725,7 +726,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
if (object) {
const bbox: SVG.BBox = object.bbox();
this.onFocusRegion(bbox.x - padding, bbox.y - padding,
bbox.width + padding, bbox.height + padding);
bbox.width + padding * 2, bbox.height + padding * 2);
}
} else if (reason === UpdateReasons.SHAPE_ACTIVATED) {
this.activate(this.controller.activeElement);
Expand Down Expand Up @@ -1014,7 +1015,26 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.content.prepend(...sorted.map((pair): SVGElement => pair[0]));
}

private deactivate(): void {
private deactivateAttribute(): void {
const { clientID, attributeID } = this.activeElement;
if (clientID !== null && attributeID !== null) {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = '';
}
}

this.activeElement = {
...this.activeElement,
attributeID: null,
};
}
}

private deactivateShape(): void {
if (this.activeElement.clientID !== null) {
const { clientID } = this.activeElement;
const drawnState = this.drawnStates[clientID];
Expand Down Expand Up @@ -1047,29 +1067,34 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.sortObjects();

this.activeElement = {
...this.activeElement,
clientID: null,
attributeID: null,
};
}
}

private activate(activeElement: ActiveElement): void {
// Check if other element have been already activated
if (this.activeElement.clientID !== null) {
// Check if it is the same element
if (this.activeElement.clientID === activeElement.clientID) {
return;
}
private deactivate(): void {
this.deactivateAttribute();
this.deactivateShape();
}

// Deactivate previous element
this.deactivate();
}
private activateAttribute(clientID: number, attributeID: number): void {
const text = this.svgTexts[clientID];
if (text) {
const [span] = text.node
.querySelectorAll(`[attrID="${attributeID}"]`) as any as SVGTSpanElement[];
if (span) {
span.style.fill = 'red';
}

const { clientID } = activeElement;
if (clientID === null) {
return;
this.activeElement = {
...this.activeElement,
attributeID,
};
}
}

private activateShape(clientID: number): void {
const [state] = this.controller.objects
.filter((_state: any): boolean => _state.clientID === clientID);

Expand All @@ -1082,7 +1107,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
return;
}

this.activeElement = { ...activeElement };
const shape = this.svgShapes[clientID];

let text = this.svgTexts[clientID];
Expand Down Expand Up @@ -1189,6 +1213,11 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
});

this.activeElement = {
...this.activeElement,
clientID,
};

this.canvas.dispatchEvent(new CustomEvent('canvas.activated', {
bubbles: false,
cancelable: true,
Expand All @@ -1198,6 +1227,30 @@ export class CanvasViewImpl implements CanvasView, Listener {
}));
}

private activate(activeElement: ActiveElement): void {
// Check if another element have been already activated
if (this.activeElement.clientID !== null) {
if (this.activeElement.clientID !== activeElement.clientID) {
// Deactivate previous shape and attribute
this.deactivate();
} else if (this.activeElement.attributeID !== activeElement.attributeID) {
this.deactivateAttribute();
}
}

const { clientID, attributeID } = activeElement;
if (clientID !== null && this.activeElement.clientID !== clientID) {
this.activateShape(clientID);
}

if (clientID !== null
&& attributeID !== null
&& this.activeElement.attributeID !== attributeID
) {
this.activateAttribute(clientID, attributeID);
}
}

// Update text position after corresponding box has been moved, resized, etc.
private updateTextPosition(text: SVG.Text, shape: SVG.Shape): void {
let box = (shape.node as any).getBBox();
Expand Down
86 changes: 68 additions & 18 deletions cvat-ui/src/actions/annotation-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,20 @@ import {
Task,
FrameSpeed,
Rotation,
Workspace,
} from 'reducers/interfaces';

import getCore from 'cvat-core';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';

interface AnnotationsParameters {
filters: string[];
frame: number;
showAllInterpolationTracks: boolean;
jobInstance: any;
}

bsekachev marked this conversation as resolved.
Show resolved Hide resolved
const cvat = getCore();
let store: null | Store<CombinedState> = null;

Expand All @@ -34,19 +42,37 @@ function getStore(): Store<CombinedState> {
return store;
}

function receiveAnnotationsParameters():
{ filters: string[]; frame: number; showAllInterpolationTracks: boolean } {
function receiveAnnotationsParameters(): AnnotationsParameters {
if (store === null) {
store = getCVATStore();
}

const state: CombinedState = getStore().getState();
const { filters } = state.annotation.annotations;
const frame = state.annotation.player.frame.number;
const { showAllInterpolationTracks } = state.settings.workspace;
const {
annotation: {
annotations: {
filters,
},
player: {
frame: {
number: frame,
},
},
job: {
instance: jobInstance,
},
},
settings: {
workspace: {
showAllInterpolationTracks,
},
},
} = state;

return {
filters,
frame,
jobInstance,
showAllInterpolationTracks,
};
}
Expand Down Expand Up @@ -138,11 +164,22 @@ export enum AnnotationActionTypes {
SWITCH_Z_LAYER = 'SWITCH_Z_LAYER',
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
}

export function changeWorkspace(workspace: Workspace): AnyAction {
return {
type: AnnotationActionTypes.CHANGE_WORKSPACE,
payload: {
workspace,
},
};
}

export function addZLayer(): AnyAction {
return {
type: AnnotationActionTypes.ADD_Z_LAYER,
payload: {},
};
}

Expand All @@ -155,12 +192,17 @@ export function switchZLayer(cur: number): AnyAction {
};
}

export function fetchAnnotationsAsync(sessionInstance: any):
export function fetchAnnotationsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const {
filters,
frame,
showAllInterpolationTracks,
jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);

Expand Down Expand Up @@ -559,11 +601,15 @@ export function selectObjects(selectedStatesID: number[]): AnyAction {
};
}

export function activateObject(activatedStateID: number | null): AnyAction {
export function activateObject(
activatedStateID: number | null,
activatedAttributeID: number | null,
): AnyAction {
return {
type: AnnotationActionTypes.ACTIVATE_OBJECT,
payload: {
activatedStateID,
activatedAttributeID,
},
};
}
Expand Down Expand Up @@ -908,19 +954,26 @@ export function splitTrack(enabled: boolean): AnyAction {
};
}

export function updateAnnotationsAsync(sessionInstance: any, frame: number, statesToUpdate: any[]):
export function updateAnnotationsAsync(statesToUpdate: any[]):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const {
jobInstance,
filters,
frame,
showAllInterpolationTracks,
} = receiveAnnotationsParameters();

try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {
// deactivate object to visualize changes immediately (UX)
dispatch(activateObject(null));
dispatch(activateObject(null, null));
}

const promises = statesToUpdate
.map((objectState: any): Promise<any> => objectState.save());
const states = await Promise.all(promises);
const history = await sessionInstance.actions.get();
const history = await jobInstance.actions.get();
const [minZ, maxZ] = computeZRange(states);

dispatch({
Expand All @@ -933,8 +986,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
},
});
} catch (error) {
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
const states = await sessionInstance.annotations
const states = await jobInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
dispatch({
type: AnnotationActionTypes.UPDATE_ANNOTATIONS_FAILED,
Expand Down Expand Up @@ -1112,8 +1164,6 @@ export function changeLabelColorAsync(
}

export function changeGroupColorAsync(
sessionInstance: any,
frameNumber: number,
group: number,
color: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
Expand All @@ -1123,9 +1173,9 @@ export function changeGroupColorAsync(
.filter((_state: any): boolean => _state.group.id === group);
if (groupStates.length) {
groupStates[0].group.color = color;
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, groupStates));
dispatch(updateAnnotationsAsync(groupStates));
} else {
dispatch(updateAnnotationsAsync(sessionInstance, frameNumber, []));
dispatch(updateAnnotationsAsync([]));
}
};
}
Expand Down
19 changes: 16 additions & 3 deletions cvat-ui/src/components/annotation-page/annotation-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@ import {
Result,
} from 'antd';

import { Workspace } from 'reducers/interfaces';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal';
import StandardWorkspaceComponent from './standard-workspace/standard-workspace';
import AttributeAnnotationWorkspace from './attribute-annotation-workspace/attribute-annotation-workspace';

interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
workspace: Workspace;
}


Expand All @@ -27,9 +30,9 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
job,
fetching,
getJob,
workspace,
} = props;


if (job === null) {
if (!fetching) {
getJob();
Expand All @@ -51,8 +54,18 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {

return (
<Layout className='cvat-annotation-page'>
<AnnotationTopBarContainer />
<StandardWorkspaceComponent />
<Layout.Header className='cvat-annotation-header'>
<AnnotationTopBarContainer />
</Layout.Header>
{ workspace === Workspace.STANDARD ? (
<Layout.Content>
<StandardWorkspaceComponent />
</Layout.Content>
) : (
<Layout.Content>
<AttributeAnnotationWorkspace />
</Layout.Content>
)}
<StatisticsModalContainer />
</Layout>
);
Expand Down
Loading