/*
* 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 icy.painter.Anchor2D;
import icy.painter.PathAnchor2D;
import icy.resource.ResourceUtil;
import icy.roi.ROI;
import icy.type.point.Point5D;
import icy.util.ShapeUtil;
import icy.util.XMLUtil;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* ROI Path.<br>
* This ROI can display a Path2D shape.<br>
* You can modify and remove points (adding new point isn't supported).
*
* @author Stephane
*/
public class ROI2DPath extends ROI2DShape
{
public static final String ID_POINTS = "points";
public static final String ID_POINT = "point";
public static final String ID_WINDING = "winding";
protected Area closedArea;
protected Path2D openPath;
static Path2D initPath(Point2D position)
{
final Path2D result = new Path2D.Double();
result.reset();
if (position != null)
result.moveTo(position.getX(), position.getY());
return result;
}
/**
* Build a new ROI2DPath from the specified path.
*/
public ROI2DPath(Path2D path, Area closedArea, Path2D openPath)
{
super(path);
rebuildControlPointsFromPath();
if (closedArea == null)
this.closedArea = new Area(ShapeUtil.getClosedPath(path));
else
this.closedArea = closedArea;
if (openPath == null)
this.openPath = ShapeUtil.getOpenPath(path);
else
this.openPath = openPath;
// set icon (default name is defined by getDefaultName())
setIcon(ResourceUtil.ICON_ROI_POLYLINE);
}
/**
* Build a new ROI2DPath from the specified path.
*/
public ROI2DPath(Path2D path)
{
this(path, null, null);
}
/**
* Build a new ROI2DPath from the specified path.
*/
public ROI2DPath(Shape shape)
{
this(new Path2D.Double(shape), (shape instanceof Area) ? (Area) shape : null, null);
}
/**
* @deprecated
*/
@Deprecated
public ROI2DPath(Point2D pt, boolean cm)
{
this(pt);
}
public ROI2DPath(Point2D position)
{
this(initPath(position));
}
/**
* Generic constructor for interactive mode
*/
public ROI2DPath(Point5D pt)
{
this(pt.toPoint2D());
}
public ROI2DPath()
{
this(new Path2D.Double(Path2D.WIND_NON_ZERO));
}
@Override
public String getDefaultName()
{
return "Path2D";
}
@Override
protected Anchor2D createAnchor(Point2D pos)
{
return new PathAnchor2D(pos.getX(), pos.getY(), getColor(), getFocusedColor());
}
protected void rebuildControlPointsFromPath()
{
beginUpdate();
try
{
// remove all point
removeAllPoint();
// add path points to the control point list
for (Anchor2D pt : ShapeUtil.getAnchorsFromShape(getPath(), getColor(), getFocusedColor()))
addPoint(pt);
}
finally
{
endUpdate();
}
}
protected Path2D getPath()
{
return (Path2D) shape;
}
/**
* Returns the closed area part of the ROI2DPath in {@link Area} shape format
*/
public Area getClosedArea()
{
return closedArea;
}
/**
* Returns the open path part of the ROI2DPath in {@link Path2D} shape format
*/
public Path2D getOpenPath()
{
return openPath;
}
@Override
public boolean canAddPoint()
{
// this ROI doesn't support point add
return false;
}
@Override
public boolean contains(double x, double y)
{
// only consider closed path
return ShapeUtil.getClosedPath(getPath()).contains(x, y);
}
@Override
public boolean contains(Point2D p)
{
// only consider closed path
return ShapeUtil.getClosedPath(getPath()).contains(p);
}
@Override
public boolean contains(double x, double y, double w, double h)
{
// only consider closed path
return ShapeUtil.getClosedPath(getPath()).contains(x, y, w, h);
}
@Override
public boolean contains(Rectangle2D r)
{
// only consider closed path
return ShapeUtil.getClosedPath(getPath()).contains(r);
}
@Override
public boolean contains(ROI roi)
{
// not closed --> do not contains anything
if (!ShapeUtil.isClosed(shape))
return false;
return super.contains(roi);
}
@Override
public ROI add(ROI roi, boolean allowCreate) 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 Path2D path = getPath();
if (roi instanceof ROI2DPath)
{
final ROI2DPath roiPath = (ROI2DPath) roi;
// compute closed area and open path parts
closedArea.add(roiPath.closedArea);
openPath.append(roiPath.openPath, false);
}
else
{
// compute closed area and open path parts
if (roiShape.getShape() instanceof Area)
closedArea.add((Area) roiShape.getShape());
else
closedArea.add(new Area(ShapeUtil.getClosedPath(roiShape)));
openPath.append(ShapeUtil.getOpenPath(roiShape), false);
}
// then rebuild path from closed and open parts
path.reset();
path.append(closedArea, false);
path.append(openPath, false);
rebuildControlPointsFromPath();
roiChanged(true);
return this;
}
}
return super.add(roi, allowCreate);
}
@Override
public ROI intersect(ROI roi, boolean allowCreate) 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 Path2D path = getPath();
if (roi instanceof ROI2DPath)
{
final ROI2DPath roiPath = (ROI2DPath) roi;
// compute closed area intersection and clear open path
closedArea.intersect(roiPath.closedArea);
openPath.reset();
}
else
{
// compute closed area intersection and clear open path
if (roiShape.getShape() instanceof Area)
closedArea.intersect((Area) roiShape.getShape());
else
closedArea.intersect(new Area(ShapeUtil.getClosedPath(roiShape)));
openPath.reset();
}
// then rebuild path from closed area (open part is empty)
path.reset();
path.append(closedArea, false);
rebuildControlPointsFromPath();
roiChanged(true);
return this;
}
}
return super.intersect(roi, allowCreate);
}
@Override
public ROI exclusiveAdd(ROI roi, boolean allowCreate) 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 Path2D path = getPath();
if (roi instanceof ROI2DPath)
{
final ROI2DPath roiPath = (ROI2DPath) roi;
// compute exclusive union on closed area and simple append for open path
closedArea.exclusiveOr(roiPath.closedArea);
openPath.append(roiPath.openPath, false);
}
else
{
// compute exclusive union on closed area and simple append for open path
if (roiShape.getShape() instanceof Area)
closedArea.exclusiveOr((Area) roiShape.getShape());
else
closedArea.exclusiveOr(new Area(ShapeUtil.getClosedPath(roiShape)));
openPath.append(ShapeUtil.getOpenPath(roiShape), false);
}
// then rebuild path from closed and open parts
path.reset();
path.append(closedArea, false);
path.append(openPath, false);
rebuildControlPointsFromPath();
roiChanged(true);
return this;
}
}
return super.exclusiveAdd(roi, allowCreate);
}
@Override
public ROI subtract(ROI roi, boolean allowCreate) 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 Path2D path = getPath();
if (roi instanceof ROI2DPath)
{
final ROI2DPath roiPath = (ROI2DPath) roi;
// compute closed area intersection and clear open path parts
closedArea.exclusiveOr(roiPath.closedArea);
if (!roiPath.closedArea.isEmpty())
openPath.reset();
}
else
{
final Area area;
// compute closed area and open path parts
if (roiShape.getShape() instanceof Area)
area = (Area) roiShape.getShape();
else
area = new Area(ShapeUtil.getClosedPath(roiShape));
if (!area.isEmpty())
{
closedArea.exclusiveOr(area);
openPath.reset();
}
}
// then rebuild path from closed and open parts
path.reset();
path.append(closedArea, false);
path.append(openPath, false);
rebuildControlPointsFromPath();
roiChanged(true);
return this;
}
}
return super.subtract(roi, allowCreate);
}
/**
* Return the list of control points for this ROI.
*/
public List<PathAnchor2D> getPathAnchors()
{
final List<PathAnchor2D> result = new ArrayList<PathAnchor2D>();
synchronized (controlPoints)
{
for (Anchor2D pt : controlPoints)
result.add((PathAnchor2D) pt);
}
return result;
}
protected void updateCachedStructures()
{
closedArea = new Area(ShapeUtil.getClosedPath(getPath()));
openPath = ShapeUtil.getOpenPath(getPath());
}
@Override
protected void updateShape()
{
ShapeUtil.buildPathFromAnchors(getPath(), getPathAnchors(), false);
// update internal closed area and open path
updateCachedStructures();
// call super method after shape has been updated
super.updateShape();
}
@Override
public boolean loadFromXML(Node node)
{
beginUpdate();
try
{
if (!super.loadFromXML(node))
return false;
removeAllPoint();
final List<Node> nodesPoint = XMLUtil.getChildren(XMLUtil.getElement(node, ID_POINTS), ID_POINT);
if (nodesPoint != null)
{
for (Node n : nodesPoint)
{
final PathAnchor2D pt = (PathAnchor2D) createAnchor(new Point2D.Double());
pt.loadPositionFromXML(n);
addPoint(pt);
}
}
getPath().setWindingRule(XMLUtil.getElementIntValue(node, ID_WINDING, Path2D.WIND_NON_ZERO));
}
finally
{
endUpdate();
}
return true;
}
@Override
public boolean saveToXML(Node node)
{
if (!super.saveToXML(node))
return false;
final Element points = XMLUtil.setElement(node, ID_POINTS);
synchronized (controlPoints)
{
for (Anchor2D pt : controlPoints)
pt.savePositionToXML(XMLUtil.addElement(points, ID_POINT));
}
XMLUtil.setElementIntValue(node, ID_WINDING, getPath().getWindingRule());
return true;
}
}