/* * (c) Copyright 2010-2011 AgileBirds * * This file is part of OpenFlexo. * * OpenFlexo 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. * * OpenFlexo 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 OpenFlexo. If not, see <http://www.gnu.org/licenses/>. * */ package org.openflexo.fge.geom; import java.awt.geom.AffineTransform; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.PathIterator; import java.awt.geom.QuadCurve2D; import java.awt.geom.QuadCurve2D.Double; import java.awt.geom.Rectangle2D; import java.util.List; import java.util.Vector; import java.util.logging.Logger; import org.openflexo.fge.geom.area.FGEArea; import org.openflexo.fge.geom.area.FGEExclusiveOrArea; import org.openflexo.fge.geom.area.FGEIntersectionArea; import org.openflexo.fge.geom.area.FGESubstractionArea; import org.openflexo.fge.geom.area.FGEUnionArea; import org.openflexo.fge.graphics.FGEGraphics; public class FGEQuadCurve extends Double implements FGEGeneralShape.GeneralShapePathElement<FGEQuadCurve> { private static final Logger logger = Logger.getLogger(FGEQuadCurve.class.getPackage().getName()); /** * This value is internally used to compute approximated data (nearest point, distance, etc...) */ private static final double FLATTENING_PATH_LEVEL = 0.01; private FGEPoint p3, pp1, pp2; public FGEQuadCurve() { super(); } /** * Build a quadratic curve given 3 points. * * @param p1 * start point * @param ctrlP * real control point (not located on curve) * @param p2 * end point */ public FGEQuadCurve(FGEPoint p1, FGEPoint ctrlP, FGEPoint p2) { super(); setCurve(p1.x, p1.y, ctrlP.x, ctrlP.y, p2.x, p2.y); } /** * Build a quadratic curve given 3 points. The curve will cross each point * * @param p1 * start point * @param p3 * control point LOCATED on curve * @param p2 * end point * @return */ public static FGEQuadCurve makeCurveFromPoints(FGEPoint p1, FGEPoint p3, FGEPoint p2) { FGEQuadCurve returned = new FGEQuadCurve(p1, p3, p2); returned.setP3(p3); return returned; } @Override public FGEPoint getP1() { return new FGEPoint(x1, y1); } public void setP1(FGEPoint aPoint) { x1 = aPoint.x; y1 = aPoint.y; update(); } @Override public FGEPoint getP2() { return new FGEPoint(x2, y2); } public void setP2(FGEPoint aPoint) { x2 = aPoint.x; y2 = aPoint.y; update(); } public FGEPoint getCtrlPoint() { return new FGEPoint(ctrlx, ctrly); } public void setCtrlPoint(FGEPoint aPoint) { ctrlx = aPoint.x; ctrly = aPoint.y; update(); } @Override public void setCurve(double x1, double y1, double ctrlx, double ctrly, double x2, double y2) { super.setCurve(x1, y1, ctrlx, ctrly, x2, y2); update(); } private void update() { pp1 = FGEPoint.middleOf(getP1(), getCtrlPoint()); pp2 = FGEPoint.middleOf(getP2(), getCtrlPoint()); p3 = FGEPoint.middleOf(pp1, pp2); } public FGEPoint getP3() { return p3; } public void setP3(FGEPoint aPoint) { FGEPoint pp = FGEPoint.middleOf(getP1(), getP2()); FGEPoint cp = new FGESegment(pp, aPoint).getScaledPoint(2); setCtrlPoint(cp); } public FGEPoint getPP1() { return pp1; } public FGEPoint getPP2() { return pp2; } @Override public void paint(FGEGraphics g) { g.useDefaultForegroundStyle(); g.drawCurve(this); // DEBUG // g.setDefaultForeground(ForegroundStyle.makeStyle(Color.RED)); // (buildFlattenPath(0.01)).paint(g); } @Override public FGEQuadCurve transform(AffineTransform t) { return new FGEQuadCurve(getP1().transform(t), getCtrlPoint().transform(t), getP2().transform(t)); } @Override public List<FGEPoint> getControlPoints() { Vector<FGEPoint> returned = new Vector<FGEPoint>(); returned.add(getP1()); returned.add(getP2()); return returned; } @Override public String toString() { return "FGEQuadCurve: [" + getP1() + "," + getCtrlPoint() + "," + getP2() + "]"; } @Override public boolean containsArea(FGEArea a) { if (a instanceof FGEPoint) { return containsPoint((FGEPoint) a); } return false; } @Override public boolean containsLine(FGEAbstractLine l) { return false; } @Override public boolean containsPoint(FGEPoint p) { return super.contains(p); } @Override public FGEArea getAnchorAreaFrom(org.openflexo.fge.geom.FGEGeometricObject.SimplifiedCardinalDirection direction) { return clone(); } @Override public FGEQuadCurve clone() { return (FGEQuadCurve) super.clone(); } private FGEPolylin buildFlattenPath(double flatness) { FGEPolylin returned = new FGEPolylin(); PathIterator p = getPathIterator(null); FlatteningPathIterator f = new FlatteningPathIterator(p, flatness); while (!f.isDone()) { float[] pts = new float[6]; switch (f.currentSegment(pts)) { case PathIterator.SEG_MOVETO: // returned.addToPoints(new FGEPoint(pts[0],pts[1])); case PathIterator.SEG_LINETO: returned.addToPoints(new FGEPoint(pts[0], pts[1])); } f.next(); } return returned; } @Override public FGEPoint getNearestPoint(FGEPoint aPoint) { // TODO do something better later // logger.warning("Please implement me better later"); return getApproximatedNearestPoint(aPoint); } public FGEPoint getApproximatedNearestPoint(FGEPoint aPoint) { double minimizedDistance = java.lang.Double.POSITIVE_INFINITY; FGEPoint returned = null; FGEPolylin flattenPath = buildFlattenPath(FLATTENING_PATH_LEVEL); for (FGESegment s : flattenPath.getSegments()) { FGEPoint nearestPoint = s.getNearestPointOnSegment(aPoint); double currentDistance = FGEPoint.distance(nearestPoint, aPoint); if (currentDistance < minimizedDistance) { minimizedDistance = currentDistance; returned = nearestPoint; } } return returned; } @Override public FGEArea getOrthogonalPerspectiveArea(org.openflexo.fge.geom.FGEGeometricObject.SimplifiedCardinalDirection orientation) { // TODO Auto-generated method stub return null; } public FGESegment getApproximatedStartTangent() { return buildFlattenPath(FLATTENING_PATH_LEVEL).getSegments().firstElement(); } public FGESegment getApproximatedEndTangent() { return buildFlattenPath(FLATTENING_PATH_LEVEL).getSegments().lastElement(); } public FGESegment getApproximatedControlPointTangent() { double minimizedDistance = java.lang.Double.POSITIVE_INFINITY; FGESegment returned = null; FGEPolylin flattenPath = buildFlattenPath(FLATTENING_PATH_LEVEL); for (FGESegment s : flattenPath.getSegments()) { FGEPoint nearestPoint = s.getNearestPointOnSegment(getP3()); double currentDistance = FGEPoint.distance(nearestPoint, getP3()); if (currentDistance < minimizedDistance) { minimizedDistance = currentDistance; returned = s; } } return returned; } @Override public FGEArea exclusiveOr(FGEArea area) { return new FGEExclusiveOrArea(this, area); } @Override public FGEArea intersect(FGEArea area) { FGEIntersectionArea returned = new FGEIntersectionArea(this, area); if (returned.isDevelopable()) { return returned.makeDevelopped(); } else { return returned; } } @Override public FGEArea union(FGEArea area) { if (containsArea(area)) { return clone(); } if (area.containsArea(this)) { return area.clone(); } return new FGEUnionArea(this, area); } @Override public FGEArea substract(FGEArea area, boolean isStrict) { return new FGESubstractionArea(this, area, isStrict); } @Override public String getStringRepresentation() { return toString(); } @Override public boolean equals(Object obj) { if (obj instanceof FGEQuadCurve) { FGEQuadCurve s = (FGEQuadCurve) obj; return (getP1().equals(s.getP1()) || getP1().equals(s.getP2())) && (getP2().equals(s.getP1()) || getP2().equals(s.getP2())) && getCtrlPoint().equals(s.getCtrlPoint()); } return false; } // http://forum.java.sun.com/thread.jspa?threadID=420749&messageID=3752991 public boolean curveIntersects(QuadCurve2D curve, double rx, double ry, double rw, double rh) { /** * A quadratic bezier curve has the following parametric equation: * * Q(t) = P0(1-t)^2 + P1(2t(1-t)) + P2(t^2) * * Where 0 <= t <= 1 and P0 is the starting point, P1 is the control point and P2 is the end point. * * Therefore, the equations for the x and y coordinates are: * * Qx(t) = Px0(1-t)^2 + Px1(2t(1-t)) + Px2(t^2) Qy(t) = Py0(1-t)^2 + Py1(2t(1-t)) + Py2(t^2) * * 0 <= t <= 1 * * A bezier curve intersects a rectangle if: * * 1 - Either one of its endpoints is inside of the rectangle 2 - The curve intersects one of the rectangles sides (top, bottom, * left or right) * * The equation for a horizontal line is: * * y = c * * The line intersects the bezier if: * * Qy(t) = c and 0 <= t <= 1 * * We can rewrite this as: * * -c + Py0 + (-2Py0 + 2Py1)t + (Py0 - 2Py1 + Py2) t^2 == 0 and 0 <= t <= 1 * * We can use the valid roots of the quadratic, to evaluate Qx(t), and see if the value falls withing the rectangle bounds. * * The case for vertical lines is analogous to this one. (juancn) */ double y1 = curve.getY1(); double y2 = curve.getY2(); double x1 = curve.getX1(); double x2 = curve.getX2(); // If the rectangle contains one of the endpoints, it intersects the curve if (rectangleContains(x1, y1, rx, ry, rw, rh) || rectangleContains(x2, y2, rx, ry, rw, rh)) { return true; } double eqn[] = new double[3]; double ctrlY = curve.getCtrlY(); double ctrlX = curve.getCtrlX(); return intersectsLine(eqn, y1, ctrlY, y2, ry, x1, ctrlX, x2, rx, rx + rw) // Top || intersectsLine(eqn, y1, ctrlY, y2, ry + rh, x1, ctrlX, x2, rx, rx + rw) // Bottom || intersectsLine(eqn, x1, ctrlX, x2, rx, y1, ctrlY, y2, ry, ry + rh) // Left || intersectsLine(eqn, x1, ctrlX, x2, rx + rw, y1, ctrlY, y2, ry, ry + rh); // Right } private boolean rectangleContains(double x, double y, double rx, double ry, double rw, double rh) { return x >= rx && y >= ry && x < rx + rw && y < ry + rh; } /** * Returns true if a line segment parallel to one of the axis intersects the specified curve. This function works fine if you reverse * the axes. * * @param eqn * a double[] of lenght 3 used to hold the quadratic equation coeficients * @param p0 * starting point of the curve at the desired axis (i.e.: curve.getX1()) * @param p1 * control point of the curve at the desired axis (i.e.: curve.getCtrlX()) * @param p2 * end point of the curve at the desired axis (i.e.: curve.getX2()) * @param c * where is the line segment (i.e.: in X axis) * @param pb0 * starting point of the curve at the other axis (i.e.: curve.getY1()) * @param pb1 * control point of the curve at the other axis (i.e.: curve.getCtrlY()) * @param pb2 * end point of the curve at the other axis (i.e.: curve.getY2()) * @param from * starting point of the line segment (i.e.: in Y axis) * @param to * end point of the line segment (i.e.: in Y axis) * @return */ private static boolean intersectsLine(double[] eqn, double p0, double p1, double p2, double c, double pb0, double pb1, double pb2, double from, double to) { /** * First we check if a line parallel to the axis we are evaluating intersects the curve (the line is at c). * * Then we check if any of the intersection points is between 'from' and 'to' in the other axis (wether it belongs to the rectangle) */ // Fill the coefficients of the equation eqn[2] = p0 - 2 * p1 + p2; eqn[1] = 2 * p1 - 2 * p0; eqn[0] = p0 - c; int nRoots = QuadCurve2D.solveQuadratic(eqn); boolean result; switch (nRoots) { case 1: result = eqn[0] >= 0 && eqn[0] <= 1; if (result) { double intersection = evalQuadraticCurve(pb0, pb1, pb2, eqn[0]); result = intersection >= from && intersection <= to; } break; case 2: result = eqn[0] >= 0 && eqn[0] <= 1; if (result) { double intersection = evalQuadraticCurve(pb0, pb1, pb2, eqn[0]); result = intersection >= from && intersection <= to; } // If the first root is not a valid intersection, try the other one if (!result) { result = eqn[1] >= 0 && eqn[1] <= 1; if (result) { double intersection = evalQuadraticCurve(pb0, pb1, pb2, eqn[1]); result = intersection >= from && intersection <= to; } } break; default: result = false; } return result; } public static double evalQuadraticCurve(double c1, double ctrl, double c2, double t) { double u = 1 - t; double res = c1 * u * u + 2 * ctrl * t * u + c2 * t * t; return res; } /** * This area is finite, so always return true */ @Override public final boolean isFinite() { return true; } /** * This area is finite, so always return null */ @Override public final FGERectangle getEmbeddingBounds() { Rectangle2D bounds2D = getBounds2D(); return new FGERectangle(bounds2D.getX(), bounds2D.getY(), bounds2D.getWidth(), bounds2D.getHeight(), Filling.FILLED); } /** * Return nearest point from point "from" following supplied orientation * * Returns null if no intersection was found * * @param from * point from which we are coming to area * @param orientation * orientation we are coming from * @return */ @Override public FGEPoint nearestPointFrom(FGEPoint from, SimplifiedCardinalDirection orientation) { // TODO: not implemented return null; } }