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

After merge, remove segment item of id that doesn't exist anymore #7729

Merged
merged 9 commits into from
Apr 8, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Changed the time-tracking overview to show times spent in annotations and tasks and filter them by teams and projects. In the linked detail view, the tracked times can also be filtered by type (annotations or tasks) and project. [#7524](https://github.com/scalableminds/webknossos/pull/7524)
- The time tracking api route `/api/users/:id/loggedTime`, which is used by the webknossos-libs client, and groups the times by month, now uses UTC when determining month limits, rather than the server’s local timezone. [#7524](https://github.com/scalableminds/webknossos/pull/7524)
- Duplicated annotations are opened in a new browser tab. [#7724](https://github.com/scalableminds/webknossos/pull/7724)
- When proofreading segments and merging two segments, the segment item that doesn't exist anymore after the merge is automatically removed. [#7729](https://github.com/scalableminds/webknossos/pull/7729)
- Changed some internal APIs to use spelling dataset instead of dataSet. This requires all connected datastores to be the latest version. [#7690](https://github.com/scalableminds/webknossos/pull/7690)

### Fixed
Expand All @@ -29,3 +30,4 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
### Removed

### Breaking Changes
- When merging two trees or segments, the active item will always "survive" the merge operation (the clicked item will be merged *into* the active one). This was not consistent for certain skeleton-based operations (i.e., merging skeletons with a shortcut and proofreading segmentations with agglomerate skeletons). [#7729](https://github.com/scalableminds/webknossos/pull/7729)
Original file line number Diff line number Diff line change
Expand Up @@ -939,29 +939,30 @@ function SkeletonTracingReducer(state: OxalisState, action: Action): OxalisState
const isProofreadingActive =
state.uiInformation.activeTool === AnnotationToolEnum.PROOFREAD;
const treeType = isProofreadingActive ? TreeTypeEnum.AGGLOMERATE : TreeTypeEnum.DEFAULT;
const trees = getTreesWithType(skeletonTracing, treeType);
return mergeTrees(trees, sourceNodeId, targetNodeId)
.map(([trees, newActiveTreeId, newActiveNodeId]) =>
update(state, {
tracing: {
skeleton: {
trees: {
$set: trees,
},
activeNodeId: {
$set: newActiveNodeId,
},
activeTreeId: {
$set: newActiveTreeId,
},
activeGroupId: {
$set: null,
},
},
const oldTrees = getTreesWithType(skeletonTracing, treeType);
const mergeResult = mergeTrees(oldTrees, sourceNodeId, targetNodeId);
if (mergeResult == null) {
return state;
}
const [trees, newActiveTreeId, newActiveNodeId] = mergeResult;
return update(state, {
tracing: {
skeleton: {
trees: {
$set: trees,
},
}),
)
.getOrElse(state);
activeNodeId: {
$set: newActiveNodeId,
},
activeTreeId: {
$set: newActiveTreeId,
},
activeGroupId: {
$set: null,
},
},
},
});
}

case "SET_TREE_NAME": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,44 +651,45 @@ export function mergeTrees(
trees: TreeMap,
sourceNodeId: number,
targetNodeId: number,
): Maybe<[TreeMap, number, number]> {
const sourceTree = findTreeByNodeId(trees, sourceNodeId);
const targetTree = findTreeByNodeId(trees, targetNodeId); // should be activeTree
): [TreeMap, number, number] | null {
// targetTree will be removed (the content will be merged into sourceTree).
const sourceTree = findTreeByNodeId(trees, sourceNodeId); // should be activeTree, so that the active tree "survives"
const targetTree = findTreeByNodeId(trees, targetNodeId);

if (sourceTree == null || targetTree == null || sourceTree === targetTree) {
return Maybe.Nothing();
if (targetTree == null || sourceTree == null || targetTree === sourceTree) {
return null;
}

const newEdge: Edge = {
source: sourceNodeId,
target: targetNodeId,
};

let newTrees = _.omit(trees, sourceTree.treeId);
let newTrees = _.omit(trees, targetTree.treeId);

const newNodes = targetTree.nodes.clone();
const newNodes = sourceTree.nodes.clone();

for (const [id, node] of sourceTree.nodes.entries()) {
for (const [id, node] of targetTree.nodes.entries()) {
newNodes.mutableSet(id, node);
}

newTrees = update(newTrees, {
[targetTree.treeId]: {
[sourceTree.treeId]: {
nodes: {
$set: newNodes,
},
edges: {
$set: targetTree.edges.addEdges(sourceTree.edges.asArray().concat(newEdge)),
$set: sourceTree.edges.addEdges(targetTree.edges.asArray().concat(newEdge)),
},
comments: {
$set: targetTree.comments.concat(sourceTree.comments),
$set: sourceTree.comments.concat(targetTree.comments),
},
branchPoints: {
$set: targetTree.branchPoints.concat(sourceTree.branchPoints),
$set: sourceTree.branchPoints.concat(targetTree.branchPoints),
},
},
});
return Maybe.Just([newTrees, targetTree.treeId, targetNodeId]);
return [newTrees, sourceTree.treeId, sourceNodeId];
}
export function shuffleTreeColor(
skeletonTracing: SkeletonTracing,
Expand Down
12 changes: 12 additions & 0 deletions frontend/javascripts/oxalis/model/sagas/proofread_saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "oxalis/model/actions/skeletontracing_actions";
import {
initializeEditableMappingAction,
removeSegmentAction,
setMappingIsEditableAction,
} from "oxalis/model/actions/volumetracing_actions";
import type { ProofreadAtPositionAction } from "oxalis/model/actions/proofread_actions";
Expand Down Expand Up @@ -238,6 +239,8 @@ function* handleSkeletonProofreadingAction(action: Action): Saga<void> {
// Actually, action is MergeTreesAction | DeleteEdgeAction | MinCutAgglomerateAction,
// but the takeEveryUnlessBusy wrapper does not understand this.
// Handles split, merge and min-cut actions on agglomerates.
// Note that the skeletontracing reducer already mutated the skeletons according to the
// received action.
if (
action.type !== "MERGE_TREES" &&
action.type !== "DELETE_EDGE" &&
Expand Down Expand Up @@ -394,12 +397,16 @@ function* handleSkeletonProofreadingAction(action: Action): Saga<void> {
),
);
if (sourceTree !== targetTree) {
// A split happened, because the new trees are not identical.
yield* put(
setTreeNameAction(
getTreeNameForAgglomerateSkeleton(newTargetAgglomerateId, volumeTracing.mappingName),
targetTree.treeId,
),
);
} else {
// A merge happened. Remove the segment that doesn't exist anymore.
yield* put(removeSegmentAction(targetAgglomerateId, volumeTracing.tracingId));
}

const pack = (agglomerateId: number, newAgglomerateId: number, nodePosition: Vector3) => ({
Expand Down Expand Up @@ -633,6 +640,11 @@ function* handleProofreadMergeOrMinCut(action: Action) {
bucket.containsValue(targetAgglomerateId),
);

if (action.type === "PROOFREAD_MERGE") {
// Remove the segment that doesn't exist anymore.
yield* put(removeSegmentAction(targetAgglomerateId, volumeTracing.tracingId));
}

const [newSourceAgglomerateId, newTargetAgglomerateId] = yield* all([
call(getDataValue, sourcePosition),
call(getDataValue, targetPosition),
Expand Down
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/view/context_menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ function getNodeContextMenuOptions({
disabled: areInSameTree,
onClick: () =>
activeNodeId != null
? Store.dispatch(mergeTreesAction(clickedNodeId, activeNodeId))
? Store.dispatch(mergeTreesAction(activeNodeId, clickedNodeId))
: null,
label: (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,7 @@ test("SkeletonTracing should merge two trees", (t) => {
viewport,
resolution,
);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 3);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(3, 1);
// create a node in first tree, then create a second tree with three nodes and merge them
const newState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand All @@ -955,8 +955,8 @@ test("SkeletonTracing should merge two trees", (t) => {
target: 4,
},
{
source: 1,
target: 3,
source: 3,
target: 1,
},
]);
});
Expand Down Expand Up @@ -987,7 +987,7 @@ test("SkeletonTracing should merge two trees with comments and branchPoints", (t
viewport,
resolution,
);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 3);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(3, 1);
const createCommentAction = SkeletonTracingActions.createCommentAction("foo");
const createBranchPointAction = SkeletonTracingActions.createBranchPointAction();
// create a node in first tree, then create a second tree with three nodes and merge them
Expand Down Expand Up @@ -1016,8 +1016,8 @@ test("SkeletonTracing should merge two trees with comments and branchPoints", (t
target: 4,
},
{
source: 1,
target: 3,
source: 3,
target: 1,
},
]);
t.is(newSkeletonTracing.trees[2].comments.length, 2);
Expand Down Expand Up @@ -1217,6 +1217,7 @@ test("SkeletonTracing should delete a comment for a node", (t) => {
const newSkeletonTracing = enforceSkeletonTracing(newState.tracing);
t.deepEqual(newSkeletonTracing.trees[1].comments.length, 0);
});

test("SkeletonTracing should only delete the comment for the active node", (t) => {
const commentText = "Wow such test comment";
const createNodeAction = SkeletonTracingActions.createNodeAction(
Expand Down
44 changes: 22 additions & 22 deletions frontend/javascripts/test/sagas/skeletontracing_saga.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ test("SkeletonTracingSaga should emit createNode and createTree update actions",
});
});
test("SkeletonTracingSaga should emit first deleteNode and then createNode update actions", (t) => {
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(2, 1);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 2);
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
.apply(SkeletonTracingReducer, createTreeAction)
Expand Down Expand Up @@ -329,8 +329,8 @@ test("SkeletonTracingSaga should emit first deleteNode and then createNode updat
name: "createEdge",
value: {
treeId: 1,
source: 2,
target: 1,
source: 1,
target: 2,
},
});
});
Expand Down Expand Up @@ -481,7 +481,7 @@ test("SkeletonTracingSaga should emit an updateTree update actions (branchpoints
});
});
test("SkeletonTracingSaga should emit update actions on merge tree", (t) => {
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 3);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(3, 1);
// create a node in first tree, then create a second tree with three nodes and merge them
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down Expand Up @@ -521,13 +521,13 @@ test("SkeletonTracingSaga should emit update actions on merge tree", (t) => {
name: "createEdge",
value: {
treeId: 2,
source: 1,
target: 3,
source: 3,
target: 1,
},
});
});
test("SkeletonTracingSaga should emit update actions on split tree", (t) => {
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 3);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(3, 1);
// create a node in first tree, then create a second tree with three nodes and merge them
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down Expand Up @@ -612,13 +612,13 @@ test("SkeletonTracingSaga should emit update actions on split tree", (t) => {
name: "deleteEdge",
value: {
treeId: 2,
source: 1,
target: 3,
source: 3,
target: 1,
},
});
});
test("compactUpdateActions should detect a tree merge (1/3)", (t) => {
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 4);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(4, 1);
// Create three nodes in the first tree, then create a second tree with one node and merge them
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down Expand Up @@ -661,15 +661,15 @@ test("compactUpdateActions should detect a tree merge (1/3)", (t) => {
name: "createEdge",
value: {
treeId: 2,
source: 1,
target: 4,
source: 4,
target: 1,
},
});
t.is(simplifiedFirstBatch.length, 3);
});
test("compactUpdateActions should detect a tree merge (2/3)", (t) => {
// In this test multiple diffs are performed and concatenated before compactUpdateActions is invoked
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 5);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(5, 1);
// Create three nodes in the first tree, then create a second tree with one node
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down Expand Up @@ -742,16 +742,16 @@ test("compactUpdateActions should detect a tree merge (2/3)", (t) => {
name: "createEdge",
value: {
treeId: 2,
source: 1,
target: 5,
source: 5,
target: 1,
},
});
t.is(simplifiedSecondBatch.length, 5);
});
test("compactUpdateActions should detect a tree merge (3/3)", (t) => {
// In this test multiple merges and diffs are performed and concatenated before compactUpdateActions is invoked
const firstMergeTreesAction = SkeletonTracingActions.mergeTreesAction(4, 1);
const secondMergeTreesAction = SkeletonTracingActions.mergeTreesAction(6, 1);
const firstMergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 4);
const secondMergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 6);
// Create three nodes in the first tree, then create a second tree with one node
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down Expand Up @@ -823,8 +823,8 @@ test("compactUpdateActions should detect a tree merge (3/3)", (t) => {
name: "createEdge",
value: {
treeId: 1,
source: 4,
target: 1,
source: 1,
target: 4,
},
});
t.is(simplifiedFirstBatch.length, 3);
Expand Down Expand Up @@ -855,8 +855,8 @@ test("compactUpdateActions should detect a tree merge (3/3)", (t) => {
name: "createEdge",
value: {
treeId: 1,
source: 6,
target: 1,
source: 1,
target: 6,
},
});
t.is(simplifiedThirdBatch.length, 3);
Expand Down Expand Up @@ -1077,7 +1077,7 @@ test("compactUpdateActions should do nothing if it cannot compact", (t) => {
// the right spot (see code comments for why)
// This case cannot happen currently as there is no action in webknossos that results in such a diff,
// it could however exist in the future and this test makes sure things won't break then
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(1, 2);
const mergeTreesAction = SkeletonTracingActions.mergeTreesAction(2, 1);
// Create three nodes in the first tree, then create a second tree with one node and merge them
const testState = ChainReducer<OxalisState, Action>(initialState)
.apply(SkeletonTracingReducer, createNodeAction)
Expand Down