/*
* 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.roi4d;
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.ROI3D;
import icy.roi.ROI4D;
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.Rectangle3D;
import icy.type.rectangle.Rectangle4D;
import icy.util.StringUtil;
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 4D ROI as a stack of individual 3D ROI slices.
*
* @author Alexandre Dufour
* @author Stephane Dallongeville
* @param <R>
* the type of 3D ROI for each slice of this 4D ROI
*/
public class ROI4DStack<R extends ROI3D> extends ROI4D 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 translateT;
/**
* Creates a new 4D ROI based on the given 3D ROI type.
*/
public ROI4DStack(Class<R> roiClass)
{
super();
this.roiClass = roiClass;
useChildColor = false;
modifyingSlice = new Semaphore(1);
translateT = 0d;
}
@Override
public String getDefaultName()
{
return "ROI3D stack";
}
@Override
protected ROIPainter createPainter()
{
return new ROI4DStackPainter();
}
/**
* Create a new empty 3D 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 3D slice color draw property and <code>false</code> if it
* uses the global 4D ROI color draw property.
*/
public boolean getUseChildColor()
{
return useChildColor;
}
/**
* Set to <code>true</code> if you want to directly use the 3D slice color draw property and <code>false</code> to
* keep the global 4D 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 t, Color value)
{
final ROI3D slice = getSlice(t);
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();
}
}
@Override
public void setC(int value)
{
beginUpdate();
try
{
super.setC(value);
modifyingSlice.acquireUninterruptibly();
try
{
for (R slice : slices.values())
slice.setC(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 T.<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 getSizeT()
{
if (slices.isEmpty())
return 0;
return (slices.lastKey().intValue() - slices.firstKey().intValue()) + 1;
}
/**
* Returns the ROI slice at given T position.
*/
public R getSlice(int t)
{
return slices.get(Integer.valueOf(t));
}
/**
* Returns the ROI slice at given T position.
*/
public R getSlice(int t, boolean createIfNull)
{
R result = getSlice(t);
if ((result == null) && createIfNull)
{
result = createSlice();
if (result != null)
setSlice(t, result);
}
return result;
}
/**
* Sets the slice for the given T position.
*/
public void setSlice(int t, R roi3d)
{
if (roi3d == null)
throw new IllegalArgumentException("Cannot set an empty slice in a 4D ROI");
// set T and C position
roi3d.setT(t);
roi3d.setC(getC());
// listen events from this ROI and its overlay
roi3d.addListener(this);
roi3d.getOverlay().addOverlayListener(this);
slices.put(Integer.valueOf(t), roi3d);
// notify ROI changed
roiChanged(true);
}
/**
* Removes slice at the given T position and returns it.
*/
public R removeSlice(int t)
{
// remove the current slice (if any)
final R result = slices.remove(Integer.valueOf(t));
// 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 Rectangle4D computeBounds4D()
{
Rectangle3D xyzBounds = null;
for (R slice : slices.values())
{
final Rectangle3D bnd3d = slice.getBounds3D();
// only add non empty bounds
if (!bnd3d.isEmpty())
{
if (xyzBounds == null)
xyzBounds = (Rectangle3D) bnd3d.clone();
else
xyzBounds.add(bnd3d);
}
}
// create empty 3D bounds
if (xyzBounds == null)
xyzBounds = new Rectangle3D.Double();
final int t;
final int sizeT;
if (!slices.isEmpty())
{
t = slices.firstKey().intValue();
sizeT = getSizeT();
}
else
{
t = 0;
sizeT = 0;
}
return new Rectangle4D.Double(xyzBounds.getX(), xyzBounds.getY(), xyzBounds.getZ(), t, xyzBounds.getSizeX(),
xyzBounds.getSizeY(), xyzBounds.getSizeZ(), sizeT);
}
@Override
public boolean contains(double x, double y, double z, double t)
{
final R roi3d = getSlice((int) Math.floor(t));
if (roi3d != null)
return roi3d.contains(x, y, z);
return false;
}
@Override
public boolean contains(double x, double y, double z, double t, double sizeX, double sizeY, double sizeZ,
double sizeT)
{
final Rectangle4D bounds = getBounds4D();
// easy discard
if (!bounds.contains(x, y, z, t, sizeX, sizeY, sizeZ, sizeT))
return false;
final int lim = (int) Math.floor(t + sizeT);
for (int tc = (int) Math.floor(t); tc < lim; tc++)
{
final R roi3d = getSlice(tc);
if ((roi3d == null) || !roi3d.contains(x, y, z, sizeX, sizeY, sizeZ))
return false;
}
return true;
}
@Override
public boolean intersects(double x, double y, double z, double t, double sizeX, double sizeY, double sizeZ,
double sizeT)
{
final Rectangle4D bounds = getBounds4D();
// easy discard
if (!bounds.intersects(x, y, z, t, sizeX, sizeY, sizeZ, sizeT))
return false;
final int lim = (int) Math.floor(t + sizeT);
for (int tc = (int) Math.floor(t); tc < lim; tc++)
{
final R roi3d = getSlice(tc);
if ((roi3d != null) && roi3d.intersects(x, y, z, sizeX, sizeY, sizeZ))
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 ROI4DStack
@Override
public double computeNumberOfContourPoints()
{
// 4D contour points = first slice points + inter slices contour points + last slice points
double result = 0;
if (slices.size() <= 2)
{
for (R slice : slices.values())
result += 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();
result = firstEntry.getValue().getNumberOfPoints();
for (R slice : slices.subMap(firstKey, false, lastKey, false).values())
result += slice.getNumberOfContourPoints();
result += lastEntry.getValue().getNumberOfPoints();
}
return result;
}
@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 T position.
*/
public void translate(int t)
{
// easy optimizations
if ((t == 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 newT = roi.getT() + t;
// only positive value accepted
if (newT >= 0)
{
roi.setT(newT);
slices.put(Integer.valueOf(newT), roi);
}
}
// notify ROI changed
roiChanged(false);
}
@Override
public void translate(double dx, double dy, double dz, double dt)
{
beginUpdate();
try
{
translateT += dt;
// convert to integer
final int dti = (int) translateT;
// keep trace of not used floating part
translateT -= dti;
translate(dti);
modifyingSlice.acquireUninterruptibly();
try
{
for (R slice : slices.values())
slice.translate(dx, dy, dz);
}
finally
{
modifyingSlice.release();
}
// notify ROI changed because we modified slice 'internally'
if ((dx != 0d) || (dy != 0d) || (dz != 0d))
roiChanged(false);
}
finally
{
endUpdate();
}
}
@Override
public boolean[] getBooleanMask2D(int x, int y, int width, int height, int z, int t, boolean inclusive)
{
final R roi3d = getSlice(t);
if (roi3d != null)
return roi3d.getBooleanMask2D(x, y, width, height, z, inclusive);
return new boolean[width * height];
}
@Override
public BooleanMask2D getBooleanMask2D(int z, int t, boolean inclusive)
{
final R roi3d = getSlice(t);
if (roi3d != null)
return roi3d.getBooleanMask2D(z, 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 3D 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.getT(), 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 ROI4DStackPainter extends ROIPainter
{
R getSliceForCanvas(IcyCanvas canvas)
{
final int t = canvas.getPositionT();
if (t >= 0)
return getSlice(t);
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);
}
}
}
}