/* * Copyright (c) 1998, 2006, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package java.awt.geom; import java.awt.Shape; import java.awt.Rectangle; import java.util.Vector; import java.util.Enumeration; import java.util.NoSuchElementException; import sun.awt.geom.Curve; import sun.awt.geom.Crossings; import sun.awt.geom.AreaOp; /** * An <code>Area</code> object stores and manipulates a * resolution-independent description of an enclosed area of * 2-dimensional space. * <code>Area</code> objects can be transformed and can perform * various Constructive Area Geometry (CAG) operations when combined * with other <code>Area</code> objects. * The CAG operations include area * {@link #add addition}, {@link #subtract subtraction}, * {@link #intersect intersection}, and {@link #exclusiveOr exclusive or}. * See the linked method documentation for examples of the various * operations. * <p> * The <code>Area</code> class implements the <code>Shape</code> * interface and provides full support for all of its hit-testing * and path iteration facilities, but an <code>Area</code> is more * specific than a generalized path in a number of ways: * <ul> * <li>Only closed paths and sub-paths are stored. * <code>Area</code> objects constructed from unclosed paths * are implicitly closed during construction as if those paths * had been filled by the <code>Graphics2D.fill</code> method. * <li>The interiors of the individual stored sub-paths are all * non-empty and non-overlapping. Paths are decomposed during * construction into separate component non-overlapping parts, * empty pieces of the path are discarded, and then these * non-empty and non-overlapping properties are maintained * through all subsequent CAG operations. Outlines of different * component sub-paths may touch each other, as long as they * do not cross so that their enclosed areas overlap. * <li>The geometry of the path describing the outline of the * <code>Area</code> resembles the path from which it was * constructed only in that it describes the same enclosed * 2-dimensional area, but may use entirely different types * and ordering of the path segments to do so. * </ul> * Interesting issues which are not always obvious when using * the <code>Area</code> include: * <ul> * <li>Creating an <code>Area</code> from an unclosed (open) * <code>Shape</code> results in a closed outline in the * <code>Area</code> object. * <li>Creating an <code>Area</code> from a <code>Shape</code> * which encloses no area (even when "closed") produces an * empty <code>Area</code>. A common example of this issue * is that producing an <code>Area</code> from a line will * be empty since the line encloses no area. An empty * <code>Area</code> will iterate no geometry in its * <code>PathIterator</code> objects. * <li>A self-intersecting <code>Shape</code> may be split into * two (or more) sub-paths each enclosing one of the * non-intersecting portions of the original path. * <li>An <code>Area</code> may take more path segments to * describe the same geometry even when the original * outline is simple and obvious. The analysis that the * <code>Area</code> class must perform on the path may * not reflect the same concepts of "simple and obvious" * as a human being perceives. * </ul> * * @since 1.2 */ public class Area implements Shape, Cloneable { private static Vector EmptyCurves = new Vector(); private Vector curves; /** * Default constructor which creates an empty area. * @since 1.2 */ public Area() { curves = EmptyCurves; } /** * The <code>Area</code> class creates an area geometry from the * specified {@link Shape} object. The geometry is explicitly * closed, if the <code>Shape</code> is not already closed. The * fill rule (even-odd or winding) specified by the geometry of the * <code>Shape</code> is used to determine the resulting enclosed area. * @param s the <code>Shape</code> from which the area is constructed * @throws NullPointerException if <code>s</code> is null * @since 1.2 */ public Area(Shape s) { if (s instanceof Area) { curves = ((Area) s).curves; } else { curves = pathToCurves(s.getPathIterator(null)); } } private static Vector pathToCurves(PathIterator pi) { Vector curves = new Vector(); int windingRule = pi.getWindingRule(); // coords array is big enough for holding: // coordinates returned from currentSegment (6) // OR // two subdivided quadratic curves (2+4+4=10) // AND // 0-1 horizontal splitting parameters // OR // 2 parametric equation derivative coefficients // OR // three subdivided cubic curves (2+6+6+6=20) // AND // 0-2 horizontal splitting parameters // OR // 3 parametric equation derivative coefficients double coords[] = new double[23]; double movx = 0, movy = 0; double curx = 0, cury = 0; double newx, newy; while (!pi.isDone()) { switch (pi.currentSegment(coords)) { case PathIterator.SEG_MOVETO: Curve.insertLine(curves, curx, cury, movx, movy); curx = movx = coords[0]; cury = movy = coords[1]; Curve.insertMove(curves, movx, movy); break; case PathIterator.SEG_LINETO: newx = coords[0]; newy = coords[1]; Curve.insertLine(curves, curx, cury, newx, newy); curx = newx; cury = newy; break; case PathIterator.SEG_QUADTO: newx = coords[2]; newy = coords[3]; Curve.insertQuad(curves, curx, cury, coords); curx = newx; cury = newy; break; case PathIterator.SEG_CUBICTO: newx = coords[4]; newy = coords[5]; Curve.insertCubic(curves, curx, cury, coords); curx = newx; cury = newy; break; case PathIterator.SEG_CLOSE: Curve.insertLine(curves, curx, cury, movx, movy); curx = movx; cury = movy; break; } pi.next(); } Curve.insertLine(curves, curx, cury, movx, movy); AreaOp operator; if (windingRule == PathIterator.WIND_EVEN_ODD) { operator = new AreaOp.EOWindOp(); } else { operator = new AreaOp.NZWindOp(); } return operator.calculate(curves, EmptyCurves); } /** * Adds the shape of the specified <code>Area</code> to the * shape of this <code>Area</code>. * The resulting shape of this <code>Area</code> will include * the union of both shapes, or all areas that were contained * in either this or the specified <code>Area</code>. * <pre> * // Example: * Area a1 = new Area([triangle 0,0 => 8,0 => 0,8]); * Area a2 = new Area([triangle 0,0 => 8,0 => 8,8]); * a1.add(a2); * * a1(before) + a2 = a1(after) * * ################ ################ ################ * ############## ############## ################ * ############ ############ ################ * ########## ########## ################ * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> * @param rhs the <code>Area</code> to be added to the * current shape * @throws NullPointerException if <code>rhs</code> is null * @since 1.2 */ public void add(Area rhs) { curves = new AreaOp.AddOp().calculate(this.curves, rhs.curves); invalidateBounds(); } /** * Subtracts the shape of the specified <code>Area</code> from the * shape of this <code>Area</code>. * The resulting shape of this <code>Area</code> will include * areas that were contained only in this <code>Area</code> * and not in the specified <code>Area</code>. * <pre> * // Example: * Area a1 = new Area([triangle 0,0 => 8,0 => 0,8]); * Area a2 = new Area([triangle 0,0 => 8,0 => 8,8]); * a1.subtract(a2); * * a1(before) - a2 = a1(after) * * ################ ################ * ############## ############## ## * ############ ############ #### * ########## ########## ###### * ######## ######## ######## * ###### ###### ###### * #### #### #### * ## ## ## * </pre> * @param rhs the <code>Area</code> to be subtracted from the * current shape * @throws NullPointerException if <code>rhs</code> is null * @since 1.2 */ public void subtract(Area rhs) { curves = new AreaOp.SubOp().calculate(this.curves, rhs.curves); invalidateBounds(); } /** * Sets the shape of this <code>Area</code> to the intersection of * its current shape and the shape of the specified <code>Area</code>. * The resulting shape of this <code>Area</code> will include * only areas that were contained in both this <code>Area</code> * and also in the specified <code>Area</code>. * <pre> * // Example: * Area a1 = new Area([triangle 0,0 => 8,0 => 0,8]); * Area a2 = new Area([triangle 0,0 => 8,0 => 8,8]); * a1.intersect(a2); * * a1(before) intersect a2 = a1(after) * * ################ ################ ################ * ############## ############## ############ * ############ ############ ######## * ########## ########## #### * ######## ######## * ###### ###### * #### #### * ## ## * </pre> * @param rhs the <code>Area</code> to be intersected with this * <code>Area</code> * @throws NullPointerException if <code>rhs</code> is null * @since 1.2 */ public void intersect(Area rhs) { curves = new AreaOp.IntOp().calculate(this.curves, rhs.curves); invalidateBounds(); } /** * Sets the shape of this <code>Area</code> to be the combined area * of its current shape and the shape of the specified <code>Area</code>, * minus their intersection. * The resulting shape of this <code>Area</code> will include * only areas that were contained in either this <code>Area</code> * or in the specified <code>Area</code>, but not in both. * <pre> * // Example: * Area a1 = new Area([triangle 0,0 => 8,0 => 0,8]); * Area a2 = new Area([triangle 0,0 => 8,0 => 8,8]); * a1.exclusiveOr(a2); * * a1(before) xor a2 = a1(after) * * ################ ################ * ############## ############## ## ## * ############ ############ #### #### * ########## ########## ###### ###### * ######## ######## ################ * ###### ###### ###### ###### * #### #### #### #### * ## ## ## ## * </pre> * @param rhs the <code>Area</code> to be exclusive ORed with this * <code>Area</code>. * @throws NullPointerException if <code>rhs</code> is null * @since 1.2 */ public void exclusiveOr(Area rhs) { curves = new AreaOp.XorOp().calculate(this.curves, rhs.curves); invalidateBounds(); } /** * Removes all of the geometry from this <code>Area</code> and * restores it to an empty area. * @since 1.2 */ public void reset() { curves = new Vector(); invalidateBounds(); } /** * Tests whether this <code>Area</code> object encloses any area. * @return <code>true</code> if this <code>Area</code> object * represents an empty area; <code>false</code> otherwise. * @since 1.2 */ public boolean isEmpty() { return (curves.size() == 0); } /** * Tests whether this <code>Area</code> consists entirely of * straight edged polygonal geometry. * @return <code>true</code> if the geometry of this * <code>Area</code> consists entirely of line segments; * <code>false</code> otherwise. * @since 1.2 */ public boolean isPolygonal() { Enumeration enum_ = curves.elements(); while (enum_.hasMoreElements()) { if (((Curve) enum_.nextElement()).getOrder() > 1) { return false; } } return true; } /** * Tests whether this <code>Area</code> is rectangular in shape. * @return <code>true</code> if the geometry of this * <code>Area</code> is rectangular in shape; <code>false</code> * otherwise. * @since 1.2 */ public boolean isRectangular() { int size = curves.size(); if (size == 0) { return true; } if (size > 3) { return false; } Curve c1 = (Curve) curves.get(1); Curve c2 = (Curve) curves.get(2); if (c1.getOrder() != 1 || c2.getOrder() != 1) { return false; } if (c1.getXTop() != c1.getXBot() || c2.getXTop() != c2.getXBot()) { return false; } if (c1.getYTop() != c2.getYTop() || c1.getYBot() != c2.getYBot()) { // One might be able to prove that this is impossible... return false; } return true; } /** * Tests whether this <code>Area</code> is comprised of a single * closed subpath. This method returns <code>true</code> if the * path contains 0 or 1 subpaths, or <code>false</code> if the path * contains more than 1 subpath. The subpaths are counted by the * number of {@link PathIterator#SEG_MOVETO SEG_MOVETO} segments * that appear in the path. * @return <code>true</code> if the <code>Area</code> is comprised * of a single basic geometry; <code>false</code> otherwise. * @since 1.2 */ public boolean isSingular() { if (curves.size() < 3) { return true; } Enumeration enum_ = curves.elements(); enum_.nextElement(); // First Order0 "moveto" while (enum_.hasMoreElements()) { if (((Curve) enum_.nextElement()).getOrder() == 0) { return false; } } return true; } private Rectangle2D cachedBounds; private void invalidateBounds() { cachedBounds = null; } private Rectangle2D getCachedBounds() { if (cachedBounds != null) { return cachedBounds; } Rectangle2D r = new Rectangle2D.Double(); if (curves.size() > 0) { Curve c = (Curve) curves.get(0); // First point is always an order 0 curve (moveto) r.setRect(c.getX0(), c.getY0(), 0, 0); for (int i = 1; i < curves.size(); i++) { ((Curve) curves.get(i)).enlarge(r); } } return (cachedBounds = r); } /** * Returns a high precision bounding {@link Rectangle2D} that * completely encloses this <code>Area</code>. * <p> * The Area class will attempt to return the tightest bounding * box possible for the Shape. The bounding box will not be * padded to include the control points of curves in the outline * of the Shape, but should tightly fit the actual geometry of * the outline itself. * @return the bounding <code>Rectangle2D</code> for the * <code>Area</code>. * @since 1.2 */ public Rectangle2D getBounds2D() { return getCachedBounds().getBounds2D(); } /** * Returns a bounding {@link Rectangle} that completely encloses * this <code>Area</code>. * <p> * The Area class will attempt to return the tightest bounding * box possible for the Shape. The bounding box will not be * padded to include the control points of curves in the outline * of the Shape, but should tightly fit the actual geometry of * the outline itself. Since the returned object represents * the bounding box with integers, the bounding box can only be * as tight as the nearest integer coordinates that encompass * the geometry of the Shape. * @return the bounding <code>Rectangle</code> for the * <code>Area</code>. * @since 1.2 */ public Rectangle getBounds() { return getCachedBounds().getBounds(); } /** * Returns an exact copy of this <code>Area</code> object. * @return Created clone object * @since 1.2 */ public Object clone() { return new Area(this); } /** * Tests whether the geometries of the two <code>Area</code> objects * are equal. * This method will return false if the argument is null. * @param other the <code>Area</code> to be compared to this * <code>Area</code> * @return <code>true</code> if the two geometries are equal; * <code>false</code> otherwise. * @since 1.2 */ public boolean equals(Area other) { // REMIND: A *much* simpler operation should be possible... // Should be able to do a curve-wise comparison since all Areas // should evaluate their curves in the same top-down order. if (other == this) { return true; } if (other == null) { return false; } Vector c = new AreaOp.XorOp().calculate(this.curves, other.curves); return c.isEmpty(); } /** * Transforms the geometry of this <code>Area</code> using the specified * {@link AffineTransform}. The geometry is transformed in place, which * permanently changes the enclosed area defined by this object. * @param t the transformation used to transform the area * @throws NullPointerException if <code>t</code> is null * @since 1.2 */ public void transform(AffineTransform t) { if (t == null) { throw new NullPointerException("transform must not be null"); } // REMIND: A simpler operation can be performed for some types // of transform. curves = pathToCurves(getPathIterator(t)); invalidateBounds(); } /** * Creates a new <code>Area</code> object that contains the same * geometry as this <code>Area</code> transformed by the specified * <code>AffineTransform</code>. This <code>Area</code> object * is unchanged. * @param t the specified <code>AffineTransform</code> used to transform * the new <code>Area</code> * @throws NullPointerException if <code>t</code> is null * @return a new <code>Area</code> object representing the transformed * geometry. * @since 1.2 */ public Area createTransformedArea(AffineTransform t) { Area a = new Area(this); a.transform(t); return a; } /** * {@inheritDoc} * @since 1.2 */ public boolean contains(double x, double y) { if (!getCachedBounds().contains(x, y)) { return false; } Enumeration enum_ = curves.elements(); int crossings = 0; while (enum_.hasMoreElements()) { Curve c = (Curve) enum_.nextElement(); crossings += c.crossingsFor(x, y); } return ((crossings & 1) == 1); } /** * {@inheritDoc} * @since 1.2 */ public boolean contains(Point2D p) { return contains(p.getX(), p.getY()); } /** * {@inheritDoc} * @since 1.2 */ public boolean contains(double x, double y, double w, double h) { if (w < 0 || h < 0) { return false; } if (!getCachedBounds().contains(x, y, w, h)) { return false; } Crossings c = Crossings.findCrossings(curves, x, y, x+w, y+h); return (c != null && c.covers(y, y+h)); } /** * {@inheritDoc} * @since 1.2 */ public boolean contains(Rectangle2D r) { return contains(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } /** * {@inheritDoc} * @since 1.2 */ public boolean intersects(double x, double y, double w, double h) { if (w < 0 || h < 0) { return false; } if (!getCachedBounds().intersects(x, y, w, h)) { return false; } Crossings c = Crossings.findCrossings(curves, x, y, x+w, y+h); return (c == null || !c.isEmpty()); } /** * {@inheritDoc} * @since 1.2 */ public boolean intersects(Rectangle2D r) { return intersects(r.getX(), r.getY(), r.getWidth(), r.getHeight()); } /** * Creates a {@link PathIterator} for the outline of this * <code>Area</code> object. This <code>Area</code> object is unchanged. * @param at an optional <code>AffineTransform</code> to be applied to * the coordinates as they are returned in the iteration, or * <code>null</code> if untransformed coordinates are desired * @return the <code>PathIterator</code> object that returns the * geometry of the outline of this <code>Area</code>, one * segment at a time. * @since 1.2 */ public PathIterator getPathIterator(AffineTransform at) { return new AreaIterator(curves, at); } /** * Creates a <code>PathIterator</code> for the flattened outline of * this <code>Area</code> object. Only uncurved path segments * represented by the SEG_MOVETO, SEG_LINETO, and SEG_CLOSE point * types are returned by the iterator. This <code>Area</code> * object is unchanged. * @param at an optional <code>AffineTransform</code> to be * applied to the coordinates as they are returned in the * iteration, or <code>null</code> if untransformed coordinates * are desired * @param flatness the maximum amount that the control points * for a given curve can vary from colinear before a subdivided * curve is replaced by a straight line connecting the end points * @return the <code>PathIterator</code> object that returns the * geometry of the outline of this <code>Area</code>, one segment * at a time. * @since 1.2 */ public PathIterator getPathIterator(AffineTransform at, double flatness) { return new FlatteningPathIterator(getPathIterator(at), flatness); } } class AreaIterator implements PathIterator { private AffineTransform transform; private Vector curves; private int index; private Curve prevcurve; private Curve thiscurve; public AreaIterator(Vector curves, AffineTransform at) { this.curves = curves; this.transform = at; if (curves.size() >= 1) { thiscurve = (Curve) curves.get(0); } } public int getWindingRule() { // REMIND: Which is better, EVEN_ODD or NON_ZERO? // The paths calculated could be classified either way. //return WIND_EVEN_ODD; return WIND_NON_ZERO; } public boolean isDone() { return (prevcurve == null && thiscurve == null); } public void next() { if (prevcurve != null) { prevcurve = null; } else { prevcurve = thiscurve; index++; if (index < curves.size()) { thiscurve = (Curve) curves.get(index); if (thiscurve.getOrder() != 0 && prevcurve.getX1() == thiscurve.getX0() && prevcurve.getY1() == thiscurve.getY0()) { prevcurve = null; } } else { thiscurve = null; } } } public int currentSegment(float coords[]) { double dcoords[] = new double[6]; int segtype = currentSegment(dcoords); int numpoints = (segtype == SEG_CLOSE ? 0 : (segtype == SEG_QUADTO ? 2 : (segtype == SEG_CUBICTO ? 3 : 1))); for (int i = 0; i < numpoints * 2; i++) { coords[i] = (float) dcoords[i]; } return segtype; } public int currentSegment(double coords[]) { int segtype; int numpoints; if (prevcurve != null) { // Need to finish off junction between curves if (thiscurve == null || thiscurve.getOrder() == 0) { return SEG_CLOSE; } coords[0] = thiscurve.getX0(); coords[1] = thiscurve.getY0(); segtype = SEG_LINETO; numpoints = 1; } else if (thiscurve == null) { throw new NoSuchElementException("area iterator out of bounds"); } else { segtype = thiscurve.getSegment(coords); numpoints = thiscurve.getOrder(); if (numpoints == 0) { numpoints = 1; } } if (transform != null) { transform.transform(coords, 0, coords, 0, numpoints); } return segtype; } }