/*
 * 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>&nbsp result[((y1 - y) * width) + (x1 - x)] = true</code><br>
     * If pixel (x1, y1, c, z, t) is not contained in the roi:<br>
     * <code>&nbsp 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>&nbsp 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>&nbsp 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);
    }
}