Skip to content

Commit

Permalink
Drag'n'drop import of multiple NML files (#2908)
Browse files Browse the repository at this point in the history
* allow uploading multiple nmls via drag'n'drop into the tracing view

* allow auto-grouping of nmls during import

* merge old import-nml-button with new drag'n'drop flow; parallelize parsing

* minor styling

* update changelog

* make code a bit more readable

* improve error-handling for import of multiple NMLs; incorporate PR feedback

* rewrite error-handling for nml import to satisfy flow

* fix linting

* rename variable
  • Loading branch information
philippotto authored Jul 24, 2018
1 parent 6f6fb3a commit fe1ca84
Show file tree
Hide file tree
Showing 19 changed files with 538 additions and 63 deletions.
1 change: 1 addition & 0 deletions .flowconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<PROJECT_ROOT>/node_modules/findup/
<PROJECT_ROOT>/node_modules/documentation/
<PROJECT_ROOT>/node_modules/flow-coverage-report/
<PROJECT_ROOT>/.nyc_output

[untyped]
<PROJECT_ROOT>/node_modules/ava/
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md).
### Added

- All dates in webknossos will be shown in the browser's timezone. On hover, a tooltip will show the date in UTC. [#2916](https://github.com/scalableminds/webknossos/pull/2916) ![image](https://user-images.githubusercontent.com/2486553/42888385-74c82bc0-8aa8-11e8-9c3e-7cfc90ce93bc.png)

- Added the possibility to import multiple NML files into the active tracing. This can be done by dragging and dropping the files directly into the tracing view. [#2908](https://github.com/scalableminds/webknossos/pull/2908)
- During the import of multiple NML files, the user can select an option to automatically create a group per file so that the imported trees are organized in a hierarchy. [#2908](https://github.com/scalableminds/webknossos/pull/2908)
### Changed

-
Expand Down
14 changes: 10 additions & 4 deletions app/assets/javascripts/components/file_upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ type Props = {
onUploading?: Function,
};

export function readFileAsText(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = reject;
reader.onload = () => resolve(reader.result.toString());
reader.readAsText(file);
});
}

class FileUpload extends React.PureComponent<Props> {
fileInput: ?HTMLInputElement;

Expand All @@ -32,10 +41,7 @@ class FileUpload extends React.PureComponent<Props> {
data: { [this.props.name]: files },
}).then(successCallback, errorCallback);
} else {
const reader = new FileReader();
reader.onerror = errorCallback;
reader.onload = () => successCallback(reader.result);
reader.readAsText(files[0]);
readFileAsText(files[0]).then(successCallback, errorCallback);
}
};

Expand Down
22 changes: 22 additions & 0 deletions app/assets/javascripts/libs/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,28 @@ const Utils = {
// Big endian
return [a, b, g, r];
},

async promiseAllWithErrors<T>(
promises: Array<Promise<T>>,
): Promise<{ successes: Array<T>, errors: Array<Error> }> {
const successOrErrorObjects = await Promise.all(promises.map(p => p.catch(error => error)));
return successOrErrorObjects.reduce(
({ successes, errors }, successOrError) => {
if (successOrError instanceof Error) {
return {
successes,
errors: errors.concat([successOrError]),
};
} else {
return {
successes: successes.concat([successOrError]),
errors,
};
}
},
{ successes: [], errors: [] },
);
},
};

export default Utils;
6 changes: 6 additions & 0 deletions app/assets/javascripts/oxalis/api/api_latest.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
setActiveNodeAction,
createCommentAction,
deleteNodeAction,
deleteTreeAction,
setNodeRadiusAction,
setTreeNameAction,
} from "oxalis/model/actions/skeletontracing_actions";
Expand Down Expand Up @@ -161,6 +162,11 @@ class TracingApi {
Store.dispatch(deleteNodeAction(nodeId, treeId));
}

deleteTree(treeId: number) {
assertSkeleton(Store.getState().tracing);
Store.dispatch(deleteTreeAction(treeId));
}

