/* * Copyright 2010-2013 Institut Pasteur. * * This file is part of Icy. * * Icy is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Icy is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Icy. If not, see <http://www.gnu.org/licenses/>. */ package plugins.kernel.roi.roi2d; import icy.canvas.IcyCanvas; import icy.canvas.IcyCanvas2D; import icy.common.EventHierarchicalChecker; import icy.painter.Anchor2D; import icy.painter.Anchor2D.Anchor2DPositionListener; import icy.painter.OverlayEvent; import icy.painter.OverlayEvent.OverlayEventType; import icy.painter.OverlayListener; import icy.painter.PainterEvent; import icy.painter.PathAnchor2D; import icy.painter.VtkPainter; import icy.roi.ROI; import icy.roi.ROI2D; import icy.roi.ROIEvent; import icy.roi.edit.Point2DAddedROIEdit; import icy.roi.edit.Point2DMovedROIEdit; import icy.roi.edit.Point2DRemovedROIEdit; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.point.Point5D; import icy.util.EventUtil; import icy.util.GraphicsUtil; import icy.util.ShapeUtil; import icy.util.ShapeUtil.BooleanOperator; import icy.vtk.VtkUtil; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.Area; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import plugins.kernel.canvas.VtkCanvas; import vtk.vtkActor; import vtk.vtkPolyData; import vtk.vtkPolyDataMapper; import vtk.vtkProp; import vtk.vtkSphereSource; /** * @author Stephane */ public abstract class ROI2DShape extends ROI2D implements Shape { public class ROI2DShapePainter extends ROI2DPainter implements VtkPainter, Runnable { // VTK 3D objects, we use Object to prevent UnsatisfiedLinkError Object vtkSource; Object polyMapper; Object actor; // 3D internal boolean needRebuild; double scaling[]; VtkCanvas canvas3d; public ROI2DShapePainter() { super(); // don't create VTK object on constructor vtkSource = null; polyMapper = null; actor = null; scaling = new double[3]; Arrays.fill(scaling, 1d); needRebuild = true; canvas3d = null; } protected void initVtkObjects() { // init 3D painters stuff vtkSource = new vtkPolyData(); polyMapper = new vtkPolyDataMapper(); ((vtkPolyDataMapper) polyMapper).SetInputData((vtkPolyData) vtkSource); actor = new vtkActor(); ((vtkActor) actor).SetMapper((vtkPolyDataMapper) polyMapper); ((vtkActor) actor).GetProperty().SetFrontfaceCulling(1); } /** * update 3D painter for 3D canvas (called only when VTK is loaded). */ protected void rebuildVtkObjects() { final VtkCanvas canvas = canvas3d; // nothing to update if (canvas == null) return; final Sequence seq = canvas.getSequence(); // nothing to update if (seq == null) return; final List<double[]> point3DList = new ArrayList<double[]>(); final List<int[]> polyList = new ArrayList<int[]>(); final double[] coords = new double[6]; // starting position double xm = 0d; double ym = 0d; double x0 = 0d; double y0 = 0d; double x1 = 0d; double y1 = 0d; double z0, z1; int ind; final double curZ = getZ(); // all slices ? if (curZ == -1) { // set object depth on whole volume z0 = 0; z1 = seq.getSizeZ(); } // fixed Z position else { // set Z position z0 = curZ - 0.5; z1 = curZ + 0.5; } // use flat path final PathIterator path = getPathIterator(null, 0.5d); // build point data while (!path.isDone()) { switch (path.currentSegment(coords)) { case PathIterator.SEG_MOVETO: x0 = xm = coords[0]; y0 = ym = coords[1]; break; case PathIterator.SEG_LINETO: x1 = coords[0]; y1 = coords[1]; ind = point3DList.size(); point3DList.add(new double[] {x0, y0, z0}); point3DList.add(new double[] {x1, y1, z0}); point3DList.add(new double[] {x0, y0, z1}); point3DList.add(new double[] {x1, y1, z1}); polyList.add(new int[] {1 + ind, 2 + ind, 0 + ind}); polyList.add(new int[] {3 + ind, 2 + ind, 1 + ind}); x0 = x1; y0 = y1; break; case PathIterator.SEG_CLOSE: x1 = xm; y1 = ym; ind = point3DList.size(); point3DList.add(new double[] {x0, y0, z0}); point3DList.add(new double[] {x1, y1, z0}); point3DList.add(new double[] {x0, y0, z1}); point3DList.add(new double[] {x1, y1, z1}); polyList.add(new int[] {1 + ind, 2 + ind, 0 + ind}); polyList.add(new int[] {3 + ind, 2 + ind, 1 + ind}); x0 = x1; y0 = y1; break; } path.next(); } // convert to array final double[][] vertices = new double[point3DList.size()][3]; final int[][] indexes = new int[polyList.size()][3]; ind = 0; for (double[] pt3D : point3DList) vertices[ind++] = pt3D; ind = 0; for (int[] poly : polyList) indexes[ind++] = poly; // actor can be accessed in canvas3d for rendering so we need to synchronize access canvas3d.lock(); try { ((vtkPolyData) vtkSource).SetPolys(VtkUtil.getCells(polyList.size(), VtkUtil.prepareCells(indexes))); ((vtkPolyData) vtkSource).SetPoints(VtkUtil.getPoints(vertices)); ((vtkPolyDataMapper) polyMapper).Update(); ((vtkActor) actor).SetScale(scaling); ((vtkActor) actor).GetProperty().SetPointSize(getStroke()); final Color color = getColor(); ((vtkActor) actor).GetProperty().SetColor(color.getRed() / 255d, color.getGreen() / 255d, color.getBlue() / 255d); // opacity is for interior only, contour can be done with layer opacity information // ((vtkActor) actor).GetProperty().SetOpacity(getOpacity()); } finally { canvas3d.unlock(); } // no more pending request if (!ThreadUtil.hasWaitingSingleTask(this)) canvas3d = null; } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { if (isActiveFor(canvas)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { ROI2DShape.this.beginUpdate(); try { // send event to controls points first synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.keyPressed(e, imagePoint, canvas); } // specific action for ROI2DShape if (!e.isConsumed()) { final Sequence sequence = canvas.getSequence(); switch (e.getKeyCode()) { case KeyEvent.VK_DELETE: case KeyEvent.VK_BACK_SPACE: final Anchor2D selectedPoint = getSelectedPoint(); // try to remove selected point if (removeSelectedPoint(canvas)) { // consume event e.consume(); // add undo operation if (sequence != null) sequence.addUndoableEdit(new Point2DRemovedROIEdit(ROI2DShape.this, selectedPoint)); } break; } } } finally { ROI2DShape.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 (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { ROI2DShape.this.beginUpdate(); try { // send event to controls points first synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.keyReleased(e, imagePoint, canvas); } } finally { ROI2DShape.this.endUpdate(); } } } } // then send event to parent super.keyReleased(e, imagePoint, canvas); } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { // send event to controls points first if (isActiveFor(canvas)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { ROI2DShape.this.beginUpdate(); try { // default anchor action on mouse pressed synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.mousePressed(e, imagePoint, canvas); } // specific action for this ROI if (!e.isConsumed()) { // 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 Anchor2D point = addNewPoint(imagePoint.toPoint2D(), insertMode); // point added ? if (point != null) { // consume event e.consume(); final Sequence sequence = canvas.getSequence(); // add undo operation if (sequence != null) sequence.addUndoableEdit(new Point2DAddedROIEdit(ROI2DShape.this, point)); } } } } } } finally { ROI2DShape.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)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { final Sequence sequence = canvas.getSequence(); ROI2DShape.this.beginUpdate(); try { // default anchor action on mouse release synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.mouseReleased(e, imagePoint, canvas); } } finally { ROI2DShape.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)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { ROI2DShape.this.beginUpdate(); try { // default anchor action on mouse click synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.mouseClick(e, imagePoint, canvas); } } finally { ROI2DShape.this.endUpdate(); } } } } // then send event to parent super.mouseClick(e, imagePoint, canvas); // not yet consumed... if (!e.isConsumed()) { // and process ROI stuff now if (isActiveFor(canvas)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { // single click if (e.getClickCount() == 1) { // right click action if (EventUtil.isRightMouseButton(e)) { // unselect (don't consume event) if (isSelected()) ROI2DShape.this.setSelected(false); } } } } } } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { // send event to controls points first if (isActiveFor(canvas)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { final Sequence sequence = canvas.getSequence(); ROI2DShape.this.beginUpdate(); try { // default anchor action on mouse drag synchronized (controlPoints) { for (Anchor2D pt : controlPoints) { final Point2D 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 ((savedPosition != null) && !savedPosition.equals(pt.getPosition())) sequence.addUndoableEdit(new Point2DMovedROIEdit(ROI2DShape.this, pt, savedPosition)); } } } finally { ROI2DShape.this.endUpdate(); } } } } // then send event to parent super.mouseDrag(e, imagePoint, canvas); } @Override public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (isSelected() && !isReadOnly()) { // send event to controls points first if (isActiveFor(canvas)) { // check we can do the action if (!(canvas instanceof VtkCanvas) && (imagePoint != null)) { ROI2DShape.this.beginUpdate(); try { // refresh control point state synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.mouseMove(e, imagePoint, canvas); } } finally { ROI2DShape.this.endUpdate(); } } } } // then send event to parent super.mouseMove(e, imagePoint, canvas); } /** * Draw the ROI */ @Override protected void drawROI(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (canvas instanceof IcyCanvas2D) { // not supported if (g == null) return; final Rectangle2D bounds = shape.getBounds2D(); // 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); final boolean small = isSmall(bounds, g, canvas); final boolean tiny = isTiny(bounds, g, canvas); // simplified draw if (small) { if (shapeVisible) { final Graphics2D g2 = (Graphics2D) g.create(); // draw shape g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke))); g2.setColor(getDisplayColor()); g2.draw(shape); if (isSelected()) { g2.fill(shape); if (!tiny) { // draw simplified control points if (!isReadOnly()) { final int ray = (int) canvas.canvasToImageDeltaX(2); for (Anchor2D pt : controlPoints) { if (pt.isVisible()) { // control point content if (pt.isSelected()) g2.setColor(pt.getSelectedColor()); else g2.setColor(pt.getColor()); g2.fillRect((int) pt.getPositionX() - ray, (int) pt.getPositionY() - ray, ray * 2, ray * 2); } } } } } g2.dispose(); } } // normal draw else { // ROI selected ? if (shapeVisible && isSelected()) { final Graphics2D g2 = (Graphics2D) g.create(); final AlphaComposite prevAlpha = (AlphaComposite) g2.getComposite(); float newAlpha = prevAlpha.getAlpha() * getOpacity(); newAlpha = Math.min(1f, newAlpha); newAlpha = Math.max(0f, newAlpha); // show content with an alpha factor g2.setComposite(prevAlpha.derive(newAlpha)); g2.setColor(getDisplayColor()); g2.fill(shape); g2.dispose(); } final Graphics2D g2 = (Graphics2D) g.create(); if (shapeVisible) { if (isSelected()) { // just draw plain object shape without border g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d))); g2.setColor(getDisplayColor()); g2.draw(shape); } else { // draw border g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d))); g2.setColor(Color.black); g2.draw(shape); // draw shape g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke))); g2.setColor(getDisplayColor()); g2.draw(shape); } } // draw from flatten shape as we use it for collision detection // ShapeUtil.drawFromPath(getPathIterator(null, 0.1), g); if (isSelected() && !isReadOnly()) { // draw control point if selected synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.paint(g2, sequence, canvas); } } g2.dispose(); } } if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // 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) { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); // request rebuild 3D objects canvas3d = cnv; ThreadUtil.runSingle(this); needRebuild = false; } // actor can be accessed in canvas3d for rendering so we need to synchronize access cnv.lock(); try { // update visibility if (actor != null) ((vtkActor) actor).SetVisibility(canvas.isVisible(this) ? 1 : 0); } finally { cnv.unlock(); } } } /** * 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 color = getColor(); final Color focusedColor = getFocusedColor(); synchronized (controlPoints) { for (Anchor2D anchor : controlPoints) { anchor.setColor(color); anchor.setSelectedColor(focusedColor); } } if (actor != null) ((vtkActor) actor).GetProperty().SetColor(color.getRed() / 255d, color.getGreen() / 255d, color.getBlue() / 255d); } finally { endUpdate(); } } @Override public void setOpacity(float value) { // opacity is for interior only, contour can be done with layer opacity information // if (actor != null) // ((vtkActor) actor).GetProperty().SetOpacity(value); super.setOpacity(value); } @Override public vtkProp[] getProps() { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); return new vtkProp[] {(vtkProp) actor}; } @Override public void run() { rebuildVtkObjects(); } } class Poly3D { public Poly3D(int p1, int p2, int p3) { this.p1 = p1; this.p2 = p2; this.p3 = p3; } int p1; int p2; int p3; } /** * ROI shape (in image coordinates) */ protected final Shape shape; /** * control points */ protected final List<Anchor2D> controlPoints; /** * internals */ protected final Anchor2DPositionListener anchor2DPositionListener; protected final OverlayListener anchor2DOverlayListener; protected boolean firstMove; public ROI2DShape(Shape shape) { super(); this.shape = shape; controlPoints = new ArrayList<Anchor2D>(); firstMove = true; anchor2DPositionListener = new Anchor2DPositionListener() { @Override public void positionChanged(Anchor2D source) { controlPointPositionChanged(source); } }; anchor2DOverlayListener = new OverlayListener() { @Override public void overlayChanged(OverlayEvent event) { controlPointOverlayChanged(event); } }; } @Override protected ROI2DShapePainter createPainter() { return new ROI2DShapePainter(); } /** * build a new anchor with specified position */ protected Anchor2D createAnchor(Point2D pos) { return new Anchor2D(pos.getX(), pos.getY(), getColor(), getFocusedColor()); } /** * build a new anchor with specified position */ protected Anchor2D createAnchor(double x, double y) { return createAnchor(new Point2D.Double(x, y)); } /** * @return the shape */ public Shape getShape() { return shape; } @Override public void setSelected(boolean value) { // unselected ? --> unselected all control points if (!value) { synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.setSelected(false); } } super.setSelected(value); } /** * Rebuild shape.<br> * This method should be overridden by derived classes which<br> * have to call the super.update() method at end. */ protected void updateShape() { // the shape should have been rebuilt here ((ROI2DShapePainter) painter).needRebuild = true; } protected Anchor2D getSelectedPoint() { synchronized (controlPoints) { for (Anchor2D pt : controlPoints) if (pt.isSelected()) return pt; } return null; } /** * @deprecated Use {@link #getSelectedPoint()} instead. */ @Deprecated protected Anchor2D getSelectedControlPoint() { return getSelectedPoint(); } @Override public boolean hasSelectedPoint() { return (getSelectedPoint() != null); } /** * 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(Anchor2D pt) { addPoint(pt, -1); } /** * Internal use only, use {@link #addNewPoint(Point2D, boolean)} instead. */ public void addPoint(Anchor2D pt, int index) { pt.addPositionListener(anchor2DPositionListener); pt.addOverlayListener(anchor2DOverlayListener); if (index == -1) controlPoints.add(pt); else controlPoints.add(index, pt); roiChanged(); } /** * @deprecated Use {@link #addNewPoint(Point2D, boolean)} instead. */ @Deprecated public boolean addPoint(Point2D pos, boolean insert) { return (addNewPoint(pos, insert) != null); } /** * @deprecated Use {@link #addNewPoint(Point2D, boolean)} instead. */ @Deprecated public boolean addPointAt(Point2D pos, boolean insert) { return (addNewPoint(pos, insert) != null); } /** * Add a new point to this shape 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 Anchor2D point if the operation succeed or <code>null</code> * otherwise (if the ROI does not support this operation for instance) */ public Anchor2D addNewPoint(Point2D pos, boolean insert) { if (!canAddPoint()) return null; final Anchor2D 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 */ @SuppressWarnings("unused") protected boolean removePoint(IcyCanvas canvas, Anchor2D pt) { boolean empty; pt.removeOverlayListener(anchor2DOverlayListener); pt.removePositionListener(anchor2DPositionListener); synchronized (controlPoints) { controlPoints.remove(pt); empty = controlPoints.isEmpty(); } // empty ROI ? --> remove from all sequence if (empty) remove(); else roiChanged(); 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(Anchor2D pt) { return removePoint(null, pt); } /** * internal use only (used for fast clear) */ protected void removeAllPoint() { synchronized (controlPoints) { for (Anchor2D pt : controlPoints) { pt.removeOverlayListener(anchor2DOverlayListener); pt.removePositionListener(anchor2DPositionListener); } controlPoints.clear(); } } /** * @deprecated Use {@link #removeSelectedPoint(IcyCanvas)} instead. */ @Deprecated public boolean removePointAt(IcyCanvas canvas, Point2D imagePoint) { if (!canRemovePoint()) return false; // first we try to remove selected point if (!removeSelectedPoint(canvas)) { // if no point selected, try to select and remove a point at specified position if (selectPointAt(canvas, imagePoint)) return removeSelectedPoint(canvas); return false; } return true; } /** * @deprecated Use {@link #removeSelectedPoint(IcyCanvas)} instead. */ @Deprecated protected boolean removeSelectedPoint(IcyCanvas canvas, @SuppressWarnings("unused") Point2D imagePoint) { return removeSelectedPoint(canvas); } /** * Remove the current selected point. */ public boolean removeSelectedPoint(IcyCanvas canvas) { if (!canRemovePoint()) return false; final Anchor2D selectedPoint = getSelectedPoint(); if (selectedPoint == null) return false; synchronized (controlPoints) { final int index = controlPoints.indexOf(selectedPoint); // try to remove point if (!removePoint(canvas, selectedPoint)) return false; // still have control points if (controlPoints.size() > 0) { // save the point position final Point2D imagePoint = selectedPoint.getPosition(); // we are using PathAnchor2D ? if (selectedPoint instanceof PathAnchor2D) { final PathAnchor2D selectedPathPoint = (PathAnchor2D) selectedPoint; switch (selectedPathPoint.getType()) { // we removed a MOVETO point ? case PathIterator.SEG_MOVETO: // try to set next point to MOVETO state if (index < controlPoints.size()) { final PathAnchor2D nextPoint = (PathAnchor2D) controlPoints.get(index); // next point is a CLOSE one ? if (nextPoint.getType() == PathIterator.SEG_CLOSE) { // delete it if (removePoint(canvas, nextPoint)) { // it was the last control point --> delete ROI if (controlPoints.size() == 0) remove(); } } else { // whatever is next point, set it to MOVETO nextPoint.setType(PathIterator.SEG_MOVETO); nextPoint.setVisible(true); } } break; // we removed a CLOSE point ? case PathIterator.SEG_CLOSE: // try to set previous point to CLOSE state if (index > 0) { final PathAnchor2D prevPoint = (PathAnchor2D) controlPoints.get(index - 1); // next point is a MOVETO one ? if (prevPoint.getType() == PathIterator.SEG_MOVETO) { // delete it if (removePoint(canvas, prevPoint)) { // it was the last control point --> delete ROI if (controlPoints.size() == 0) remove(); } } else { // whatever is previous point, set it to CLOSE prevPoint.setType(PathIterator.SEG_CLOSE); prevPoint.setVisible(false); } } break; } } // select a new point if possible if (controlPoints.size() > 0) selectPointAt(canvas, imagePoint); } } return true; } protected boolean selectPointAt(IcyCanvas canvas, Point2D imagePoint) { synchronized (controlPoints) { // find the new selected control point for (Anchor2D pt : controlPoints) { // control point is overlapped ? if (pt.isOver(canvas, imagePoint)) { // select it pt.setSelected(true); return true; } } } return false; } /** * Return total distance of the specified list of points. */ protected double getTotalDistance(List<Point2D> points, boolean connectLastPoint) { final int size = points.size(); double result = 0d; if (size > 1) { for (int i = 0; i < size - 1; i++) result += points.get(i).distance(points.get(i + 1)); // add last to first point distance if (connectLastPoint) result += points.get(size - 1).distance(points.get(0)); } return result; } /** * Return total distance of the specified list of points. */ protected double getTotalDistance(List<Point2D> points) { // by default the total length need last point connection return getTotalDistance(points, true); } /** * Find best insert position for specified point */ protected int getInsertPointPosition(Point2D pos) { final List<Point2D> points = getPoints(); 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); // minimum distance ? if (d < minDistance) { // save index minDistance = d; result = i; } // remove point from current position points.remove(i); } return result; } /** * Returns true if specified point coordinates overlap the ROI edge. */ @Override public boolean isOverEdge(IcyCanvas canvas, double x, double y) { // use bigger stroke for isOver test for easier intersection final double strk = painter.getAdjustedStroke(canvas) * 3; final Rectangle2D rect = new Rectangle2D.Double(x - (strk * 0.5), y - (strk * 0.5), strk, strk); final Rectangle2D roiBounds = getBounds2D(); // special test for empty object (point or orthogonal line) if (roiBounds.isEmpty()) return rect.intersectsLine(roiBounds.getMinX(), roiBounds.getMinY(), roiBounds.getMaxX(), roiBounds.getMaxY()); // fast intersect test to start with if (roiBounds.intersects(rect)) // use flatten path, intersects on curved shape return incorrect result return ShapeUtil.pathIntersects(getPathIterator(null, 0.1), rect); return false; } // @Override // public boolean isOverPoint(IcyCanvas canvas, double x, double y) // { // if (isSelected()) // { // for (Anchor2D pt : controlPoints) // if (pt.isOver(canvas, x, y)) // return true; // } // // return false; // } /** * Return the list of control points for this ROI. */ public List<Anchor2D> getControlPoints() { synchronized (controlPoints) { return new ArrayList<Anchor2D>(controlPoints); } } /** * Return the list of positions of control points for this ROI. */ public ArrayList<Point2D> getPoints() { final ArrayList<Point2D> result = new ArrayList<Point2D>(); synchronized (controlPoints) { for (Anchor2D pt : controlPoints) result.add(pt.getPosition()); } return result; } @Override public PathIterator getPathIterator(AffineTransform at) { return shape.getPathIterator(at); } @Override public PathIterator getPathIterator(AffineTransform at, double flatness) { return shape.getPathIterator(at, flatness); } @Override public boolean contains(Point2D p) { return shape.contains(p); } @Override public boolean contains(Rectangle2D r) { return shape.contains(r); } @Override public boolean contains(double x, double y) { return shape.contains(x, y); } @Override public boolean contains(double x, double y, double w, double h) { return shape.contains(x, y, w, h); } @Override public boolean contains(ROI roi) { if (roi instanceof ROI2DShape) // if the union of both ROI equals base ROI then base ROI contains the other one return ShapeUtil.union(shape, ((ROI2DShape) roi).shape).equals(new Area(shape)); return super.contains(roi); } @Override public boolean intersects(Rectangle2D r) { return shape.intersects(r); } @Override public boolean intersects(double x, double y, double w, double h) { return shape.intersects(x, y, w, h); } @Override public boolean intersects(ROI roi) { if (roi instanceof ROI2DShape) // if the intersection of both ROI is not empty return !ShapeUtil.intersect(shape, ((ROI2DShape) roi).shape).isEmpty(); return super.intersects(roi); } @Override public Rectangle2D computeBounds2D() { return shape.getBounds2D(); } @Override protected ROI computeOperation(ROI roi, BooleanOperator op) throws UnsupportedOperationException { if (roi instanceof ROI2DShape) { final ROI2DShape roiShape = (ROI2DShape) roi; ROI2DPath result = null; // only if on same position if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC())) { // special case for subtraction if (op == null) result = new ROI2DPath(ShapeUtil.subtract(this, roiShape)); else if (op == BooleanOperator.AND) result = new ROI2DPath(ShapeUtil.intersect(this, roiShape)); else if (op == BooleanOperator.OR) result = new ROI2DPath(ShapeUtil.union(this, roiShape)); else if (op == BooleanOperator.XOR) result = new ROI2DPath(ShapeUtil.exclusiveUnion(this, roiShape)); } if (result != null) { // don't forget to restore 5D position result.setZ(getZ()); result.setT(getT()); result.setC(getC()); return result; } } return super.computeOperation(roi, op); } @Override public boolean canTranslate() { return true; } @Override public void translate(double dx, double dy) { beginUpdate(); try { synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.translate(dx, dy); } } finally { endUpdate(); } } /** * Called when anchor position changed */ public void controlPointPositionChanged(Anchor2D source) { // anchor(s) position changed --> ROI changed roiChanged(); } /** * 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(); } } /** * Called when anchor painter changed, provided only for backward compatibility.<br> * Don't use it. */ @SuppressWarnings({"deprecation", "unused"}) public void painterChanged(PainterEvent event) { // ignore it now } /** * roi changed */ @Override public void onChanged(EventHierarchicalChecker object) { final ROIEvent event = (ROIEvent) object; // do here global process on ROI change switch (event.getType()) { case ROI_CHANGED: // refresh shape updateShape(); break; } super.onChanged(object); } }