/*
 * 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 plugins.kernel.roi.roi2d;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.lang.ref.WeakReference;
import java.util.Arrays;

import org.w3c.dom.Node;

import icy.canvas.IcyCanvas;
import icy.canvas.IcyCanvas2D;
import icy.common.CollapsibleEvent;
import icy.gui.inspector.RoisPanel;
import icy.image.ImageUtil;
import icy.main.Icy;
import icy.painter.VtkPainter;
import icy.resource.ResourceUtil;
import icy.roi.BooleanMask2D;
import icy.roi.ROI;
import icy.roi.ROI2D;
import icy.roi.ROIEvent;
import icy.roi.edit.Area2DChangeROIEdit;
import icy.sequence.Sequence;
import icy.system.thread.ThreadUtil;
import icy.type.point.Point5D;
import icy.type.point.Point5D.Double;
import icy.type.rectangle.Rectangle3D;
import icy.util.EventUtil;
import icy.util.GraphicsUtil;
import icy.util.ShapeUtil;
import icy.util.StringUtil;
import icy.util.XMLUtil;
import icy.vtk.IcyVtkPanel;
import icy.vtk.VtkUtil;
import plugins.kernel.canvas.VtkCanvas;
import vtk.vtkActor;
import vtk.vtkImageData;
import vtk.vtkInformation;
import vtk.vtkPolyData;
import vtk.vtkPolyDataMapper;
import vtk.vtkProp;

/**
 * ROI Area type.<br>
 * Use a bitmap mask internally for fast boolean mask operation.<br>
 * 
 * @author Stephane
 */
public class ROI2DArea extends ROI2D
{
    protected static final float DEFAULT_CURSOR_SIZE = 15f;

    // we want to keep a static brush
    protected static final Ellipse2D brush = new Ellipse2D.Double();
    // protected static final Point2D.Double cursorPosition = new Point2D.Double();
    protected static Color brushColor = Color.red;
    protected static float brushSize = DEFAULT_CURSOR_SIZE;

    public class ROI2DAreaPainter extends ROI2DPainter implements VtkPainter, Runnable
    {
        /**
         * @deprecated Use {@link #getOpacity()} instead.
         */
        @Deprecated
        public static final float CONTENT_ALPHA = 0.3f;

        private static final float MIN_CURSOR_SIZE = 0.3f;
        private static final float MAX_CURSOR_SIZE = 500f;

        // VTK 3D objects
        protected vtkPolyData outline;
        protected vtkPolyDataMapper outlineMapper;
        protected vtkActor outlineActor;
        protected vtkInformation vtkInfo;
        protected vtkPolyData polyData;
        protected vtkPolyDataMapper polyMapper;
        protected vtkActor surfaceActor;
        // 3D internal
        protected boolean needRebuild;
        protected double scaling[];
        protected WeakReference<VtkCanvas> canvas3d;
        protected int lastBuildPosZ;

        // internal
        protected final Point2D brushPosition;

        public ROI2DAreaPainter()
        {
            super();

            brushPosition = new Point2D.Double();

            outline = null;
            outlineMapper = null;
            outlineActor = null;
            vtkInfo = null;
            polyData = null;
            polyMapper = null;
            surfaceActor = null;

            scaling = new double[3];
            Arrays.fill(scaling, 1d);

            needRebuild = true;
            canvas3d = new WeakReference<VtkCanvas>(null);
            lastBuildPosZ = getZ();
        }

        @Override
        protected void finalize() throws Throwable
        {
            super.finalize();

            // release allocated VTK resources
            if (surfaceActor != null)
                surfaceActor.Delete();
            if (polyMapper != null)
                polyMapper.Delete();
            if (polyData != null)
            {
                polyData.GetPointData().GetScalars().Delete();
                polyData.GetPointData().Delete();
                polyData.Delete();
            }
            if (outlineActor != null)
            {
                outlineActor.SetPropertyKeys(null);
                outlineActor.Delete();
            }
            if (vtkInfo != null)
            {
                vtkInfo.Remove(VtkCanvas.visibilityKey);
                vtkInfo.Delete();
            }
            if (outlineMapper != null)
                outlineMapper.Delete();
            if (outline != null)
            {
                outline.GetPointData().GetScalars().Delete();
                outline.GetPointData().Delete();
                outline.Delete();
            }
        };

        protected void initVtkObjects()
        {
            outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d);
            outlineMapper = new vtkPolyDataMapper();
            outlineMapper.SetInputData(outline);
            outlineActor = new vtkActor();
            outlineActor.SetMapper(outlineMapper);
            // disable picking on the outline
            outlineActor.SetPickable(0);
            // and set it to wireframe representation
            outlineActor.GetProperty().SetRepresentationToWireframe();
            // use vtkInformations to store outline visibility state (hacky)
            vtkInfo = new vtkInformation();
            vtkInfo.Set(VtkCanvas.visibilityKey, 0);
            // VtkCanvas use this to restore correctly outline visibility flag
            outlineActor.SetPropertyKeys(vtkInfo);

            polyMapper = new vtkPolyDataMapper();
            surfaceActor = new vtkActor();
            surfaceActor.SetMapper(polyMapper);

            final Color col = getColor();
            final double r = col.getRed() / 255d;
            final double g = col.getGreen() / 255d;
            final double b = col.getBlue() / 255d;

            // set actors color
            outlineActor.GetProperty().SetColor(r, g, b);
            surfaceActor.GetProperty().SetColor(r, g, b);
        }

        /**
         * rebuild VTK objects (called only when VTK canvas is selected).
         */
        protected void rebuildVtkObjects()
        {
            final VtkCanvas canvas = canvas3d.get();
            // canvas was closed
            if (canvas == null)
                return;

            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
            // canvas was closed
            if (vtkPanel == null)
                return;

            final Sequence seq = canvas.getSequence();
            // nothing to update
            if (seq == null)
                return;

            // get previous polydata object
            final vtkPolyData previousPolyData = polyData;
            // get VTK binary image from ROI mask
            final vtkImageData imageData = VtkUtil.getBinaryImageData(ROI2DArea.this, seq.getSizeZ(),
                    canvas.getPositionT());
            // adjust spacing
            imageData.SetSpacing(scaling[0], scaling[1], scaling[2]);
            // get VTK polygon data representing the surface of the binary image
            polyData = VtkUtil.getSurfaceFromImage(imageData, 0.5d);

            // get bounds
            final Rectangle3D bounds = getBounds5D().toRectangle3D();
            // apply scaling on bounds
            bounds.setX(bounds.getX() * scaling[0]);
            bounds.setSizeX(bounds.getSizeX() * scaling[0]);
            bounds.setY(bounds.getY() * scaling[1]);
            bounds.setSizeY(bounds.getSizeY() * scaling[1]);
            if (bounds.isInfiniteZ())
            {
                bounds.setZ(0);
                bounds.setSizeZ(seq.getSizeZ() * scaling[2]);
                lastBuildPosZ = -1;
            }
            else
            {
                lastBuildPosZ = getZ();
                bounds.setZ(bounds.getZ() * scaling[2]);
                bounds.setSizeZ(1d * scaling[2]);
            }

            // actor can be accessed in canvas3d for rendering so we need to synchronize access
            vtkPanel.lock();
            try
            {
                // update outline data
                VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
                        bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas);
                outlineMapper.Update();
                // update polygon data from image
                polyMapper.SetInputData(polyData);
                polyMapper.Update();

                // update actor position
                surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ());