/**
* Sets the comment for a node.
*
Expand Down
4 changes: 3 additions & 1 deletion app/assets/javascripts/oxalis/model/actions/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { ViewModeActionType } from "oxalis/model/actions/view_mode_actions"
import type { AnnotationActionTypes } from "oxalis/model/actions/annotation_actions";
import type { FlycamActionType } from "oxalis/model/actions/flycam_actions";
import type { UserActionType } from "oxalis/model/actions/user_actions";
import type { UiActionType } from "oxalis/model/actions/ui_actions";

export type ActionType =
| SkeletonTracingActionType
Expand All @@ -20,7 +21,8 @@ export type ActionType =
| ViewModeActionType
| AnnotationActionTypes
| FlycamActionType
| UserActionType;
| UserActionType
| UiActionType;

export const wkReadyAction = () => ({
type: "WK_READY",
Expand Down
16 changes: 16 additions & 0 deletions app/assets/javascripts/oxalis/model/actions/ui_actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// @flow
/* eslint-disable import/prefer-default-export */

type SetDropzoneModalVisibilityActionType = {
type: "SET_DROPZONE_MODAL_VISIBILITY_ACTION_TYPE",
visible: boolean,
};

export type UiActionType = SetDropzoneModalVisibilityActionType;

export const setDropzoneModalVisibilityAction = (
visible: boolean,
): SetDropzoneModalVisibilityActionType => ({
type: "SET_DROPZONE_MODAL_VISIBILITY_ACTION_TYPE",
visible,
});
42 changes: 41 additions & 1 deletion app/assets/javascripts/oxalis/model/helpers/nml_helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
} from "oxalis/store";
import type { BoundingBoxType } from "oxalis/constants";
import type { APIBuildInfoType } from "admin/api_flow_types";
import { getMaximumGroupId } from "oxalis/model/reducers/skeletontracing_reducer_helpers";

// NML Defaults
const DEFAULT_COLOR = [1, 0, 0];
Expand Down Expand Up @@ -363,8 +364,35 @@ function getEdgeHash(source: number, target: number) {
return source < target ? `${source}-${target}` : `${target}-${source}`;
}

function wrapInNewGroup(
originalTrees: TreeMapType,
originalTreeGroups: Array<TreeGroupType>,
wrappingGroupName: string,
): [TreeMapType, Array<TreeGroupType>] {
// It does not matter whether the group id is used in the active tracing, since
// this case will be handled during import, anyway. The group id just shouldn't clash
// with the nml itself.
const unusedGroupId = getMaximumGroupId(originalTreeGroups) + 1;
const trees = _.mapValues(originalTrees, tree => ({
...tree,
// Give parentless trees the new treeGroup as parent
groupId: tree.groupId || unusedGroupId,
}));
const treeGroups = [
// Create a new tree group which holds the old ones
{
name: wrappingGroupName,
groupId: unusedGroupId,
children: originalTreeGroups,
},
];

return [trees, treeGroups];
}

