diff --git a/.eslintrc.cjs b/.eslintrc.cjs index b5ef05b..bdc7d31 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,5 +1,5 @@ /* - Copyright (c) 2021-2024 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. + Copyright (c) 2021-2026 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/LICENSE b/LICENSE index b8ec035..12d3f32 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021-2024 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. +Copyright (c) 2021-2026 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/env.d.ts b/env.d.ts index 3bc14fe..f25dfdc 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2021-2024 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. + Copyright (c) 2021-2026 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis and Computer Technologies Laboratory ITMO University team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/index.html b/index.html index 279d696..248b5b1 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@
+
+ + +
+
= ref(""); +let openProgressTimer: number | undefined; +const openProgressVisible = ref(false); +const openProgressStage = ref("starting"); +const openProgressPct = ref(0); +let openProgressInFlight = false; + +function startOpenProgress() { + if (openProgressTimer !== undefined) { + return; + } + openProgressVisible.value = true; + openProgressTimer = window.setInterval(() => { + if (openProgressInFlight) return; + openProgressInFlight = true; + networkManager.requestManager + .getOpenProgress() + .then((p) => { + openProgressStage.value = p.stage ?? "working"; + openProgressPct.value = Math.max(0, Math.min(100, Math.round((p.progress ?? 0) * 100))); + }) + .catch(() => undefined) + .finally(() => { + openProgressInFlight = false; + }); + }, 500); +} + +function stopOpenProgress() { + if (openProgressTimer !== undefined) { + window.clearInterval(openProgressTimer); + openProgressTimer = undefined; + } + openProgressVisible.value = false; +} + +function closeOpenProgress() { + openProgressVisible.value = false; +} + +function safeColorTranslator(value: unknown, fallback: string): ColorTranslator { + if (typeof value !== "string" || value.length > 128) { + return new ColorTranslator(fallback, { legacyCSS: true }); + } + try { + return new ColorTranslator(value, { legacyCSS: true }); + } catch { + return new ColorTranslator(fallback, { legacyCSS: true }); + } +} function resetState() { mapManager.value?.dispose(); @@ -80,29 +192,27 @@ function resetState() { } function onClosed() { - resetState(); + networkManager.requestManager + .closeFile() + .catch(() => undefined) + .finally(() => resetState()); } -function displayNewMap() { - const fname = filename.value; - const ffname = fastaFilename.value; - if (!fname) { - const message = - "Cannot open non-specified files: filename=" + - fname + - " fastaFilename=" + - ffname; - toast.error(message); - throw new Error(message); - } +function onAttached() { networkManager.requestManager - .openFile(fname, ffname) - .then((openFileResponse) => { + .attachSession() + .then(({ filename: attachedName, fastaFilename: attachedFastaName, response }) => { + if (!attachedName) { + toast.error("No active session to attach"); + return; + } mapManager.value?.dispose(); + filename.value = attachedName; + fastaFilename.value = attachedFastaName ?? ""; const newManager = new ContactMapManager({ - response: openFileResponse, - filename: fname, - fastaFilename: ffname ?? "", + response, + filename: attachedName, + fastaFilename: attachedFastaName ?? "", tileSize: tileSize.value, contigBorderColor: contigBorderColor.value, mapTargetSelector: "hic-contact-map", @@ -112,14 +222,369 @@ function displayNewMap() { mapManager.value = newManager; networkManager.mapManager = mapManager.value; newManager.initializeMap(); + toast.success("Attached to session " + attachedName); + }) + .catch((err) => { + const message = + err?.response?.data?.error ?? + err?.message ?? + "Failed to attach session"; + toast.error(message); + }); +} + +async function openFileWithOptions( + fname: string, + ffname: string | undefined +): Promise { + startOpenProgress(); + const openFileResponse = await networkManager.requestManager.openFile( + fname, + ffname + ); + mapManager.value?.dispose(); + const newManager = new ContactMapManager({ + response: openFileResponse, + filename: fname, + fastaFilename: ffname ?? "", + tileSize: tileSize.value, + contigBorderColor: contigBorderColor.value, + mapTargetSelector: "hic-contact-map", + networkManager: networkManager, + minimapTarget: miniMapTarget, + }); + mapManager.value = newManager; + networkManager.mapManager = mapManager.value; + newManager.initializeMap(); + applyDefaultVisualizationPreset(); + if (ffname && ffname.trim() !== "") { + try { + const linkResponse = await networkManager.requestManager.linkFASTA( + new LinkFASTARequest({ fastaFilename: ffname, allowMismatch: true }) + ); + linkResponse.warnings.forEach((warning) => + toast(warning, { + style: { + "background-color": "lightyellow", + color: "black", + }, + }) + ); + } catch (error) { + console.error(error); + toast.error("Failed to link FASTA file " + ffname); + } + } + stopOpenProgress(); +} + +function displayNewMap() { + const fname = filename.value; + const ffname = fastaFilename.value; + if (!fname) { + const message = + "Cannot open non-specified files: filename=" + + fname + + " fastaFilename=" + + ffname; + toast.error(message); + throw new Error(message); + } + openFileWithOptions(fname, ffname) + .then(() => { toast.success("Opened file " + fname); }) .catch((a) => { console.log(a); toast.error(a); + stopOpenProgress(); }); } +function applyDefaultVisualizationPreset() { + const presets = + (defaultOptions as unknown as { + data?: { savedLocations?: unknown[]; savedVisualizationPresets?: unknown[] }; + }).data?.savedLocations ?? + (defaultOptions as unknown as { + data?: { savedVisualizationPresets?: unknown[] }; + }).data?.savedVisualizationPresets ?? + []; + if (!presets || presets.length === 0) { + return; + } + const first = presets[0] as Record; + const opt = (first["options"] as Record) ?? {}; + const signalThresholds = first["signalThresholds"] as + | { lowerSignalBound?: number; upperSignalBound?: number } + | undefined; + const trackStyles = first["trackStyles"] as Record | undefined; + const cmap = (opt["colormap"] as Record) ?? {}; + const startColor = (cmap["startColorRGBAString"] as string) ?? "rgba(0,255,0,0.0)"; + const endColor = (cmap["endColorRGBAString"] as string) ?? "rgba(0,96,0,1.0)"; + const minSignal = (cmap["minSignal"] as number) ?? 0; + const maxSignal = (cmap["maxSignal"] as number) ?? 1; + const preLogBase = (opt["preLogBase"] as number) ?? -1; + const postLogBase = (opt["postLogBase"] as number) ?? 10; + const applyCoolerWeights = (opt["applyCoolerWeights"] as boolean) ?? false; + const resolutionScaling = (opt["resolutionScaling"] as boolean) ?? false; + const resolutionLinearScaling = + (opt["resolutionLinearScaling"] as boolean) ?? false; + const cmapObj = new SimpleLinearGradient( + safeColorTranslator(startColor, "rgba(0,255,0,0.0)"), + safeColorTranslator(endColor, "rgba(0,96,0,1.0)"), + minSignal, + maxSignal + ); + let finalCmap = cmapObj; + if (signalThresholds && typeof signalThresholds.lowerSignalBound === "number") { + finalCmap = new SimpleLinearGradient( + cmapObj.startColorRGBA, + cmapObj.endColorRGBA, + signalThresholds.lowerSignalBound, + typeof signalThresholds.upperSignalBound === "number" + ? signalThresholds.upperSignalBound + : cmapObj.maxSignal + ); + } + visualizationOptionsStore.setVisualizationOptions( + new VisualizationOptions( + preLogBase, + postLogBase, + applyCoolerWeights, + resolutionScaling, + resolutionLinearScaling, + finalCmap + ) + ); + const bg = (first["backgroundColor"] as string) ?? "rgba(255,255,255,1)"; + stylesStore.setMapBackground( + safeColorTranslator(bg, "rgba(255,255,255,1)") + ); + if (trackStyles && mapManager.value) { + mapManager.value.getLayersManager().applyTrackStylePreset(trackStyles as never); + } + mapManager.value?.visualizationManager.sendVisualizationOptionsToServer().then(() => { + mapManager.value?.reloadTiles(); + }); +} + +function onAgpLoaded(filename: string): void { + lastAgpFilename.value = filename; + sessionStore.setLastAgpFilename(filename); +} + +function onFastaLinked(filename: string): void { + fastaFilename.value = filename; +} + +function serializeCurrentVisualizationOptions(): Record { + const options = visualizationOptionsStore.asVisualizationOptions(); + const cmap = options.colormap; + if (cmap instanceof SimpleLinearGradient) { + return { + preLogBase: options.preLogBase, + postLogBase: options.postLogBase, + applyCoolerWeights: options.applyCoolerWeights ?? false, + resolutionScaling: options.resolutionScaling ?? false, + resolutionLinearScaling: options.resolutionLinearScaling ?? false, + colormap: { + colormapType: cmap.colormapType, + startColorRGBAString: cmap.startColorRGBA.RGBA, + endColorRGBAString: cmap.endColorRGBA.RGBA, + minSignal: cmap.minSignal, + maxSignal: cmap.maxSignal, + }, + }; + } + return { + preLogBase: options.preLogBase, + postLogBase: options.postLogBase, + applyCoolerWeights: options.applyCoolerWeights ?? false, + resolutionScaling: options.resolutionScaling ?? false, + resolutionLinearScaling: options.resolutionLinearScaling ?? false, + colormap: { + colormapType: options.colormap?.colormapType ?? "Unknown", + }, + }; +} + +function onSaveSession(): void { + if (!mapManager.value) { + toast.error("No open map to save session"); + return; + } + const view = mapManager.value.getView(); + const session = { + version: 1, + filename: filename.value ?? "", + fastaFilename: fastaFilename.value ?? "", + agpFilename: lastAgpFilename.value ?? "", + visualizationOptions: serializeCurrentVisualizationOptions(), + backgroundColor: mapBackgroundColor.value?.RGBA ?? "rgba(255,255,255,1)", + trackStyles: mapManager.value.getLayersManager().getTrackStylePreset(), + savedLocations: sessionStore.savedLocations, + savedVisualizationPresets: sessionStore.savedVisualizationPresets, + view: { + center: view.getCenter(), + resolution: view.getResolution(), + rotation: view.getRotation(), + bpResolution: + mapManager.value.viewAndLayersManager.currentViewState + .resolutionDesciptor.bpResolution, + }, + }; + const blob = new Blob([JSON.stringify(session, null, 2)], { + type: "application/json", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "hict_session.json"; + a.click(); + URL.revokeObjectURL(url); +} + +async function resolveFilename( + label: string, + original: string, + list: string[] +): Promise { + if (!original) return ""; + if (list.includes(original)) return original; + const replacement = window.prompt( + `${label} file '${original}' not found. Enter an alternative filename or leave empty to cancel:` + ); + if (!replacement) return null; + if (!list.includes(replacement)) { + toast.error(`${label} file '${replacement}' not found`); + return null; + } + return replacement; +} + +async function onOpenSession(file: File): Promise { + try { + const text = await file.text(); + const session = JSON.parse(text) as Record; + const sessionFilename = (session["filename"] as string) ?? ""; + const sessionFasta = (session["fastaFilename"] as string) ?? ""; + const sessionAgp = (session["agpFilename"] as string) ?? ""; + + const fileList = await networkManager.requestManager.listFiles(); + const fastaList = await networkManager.requestManager.listFASTAFiles(); + const agpList = await networkManager.requestManager.listAGPFiles(); + + const resolvedFile = await resolveFilename( + "HiCT", + sessionFilename, + fileList + ); + if (resolvedFile === null) return; + + const resolvedFasta = await resolveFilename( + "FASTA", + sessionFasta, + fastaList + ); + if (resolvedFasta === null) return; + + const resolvedAgp = await resolveFilename("AGP", sessionAgp, agpList); + if (resolvedAgp === null) return; + + filename.value = resolvedFile ?? ""; + fastaFilename.value = resolvedFasta ?? ""; + await openFileWithOptions(filename.value, fastaFilename.value); + + if (resolvedAgp) { + await networkManager.requestManager.loadAGP( + new LoadAGPRequest({ agpFilename: resolvedAgp }) + ); + lastAgpFilename.value = resolvedAgp; + sessionStore.setLastAgpFilename(resolvedAgp); + } + + const visRaw = session["visualizationOptions"] as Record; + if (visRaw) { + const cmap = (visRaw["colormap"] as Record) ?? {}; + const startColor = + (cmap["startColorRGBAString"] as string) ?? "rgba(0,255,0,0.0)"; + const endColor = + (cmap["endColorRGBAString"] as string) ?? "rgba(0,96,0,1.0)"; + const minSignal = (cmap["minSignal"] as number) ?? 0; + const maxSignal = (cmap["maxSignal"] as number) ?? 1; + const preLogBase = (visRaw["preLogBase"] as number) ?? -1; + const postLogBase = (visRaw["postLogBase"] as number) ?? 10; + const applyCoolerWeights = + (visRaw["applyCoolerWeights"] as boolean) ?? false; + const resolutionScaling = + (visRaw["resolutionScaling"] as boolean) ?? false; + const resolutionLinearScaling = + (visRaw["resolutionLinearScaling"] as boolean) ?? false; + const cmapObj = new SimpleLinearGradient( + safeColorTranslator(startColor, "rgba(0,255,0,0.0)"), + safeColorTranslator(endColor, "rgba(0,96,0,1.0)"), + minSignal, + maxSignal + ); + visualizationOptionsStore.setVisualizationOptions( + new VisualizationOptions( + preLogBase, + postLogBase, + applyCoolerWeights, + resolutionScaling, + resolutionLinearScaling, + cmapObj + ) + ); + } + + const bg = (session["backgroundColor"] as string) ?? null; + if (bg) { + stylesStore.setMapBackground( + safeColorTranslator(bg, "rgba(255,255,255,1)") + ); + } + + const trackStyles = session["trackStyles"] as Record; + if (trackStyles && mapManager.value) { + mapManager.value + .getLayersManager() + .applyTrackStylePreset(trackStyles as never); + } + + const savedLocs = (session["savedLocations"] as unknown[]) ?? []; + sessionStore.setSavedLocations(savedLocs as SessionSavedLocation[]); + + const savedPresets = + (session["savedVisualizationPresets"] as unknown[]) ?? []; + sessionStore.setSavedVisualizationPresets( + savedPresets as SessionVisualizationPreset[] + ); + + const viewState = session["view"] as Record; + if (viewState && mapManager.value) { + const view = mapManager.value.getView(); + if (Array.isArray(viewState["center"])) { + view.setCenter(viewState["center"] as [number, number]); + } + if (typeof viewState["resolution"] === "number") { + view.setResolution(viewState["resolution"] as number); + } + if (typeof viewState["rotation"] === "number") { + view.setRotation(viewState["rotation"] as number); + } + mapManager.value.viewAndLayersManager.updateCurrentHiCViewState(); + } + + await mapManager.value?.visualizationManager.sendVisualizationOptionsToServer(); + mapManager.value?.reloadTiles(); + toast.success("Session restored"); + } catch (e) { + toast.error("Failed to open session"); + } +} + watch( () => tileSize.value, (newTileSize, oldTileSize) => { @@ -156,4 +621,11 @@ function onFileSelected(newFilename: string) { width: 100%; height: 100vh; } + +.open-progress-modal { + z-index: 1055; +} +.open-progress-backdrop { + z-index: 1050; +} diff --git a/src/app/ui/components/ComponentCommon.ts b/src/app/ui/components/ComponentCommon.ts index 679d2eb..bf9c1d3 100644 --- a/src/app/ui/components/ComponentCommon.ts +++ b/src/app/ui/components/ComponentCommon.ts @@ -1,5 +1,5 @@ /* - Copyright (c) 2021-2024 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis, Zakhar Lobanov, Nikita Zheleznov and Computer Technologies Laboratory ITMO University team. + Copyright (c) 2021-2026 Aleksandr Serdiukov, Anton Zamyatin, Aleksandr Sinitsyn, Vitalii Dravgelis, Zakhar Lobanov, Nikita Zheleznov and Computer Technologies Laboratory ITMO University team. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/app/ui/components/notifications/NotificationCenterModal.vue b/src/app/ui/components/notifications/NotificationCenterModal.vue new file mode 100644 index 0000000..678d3dc --- /dev/null +++ b/src/app/ui/components/notifications/NotificationCenterModal.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/src/app/ui/components/sidebar/ColorPickerRectangle.vue b/src/app/ui/components/sidebar/ColorPickerRectangle.vue index 72323c5..3405413 100644 --- a/src/app/ui/components/sidebar/ColorPickerRectangle.vue +++ b/src/app/ui/components/sidebar/ColorPickerRectangle.vue @@ -1,5 +1,5 @@ @@ -77,13 +216,19 @@ diff --git a/src/app/ui/components/sidebar/SavedVisualOptions.vue b/src/app/ui/components/sidebar/SavedVisualOptions.vue index 089fd69..bb448b1 100644 --- a/src/app/ui/components/sidebar/SavedVisualOptions.vue +++ b/src/app/ui/components/sidebar/SavedVisualOptions.vue @@ -1,5 +1,5 @@ - + diff --git a/src/app/ui/components/tracks/VerticalIGVTrack.vue b/src/app/ui/components/tracks/VerticalIGVTrack.vue index 55877aa..1525d4b 100644 --- a/src/app/ui/components/tracks/VerticalIGVTrack.vue +++ b/src/app/ui/components/tracks/VerticalIGVTrack.vue @@ -1,5 +1,5 @@ - + diff --git a/src/app/ui/components/upper_ribbon/AGPFileSelector.vue b/src/app/ui/components/upper_ribbon/AGPFileSelector.vue index 7195f2d..9d4774c 100644 --- a/src/app/ui/components/upper_ribbon/AGPFileSelector.vue +++ b/src/app/ui/components/upper_ribbon/AGPFileSelector.vue @@ -1,5 +1,5 @@ @@ -123,8 +147,55 @@ - +