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.roi3d;
020
021import java.awt.Color;
022import java.awt.Graphics2D;
023import java.awt.Rectangle;
024import java.awt.event.KeyEvent;
025import java.awt.event.MouseEvent;
026import java.awt.event.MouseWheelEvent;
027import java.awt.geom.Rectangle2D;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.Map;
032import java.util.Map.Entry;
033import java.util.Set;
034import java.util.TreeMap;
035import java.util.concurrent.Semaphore;
036
037import org.w3c.dom.Element;
038import org.w3c.dom.Node;
039
040import icy.canvas.IcyCanvas;
041import icy.painter.OverlayEvent;
042import icy.painter.OverlayListener;
043import icy.roi.BooleanMask2D;
044import icy.roi.ROI;
045import icy.roi.ROI2D;
046import icy.roi.ROI2D.ROI2DPainter;
047import icy.roi.ROI3D;
048import icy.roi.ROIEvent;
049import icy.roi.ROIListener;
050import icy.sequence.Sequence;
051import icy.system.IcyExceptionHandler;
052import icy.type.point.Point5D;
053import icy.type.rectangle.Rectangle3D;
054import icy.util.XMLUtil;
055
056/**
057 * Base class defining a generic 3D ROI as a stack of individual 2D ROI slices.
058 * 
059 * @author Alexandre Dufour
060 * @author Stephane Dallongeville
061 * @param <R>
062 *        the type of 2D ROI for each slice of this 3D ROI
063 */
064public class ROI3DStack<R extends ROI2D> extends ROI3D implements ROIListener, OverlayListener, Iterable<R>
065{
066    /**
067     * @deprecated this property does not exist anymore
068     */
069    @Deprecated
070    public static final String PROPERTY_USECHILDCOLOR = "useChildColor";
071
072    protected final TreeMap<Integer, R> slices = new TreeMap<Integer, R>();
073
074    protected final Class<? extends R> roiClass;
075    protected Semaphore modifyingSlice;
076    protected double translateZ;
077
078    /**
079     * Creates a new 3D ROI based on the given 2D ROI type.
080     */
081    public ROI3DStack(Class<? extends R> roiClass)
082    {
083        super();
084
085        this.roiClass = roiClass;
086        modifyingSlice = new Semaphore(1);
087        translateZ = 0d;
088    }
089
090    @Override
091    public String getDefaultName()
092    {
093        return "ROI2D stack";
094    }
095
096    @Override
097    protected ROIPainter createPainter()
098    {
099        return new ROI3DStackPainter();
100    }
101
102    /**
103     * Create a new empty 2D ROI slice.
104     */
105    protected R createSlice()
106    {
107        try
108        {
109            return roiClass.newInstance();
110        }
111        catch (Exception e)
112        {
113            IcyExceptionHandler.showErrorMessage(e, true, true);
114            return null;
115        }
116    }
117
118    /**
119     * Returns <code>true</code> if the ROI directly uses the 2D slice color draw property and <code>false</code> if it
120     * uses the global 3D ROI color draw property.
121     */
122    @SuppressWarnings("unchecked")
123    public boolean getUseChildColor()
124    {
125        return ((ROI3DStackPainter) getOverlay()).getUseChildColor();
126    }
127
128    /**
129     * Set to <code>true</code> if you want to directly use the 2D slice color draw property and <code>false</code> to
130     * keep the global 3D ROI color draw property.
131     * 
132     * @see #setColor(int, Color)
133     */
134    @SuppressWarnings("unchecked")
135    public void setUseChildColor(boolean value)
136    {
137        ((ROI3DStackPainter) getOverlay()).setUseChildColor(value);
138    }
139
140    /**
141     * Set the painter color for the specified ROI slice.
142     * 
143     * @see #setUseChildColor(boolean)
144     */
145    @SuppressWarnings("unchecked")
146    public void setColor(int z, Color value)
147    {
148        ((ROI3DStackPainter) getOverlay()).setColor(z, value);
149    }
150
151    @Override
152    public void setCreating(boolean value)
153    {
154        beginUpdate();
155        try
156        {
157            super.setCreating(value);
158
159            modifyingSlice.acquireUninterruptibly();
160            try
161            {
162                synchronized (slices)
163                {
164                    for (R slice : slices.values())
165                        slice.setCreating(value);
166                }
167            }
168            finally
169            {
170                modifyingSlice.release();
171            }
172        }
173        finally
174        {
175            endUpdate();
176        }
177    }
178
179    @Override
180    public void setReadOnly(boolean value)
181    {
182        beginUpdate();
183        try
184        {
185            super.setReadOnly(value);
186
187            modifyingSlice.acquireUninterruptibly();
188            try
189            {
190                synchronized (slices)
191                {
192                    for (R slice : slices.values())
193                        slice.setReadOnly(value);
194                }
195            }
196            finally
197            {
198                modifyingSlice.release();
199            }
200        }
201        finally
202        {
203            endUpdate();
204        }
205    }
206
207    @Override
208    public void setFocused(boolean value)
209    {
210        beginUpdate();
211        try
212        {
213            super.setFocused(value);
214
215            modifyingSlice.acquireUninterruptibly();
216            try
217            {
218                synchronized (slices)
219                {
220                    for (R slice : slices.values())
221                        slice.setFocused(value);
222                }
223            }
224            finally
225            {
226                modifyingSlice.release();
227            }
228        }
229        finally
230        {
231            endUpdate();
232        }
233    }
234
235    @Override
236    public void setSelected(boolean value)
237    {
238        beginUpdate();
239        try
240        {
241            super.setSelected(value);
242
243            modifyingSlice.acquireUninterruptibly();
244            try
245            {
246                synchronized (slices)
247                {
248                    for (R slice : slices.values())
249                        slice.setSelected(value);
250                }
251            }
252            finally
253            {
254                modifyingSlice.release();
255            }
256        }
257        finally
258        {
259            endUpdate();
260        }
261    }
262
263    @Override
264    public void setName(String value)
265    {
266        beginUpdate();
267        try
268        {
269            super.setName(value);
270
271            modifyingSlice.acquireUninterruptibly();
272            try
273            {
274                synchronized (slices)
275                {
276                    for (R slice : slices.values())
277                        slice.setName(value);
278                }
279            }
280            finally
281            {
282                modifyingSlice.release();
283            }
284        }
285        finally
286        {
287            endUpdate();
288        }
289    }
290
291    @Override
292    public void setT(int value)
293    {
294        beginUpdate();
295        try
296        {
297            super.setT(value);
298
299            modifyingSlice.acquireUninterruptibly();
300            try
301            {
302                synchronized (slices)
303                {
304                    for (R slice : slices.values())
305                        slice.setT(value);
306                }
307            }
308            finally
309            {
310                modifyingSlice.release();
311            }
312        }
313        finally
314        {
315            endUpdate();
316        }
317    }
318
319    @Override
320    public void setC(int value)
321    {
322        beginUpdate();
323        try
324        {
325            super.setC(value);
326
327            modifyingSlice.acquireUninterruptibly();
328            try
329            {
330                synchronized (slices)
331                {
332                    for (R slice : slices.values())
333                        slice.setC(value);
334                }
335            }
336            finally
337            {
338                modifyingSlice.release();
339            }
340        }
341        finally
342        {
343            endUpdate();
344        }
345    }
346
347    /**
348     * Returns <code>true</code> if the ROI stack is empty.
349     */
350    @Override
351    public boolean isEmpty()
352    {
353        return slices.isEmpty();
354    }
355
356    /**
357     * @return The size of this ROI stack along Z.<br>
358     *         Note that the returned value indicates the difference between upper and lower bounds
359     *         of this ROI, but doesn't guarantee that all slices in-between exist ( {@link #getSlice(int)} may still
360     *         return <code>null</code>.<br>
361     */
362    public int getSizeZ()
363    {
364        synchronized (slices)
365        {
366            if (slices.isEmpty())
367                return 0;
368
369            return (slices.lastKey().intValue() - slices.firstKey().intValue()) + 1;
370        }
371    }
372
373    /**
374     * Returns the ROI slice at given Z position.
375     */
376    public R getSlice(int z)
377    {
378        return slices.get(Integer.valueOf(z));
379    }
380
381    /**
382     * Returns the ROI slice at given Z position.
383     */
384    public R getSlice(int z, boolean createIfNull)
385    {
386        R result = getSlice(z);
387
388        if ((result == null) && createIfNull)
389        {
390            result = createSlice();
391            if (result != null)
392                setSlice(z, result);
393        }
394
395        return result;
396    }
397
398    /**
399     * Sets the ROI slice for the given Z position.
400     */
401    public void setSlice(int z, R roi2d)
402    {
403        // nothing to do
404        if (getSlice(z) == roi2d)
405            return;
406
407        // remove previous
408        removeSlice(z);
409
410        if (roi2d != null)
411        {
412            roi2d.beginUpdate();
413            try
414            {
415                // set Z, T and C position
416                roi2d.setZ(z);
417                roi2d.setT(getT());
418                roi2d.setC(getC());
419            }
420            finally
421            {
422                roi2d.endUpdate();
423            }
424
425            // listen events from this ROI and its overlay
426            roi2d.addListener(this);
427            roi2d.getOverlay().addOverlayListener(this);
428
429            synchronized (slices)
430            {
431                // set new slice
432                slices.put(Integer.valueOf(z), roi2d);
433            }
434        }
435
436        // notify ROI changed
437        roiChanged(true);
438    }
439
440    /**
441     * Removes slice at the given Z position and returns it.
442     */
443    public R removeSlice(int z)
444    {
445        final R result;
446
447        synchronized (slices)
448        {
449            // remove the current slice (if any)
450            result = slices.remove(Integer.valueOf(z));
451        }
452
453        // remove listeners
454        if (result != null)
455        {
456            result.removeListener(this);
457            result.getOverlay().removeOverlayListener(this);
458
459            // notify ROI changed
460            roiChanged(true);
461        }
462
463        return result;
464    }
465
466    /**
467     * Removes all slices.
468     */
469    public void clear()
470    {
471        // nothing to do
472        if (isEmpty())
473            return;
474
475        synchronized (slices)
476        {
477            for (R slice : slices.values())
478            {
479                slice.removeListener(this);
480                slice.getOverlay().removeOverlayListener(this);
481            }
482
483            slices.clear();
484        }
485
486        roiChanged(true);
487    }
488
489    /**
490     * Add the specified {@link ROI3DStack} content to this ROI3DStack
491     */
492    public void add(ROI3DStack<R> roi) throws UnsupportedOperationException
493    {
494        beginUpdate();
495        try
496        {
497            synchronized (slices)
498            {
499                for (Entry<Integer, R> entry : roi.slices.entrySet())
500                    add(entry.getKey().intValue(), entry.getValue());
501            }
502        }
503        finally
504        {
505            endUpdate();
506        }
507    }
508
509    /**
510     * Exclusively add the specified {@link ROI3DStack} content to this ROI3DStack
511     */
512    public void exclusiveAdd(ROI3DStack<R> roi) throws UnsupportedOperationException
513    {
514        beginUpdate();
515        try
516        {
517            synchronized (slices)
518            {
519                for (Entry<Integer, R> entry : roi.slices.entrySet())
520                    exclusiveAdd(entry.getKey().intValue(), entry.getValue());
521            }
522        }
523        finally
524        {
525            endUpdate();
526        }
527    }
528
529    /**
530     * Process intersection of the specified {@link ROI3DStack} with this ROI3DStack.
531     */
532    public void intersect(ROI3DStack<R> roi) throws UnsupportedOperationException
533    {
534        beginUpdate();
535        try
536        {
537            synchronized (slices)
538            {
539                final Set<Integer> keys = roi.slices.keySet();
540                final Set<Integer> toRemove = new HashSet<Integer>();
541
542                // remove slices which are not contained
543                for (Integer key : slices.keySet())
544                    if (!keys.contains(key))
545                        toRemove.add(key);
546
547                // do remove first
548                for (Integer key : toRemove)
549                    removeSlice(key.intValue());
550
551                // then process intersection
552                for (Entry<Integer, R> entry : roi.slices.entrySet())
553                    intersect(entry.getKey().intValue(), entry.getValue());
554            }
555        }
556        finally
557        {
558            endUpdate();
559        }
560    }
561
562    /**
563     * Remove the specified {@link ROI3DStack} from this ROI3DStack
564     */
565    public void subtract(ROI3DStack<R> roi) throws UnsupportedOperationException
566    {
567        beginUpdate();
568        try
569        {
570            synchronized (slices)
571            {
572                for (Entry<Integer, R> entry : roi.slices.entrySet())
573                    subtract(entry.getKey().intValue(), entry.getValue());
574            }
575        }
576        finally
577        {
578            endUpdate();
579        }
580    }
581
582    @Override
583    public ROI add(ROI roi, boolean allowCreate) throws UnsupportedOperationException
584    {
585        if (roi instanceof ROI3D)
586        {
587            final ROI3D roi3d = (ROI3D) roi;
588
589            // only if on same position
590            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
591            {
592                if (this.getClass().isInstance(roi3d))
593                {
594                    add((ROI3DStack) roi3d);
595                    return this;
596                }
597            }
598        }
599        else if (roiClass.isInstance(roi))
600        {
601            final ROI2D roi2d = (ROI2D) roi;
602
603            // only if on same position
604            if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
605            {
606                try
607                {
608                    add(roi2d.getZ(), (R) roi2d);
609                    return this;
610                }
611                catch (UnsupportedOperationException e)
612                {
613                    // not supported, try generic method instead
614                    return super.add(roi, allowCreate);
615                }
616            }
617        }
618
619        return super.add(roi, allowCreate);
620    }
621
622    @Override
623    public ROI intersect(ROI roi, boolean allowCreate) throws UnsupportedOperationException
624    {
625        if (roi instanceof ROI3D)
626        {
627            final ROI3D roi3d = (ROI3D) roi;
628
629            // only if on same position
630            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
631            {
632                if (this.getClass().isInstance(roi3d))
633                {
634                    intersect((ROI3DStack) roi3d);
635                    return this;
636                }
637            }
638            else if (roiClass.isInstance(roi))
639            {
640                final ROI2D roi2d = (ROI2D) roi;
641
642                // only if on same position
643                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
644                {
645                    try
646                    {
647                        intersect(roi2d.getZ(), (R) roi2d);
648                        return this;
649                    }
650                    catch (UnsupportedOperationException e)
651                    {
652                        // not supported, try generic method instead
653                        return super.intersect(roi, allowCreate);
654                    }
655                }
656            }
657        }
658
659        return super.intersect(roi, allowCreate);
660    }
661
662    @Override
663    public ROI exclusiveAdd(ROI roi, boolean allowCreate) throws UnsupportedOperationException
664    {
665        if (roi instanceof ROI3D)
666        {
667            final ROI3D roi3d = (ROI3D) roi;
668
669            // only if on same position
670            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
671            {
672                if (this.getClass().isInstance(roi3d))
673                {
674                    exclusiveAdd((ROI3DStack) roi3d);
675                    return this;
676                }
677            }
678            else if (roiClass.isInstance(roi))
679            {
680                final ROI2D roi2d = (ROI2D) roi;
681
682                // only if on same position
683                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
684                {
685                    try
686                    {
687                        exclusiveAdd(roi2d.getZ(), (R) roi2d);
688                        return this;
689                    }
690                    catch (UnsupportedOperationException e)
691                    {
692                        // not supported, try generic method instead
693                        return super.add(roi, allowCreate);
694                    }
695                }
696            }
697        }
698
699        return super.add(roi, allowCreate);
700    }
701
702    @Override
703    public ROI subtract(ROI roi, boolean allowCreate) throws UnsupportedOperationException
704    {
705        if (roi instanceof ROI3D)
706        {
707            final ROI3D roi3d = (ROI3D) roi;
708
709            // only if on same position
710            if ((getT() == roi3d.getT()) && (getC() == roi3d.getC()))
711            {
712                if (this.getClass().isInstance(roi3d))
713                {
714                    subtract((ROI3DStack<R>) roi3d);
715                    return this;
716                }
717            }
718            else if (roiClass.isInstance(roi))
719            {
720                final ROI2D roi2d = (ROI2D) roi;
721
722                // only if on same position
723                if ((roi2d.getZ() != -1) && (getT() == roi2d.getT()) && (getC() == roi2d.getC()))
724                {
725                    try
726                    {
727                        subtract(roi2d.getZ(), (R) roi2d);
728                        return this;
729                    }
730                    catch (UnsupportedOperationException e)
731                    {
732                        // not supported, try generic method instead
733                        return super.subtract(roi, allowCreate);
734                    }
735                }
736            }
737        }
738
739        return super.subtract(roi, allowCreate);
740    }
741
742    /**
743     * Adds content of specified <code>ROI</code> slice into the <code>ROI</code> slice at given Z position.
744     * The resulting content of this <code>ROI</code> will include the union of both ROI's contents.<br>
745     * If no slice was present at the specified Z position then the method is equivalent to
746     * {@link #setSlice(int, ROI2D)}
747     * 
748     * @param z
749     *        the position where the slice must be merged
750     * @param roiSlice
751     *        the 2D ROI to merge
752     * @throws UnsupportedOperationException
753     *         if the given ROI slice cannot be added to this ROI
754     */
755    public void add(int z, R roiSlice)
756    {
757        if (roiSlice == null)
758            return;
759
760        final R currentSlice = getSlice(z);
761        final ROI newSlice;
762
763        // merge both slice
764        if (currentSlice != null)
765        {
766            // we need to modify the Z, T and C position so we do the merge correctly
767            roiSlice.setZ(z);
768            roiSlice.setT(getT());
769            roiSlice.setC(getC());
770            // do ROI union
771            newSlice = currentSlice.add(roiSlice, true);
772
773            // check the resulting ROI is the same type
774            if (!newSlice.getClass().isInstance(currentSlice))
775                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
776                        + ": " + newSlice.getClassName());
777        }
778        else
779            // get a copy
780            newSlice = roiSlice.getCopy();
781
782        // set slice
783        setSlice(z, (R) newSlice);
784    }
785
786    /**
787     * Sets the content of the <code>ROI</code> slice at given Z position to be the union of its current content and the
788     * content of the specified <code>ROI</code>, minus their intersection.
789     * The resulting <code>ROI</code> will include only content that were contained in either this <code>ROI</code> or
790     * in the specified <code>ROI</code>, but not in both.<br>
791     * If no slice was present at the specified Z position then the method is equivalent to
792     * {@link #setSlice(int, ROI2D)}
793     * 
794     * @param z
795     *        the position where the slice must be merged
796     * @param roiSlice
797     *        the 2D ROI to merge
798     * @throws UnsupportedOperationException
799     *         if the given ROI slice cannot be exclusively added to this ROI
800     */
801    public void exclusiveAdd(int z, R roiSlice)
802    {
803        if (roiSlice == null)
804            return;
805
806        final R currentSlice = getSlice(z);
807        final ROI newSlice;
808
809        // merge both slice
810        if (currentSlice != null)
811        {
812            // we need to modify the Z, T and C position so we do the merge correctly
813            roiSlice.setZ(z);
814            roiSlice.setT(getT());
815            roiSlice.setC(getC());
816            // do ROI exclusive union
817            newSlice = currentSlice.exclusiveAdd(roiSlice, true);
818
819            // check the resulting ROI is same type
820            if (!newSlice.getClass().isInstance(currentSlice))
821                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
822                        + ": " + newSlice.getClassName());
823        }
824        else
825            // get a copy
826            newSlice = roiSlice.getCopy();
827
828        if (newSlice.isEmpty())
829            removeSlice(z);
830        else
831            setSlice(z, (R) newSlice);
832    }
833
834    /**
835     * Sets the content of the <code>ROI</code> slice at given Z position to the intersection of
836     * its current content and the content of the specified <code>ROI</code>.
837     * The resulting ROI will include only contents that were contained in both ROI.<br>
838     * If no slice was present at the specified Z position then the method does nothing.
839     * 
840     * @param z
841     *        the position where the slice must be merged
842     * @param roiSlice
843     *        the 2D ROI to merge
844     * @throws UnsupportedOperationException
845     *         if the given ROI slice cannot be intersected with this ROI
846     */
847    public void intersect(int z, R roiSlice)
848    {
849        // better to throw an exception here than removing slice
850        if (roiSlice == null)
851            throw new IllegalArgumentException("Cannot intersect an empty slice in a 3D ROI");
852
853        final R currentSlice = getSlice(z);
854
855        // merge both slice
856        if (currentSlice != null)
857        {
858            // we need to modify the Z, T and C position so we do the merge correctly
859            roiSlice.setZ(z);
860            roiSlice.setT(getT());
861            roiSlice.setC(getC());
862            // do ROI intersection
863            final ROI newSlice = currentSlice.intersect(roiSlice, true);
864
865            // check the resulting ROI is same type
866            if (!newSlice.getClass().isInstance(currentSlice))
867                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
868                        + ": " + newSlice.getClassName());
869
870            if (newSlice.isEmpty())
871                removeSlice(z);
872            else
873                setSlice(z, (R) newSlice);
874        }
875    }
876
877    /**
878     * Subtract the specified <code>ROI</code> content from the <code>ROI</code> slice at given Z position.<br>
879     * If no slice was present at the specified Z position then the method does nothing.
880     * 
881     * @param z
882     *        the position where the subtraction should be done
883     * @param roiSlice
884     *        the 2D ROI to subtract from Z slice
885     * @throws UnsupportedOperationException
886     *         if the given ROI slice cannot be subtracted from this ROI
887     */
888    public void subtract(int z, R roiSlice) throws UnsupportedOperationException
889    {
890        if (roiSlice == null)
891            return;
892
893        final R currentSlice = getSlice(z);
894
895        // merge both slice
896        if (currentSlice != null)
897        {
898            // we need to modify the Z, T and C position so we do the merge correctly
899            roiSlice.setZ(z);
900            roiSlice.setT(getT());
901            roiSlice.setC(getC());
902            // do ROI subtraction
903            final ROI newSlice = currentSlice.subtract(roiSlice, true);
904
905            // check the resulting ROI is same type
906            if (!newSlice.getClass().isInstance(currentSlice))
907                throw new UnsupportedOperationException("Can't add the result of the merge operation on 2D slice " + z
908                        + ": " + newSlice.getClassName());
909
910            if (newSlice.isEmpty())
911                removeSlice(z);
912            else
913                setSlice(z, (R) newSlice);
914        }
915    }
916
917    /**
918     * Called when a ROI slice has changed.
919     */
920    protected void sliceChanged(ROIEvent event)
921    {
922        if (modifyingSlice.availablePermits() <= 0)
923            return;
924
925        final ROI source = event.getSource();
926
927        switch (event.getType())
928        {
929            case ROI_CHANGED:
930                // position change of a slice can change global bounds --> transform to 'content changed' event type
931                roiChanged(true);
932                // roiChanged(StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL));
933                break;
934
935            case FOCUS_CHANGED:
936                setFocused(source.isFocused());
937                break;
938
939            case SELECTION_CHANGED:
940                setSelected(source.isSelected());
941                break;
942
943            case PROPERTY_CHANGED:
944                final String propertyName = event.getPropertyName();
945
946                if ((propertyName == null) || propertyName.equals(PROPERTY_READONLY))
947                    setReadOnly(source.isReadOnly());
948                if ((propertyName == null) || propertyName.equals(PROPERTY_CREATING))
949                    setCreating(source.isCreating());
950                break;
951        }
952    }
953
954    /**
955     * Called when a ROI slice overlay has changed.
956     */
957    protected void sliceOverlayChanged(OverlayEvent event)
958    {
959        switch (event.getType())
960        {
961            case PAINTER_CHANGED:
962                // forward the event to ROI stack overlay
963                getOverlay().painterChanged();
964                break;
965
966            case PROPERTY_CHANGED:
967                // forward the event to ROI stack overlay
968                getOverlay().propertyChanged(event.getPropertyName());
969                break;
970        }
971    }
972
973    @Override
974    public Rectangle3D computeBounds3D()
975    {
976        Rectangle2D xyBounds = null;
977
978        synchronized (slices)
979        {
980            for (R slice : slices.values())
981            {
982                final Rectangle2D bnd2d = slice.getBounds2D();
983
984                // only add non empty bounds
985                if (!bnd2d.isEmpty())
986                {
987                    if (xyBounds == null)
988                        xyBounds = (Rectangle2D) bnd2d.clone();
989                    else
990                        xyBounds.add(bnd2d);
991                }
992            }
993        }
994
995        // create empty 2D bounds
996        if (xyBounds == null)
997            xyBounds = new Rectangle2D.Double();
998
999        final int z;
1000        final int sizeZ;
1001
1002        if (!slices.isEmpty())
1003        {
1004            z = slices.firstKey().intValue();
1005            sizeZ = getSizeZ();
1006        }
1007        else
1008        {
1009            z = 0;
1010            sizeZ = 0;
1011        }
1012
1013        return new Rectangle3D.Double(xyBounds.getX(), xyBounds.getY(), z, xyBounds.getWidth(), xyBounds.getHeight(),
1014                sizeZ);
1015    }
1016
1017    @Override
1018    public boolean contains(double x, double y, double z)
1019    {
1020        final R roi2d = getSlice((int) Math.floor(z));
1021
1022        if (roi2d != null)
1023            return roi2d.contains(x, y);
1024
1025        return false;
1026    }
1027
1028    @Override
1029    public boolean contains(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
1030    {
1031        final Rectangle3D bounds = getBounds3D();
1032
1033        // easy discard
1034        if (!bounds.contains(x, y, z, sizeX, sizeY, sizeZ))
1035            return false;
1036
1037        final int lim = (int) Math.floor(z + sizeZ);
1038        for (int zc = (int) Math.floor(z); zc < lim; zc++)
1039        {
1040            final R roi2d = getSlice(zc);
1041            if ((roi2d == null) || !roi2d.contains(x, y, sizeX, sizeY))
1042                return false;
1043        }
1044
1045        return true;
1046    }
1047
1048    @Override
1049    public boolean intersects(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
1050    {
1051        final Rectangle3D bounds = getBounds3D();
1052
1053        // easy discard
1054        if (!bounds.intersects(x, y, z, sizeX, sizeY, sizeZ))
1055            return false;
1056
1057        final int lim = (int) Math.floor(z + sizeZ);
1058        for (int zc = (int) Math.floor(z); zc < lim; zc++)
1059        {
1060            final R roi2d = getSlice(zc);
1061            if ((roi2d != null) && roi2d.intersects(x, y, sizeX, sizeY))
1062                return true;
1063        }
1064
1065        return false;
1066    }
1067
1068    @Override
1069    public boolean hasSelectedPoint()
1070    {
1071        // default
1072        return false;
1073    }
1074
1075    @Override
1076    public void unselectAllPoints()
1077    {
1078        beginUpdate();
1079        try
1080        {
1081            modifyingSlice.acquireUninterruptibly();
1082            try
1083            {
1084                synchronized (slices)
1085                {
1086                    for (R slice : slices.values())
1087                        slice.unselectAllPoints();
1088                }
1089            }
1090            finally
1091            {
1092                modifyingSlice.release();
1093            }
1094        }
1095        finally
1096        {
1097            endUpdate();
1098        }
1099    }
1100
1101    // default approximated implementation for ROI3DStack
1102    @Override
1103    public double computeSurfaceArea(Sequence sequence) throws UnsupportedOperationException
1104    {
1105        // 3D contour points = first slice points + all slices perimeter + last slice points
1106        double result = 0;
1107
1108        synchronized (slices)
1109        {
1110            if (!slices.isEmpty())
1111            {
1112                final double psx = sequence.getPixelSizeX();
1113                final double psy = sequence.getPixelSizeY();
1114                final double psz = sequence.getPixelSizeZ();
1115
1116                result = slices.firstEntry().getValue().getNumberOfPoints() * psx * psy;
1117                result += slices.lastEntry().getValue().getNumberOfPoints() * psx * psy;
1118
1119                for (R slice : slices.values())
1120                    result += slice.getLength(sequence) * psz;
1121            }
1122        }
1123
1124        return result;
1125    }
1126
1127    // default approximated implementation for ROI3DStack
1128    @Override
1129    public double computeNumberOfContourPoints()
1130    {
1131        // 3D contour points = first slice points + inter slices contour points + last slice points
1132        double result = 0;
1133
1134        synchronized (slices)
1135        {
1136            if (slices.size() <= 2)
1137            {
1138                for (R slice : slices.values())
1139                    result += slice.getNumberOfPoints();
1140            }
1141            else
1142            {
1143                final Entry<Integer, R> firstEntry = slices.firstEntry();
1144                final Entry<Integer, R> lastEntry = slices.lastEntry();
1145                final Integer firstKey = firstEntry.getKey();
1146                final Integer lastKey = lastEntry.getKey();
1147
1148                result = firstEntry.getValue().getNumberOfPoints();
1149
1150                for (R slice : slices.subMap(firstKey, false, lastKey, false).values())
1151                    result += slice.getNumberOfContourPoints();
1152
1153                result += lastEntry.getValue().getNumberOfPoints();
1154            }
1155        }
1156
1157        return result;
1158    }
1159
1160    @Override
1161    public double computeNumberOfPoints()
1162    {
1163        double volume = 0;
1164
1165        synchronized (slices)
1166        {
1167            for (R slice : slices.values())
1168                volume += slice.getNumberOfPoints();
1169        }
1170
1171        return volume;
1172    }
1173
1174    @Override
1175    public boolean canTranslate()
1176    {
1177        synchronized (slices)
1178        {
1179            // only need to test the first entry
1180            if (!slices.isEmpty())
1181                return slices.firstEntry().getValue().canTranslate();
1182        }
1183
1184        return false;
1185    }
1186
1187    /**
1188     * Translate the stack of specified Z position.
1189     */
1190    public void translate(int z)
1191    {
1192        // easy optimizations
1193        if ((z == 0) || isEmpty())
1194            return;
1195
1196        synchronized (slices)
1197        {
1198            final Map<Integer, R> map = new HashMap<Integer, R>(slices);
1199
1200            slices.clear();
1201            for (Entry<Integer, R> entry : map.entrySet())
1202            {
1203                final R roi = entry.getValue();
1204                final int newZ = roi.getZ() + z;
1205
1206                // // only positive value accepted
1207                // if (newZ >= 0)
1208                // {
1209                roi.setZ(newZ);
1210                slices.put(Integer.valueOf(newZ), roi);
1211                // }
1212            }
1213        }
1214
1215        // notify ROI changed
1216        roiChanged(false);
1217    }
1218
1219    @Override
1220    public void translate(double dx, double dy, double dz)
1221    {
1222        beginUpdate();
1223        try
1224        {
1225            translateZ += dz;
1226            // convert to integer
1227            final int dzi = (int) translateZ;
1228            // keep trace of not used floating part
1229            translateZ -= dzi;
1230
1231            translate(dzi);
1232
1233            modifyingSlice.acquireUninterruptibly();
1234            try
1235            {
1236                synchronized (slices)
1237                {
1238                    for (R slice : slices.values())
1239                        slice.translate(dx, dy);
1240                }
1241            }
1242            finally
1243            {
1244                modifyingSlice.release();
1245            }
1246
1247            // notify ROI changed because we modified slice 'internally'
1248            if ((dx != 0d) || (dy != 0d))
1249                roiChanged(false);
1250        }
1251        finally
1252        {
1253            endUpdate();
1254        }
1255    }
1256
1257    @Override
1258    public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, boolean inclusive)
1259    {
1260        final R roi2d = getSlice(z);
1261
1262        if (roi2d != null)
1263            return roi2d.getBooleanMask(x, y, width, height, inclusive);
1264
1265        return new boolean[width * height];
1266    }
1267
1268    @Override
1269    public BooleanMask2D getBooleanMask2D(int z, boolean inclusive)
1270    {
1271        final R roi2d = getSlice(z);
1272
1273        if (roi2d != null)
1274            return roi2d.getBooleanMask(inclusive);
1275
1276        return new BooleanMask2D(new Rectangle(), new boolean[0]);
1277    }
1278
1279    // called when one of the slice ROI changed
1280    @Override
1281    public void roiChanged(ROIEvent event)
1282    {
1283        // propagate children change event
1284        sliceChanged(event);
1285    }
1286
1287    // called when one of the slice ROI overlay changed
1288    @Override
1289    public void overlayChanged(OverlayEvent event)
1290    {
1291        // propagate children overlay change event
1292        sliceOverlayChanged(event);
1293    }
1294
1295    @Override
1296    public Iterator<R> iterator()
1297    {
1298        return slices.values().iterator();
1299    }
1300
1301    @Override
1302    public boolean loadFromXML(Node node)
1303    {
1304        beginUpdate();
1305        try
1306        {
1307            if (!super.loadFromXML(node))
1308                return false;
1309
1310            // we don't need to save the 2D ROI class as the parent class already do it
1311            clear();
1312
1313            for (Element e : XMLUtil.getElements(node, "slice"))
1314            {
1315                // faster than using complete XML serialization
1316                final R slice = createSlice();
1317
1318                // error while reloading the ROI from XML
1319                if ((slice == null) || !slice.loadFromXML(e))
1320                    return false;
1321
1322                setSlice(slice.getZ(), slice);
1323            }
1324        }
1325        finally
1326        {
1327            endUpdate();
1328        }
1329
1330        return true;
1331    }
1332
1333    @Override
1334    public boolean saveToXML(Node node)
1335    {
1336        if (!super.saveToXML(node))
1337            return false;
1338
1339        synchronized (slices)
1340        {
1341            for (R slice : slices.values())
1342            {
1343                Element sliceNode = XMLUtil.addElement(node, "slice");
1344
1345                if (!slice.saveToXML(sliceNode))
1346                    return false;
1347            }
1348        }
1349
1350        return true;
1351    }
1352
1353    public class ROI3DStackPainter extends ROI3DPainter
1354    {
1355        protected ROIPainter getSliceOverlayForCanvas(IcyCanvas canvas)
1356        {
1357            final int z = canvas.getPositionZ();
1358
1359            // canvas position of -1 mean 3D canvas (all Z visible)
1360            if (z >= 0)
1361                return getSliceOverlay(z);
1362
1363            return null;
1364        }
1365
1366        /**
1367         * Returns the ROI overlay at given Z position.
1368         */
1369        protected ROIPainter getSliceOverlay(int z)
1370        {
1371            R roi = getSlice(z);
1372
1373            if (roi != null)
1374                return roi.getOverlay();
1375
1376            return null;
1377        }
1378
1379        /**
1380         * @deprecated this property does not exist anymore (always return <code>false</code>)
1381         */
1382        @Deprecated
1383        public boolean getUseChildColor()
1384        {
1385            return false;
1386        }
1387
1388        /**
1389         * @deprecated this property does not exist anymore
1390         */
1391        @Deprecated
1392        public void setUseChildColor(boolean value)
1393        {
1394            //
1395        }
1396
1397        /**
1398         * Set the painter color for the specified ROI slice.
1399         * 
1400         * @see #setUseChildColor(boolean)
1401         */
1402        public void setColor(int z, Color value)
1403        {
1404            final ROIPainter sliceOverlay = getSliceOverlay(z);
1405
1406            if (sliceOverlay != null)
1407            {
1408                modifyingSlice.acquireUninterruptibly();
1409                try
1410                {
1411                    sliceOverlay.setColor(value);
1412                }
1413                finally
1414                {
1415                    modifyingSlice.release();
1416                }
1417            }
1418        }
1419
1420        @Override
1421        public void setColor(Color value)
1422        {
1423            beginUpdate();
1424            try
1425            {
1426                super.setColor(value);
1427
1428                if (!getUseChildColor())
1429                {
1430                    modifyingSlice.acquireUninterruptibly();
1431                    try
1432                    {
1433                        synchronized (slices)
1434                        {
1435                            for (R slice : slices.values())
1436                                slice.getOverlay().setColor(value);
1437                        }
1438                    }
1439                    finally
1440                    {
1441                        modifyingSlice.release();
1442                    }
1443                }
1444            }
1445            finally
1446            {
1447                endUpdate();
1448            }
1449        }
1450
1451        @Override
1452        public void setOpacity(float value)
1453        {
1454            beginUpdate();
1455            try
1456            {
1457                super.setOpacity(value);
1458
1459                modifyingSlice.acquireUninterruptibly();
1460                try
1461                {
1462                    synchronized (slices)
1463                    {
1464                        for (R slice : slices.values())
1465                            slice.getOverlay().setOpacity(value);
1466                    }
1467                }
1468                finally
1469                {
1470                    modifyingSlice.release();
1471                }
1472            }
1473            finally
1474            {
1475                endUpdate();
1476            }
1477        }
1478
1479        @Override
1480        public void setStroke(double value)
1481        {
1482            beginUpdate();
1483            try
1484            {
1485                super.setStroke(value);
1486
1487                modifyingSlice.acquireUninterruptibly();
1488                try
1489                {
1490                    synchronized (slices)
1491                    {
1492                        for (R slice : slices.values())
1493                            slice.getOverlay().setStroke(value);
1494                    }
1495                }
1496                finally
1497                {
1498                    modifyingSlice.release();
1499                }
1500            }
1501            finally
1502            {
1503                endUpdate();
1504            }
1505        }
1506
1507        @Override
1508        public void setShowName(boolean value)
1509        {
1510            beginUpdate();
1511            try
1512            {
1513                super.setShowName(value);
1514
1515                modifyingSlice.acquireUninterruptibly();
1516                try
1517                {
1518                    synchronized (slices)
1519                    {
1520                        for (R slice : slices.values())
1521                            slice.getOverlay().setShowName(value);
1522                    }
1523                }
1524                finally
1525                {
1526                    modifyingSlice.release();
1527                }
1528            }
1529            finally
1530            {
1531                endUpdate();
1532            }
1533        }
1534
1535        @Override
1536        public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas)
1537        {
1538            // 2D canvas --> use slice implementation
1539            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1540            {
1541                // forward event to current slice
1542                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1543
1544                if (sliceOverlay != null)
1545                    sliceOverlay.paint(g, sequence, canvas);
1546            }
1547            // use default parent implementation
1548            else
1549                super.paint(g, sequence, canvas);
1550        }
1551
1552        @Override
1553        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1554        {
1555            // 2D canvas --> use slice implementation
1556            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1557            {
1558                // forward event to current slice
1559                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1560
1561                if (sliceOverlay != null)
1562                    sliceOverlay.keyPressed(e, imagePoint, canvas);
1563            }
1564            // use default parent implementation
1565            else
1566                super.keyPressed(e, imagePoint, canvas);
1567        }
1568
1569        @Override
1570        public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1571        {
1572            // 2D canvas --> use slice implementation
1573            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1574            {
1575                // forward event to current slice
1576                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1577
1578                if (sliceOverlay != null)
1579                    sliceOverlay.keyReleased(e, imagePoint, canvas);
1580            }
1581            // use default parent implementation
1582            else
1583                super.keyReleased(e, imagePoint, canvas);
1584        }
1585
1586        @Override
1587        public void mouseEntered(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1588        {
1589            // 2D canvas --> use slice implementation
1590            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1591            {
1592                // forward event to current slice
1593                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1594
1595                if (sliceOverlay != null)
1596                    sliceOverlay.mouseEntered(e, imagePoint, canvas);
1597            }
1598            // use default parent implementation
1599            else
1600                super.mouseEntered(e, imagePoint, canvas);
1601        }
1602
1603        @Override
1604        public void mouseExited(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1605        {
1606            // 2D canvas --> use slice implementation
1607            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1608            {
1609                // forward event to current slice
1610                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1611
1612                if (sliceOverlay != null)
1613                    sliceOverlay.mouseExited(e, imagePoint, canvas);
1614            }
1615            // use default parent implementation
1616            else
1617                super.mouseExited(e, imagePoint, canvas);
1618        }
1619
1620        @Override
1621        public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1622        {
1623            // 2D canvas --> use slice implementation
1624            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1625            {
1626                // forward event to current slice
1627                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1628
1629                if (sliceOverlay != null)
1630                    sliceOverlay.mouseMove(e, imagePoint, canvas);
1631            }
1632            // use default parent implementation
1633            else
1634                super.mouseMove(e, imagePoint, canvas);
1635        }
1636
1637        @Override
1638        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1639        {
1640            // 2D canvas --> use slice implementation
1641            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1642            {
1643                // forward event to current slice
1644                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1645
1646                if (sliceOverlay != null)
1647                    sliceOverlay.mouseDrag(e, imagePoint, canvas);
1648            }
1649            // use default parent implementation
1650            else
1651                super.mouseDrag(e, imagePoint, canvas);
1652        }
1653
1654        @Override
1655        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1656        {
1657            // 2D canvas --> use slice implementation
1658            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1659            {
1660                // forward event to current slice
1661                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1662
1663                if (sliceOverlay != null)
1664                    sliceOverlay.mousePressed(e, imagePoint, canvas);
1665            }
1666            // use default parent implementation
1667            else
1668                super.mousePressed(e, imagePoint, canvas);
1669        }
1670
1671        @Override
1672        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1673        {
1674            // 2D canvas --> use slice implementation
1675            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1676            {
1677                // forward event to current slice
1678                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1679
1680                if (sliceOverlay != null)
1681                    sliceOverlay.mouseReleased(e, imagePoint, canvas);
1682            }
1683            // use default parent implementation
1684            else
1685                super.mouseReleased(e, imagePoint, canvas);
1686        }
1687
1688        @Override
1689        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1690        {
1691            // 2D canvas --> use slice implementation
1692            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1693            {
1694                // forward event to current slice
1695                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1696
1697                if (sliceOverlay != null)
1698                    sliceOverlay.mouseClick(e, imagePoint, canvas);
1699            }
1700            // use default parent implementation
1701            else
1702                super.mouseClick(e, imagePoint, canvas);
1703        }
1704
1705        @Override
1706        public void mouseWheelMoved(MouseWheelEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
1707        {
1708            // 2D canvas --> use slice implementation
1709            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1710            {
1711                // forward event to current slice
1712                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1713
1714                if (sliceOverlay != null)
1715                    sliceOverlay.mouseWheelMoved(e, imagePoint, canvas);
1716            }
1717            // use default parent implementation
1718            else
1719                super.mouseWheelMoved(e, imagePoint, canvas);
1720        }
1721
1722        @Override
1723        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
1724        {
1725            // 2D canvas --> use slice implementation if possible
1726            if ((canvas.getPositionZ() >= 0) && isActiveFor(canvas))
1727            {
1728                // forward event to current slice
1729                final ROIPainter sliceOverlay = getSliceOverlayForCanvas(canvas);
1730
1731                if (sliceOverlay instanceof ROI2DPainter)
1732                    ((ROI2DPainter) sliceOverlay).drawROI(g, sequence, canvas);
1733            }
1734
1735            // nothing to do...
1736        }
1737    }
1738}