/* * 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.action.IcyAbstractAction; import icy.gui.component.BorderedPanel; import icy.image.colormap.IcyColorMap; import icy.image.colormap.IcyColorMap.IcyColorMapType; import icy.image.colormap.IcyColorMapComponent; import icy.image.colormap.IcyColorMapComponent.ControlPoint; 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.resource.icon.IcyIcon; import icy.util.ColorUtil; import icy.util.EventUtil; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Cursor; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.geom.GeneralPath; import java.awt.geom.Path2D; import java.awt.geom.Point2D; import java.util.ArrayList; import java.util.EventListener; import java.util.List; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.InputMap; import javax.swing.JComponent; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; import javax.swing.KeyStroke; import javax.swing.event.EventListenerList; /** * @author stephane */ public class ColormapViewer extends BorderedPanel implements MouseListener, MouseMotionListener, LUTChannelListener { private enum ActionType { NULL, MODIFY_CONTROLPOINT } public interface ColormapPositionListener extends EventListener { public void positionChanged(int index, int value); } /** * */ private static final long serialVersionUID = -8338817004756013113L; private static final int POINT_SIZE = 10; private static final int LINE_SIZE = 3; private static final int BORDER_WIDTH = 4; private static final int BORDER_HEIGHT = 4; private static final int MIN_INDEX = 0; private static final int MAX_INDEX = IcyColorMap.MAX_INDEX; private static final int MIN_VALUE = 0; private static final int MAX_VALUE = IcyColorMap.MAX_LEVEL; /** * associated {@link LUTChannel} */ private final LUTChannel lutChannel; /** * alpha enabled */ private boolean alphaEnabled; /** * gui */ private final JPopupMenu menu; /** * listeners */ private final EventListenerList colorMapPositionListeners; /** * cached */ final IcyColorMap colormap; /** * internals */ private float pixToIndexRatio; private float indexToPixRatio; private float pixToValueRatio; private float valueToPixRatio; private ActionType action; private IcyColorMapComponent currentComponent; ControlPoint currentControlPoint; public ColormapViewer(LUTChannel lutChannel) { super(); // dimension (don't change or you will regret !) setMinimumSize(new Dimension(100, 100)); setPreferredSize(new Dimension(240, 100)); setMaximumSize(new Dimension(Short.MAX_VALUE, Short.MAX_VALUE)); // faster draw setOpaque(true); // set border setBorder(BorderFactory.createEmptyBorder(BORDER_HEIGHT, BORDER_WIDTH, BORDER_HEIGHT, BORDER_WIDTH)); // for the input map setFocusable(true); this.lutChannel = lutChannel; colormap = lutChannel.getColorMap(); colorMapPositionListeners = new EventListenerList(); // gui menu = new JPopupMenu(); alphaEnabled = true; action = ActionType.NULL; currentComponent = null; currentControlPoint = null; // calculate ratios updateRatios(); // we can't get key events without focus and having focus here is a problem // as we can have externalized windows... // addKeyListener(this); // add listeners addMouseListener(this); addMouseMotionListener(this); buildActionMap(); } @Override public void addNotify() { super.addNotify(); // add listeners lutChannel.addListener(this); } @Override public void removeNotify() { super.removeNotify(); // remove listeners lutChannel.removeListener(this); } void buildActionMap() { final InputMap imap = getInputMap(JComponent.WHEN_FOCUSED); final ActionMap amap = getActionMap(); imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete"); amap.put("delete", new IcyAbstractAction("delete", (IcyIcon) null, "Delete control point", KeyEvent.VK_DELETE, 0) { /** * */ private static final long serialVersionUID = 2375566345981567387L; @Override protected boolean doAction(ActionEvent e) { // remove current control point if (currentControlPoint != null) { currentControlPoint.remove(); return true; } return false; } }); } /** * @return the colormap */ public IcyColorMap getColormap() { return colormap; } /** * Translate index to pixel * * @param index */ public int indexToPix(int index) { final int clientX = getClientX(); final int pix = (int) (index * indexToPixRatio) + clientX; return Math.max(Math.min(pix, getClientWidth() + clientX), clientX); } /** * Translate pixel to index * * @param pixel */ public int pixToIndex(int pixel) { final int ind = (int) ((pixel - getClientX()) * pixToIndexRatio); return Math.max(Math.min(ind, MAX_INDEX), MIN_INDEX); } /** * Translate value to pixel * * @param value * @return pixel for specified value */ public int valueToPix(int value) { final int clientY = getClientY(); final int hl = (getClientHeight() - 1) + clientY; final int pix = hl - (int) (value * valueToPixRatio); return Math.max(Math.min(pix, hl), clientY); } /** * Translate pixel to value * * @param pixel * @return value for specified pixel */ public int pixToValue(int pixel) { final int hl = (getClientHeight() - 1) + getClientY(); final int value = (int) ((hl - pixel) * pixToValueRatio); return Math.max(Math.min(value, MAX_VALUE), MIN_VALUE); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); // we do it here as componentResized event occurs after paint (and it is not time consuming) updateRatios(); final Graphics2D g2 = (Graphics2D) g.create(); final int w = getWidth(); final int h = getHeight(); // draw colored background mesh for (int i = 0; i < w; i++) { // get current color from pixel position final Color curColor = getColorFromPixel(i); final Color grayMixed = ColorUtil.mixOver(Color.gray, curColor); final Color whiteMixed = ColorUtil.mixOver(Color.white, curColor); for (int j = 0; j < h; j += 16) { // set graphics color if (((i ^ j) & 16) != 0) g2.setColor(grayMixed); else g2.setColor(whiteMixed); g2.drawLine(i, j, i, j + 15); } } if (alphaEnabled) drawColormapBand(g2, colormap.alpha); switch (colormap.getType()) { case RGB: drawColormapBand(g2, colormap.blue); drawColormapBand(g2, colormap.green); drawColormapBand(g2, colormap.red); break; case GRAY: drawColormapBand(g2, colormap.gray); break; } g2.setColor(Color.black); g2.drawRect(0, 0, w - 1, h - 1); g2.dispose(); } private void drawColormapBand(Graphics2D g, IcyColorMapComponent band) { drawColormap(g, band); drawControlPoints(g, band); } private void drawColormap(Graphics2D g, IcyColorMapComponent cmc) { final Graphics2D g2 = (Graphics2D) g.create(); // enable anti alias for better rendering g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); GeneralPath polyline = null; // the LUT is defined directly, without control points if (cmc.isRawData()) { final int x = getClientX(); final int w = getClientWidth(); polyline = new GeneralPath(Path2D.WIND_EVEN_ODD, w); int intensity = valueToPix(cmc.map[pixToIndex(0)]); polyline.moveTo(x, intensity); for (int i = x; i < (w + x); i++) { intensity = valueToPix(cmc.map[pixToIndex(i)]); polyline.lineTo(i, intensity); } } else // the LUT is defined through control points, use them. { polyline = new GeneralPath(Path2D.WIND_EVEN_ODD, cmc.getControlPointCount()); ArrayList<ControlPoint> controlPoints = cmc.getControlPoints(); int x = getPixelPosX(controlPoints.get(0)); int y = getPixelPosY(controlPoints.get(0)); polyline.moveTo(x, y); for (int i = 1; i < cmc.getControlPointCount(); i++) { x = getPixelPosX(controlPoints.get(i)); y = getPixelPosY(controlPoints.get(i)); polyline.lineTo(x, y); } } if (isFocused(cmc)) g2.setColor(Color.lightGray); else g2.setColor(Color.black); g2.setStroke(new BasicStroke(LINE_SIZE + 1)); g2.draw(polyline); g2.setColor(getColor(cmc)); g2.setStroke(new BasicStroke(LINE_SIZE)); g2.draw(polyline); g2.dispose(); } private void drawControlPoints(Graphics2D g, IcyColorMapComponent cmc) { final Graphics2D g2 = (Graphics2D) g.create(); // enable anti alias for better rendering g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); final int offset_oval = POINT_SIZE / 2; // define color final Color color = getColor(cmc); final List<ControlPoint> controlPoints; synchronized (cmc.getControlPoints()) { controlPoints = new ArrayList<IcyColorMapComponent.ControlPoint>(cmc.getControlPoints()); } for (ControlPoint controlPoint : controlPoints) { final int x = getPixelPosX(controlPoint); final int y = getPixelPosY(controlPoint); if (controlPoint.isFixed()) { // draw square control point g2.setColor(color); g2.fillRect(x - (offset_oval - 1), y - (offset_oval - 1), POINT_SIZE - 2, POINT_SIZE - 2); g2.setColor(Color.darkGray); g2.drawRect(x - (offset_oval - 1), y - (offset_oval - 1), POINT_SIZE - 2, POINT_SIZE - 2); if (isFocused(controlPoint)) g2.setColor(Color.white); else g2.setColor(Color.black); g2.drawRect(x - (offset_oval - 0), y - (offset_oval - 0), POINT_SIZE - 0, POINT_SIZE - 0); } else { // draw round control point g2.setColor(color); g2.fillOval(x - (offset_oval - 1), y - (offset_oval - 1), POINT_SIZE - 2, POINT_SIZE - 2); g2.setColor(Color.darkGray); g2.drawOval(x - (offset_oval - 1), y - (offset_oval - 1), POINT_SIZE - 2, POINT_SIZE - 2); if (isFocused(controlPoint)) g2.setColor(Color.white); else g2.setColor(Color.black); g2.drawOval(x - (offset_oval - 0), y - (offset_oval - 0), POINT_SIZE - 0, POINT_SIZE - 0); } } g2.dispose(); } public boolean isAlphaEnabled() { return alphaEnabled; } public void setAlphaEnabled(boolean value) { if (alphaEnabled != value) { alphaEnabled = value; if (!value) colormap.alpha.removeAllControlPoint(); repaint(); } } /** * set current controller or control point */ public void setCurrentElements(IcyColorMapComponent cmc, ControlPoint cp) { if (currentControlPoint != cp) { currentControlPoint = cp; repaint(); } if (currentComponent != cmc) { currentComponent = cmc; repaint(); } final int cursor; if ((cmc != null) || (cp != null)) cursor = Cursor.HAND_CURSOR; else cursor = Cursor.DEFAULT_CURSOR; // set cursor only only if different if (getCursor().getType() != cursor) setCursor(new Cursor(cursor)); } private boolean isFocused(IcyColorMapComponent cmc) { return (cmc != null) && (currentComponent == cmc); } private boolean isFocused(ControlPoint cp) { return (cp != null) && (currentControlPoint == cp); } /** * return the final color for specified index */ public Color getColor(int index) { return colormap.getColor(index); } /** * get color of specified band */ public Color getColor(IcyColorMapComponent cmc) { if (cmc == colormap.red) return Color.red; if (cmc == colormap.green) return Color.green; if (cmc == colormap.blue) return Color.blue; if (cmc == colormap.gray) return Color.gray; if (cmc == colormap.alpha) return Color.white; return Color.black; } /** * return the final color for specified pixel position */ public Color getColorFromPixel(int pixel) { return getColor(pixToIndex(pixel)); } /** * update ratios for data <--> pix conversion */ private void updateRatios() { final int w = getClientWidth(); final int h = getClientHeight(); if (w <= 0) { indexToPixRatio = 0f; pixToIndexRatio = 0f; } else { indexToPixRatio = (float) (w - 1) / (float) (IcyColorMap.SIZE - 1); if (indexToPixRatio != 0f) pixToIndexRatio = 1f / indexToPixRatio; else pixToIndexRatio = 0f; } if (h <= 0) { valueToPixRatio = 0f; pixToValueRatio = 0f; } else { valueToPixRatio = (float) (h - 1) / (float) (IcyColorMap.MAX_LEVEL); if (valueToPixRatio != 0f) pixToValueRatio = 1f / valueToPixRatio; else pixToValueRatio = 0f; } } // /** // * Check if point is over any control point // * // * @param pos // * point // * @return boolean // */ // private boolean isOverControlPoint(Point pos) // { // boolean result = false; // final IcyColorMapType type = colormap.getType(); // // // check only if alpha enabled // if (alphaEnabled) // result = result || isOverControlPoint(colormap.alpha, pos); // // // test according to display order (ARGB) // if (type == IcyColorMapType.RGB) // result = result || isOverControlPoint(colormap.red, pos) || // isOverControlPoint(colormap.green, pos) // || isOverControlPoint(colormap.blue, pos); // if (type == IcyColorMapType.GRAY) // result = result || isOverControlPoint(colormap.gray, pos); // // return result; // } // /** // * Check if point is over any control point from this band // * // * @param pos // * point // * @return boolean // */ // private boolean isOverControlPoint(IcyColorMapComponent cmc, Point pos) // { // for (ControlPoint cp : cmc.getControlPoints()) // if (isOverlapped(cp, pos)) // return true; // // return false; // } /** * Check if point is over any point in colormap * * @param pos * point * @return boolean */ public boolean isOverlapped(IcyColorMapComponent cmc, Point pos) { final int index_min = Math.max(0, pixToIndex(pos.x - LINE_SIZE)); final int index_max = Math.min(IcyColorMap.MAX_INDEX, pixToIndex(pos.x + LINE_SIZE)); for (int ind = index_min; ind < index_max; ind++) if (Point2D.distance(pos.x, pos.y, indexToPix(ind), valueToPix(cmc.map[ind])) <= (LINE_SIZE + 1)) return true; return false; } /** * Return true if pixel (x, y) is over the control point * * @param p * point * @return boolean */ public boolean isOverlapped(ControlPoint cp, Point p) { return getDistance(cp, p) <= (POINT_SIZE / 2); } /** * Return distance between control point and the specified point * * @param p * point * @return boolean */ public double getDistance(ControlPoint cp, Point p) { return Point2D.distance(p.x, p.y, indexToPix(cp.getIndex()), valueToPix(cp.getValue())); } /** * Set position from a pixel position * * @param x * @param y */ public void setPixelPosition(ControlPoint cp, int x, int y) { cp.setPosition(pixToIndex(x), pixToValue(y)); } /** * Get X pixel position * * @return X pixel position */ public int getPixelPosX(ControlPoint cp) { return indexToPix(cp.getIndex()); } /** * Get Y pixel position * * @return Y pixel position */ public int getPixelPosY(ControlPoint cp) { return valueToPix(cp.getValue()); } /** * Find the overlapped colormap band by specified point * * @param pos * point * @return ColormapController */ private IcyColorMapComponent getOverlappedColormapController(Point pos) { final IcyColorMapType type = colormap.getType(); // test according to display order (ARGB) if (type == IcyColorMapType.RGB) { if (isOverlapped(colormap.red, pos)) return colormap.red; if (isOverlapped(colormap.green, pos)) return colormap.green; if (isOverlapped(colormap.blue, pos)) return colormap.blue; } if (type == IcyColorMapType.GRAY) if (isOverlapped(colormap.gray, pos)) return colormap.gray; // check only if alpha enabled if (alphaEnabled) if (isOverlapped(colormap.alpha, pos)) return colormap.alpha; return null; } /** * Find the closest overlapped control point by specified point * * @param pos * point * @return ControlPoint */ private ControlPoint getClosestOverlappedControlPoint(Point pos) { ControlPoint point; final IcyColorMapType type = colormap.getType(); // test according to display order (RGBA) if (type == IcyColorMapType.RGB) { point = getClosestOverlappedControlPoint(colormap.red, pos); if (point != null) return point; point = getClosestOverlappedControlPoint(colormap.green, pos); if (point != null) return point; point = getClosestOverlappedControlPoint(colormap.blue, pos); if (point != null) return point; } if (type == IcyColorMapType.GRAY) { point = getClosestOverlappedControlPoint(colormap.gray, pos); if (point != null) return point; } // check only if alpha enabled if (alphaEnabled) { point = getClosestOverlappedControlPoint(colormap.alpha, pos); if (point != null) return point; } return null; } /** * Find the closest overlapped control point by specified point * * @param pos * point * @return ControlPoint */ private ControlPoint getClosestOverlappedControlPoint(IcyColorMapComponent cmc, Point pos) { final List<ControlPoint> overlapped = new ArrayList<ControlPoint>(); // add all overlapped control points to the list for (ControlPoint point : cmc.getControlPoints()) if (isOverlapped(point, pos)) overlapped.add(point); final int size = overlapped.size(); // we have at least one overlapped control point ? if (size > 0) { // find the closest from the specified position ControlPoint closestPoint = overlapped.get(0); double minDist = getDistance(closestPoint, pos); for (int i = 1; i < size; i++) { final ControlPoint currentPoint = overlapped.get(i); final double curDist = getDistance(currentPoint, pos); if (curDist < minDist) { closestPoint = currentPoint; minDist = curDist; } } return closestPoint; } return null; } /** * Set a control point to specified index and value * * @param pos * position */ ControlPoint setControlPoint(IcyColorMapComponent comp, Point pos) { return comp.setControlPoint(pixToIndex(pos.x), pixToValue(pos.y)); } /** * show popup menu */ private void showPopupMenu(final Point pos) { // rebuild menu menu.removeAll(); // keep a copy of current control point final ControlPoint cp = currentControlPoint; if (cp != null) { // fixed control point --> no popup menu if (cp.isFixed()) return; final JMenuItem removeItem = new JMenuItem("remove (Shift + Click)"); removeItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // remove the control point cp.remove(); } }); menu.add(removeItem); } else { final IcyColorMapType type = colormap.getType(); if (type == IcyColorMapType.GRAY) { final JMenuItem addCPItem = new JMenuItem("add Gray point"); addCPItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add gray control point setControlPoint(colormap.gray, pos); } }); menu.add(addCPItem); } if (type == IcyColorMapType.RGB) { final JMenuItem addCRPItem = new JMenuItem("add Red point"); final JMenuItem addCGPItem = new JMenuItem("add Green point"); final JMenuItem addCBPItem = new JMenuItem("add Blue point"); addCRPItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add red control point setControlPoint(colormap.red, pos); } }); addCGPItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add green control point setControlPoint(colormap.green, pos); } }); addCBPItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add blue control point setControlPoint(colormap.blue, pos); } }); menu.add(addCRPItem); menu.add(addCGPItem); menu.add(addCBPItem); } if (alphaEnabled) { final JMenuItem addAlphaCPItem = new JMenuItem("add Alpha point"); addAlphaCPItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { // add alpha control point setControlPoint(colormap.alpha, pos); } }); menu.add(addAlphaCPItem); } } menu.pack(); menu.validate(); // display menu menu.show(this, pos.x, pos.y); } /** * update current controller and control point from mouse position */ private void updateCurrentElements(Point pos) { final IcyColorMapComponent cmc; // by default we search for an overlapped control point final ControlPoint cp = getClosestOverlappedControlPoint(pos); // if no overlapped control point we search for overlapped controller if (cp == null) cmc = getOverlappedColormapController(pos); else cmc = null; // define current elements setCurrentElements(cmc, cp); } /** * update colormap position info */ private void updateColormapPositionInfo(Point pos) { final ControlPoint cp; final int index; final int value; if (action != ActionType.NULL) cp = currentControlPoint; else cp = getClosestOverlappedControlPoint(pos); if (cp != null) { index = cp.getIndex(); value = cp.getValue(); } else { index = pixToIndex(pos.x); value = pixToValue(pos.y); } // setToolTipText("<html>" + "index : " + index + "<br>" + "value : " + value); colormapPositionChanged(index, value); } /** * process on colormap change */ public void onColormapChanged() { // repaint the colormap repaint(); } /** * Add a listener * * @param listener */ public void addColormapPositionListener(ColormapPositionListener listener) { colorMapPositionListeners.add(ColormapPositionListener.class, listener); } /** * Remove a listener * * @param listener */ public void removeColormapPositionListener(ColormapPositionListener listener) { colorMapPositionListeners.remove(ColormapPositionListener.class, listener); } /** * mouse position on colormap info changed */ public void colormapPositionChanged(int index, int value) { for (ColormapPositionListener listener : colorMapPositionListeners.getListeners(ColormapPositionListener.class)) listener.positionChanged(index, value); } @Override public void lutChannelChanged(LUTChannelEvent e) { if (e.getType() == LUTChannelEventType.COLORMAP_CHANGED) onColormapChanged(); } @Override public void mouseClicked(MouseEvent e) { // nothing to do here } @Override public void mouseEntered(MouseEvent e) { // // get the focus while mouse is on the component // setFocusable(true); // requestFocus(); // // repaint(); // KeyboardFocusManager.getCurrentKeyboardFocusManager().downFocusCycle(this); } @Override public void mouseExited(MouseEvent e) { // clear position info colormapPositionChanged(-1, -1); // unfocus if no action if (action == ActionType.NULL) setCurrentElements(null, null); // KeyboardFocusManager.getCurrentKeyboardFocusManager().focusPreviousComponent(this); // // remove focus // setFocusable(false); // // // set focus back to last active viewer // final Viewer viewer = Icy.getMainInterface().getActiveViewer(); // if (viewer != null) // viewer.requestFocus(); // // repaint(); } @Override public void mousePressed(MouseEvent e) { final Point pos = e.getPoint(); if (EventUtil.isLeftMouseButton(e)) { // we have a selected control point ? if (currentControlPoint != null) { // Shift pressed --> remove control point if (EventUtil.isShiftDown(e)) currentControlPoint.remove(); // else we start modification else action = ActionType.MODIFY_CONTROLPOINT; } // we have a selected controller ? else if (currentComponent != null) { action = ActionType.MODIFY_CONTROLPOINT; // add a new control point to the controller which become the active control point setCurrentElements(null, setControlPoint(currentComponent, pos)); } } else if (EventUtil.isRightMouseButton(e)) { showPopupMenu(pos); } } @Override public void mouseReleased(MouseEvent e) { if (EventUtil.isLeftMouseButton(e)) { action = ActionType.NULL; updateCurrentElements(e.getPoint()); } } @Override public void mouseDragged(MouseEvent e) { final Point pos = e.getPoint(); switch (action) { case MODIFY_CONTROLPOINT: setPixelPosition(currentControlPoint, pos.x, pos.y); break; } updateColormapPositionInfo(pos); } @Override public void mouseMoved(MouseEvent e) { final Point pos = e.getPoint(); updateCurrentElements(pos); updateColormapPositionInfo(pos); } }