export function parseNml(
nmlString: string,
wrappingGroupName?: ?string,
): Promise<{ trees: TreeMapType, treeGroups: Array<TreeGroupType> }> {
return new Promise((resolve, reject) => {
const parser = new Saxophone();
Expand Down Expand Up @@ -547,7 +575,19 @@ export function parseNml(
}
})
.on("end", () => {
resolve({ trees, treeGroups });
if (wrappingGroupName != null) {
const [wrappedTrees, wrappedTreeGroups] = wrapInNewGroup(
trees,
treeGroups,
wrappingGroupName,
);
resolve({
trees: wrappedTrees,
treeGroups: wrappedTreeGroups,
});
} else {
resolve({ trees, treeGroups });
}
})
.on("error", reject);

Expand Down
22 changes: 22 additions & 0 deletions app/assets/javascripts/oxalis/model/reducers/ui_reducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @flow

import update from "immutability-helper";
import type { OxalisState } from "oxalis/store";
import type { ActionType } from "oxalis/model/actions/actions";

function UiReducer(state: OxalisState, action: ActionType): OxalisState {
switch (action.type) {
case "SET_DROPZONE_MODAL_VISIBILITY_ACTION_TYPE": {
return update(state, {
uiInformation: {
showDropzoneModal: { $set: action.visible },
},
});
}

default:
return state;
}
}

export default UiReducer;
10 changes: 10 additions & 0 deletions app/assets/javascripts/oxalis/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import FlycamReducer from "oxalis/model/reducers/flycam_reducer";
import ViewModeReducer from "oxalis/model/reducers/view_mode_reducer";
import AnnotationReducer from "oxalis/model/reducers/annotation_reducer";
import UserReducer from "oxalis/model/reducers/user_reducer";
import UiReducer from "oxalis/model/reducers/ui_reducer";
import rootSaga from "oxalis/model/sagas/root_saga";
import overwriteActionMiddleware from "oxalis/model/helpers/overwrite_action_middleware";
import googleAnalyticsMiddleware from "oxalis/model/helpers/google_analytics_middleware";
Expand Down Expand Up @@ -293,6 +294,10 @@ export type ViewModeData = {
+flight: ?FlightModeData,
};

type UiInformationType = {
+showDropzoneModal: boolean,
};

export type OxalisState = {
+datasetConfiguration: DatasetConfigurationType,
+userConfiguration: UserConfigurationType,
Expand All @@ -304,6 +309,7 @@ export type OxalisState = {
+flycam: FlycamType,
+viewModeData: ViewModeData,
+activeUser: ?APIUserType,
+uiInformation: UiInformationType,
};

export const defaultState: OxalisState = {
Expand Down Expand Up @@ -429,6 +435,9 @@ export const defaultState: OxalisState = {
flight: null,
},
activeUser: null,
uiInformation: {
showDropzoneModal: false,
},
};

const sagaMiddleware = createSagaMiddleware();
Expand All @@ -445,6 +454,7 @@ const combinedReducers = reduceReducers(
ViewModeReducer,
AnnotationReducer,
UserReducer,
UiReducer,
);

const store = createStore(
Expand Down
55 changes: 33 additions & 22 deletions app/assets/javascripts/oxalis/view/action-bar/merge_modal_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import Toast from "libs/toast";
import Request from "libs/request";
import { Modal, Button, Upload, Select, Form, Spin } from "antd";
import { Alert, Modal, Button, Upload, Select, Form, Spin } from "antd";
import messages from "messages";
import InputComponent from "oxalis/view/components/input_component";
import api from "oxalis/api/internal_api";
Expand Down Expand Up @@ -164,8 +164,21 @@ class MergeModalView extends PureComponent<Props, MergeModalViewState> {
onOk={this.props.onOk}
onCancel={this.props.onOk}
className="merge-modal"
width={600}
>
<Spin spinning={this.state.isUploading}>
<Alert
message={
<span>
The merged tracing will be saved as a <b>new</b> explorative tracing in your
account. If you wish to import NML files right into the current tracing, just drag
and drop them into the tracing view.
</span>
}
type="warning"
style={{ marginBottom: 12 }}
/>

<Form layout="inline" onSubmit={this.handleMergeTaskType}>
<Form.Item label="Task Type">
<Select
Expand Down Expand Up @@ -218,25 +231,6 @@ class MergeModalView extends PureComponent<Props, MergeModalViewState> {
</Form.Item>
</Form>

<Form layout="inline">
<Form.Item label="NML">
<Upload
name="nmlFile"
action="/api/annotations/upload"
headers={{ authorization: "authorization-text" }}
beforeUpload={this.handleBeforeUploadNML}
onChange={this.handleChangeNML}
value={this.state.selectedNML}
accept=".nml"
showUploadList={false}
>
<Button icon="upload" style={{ width: 200 }}>
Upload NML and merge
</Button>
</Upload>
</Form.Item>
</Form>

<Form layout="inline" onSubmit={this.handleMergeExplorativeAnnotation}>
<Form.Item label="Explorative Annotation">
<InputComponent
Expand All @@ -256,8 +250,25 @@ class MergeModalView extends PureComponent<Props, MergeModalViewState> {
</Button>
</Form.Item>
</Form>
<hr />
<p>The merged tracing will be saved as a new explorative tracing.</p>

<Form layout="inline">
<Form.Item label="NML">
<Upload
name="nmlFile"
action="/api/annotations/upload"
headers={{ authorization: "authorization-text" }}
beforeUpload={this.handleBeforeUploadNML}
onChange={this.handleChangeNML}
value={this.state.selectedNML}
accept=".nml"
showUploadList={false}
>
<Button icon="upload" style={{ width: 200 }}>
Upload NML and merge
</Button>
</Upload>
</Form.Item>
</Form>
</Spin>
</Modal>
);
Expand Down
Loading

0 comments on commit fe1ca84

Please sign in to comment.