From 5aad836d9dabca8651f3e1d8d63218dd62bddb16 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 4 Feb 2019 11:16:24 +0100 Subject: [PATCH 1/4] allow to import NMLs which were created for different datasets (fixes #3690) --- app/assets/javascripts/messages.js | 3 +- .../oxalis/model/helpers/nml_helpers.js | 12 +++--- .../oxalis/view/right-menu/trees_tab_view.js | 42 +++++++++++++------ 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/messages.js b/app/assets/javascripts/messages.js index 356139430b0..024710dac0b 100644 --- a/app/assets/javascripts/messages.js +++ b/app/assets/javascripts/messages.js @@ -212,7 +212,8 @@ In order to restore the current window, a reload is necessary.`, "nml.edge_with_same_source_target": "NML contains with same source and target id: Edge", "nml.tree_not_connected": "NML contains tree that is not fully connected: Tree with id", - "nml.different_dataset": "Imported NML was originally for a different dataset.", + "nml.different_dataset": + "At least one NML file was originally created for a different dataset. Are you sure you want to continue with the import?", "merge.different_dataset": "The merge cannot be executed, because the underlying datasets are not the same.", "merge.volume_unsupported": "Merging is not supported for volume tracings.", diff --git a/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js b/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js index 1096782eef6..e7931854d0c 100644 --- a/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js +++ b/app/assets/javascripts/oxalis/model/helpers/nml_helpers.js @@ -11,7 +11,7 @@ import { getPosition, getRotation } from "oxalis/model/accessors/flycam_accessor import Date from "libs/date"; import DiffableMap from "libs/diffable_map"; import EdgeCollection from "oxalis/model/edge_collection"; -import Store, { +import { type NodeMap, type OxalisState, type SkeletonTracing, @@ -428,7 +428,7 @@ function wrapInNewGroup( export function parseNml( nmlString: string, wrappingGroupName?: ?string, -): Promise<{ trees: TreeMap, treeGroups: Array }> { +): Promise<{ trees: TreeMap, treeGroups: Array, datasetName: ?string }> { return new Promise((resolve, reject) => { const parser = new Saxophone(); @@ -440,14 +440,13 @@ export function parseNml( let currentTree: ?Tree = null; let currentGroup: ?TreeGroup = null; const groupIdToParent: { [number]: ?TreeGroup } = {}; + let datasetName = null; parser .on("tagopen", node => { const attr = Saxophone.parseAttrs(node.attrs); switch (node.name) { case "experiment": { - if (attr.name !== Store.getState().dataset.name) { - throw new NmlParseError(messages["nml.different_dataset"]); - } + datasetName = attr.name; break; } case "thing": { @@ -619,9 +618,10 @@ export function parseNml( resolve({ trees: wrappedTrees, treeGroups: wrappedTreeGroups, + datasetName, }); } else { - resolve({ trees, treeGroups }); + resolve({ trees, treeGroups, datasetName }); } }) .on("error", reject); diff --git a/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js b/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js index b4d8de09ec1..90e5fab304e 100644 --- a/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js +++ b/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js @@ -2,13 +2,19 @@ * list_tree_view.js * @flow */ -import _ from "lodash"; import { Alert, Button, Dropdown, Input, Menu, Icon, Spin, Modal, Tooltip } from "antd"; import type { Dispatch } from "redux"; import { connect } from "react-redux"; import { saveAs } from "file-saver"; import * as React from "react"; +import _ from "lodash"; +import { binaryConfirm } from "libs/async_confirm"; +import { + createGroupToTreesMap, + callDeep, + MISSING_GROUP_ID, +} from "oxalis/view/right-menu/tree_hierarchy_view_helpers"; import { getActiveTree, getActiveGroup } from "oxalis/model/accessors/skeletontracing_accessor"; import { getBuildInfo } from "admin/admin_rest_api"; import { readFileAsText } from "libs/read_file"; @@ -31,12 +37,6 @@ import { setTreeGroupsAction, addTreesAndGroupsAction, } from "oxalis/model/actions/skeletontracing_actions"; -import messages from "messages"; -import { - createGroupToTreesMap, - callDeep, - MISSING_GROUP_ID, -} from "oxalis/view/right-menu/tree_hierarchy_view_helpers"; import { updateUserSettingAction } from "oxalis/model/actions/settings_actions"; import ButtonComponent from "oxalis/view/components/button_component"; import InputComponent from "oxalis/view/components/input_component"; @@ -51,8 +51,10 @@ import Toast from "libs/toast"; import TreeHierarchyView from "oxalis/view/right-menu/tree_hierarchy_view"; import * as Utils from "libs/utils"; import api from "oxalis/api/internal_api"; -import SearchPopover from "./search_popover"; +import messages from "messages"; + import DeleteGroupModalView from "./delete_group_modal_view"; +import SearchPopover from "./search_popover"; const ButtonGroup = Button.Group; const InputGroup = Input.Group; @@ -91,15 +93,18 @@ type State = { export async function importNmls(files: Array, createGroupForEachFile: boolean) { try { - const { successes: importActions, errors } = await Utils.promiseAllWithErrors( + const { successes: importActionsWithDatasetNames, errors } = await Utils.promiseAllWithErrors( files.map(async file => { const nmlString = await readFileAsText(file); try { - const { trees, treeGroups } = await parseNml( + const { trees, treeGroups, datasetName } = await parseNml( nmlString, createGroupForEachFile ? file.name : null, ); - return addTreesAndGroupsAction(trees, treeGroups); + return { + importAction: addTreesAndGroupsAction(trees, treeGroups), + datasetName, + }; } catch (e) { throw new Error(`"${file.name}" could not be parsed. ${e.message}`); } @@ -110,10 +115,23 @@ export async function importNmls(files: Array, createGroupForEachFile: boo throw errors; } + const currentDatasetName = Store.getState().dataset.name; + const doDatasetNamesDiffer = importActionsWithDatasetNames + .map(el => el.datasetName) + .some(name => name != null && name !== currentDatasetName); + if (doDatasetNamesDiffer) { + const shouldImport = await binaryConfirm("Are you sure?", messages["nml.different_dataset"]); + if (!shouldImport) { + return; + } + } + // Dispatch the actual actions as the very last step, so that // not a single store mutation happens if something above throws // an error - importActions.forEach(action => Store.dispatch(action)); + importActionsWithDatasetNames + .map(el => el.importAction) + .forEach(action => Store.dispatch(action)); } catch (e) { (Array.isArray(e) ? e : [e]).forEach(err => Toast.error(err.message)); } From 84a047cd2ea69798c4b6f6399c867f9d6581fc1d Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 4 Feb 2019 11:22:30 +0100 Subject: [PATCH 2/4] check in missing file --- app/assets/javascripts/libs/async_confirm.js | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 app/assets/javascripts/libs/async_confirm.js diff --git a/app/assets/javascripts/libs/async_confirm.js b/app/assets/javascripts/libs/async_confirm.js new file mode 100644 index 00000000000..2007a2140f8 --- /dev/null +++ b/app/assets/javascripts/libs/async_confirm.js @@ -0,0 +1,24 @@ +// @flow +import { Modal } from "antd"; + +const { confirm } = Modal; + +export async function binaryConfirm(title: string, content: string): Promise { + return new Promise(resolve => { + confirm({ + title, + content, + okText: "Yes", + okType: "danger", + cancelText: "No", + onOk() { + resolve(true); + }, + onCancel() { + resolve(false); + }, + }); + }); +} + +export default {}; From 6334820983e6a588573ff898d1e40ed6e76b76c7 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 4 Feb 2019 11:22:36 +0100 Subject: [PATCH 3/4] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0112827839..4132de62670 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.md). - The volume annotation brush tool will now automatically fill any enclosed areas if the brushed outline is closed in one stroke. [#3698](https://github.com/scalableminds/webknossos/pull/3698) ![brush-fill-4](https://user-images.githubusercontent.com/1702075/51846983-02d34480-231b-11e9-86f2-2d8c4b0c9bd0.gif) - Statistics are now separated by organization, rather than showing the webKnossos instance’s totals. [#3663](https://github.com/scalableminds/webknossos/pull/3663) +- NML files can be imported into arbitrary datasets. Users will be asked to confirm the import process if the dataset of the NML differs from the currently active dataset. [#3716](https://github.com/scalableminds/webknossos/pull/3716) ### Fixed From 5a0d41aa1b9f16cdf40e78c5bc9cb7a3c15cd38b Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 4 Feb 2019 15:48:56 +0100 Subject: [PATCH 4/4] also check for empty dataset name --- app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js b/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js index 90e5fab304e..b0452868b1b 100644 --- a/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js +++ b/app/assets/javascripts/oxalis/view/right-menu/trees_tab_view.js @@ -118,7 +118,7 @@ export async function importNmls(files: Array, createGroupForEachFile: boo const currentDatasetName = Store.getState().dataset.name; const doDatasetNamesDiffer = importActionsWithDatasetNames .map(el => el.datasetName) - .some(name => name != null && name !== currentDatasetName); + .some(name => name !== "" && name != null && name !== currentDatasetName); if (doDatasetNamesDiffer) { const shouldImport = await binaryConfirm("Are you sure?", messages["nml.different_dataset"]); if (!shouldImport) {