From 60503f746e587579bc25d85484a1f892f1443623 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Fri, 6 Dec 2024 18:16:10 +0000 Subject: [PATCH 01/13] CELE-116 Add layer toggle UI component in EM viewer (logic yet to be done) --- .../viewers/EM/EMStackTilesViewer.tsx | 8 +- .../components/viewers/EM/SceneControls.tsx | 76 ++++++++++++++++++- .../viewers/ThreeD/CustomFormControlLabel.tsx | 11 ++- 3 files changed, 89 insertions(+), 6 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 20f02e12..029ca49e 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -374,7 +374,13 @@ const EMStackViewer = () => { return ( - + console.log("toggle", layer, checked)} + />
); diff --git a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx index d730f7a1..8db5f2dc 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx @@ -1,17 +1,36 @@ -import { FileDownloadOutlined, HomeOutlined } from "@mui/icons-material"; +import { FileDownloadOutlined, HomeOutlined, SettingsOutlined } from "@mui/icons-material"; import ZoomInIcon from "@mui/icons-material/ZoomIn"; import ZoomOutIcon from "@mui/icons-material/ZoomOut"; -import { Box, Divider, IconButton } from "@mui/material"; +import { Box, Divider, IconButton, Popover, Typography } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; +import { useState } from "react"; +import CustomFormControlLabel from "../ThreeD/CustomFormControlLabel"; +import { vars } from "../../../theme/variables.ts"; + +const { gray500 } = vars; interface ScaleControlsHandlers { onZoomIn: () => void; onResetView: () => void; onZoomOut: () => void; onPrint: () => void; + onHideLayer: (layer: "neurons" | "synapses", checked: boolean) => void; } -function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint }: ScaleControlsHandlers) { +function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint, onHideLayer }: ScaleControlsHandlers) { + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + const id = open ? "settings-popover" : undefined; + + const handleOpenSettings = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseSettings = () => { + setAnchorEl(null); + }; + return ( + + + + + + + + + EM viewer settings + + onHideLayer("neurons", checked)} /> + onHideLayer("synapses", checked)} + /> + + diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx index ed847177..1e5121f5 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx @@ -3,13 +3,20 @@ import { Box, FormControlLabel, Stack, Tooltip, Typography } from "@mui/material import { vars } from "../../../theme/variables.ts"; // Adjust the import path as needed import CustomSwitch from "../../ViewerContainer/CustomSwitch.tsx"; +interface CustomFormControlLabel { + label: React.ReactNode; + tooltipTitle: React.ReactNode; + helpText: React.ReactNode; + onChange?: (event: React.ChangeEvent, checked: boolean) => void; +} + const { gray50, gray600, gray400B } = vars; -const CustomFormControlLabel = ({ label, tooltipTitle, helpText }) => { +const CustomFormControlLabel = ({ label, tooltipTitle, helpText, onChange }: CustomFormControlLabel) => { return ( - + } sx={{ From 8f6db97ce64de984e7a2f15714e1672cf599709b Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 9 Dec 2024 16:01:37 +0000 Subject: [PATCH 02/13] CELE-116 Add EM Viewer toggle neurons and synapses segmentation layers --- .../viewers/EM/EMStackTilesViewer.tsx | 95 +++++++++---------- .../components/viewers/EM/SceneControls.tsx | 36 +++++-- .../src/components/viewers/EM/slidingLayer.ts | 90 ++++++++++++++++++ .../viewers/ThreeD/CustomFormControlLabel.tsx | 5 +- .../frontend/src/helpers/slidingRing.ts | 4 +- 5 files changed, 166 insertions(+), 64 deletions(-) create mode 100644 applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 029ca49e..b0efa502 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -22,6 +22,7 @@ import type { Workspace } from "../../../models/workspace.ts"; import type { Dataset } from "../../../rest/index.ts"; import SceneControls from "./SceneControls.tsx"; import { activeNeuronStyle, neuronFeatureName, selectedNeuronStyle } from "./neuronsMapFeature.ts"; +import { SlidingLayer } from "./slidingLayer.ts"; const newEMLayer = (dataset: Dataset, slice: number, tilegrid: TileGrid, projection: Projection): TileLayer => { return new TileLayer({ @@ -143,9 +144,12 @@ const EMStackViewer = () => { const currSegLayer = useRef | null>(null); const currSynSegLayer = useRef | null>(null); - const ringEM = useRef>>(); - const ringSeg = useRef>>(); - const ringSynSeg = useRef>>(); + const ringEM = useRef>>(); + const ringSeg = useRef>>(); + const ringSynSeg = useRef>>(); + + const [showNeurons, setShowNeurons] = useState(true); + const [showSynapses, setShowSynapses] = useState(true); const startZoom = useMemo(() => { const emData = firstActiveDataset.emData; @@ -195,6 +199,20 @@ const EMStackViewer = () => { currSegLayer.current.getSource().changed(); }, [currentWorkspace.getVisibleNeuronsInEM(), currentWorkspace.visibilities, currentWorkspace.getSelection(ViewerType.EM), segSlice]); + useEffect(() => { + if (!ringSeg.current) { + return; + } + showNeurons ? ringSeg.current.enable() : ringSeg.current.disable(); + }, [showNeurons]); + + useEffect(() => { + if (!ringSynSeg.current) { + return; + } + showSynapses ? ringSynSeg.current.enable() : ringSynSeg.current.disable(); + }, [showSynapses]); + useEffect(() => { if (mapRef.current) { return; @@ -213,77 +231,41 @@ const EMStackViewer = () => { interactions: interactions, }); - ringEM.current = new SlidingRing({ + ringEM.current = new SlidingLayer({ + map: map, cacheSize: ringSize, startAt: startSlice, extent: [minSlice, maxSlice], - onPush: (slice) => { - const layer = newEMLayer(firstActiveDataset, slice, tilegrid, projection); - layer.setOpacity(0); - map.addLayer(layer); - return layer; - }, - onSelected: (_, layer) => { - layer.setOpacity(1); - }, - onUnselected: (_, layer) => { - layer.setOpacity(0); - }, - onEvict: (_, layer) => { - map.removeLayer(layer); - }, + newLayer: (slice) => newEMLayer(firstActiveDataset, slice, tilegrid, projection), }); - ringSeg.current = new SlidingRing({ + ringSeg.current = new SlidingLayer({ + map: map, cacheSize: ringSize, startAt: startSlice, extent: [minSlice, maxSlice], - onPush: (slice) => { - const layer = newSegLayer(firstActiveDataset, slice); - layer.setOpacity(0); + newLayer: (slice) => newSegLayer(firstActiveDataset, slice), + onSlide: (slice, layer) => { layer.setStyle((feature) => neuronsStyleRef.current(feature)); - map.addLayer(layer); - return layer; - }, - onSelected: (slice, layer) => { - layer.setOpacity(1); currSegLayer.current = layer; segSetSlice(slice); }, - onUnselected: (_, layer) => { - layer.setOpacity(0); - }, - onEvict: (_, layer) => { - map.removeLayer(layer); - }, }); map.on("click", (e) => onNeuronSelectRef.current(e.coordinate)); - ringSynSeg.current = new SlidingRing({ + ringSynSeg.current = new SlidingLayer({ + map: map, cacheSize: ringSize, startAt: startSlice, extent: [minSlice, maxSlice], - onPush: (slice) => { - const layer = newSynapsesSegLayer(firstActiveDataset, slice); - layer.setOpacity(0); + newLayer: (slice) => newSynapsesSegLayer(firstActiveDataset, slice), + onSlide: (_, layer) => { layer.setStyle({ "fill-color": "blue", "stroke-color": "blue", }); - map.addLayer(layer); - return layer; - }, - onSelected: (slice, layer) => { - layer.setOpacity(1); currSynSegLayer.current = layer; - segSetSlice(slice); - }, - onUnselected: (_, layer) => { - layer.setOpacity(0); - }, - onEvict: (_, layer) => { - map.removeLayer(layer); }, }); @@ -379,7 +361,18 @@ const EMStackViewer = () => { onResetView={onResetView} onZoomOut={onControlZoomOut} onPrint={onPrint} - onHideLayer={(layer, checked) => console.log("toggle", layer, checked)} + layers={{ + neurons: { + label: "Neurons", + checked: showNeurons, + onToggle: setShowNeurons, + }, + synapses: { + label: "Synapses", + checked: showSynapses, + onToggle: setShowSynapses, + }, + }} />
diff --git a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx index 8db5f2dc..bea37d67 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx @@ -9,15 +9,25 @@ import { vars } from "../../../theme/variables.ts"; const { gray500 } = vars; +interface LayerControlHandler { + label: string; + checked: boolean; + onToggle: (checked: boolean) => void; +} + +type LayersControlsHandlers = { + [key in "neurons" | "synapses"]: LayerControlHandler; +}; + interface ScaleControlsHandlers { onZoomIn: () => void; onResetView: () => void; onZoomOut: () => void; onPrint: () => void; - onHideLayer: (layer: "neurons" | "synapses", checked: boolean) => void; + layers: LayersControlsHandlers; } -function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint, onHideLayer }: ScaleControlsHandlers) { +function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint, layers }: ScaleControlsHandlers) { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); @@ -31,6 +41,20 @@ function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint, onHideLayer setAnchorEl(null); }; + const layersControlsElems = Object.keys(layers).map((key) => { + const { label, checked, onToggle } = layers[key as keyof LayersControlsHandlers]; + return ( + onToggle(checked)} + /> + ); + }); + return ( EM viewer settings - onHideLayer("neurons", checked)} /> - onHideLayer("synapses", checked)} - /> + {layersControlsElems} diff --git a/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts b/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts new file mode 100644 index 00000000..f5976709 --- /dev/null +++ b/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts @@ -0,0 +1,90 @@ +import { Map } from "ol"; +import Layer from "ol/layer/Layer"; +import { SlidingRing } from "../../../helpers/slidingRing"; +import { type SlidingRingOptions } from "../../../helpers/slidingRing"; + +interface SlidingLayerOptions extends Pick, "cacheSize" | "startAt" | "extent"> { + map: Map; + newLayer: (slice: number) => T; + onSlide?: (slice: number, layer: T) => void; +} + +export class SlidingLayer { + private swindow: SlidingRing; + private opaque: boolean = true; // loaded but is transparent + private visible: boolean = true; // not loaded and can not be seen + + constructor(options: SlidingLayerOptions) { + const { map, newLayer, onSlide, ...ringOpts } = options; + + const opts: SlidingRingOptions = { + ...ringOpts, + onPush: (slice) => { + const layer = newLayer(slice); + layer.setOpacity(0); + layer.setVisible(this.visible); + map.addLayer(layer); + return layer; + }, + onSelected: (slice, layer) => { + layer.setOpacity(Number(this.opaque)); + onSlide && onSlide(slice, layer); + }, + onUnselected: (_, layer) => { + layer.setOpacity(0); + }, + onEvict: (_, layer) => { + map.removeLayer(layer); + }, + }; + + this.swindow = new SlidingRing(opts); + } + + show() { + this.swindow.ring[this.swindow.pos].o.setOpacity(1); + this.opaque = true; + } + + hide() { + this.swindow.ring[this.swindow.pos].o.setOpacity(0); + this.opaque = false; + } + + disable() { + if (!this.visible) { + return; + } + + this.swindow.ring.forEach(({ o }) => { + o.setVisible(false); + }); + + this.visible = false; + } + + enable() { + if (this.visible) { + return; + } + + this.swindow.ring.forEach(({ o }) => { + o.setVisible(true); + }); + + this.visible = true; + } + + next() { + this.swindow.next(); + } + prev() { + this.swindow.prev(); + } + goto(slice: number) { + this.swindow.goto(slice); + } + debug() { + this.swindow.debug(); + } +} diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx b/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx index 1e5121f5..0ee2b15b 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx +++ b/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx @@ -7,16 +7,17 @@ interface CustomFormControlLabel { label: React.ReactNode; tooltipTitle: React.ReactNode; helpText: React.ReactNode; + checked?: boolean; onChange?: (event: React.ChangeEvent, checked: boolean) => void; } const { gray50, gray600, gray400B } = vars; -const CustomFormControlLabel = ({ label, tooltipTitle, helpText, onChange }: CustomFormControlLabel) => { +const CustomFormControlLabel = ({ label, tooltipTitle, helpText, checked, onChange }: CustomFormControlLabel) => { return ( - + } sx={{ diff --git a/applications/visualizer/frontend/src/helpers/slidingRing.ts b/applications/visualizer/frontend/src/helpers/slidingRing.ts index 05b22ab4..3d43f2de 100644 --- a/applications/visualizer/frontend/src/helpers/slidingRing.ts +++ b/applications/visualizer/frontend/src/helpers/slidingRing.ts @@ -31,12 +31,12 @@ export interface SlidingRingOptions extends SlidingRingCb { export class SlidingRing { private extent: [number, number]; - private ring: Array<{ + public ring: Array<{ n: number; // position within extent o: T; }>; - private pos: number; // current buffer position + public pos: number; // current buffer position private tail: number; // buffer tail private head: number; // buffer head From b226cb2549e53bac78f8b20231c22f2a9aeea500 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 9 Dec 2024 16:05:42 +0000 Subject: [PATCH 03/13] CELE-116 Remove unused dependency --- .../frontend/src/components/viewers/EM/EMStackTilesViewer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index b0efa502..b6482b4c 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -16,7 +16,6 @@ import VectorSource from "ol/source/Vector"; import { TileGrid } from "ol/tilegrid"; import { useEffect, useMemo, useRef, useState } from "react"; import { useGlobalContext } from "../../../contexts/GlobalContext.tsx"; -import { SlidingRing } from "../../../helpers/slidingRing"; import { ViewerType, getEMDataURL, getSegmentationURL, getSynapsesSegmentationURL } from "../../../models/models.ts"; import type { Workspace } from "../../../models/workspace.ts"; import type { Dataset } from "../../../rest/index.ts"; From a30f835c9b61fb2fb6349fd777836d4721207bc6 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 9 Dec 2024 16:52:45 +0000 Subject: [PATCH 04/13] CELE-116 Fix momentarily unstyled layer on EM viewer when fast scrolling --- .../frontend/src/components/viewers/EM/slidingLayer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts b/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts index f5976709..1456784b 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts +++ b/applications/visualizer/frontend/src/components/viewers/EM/slidingLayer.ts @@ -27,8 +27,8 @@ export class SlidingLayer { return layer; }, onSelected: (slice, layer) => { - layer.setOpacity(Number(this.opaque)); onSlide && onSlide(slice, layer); + layer.setOpacity(Number(this.opaque)); }, onUnselected: (_, layer) => { layer.setOpacity(0); From f9aee2008c252bba075273f654d31714560e45ba Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 9 Dec 2024 17:06:37 +0000 Subject: [PATCH 05/13] CELE-116 Select neuron or synapse across EM viewer layers --- .../viewers/EM/EMStackTilesViewer.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index b6482b4c..2da99d73 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -89,13 +89,24 @@ function neuronsStyle(feature: FeatureLike, workspace: Workspace) { return null; } -function onNeuronSelect(position: Coordinate, source: VectorSource | undefined, workspace: Workspace) { - const features = source?.getFeaturesAtCoordinate(position); - if (!features || features.length === 0) { - return; +function selectAcrossLayers(position: Coordinate, ...layers: (VectorLayer | undefined)[]): Feature | undefined { + for (let i = 0; i < layers.length; i++) { + const source = layers[i]?.getSource(); + const features = source?.getFeaturesAtCoordinate(position); + if (!features || features.length === 0) { + continue; + } + + if (features.length > 1) { + console.warn("found overlapping neurons on the same layer"); + } + return features[0]; } - const feature = features[0]; + return undefined; +} + +function onNeuronSelect(feature: Feature, workspace: Workspace) { const neuronName = neuronFeatureName(feature); if (isNeuronSelected(neuronName, workspace)) { @@ -186,7 +197,7 @@ const EMStackViewer = () => { // }); const neuronsStyleRef = useRef((feature) => neuronsStyle(feature, currentWorkspace)); - const onNeuronSelectRef = useRef((position) => onNeuronSelect(position, currSegLayer.current?.getSource(), currentWorkspace)); + const onNeuronSelectRef = useRef((position) => onNeuronSelect(selectAcrossLayers(position, currSegLayer.current, currSynSegLayer.current), currentWorkspace)); useEffect(() => { if (!currSegLayer.current?.getSource()) { @@ -194,7 +205,7 @@ const EMStackViewer = () => { } neuronsStyleRef.current = (feature: Feature) => neuronsStyle(feature, currentWorkspace); - onNeuronSelectRef.current = (position) => onNeuronSelect(position, currSegLayer.current.getSource(), currentWorkspace); + onNeuronSelectRef.current = (position) => onNeuronSelect(selectAcrossLayers(position, currSegLayer.current, currSynSegLayer.current), currentWorkspace); currSegLayer.current.getSource().changed(); }, [currentWorkspace.getVisibleNeuronsInEM(), currentWorkspace.visibilities, currentWorkspace.getSelection(ViewerType.EM), segSlice]); @@ -260,6 +271,7 @@ const EMStackViewer = () => { extent: [minSlice, maxSlice], newLayer: (slice) => newSynapsesSegLayer(firstActiveDataset, slice), onSlide: (_, layer) => { + // TODO: change the style layer.setStyle({ "fill-color": "blue", "stroke-color": "blue", From 1525485c4838f0b12986db4ac8726f8e85b61018 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Mon, 9 Dec 2024 17:11:01 +0000 Subject: [PATCH 06/13] CELE-116 Fix error when no neuron is selected on click --- .../src/components/viewers/EM/EMStackTilesViewer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 2da99d73..b0d80c2a 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -106,7 +106,11 @@ function selectAcrossLayers(position: Coordinate, ...layers: (VectorLayer Date: Mon, 9 Dec 2024 23:51:58 +0000 Subject: [PATCH 07/13] CELE-116 Add synapse segmentation select handler --- .../viewers/EM/EMStackTilesViewer.tsx | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index b0d80c2a..decd787c 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -89,9 +89,10 @@ function neuronsStyle(feature: FeatureLike, workspace: Workspace) { return null; } -function selectAcrossLayers(position: Coordinate, ...layers: (VectorLayer | undefined)[]): Feature | undefined { - for (let i = 0; i < layers.length; i++) { - const source = layers[i]?.getSource(); +function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer | undefined, (Feature) => void][]) { + for (let i = 0; i < selectors.length; i++) { + const [layer, handler] = selectors[i]; + const source = layer?.getSource(); const features = source?.getFeaturesAtCoordinate(position); if (!features || features.length === 0) { continue; @@ -100,17 +101,12 @@ function selectAcrossLayers(position: Coordinate, ...layers: (VectorLayer 1) { console.warn("found overlapping neurons on the same layer"); } - return features[0]; - } - - return undefined; -} -function onNeuronSelect(feature: Feature | undefined, workspace: Workspace) { - if (!feature) { - return; + handler(features[0]); } +} +function onNeuronSelect(feature: Feature, workspace: Workspace) { const neuronName = neuronFeatureName(feature); if (isNeuronSelected(neuronName, workspace)) { @@ -132,6 +128,11 @@ function onNeuronSelect(feature: Feature | undefined, workspace: Workspace) { workspace.addSelection(neuronName, ViewerType.EM); } +function onSynapseSelect(feature: Feature, _: Workspace) { + const synapseName = neuronFeatureName(feature); + console.log("synaspse click", synapseName); +} + const scale = new ScaleLine({ units: "metric", }); @@ -200,8 +201,15 @@ const EMStackViewer = () => { // }), // }); + const makeFeatureClickHandler = () => (position) => + selectAcrossLayers( + position, + [currSegLayer.current, (feature) => onNeuronSelect(feature, currentWorkspace)], + [currSynSegLayer.current, (feature) => onSynapseSelect(feature, currentWorkspace)], + ); + const neuronsStyleRef = useRef((feature) => neuronsStyle(feature, currentWorkspace)); - const onNeuronSelectRef = useRef((position) => onNeuronSelect(selectAcrossLayers(position, currSegLayer.current, currSynSegLayer.current), currentWorkspace)); + const onFeatureClickRef = useRef(makeFeatureClickHandler()); useEffect(() => { if (!currSegLayer.current?.getSource()) { @@ -209,7 +217,7 @@ const EMStackViewer = () => { } neuronsStyleRef.current = (feature: Feature) => neuronsStyle(feature, currentWorkspace); - onNeuronSelectRef.current = (position) => onNeuronSelect(selectAcrossLayers(position, currSegLayer.current, currSynSegLayer.current), currentWorkspace); + onFeatureClickRef.current = makeFeatureClickHandler(); currSegLayer.current.getSource().changed(); }, [currentWorkspace.getVisibleNeuronsInEM(), currentWorkspace.visibilities, currentWorkspace.getSelection(ViewerType.EM), segSlice]); @@ -266,7 +274,7 @@ const EMStackViewer = () => { }, }); - map.on("click", (e) => onNeuronSelectRef.current(e.coordinate)); + map.on("click", (e) => onFeatureClickRef.current(e.coordinate)); ringSynSeg.current = new SlidingLayer({ map: map, From 175aae3aa785284e2ef063e69d84649bc96d1dc0 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 10 Dec 2024 11:43:45 +0000 Subject: [PATCH 08/13] CELE-116 Fix regression on layer feature selection --- .../frontend/src/components/viewers/EM/EMStackTilesViewer.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index decd787c..7523bf00 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -103,6 +103,7 @@ function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer Date: Tue, 10 Dec 2024 11:45:01 +0000 Subject: [PATCH 09/13] CELE-116 Add synapses segmentation colors in EM viewer --- .../viewers/EM/EMStackTilesViewer.tsx | 23 +++- .../viewers/EM/neuronsMapFeature.ts | 103 ++++++++++++++++++ 2 files changed, 120 insertions(+), 6 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 7523bf00..75642501 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -20,7 +20,7 @@ import { ViewerType, getEMDataURL, getSegmentationURL, getSynapsesSegmentationUR import type { Workspace } from "../../../models/workspace.ts"; import type { Dataset } from "../../../rest/index.ts"; import SceneControls from "./SceneControls.tsx"; -import { activeNeuronStyle, neuronFeatureName, selectedNeuronStyle } from "./neuronsMapFeature.ts"; +import { activeNeuronStyle, neuronFeatureName, selectedNeuronStyle, selectedSynapseStyle, activeSynapseStyle } from "./neuronsMapFeature.ts"; import { SlidingLayer } from "./slidingLayer.ts"; const newEMLayer = (dataset: Dataset, slice: number, tilegrid: TileGrid, projection: Projection): TileLayer => { @@ -131,7 +131,16 @@ function onNeuronSelect(feature: Feature, workspace: Workspace) { function onSynapseSelect(feature: Feature, _: Workspace) { const synapseName = neuronFeatureName(feature); - console.log("synaspse click", synapseName); + + const selected = feature.get("selected"); + if (selected) { + feature.set("selected", false); + console.debug("synaspse unselected", synapseName); + return; + } + + feature.set("selected", true); + console.debug("synaspse selected", synapseName); } const scale = new ScaleLine({ @@ -284,10 +293,12 @@ const EMStackViewer = () => { extent: [minSlice, maxSlice], newLayer: (slice) => newSynapsesSegLayer(firstActiveDataset, slice), onSlide: (_, layer) => { - // TODO: change the style - layer.setStyle({ - "fill-color": "blue", - "stroke-color": "blue", + layer.setStyle((feature) => { + const isSelected = feature.get("selected"); + if (isSelected) { + return selectedSynapseStyle(feature); + } + return activeSynapseStyle(feature); }); currSynSegLayer.current = layer; }, diff --git a/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts b/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts index 333a4c9c..661bc47d 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts +++ b/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts @@ -1,3 +1,4 @@ +import { Color } from "ol/color"; import type { FeatureLike } from "ol/Feature"; import Fill from "ol/style/Fill"; import Stroke from "ol/style/Stroke"; @@ -58,3 +59,105 @@ export function neuronFeatureName(feature: FeatureLike): string { return neuronName; } + +function synapseColor(feature: FeatureLike): Color { + const neuronName = neuronFeatureName(feature); + const colorer = new String2HexCodeColor(); + return hexToRGBArray(colorer.stringToColor(neuronName)); +} + +export function activeSynapseStyle(feature: FeatureLike, color?: string): Style { + const opacity = 0.2; + const [r, g, b] = color ? hexToRGBArray(color) : synapseColor(feature); + const rgbaColor = `rgba(${r}, ${g}, ${b}, ${opacity})`; + + return new Style({ + stroke: new Stroke({ + color: [r, g, b], + width: 2, + }), + fill: new Fill({ + color: rgbaColor, + }), + }); +} + +export function selectedSynapseStyle(feature: FeatureLike, color?: string): Style { + const opacity = 0.5; + const [r, g, b] = color ? hexToRGBArray(color) : synapseColor(feature); + const rgbaColor = `rgba(${r}, ${g}, ${b}, ${opacity})`; + + return new Style({ + stroke: new Stroke({ + color: [r, g, b], + width: 4, + }), + fill: new Fill({ + color: rgbaColor, + }), + text: new Text({ + text: feature.get("name"), + scale: 2, + }), + }); +} + +// from: github:HugoJBello/string-to-hex-code-color +class String2HexCodeColor { + defaultShadePercentage = 0; + + constructor(defaultShadePercentage?: number) { + if (defaultShadePercentage) this.defaultShadePercentage = defaultShadePercentage; + } + + shadeColor(color: string, percent: number | undefined) { + if (!percent) { + percent = this.defaultShadePercentage; + } + const f = parseInt(color.slice(1), 16); + const t = percent < 0 ? 0 : 255; + const p = percent < 0 ? percent * -1 : percent; + const R = f >> 16; + const G = (f >> 8) & 0x00ff; + const B = f & 0x0000ff; + const result = + "#" + (0x1000000 + (Math.round((t - R) * p) + R) * 0x10000 + (Math.round((t - G) * p) + G) * 0x100 + (Math.round((t - B) * p) + B)).toString(16).slice(1); + return result; + } + + stringToColor(str: string, shadePercentage?: number) { + if (!str) str = ""; + if (str.length < 4) { + str = str + this.preHash(str).toString(); + } + + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = "#"; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += ("00" + value.toString(16)).substr(-2); + } + if (shadePercentage || this.defaultShadePercentage !== 0) { + return this.shadeColor(colour, shadePercentage); + } + return colour; + } + + preHash(str: string) { + { + let hash = 0; + if (str.length === 0) { + return hash; + } + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = (hash << 5) - hash + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash; + } + } +} From d808d924dd2555f28befd7184cc0975c4cf42b98 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 10 Dec 2024 13:05:58 +0000 Subject: [PATCH 10/13] CELE-116 Integrate synchronizers with synapse selection in EM Viewer --- .../viewers/EM/EMStackTilesViewer.tsx | 59 +++++++++++-------- .../viewers/EM/neuronsMapFeature.ts | 4 +- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 75642501..315ae98b 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -20,8 +20,9 @@ import { ViewerType, getEMDataURL, getSegmentationURL, getSynapsesSegmentationUR import type { Workspace } from "../../../models/workspace.ts"; import type { Dataset } from "../../../rest/index.ts"; import SceneControls from "./SceneControls.tsx"; -import { activeNeuronStyle, neuronFeatureName, selectedNeuronStyle, selectedSynapseStyle, activeSynapseStyle } from "./neuronsMapFeature.ts"; +import { activeNeuronStyle, cellFeatureName, selectedNeuronStyle, selectedSynapseStyle, activeSynapseStyle } from "./neuronsMapFeature.ts"; import { SlidingLayer } from "./slidingLayer.ts"; +import Style from "ol/style/Style"; const newEMLayer = (dataset: Dataset, slice: number, tilegrid: TileGrid, projection: Projection): TileLayer => { return new TileLayer({ @@ -60,12 +61,12 @@ function isNeuronActive(neuronId: string, workspace: Workspace): boolean { return emViewerVisibleNeurons.includes(neuronId) || emViewerVisibleNeurons.includes(workspace.getNeuronClass(neuronId)); } -function isNeuronSelected(neuronId: string, workspace: Workspace): boolean { - return workspace.getSelection(ViewerType.EM).includes(neuronId); +function isCellSelected(cellId: string, workspace: Workspace): boolean { + return workspace.getSelection(ViewerType.EM).includes(cellId); } function isNeuronVisible(neuronId: string, workspace: Workspace): boolean { - return isNeuronActive(neuronId, workspace) || isNeuronSelected(neuronId, workspace); + return isNeuronActive(neuronId, workspace) || isCellSelected(neuronId, workspace); } function neuronColor(neuronId, workspace: Workspace): string { @@ -74,11 +75,11 @@ function neuronColor(neuronId, workspace: Workspace): string { } function neuronsStyle(feature: FeatureLike, workspace: Workspace) { - const neuronName = neuronFeatureName(feature); + const neuronName = cellFeatureName(feature); const color = neuronColor(neuronName, workspace); - if (isNeuronSelected(neuronName, workspace)) { + if (isCellSelected(neuronName, workspace)) { return selectedNeuronStyle(feature, color); } @@ -89,6 +90,16 @@ function neuronsStyle(feature: FeatureLike, workspace: Workspace) { return null; } +function synapsesStyle(feature: FeatureLike, workspace: Workspace): Style { + const synapseName = cellFeatureName(feature); + + if (isCellSelected(synapseName, workspace)) { + return selectedSynapseStyle(feature); + } + + return activeSynapseStyle(feature); +} + function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer | undefined, (Feature) => void][]) { for (let i = 0; i < selectors.length; i++) { const [layer, handler] = selectors[i]; @@ -108,9 +119,9 @@ function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer { ); const neuronsStyleRef = useRef((feature) => neuronsStyle(feature, currentWorkspace)); + const synapsesStyleRef = useRef((feature) => synapsesStyle(feature, currentWorkspace)); const onFeatureClickRef = useRef(makeFeatureClickHandler()); useEffect(() => { @@ -231,6 +240,16 @@ const EMStackViewer = () => { currSegLayer.current.getSource().changed(); }, [currentWorkspace.getVisibleNeuronsInEM(), currentWorkspace.visibilities, currentWorkspace.getSelection(ViewerType.EM), segSlice]); + useEffect(() => { + if (!currSynSegLayer.current?.getSource()) { + return; + } + + synapsesStyleRef.current = (feature: Feature) => synapsesStyle(feature, currentWorkspace); + onFeatureClickRef.current = makeFeatureClickHandler(); + currSynSegLayer.current.getSource().changed(); + }, [currentWorkspace.getSelection(ViewerType.EM), segSlice]); + useEffect(() => { if (!ringSeg.current) { return; @@ -293,13 +312,7 @@ const EMStackViewer = () => { extent: [minSlice, maxSlice], newLayer: (slice) => newSynapsesSegLayer(firstActiveDataset, slice), onSlide: (_, layer) => { - layer.setStyle((feature) => { - const isSelected = feature.get("selected"); - if (isSelected) { - return selectedSynapseStyle(feature); - } - return activeSynapseStyle(feature); - }); + layer.setStyle((feature) => synapsesStyleRef.current(feature)); currSynSegLayer.current = layer; }, }); diff --git a/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts b/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts index 661bc47d..10a30cb4 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts +++ b/applications/visualizer/frontend/src/components/viewers/EM/neuronsMapFeature.ts @@ -50,7 +50,7 @@ export function selectedNeuronStyle(feature: FeatureLike, color?: string): Style }); } -export function neuronFeatureName(feature: FeatureLike): string { +export function cellFeatureName(feature: FeatureLike): string { const neuronName = feature.getProperties()?.name; if (typeof neuronName !== "string") { @@ -61,7 +61,7 @@ export function neuronFeatureName(feature: FeatureLike): string { } function synapseColor(feature: FeatureLike): Color { - const neuronName = neuronFeatureName(feature); + const neuronName = cellFeatureName(feature); const colorer = new String2HexCodeColor(); return hexToRGBArray(colorer.stringToColor(neuronName)); } From 76f79ed60681c92c39a54ede13578541c4090f39 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 10 Dec 2024 16:07:24 +0000 Subject: [PATCH 11/13] CELE-116 Fix synapses not being selected due to invisible neuron in segmentation layer --- .../viewers/EM/EMStackTilesViewer.tsx | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 315ae98b..44ec346d 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -100,7 +100,11 @@ function synapsesStyle(feature: FeatureLike, workspace: Workspace): Style { return activeSynapseStyle(feature); } -function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer | undefined, (Feature) => void][]) { +// LayerSelect specified a layer for features to be selected from and an handler function to be called if features are found. +// The handler next function forces a jump to the next layer selector. +type LayerSelector = [VectorLayer | undefined, (feature: Feature, next: () => void) => void]; + +function selectAcrossLayers(position: Coordinate, ...selectors: LayerSelector[]) { for (let i = 0; i < selectors.length; i++) { const [layer, handler] = selectors[i]; const source = layer?.getSource(); @@ -113,8 +117,16 @@ function selectAcrossLayers(position: Coordinate, ...selectors: [VectorLayer { + shouldContinue = true; + }; + + handler(features[0], next); + + if (!shouldContinue) { + return; + } } } @@ -222,7 +234,15 @@ const EMStackViewer = () => { const makeFeatureClickHandler = () => (position) => selectAcrossLayers( position, - [currSegLayer.current, (feature) => onNeuronSelect(feature, currentWorkspace)], + [ + currSegLayer.current, + (feature, next) => { + if (!isNeuronVisible(cellFeatureName(feature), currentWorkspace)) { + return next(); + } + onNeuronSelect(feature, currentWorkspace); + }, + ], [currSynSegLayer.current, (feature) => onSynapseSelect(feature, currentWorkspace)], ); From aa035941b8af8b75f575d52ef22e436f14103b14 Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 10 Dec 2024 16:10:17 +0000 Subject: [PATCH 12/13] CELE-116 Code comment typo --- .../frontend/src/components/viewers/EM/EMStackTilesViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 44ec346d..80539280 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -100,7 +100,7 @@ function synapsesStyle(feature: FeatureLike, workspace: Workspace): Style { return activeSynapseStyle(feature); } -// LayerSelect specified a layer for features to be selected from and an handler function to be called if features are found. +// LayerSelect specifies a layer for features to be selected from and a handler function to be called if a feature are found. // The handler next function forces a jump to the next layer selector. type LayerSelector = [VectorLayer | undefined, (feature: Feature, next: () => void) => void]; From 336c7efb0454c75955c0126d4c43e76ba9418cda Mon Sep 17 00:00:00 2001 From: Diogo Correia Date: Tue, 17 Dec 2024 15:24:59 +0000 Subject: [PATCH 13/13] CELE-116 Code review changes --- .../{viewers/ThreeD => }/CustomFormControlLabel.tsx | 4 ++-- .../src/components/viewers/EM/EMStackTilesViewer.tsx | 3 +-- .../frontend/src/components/viewers/EM/SceneControls.tsx | 5 ++--- .../frontend/src/components/viewers/ThreeD/SceneControls.tsx | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) rename applications/visualizer/frontend/src/components/{viewers/ThreeD => }/CustomFormControlLabel.tsx (91%) diff --git a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx b/applications/visualizer/frontend/src/components/CustomFormControlLabel.tsx similarity index 91% rename from applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx rename to applications/visualizer/frontend/src/components/CustomFormControlLabel.tsx index 0ee2b15b..ef957d7e 100644 --- a/applications/visualizer/frontend/src/components/viewers/ThreeD/CustomFormControlLabel.tsx +++ b/applications/visualizer/frontend/src/components/CustomFormControlLabel.tsx @@ -1,7 +1,7 @@ import HelpOutlineIcon from "@mui/icons-material/HelpOutline"; import { Box, FormControlLabel, Stack, Tooltip, Typography } from "@mui/material"; -import { vars } from "../../../theme/variables.ts"; // Adjust the import path as needed -import CustomSwitch from "../../ViewerContainer/CustomSwitch.tsx"; +import { vars } from "./../theme/variables.ts"; // Adjust the import path as needed +import CustomSwitch from "./ViewerContainer/CustomSwitch.tsx"; interface CustomFormControlLabel { label: React.ReactNode; diff --git a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx index 80539280..05970abe 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/EMStackTilesViewer.tsx @@ -105,8 +105,7 @@ function synapsesStyle(feature: FeatureLike, workspace: Workspace): Style { type LayerSelector = [VectorLayer | undefined, (feature: Feature, next: () => void) => void]; function selectAcrossLayers(position: Coordinate, ...selectors: LayerSelector[]) { - for (let i = 0; i < selectors.length; i++) { - const [layer, handler] = selectors[i]; + for (const [layer, handler] of selectors) { const source = layer?.getSource(); const features = source?.getFeaturesAtCoordinate(position); if (!features || features.length === 0) { diff --git a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx index bea37d67..20a69e66 100644 --- a/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx +++ b/applications/visualizer/frontend/src/components/viewers/EM/SceneControls.tsx @@ -4,7 +4,7 @@ import ZoomOutIcon from "@mui/icons-material/ZoomOut"; import { Box, Divider, IconButton, Popover, Typography } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; import { useState } from "react"; -import CustomFormControlLabel from "../ThreeD/CustomFormControlLabel"; +import CustomFormControlLabel from "../../CustomFormControlLabel"; import { vars } from "../../../theme/variables.ts"; const { gray500 } = vars; @@ -41,8 +41,7 @@ function SceneControls({ onZoomIn, onResetView, onZoomOut, onPrint, layers }: Sc setAnchorEl(null); }; - const layersControlsElems = Object.keys(layers).map((key) => { - const { label, checked, onToggle } = layers[key as keyof LayersControlsHandlers]; + const layersControlsElems = Object.entries(layers).map(([key, { label, checked, onToggle }]) => { return (