From 9cda42614fbb5f9370f9878765e0f5f61f0e5e4f Mon Sep 17 00:00:00 2001 From: Chris Jordan Date: Wed, 22 Nov 2023 18:00:12 -0500 Subject: [PATCH] trying to improve find path ux --- config/custom-keybinds.json | 2 +- .../datasource/graphene/frontend.ts | 56 ++++++++++++++----- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/config/custom-keybinds.json b/config/custom-keybinds.json index b58b437e39..788adebc00 100644 --- a/config/custom-keybinds.json +++ b/config/custom-keybinds.json @@ -9,7 +9,7 @@ "tool": "grapheneMulticutSegments", "provider": "graphene" }, - "keyp": { + "keyf": { "layer": "segmentation", "tool": "grapheneFindPath", "provider": "graphene" diff --git a/src/neuroglancer/datasource/graphene/frontend.ts b/src/neuroglancer/datasource/graphene/frontend.ts index c14eae8cd0..093deb3c59 100644 --- a/src/neuroglancer/datasource/graphene/frontend.ts +++ b/src/neuroglancer/datasource/graphene/frontend.ts @@ -65,6 +65,7 @@ import {Uint64} from 'neuroglancer/util/uint64'; import {makeDeleteButton} from 'neuroglancer/widget/delete_button'; import {DependentViewContext} from 'neuroglancer/widget/dependent_view_widget'; import {makeIcon} from 'neuroglancer/widget/icon'; +import {CancellationToken, CancellationTokenSource} from 'src/neuroglancer/util/cancellation'; function vec4FromVec3(vec: vec3, alpha = 0) { const res = vec4.clone([...vec]); @@ -1070,7 +1071,11 @@ class GraphConnection extends SegmentationGraphSourceConnection { findPathState.target.value = undefined; } }); + let findPathCancellation: CancellationTokenSource|undefined = undefined; const findPathChanged = () => { + if (findPathCancellation) { + findPathCancellation.cancel(); + } const {path, source, target} = findPathState; const annotationSource = findPathGroup.source; if (source.value && !source.value.annotationReference) { @@ -1093,12 +1098,18 @@ class GraphConnection extends SegmentationGraphSourceConnection { }; this.registerDisposer(findPathState.changed.add(findPathChanged)); this.registerDisposer(findPathState.triggerPathUpdate.add(() => { + if (findPathCancellation) { + findPathCancellation.cancel(); + } const loadedSubsource = getGraphLoadedSubsource(this.layer)!; const annotationToNanometers = loadedSubsource.loadedDataSource.transform.inputSpace.value.scales.map(x => x / 1e-9); - this.submitFindPath(findPathState.precisionMode.value, annotationToNanometers) + findPathCancellation = new CancellationTokenSource(); + this.submitFindPath( + findPathState.precisionMode.value, annotationToNanometers, findPathCancellation) .then(success => { success; + findPathCancellation = undefined; }); })); findPathChanged(); // initial state @@ -1380,13 +1391,14 @@ class GraphConnection extends SegmentationGraphSourceConnection { merges.changed.dispatch(); } - async submitFindPath(precisionMode: boolean, annotationToNanometers: Float64Array): - Promise { + async submitFindPath( + precisionMode: boolean, annotationToNanometers: Float64Array, + cancellationToken?: CancellationToken): Promise { const {state: {findPathState}} = this; const {source, target} = findPathState; if (!source.value || !target.value) return false; const centroids = await this.graph.findPath( - source.value, target.value, precisionMode, annotationToNanometers); + source.value, target.value, precisionMode, annotationToNanometers, cancellationToken); StatusMessage.showTemporaryMessage('Path found!', 5000); findPathState.centroids.value = centroids; return true; @@ -1431,6 +1443,10 @@ async function withErrorMessageHTTP(promise: Promise, options: { status.setVisible(true); throw new Error(`[${e.response.status}] ${errorPrefix}${msg}`); } + if (e instanceof DOMException && e.name === 'AbortError') { + dispose(); + StatusMessage.showTemporaryMessage('Request was aborted.'); + } throw e; } } @@ -1546,7 +1562,9 @@ class GrapheneGraphServerInterface { return res; } - async findPath(first: SegmentSelection, second: SegmentSelection, precisionMode: boolean) { + async findPath( + first: SegmentSelection, second: SegmentSelection, precisionMode: boolean, + cancellationToken?: CancellationToken) { const {url} = this; if (url === '') { return Promise.reject(GRAPH_SERVER_NOT_SPECIFIED); @@ -1560,7 +1578,7 @@ class GrapheneGraphServerInterface { [String(second.rootId), ...second.position], ]), }, - responseIdentity); + responseIdentity, cancellationToken); const response = await withErrorMessageHTTP(promise, { initialMessage: `Finding path between ${first.segmentId} and ${second.segmentId}`, @@ -1636,13 +1654,15 @@ class GrapheneGraphSource extends SegmentationGraphSource { async findPath( first: SegmentSelection, second: SegmentSelection, precisionMode: boolean, - annotationToNanometers: Float64Array): Promise { + annotationToNanometers: Float64Array, + cancellationToken?: CancellationToken): Promise { const {l2CacheUrl, table} = this.info.app; const l2CacheAvailable = precisionMode && await this.isL2CacheUrlAvailable(); // don't check if available if we don't need it let {centroids, l2_path} = await this.graphServer.findPath( selectionInNanometers(first, annotationToNanometers), - selectionInNanometers(second, annotationToNanometers), precisionMode && !l2CacheAvailable); + selectionInNanometers(second, annotationToNanometers), precisionMode && !l2CacheAvailable, + cancellationToken); if (precisionMode && l2CacheAvailable && l2_path) { const repCoordinatesUrl = `${l2CacheUrl}/table/${table}/attributes`; try { @@ -1653,7 +1673,7 @@ class GrapheneGraphSource extends SegmentationGraphSource { l2_ids: l2_path, }), }, - responseJson); + responseJson, cancellationToken); // many reasons why an l2 id might not have info // l2 cache has a process that takes time for new ids (even hours) @@ -1875,7 +1895,7 @@ const addSelection = }; const ref = source.add(annotation); selection.annotationReference = ref; - } + }; const synchronizeAnnotationSource = (source: WatchableSet, state: AnnotationLayerState) => { @@ -2329,6 +2349,7 @@ class MergeSegmentsTool extends LayerTool { const FIND_PATH_INPUT_EVENT_MAP = EventActionMap.fromObject({ 'at:shift?+enter': {action: 'submit'}, + 'escape': {action: 'clearPath'}, 'at:shift?+control+mousedown0': {action: 'add-point'}, }); @@ -2347,6 +2368,11 @@ class FindPathTool extends LayerTool { const submitAction = () => { findPathState.triggerPathUpdate.dispatch(); }; + const clearPath = () => { + findPathState.source.reset(); + findPathState.target.reset(); + findPathState.centroids.reset(); + }; body.appendChild(makeIcon({ text: 'Submit', title: 'Submit Find Path', @@ -2357,11 +2383,7 @@ class FindPathTool extends LayerTool { body.appendChild(makeIcon({ text: 'Clear', title: 'Clear Find Path', - onClick: () => { - findPathState.source.reset(); - findPathState.target.reset(); - findPathState.centroids.reset(); - } + onClick: clearPath, })); const checkbox = activation.registerDisposer(new TrackableBooleanCheckbox(precisionMode)); const label = document.createElement('label'); @@ -2410,6 +2432,9 @@ class FindPathTool extends LayerTool { activation.bindAction('add-point', event => { event.stopPropagation(); (async () => { + if (source.value && target.value) { + clearPath(); + } if (!source.value) { // first selection const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments); if (selection) { @@ -2423,6 +2448,7 @@ class FindPathTool extends LayerTool { } })(); }); + activation.bindAction('clearPath', clearPath); } toJSON() {