001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package plugins.kernel.roi.roi2d;
020
021import java.awt.AlphaComposite;
022import java.awt.BasicStroke;
023import java.awt.Color;
024import java.awt.Graphics2D;
025import java.awt.Point;
026import java.awt.Rectangle;
027import java.awt.RenderingHints;
028import java.awt.Shape;
029import java.awt.event.InputEvent;
030import java.awt.event.KeyEvent;
031import java.awt.event.MouseEvent;
032import java.awt.geom.Ellipse2D;
033import java.awt.geom.Point2D;
034import java.awt.geom.Rectangle2D;
035import java.awt.image.BufferedImage;
036import java.awt.image.DataBufferByte;
037import java.awt.image.IndexColorModel;
038import java.lang.ref.WeakReference;
039import java.util.Arrays;
040
041import org.w3c.dom.Node;
042
043import icy.canvas.IcyCanvas;
044import icy.canvas.IcyCanvas2D;
045import icy.common.CollapsibleEvent;
046import icy.gui.inspector.RoisPanel;
047import icy.image.ImageUtil;
048import icy.main.Icy;
049import icy.painter.VtkPainter;
050import icy.resource.ResourceUtil;
051import icy.roi.BooleanMask2D;
052import icy.roi.ROI;
053import icy.roi.ROI2D;
054import icy.roi.ROIEvent;
055import icy.roi.edit.Area2DChangeROIEdit;
056import icy.sequence.Sequence;
057import icy.system.thread.ThreadUtil;
058import icy.type.point.Point5D;
059import icy.type.point.Point5D.Double;
060import icy.type.rectangle.Rectangle3D;
061import icy.util.EventUtil;
062import icy.util.GraphicsUtil;
063import icy.util.ShapeUtil;
064import icy.util.StringUtil;
065import icy.util.XMLUtil;
066import icy.vtk.IcyVtkPanel;
067import icy.vtk.VtkUtil;
068import plugins.kernel.canvas.VtkCanvas;
069import vtk.vtkActor;
070import vtk.vtkImageData;
071import vtk.vtkInformation;
072import vtk.vtkPolyData;
073import vtk.vtkPolyDataMapper;
074import vtk.vtkProp;
075
076/**
077 * ROI Area type.<br>
078 * Use a bitmap mask internally for fast boolean mask operation.<br>
079 * 
080 * @author Stephane
081 */
082public class ROI2DArea extends ROI2D
083{
084    protected static final float DEFAULT_CURSOR_SIZE = 15f;
085
086    // we want to keep a static brush
087    protected static final Ellipse2D brush = new Ellipse2D.Double();
088    // protected static final Point2D.Double cursorPosition = new Point2D.Double();
089    protected static Color brushColor = Color.red;
090    protected static float brushSize = DEFAULT_CURSOR_SIZE;
091
092    public class ROI2DAreaPainter extends ROI2DPainter implements Runnable
093    {
094        /**
095         * @deprecated Use {@link #getOpacity()} instead.
096         */
097        @Deprecated
098        public static final float CONTENT_ALPHA = 0.3f;
099
100        private static final float MIN_CURSOR_SIZE = 0.3f;
101        private static final float MAX_CURSOR_SIZE = 500f;
102
103        // VTK 3D objects
104        protected vtkPolyData outline;
105        protected vtkPolyDataMapper outlineMapper;
106        protected vtkActor outlineActor;
107        protected vtkInformation vtkInfo;
108        protected vtkPolyData polyData;
109        protected vtkPolyDataMapper polyMapper;
110        protected vtkActor surfaceActor;
111        // 3D internal
112        protected boolean needRebuild;
113        protected double scaling[];
114        protected WeakReference<VtkCanvas> canvas3d;
115        protected int lastBuildPosZ;
116
117        // internal
118        protected final Point2D brushPosition;
119
120        public ROI2DAreaPainter()
121        {
122            super();
123
124            brushPosition = new Point2D.Double();
125
126            outline = null;
127            outlineMapper = null;
128            outlineActor = null;
129            vtkInfo = null;
130            polyData = null;
131            polyMapper = null;
132            surfaceActor = null;
133
134            scaling = new double[3];
135            Arrays.fill(scaling, 1d);
136
137            needRebuild = true;
138            canvas3d = new WeakReference<VtkCanvas>(null);
139            lastBuildPosZ = getZ();
140        }
141
142        @Override
143        protected void finalize() throws Throwable
144        {
145            super.finalize();
146
147            // release allocated VTK resources
148            if (surfaceActor != null)
149                surfaceActor.Delete();
150            if (polyMapper != null)
151                polyMapper.Delete();
152            if (polyData != null)
153            {
154                polyData.GetPointData().GetScalars().Delete();
155                polyData.GetPointData().Delete();
156                polyData.Delete();
157            }
158            if (outlineActor != null)
159            {
160                outlineActor.SetPropertyKeys(null);
161                outlineActor.Delete();
162            }
163            if (vtkInfo != null)
164            {
165                vtkInfo.Remove(VtkCanvas.visibilityKey);
166                vtkInfo.Delete();
167            }
168            if (outlineMapper != null)
169                outlineMapper.Delete();
170            if (outline != null)
171            {
172                outline.GetPointData().GetScalars().Delete();
173                outline.GetPointData().Delete();
174                outline.Delete();
175            }
176        };
177
178        protected void initVtkObjects()
179        {
180            outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d);
181            outlineMapper = new vtkPolyDataMapper();
182            outlineMapper.SetInputData(outline);
183            outlineActor = new vtkActor();
184            outlineActor.SetMapper(outlineMapper);
185            // disable picking on the outline
186            outlineActor.SetPickable(0);
187            // and set it to wireframe representation
188            outlineActor.GetProperty().SetRepresentationToWireframe();
189            // use vtkInformations to store outline visibility state (hacky)
190            vtkInfo = new vtkInformation();
191            vtkInfo.Set(VtkCanvas.visibilityKey, 0);
192            // VtkCanvas use this to restore correctly outline visibility flag
193            outlineActor.SetPropertyKeys(vtkInfo);
194
195            polyMapper = new vtkPolyDataMapper();
196            surfaceActor = new vtkActor();
197            surfaceActor.SetMapper(polyMapper);
198
199            final Color col = getColor();
200            final double r = col.getRed() / 255d;
201            final double g = col.getGreen() / 255d;
202            final double b = col.getBlue() / 255d;
203
204            // set actors color
205            outlineActor.GetProperty().SetColor(r, g, b);
206            surfaceActor.GetProperty().SetColor(r, g, b);
207        }
208
209        /**
210         * rebuild VTK objects (called only when VTK canvas is selected).
211         */
212        protected void rebuildVtkObjects()
213        {
214            final VtkCanvas canvas = canvas3d.get();
215            // canvas was closed
216            if (canvas == null)
217                return;
218
219            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
220            // canvas was closed
221            if (vtkPanel == null)
222                return;
223
224            final Sequence seq = canvas.getSequence();
225            // nothing to update
226            if (seq == null)
227                return;
228
229            // get previous polydata object
230            final vtkPolyData previousPolyData = polyData;
231            // get VTK binary image from ROI mask
232            final vtkImageData imageData = VtkUtil.getBinaryImageData(ROI2DArea.this, seq.getSizeZ(),
233                    canvas.getPositionT());
234            // adjust spacing
235            imageData.SetSpacing(scaling[0], scaling[1], scaling[2]);
236            // get VTK polygon data representing the surface of the binary image
237            polyData = VtkUtil.getSurfaceFromImage(imageData, 0.5d);
238
239            // get bounds
240            final Rectangle3D bounds = getBounds5D().toRectangle3D();
241            // apply scaling on bounds
242            bounds.setX(bounds.getX() * scaling[0]);
243            bounds.setSizeX(bounds.getSizeX() * scaling[0]);
244            bounds.setY(bounds.getY() * scaling[1]);
245            bounds.setSizeY(bounds.getSizeY() * scaling[1]);
246            if (bounds.isInfiniteZ())
247            {
248                bounds.setZ(0);
249                bounds.setSizeZ(seq.getSizeZ() * scaling[2]);
250                lastBuildPosZ = -1;
251            }
252            else
253            {
254                lastBuildPosZ = getZ();
255                bounds.setZ(bounds.getZ() * scaling[2]);
256                bounds.setSizeZ(1d * scaling[2]);
257            }
258
259            // actor can be accessed in canvas3d for rendering so we need to synchronize access
260            vtkPanel.lock();
261            try
262            {
263                // update outline data
264                VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
265                        bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas);
266                outlineMapper.Update();
267                // update polygon data from image
268                polyMapper.SetInputData(polyData);
269                polyMapper.Update();
270
271                // update actor position
272                surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ());
273
274                // release image data
275                imageData.GetPointData().GetScalars().Delete();
276                imageData.GetPointData().Delete();
277                imageData.Delete();
278
279                // release previous polydata
280                if (previousPolyData != null)
281                {
282                    previousPolyData.GetPointData().GetScalars().Delete();
283                    previousPolyData.GetPointData().Delete();
284                    previousPolyData.Delete();
285                }
286            }
287            finally
288            {
289                vtkPanel.unlock();
290            }
291
292            // update color and others properties
293            updateVtkDisplayProperties();
294        }
295
296        protected void updateVtkDisplayProperties()
297        {
298            if (surfaceActor == null)
299                return;
300
301            final VtkCanvas cnv = canvas3d.get();
302            final Color col = getDisplayColor();
303            final double r = col.getRed() / 255d;
304            final double g = col.getGreen() / 255d;
305            final double b = col.getBlue() / 255d;
306            // final double strk = getStroke();
307            // final float opacity = getOpacity();
308
309            final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null;
310
311            // we need to lock canvas as actor can be accessed during rendering
312            if (vtkPanel != null)
313                vtkPanel.lock();
314            try
315            {
316                // set actors color
317                outlineActor.GetProperty().SetColor(r, g, b);
318                if (isSelected())
319                {
320                    outlineActor.GetProperty().SetRepresentationToWireframe();
321                    outlineActor.SetVisibility(1);
322                    vtkInfo.Set(VtkCanvas.visibilityKey, 1);
323                }
324                else
325                {
326                    outlineActor.GetProperty().SetRepresentationToPoints();
327                    outlineActor.SetVisibility(0);
328                    vtkInfo.Set(VtkCanvas.visibilityKey, 0);
329                }
330                surfaceActor.GetProperty().SetColor(r, g, b);
331                // opacity here is about ROI content, global opacity is handled by Layer
332                // surfaceActor.GetProperty().SetOpacity(opacity);
333                setVtkObjectsColor(col);
334            }
335            finally
336            {
337                if (vtkPanel != null)
338                    vtkPanel.unlock();
339            }
340
341            // need to repaint
342            painterChanged();
343        }
344
345        protected void setVtkObjectsColor(Color color)
346        {
347            if (outline != null)
348                VtkUtil.setPolyDataColor(outline, color, canvas3d.get());
349            if (polyData != null)
350                VtkUtil.setPolyDataColor(polyData, color, canvas3d.get());
351        }
352
353        protected void updateVtkObjectsBounds()
354        {
355            final VtkCanvas canvas = canvas3d.get();
356            // canvas was closed
357            if (canvas == null)
358                return;
359
360            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
361            // canvas was closed
362            if (vtkPanel == null)
363                return;
364
365            final Sequence seq = canvas.getSequence();
366            // nothing to update
367            if (seq == null)
368                return;
369
370            final Rectangle3D bounds = getBounds5D().toRectangle3D();
371            // apply scaling on bounds
372            bounds.setX(bounds.getX() * scaling[0]);
373            bounds.setSizeX(bounds.getSizeX() * scaling[0]);
374            bounds.setY(bounds.getY() * scaling[1]);
375            bounds.setSizeY(bounds.getSizeY() * scaling[1]);
376            if (bounds.isInfiniteZ())
377            {
378                bounds.setZ(0);
379                bounds.setSizeZ(seq.getSizeZ() * scaling[2]);
380            }
381            else
382            {
383                bounds.setZ(bounds.getZ() * scaling[2]);
384                bounds.setSizeZ(1d * scaling[2]);
385            }
386
387            // actor can be accessed in canvas3d for rendering so we need to synchronize access
388            vtkPanel.lock();
389            try
390            {
391                // update outline position
392                VtkUtil.setOutlineBounds(outline, bounds.getMinX(), bounds.getMaxX(), bounds.getMinY(),
393                        bounds.getMaxY(), bounds.getMinZ(), bounds.getMaxZ(), canvas);
394                outlineMapper.Update();
395
396                // update actor position
397                surfaceActor.SetPosition(bounds.getX(), bounds.getY(), bounds.getZ());
398            }
399            finally
400            {
401                vtkPanel.unlock();
402            }
403        }
404
405        void updateCursor()
406        {
407            final double x = brushPosition.getX();
408            final double y = brushPosition.getY();
409
410            brush.setFrameFromDiagonal(x - brushSize, y - brushSize, x + brushSize, y + brushSize);
411
412            // if roi selected (cursor displayed) --> painter changed
413            if (isSelected())
414                painterChanged();
415        }
416
417        /**
418         * Returns the brush position.
419         */
420        public Point2D getBrushPosition()
421        {
422            return (Point) brushPosition.clone();
423        }
424
425        /**
426         * Set the brush position.
427         */
428        public void setBrushPosition(Point2D position)
429        {
430            if (!brushPosition.equals(position))
431            {
432                brushPosition.setLocation(position);
433                updateCursor();
434            }
435        }
436
437        /**
438         * @deprecated Use {@link #getBrushPosition()} instead.
439         */
440        @Deprecated
441        public Point2D getCursorPosition()
442        {
443            return getBrushPosition();
444        }
445
446        /**
447         * @deprecated Use {@link #setBrushPosition(Point2D)} instead.
448         */
449        @Deprecated
450        public void setCursorPosition(Point2D position)
451        {
452            setBrushPosition(position);
453        }
454
455        /**
456         * Returns the brush size.
457         */
458        public float getBrushSize()
459        {
460            return brushSize;
461        }
462
463        /**
464         * Sets the brush size.
465         */
466        public void setBrushSize(float value)
467        {
468            final float adjValue = Math.max(Math.min(value, MAX_CURSOR_SIZE), MIN_CURSOR_SIZE);
469
470            if (brushSize != adjValue)
471            {
472                brushSize = adjValue;
473                updateCursor();
474            }
475        }
476
477        /**
478         * @deprecated Use {@link #getBrushSize()} instead
479         */
480        @Deprecated
481        public float getCursorSize()
482        {
483            return getBrushSize();
484        }
485
486        /**
487         * @deprecated Use {@link #setBrushSize(float)} instead
488         */
489        @Deprecated
490        public void setCursorSize(float value)
491        {
492            setBrushSize(value);
493        }
494
495        /**
496         * Returns the brush color
497         */
498        public Color getBrushColor()
499        {
500            return brushColor;
501        }
502
503        /**
504         * Sets the brush color
505         */
506        public void setBrushColor(Color value)
507        {
508            if (!brushColor.equals(value))
509            {
510                brushColor = value;
511                painterChanged();
512            }
513        }
514
515        /**
516         * @deprecated Use {@link #getBrushColor()} instead
517         */
518        @Deprecated
519        public Color getCursorColor()
520        {
521            return getBrushColor();
522        }
523
524        /**
525         * @deprecated Use {@link #setBrushColor(Color)} instead
526         */
527        @Deprecated
528        public void setCursorColor(Color value)
529        {
530            setBrushColor(value);
531        }
532
533        public void addToMask(Point2D pos)
534        {
535            setBrushPosition(pos);
536            updateMask(brush, false);
537        }
538
539        public void removeFromMask(Point2D pos)
540        {
541            setBrushPosition(pos);
542            updateMask(brush, true);
543        }
544
545        @Override
546        public void painterChanged()
547        {
548            updateMaskColor(true);
549
550            super.painterChanged();
551        }
552
553        @Override
554        protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas)
555        {
556            // specific VTK canvas processing
557            if (canvas instanceof VtkCanvas)
558            {
559                // mouse is over the ROI actor ? --> focus the ROI
560                final boolean focused = (surfaceActor != null)
561                        && (surfaceActor == ((VtkCanvas) canvas).getPickedObject());
562
563                setFocused(focused);
564
565                return focused;
566            }
567
568            return super.updateFocus(e, imagePoint, canvas);
569        }
570
571        @Override
572        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
573        {
574            // send event to parent first
575            super.keyPressed(e, imagePoint, canvas);
576
577            // edition not supported on VtkCanvas
578            if (canvas instanceof VtkCanvas)
579                return;
580
581            // not yet consumed and ROI editable...
582            if (!e.isConsumed() && !isReadOnly())
583            {
584                // then process it here
585                if (isActiveFor(canvas))
586                {
587                    ROI2DArea.this.beginUpdate();
588                    try
589                    {
590                        switch (e.getKeyChar())
591                        {
592                            case '+':
593                                if (isSelected())
594                                {
595                                    setBrushSize(getBrushSize() * 1.1f);
596                                    e.consume();
597                                }
598                                break;
599
600                            case '-':
601                                if (isSelected())
602                                {
603                                    setBrushSize(getBrushSize() * 0.9f);
604                                    e.consume();
605                                }
606                                break;
607                        }
608                    }
609                    finally
610                    {
611                        ROI2DArea.this.endUpdate();
612                    }
613                }
614            }
615        }
616
617        @Override
618        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
619        {
620            // send event to parent first
621            super.mousePressed(e, imagePoint, canvas);
622
623            // edition not supported on VtkCanvas
624            if (canvas instanceof VtkCanvas)
625                return;
626            // we need it
627            if (imagePoint == null)
628                return;
629
630            // not yet consumed, ROI editable, selected and not focused...
631            if (!e.isConsumed() && !isReadOnly() && isSelected() && !isFocused())
632            {
633                // then process it here
634                if (isActiveFor(canvas))
635                {
636                    // keep trace of roi changes from user mouse action
637                    roiModifiedByMouse = false;
638                    // save current ROI
639                    undoSave = getBooleanMask(true);
640
641                    ROI2DArea.this.beginUpdate();
642                    try
643                    {
644                        // left button action
645                        if (EventUtil.isLeftMouseButton(e))
646                        {
647                            // add point first
648                            addToMask(imagePoint.toPoint2D());
649                            roiModifiedByMouse = true;
650                            e.consume();
651                        }
652                        // right button action
653                        else if (EventUtil.isRightMouseButton(e))
654                        {
655                            // remove point
656                            removeFromMask(imagePoint.toPoint2D());
657                            roiModifiedByMouse = true;
658                            e.consume();
659                        }
660                    }
661                    finally
662                    {
663                        ROI2DArea.this.endUpdate();
664                    }
665                }
666            }
667        }
668
669        @Override
670        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
671        {
672            // send event to parent first
673            super.mouseReleased(e, imagePoint, canvas);
674
675            // update only on release as it can be long
676            if (!isReadOnly())
677            {
678                if (roiModifiedByMouse)
679                {
680                    if (boundsNeedUpdate)
681                    {
682                        if (optimizeBounds())
683                        {
684                            roiChanged(true);
685
686                            // empty ? delete ROI
687                            if (bounds.isEmpty())
688                            {
689                                ROI2DArea.this.remove();
690                                // nothing more to do
691                                return;
692                            }
693                        }
694                    }
695
696                    final Sequence sequence = canvas.getSequence();
697
698                    // add undo operation
699                    try
700                    {
701                        if ((sequence != null) && (undoSave != null))
702                            sequence.addUndoableEdit(new Area2DChangeROIEdit(ROI2DArea.this, undoSave));
703                    }
704                    catch (OutOfMemoryError err)
705                    {
706                        // can't create undo operation, show message and clear undo manager
707                        System.out.println("Warning: not enough memory to create undo point for ROI area change");
708                        sequence.clearUndoManager();
709                    }
710
711                    // release save
712                    undoSave = null;
713                    roiModifiedByMouse = false;
714                }
715            }
716        }
717
718        @Override
719        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
720        {
721            // provide backward compatibility
722            if (imagePoint != null)
723                mouseClick(e, imagePoint.toPoint2D(), canvas);
724            else
725                mouseClick(e, (Point2D) null, canvas);
726
727            // not yet consumed...
728            if (!e.isConsumed())
729            {
730                // and process ROI stuff now
731                if (isActiveFor(canvas))
732                {
733                    final int clickCount = e.getClickCount();
734
735                    // double click
736                    if (clickCount == 2)
737                    {
738                        // focused ?
739                        if (isFocused())
740                        {
741                            // show in ROI panel
742                            final RoisPanel roiPanel = Icy.getMainInterface().getRoisPanel();
743
744                            if (roiPanel != null)
745                            {
746                                roiPanel.scrollTo(ROI2DArea.this);
747                                // consume event
748                                e.consume();
749                            }
750                        }
751                    }
752                }
753            }
754
755        }
756
757        @Override
758        public void mouseMove(MouseEvent e, Double imagePoint, IcyCanvas canvas)
759        {
760            // send event to parent first
761            super.mouseMove(e, imagePoint, canvas);
762
763            // edition not supported on VtkCanvas
764            if (canvas instanceof VtkCanvas)
765                return;
766            // we need it
767            if (imagePoint == null)
768                return;
769
770            // not yet consumed, ROI editable and selected...
771            if (!e.isConsumed() && !isReadOnly() && isSelected())
772            {
773                // then process it here
774                if (isActiveFor(canvas))
775                {
776                    setBrushPosition(imagePoint.toPoint2D());
777                }
778            }
779        }
780
781        @Override
782        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
783        {
784            // send event to parent first
785            super.mouseDrag(e, imagePoint, canvas);
786
787            // edition not supported on VtkCanvas
788            if (canvas instanceof VtkCanvas)
789                return;
790            // we need it
791            if (imagePoint == null)
792                return;
793
794            // not yet consumed, ROI editable and selected...
795            if (!e.isConsumed() && !isReadOnly() && isSelected())
796            {
797                // then process it here
798                if (isActiveFor(canvas))
799                {
800                    ROI2DArea.this.beginUpdate();
801                    try
802                    {
803                        // left button action
804                        if (EventUtil.isLeftMouseButton(e))
805                        {
806                            // add point first
807                            addToMask(imagePoint.toPoint2D());
808                            roiModifiedByMouse = true;
809                            e.consume();
810                        }
811                        // right button action
812                        else if (EventUtil.isRightMouseButton(e))
813                        {
814                            // remove point
815                            removeFromMask(imagePoint.toPoint2D());
816                            roiModifiedByMouse = true;
817                            e.consume();
818                        }
819                    }
820                    finally
821                    {
822                        ROI2DArea.this.endUpdate();
823                    }
824                }
825            }
826        }
827
828        @Override
829        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
830        {
831            super.paint(g, sequence, canvas);
832
833            if (isActiveFor(canvas))
834            {
835                // ROI selected ? draw cursor
836                if (isSelected() && !isFocused() && !isReadOnly())
837                    drawCursor(g, sequence, canvas);
838            }
839        }
840
841        /**
842         * Draw the ROI itself
843         */
844        @Override
845        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
846        {
847            if (canvas instanceof IcyCanvas2D)
848            {
849                // not supported
850                if (g == null)
851                    return;
852
853                final Rectangle bounds = getBounds();
854                // trivial paint optimization
855                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);
856
857                if (shapeVisible)
858                {
859                    final Graphics2D g2 = (Graphics2D) g.create();
860                    final boolean small;
861
862                    // disable LOD when creating the ROI
863                    if (isCreating())
864                        small = false;
865                    else
866                    {
867                        final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
868                        small = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_SMALL;
869                    }
870
871                    // simplified draw
872                    if (small)
873                    {
874                        g2.setColor(getDisplayColor());
875                        g2.drawImage(imageMask, null, bounds.x, bounds.y);
876                    }
877                    // normal draw
878                    else
879                    {
880                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
881                        float newAlpha = prevAlpha.getAlpha() * getOpacity();
882                        newAlpha = Math.min(1f, newAlpha);
883                        newAlpha = Math.max(0f, newAlpha);
884
885                        // show content with an alpha factor
886                        g2.setComposite(prevAlpha.derive(newAlpha));
887
888                        // draw mask
889                        g2.drawImage(imageMask, null, bounds.x, bounds.y);
890
891                        // restore alpha
892                        g2.setComposite(prevAlpha);
893
894                        // draw border
895                        if (isSelected())
896                        {
897                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
898                            g2.setColor(getDisplayColor());
899                            g2.draw(bounds);
900                        }
901                        else
902                        {
903                            // outside border
904                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d)));
905                            g2.setColor(Color.black);
906                            g2.draw(bounds);
907                            // internal border
908                            g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
909                            g2.setColor(getDisplayColor());
910                            g2.draw(bounds);
911                        }
912                    }
913
914                    g2.dispose();
915                }
916
917                // for (Point2D pt : getBooleanMask().getEdgePoints())
918                // g2.drawRect((int) pt.getX(), (int) pt.getY(), 1, 1);
919            }
920
921            if (canvas instanceof VtkCanvas)
922            {
923                // 3D canvas
924                final VtkCanvas cnv = (VtkCanvas) canvas;
925                // update reference if needed
926                if (canvas3d.get() != cnv)
927                    canvas3d = new WeakReference<VtkCanvas>(cnv);
928
929                // initialize VTK objects if not yet done
930                if (surfaceActor == null)
931                    initVtkObjects();
932
933                // FIXME : need a better implementation
934                final double[] s = cnv.getVolumeScale();
935
936                // scaling changed ?
937                if (!Arrays.equals(scaling, s))
938                {
939                    // update scaling
940                    scaling = s;
941                    // need rebuild
942                    needRebuild = true;
943                }
944
945                // need to rebuild 3D data structures ?
946                if (needRebuild)
947                {
948                    // request rebuild 3D objects
949                    ThreadUtil.runSingle(this);
950                    needRebuild = false;
951                }
952            }
953        }
954
955        /**
956         * draw the ROI cursor
957         */
958        protected void drawCursor(Graphics2D g, Sequence sequence, IcyCanvas canvas)
959        {
960            if (canvas instanceof IcyCanvas2D)
961            {
962                // not supported
963                if (g == null)
964                    return;
965
966                final Rectangle bounds = brush.getBounds();
967                // trivial paint optimization
968                final boolean shapeVisible = GraphicsUtil.isVisible(g, bounds);
969
970                if (shapeVisible)
971                {
972                    final Graphics2D g2 = (Graphics2D) g.create();
973                    final boolean tiny;
974
975                    // disable LOD when creating the ROI
976                    if (isCreating())
977                        tiny = false;
978                    else
979                    {
980                        final double scale = Math.max(canvas.getScaleX(), canvas.getScaleY());
981                        tiny = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()) < LOD_TINY;
982                    }
983
984                    // simplified draw
985                    if (tiny)
986                    {
987                        // cursor color
988                        g2.setColor(brushColor);
989                        // draw cursor
990                        g2.fill(brush);
991                    }
992                    // normal draw
993                    else
994                    {
995                        final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite();
996                        float newAlpha = prevAlpha.getAlpha() * getOpacity() * 2f;
997                        newAlpha = Math.min(1f, newAlpha);
998                        newAlpha = Math.max(0f, newAlpha);
999
1000                        // show cursor with an alpha factor
1001                        g2.setComposite(prevAlpha.derive(newAlpha));
1002
1003                        // draw cursor border
1004                        g2.setColor(Color.black);
1005                        g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke)));
1006                        g2.draw(brush);
1007                        // draw cursor
1008                        g2.setColor(brushColor);
1009                        g2.fill(brush);
1010                    }
1011
1012                    g2.dispose();
1013                }
1014            }
1015        }
1016
1017        @Override
1018        public vtkProp[] getProps()
1019        {
1020            // initialize VTK objects if not yet done
1021            if (surfaceActor == null)
1022                initVtkObjects();
1023
1024            return new vtkActor[] {surfaceActor, outlineActor};
1025        }
1026
1027        @Override
1028        public void run()
1029        {
1030            rebuildVtkObjects();
1031        }
1032    }
1033
1034    public static final String ID_BOUNDS_X = "boundsX";
1035    public static final String ID_BOUNDS_Y = "boundsY";
1036    public static final String ID_BOUNDS_W = "boundsW";
1037    public static final String ID_BOUNDS_H = "boundsH";
1038    // protected static final String ID_BOOLMASK_LEN = "boolMaskLen";
1039    public static final String ID_BOOLMASK_DATA = "boolMaskData";
1040
1041    /**
1042     * image containing the mask
1043     */
1044    protected BufferedImage imageMask;
1045    /**
1046     * rectangle bounds
1047     */
1048    protected Rectangle bounds;
1049
1050    /**
1051     * internals
1052     */
1053    protected final byte[] red;
1054    protected final byte[] green;
1055    protected final byte[] blue;
1056    protected IndexColorModel colorModel;
1057    protected byte[] maskData; // 0 = false, 1 = true
1058    protected double translateX, translateY;
1059    protected Color previousColor;
1060    protected boolean boundsNeedUpdate;
1061    protected boolean roiModifiedByMouse;
1062    protected BooleanMask2D undoSave;
1063
1064    /**
1065     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
1066     */
1067    public ROI2DArea()
1068    {
1069        super();
1070
1071        bounds = new Rectangle();
1072        boundsNeedUpdate = false;
1073        roiModifiedByMouse = false;
1074        undoSave = null;
1075        translateX = 0d;
1076        translateY = 0d;
1077
1078        // prepare indexed image
1079        red = new byte[256];
1080        green = new byte[256];
1081        blue = new byte[256];
1082
1083        // keep trace of previous color
1084        previousColor = getDisplayColor();
1085
1086        // set colormap
1087        red[1] = (byte) previousColor.getRed();
1088        green[1] = (byte) previousColor.getGreen();
1089        blue[1] = (byte) previousColor.getBlue();
1090
1091        // classic 8 bits indexed with one transparent color (index = 0)
1092        colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
1093        // create default image
1094        imageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
1095        // get data pointer
1096        maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();
1097
1098        // set icon (default name is defined by getDefaultName())
1099        setIcon(ResourceUtil.ICON_ROI_AREA);
1100    }
1101
1102    /**
1103     * @deprecated Use {@link #ROI2DArea(Point5D)} instead
1104     */
1105    @Deprecated
1106    public ROI2DArea(Point2D position, boolean cm)
1107    {
1108        this(position);
1109    }
1110
1111    /**
1112     * Create a ROI2D Area type with a single point.
1113     */
1114    public ROI2DArea(Point2D position)
1115    {
1116        this();
1117
1118        // add current point to mask
1119        addBrush(position);
1120    }
1121
1122    /**
1123     * Generic constructor for interactive mode.
1124     */
1125    public ROI2DArea(Point5D position)
1126    {
1127        this(position.toPoint2D());
1128    }
1129
1130    /**
1131     * Create a ROI2D Area type from the specified {@link BooleanMask2D}.
1132     */
1133    public ROI2DArea(BooleanMask2D mask)
1134    {
1135        this();
1136
1137        setAsBooleanMask(mask);
1138    }
1139
1140    /**
1141     * Create a copy of the specified 2D Area ROI
1142     */
1143    public ROI2DArea(ROI2DArea area)
1144    {
1145        super();
1146
1147        bounds = new Rectangle();
1148        boundsNeedUpdate = false;
1149        roiModifiedByMouse = false;
1150        undoSave = null;
1151        translateX = 0d;
1152        translateY = 0d;
1153
1154        // prepare indexed image
1155        red = new byte[256];
1156        green = new byte[256];
1157        blue = new byte[256];
1158
1159        // keep trace of previous color
1160        previousColor = getDisplayColor();
1161
1162        // set colormap
1163        red[1] = (byte) previousColor.getRed();
1164        green[1] = (byte) previousColor.getGreen();
1165        blue[1] = (byte) previousColor.getBlue();
1166
1167        // classic 8 bits indexed with one transparent color (index = 0)
1168        colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
1169        imageMask = new BufferedImage(area.bounds.width, area.bounds.height, BufferedImage.TYPE_BYTE_INDEXED,
1170                colorModel);
1171        maskData = ((DataBufferByte) imageMask.getRaster().getDataBuffer()).getData();
1172
1173        System.arraycopy(area.maskData, 0, maskData, 0, maskData.length);
1174
1175        bounds.setBounds(area.bounds);
1176
1177        // set icon (default name is defined by getDefaultName())
1178        setIcon(ResourceUtil.ICON_ROI_AREA);
1179    }
1180
1181    @Override
1182    public String getDefaultName()
1183    {
1184        return "Area2D";
1185    }
1186
1187    void addToBounds(Rectangle bnd)
1188    {
1189        final Rectangle newBounds;
1190
1191        if (bounds.isEmpty())
1192            newBounds = new Rectangle(bnd);
1193        else
1194        {
1195            newBounds = new Rectangle(bounds);
1196            newBounds.add(bnd);
1197        }
1198
1199        try
1200        {
1201            // update image to the new bounds
1202            updateImage(newBounds);
1203        }
1204        catch (Error E)
1205        {
1206            // perhaps a "out of memory" error, restore back old bounds
1207            System.err.println("can't enlarge ROI, no enough memory !");
1208        }
1209    }
1210
1211    /**
1212     * @deprecated Use {@link #optimizeBounds()} instead.
1213     */
1214    @Deprecated
1215    public void optimizeBounds(boolean removeIfEmpty)
1216    {
1217        optimizeBounds();
1218        if (removeIfEmpty && bounds.isEmpty())
1219            remove();
1220    }
1221
1222    /**
1223     * Returns true if the ROI is empty (the mask does not contains any point).
1224     */
1225    @Override
1226    public boolean isEmpty()
1227    {
1228        if (bounds.isEmpty())
1229            return true;
1230
1231        final byte[] data = maskData;
1232
1233        for (byte b : data)
1234            if (b != 0)
1235                return false;
1236
1237        return true;
1238    }
1239
1240    /**
1241     * Optimize the bounds size to the minimum surface which still include all mask<br>
1242     * You should call it after consecutive remove operations.
1243     */
1244    public boolean optimizeBounds()
1245    {
1246        // bounds are being updated
1247        boundsNeedUpdate = false;
1248
1249        final byte[] data;
1250        final Rectangle bnds;
1251
1252        // recompute bound from the mask data
1253        synchronized (this)
1254        {
1255            data = maskData;
1256            bnds = bounds;
1257        }
1258
1259        final int sizeX = bnds.width;
1260        final int sizeY = bnds.height;
1261
1262        int minX, minY, maxX, maxY;
1263        minX = maxX = minY = maxY = 0;
1264        boolean empty = true;
1265        int offset = 0;
1266
1267        for (int y = 0; y < sizeY; y++)
1268        {
1269            for (int x = 0; x < sizeX; x++)
1270            {
1271                if (data[offset++] != 0)
1272                {
1273                    if (empty)
1274                    {
1275                        minX = maxX = x;
1276                        minY = maxY = y;
1277                        empty = false;
1278                    }
1279                    else
1280                    {
1281                        if (x < minX)
1282                            minX = x;
1283                        else if (x > maxX)
1284                            maxX = x;
1285                        if (y < minY)
1286                            minY = y;
1287                        else if (y > maxY)
1288                            maxY = y;
1289                    }
1290                }
1291            }
1292        }
1293
1294        if (!empty)
1295            // update image to the new bounds
1296            return updateImage(new Rectangle(bnds.x + minX, bnds.y + minY, (maxX - minX) + 1, (maxY - minY) + 1));
1297
1298        // update to empty bounds
1299        return updateImage(new Rectangle(bnds.x, bnds.y, 0, 0));
1300    }
1301
1302    /**
1303     * @deprecated Use {@link #getDisplayColor()} instead.
1304     */
1305    @Deprecated
1306    public Color getMaskColor()
1307    {
1308        return getOverlay().getDisplayColor();
1309    }
1310
1311    void updateMaskColor(boolean rebuildImage)
1312    {
1313        final Color color = getOverlay().getDisplayColor();
1314
1315        // roi color changed ?
1316        if (!previousColor.equals(color))
1317        {
1318            // update colormap
1319            red[1] = (byte) color.getRed();
1320            green[1] = (byte) color.getGreen();
1321            blue[1] = (byte) color.getBlue();
1322
1323            colorModel = new IndexColorModel(8, 256, red, green, blue, 0);
1324
1325            // recreate image (so the new colormodel takes effect)
1326            if (rebuildImage)
1327                imageMask = ImageUtil.createIndexedImage(imageMask.getWidth(), imageMask.getHeight(), colorModel,
1328                        maskData);
1329
1330            // set to new color
1331            previousColor = color;
1332        }
1333    }
1334
1335    /**
1336     * Returns the internal image mask.
1337     */
1338    public BufferedImage getImageMask()
1339    {
1340        return imageMask;
1341    }
1342
1343    boolean updateImage(Rectangle newBnd)
1344    {
1345        final byte[] data;
1346        final Rectangle bnds;
1347
1348        synchronized (this)
1349        {
1350            data = maskData;
1351            bnds = bounds;
1352        }
1353
1354        // copy rectangle
1355        final Rectangle oldBounds = new Rectangle(bnds);
1356        final Rectangle newBounds = new Rectangle(newBnd);
1357
1358        // replace to oldBounds origin
1359        oldBounds.translate(-bnds.x, -bnds.y);
1360        newBounds.translate(-bnds.x, -bnds.y);
1361
1362        // dimension changed ?
1363        if ((oldBounds.width != newBounds.width) || (oldBounds.height != newBounds.height))
1364        {
1365            final BufferedImage newImageMask;
1366            final byte[] newMaskData;
1367
1368            if (!newBounds.isEmpty())
1369            {
1370                // new bounds not empty
1371                newImageMask = new BufferedImage(newBounds.width, newBounds.height, BufferedImage.TYPE_BYTE_INDEXED,
1372                        colorModel);
1373                newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();
1374
1375                final Rectangle intersect = newBounds.intersection(oldBounds);
1376
1377                if (!intersect.isEmpty())
1378                {
1379                    int offSrc = 0;
1380                    int offDst = 0;
1381
1382                    // adjust offset in source mask
1383                    if (intersect.x > 0)
1384                        offSrc += intersect.x;
1385                    if (intersect.y > 0)
1386                        offSrc += intersect.y * oldBounds.width;
1387                    // adjust offset in destination mask
1388                    if (newBounds.x < 0)
1389                        offDst += -newBounds.x;
1390                    if (newBounds.y < 0)
1391                        offDst += -newBounds.y * newBounds.width;
1392
1393                    // preserve data
1394                    for (int j = 0; j < intersect.height; j++)
1395                    {
1396                        System.arraycopy(data, offSrc, newMaskData, offDst, intersect.width);
1397
1398                        offSrc += oldBounds.width;
1399                        offDst += newBounds.width;
1400                    }
1401                }
1402            }
1403            else
1404            {
1405                // new bounds empty --> use single pixel image to avoid NPE
1406                newImageMask = new BufferedImage(1, 1, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
1407                newMaskData = ((DataBufferByte) newImageMask.getRaster().getDataBuffer()).getData();
1408            }
1409
1410            synchronized (this)
1411            {
1412                // set new image and maskData
1413                imageMask = newImageMask;
1414                maskData = newMaskData;
1415                bounds = newBnd;
1416            }
1417
1418            return true;
1419        }
1420
1421        return false;
1422    }
1423
1424    /**
1425     * Set the value of the specified point.<br>
1426     * Don't forget to call optimizeBounds() after consecutive remove operation to refresh the mask
1427     * bounds.
1428     */
1429    public void setPoint(int x, int y, boolean value)
1430    {
1431        final byte[] data;
1432        final Rectangle bnds;
1433
1434        if (value)
1435        {
1436            // set point in mask
1437            addToBounds(new Rectangle(x, y, 1, 1));
1438
1439            synchronized (this)
1440            {
1441                data = maskData;
1442                bnds = bounds;
1443            }
1444
1445            // set color depending remove or adding to mask
1446            data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 1;
1447            // notify roi changed
1448            roiChanged(true);
1449        }
1450        else
1451        {
1452            synchronized (this)
1453            {
1454                data = maskData;
1455                bnds = bounds;
1456            }
1457
1458            if (bnds.contains(x, y))
1459            {
1460                // remove point from mask
1461                data[(x - bnds.x) + ((y - bnds.y) * bnds.width)] = 0;
1462                // mark that bounds need to be updated
1463                boundsNeedUpdate = true;
1464                // notify roi changed
1465                roiChanged(true);
1466            }
1467        }
1468    }
1469
1470    /**
1471     * @deprecated Use {@link #setPoint(int, int, boolean)} instead.
1472     */
1473    @Deprecated
1474    public void updateMask(int x, int y, boolean remove)
1475    {
1476        setPoint(x, y, !remove);
1477    }
1478
1479    /**
1480     * Add the specified {@link ROI2DArea} content to this ROI2DArea
1481     */
1482    public void add(ROI2DArea roi)
1483    {
1484        final Rectangle boundsToAdd = roi.getBounds();
1485        final byte[] maskToAdd = roi.maskData;
1486
1487        // update bounds (this update the image dimension if needed)
1488        addToBounds(boundsToAdd);
1489
1490        int offDst, offSrc;
1491        final byte[] data;
1492        final Rectangle bnds;
1493
1494        synchronized (this)
1495        {
1496            data = maskData;
1497            bnds = bounds;
1498        }
1499
1500        // calculate offset
1501        offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x);
1502        offSrc = 0;
1503
1504        for (int y = 0; y < boundsToAdd.height; y++)
1505        {
1506            for (int x = 0; x < boundsToAdd.width; x++)
1507                if (maskToAdd[offSrc++] != 0)
1508                    data[offDst + x] = 1;
1509
1510            offDst += bnds.width;
1511        }
1512
1513        // notify roi changed
1514        roiChanged(true);
1515    }
1516
1517    /**
1518     * Add the specified {@link BooleanMask2D} content to this ROI2DArea
1519     */
1520    public void add(BooleanMask2D mask)
1521    {
1522        final Rectangle boundsToAdd = mask.bounds;
1523        final boolean[] maskToAdd = mask.mask;
1524
1525        // update bounds (this update the image dimension if needed)
1526        addToBounds(boundsToAdd);
1527
1528        int offDst, offSrc;
1529        final byte[] data;
1530        final Rectangle bnds;
1531
1532        synchronized (this)
1533        {
1534            data = maskData;
1535            bnds = bounds;
1536        }
1537
1538        // calculate offset
1539        offDst = ((boundsToAdd.y - bnds.y) * bnds.width) + (boundsToAdd.x - bnds.x);
1540        offSrc = 0;
1541
1542        for (int y = 0; y < boundsToAdd.height; y++)
1543        {
1544            for (int x = 0; x < boundsToAdd.width; x++)
1545                if (maskToAdd[offSrc++])
1546                    data[offDst + x] = 1;
1547
1548            offDst += bnds.width;
1549        }
1550
1551        // notify roi changed
1552        roiChanged(true);
1553    }
1554
1555    /**
1556     * Exclusively add the specified {@link ROI2DArea} content to this ROI2DArea:
1557     * 
1558     * <pre>
1559     *          mask1       xor      mask2        =       result
1560     * 
1561     *     ################     ################
1562     *     ##############         ##############     ##            ##
1563     *     ############             ############     ####        ####
1564     *     ##########                 ##########     ######    ######
1565     *     ########                     ########     ################
1566     *     ######                         ######     ######    ######
1567     *     ####                             ####     ####        ####
1568     *     ##                                 ##     ##            ##
1569     * </pre>
1570     */
1571    public void exclusiveAdd(ROI2DArea roi)
1572    {
1573        final Rectangle boundsToXAdd = roi.getBounds();
1574        final byte[] maskToXAdd = roi.maskData;
1575
1576        // update bounds (this update the image dimension if needed)
1577        addToBounds(boundsToXAdd);
1578
1579        int offDst, offSrc;
1580        final byte[] data;
1581        final Rectangle bnds;
1582
1583        synchronized (this)
1584        {
1585            data = maskData;
1586            bnds = bounds;
1587        }
1588
1589        // calculate offset
1590        offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x);
1591        offSrc = 0;
1592
1593        for (int y = 0; y < boundsToXAdd.height; y++)
1594        {
1595            for (int x = 0; x < boundsToXAdd.width; x++)
1596                if (maskToXAdd[offSrc++] != 0)
1597                    data[offDst + x] ^= 1;
1598
1599            offDst += bnds.width;
1600        }
1601
1602        // optimize bounds
1603        if (isUpdating())
1604            boundsNeedUpdate = true;
1605        else
1606            optimizeBounds();
1607
1608        // notify roi changed
1609        roiChanged(true);
1610    }
1611
1612    /**
1613     * Exclusively add the specified {@link BooleanMask2D} content to this ROI2DArea:
1614     * 
1615     * <pre>
1616     *          mask1       xor      mask2        =       result
1617     * 
1618     *     ################     ################
1619     *     ##############         ##############     ##            ##
1620     *     ############             ############     ####        ####
1621     *     ##########                 ##########     ######    ######
1622     *     ########                     ########     ################
1623     *     ######                         ######     ######    ######
1624     *     ####                             ####     ####        ####
1625     *     ##                                 ##     ##            ##
1626     * </pre>
1627     */
1628    public void exclusiveAdd(BooleanMask2D mask)
1629    {
1630        final Rectangle boundsToXAdd = mask.bounds;
1631        final boolean[] maskToXAdd = mask.mask;
1632
1633        // update bounds (this update the image dimension if needed)
1634        addToBounds(boundsToXAdd);
1635
1636        int offDst, offSrc;
1637        final byte[] data;
1638        final Rectangle bnds;
1639
1640        synchronized (this)
1641        {
1642            data = maskData;
1643            bnds = bounds;
1644        }
1645
1646        // calculate offset
1647        offDst = ((boundsToXAdd.y - bnds.y) * bnds.width) + (boundsToXAdd.x - bnds.x);
1648        offSrc = 0;
1649
1650        for (int y = 0; y < boundsToXAdd.height; y++)
1651        {
1652            for (int x = 0; x < boundsToXAdd.width; x++)
1653                if (maskToXAdd[offSrc++])
1654                    data[offDst + x] ^= 1;
1655
1656            offDst += bnds.width;
1657        }
1658
1659        // optimize bounds
1660        if (isUpdating())
1661            boundsNeedUpdate = true;
1662        else
1663            optimizeBounds();
1664
1665        // notify roi changed
1666        roiChanged(true);
1667    }
1668
1669    /**
1670     * Subtract the specified {@link ROI2DArea} from this ROI2DArea
1671     */
1672    public void subtract(ROI2DArea roi)
1673    {
1674        final Rectangle boundsToRemove = roi.getBounds();
1675        final byte[] maskToRemove = roi.maskData;
1676        final byte[] data;
1677        final Rectangle bnds;
1678
1679        synchronized (this)
1680        {
1681            data = maskData;
1682            bnds = bounds;
1683        }
1684
1685        // compute intersection
1686        final Rectangle intersection = bnds.intersection(boundsToRemove);
1687
1688        // nothing to remove so nothing to do...
1689        if (intersection.isEmpty())
1690            return;
1691
1692        // calculate offset
1693        int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x);
1694        int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x);
1695
1696        for (int y = 0; y < intersection.height; y++)
1697        {
1698            for (int x = 0; x < intersection.width; x++)
1699                if (maskToRemove[offSrc + x] != 0)
1700                    data[offDst + x] = 0;
1701
1702            offDst += bnds.width;
1703            offSrc += boundsToRemove.width;
1704        }
1705
1706        // optimize bounds
1707        if (isUpdating())
1708            boundsNeedUpdate = true;
1709        else
1710            optimizeBounds();
1711
1712        // notify roi changed
1713        roiChanged(true);
1714    }
1715
1716    /**
1717     * Subtract the specified {@link BooleanMask2D} from this ROI2DArea
1718     */
1719    public void subtract(BooleanMask2D mask)
1720    {
1721        final Rectangle boundsToRemove = mask.bounds;
1722        final boolean[] maskToRemove = mask.mask;
1723        final byte[] data;
1724        final Rectangle bnds;
1725
1726        synchronized (this)
1727        {
1728            data = maskData;
1729            bnds = bounds;
1730        }
1731
1732        // compute intersection
1733        final Rectangle intersection = bnds.intersection(boundsToRemove);
1734
1735        // nothing to remove so nothing to do...
1736        if (intersection.isEmpty())
1737            return;
1738
1739        // calculate offset
1740        int offDst = ((intersection.y - bnds.y) * bnds.width) + (intersection.x - bnds.x);
1741        int offSrc = ((intersection.y - boundsToRemove.y) * boundsToRemove.width) + (intersection.x - boundsToRemove.x);
1742
1743        for (int y = 0; y < intersection.height; y++)
1744        {
1745            for (int x = 0; x < intersection.width; x++)
1746                if (maskToRemove[offSrc + x])
1747                    data[offDst + x] = 0;
1748
1749            offDst += bnds.width;
1750            offSrc += boundsToRemove.width;
1751        }
1752
1753        // optimize bounds
1754        if (isUpdating())
1755            boundsNeedUpdate = true;
1756        else
1757            optimizeBounds();
1758
1759        // notify roi changed
1760        roiChanged(true);
1761    }
1762
1763    /**
1764     * @deprecated Use {@link #subtract(ROI2DArea)} instead
1765     */
1766    @Deprecated
1767    public void remove(ROI2DArea roi)
1768    {
1769        subtract(roi);
1770    }
1771
1772    /**
1773     * @deprecated Use {@link #subtract(BooleanMask2D)} instead
1774     */
1775    @Deprecated
1776    public void remove(BooleanMask2D mask)
1777    {
1778        subtract(mask);
1779    }
1780
1781    /**
1782     * Update mask by adding/removing the specified shape to/from it.
1783     * 
1784     * @param shape
1785     *        the shape to add in or remove from the mask
1786     * @param remove
1787     *        if set to <code>true</code> the shape will be removed from the mask
1788     * @param inclusive
1789     *        if we should also consider the edge of the shape to update the mask
1790     * @param accurate
1791     *        if set to <code>true</code> the operation will be done to be as pixel accurate as
1792     *        possible
1793     * @param immediateUpdate
1794     *        if set to <code>true</code> the bounds of the mask will be immediately recomputed
1795     *        (only meaningful for a
1796     *        remove operation)
1797     */
1798    public void updateMask(Shape shape, boolean remove, boolean inclusive, boolean accurate, boolean immediateUpdate)
1799    {
1800        if (remove)
1801        {
1802            // outside bounds ? --> nothing to remove so nothing to do...
1803            if (!bounds.intersects(shape.getBounds2D()))
1804                return;
1805
1806            // mark that bounds need to be updated
1807            if (isUpdating() || !immediateUpdate)
1808                boundsNeedUpdate = true;
1809        }
1810        else
1811            // update bounds (this update the image dimension if needed)
1812            addToBounds(shape.getBounds());
1813
1814        // get image graphics object
1815        final Graphics2D g = imageMask.createGraphics();
1816
1817        // we don't need anti aliasing here
1818        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
1819        // force accurate stroke rendering
1820        if (accurate)
1821            g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
1822
1823        g.setComposite(AlphaComposite.Src);
1824        // set color depending remove or adding to mask
1825        if (remove)
1826            g.setColor(new Color(colorModel.getRGB(0), true));
1827        else
1828            g.setColor(new Color(colorModel.getRGB(1), true));
1829        // translate to origin of image and pixel center
1830        g.translate(-(bounds.x + 0.5d), -(bounds.y + 0.5d));
1831        // draw shape into the mask
1832        g.fill(ShapeUtil.getClosedPath(shape));
1833        // we want edge as well
1834        if (inclusive)
1835            g.draw(shape);
1836
1837        g.dispose();
1838
1839        // need to optimize bounds
1840        if (remove && !isUpdating() && immediateUpdate)
1841            optimizeBounds();
1842
1843        // notify roi changed
1844        roiChanged(true);
1845    }
1846
1847    /**
1848     * Update mask from specified shape
1849     */
1850    public void updateMask(Shape shape, boolean remove)
1851    {
1852        updateMask(shape, remove, true, false, false);
1853    }
1854
1855    @Deprecated
1856    @Override
1857    public ROI2DAreaPainter getPainter()
1858    {
1859        return getOverlay();
1860    }
1861
1862    @Override
1863    public ROI2DAreaPainter getOverlay()
1864    {
1865        return (ROI2DAreaPainter) super.painter;
1866    }
1867
1868    @Override
1869    protected ROI2DAreaPainter createPainter()
1870    {
1871        return new ROI2DAreaPainter();
1872    }
1873
1874    @Override
1875    public boolean hasSelectedPoint()
1876    {
1877        return false;
1878    }
1879
1880    /**
1881     * @deprecated useless method.
1882     */
1883    @Deprecated
1884    public boolean canAddPoint()
1885    {
1886        return true;
1887    }
1888
1889    /**
1890     * @deprecated useless method.
1891     */
1892    @Deprecated
1893    public boolean canRemovePoint()
1894    {
1895        return true;
1896    }
1897
1898    /**
1899     * @deprecated Use {@link #addBrush(Point2D)} instead.
1900     */
1901    @Deprecated
1902    public boolean addPointAt(Point2D pos, boolean ctrl)
1903    {
1904        addBrush(pos);
1905        return true;
1906    }
1907
1908    /**
1909     * @deprecated Use {@link #removeBrush(Point2D)} instead.
1910     */
1911    @Deprecated
1912    public boolean removePointAt(IcyCanvas canvas, Point2D pos)
1913    {
1914        removeBrush(pos);
1915        return true;
1916    }
1917
1918    /**
1919     * @deprecated Useless method.
1920     */
1921    @Deprecated
1922    protected boolean removeSelectedPoint(IcyCanvas canvas, Point2D imagePoint)
1923    {
1924        // no selected point for this ROI
1925        return false;
1926    }
1927
1928    /**
1929     * Add brush point at specified position.
1930     */
1931    public void addBrush(Point2D pos)
1932    {
1933        getOverlay().addToMask(pos);
1934    }
1935
1936    /**
1937     * Remove brush point from the mask at specified position.<br>
1938     * Don't forget to call optimizeBounds() after consecutive remove operation
1939     * to refresh the mask bounds.
1940     */
1941    public void removeBrush(Point2D pos)
1942    {
1943        getOverlay().removeFromMask(pos);
1944    }
1945
1946    /**
1947     * Add a point to the mask
1948     */
1949    public void addPoint(Point pos)
1950    {
1951        addPoint(pos.x, pos.y);
1952    }
1953
1954    /**
1955     * Add a point to the mask
1956     */
1957    public void addPoint(int x, int y)
1958    {
1959        setPoint(x, y, true);
1960    }
1961
1962    /**
1963     * Remove a point from the mask.<br>
1964     * Don't forget to call optimizeBounds() after consecutive remove operation
1965     * to refresh the mask bounds.
1966     */
1967    public void removePoint(Point pos)
1968    {
1969        removePoint(pos.x, pos.y);
1970    }
1971
1972    /**
1973     * Remove a point to the mask.<br>
1974     * Don't forget to call optimizeBounds() after consecutive remove operation
1975     * to refresh the mask bounds.
1976     */
1977    public void removePoint(int x, int y)
1978    {
1979        setPoint(x, y, false);
1980    }
1981
1982    /**
1983     * Add a rectangle to the mask
1984     */
1985    public void addRect(Rectangle r)
1986    {
1987        updateMask(r, false, false, true, true);
1988    }
1989
1990    /**
1991     * Add a rectangle to the mask
1992     */
1993    public void addRect(int x, int y, int w, int h)
1994    {
1995        addRect(new Rectangle(x, y, w, h));
1996    }
1997
1998    /**
1999     * Remove a rectangle from the mask.<br>
2000     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
2001     * to refresh the mask bounds.
2002     */
2003    public void removeRect(Rectangle r)
2004    {
2005        updateMask(r, true, false, true, true);
2006    }
2007
2008    /**
2009     * Remove a rectangle from the mask.<br>
2010     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
2011     * to refresh the mask bounds.
2012     */
2013    public void removeRect(int x, int y, int w, int h)
2014    {
2015        removeRect(new Rectangle(x, y, w, h));
2016    }
2017
2018    /**
2019     * Add a shape to the mask
2020     */
2021    public void addShape(Shape s)
2022    {
2023        updateMask(s, false, false, true, true);
2024    }
2025
2026    /**
2027     * Remove a shape to the mask.<br>
2028     * Don't forget to call optimizeBounds() after consecutive remove operation<br>
2029     * to refresh the mask bounds.
2030     */
2031    public void removeShape(Shape s)
2032    {
2033        updateMask(s, true, false, true, true);
2034    }
2035
2036    @Override
2037    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
2038    {
2039        if (roi instanceof ROI2D)
2040        {
2041            final ROI2D roi2d = (ROI2D) roi;
2042
2043            // only if on same position
2044            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
2045            {
2046                if (roi2d instanceof ROI2DArea)
2047                    add((ROI2DArea) roi2d);
2048                else if (roi2d instanceof ROI2DShape)
2049                    updateMask(((ROI2DShape) roi2d).getShape(), false, true, true, true);
2050                else
2051                    add(roi2d.getBooleanMask(true));
2052
2053                return this;
2054            }
2055        }
2056
2057        return super.add(roi, allowCreate);
2058    }
2059
2060    @Override
2061    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
2062    {
2063        if (roi instanceof ROI2D)
2064        {
2065            final ROI2D roi2d = (ROI2D) roi;
2066
2067            // only if on same position
2068            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
2069            {
2070                final Rectangle intersection = getBounds().intersection(roi2d.getBounds());
2071                final BooleanMask2D mask = new BooleanMask2D(intersection, getBooleanMask(intersection, true));
2072                final BooleanMask2D roiMask = new BooleanMask2D(intersection, roi2d.getBooleanMask(intersection, true));
2073
2074                setAsBooleanMask(BooleanMask2D.getIntersection(mask, roiMask));
2075
2076                return this;
2077            }
2078        }
2079
2080        return super.intersect(roi, allowCreate);
2081    }
2082
2083    @Override
2084    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
2085    {
2086        if (roi instanceof ROI2D)
2087        {
2088            final ROI2D roi2d = (ROI2D) roi;
2089
2090            // only if on same position
2091            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
2092            {
2093                if (roi2d instanceof ROI2DArea)
2094                    exclusiveAdd((ROI2DArea) roi2d);
2095                else
2096                    exclusiveAdd(roi2d.getBooleanMask(true));
2097
2098                return this;
2099            }
2100        }
2101
2102        return super.exclusiveAdd(roi, allowCreate);
2103    }
2104
2105    @Override
2106    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
2107    {
2108        if (roi instanceof ROI2D)
2109        {
2110            final ROI2D roi2d = (ROI2D) roi;
2111
2112            // only if on same position
2113            if ((getZ() == roi2d.getZ()) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
2114            {
2115                if (roi2d instanceof ROI2DArea)
2116                    subtract((ROI2DArea) roi2d);
2117                else if (roi2d instanceof ROI2DShape)
2118                    updateMask(((ROI2DShape) roi2d).getShape(), true, true, true, true);
2119                else
2120                    subtract(roi2d.getBooleanMask(true));
2121
2122                return this;
2123            }
2124        }
2125
2126        return super.subtract(roi, allowCreate);
2127    }
2128
2129    /**
2130     * Return true if bounds need to be updated by calling optimizeBounds() method.
2131     */
2132    public boolean getBoundsNeedUpdate()
2133    {
2134        return boundsNeedUpdate;
2135    }
2136
2137    /**
2138     * Clear the mask
2139     */
2140    public void clear()
2141    {
2142        // reset image with new rectangle
2143        updateImage(new Rectangle());
2144    }
2145
2146    @Override
2147    public boolean isOverEdge(IcyCanvas canvas, double x, double y)
2148    {
2149        // use bigger stroke for isOverEdge test for easier intersection
2150        final double strk = getAdjustedStroke(canvas) * 3;
2151        final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk);
2152
2153        // fast intersect test to start with
2154        if (getBounds2D().intersects(rect))
2155            // use flatten path, intersects on curved shape return incorrect result
2156            return ShapeUtil.pathIntersects(bounds.getPathIterator(null, 0.1), rect);
2157
2158        return false;
2159    }
2160
2161    @Override
2162    public boolean contains(double x, double y)
2163    {
2164        final byte[] data;
2165        final Rectangle bnds;
2166
2167        synchronized (this)
2168        {
2169            data = maskData;
2170            bnds = bounds;
2171        }
2172
2173        // fast discard
2174        if (!bnds.contains(x, y))
2175            return false;
2176
2177        // replace to origin
2178        final int xi = (int) x - bnds.x;
2179        final int yi = (int) y - bnds.y;
2180
2181        return (data[(yi * bnds.width) + xi] != 0);
2182    }
2183
2184    @Override
2185    public boolean contains(double x, double y, double w, double h)
2186    {
2187        final byte[] data;
2188        final Rectangle bnds;
2189
2190        synchronized (this)
2191        {
2192            data = maskData;
2193            bnds = bounds;
2194        }
2195
2196        // fast discard
2197        if (!bnds.contains(x, y, w, h))
2198            return false;
2199
2200        // replace to origin
2201        final int xi = (int) x - bnds.x;
2202        final int yi = (int) y - bnds.y;
2203        final int wi = (int) (x + w) - (int) x;
2204        final int hi = (int) (y + h) - (int) y;
2205
2206        // scan all pixels, can take sometime if mask is large
2207        int offset = (yi * bnds.width) + xi;
2208        for (int j = 0; j < hi; j++)
2209        {
2210            for (int i = 0; i < wi; i++)
2211                if (data[offset++] == 0)
2212                    return false;
2213
2214            offset += bnds.width - wi;
2215        }
2216
2217        return true;
2218    }
2219
2220    /*
2221     * already calculated
2222     */
2223    @Override
2224    public Rectangle2D computeBounds2D()
2225    {
2226        return bounds;
2227    }
2228
2229    /*
2230     * We can override directly this method as we use our own bounds calculation method here
2231     */
2232    @Override
2233    public Rectangle2D getBounds2D()
2234    {
2235        return bounds;
2236    }
2237
2238    @Override
2239    public boolean intersects(double x, double y, double w, double h)
2240    {
2241        final byte[] data;
2242        final Rectangle bnds;
2243
2244        synchronized (this)
2245        {
2246            data = maskData;
2247            bnds = bounds;
2248        }
2249
2250        // fast discard
2251        if (!bnds.intersects(x, y, w, h))
2252            return false;
2253
2254        // replace to origin
2255        int xi = (int) x - bnds.x;
2256        int yi = (int) y - bnds.y;
2257        int wi = (int) (x + w) - (int) x;
2258        int hi = (int) (y + h) - (int) y;
2259
2260        // adjust box to mask size
2261        if (xi < 0)
2262        {
2263            wi += xi;
2264            xi = 0;
2265        }
2266        if (yi < 0)
2267        {
2268            hi += yi;
2269            yi = 0;
2270        }
2271        if ((xi + wi) > bnds.width)
2272            wi -= (xi + wi) - bnds.width;
2273        if ((yi + hi) > bnds.height)
2274            hi -= (yi + hi) - bnds.height;
2275
2276        // scan all pixels, can take sometime if mask is large
2277        int offset = (yi * bnds.width) + xi;
2278        for (int j = 0; j < hi; j++)
2279        {
2280            for (int i = 0; i < wi; i++)
2281                if (data[offset++] != 0)
2282                    return true;
2283
2284            offset += bnds.width - wi;
2285        }
2286
2287        return false;
2288    }
2289
2290    @Override
2291    public boolean[] getBooleanMask(int x, int y, int w, int h, boolean inclusive)
2292    {
2293        final boolean[] result = new boolean[Math.max(0, w) * Math.max(0, h)];
2294        final byte[] data;
2295        final Rectangle bnds;
2296
2297        synchronized (this)
2298        {
2299            data = maskData;
2300            bnds = bounds;
2301        }
2302
2303        // calculate intersection
2304        final Rectangle intersect = bnds.intersection(new Rectangle(x, y, w, h));
2305
2306        // no intersection between mask and specified rectangle
2307        if (intersect.isEmpty())
2308            return result;
2309
2310        // this ROI doesn't take care of inclusive parameter as intersect = contains
2311        int offSrc = 0;
2312        int offDst = 0;
2313
2314        // adjust offset in source mask
2315        if (intersect.x > bnds.x)
2316            offSrc += (intersect.x - bnds.x);
2317        if (intersect.y > bnds.y)
2318            offSrc += (intersect.y - bnds.y) * bnds.width;
2319        // adjust offset in destination mask
2320        if (bnds.x > x)
2321            offDst += (bnds.x - x);
2322        if (bnds.y > y)
2323            offDst += (bnds.y - y) * w;
2324
2325        for (int j = 0; j < intersect.height; j++)
2326        {
2327            for (int i = 0; i < intersect.width; i++)
2328                result[offDst++] = (data[offSrc++] != 0);
2329
2330            offSrc += bnds.width - intersect.width;
2331            offDst += w - intersect.width;
2332        }
2333
2334        return result;
2335    }
2336
2337    @Override
2338    public double computeNumberOfPoints()
2339    {
2340        // just count the number of point contained in the mask
2341        double result = 0d;
2342        final byte[] data = maskData;
2343
2344        for (int i = 0; i < data.length; i++)
2345            if (data[i] != 0)
2346                result += 1d;
2347
2348        return result;
2349    }
2350
2351    @Override
2352    public boolean canTranslate()
2353    {
2354        return true;
2355    }
2356
2357    @Override
2358    public void translate(double dx, double dy)
2359    {
2360        translateX += dx;
2361        translateY += dy;
2362
2363        // convert to integer
2364        final int dxi = (int) translateX;
2365        final int dyi = (int) translateY;
2366        // keep trace of not used floating part
2367        translateX -= dxi;
2368        translateY -= dyi;
2369
2370        if ((dxi != 0) || (dyi != 0))
2371        {
2372            bounds.translate(dxi, dyi);
2373            roiChanged(false);
2374        }
2375    }
2376
2377    @Override
2378    public boolean canSetPosition()
2379    {
2380        return true;
2381    }
2382
2383    @Override
2384    public void setPosition2D(Point2D newPosition)
2385    {
2386        bounds = new Rectangle((int) newPosition.getX(), (int) newPosition.getY(), bounds.width, bounds.height);
2387
2388        roiChanged(false);
2389    }
2390
2391    /**
2392     * Set the mask from a BooleanMask2D object.<br>
2393     * If specified mask is <i>null</i> then ROI is cleared.
2394     */
2395    public void setAsBooleanMask(BooleanMask2D mask)
2396    {
2397        // mask empty ? --> just clear the ROI
2398        if ((mask == null) || mask.isEmpty())
2399            clear();
2400        // don't need bounds optimization as BooleanMask2D should be already optimized
2401        else
2402            setAsBooleanMask(mask.bounds, mask.mask, false);
2403    }
2404
2405    /**
2406     * Set the mask from a boolean array.<br>
2407     * r represents the region defined by the boolean array.
2408     * 
2409     * @param r
2410     * @param booleanMask
2411     */
2412    protected void setAsByteMask(Rectangle r, byte[] mask, boolean doBoundsOptimization)
2413    {
2414        // reset image with new rectangle
2415        updateImage(r);
2416
2417        System.arraycopy(mask, 0, maskData, 0, r.width * r.height);
2418
2419        if (doBoundsOptimization)
2420        {
2421            // optimize bounds
2422            if (isUpdating())
2423                boundsNeedUpdate = true;
2424            else
2425                optimizeBounds();
2426        }
2427
2428        // notify roi changed
2429        roiChanged(true);
2430    }
2431
2432    /**
2433     * Set the mask from a boolean array.<br>
2434     * r represents the region defined by the boolean array.
2435     * 
2436     * @param r
2437     * @param booleanMask
2438     */
2439    protected void setAsBooleanMask(Rectangle r, boolean[] booleanMask, boolean doBoundsOptimization)
2440    {
2441        // reset image with new rectangle
2442        updateImage(r);
2443
2444        final byte[] data = maskData;
2445
2446        for (int i = 0; i < data.length; i++)
2447            data[i] = (byte) (booleanMask[i] ? 1 : 0);
2448
2449        if (doBoundsOptimization)
2450        {
2451            // optimize bounds
2452            if (isUpdating())
2453                boundsNeedUpdate = true;
2454            else
2455                optimizeBounds();
2456        }
2457
2458        // notify roi changed
2459        roiChanged(true);
2460    }
2461
2462    /**
2463     * Set the mask from a boolean array.<br>
2464     * r represents the region defined by the boolean array.
2465     * 
2466     * @param r
2467     * @param booleanMask
2468     */
2469    public void setAsBooleanMask(Rectangle r, boolean[] booleanMask)
2470    {
2471        setAsBooleanMask(r, booleanMask, true);
2472    }
2473
2474    public void setAsBooleanMask(int x, int y, int w, int h, boolean[] booleanMask)
2475    {
2476        setAsBooleanMask(new Rectangle(x, y, w, h), booleanMask);
2477    }
2478
2479    /**
2480     * Fast up scaling by a factor of 2 (each point become a 2x2 block points)
2481     */
2482    public void upscale()
2483    {
2484        setAsBooleanMask(getBooleanMask(true).upscale());
2485    }
2486
2487    /**
2488     * Fast 2x down scaling (each 2x2 block points become 1 point).<br>
2489     * 
2490     * @param nbPointForTrue
2491     *        the minimum number of <code>true</code>points from a 2x2 block to give a <code>true</code> resulting
2492     *        point.<br>
2493     *        Accepted value: 1 to 4
2494     */
2495    public void downscale(int nbPointForTrue)
2496    {
2497        setAsBooleanMask(getBooleanMask(true).downscale(nbPointForTrue));
2498    }
2499
2500    /**
2501     * Fast 2x down scaling (each 2x2 block points become 1 point).<br>
2502     */
2503    public void downscale()
2504    {
2505        setAsBooleanMask(getBooleanMask(true).downscale());
2506    }
2507
2508    @Override
2509    public void onChanged(CollapsibleEvent object)
2510    {
2511        final ROIEvent event = (ROIEvent) object;
2512
2513        // do here global process on ROI change
2514        switch (event.getType())
2515        {
2516            case ROI_CHANGED:
2517                // update bounds if needed
2518                if (boundsNeedUpdate && !roiModifiedByMouse)
2519                {
2520                    if (optimizeBounds())
2521                        // need to send a new change event !
2522                        roiChanged(true);
2523                }
2524                // we need to rebuild shape
2525                if (StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL))
2526                    getOverlay().needRebuild = true;
2527                else
2528                {
2529                    final ROI2DAreaPainter overlay = getOverlay();
2530
2531                    // z position change ? --> need total rebuild
2532                    if (overlay.lastBuildPosZ != getZ())
2533                        overlay.needRebuild = true;
2534                    // just need to change position
2535                    else
2536                        overlay.updateVtkObjectsBounds();
2537                }
2538                break;
2539
2540            case FOCUS_CHANGED:
2541            case SELECTION_CHANGED:
2542                getOverlay().updateVtkDisplayProperties();
2543                break;
2544
2545            case PROPERTY_CHANGED:
2546                final String property = event.getPropertyName();
2547
2548                if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR)
2549                        || StringUtil.equals(property, PROPERTY_OPACITY))
2550                    getOverlay().updateVtkDisplayProperties();
2551                break;
2552
2553            default:
2554                break;
2555        }
2556
2557        super.onChanged(object);
2558    }
2559
2560    @Override
2561    public boolean loadFromXML(Node node)
2562    {
2563        beginUpdate();
2564        try
2565        {
2566            if (!super.loadFromXML(node))
2567                return false;
2568
2569            final Rectangle rect = new Rectangle();
2570
2571            // retrieve mask bounds
2572            rect.x = XMLUtil.getElementIntValue(node, ID_BOUNDS_X, 0);
2573            rect.y = XMLUtil.getElementIntValue(node, ID_BOUNDS_Y, 0);
2574            rect.width = XMLUtil.getElementIntValue(node, ID_BOUNDS_W, 0);
2575            rect.height = XMLUtil.getElementIntValue(node, ID_BOUNDS_H, 0);
2576
2577            // retrieve mask data
2578            final byte[] data = XMLUtil.getElementBytesValue(node, ID_BOOLMASK_DATA, new byte[0]);
2579
2580            // an error occurred while retrieved XML data
2581            if (data == null)
2582                return false;
2583
2584            // set the ROI from the unpacked boolean mask
2585            setAsByteMask(rect, data, false);
2586        }
2587        finally
2588        {
2589            endUpdate();
2590        }
2591
2592        return true;
2593    }
2594
2595    @Override
2596    public boolean saveToXML(Node node)
2597    {
2598        if (!super.saveToXML(node))
2599            return false;
2600
2601        final byte[] data;
2602        final Rectangle bnds;
2603
2604        synchronized (maskData)
2605        {
2606            // need to duplicate to avoid array change during XML saving (ZIP packing don't like that)
2607            data = maskData.clone();
2608            bnds = bounds;
2609        }
2610
2611        final int len = bnds.width * bnds.height;
2612
2613        // invalid --> return false
2614        if ((len > 0) && (len != data.length))
2615            return false;
2616
2617        // retrieve mask bounds
2618        XMLUtil.setElementIntValue(node, ID_BOUNDS_X, bnds.x);
2619        XMLUtil.setElementIntValue(node, ID_BOUNDS_Y, bnds.y);
2620        XMLUtil.setElementIntValue(node, ID_BOUNDS_W, bnds.width);
2621        XMLUtil.setElementIntValue(node, ID_BOUNDS_H, bnds.height);
2622
2623        // set mask data as byte array
2624        if (len > 0)
2625            XMLUtil.setElementBytesValue(node, ID_BOOLMASK_DATA, data);
2626
2627        return true;
2628    }
2629}