/* * #%L * Bio-Formats Plugins for ImageJ: a collection of ImageJ plugins including the * Bio-Formats Importer, Bio-Formats Exporter, Bio-Formats Macro Extensions, * Data Browser and Stack Slicer. * %% * Copyright (C) 2006 - 2015 Open Microscopy Environment: * - Board of Regents of the University of Wisconsin-Madison * - Glencoe Software, Inc. * - University of Dundee * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as * published by the Free Software Foundation, either version 2 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public * License along with this program. If not, see * <http://www.gnu.org/licenses/gpl-2.0.html>. * #L% */ package loci.plugins.util; import ij.IJ; import ij.ImageJ; import ij.ImagePlus; import ij.ImageStack; import ij.gui.ImageCanvas; import ij.gui.StackWindow; import ij.io.FileInfo; import java.awt.BorderLayout; import java.awt.Button; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.Label; import java.awt.Panel; import java.awt.Rectangle; import java.awt.Scrollbar; import java.awt.event.ActionEvent; import java.awt.event.AdjustmentEvent; import java.awt.event.MouseWheelEvent; import java.io.IOException; import javax.swing.JSpinner; import javax.swing.SpinnerNumberModel; import javax.xml.parsers.ParserConfigurationException; import loci.formats.FormatTools; import loci.formats.cache.Cache; import loci.formats.gui.CacheIndicator; import loci.formats.gui.XMLWindow; import org.xml.sax.SAXException; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; /** * Extension of StackWindow with additional UI trimmings for animation, * virtual stack caching options, metadata, and general beautification. */ public class DataBrowser extends StackWindow { // -- Constants -- protected static final int MIN_BROWSER_WIDTH = 400; // -- Fields -- // protected volatile boolean done; protected JSpinner fpsSpin; protected Button animate, options, metadata; protected boolean anim = false; protected boolean allowShow = false; protected XMLWindow metaWindow; protected BrowserOptionsWindow optionsWindow; protected String xml; protected Scrollbar zScroll, cScroll, tScroll; protected Scrollbar[] cSliders; protected int[] cLengths; protected int[] cIndex; //private int slice; // -- Constructors -- public DataBrowser(ImagePlus imp) { this(imp, null, null, null, null); } public DataBrowser(final ImagePlus imp, ImageCanvas ic, String[] channels, int[] cLengths) { this(imp, ic, channels, cLengths, null); } public DataBrowser(final ImagePlus imp, ImageCanvas ic, String[] channels, int[] cLengths, XMLWindow xmlWindow) { super(imp, ic); if (channels == null || channels.length == 0) { channels = new String[] {"Channel"}; } if (cLengths == null || cLengths.length == 0) { cLengths = new int[] {imp.getNChannels()}; } this.cLengths = cLengths; cIndex = new int[cLengths.length]; // build metadata window metaWindow = xmlWindow; if (metaWindow == null) { metaWindow = new XMLWindow("OME Metadata - " + getTitle()); } // build fancy UI widgets while (getComponentCount() > 1) remove(1); Panel controls = new Panel() { @Override public Dimension getPreferredSize() { int minWidth = MIN_BROWSER_WIDTH; int w = imp.getCanvas().getWidth(); if (w < minWidth) w = minWidth; int h = super.getPreferredSize().height; return new Dimension(w, h); } }; String cols = "5dlu, right:pref, 3dlu, pref:grow, 5dlu, pref, 5dlu, pref, 5dlu"; // <-labels-> <------sliders------> <misc> String rows = "4dlu, pref, 3dlu, pref"; // <Z-> <T-> <C-> for (int i=0; i<channels.length; i++) rows += ", 3dlu, pref"; rows += ", 6dlu"; controls.setLayout(new FormLayout(cols, rows)); controls.setBackground(Color.white); int c = imp.getNChannels(); int z = imp.getNSlices(); int t = imp.getNFrames(); boolean hasZ = z > 1; boolean hasC = c > 1; boolean hasT = t > 1; // remove everything except the image canvas Component[] comps = getComponents(); for (Component comp : comps) { if (!(comp instanceof ImageCanvas)) remove(comp); } ImageJ ij = IJ.getInstance(); if (hasC) { cScroll = new Scrollbar(Scrollbar.HORIZONTAL, 1, 1, 1, c + 1); add(cScroll); if (ij != null) cScroll.addKeyListener(ij); cScroll.addAdjustmentListener(this); // prevents scroll bar from blinking on Windows cScroll.setFocusable(false); cScroll.setUnitIncrement(1); cScroll.setBlockIncrement(1); } if (hasZ) { zScroll = new Scrollbar(Scrollbar.HORIZONTAL, 1, 1, 1, z + 1); add(zScroll); if (ij != null) zScroll.addKeyListener(ij); zScroll.addAdjustmentListener(this); zScroll.setFocusable(false); int blockIncrement = Math.max(z / 10, 1); zScroll.setUnitIncrement(1); zScroll.setBlockIncrement(blockIncrement); } if (hasT) { tScroll = new Scrollbar(Scrollbar.HORIZONTAL, 1, 1, 1, t + 1); add(tScroll); if (ij != null) tScroll.addKeyListener(ij); tScroll.addAdjustmentListener(this); tScroll.setFocusable(false); int blockIncrement = Math.max(t / 10, 1); tScroll.setUnitIncrement(1); tScroll.setBlockIncrement(blockIncrement); } Label zLabel = new Label("Z-depth"); zLabel.setEnabled(hasZ); Label tLabel = new Label("Time"); tLabel.setEnabled(hasT); Label[] cLabels = new Label[channels.length]; for (int i=0; i<channels.length; i++) { cLabels[i] = new Label(channels[i]); cLabels[i].setEnabled(hasC); } final Scrollbar zSlider = hasZ ? zScroll : makeDummySlider(); final Scrollbar tSlider = hasT ? tScroll : makeDummySlider(); cSliders = new Scrollbar[channels.length]; Panel[] cPanels = new Panel[channels.length]; for (int i=0; i<channels.length; i++) { if (channels.length == 1) { cSliders[i] = hasC ? cScroll : makeDummySlider(); } else if (cLengths[i] == 1) { cSliders[i] = makeDummySlider(); } else { cSliders[i] = new Scrollbar(Scrollbar.HORIZONTAL, 1, 1, 1, cLengths[i] + 1); cSliders[i].addAdjustmentListener(this); } cPanels[i] = makeHeavyPanel(cSliders[i]); } Panel zPanel = makeHeavyPanel(zSlider); Panel tPanel = makeHeavyPanel(tSlider); fpsSpin = new JSpinner(new SpinnerNumberModel(10, 1, 99, 1)); fpsSpin.setToolTipText("Animation rate in frames per second"); Label fpsLabel = new Label(" FPS"); Panel fpsPanel = new Panel(); fpsPanel.setLayout(new BorderLayout()); fpsPanel.add(fpsSpin, BorderLayout.CENTER); fpsPanel.add(fpsLabel, BorderLayout.EAST); ImageStack stack = imp.getStack(); if (stack instanceof BFVirtualStack) { BFVirtualStack bfvs = (BFVirtualStack) stack; Cache cache = bfvs.getCache(); if (hasZ) { CacheIndicator zCache = new CacheIndicator(cache, channels.length, zSlider, 10, 20); zPanel.add(zCache, BorderLayout.SOUTH); } if (hasT) { CacheIndicator tCache = new CacheIndicator(cache, channels.length + 1, tSlider, 10, 20); tPanel.add(tCache, BorderLayout.SOUTH); } for (int i=0; i<channels.length; i++) { if (cLengths[i] > 1) { CacheIndicator cCache = new CacheIndicator(cache, i, cSliders[i], 10, 20); cPanels[i].add(cCache, BorderLayout.SOUTH); } } String[] axes = new String[channels.length + 2]; System.arraycopy(channels, 0, axes, 0, channels.length); axes[channels.length] = "Z"; axes[channels.length + 1] = "T"; optionsWindow = new BrowserOptionsWindow("Options - " + getTitle(), cache, axes); } animate = new Button("Animate"); animate.addActionListener(this); fpsSpin.setEnabled(hasT); fpsLabel.setEnabled(hasT); animate.setEnabled(hasT); options = new Button("Options"); options.addActionListener(this); options.setEnabled(optionsWindow != null); metadata = new Button("Metadata"); metadata.addActionListener(this); metadata.setEnabled(false); CellConstraints cc = new CellConstraints(); controls.add(zLabel, cc.xy(2, 2)); controls.add(zPanel, cc.xyw(4, 2, 3)); controls.add(fpsPanel, cc.xy(8, 2)); controls.add(tLabel, cc.xy(2, 4)); controls.add(tPanel, cc.xyw(4, 4, 3)); controls.add(animate, cc.xy(8, 4)); int row = 6; // place Options and Metadata buttons intelligently if (channels.length == 1) { controls.add(options, cc.xy(6, row)); controls.add(metadata, cc.xy(8, row)); controls.add(cLabels[0], cc.xy(2, row)); controls.add(cPanels[0], cc.xy(4, row)); } else { controls.add(options, cc.xy(8, row)); controls.add(metadata, cc.xy(8, row + 2)); for (int i=0; i<channels.length; i++) { int w = i < 2 ? 3 : 5; controls.add(cLabels[i], cc.xy(2, row)); controls.add(cPanels[i], cc.xyw(4, row, w)); row += 2; } } add(controls, BorderLayout.SOUTH); FileInfo fi = imp.getOriginalFileInfo(); if (fi.description != null && fi.description.startsWith("<?xml")) { setXML(fi.description); } allowShow = true; pack(); setVisible(true); // start up animation thread if (hasT) { // NB: Cannot implement Runnable because one of the superclasses does so // for its SliceSelector thread, and overriding results in a conflict. new Thread("DataBrowser-Animation") { @Override public void run() { while (isVisible()) { int ms = 200; if (anim) { int c = imp.getChannel(); int z = imp.getSlice(); int t = imp.getFrame() + 1; int sizeT = tSlider.getMaximum() - 1; if (t > sizeT) t = 1; imp.setPosition(c, z, t); syncSliders(); int fps = ((Number) fpsSpin.getValue()).intValue(); ms = 1000 / fps; } try { Thread.sleep(ms); } catch (InterruptedException exc) { } } } }.start(); } } // -- DataBrowser methods -- /** * Sets XML block associated with this window. This information will be * displayed in a tree structure when the Metadata button is clicked. */ public void setXML(String xml) { try { metaWindow.setXML(xml); } catch (ParserConfigurationException exc) { exc.printStackTrace(); } catch (SAXException exc) { exc.printStackTrace(); } catch (IOException exc) { exc.printStackTrace(); } metadata.setEnabled(metaWindow.getDocument() != null); } /** Toggles whether the data browser is animating. */ public void toggleAnimation() { animate.setLabel(anim ? "Animate" : "Stop"); anim = !anim; } /** Displays the caching options window onscreen. */ public void showOptionsWindow() { // center window and show Rectangle r = getBounds(); Dimension w = optionsWindow.getSize(); int x = Math.max(5, r.x + (r.width - w.width) / 2); int y = Math.max(5, r.y + (r.height - w.height) / 2); optionsWindow.setLocation(x, y); optionsWindow.setVisible(true); } /** Displays the OME-XML metadata window onscreen. */ public void showMetadataWindow() { // center window and show Rectangle r = getBounds(); Dimension w = metaWindow.getSize(); int x = r.x + (r.width - w.width) / 2; int y = r.y + (r.height - w.height) / 2; if (x < 5) x = 5; if (y < 5) y = 5; metaWindow.setLocation(x, y); metaWindow.setVisible(true); } // -- Window methods -- /** Overridden pack method to allow us to delay initial window sizing. */ @Override public void pack() { if (allowShow) super.pack(); } // -- Component methods -- /** Overridden show method to allow us to delay initial window display. */ @Override public void setVisible(boolean b) { if (allowShow) super.setVisible(b); } // -- ActionListener methods -- @Override public void actionPerformed(ActionEvent e) { Object src = e.getSource(); if (src == animate) toggleAnimation(); else if (src == options) showOptionsWindow(); else if (src == metadata) showMetadataWindow(); // NB: Do not eat superclass events. Om nom nom nom. :-) else super.actionPerformed(e); } // -- AdjustmentListener methods -- @Override public synchronized void adjustmentValueChanged(AdjustmentEvent e) { super.adjustmentValueChanged(e); syncPlane(); } // -- MouseWheelListener methods -- @Override public void mouseWheelMoved(MouseWheelEvent event) { super.mouseWheelMoved(event); syncSliders(); } /* // -- Runnable methods -- @Override public void run() { while (!done) { synchronized (this) { try { wait(); } catch (InterruptedException e) { } } if (done) return; if (slice > 0 && slice != imp.getCurrentSlice()) { imp.setSlice(slice); slice = 0; } } } */ // -- Helper methods -- /* private void updateSlice() { int sizeZ = imp.getNSlices(); int sizeC = imp.getNChannels(); int sizeT = imp.getNFrames(); int[] dims = new int[] {sizeZ, sizeC, sizeT}; int z = imp.getSlice() - 1; int c = imp.getChannel() - 1; int t = imp.getFrame() - 1; int[] pos = new int[] {z, c, t}; slice = FormatTools.positionToRaster(dims, pos) + 1; } */ /** Updates the ImagePlus's displayed plane to match the slider values. */ private void syncPlane() { for (int i=0; i<cSliders.length; i++) { cIndex[i] = cSliders[i].getValue() - 1; } int c = FormatTools.positionToRaster(cLengths, cIndex) + 1; int z = zScroll == null ? 1 : zScroll.getValue(); int t = tScroll == null ? 1 : tScroll.getValue(); setPosition(c, z, t); imp.setPosition(c, z, t); } /** Updates the slider values to match the ImagePlus's displayed plane. */ private void syncSliders() { cIndex = FormatTools.rasterToPosition(cLengths, imp.getChannel() - 1); for (int i=0; i<cSliders.length; i++) { cSliders[i].setValue(cIndex[i] + 1); } if (zScroll != null) zScroll.setValue(imp.getSlice()); if (tScroll != null) tScroll.setValue(imp.getFrame()); } protected static Scrollbar makeDummySlider() { Scrollbar scrollbar = new Scrollbar(Scrollbar.HORIZONTAL, 1, 1, 1, 2); scrollbar.setFocusable(false); scrollbar.setUnitIncrement(1); scrollbar.setBlockIncrement(1); scrollbar.setEnabled(false); return scrollbar; } /** Makes AWT play nicely with Swing components. */ protected static Panel makeHeavyPanel(Component c) { Panel panel = new Panel(); panel.setLayout(new BorderLayout()); panel.add(c, BorderLayout.CENTER); return panel; } }