/* * 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 icy.painter; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Ellipse2D; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.EventListener; import java.util.List; import org.w3c.dom.Node; import icy.canvas.IcyCanvas; import icy.canvas.IcyCanvas2D; import icy.common.CollapsibleEvent; import icy.sequence.Sequence; import icy.system.thread.ThreadUtil; import icy.type.point.Point3D; import icy.type.point.Point5D; import icy.util.EventUtil; import icy.util.GraphicsUtil; import icy.util.ShapeUtil; import icy.util.XMLUtil; import icy.vtk.IcyVtkPanel; import plugins.kernel.canvas.VtkCanvas; import vtk.vtkActor; import vtk.vtkInformation; import vtk.vtkPolyDataMapper; import vtk.vtkProp; import vtk.vtkSphereSource; /** * Anchor3D class, used for 3D point control. * * @author Stephane */ public class Anchor3D extends Overlay implements VtkPainter, Runnable { /** * Interface to listen Anchor3D position change */ public static interface Anchor3DPositionListener extends EventListener { public void positionChanged(Anchor3D source); } public static class Anchor3DEvent implements CollapsibleEvent { private final Anchor3D source; public Anchor3DEvent(Anchor3D source) { super(); this.source = source; } /** * @return the source */ public Anchor3D getSource() { return source; } @Override public boolean collapse(CollapsibleEvent event) { if (equals(event)) { // nothing to do here return true; } return false; } @Override public int hashCode() { return source.hashCode(); } @Override public boolean equals(Object obj) { if (obj == this) return true; if (obj instanceof Anchor3DEvent) { final Anchor3DEvent event = (Anchor3DEvent) obj; return (event.getSource() == source); } return super.equals(obj); } } protected static final String ID_COLOR = "color"; protected static final String ID_SELECTEDCOLOR = "selected_color"; protected static final String ID_SELECTED = "selected"; protected static final String ID_POS_X = "pos_x"; protected static final String ID_POS_Y = "pos_y"; protected static final String ID_POS_Z = "pos_z"; protected static final String ID_RAY = "ray"; protected static final String ID_VISIBLE = "visible"; public static final int DEFAULT_RAY = 6; public static final Color DEFAULT_NORMAL_COLOR = Color.GREEN; public static final Color DEFAULT_SELECTED_COLOR = Color.WHITE; /** * position (canvas) */ protected final Point3D.Double position; /** * radius (integer as we express it in pixel) */ protected int ray; /** * color */ protected Color color; /** * selection color */ protected Color selectedColor; /** * selection flag */ protected boolean selected; /** * flag that indicate if anchor is visible */ protected boolean visible; // drag internals protected Point3D startDragMousePosition; protected Point3D startDragPainterPosition; // 2D shape for X,Y contains test protected final Ellipse2D ellipse; // VTK 3D objects vtkSphereSource vtkSource; protected vtkPolyDataMapper polyMapper; protected vtkActor actor; protected vtkInformation vtkInfo; // 3D internal protected boolean needRebuild; protected boolean needPropertiesUpdate; protected double scaling[]; protected WeakReference<VtkCanvas> canvas3d; // listeners protected final List<Anchor3DPositionListener> anchor3DPositionlisteners; public Anchor3D(double x, double y, double z, int ray, Color color, Color selectedColor) { super("Anchor", OverlayPriority.SHAPE_NORMAL); position = new Point3D.Double(x, y, z); this.ray = ray; this.color = color; this.selectedColor = selectedColor; selected = false; visible = true; startDragMousePosition = null; startDragPainterPosition = null; ellipse = new Ellipse2D.Double(); vtkSource = null; polyMapper = null; actor = null; vtkInfo = null; scaling = new double[3]; Arrays.fill(scaling, 1d); needRebuild = true; needPropertiesUpdate = false; canvas3d = new WeakReference<VtkCanvas>(null); anchor3DPositionlisteners = new ArrayList<Anchor3DPositionListener>(); } public Anchor3D(double x, double y, double z, Color color, Color selectedColor) { this(x, y, z, DEFAULT_RAY, color, selectedColor); } public Anchor3D(double x, double y, double z, int ray) { this(x, y, z, ray, DEFAULT_NORMAL_COLOR, DEFAULT_SELECTED_COLOR); } public Anchor3D(double x, double y, double z) { this(x, y, z, DEFAULT_RAY, DEFAULT_NORMAL_COLOR, DEFAULT_SELECTED_COLOR); } public Anchor3D() { this(0d, 0d, 0d, DEFAULT_RAY, DEFAULT_NORMAL_COLOR, DEFAULT_SELECTED_COLOR); } @Override protected void finalize() throws Throwable { super.finalize(); // release allocated VTK resources if (vtkSource != null) vtkSource.Delete(); if (actor != null) { actor.SetPropertyKeys(null); actor.Delete(); } if (vtkInfo != null) { vtkInfo.Remove(VtkCanvas.visibilityKey); vtkInfo.Delete(); } if (polyMapper != null) polyMapper.Delete(); } /** * @return X coordinate position */ public double getX() { return position.x; } /** * Sets the X coordinate position */ public void setX(double value) { setPosition(value, position.y, position.z); } /** * @return Y coordinate position */ public double getY() { return position.y; } /** * Sets the Y coordinate position */ public void setY(double value) { setPosition(position.x, value, position.z); } /** * @return Z coordinate position */ public double getZ() { return position.z; } /** * Sets the Z coordinate position */ public void setZ(double value) { setPosition(position.x, position.y, value); } /** * Get anchor position (return the internal reference) */ public Point3D getPositionInternal() { return position; } /** * Get anchor position */ public Point3D getPosition() { return new Point3D.Double(position.x, position.y, position.z); } /** * Sets anchor position */ public void setPosition(Point3D p) { setPosition(p.getX(), p.getY(), p.getZ()); } /** * Sets anchor position */ public void setPosition(double x, double y, double z) { if ((position.x != x) || (position.y != y) || (position.z != z)) { position.x = x; position.y = y; position.z = z; needRebuild = true; positionChanged(); painterChanged(); } } /** * Performs a translation on the anchor position */ public void translate(double dx, double dy, double dz) { setPosition(position.x + dx, position.y + dy, position.z + dz); } /** * @return the ray */ public int getRay() { return ray; } /** * Sets the ray */ public void setRay(int value) { if (ray != value) { ray = value; needRebuild = true; painterChanged(); } } /** * @return the color */ public Color getColor() { return color; } /** * Sets the color */ public void setColor(Color value) { if (color != value) { color = value; needPropertiesUpdate = true; painterChanged(); } } /** * @return the <code>selected</code> state color */ public Color getSelectedColor() { return selectedColor; } /** * Sets the <code>selected</code> state color */ public void setSelectedColor(Color value) { if (selectedColor != value) { selectedColor = value; needPropertiesUpdate = true; painterChanged(); } } /** * @return the <code>selected</code> state */ public boolean isSelected() { return selected; } /** * Sets the <code>selected</code> state */ public void setSelected(boolean value) { if (selected != value) { selected = value; // end drag if (!value) startDragMousePosition = null; needPropertiesUpdate = true; painterChanged(); } } /** * @return the visible */ public boolean isVisible() { return visible; } /** * @param value * the visible to set */ public void setVisible(boolean value) { if (visible != value) { visible = value; needPropertiesUpdate = true; painterChanged(); } } /** * Returns <code>true</code> if specified Point3D is over the anchor in the specified canvas */ public boolean isOver(IcyCanvas canvas, Point3D imagePoint) { updateEllipseForCanvas(canvas); // specific VTK canvas processing if (canvas instanceof VtkCanvas) { // faster to use picked object return (actor != null) && (actor == ((VtkCanvas) canvas).getPickedObject()); } // at this point we need image position if (imagePoint == null) return false; final double x = imagePoint.getX(); final double y = imagePoint.getY(); final double z = imagePoint.getZ(); // default processing for other canvas final int cnvZ = canvas.getPositionZ(); // same Z position ? if ((cnvZ == -1) || (z == -1d) || (cnvZ == (int) z)) { // fast contains test to start with if (ellipse.getBounds2D().contains(x, y)) return ellipse.contains(x, y); } return false; } /** * Returns adjusted ray for specified Canvas */ protected double getAdjRay(IcyCanvas canvas) { // assume X dimension is ok here return canvas.canvasToImageLogDeltaX(ray); } /** * Update internal ellipse for specified Canvas */ protected void updateEllipseForCanvas(IcyCanvas canvas) { // specific VTK canvas processing if (canvas instanceof VtkCanvas) { // 3D canvas final VtkCanvas cnv = (VtkCanvas) canvas; // update reference if needed if (canvas3d.get() != cnv) canvas3d = new WeakReference<VtkCanvas>(cnv); // 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; } if (needRebuild) { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); // request rebuild 3D objects ThreadUtil.runSingle(this); needRebuild = false; } if (needPropertiesUpdate) { updateVtkDisplayProperties(); needPropertiesUpdate = false; } // update sphere radius updateVtkRadius(); } else { final double adjRay = getAdjRay(canvas); ellipse.setFrame(position.x - adjRay, position.y - adjRay, adjRay * 2, adjRay * 2); } } protected boolean updateDrag(InputEvent e, double x, double y, double z) { // not dragging --> exit if (startDragMousePosition == null) return false; double dx = x - startDragMousePosition.getX(); double dy = y - startDragMousePosition.getY(); double dz = z - startDragMousePosition.getZ(); // shift action --> limit to one direction if (EventUtil.isShiftDown(e)) { // X or Z drag if (Math.abs(dx) > Math.abs(dy)) { dy = 0d; // Z drag if (Math.abs(dz) > Math.abs(dx)) dx = 0d; else dz = 0d; } // Y or Z drag else { dx = 0d; // Z drag if (Math.abs(dz) > Math.abs(dy)) dy = 0d; else dz = 0d; } } // set new position setPosition(new Point3D.Double(startDragPainterPosition.getX() + dx, startDragPainterPosition.getY() + dy, startDragPainterPosition.getZ() + dz)); return true; } protected boolean updateDrag(InputEvent e, Point3D pt) { return updateDrag(e, pt.getX(), pt.getY(), pt.getZ()); } /** * called when anchor position has changed */ protected void positionChanged() { updater.changed(new Anchor3DEvent(this)); } @Override public void onChanged(CollapsibleEvent object) { // we got a position change event if (object instanceof Anchor3DEvent) { firePositionChangedEvent(((Anchor3DEvent) object).getSource()); return; } super.onChanged(object); } protected void firePositionChangedEvent(Anchor3D source) { for (Anchor3DPositionListener listener : new ArrayList<Anchor3DPositionListener>(anchor3DPositionlisteners)) listener.positionChanged(source); } public void addPositionListener(Anchor3DPositionListener listener) { anchor3DPositionlisteners.add(listener); } public void removePositionListener(Anchor3DPositionListener listener) { anchor3DPositionlisteners.remove(listener); } protected void initVtkObjects() { // init 3D painters stuff vtkSource = new vtkSphereSource(); vtkSource.SetRadius(getRay()); vtkSource.SetThetaResolution(12); vtkSource.SetPhiResolution(12); polyMapper = new vtkPolyDataMapper(); polyMapper.SetInputConnection((vtkSource).GetOutputPort()); actor = new vtkActor(); actor.SetMapper(polyMapper); // 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 actor.SetPropertyKeys(vtkInfo); // initialize color final Color col = getColor(); actor.GetProperty().SetColor(col.getRed() / 255d, col.getGreen() / 255d, col.getBlue() / 255d); } /** * update 3D painter for 3D canvas (called only when VTK is loaded). */ protected boolean rebuildVtkObjects() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return false; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return false; final Point3D pos = getPosition(); // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { // need to handle scaling on radius and position to keep a "round" sphere (else we obtain ellipsoid) vtkSource.SetCenter(pos.getX() * scaling[0], pos.getY() * scaling[1], pos.getZ() * scaling[2]); vtkSource.Update(); polyMapper.Update(); // actor.SetScale(scaling[0], scaling[1], scaling[0]); } finally { vtkPanel.unlock(); } // need to repaint painterChanged(); return true; } protected void updateVtkDisplayProperties() { if (actor == null) return; final VtkCanvas cnv = canvas3d.get(); final Color col = isSelected() ? getSelectedColor() : getColor(); final double r = col.getRed() / 255d; final double g = col.getGreen() / 255d; final double b = col.getBlue() / 255d; // 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 { actor.GetProperty().SetColor(r, g, b); if (isVisible()) { actor.SetVisibility(1); vtkInfo.Set(VtkCanvas.visibilityKey, 1); } else { actor.SetVisibility(0); vtkInfo.Set(VtkCanvas.visibilityKey, 0); } } finally { if (vtkPanel != null) vtkPanel.unlock(); } // need to repaint painterChanged(); } protected void updateVtkRadius() { final VtkCanvas canvas = canvas3d.get(); // canvas was closed if (canvas == null) return; final IcyVtkPanel vtkPanel = canvas.getVtkPanel(); // canvas was closed if (vtkPanel == null) return; if (vtkSource == null) return; // update sphere radius base on canvas scale X final double radius = getAdjRay(canvas) * scaling[0]; if (vtkSource.GetRadius() != radius) { // actor can be accessed in canvas3d for rendering so we need to synchronize access vtkPanel.lock(); try { vtkSource.SetRadius(radius); vtkSource.Update(); } finally { vtkPanel.unlock(); } // need to repaint painterChanged(); } } @Override public vtkProp[] getProps() { // initialize VTK objects if not yet done if (actor == null) initVtkObjects(); return new vtkActor[] {actor}; } @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { paint(g, sequence, canvas, false); } public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas, boolean simplified) { // this will update VTK objects if needed updateEllipseForCanvas(canvas); if (canvas instanceof IcyCanvas2D) { // nothing to do here when not visible if (!isVisible()) return; // get canvas Z position final int cnvZ = canvas.getPositionZ(); // get delta Z (difference between canvas Z position and anchor Z pos) final int dz = Math.abs(((int) getZ()) - cnvZ); // calculate z fade range final int zRange = Math.min(10, Math.max(3, sequence.getSizeZ() / 10)); // not visible on this Z position if (dz > zRange) return; // trivial paint optimization final boolean shapeVisible = ShapeUtil.isVisible(g, ellipse); if (shapeVisible) { final Graphics2D g2 = (Graphics2D) g.create(); // ration for size / opacity final float ratio = 1f - ((float) dz / (float) zRange); if (ratio != 1f) GraphicsUtil.mixAlpha(g2, ratio); // draw content if (isSelected()) g2.setColor(getSelectedColor()); else g2.setColor(getColor()); // simplified small drawing if (simplified) { final int ray = (int) canvas.canvasToImageDeltaX(2); g2.fillRect((int) getX() - ray, (int) getY() - ray, ray * 2, ray * 2); } // normal drawing else { // draw ellipse content g2.fill(ellipse); // draw black border g2.setStroke(new BasicStroke((float) (getAdjRay(canvas) / 8f))); g2.setColor(Color.black); g2.draw(ellipse); } g2.dispose(); } } else if (canvas instanceof VtkCanvas) { // nothing to do here } } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!isVisible() && !getReceiveKeyEventOnHidden()) return; // no image position --> exit if (imagePoint == null) return; // just for the shift key state change updateDrag(e, imagePoint.x, imagePoint.y, imagePoint.z); } @Override public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!isVisible() && !getReceiveKeyEventOnHidden()) return; // no image position --> exit if (imagePoint == null) return; // just for the shift key state change updateDrag(e, imagePoint.x, imagePoint.y, imagePoint.z); } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!isVisible() && !getReceiveMouseEventOnHidden()) return; if (e.isConsumed()) return; // no image position --> exit if (imagePoint == null) return; if (EventUtil.isLeftMouseButton(e)) { // consume event to activate drag if (isSelected()) { startDragMousePosition = imagePoint.toPoint3D(); startDragPainterPosition = getPosition(); e.consume(); } } } @Override public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { startDragMousePosition = null; } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!isVisible() && !getReceiveMouseEventOnHidden()) return; if (e.isConsumed()) return; // no image position --> exit if (imagePoint == null) return; if (EventUtil.isLeftMouseButton(e)) { // if selected then move according to mouse position if (isSelected()) { // force start drag if not already the case if (startDragMousePosition == null) { startDragMousePosition = imagePoint.toPoint3D(); startDragPainterPosition = getPosition(); } updateDrag(e, imagePoint.x, imagePoint.y, imagePoint.z); e.consume(); } } } @Override public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { if (!isVisible() && !getReceiveMouseEventOnHidden()) return; // already consumed, no selection possible if (e.isConsumed()) setSelected(false); else { final boolean overlapped = isOver(canvas, (imagePoint != null) ? imagePoint.toPoint3D() : null); setSelected(overlapped); // so we can only have one selected at once if (overlapped) e.consume(); } } @Override public void run() { rebuildVtkObjects(); } public boolean loadPositionFromXML(Node node) { if (node == null) return false; beginUpdate(); try { setX(XMLUtil.getElementDoubleValue(node, ID_POS_X, 0d)); setY(XMLUtil.getElementDoubleValue(node, ID_POS_Y, 0d)); setZ(XMLUtil.getElementDoubleValue(node, ID_POS_Z, 0d)); } finally { endUpdate(); } return true; } public boolean savePositionToXML(Node node) { if (node == null) return false; XMLUtil.setElementDoubleValue(node, ID_POS_X, getX()); XMLUtil.setElementDoubleValue(node, ID_POS_Y, getY()); XMLUtil.setElementDoubleValue(node, ID_POS_Z, getZ()); return true; } @Override public boolean loadFromXML(Node node) { if (node == null) return false; beginUpdate(); try { setColor(new Color(XMLUtil.getElementIntValue(node, ID_COLOR, DEFAULT_NORMAL_COLOR.getRGB()))); setSelectedColor( new Color(XMLUtil.getElementIntValue(node, ID_SELECTEDCOLOR, DEFAULT_SELECTED_COLOR.getRGB()))); setX(XMLUtil.getElementDoubleValue(node, ID_POS_X, 0d)); setY(XMLUtil.getElementDoubleValue(node, ID_POS_Y, 0d)); setZ(XMLUtil.getElementDoubleValue(node, ID_POS_Z, 0d)); setRay(XMLUtil.getElementIntValue(node, ID_RAY, DEFAULT_RAY)); setVisible(XMLUtil.getElementBooleanValue(node, ID_VISIBLE, true)); } finally { endUpdate(); } return true; } @Override public boolean saveToXML(Node node) { if (node == null) return false; XMLUtil.setElementIntValue(node, ID_COLOR, getColor().getRGB()); XMLUtil.setElementIntValue(node, ID_SELECTEDCOLOR, getSelectedColor().getRGB()); XMLUtil.setElementDoubleValue(node, ID_POS_X, getX()); XMLUtil.setElementDoubleValue(node, ID_POS_Y, getY()); XMLUtil.setElementDoubleValue(node, ID_POS_Z, getY()); XMLUtil.setElementIntValue(node, ID_RAY, getRay()); XMLUtil.setElementBooleanValue(node, ID_VISIBLE, isVisible()); return true; } }