Skip to content
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
192 changes: 154 additions & 38 deletions src/neuroglancer/datasource/graphene/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import { makeIcon } from 'neuroglancer/widget/icon';
import { EventActionMap } from 'neuroglancer/util/event_action_map';
import { packColor } from 'neuroglancer/util/color';
import { Uint64Set } from 'neuroglancer/uint64_set';
import { addLayerControlToOptionsTab, LayerControlFactory, registerLayerControl } from 'neuroglancer/widget/layer_control';
import { DateTimeInputWidget } from 'neuroglancer/widget/datetime_input';

function vec4FromVec3(vec: vec3, alpha = 0) {
const res = vec4.clone([...vec]);
Expand Down Expand Up @@ -506,31 +508,42 @@ const SEGMENT_ID_JSON_KEY = "segmentId";
const ROOT_ID_JSON_KEY = "rootId";
const POSITION_JSON_KEY = "position";

const TIMESTAMP_JSON_KEY = "timestamp";

class GrapheneState implements Trackable {
changed = new NullarySignal();

public multicutState = new MulticutState();
public timestamp: TrackableValue<number> = new TrackableValue(0, x => x);

constructor() {
this.multicutState.changed.add(() => {
this.changed.dispatch();
});
this.timestamp.changed.add(() => {
this.changed.dispatch();
});
}

reset() {
this.multicutState.reset();
this.timestamp.reset();
}

toJSON() {
return {
[MULTICUT_JSON_KEY]: this.multicutState.toJSON(),
[TIMESTAMP_JSON_KEY]: this.timestamp.toJSON(),
}
}

restoreState(x: any) {
verifyOptionalObjectProperty(x, MULTICUT_JSON_KEY, value => {
this.multicutState.restoreState(value);
});
verifyOptionalObjectProperty(x, TIMESTAMP_JSON_KEY, value => {
this.timestamp.restoreState(value);
});
}
}

Expand Down Expand Up @@ -647,6 +660,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 +701,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 +740,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 +786,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 @@ -793,11 +832,18 @@ export const GRAPH_SERVER_NOT_SPECIFIED = Symbol('Graph Server Not Specified.');
class GrapheneGraphServerInterface {
constructor(private url: string, private credentialsProvider: SpecialProtocolCredentialsProvider) {}

async getRoot(segment: Uint64, timestamp = '') {
const timestampEpoch = (new Date(timestamp)).valueOf() / 1000;
async getTimestampLimit() {
const response = await cancellableFetchSpecialOk(
this.credentialsProvider, `${this.url}/oldest_timestamp`, {}, responseJson);
const isoString = verifyObjectProperty(response, 'iso', verifyString);
return new Date(isoString).valueOf();
}

async getRoot(segment: Uint64, timestamp = 0) {
const timestampEpoch = timestamp / 1000;

const url = `${this.url}/node/${String(segment)}/root?int64_as_str=1${
Number.isNaN(timestampEpoch) ? '' : `&timestamp=${timestampEpoch}`}`
timestamp > 0 ? `&timestamp=${timestampEpoch}` : ''}`

const promise = cancellableFetchSpecialOk(
this.credentialsProvider,
Expand Down Expand Up @@ -864,13 +910,17 @@ class GrapheneGraphServerInterface {
class GrapheneGraphSource extends SegmentationGraphSource {
private connections = new Set<GraphConnection>();
public graphServer: GrapheneGraphServerInterface;
public timestampLimit: TrackableValue<number> = new TrackableValue(0, x => x);

constructor(public info: GrapheneMultiscaleVolumeInfo,
credentialsProvider: SpecialProtocolCredentialsProvider,
private chunkSource: GrapheneMultiscaleVolumeChunkSource,
public state: GrapheneState) {
super();
this.graphServer = new GrapheneGraphServerInterface(info.app!.segmentationUrl, credentialsProvider);
this.graphServer.getTimestampLimit().then(limit => {
this.timestampLimit.value = limit;
});
}

connect(layer: SegmentationUserLayer): Owned<SegmentationGraphSourceConnection> {
Expand All @@ -890,12 +940,13 @@ class GrapheneGraphSource extends SegmentationGraphSource {
}

getRoot(segment: Uint64) {
return this.graphServer.getRoot(segment);
return this.graphServer.getRoot(segment, this.state.timestamp.value);
}

tabContents(layer: SegmentationUserLayer, context: DependentViewContext, tab: SegmentationGraphSourceTab) {
const parent = document.createElement('div');
parent.style.display = 'contents';
parent.appendChild(addLayerControlToOptionsTab(tab, layer, tab.visibility, timeControl));
const toolbox = document.createElement('div');
toolbox.className = 'neuroglancer-segmentation-toolbox';
toolbox.appendChild(makeToolButton(context, layer, {
Expand Down Expand Up @@ -1261,7 +1312,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, graphConnection.graph);
if (!currentSegmentSelection) return;
const {rootId, segmentId} = currentSegmentSelection;
const {focusSegment, segments} = multicutState;
Expand Down Expand Up @@ -1295,7 +1346,11 @@ class MulticutSegmentsTool extends Tool<SegmentationUserLayer> {
}
}

const maybeGetSelection = (tool: Tool<SegmentationUserLayer>, visibleSegments: Uint64Set): SegmentSelection|undefined => {
const maybeGetSelection = (tool: Tool<SegmentationUserLayer>, visibleSegments: Uint64Set, graph: GrapheneGraphSource): SegmentSelection|undefined => {
if (graph.state.timestamp.value !== 0) {
StatusMessage.showTemporaryMessage('Editing can not be performed with a segmentation at an older state.');
return;
}
const {layer, mouseState} = tool;
const {segmentSelectionState: {value, baseValue}} = layer.displayState;
if (!baseValue || !value) return;
Expand Down Expand Up @@ -1384,22 +1439,23 @@ class MergeSegmentsTool extends Tool<SegmentationUserLayer> {
(async () => {
const lastSegmentSelection = this.lastAnchorSelection.value;
if (!lastSegmentSelection) { // first selection
const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments);
const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments, graph);
if (selection) {
this.lastAnchorSelection.value = selection;
setSink(selection.rootId);
}
} else if (!activeSubmission) {
const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments);
const selection = maybeGetSelection(this, segmentationGroupState.visibleSegments, graph);
if (selection) {
activeSubmission = true;
setSource(selection.rootId);
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 +1481,63 @@ registerLayerTool(SegmentationUserLayer, GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID, lay
registerLayerTool(SegmentationUserLayer, GRAPHENE_MERGE_SEGMENTS_TOOL_ID, layer => {
return new MergeSegmentsTool(layer, true);
});

const GRAPHENE_TIME_JSON_KEY = 'grapheneTime';

const timeControl = {
label: 'Time',
title: 'View segmentation at earlier point of time',
toolJson: GRAPHENE_TIME_JSON_KEY,
hideStatus: true,
foo: true,
...timeLayerControl(),
};

registerLayerControl(SegmentationUserLayer, timeControl);

function timeLayerControl(): LayerControlFactory<SegmentationUserLayer> {
return {
makeControl: (layer, context) => {
const segmentationGroupState = layer.displayState.segmentationGroupState.value;
const {graph: {value: graph}} = segmentationGroupState;
const timestamp = graph instanceof GrapheneGraphSource ? graph.state.timestamp : new TrackableValue<number>(0, x => x);
const timestampLimit = graph instanceof GrapheneGraphSource ? graph.timestampLimit : new TrackableValue<number>(0, x => x);
const controlElement = document.createElement('div');
controlElement.classList.add('neuroglancer-time-control');
const intermediateTimestamp = new TrackableValue<number>(timestamp.value, x => x);
intermediateTimestamp.changed.add(() => {
if (intermediateTimestamp.value === timestamp.value) {
return;
}
if (confirm('Changing timestamp will clear all selected segments')) {
if (graph instanceof GrapheneGraphSource) {
graph.state.multicutState.reset();
}
timestamp.value = intermediateTimestamp.value;
} else {
intermediateTimestamp.value = timestamp.value;
}
});
const widget =
context.registerDisposer(new DateTimeInputWidget(intermediateTimestamp, new Date(timestampLimit.value), new Date()));
timestampLimit.changed.add(() => {
widget.setMin(new Date(timestampLimit.value));
});
timestamp.changed.add(() => {
segmentationGroupState.selectedSegments.clear();
segmentationGroupState.temporaryVisibleSegments.clear(); // TODO, do we care about this? maybe we need to force close/clear multicut
if (timestamp.value !== intermediateTimestamp.value) {
intermediateTimestamp.value = timestamp.value;
}
});
controlElement.appendChild(widget.element);
return {controlElement, control: widget};
},
activateTool: (_activation, control: DateTimeInputWidget) => {
if ('showPicker' in HTMLInputElement.prototype) {
// esbuild complaining, maybe update
(control.element as any).showPicker();
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import {defaultCredentialsManager} from 'neuroglancer/credentials_provider/default_manager';
import {MiddleAuthCredentialsProvider, MiddleAuthAppCredentialsProvider} from 'neuroglancer/datasource/middleauth/credentials_provider';
import { CredentialsManager } from 'src/neuroglancer/credentials_provider';
import { CredentialsManager } from 'neuroglancer/credentials_provider';

defaultCredentialsManager.register('middleauth', serverUrl => new MiddleAuthCredentialsProvider(serverUrl));

Expand Down
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