diff --git a/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStarter.java b/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStarter.java new file mode 100644 index 00000000..78b9eb52 --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStarter.java @@ -0,0 +1,141 @@ +package sc.fiji.bdvpg.bdv.navigate; + +import bdv.util.BdvHandle; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.ui.TransformListener; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Action which synchronizes the display location of n BdvHandle + * + * Works in combination with the action ViewerTransformSyncStopper + * + * See also ViewTransformSynchronizationDemo + * + * Principle : for every changed view transform of a specific BdvHandle, + * the view transform change is triggered to the following BdvHandle in a closed loop manner + * + * To avoid inifinite loop, the stop condition is : if the view transform is unnecessary (between + * the view target is equal to the source), then there's no need to trigger a view transform change + * to the next BdvHandle + * + * author Nicolas Chiaruttini, BIOP EPFL, nicolas.chiaruttini@epfl.ch + */ + +public class ViewerTransformSyncStarter implements Runnable { + + /** + * Array of BdvHandles to synchronize + */ + BdvHandle[] bdvHandles; + + /** + * Reference to the BdvHandle which will serve as a reference for the + * first synchronization. Most of the time this has to be the BdvHandle + * currently used by the user. If not set, the first synchronization + * will look like it's a random BdvHandle which is used (one not in focus) + */ + BdvHandle bdvHandleInitialReference = null; + + /** + * Map which links each BdvHandle to the TransformListener which has been added + * for synchronization purpose. This object contains all what's neede to stop + * the synchronization + */ + Map> bdvHandleToTransformListener = new HashMap<>(); + + public ViewerTransformSyncStarter(BdvHandle[] bdvHandles) { + this.bdvHandles = bdvHandles; + } + + public void setBdvHandleInitialReference(BdvHandle bdvHandle) { + bdvHandleInitialReference = bdvHandle; + } + + @Override + public void run() { + + // Getting transform for initial sync + AffineTransform3D at3Dorigin = getViewTransformForInitialSynchronization(); + + // Building circularly linked listeners with stop condition when all transforms are equal, + // cf documentation + + for (int i = 0; i< bdvHandles.length; i++) { + + // The idea is that bdvHandles[i], when it has a view transform, + // triggers an identical ViewTransform to the next bdvHandle in the array + // (called nextBdvHandle). nextBdvHandle is bdvHandles[i+1] in most cases, + // unless it's the end of the array, + // where in this case nextBdvHandle is bdvHandles[0] + BdvHandle currentBdvHandle = bdvHandles[i]; + BdvHandle nextBdvHandle; + + // Identifying nextBdvHandle + if (i == bdvHandles.length-1) { + nextBdvHandle = bdvHandles[0]; + } else { + nextBdvHandle = bdvHandles[i+1]; + } + + // Building the TransformListener of currentBdvHandle + TransformListener listener = + (at3D) -> { + // Is the transform necessary ? That's the stop condition + AffineTransform3D ati = new AffineTransform3D(); + nextBdvHandle.getViewerPanel().getState().getViewerTransform(ati); + if (!Arrays.equals(at3D.getRowPackedCopy(), ati.getRowPackedCopy())) { + // Yes -> triggers a transform change to the nextBdvHandle + nextBdvHandle.getViewerPanel().setCurrentViewerTransform(at3D.copy()); + nextBdvHandle.getViewerPanel().requestRepaint(); + } + }; + + // Adding this transform listener to the currenBdvHandle + currentBdvHandle.getViewerPanel().addTransformListener(listener); + + // Storing the transform listener -> needed to remove them in order to stop synchronization when needed + bdvHandleToTransformListener.put(bdvHandles[i], listener); + } + + // Setting first transform for initial synchronization, + // but only if the two necessary objects are present (the origin BdvHandle and the transform + if ((bdvHandleInitialReference !=null)&&(at3Dorigin!=null)) { + for (BdvHandle bdvh: bdvHandles) { + bdvh.getViewerPanel().setCurrentViewerTransform(at3Dorigin.copy()); + bdvh.getViewerPanel().requestRepaint(); + } + } + } + + /** + * A simple search to identify the view transform of the BdvHandle that will be used + * for the initial synchronization (first reference) + * @return + */ + private AffineTransform3D getViewTransformForInitialSynchronization() { + AffineTransform3D at3Dorigin = null; + for (int i = 0; i< bdvHandles.length; i++) { + BdvHandle bdvHandle = bdvHandles[i]; + // if the BdvHandle is the one that should be used for initial synchronization + if (bdvHandle.equals(bdvHandleInitialReference)) { + // Storing the transform that will be used for first synchronization + at3Dorigin = new AffineTransform3D(); + bdvHandle.getViewerPanel().getState().getViewerTransform(at3Dorigin); + } + } + return at3Dorigin; + } + + /** + * output of this action : this map can be used to stop the synchronization + * see ViewerTransformSyncStopper + * @return + */ + public Map> getSynchronizers() { + return bdvHandleToTransformListener; + } +} diff --git a/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStopper.java b/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStopper.java new file mode 100644 index 00000000..f0e32474 --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/bdv/navigate/ViewerTransformSyncStopper.java @@ -0,0 +1,33 @@ +package sc.fiji.bdvpg.bdv.navigate; + +import bdv.util.BdvHandle; +import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.ui.TransformListener; +import java.util.Map; + +/** + * Action which stops the synchronization of the display location of n BdvHandle + * Works in combination with the action ViewerTransformSyncStarter + * + * See also ViewTransformSynchronizationDemo + * + * author Nicolas Chiaruttini, BIOP EPFL, nicolas.chiaruttini@epfl.ch + **/ + +public class ViewerTransformSyncStopper implements Runnable { + + Map> bdvHandleToTransformListener; + + public ViewerTransformSyncStopper(Map> bdvHandleToTransformListener) { + this.bdvHandleToTransformListener = bdvHandleToTransformListener; + } + + @Override + public void run() { + bdvHandleToTransformListener.forEach((bdvHandle, listener) -> { + bdvHandle.getViewerPanel().removeTransformListener(listener); + }); + } + + +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/command/bdv/ViewSynchronizerCommand.java b/src/main/java/sc/fiji/bdvpg/scijava/command/bdv/ViewSynchronizerCommand.java new file mode 100644 index 00000000..684feab2 --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/command/bdv/ViewSynchronizerCommand.java @@ -0,0 +1,71 @@ +package sc.fiji.bdvpg.scijava.command.bdv; + +import bdv.util.BdvHandle; +import org.scijava.command.Command; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import sc.fiji.bdvpg.bdv.navigate.ViewerTransformSyncStarter; +import sc.fiji.bdvpg.bdv.navigate.ViewerTransformSyncStopper; +import sc.fiji.bdvpg.scijava.BdvHandleHelper; +import sc.fiji.bdvpg.scijava.ScijavaBdvDefaults; +import sc.fiji.bdvpg.services.BdvService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +/** + * I wanted to do this as an Interactive Command but there's no callback + * when an interactive command is closed (bug https://github.com/scijava/scijava-common/issues/379) + * -> we cannot stop the synchronization appropriately. + * + * Hence the dirty JFrame the user has to close to stop synchronization ... + * + * author Nicolas Chiaruttini + */ + +@Plugin(type = Command.class, menuPath = ScijavaBdvDefaults.RootMenu+"Bdv>Synchronize Views") +public class ViewSynchronizerCommand implements Command { + + @Parameter(label = "Select Windows to synchronize") + BdvHandle[] bdvhs; + + ViewerTransformSyncStarter sync; + + public void run() { + // Starting synchronnization of selected bdvhandles + sync = new ViewerTransformSyncStarter(bdvhs); + sync.setBdvHandleInitialReference(BdvService.getSourceAndConverterDisplayService().getActiveBdv()); + sync.run(); + + // JFrame serving the purpose of stopping synchronization when it is being closed + JFrame frameStopSync = new JFrame(); + frameStopSync.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + super.windowClosing(e); + new ViewerTransformSyncStopper(sync.getSynchronizers()).run(); + e.getWindow().dispose(); + } + }); + frameStopSync.setTitle("Close window to stop synchronization"); + + // Building JFrame with a simple panel and textarea + String text = ""; + for (BdvHandle bdvh:bdvhs) { + text+= BdvHandleHelper.getWindowTitle(bdvh)+"\n"; + } + + JPanel pane = new JPanel(); + JTextArea textArea = new JTextArea(text); + textArea.setEditable(false); + pane.add(textArea); + frameStopSync.add(pane); + frameStopSync.setPreferredSize(new Dimension(600,100)); + + frameStopSync.pack(); + frameStopSync.setVisible(true); + } + +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/converters/StringToBdvHandle.java b/src/main/java/sc/fiji/bdvpg/scijava/converters/StringToBdvHandle.java new file mode 100644 index 00000000..6742c2fa --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/converters/StringToBdvHandle.java @@ -0,0 +1,38 @@ +package sc.fiji.bdvpg.scijava.converters; + +import bdv.util.BdvHandle; +import org.scijava.convert.AbstractConverter; +import org.scijava.object.ObjectService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import sc.fiji.bdvpg.scijava.BdvHandleHelper; + +import java.util.Optional; + +@Plugin(type = org.scijava.convert.Converter.class) +public class StringToBdvHandle extends AbstractConverter { + @Parameter + ObjectService os; + + @Override + public T convert(Object src, Class dest) { + Optional ans = os.getObjects(BdvHandle.class).stream().filter(bdvh -> + (bdvh.toString().equals(src))||(BdvHandleHelper.getWindowTitle(bdvh).equals(src)) + ).findFirst(); + if (ans.isPresent()) { + return (T) ans.get(); + } else { + return null; + } + } + + @Override + public Class getOutputType() { + return (Class) BdvHandle.class; + } + + @Override + public Class getInputType() { + return (Class) String.class; + } +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/services/BdvSourceAndConverterDisplayService.java b/src/main/java/sc/fiji/bdvpg/scijava/services/BdvSourceAndConverterDisplayService.java index fffb2428..3ba218fe 100644 --- a/src/main/java/sc/fiji/bdvpg/scijava/services/BdvSourceAndConverterDisplayService.java +++ b/src/main/java/sc/fiji/bdvpg/scijava/services/BdvSourceAndConverterDisplayService.java @@ -89,24 +89,29 @@ public class BdvSourceAndConverterDisplayService extends AbstractService impleme **/ Map> sacToBdvHandleRefs; + public BdvHandle getNewBdv() { + try + { + return (BdvHandle) + cs.run(BdvWindowCreatorCommand.class, + true, + "is2D", false, + "windowTitle", "Bdv").get().getOutput("bdvh"); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return null; + } + /** * Returns the last active Bdv or create a new one */ public BdvHandle getActiveBdv() { List bdvhs = os.getObjects(BdvHandle.class); if ((bdvhs == null)||(bdvhs.size()==0)) { - try - { - return (BdvHandle) - cs.run(BdvWindowCreatorCommand.class, - true, - "is2D", false, - "windowTitle", "Bdv").get().getOutput("bdvh"); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } + return getNewBdv(); } if (bdvhs.size()==1) { diff --git a/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleListWidget.java b/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleListWidget.java new file mode 100644 index 00000000..c6d97709 --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleListWidget.java @@ -0,0 +1,7 @@ +package sc.fiji.bdvpg.scijava.widget; + +import bdv.util.BdvHandle; +import org.scijava.widget.InputWidget; + +public interface BdvHandleListWidget extends InputWidget { +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleWidget.java b/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleWidget.java new file mode 100644 index 00000000..1b28e21d --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/widget/BdvHandleWidget.java @@ -0,0 +1,8 @@ +package sc.fiji.bdvpg.scijava.widget; + +import bdv.util.BdvHandle; +import bdv.viewer.SourceAndConverter; +import org.scijava.widget.InputWidget; + +public interface BdvHandleWidget extends InputWidget { +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleListWidget.java b/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleListWidget.java new file mode 100644 index 00000000..61138f0a --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleListWidget.java @@ -0,0 +1,81 @@ +package sc.fiji.bdvpg.scijava.widget; + +import bdv.util.BdvHandle; +import org.scijava.Priority; +import org.scijava.object.ObjectService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.ui.swing.widget.SwingInputWidget; +import org.scijava.widget.InputWidget; +import org.scijava.widget.WidgetModel; +import sc.fiji.bdvpg.scijava.BdvHandleHelper; +import sc.fiji.bdvpg.scijava.services.BdvSourceAndConverterService; + +import javax.swing.*; +import java.awt.*; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Swing implementation of {@link BdvHandleListWidget}. + * + * @author Nicolas Chiaruttini + */ + +@Plugin(type = InputWidget.class, priority = Priority.EXTREMELY_HIGH) +public class SwingBdvHandleListWidget extends SwingInputWidget implements + BdvHandleListWidget { + + @Override + protected void doRefresh() { + } + + @Override + public boolean supports(final WidgetModel model) { + return super.supports(model) && model.isType(BdvHandle[].class); + } + + @Override + public BdvHandle[] getValue() { + return getSelectedBdvHandles(); + } + + JList list; + + public BdvHandle[] getSelectedBdvHandles() { + List selected = list.getSelectedValuesList(); + return selected.stream().map((e) -> e.bdvh) + .collect(Collectors.toList()).toArray(new BdvHandle[selected.size()]); + } + + @Parameter + ObjectService os; + + @Override + public void set(final WidgetModel model) { + super.set(model); + List bdvhs = os.getObjects(BdvHandle.class).stream().map(bdvh -> new RenamableBdvHandle(bdvh)).collect(Collectors.toList()); + RenamableBdvHandle[] data = bdvhs.toArray(new RenamableBdvHandle[bdvhs.size()]); + list = new JList(data); //data has type Object[] + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + JScrollPane listScroller = new JScrollPane(list); + listScroller.setPreferredSize(new Dimension(250, 80)); + list.addListSelectionListener((e)-> model.setValue(getValue())); + getComponent().add(listScroller); + } + + public class RenamableBdvHandle { + + public BdvHandle bdvh; + + public RenamableBdvHandle(BdvHandle bdvh) { + this.bdvh = bdvh; + } + + public String toString() { + return BdvHandleHelper.getWindowTitle(bdvh); + } + + } + +} diff --git a/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleWidget.java b/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleWidget.java new file mode 100644 index 00000000..6c488836 --- /dev/null +++ b/src/main/java/sc/fiji/bdvpg/scijava/widget/SwingBdvHandleWidget.java @@ -0,0 +1,91 @@ +package sc.fiji.bdvpg.scijava.widget; + +import bdv.util.BdvHandle; +import bdv.viewer.SourceAndConverter; +import org.scijava.Priority; +import org.scijava.object.ObjectService; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.Plugin; +import org.scijava.ui.swing.widget.SwingInputWidget; +import org.scijava.widget.InputWidget; +import org.scijava.widget.WidgetModel; +import sc.fiji.bdvpg.scijava.BdvHandleHelper; +import sc.fiji.bdvpg.scijava.services.BdvSourceAndConverterService; +import sc.fiji.bdvpg.scijava.services.ui.BdvSourceServiceUI; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.awt.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Swing implementation of {@link BdvHandleWidget}. + * + * @author Nicolas Chiaruttini + */ + +@Plugin(type = InputWidget.class, priority = Priority.EXTREMELY_HIGH) +public class SwingBdvHandleWidget extends SwingInputWidget implements + BdvHandleWidget { + + @Override + protected void doRefresh() { + } + + @Override + public boolean supports(final WidgetModel model) { + return super.supports(model) && model.isType(BdvHandle.class); + } + + @Override + public BdvHandle getValue() { + return getSelectedBdvHandle(); + } + + @Parameter + BdvSourceAndConverterService bss; + + JList list; + + public BdvHandle getSelectedBdvHandle() { + return ((RenamableBdvHandle) list.getSelectedValue()).bdvh; + } + + @Parameter + ObjectService os; + + + @Override + public void set(final WidgetModel model) { + super.set(model); + List bdvhs = os.getObjects(BdvHandle.class).stream().map(bdvh -> new RenamableBdvHandle(bdvh)).collect(Collectors.toList()); + RenamableBdvHandle[] data = bdvhs.toArray(new RenamableBdvHandle[bdvhs.size()]); + list = new JList(data); //data has type Object[] + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + JScrollPane listScroller = new JScrollPane(list); + listScroller.setPreferredSize(new Dimension(250, 80)); + list.addListSelectionListener((e)-> model.setValue(getValue())); + getComponent().add(listScroller); + } + + public class RenamableBdvHandle { + + public BdvHandle bdvh; + + public RenamableBdvHandle(BdvHandle bdvh) { + this.bdvh = bdvh; + } + + public String toString() { + return BdvHandleHelper.getWindowTitle(bdvh); + } + + } + +} diff --git a/src/main/java/sc/fiji/bdvpg/services/IBdvSourceAndConverterDisplayService.java b/src/main/java/sc/fiji/bdvpg/services/IBdvSourceAndConverterDisplayService.java index 6ad8b9eb..e28e33c9 100644 --- a/src/main/java/sc/fiji/bdvpg/services/IBdvSourceAndConverterDisplayService.java +++ b/src/main/java/sc/fiji/bdvpg/services/IBdvSourceAndConverterDisplayService.java @@ -50,6 +50,11 @@ public interface IBdvSourceAndConverterDisplayService { */ BdvHandle getActiveBdv(); + /** + * Returns a new Bdv window + */ + BdvHandle getNewBdv(); + /** * Returns SourceAndConverter object * @param sac diff --git a/src/test/src/sc/fiji/bdvpg/bdv/navigate/ViewTransformSynchronizationDemo.java b/src/test/src/sc/fiji/bdvpg/bdv/navigate/ViewTransformSynchronizationDemo.java new file mode 100644 index 00000000..58aa62cf --- /dev/null +++ b/src/test/src/sc/fiji/bdvpg/bdv/navigate/ViewTransformSynchronizationDemo.java @@ -0,0 +1,82 @@ +package sc.fiji.bdvpg.bdv.navigate; + +import bdv.util.BdvHandle; +import bdv.util.RandomAccessibleIntervalSource; +import bdv.viewer.Source; +import bdv.viewer.SourceAndConverter; +import ij.IJ; +import ij.ImagePlus; +import net.imglib2.RandomAccessibleInterval; +import net.imglib2.img.display.imagej.ImageJFunctions; +import net.imglib2.util.Util; +import net.imglib2.view.Views; +import sc.fiji.bdvpg.behaviour.ClickBehaviourInstaller; +import sc.fiji.bdvpg.services.BdvService; +import sc.fiji.bdvpg.sourceandconverter.SourceAndConverterUtils; + +/** + * ViewTransformSynchronizationDemo + *

+ *

+ *

+ * Author: Nicolas Chiaruttini + * 01 2020 + */ +public class ViewTransformSynchronizationDemo { + + + static boolean isSynchronizing; + + public static void main(String[] args) { + + // Initializes static SourceService and Display Service + BdvService.InitScijavaServices(); + + // load and convert an image + ImagePlus imp = IJ.openImage("src/test/resources/blobs.tif"); + RandomAccessibleInterval rai = ImageJFunctions.wrapReal(imp); + // Adds a third dimension because Bdv needs 3D + rai = Views.addDimension( rai, 0, 0 ); + + // Makes Bdv Source + Source source = new RandomAccessibleIntervalSource(rai, Util.getTypeFromInterval(rai), "blobs"); + SourceAndConverter sac = SourceAndConverterUtils.createSourceAndConverter(source); + + // Creates a BdvHandle + BdvHandle bdvHandle1 = BdvService.getSourceAndConverterDisplayService().getNewBdv(); + // Creates a BdvHandle + BdvHandle bdvHandle2 = BdvService.getSourceAndConverterDisplayService().getNewBdv(); + // Creates a BdvHandles + BdvHandle bdvHandle3 = BdvService.getSourceAndConverterDisplayService().getNewBdv(); + + BdvHandle[] bdvhs = new BdvHandle[]{bdvHandle1,bdvHandle2,bdvHandle3}; + + ViewerTransformSyncStarter syncstart = new ViewerTransformSyncStarter(bdvhs); + ViewerTransformSyncStopper syncstop = new ViewerTransformSyncStopper(syncstart.getSynchronizers()); + + syncstart.run(); + isSynchronizing = true; + + for (BdvHandle bdvHandle:bdvhs) { + // Show the sourceandconverter + BdvService.getSourceAndConverterDisplayService().show(bdvHandle, sac); + + // Adjust view on sourceandconverter + new ViewerTransformAdjuster(bdvHandle, sac).run(); + + new ClickBehaviourInstaller(bdvHandle, (x,y) -> { + if (isSynchronizing) { + syncstop.run(); + } else { + syncstart.setBdvHandleInitialReference(bdvHandle); + syncstart.run(); + } + isSynchronizing = !isSynchronizing; + }).install("Toggle Synchronization", "ctrl S"); + } + + + + + } +}