/* * Copyright 2010-2015 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 java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.Shape; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.awt.image.DataBufferByte; 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.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.Point2DUtil; import icy.type.point.Point5D; 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; /** * @author Stephane */ public abstract class ROI2DShape extends ROI2D implements Shape { public class ROI2DShapePainter extends ROI2DPainter 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<Anchor2D> actorsToAdd; protected Set<Anchor2D> actorsToRemove; public ROI2DShapePainter() { 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<Anchor2D>(); actorsToRemove = new HashSet<Anchor2D>(); 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 bounds final double xs = scaling[0]; final double ys = scaling[1]; final double zs = scaling[2]; double z0, z1; final double curZ = getZ(); // all slices ? if (curZ == -1d) { // set object depth on whole volume z0 = 0; z1 = seq.getSizeZ() * zs; } // fixed Z position else { // set Z position z0 = curZ * zs; z1 = (curZ + 1d) * zs; // z0 = (curZ - 0.5) * scaling[2]; // z1 = (curZ + 0.5) * scaling[2]; } // update polydata object 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; int ind; // 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] * xs; y0 = ym = coords[1] * ys; break; case PathIterator.SEG_LINETO: x1 = coords[0] * xs; y1 = coords[1] * ys; 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; final vtkCellArray previousCells = vCells; final vtkPoints previousPoints = vPoints; vCells = VtkUtil.getCells(polyList.size(), VtkUtil.prepareCells(indexes)); vPoints = VtkUtil.getPoints(vertices); final Rectangle2D bounds = getBounds2D(); // 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, z0, z1, canvas); outlineMapper.Update(); // update polygon data from cell and points polyData.SetPolys(vCells); polyData.SetPoints(vPoints); 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); // 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)) { ROI2DShape.this.beginUpdate(); try { // get control points list final List<Anchor2D> controlPoints = getControlPoints(); // send event to controls points first 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, controlPoints, 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)) { 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 (isActiveFor(canvas)) { // check we can do the action if (isSelected() && !isReadOnly()) { ROI2DShape.this.beginUpdate(); try { // send event to controls points first synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.mousePressed(e, imagePoint, canvas); } // specific action for this ROI if (!e.isConsumed()) { // add point operation not supported on VtkCanvas (it could be but we don't want it) if (canvas instanceof VtkCanvas) return; // we need it if (imagePoint == null) return; // 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)) { 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)) { 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); } @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 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 ((sequence != null) && (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 (isActiveFor(canvas)) { // check we can do the action if (isSelected() && !isReadOnly()) { // send event to controls points first 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 public 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); if (shapeVisible) { final boolean small = isSmall(bounds, g, canvas); final boolean tiny = isTiny(bounds, g, canvas); // draw shape drawShape(g, sequence, canvas, small); // draw control points (only if not tiny) if (!tiny && isSelected() && !isReadOnly()) { // draw control point if selected synchronized (controlPoints) { for (Anchor2D 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 (Anchor2D anchor : actorsToRemove) for (vtkProp prop : anchor.getProps()) VtkUtil.removeProp(renderer, prop); // done actorsToRemove.clear(); } // need to add control points actor ? synchronized (actorsToAdd) { for (Anchor2D 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 (Anchor2D pt : controlPoints) pt.paint(null, sequence, canvas); } } } /** * Draw the shape */ protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified) { drawShape(g, sequence, canvas, shape, simplified); } /** * Draw the shape */ protected void drawShape(Graphics2D g, Sequence sequence, IcyCanvas canvas, Shape shape, boolean simplified) { final Graphics2D g2 = (Graphics2D) g.create(); // simplified draw if (simplified) { g2.setColor(getDisplayColor()); // fill content if selected if (isSelected()) g2.fill(shape); // then draw shape g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke))); g2.draw(shape); } // normal draw else { // ROI selected and has content to draw ? if (isSelected()) { 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()); // only fill closed shape g2.fill(ShapeUtil.getClosedPath(shape)); // restore composite and set stroke g2.setComposite(prevAlpha); g2.setStroke(new BasicStroke((float) ROI.getAdjustedStroke(canvas, stroke + 1d))); // then draw object shape without border 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); } } 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 (Anchor2D 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 (Anchor2D pt : controlPoints) for (vtkProp prop : pt.getProps()) result.add(prop); } return result.toArray(new vtkProp[result.size()]); } @Override public void run() { rebuildVtkObjects(); } } /** * 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 public String getDefaultName() { return "Shape2D"; } @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; } /** * 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() { final int z = getZ(); beginUpdate(); try { // fix control points Z position if needed synchronized (controlPoints) { for (Anchor2D points : controlPoints) points.setZ(z); } } finally { endUpdate(); } // the shape should have been rebuilt here ((ROI2DShapePainter) painter).needRebuild = true; } /** * 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) { // set Z position pt.setZ(getZ()); // set visible state pt.setVisible(isSelected()); // add listeners pt.addPositionListener(anchor2DPositionListener); pt.addOverlayListener(anchor2DOverlayListener); synchronized (controlPoints) { if (index == -1) controlPoints.add(pt); else controlPoints.add(index, pt); } synchronized (((ROI2DShapePainter) getOverlay()).actorsToAdd) { // store it in the "actor to add" list ((ROI2DShapePainter) getOverlay()).actorsToAdd.add(pt); } synchronized (((ROI2DShapePainter) getOverlay()).actorsToRemove) { // and remove it from the "actor to remove" list ((ROI2DShapePainter) getOverlay()).actorsToRemove.remove(pt); } roiChanged(true); } /** * @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 * @param select * select the new created point * @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, boolean select) { 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 if (select) pt.setSelected(true); return pt; } /** * 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) { return addNewPoint(pos, insert, true); } /** * internal use only */ protected boolean removePoint(IcyCanvas canvas, Anchor2D pt) { boolean empty; pt.removeOverlayListener(anchor2DOverlayListener); pt.removePositionListener(anchor2DPositionListener); synchronized (controlPoints) { controlPoints.remove(pt); empty = controlPoints.isEmpty(); } synchronized (((ROI2DShapePainter) getOverlay()).actorsToRemove) { // store it in the "actor to remove" list ((ROI2DShapePainter) getOverlay()).actorsToRemove.add(pt); } synchronized (((ROI2DShapePainter) getOverlay()).actorsToAdd) { // and remove it from the "actor to add" list ((ROI2DShapePainter) 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(Anchor2D pt) { return removePoint(null, pt); } /** * internal use only (used for fast clear) */ protected void removeAllPoint() { synchronized (controlPoints) { synchronized (((ROI2DShapePainter) getOverlay()).actorsToRemove) { // store all points in the "actor to remove" list ((ROI2DShapePainter) getOverlay()).actorsToRemove.addAll(controlPoints); } synchronized (((ROI2DShapePainter) getOverlay()).actorsToAdd) { // and remove them from the "actor to add" list ((ROI2DShapePainter) getOverlay()).actorsToAdd.removeAll(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, 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.isEmpty()) { // 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); } 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); } break; } } // select a new point if possible if (controlPoints.size() > 0) selectPointAt(canvas, imagePoint); } } return 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); } 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; } @Override public void unselectAllPoints() { beginUpdate(); try { synchronized (controlPoints) { // unselect all point for (Anchor2D pt : controlPoints) pt.setSelected(false); } } finally { endUpdate(); } }; @SuppressWarnings("static-method") protected double getTotalDistance(List<Point2D> points, double factorX, double factorY) { // default implementation of total length connect the last point return Point2DUtil.getTotalDistance(points, factorX, factorY, true); } // default implementation for ROI2DShape @Override public double computeNumberOfContourPoints() { return getTotalDistance(getPointsInternal(), 1d, 1d); } @Override public double getLength(Sequence sequence) throws UnsupportedOperationException { // cannot be cached because dependent from Sequence metadata return getTotalDistance(getPointsInternal(), sequence.getPixelSizeX(), sequence.getPixelSizeY()); } /** * Find best insert position for specified point */ protected int getInsertPointPosition(Point2D pos) { final List<Point2D> 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); // 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; } /** * Return the list of positions of control points for this ROI.<br> * This is the direct internal position reference, don't modify them ! */ protected ArrayList<Point2D> getPointsInternal() { final ArrayList<Point2D> result = new ArrayList<Point2D>(); synchronized (controlPoints) { for (Anchor2D pt : controlPoints) result.add(pt.getPositionInternal()); } 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[] getBooleanMask(int x, int y, int width, int height, boolean inclusive) { if ((width <= 0) || (height <= 0)) return new boolean[0]; final BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); final Graphics2D g = img.createGraphics(); // we want accurate rendering as we use the image for the mask g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); // translate back to origin and pixel center g.translate(-x, -y); // fill content g.setColor(Color.white); // only fill closed shapes g.fill(ShapeUtil.getClosedPath(shape)); // we want edge as well if (inclusive) g.draw(shape); // TODO: do we really need that ?? else { // remove edge from content as fill operation may be a bit off g.setColor(Color.black); g.draw(shape); } g.dispose(); final byte[] buffer = ((DataBufferByte) img.getRaster().getDataBuffer()).getData(); final boolean[] result = new boolean[width * height]; // compute mask from image for (int i = 0; i < result.length; i++) result[i] = (buffer[i] != 0); return result; } @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 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 Rectangle2D computeBounds2D() { return shape.getBounds2D(); } @Override public ROI getUnion(ROI roi) throws UnsupportedOperationException { if (roi instanceof ROI2DShape) { final ROI2DShape roiShape = (ROI2DShape) roi; // only if on same position if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC())) { final ROI2DPath result = new ROI2DPath(ShapeUtil.union(this, roiShape)); // don't forget to restore 5D position result.setZ(getZ()); result.setT(getT()); result.setC(getC()); return result; } } return super.getUnion(roi); } @Override public ROI getIntersection(ROI roi) throws UnsupportedOperationException { if (roi instanceof ROI2DShape) { final ROI2DShape roiShape = (ROI2DShape) roi; // only if on same position if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC())) { final ROI2DPath result = new ROI2DPath(ShapeUtil.intersect(this, roiShape)); // don't forget to restore 5D position result.setZ(getZ()); result.setT(getT()); result.setC(getC()); return result; } } return super.getIntersection(roi); } @Override public ROI getExclusiveUnion(ROI roi) throws UnsupportedOperationException { if (roi instanceof ROI2DShape) { final ROI2DShape roiShape = (ROI2DShape) roi; // only if on same position if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC())) { final ROI2DPath result = new ROI2DPath(ShapeUtil.exclusiveUnion(this, roiShape)); // don't forget to restore 5D position result.setZ(getZ()); result.setT(getT()); result.setC(getC()); return result; } } return super.getExclusiveUnion(roi); } @Override public ROI getSubtraction(ROI roi) throws UnsupportedOperationException { if (roi instanceof ROI2DShape) { final ROI2DShape roiShape = (ROI2DShape) roi; // only if on same position if ((getZ() == roiShape.getZ()) && (getT() == roiShape.getT()) && (getC() == roiShape.getC())) { final ROI2DPath result = new ROI2DPath(ShapeUtil.subtract(this, roiShape)); // don't forget to restore 5D position result.setZ(getZ()); result.setT(getT()); result.setC(getC()); return result; } } return super.getSubtraction(roi); } @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(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(); } } /** * Called when anchor painter changed, provided only for backward compatibility.<br> * Don't use it. */ @SuppressWarnings({"deprecation"}) public void painterChanged(PainterEvent event) { // ignore it now } /** * 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: ((ROI2DShapePainter) getOverlay()).updateVtkDisplayProperties(); break; case SELECTION_CHANGED: final boolean s = isSelected(); beginUpdate(); try { // set control points visible or not synchronized (controlPoints) { for (Anchor2D pt : controlPoints) pt.setVisible(s); } // unselect if not visible if (!s) unselectAllPoints(); } finally { endUpdate(); } ((ROI2DShapePainter) 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)) ((ROI2DShapePainter) getOverlay()).updateVtkDisplayProperties(); break; default: break; } super.onChanged(object); } @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; } }