Skip to content

feat(datasource/graphene) added refresh mesh tool #606

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

Open
wants to merge 2 commits into
base: cj_starred_segments
Choose a base branch
from
Open
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
39 changes: 35 additions & 4 deletions src/neuroglancer/datasource/graphene/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
import {WithParameters} from 'neuroglancer/chunk_manager/backend';
import {WithSharedCredentialsProviderCounterpart} from 'neuroglancer/credentials_provider/shared_counterpart';
import {assignMeshFragmentData, FragmentChunk, ManifestChunk, MeshSource} from 'neuroglancer/mesh/backend';
import {getGrapheneFragmentKey, responseIdentity} from 'neuroglancer/datasource/graphene/base';
import {getGrapheneFragmentKey, GRAPHENE_REFRESH_MESH_RPC_ID, responseIdentity} from 'neuroglancer/datasource/graphene/base';
import {CancellationToken} from 'neuroglancer/util/cancellation';
import {isNotFoundError, responseArrayBuffer, responseJson} from 'neuroglancer/util/http_request';
import {HttpError, isNotFoundError, responseArrayBuffer, responseJson} from 'neuroglancer/util/http_request';
import {cancellableFetchSpecialOk, SpecialProtocolCredentials, SpecialProtocolCredentialsProvider} from 'neuroglancer/util/special_protocol_request';
import {Uint64} from 'neuroglancer/util/uint64';
import {registerSharedObject} from 'neuroglancer/worker_rpc';
Expand Down Expand Up @@ -68,6 +68,14 @@ function getVerifiedFragmentPromise(
cancellationToken);
}

function reject404(url: string): Promise<ArrayBuffer> {
return new Promise((_f, r) => {
setTimeout(() => {
r(new HttpError(url, 404, "404", undefined));
}, 1000);
})
}

