001/**
002 * 
003 */
004package plugins.kernel.roi.roi3d;
005
006import java.awt.BasicStroke;
007import java.awt.Color;
008import java.awt.Composite;
009import java.awt.Graphics2D;
010import java.awt.event.InputEvent;
011import java.awt.event.KeyEvent;
012import java.awt.event.MouseEvent;
013import java.awt.geom.Line2D;
014import java.awt.geom.Rectangle2D;
015import java.lang.ref.WeakReference;
016import java.util.ArrayList;
017import java.util.Arrays;
018import java.util.HashSet;
019import java.util.List;
020import java.util.Set;
021
022import org.w3c.dom.Node;
023
024import icy.canvas.IcyCanvas;
025import icy.canvas.IcyCanvas2D;
026import icy.common.CollapsibleEvent;
027import icy.math.Line3DIterator;
028import icy.painter.Anchor3D;
029import icy.painter.Anchor3D.Anchor3DPositionListener;
030import icy.painter.OverlayEvent;
031import icy.painter.OverlayEvent.OverlayEventType;
032import icy.painter.OverlayListener;
033import icy.painter.VtkPainter;
034import icy.roi.ROI;
035import icy.roi.ROI3D;
036import icy.roi.ROIEvent;
037import icy.roi.edit.Point3DAddedROIEdit;
038import icy.roi.edit.Point3DMovedROIEdit;
039import icy.roi.edit.Point3DRemovedROIEdit;
040import icy.sequence.Sequence;
041import icy.system.thread.ThreadUtil;
042import icy.type.geom.Line3D;
043import icy.type.geom.Shape3D;
044import icy.type.point.Point3D;
045import icy.type.point.Point5D;
046import icy.type.rectangle.Rectangle3D;
047import icy.util.EventUtil;
048import icy.util.GraphicsUtil;
049import icy.util.ShapeUtil;
050import icy.util.StringUtil;
051import icy.vtk.IcyVtkPanel;
052import icy.vtk.VtkUtil;
053import plugins.kernel.canvas.VtkCanvas;
054import vtk.vtkActor;
055import vtk.vtkCellArray;
056import vtk.vtkInformation;
057import vtk.vtkPoints;
058import vtk.vtkPolyData;
059import vtk.vtkPolyDataMapper;
060import vtk.vtkProp;
061import vtk.vtkProperty;
062import vtk.vtkRenderer;
063
064/**
065 * Base class for 3D shape ROI (working from 3D control points).
066 * 
067 * @author Stephane Dallongeville
068 */
069public class ROI3DShape extends ROI3D implements Shape3D
070{
071    public class ROI3DShapePainter extends ROI3DPainter implements Runnable
072    {
073        // VTK 3D objects
074        protected vtkPolyData outline;
075        protected vtkPolyDataMapper outlineMapper;
076        protected vtkActor outlineActor;
077        protected vtkInformation vtkInfo;
078        protected vtkCellArray vCells;
079        protected vtkPoints vPoints;
080        protected vtkPolyData polyData;
081        protected vtkPolyDataMapper polyMapper;
082        protected vtkActor actor;
083        // 3D internal
084        protected boolean needRebuild;
085        protected double scaling[];
086        protected WeakReference<VtkCanvas> canvas3d;
087        protected Set<Anchor3D> actorsToAdd;
088        protected Set<Anchor3D> actorsToRemove;
089
090        public ROI3DShapePainter()
091        {
092            super();
093
094            // don't create VTK object on constructor
095            outline = null;
096            outlineMapper = null;
097            outlineActor = null;
098            vtkInfo = null;
099            vCells = null;
100            vPoints = null;
101            polyData = null;
102            polyMapper = null;
103            actor = null;
104
105            scaling = new double[3];
106            Arrays.fill(scaling, 1d);
107
108            actorsToAdd = new HashSet<Anchor3D>();
109            actorsToRemove = new HashSet<Anchor3D>();
110
111            needRebuild = true;
112            canvas3d = new WeakReference<VtkCanvas>(null);
113        }
114
115        @Override
116        protected void finalize() throws Throwable
117        {
118            super.finalize();
119
120            // release allocated VTK resources
121            if (actor != null)
122                actor.Delete();
123            if (polyMapper != null)
124                polyMapper.Delete();
125            if (polyData != null)
126                polyData.Delete();
127            if (vPoints != null)
128                vPoints.Delete();
129            if (vCells != null)
130                vCells.Delete();
131            if (outlineActor != null)
132            {
133                outlineActor.SetPropertyKeys(null);
134                outlineActor.Delete();
135            }
136            if (vtkInfo != null)
137            {
138                vtkInfo.Remove(VtkCanvas.visibilityKey);
139                vtkInfo.Delete();
140            }
141            if (outlineMapper != null)
142                outlineMapper.Delete();
143            if (outline != null)
144            {
145                outline.GetPointData().GetScalars().Delete();
146                outline.GetPointData().Delete();
147                outline.Delete();
148            }
149        };
150
151        protected void initVtkObjects()
152        {
153            outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d);
154            outlineMapper = new vtkPolyDataMapper();
155            outlineMapper.SetInputData(outline);
156            outlineActor = new vtkActor();
157            outlineActor.SetMapper(outlineMapper);
158            // disable picking on the outline
159            outlineActor.SetPickable(0);
160            // and set it to wireframe representation
161            outlineActor.GetProperty().SetRepresentationToWireframe();
162            // use vtkInformations to store outline visibility state (hacky)
163            vtkInfo = new vtkInformation();
164            vtkInfo.Set(VtkCanvas.visibilityKey, 0);
165            // VtkCanvas use this to restore correctly outline visibility flag
166            outlineActor.SetPropertyKeys(vtkInfo);
167
168            // init poly data object
169            polyData = new vtkPolyData();
170            polyMapper = new vtkPolyDataMapper();
171            polyMapper.SetInputData(polyData);
172            actor = new vtkActor();
173            actor.SetMapper(polyMapper);
174
175            // initialize color and stroke
176            final Color col = getColor();
177            final double r = col.getRed() / 255d;
178            final double g = col.getGreen() / 255d;
179            final double b = col.getBlue() / 255d;
180
181            outlineActor.GetProperty().SetColor(r, g, b);
182            final vtkProperty property = actor.GetProperty();
183            property.SetPointSize(getStroke());
184            property.SetColor(r, g, b);
185        }
186
187        /**
188         * update 3D painter for 3D canvas (called only when VTK is loaded).
189         */
190        protected void rebuildVtkObjects()
191        {
192            final VtkCanvas canvas = canvas3d.get();
193            // canvas was closed
194            if (canvas == null)
195                return;
196
197            final IcyVtkPanel vtkPanel = canvas.getVtkPanel();
198            // canvas was closed
199            if (vtkPanel == null)
200                return;
201
202            final Sequence seq = canvas.getSequence();
203            // nothing to update
204            if (seq == null)
205                return;
206
207            // get scaling
208            final double xs = scaling[0];
209            final double ys = scaling[1];
210            final double zs = scaling[2];
211
212            // update polydata
213            final int numPts = controlPoints.size();
214            final double[][] vertices = new double[numPts][3];
215            final int[] indexes = new int[numPts + 1];
216            indexes[0] = numPts;
217
218            if (!controlPoints.isEmpty())
219            {
220                // add all controls point position
221                for (int i = 0; i < numPts; i++)
222                {
223                    final Point3D point = controlPoints.get(i).getPosition();
224                    final double[] vertex = vertices[i];
225
226                    vertex[0] = point.getX() * xs;
227                    vertex[1] = point.getY() * ys;
228                    vertex[2] = point.getZ() * zs;
229
230                    indexes[i + 1] = i;
231                }
232            }
233
234            final vtkCellArray previousCells = vCells;
235            final vtkPoints previousPoints = vPoints;
236            vCells = VtkUtil.getCells(1, indexes);
237            vPoints = VtkUtil.getPoints(vertices);
238
239            // get bounds
240            final Rectangle3D bounds = getBounds3D();
241
242            // actor can be accessed in canvas3d for rendering so we need to synchronize access
243            vtkPanel.lock();
244            try
245            {
246                // update outline data
247                VtkUtil.setOutlineBounds(outline, bounds.getMinX() * xs, bounds.getMaxX() * xs, bounds.getMinY() * ys,
248                        bounds.getMaxY() * ys, bounds.getMinZ() * zs, bounds.getMaxZ() * zs, canvas);
249                outlineMapper.Update();
250                // update polygon data from cell and points
251                polyData.SetPoints(vPoints);
252                polyData.SetLines(vCells);
253                polyMapper.Update();
254
255                // release previous allocated VTK objects
256                if (previousCells != null)
257                    previousCells.Delete();
258                if (previousPoints != null)
259                    previousPoints.Delete();
260            }
261            finally
262            {
263                vtkPanel.unlock();
264            }
265
266            // update color and others properties
267            updateVtkDisplayProperties();
268        }
269
270        protected void updateVtkDisplayProperties()
271        {
272            if (actor == null)
273                return;
274
275            final VtkCanvas cnv = canvas3d.get();
276            final vtkProperty vtkProperty = actor.GetProperty();
277            final Color col = getDisplayColor();
278            final double r = col.getRed() / 255d;
279            final double g = col.getGreen() / 255d;
280            final double b = col.getBlue() / 255d;
281            final double strk = getStroke();
282            // final float opacity = getOpacity();
283
284            final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null;
285
286            // we need to lock canvas as actor can be accessed during rendering
287            if (vtkPanel != null)
288                vtkPanel.lock();
289            try
290            {
291                // set actors color
292                outlineActor.GetProperty().SetColor(r, g, b);
293                if (isSelected())
294                {
295                    outlineActor.GetProperty().SetRepresentationToWireframe();
296                    outlineActor.SetVisibility(1);
297                    vtkInfo.Set(VtkCanvas.visibilityKey, 1);
298                }
299                else
300                {
301                    outlineActor.GetProperty().SetRepresentationToPoints();
302                    outlineActor.SetVisibility(0);
303                    vtkInfo.Set(VtkCanvas.visibilityKey, 0);
304                }
305                vtkProperty.SetColor(r, g, b);
306                vtkProperty.SetPointSize(strk);
307                vtkProperty.SetLineWidth(strk);
308                // opacity here is about ROI content, global opacity is handled by Layer
309                // vtkProperty.SetOpacity(opacity);
310                setVtkObjectsColor(col);
311            }
312            finally
313            {
314                if (vtkPanel != null)
315                    vtkPanel.unlock();
316            }
317
318            // need to repaint
319            painterChanged();
320        }
321
322        protected void setVtkObjectsColor(Color color)
323        {
324            if (outline != null)
325                VtkUtil.setPolyDataColor(outline, color, canvas3d.get());
326            if (polyData != null)
327                VtkUtil.setPolyDataColor(polyData, color, canvas3d.get());
328        }
329
330        @Override
331        protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas)
332        {
333            // specific VTK canvas processing
334            if (canvas instanceof VtkCanvas)
335            {
336                // mouse is over the ROI actor ? --> focus the ROI
337                final boolean focused = (actor != null) && (actor == ((VtkCanvas) canvas).getPickedObject());
338
339                setFocused(focused);
340
341                return focused;
342            }
343
344            return super.updateFocus(e, imagePoint, canvas);
345        }
346
347        @Override
348        public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
349        {
350            if (isSelected() && !isReadOnly())
351            {
352                if (isActiveFor(canvas))
353                {
354                    ROI3DShape.this.beginUpdate();
355                    try
356                    {
357                        // get control points list
358                        final List<Anchor3D> controlPoints = getControlPoints();
359
360                        // send event to controls points first
361                        for (Anchor3D pt : controlPoints)
362                            pt.keyPressed(e, imagePoint, canvas);
363
364                        // specific action for ROI3DPolyLine
365                        if (!e.isConsumed())
366                        {
367                            final Sequence sequence = canvas.getSequence();
368
369                            switch (e.getKeyCode())
370                            {
371                                case KeyEvent.VK_DELETE:
372                                case KeyEvent.VK_BACK_SPACE:
373                                    final Anchor3D selectedPoint = getSelectedPoint();
374
375                                    // try to remove selected point
376                                    if (removeSelectedPoint(canvas))
377                                    {
378                                        // consume event
379                                        e.consume();
380
381                                        // add undo operation
382                                        if (sequence != null)
383                                            sequence.addUndoableEdit(new Point3DRemovedROIEdit(ROI3DShape.this,
384                                                    controlPoints, selectedPoint));
385                                    }
386                                    break;
387                            }
388                        }
389                    }
390                    finally
391                    {
392                        ROI3DShape.this.endUpdate();
393                    }
394                }
395            }
396
397            // then send event to parent
398            super.keyPressed(e, imagePoint, canvas);
399        }
400
401        @Override
402        public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
403        {
404            if (isSelected() && !isReadOnly())
405            {
406                if (isActiveFor(canvas))
407                {
408                    // check we can do the action
409                    if (imagePoint != null)
410                    {
411                        ROI3DShape.this.beginUpdate();
412                        try
413                        {
414                            // send event to controls points first
415                            synchronized (controlPoints)
416                            {
417                                for (Anchor3D pt : controlPoints)
418                                    pt.keyReleased(e, imagePoint, canvas);
419                            }
420                        }
421                        finally
422                        {
423                            ROI3DShape.this.endUpdate();
424                        }
425                    }
426                }
427            }
428
429            // then send event to parent
430            super.keyReleased(e, imagePoint, canvas);
431        }
432
433        @Override
434        public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
435        {
436            if (isActiveFor(canvas))
437            {
438                // check we can do the action
439                if (isSelected() && !isReadOnly())
440                {
441                    ROI3DShape.this.beginUpdate();
442                    try
443                    {
444                        // send event to controls points first
445                        synchronized (controlPoints)
446                        {
447                            for (Anchor3D pt : controlPoints)
448                                pt.mousePressed(e, imagePoint, canvas);
449                        }
450
451                        // specific action for this ROI
452                        if (!e.isConsumed())
453                        {
454                            if (imagePoint != null)
455                            {
456                                // left button action
457                                if (EventUtil.isLeftMouseButton(e))
458                                {
459                                    // ROI should not be focused to add point (for multi selection)
460                                    if (!isFocused())
461                                    {
462                                        final boolean insertMode = EventUtil.isControlDown(e);
463
464                                        // insertion mode or creating the ROI ? --> add a new point
465                                        if (insertMode || isCreating())
466                                        {
467                                            // try to add point
468                                            final Anchor3D point = addNewPoint(imagePoint.toPoint3D(), insertMode);
469
470                                            // point added ?
471                                            if (point != null)
472                                            {
473                                                // consume event
474                                                e.consume();
475
476                                                final Sequence sequence = canvas.getSequence();
477
478                                                // add undo operation
479                                                if (sequence != null)
480                                                    sequence.addUndoableEdit(
481                                                            new Point3DAddedROIEdit(ROI3DShape.this, point));
482                                            }
483                                        }
484                                    }
485                                }
486                            }
487                        }
488                    }
489                    finally
490                    {
491                        ROI3DShape.this.endUpdate();
492                    }
493                }
494            }
495
496            // then send event to parent
497            super.mousePressed(e, imagePoint, canvas);
498        }
499
500        @Override
501        public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
502        {
503            // not anymore the first move
504            firstMove = false;
505
506            if (isSelected() && !isReadOnly())
507            {
508                // send event to controls points first
509                if (isActiveFor(canvas))
510                {
511                    final Sequence sequence = canvas.getSequence();
512
513                    ROI3DShape.this.beginUpdate();
514                    try
515                    {
516                        // default anchor action on mouse release
517                        synchronized (controlPoints)
518                        {
519                            for (Anchor3D pt : controlPoints)
520                                pt.mouseReleased(e, imagePoint, canvas);
521                        }
522                    }
523                    finally
524                    {
525                        ROI3DShape.this.endUpdate();
526                    }
527
528                    // prevent undo operation merging
529                    if (sequence != null)
530                        sequence.getUndoManager().noMergeForNextEdit();
531                }
532            }
533
534            // then send event to parent
535            super.mouseReleased(e, imagePoint, canvas);
536        }
537
538        @Override
539        public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
540        {
541            if (isSelected() && !isReadOnly())
542            {
543                // send event to controls points first
544                if (isActiveFor(canvas))
545                {
546                    ROI3DShape.this.beginUpdate();
547                    try
548                    {
549                        // default anchor action on mouse click
550                        synchronized (controlPoints)
551                        {
552                            for (Anchor3D pt : controlPoints)
553                                pt.mouseClick(e, imagePoint, canvas);
554                        }
555                    }
556                    finally
557                    {
558                        ROI3DShape.this.endUpdate();
559                    }
560                }
561            }
562
563            // then send event to parent
564            super.mouseClick(e, imagePoint, canvas);
565        }
566
567        @Override
568        public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
569        {
570            if (isActiveFor(canvas))
571            {
572                // check we can do the action
573                if (isSelected() && !isReadOnly())
574                {
575                    final Sequence sequence = canvas.getSequence();
576
577                    // send event to controls points first
578                    ROI3DShape.this.beginUpdate();
579                    try
580                    {
581                        // default anchor action on mouse drag
582                        synchronized (controlPoints)
583                        {
584                            for (Anchor3D pt : controlPoints)
585                            {
586                                final Point3D savedPosition;
587
588                                // don't want to undo position change on first creation movement
589                                if ((sequence != null) && (!isCreating() || !firstMove))
590                                    savedPosition = pt.getPosition();
591                                else
592                                    savedPosition = null;
593
594                                pt.mouseDrag(e, imagePoint, canvas);
595
596                                // position changed and undo supported --> add undo operation
597                                if ((sequence != null) && (savedPosition != null)
598                                        && !savedPosition.equals(pt.getPosition()))
599                                    sequence.addUndoableEdit(
600                                            new Point3DMovedROIEdit(ROI3DShape.this, pt, savedPosition));
601                            }
602                        }
603                    }
604                    finally
605                    {
606                        ROI3DShape.this.endUpdate();
607                    }
608                }
609            }
610
611            // then send event to parent
612            super.mouseDrag(e, imagePoint, canvas);
613        }
614
615        @Override
616        public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas)
617        {
618            if (isActiveFor(canvas))
619            {
620                // check we can do the action
621                if (isSelected() && !isReadOnly())
622                {
623                    // send event to controls points first
624                    ROI3DShape.this.beginUpdate();
625                    try
626                    {
627                        // refresh control point state
628                        synchronized (controlPoints)
629                        {
630                            for (Anchor3D pt : controlPoints)
631                                pt.mouseMove(e, imagePoint, canvas);
632                        }
633                    }
634                    finally
635                    {
636                        ROI3DShape.this.endUpdate();
637                    }
638                }
639            }
640
641            // then send event to parent
642            super.mouseMove(e, imagePoint, canvas);
643        }
644
645        /**
646         * Draw the ROI
647         */
648        @Override
649        public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas)
650        {
651            if (canvas instanceof IcyCanvas2D)
652            {
653                // not supported
654                if (g == null)
655                    return;
656
657                final Rectangle2D bounds = getBounds3D().toRectangle2D();
658
659                // enlarge bounds with stroke
660                final double over = getAdjustedStroke(canvas) * 2;
661                ShapeUtil.enlarge(bounds, over, over, true);
662
663                // define LOD level
664                final boolean shapeVisible = isVisible(bounds, g, canvas);
665
666                if (shapeVisible)
667                {
668                    final boolean small = isSmall(bounds, g, canvas);
669
670                    // draw shape
671                    drawShape(g, sequence, canvas, small);
672
673                    // draw control points (only if not tiny)
674                    if (!isTiny(bounds, g, canvas) && isSelected() && !isReadOnly())
675                    {
676                        // draw control point if selected
677                        synchronized (controlPoints)
678                        {
679                            for (Anchor3D pt : controlPoints)
680                                pt.paint(g, sequence, canvas, small);
681                        }
682                    }
683                }
684            }
685
686            if (canvas instanceof VtkCanvas)
687            {
688                // 3D canvas
689                final VtkCanvas cnv = (VtkCanvas) canvas;
690                // update reference if needed
691                if (canvas3d.get() != cnv)
692                    canvas3d = new WeakReference<VtkCanvas>(cnv);
693
694                // initialize VTK objects if not yet done
695                if (actor == null)
696                    initVtkObjects();
697
698                // FIXME : need a better implementation
699                final double[] s = cnv.getVolumeScale();
700
701                // scaling changed ?
702                if (!Arrays.equals(scaling, s))
703                {
704                    // update scaling
705                    scaling = s;
706                    // need rebuild
707                    needRebuild = true;
708                }
709
710                // need to rebuild 3D data structures ?
711                if (needRebuild)
712                {
713                    // request rebuild 3D objects
714                    ThreadUtil.runSingle(this);
715                    needRebuild = false;
716                }
717
718                final vtkRenderer renderer = cnv.getRenderer();
719
720                // need to remove control points actor ?
721                synchronized (actorsToRemove)
722                {
723                    for (Anchor3D anchor : actorsToRemove)
724                        for (vtkProp prop : anchor.getProps())
725                            VtkUtil.removeProp(renderer, prop);
726
727                    // done
728                    actorsToRemove.clear();
729                }
730                // need to add control points actor ?
731                synchronized (actorsToAdd)
732                {
733                    for (Anchor3D anchor : actorsToAdd)
734                        for (vtkProp prop : anchor.getProps())
735                            VtkUtil.addProp(renderer, prop);
736
737                    // done
738                    actorsToAdd.clear();
739                }
740
741                // needed to forward paint event to control point
742                synchronized (controlPoints)
743                {
744                    for (Anchor3D pt : controlPoints)
745                        pt.paint(null, sequence, canvas);
746                }
747            }
748        }
749
750        /**
751         * Draw the shape in specified Graphics2D context.<br>
752         * Override {@link #drawShape(Graphics2D, Sequence, IcyCanvas, boolean, boolean)} instead if possible.
753         */
754        protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified)
755        {
756            drawShape(g, sequence, canvas, simplified, true);
757        }
758
759        /**
760         * Draw the shape in specified Graphics2D context.<br>
761         * Default implementation just draw '3D' lines between all controls points
762         */
763        protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified,
764                boolean connectLastPoint)
765        {
766            final List<Point3D> points = getPointsInternal();
767            final Graphics2D g2 = (Graphics2D) g.create();
768
769            // normal rendering without selection --> draw border first
770            if (!simplified && !isSelected())
771            {
772                // draw border
773                g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d), BasicStroke.CAP_BUTT,
774                        BasicStroke.JOIN_MITER));
775                g2.setColor(Color.black);
776
777                for (int i = 1; i < points.size(); i++)
778                    drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i));
779                // connect last point
780                if (connectLastPoint && (points.size() > 2))
781                    drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0));
782            }
783
784            // then draw shape
785            g2.setStroke(new BasicStroke(
786                    (float) ROI.getAdjustedStroke(canvas, (!simplified && isSelected()) ? stroke + 1 : stroke),
787                    BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER));
788            g2.setColor(getDisplayColor());
789
790            for (int i = 1; i < points.size(); i++)
791                drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i));
792            // connect last point
793            if (connectLastPoint && (points.size() > 2))
794                drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0));
795
796            g2.dispose();
797        }
798
799        /**
800         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
801         * specified canvas / graphics context.
802         */
803        protected boolean isVisible(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
804        {
805            return GraphicsUtil.isVisible(g, bounds);
806        }
807
808        /**
809         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
810         * specified canvas / graphics context.
811         */
812        protected boolean isSmall(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
813        {
814            if (isCreating())
815                return false;
816
817            final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
818            final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight());
819
820            return size < LOD_SMALL;
821        }
822
823        /**
824         * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the
825         * specified canvas / graphics context.
826         */
827        protected boolean isTiny(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas)
828        {
829            if (isCreating())
830                return false;
831
832            final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY()));
833            final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight());
834
835            return size < LOD_TINY;
836        }
837
838        @Override
839        public void setColor(Color value)
840        {
841            beginUpdate();
842            try
843            {
844                super.setColor(value);
845
846                // also change colors of controls points
847                final Color focusedColor = getFocusedColor();
848
849                synchronized (controlPoints)
850                {
851                    for (Anchor3D anchor : controlPoints)
852                    {
853                        anchor.setColor(value);
854                        anchor.setSelectedColor(focusedColor);
855                    }
856                }
857            }
858            finally
859            {
860                endUpdate();
861            }
862        }
863
864        @Override
865        public vtkProp[] getProps()
866        {
867            // initialize VTK objects if not yet done
868            if (actor == null)
869                initVtkObjects();
870
871            final List<vtkProp> result = new ArrayList<vtkProp>();
872
873            // add VTK objects from ROI shape
874            result.add(actor);
875            result.add(outlineActor);
876
877            // then add VTK objects from controls points
878            synchronized (controlPoints)
879            {
880                for (Anchor3D pt : controlPoints)
881                    for (vtkProp prop : pt.getProps())
882                        result.add(prop);
883            }
884
885            return result.toArray(new vtkProp[result.size()]);
886        }
887
888        @Override
889        public void run()
890        {
891            rebuildVtkObjects();
892        }
893    }
894
895    /**
896     * Draw a 3D line in specified graphics object
897     */
898    protected static void drawLine3D(Graphics2D g, Sequence sequence, IcyCanvas canvas, Point3D p1, Point3D p2)
899    {
900        final Line2D line2d = new Line2D.Double();
901
902        // get canvas Z position
903        final int cnvZ = canvas.getPositionZ();
904        // calculate z fade range
905        final double zRange = Math.min(10d, Math.max(3d, sequence.getSizeZ() / 8d));
906
907        // same Z, don't need to split lines
908        if (p1.getZ() == p2.getZ())
909            drawSegment3D(g, p1, p2, zRange, cnvZ, line2d);
910        else
911        {
912            final Line3DIterator it = new Line3DIterator(new Line3D(p1, p2), 4d / canvas.getScaleX());
913            // start position
914            Point3D pos = it.next();
915
916            do
917            {
918                // get next position
919                final Point3D nextPos = it.next();
920                // draw line
921                drawSegment3D(g, pos, nextPos, zRange, cnvZ, line2d);
922                // update current pos
923                pos = nextPos;
924            }
925            while (it.hasNext());
926        }
927    }
928
929    /**
930     * Draw a 3D line in specified graphics object
931     */
932    protected static void drawSegment3D(Graphics2D g, Point3D p1, Point3D p2, double zRange, int canvasZ, Line2D line2d)
933    {
934        // get Line Z pos
935        final double meanZ = (p1.getZ() + p2.getZ()) / 2d;
936        // get delta Z (difference between canvas Z position and line Z pos)
937        final double dz = Math.abs(meanZ - canvasZ);
938
939        // not visible on this Z position
940        if (dz > zRange)
941            return;
942
943        // ratio for size / opacity
944        final float ratio = 1f - (float) (dz / zRange);
945        final Composite prevComposite = g.getComposite();
946
947        if (ratio != 1f)
948            GraphicsUtil.mixAlpha(g, ratio);
949
950        // draw line
951        line2d.setLine(p1.getX(), p1.getY(), p2.getX(), p2.getY());
952        g.draw(line2d);
953
954        // restore composite
955        g.setComposite(prevComposite);
956    }
957
958    public static final String ID_POINTS = "points";
959    public static final String ID_POINT = "point";
960
961    /**
962     * Polyline3D shape (in image coordinates)
963     */
964    protected final Shape3D shape;
965    /**
966     * control points
967     */
968    protected final List<Anchor3D> controlPoints;
969
970    /**
971     * internals
972     */
973    protected final Anchor3DPositionListener anchor2DPositionListener;
974    protected final OverlayListener anchor2DOverlayListener;
975    protected boolean firstMove;
976
977    /**
978     * 
979     */
980    public ROI3DShape(Shape3D shape)
981    {
982        super();
983
984        this.shape = shape;
985        controlPoints = new ArrayList<Anchor3D>();
986        firstMove = true;
987
988        anchor2DPositionListener = new Anchor3DPositionListener()
989        {
990            @Override
991            public void positionChanged(Anchor3D source)
992            {
993                controlPointPositionChanged(source);
994            }
995        };
996        anchor2DOverlayListener = new OverlayListener()
997        {
998            @Override
999            public void overlayChanged(OverlayEvent event)
1000            {
1001                controlPointOverlayChanged(event);
1002            }
1003        };
1004    }
1005
1006    @Override
1007    public String getDefaultName()
1008    {
1009        return "Shape3D";
1010    }
1011
1012    @Override
1013    protected ROI3DShapePainter createPainter()
1014    {
1015        return new ROI3DShapePainter();
1016    }
1017
1018    /**
1019     * build a new anchor with specified position
1020     */
1021    protected Anchor3D createAnchor(Point3D pos)
1022    {
1023        return new Anchor3D(pos.getX(), pos.getY(), pos.getZ(), getColor(), getFocusedColor());
1024    }
1025
1026    /**
1027     * @return the shape
1028     */
1029    public Shape3D getShape()
1030    {
1031        return shape;
1032    }
1033
1034    /**
1035     * Return true if this ROI support adding new point
1036     */
1037    public boolean canAddPoint()
1038    {
1039        return true;
1040    }
1041
1042    /**
1043     * Return true if this ROI support removing point
1044     */
1045    public boolean canRemovePoint()
1046    {
1047        return true;
1048    }
1049
1050    /**
1051     * Internal use only
1052     */
1053    protected void addPoint(Anchor3D pt)
1054    {
1055        addPoint(pt, -1);
1056    }
1057
1058    /**
1059     * Internal use only, use {@link #addNewPoint(Point3D, boolean)} instead.
1060     */
1061    public void addPoint(Anchor3D pt, int index)
1062    {
1063        // set visible state
1064        pt.setVisible(isSelected());
1065
1066        pt.addPositionListener(anchor2DPositionListener);
1067        pt.addOverlayListener(anchor2DOverlayListener);
1068
1069        if (index == -1)
1070            controlPoints.add(pt);
1071        else
1072            controlPoints.add(index, pt);
1073
1074        synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
1075        {
1076            // store it in the "actor to add" list
1077            ((ROI3DShapePainter) getOverlay()).actorsToAdd.add(pt);
1078        }
1079        synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
1080        {
1081            // and remove it from the "actor to remove" list
1082            ((ROI3DShapePainter) getOverlay()).actorsToRemove.remove(pt);
1083        }
1084
1085        roiChanged(true);
1086    }
1087
1088    /**
1089     * Add a new point to the Polyline 3D ROI.
1090     * 
1091     * @param pos
1092     *        position of the new point
1093     * @param insert
1094     *        if set to <code>true</code> the new point will be inserted between the 2 closest
1095     *        points (in pixels distance) else the new point is inserted at the end of the point
1096     *        list
1097     * @return the new created Anchor3D point
1098     */
1099    public Anchor3D addNewPoint(Point3D pos, boolean insert)
1100    {
1101        if (!canAddPoint())
1102            return null;
1103
1104        final Anchor3D pt = createAnchor(pos);
1105
1106        if (insert)
1107            // insert mode ? --> place the new point with closest points
1108            addPoint(pt, getInsertPointPosition(pos));
1109        else
1110            // just add the new point at last position
1111            addPoint(pt);
1112
1113        // always select
1114        pt.setSelected(true);
1115
1116        return pt;
1117    }
1118
1119    /**
1120     * internal use only
1121     */
1122    protected boolean removePoint(IcyCanvas canvas, Anchor3D pt)
1123    {
1124        boolean empty;
1125
1126        pt.removeOverlayListener(anchor2DOverlayListener);
1127        pt.removePositionListener(anchor2DPositionListener);
1128
1129        synchronized (controlPoints)
1130        {
1131            controlPoints.remove(pt);
1132            empty = controlPoints.isEmpty();
1133        }
1134
1135        synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
1136        {
1137            // store it in the "actor to remove" list
1138            ((ROI3DShapePainter) getOverlay()).actorsToRemove.add(pt);
1139        }
1140        synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
1141        {
1142            // and remove it from the "actor to add" list
1143            ((ROI3DShapePainter) getOverlay()).actorsToAdd.remove(pt);
1144        }
1145
1146        // empty ROI ? --> remove from all sequence
1147        if (empty)
1148            remove();
1149        else
1150            roiChanged(true);
1151
1152        return true;
1153    }
1154
1155    /**
1156     * This method give you lower level access on point remove operation but can be unsafe.<br/>
1157     * Use {@link #removeSelectedPoint(IcyCanvas)} when possible.
1158     */
1159    public boolean removePoint(Anchor3D pt)
1160    {
1161        return removePoint(null, pt);
1162    }
1163
1164    /**
1165     * internal use only (used for fast clear)
1166     */
1167    protected void removeAllPoint()
1168    {
1169        synchronized (controlPoints)
1170        {
1171            synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove)
1172            {
1173                // store all points in the "actor to remove" list
1174                ((ROI3DShapePainter) getOverlay()).actorsToRemove.addAll(controlPoints);
1175            }
1176            synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd)
1177            {
1178                // and remove them from the "actor to add" list
1179                ((ROI3DShapePainter) getOverlay()).actorsToAdd.removeAll(controlPoints);
1180            }
1181
1182            for (Anchor3D pt : controlPoints)
1183            {
1184                pt.removeOverlayListener(anchor2DOverlayListener);
1185                pt.removePositionListener(anchor2DPositionListener);
1186            }
1187
1188            controlPoints.clear();
1189        }
1190    }
1191
1192    /**
1193     * Remove the current selected point.
1194     */
1195    public boolean removeSelectedPoint(IcyCanvas canvas)
1196    {
1197        if (!canRemovePoint())
1198            return false;
1199
1200        final Anchor3D selectedPoint = getSelectedPoint();
1201
1202        if (selectedPoint == null)
1203            return false;
1204
1205        synchronized (controlPoints)
1206        {
1207            // try to remove point
1208            if (!removePoint(canvas, selectedPoint))
1209                return false;
1210
1211            // still have control points
1212            if (controlPoints.size() > 0)
1213            {
1214                // save the point position
1215                final Point3D imagePoint = selectedPoint.getPosition();
1216
1217                // select a new point if possible
1218                if (controlPoints.size() > 0)
1219                    selectPointAt(canvas, imagePoint);
1220            }
1221        }
1222
1223        return true;
1224    }
1225
1226    protected Anchor3D getSelectedPoint()
1227    {
1228        synchronized (controlPoints)
1229        {
1230            for (Anchor3D pt : controlPoints)
1231                if (pt.isSelected())
1232                    return pt;
1233        }
1234
1235        return null;
1236    }
1237
1238    @Override
1239    public boolean hasSelectedPoint()
1240    {
1241        return (getSelectedPoint() != null);
1242    }
1243
1244    protected boolean selectPointAt(IcyCanvas canvas, Point3D imagePoint)
1245    {
1246        synchronized (controlPoints)
1247        {
1248            // find the new selected control point
1249            for (Anchor3D pt : controlPoints)
1250            {
1251                // control point is overlapped ?
1252                if (pt.isOver(canvas, imagePoint))
1253                {
1254                    // select it
1255                    pt.setSelected(true);
1256                    return true;
1257                }
1258            }
1259        }
1260
1261        return false;
1262    }
1263
1264    @Override
1265    public void unselectAllPoints()
1266    {
1267        beginUpdate();
1268        try
1269        {
1270            synchronized (controlPoints)
1271            {
1272                // unselect all point
1273                for (Anchor3D pt : controlPoints)
1274                    pt.setSelected(false);
1275            }
1276        }
1277        finally
1278        {
1279            endUpdate();
1280        }
1281    };
1282
1283    @SuppressWarnings("static-method")
1284    protected double getTotalDistance(List<Point3D> points, double factorX, double factorY, double factorZ)
1285    {
1286        // default implementation
1287        return Point3D.getTotalDistance(points, factorX, factorY, factorZ, true);
1288    }
1289
1290    @Override
1291    public double getLength(Sequence sequence)
1292    {
1293        return getTotalDistance(getPointsInternal(), sequence.getPixelSizeX(), sequence.getPixelSizeY(),
1294                sequence.getPixelSizeZ());
1295    }
1296
1297    @Override
1298    public double computeSurfaceArea(Sequence sequence)
1299    {
1300        return 0d;
1301    }
1302
1303    @Override
1304    public double computeNumberOfContourPoints()
1305    {
1306        return getTotalDistance(getPointsInternal(), 1d, 1d, 1d);
1307    }
1308
1309    /**
1310     * Find best insert position for specified point
1311     */
1312    protected int getInsertPointPosition(Point3D pos)
1313    {
1314        final List<Point3D> points = getPointsInternal();
1315
1316        final int size = points.size();
1317        // by default we use last position
1318        int result = size;
1319        double minDistance = Double.MAX_VALUE;
1320
1321        // we try all cases
1322        for (int i = size; i >= 0; i--)
1323        {
1324            // add point at current position
1325            points.add(i, pos);
1326
1327            // calculate total distance
1328            final double d = getTotalDistance(points, 1d, 1d, 1d);
1329            // minimum distance ?
1330            if (d < minDistance)
1331            {
1332                // save index
1333                minDistance = d;
1334                result = i;
1335            }
1336
1337            // remove point from current position
1338            points.remove(i);
1339        }
1340
1341        return result;
1342    }
1343
1344    // @Override
1345    // public boolean isOverPoint(IcyCanvas canvas, double x, double y)
1346    // {
1347    // if (isSelected())
1348    // {
1349    // for (Anchor3D pt : controlPoints)
1350    // if (pt.isOver(canvas, x, y))
1351    // return true;
1352    // }
1353    //
1354    // return false;
1355    // }
1356
1357    /**
1358     * Return the list of control points for this ROI.
1359     */
1360    public List<Anchor3D> getControlPoints()
1361    {
1362        synchronized (controlPoints)
1363        {
1364            return new ArrayList<Anchor3D>(controlPoints);
1365        }
1366    }
1367
1368    /**
1369     * Return the list of position for all control points of the ROI.
1370     */
1371    public List<Point3D> getPoints()
1372    {
1373        final List<Point3D> result = new ArrayList<Point3D>();
1374
1375        synchronized (controlPoints)
1376        {
1377            for (Anchor3D pt : controlPoints)
1378                result.add(pt.getPosition());
1379        }
1380
1381        return result;
1382    }
1383
1384    /**
1385     * Return the list of positions of control points for this ROI.<br>
1386     * This is the direct internal position reference, don't modify them !
1387     */
1388    protected List<Point3D> getPointsInternal()
1389    {
1390        final List<Point3D> result = new ArrayList<Point3D>();
1391
1392        synchronized (controlPoints)
1393        {
1394            for (Anchor3D pt : controlPoints)
1395                result.add(pt.getPositionInternal());
1396        }
1397
1398        return result;
1399    }
1400
1401    /**
1402     * Returns true if specified point coordinates overlap the ROI edge.
1403     */
1404    @Override
1405    public boolean isOverEdge(IcyCanvas canvas, double x, double y, double z)
1406    {
1407        // use bigger stroke for isOver test for easier intersection
1408        final double strk = painter.getAdjustedStroke(canvas) * 3;
1409        final Rectangle3D rect = new Rectangle3D.Double(x - (strk * 0.5), y - (strk * 0.5), z - (strk * 0.5), strk,
1410                strk, strk);
1411
1412        return intersects(rect);
1413    }
1414
1415    @Override
1416    public boolean contains(Point3D p)
1417    {
1418        return shape.contains(p);
1419    }
1420
1421    @Override
1422    public boolean contains(Rectangle3D r)
1423    {
1424        return shape.contains(r);
1425    }
1426
1427    @Override
1428    public boolean contains(double x, double y, double z)
1429    {
1430        return shape.contains(x, y, z);
1431    }
1432
1433    @Override
1434    public boolean contains(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
1435    {
1436        return shape.contains(x, y, z, sizeX, sizeY, sizeZ);
1437    }
1438
1439    @Override
1440    public boolean intersects(Rectangle3D r)
1441    {
1442        return shape.intersects(r);
1443    }
1444
1445    @Override
1446    public boolean intersects(double x, double y, double z, double sizeX, double sizeY, double sizeZ)
1447    {
1448        return shape.intersects(x, y, z, sizeX, sizeY, sizeZ);
1449    }
1450
1451    @Override
1452    public Rectangle3D computeBounds3D()
1453    {
1454        final Rectangle3D result = shape.getBounds();
1455
1456        // shape shouldn't be empty (even for single Point) --> always use a minimal bounds
1457        if (result.isEmpty())
1458        {
1459            result.setSizeX(Math.max(result.getSizeX(), 0.001d));
1460            result.setSizeY(Math.max(result.getSizeY(), 0.001d));
1461            result.setSizeZ(Math.max(result.getSizeZ(), 0.001d));
1462        }
1463
1464        return result;
1465    }
1466
1467    @Override
1468    public boolean canTranslate()
1469    {
1470        return true;
1471    }
1472
1473    @Override
1474    public void translate(double dx, double dy, double dz)
1475    {
1476        beginUpdate();
1477        try
1478        {
1479            synchronized (controlPoints)
1480            {
1481                for (Anchor3D pt : controlPoints)
1482                    pt.translate(dx, dy, dz);
1483            }
1484        }
1485        finally
1486        {
1487            endUpdate();
1488        }
1489    }
1490
1491    /**
1492     * Called when anchor position changed
1493     */
1494    public void controlPointPositionChanged(Anchor3D source)
1495    {
1496        // anchor(s) position changed --> ROI changed
1497        roiChanged(true);
1498    }
1499
1500    /**
1501     * Called when anchor overlay changed
1502     */
1503    public void controlPointOverlayChanged(OverlayEvent event)
1504    {
1505        // we only mind about painter change from anchor...
1506        if (event.getType() == OverlayEventType.PAINTER_CHANGED)
1507        {
1508            // we have a control point selected --> remove focus on ROI
1509            if (hasSelectedPoint())
1510                setFocused(false);
1511
1512            // anchor changed --> ROI painter changed
1513            getOverlay().painterChanged();
1514        }
1515    }
1516
1517    /**
1518     * roi changed
1519     */
1520    @Override
1521    public void onChanged(CollapsibleEvent object)
1522    {
1523        final ROIEvent event = (ROIEvent) object;
1524
1525        // do here global process on ROI change
1526        switch (event.getType())
1527        {
1528            case ROI_CHANGED:
1529                // refresh shape
1530                updateShape();
1531                break;
1532
1533            case FOCUS_CHANGED:
1534                ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties();
1535                break;
1536
1537            case SELECTION_CHANGED:
1538                final boolean s = isSelected();
1539
1540                beginUpdate();
1541                try
1542                {
1543                    // set control points visible or not
1544                    synchronized (controlPoints)
1545                    {
1546                        for (Anchor3D pt : controlPoints)
1547                            pt.setVisible(s);
1548                    }
1549
1550                    // unselect if not visible
1551                    if (!s)
1552                        unselectAllPoints();
1553                }
1554                finally
1555                {
1556                    endUpdate();
1557                }
1558
1559                ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties();
1560                break;
1561
1562            case PROPERTY_CHANGED:
1563                final String property = event.getPropertyName();
1564
1565                if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR)
1566                        || StringUtil.equals(property, PROPERTY_OPACITY))
1567                    ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties();
1568                break;
1569
1570            default:
1571                break;
1572        }
1573
1574        super.onChanged(object);
1575    }
1576
1577    @Override
1578    public double computeNumberOfPoints()
1579    {
1580        return 0d;
1581    }
1582
1583    /**
1584     * Rebuild shape.<br>
1585     * This method should be overridden by derived classes which<br>
1586     * have to call the super.updateShape() method at end.
1587     */
1588    protected void updateShape()
1589    {
1590        // the shape should have been rebuilt here
1591        ((ROI3DShapePainter) painter).needRebuild = true;
1592    }
1593
1594    @Override
1595    public boolean loadFromXML(Node node)
1596    {
1597        beginUpdate();
1598        try
1599        {
1600            if (!super.loadFromXML(node))
1601                return false;
1602
1603            firstMove = false;
1604            // unselect all control points
1605            unselectAllPoints();
1606        }
1607        finally
1608        {
1609            endUpdate();
1610        }
1611
1612        return true;
1613    }
1614}