Skip to content
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

feat(datasource/graphene) added timestamp tool to graphene, timestamp property to SegmentationUserLayer #613

Open
wants to merge 1 commit into
base: master
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
298 changes: 247 additions & 51 deletions src/datasource/graphene/frontend.ts

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions src/layer/segmentation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
makeCachedLazyDerivedWatchableValue,
registerNestedSync,
TrackableValue,
WatchableSet,
WatchableValue,
} from "#src/trackable_value.js";
import { UserLayerWithAnnotationsMixin } from "#src/ui/annotations.js";
Expand Down Expand Up @@ -124,6 +125,7 @@ import {
verifyObjectAsMap,
verifyOptionalObjectProperty,
verifyString,
verifyStringArray,
} from "#src/util/json.js";
import { Signal } from "#src/util/signal.js";
import { Uint64 } from "#src/util/uint64.js";
Expand Down Expand Up @@ -161,6 +163,9 @@ export class SegmentationUserLayerGroupState
}
}
});

this.timestamp.changed.add(specificationChanged.dispatch);
this.timestampOwner.changed.add(specificationChanged.dispatch);
}

restoreState(specification: unknown) {
Expand Down Expand Up @@ -202,6 +207,24 @@ export class SegmentationUserLayerGroupState
json_keys.SEGMENT_QUERY_JSON_KEY,
(value) => this.segmentQuery.restoreState(value),
);
verifyOptionalObjectProperty(
specification,
json_keys.TIMESTAMP_OWNER_JSON_KEY,
(value) => {
const owners = verifyStringArray(value);
this.timestampOwner.clear();
for (const owner of owners) {
this.timestampOwner.add(owner);
}
},
);
verifyOptionalObjectProperty(
specification,
json_keys.TIMESTAMP_JSON_KEY,
(value) => {
this.timestamp.restoreState(value);
},
);
}

toJSON() {
Expand All @@ -223,6 +246,10 @@ export class SegmentationUserLayerGroupState
x[json_keys.EQUIVALENCES_JSON_KEY] = segmentEquivalences.toJSON();
}
x[json_keys.SEGMENT_QUERY_JSON_KEY] = this.segmentQuery.toJSON();
x[json_keys.TIMESTAMP_JSON_KEY] = this.timestamp.toJSON();
if (this.timestampOwner.size > 0) {
x[json_keys.TIMESTAMP_OWNER_JSON_KEY] = [...this.timestampOwner];
}
return x;
}

Expand All @@ -232,9 +259,13 @@ export class SegmentationUserLayerGroupState
this.selectedSegments.assignFrom(other.selectedSegments);
this.visibleSegments.assignFrom(other.visibleSegments);
this.segmentEquivalences.assignFrom(other.segmentEquivalences);
this.timestamp.value = other.timestamp.value;
this.timestampOwner.values = new Set(other.timestampOwner); // TODO this won't trigger changed properly
}

localGraph = new LocalSegmentationGraphSource();
timestamp = new TrackableValue<number | undefined>(undefined, (x) => x);
timestampOwner = new WatchableSet<string>();
visibleSegments = this.registerDisposer(
Uint64Set.makeWithCounterpart(this.layer.manager.rpc),
);
Expand Down Expand Up @@ -277,6 +308,14 @@ export class SegmentationUserLayerGroupState
useTemporarySegmentEquivalences = this.layer.registerDisposer(
SharedWatchableValue.make(this.layer.manager.rpc, false),
);

canSetTimestamp(owner?: string) {
const otherOwners = [...this.timestampOwner].filter((x) => x !== owner);
if (otherOwners.length) {
return false;
}
return true;
}
}

