Skip to content

Commit

Permalink
feat(datasource/graphene): add time tool for graphene segmentation
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisj committed May 8, 2024
1 parent 08868e5 commit 4b854cf
Showing 1 changed file with 143 additions and 9 deletions.
152 changes: 143 additions & 9 deletions src/datasource/graphene/frontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,15 @@ import {
} from "#src/util/special_protocol_request.js";
import type { Trackable } from "#src/util/trackable.js";
import { Uint64 } from "#src/util/uint64.js";
import { DateTimeInputWidget } from "#src/widget/datetime.js";
import { makeDeleteButton } from "#src/widget/delete_button.js";
import type { DependentViewContext } from "#src/widget/dependent_view_widget.js";
import { makeIcon } from "#src/widget/icon.js";
import type { LayerControlFactory } from "#src/widget/layer_control.js";
import {
addLayerControlToOptionsTab,
registerLayerControl,
} from "#src/widget/layer_control.js";

function vec4FromVec3(vec: vec3, alpha = 0) {
const res = vec4.clone([...vec]);
Expand Down Expand Up @@ -816,6 +822,7 @@ function restoreSegmentSelection(obj: any): SegmentSelection {

const ID_JSON_KEY = "id";
const ERROR_JSON_KEY = "error";
const TIMESTAMP_JSON_KEY = "timestamp";
const MULTICUT_JSON_KEY = "multicut";
const FOCUS_SEGMENT_JSON_KEY = "focusSegment";
const SINKS_JSON_KEY = "sinks";
Expand All @@ -834,10 +841,15 @@ const LOCKED_JSON_KEY = "locked";
class GrapheneState implements Trackable {
changed = new NullarySignal();

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

constructor() {
this.timestamp.changed.add(() => {
this.multicutState.reset();
this.changed.dispatch();
});
this.multicutState.changed.add(() => {
this.changed.dispatch();
});
Expand All @@ -847,18 +859,23 @@ class GrapheneState implements Trackable {
}

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

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

restoreState(x: any) {
verifyOptionalObjectProperty(x, TIMESTAMP_JSON_KEY, (value) => {
this.timestamp.restoreState(value);
});
verifyOptionalObjectProperty(x, MULTICUT_JSON_KEY, (value) => {
this.multicutState.restoreState(value);
});
Expand Down Expand Up @@ -1076,7 +1093,7 @@ class MulticutState extends RefCounted implements Trackable {
}
}

class GraphConnection extends SegmentationGraphSourceConnection {
export class GraphConnection extends SegmentationGraphSourceConnection {
public annotationLayerStates: AnnotationLayerState[] = [];
public mergeAnnotationState: AnnotationLayerState;

Expand Down Expand Up @@ -1110,6 +1127,14 @@ class GraphConnection extends SegmentationGraphSourceConnection {
annotationLayerStates,
state: { multicutState },
} = this;

this.registerDisposer(
state.timestamp.changed.add(() => {
segmentsState.selectedSegments.clear();
segmentsState.temporaryVisibleSegments.clear();
}),
);

const loadedSubsource = getGraphLoadedSubsource(layer)!;
const redGroup = makeColoredAnnotationState(
layer,
Expand Down Expand Up @@ -1599,11 +1624,22 @@ class GrapheneGraphServerInterface {
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(
Expand Down Expand Up @@ -1737,6 +1773,7 @@ class GrapheneGraphServerInterface {
class GrapheneGraphSource extends SegmentationGraphSource {
private connections = new Set<GraphConnection>();
public graphServer: GrapheneGraphServerInterface;
public timestampLimit = new TrackableValue<number>(0, (x) => x);

constructor(
public info: GrapheneMultiscaleVolumeInfo,
Expand All @@ -1749,6 +1786,9 @@ class GrapheneGraphSource extends SegmentationGraphSource {
info.app!.segmentationUrl,
credentialsProvider,
);
this.graphServer.getTimestampLimit().then((limit) => {
this.timestampLimit.value = limit;
});
}

connect(
Expand Down Expand Up @@ -1777,7 +1817,7 @@ class GrapheneGraphSource extends SegmentationGraphSource {
}

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

tabContents(
Expand All @@ -1789,6 +1829,9 @@ class GrapheneGraphSource extends SegmentationGraphSource {
parent.style.display = "contents";
const toolbox = document.createElement("div");
toolbox.className = "neuroglancer-segmentation-toolbox";
parent.appendChild(
addLayerControlToOptionsTab(tab, layer, tab.visibility, timeControl),
);
toolbox.appendChild(
makeToolButton(context, layer.toolBinder, {
toolJson: GRAPHENE_MULTICUT_SEGMENTS_TOOL_ID,
Expand Down Expand Up @@ -2110,6 +2153,80 @@ const getPoint = (
return undefined;
};

const GRAPHENE_TIME_JSON_KEY = "grapheneTime";

const timeControl = {
label: "Time",
title: "View segmentation at earlier point of time",
toolJson: GRAPHENE_TIME_JSON_KEY,
...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 (graph instanceof GrapheneGraphSource) {
const hasSelectedSegments =
segmentationGroupState.selectedSegments.size +
segmentationGroupState.temporaryVisibleSegments.size >
0;
if (
!hasSelectedSegments ||
confirm("Changing graphene time will clear all selected segments.")
) {
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(() => {
if (timestamp.value !== intermediateTimestamp.value) {
intermediateTimestamp.value = timestamp.value;
}
});
controlElement.appendChild(widget.element);
return { controlElement, control: widget };
},
activateTool: (_activation) => {},
};
}

const MULTICUT_SEGMENTS_INPUT_EVENT_MAP = EventActionMap.fromObject({
"at:shift?+control+mousedown0": { action: "set-anchor" },
"at:shift?+keyg": { action: "swap-group" },
Expand All @@ -2129,11 +2246,21 @@ class MulticutSegmentsTool extends LayerTool<SegmentationUserLayer> {
if (!graphConnection || !(graphConnection instanceof GraphConnection))
return;
const {
state: { multicutState },
state: { multicutState, timestamp },
segmentsState,
} = graphConnection;
if (timestamp.value !== 0) {
StatusMessage.showTemporaryMessage(
"Editing can not be performed with a segmentation at an older state.",
);
return;
}
activation.registerDisposer(
timestamp.changed.add(() => {
activation.cancel();
}),
);
if (multicutState === undefined) return;

const { body, header } =
makeToolActivationStatusMessageWithHeader(activation);
header.textContent = "Multicut segments";
Expand Down Expand Up @@ -2444,11 +2571,18 @@ class MergeSegmentsTool extends LayerTool<SegmentationUserLayer> {
graphConnection: { value: graphConnection },
tool,
} = this.layer;
if (!graphConnection || !(graphConnection instanceof GraphConnection))
if (!graphConnection || !(graphConnection instanceof GraphConnection)) {
return;
}
const {
state: { mergeState },
state: { mergeState, timestamp },
} = graphConnection;
if (timestamp.value !== 0) {
StatusMessage.showTemporaryMessage(
"Editing can not be performed with a segmentation at an older state.",
);
return;
}
if (mergeState === undefined) return;
const { merges, autoSubmit } = mergeState;

Expand Down

0 comments on commit 4b854cf

Please sign in to comment.