diff --git a/src/datasource/graphene/backend.ts b/src/datasource/graphene/backend.ts index fb28b3a1f..e1ce48290 100644 --- a/src/datasource/graphene/backend.ts +++ b/src/datasource/graphene/backend.ts @@ -380,6 +380,12 @@ export class ChunkedGraphLayer extends withSegmentationLayerBackendState( this.registerDisposer( this.chunkManager.recomputeChunkPriorities.add(() => { + if (this.wasDisposed) { + console.log( + "we were disposed!", + this.segmentEquivalences.disjointSets, + ); + } this.updateChunkPriorities(); this.debouncedUpdateDisplayState(); }), diff --git a/src/datasource/graphene/frontend.ts b/src/datasource/graphene/frontend.ts index 2a5a5a1e6..7d3745a4a 100644 --- a/src/datasource/graphene/frontend.ts +++ b/src/datasource/graphene/frontend.ts @@ -714,52 +714,52 @@ export class GrapheneDataSource extends PrecomputedDataSource { ); const state = options.state; state; - return options.chunkManager.memoize.getUncounted( - { type: "graphene:get", providerUrl, parameters, state }, - async (): Promise => { - const { url, credentialsProvider } = parseSpecialUrl( - providerUrl, - options.credentialsManager, - ); - let metadata: any; - try { - metadata = await getJsonMetadata( - options.chunkManager, - credentialsProvider, - url, - ); - } catch (e) { - if (isNotFoundError(e)) { - if (parameters["type"] === "mesh") { - console.log("does this happen?"); - } - } - throw e; + // return options.chunkManager.memoize.getUncounted( + // { type: "graphene:get", providerUrl, parameters, state }, + // async (): Promise => { + const { url, credentialsProvider } = parseSpecialUrl( + providerUrl, + options.credentialsManager, + ); + let metadata: any; + try { + metadata = await getJsonMetadata( + options.chunkManager, + credentialsProvider, + url, + ); + } catch (e) { + if (isNotFoundError(e)) { + if (parameters["type"] === "mesh") { + console.log("does this happen?"); } - verifyObject(metadata); - const redirect = verifyOptionalObjectProperty( + } + throw e; + } + verifyObject(metadata); + const redirect = verifyOptionalObjectProperty( + metadata, + "redirect", + verifyString, + ); + if (redirect !== undefined) { + throw new RedirectError(redirect); + } + const t = verifyOptionalObjectProperty(metadata, "@type", verifyString); + switch (t) { + case "neuroglancer_multiscale_volume": + case undefined: + return await getVolumeDataSource( + options, + credentialsProvider, + url, metadata, - "redirect", - verifyString, ); - if (redirect !== undefined) { - throw new RedirectError(redirect); - } - const t = verifyOptionalObjectProperty(metadata, "@type", verifyString); - switch (t) { - case "neuroglancer_multiscale_volume": - case undefined: - return await getVolumeDataSource( - options, - credentialsProvider, - url, - metadata, - ); - default: - throw new Error(`Invalid type: ${JSON.stringify(t)}`); - } - }, - ); + default: + throw new Error(`Invalid type: ${JSON.stringify(t)}`); + } + // }, + // ); } } diff --git a/src/segmentation_display_state/frontend.ts b/src/segmentation_display_state/frontend.ts index 5f22ea0e3..b43973545 100644 --- a/src/segmentation_display_state/frontend.ts +++ b/src/segmentation_display_state/frontend.ts @@ -1092,7 +1092,7 @@ export class SegmentationLayerSharedObject extends Base { } disposed(): void { - console.log("SegmentationLayerSharedObject disposed"); + // console.log("SegmentationLayerSharedObject disposed"); super.disposed(); } diff --git a/src/shared_disjoint_sets.ts b/src/shared_disjoint_sets.ts index a330594b8..91b01478f 100644 --- a/src/shared_disjoint_sets.ts +++ b/src/shared_disjoint_sets.ts @@ -193,6 +193,7 @@ const tempB = new Uint64(); registerRPC(ADD_METHOD_ID, function (x) { const obj = this.get(x.id); + if (!obj) return; tempA.low = x.al; tempA.high = x.ah; tempB.low = x.bl; diff --git a/src/ui/default_viewer_setup.ts b/src/ui/default_viewer_setup.ts index 3757f2bb5..d5a9ea9ee 100644 --- a/src/ui/default_viewer_setup.ts +++ b/src/ui/default_viewer_setup.ts @@ -16,6 +16,7 @@ import type { UserLayer, UserLayerConstructor } from "#src/layer/index.js"; import { layerTypes } from "#src/layer/index.js"; +import { Overlay } from "#src/overlay.js"; import { StatusMessage } from "#src/status.js"; import { bindDefaultCopyHandler, @@ -23,6 +24,7 @@ import { } from "#src/ui/default_clipboard_handling.js"; import { setDefaultInputEventBindings } from "#src/ui/default_input_event_bindings.js"; import { makeDefaultViewer } from "#src/ui/default_viewer.js"; +import "#src/ui/history.css"; import { bindTitle } from "#src/ui/title.js"; import type { Tool } from "#src/ui/tool.js"; import { restoreTool } from "#src/ui/tool.js"; @@ -205,21 +207,27 @@ export function setupDefaultViewer() { let inReplay = false; - (window as any).ngReplay = (sessionId: string, skipForward = true) => { + const ngReplay = (sessionId: string, skipForward = true) => { if (inReplay) return; hashBinding.recording = false; inReplay = true; - let startTime = Date.now(); let historyIndex = 0; - let nextState = localStorage.getItem(`${sessionId}_${historyIndex}`); + let nextState = localStorage.getItem( + `history_state_${sessionId}_${historyIndex}`, + ); if (!nextState) { console.log(`no history for session ${sessionId}`); return; } let nextTime = parseInt( - localStorage.getItem(`${sessionId}_${historyIndex}_time`)!, + localStorage.getItem(`history_time_${sessionId}_${historyIndex}`)!, ); + let startTime = Date.now() - nextTime; + + console.log("starting replay"); + StatusMessage.showTemporaryMessage("starting replay"); + const loop = () => { let elapsedTime = Date.now() - startTime; if (skipForward && nextTime - elapsedTime > 5000) { @@ -233,14 +241,17 @@ export function setupDefaultViewer() { viewer.state.restoreState(JSON.parse(nextState!)); // get next states historyIndex++; - nextState = localStorage.getItem(`${sessionId}_${historyIndex}`); + nextState = localStorage.getItem( + `history_state_${sessionId}_${historyIndex}`, + ); if (!nextState) { inReplay = false; console.log("done with replay"); + StatusMessage.showTemporaryMessage("replay complete!"); return; } nextTime = parseInt( - localStorage.getItem(`${sessionId}_${historyIndex}_time`)!, + localStorage.getItem(`history_time_${sessionId}_${historyIndex}`)!, ); } requestAnimationFrame(loop); @@ -248,5 +259,116 @@ export function setupDefaultViewer() { requestAnimationFrame(loop); }; + let currentOverlay: Overlay | undefined = undefined; + + document.addEventListener("keypress", (evt) => { + console.log(evt.key, evt.ctrlKey); + if (evt.key === "r" && evt.ctrlKey) { + if (!currentOverlay || currentOverlay.wasDisposed) { + currentOverlay = showHistoryViewer(); + } else { + currentOverlay.dispose(); + } + } + }); + + const deleteHistory = (key: string) => { + const relevantKeys = Object.keys(localStorage).filter((x) => + x.includes(key), + ); + for (const item of relevantKeys) { + localStorage.removeItem(item); + } + }; + + const showHistoryViewer = () => { + const historyViewer = document.createElement("div"); + historyViewer.classList.add("historyViewer"); + + const title = document.createElement("div"); + title.classList.add("title"); + title.textContent = "History"; + historyViewer.append(title); + + const buttonClose = document.createElement("button"); + buttonClose.classList.add("close-button"); + buttonClose.textContent = "Close"; + historyViewer.appendChild(buttonClose); + buttonClose.addEventListener("click", () => overlay.dispose()); + + const overlay = new Overlay(); + overlay.content.append(historyViewer); + + const times = Object.keys(localStorage).filter((x) => + x.startsWith("history_time"), + ); + + const historyCounts: { [key: string]: number } = {}; + const historyTimes: { [key: string]: number } = {}; + + for (const timeKey of times) { + const [_a, _b, key, idx] = timeKey.split("_"); + _a; + _b; + const time = localStorage.getItem(timeKey); + if (!time) continue; + historyCounts[key] = Math.max(historyCounts[key] || 0, parseInt(idx) + 1); + historyTimes[key] = Math.max(historyTimes[key] || 0, parseInt(time) + 1); + } + + const listEl = document.createElement("div"); + listEl.classList.add("historyList"); + + let idx = 1; + + for (const key of Object.keys(historyCounts)) { + const count = historyCounts[key]; + const lastUpdateTime = historyTimes[key]; + + if (key === hashBinding.sessionId) continue; + + if (count < 10) { + deleteHistory(key); + continue; + } + + const itemEl = document.createElement("div"); + itemEl.textContent = `#${idx}`; + listEl.appendChild(itemEl); + + const entriesEl = document.createElement("div"); + entriesEl.textContent = `states: ${count}`; + listEl.appendChild(entriesEl); + + const lastUpdatedEl = document.createElement("div"); + lastUpdatedEl.textContent = `Last update: ${new Date(lastUpdateTime)}`; + listEl.appendChild(lastUpdatedEl); + + const buttonReplay = document.createElement("button"); + buttonReplay.classList.add("replay-button"); + buttonReplay.textContent = "Replay"; + buttonReplay.addEventListener("click", () => { + ngReplay(key); + overlay.dispose(); + }); + listEl.appendChild(buttonReplay); + + const deleteHistoryBtn = document.createElement("button"); + deleteHistoryBtn.classList.add("delete-button"); + deleteHistoryBtn.textContent = "Delete"; + deleteHistoryBtn.addEventListener("click", () => { + deleteHistory(key); + overlay.dispose(); + showHistoryViewer(); + }); + listEl.appendChild(deleteHistoryBtn); + + idx++; + } + + historyViewer.appendChild(listEl); + + return overlay; + }; return viewer; } diff --git a/src/ui/history.css b/src/ui/history.css new file mode 100644 index 000000000..bc9667c98 --- /dev/null +++ b/src/ui/history.css @@ -0,0 +1,14 @@ +.historyViewer { + display: grid; + min-width: 300px; + min-height: 200px; +} + +.historyList { + height: min-content; + margin-top: 20px; + display: grid; + white-space: nowrap; + grid-template-columns: auto auto auto min-content min-content; + column-gap: 10px; +} \ No newline at end of file diff --git a/src/ui/url_hash_binding.ts b/src/ui/url_hash_binding.ts index 204a31e02..d81a7a593 100644 --- a/src/ui/url_hash_binding.ts +++ b/src/ui/url_hash_binding.ts @@ -125,11 +125,15 @@ export class UrlHashBinding extends RefCounted { if (this.recording) { console.log("recording", this.sessionId, this.historyIndex); localStorage.setItem( - `${this.sessionId}_${this.historyIndex}_time`, + `history_elapsed_${this.sessionId}_${this.historyIndex}`, (Date.now() - this.startTime).toString(), ); localStorage.setItem( - `${this.sessionId}_${this.historyIndex}`, + `history_time_${this.sessionId}_${this.historyIndex}`, + Date.now().toString(), + ); + localStorage.setItem( + `history_state_${this.sessionId}_${this.historyIndex}`, jsonString, ); this.historyIndex++; diff --git a/src/viewer.ts b/src/viewer.ts index c0a44f9d8..9acd53cd9 100644 --- a/src/viewer.ts +++ b/src/viewer.ts @@ -332,7 +332,7 @@ class TrackableViewerState extends CompoundTrackable { } restoreState(obj: any) { - console.log("restore state!"); + // console.log("restore state!"); const { viewer } = this; super.restoreState(obj); // Handle legacy properties