                // release image data
                imageData.GetPointData().GetScalars().Delete();
                imageData.GetPointData().Delete();
                imageData.Delete();

                // release previous polydata
                if (previousPolyData != null)
                {
                    previousPolyData.GetPointData().GetScalars().Delete();
                    previousPolyData.GetPointData().Delete();
                    previousPolyData.Delete();
                }
            }
            finally
            {
                vtkPanel.unlock();
            }

            // update color and others properties
            updateVtkDisplayProperties();
        }

        protected void updateVtkDisplayProperties()
        {
            if (surfaceActor == null)
                return;

            final VtkCanvas cnv = canvas3d.get();
            final Color col = getDisplayColor();
            final double r = col.getRed() / 255d;
            final double g = col.getGreen() / 255d;
            final double b = col.getBlue() / 255d;
            // final double strk = getStroke();
            // final float opacity = getOpacity();

            final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null;

            // we need to lock canvas as actor can be accessed during rendering
            if (vtkPanel != null)
                vtkPanel.lock();
            try
            {
                // set actors color
                outlineActor.GetProperty().SetColor(r, g, b);
                if (isSelected())
                {
                    outlineActor.GetProperty().SetRepresentationToWireframe();
                    outlineActor.SetVisibility(1);
                    vtkInfo.Set(VtkCanvas.visibilityKey, 1);
                }
                else
                {
                    outlineActor.GetProperty().SetRepresentationToPoints();
                    outlineActor.SetVisibility(0);
                    vtkInfo.Set(VtkCanvas.visibilityKey, 0);
                }
                surfaceActor.GetProperty().SetColor(r, g, b);
                // opacity here is about ROI content, global opacity is handled by Layer
                // surfaceActor.GetProperty().SetOpacity(opacity);
                setVtkObjectsColor(col);
            }
            finally
            {
                if (vtkPanel != null)
                    vtkPanel.unlock();
            }

            // need to repaint
            painterChanged();
        }

        protected void setVtkObjectsColor(Color color)
        {
            if (outline != null)
                VtkUtil.setPolyDataColor(outline, color, canvas3d.get());
            if (polyData != null)
                VtkUtil.setPolyDataColor(polyData, color, canvas3d.get());
        }

        protected void updateVtkObjectsBounds()
        {
            final VtkCanvas canvas = canvas3d.get();
            // canvas was closed
            if (canvas == null)
                return;

            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
            // canvas was closed
            if (vtkPanel == null)
                return;

            final Sequence seq = canvas.getSequence();
            // nothing to update
            if (seq == null)
                return;

            final Rectangle3D bounds = getBounds5D().toRectangle3D();
            // apply scaling on bounds
            bounds.setX(bounds.getX() * scaling[0]);
            bounds.setSizeX(bounds.getSizeX() * scaling[0]);
            bounds.setY(bounds.getY() * scaling[1]);
            bounds.setSizeY(bounds.getSizeY() * scaling[1]);
            if (bounds.isInfiniteZ())
            {
                bounds.setZ(0);
                bounds.setSizeZ(seq.getSizeZ() * scaling[2]);
            }
            else
            {
                bounds.setZ(bounds.getZ() * scaling[2]);
                bounds.setSizeZ(1d * scaling[2]);
            }

            // actor can be accessed in canvas3d for rendering so we need to synchronize access
            vtkPanel.lock();
            try
            {
                // update outline position
                VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
                        bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas);
                outlineMapper.Update();

                // update actor position
                surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ());
            }
            finally
            {
                vtkPanel.unlock();
            }
        }

        void updateCursor()
        {
            final double x = brushPosition.getX();
            final double y = brushPosition.getY();

            brush.setFrameFromDiagonal(x - brushSize, y - brushSize, x + brushSize, y + brushSize);

            // if roi selected (cursor displayed) --> painter changed
            if (isSelected())
                painterChanged();
        }

        /**
         * Returns the brush position.
         */
        public Point2D getBrushPosition()
        {
            return (Point) brushPosition.clone();
        }

        /**
         * Set the brush position.
         */
        public void setBrushPosition(Point2D position)
        {
            if (!brushPosition.equals(position))
            {
                brushPosition.setLocation(position);
                updateCursor();
            }
        }

        /**
         * @deprecated Use {@link #getBrushPosition()} instead.
         */
        @Deprecated
        public Point2D getCursorPosition()
        {
            return getBrushPosition();
        }

        /**
         * @deprecated Use {@link #setBrushPosition(Point2D)} instead.
         */
        @Deprecated
        public void setCursorPosition(Point2D position)
        {
            setBrushPosition(position);
        }

        /**
         * Returns the brush size.
         */
        public float getBrushSize()
        {
            return brushSize;
        }

        /**
         * Sets the brush size.
         */
        public void setBrushSize(float value)
        {
            final float adjValue = Math.max(Math.min(value, MAX_CURSOR_SIZE), MIN_CURSOR_SIZE);

            if (brushSize != adjValue)
            {
                brushSize = adjValue;
                updateCursor();
            }
        }

        /**
         * @deprecated Use {@link #getBrushSize()} instead
         */
        @Deprecated
        public float getCursorSize()
        {
            return getBrushSize();
        }

        /**
         * @deprecated Use {@link #setBrushSize(float)} instead
         */
        @Deprecated
        public void setCursorSize(float value)
        {
            setBrushSize(value);
        }

        /**
         * Returns the brush color
         */
        public Color getBrushColor()
        {
            return brushColor;
        }

        /**
         * Sets the brush color
         */
        public void setBrushColor(Color value)
        {
            if (!brushColor.equals(value))
            {
                brushColor = value;
                painterChanged();
            }
        }

        /**
         * @deprecated Use {@link #getBrushColor()} instead
         */
        @Deprecated
        public Color getCursorColor()
        {
            return getBrushColor();
        }

        /**
         * @deprecated Use {@link #setBrushColor(Color)} instead
         */
        @Deprecated
        public void setCursorColor(Color value)
        {
            setBrushColor(value);
        }

        public void addToMask(Point2D pos)
        {
            setBrushPosition(pos);
            updateMask(brush, false);
        }

        public void removeFromMask(Point2D pos)
        {
            setBrushPosition(pos);
            updateMask(brush, true);
        }

        @Override
        public void painterChanged()
        {
            updateMaskColor(true);

            super.painterChanged();
        }

        @Override
        protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas)
        {
            // specific VTK canvas processing
            if (canvas instanceof VtkCanvas)
            {
                // mouse is over the ROI actor ? --> focus the ROI
                final boolean focused = (surfaceActor != null)
                        && (surfaceActor == ((VtkCanvas) canvas).getPickedObject());

                setFocused(focused);

                return focused;
            }

            return super.updateFocus(e, imagePoint, canvas);
        }

        @Override
        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.keyPressed(e, imagePoint, canvas);

            // edition not supported on VtkCanvas
            if (canvas instanceof VtkCanvas)
                return;

            // not yet consumed and ROI editable...
            if (!e.isConsumed() && !isReadOnly())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    ROI2DArea.this.beginUpdate();
                    try
                    {
                        switch (e.getKeyChar())
                        {
                            case '+':
                                if (isSelected())
                                {
                                    setBrushSize(getBrushSize() * 1.1f);
                                    e.consume();
                                }
                                break;

                            case '-':
                                if (isSelected())
                                {
                                    setBrushSize(getBrushSize() * 0.9f);
                                    e.consume();
                                }
                                break;
                        }
                    }
                    finally
                    {
                        ROI2DArea.this.endUpdate();
                    }
                }
            }
        }

        @Override
        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mousePressed(e, imagePoint, canvas);

            // edition not supported on VtkCanvas
            if (canvas instanceof VtkCanvas)
                return;
            // we need it
            if (imagePoint == null)
                return;

            // not yet consumed, ROI editable, selected and not focused...
            if (!e.isConsumed() && !isReadOnly() && isSelected() && !isFocused())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    // keep trace of roi changes from user mouse action
                    roiModifiedByMouse = false;
                    // save current ROI
                    undoSave = getBooleanMask(true);

                    ROI2DArea.this.beginUpdate();
                    try
                    {
                        // left button action
                        if (EventUtil.isLeftMouseButton(e))
                        {
                            // add point first
                            addToMask(imagePoint.toPoint2D());
                            roiModifiedByMouse = true;
                            e.consume();
                        }
                        // right button action
                        else if (EventUtil.isRightMouseButton(e))
                        {
                            // remove point
                            removeFromMask(imagePoint.toPoint2D());
                            roiModifiedByMouse = true;
                            e.consume();
                        }
                    }
                    finally
                    {
                        ROI2DArea.this.endUpdate();
                    }
                }
            }
        }

        @Override
        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseReleased(e, imagePoint, canvas);

            // update only on release as it can be long
            if (!isReadOnly())
            {
                if (roiModifiedByMouse)
                {
                    if (boundsNeedUpdate)
                    {
                        if (optimizeBounds())
                        {
                            roiChanged(true);

                            // empty ? delete ROI
                            if (bounds.isEmpty())
                            {
                                ROI2DArea.this.remove();
                                // nothing more to do
                                return;
                            }
                        }
                    }

                    final Sequence sequence = canvas.getSequence();

                    // add undo operation
                    try
                    {
                        if ((sequence != null) && (undoSave != null))
                            sequence.addUndoableEdit(new Area2DChangeROIEdit(ROI2DArea.this, undoSave));
                    }
                    catch (OutOfMemoryError err)
                    {
                        // can't create undo operation, show message and clear undo manager
                        System.out.println("Warning: not enough memory to create undo point for ROI area change");
                        sequence.clearUndoManager();
                    }

                    // release save
                    undoSave = null;
                    roiModifiedByMouse = false;
                }
            }
        }

        @Override
        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // provide backward compatibility
            if (imagePoint != null)
                mouseClick(e, imagePoint.toPoint2D(), canvas);
            else
                mouseClick(e, (Point2D) null, canvas);

            // not yet consumed...
            if (!e.isConsumed())
            {
                // and process ROI stuff now
                if (isActiveFor(canvas))
                {
                    final int clickCount = e.getClickCount();

                    // double click
                    if (clickCount == 2)
                    {
                        // focused ?
                        if (isFocused())
                        {
                            // show in ROI panel
                            final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel();

                            if (roiPanel != null)
                            {
                                roiPanel.scrollTo(ROI2DArea.this);
                                // consume event
                                e.consume();
                            }
                        }
                    }
                }
            }
            
        }

        @Override
        public void mouseMove(MouseEvent e, Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseMove(e, imagePoint, canvas);

            // edition not supported on VtkCanvas
            if (canvas instanceof VtkCanvas)
                return;
            // we need it
            if (imagePoint == null)
                return;

            // not yet consumed, ROI editable and selected...
            if (!e.isConsumed() && !isReadOnly() && isSelected())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    setBrushPosition(imagePoint.toPoint2D());
                }
            }
        }

        @Override
        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
        {
            // send event to parent first
            super.mouseDrag(e, imagePoint, canvas);

            // edition not supported on VtkCanvas
            if (canvas instanceof VtkCanvas)
                return;
            // we need it
            if (imagePoint == null)
                return;

            // not yet consumed, ROI editable and selected...
            if (!e.isConsumed() && !isReadOnly() && isSelected())
            {
                // then process it here
                if (isActiveFor(canvas))
                {
                    ROI2DArea.this.beginUpdate();
                    try
                    {
                        // left button action
                        if (EventUtil.isLeftMouseButton(e))
                        {
                            // add point first
                            addToMask(imagePoint.toPoint2D());
                            roiModifiedByMouse = true;
                            e.consume();
                        }
                        // right button action
                        else if (EventUtil.isRightMouseButton(e))
                        {
                            // remove point
                            removeFromMask(imagePoint.toPoint2D());
                            roiModifiedByMouse = true;
                            e.consume();
                        }
                    }
                    finally
                    {
                        ROI2DArea.this.endUpdate();
                    }
                }
            }
        }

        @Override
        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            super.paint(g, sequence, canvas);

            if (isActiveFor(canvas))
            {
                // ROI selected ? draw cursor
                if (isSelected() && !isFocused() && !isReadOnly())
                    drawCursor(g, sequence, canvas);
            }
        }

        /**
         * Draw the ROI itself
         */
        @Override
        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // not supported
                if (g == null)
                    return;

                final Rectangle bounds = getBounds();
                // trivial paint optimization
                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);

                if (shapeVisible)
                {
                    final Graphics2D g2 = (Graphics2D) g.create();
                    final boolean small;

                    // disable LOD when creating the ROI
                    if (isCreating())
                        small = false;
                    else
                    {
                        final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
                        small = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_SMALL;
                    }

                    // simplified draw
                    if (small)
                    {
                        g2.setColor(getDisplayColor());
                        g2.drawImage(imageMask, null, bounds.x, bounds.y);
                    }
                    // normal draw
                    else
                    {
                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
                        float newAlpha = prevAlpha.getAlpha() * getOpacity();
                        newAlpha = Math.min(1f, newAlpha);
                        newAlpha = Math.max(0f, newAlpha);

                        // show content with an alpha factor
                        g2.setComposite(prevAlpha.derive(newAlpha));

                        // draw mask
                        g2.drawImage(imageMask, null, bounds.x, bounds.y);

                        // restore alpha
                        g2.setComposite(prevAlpha);

                        // draw border
                        if (isSelected())
                        {
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                            g2.setColor(getDisplayColor());
                            g2.draw(bounds);
                        }
                        else
                        {
                            // outside border
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
                            g2.setColor(Color.black);
                            g2.draw(bounds);
                            // internal border
                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
                            g2.setColor(getDisplayColor());
                            g2.draw(bounds);
                        }
                    }

                    g2.dispose();
                }

                // for (Point2D pt : getBooleanMask().getEdgePoints())
                // g2.drawRect((int) pt.getX(), (int) pt.getY(), 1, 1);
            }

            if (canvas instanceof VtkCanvas)
            {
                // 3D canvas
                final VtkCanvas cnv = (VtkCanvas) canvas;
                // update reference if needed
                if (canvas3d.get() != cnv)
                    canvas3d = new WeakReference<VtkCanvas>(cnv);

                // initialize VTK objects if not yet done
                if (surfaceActor == null)
                    initVtkObjects();

                // FIXME : need a better implementation
                final double[] s = cnv.getVolumeScale();

                // scaling changed ?
                if (!Arrays.equals(scaling, s))
                {
                    // update scaling
                    scaling = s;
                    // need rebuild
                    needRebuild = true;
                }

                // need to rebuild 3D data structures ?
                if (needRebuild)
                {
                    // request rebuild 3D objects
                    ThreadUtil.runSingle(this);
                    needRebuild = false;
                }
            }
        }

        /**
         * draw the ROI cursor
         */
        protected void drawCursor(Graphics2D g, Sequence sequence, IcyCanvas canvas)
        {
            if (canvas instanceof IcyCanvas2D)
            {
                // not supported
                if (g == null)
                    return;

                final Rectangle bounds = brush.getBounds();
                // trivial paint optimization
                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);

                if (shapeVisible)
                {
                    final Graphics2D g2 = (Graphics2D) g.create();
                    final boolean tiny;

                    // disable LOD when creating the ROI
                    if (isCreating())
                        tiny = false;
                    else
                    {
                        final double scale = Math.max(canvas.getScaleX(), canvas.getScaleY());
                        tiny = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_TINY;
                    }

                    // simplified draw
                    if (tiny)
                    {
                        // cursor color
                        g2.setColor(brushColor);
                        // draw cursor
                        g2.fill(brush);
                    }
                    // normal draw
                    else
                    {
                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
                        float newAlpha = prevAlpha.getAlpha() * getOpacity() * 2f;
                        newAlpha = Math.min(1f, newAlpha);
                        newAlpha = Math.max(0f, newAlpha);

                        // show cursor with an alpha factor
                        g2.setComposite(prevAlpha.derive(newAlpha));

                        // draw cursor border
                        g2.setColor(Color.black);
                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
                        g2.draw(brush);
                        // draw cursor
                        g2.setColor(brushColor);
                        g2.fill(brush);
                    }

                    g2.dispose();
                }
            }
        }

        @Override
        public vtkProp[] getProps()
        {
            // initialize VTK objects if not yet done
            if (surfaceActor == null)
                initVtkObjects();

            return new vtkActor[] {surfaceActor, outlineActor};
        }

        @Override
        public void run()
        {
            rebuildVtkObjects();
        }
    }

    public static final String ID_BOUNDS_X = "boundsX";
    public static final String ID_BOUNDS_Y = "boundsY";
    public static final String ID_BOUNDS_W = "boundsW";
    public static final String ID_BOUNDS_H = "boundsH";
    // protected static final String ID_BOOLMASK_LEN = "boolMaskLen";
    public static final String ID_BOOLMASK_DATA = "boolMaskData";

    /**
     * image containing the mask
     */
    protected BufferedImage imageMask;
    /**
     * rectangle bounds
     */
    protected Rectangle bounds;

    /**
     * internals
     */
    protected final byte[] red;
    protected final byte[] green;
    protected final byte[] blue;
    protected IndexColorModel colorModel;
    protected byte[] maskData; // 0 = false, 1 = true
    protected double translateX, translateY;
    protected Color previousColor;
    protected boolean boundsNeedUpdate;
    protected boolean roiModifiedByMouse;
    protected BooleanMask2D undoSave;

    /**
     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
     */
    public ROI2DArea()
    {
        super();

        bounds = new Rectangle();
        boundsNeedUpdate = false;
        roiModifiedByMouse = false;
        undoSave = null;
        translateX = 0d;
        translateY = 0d;

        // prepare indexed image
        red = new byte[256];
        green = new byte[256];
        blue = new byte[256];

        // keep trace of previous color
        previousColor = getDisplayColor();

        // set colormap
        red[1] = (byte) previousColor.getRed();
        green[1] = (byte) previousColor.getGreen();
        blue[1] = (byte) previousColor.getBlue();

        // classic 8 bits indexed with one transparent color (index = 0)
        colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
        // create default image
        imageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
        // get data pointer
        maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();

        // set icon (default name is defined by getDefaultName())
        setIcon(ResourceUtil.ICON_ROI_AREA);
    }

    /**
     * @deprecated Use {@link #ROI2DArea(Point5D)} instead
     */
    @Deprecated
    public ROI2DArea(Point2D position, boolean cm)
    {
        this(position);
    }

    /**
     * Create a ROI2D Area type with a single point.
     */
    public ROI2DArea(Point2D position)
    {
        this();

        // add current point to mask
        addBrush(position);
    }

    /**
     * Generic constructor for interactive mode.
     */
    public ROI2DArea(Point5D position)
    {
        this(position.toPoint2D());
    }

    /**
     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
     */
    public ROI2DArea(BooleanMask2D mask)
    {
        this();

        setAsBooleanMask(mask);
    }

    /**
     * Create a copy of the specified 2D Area ROI
     */
    public ROI2DArea(ROI2DArea area)
    {
        super();

        bounds = new Rectangle();
        boundsNeedUpdate = false;
        roiModifiedByMouse = false;
        undoSave = null;
        translateX = 0d;
        translateY = 0d;

        // prepare indexed image
        red = new byte[256];
        green = new byte[256];
        blue = new byte[256];

        // keep trace of previous color
        previousColor = getDisplayColor();

        // set colormap
        red[1] = (byte) previousColor.getRed();
        green[1] = (byte) previousColor.getGreen();
        blue[1] = (byte) previousColor.getBlue();

        // classic 8 bits indexed with one transparent color (index = 0)
        colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
        imageMask = new BufferedImage(area.bounds.width, area.bounds.height, BufferedImage.TYPE_BYTE_INDEXED,
                colorModel);
        maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();

        System.arraycopy(area.maskData, 0, maskData, 0, maskData.length);

        bounds.setBounds(area.bounds);

        // set icon (default name is defined by getDefaultName())
        setIcon(ResourceUtil.ICON_ROI_AREA);
    }

    @Override
    public String getDefaultName()
    {
        return "Area2D";
    }

    void addToBounds(Rectangle bnd)
    {
        final Rectangle newBounds;

        if (bounds.isEmpty())
            newBounds = new Rectangle(bnd);
        else
        {
            newBounds = new Rectangle(bounds);
            newBounds.add(bnd);
        }

        try
        {
            // update image to the new bounds
            updateImage(newBounds);
        }
        catch (Error E)
        {
            // perhaps a "out of memory" error, restore back old bounds
            System.err.println("can't enlarge ROI, no enough memory !");
        }
    }

    /**
     * @deprecated Use {@link #optimizeBounds()} instead.
     */
    @Deprecated
    public void optimizeBounds(boolean removeIfEmpty)
    {
        optimizeBounds();
        if (removeIfEmpty && bounds.isEmpty())
            remove();
    }

    /**
     * Returns true if the ROI is empty (the mask does not contains any point).
     */
    @Override
    public boolean isEmpty()
    {
        if (bounds.isEmpty())
            return true;

        final byte[] data = maskData;

        for (byte b : data)
            if (b != 0)
                return false;

        return true;
    }

    /**
     * Optimize the bounds size to the minimum surface which still include all mask<br>
     * You should call it after consecutive remove operations.
     */
    public boolean optimizeBounds()
    {
        // bounds are being updated
        boundsNeedUpdate = false;

        final byte[] data;
        final Rectangle bnds;

        // recompute bound from the mask data
        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        final int sizeX = bnds.width;
        final int sizeY = bnds.height;

        int minX, minY, maxX, maxY;
        minX = maxX = minY = maxY = 0;
        boolean empty = true;
        int offset = 0;

        for (int y = 0; y < sizeY; y++)
        {
            for (int x = 0; x < sizeX; x++)
            {
                if (data[offset++] != 0)
                {
                    if (empty)
                    {
                        minX = maxX = x;
                        minY = maxY = y;
                        empty = false;
                    }
                    else
                    {
                        if (x < minX)
                            minX = x;
                        else if (x > maxX)
                            maxX = x;
                        if (y < minY)
                            minY = y;
                        else if (y > maxY)
                            maxY = y;
                    }
                }
            }
        }

        if (!empty)
            // update image to the new bounds
            return updateImage(new Rectangle(bnds.x + minX, bnds.y + minY, (maxX - minX) + 1, (maxY - minY) + 1));

        // update to empty bounds
        return updateImage(new Rectangle(bnds.x, bnds.y, 0, 0));
    }

    /**
     * @deprecated Use {@link #getDisplayColor()} instead.
     */
    @Deprecated
    public Color getMaskColor()
    {
        return getOverlay().getDisplayColor();
    }

    void updateMaskColor(boolean rebuildImage)
    {
        final Color color = getOverlay().getDisplayColor();

        // roi color changed ?
        if (!previousColor.equals(color))
        {
            // update colormap
            red[1] = (byte) color.getRed();
            green[1] = (byte) color.getGreen();
            blue[1] = (byte) color.getBlue();

            colorModel = new IndexColorModel(8, 256, red, green, blue, 0);

            // recreate image (so the new colormodel takes effect)
            if (rebuildImage)
                imageMask = ImageUtil.createIndexedImage(imageMask.getWidth(), imageMask.getHeight(), colorModel,
                        maskData);

            // set to new color
            previousColor = color;
        }
    }

    /**
     * Returns the internal image mask.
     */
    public BufferedImage getImageMask()
    {
        return imageMask;
    }

    boolean updateImage(Rectangle newBnd)
    {
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // copy rectangle
        final Rectangle oldBounds = new Rectangle(bnds);
        final Rectangle newBounds = new Rectangle(newBnd);

        // replace to oldBounds origin
        oldBounds.translate(-bnds.x, -bnds.y);
        newBounds.translate(-bnds.x, -bnds.y);

        // dimension changed ?
        if ((oldBounds.width != newBounds.width) || (oldBounds.height != newBounds.height))
        {
            final BufferedImage newImageMask;
            final byte[] newMaskData;

            if (!newBounds.isEmpty())
            {
                // new bounds not empty
                newImageMask = new BufferedImage(newBounds.width, newBounds.height, BufferedImage.TYPE_BYTE_INDEXED,
                        colorModel);
                newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();

                final Rectangle intersect = newBounds.intersection(oldBounds);

                if (!intersect.isEmpty())
                {
                    int offSrc = 0;
                    int offDst = 0;

                    // adjust offset in source mask
                    if (intersect.x > 0)
                        offSrc += intersect.x;
                    if (intersect.y > 0)
                        offSrc += intersect.y * oldBounds.width;
                    // adjust offset in destination mask
                    if (newBounds.x < 0)
                        offDst += -newBounds.x;
                    if (newBounds.y < 0)
                        offDst += -newBounds.y * newBounds.width;

                    // preserve data
                    for (int j = 0; j < intersect.height; j++)
                    {
                        System.arraycopy(data, offSrc, newMaskData, offDst, intersect.width);

                        offSrc += oldBounds.width;
                        offDst += newBounds.width;
                    }
                }
            }
            else
            {
                // new bounds empty --> use single pixel image to avoid NPE
                newImageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
                newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();
            }

            synchronized (this)
            {
                // set new image and maskData
                imageMask = newImageMask;
                maskData = newMaskData;
                bounds = newBnd;
            }

            return true;
        }

        return false;
    }

    /**
     * Set the value of the specified point.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation to refresh the mask
     * bounds.
     */
    public void setPoint(int x, int y, boolean value)
    {
        final byte[] data;
        final Rectangle bnds;

        if (value)
        {
            // set point in mask
            addToBounds(new Rectangle(x, y, 1, 1));

            synchronized (this)
            {
                data = maskData;
                bnds = bounds;
            }

            // set color depending remove or adding to mask
            data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 1;
            // notify roi changed
            roiChanged(true);
        }
        else
        {
            synchronized (this)
            {
                data = maskData;
                bnds = bounds;
            }

            if (bnds.contains(x, y))
            {
                // remove point from mask
                data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 0;
                // mark that bounds need to be updated
                boundsNeedUpdate = true;
                // notify roi changed
                roiChanged(true);
            }
        }
    }

    /**
     * @deprecated Use {@link #setPoint(int, int, boolean)} instead.
     */
    @Deprecated
    public void updateMask(int x, int y, boolean remove)
    {
        setPoint(x, y, !remove);
    }

    /**
     * Add the specified {@link ROI2DArea} content to this ROI2DArea
     */
    public void add(ROI2DArea roi)
    {
        final Rectangle boundsToAdd = roi.getBounds();
        final byte[] maskToAdd = roi.maskData;

        // update bounds (this update the image dimension if needed)
        addToBounds(boundsToAdd);

        int offDst, offSrc;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // calculate offset
        offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x);
        offSrc = 0;

        for (int y = 0; y < boundsToAdd.height; y++)
        {
            for (int x = 0; x < boundsToAdd.width; x++)
                if (maskToAdd[offSrc++] != 0)
                    data[offDst + x] = 1;

            offDst += bnds.width;
        }

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Add the specified {@link BooleanMask2D} content to this ROI2DArea
     */
    public void add(BooleanMask2D mask)
    {
        final Rectangle boundsToAdd = mask.bounds;
        final boolean[] maskToAdd = mask.mask;

        // update bounds (this update the image dimension if needed)
        addToBounds(boundsToAdd);

        int offDst, offSrc;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // calculate offset
        offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x);
        offSrc = 0;

        for (int y = 0; y < boundsToAdd.height; y++)
        {
            for (int x = 0; x < boundsToAdd.width; x++)
                if (maskToAdd[offSrc++])
                    data[offDst + x] = 1;

            offDst += bnds.width;
        }

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Exclusively add the specified {@link ROI2DArea} content to this ROI2DArea:
     * 
     * <pre>
     *          mask1       xor      mask2        =       result
     * 
     *     ################     ################
     *     ##############         ##############     ##            ##
     *     ############             ############     ####        ####
     *     ##########                 ##########     ######    ######
     *     ########                     ########     ################
     *     ######                         ######     ######    ######
     *     ####                             ####     ####        ####
     *     ##                                 ##     ##            ##
     * </pre>
     */
    public void exclusiveAdd(ROI2DArea roi)
    {
        final Rectangle boundsToXAdd = roi.getBounds();
        final byte[] maskToXAdd = roi.maskData;

        // update bounds (this update the image dimension if needed)
        addToBounds(boundsToXAdd);

        int offDst, offSrc;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // calculate offset
        offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x);
        offSrc = 0;

        for (int y = 0; y < boundsToXAdd.height; y++)
        {
            for (int x = 0; x < boundsToXAdd.width; x++)
                if (maskToXAdd[offSrc++] != 0)
                    data[offDst + x] ^= 1;

            offDst += bnds.width;
        }

        // optimize bounds
        if (isUpdating())
            boundsNeedUpdate = true;
        else
            optimizeBounds();

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Exclusively add the specified {@link BooleanMask2D} content to this ROI2DArea:
     * 
     * <pre>
     *          mask1       xor      mask2        =       result
     * 
     *     ################     ################
     *     ##############         ##############     ##            ##
     *     ############             ############     ####        ####
     *     ##########                 ##########     ######    ######
     *     ########                     ########     ################
     *     ######                         ######     ######    ######
     *     ####                             ####     ####        ####
     *     ##                                 ##     ##            ##
     * </pre>
     */
    public void exclusiveAdd(BooleanMask2D mask)
    {
        final Rectangle boundsToXAdd = mask.bounds;
        final boolean[] maskToXAdd = mask.mask;

        // update bounds (this update the image dimension if needed)
        addToBounds(boundsToXAdd);

        int offDst, offSrc;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // calculate offset
        offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x);
        offSrc = 0;

        for (int y = 0; y < boundsToXAdd.height; y++)
        {
            for (int x = 0; x < boundsToXAdd.width; x++)
                if (maskToXAdd[offSrc++])
                    data[offDst + x] ^= 1;

            offDst += bnds.width;
        }

        // optimize bounds
        if (isUpdating())
            boundsNeedUpdate = true;
        else
            optimizeBounds();

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Subtract the specified {@link ROI2DArea} from this ROI2DArea
     */
    public void subtract(ROI2DArea roi)
    {
        final Rectangle boundsToRemove = roi.getBounds();
        final byte[] maskToRemove = roi.maskData;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // compute intersection
        final Rectangle intersection = bnds.intersection(boundsToRemove);

        // nothing to remove so nothing to do...
        if (intersection.isEmpty())
            return;

        // calculate offset
        int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x);
        int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x);

        for (int y = 0; y < intersection.height; y++)
        {
            for (int x = 0; x < intersection.width; x++)
                if (maskToRemove[offSrc + x] != 0)
                    data[offDst + x] = 0;

            offDst += bnds.width;
            offSrc += boundsToRemove.width;
        }

        // optimize bounds
        if (isUpdating())
            boundsNeedUpdate = true;
        else
            optimizeBounds();

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Subtract the specified {@link BooleanMask2D} from this ROI2DArea
     */
    public void subtract(BooleanMask2D mask)
    {
        final Rectangle boundsToRemove = mask.bounds;
        final boolean[] maskToRemove = mask.mask;
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // compute intersection
        final Rectangle intersection = bnds.intersection(boundsToRemove);

        // nothing to remove so nothing to do...
        if (intersection.isEmpty())
            return;

        // calculate offset
        int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x);
        int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x);

        for (int y = 0; y < intersection.height; y++)
        {
            for (int x = 0; x < intersection.width; x++)
                if (maskToRemove[offSrc + x])
                    data[offDst + x] = 0;

            offDst += bnds.width;
            offSrc += boundsToRemove.width;
        }

        // optimize bounds
        if (isUpdating())
            boundsNeedUpdate = true;
        else
            optimizeBounds();

        // notify roi changed
        roiChanged(true);
    }

    /**
     * @deprecated Use {@link #subtract(ROI2DArea)} instead
     */
    @Deprecated
    public void remove(ROI2DArea roi)
    {
        subtract(roi);
    }

    /**
     * @deprecated Use {@link #subtract(BooleanMask2D)} instead
     */
    @Deprecated
    public void remove(BooleanMask2D mask)
    {
        subtract(mask);
    }

    /**
     * Update mask by adding/removing the specified shape to/from it.
     * 
     * @param shape
     *        the shape to add in or remove from the mask
     * @param remove
     *        if set to <code>true</code> the shape will be removed from the mask
     * @param inclusive
     *        if we should also consider the edge of the shape to update the mask
     * @param accurate
     *        if set to <code>true</code> the operation will be done to be as pixel accurate as
     *        possible
     * @param immediateUpdate
     *        if set to <code>true</code> the bounds of the mask will be immediately recomputed
     *        (only meaningful for a
     *        remove operation)
     */
    public void updateMask(Shape shape, boolean remove, boolean inclusive, boolean accurate, boolean immediateUpdate)
    {
        if (remove)
        {
            // outside bounds ? --> nothing to remove so nothing to do...
            if (!bounds.intersects(shape.getBounds2D()))
                return;

            // mark that bounds need to be updated
            if (isUpdating() || !immediateUpdate)
                boundsNeedUpdate = true;
        }
        else
            // update bounds (this update the image dimension if needed)
            addToBounds(shape.getBounds());

        // get image graphics object
        final Graphics2D g = imageMask.createGraphics();

        // we don't need anti aliasing here
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
        // force accurate stroke rendering
        if (accurate)
            g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);

        g.setComposite(AlphaComposite.Src);
        // set color depending remove or adding to mask
        if (remove)
            g.setColor(new Color(colorModel.getRGB(0), true));
        else
            g.setColor(new Color(colorModel.getRGB(1), true));
        // translate to origin of image and pixel center
        g.translate(-(bounds.x + 0.5d), -(bounds.y + 0.5d));
        // draw shape into the mask
        g.fill(ShapeUtil.getClosedPath(shape));
        // we want edge as well
        if (inclusive)
            g.draw(shape);

        g.dispose();

        // need to optimize bounds
        if (remove && !isUpdating() && immediateUpdate)
            optimizeBounds();

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Update mask from specified shape
     */
    public void updateMask(Shape shape, boolean remove)
    {
        updateMask(shape, remove, true, false, false);
    }

    @Deprecated
    @Override
    public ROI2DAreaPainter getPainter()
    {
        return getOverlay();
    }

    @Override
    public ROI2DAreaPainter getOverlay()
    {
        return (ROI2DAreaPainter) super.painter;
    }

    @Override
    protected ROI2DAreaPainter createPainter()
    {
        return new ROI2DAreaPainter();
    }

    @Override
    public boolean hasSelectedPoint()
    {
        return false;
    }

    /**
     * @deprecated useless method.
     */
    @Deprecated
    public boolean canAddPoint()
    {
        return true;
    }

    /**
     * @deprecated useless method.
     */
    @Deprecated
    public boolean canRemovePoint()
    {
        return true;
    }

    /**
     * @deprecated Use {@link #addBrush(Point2D)} instead.
     */
    @Deprecated
    public boolean addPointAt(Point2D pos, boolean ctrl)
    {
        addBrush(pos);
        return true;
    }

    /**
     * @deprecated Use {@link #removeBrush(Point2D)} instead.
     */
    @Deprecated
    public boolean removePointAt(IcyCanvas canvas, Point2D pos)
    {
        removeBrush(pos);
        return true;
    }

    /**
     * @deprecated Useless method.
     */
    @Deprecated
    protected boolean removeSelectedPoint(IcyCanvas canvas, Point2D imagePoint)
    {
        // no selected point for this ROI
        return false;
    }

    /**
     * Add brush point at specified position.
     */
    public void addBrush(Point2D pos)
    {
        getOverlay().addToMask(pos);
    }

    /**
     * Remove brush point from the mask at specified position.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removeBrush(Point2D pos)
    {
        getOverlay().removeFromMask(pos);
    }

    /**
     * Add a point to the mask
     */
    public void addPoint(Point pos)
    {
        addPoint(pos.x, pos.y);
    }

    /**
     * Add a point to the mask
     */
    public void addPoint(int x, int y)
    {
        setPoint(x, y, true);
    }

    /**
     * Remove a point from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removePoint(Point pos)
    {
        removePoint(pos.x, pos.y);
    }

    /**
     * Remove a point to the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation
     * to refresh the mask bounds.
     */
    public void removePoint(int x, int y)
    {
        setPoint(x, y, false);
    }

    /**
     * Add a rectangle to the mask
     */
    public void addRect(Rectangle r)
    {
        updateMask(r, false, false, true, true);
    }

    /**
     * Add a rectangle to the mask
     */
    public void addRect(int x, int y, int w, int h)
    {
        addRect(new Rectangle(x, y, w, h));
    }

    /**
     * Remove a rectangle from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeRect(Rectangle r)
    {
        updateMask(r, true, false, true, true);
    }

    /**
     * Remove a rectangle from the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeRect(int x, int y, int w, int h)
    {
        removeRect(new Rectangle(x, y, w, h));
    }

    /**
     * Add a shape to the mask
     */
    public void addShape(Shape s)
    {
        updateMask(s, false, false, true, true);
    }

    /**
     * Remove a shape to the mask.<br>
     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
     * to refresh the mask bounds.
     */
    public void removeShape(Shape s)
    {
        updateMask(s, true, false, true, true);
    }

    @Override
    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;

            // only if on same position
            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
            {
                if (roi2d instanceof ROI2DArea)
                    add((ROI2DArea) roi2d);
                else if (roi2d instanceof ROI2DShape)
                    updateMask(((ROI2DShape) roi2d).getShape(), false, true, true, true);
                else
                    add(roi2d.getBooleanMask(true));

                return this;
            }
        }

        return super.add(roi, allowCreate);
    }

    @Override
    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;

            // only if on same position
            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
            {
                final Rectangle intersection = getBounds().intersection(roi2d.getBounds());
                final BooleanMask2D mask = new BooleanMask2D(intersection, getBooleanMask(intersection, true));
                final BooleanMask2D roiMask = new BooleanMask2D(intersection, roi2d.getBooleanMask(intersection, true));

                setAsBooleanMask(BooleanMask2D.getIntersection(mask, roiMask));

                return this;
            }
        }

        return super.intersect(roi, allowCreate);
    }

    @Override
    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;

            // only if on same position
            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
            {
                if (roi2d instanceof ROI2DArea)
                    exclusiveAdd((ROI2DArea) roi2d);
                else
                    exclusiveAdd(roi2d.getBooleanMask(true));

                return this;
            }
        }

        return super.exclusiveAdd(roi, allowCreate);
    }

    @Override
    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
    {
        if (roi instanceof ROI2D)
        {
            final ROI2D roi2d = (ROI2D) roi;

            // only if on same position
            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
            {
                if (roi2d instanceof ROI2DArea)
                    subtract((ROI2DArea) roi2d);
                else if (roi2d instanceof ROI2DShape)
                    updateMask(((ROI2DShape) roi2d).getShape(), true, true, true, true);
                else
                    subtract(roi2d.getBooleanMask(true));

                return this;
            }
        }

        return super.subtract(roi, allowCreate);
    }

    /**
     * Return true if bounds need to be updated by calling optimizeBounds() method.
     */
    public boolean getBoundsNeedUpdate()
    {
        return boundsNeedUpdate;
    }

    /**
     * Clear the mask
     */
    public void clear()
    {
        // reset image with new rectangle
        updateImage(new Rectangle());
    }

    @Override
    public boolean isOverEdge(IcyCanvas canvas, double x, double y)
    {
        // use bigger stroke for isOverEdge test for easier intersection
        final double strk = getAdjustedStroke(canvas) * 3;
        final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk);

        // fast intersect test to start with
        if (getBounds2D().intersects(rect))
            // use flatten path, intersects on curved shape return incorrect result
            return ShapeUtil.pathIntersects(bounds.getPathIterator(null, 0.1), rect);

        return false;
    }

    @Override
    public boolean contains(double x, double y)
    {
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // fast discard
        if (!bnds.contains(x, y))
            return false;

        // replace to origin
        final int xi = (int) x - bnds.x;
        final int yi = (int) y - bnds.y;

        return (data[(yi * bnds.width) + xi] != 0);
    }

    @Override
    public boolean contains(double x, double y, double w, double h)
    {
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // fast discard
        if (!bnds.contains(x, y, w, h))
            return false;

        // replace to origin
        final int xi = (int) x - bnds.x;
        final int yi = (int) y - bnds.y;
        final int wi = (int) (x + w) - (int) x;
        final int hi = (int) (y + h) - (int) y;

        // scan all pixels, can take sometime if mask is large
        int offset = (yi * bnds.width) + xi;
        for (int j = 0; j < hi; j++)
        {
            for (int i = 0; i < wi; i++)
                if (data[offset++] == 0)
                    return false;

            offset += bnds.width - wi;
        }

        return true;
    }

    /*
     * already calculated
     */
    @Override
    public Rectangle2D computeBounds2D()
    {
        return bounds;
    }

    /*
     * We can override directly this method as we use our own bounds calculation method here
     */
    @Override
    public Rectangle2D getBounds2D()
    {
        return bounds;
    }

    @Override
    public boolean intersects(double x, double y, double w, double h)
    {
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // fast discard
        if (!bnds.intersects(x, y, w, h))
            return false;

        // replace to origin
        int xi = (int) x - bnds.x;
        int yi = (int) y - bnds.y;
        int wi = (int) (x + w) - (int) x;
        int hi = (int) (y + h) - (int) y;

        // adjust box to mask size
        if (xi < 0)
        {
            wi += xi;
            xi = 0;
        }
        if (yi < 0)
        {
            hi += yi;
            yi = 0;
        }
        if ((xi + wi) > bnds.width)
            wi -= (xi + wi) - bnds.width;
        if ((yi + hi) > bnds.height)
            hi -= (yi + hi) - bnds.height;

        // scan all pixels, can take sometime if mask is large
        int offset = (yi * bnds.width) + xi;
        for (int j = 0; j < hi; j++)
        {
            for (int i = 0; i < wi; i++)
                if (data[offset++] != 0)
                    return true;

            offset += bnds.width - wi;
        }

        return false;
    }

    @Override
    public boolean[] getBooleanMask(int x, int y, int w, int h, boolean inclusive)
    {
        final boolean[] result = new boolean[Math.max(0, w) * Math.max(0, h)];
        final byte[] data;
        final Rectangle bnds;

        synchronized (this)
        {
            data = maskData;
            bnds = bounds;
        }

        // calculate intersection
        final Rectangle intersect = bnds.intersection(new Rectangle(x, y, w, h));

        // no intersection between mask and specified rectangle
        if (intersect.isEmpty())
            return result;

        // this ROI doesn't take care of inclusive parameter as intersect = contains
        int offSrc = 0;
        int offDst = 0;

        // adjust offset in source mask
        if (intersect.x > bnds.x)
            offSrc += (intersect.x - bnds.x);
        if (intersect.y > bnds.y)
            offSrc += (intersect.y - bnds.y) * bnds.width;
        // adjust offset in destination mask
        if (bnds.x > x)
            offDst += (bnds.x - x);
        if (bnds.y > y)
            offDst += (bnds.y - y) * w;

        for (int j = 0; j < intersect.height; j++)
        {
            for (int i = 0; i < intersect.width; i++)
                result[offDst++] = (data[offSrc++] != 0);

            offSrc += bnds.width - intersect.width;
            offDst += w - intersect.width;
        }

        return result;
    }

    @Override
    public double computeNumberOfPoints()
    {
        // just count the number of point contained in the mask
        double result = 0d;
        final byte[] data = maskData;

        for (int i = 0; i < data.length; i++)
            if (data[i] != 0)
                result += 1d;

        return result;
    }

    @Override
    public boolean canTranslate()
    {
        return true;
    }

    @Override
    public void translate(double dx, double dy)
    {
        translateX += dx;
        translateY += dy;

        // convert to integer
        final int dxi = (int) translateX;
        final int dyi = (int) translateY;
        // keep trace of not used floating part
        translateX -= dxi;
        translateY -= dyi;

        if ((dxi != 0) || (dyi != 0))
        {
            bounds.translate(dxi, dyi);
            roiChanged(false);
        }
    }

    @Override
    public boolean canSetPosition()
    {
        return true;
    }

    @Override
    public void setPosition2D(Point2D newPosition)
    {
        bounds = new Rectangle((int) newPosition.getX(), (int) newPosition.getY(), bounds.width, bounds.height);

        roiChanged(false);
    }

    /**
     * Set the mask from a BooleanMask2D object.<br>
     * If specified mask is <i>null</i> then ROI is cleared.
     */
    public void setAsBooleanMask(BooleanMask2D mask)
    {
        // mask empty ? --> just clear the ROI
        if ((mask == null) || mask.isEmpty())
            clear();
        // don't need bounds optimization as BooleanMask2D should be already optimized
        else
            setAsBooleanMask(mask.bounds, mask.mask, false);
    }

    /**
     * Set the mask from a boolean array.<br>
     * r represents the region defined by the boolean array.
     * 
     * @param r
     * @param booleanMask
     */
    protected void setAsByteMask(Rectangle r, byte[] mask, boolean doBoundsOptimization)
    {
        // reset image with new rectangle
        updateImage(r);

        System.arraycopy(mask, 0, maskData, 0, r.width * r.height);

        if (doBoundsOptimization)
        {
            // optimize bounds
            if (isUpdating())
                boundsNeedUpdate = true;
            else
                optimizeBounds();
        }

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Set the mask from a boolean array.<br>
     * r represents the region defined by the boolean array.
     * 
     * @param r
     * @param booleanMask
     */
    protected void setAsBooleanMask(Rectangle r, boolean[] booleanMask, boolean doBoundsOptimization)
    {
        // reset image with new rectangle
        updateImage(r);

        final byte[] data = maskData;

        for (int i = 0; i < data.length; i++)
            data[i] = (byte) (booleanMask[i] ? 1 : 0);

        if (doBoundsOptimization)
        {
            // optimize bounds
            if (isUpdating())
                boundsNeedUpdate = true;
            else
                optimizeBounds();
        }

        // notify roi changed
        roiChanged(true);
    }

    /**
     * Set the mask from a boolean array.<br>
     * r represents the region defined by the boolean array.
     * 
     * @param r
     * @param booleanMask
     */
    public void setAsBooleanMask(Rectangle r, boolean[] booleanMask)
    {
        setAsBooleanMask(r, booleanMask, true);
    }

    public void setAsBooleanMask(int x, int y, int w, int h, boolean[] booleanMask)
    {
        setAsBooleanMask(new Rectangle(x, y, w, h), booleanMask);
    }

    @Override
    public void onChanged(CollapsibleEvent object)
    {
        final ROIEvent event = (ROIEvent) object;

        // do here global process on ROI change
        switch (event.getType())
        {
            case ROI_CHANGED:
                // update bounds if needed
                if (boundsNeedUpdate && !roiModifiedByMouse)
                {
                    if (optimizeBounds())
                        // need to send a new change event !
                        roiChanged(true);
                }
                // we need to rebuild shape
                if (StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL))
                    getOverlay().needRebuild = true;
                else
                {
                    final ROI2DAreaPainter overlay = getOverlay();

                    // z position change ? --> need total rebuild
                    if (overlay.lastBuildPosZ != getZ())
                        overlay.needRebuild = true;
                    // just need to change position
                    else
                        overlay.updateVtkObjectsBounds();
                }
                break;

            case FOCUS_CHANGED:
            case SELECTION_CHANGED:
                getOverlay().updateVtkDisplayProperties();
                break;

            case PROPERTY_CHANGED:
                final String property = event.getPropertyName();

                if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR)
                        || StringUtil.equals(property, PROPERTY_OPACITY))
                    getOverlay().updateVtkDisplayProperties();
                break;

            default:
                break;
        }

        super.onChanged(object);
    }

    @Override
    public boolean loadFromXML(Node node)
    {
        beginUpdate();
        try
        {
            if (!super.loadFromXML(node))
                return false;

            final Rectangle rect = new Rectangle();

            // retrieve mask bounds
            rect.x = XMLUtil.getElementIntValue(node, ID_BOUNDS_X, 0);
            rect.y = XMLUtil.getElementIntValue(node, ID_BOUNDS_Y, 0);
            rect.width = XMLUtil.getElementIntValue(node, ID_BOUNDS_W, 0);
            rect.height = XMLUtil.getElementIntValue(node, ID_BOUNDS_H, 0);

            // retrieve mask data
            final byte[] data = XMLUtil.getElementBytesValue(node, ID_BOOLMASK_DATA, new byte[0]);

            // an error occurred while retrieved XML data
            if (data == null)
                return false;

            // set the ROI from the unpacked boolean mask
            setAsByteMask(rect, data, false);
        }
        finally
        {
            endUpdate();
        }

        return true;
    }

    @Override
    public boolean saveToXML(Node node)
    {
        if (!super.saveToXML(node))
            return false;

        final byte[] data;
        final Rectangle bnds;

        synchronized (maskData)
        {
            // need to duplicate to avoid array change during XML saving (ZIP packing don't like that)
            data = maskData.clone();
            bnds = bounds;
        }

        final int len = bnds.width * bnds.height;

        // invalid --> return false
        if ((len > 0) && (len != data.length))
            return false;

        // retrieve mask bounds
        XMLUtil.setElementIntValue(node, ID_BOUNDS_X, bnds.x);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_Y, bnds.y);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_W, bnds.width);
        XMLUtil.setElementIntValue(node, ID_BOUNDS_H, bnds.height);

        // set mask data as byte array
        if (len > 0)
            XMLUtil.setElementBytesValue(node, ID_BOOLMASK_DATA, data);

        return true;
    }
}