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}