function getFragmentDownloadPromise(
credentialsProvider: SpecialProtocolCredentialsProvider,
chunk: FragmentChunk,
Expand All @@ -77,6 +85,9 @@ function getFragmentDownloadPromise(
if (parameters.sharding){
fragmentDownloadPromise = getVerifiedFragmentPromise(credentialsProvider, chunk, parameters, cancellationToken);
} else {
if (Math.random() < 0.5) {
return reject404(`${parameters.fragmentUrl}/${chunk.fragmentId}`);
}
fragmentDownloadPromise = cancellableFetchSpecialOk(
credentialsProvider,
`${parameters.fragmentUrl}/${chunk.fragmentId}`, {}, responseArrayBuffer,
Expand All @@ -94,6 +105,17 @@ async function decodeDracoFragmentChunk(

@registerSharedObject() export class GrapheneMeshSource extends
(WithParameters(WithSharedCredentialsProviderCounterpart<SpecialProtocolCredentials>()(MeshSource), MeshSourceParameters)) {
chunksNotFound = new Map<string, FragmentChunk[]>();

redownload(segment: Uint64) {
const segmentString = segment.toJSON();
const segmentChunks = this.chunksNotFound.get(segmentString) || [];
this.chunksNotFound.delete(segmentString);
for (let chunk of segmentChunks) {
this.chunkManager.queueManager.updateChunkState(chunk, ChunkState.QUEUED);
}
}

async download(chunk: ManifestChunk, cancellationToken: CancellationToken) {
const {parameters} = this;
if (isBaseSegmentId(chunk.objectId, parameters.nBitsForLayerId)) {
Expand All @@ -115,9 +137,13 @@ async function decodeDracoFragmentChunk(
await decodeDracoFragmentChunk(chunk, response);
} catch (e) {
if (isNotFoundError(e)) {
chunk.source!.removeChunk(chunk);
const segmentString = chunk.manifestChunk?.objectId?.toJSON();
if (segmentString) {
this.chunksNotFound.set(segmentString, this.chunksNotFound.get(segmentString) || []);
this.chunksNotFound.get(segmentString)!.push(chunk);
}
}
Promise.reject(e);
throw e;
}
}

Expand Down Expand Up @@ -421,3 +447,8 @@ registerRPC(CHUNKED_GRAPH_RENDER_LAYER_UPDATE_SOURCES_RPC_ID, function(x) {
attachment.state!.displayDimensionRenderInfo = x.displayDimensionRenderInfo;
layer.chunkManager.scheduleUpdateChunkPriorities();
});

registerRPC(GRAPHENE_REFRESH_MESH_RPC_ID, function(x) {
let obj = <GrapheneMeshSource>this.get(x.rpcId);
obj.redownload(Uint64.parseString(x.segment));
});
1 change: 1 addition & 0 deletions src/neuroglancer/datasource/graphene/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {ChunkLayoutOptions, makeSliceViewChunkSpecification, SliceViewChunkSourc
import {DataType} from 'neuroglancer/sliceview/base';

export const PYCG_APP_VERSION = 1;
export const GRAPHENE_REFRESH_MESH_RPC_ID = 'GrapheneMeshSource:RefreshMesh';

export enum VolumeChunkEncoding {
RAW,
Expand Down
148 changes: 115 additions & 33 deletions src/neuroglancer/datasource/graphene/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ import {ChunkManager, WithParameters} from 'neuroglancer/chunk_manager/frontend'
import {makeIdentityTransform} from 'neuroglancer/coordinate_transform';
import {WithCredentialsProvider} from 'neuroglancer/credentials_provider/chunk_source_frontend';
import {DataSource, DataSubsourceEntry, GetDataSourceOptions, RedirectError} from 'neuroglancer/datasource';
import {MeshSource} from 'neuroglancer/mesh/frontend';
import {MeshLayer, MeshSource, MultiscaleMeshLayer} from 'neuroglancer/mesh/frontend';
import {Owned} from 'neuroglancer/util/disposable';
import {mat4, vec3, vec4} from 'neuroglancer/util/geom';
import {HttpError, isNotFoundError, responseJson} from 'neuroglancer/util/http_request';
import {parseArray, parseFixedLengthArray, verifyEnumString, verifyFiniteFloat, verifyFinitePositiveFloat, verifyInt, verifyObject, verifyObjectProperty, verifyOptionalObjectProperty, verifyOptionalString, verifyPositiveInt, verifyString, verifyNonnegativeInt, verify3dVec} from 'neuroglancer/util/json';
import {getObjectId} from 'neuroglancer/util/object_id';
import {cancellableFetchSpecialOk, parseSpecialUrl, SpecialProtocolCredentials, SpecialProtocolCredentialsProvider} from 'neuroglancer/util/special_protocol_request';
import {Uint64} from 'neuroglancer/util/uint64';
import {getGrapheneFragmentKey, isBaseSegmentId, responseIdentity} from 'neuroglancer/datasource/graphene/base';
import {getGrapheneFragmentKey, GRAPHENE_REFRESH_MESH_RPC_ID, isBaseSegmentId, responseIdentity} from 'neuroglancer/datasource/graphene/base';
import {ChunkedGraphSourceParameters, MeshSourceParameters, MultiscaleMeshMetadata, PYCG_APP_VERSION} from 'neuroglancer/datasource/graphene/base';
import {DataEncoding, ShardingHashFunction, ShardingParameters} from 'neuroglancer/datasource/precomputed/base';
import {StatusMessage} from 'neuroglancer/status';
Expand Down Expand Up @@ -647,6 +647,13 @@ class GraphConnection extends SegmentationGraphSourceConnection {
public state: GrapheneState) {
super(graph, layer.displayState.segmentationGroupState.value);
const segmentsState = layer.displayState.segmentationGroupState.value;
segmentsState.selectedSegments.changed.add((segmentIds: Uint64[]|Uint64|null, add: boolean) => {
if (segmentIds !== null) {
segmentIds = Array<Uint64>().concat(segmentIds);
}
this.selectedSegmentsChanged(segmentIds, add);
});

segmentsState.visibleSegments.changed.add((segmentIds: Uint64[]|Uint64|null, add: boolean) => {
if (segmentIds !== null) {
segmentIds = Array<Uint64>().concat(segmentIds);
Expand Down Expand Up @@ -681,44 +688,35 @@ class GraphConnection extends SegmentationGraphSourceConnection {

private visibleSegmentsChanged(segments: Uint64[]|null, added: boolean) {
const {segmentsState} = this;

const {focusSegment: {value: focusSegment}} = this.graph.state.multicutState;
if (focusSegment && !segmentsState.visibleSegments.has(focusSegment)) {
if (segmentsState.selectedSegments.has(focusSegment)) {
StatusMessage.showTemporaryMessage(`Can't hide active multicut segment.`, 3000);
} else {
StatusMessage.showTemporaryMessage(`Can't deselect active multicut segment.`, 3000);
}
segmentsState.selectedSegments.add(focusSegment);
segmentsState.visibleSegments.add(focusSegment);
if (segments) {
segments = segments.filter(segment => !Uint64.equal(segment, focusSegment));
}
}
if (segments === null) {
const leafSegmentCount = this.segmentsState.visibleSegments.size;
const leafSegmentCount = this.segmentsState.selectedSegments.size;
this.segmentsState.segmentEquivalences.clear();
StatusMessage.showTemporaryMessage(`Deselected all ${leafSegmentCount} segments.`, 3000);
StatusMessage.showTemporaryMessage(`Hid all ${leafSegmentCount} segments.`, 3000);
return;
}

for (const segmentId of segments) {
const isBaseSegment = isBaseSegmentId(segmentId, this.graph.info.graph.nBitsForLayerId);

const segmentConst = segmentId.clone();

if (added) {
if (isBaseSegment) {
this.graph.getRoot(segmentConst).then(rootId => {
segmentsState.visibleSegments.delete(segmentConst);
segmentsState.visibleSegments.add(rootId);
});
}
} else if (!isBaseSegment) {
const {focusSegment: {value: focusSegment}} = this.graph.state.multicutState;
if (focusSegment && Uint64.equal(segmentId, focusSegment)) {
segmentsState.visibleSegments.add(segmentId);
StatusMessage.showTemporaryMessage(`Can't deselect active multicut segment.`, 3000);
return;
}

if (!added) {
const segmentCount = [...segmentsState.segmentEquivalences.setElements(segmentId)].length; // Approximation

segmentsState.segmentEquivalences.deleteSet(segmentId);

if (this.lastDeselectionMessage && this.lastDeselectionMessageExists) {
this.lastDeselectionMessage.dispose();
this.lastDeselectionMessageExists = false;
}
this.lastDeselectionMessage =
StatusMessage.showMessage(`Deselected ${segmentCount} segments.`);
StatusMessage.showMessage(`Hid ${segmentCount} segments.`);
this.lastDeselectionMessageExists = true;
setTimeout(() => {
if (this.lastDeselectionMessageExists) {
Expand All @@ -729,6 +727,30 @@ class GraphConnection extends SegmentationGraphSourceConnection {
}
}
}

private selectedSegmentsChanged(segments: Uint64[]|null, added: boolean) {
const {segmentsState} = this;
if (segments === null) {
const leafSegmentCount = this.segmentsState.selectedSegments.size;
StatusMessage.showTemporaryMessage(`Deselected all ${leafSegmentCount} segments.`, 3000);
return;
}
for (const segmentId of segments) {
const isBaseSegment = isBaseSegmentId(segmentId, this.graph.info.graph.nBitsForLayerId);
const segmentConst = segmentId.clone();
if (added) {
if (isBaseSegment) {
this.graph.getRoot(segmentConst).then(rootId => {
if (segmentsState.visibleSegments.has(segmentConst)) {
segmentsState.visibleSegments.add(rootId);
}
segmentsState.selectedSegments.delete(segmentConst);
segmentsState.selectedSegments.add(rootId);
});
}
}
}
}

computeSplit(include: Uint64, exclude: Uint64): ComputedSplit|undefined {
include;
Expand All @@ -751,7 +773,11 @@ class GraphConnection extends SegmentationGraphSourceConnection {
const focusSegment = multicutState.focusSegment.value!;
multicutState.reset(); // need to clear the focus segment before deleting the multicut segment
const {segmentsState} = this;
segmentsState.visibleSegments.delete(focusSegment);
segmentsState.selectedSegments.delete(focusSegment);
for (const segment of [...sinks, ...sources]) {
segmentsState.selectedSegments.delete(segment.rootId);
}
segmentsState.selectedSegments.add(splitRoots);
segmentsState.visibleSegments.add(splitRoots);
return true;
}
Expand Down Expand Up @@ -908,6 +934,11 @@ class GrapheneGraphSource extends SegmentationGraphSource {
label: 'Merge',
title: 'Merge segments'
}));
toolbox.appendChild(makeToolButton(context, layer, {
toolJson: GRAPHENE_REFRESH_MESH_TOOL_ID,
label: 'Refresh Mesh',
title: 'Refresh Meshes'
}));
parent.appendChild(toolbox);
parent.appendChild(
context.registerDisposer(new MulticutAnnotationLayerView(layer, layer.annotationDisplayState))
Expand Down Expand Up @@ -1048,6 +1079,7 @@ class SliceViewPanelChunkedGraphLayer extends SliceViewPanelRenderLayer {

const GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID = 'grapheneMulticutSegments';
const GRAPHENE_MERGE_SEGMENTS_TOOL_ID = 'grapheneMergeSegments';
const GRAPHENE_REFRESH_MESH_TOOL_ID = 'grapheneRefreshMesh';

class MulticutAnnotationLayerView extends AnnotationLayerView {
private _annotationStates: MergedAnnotationStates;
Expand Down Expand Up @@ -1261,7 +1293,7 @@ class MulticutSegmentsTool extends Tool<SegmentationUserLayer> {

activation.bindAction('set-anchor', event => {
event.stopPropagation();
const currentSegmentSelection = maybeGetSelection(this, segmentationGroupState.visibleSegments);
const currentSegmentSelection = maybeGetSelection(this, segmentationGroupState.visibleSegments); // or visible segments?
if (!currentSegmentSelection) return;
const {rootId, segmentId} = currentSegmentSelection;
const {focusSegment, segments} = multicutState;
Expand Down Expand Up @@ -1397,9 +1429,10 @@ class MergeSegmentsTool extends Tool<SegmentationUserLayer> {
const loadedSubsource = getGraphLoadedSubsource(this.layer)!;
const annotationToNanometers = loadedSubsource.loadedDataSource.transform.inputSpace.value.scales.map(x => x / 1e-9);
const mergedRoot = await graph.graphServer.mergeSegments(lastSegmentSelection, selection, annotationToNanometers);
const {visibleSegments} = segmentationGroupState;
visibleSegments.delete(lastSegmentSelection.rootId);
visibleSegments.delete(selection.rootId);
const {selectedSegments, visibleSegments} = segmentationGroupState;
selectedSegments.delete(lastSegmentSelection.rootId);
selectedSegments.delete(selection.rootId);
selectedSegments.add(mergedRoot);
visibleSegments.add(mergedRoot);
this.lastAnchorSelection.value = undefined;
activation.cancel();
Expand All @@ -1425,3 +1458,52 @@ registerLayerTool(SegmentationUserLayer, GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID, lay
registerLayerTool(SegmentationUserLayer, GRAPHENE_MERGE_SEGMENTS_TOOL_ID, layer => {
return new MergeSegmentsTool(layer, true);
});

registerLayerTool(SegmentationUserLayer, GRAPHENE_REFRESH_MESH_TOOL_ID, layer => {
return new RefreshMeshTool(layer);
});

const REFRESH_MESH_INPUT_EVENT_MAP = EventActionMap.fromObject({
'at:shift?+mousedown0': {action: 'refresh-mesh'},
});

class RefreshMeshTool extends Tool<SegmentationUserLayer> {
activate(activation: ToolActivation<this>) {
const {body, header} = makeToolActivationStatusMessageWithHeader(activation);
header.textContent = 'Refresh mesh';
body.classList.add('neuroglancer-merge-segments-status');

activation.bindInputEventMap(REFRESH_MESH_INPUT_EVENT_MAP); // has to be after makeToolActivationStatusMessageWithHeader


const someMeshLayer = (layer: SegmentationUserLayer) => {
for (let x of layer.renderLayers) {
if (x instanceof MeshLayer || x instanceof MultiscaleMeshLayer) {
return x;
}
}
return undefined;
};

activation.bindAction('refresh-mesh', event => {
event.stopPropagation();
const {segmentSelectionState, segmentationGroupState} = this.layer.displayState;
if (!segmentSelectionState.hasSelectedSegment) return;
const segment = segmentSelectionState.selectedSegment;
const {visibleSegments} = segmentationGroupState.value;
if (!visibleSegments.has(segment)) return;
const meshLayer = someMeshLayer(this.layer);
if (!meshLayer) return;
const meshSource = meshLayer.source;
meshSource.rpc!.invoke(GRAPHENE_REFRESH_MESH_RPC_ID, {'rpcId': meshSource.rpcId!, 'segment': segment.toString()});
});
}

toJSON() {
return GRAPHENE_REFRESH_MESH_TOOL_ID;
}

get description() {
return `refresh mesh`;
}
}
5 changes: 4 additions & 1 deletion src/neuroglancer/datasource/nggraph/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,12 @@ class GraphConnection extends SegmentationGraphSourceConnection {
try {
this.ignoreVisibleSegmentsChanged = true;
if (this.segmentsState.visibleSegments.has(oldId)) {
this.segmentsState.visibleSegments.delete(oldId);
this.segmentsState.visibleSegments.add(newId);
}
if (this.segmentsState.selectedSegments.has(oldId)) {
this.segmentsState.selectedSegments.delete(oldId);
this.segmentsState.selectedSegments.add(newId);
}
if (this.segmentsState.temporaryVisibleSegments.has(oldId)) {
this.segmentsState.temporaryVisibleSegments.delete(oldId);
this.segmentsState.temporaryVisibleSegments.add(newId);
Expand Down
1 change: 1 addition & 0 deletions src/neuroglancer/segmentation_display_state/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const withSegmentationLayerBackendState =
<TBase extends AnyConstructor<ChunkRequester>>(Base: TBase) =>
class SegmentationLayerState extends Base implements VisibleSegmentsState {
visibleSegments: Uint64Set;
selectedSegments: Uint64Set;
segmentEquivalences: SharedDisjointUint64Sets;
temporaryVisibleSegments: Uint64Set;
temporarySegmentEquivalences: SharedDisjointUint64Sets;
Expand Down
1 change: 1 addition & 0 deletions src/neuroglancer/segmentation_display_state/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {VisibleSegmentEquivalencePolicy} from 'neuroglancer/segmentation_graph/s

export interface VisibleSegmentsState {
visibleSegments: Uint64Set;
selectedSegments: Uint64Set;
segmentEquivalences: SharedDisjointUint64Sets;

// Specifies a temporary/alternative set of segments/equivalences to use for display purposes,
Expand Down
Loading