/* * 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.roi; import icy.canvas.IcyCanvas; import icy.common.CollapsibleEvent; import icy.common.UpdateEventHandler; import icy.common.listener.ChangeListener; import icy.file.xml.XMLPersistent; import icy.gui.inspector.RoisPanel; import icy.main.Icy; import icy.painter.Overlay; import icy.plugin.abstract_.Plugin; import icy.plugin.interface_.PluginROI; import icy.preferences.GeneralPreferences; import icy.resource.ResourceUtil; import icy.roi.ROIEvent.ROIEventType; import icy.roi.ROIEvent.ROIPointEventType; import icy.sequence.Sequence; import icy.system.IcyExceptionHandler; import icy.type.point.Point5D; import icy.type.rectangle.Rectangle5D; import icy.util.ClassUtil; import icy.util.ColorUtil; import icy.util.EventUtil; import icy.util.ShapeUtil.BooleanOperator; import icy.util.StringUtil; import icy.util.XMLUtil; import java.awt.Color; import java.awt.Image; import java.awt.Rectangle; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import org.w3c.dom.Node; import plugins.kernel.roi.roi2d.ROI2DArea; import plugins.kernel.roi.roi3d.ROI3DArea; import plugins.kernel.roi.roi4d.ROI4DArea; import plugins.kernel.roi.roi5d.ROI5DArea; public abstract class ROI implements ChangeListener, XMLPersistent { public static class ROIIdComparator implements Comparator<ROI> { @Override public int compare(ROI roi1, ROI roi2) { if (roi1 == roi2) return 0; if (roi1 == null) return -1; if (roi2 == null) return 1; if (roi1.id < roi2.id) return -1; if (roi1.id > roi2.id) return 1; return 0; } } public static class ROINameComparator implements Comparator<ROI> { @Override public int compare(ROI roi1, ROI roi2) { if (roi1 == roi2) return 0; if (roi1 == null) return -1; if (roi2 == null) return 1; return roi1.getName().compareTo(roi2.getName()); } } /** * Group if for ROI (used to do group type operation) * * @author Stephane */ public static enum ROIGroupId { A, B } public static final String ID_ROI = "roi"; public static final String ID_CLASSNAME = "classname"; public static final String ID_ID = "id"; public static final String ID_NAME = "name"; public static final String ID_GROUPID = "groupid"; public static final String ID_COLOR = "color"; public static final String ID_STROKE = "stroke"; public static final String ID_OPACITY = "opacity"; public static final String ID_SELECTED = "selected"; public static final String ID_READONLY = "readOnly"; public static final String ID_SHOWNAME = "showName"; public static final ROIIdComparator idComparator = new ROIIdComparator(); public static final ROINameComparator nameComparator = new ROINameComparator(); public static final double DEFAULT_STROKE = 2; public static final Color DEFAULT_COLOR = Color.GREEN; public static final float DEFAULT_OPACITY = 0.3f; /** * @deprecated Use {@link #DEFAULT_COLOR} instead. */ @Deprecated public static final Color DEFAULT_NORMAL_COLOR = DEFAULT_COLOR; public static final String PROPERTY_NAME = ID_NAME; public static final String PROPERTY_GROUPID = ID_GROUPID; public static final String PROPERTY_ICON = "icon"; public static final String PROPERTY_CREATING = "creating"; public static final String PROPERTY_READONLY = ID_READONLY; public static final String PROPERTY_SHOWNAME = ID_SHOWNAME; public static final String PROPERTY_COLOR = ID_COLOR; public static final String PROPERTY_STROKE = ID_STROKE; public static final String PROPERTY_OPACITY = ID_OPACITY; // special properties for ROI_CHANGED event public static final String ROI_CHANGED_POSITION = "position"; public static final String ROI_CHANGED_ALL = "all"; /** * Create a ROI from its class name or {@link PluginROI} class name. * * @param className * roi class name or {@link PluginROI} class name. * @return ROI (null if command is an incorrect ROI class name) */ public static ROI create(String className) { ROI result = null; try { // search for the specified className final Class<?> clazz = ClassUtil.findClass(className); // class found if (clazz != null) { try { // we first check if we have a PluginROI class here final Class<? extends PluginROI> roiClazz = clazz.asSubclass(PluginROI.class); // create the plugin final PluginROI plugin = roiClazz.newInstance(); // create ROI result = plugin.createROI(); // set ROI icon from plugin icon final Image icon = ((Plugin) plugin).getDescriptor().getIconAsImage(); if (icon != null) result.setIcon(icon); } catch (ClassCastException e0) { // check if this is a ROI class final Class<? extends ROI> roiClazz = clazz.asSubclass(ROI.class); // default constructor final Constructor<? extends ROI> constructor = roiClazz.getConstructor(new Class[] {}); // build ROI result = constructor.newInstance(); } } } catch (NoSuchMethodException e) { IcyExceptionHandler.handleException(new NoSuchMethodException("Default constructor not found in class '" + className + "', cannot create the ROI."), true); } catch (ClassNotFoundException e) { IcyExceptionHandler.handleException(new ClassNotFoundException("Cannot find '" + className + "' class, cannot create the ROI."), true); } catch (Exception e) { IcyExceptionHandler.handleException(e, true); } return result; } /** * Create a ROI from its class name or {@link PluginROI} class name (interactive mode). * * @param className * roi class name or {@link PluginROI} class name. * @param imagePoint * initial point position in image coordinates (interactive mode). * @return ROI (null if the specified class name is an incorrect ROI class name) */ public static ROI create(String className, Point5D imagePoint) { if (imagePoint == null) return create(className); ROI result = null; try { // search for the specified className final Class<?> clazz = ClassUtil.findClass(className); // class found if (clazz != null) { try { // we first check if we have a PluginROI class here final Class<? extends PluginROI> roiClazz = clazz.asSubclass(PluginROI.class); // create the plugin final PluginROI plugin = roiClazz.newInstance(); // then create ROI with the Point5D constructor result = plugin.createROI(imagePoint); // not supported --> use default constructor if (result == null) result = plugin.createROI(); // set ROI icon from plugin icon final Image icon = ((Plugin) plugin).getDescriptor().getIconAsImage(); if (icon != null) result.setIcon(icon); } catch (ClassCastException e0) { // check if this is a ROI class final Class<? extends ROI> roiClazz = clazz.asSubclass(ROI.class); try { // get constructor (Point5D) final Constructor<? extends ROI> constructor = roiClazz .getConstructor(new Class[] {Point5D.class}); // build ROI result = constructor.newInstance(new Object[] {imagePoint}); } catch (NoSuchMethodException e1) { // try default constructor as last chance... final Constructor<? extends ROI> constructor = roiClazz.getConstructor(new Class[] {}); // build ROI result = constructor.newInstance(); } } } } catch (Exception e) { IcyExceptionHandler.handleException(new NoSuchMethodException("Default constructor not found in class '" + className + "', cannot create the ROI."), true); } return result; } /** * @deprecated Use {@link #create(String, Point5D)} instead */ @Deprecated public static ROI create(String className, Point2D imagePoint) { return create(className, new Point5D.Double(imagePoint.getX(), imagePoint.getY(), -1d, -1d, -1d)); } /** * @deprecated Use {@link ROI#create(String, Point5D)} instead. */ @Deprecated public static ROI create(String className, Sequence seq, Point2D imagePoint, boolean creation) { final ROI result = create(className, imagePoint); // attach to sequence once ROI is initialized if ((seq != null) && (result != null)) seq.addROI(result, true); return result; } /** * Create a ROI from a xml definition * * @param node * xml node defining the roi * @return ROI (null if node is an incorrect ROI definition) */ public static ROI createFromXML(Node node) { if (node == null) return null; final String className = XMLUtil.getElementValue(node, ID_CLASSNAME, ""); if (StringUtil.isEmpty(className)) return null; final ROI roi = create(className); // load properties from XML if (roi != null) { // error while loading infos --> return null if (!roi.loadFromXML(node)) return null; roi.setSelected(false); } return roi; } public static double getAdjustedStroke(IcyCanvas canvas, double stroke) { final double adjStrkX = canvas.canvasToImageLogDeltaX((int) stroke); final double adjStrkY = canvas.canvasToImageLogDeltaY((int) stroke); return Math.max(adjStrkX, adjStrkY); } /** * Return ROI of specified type from the ROI list */ public static List<ROI> getROIList(List<? extends ROI> rois, Class<? extends ROI> clazz) { final List<ROI> result = new ArrayList<ROI>(); for (ROI roi : rois) if (clazz.isInstance(roi)) result.add(roi); return result; } /** * @deprecated Use {@link #getROIList(List, Class)} instead. */ @Deprecated public static ArrayList<ROI> getROIList(ArrayList<? extends ROI> rois, Class<? extends ROI> clazz) { final ArrayList<ROI> result = new ArrayList<ROI>(); for (ROI roi : rois) if (clazz.isInstance(roi)) result.add(roi); return result; } /** * @deprecated Use {@link #getROIList(List, Class)} instead. */ @Deprecated public static List<ROI> getROIList(ROI rois[], Class<? extends ROI> clazz) { final List<ROI> result = new ArrayList<ROI>(); for (ROI roi : rois) if (clazz.isInstance(roi)) result.add(roi); return result; } /** * Return the number of ROI defined in the specified XML node. * * @param node * XML node defining the ROI list * @return the number of ROI defined in the XML node. */ public static int getROICount(Node node) { if (node != null) { final List<Node> nodesROI = XMLUtil.getChildren(node, ID_ROI); if (nodesROI != null) return nodesROI.size(); } return 0; } /** * Return a list of ROI from a XML node. * * @param node * XML node defining the ROI list * @return a list of ROI */ public static List<ROI> loadROIsFromXML(Node node) { final List<ROI> result = new ArrayList<ROI>(); if (node != null) { final List<Node> nodesROI = XMLUtil.getChildren(node, ID_ROI); if (nodesROI != null) { for (Node n : nodesROI) { final ROI roi = createFromXML(n); if (roi != null) result.add(roi); } } } return result; } /** * @deprecated Use {@link #loadROIsFromXML(Node)} instead. */ @Deprecated public static List<ROI> getROIsFromXML(Node node) { return loadROIsFromXML(node); } /** * Set a list of ROI to a XML node. * * @param node * XML node which is used to store the list of ROI * @param rois * the list of ROI to store in the XML node */ public static void saveROIsToXML(Node node, List<ROI> rois) { if (node != null) { for (ROI roi : rois) { final Node nodeROI = XMLUtil.addElement(node, ID_ROI); if (!roi.saveToXML(nodeROI)) { XMLUtil.removeNode(node, nodeROI); System.err.println("Error: the roi " + roi.getName() + " was not correctly saved to XML !"); } } } } /** * @deprecated Use {@link #saveROIsToXML(Node, List)} instead */ @Deprecated public static void setROIsFromXML(Node node, List<ROI> rois) { saveROIsToXML(node, rois); } public static Color getDefaultColor() { return new Color(GeneralPreferences.getPreferencesRoiOverlay().getInt(ID_COLOR, DEFAULT_COLOR.getRGB())); } public static float getDefaultOpacity() { return GeneralPreferences.getPreferencesRoiOverlay().getFloat(ID_OPACITY, DEFAULT_OPACITY); } public static double getDefaultStroke() { return GeneralPreferences.getPreferencesRoiOverlay().getDouble(ID_STROKE, DEFAULT_STROKE); } public static boolean getDefaultShowName() { return GeneralPreferences.getPreferencesRoiOverlay().getBoolean(ID_SHOWNAME, false); } public static void setDefaultColor(Color value) { GeneralPreferences.getPreferencesRoiOverlay().putInt(ID_COLOR, value.getRGB()); } public static void setDefaultOpacity(float value) { GeneralPreferences.getPreferencesRoiOverlay().putFloat(ID_OPACITY, value); } public static void setDefaultStroke(double value) { GeneralPreferences.getPreferencesRoiOverlay().putDouble(ID_STROKE, value); } public static void setDefaultShowName(boolean value) { GeneralPreferences.getPreferencesRoiOverlay().putBoolean(ID_SHOWNAME, value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageDeltaX(IcyCanvas canvas, int value) { return canvas.canvasToImageDeltaX(value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaX(IcyCanvas canvas, double value, double logFactor) { return canvas.canvasToImageLogDeltaX((int) value, logFactor); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaX(IcyCanvas canvas, double value) { return canvas.canvasToImageLogDeltaX((int) value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaX(IcyCanvas canvas, int value, double logFactor) { return canvas.canvasToImageLogDeltaX(value, logFactor); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaX(IcyCanvas canvas, int value) { return canvas.canvasToImageLogDeltaX(value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageDeltaY(IcyCanvas canvas, int value) { return canvas.canvasToImageDeltaY(value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaY(IcyCanvas canvas, double value, double logFactor) { return canvas.canvasToImageLogDeltaY((int) value, logFactor); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaY(IcyCanvas canvas, double value) { return canvas.canvasToImageLogDeltaY((int) value); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaY(IcyCanvas canvas, int value, double logFactor) { return canvas.canvasToImageLogDeltaY(value, logFactor); } /** * @deprecated Use {@link IcyCanvas} methods instead */ @Deprecated public static double canvasToImageLogDeltaY(IcyCanvas canvas, int value) { return canvas.canvasToImageLogDeltaY(value); } /** * Abstract basic class for ROI overlay */ public abstract class ROIPainter extends Overlay { /** * Overlay properties */ protected double stroke; protected Color color; protected float opacity; protected boolean showName; /** * Last mouse position (image coordinates). * Needed for some internals operation */ protected final Point5D.Double mousePos; public ROIPainter() { super("ROI painter", OverlayPriority.SHAPE_NORMAL); stroke = getDefaultStroke(); color = getDefaultColor(); opacity = getDefaultOpacity(); showName = getDefaultShowName(); mousePos = new Point5D.Double(); // we fix the ROI overlay canBeRemoved = false; } /** * Return the ROI painter stroke. */ public double getStroke() { return painter.stroke; } /** * Get adjusted stroke for the current canvas transformation */ public double getAdjustedStroke(IcyCanvas canvas) { return ROI.getAdjustedStroke(canvas, getStroke()); } /** * Set ROI painter stroke. */ public void setStroke(double value) { if (stroke != value) { stroke = value; // painter changed event is done on property changed ROI.this.propertyChanged(PROPERTY_STROKE); } } /** * Returns the content opacity factor (0 = transparent while 1 means opaque). */ public float getOpacity() { return opacity; } /** * Sets the content opacity factor (0 = transparent while 1 means opaque). */ public void setOpacity(float value) { if (opacity != value) { opacity = value; // painter changed event is done on property changed ROI.this.propertyChanged(PROPERTY_OPACITY); } } /** * Returns the color for focused state */ public Color getFocusedColor() { final int lum = ColorUtil.getLuminance(getColor()); if (lum < (256 - 32)) return Color.white; return Color.gray; } /** * @deprecated */ @Deprecated public Color getSelectedColor() { return getColor(); } /** * Returns the color used to display the ROI depending its current state. */ public Color getDisplayColor() { if (isFocused()) return getFocusedColor(); return getColor(); } /** * Return the ROI painter base color. */ public Color getColor() { return color; } /** * Set the ROI painter base color. */ public void setColor(Color value) { if ((color != null) && (color != value)) { color = value; // painter changed event is done on property changed ROI.this.propertyChanged(PROPERTY_COLOR); } } /** * Return <code>true</code> if ROI painter should display the ROI name at draw time.<br> */ public boolean getShowName() { return showName; } /** * When set to <code>true</code> the ROI painter display the ROI name at draw time. */ public void setShowName(boolean value) { if (showName != value) { showName = value; ROI.this.propertyChanged(PROPERTY_SHOWNAME); } } /** * @deprecated Selected color is now automatically calculated */ @Deprecated public void setSelectedColor(Color value) { // } /** * @deprecated Better to retrieve mouse position from the {@link IcyCanvas} object. */ @Deprecated public Point5D.Double getMousePos() { return mousePos; } /** * @deprecated Better to retrieve mouse position from the {@link IcyCanvas} object. */ @Deprecated public void setMousePos(Point5D pos) { if (!mousePos.equals(pos)) mousePos.setLocation(pos); } public void computePriority() { if (isFocused()) setPriority(OverlayPriority.SHAPE_TOP); else if (isSelected()) setPriority(OverlayPriority.SHAPE_HIGH); else setPriority(OverlayPriority.SHAPE_LOW); } @Override public boolean isReadOnly() { // use ROI read only property return ROI.this.isReadOnly(); } @Override public String getName() { // use ROI name property return ROI.this.getName(); } @Override public void setName(String name) { // modifying layer name modify ROI name ROI.this.setName(name); } /** * Update the focus state of the ROI */ protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // empty implementation by default return false; } /** * Update the selection state of the ROI (default implementation) */ protected boolean updateSelect(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // nothing to do if the ROI does not have focus if (!isFocused()) return false; // union selection if (EventUtil.isShiftDown(e)) { // not already selected --> add ROI to selection if (!isSelected()) { setSelected(true); return true; } } else if (EventUtil.isControlDown(e)) // switch selection { // inverse state setSelected(!isSelected()); return true; } else // exclusive selection { // not selected --> exclusive ROI selection if (!isSelected()) { // exclusive selection can fail if we use embedded ROI (as ROIStack) if (!canvas.getSequence().setSelectedROI(ROI.this)) ROI.this.setSelected(true); return true; } } return false; } protected boolean updateDrag(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // empty implementation by default return false; } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!e.isConsumed()) { if (isActiveFor(canvas)) { switch (e.getKeyCode()) { case KeyEvent.VK_ESCAPE: // roi selected ? --> global unselect ROI if (isSelected()) { canvas.getSequence().setSelectedROI(null); e.consume(); } break; case KeyEvent.VK_DELETE: case KeyEvent.VK_BACK_SPACE: if (!isReadOnly()) { // roi selected ? if (isSelected()) { final boolean result; // if (isFocused()) // // remove ROI from sequence // result = canvas.getSequence().removeROI(ROI.this); // else // remove all selected ROI from the sequence result = canvas.getSequence().removeSelectedROIs(false, true); if (result) e.consume(); } // roi focused ? --> delete ROI else if (isFocused()) { // remove ROI from sequence if (canvas.getSequence().removeROI(ROI.this, true)) e.consume(); } } break; } // control modifier is used for ROI modification from keyboard if (EventUtil.isMenuControlDown(e) && isSelected() && !isReadOnly()) { switch (e.getKeyCode()) { case KeyEvent.VK_LEFT: if (EventUtil.isAltDown(e)) { // resize if (canSetBounds()) { final Rectangle5D bnd = getBounds5D(); if (EventUtil.isShiftDown(e)) bnd.setSizeX(Math.max(1, bnd.getSizeX() - 10)); else bnd.setSizeX(Math.max(1, bnd.getSizeX() - 1)); setBounds5D(bnd); e.consume(); } } else { // move if (canSetPosition()) { final Point5D pos = getPosition5D(); if (EventUtil.isShiftDown(e)) pos.setX(pos.getX() - 10); else pos.setX(pos.getX() - 1); setPosition5D(pos); e.consume(); } } break; case KeyEvent.VK_RIGHT: if (EventUtil.isAltDown(e)) { // resize if (canSetBounds()) { final Rectangle5D bnd = getBounds5D(); if (EventUtil.isShiftDown(e)) bnd.setSizeX(Math.max(1, bnd.getSizeX() + 10)); else bnd.setSizeX(Math.max(1, bnd.getSizeX() + 1)); setBounds5D(bnd); e.consume(); } } else { // move if (canSetPosition()) { final Point5D pos = getPosition5D(); if (EventUtil.isShiftDown(e)) pos.setX(pos.getX() + 10); else pos.setX(pos.getX() + 1); setPosition5D(pos); e.consume(); } } break; case KeyEvent.VK_UP: if (EventUtil.isAltDown(e)) { // resize if (canSetBounds()) { final Rectangle5D bnd = getBounds5D(); if (EventUtil.isShiftDown(e)) bnd.setSizeY(Math.max(1, bnd.getSizeY() - 10)); else bnd.setSizeY(Math.max(1, bnd.getSizeY() - 1)); setBounds5D(bnd); e.consume(); } } else { // move if (canSetPosition()) { final Point5D pos = getPosition5D(); if (EventUtil.isShiftDown(e)) pos.setY(pos.getY() - 10); else pos.setY(pos.getY() - 1); setPosition5D(pos); e.consume(); } } break; case KeyEvent.VK_DOWN: if (EventUtil.isAltDown(e)) { // resize if (canSetBounds()) { final Rectangle5D bnd = getBounds5D(); if (EventUtil.isShiftDown(e)) bnd.setSizeY(Math.max(1, bnd.getSizeY() + 10)); else bnd.setSizeY(Math.max(1, bnd.getSizeY() + 1)); setBounds5D(bnd); e.consume(); } } else { // move if (canSetPosition()) { final Point5D pos = getPosition5D(); if (EventUtil.isShiftDown(e)) pos.setY(pos.getY() + 10); else pos.setY(pos.getY() + 1); setPosition5D(pos); e.consume(); } } break; } } } } // this allow to keep the backward compatibility super.keyPressed(e, imagePoint, canvas); } @Override public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.keyReleased(e, imagePoint, canvas); if (isActiveFor(canvas)) { // just for the shift key state change if (!isReadOnly()) updateDrag(e, imagePoint, canvas); } } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.mousePressed(e, imagePoint, canvas); // not yet consumed... if (!e.isConsumed()) { if (isActiveFor(canvas)) { // left button action if (EventUtil.isLeftMouseButton(e)) { ROI.this.beginUpdate(); try { // update selection if (updateSelect(e, imagePoint, canvas)) e.consume(); // always consume when focused to enable dragging else if (isFocused()) e.consume(); } finally { ROI.this.endUpdate(); } } } } } @Override public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.mouseReleased(e, imagePoint, canvas); } @Override public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.mouseClick(e, imagePoint, canvas); // not yet consumed... if (!e.isConsumed()) { // and process ROI stuff now if (isActiveFor(canvas)) { final int clickCount = e.getClickCount(); // single click if (clickCount == 1) { // right click action if (EventUtil.isRightMouseButton(e)) { // unselect (don't consume event) if (isSelected()) ROI.this.setSelected(false); } } // double click else if (clickCount == 2) { // focused ? if (isFocused()) { // show in ROI panel final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel(); if (roiPanel != null) { roiPanel.scrollTo(ROI.this); // consume event e.consume(); } } } } } } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.mouseDrag(e, imagePoint, canvas); // nothing here by default, should be implemented in deriving classes... } @Override public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // this allow to keep the backward compatibility super.mouseMove(e, imagePoint, canvas); // update focus if (!e.isConsumed()) { if (isActiveFor(canvas)) { if (updateFocus(e, imagePoint, canvas)) e.consume(); } } } @Override public boolean loadFromXML(Node node) { if (node == null) return false; beginUpdate(); try { setColor(new Color(XMLUtil.getElementIntValue(node, ID_COLOR, getDefaultColor().getRGB()))); setStroke(XMLUtil.getElementDoubleValue(node, ID_STROKE, getDefaultStroke())); setOpacity(XMLUtil.getElementFloatValue(node, ID_OPACITY, getDefaultOpacity())); } finally { endUpdate(); } return true; } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setElementIntValue(node, ID_COLOR, color.getRGB()); XMLUtil.setElementDoubleValue(node, ID_STROKE, stroke); XMLUtil.setElementFloatValue(node, ID_OPACITY, opacity); return true; } } /** * id generator */ private static int id_generator = 1; /** * associated ROI painter */ protected final ROIPainter painter; protected int id; protected String name; protected ROIGroupId groupid; protected boolean creating; protected boolean focused; protected boolean selected; protected boolean readOnly; // attached ROI icon protected Image icon; /** * cached calculated properties */ protected Rectangle5D cachedBounds; protected double cachedNumberOfPoints; protected double cachedNumberOfContourPoints; protected boolean boundsInvalid; protected boolean numberOfContourPointsInvalid; protected boolean numberOfPointsInvalid; /** * listeners */ protected final List<ROIListener> listeners; /** * internal updater */ protected final UpdateEventHandler updater; public ROI() { super(); // ensure unique id id = generateId(); painter = createPainter(); name = ""; groupid = null; readOnly = false; creating = false; focused = false; selected = false; cachedBounds = new Rectangle5D.Double(); cachedNumberOfPoints = 0d; cachedNumberOfContourPoints = 0d; boundsInvalid = true; numberOfPointsInvalid = true; numberOfContourPointsInvalid = true; listeners = new ArrayList<ROIListener>(); updater = new UpdateEventHandler(this, false); // default icon & name icon = ResourceUtil.ICON_ROI; name = getDefaultName(); } protected abstract ROIPainter createPainter(); /** * Returns the number of dimension of the ROI:<br> * 2 for ROI2D<br> * 3 for ROI3D<br> * 4 for ROI4D<br> * 5 for ROI5D<br> */ public abstract int getDimension(); /** * generate unique id */ private static synchronized int generateId() { return id_generator++; } /** * @deprecated use {@link Sequence#addROI(ROI)} instead */ @Deprecated public void attachTo(Sequence sequence) { if (sequence != null) sequence.addROI(this); } /** * @deprecated use {@link Sequence#removeROI(ROI)} instead */ @Deprecated public void detachFrom(Sequence sequence) { if (sequence != null) sequence.removeROI(this); } /** * @deprecated Use {@link #remove(boolean)} instead. */ @Deprecated public void detachFromAll(boolean canUndo) { remove(canUndo); } /** * @deprecated Use {@link #remove()} instead. */ @Deprecated public void detachFromAll() { remove(false); } /** * Return true is this ROI is attached to at least one sequence */ public boolean isAttached(Sequence sequence) { if (sequence != null) return sequence.contains(this); return false; } /** * Return first sequence where ROI is attached */ public Sequence getFirstSequence() { return Icy.getMainInterface().getFirstSequenceContaining(this); } /** * Return sequences where ROI is attached */ public ArrayList<Sequence> getSequences() { return Icy.getMainInterface().getSequencesContaining(this); } /** * Remove this ROI (detach from all sequence) */ public void remove(boolean canUndo) { final List<Sequence> sequences = Icy.getMainInterface().getSequencesContaining(this); for (Sequence sequence : sequences) sequence.removeROI(this, canUndo); } /** * Remove this ROI (detach from all sequence) */ public void remove() { remove(true); } /** * @deprecated Use {@link #remove(boolean)} instead. */ @Deprecated public void delete(boolean canUndo) { remove(canUndo); } /** * @deprecated Use {@link #remove()} instead. */ @Deprecated public void delete() { remove(true); } public String getClassName() { return getClass().getName(); } public String getSimpleClassName() { return ClassUtil.getSimpleClassName(getClassName()); } /** * ROI unique id */ public int getId() { return id; } /** * @deprecated Use {@link #getOverlay()} instead. */ @Deprecated public ROIPainter getPainter() { return getOverlay(); } /** * Returns the ROI overlay (used to draw and interact with {@link ROI} on {@link IcyCanvas}) */ public ROIPainter getOverlay() { return painter; } /** * Return the ROI painter stroke. */ public double getStroke() { return getOverlay().getStroke(); } /** * Get adjusted stroke for the current canvas transformation */ public double getAdjustedStroke(IcyCanvas canvas) { return getOverlay().getAdjustedStroke(canvas); } /** * Set ROI painter stroke. */ public void setStroke(double value) { getOverlay().setStroke(value); } /** * Returns the ROI painter opacity factor (0 = transparent while 1 means opaque). */ public float getOpacity() { return getOverlay().getOpacity(); } /** * Sets the ROI painter content opacity factor (0 = transparent while 1 means opaque). */ public void setOpacity(float value) { getOverlay().setOpacity(value); } /** * Return the ROI painter focused color. */ public Color getFocusedColor() { return getOverlay().getFocusedColor(); } /** * @deprecated */ @Deprecated public Color getSelectedColor() { return getOverlay().getSelectedColor(); } /** * Returns the color used to display the ROI depending its current state. */ public Color getDisplayColor() { return getOverlay().getDisplayColor(); } /** * Return the ROI painter base color. */ public Color getColor() { return getOverlay().getColor(); } /** * Set the ROI painter base color. */ public void setColor(Color value) { getOverlay().setColor(value); } /** * @deprecated selected color is automatically calculated. */ @Deprecated public void setSelectedColor(Color value) { // } /** * @return the icon */ public Image getIcon() { return icon; } /** * @param value * the icon to set */ public void setIcon(Image value) { if (icon != value) { icon = value; propertyChanged(PROPERTY_ICON); } } /** * @return the group id */ public ROIGroupId getGroupId() { return groupid; } /** * @param value * the group id to set */ public void setGroupId(ROIGroupId value) { if (groupid != value) { groupid = value; propertyChanged(PROPERTY_GROUPID); } } /** * @return the default name for this ROI class */ public String getDefaultName() { return "ROI"; } /** * Returns <code>true</code> if the ROI has its default name */ public boolean isDefaultName() { return getName().equals(getDefaultName()); } /** * @return the name */ public String getName() { return name; } /** * @param value * the name to set */ public void setName(String value) { if (name != value) { name = value; propertyChanged(PROPERTY_NAME); // painter name is ROI name so we notify it painter.propertyChanged(Overlay.PROPERTY_NAME); } } /** * Generic way to retrieve a ROI property value.<br> * Returns <code>null</code> if property name is invalid. * * @param propertyName * property name (for instance {@link #PROPERTY_COLOR}) */ public Object getPropertyValue(String propertyName) { if (StringUtil.equals(propertyName, PROPERTY_COLOR)) return getColor(); if (StringUtil.equals(propertyName, PROPERTY_CREATING)) return Boolean.valueOf(isCreating()); if (StringUtil.equals(propertyName, PROPERTY_ICON)) return getIcon(); if (StringUtil.equals(propertyName, PROPERTY_NAME)) return getName(); if (StringUtil.equals(propertyName, PROPERTY_OPACITY)) return Float.valueOf(getOpacity()); if (StringUtil.equals(propertyName, PROPERTY_READONLY)) return Boolean.valueOf(isReadOnly()); if (StringUtil.equals(propertyName, PROPERTY_SHOWNAME)) return Boolean.valueOf(getShowName()); if (StringUtil.equals(propertyName, PROPERTY_STROKE)) return Double.valueOf(getStroke()); return null; } /** * Generic way to set ROI property value. * * @param propertyName * property name (for instance {@value #PROPERTY_COLOR}) * @param value * the value to set in the property (for instance Color.red for {@link #PROPERTY_COLOR}) */ public void setPropertyValue(String propertyName, Object value) { if (StringUtil.equals(propertyName, PROPERTY_COLOR)) setColor((Color) value); if (StringUtil.equals(propertyName, PROPERTY_CREATING)) setCreating(((Boolean) value).booleanValue()); if (StringUtil.equals(propertyName, PROPERTY_ICON)) setIcon((Image) value); if (StringUtil.equals(propertyName, PROPERTY_NAME)) setName((String) value); if (StringUtil.equals(propertyName, PROPERTY_OPACITY)) setOpacity(((Float) value).floatValue()); if (StringUtil.equals(propertyName, PROPERTY_READONLY)) setReadOnly(((Boolean) value).booleanValue()); if (StringUtil.equals(propertyName, PROPERTY_SHOWNAME)) setShowName(((Boolean) value).booleanValue()); if (StringUtil.equals(propertyName, PROPERTY_STROKE)) setStroke(((Double) value).doubleValue()); } /** * @return the creating */ public boolean isCreating() { return creating; } /** * Set the internal <i>creation mode</i> state.<br> * The ROI interaction behave differently when in <i>creation mode</i>.<br> * You should not set this state when you create an ROI from the code. */ public void setCreating(boolean value) { if (creating != value) { creating = value; propertyChanged(PROPERTY_CREATING); } } /** * Returns true if the ROI has a (control) point which is currently focused/selected */ public abstract boolean hasSelectedPoint(); /** * Remove focus/selected state on all (control) points.<br> * Override this method depending implementation */ public void unselectAllPoints() { // do nothing by default }; /** * @return the focused */ public boolean isFocused() { return focused; } /** * @param value * the focused to set */ public void setFocused(boolean value) { boolean done = false; if (value) { // only one ROI focused per sequence final List<Sequence> attachedSeqs = Icy.getMainInterface().getSequencesContaining(this); for (Sequence seq : attachedSeqs) done |= seq.setFocusedROI(this); } if (!done) { if (value) internalFocus(); else internalUnfocus(); } } public void internalFocus() { if (focused != true) { focused = true; focusChanged(); } } public void internalUnfocus() { if (focused != false) { focused = false; focusChanged(); } } /** * @return the selected */ public boolean isSelected() { return selected; } /** * Set the selected state of this ROI.<br> * Use {@link Sequence#setSelectedROI(ROI)} for exclusive ROI selection. * * @param value * the selected to set */ public void setSelected(boolean value) { if (selected != value) { selected = value; // as soon ROI has been unselected, we're not in create mode anymore if (!value) setCreating(false); selectionChanged(); } } /** * @deprecated Use {@link #setSelected(boolean)} or {@link Sequence#setSelectedROI(ROI)} depending you want * exclusive selection or not. */ @Deprecated public void setSelected(boolean value, boolean exclusive) { if (exclusive) { // use the sequence for ROI selection with exclusive parameter final List<Sequence> attachedSeqs = Icy.getMainInterface().getSequencesContaining(this); for (Sequence seq : attachedSeqs) seq.setSelectedROI(value ? this : null); } else setSelected(value); } /** * @deprecated Use {@link #setSelected(boolean)} instead. */ @Deprecated public void internalUnselect() { if (selected != false) { selected = false; // as soon ROI has been unselected, we're not in create mode anymore setCreating(false); selectionChanged(); } } /** * @deprecated Use {@link #setSelected(boolean)} instead. */ @Deprecated public void internalSelect() { if (selected != true) { selected = true; selectionChanged(); } } /** * @deprecated Use {@link #isReadOnly()} instead. */ @Deprecated public boolean isEditable() { return !isReadOnly(); } /** * @deprecated Use {@link #setReadOnly(boolean)} instead. */ @Deprecated public void setEditable(boolean value) { setReadOnly(!value); } /** * Return true if ROI is in <i>read only</i> state (cannot be modified from GUI). */ public boolean isReadOnly() { return readOnly; } /** * Set the <i>read only</i> state of ROI. */ public void setReadOnly(boolean value) { if (readOnly != value) { readOnly = value; propertyChanged(PROPERTY_READONLY); if (value) setSelected(false); } } /** * Return <code>true</code> if ROI should display its name at draw time.<br> */ public boolean getShowName() { return getOverlay().getShowName(); } /** * Set the <i>show name</i> property of ROI.<br> * When set to <code>true</code> the ROI shows its name at draw time. */ public void setShowName(boolean value) { getOverlay().setShowName(value); } /** * Return true if the ROI is active for the specified canvas. */ public abstract boolean isActiveFor(IcyCanvas canvas); /** * Calculate and returns the bounding box of the <code>ROI</code>.<br> * This method is used by {@link #getBounds5D()} which should try to cache the result as the * bounding box calculation can take some computation time for complex ROI. */ public abstract Rectangle5D computeBounds5D(); /** * Returns the bounding box of the <code>ROI</code>. Note that there is no guarantee that the * returned {@link Rectangle5D} is the smallest bounding box that encloses the <code>ROI</code>, * only that the <code>ROI</code> lies entirely within the indicated <code>Rectangle5D</code>. * * @return an instance of <code>Rectangle5D</code> that is a bounding box of the <code>ROI</code>. * @see #computeBounds5D() */ public Rectangle5D getBounds5D() { // we need to recompute bounds if (boundsInvalid) { cachedBounds = computeBounds5D(); boundsInvalid = false; } return (Rectangle5D) cachedBounds.clone(); } /** * Returns the ROI position which normally correspond to the <i>minimum</i> point of the ROI * bounds.<br> * * @see #getBounds5D() */ public Point5D getPosition5D() { return getBounds5D().getPosition(); } /** * Returns <code>true</code> if this ROI accepts bounds change through the {@link #setBounds5D(Rectangle5D)} method. */ public abstract boolean canSetBounds(); /** * Returns <code>true</code> if this ROI accepts position change through the {@link #setPosition5D(Point5D)} method. */ public abstract boolean canSetPosition(); /** * Set the <code>ROI</code> bounds.<br> * Note that not all ROI supports bounds modification and you should call {@link #canSetBounds()} first to test if * the operation is supported.<br> * * @param bounds * new ROI bounds */ public abstract void setBounds5D(Rectangle5D bounds); /** * Set the <code>ROI</code> position.<br> * Note that not all ROI supports position modification and you should call {@link #canSetPosition()} first to test * if the operation is supported.<br> * * @param position * new ROI position */ public abstract void setPosition5D(Point5D position); /** * Returns <code>true</code> if the ROI is empty (does not contains anything). */ public boolean isEmpty() { return getBounds5D().isEmpty(); } /** * Tests if a specified 5D point is inside the ROI. * * @return <code>true</code> if the specified <code>Point5D</code> is inside the boundary of the <code>ROI</code>; * <code>false</code> otherwise. */ public abstract boolean contains(double x, double y, double z, double t, double c); /** * Tests if a specified {@link Point5D} is inside the ROI. * * @param p * the specified <code>Point5D</code> to be tested * @return <code>true</code> if the specified <code>Point2D</code> is inside the boundary of the <code>ROI</code>; * <code>false</code> otherwise. */ public boolean contains(Point5D p) { if (p == null) return false; return contains(p.getX(), p.getY(), p.getZ(), p.getT(), p.getC()); } /** * Tests if the <code>ROI</code> entirely contains the specified 5D rectangular area. All * coordinates that lie inside the rectangular area must lie within the <code>ROI</code> for the * entire rectangular area to be considered contained within the <code>ROI</code>. * <p> * The {@code ROI.contains()} method allows a {@code ROI} implementation to conservatively return {@code false} * when: * <ul> * <li>the <code>intersect</code> method returns <code>true</code> and * <li>the calculations to determine whether or not the <code>ROI</code> entirely contains the rectangular area are * prohibitively expensive. * </ul> * This means that for some {@code ROIs} this method might return {@code false} even though the {@code ROI} contains * the rectangular area. * * @param x * the X coordinate of the start corner of the specified rectangular area * @param y * the Y coordinate of the start corner of the specified rectangular area * @param z * the Z coordinate of the start corner of the specified rectangular area * @param t * the T coordinate of the start corner of the specified rectangular area * @param c * the C coordinate of the start corner of the specified rectangular area * @param sizeX * the X size of the specified rectangular area * @param sizeY * the Y size of the specified rectangular area * @param sizeZ * the Z size of the specified rectangular area * @param sizeT * the T size of the specified rectangular area * @param sizeC * the C size of the specified rectangular area * @return <code>true</code> if the interior of the <code>ROI</code> entirely contains the * specified rectangular area; <code>false</code> otherwise or, if the <code>ROI</code> contains the * rectangular area and the <code>intersects</code> method returns <code>true</code> and * the containment * calculations would be too expensive to perform. */ public abstract boolean contains(double x, double y, double z, double t, double c, double sizeX, double sizeY, double sizeZ, double sizeT, double sizeC); /** * Tests if the <code>ROI</code> entirely contains the specified <code>Rectangle5D</code>. The * {@code ROI.contains()} method allows a implementation to conservatively return {@code false} when: * <ul> * <li>the <code>intersect</code> method returns <code>true</code> and * <li>the calculations to determine whether or not the <code>ROI</code> entirely contains the * <code>Rectangle2D</code> are prohibitively expensive. * </ul> * This means that for some ROIs this method might return {@code false} even though the {@code ROI} contains the * {@code Rectangle5D}. * * @param r * The specified <code>Rectangle5D</code> * @return <code>true</code> if the interior of the <code>ROI</code> entirely contains the <code>Rectangle5D</code>; * <code>false</code> otherwise or, if the <code>ROI</code> contains the <code>Rectangle5D</code> and the * <code>intersects</code> method returns <code>true</code> and the containment * calculations would be too * expensive to perform. * @see #contains(double, double, double, double, double, double, double, double, double, double) */ public boolean contains(Rectangle5D r) { if (r == null) return false; return contains(r.getX(), r.getY(), r.getZ(), r.getT(), r.getC(), r.getSizeX(), r.getSizeY(), r.getSizeZ(), r.getSizeT(), r.getSizeC()); } /** * Tests if the <code>ROI</code> entirely contains the specified <code>ROI</code>. * WARNING: this method may be "pixel accurate" only depending the internal implementation. * * @return <code>true</code> if the current <code>ROI</code> entirely contains the * specified <code>ROI</code>; <code>false</code> otherwise. */ public boolean contains(ROI roi) { // default implementation using BooleanMask final Rectangle5D.Integer bounds = getBounds5D().toInteger(); final Rectangle5D.Integer roiBounds = roi.getBounds5D().toInteger(); // trivial optimization if (bounds.isEmpty()) return false; // special case of ROI Point --> just test position if contained if (roiBounds.isEmpty()) return contains(roiBounds.getPosition()); // simple bounds contains test if (bounds.contains(roiBounds)) { final Rectangle5D.Integer containedBounds = bounds.createIntersection(roiBounds).toInteger(); int minZ; int maxZ; int minT; int maxT; int minC; int maxC; // special infinite case if (containedBounds.isInfiniteZ()) { minZ = -1; maxZ = -1; } else { minZ = (int) containedBounds.getMinZ(); maxZ = (int) containedBounds.getMaxZ(); } if (containedBounds.isInfiniteT()) { minT = -1; maxT = -1; } else { minT = (int) containedBounds.getMinT(); maxT = (int) containedBounds.getMaxT(); } if (containedBounds.isInfiniteC()) { minC = -1; maxC = -1; } else { minC = (int) containedBounds.getMinC(); maxC = (int) containedBounds.getMaxC(); } final Rectangle containedBounds2D = (Rectangle) containedBounds.toRectangle2D(); // slow method using the boolean mask for (int c = minC; c <= maxC; c++) { for (int t = minT; t <= maxT; t++) { for (int z = minZ; z <= maxZ; z++) { BooleanMask2D mask; BooleanMask2D roiMask; // take content first mask = new BooleanMask2D(containedBounds2D, getBooleanMask2D(containedBounds2D, z, t, c, false)); roiMask = new BooleanMask2D(containedBounds2D, roi.getBooleanMask2D(containedBounds2D, z, t, c, false)); // test first only on content if (!mask.contains(roiMask)) return false; // take content and edge mask = new BooleanMask2D(containedBounds2D, getBooleanMask2D(containedBounds2D, z, t, c, true)); roiMask = new BooleanMask2D(containedBounds2D, roi.getBooleanMask2D(containedBounds2D, z, t, c, true)); // then test on content and edge if (!mask.contains(roiMask)) return false; } } } return true; } return false; } /** * Tests if the interior of the <code>ROI</code> intersects the interior of a specified * rectangular area. The rectangular area is considered to intersect the <code>ROI</code> if any * point is contained in both the interior of the <code>ROI</code> and the specified rectangular * area. * <p> * The {@code ROI.intersects()} method allows a {@code ROI} implementation to conservatively return {@code true} * when: * <ul> * <li>there is a high probability that the rectangular area and the <code>ROI</code> intersect, but * <li>the calculations to accurately determine this intersection are prohibitively expensive. * </ul> * This means that for some {@code ROIs} this method might return {@code true} even though the rectangular area does * not intersect the {@code ROI}. * * @return <code>true</code> if the interior of the <code>ROI</code> and the interior of the * rectangular area intersect, or are both highly likely to intersect and intersection * calculations would be too expensive to perform; <code>false</code> otherwise. */ public abstract boolean intersects(double x, double y, double z, double t, double c, double sizeX, double sizeY, double sizeZ, double sizeT, double sizeC); /** * Tests if the interior of the <code>ROI</code> intersects the interior of a specified * rectangular area. The rectangular area is considered to intersect the <code>ROI</code> if any * point is contained in both the interior of the <code>ROI</code> and the specified rectangular * area. * <p> * The {@code ROI.intersects()} method allows a {@code ROI} implementation to conservatively return {@code true} * when: * <ul> * <li>there is a high probability that the rectangular area and the <code>ROI</code> intersect, but * <li>the calculations to accurately determine this intersection are prohibitively expensive. * </ul> * This means that for some {@code ROIs} this method might return {@code true} even though the rectangular area does * not intersect the {@code ROI}. * * @return <code>true</code> if the interior of the <code>ROI</code> and the interior of the * rectangular area intersect, or are both highly likely to intersect and intersection * calculations would be too expensive to perform; <code>false</code> otherwise. */ public boolean intersects(Rectangle5D r) { if (r == null) return false; return intersects(r.getX(), r.getY(), r.getZ(), r.getT(), r.getC(), r.getSizeX(), r.getSizeY(), r.getSizeZ(), r.getSizeT(), r.getSizeC()); } /** * Tests if the current <code>ROI</code> intersects the specified <code>ROI</code>.<br> * Note that this method may be "pixel accurate" only depending the internal implementation. * * @return <code>true</code> if <code>ROI</code> intersect, <code>false</code> otherwise. */ public boolean intersects(ROI roi) { // default implementation using BooleanMask final Rectangle5D.Integer bounds = getBounds5D().toInteger(); final Rectangle5D.Integer roiBounds = roi.getBounds5D().toInteger(); final Rectangle5D.Integer intersection = bounds.createIntersection(roiBounds).toInteger(); int minZ; int maxZ; int minT; int maxT; int minC; int maxC; // special infinite case if (intersection.isInfiniteZ()) { minZ = -1; maxZ = -1; } else { minZ = (int) intersection.getMinZ(); maxZ = (int) intersection.getMaxZ(); } if (intersection.isInfiniteT()) { minT = -1; maxT = -1; } else { minT = (int) intersection.getMinT(); maxT = (int) intersection.getMaxT(); } if (intersection.isInfiniteC()) { minC = -1; maxC = -1; } else { minC = (int) intersection.getMinC(); maxC = (int) intersection.getMaxC(); } // slow method using the boolean mask for (int c = minC; c <= maxC; c++) { for (int t = minT; t <= maxT; t++) { for (int z = minZ; z <= maxZ; z++) { if (getBooleanMask2D(z, t, c, true).intersects(roi.getBooleanMask2D(z, t, c, true))) return true; } } } return false; } /** * Returns the boolean array mask for the specified rectangular region at specified C, Z, T * position.<br> * <br> * If pixel (x1, y1, c, z, t) is contained in the roi:<br> * <code>  result[((y1 - y) * width) + (x1 - x)] = true</code><br> * If pixel (x1, y1, c, z, t) is not contained in the roi:<br> * <code>  result[((y1 - y) * width) + (x1 - x)] = false</code><br> * * @param x * the X coordinate of the upper-left corner of the specified rectangular region * @param y * the Y coordinate of the upper-left corner of the specified rectangular region * @param width * the width of the specified rectangular region * @param height * the height of the specified rectangular region * @param z * Z position we want to retrieve the boolean mask * @param t * T position we want to retrieve the boolean mask * @param c * C position we want to retrieve the boolean mask * @param inclusive * If true then all partially contained (intersected) pixels are included in the mask. * @return the boolean bitmap mask */ public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, int t, int c, boolean inclusive) { final boolean[] result = new boolean[Math.max(0, width) * Math.max(0, height)]; // simple and basic implementation, override it to have better performance int offset = 0; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { if (inclusive) result[offset] = intersects(x + i, y + j, z, t, c, 1d, 1d, 1d, 1d, 1d); else result[offset] = contains(x + i, y + j, z, t, c, 1d, 1d, 1d, 1d, 1d); offset++; } } return result; } /** * Get the boolean bitmap mask for the specified rectangular area of the roi and for the * specified Z,T position.<br> * if the pixel (x,y) is contained in the roi Z,T position then result[(y * width) + x] = true <br> * if the pixel (x,y) is not contained in the roi Z,T position then result[(y * width) + x] = * false * * @param rect * 2D rectangular area we want to retrieve the boolean mask * @param z * Z position we want to retrieve the boolean mask * @param t * T position we want to retrieve the boolean mask * @param c * C position we want to retrieve the boolean mask * @param inclusive * If true then all partially contained (intersected) pixels are included in the mask. */ public boolean[] getBooleanMask2D(Rectangle rect, int z, int t, int c, boolean inclusive) { return getBooleanMask2D(rect.x, rect.y, rect.width, rect.height, z, t, c, inclusive); } /** * Returns the {@link BooleanMask2D} object representing the XY plan content at specified Z, T, * C position.<br> * <br> * If pixel (x, y, c, z, t) is contained in the roi:<br> * <code>  mask[(y - bounds.y) * bounds.width) + (x - bounds.x)] = true</code> <br> * If pixel (x, y, c, z, t) is not contained in the roi:<br> * <code>  mask[(y - bounds.y) * bounds.width) + (x - bounds.x)] = false</code> * * @param z * Z position we want to retrieve the boolean mask.<br> * Set it to -1 to retrieve the mask whatever is the Z position of ROI2D. * @param t * T position we want to retrieve the boolean mask.<br> * Set it to -1 to retrieve the mask whatever is the T position of ROI2D/ROI3D. * @param c * C position we want to retrieve the boolean mask.<br> * Set it to -1 to retrieve the mask whatever is the C position of ROI2D/ROI3D/ROI4D. * @param inclusive * If true then all partially contained (intersected) pixels are included in the mask. */ public BooleanMask2D getBooleanMask2D(int z, int t, int c, boolean inclusive) { final Rectangle bounds2D = getBounds5D().toRectangle2D().getBounds(); // empty ROI --> return empty mask if (bounds2D.isEmpty()) return new BooleanMask2D(new Rectangle(), new boolean[0]); return new BooleanMask2D(bounds2D, getBooleanMask2D(bounds2D.x, bounds2D.y, bounds2D.width, bounds2D.height, z, t, c, inclusive)); } /** * @deprecated Override directly these methods:<br> * {@link #getUnion(ROI)}<br> * {@link #getIntersection(ROI)}<br> * {@link #getExclusiveUnion(ROI)}<br> * {@link #getSubtraction(ROI)}<br> * or use {@link #merge(ROI, BooleanOperator)} method instead. */ /* * Generic implementation for ROI using the BooleanMask object so the result is just an * approximation. Override to optimize for specific ROI. */ @Deprecated protected ROI computeOperation(ROI roi, BooleanOperator op) throws UnsupportedOperationException { System.out.println("Deprecated method " + getClassName() + ".computeOperation(ROI, BooleanOperator) called !"); return null; } /** * Same as {@link #merge(ROI, BooleanOperator)} except it modifies the current <code>ROI</code> to reflect the * result of the boolean operation with specified <code>ROI</code>.<br> * Note that this operation work only if the 2 ROIs are compatible for that type of operation. * If that is not * the case a {@link UnsupportedOperationException} is thrown if <code>allowCreate</code> parameter is set to * <code>false</code>, if the parameter is set to <code>true</code> the result may be returned * in a new created ROI. * * @param roi * the <code>ROI</code> to merge with current <code>ROI</code> * @param op * the boolean operation to process * @param allowCreate * if set to <code>true</code> the method will create a new ROI to return the result of * the operation if it * cannot be directly processed on the current <code>ROI</code> * @return the modified ROI or a new created ROI if the operation cannot be directly processed * on the current ROI * and <code>allowCreate</code> parameter was set to <code>true</code> * @throws UnsupportedOperationException * if the two ROI cannot be merged together. * @see #merge(ROI, BooleanOperator) */ public ROI mergeWith(ROI roi, BooleanOperator op, boolean allowCreate) throws UnsupportedOperationException { switch (op) { case AND: return intersect(roi, allowCreate); case OR: return add(roi, allowCreate); case XOR: return exclusiveAdd(roi, allowCreate); } return this; } /** * Adds content of specified <code>ROI</code> into this <code>ROI</code>. * The resulting content of this <code>ROI</code> will include * the union of both ROI's contents.<br> * Note that this operation work only if the 2 ROIs are compatible for that type of operation. * If that is not the case a {@link UnsupportedOperationException} is thrown if <code>allowCreate</code> parameter * is set to <code>false</code>, if the parameter is set to <code>true</code> the result may be returned in a new * created ROI. * * <pre> * // Example: * roi1 (before) + roi2 = roi1 (after) * * ################ ################ ################ * ############## ############## ################ * ############ ############ ################ * ########## ########## ################ * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> * * @param roi * the <code>ROI</code> to be added to the current <code>ROI</code> * @param allowCreate * if set to <code>true</code> the method will create a new ROI to return the result of * the operation if it * cannot be directly processed on the current <code>ROI</code> * @return the modified ROI or a new created ROI if the operation cannot be directly processed * on the current ROI * and <code>allowCreate</code> parameter was set to <code>true</code> * @throws UnsupportedOperationException * if the two ROI cannot be added together. * @see #getUnion(ROI) */ public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException { // nothing to do if (roi == null) return this; if (allowCreate) return getUnion(roi); throw new UnsupportedOperationException(getClassName() + " does not support add(ROI) operation !"); } /** * Sets the content of this <code>ROI</code> to the intersection of * its current content and the content of the specified <code>ROI</code>. * The resulting ROI will include only contents that were contained in both ROI.<br> * Note that this operation work only if the 2 ROIs are compatible for that type of operation. * If that is not the case a {@link UnsupportedOperationException} is thrown if <code>allowCreate</code> parameter * is set to <code>false</code>, if the parameter is set to <code>true</code> the result may be returned in a new * created ROI. * * <pre> * // Example: * roi1 (before) intersect roi2 = roi1 (after) * * ################ ################ ################ * ############## ############## ############ * ############ ############ ######## * ########## ########## #### * ######## ######## * ###### ###### * #### #### * ## ## * </pre> * * @param roi * the <code>ROI</code> to be intersected to the current <code>ROI</code> * @param allowCreate * if set to <code>true</code> the method will create a new ROI to return the result of * the operation if it * cannot be directly processed on the current <code>ROI</code> * @return the modified ROI or a new created ROI if the operation cannot be directly processed * on the current ROI * and <code>allowCreate</code> parameter was set to <code>true</code> * @throws UnsupportedOperationException * if the two ROI cannot be intersected together. * @see #getIntersection(ROI) */ public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException { // nothing to do if (roi == null) return this; if (allowCreate) return getIntersection(roi); throw new UnsupportedOperationException(getClassName() + " does not support intersect(ROI) operation !"); } /** * Sets the content of this <code>ROI</code> to be the union of its current content and the * content of the specified <code>ROI</code>, minus their intersection. * The resulting <code>ROI</code> will include only content that were contained in either this <code>ROI</code> or * in the specified <code>ROI</code>, but not in both.<br> * Note that this operation work only if the 2 ROIs are compatible for that type of operation. * If that is not * the case a {@link UnsupportedOperationException} is thrown if <code>allowCreate</code> parameter is set to * <code>false</code>, if the parameter is set to <code>true</code> the result may be returned * in a new created ROI. * * <pre> * // Example: * roi1 (before) xor roi2 = roi1 (after) * * ################ ################ * ############## ############## ## ## * ############ ############ #### #### * ########## ########## ###### ###### * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> * * @param roi * the <code>ROI</code> to be exclusively added to the current <code>ROI</code> * @param allowCreate * if set to <code>true</code> the method will create a new ROI to return the result of * the operation if it * cannot be directly processed on the current <code>ROI</code> * @return the modified ROI or a new created ROI if the operation cannot be directly processed * on the current ROI * and <code>allowCreate</code> parameter was set to <code>true</code> * @throws UnsupportedOperationException * if the two ROI cannot be exclusively added together. * @see #getExclusiveUnion(ROI) */ public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException { // nothing to do if (roi == null) return this; if (allowCreate) return getExclusiveUnion(roi); throw new UnsupportedOperationException(getClassName() + " does not support exclusiveAdd(ROI) operation !"); } /** * Subtract the specified <code>ROI</code> content from current <code>ROI</code>.<br> * Note that this operation work only if the 2 ROIs are compatible for that type of operation. * If that is not the case a {@link UnsupportedOperationException} is thrown if <code>allowCreate</code> parameter * is set to <code>false</code>, if the parameter is set to <code>true</code> the result may be returned in a new * created ROI. * * @param roi * the <code>ROI</code> to subtract from the current <code>ROI</code> * @param allowCreate * if set to <code>true</code> the method will create a new ROI to return the result of * the operation if it * cannot be directly processed on the current <code>ROI</code> * @return the modified ROI or a new created ROI if the operation cannot be directly processed * on the current ROI * and <code>allowCreate</code> parameter was set to <code>true</code> * @throws UnsupportedOperationException * if we can't subtract the specified <code>ROI</code> from this <code>ROI</code> * @see #getSubtraction(ROI) */ public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException { // nothing to do if (roi == null) return this; if (allowCreate) return getSubtraction(roi); throw new UnsupportedOperationException(getClassName() + " does not support subtract(ROI) operation !"); } /** * Compute the boolean operation with specified <code>ROI</code> and return result in a new <code>ROI</code>. */ public ROI merge(ROI roi, BooleanOperator op) throws UnsupportedOperationException { switch (op) { case AND: return getIntersection(roi); case OR: return getUnion(roi); case XOR: return getExclusiveUnion(roi); } return null; } /** * Compute union with specified <code>ROI</code> and return result in a new <code>ROI</code>.<br> * This method actually call <code>ROIUtil.getUnion(ROI, ROI)</code> internally. */ public ROI getUnion(ROI roi) throws UnsupportedOperationException { return ROIUtil.getUnion(this, roi); } /** * Compute intersection with specified <code>ROI</code> and return result in a new <code>ROI</code>.<br> * This method actually call <code>ROIUtil.getIntersection(ROI, ROI)</code> internally. */ public ROI getIntersection(ROI roi) throws UnsupportedOperationException { return ROIUtil.getIntersection(this, roi); } /** * Compute exclusive union with specified <code>ROI</code> and return result in a new <code>ROI</code>.<br> * This method actually call <code>ROIUtil.getExclusiveUnion(ROI, ROI)</code> internally. */ public ROI getExclusiveUnion(ROI roi) throws UnsupportedOperationException { return ROIUtil.getExclusiveUnion(this, roi); } /** * Subtract the specified <code>ROI</code> and return result in a new <code>ROI</code>.<br> * This method actually call <code>ROIUtil.getSubtraction(ROI, ROI)</code> internally. */ public ROI getSubtraction(ROI roi) throws UnsupportedOperationException { return ROIUtil.getSubtraction(this, roi); } /** * Compute and returns the number of point (pixel) composing the ROI contour. */ /* * Override this method to adapt and optimize for a specific ROI. */ public abstract double computeNumberOfContourPoints(); /** * Returns the number of point (pixel) composing the ROI contour.<br> * It is used to calculate the perimeter (2D) or surface area (3D) of the ROI. * * @see #computeNumberOfContourPoints() */ public double getNumberOfContourPoints() { // we need to recompute the number of edge point if (numberOfContourPointsInvalid) { cachedNumberOfContourPoints = computeNumberOfContourPoints(); numberOfContourPointsInvalid = false; } return cachedNumberOfContourPoints; } /** * Compute and returns the number of point (pixel) contained in the ROI. */ /* * Override this method to adapt and optimize for a specific ROI. */ public abstract double computeNumberOfPoints(); /** * Returns the number of point (pixel) contained in the ROI.<br> * It is used to calculate the area (2D) or volume (3D) of the ROI. */ public double getNumberOfPoints() { // we need to recompute the number of point if (numberOfPointsInvalid) { cachedNumberOfPoints = computeNumberOfPoints(); numberOfPointsInvalid = false; } return cachedNumberOfPoints; } /** * Computes and returns the length/perimeter of the ROI in um given the pixel size informations from the specified * Sequence.<br> * Generic implementation of length computation uses the number of contour point (approximation). * This method should be overridden whenever possible to provide faster and accurate calculation.<br> * Throws a UnsupportedOperationException if the operation is not supported for this ROI. * * @see #getNumberOfContourPoints() */ public double getLength(Sequence sequence) throws UnsupportedOperationException { return sequence.calculateSize(getNumberOfContourPoints(), getDimension(), 1); } /** * @deprecated Use {@link #getLength(Sequence)} or {@link #getNumberOfContourPoints()} instead. */ @Deprecated public double getPerimeter() { return getNumberOfContourPoints(); } /** * @deprecated Only for ROI3D object, use {@link #getNumberOfPoints()} instead for other type of * ROI. */ @Deprecated public double getVolume() { return getNumberOfPoints(); } /** * @deprecated Use <code>getOverlay().setMousePos(..)</code> instead. */ @Deprecated public void setMousePos(Point2D pos) { if (pos != null) getOverlay().setMousePos(new Point5D.Double(pos.getX(), pos.getY(), -1, -1, -1)); } /** * Returns a copy of the ROI or <code>null</code> if the operation failed. */ public ROI getCopy() { // use XML persistence for cloning final Node node = XMLUtil.createDocument(true).getDocumentElement(); int retry; // XML methods sometime fails, better to offer retry (hacky) retry = 3; // save while ((retry > 0) && !saveToXML(node)) retry--; if (retry <= 0) { System.err.println("Cannot get a copy of roi " + getName() + ": XML save operation failed."); // throw new RuntimeException("Cannot get a copy of roi " + getName() + ": XML save // operation failed !"); return null; } ROI result; // XML methods sometime fails, better to offer retry (hacky) retry = 3; result = null; while ((retry > 0) && (result == null)) { result = createFromXML(node); retry--; } if (result == null) { System.err.println("Cannot get a copy of roi " + getName() + ": creation from XML failed."); // throw new RuntimeException("Cannot get a copy of roi " + getName() + ": creation from // XML failed !"); return null; } // then generate new id result.id = generateId(); return result; } /** * Returns the name suffix when we want to obtain only a sub part of the ROI (always in Z,T,C * order).<br/> * For instance if we use for z=1, t=5 and c=-1 this method will return <code>[Z=1, T=5]</code> * * @param z * the specific Z position (slice) we want to retrieve (<code>-1</code> to retrieve the * whole ROI Z dimension) * @param t * the specific T position (frame) we want to retrieve (<code>-1</code> to retrieve the * whole ROI T dimension) * @param c * the specific C position (channel) we want to retrieve (<code>-1</code> to retrieve the * whole ROI C dimension) */ static public String getNameSuffix(int z, int t, int c) { String result = ""; if (z != -1) { if (StringUtil.isEmpty(result)) result = " ["; else result += ", "; result += "Z=" + z; } if (t != -1) { if (StringUtil.isEmpty(result)) result = " ["; else result += ", "; result += "T=" + t; } if (c != -1) { if (StringUtil.isEmpty(result)) result = " ["; else result += ", "; result += "C=" + c; } if (!StringUtil.isEmpty(result)) result += "]"; return result; } /** * Returns a sub part of the ROI.<br/> * The default implementation returns result in "area" format: ({@link ROI2DArea}, {@link ROI3DArea}, * {@link ROI4DArea} or {@link ROI5DArea}) where only internals pixels are preserved.</br> * Note that this function can eventually return <code>null</code> when the result ROI is empty. * * @param z * the specific Z position (slice) we want to retrieve (<code>-1</code> to retrieve the * whole ROI Z dimension) * @param t * the specific T position (frame) we want to retrieve (<code>-1</code> to retrieve the * whole ROI T dimension) * @param c * the specific C position (channel) we want to retrieve (<code>-1</code> to retrieve the * whole ROI C dimension) */ public ROI getSubROI(int z, int t, int c) { final ROI result; switch (getDimension()) { default: result = new ROI2DArea(getBooleanMask2D(z, t, c, false)); break; case 3: if (z == -1) result = new ROI3DArea(((ROI3D) this).getBooleanMask3D(z, t, c, false)); else result = new ROI2DArea(((ROI3D) this).getBooleanMask2D(z, t, c, false)); break; case 4: if (z == -1) { if (t == -1) result = new ROI4DArea(((ROI4D) this).getBooleanMask4D(z, t, c, false)); else result = new ROI3DArea(((ROI4D) this).getBooleanMask3D(z, t, c, false)); } else { if (t == -1) result = new ROI4DArea(((ROI4D) this).getBooleanMask4D(z, t, c, false)); else result = new ROI2DArea(((ROI4D) this).getBooleanMask2D(z, t, c, false)); } break; case 5: if (z == -1) { if (t == -1) { if (c == -1) result = new ROI5DArea(((ROI5D) this).getBooleanMask5D(z, t, c, false)); else result = new ROI4DArea(((ROI5D) this).getBooleanMask4D(z, t, c, false)); } else { if (c == -1) result = new ROI5DArea(((ROI5D) this).getBooleanMask5D(z, t, c, false)); else result = new ROI3DArea(((ROI5D) this).getBooleanMask3D(z, t, c, false)); } } else { if (t == -1) { if (c == -1) result = new ROI5DArea(((ROI5D) this).getBooleanMask5D(z, t, c, false)); else result = new ROI4DArea(((ROI5D) this).getBooleanMask4D(z, t, c, false)); } else { if (c == -1) result = new ROI5DArea(((ROI5D) this).getBooleanMask5D(z, t, c, false)); else result = new ROI2DArea(((ROI5D) this).getBooleanMask2D(z, t, c, false)); } } break; } result.beginUpdate(); try { if (result.canSetPosition()) { final Point5D pos = result.getPosition5D(); // set Z, T, C position if (z != -1) pos.setZ(z); if (t != -1) pos.setT(t); if (c != -1) pos.setC(c); result.setPosition5D(pos); } // copy other properties result.setColor(getColor()); result.setName(getName() + getNameSuffix(z, t, c)); result.setOpacity(getOpacity()); result.setStroke(getStroke()); result.setShowName(getShowName()); } finally { result.endUpdate(); } return result; } /** * Copy all properties from the given ROI.<br> * All compatible properties from the source ROI are copied into current ROI except the internal * id.<br> * Return <code>false</code> if the operation failed */ public boolean copyFrom(ROI roi) { // use XML persistence for cloning final Node node = XMLUtil.createDocument(true).getDocumentElement(); // save operation can fails sometime... if (roi.saveToXML(node)) if (loadFromXML(node, true)) return true; return false; // if (tries == 0) // throw new RuntimeException("Cannot copy roi from " + roi.getName() + ": XML load // operation failed !"); } public boolean loadFromXML(Node node, boolean preserveId) { if (node == null) return false; beginUpdate(); try { // FIXME : this can make duplicate id but it is also important to preserve id if (!preserveId) { id = XMLUtil.getElementIntValue(node, ID_ID, 0); synchronized (ROI.class) { // avoid having same id if (id_generator <= id) id_generator = id + 1; } } setName(XMLUtil.getElementValue(node, ID_NAME, "")); setSelected(XMLUtil.getElementBooleanValue(node, ID_SELECTED, false)); setReadOnly(XMLUtil.getElementBooleanValue(node, ID_READONLY, false)); setShowName(XMLUtil.getElementBooleanValue(node, ID_SHOWNAME, false)); painter.loadFromXML(node); } finally { endUpdate(); } return true; } @Override public boolean loadFromXML(Node node) { return loadFromXML(node, false); } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setElementValue(node, ID_CLASSNAME, getClassName()); XMLUtil.setElementIntValue(node, ID_ID, id); XMLUtil.setElementValue(node, ID_NAME, getName()); XMLUtil.setElementBooleanValue(node, ID_SELECTED, isSelected()); XMLUtil.setElementBooleanValue(node, ID_READONLY, isReadOnly()); XMLUtil.setElementBooleanValue(node, ID_SHOWNAME, getShowName()); painter.saveToXML(node); return true; } /** * @deprecated Use {@link #roiChanged(boolean)} instead */ @Deprecated public void roiChanged(ROIPointEventType pointEventType, Object point) { // handle with updater updater.changed(new ROIEvent(this, ROIEventType.ROI_CHANGED, pointEventType, point)); } /** * Called when ROI has changed its content and/or position.<br> * * @param contentChanged * mean that ROI content has changed otherwise we consider only a position change */ public void roiChanged(boolean contentChanged) { // handle with updater if (contentChanged) updater.changed(new ROIEvent(this, ROIEventType.ROI_CHANGED, ROI_CHANGED_ALL)); else updater.changed(new ROIEvent(this, ROIEventType.ROI_CHANGED, ROI_CHANGED_POSITION)); } /** * @deprecated Use {@link #roiChanged(boolean)} instead. */ @Deprecated public void roiChanged() { // handle with updater roiChanged(true); } /** * Called when ROI selected state changed. */ public void selectionChanged() { // handle with updater updater.changed(new ROIEvent(this, ROIEventType.SELECTION_CHANGED)); } /** * Called when ROI focus state changed. */ public void focusChanged() { // handle with updater updater.changed(new ROIEvent(this, ROIEventType.FOCUS_CHANGED)); } /** * Called when ROI painter changed. * * @deprecated */ @Deprecated public void painterChanged() { // handle with updater updater.changed(new ROIEvent(this, ROIEventType.PAINTER_CHANGED)); } /** * Called when ROI name has changed. * * @deprecated Use {@link #propertyChanged(String)} instead. */ @Deprecated public void nameChanged() { // handle with updater updater.changed(new ROIEvent(this, ROIEventType.NAME_CHANGED)); } /** * Called when ROI property has changed */ public void propertyChanged(String propertyName) { // handle with updater updater.changed(new ROIEvent(this, propertyName)); // backward compatibility if (StringUtil.equals(propertyName, PROPERTY_NAME)) updater.changed(new ROIEvent(this, ROIEventType.NAME_CHANGED)); } /** * Add a listener * * @param listener */ public void addListener(ROIListener listener) { listeners.add(listener); } /** * Remove a listener * * @param listener */ public void removeListener(ROIListener listener) { listeners.remove(listener); } private void fireChangedEvent(ROIEvent event) { for (ROIListener listener : new ArrayList<ROIListener>(listeners)) listener.roiChanged(event); } public void beginUpdate() { updater.beginUpdate(); painter.beginUpdate(); } public void endUpdate() { painter.endUpdate(); updater.endUpdate(); } public boolean isUpdating() { return updater.isUpdating(); } @Override public void onChanged(CollapsibleEvent object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // cached properties need to be recomputed boundsInvalid = true; // need to recompute points if (StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL)) { numberOfContourPointsInvalid = true; numberOfPointsInvalid = true; } painter.painterChanged(); break; case SELECTION_CHANGED: case FOCUS_CHANGED: // compute painter priority painter.computePriority(); painter.painterChanged(); break; case PROPERTY_CHANGED: final String property = event.getPropertyName(); // painter affecting display if (StringUtil.isEmpty(property) || StringUtil.equals(property, PROPERTY_NAME) || StringUtil.equals(property, PROPERTY_SHOWNAME) || StringUtil.equals(property, PROPERTY_COLOR) || StringUtil.equals(property, PROPERTY_OPACITY) || StringUtil.equals(property, PROPERTY_SHOWNAME) || StringUtil.equals(property, PROPERTY_STROKE)) painter.painterChanged(); break; default: break; } // notify listener we have changed fireChangedEvent(event); } }