/*
* 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.util;
import icy.painter.Anchor2D;
import icy.painter.PathAnchor2D;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Line2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author Stephane
*/
public class ShapeUtil
{
public static interface PathConsumer
{
/**
* Consume the specified path.<br>
* Return false to interrupt consumption.
*/
public boolean consumePath(Path2D path, boolean closed);
}
public static interface ShapeConsumer
{
/**
* Consume the specified Shape.<br>
* Return false to interrupt consumption.
*/
public boolean consume(Shape shape);
}
public static enum BooleanOperator
{
OR, AND, XOR
}
/**
* @deprecated Use {@link BooleanOperator} instead.
*/
@Deprecated
public static enum ShapeOperation
{
OR
{
@Override
public BooleanOperator getBooleanOperator()
{
return BooleanOperator.OR;
}
},
AND
{
@Override
public BooleanOperator getBooleanOperator()
{
return BooleanOperator.AND;
}
},
XOR
{
@Override
public BooleanOperator getBooleanOperator()
{
return BooleanOperator.XOR;
}
};
public abstract BooleanOperator getBooleanOperator();
}
/**
* Use the {@link Graphics} clip area and {@link Shape} bounds informations to determine if
* the specified {@link Shape} is visible in the specified Graphics object.
*/
public static boolean isVisible(Graphics g, Shape shape)
{
if (shape == null)
return false;
return GraphicsUtil.isVisible(g, shape.getBounds2D());
}
/**
* Returns <code>true</code> if the specified Shape define a closed Shape (Area).<br>
* Returns <code>false</code> if the specified Shape define a open Shape (Path).<br>
*/
public static boolean isClosed(Shape shape)
{
final PathIterator path = shape.getPathIterator(null);
final double crd[] = new double[6];
while (!path.isDone())
{
if (path.currentSegment(crd) == PathIterator.SEG_CLOSE)
return true;
path.next();
}
return false;
}
/**
* Merge the specified list of {@link Shape} with the given {@link BooleanOperator}.<br>
*
* @param shapes
* Shapes we want to merge.
* @param operator
* {@link BooleanOperator} to apply.
* @return {@link Area} shape representing the result of the merge operation.
*/
public static Shape merge(List<Shape> shapes, BooleanOperator operator)
{
Shape result = new Area();
// merge shapes
for (Shape shape : shapes)
{
switch (operator)
{
case OR:
result = union(result, shape);
break;
case AND:
result = intersect(result, shape);
break;
case XOR:
result = exclusiveUnion(result, shape);
break;
}
}
return result;
}
/**
* @deprecated Use {@link #merge(List, BooleanOperator)} instead.
*/
@Deprecated
public static Shape merge(Shape[] shapes, ShapeOperation operation)
{
return merge(Arrays.asList(shapes), operation.getBooleanOperator());
}
/**
* Process union between the 2 shapes and return result in a new Shape.
*/
public static Shape union(Shape shape1, Shape shape2)
{
// first compute closed area union
final Area area = new Area(getClosedPath(shape1));
area.add(new Area(getClosedPath(shape2)));
// then compute open path (polyline) union
final Path2D result = new Path2D.Double(getOpenPath(shape1));
result.append(getOpenPath(shape2), false);
// then append result
result.append(area, false);
return result;
}
/**
* @deprecated Use {@link #union(Shape, Shape)} instead
*/
@Deprecated
public static Shape add(Shape shape1, Shape shape2)
{
return union(shape1, shape2);
}
/**
* Intersects 2 shapes and return result in an {@link Area} type shape.<br>
* If one of the specified Shape is not an Area (do not contains any pixel) then an empty Area is returned.
*/
public static Area intersect(Shape shape1, Shape shape2)
{
// trivial optimization
if (!isClosed(shape1) || !isClosed(shape2))
return new Area();
final Area result = new Area(getClosedPath(shape1));
result.intersect(new Area(getClosedPath(shape2)));
return result;
}
/**
* Do exclusive union between the 2 shapes and return result in an {@link Area} type shape.<br>
* If one of the specified Shape is not an Area (do not contains any pixel) then it just return the other Shape in
* Area format. If both Shape are not Area then an empty Area is returned.
*/
public static Area exclusiveUnion(Shape shape1, Shape shape2)
{
// trivial optimization
if (!isClosed(shape1))
{
if (!isClosed(shape2))
return new Area();
return new Area(shape2);
}
// trivial optimization
if (!isClosed(shape2))
return new Area(shape1);
final Area result = new Area(getClosedPath(shape1));
result.exclusiveOr(new Area(getClosedPath(shape2)));
return result;
}
/**
* @deprecated Use {@link #exclusiveUnion(Shape, Shape)} instead.
*/
@Deprecated
public static Area xor(Shape shape1, Shape shape2)
{
return exclusiveUnion(shape1, shape2);
}
/**
* Subtract shape2 from shape1 return result in an {@link Area} type shape.
*/
public static Area subtract(Shape shape1, Shape shape2)
{
// trivial optimization
if (!isClosed(shape1))
return new Area();
if (!isClosed(shape2))
return new Area(shape1);
final Area result = new Area(getClosedPath(shape1));
result.subtract(new Area(getClosedPath(shape2)));
return result;
}
/**
* Scale the specified {@link RectangularShape} by specified factor.
*
* @param shape
* the {@link RectangularShape} to scale
* @param factor
* the scale factor
* @param centered
* if true then scaling is centered (shape location is modified)
*/
public static void scale(RectangularShape shape, double factor, boolean centered)
{
final double w = shape.getWidth();
final double h = shape.getHeight();
final double newW = w * factor;
final double newH = h * factor;
if (centered)
{
final double deltaW = (newW - w) / 2;
final double deltaH = (newH - h) / 2;
shape.setFrame(shape.getX() - deltaW, shape.getY() - deltaH, newW, newH);
}
else
shape.setFrame(shape.getX(), shape.getY(), newW, newH);
}
/**
* Enlarge the specified {@link RectangularShape} by specified width and height.
*
* @param shape
* the {@link RectangularShape} to scale
* @param width
* the width to add
* @param height
* the height to add
* @param centered
* if true then enlargement is centered (shape location is modified)
*/
public static void enlarge(RectangularShape shape, double width, double height, boolean centered)
{
final double w = shape.getWidth();
final double h = shape.getHeight();
final double newW = w + width;
final double newH = h + height;
if (centered)
{
final double deltaW = (newW - w) / 2;
final double deltaH = (newH - h) / 2;
shape.setFrame(shape.getX() - deltaW, shape.getY() - deltaH, newW, newH);
}
else
shape.setFrame(shape.getX(), shape.getY(), newW, newH);
}
/**
* Translate a rectangular shape by the specified dx and dy value
*/
public static void translate(RectangularShape shape, int dx, int dy)
{
shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight());
}
/**
* Translate a rectangular shape by the specified dx and dy value
*/
public static void translate(RectangularShape shape, double dx, double dy)
{
shape.setFrame(shape.getX() + dx, shape.getY() + dy, shape.getWidth(), shape.getHeight());
}
/**
* Permit to describe any PathIterator in a list of Shape which are returned
* to the specified ShapeConsumer
*/
public static boolean consumeShapeFromPath(PathIterator path, ShapeConsumer consumer)
{
final Line2D.Double line = new Line2D.Double();
final QuadCurve2D.Double quadCurve = new QuadCurve2D.Double();
final CubicCurve2D.Double cubicCurve = new CubicCurve2D.Double();
double lastX, lastY, curX, curY, movX, movY;
final double crd[] = new double[6];
curX = 0;
curY = 0;
movX = 0;
movY = 0;
while (!path.isDone())
{
final int segType = path.currentSegment(crd);
lastX = curX;
lastY = curY;
switch (segType)
{
case PathIterator.SEG_MOVETO:
curX = crd[0];
curY = crd[1];
movX = curX;
movY = curY;
break;
case PathIterator.SEG_LINETO:
curX = crd[0];
curY = crd[1];
line.setLine(lastX, lastY, curX, curY);
if (!consumer.consume(line))
return false;
break;
case PathIterator.SEG_QUADTO:
curX = crd[2];
curY = crd[3];
quadCurve.setCurve(lastX, lastY, crd[0], crd[1], curX, curY);
if (!consumer.consume(quadCurve))
return false;
break;
case PathIterator.SEG_CUBICTO:
curX = crd[4];
curY = crd[5];
cubicCurve.setCurve(lastX, lastY, crd[0], crd[1], crd[2], crd[3], curX, curY);
if (!consumer.consume(cubicCurve))
return false;
break;
case PathIterator.SEG_CLOSE:
line.setLine(lastX, lastY, movX, movY);
if (!consumer.consume(line))
return false;
break;
}
path.next();
}
return true;
}
/**
* Consume all sub path of the specified {@link PathIterator}.<br>
* We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a
* {@link PathIterator#SEG_CLOSE} segment (except the ending one).
*/
public static void consumeSubPath(PathIterator pathIt, PathConsumer consumer)
{
final double crd[] = new double[6];
Path2D current = null;
while (!pathIt.isDone())
{
switch (pathIt.currentSegment(crd))
{
case PathIterator.SEG_MOVETO:
// had a previous not closed path ? --> consume it
if (current != null)
consumer.consumePath(current, false);
// create new path
current = new Path2D.Double(pathIt.getWindingRule());
current.moveTo(crd[0], crd[1]);
break;
case PathIterator.SEG_LINETO:
current.lineTo(crd[0], crd[1]);
break;
case PathIterator.SEG_QUADTO:
current.quadTo(crd[0], crd[1], crd[2], crd[3]);
break;
case PathIterator.SEG_CUBICTO:
current.curveTo(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5]);
break;
case PathIterator.SEG_CLOSE:
// close path and consume it
current.closePath();
consumer.consumePath(current, true);
// clear path
current = null;
break;
}
pathIt.next();
}
// have a last not closed path ? --> consume it
if (current != null)
consumer.consumePath(current, false);
}
/**
* Consume all sub path of the specified {@link Shape}.<br>
* We consider a new sub path when we meet both a {@link PathIterator#SEG_MOVETO} segment or after a
* {@link PathIterator#SEG_CLOSE} segment (except the ending one).
*/
public static void consumeSubPath(Shape shape, PathConsumer consumer)
{
consumeSubPath(shape.getPathIterator(null), consumer);
}
/**
* Returns only the open path part of the specified Shape.<br>
* By default all sub path inside a Shape are considered closed which can be a problem when drawing or using
* {@link Path2D#contains(double, double)} method.
*/
public static Path2D getOpenPath(Shape shape)
{
final PathIterator pathIt = shape.getPathIterator(null);
final Path2D result = new Path2D.Double(pathIt.getWindingRule());
consumeSubPath(pathIt, new PathConsumer()
{
@Override
public boolean consumePath(Path2D path, boolean closed)
{
if (!closed)
result.append(path, false);
return true;
}
});
return result;
}
/**
* Returns only the closed path part of the specified Shape.<br>
* By default all sub path inside a Shape are considered closed which can be a problem when drawing or using
* {@link Path2D#contains(double, double)} method.
*/
public static Path2D getClosedPath(Shape shape)
{
final PathIterator pathIt = shape.getPathIterator(null);
final Path2D result = new Path2D.Double(pathIt.getWindingRule());
consumeSubPath(pathIt, new PathConsumer()
{
@Override
public boolean consumePath(Path2D path, boolean closed)
{
if (closed)
result.append(path, false);
return true;
}
});
return result;
}
/**
* Return all PathAnchor points from the specified shape
*/
public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape, Color color, Color selectedColor)
{
final PathIterator pathIt = shape.getPathIterator(null);
final ArrayList<PathAnchor2D> result = new ArrayList<PathAnchor2D>();
final double crd[] = new double[6];
final double mov[] = new double[2];
while (!pathIt.isDone())
{
final int segType = pathIt.currentSegment(crd);
PathAnchor2D pt = null;
switch (segType)
{
case PathIterator.SEG_MOVETO:
mov[0] = crd[0];
mov[1] = crd[1];
case PathIterator.SEG_LINETO:
pt = new PathAnchor2D(crd[0], crd[1], color, selectedColor, segType);
break;
case PathIterator.SEG_QUADTO:
pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], color, selectedColor);
break;
case PathIterator.SEG_CUBICTO:
pt = new PathAnchor2D(crd[0], crd[1], crd[2], crd[3], crd[4], crd[5], color, selectedColor);
break;
case PathIterator.SEG_CLOSE:
pt = new PathAnchor2D(mov[0], mov[1], color, selectedColor, segType);
// CLOSE points aren't visible
pt.setVisible(false);
break;
}
if (pt != null)
result.add(pt);
pathIt.next();
}
return result;
}
/**
* Return all PathAnchor points from the specified shape
*/
public static ArrayList<PathAnchor2D> getAnchorsFromShape(Shape shape)
{
return getAnchorsFromShape(shape, Anchor2D.DEFAULT_NORMAL_COLOR, Anchor2D.DEFAULT_SELECTED_COLOR);
}
/**
* Update specified path from the specified list of PathAnchor2D
*/
public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points, boolean closePath)
{
path.reset();
for (PathAnchor2D pt : points)
{
switch (pt.getType())
{
case PathIterator.SEG_MOVETO:
path.moveTo(pt.getX(), pt.getY());
break;
case PathIterator.SEG_LINETO:
path.lineTo(pt.getX(), pt.getY());
break;
case PathIterator.SEG_QUADTO:
path.quadTo(pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(), pt.getY());
break;
case PathIterator.SEG_CUBICTO:
path.curveTo(pt.getPosCExtX(), pt.getPosCExtY(), pt.getPosQExtX(), pt.getPosQExtY(), pt.getX(),
pt.getY());
break;
case PathIterator.SEG_CLOSE:
path.closePath();
break;
}
}
if ((points.size() > 1) && closePath)
path.closePath();
return path;
}
/**
* Update specified path from the specified list of PathAnchor2D
*/
public static Path2D buildPathFromAnchors(Path2D path, List<PathAnchor2D> points)
{
return buildPathFromAnchors(path, points, true);
}
/**
* Create and return a path from the specified list of PathAnchor2D
*/
public static Path2D getPathFromAnchors(List<PathAnchor2D> points, boolean closePath)
{
return buildPathFromAnchors(new Path2D.Double(), points, closePath);
}
/**
* Create and return a path from the specified list of PathAnchor2D
*/
public static Path2D getPathFromAnchors(List<PathAnchor2D> points)
{
return buildPathFromAnchors(new Path2D.Double(), points, true);
}
/**
* @deprecated Use {@link GraphicsUtil#drawPathIterator(PathIterator, Graphics2D)} instead
*/
@Deprecated
public static void drawFromPath(PathIterator path, final Graphics2D g)
{
GraphicsUtil.drawPathIterator(path, g);
}
/**
* Return true if the specified PathIterator intersects with the specified Rectangle
*/
public static boolean pathIntersects(PathIterator path, final Rectangle2D rect)
{
return !consumeShapeFromPath(path, new ShapeConsumer()
{
@Override
public boolean consume(Shape shape)
{
if (shape.intersects(rect))
return false;
return true;
}
});
}
}