/* * Copyright 2010-2015 Institut Pasteur. * * This file is part of Icy. * * Icy 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 3 of the License, or * (at your option) any later version. * * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>. */ package icy.gui.lut; import icy.gui.component.math.HistogramPanel; import icy.gui.component.math.HistogramPanel.HistogramPanelListener; import icy.gui.viewer.Viewer; import icy.gui.viewer.ViewerEvent; import icy.gui.viewer.ViewerEvent.ViewerEventType; import icy.gui.viewer.ViewerListener; import icy.image.lut.LUT.LUTChannel; import icy.image.lut.LUT.LUTChannelEvent; import icy.image.lut.LUT.LUTChannelEvent.LUTChannelEventType; import icy.image.lut.LUT.LUTChannelListener; import icy.math.Histogram; import icy.math.MathUtil; import icy.math.Scaler; import icy.sequence.Sequence; import icy.sequence.SequenceEvent; import icy.sequence.SequenceEvent.SequenceEventSourceType; import icy.sequence.SequenceListener; import icy.system.thread.ThreadUtil; import icy.type.DataType; import icy.type.collection.array.Array1DUtil; import icy.util.ColorUtil; import icy.util.EventUtil; import icy.util.GraphicsUtil; import icy.util.StringUtil; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.awt.geom.Point2D; import java.lang.reflect.Array; import java.util.EventListener; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.event.EventListenerList; /** * @author stephane */ public class ScalerViewer extends JPanel implements SequenceListener, LUTChannelListener, ViewerListener { protected static enum actionType { NULL, MODIFY_LOWBOUND, MODIFY_HIGHBOUND, MODIFY_MIDDLE } public static interface ScalerPositionListener extends EventListener { public void positionChanged(double index, int value, double normalizedValue); } public class ScalerHistogramPanel extends HistogramPanel implements MouseListener, MouseMotionListener, MouseWheelListener { /** * */ private static final long serialVersionUID = -7020904979961676368L; /** * internals */ private actionType action; private final Point2D positionInfo; private boolean mouseOnLeft; public ScalerHistogramPanel(Scaler s) { super(s.getAbsLeftIn(), s.getAbsRightIn(), s.isIntegerData()); action = actionType.NULL; positionInfo = new Point2D.Double(); mouseOnLeft = false; // we want to display our own background // setOpaque(false); // dimension (don't change it or you will regret !) setMinimumSize(new Dimension(100, 100)); setPreferredSize(new Dimension(240, 100)); // add listeners addMouseListener(this); addMouseMotionListener(this); addMouseWheelListener(this); } /** * update mouse cursor */ private void updateCursor(Point pos) { final int cursor; if (action != actionType.NULL) cursor = Cursor.W_RESIZE_CURSOR; else if (isOverX(pos, getLowBoundPos()) || isOverX(pos, getHighBoundPos()) || isOverX(pos, getMiddlePos())) cursor = Cursor.HAND_CURSOR; else cursor = Cursor.DEFAULT_CURSOR; // only if different if (getCursor().getType() != cursor) setCursor(Cursor.getPredefinedCursor(cursor)); } private void setPositionInfo(double index, int value, double normalizedValue) { if ((positionInfo.getX() != index) || (positionInfo.getY() != value)) { positionInfo.setLocation(index, normalizedValue); scalerPositionChanged(index, value, normalizedValue); repaint(); } } /** * Check if Point p is over area (u, *) * * @param p * point * @param x * area position * @return boolean */ private boolean isOverX(Point p, int u) { return isOver(p.x, p.y, u, -1, ISOVER_DEFAULT_MARGIN); } /** * Check if (x, y) is over area (u, v) * * @param x * @param y * pointer * @param u * @param v * area position * @param margin * allowed margin * @return boolean */ private boolean isOver(int x, int y, int u, int v, int margin) { final boolean x_ok; final boolean y_ok; x_ok = (u == -1) || ((x >= (u - margin)) && (x <= (u + margin))); y_ok = (v == -1) || ((y >= (v - margin)) && (y <= (v + margin))); return x_ok && y_ok; } public int getLowBoundPos() { return dataToPixel(getLowBound()); } public int getHighBoundPos() { return dataToPixel(getHighBound()); } public int getMiddlePos() { return (getHighBoundPos() + getLowBoundPos()) / 2; } private void setLowBoundPos(int pos) { setLowBound(pixelToData(pos)); } private void setHighBoundPos(int pos) { setHighBound(pixelToData(pos)); } @Override protected void paintComponent(Graphics g) { updateHisto(); super.paintComponent(g); final Graphics2D g2 = (Graphics2D) g.create(); try { // display mouse position infos if (positionInfo.getX() != -1) { final int x = dataToPixel(positionInfo.getX()); final int hRange = getClientHeight() - 1; final int bottom = hRange + getClientY(); final int y = bottom - (int) (positionInfo.getY() * hRange); g2.setColor(ColorUtil.xor(getForeground())); g2.drawLine(x, bottom, x, y); } paintBounds(g2); if (!StringUtil.isEmpty(message)) { // string display g2.setFont(new Font(Font.SANS_SERIF, Font.BOLD, 12)); final Rectangle hintBounds = GraphicsUtil.getHintBounds(g2, message, 10, 4); if (mouseOnLeft) GraphicsUtil.drawHint(g2, message, getWidth() - (10 + hintBounds.width), 4, getForeground(), getBackground()); else GraphicsUtil.drawHint(g2, message, 10, 4, getForeground(), getBackground()); } } finally { g2.dispose(); } } /** * draw bounds */ private void paintBounds(Graphics2D g) { final int h = getClientHeight() - 1; final int y = getClientY(); final int lowBound = getLowBoundPos(); final int highBound = getHighBoundPos(); final int middle = getMiddlePos(); g.setColor(ColorUtil.mix(Color.blue, Color.white, false)); g.drawRect(lowBound - 2, y, 2, h); g.setColor(Color.blue); g.fillRect(lowBound - 1, y + 1, 1, h - 1); g.setColor(ColorUtil.mix(Color.red, Color.white, false)); g.drawRect(highBound - 1, y, 2, h); g.setColor(Color.red); g.fillRect(highBound, y + 1, 1, h - 1); g.setColor(ColorUtil.mix(Color.green, Color.white, false)); g.drawRect(middle - 1, y + 10, 1, h - 10); g.setColor(Color.green); g.fillRect(middle, y + 11, 0, h - 11); } @Override public void mouseClicked(MouseEvent e) { if (e.isConsumed()) return; if (e.getClickCount() == 2) { showRangeSettingDialog(); e.consume(); } } @Override public void mouseEntered(MouseEvent e) { updateCursor(e.getPoint()); } @Override public void mouseExited(MouseEvent e) { if (getCursor().getType() != Cursor.getDefaultCursor().getType()) setCursor(Cursor.getDefaultCursor()); // hide message setMessage(""); setPositionInfo(-1, -1, -1); } @Override public void mousePressed(MouseEvent e) { if (e.isConsumed()) return; final Point pos = e.getPoint(); if (EventUtil.isLeftMouseButton(e)) { if (isOverX(pos, getLowBoundPos())) action = actionType.MODIFY_LOWBOUND; else if (isOverX(pos, getHighBoundPos())) action = actionType.MODIFY_HIGHBOUND; else if (isOverX(pos, getMiddlePos())) action = actionType.MODIFY_MIDDLE; // show message if (action != actionType.NULL) { if (EventUtil.isShiftDown(e)) setMessage("GLOBAL MOVE"); else setMessage("Maintain 'Shift' for global move"); } updateCursor(e.getPoint()); e.consume(); } else if (EventUtil.isRightMouseButton(e)) { showSettingPopup(pos); e.consume(); } } @Override public void mouseReleased(MouseEvent e) { if (EventUtil.isLeftMouseButton(e)) { action = actionType.NULL; updateCursor(e.getPoint()); setMessage(""); } } @Override public void mouseDragged(MouseEvent e) { if (e.isConsumed()) return; final Point pos = e.getPoint(); final boolean shift = EventUtil.isShiftDown(e); mouseOnLeft = pos.x < (getWidth() / 2); switch (action) { case MODIFY_LOWBOUND: setLowBoundPos(pos.x); // also modify others bounds if (shift) { final double newLowBound = getLowBound(); for (LUTChannel lc : lutChannel.getLut().getLutChannels()) lc.setMin(newLowBound); } e.consume(); break; case MODIFY_HIGHBOUND: setHighBoundPos(pos.x); // also modify others bounds if (shift) { final double newHighBound = getHighBound(); for (LUTChannel lc : lutChannel.getLut().getLutChannels()) lc.setMax(newHighBound); } e.consume(); break; case MODIFY_MIDDLE: final double width = (getHighBound() - getLowBound()) / 2d; final double value = pixelToData(pos.x); final double min = value - width; final double max = value + width; // global change if (shift) { for (LUTChannel lc : lutChannel.getLut().getLutChannels()) { if ((min >= lc.getMinBound()) && (max <= lc.getMaxBound())) { lc.setMin(min); lc.setMax(max); } } } else { if ((min >= lutChannel.getMinBound()) && (max <= lutChannel.getMaxBound())) { setLowBound(min); setHighBound(max); } } e.consume(); break; } // message if (action != actionType.NULL) { if (shift) setMessage("GLOBAL MOVE"); else setMessage("Maintain 'Shift' for global move"); } if (getBinNumber() > 0) { final int bin = pixelToBin(pos.x); double index = pixelToData(pos.x); final int value = getBinSize(bin); // use integer index with integer data type if (isIntegerType()) index = Math.floor(index); if (action == actionType.NULL) { final String valueText = "value : " + MathUtil.roundSignificant(index, 5, true); final String pixelText = "pixel number : " + value; setMessage(valueText + "\n" + pixelText); // setToolTipText("<html>" + valueText + "<br>" + pixelText); } setPositionInfo(index, value, getAdjustedBinSize(bin)); } } @Override public void mouseMoved(MouseEvent e) { final Point pos = e.getPoint(); mouseOnLeft = pos.x < (getWidth() / 2); updateCursor(e.getPoint()); if (getBinNumber() > 0) { final int bin = pixelToBin(pos.x); double index = pixelToData(pos.x); final int value = getBinSize(bin); // use integer index with integer data type if (isIntegerType()) index = Math.round(index); final String valueText = "value : " + MathUtil.roundSignificant(index, 5, true); final String pixelText = "pixel number : " + value; setMessage(valueText + "\n" + pixelText); // setToolTipText("<html>" + valueText + "<br>" + pixelText); setPositionInfo(index, value, getAdjustedBinSize(bin)); } } @Override public void mouseWheelMoved(MouseWheelEvent e) { } } /** * */ private static final long serialVersionUID = -1236985071716650592L; private static final int ISOVER_DEFAULT_MARGIN = 3; /** * associated viewer & lutChannel */ Viewer viewer; LUTChannel lutChannel; /** * histogram */ private ScalerHistogramPanel histogram; private boolean histoNeedRefresh; /** * listeners */ private final EventListenerList scalerMapPositionListeners; /** * internals */ private final Runnable histoUpdater; String message; private int retry; /** * */ public ScalerViewer(Viewer viewer, LUTChannel lutChannel) { super(); this.viewer = viewer; this.lutChannel = lutChannel; message = ""; retry = 0; scalerMapPositionListeners = new EventListenerList(); histoUpdater = new Runnable() { @Override public void run() { try { // refresh histogram refreshHistoDataInternal(); } catch (Exception e) { // just ignore error, it's permitted here } } }; histogram = new ScalerHistogramPanel(lutChannel.getScaler()); // listen for need refresh event histogram.addListener(new HistogramPanelListener() { @Override public void histogramNeedRefresh(HistogramPanel source) { internalRequestHistoDataRefresh(); } }); histoNeedRefresh = false; setLayout(new BorderLayout()); add(histogram, BorderLayout.CENTER); validate(); // force first refresh internalRequestHistoDataRefresh(); // add listeners final Sequence sequence = viewer.getSequence(); if (sequence != null) sequence.addListener(this); viewer.addListener(this); lutChannel.addListener(this); } public void requestHistoDataRefresh() { internalRequestHistoDataRefresh(); } private boolean isHistoVisible() { if (!isValid()) return false; return getVisibleRect().intersects(histogram.getBounds()); } void internalRequestHistoDataRefresh() { if (isHistoVisible()) refreshHistoData(); else histoNeedRefresh = true; } void updateHisto() { if (histoNeedRefresh) { refreshHistoData(); histoNeedRefresh = false; } } private void refreshHistoData() { // send refresh operation ThreadUtil.bgRunSingle(histoUpdater); } // this method is called by processor, we don't mind about exception here void refreshHistoDataInternal() { final Histogram histo = histogram.getHistogram(); final Sequence seq = viewer.getSequence(); histogram.reset(); try { if (seq != null) { final int maxZ; final int maxT; int t = viewer.getPositionT(); int z = viewer.getPositionZ(); if (t != -1) maxT = t; else { t = 0; maxT = seq.getSizeT() - 1; } if (z != -1) maxZ = z; else { z = 0; maxZ = seq.getSizeZ() - 1; } final int c = lutChannel.getChannel(); for (; t <= maxT; t++) { for (; z <= maxZ; z++) { final Object data = seq.getDataXY(t, z, c); // need to test for empty sequence if (data != null) { final DataType dataType = seq.getDataType_(); final int len = Array.getLength(data); for (int i = 0; i < len; i++) { if ((i & 0xFFF) == 0) { // need to be recalculated so don't waste time here... if (ThreadUtil.hasWaitingBgSingleTask(histoUpdater)) return; } histo.addValue(Array1DUtil.getValue(data, i, dataType)); } } } } } retry = 0; } catch (Exception e) { // just redo it later if (retry++ < 3) refreshHistoData(); } finally { // notify that histogram computation is done histogram.done(); // histogram changed in the meantime --> recompute if (histo != histogram.getHistogram()) refreshHistoData(); } } /** * @return the histogram */ public HistogramPanel getHistogram() { return histogram; } /** * @return the histoData */ public double[] getHistoData() { return histogram.getHistogramData(); } /** * @return the scaler */ public Scaler getScaler() { return lutChannel.getScaler(); } public double getLowBound() { return lutChannel.getMin(); } public double getHighBound() { return lutChannel.getMax(); } void setLowBound(double value) { lutChannel.setMin(value); } void setHighBound(double value) { lutChannel.setMax(value); } /** * tasks to do on scaler changes */ public void onScalerChanged() { final Scaler s = getScaler(); histogram.setMinMaxIntValues(s.getAbsLeftIn(), s.getAbsRightIn(), s.isIntegerData()); // repaint component now as bounds may have changed repaint(); } /** * process on sequence change */ void onSequenceDataChanged() { final LUTViewer lutViewer = viewer.getLutViewer(); // update histogram if ((lutViewer != null) && lutViewer.getAutoRefreshHistogram()) requestHistoDataRefresh(); } /** * process on position changed */ private void onPositionChanged() { final LUTViewer lutViewer = viewer.getLutViewer(); // update histogram if ((lutViewer != null) && lutViewer.getAutoRefreshHistogram()) requestHistoDataRefresh(); } /** * @return the message */ public String getMessage() { return message; } /** * @param value * the message to set */ public void setMessage(String value) { if (!StringUtil.equals(message, value)) { message = value; repaint(); } } /** * Should be called when histogram scaling type changed */ public void scaleTypeChanged(boolean log) { if (log) histogram.setLogScaling(true); else histogram.setLogScaling(false); } /** * show popup menu */ protected void showSettingPopup(final Point pos) { // rebuild menu final JPopupMenu menu = new JPopupMenu("Actions"); final JMenuItem refreshItem = new JMenuItem("Refresh now"); refreshItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { requestHistoDataRefresh(); } }); final JMenuItem setBoundsItem = new JMenuItem("Set range"); setBoundsItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { showRangeSettingDialog(); } }); menu.add(refreshItem); menu.add(setBoundsItem); menu.pack(); menu.validate(); // display menu menu.show(this, pos.x, pos.y); } void showRangeSettingDialog() { final ScalerBoundsSettingDialog boundsSettingDialog = new ScalerBoundsSettingDialog(lutChannel); boundsSettingDialog.pack(); boundsSettingDialog.setLocationRelativeTo(this); boundsSettingDialog.setVisible(true); } /** * Add a listener * * @param listener */ public void addScalerPositionListener(ScalerPositionListener listener) { scalerMapPositionListeners.add(ScalerPositionListener.class, listener); } /** * Remove a listener * * @param listener */ public void removeScalerPositionListener(ScalerPositionListener listener) { scalerMapPositionListeners.remove(ScalerPositionListener.class, listener); } /** * mouse position on scaler info changed */ public void scalerPositionChanged(double index, int value, double normalizedValue) { for (ScalerPositionListener listener : scalerMapPositionListeners.getListeners(ScalerPositionListener.class)) listener.positionChanged(index, value, normalizedValue); } @Override public void lutChannelChanged(LUTChannelEvent event) { if (event.getType() == LUTChannelEventType.SCALER_CHANGED) onScalerChanged(); } @Override public void viewerChanged(ViewerEvent event) { if (event.getType() == ViewerEventType.POSITION_CHANGED) onPositionChanged(); } @Override public void viewerClosed(Viewer viewer) { viewer.removeListener(this); } @Override public void sequenceChanged(SequenceEvent sequenceEvent) { if (sequenceEvent.getSourceType() == SequenceEventSourceType.SEQUENCE_DATA) onSequenceDataChanged(); } @Override public void sequenceClosed(Sequence sequence) { sequence.removeListener(this); } }