/* * 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.roi5d; import icy.canvas.IcyCanvas; import icy.canvas.IcyCanvas2D; import icy.canvas.IcyCanvas3D; import icy.painter.OverlayEvent; import icy.painter.OverlayListener; import icy.roi.BooleanMask2D; import icy.roi.ROI; import icy.roi.ROI4D; import icy.roi.ROI5D; import icy.roi.ROIEvent; import icy.roi.ROIListener; import icy.sequence.Sequence; import icy.system.IcyExceptionHandler; import icy.type.point.Point5D; import icy.type.rectangle.Rectangle4D; import icy.type.rectangle.Rectangle5D; import icy.util.XMLUtil; import java.awt.Color; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; import java.util.concurrent.Semaphore; import org.w3c.dom.Element; import org.w3c.dom.Node; /** * Abstract class defining a generic 5D ROI as a stack of individual 4D ROI slices. * * @author Alexandre Dufour * @author Stephane Dallongeville * @param <R> * the type of 4D ROI for each slice of this 5D ROI */ public class ROI5DStack<R extends ROI4D> extends ROI5D implements ROIListener, OverlayListener, Iterable<R> { public static final String PROPERTY_USECHILDCOLOR = "useChildColor"; protected final TreeMap<Integer, R> slices = new TreeMap<Integer, R>(); protected final Class<R> roiClass; protected boolean useChildColor; protected Semaphore modifyingSlice; protected double translateC; /** * Creates a new 5D ROI based on the given 4D ROI type. */ public ROI5DStack(Class<R> roiClass) { super(); this.roiClass = roiClass; useChildColor = false; modifyingSlice = new Semaphore(1); translateC = 0d; } @Override public String getDefaultName() { return "ROI4D stack"; } @Override protected ROIPainter createPainter() { return new ROI5DStackPainter(); } /** * Create a new empty 4D ROI slice. */ protected R createSlice() { try { return roiClass.newInstance(); } catch (Exception e) { IcyExceptionHandler.showErrorMessage(e, true, true); return null; } } /** * Returns <code>true</code> if the ROI directly uses the 4D slice color draw property and <code>false</code> if it * uses the global 5D ROI color draw property. */ public boolean getUseChildColor() { return useChildColor; } /** * Set to <code>true</code> if you want to directly use the 4D slice color draw property and <code>false</code> to * keep the global 5D ROI color draw property. * * @see #setColor(int, Color) */ public void setUseChildColor(boolean value) { if (useChildColor != value) { useChildColor = value; propertyChanged(PROPERTY_USECHILDCOLOR); // need to redraw it getOverlay().painterChanged(); } } /** * Set the painter color for the specified ROI slice. * * @see #setUseChildColor(boolean) */ public void setColor(int c, Color value) { final ROI4D slice = getSlice(c); modifyingSlice.acquireUninterruptibly(); try { if (slice != null) slice.setColor(value); } finally { modifyingSlice.release(); } } @Override public void setColor(Color value) { beginUpdate(); try { super.setColor(value); if (!getUseChildColor()) { modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setColor(value); } finally { modifyingSlice.release(); } } } finally { endUpdate(); } } @Override public void setOpacity(float value) { beginUpdate(); try { super.setOpacity(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setOpacity(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } @Override public void setStroke(double value) { beginUpdate(); try { super.setStroke(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setStroke(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } @Override public void setCreating(boolean value) { beginUpdate(); try { super.setCreating(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setCreating(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } @Override public void setReadOnly(boolean value) { beginUpdate(); try { super.setReadOnly(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setReadOnly(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } @Override public void setFocused(boolean value) { beginUpdate(); try { super.setFocused(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setFocused(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } @Override public void setSelected(boolean value) { beginUpdate(); try { super.setSelected(value); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.setSelected(value); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } /** * Returns <code>true</code> if the ROI stack is empty. */ public boolean isEmpty() { return slices.isEmpty(); } /** * @return The size of this ROI stack along C.<br> * Note that the returned value indicates the difference between upper and lower bounds * of this ROI, but doesn't guarantee that all slices in-between exist ( {@link #getSlice(int)} may still * return <code>null</code>.<br> */ public int getSizeC() { if (slices.isEmpty()) return 0; return (slices.lastKey().intValue() - slices.firstKey().intValue()) + 1; } /** * Returns the ROI slice at given C position. */ public R getSlice(int c) { return slices.get(Integer.valueOf(c)); } /** * Returns the ROI slice at given C position. */ public R getSlice(int c, boolean createIfNull) { R result = getSlice(c); if ((result == null) && createIfNull) { result = createSlice(); if (result != null) setSlice(c, result); } return result; } /** * Sets the slice for the given C position. */ public void setSlice(int c, R roi4d) { if (roi4d == null) throw new IllegalArgumentException("Cannot set an empty slice in a 5D ROI"); // set C position roi4d.setC(c); // listen events from this ROI and its overlay roi4d.addListener(this); roi4d.getOverlay().addOverlayListener(this); slices.put(Integer.valueOf(c), roi4d); // notify ROI changed roiChanged(true); } /** * Removes slice at the given C position and returns it. */ public R removeSlice(int c) { // remove the current slice (if any) final R result = slices.remove(Integer.valueOf(c)); // remove listeners if (result != null) { result.removeListener(this); result.getOverlay().removeOverlayListener(this); } // notify ROI changed roiChanged(true); return result; } /** * Removes all slices. */ public void clear() { // nothing to do if (isEmpty()) return; for (R slice : slices.values()) { slice.removeListener(this); slice.getOverlay().removeOverlayListener(this); } slices.clear(); roiChanged(true); } /** * Called when a ROI slice has changed. */ protected void sliceChanged(ROIEvent event) { if (modifyingSlice.availablePermits() <= 0) return; final ROI source = event.getSource(); switch (event.getType()) { case ROI_CHANGED: // position change of a slice can change global bounds --> transform to 'content changed' event type roiChanged(true); // roiChanged(StringUtil.equals(event.getPropertyName(), ROI_CHANGED_ALL)); break; case FOCUS_CHANGED: setFocused(source.isFocused()); break; case SELECTION_CHANGED: setSelected(source.isSelected()); break; case PROPERTY_CHANGED: final String propertyName = event.getPropertyName(); if ((propertyName == null) || propertyName.equals(PROPERTY_READONLY)) setReadOnly(source.isReadOnly()); if ((propertyName == null) || propertyName.equals(PROPERTY_CREATING)) setCreating(source.isCreating()); break; } } /** * Called when a ROI slice overlay has changed. */ protected void sliceOverlayChanged(OverlayEvent event) { switch (event.getType()) { case PAINTER_CHANGED: // forward the event to ROI stack overlay getOverlay().painterChanged(); break; case PROPERTY_CHANGED: // forward the event to ROI stack overlay getOverlay().propertyChanged(event.getPropertyName()); break; } } @Override public Rectangle5D computeBounds5D() { Rectangle4D xyztBounds = null; for (R slice : slices.values()) { final Rectangle4D bnd4d = slice.getBounds4D(); // only add non empty bounds if (!bnd4d.isEmpty()) { if (xyztBounds == null) xyztBounds = (Rectangle4D) bnd4d.clone(); else xyztBounds.add(bnd4d); } } // create empty 4D bounds if (xyztBounds == null) xyztBounds = new Rectangle4D.Double(); final int c; final int sizeC; if (!slices.isEmpty()) { c = slices.firstKey().intValue(); sizeC = getSizeC(); } else { c = 0; sizeC = 0; } return new Rectangle5D.Double(xyztBounds.getX(), xyztBounds.getY(), xyztBounds.getZ(), xyztBounds.getT(), c, xyztBounds.getSizeX(), xyztBounds.getSizeY(), xyztBounds.getSizeZ(), xyztBounds.getSizeT(), sizeC); } @Override public boolean contains(double x, double y, double z, double t, double c) { final R roi4d = getSlice((int) Math.floor(c)); if (roi4d != null) return roi4d.contains(x, y, z, t); return false; } @Override public boolean contains(double x, double y, double z, double t, double c, double sizeX, double sizeY, double sizeZ, double sizeT, double sizeC) { final Rectangle5D bounds = getBounds5D(); // easy discard if (!bounds.contains(x, y, z, t, c, sizeX, sizeY, sizeZ, sizeT, sizeC)) return false; final int lim = (int) Math.floor(c + sizeC); for (int cc = (int) Math.floor(c); cc < lim; cc++) { final R roi4d = getSlice(cc); if ((roi4d == null) || !roi4d.contains(x, y, z, t, sizeX, sizeY, sizeZ, sizeT)) return false; } return true; } @Override public boolean intersects(double x, double y, double z, double t, double c, double sizeX, double sizeY, double sizeZ, double sizeT, double sizeC) { final Rectangle5D bounds = getBounds5D(); // easy discard if (!bounds.intersects(x, y, z, t, c, sizeX, sizeY, sizeZ, sizeT, sizeC)) return false; final int lim = (int) Math.floor(c + sizeC); for (int cc = (int) Math.floor(c); cc < lim; cc++) { final R roi4d = getSlice(cc); if ((roi4d != null) && roi4d.intersects(x, y, z, t, sizeX, sizeY, sizeZ, sizeT)) return true; } return false; } @Override public boolean hasSelectedPoint() { // default return false; } @Override public void unselectAllPoints() { beginUpdate(); try { modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.unselectAllPoints(); } finally { modifyingSlice.release(); } } finally { endUpdate(); } } // default approximated implementation for ROI5DStack @Override public double computeNumberOfContourPoints() { // 5D contour points = first slice points + inter slices contour points + last slice points double perimeter = 0; if (slices.size() <= 2) { for (R slice : slices.values()) perimeter += slice.getNumberOfPoints(); } else { final Entry<Integer, R> firstEntry = slices.firstEntry(); final Entry<Integer, R> lastEntry = slices.lastEntry(); final Integer firstKey = firstEntry.getKey(); final Integer lastKey = lastEntry.getKey(); perimeter = firstEntry.getValue().getNumberOfPoints(); for (R slice : slices.subMap(firstKey, false, lastKey, false).values()) perimeter += slice.getNumberOfContourPoints(); perimeter += lastEntry.getValue().getNumberOfPoints(); } return perimeter; } @Override public double computeNumberOfPoints() { double volume = 0; for (R slice : slices.values()) volume += slice.getNumberOfPoints(); return volume; } @Override public boolean canTranslate() { // only need to test the first entry if (!slices.isEmpty()) return slices.firstEntry().getValue().canTranslate(); return false; } /** * Translate the stack of specified C position. */ public void translate(int c) { // easy optimizations if ((c == 0) || isEmpty()) return; final Map<Integer, R> map = new HashMap<Integer, R>(slices); slices.clear(); for (Entry<Integer, R> entry : map.entrySet()) { final R roi = entry.getValue(); final int newC = roi.getC() + c; // only positive value accepted if (newC >= 0) { roi.setC(newC); slices.put(Integer.valueOf(newC), roi); } } // notify ROI changed roiChanged(false); } @Override public void translate(double dx, double dy, double dz, double dt, double dc) { beginUpdate(); try { translateC += dc; // convert to integer final int dci = (int) translateC; // keep trace of not used floating part translateC -= dci; translate(dci); modifyingSlice.acquireUninterruptibly(); try { for (R slice : slices.values()) slice.translate(dx, dy, dz, dt); } finally { modifyingSlice.release(); } // notify ROI changed because we modified slice 'internally' if ((dx != 0d) || (dy != 0d) || (dz != 0d) || (dt != 0d)) roiChanged(false); } finally { endUpdate(); } } @Override public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, int t, int c, boolean inclusive) { final R roi4d = getSlice(c); if (roi4d != null) return roi4d.getBooleanMask2D(x, y, width, height, z, t, inclusive); return new boolean[width * height]; } @Override public BooleanMask2D getBooleanMask2D(int z, int t, int c, boolean inclusive) { final R roi4d = getSlice(c); if (roi4d != null) return roi4d.getBooleanMask2D(z, t, inclusive); return new BooleanMask2D(new Rectangle(), new boolean[0]); } // called when one of the slice ROI changed @Override public void roiChanged(ROIEvent event) { // propagate children change event sliceChanged(event); } // called when one of the slice ROI overlay changed @Override public void overlayChanged(OverlayEvent event) { // propagate children overlay change event sliceOverlayChanged(event); } @Override public Iterator<R> iterator() { return slices.values().iterator(); } @Override public boolean loadFromXML(Node node) { beginUpdate(); try { if (!super.loadFromXML(node)) return false; // we don't need to save the 4D ROI class as the parent class already do it clear(); for (Element e : XMLUtil.getElements(node, "slice")) { // faster than using complete XML serialization final R slice = createSlice(); // error while reloading the ROI from XML if ((slice == null) || !slice.loadFromXML(e)) return false; setSlice(slice.getC(), slice); } } finally { endUpdate(); } return true; } @Override public boolean saveToXML(Node node) { if (!super.saveToXML(node)) return false; for (R slice : slices.values()) { Element sliceNode = XMLUtil.addElement(node, "slice"); if (!slice.saveToXML(sliceNode)) return false; } return true; } public class ROI5DStackPainter extends ROIPainter { R getSliceForCanvas(IcyCanvas canvas) { final int c = canvas.getPositionC(); if (c >= 0) return getSlice(c); return null; } @Override public void paint(Graphics2D g, Sequence sequence, IcyCanvas canvas) { if (isActiveFor(canvas)) { if (canvas instanceof IcyCanvas3D) { // TODO } else if (canvas instanceof IcyCanvas2D) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().paint(g, sequence, canvas); } } } @Override public void keyPressed(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.keyPressed(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().keyPressed(e, imagePoint, canvas); } } @Override public void keyReleased(KeyEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.keyReleased(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().keyReleased(e, imagePoint, canvas); } } @Override public void mouseEntered(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseEntered(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseEntered(e, imagePoint, canvas); } } @Override public void mouseExited(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseExited(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseExited(e, imagePoint, canvas); } } @Override public void mouseMove(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseMove(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseMove(e, imagePoint, canvas); } } @Override public void mouseDrag(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseDrag(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseDrag(e, imagePoint, canvas); } } @Override public void mousePressed(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mousePressed(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mousePressed(e, imagePoint, canvas); } } @Override public void mouseReleased(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseReleased(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseReleased(e, imagePoint, canvas); } } @Override public void mouseClick(MouseEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseClick(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseClick(e, imagePoint, canvas); } } @Override public void mouseWheelMoved(MouseWheelEvent e, Point5D.Double imagePoint, IcyCanvas canvas) { // send event to parent first super.mouseWheelMoved(e, imagePoint, canvas); // then send it to active slice if (isActiveFor(canvas)) { // forward event to current slice final R slice = getSliceForCanvas(canvas); if (slice != null) slice.getOverlay().mouseWheelMoved(e, imagePoint, canvas); } } } }