export class SegmentationUserLayerColorGroupState
Expand Down
2 changes: 2 additions & 0 deletions src/layer/segmentation/json_keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ export const SEGMENT_DEFAULT_COLOR_JSON_KEY = "segmentDefaultColor";
export const ANCHOR_SEGMENT_JSON_KEY = "anchorSegment";
export const SKELETON_RENDERING_SHADER_CONTROL_TOOL_ID =
"skeletonShaderControl";
export const TIMESTAMP_JSON_KEY = "timestamp";
export const TIMESTAMP_OWNER_JSON_KEY = "timestampOwner";
4 changes: 3 additions & 1 deletion src/segmentation_display_state/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ import type {
VisibleSegmentsState,
} from "#src/segmentation_display_state/base.js";
import {
VISIBLE_SEGMENTS_STATE_PROPERTIES,
onTemporaryVisibleSegmentsStateChanged,
onVisibleSegmentsStateChanged,
VISIBLE_SEGMENTS_STATE_PROPERTIES,
} from "#src/segmentation_display_state/base.js";
import type { SharedDisjointUint64Sets } from "#src/shared_disjoint_sets.js";
import type { SharedWatchableValue } from "#src/shared_watchable_value.js";
import type { WatchableValue } from "#src/trackable_value.js";
import type { Uint64OrderedSet } from "#src/uint64_ordered_set.js";
import type { Uint64Set } from "#src/uint64_set.js";
import type { AnyConstructor } from "#src/util/mixin.js";
Expand All @@ -57,6 +58,7 @@ export const withSegmentationLayerBackendState = <
Base: TBase,
) =>
class SegmentationLayerState extends Base implements VisibleSegmentsState {
timestamp: WatchableValue<number | undefined>;
visibleSegments: Uint64Set;
selectedSegments: Uint64OrderedSet;
segmentEquivalences: SharedDisjointUint64Sets;
Expand Down
2 changes: 2 additions & 0 deletions src/segmentation_display_state/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import { VisibleSegmentEquivalencePolicy } from "#src/segmentation_graph/segment_id.js";
import type { SharedDisjointUint64Sets } from "#src/shared_disjoint_sets.js";
import type { SharedWatchableValue } from "#src/shared_watchable_value.js";
import type { WatchableValue } from "#src/trackable_value.js";
import type { Uint64OrderedSet } from "#src/uint64_ordered_set.js";
import type { Uint64Set } from "#src/uint64_set.js";
import type { RefCounted } from "#src/util/disposable.js";
import type { Uint64 } from "#src/util/uint64.js";

export interface VisibleSegmentsState {
timestamp: WatchableValue<number | undefined>;
visibleSegments: Uint64Set;
selectedSegments: Uint64OrderedSet;
segmentEquivalences: SharedDisjointUint64Sets;
Expand Down
60 changes: 60 additions & 0 deletions src/ui/layer_bar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ import "#src/ui/layer_bar.css";
import svg_plus from "ikonate/icons/plus.svg?raw";
import type { ManagedUserLayer } from "#src/layer/index.js";
import { addNewLayer, deleteLayer, makeLayer } from "#src/layer/index.js";
import { SegmentationUserLayer } from "#src/layer/segmentation/index.js";
import type { LayerGroupViewer } from "#src/layer_group_viewer.js";
import { NavigationLinkType } from "#src/navigation_state.js";
import { StatusMessage } from "#src/status.js";
import type { WatchableValueInterface } from "#src/trackable_value.js";
import type { DropLayers } from "#src/ui/layer_drag_and_drop.js";
import {
Expand Down Expand Up @@ -67,6 +69,64 @@ class LayerWidget extends RefCounted {
element.appendChild(prefetchProgress);
labelElement.className = "neuroglancer-layer-item-label";
labelElement.appendChild(labelElementText);

this.registerDisposer(
layer.readyStateChanged.add(() => {
if (layer.isReady() && layer.layer instanceof SegmentationUserLayer) {
const timeButton = makeIcon({
text: "🕘",
});
element.appendChild(timeButton);
const { segmentationGroupState } = layer.layer.displayState;
const { timestamp, timestampOwner } = segmentationGroupState.value;
const updateTimeButton = () => {
const otherOwners = [...timestampOwner].filter(
(x) => x !== layer.name,
);
if (timestamp.value) {
if (otherOwners.length) {
timeButton.title = `${new Date(timestamp.value)}.\nBound to layer(s): ${otherOwners.join(", ")}`;
} else {
timeButton.title = `${new Date(timestamp.value)}.\nClick to return to current form.`;
}
timeButton.classList.toggle("locked", otherOwners.length > 0);
}
timeButton.style.display =
timestamp.value === undefined ? "none" : "inherit";
};
updateTimeButton();
timestampOwner.changed.add(() => {
updateTimeButton();
const layerNames = layer.manager.layerManager.managedLayers.map(
(x) => x.name,
);
const invalidOwners = [...timestampOwner].filter(
(x) => !layerNames.includes(x),
);
for (const owner of invalidOwners) {
timestampOwner.delete(owner);
}
});
timestamp.changed.add(() => {
updateTimeButton();
});
timeButton.addEventListener("click", (evt) => {
evt.stopPropagation();
if (segmentationGroupState.value.canSetTimestamp(layer.name)) {
timestamp.reset();
timestampOwner.clear(); // TODO(chrisj) should we reset timestamp owner or should that be layer controlled?
} else {
const otherOwners = [...timestampOwner].filter(
(x) => x !== layer.name,
);
StatusMessage.showTemporaryMessage(
`Segmentation time bound to layer(s): ${otherOwners.join(", ")}`,
);
}
});
}
}),
);
visibleProgress.className = "neuroglancer-layer-item-visible-progress";
prefetchProgress.className = "neuroglancer-layer-item-prefetch-progress";
layerNumberElement.className = "neuroglancer-layer-item-number";
Expand Down
10 changes: 10 additions & 0 deletions src/util/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,16 @@ export function verifyIntegerArray(a: unknown) {
return <number[]>a;
}

export function verifyFloatArray(a: unknown) {
if (!Array.isArray(a)) {
throw new Error(`Expected array, received: ${JSON.stringify(a)}.`);
}
for (const x of a) {
verifyFloat(x);
}
return <number[]>a;
}

export function verifyBoolean(x: any) {
if (typeof x !== "boolean") {
throw new Error(`Expected boolean, received: ${JSON.stringify(x)}`);
Expand Down
82 changes: 82 additions & 0 deletions src/widget/datetime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @license
* Copyright 2020 Google Inc.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import type { WatchableValue } from "#src/trackable_value.js";
import { RefCounted } from "#src/util/disposable.js";
import { removeFromParent } from "#src/util/dom.js";

function toDateTimeLocalString(date: Date) {
return new Date(date.getTime() - date.getTimezoneOffset() * 60000)
.toISOString()
.slice(0, -8);
}

export class DateTimeInputWidget extends RefCounted {
element = document.createElement("input");
constructor(
public model: WatchableValue<number | undefined>,
minDate?: Date,
maxDate?: Date,
) {
super();
this.registerDisposer(model.changed.add(() => this.updateView()));
const { element } = this;
element.type = "datetime-local";
if (minDate) {
this.setMin(minDate);
}
if (maxDate) {
this.setMax(maxDate);
}
this.registerEventListener(element, "change", () => this.updateModel());
this.updateView();
}

setMin(date: Date) {
const { element } = this;
element.min = toDateTimeLocalString(date);
}

setMax(date: Date) {
const { element } = this;
element.max = toDateTimeLocalString(date);
}

disposed() {
removeFromParent(this.element);
}

private updateView() {
if (this.model.value !== undefined) {
this.element.value = toDateTimeLocalString(new Date(this.model.value));
} else {
this.element.value = "";
}
}

private updateModel() {
try {
if (this.element.value) {
this.model.value = new Date(this.element.value).valueOf();
} else {
this.model.value = undefined;
}
} catch {
// Ignore invalid input.
}
this.updateView();
}
}
4 changes: 4 additions & 0 deletions src/widget/icon.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
background-color: #db4437;
}

.neuroglancer-icon.locked {
background-color: yellow;
}

.neuroglancer-icon-hover:not(:hover) svg:last-child {
display: none;
}
Expand Down