From ee5dbc716ea63749c36ac5a836d4763b269b6749 Mon Sep 17 00:00:00 2001 From: Philipp Otto Date: Mon, 15 Mar 2021 13:32:35 +0100 Subject: [PATCH] Also show measured distances in vx (in addition to nm) (#5240) * also show measured distance in vx * clean up code so that voxel distance is integrated at all relevant places and formatted nicely * update changelog * fix type * add missing fullstop and add breaking change section to changelog --- CHANGELOG.unreleased.md | 5 +- frontend/javascripts/libs/format_utils.js | 17 ++++--- frontend/javascripts/messages.js | 4 +- frontend/javascripts/oxalis/api/api_latest.js | 49 +++++++++++++------ .../oxalis/view/node_context_menu.js | 37 +++++++++----- .../view/right-menu/tree_hierarchy_view.js | 11 +++-- .../oxalis/view/right-menu/trees_tab_view.js | 8 +-- 7 files changed, 90 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 6c52a581dc5..4df32c1a56c 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -18,10 +18,13 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Added a link to dataset view mode from annotation mode info tab. [#5262](https://github.com/scalableminds/webknossos/pull/5262) ### Changed -- +- Measured distances will be shown in voxel space, too. [#5240](https://github.com/scalableminds/webknossos/pull/5240) ### Fixed - Fixed a regression in the task search which could lead to a frontend crash. [#5267](https://github.com/scalableminds/webknossos/pull/5267) ### Removed - + +### Breaking Change +- The front-end API methods `measurePathLengthBetweenNodes`, `measureAllTrees` and `measureTreeLength` were changed to return a tuple containing the distance in nm and in vx (instead of only returning the distance in nm). [#5240](https://github.com/scalableminds/webknossos/pull/5240) diff --git a/frontend/javascripts/libs/format_utils.js b/frontend/javascripts/libs/format_utils.js index ea0c29c8c1c..9647f7ccfb3 100644 --- a/frontend/javascripts/libs/format_utils.js +++ b/frontend/javascripts/libs/format_utils.js @@ -59,16 +59,21 @@ export function formatScale(scaleArr: ?Vector3, roundTo?: number = 2): string { } } -export function formatNumberToLength(numberInNm: number): string { - if (numberInNm < 1000) { - return `${numberInNm.toFixed(0)}${ThinSpace}nm`; - } else if (numberInNm < 1000000) { - return `${(numberInNm / 1000).toFixed(1)}${ThinSpace}µm`; +export function formatNumberToLength(lengthInNm: number): string { + if (lengthInNm < 1000) { + return `${lengthInNm.toFixed(0)}${ThinSpace}nm`; + } else if (lengthInNm < 1000000) { + return `${(lengthInNm / 1000).toFixed(1)}${ThinSpace}µm`; } else { - return `${(numberInNm / 1000000).toFixed(1)}${ThinSpace}mm`; + return `${(lengthInNm / 1000000).toFixed(1)}${ThinSpace}mm`; } } +export function formatLengthAsVx(lengthInVx: number, roundTo?: number = 2): string { + const roundedLength = Utils.roundTo(lengthInVx, roundTo); + return `${roundedLength} vx`; +} + export function formatExtentWithLength( extent: BoundingBoxObject, formattingFunction: number => string, diff --git a/frontend/javascripts/messages.js b/frontend/javascripts/messages.js index 02ab65b8e44..bbc7d0e3f2e 100644 --- a/frontend/javascripts/messages.js +++ b/frontend/javascripts/messages.js @@ -104,8 +104,8 @@ instead. Only enable this option if you understand its effect. All layers will n "tracing.out_of_task_bounds": "The current position is outside of the task's bounding box.", "tracing.copy_position": "Copy position to clipboard.", "tracing.copy_rotation": "Copy rotation to clipboard.", - "tracing.tree_length_notification": (treeName: string, length: string) => - `The tree ${treeName} has a total path length of ${length}.`, + "tracing.tree_length_notification": (treeName: string, lengthInNm: string, lengthInVx: string) => + `The tree ${treeName} has a total path length of ${lengthInNm} (${lengthInVx}).`, "tracing.sharing_modal_basic_information": (sharingActiveNode?: boolean) => `This link includes the ${ sharingActiveNode ? "active tree node," : "" diff --git a/frontend/javascripts/oxalis/api/api_latest.js b/frontend/javascripts/oxalis/api/api_latest.js index eb49a186b78..fe08c22ea3d 100644 --- a/frontend/javascripts/oxalis/api/api_latest.js +++ b/frontend/javascripts/oxalis/api/api_latest.js @@ -633,7 +633,10 @@ class TracingApi { return result; } - measureTreeLength(treeId: number): number { + /** + * Measures the length of the given tree and returns the length in nanometer and in voxels. + */ + measureTreeLength(treeId: number): [number, number] { const skeletonTracing = assertSkeleton(Store.getState().tracing); const tree = skeletonTracing.trees[treeId]; if (!tree) { @@ -644,28 +647,39 @@ class TracingApi { // Pre-allocate vectors - let lengthAcc = 0; + let lengthNmAcc = 0; + let lengthVxAcc = 0; for (const edge of tree.edges.all()) { const sourceNode = tree.nodes.get(edge.source); const targetNode = tree.nodes.get(edge.target); - lengthAcc += V3.scaledDist(sourceNode.position, targetNode.position, datasetScale); + lengthNmAcc += V3.scaledDist(sourceNode.position, targetNode.position, datasetScale); + lengthVxAcc += V3.length(V3.sub(sourceNode.position, targetNode.position)); } - return lengthAcc; + return [lengthNmAcc, lengthVxAcc]; } - measureAllTrees(): number { + /** + * Measures the length of all trees and returns the length in nanometer and in voxels. + */ + measureAllTrees(): [number, number] { const skeletonTracing = assertSkeleton(Store.getState().tracing); - const totalLength = _.values(skeletonTracing.trees).reduce( - (sum, currentTree) => sum + this.measureTreeLength(currentTree.treeId), - 0, - ); + let totalLengthNm = 0; + let totalLengthVx = 0; + _.values(skeletonTracing.trees).forEach(currentTree => { + const [lengthNm, lengthVx] = this.measureTreeLength(currentTree.treeId); + totalLengthNm += lengthNm; + totalLengthVx += lengthVx; + }); - return totalLength; + return [totalLengthNm, totalLengthVx]; } - measurePathLengthBetweenNodes(sourceNodeId: number, targetNodeId: number): number { + /** + * Returns the length of the shortest path between two nodes in nanometer and in voxels. + */ + measurePathLengthBetweenNodes(sourceNodeId: number, targetNodeId: number): [number, number] { const skeletonTracing = assertSkeleton(Store.getState().tracing); const { node: sourceNode, tree: sourceTree } = getNodeAndTreeOrNull( skeletonTracing, @@ -684,9 +698,14 @@ class TracingApi { const datasetScale = Store.getState().dataset.dataSource.scale; // We use the Dijkstra algorithm to get the shortest path between the nodes. const distanceMap = {}; + // The distance map is also maintained in voxel space. This information is only + // used when returning the final distance. The actual path finding is only done in + // the physical space (nm-based). + const distanceMapVx = {}; const getDistance = nodeId => distanceMap[nodeId] != null ? distanceMap[nodeId] : Number.POSITIVE_INFINITY; distanceMap[sourceNode.id] = 0; + distanceMapVx[sourceNode.id] = 0; // The priority queue saves node id and distance tuples. const priorityQueue = new PriorityQueue<[number, number]>({ comparator: ([_first, firstDistance], [_second, secondDistance]) => @@ -699,16 +718,18 @@ class TracingApi { // Calculate the distance to all neighbours and update the distances. for (const { source, target } of sourceTree.edges.getEdgesForNode(nextNodeId)) { const neighbourNodeId = source === nextNodeId ? target : source; - const neightbourPosition = sourceTree.nodes.get(neighbourNodeId).position; + const neighbourPosition = sourceTree.nodes.get(neighbourNodeId).position; const neighbourDistance = - distance + V3.scaledDist(nextNodePosition, neightbourPosition, datasetScale); + distance + V3.scaledDist(nextNodePosition, neighbourPosition, datasetScale); if (neighbourDistance < getDistance(neighbourNodeId)) { distanceMap[neighbourNodeId] = neighbourDistance; + const neighbourDistanceVx = V3.length(V3.sub(nextNodePosition, neighbourPosition)); + distanceMapVx[neighbourNodeId] = neighbourDistanceVx; priorityQueue.queue([neighbourNodeId, neighbourDistance]); } } } - return distanceMap[targetNodeId]; + return [distanceMap[targetNodeId], distanceMapVx[targetNodeId]]; } /** diff --git a/frontend/javascripts/oxalis/view/node_context_menu.js b/frontend/javascripts/oxalis/view/node_context_menu.js index ae539f52187..00ffc9cb912 100644 --- a/frontend/javascripts/oxalis/view/node_context_menu.js +++ b/frontend/javascripts/oxalis/view/node_context_menu.js @@ -20,7 +20,8 @@ import Toast from "libs/toast"; import Clipboard from "clipboard-js"; import messages from "messages"; import { getNodeAndTree, findTreeByNodeId } from "oxalis/model/accessors/skeletontracing_accessor"; -import { formatNumberToLength } from "libs/format_utils"; +import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; +import { roundTo } from "libs/utils"; /* eslint-disable react/no-unused-prop-types */ // The newest eslint version thinks the props listed below aren't used. @@ -64,17 +65,26 @@ function copyIconWithTooltip(value: string | number, title: string) { } function measureAndShowLengthBetweenNodes(sourceNodeId: number, targetNodeId: number) { - const length = api.tracing.measurePathLengthBetweenNodes(sourceNodeId, targetNodeId); + const [lengthNm, lengthVx] = api.tracing.measurePathLengthBetweenNodes( + sourceNodeId, + targetNodeId, + ); notification.open({ - message: `The shortest path length between the nodes is ${formatNumberToLength(length)}.`, + message: `The shortest path length between the nodes is ${formatNumberToLength( + lengthNm, + )} (${formatLengthAsVx(lengthVx)}).`, icon: , }); } function measureAndShowFullTreeLength(treeId: number, treeName: string) { - const length = api.tracing.measureTreeLength(treeId); + const [lengthInNm, lengthInVx] = api.tracing.measureTreeLength(treeId); notification.open({ - message: messages["tracing.tree_length_notification"](treeName, formatNumberToLength(length)), + message: messages["tracing.tree_length_notification"]( + treeName, + formatNumberToLength(lengthInNm), + formatLengthAsVx(lengthInVx), + ), icon: , }); } @@ -224,13 +234,16 @@ function NodeContextMenu(props: Props) { : null; const distanceToSelection = activeNode != null - ? formatNumberToLength( - V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale), - ) + ? [ + formatNumberToLength( + V3.scaledDist(activeNode.position, positionToMeasureDistanceTo, datasetScale), + ), + formatLengthAsVx(V3.length(V3.sub(activeNode.position, positionToMeasureDistanceTo))), + ] : null; const nodePositionAsString = nodeContextMenuNode != null - ? nodeContextMenuNode.position.map(value => value.toFixed(2)).join(", ") + ? nodeContextMenuNode.position.map(value => roundTo(value, 2)).join(", ") : ""; return ( @@ -263,9 +276,9 @@ function NodeContextMenu(props: Props) { ) : null} {distanceToSelection != null ? (
- {distanceToSelection} to this{" "} - {clickedNodeId != null ? "Node" : "Position"} - {copyIconWithTooltip(distanceToSelection, "Copy the distance")} + {distanceToSelection[0]} ({distanceToSelection[1]}) to + this {clickedNodeId != null ? "Node" : "Position"} + {copyIconWithTooltip(distanceToSelection[0], "Copy the distance")}
) : null} diff --git a/frontend/javascripts/oxalis/view/right-menu/tree_hierarchy_view.js b/frontend/javascripts/oxalis/view/right-menu/tree_hierarchy_view.js index f39b661af0f..48ee1171949 100644 --- a/frontend/javascripts/oxalis/view/right-menu/tree_hierarchy_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/tree_hierarchy_view.js @@ -39,7 +39,7 @@ import { setTreeGroupAction, } from "oxalis/model/actions/skeletontracing_actions"; import messages from "messages"; -import { formatNumberToLength } from "libs/format_utils"; +import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; import api from "oxalis/api/internal_api"; const CHECKBOX_STYLE = { verticalAlign: "middle" }; @@ -290,9 +290,14 @@ class TreeHierarchyView extends React.PureComponent { }; handleMeasureSkeletonLength = (treeId: number, treeName: string) => { - const length = api.tracing.measureTreeLength(treeId); + const [lengthInNm, lengthInVx] = api.tracing.measureTreeLength(treeId); + notification.open({ - message: messages["tracing.tree_length_notification"](treeName, formatNumberToLength(length)), + message: messages["tracing.tree_length_notification"]( + treeName, + formatNumberToLength(lengthInNm), + formatLengthAsVx(lengthInVx), + ), icon: , }); }; diff --git a/frontend/javascripts/oxalis/view/right-menu/trees_tab_view.js b/frontend/javascripts/oxalis/view/right-menu/trees_tab_view.js index bdcfe64d4e1..fc3625286b1 100644 --- a/frontend/javascripts/oxalis/view/right-menu/trees_tab_view.js +++ b/frontend/javascripts/oxalis/view/right-menu/trees_tab_view.js @@ -86,7 +86,7 @@ import * as Utils from "libs/utils"; import api from "oxalis/api/internal_api"; import messages from "messages"; import JSZip from "jszip"; -import { formatNumberToLength } from "libs/format_utils"; +import { formatNumberToLength, formatLengthAsVx } from "libs/format_utils"; import DeleteGroupModalView from "./delete_group_modal_view"; import AdvancedSearchPopover from "./advanced_search_popover"; @@ -640,10 +640,12 @@ class TreesTabView extends React.PureComponent { ) : null; handleMeasureAllSkeletonsLength = () => { - const totalLength = api.tracing.measureAllTrees(); + const [totalLengthNm, totalLengthVx] = api.tracing.measureAllTrees(); notification.open({ - message: `The total length of all skeletons is ${formatNumberToLength(totalLength)}.`, + message: `The total length of all skeletons is ${formatNumberToLength( + totalLengthNm, + )} (${formatLengthAsVx(totalLengthVx)}).`, icon: , }); };