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

Drag'n'drop import of multiple NML files #2908

Merged
merged 14 commits into from
Jul 24, 2018
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
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