/** * */ package plugins.kernel.roi.roi3d; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Composite; import java.awt.Graphics2D; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import org.w3c.dom.Node; import icy.canvas.IcyCanvas; import icy.canvas.IcyCanvas2D; import icy.common.CollapsibleEvent; import icy.math.Line3DIterator; import icy.painter.Anchor3D; import icy.painter.Anchor3D.Anchor3DPositionListener; import icy.painter.OverlayEvent; import icy.painter.OverlayEvent.OverlayEventType; import icy.painter.OverlayListener; import icy.painter.VtkPainter; import icy.roi.ROI; import icy.roi.ROI3D; import icy.roi.ROIEvent; import icy.roi.edit.Point3DAddedROIEdit; import icy.roi.edit.Point3DMovedROIEdit; import icy.roi.edit.Point3DRemovedROIEdit; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.geom.Line3D; import icy.type.geom.Shape3D; import icy.type.point.Point3D; import icy.type.point.Point5D; import icy.type.rectangle.Rectangle3D; import icy.util.EventUtil; import icy.util.GraphicsUtil; import icy.util.ShapeUtil; import icy.util.StringUtil; import icy.vtk.IcyVtkPanel; import icy.vtk.VtkUtil; import plugins.kernel.canvas.VtkCanvas; import vtk.vtkActor; import vtk.vtkCellArray; import vtk.vtkInformation; import vtk.vtkPoints; import vtk.vtkPolyData; import vtk.vtkPolyDataMapper; import vtk.vtkProp; import vtk.vtkProperty; import vtk.vtkRenderer; /** * Base class for 3D shape ROI (working from 3D control points). * * @author Stephane Dallongeville */ public class ROI3DShape extends ROI3D implements Shape3D { public class ROI3DShapePainter extends ROI3DPainter implements VtkPainter, Runnable { // VTK 3D objects protected vtkPolyData outline; protected vtkPolyDataMapper outlineMapper; protected vtkActor outlineActor; protected vtkInformation vtkInfo; protected vtkCellArray vCells; protected vtkPoints vPoints; protected vtkPolyData polyData; protected vtkPolyDataMapper polyMapper; protected vtkActor actor; // 3D internal protected boolean needRebuild; protected double scaling[]; protected WeakReference<VtkCanvas> canvas3d; protected Set<Anchor3D> actorsToAdd; protected Set<Anchor3D> actorsToRemove; public ROI3DShapePainter() { super(); // don't create VTK object on constructor outline = null; outlineMapper = null; outlineActor = null; vtkInfo = null; vCells = null; vPoints = null; polyData = null; polyMapper = null; actor = null; scaling = new double[3]; Arrays.fill(scaling, 1d); actorsToAdd = new HashSet<Anchor3D>(); actorsToRemove = new HashSet<Anchor3D>(); needRebuild = true; canvas3d = new WeakReference<VtkCanvas>(null); } @Override protected void finalize() throws Throwable { super.finalize(); // release allocated VTK resources if (actor != null) actor.Delete(); if (polyMapper != null) polyMapper.Delete(); if (polyData != null) polyData.Delete(); if (vPoints != null) vPoints.Delete(); if (vCells != null) vCells.Delete(); if (outlineActor != null) { outlineActor.SetPropertyKeys(null); outlineActor.Delete(); } if (vtkInfo != null) { vtkInfo.Remove(VtkCanvas.visibilityKey); vtkInfo.Delete(); } if (outlineMapper != null) outlineMapper.Delete(); if (outline != null) { outline.GetPointData().GetScalars().Delete(); outline.GetPointData().Delete(); outline.Delete(); } }; protected void initVtkObjects() { outline = VtkUtil.getOutline(0d, 1d, 0d, 1d, 0d, 1d); outlineMapper = new vtkPolyDataMapper(); outlineMapper.SetInputData(outline); outlineActor = new vtkActor(); outlineActor.SetMapper(outlineMapper); // disable picking on the outline outlineActor.SetPickable(0); // and set it to wireframe representation outlineActor.GetProperty().SetRepresentationToWireframe(); // use vtkInformations to store outline visibility state (hacky) vtkInfo = new vtkInformation(); vtkInfo.Set(VtkCanvas.visibilityKey, 0); // VtkCanvas use this to restore correctly outline visibility flag outlineActor.SetPropertyKeys(vtkInfo); // init poly data object polyData = new vtkPolyData(); polyMapper = new vtkPolyDataMapper(); polyMapper.SetInputData(polyData); actor = new vtkActor(); actor.SetMapper(polyMapper); // initialize color and stroke final Color col = getColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; outlineActor.GetProperty().SetColor(r, g, b); final vtkProperty property = actor.GetProperty(); property.SetPointSize(getStroke()); property.SetColor(r, g, b); } /** * update 3D painter for 3D canvas (called only when VTK is loaded). */ protected void rebuildVtkObjects() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; // get scaling final double xs = scaling[0]; final double ys = scaling[1]; final double zs = scaling[2]; // update polydata final int numPts = controlPoints.size(); final double[][] vertices = new double[numPts][3]; final int[] indexes = new int[numPts + 1]; indexes[0] = numPts; if (!controlPoints.isEmpty()) { // add all controls point position for (int i = 0; i < numPts; i++) { final Point3D point = controlPoints.get(i).getPosition(); final double[] vertex = vertices[i]; vertex[0] = point.getX() * xs; vertex[1] = point.getY() * ys; vertex[2] = point.getZ() * zs; indexes[i + 1] = i; } } final vtkCellArray previousCells = vCells; final vtkPoints previousPoints = vPoints; vCells = VtkUtil.getCells(1, indexes); vPoints = VtkUtil.getPoints(vertices); // get bounds final Rectangle3D bounds = getBounds3D(); // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { // update outline data VtkUtil.setOutlineBounds(outline, bounds.getMinX() * xs, bounds.getMaxX() * xs, bounds.getMinY() * ys, bounds.getMaxY() * ys, bounds.getMinZ() * zs, bounds.getMaxZ() * zs, canvas); outlineMapper.Update(); // update polygon data from cell and points polyData.SetPoints(vPoints); polyData.SetLines(vCells); polyMapper.Update(); // release previous allocated VTK objects if (previousCells != null) previousCells.Delete(); if (previousPoints != null) previousPoints.Delete(); } finally { vtkPanel.unlock(); } // update color and others properties updateVtkDisplayProperties(); } protected void updateVtkDisplayProperties() { if (actor == null) return; final VtkCanvas cnv = canvas3d.get(); final vtkProperty vtkProperty = actor.GetProperty(); final Color col = getDisplayColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; final double strk = getStroke(); // final float opacity = getOpacity(); final IcyVtkPanel vtkPanel = (cnv != null) ? cnv.getVtkPanel() : null; // we need to lock canvas as actor can be accessed during rendering if (vtkPanel != null) vtkPanel.lock(); try { // set actors color outlineActor.GetProperty().SetColor(r, g, b); if (isSelected()) { outlineActor.GetProperty().SetRepresentationToWireframe(); outlineActor.SetVisibility(1); vtkInfo.Set(VtkCanvas.visibilityKey, 1); } else { outlineActor.GetProperty().SetRepresentationToPoints(); outlineActor.SetVisibility(0); vtkInfo.Set(VtkCanvas.visibilityKey, 0); } vtkProperty.SetColor(r, g, b); vtkProperty.SetPointSize(strk); vtkProperty.SetLineWidth(strk); // opacity here is about ROI content, global opacity is handled by Layer // vtkProperty.SetOpacity(opacity); setVtkObjectsColor(col); } finally { if (vtkPanel != null) vtkPanel.unlock(); } // need to repaint painterChanged(); } protected void setVtkObjectsColor(Color color) { if (outline != null) VtkUtil.setPolyDataColor(outline, color, canvas3d.get()); if (polyData != null) VtkUtil.setPolyDataColor(polyData, color, canvas3d.get()); } @Override protected boolean updateFocus(InputEvent e, Point5D imagePoint, IcyCanvas canvas) { // specific VTK canvas processing if (canvas instanceof VtkCanvas) { // mouse is over the ROI actor ? --> focus the ROI final boolean focused = (actor != null) && (actor == ((VtkCanvas) canvas).getPickedObject()); setFocused(focused); return focused; } return super.updateFocus(e, imagePoint, canvas); } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { if (isActiveFor(canvas)) { ROI3DShape.this.beginUpdate(); try { // get control points list final List<Anchor3D> controlPoints = getControlPoints(); // send event to controls points first for (Anchor3D pt : controlPoints) pt.keyPressed(e, imagePoint, canvas); // specific action for ROI3DPolyLine if (!e.isConsumed()) { final Sequence sequence = canvas.getSequence(); switch (e.getKeyCode()) { case KeyEvent.VK_DELETE: case KeyEvent.VK_BACK_SPACE: final Anchor3D selectedPoint = getSelectedPoint(); // try to remove selected point if (removeSelectedPoint(canvas)) { // consume event e.consume(); // add undo operation if (sequence != null) sequence.addUndoableEdit(new Point3DRemovedROIEdit(ROI3DShape.this, controlPoints, selectedPoint)); } break; } } } finally { ROI3DShape.this.endUpdate(); } } } // then send event to parent super.keyPressed(e, imagePoint, canvas); } @Override public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { if (isActiveFor(canvas)) { // check we can do the action if (imagePoint != null) { ROI3DShape.this.beginUpdate(); try { // send event to controls points first synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.keyReleased(e, imagePoint, canvas); } } finally { ROI3DShape.this.endUpdate(); } } } } // then send event to parent super.keyReleased(e, imagePoint, canvas); } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isActiveFor(canvas)) { // check we can do the action if (isSelected() && !isReadOnly()) { ROI3DShape.this.beginUpdate(); try { // send event to controls points first synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.mousePressed(e, imagePoint, canvas); } // specific action for this ROI if (!e.isConsumed()) { if (imagePoint != null) { // left button action if (EventUtil.isLeftMouseButton(e)) { // ROI should not be focused to add point (for multi selection) if (!isFocused()) { final boolean insertMode = EventUtil.isControlDown(e); // insertion mode or creating the ROI ? --> add a new point if (insertMode || isCreating()) { // try to add point final Anchor3D point = addNewPoint(imagePoint.toPoint3D(), insertMode); // point added ? if (point != null) { // consume event e.consume(); final Sequence sequence = canvas.getSequence(); // add undo operation if (sequence != null) sequence.addUndoableEdit( new Point3DAddedROIEdit(ROI3DShape.this, point)); } } } } } } } finally { ROI3DShape.this.endUpdate(); } } } // then send event to parent super.mousePressed(e, imagePoint, canvas); } @Override public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // not anymore the first move firstMove = false; if (isSelected() && !isReadOnly()) { // send event to controls points first if (isActiveFor(canvas)) { final Sequence sequence = canvas.getSequence(); ROI3DShape.this.beginUpdate(); try { // default anchor action on mouse release synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.mouseReleased(e, imagePoint, canvas); } } finally { ROI3DShape.this.endUpdate(); } // prevent undo operation merging if (sequence != null) sequence.getUndoManager().noMergeForNextEdit(); } } // then send event to parent super.mouseReleased(e, imagePoint, canvas); } @Override public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { // send event to controls points first if (isActiveFor(canvas)) { ROI3DShape.this.beginUpdate(); try { // default anchor action on mouse click synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.mouseClick(e, imagePoint, canvas); } } finally { ROI3DShape.this.endUpdate(); } } } // then send event to parent super.mouseClick(e, imagePoint, canvas); } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isActiveFor(canvas)) { // check we can do the action if (isSelected() && !isReadOnly()) { final Sequence sequence = canvas.getSequence(); // send event to controls points first ROI3DShape.this.beginUpdate(); try { // default anchor action on mouse drag synchronized (controlPoints) { for (Anchor3D pt : controlPoints) { final Point3D savedPosition; // don't want to undo position change on first creation movement if ((sequence != null) && (!isCreating() || !firstMove)) savedPosition = pt.getPosition(); else savedPosition = null; pt.mouseDrag(e, imagePoint, canvas); // position changed and undo supported --> add undo operation if ((sequence != null) && (savedPosition != null) && !savedPosition.equals(pt.getPosition())) sequence.addUndoableEdit( new Point3DMovedROIEdit(ROI3DShape.this, pt, savedPosition)); } } } finally { ROI3DShape.this.endUpdate(); } } } // then send event to parent super.mouseDrag(e, imagePoint, canvas); } @Override public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isActiveFor(canvas)) { // check we can do the action if (isSelected() && !isReadOnly()) { // send event to controls points first ROI3DShape.this.beginUpdate(); try { // refresh control point state synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.mouseMove(e, imagePoint, canvas); } } finally { ROI3DShape.this.endUpdate(); } } } // then send event to parent super.mouseMove(e, imagePoint, canvas); } /** * Draw the ROI */ @Override public void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (canvas instanceof IcyCanvas2D) { // not supported if (g == null) return; final Rectangle2D bounds = getBounds3D().toRectangle2D(); // enlarge bounds with stroke final double over = getAdjustedStroke(canvas) * 2; ShapeUtil.enlarge(bounds, over, over, true); // define LOD level final boolean shapeVisible = isVisible(bounds, g, canvas); if (shapeVisible) { final boolean small = isSmall(bounds, g, canvas); // draw shape drawShape(g, sequence, canvas, small); // draw control points (only if not tiny) if (!isTiny(bounds, g, canvas) && isSelected() && !isReadOnly()) { // draw control point if selected synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.paint(g, sequence, canvas, small); } } } } if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // update reference if needed if (canvas3d.get() != cnv) canvas3d = new WeakReference<VtkCanvas>(cnv); // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); // FIXME : need a better implementation final double[] s = cnv.getVolumeScale(); // scaling changed ? if (!Arrays.equals(scaling, s)) { // update scaling scaling = s; // need rebuild needRebuild = true; } // need to rebuild 3D data structures ? if (needRebuild) { // request rebuild 3D objects ThreadUtil.runSingle(this); needRebuild = false; } final vtkRenderer renderer = cnv.getRenderer(); // need to remove control points actor ? synchronized (actorsToRemove) { for (Anchor3D anchor : actorsToRemove) for (vtkProp prop : anchor.getProps()) VtkUtil.removeProp(renderer, prop); // done actorsToRemove.clear(); } // need to add control points actor ? synchronized (actorsToAdd) { for (Anchor3D anchor : actorsToAdd) for (vtkProp prop : anchor.getProps()) VtkUtil.addProp(renderer, prop); // done actorsToAdd.clear(); } // needed to forward paint event to control point synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.paint(null, sequence, canvas); } } } /** * Draw the shape in specified Graphics2D context.<br> * Override {@link #drawShape(Graphics2D, Sequence, IcyCanvas, boolean, boolean)} instead if possible. */ protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified) { drawShape(g, sequence, canvas, simplified, true); } /** * Draw the shape in specified Graphics2D context.<br> * Default implementation just draw '3D' lines between all controls points */ protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified, boolean connectLastPoint) { final List<Point3D> points = getPointsInternal(); final Graphics2D g2 = (Graphics2D) g.create(); // normal rendering without selection --> draw border first if (!simplified && !isSelected()) { // draw border g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); g2.setColor(Color.black); for (int i = 1; i < points.size(); i++) drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i)); // connect last point if (connectLastPoint && (points.size() > 2)) drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0)); } // then draw shape g2.setStroke(new BasicStroke( (float) ROI.getAdjustedStroke(canvas, (!simplified && isSelected()) ? stroke + 1 : stroke), BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); g2.setColor(getDisplayColor()); for (int i = 1; i < points.size(); i++) drawLine3D(g2, sequence, canvas, points.get(i - 1), points.get(i)); // connect last point if (connectLastPoint && (points.size() > 2)) drawLine3D(g2, sequence, canvas, points.get(points.size() - 1), points.get(0)); g2.dispose(); } /** * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the * specified canvas / graphics context. */ protected boolean isVisible(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas) { return GraphicsUtil.isVisible(g, bounds); } /** * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the * specified canvas / graphics context. */ protected boolean isSmall(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas) { if (isCreating()) return false; final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY())); final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()); return size < LOD_SMALL; } /** * Returns <code>true</code> if the specified bounds should be considered as "tiny" in the * specified canvas / graphics context. */ protected boolean isTiny(Rectangle2D bounds, Graphics2D g, IcyCanvas canvas) { if (isCreating()) return false; final double scale = Math.max(Math.abs(canvas.getScaleX()), Math.abs(canvas.getScaleY())); final double size = Math.max(scale * bounds.getWidth(), scale * bounds.getHeight()); return size < LOD_TINY; } @Override public void setColor(Color value) { beginUpdate(); try { super.setColor(value); // also change colors of controls points final Color focusedColor = getFocusedColor(); synchronized (controlPoints) { for (Anchor3D anchor : controlPoints) { anchor.setColor(value); anchor.setSelectedColor(focusedColor); } } } finally { endUpdate(); } } @Override public vtkProp[] getProps() { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); final List<vtkProp> result = new ArrayList<vtkProp>(); // add VTK objects from ROI shape result.add(actor); result.add(outlineActor); // then add VTK objects from controls points synchronized (controlPoints) { for (Anchor3D pt : controlPoints) for (vtkProp prop : pt.getProps()) result.add(prop); } return result.toArray(new vtkProp[result.size()]); } @Override public void run() { rebuildVtkObjects(); } } /** * Draw a 3D line in specified graphics object */ protected static void drawLine3D(Graphics2D g, Sequence sequence, IcyCanvas canvas, Point3D p1, Point3D p2) { final Line2D line2d = new Line2D.Double(); // get canvas Z position final int cnvZ = canvas.getPositionZ(); // calculate z fade range final double zRange = Math.min(10d, Math.max(3d, sequence.getSizeZ() / 8d)); // same Z, don't need to split lines if (p1.getZ() == p2.getZ()) drawSegment3D(g, p1, p2, zRange, cnvZ, line2d); else { final Line3DIterator it = new Line3DIterator(new Line3D(p1, p2), 4d / canvas.getScaleX()); // start position Point3D pos = it.next(); do { // get next position final Point3D nextPos = it.next(); // draw line drawSegment3D(g, pos, nextPos, zRange, cnvZ, line2d); // update current pos pos = nextPos; } while (it.hasNext()); } } /** * Draw a 3D line in specified graphics object */ protected static void drawSegment3D(Graphics2D g, Point3D p1, Point3D p2, double zRange, int canvasZ, Line2D line2d) { // get Line Z pos final double meanZ = (p1.getZ() + p2.getZ()) / 2d; // get delta Z (difference between canvas Z position and line Z pos) final double dz = Math.abs(meanZ - canvasZ); // not visible on this Z position if (dz > zRange) return; // ratio for size / opacity final float ratio = 1f - (float) (dz / zRange); final Composite prevComposite = g.getComposite(); if (ratio != 1f) GraphicsUtil.mixAlpha(g, ratio); // draw line line2d.setLine(p1.getX(), p1.getY(), p2.getX(), p2.getY()); g.draw(line2d); // restore composite g.setComposite(prevComposite); } public static final String ID_POINTS = "points"; public static final String ID_POINT = "point"; /** * Polyline3D shape (in image coordinates) */ protected final Shape3D shape; /** * control points */ protected final List<Anchor3D> controlPoints; /** * internals */ protected final Anchor3DPositionListener anchor2DPositionListener; protected final OverlayListener anchor2DOverlayListener; protected boolean firstMove; /** * */ public ROI3DShape(Shape3D shape) { super(); this.shape = shape; controlPoints = new ArrayList<Anchor3D>(); firstMove = true; anchor2DPositionListener = new Anchor3DPositionListener() { @Override public void positionChanged(Anchor3D source) { controlPointPositionChanged(source); } }; anchor2DOverlayListener = new OverlayListener() { @Override public void overlayChanged(OverlayEvent event) { controlPointOverlayChanged(event); } }; } @Override public String getDefaultName() { return "Shape3D"; } @Override protected ROI3DShapePainter createPainter() { return new ROI3DShapePainter(); } /** * build a new anchor with specified position */ protected Anchor3D createAnchor(Point3D pos) { return new Anchor3D(pos.getX(), pos.getY(), pos.getZ(), getColor(), getFocusedColor()); } /** * @return the shape */ public Shape3D getShape() { return shape; } /** * Return true if this ROI support adding new point */ public boolean canAddPoint() { return true; } /** * Return true if this ROI support removing point */ public boolean canRemovePoint() { return true; } /** * Internal use only */ protected void addPoint(Anchor3D pt) { addPoint(pt, -1); } /** * Internal use only, use {@link #addNewPoint(Point3D, boolean)} instead. */ public void addPoint(Anchor3D pt, int index) { // set visible state pt.setVisible(isSelected()); pt.addPositionListener(anchor2DPositionListener); pt.addOverlayListener(anchor2DOverlayListener); if (index == -1) controlPoints.add(pt); else controlPoints.add(index, pt); synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd) { // store it in the "actor to add" list ((ROI3DShapePainter) getOverlay()).actorsToAdd.add(pt); } synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove) { // and remove it from the "actor to remove" list ((ROI3DShapePainter) getOverlay()).actorsToRemove.remove(pt); } roiChanged(true); } /** * Add a new point to the Polyline 3D ROI. * * @param pos * position of the new point * @param insert * if set to <code>true</code> the new point will be inserted between the 2 closest * points (in pixels distance) else the new point is inserted at the end of the point * list * @return the new created Anchor3D point */ public Anchor3D addNewPoint(Point3D pos, boolean insert) { if (!canAddPoint()) return null; final Anchor3D pt = createAnchor(pos); if (insert) // insert mode ? --> place the new point with closest points addPoint(pt, getInsertPointPosition(pos)); else // just add the new point at last position addPoint(pt); // always select pt.setSelected(true); return pt; } /** * internal use only */ protected boolean removePoint(IcyCanvas canvas, Anchor3D pt) { boolean empty; pt.removeOverlayListener(anchor2DOverlayListener); pt.removePositionListener(anchor2DPositionListener); synchronized (controlPoints) { controlPoints.remove(pt); empty = controlPoints.isEmpty(); } synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove) { // store it in the "actor to remove" list ((ROI3DShapePainter) getOverlay()).actorsToRemove.add(pt); } synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd) { // and remove it from the "actor to add" list ((ROI3DShapePainter) getOverlay()).actorsToAdd.remove(pt); } // empty ROI ? --> remove from all sequence if (empty) remove(); else roiChanged(true); return true; } /** * This method give you lower level access on point remove operation but can be unsafe.<br/> * Use {@link #removeSelectedPoint(IcyCanvas)} when possible. */ public boolean removePoint(Anchor3D pt) { return removePoint(null, pt); } /** * internal use only (used for fast clear) */ protected void removeAllPoint() { synchronized (controlPoints) { synchronized (((ROI3DShapePainter) getOverlay()).actorsToRemove) { // store all points in the "actor to remove" list ((ROI3DShapePainter) getOverlay()).actorsToRemove.addAll(controlPoints); } synchronized (((ROI3DShapePainter) getOverlay()).actorsToAdd) { // and remove them from the "actor to add" list ((ROI3DShapePainter) getOverlay()).actorsToAdd.removeAll(controlPoints); } for (Anchor3D pt : controlPoints) { pt.removeOverlayListener(anchor2DOverlayListener); pt.removePositionListener(anchor2DPositionListener); } controlPoints.clear(); } } /** * Remove the current selected point. */ public boolean removeSelectedPoint(IcyCanvas canvas) { if (!canRemovePoint()) return false; final Anchor3D selectedPoint = getSelectedPoint(); if (selectedPoint == null) return false; synchronized (controlPoints) { // try to remove point if (!removePoint(canvas, selectedPoint)) return false; // still have control points if (controlPoints.size() > 0) { // save the point position final Point3D imagePoint = selectedPoint.getPosition(); // select a new point if possible if (controlPoints.size() > 0) selectPointAt(canvas, imagePoint); } } return true; } protected Anchor3D getSelectedPoint() { synchronized (controlPoints) { for (Anchor3D pt : controlPoints) if (pt.isSelected()) return pt; } return null; } @Override public boolean hasSelectedPoint() { return (getSelectedPoint() != null); } protected boolean selectPointAt(IcyCanvas canvas, Point3D imagePoint) { synchronized (controlPoints) { // find the new selected control point for (Anchor3D pt : controlPoints) { // control point is overlapped ? if (pt.isOver(canvas, imagePoint)) { // select it pt.setSelected(true); return true; } } } return false; } @Override public void unselectAllPoints() { beginUpdate(); try { synchronized (controlPoints) { // unselect all point for (Anchor3D pt : controlPoints) pt.setSelected(false); } } finally { endUpdate(); } }; @SuppressWarnings("static-method") protected double getTotalDistance(List<Point3D> points, double factorX, double factorY, double factorZ) { // default implementation return Point3D.getTotalDistance(points, factorX, factorY, factorZ, true); } @Override public double getLength(Sequence sequence) { return getTotalDistance(getPointsInternal(), sequence.getPixelSizeX(), sequence.getPixelSizeY(), sequence.getPixelSizeZ()); } @Override public double computeSurfaceArea(Sequence sequence) { return 0d; } @Override public double computeNumberOfContourPoints() { return getTotalDistance(getPointsInternal(), 1d, 1d, 1d); } /** * Find best insert position for specified point */ protected int getInsertPointPosition(Point3D pos) { final List<Point3D> points = getPointsInternal(); final int size = points.size(); // by default we use last position int result = size; double minDistance = Double.MAX_VALUE; // we try all cases for (int i = size; i >= 0; i--) { // add point at current position points.add(i, pos); // calculate total distance final double d = getTotalDistance(points, 1d, 1d, 1d); // minimum distance ? if (d < minDistance) { // save index minDistance = d; result = i; } // remove point from current position points.remove(i); } return result; } // @Override // public boolean isOverPoint(IcyCanvas canvas, double x, double y) // { // if (isSelected()) // { // for (Anchor3D pt : controlPoints) // if (pt.isOver(canvas, x, y)) // return true; // } // // return false; // } /** * Return the list of control points for this ROI. */ public List<Anchor3D> getControlPoints() { synchronized (controlPoints) { return new ArrayList<Anchor3D>(controlPoints); } } /** * Return the list of position for all control points of the ROI. */ public List<Point3D> getPoints() { final List<Point3D> result = new ArrayList<Point3D>(); synchronized (controlPoints) { for (Anchor3D pt : controlPoints) result.add(pt.getPosition()); } return result; } /** * Return the list of positions of control points for this ROI.<br> * This is the direct internal position reference, don't modify them ! */ protected List<Point3D> getPointsInternal() { final List<Point3D> result = new ArrayList<Point3D>(); synchronized (controlPoints) { for (Anchor3D pt : controlPoints) result.add(pt.getPositionInternal()); } return result; } /** * Returns true if specified point coordinates overlap the ROI edge. */ @Override public boolean isOverEdge(IcyCanvas canvas, double x, double y, double z) { // use bigger stroke for isOver test for easier intersection final double strk = painter.getAdjustedStroke(canvas) * 3; final Rectangle3D rect = new Rectangle3D.Double(x - (strk * 0.5), y - (strk * 0.5), z - (strk * 0.5), strk, strk, strk); return intersects(rect); } @Override public boolean contains(Point3D p) { return shape.contains(p); } @Override public boolean contains(Rectangle3D r) { return shape.contains(r); } @Override public boolean contains(double x, double y, double z) { return shape.contains(x, y, z); } @Override public boolean contains(double x, double y, double z, double sizeX, double sizeY, double sizeZ) { return shape.contains(x, y, z, sizeX, sizeY, sizeZ); } @Override public boolean intersects(Rectangle3D r) { return shape.intersects(r); } @Override public boolean intersects(double x, double y, double z, double sizeX, double sizeY, double sizeZ) { return shape.intersects(x, y, z, sizeX, sizeY, sizeZ); } @Override public Rectangle3D computeBounds3D() { return shape.getBounds(); } @Override public boolean canTranslate() { return true; } @Override public void translate(double dx, double dy, double dz) { beginUpdate(); try { synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.translate(dx, dy, dz); } } finally { endUpdate(); } } /** * Called when anchor position changed */ public void controlPointPositionChanged(Anchor3D source) { // anchor(s) position changed --> ROI changed roiChanged(true); } /** * Called when anchor overlay changed */ public void controlPointOverlayChanged(OverlayEvent event) { // we only mind about painter change from anchor... if (event.getType() == OverlayEventType.PAINTER_CHANGED) { // we have a control point selected --> remove focus on ROI if (hasSelectedPoint()) setFocused(false); // anchor changed --> ROI painter changed getOverlay().painterChanged(); } } /** * roi changed */ @Override public void onChanged(CollapsibleEvent object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // refresh shape updateShape(); break; case FOCUS_CHANGED: ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties(); break; case SELECTION_CHANGED: final boolean s = isSelected(); beginUpdate(); try { // set control points visible or not synchronized (controlPoints) { for (Anchor3D pt : controlPoints) pt.setVisible(s); } // unselect if not visible if (!s) unselectAllPoints(); } finally { endUpdate(); } ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties(); break; case PROPERTY_CHANGED: final String property = event.getPropertyName(); if (StringUtil.equals(property, PROPERTY_STROKE) || StringUtil.equals(property, PROPERTY_COLOR) || StringUtil.equals(property, PROPERTY_OPACITY)) ((ROI3DShapePainter) getOverlay()).updateVtkDisplayProperties(); break; default: break; } super.onChanged(object); } @Override public double computeNumberOfPoints() { return 0d; } /** * Rebuild shape.<br> * This method should be overridden by derived classes which<br> * have to call the super.updateShape() method at end. */ protected void updateShape() { // the shape should have been rebuilt here ((ROI3DShapePainter) painter).needRebuild = true; } @Override public boolean loadFromXML(Node node) { beginUpdate(); try { if (!super.loadFromXML(node)) return false; firstMove = false; // unselect all control points unselectAllPoints(); } finally { endUpdate(); } return true; } }