    001/* ========================================================================
    002 * JCommon : a free general purpose class library for the Java(tm) platform
    003 * ========================================================================
    004 *
    005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jcommon/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025 * in the United States and other countries.]
    026 *
    027 * -------------------
    028 * ShapeUtilities.java
    029 * -------------------
    030 * (C)opyright 2003-2008, by Object Refinery Limited and Contributors.
    031 *
    032 * Original Author:  David Gilbert (for Object Refinery Limited);
    033 * Contributor(s):   -;
    034 *
    035 * $Id: ShapeUtilities.java,v 1.18 2008/06/02 06:58:28 mungady Exp $
    036 *
    037 * Changes
    038 * -------
    039 * 13-Aug-2003 : Version 1 (DG);
    040 * 16-Mar-2004 : Moved rotateShape() from RefineryUtilities.java to here (DG);
    041 * 13-May-2004 : Added new shape creation methods (DG);
    042 * 30-Sep-2004 : Added createLineRegion() method (DG);
    043 *               Moved drawRotatedShape() method from RefineryUtilities class
    044 *               to this class (DG);
    045 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
    046 * 26-Oct-2004 : Added a method to test the equality of two Line2D
    047 *               instances (DG);
    048 * 10-Nov-2004 : Added new translateShape() and equal(Ellipse2D, Ellipse2D)
    049 *               methods (DG);
    050 * 11-Nov-2004 : Renamed translateShape() --> createTranslatedShape() (DG);
    051 * 07-Jan-2005 : Minor Javadoc fix (DG);
    052 * 11-Jan-2005 : Removed deprecated code in preparation for 1.0.0 release (DG);
    053 * 21-Jan-2005 : Modified return type of RectangleAnchor.coordinates()
    054 *               method (DG);
    055 * 22-Feb-2005 : Added equality tests for Arc2D and GeneralPath (DG);
    056 * 16-Mar-2005 : Fixed bug where equal(Shape, Shape) fails for two Polygon
    057 *               instances (DG);
    058 * 01-Jun-2008 : Fixed bug in equal(GeneralPath, GeneralPath) method (DG);
    059 *
    060 */
    061
    062package org.jfree.util;
    063
    064import java.awt.Graphics2D;
    065import java.awt.Polygon;
    066import java.awt.Shape;
    067import java.awt.geom.AffineTransform;
    068import java.awt.geom.Arc2D;
    069import java.awt.geom.Ellipse2D;
    070import java.awt.geom.GeneralPath;
    071import java.awt.geom.Line2D;
    072import java.awt.geom.PathIterator;
    073import java.awt.geom.Point2D;
    074import java.awt.geom.Rectangle2D;
    075import java.util.Arrays;
    076
    077import org.jfree.ui.RectangleAnchor;
    078
    079/**
    080 * Utility methods for {@link Shape} objects.
    081 *
    082 * @author David Gilbert
    083 */
    084public class ShapeUtilities {
    085
    086    /**
    087     * Prevents instantiation.
    088     */
    089    private ShapeUtilities() {
    090    }
    091
    092    /**
    093     * Returns a clone of the specified shape, or <code>null</code>.  At the
    094     * current time, this method supports cloning for instances of
    095     * <code>Line2D</code>, <code>RectangularShape</code>, <code>Area</code>
    096     * and <code>GeneralPath</code>.
    097     * <p>
    098     * <code>RectangularShape</code> includes <code>Arc2D</code>,
    099     * <code>Ellipse2D</code>, <code>Rectangle2D</code>,
    100     * <code>RoundRectangle2D</code>.
    101     *
    102     * @param shape  the shape to clone (<code>null</code> permitted,
    103     *               returns <code>null</code>).
    104     *
    105     * @return A clone or <code>null</code>.
    106     */
    107    public static Shape clone(final Shape shape) {
    108        if (shape instanceof Cloneable) {
    109            try {
    110                return (Shape) ObjectUtilities.clone(shape);
    111            }
    112            catch (CloneNotSupportedException cnse) {
    113            }
    114        }
    115        final Shape result = null;
    116        return result;
    117    }
    118
    119    /**
    120     * Tests two shapes for equality.  If both shapes are <code>null</code>,
    121     * this method will return <code>true</code>.
    122     * <p>
    123     * In the current implementation, the following shapes are supported:
    124     * <code>Ellipse2D</code>, <code>Line2D</code> and <code>Rectangle2D</code>
    125     * (implicit).
    126     *
    127     * @param s1  the first shape (<code>null</code> permitted).
    128     * @param s2  the second shape (<code>null</code> permitted).
    129     *
    130     * @return A boolean.
    131     */
    132    public static boolean equal(final Shape s1, final Shape s2) {
    133        if (s1 instanceof Line2D && s2 instanceof Line2D) {
    134            return equal((Line2D) s1, (Line2D) s2);
    135        }
    136        else if (s1 instanceof Ellipse2D && s2 instanceof Ellipse2D) {
    137            return equal((Ellipse2D) s1, (Ellipse2D) s2);
    138        }
    139        else if (s1 instanceof Arc2D && s2 instanceof Arc2D) {
    140            return equal((Arc2D) s1, (Arc2D) s2);
    141        }
    142        else if (s1 instanceof Polygon && s2 instanceof Polygon) {
    143            return equal((Polygon) s1, (Polygon) s2);
    144        }
    145        else if (s1 instanceof GeneralPath && s2 instanceof GeneralPath) {
    146            return equal((GeneralPath) s1, (GeneralPath) s2);
    147        }
    148        else {
    149            // this will handle Rectangle2D...
    150            return ObjectUtilities.equal(s1, s2);
    151        }
    152    }
    153
    154    /**
    155     * Compares two lines are returns <code>true</code> if they are equal or
    156     * both <code>null</code>.
    157     *
    158     * @param l1  the first line (<code>null</code> permitted).
    159     * @param l2  the second line (<code>null</code> permitted).
    160     *
    161     * @return A boolean.
    162     */
    163    public static boolean equal(final Line2D l1, final Line2D l2) {
    164        if (l1 == null) {
    165            return (l2 == null);
    166        }
    167        if (l2 == null) {
    168            return false;
    169        }
    170        if (!l1.getP1().equals(l2.getP1())) {
    171            return false;
    172        }
    173        if (!l1.getP2().equals(l2.getP2())) {
    174            return false;
    175        }
    176        return true;
    177    }
    178
    179    /**
    180     * Compares two ellipses and returns <code>true</code> if they are equal or
    181     * both <code>null</code>.
    182     *
    183     * @param e1  the first ellipse (<code>null</code> permitted).
    184     * @param e2  the second ellipse (<code>null</code> permitted).
    185     *
    186     * @return A boolean.
    187     */
    188    public static boolean equal(final Ellipse2D e1, final Ellipse2D e2) {
    189        if (e1 == null) {
    190            return (e2 == null);
    191        }
    192        if (e2 == null) {
    193            return false;
    194        }
    195        if (!e1.getFrame().equals(e2.getFrame())) {
    196            return false;
    197        }
    198        return true;
    199    }
    200
    201    /**
    202     * Compares two arcs and returns <code>true</code> if they are equal or
    203     * both <code>null</code>.
    204     *
    205     * @param a1  the first arc (<code>null</code> permitted).
    206     * @param a2  the second arc (<code>null</code> permitted).
    207     *
    208     * @return A boolean.
    209     */
    210    public static boolean equal(final Arc2D a1, final Arc2D a2) {
    211        if (a1 == null) {
    212            return (a2 == null);
    213        }
    214        if (a2 == null) {
    215            return false;
    216        }
    217        if (!a1.getFrame().equals(a2.getFrame())) {
    218            return false;
    219        }
    220        if (a1.getAngleStart() != a2.getAngleStart()) {
    221            return false;
    222        }
    223        if (a1.getAngleExtent() != a2.getAngleExtent()) {
    224            return false;
    225        }
    226        if (a1.getArcType() != a2.getArcType()) {
    227            return false;
    228        }
    229        return true;
    230    }
    231
    232    /**
    233     * Tests two polygons for equality.  If both are <code>null</code> this
    234     * method returns <code>true</code>.
    235     *
    236     * @param p1  polygon 1 (<code>null</code> permitted).
    237     * @param p2  polygon 2 (<code>null</code> permitted).
    238     *
    239     * @return A boolean.
    240     */
    241    public static boolean equal(final Polygon p1, final Polygon p2) {
    242        if (p1 == null) {
    243            return (p2 == null);
    244        }
    245        if (p2 == null) {
    246            return false;
    247        }
    248        if (p1.npoints != p2.npoints) {
    249            return false;
    250        }
    251        if (!Arrays.equals(p1.xpoints, p2.xpoints)) {
    252            return false;
    253        }
    254        if (!Arrays.equals(p1.ypoints, p2.ypoints)) {
    255            return false;
    256        }
    257        return true;
    258    }
    259
    260    /**
    261     * Tests two polygons for equality.  If both are <code>null</code> this
    262     * method returns <code>true</code>.
    263     *
    264     * @param p1  path 1 (<code>null</code> permitted).
    265     * @param p2  path 2 (<code>null</code> permitted).
    266     *
    267     * @return A boolean.
    268     */
    269    public static boolean equal(final GeneralPath p1, final GeneralPath p2) {
    270        if (p1 == null) {
    271            return (p2 == null);
    272        }
    273        if (p2 == null) {
    274            return false;
    275        }
    276        if (p1.getWindingRule() != p2.getWindingRule()) {
    277            return false;
    278        }
    279        PathIterator iterator1 = p1.getPathIterator(null);
    280        PathIterator iterator2 = p2.getPathIterator(null);
    281        double[] d1 = new double[6];
    282        double[] d2 = new double[6];
    283        boolean done = iterator1.isDone() && iterator2.isDone();
    284        while (!done) {
    285            if (iterator1.isDone() != iterator2.isDone()) {
    286                return false;
    287            }
    288            int seg1 = iterator1.currentSegment(d1);
    289            int seg2 = iterator2.currentSegment(d2);
    290            if (seg1 != seg2) {
    291                return false;
    292            }
    293            if (!Arrays.equals(d1, d2)) {
    294                return false;
    295            }
    296            iterator1.next();
    297            iterator2.next();
    298            done = iterator1.isDone() && iterator2.isDone();
    299        }
    300        return true;
    301    }
    302
    303    /**
    304     * Creates and returns a translated shape.
    305     *
    306     * @param shape  the shape (<code>null</code> not permitted).
    307     * @param transX  the x translation (in Java2D space).
    308     * @param transY  the y translation (in Java2D space).
    309     *
    310     * @return The translated shape.
    311     */
    312    public static Shape createTranslatedShape(final Shape shape,
    313                                              final double transX,
    314                                              final double transY) {
    315        if (shape == null) {
    316            throw new IllegalArgumentException("Null 'shape' argument.");
    317        }
    318        final AffineTransform transform = AffineTransform.getTranslateInstance(
    319                transX, transY);
    320        return transform.createTransformedShape(shape);
    321    }
    322
    323    /**
    324     * Translates a shape to a new location such that the anchor point
    325     * (relative to the rectangular bounds of the shape) aligns with the
    326     * specified (x, y) coordinate in Java2D space.
    327     *
    328     * @param shape  the shape (<code>null</code> not permitted).
    329     * @param anchor  the anchor (<code>null</code> not permitted).
    330     * @param locationX  the x-coordinate (in Java2D space).
    331     * @param locationY  the y-coordinate (in Java2D space).
    332     *
    333     * @return A new and translated shape.
    334     */
    335    public static Shape createTranslatedShape(final Shape shape,
    336                                              final RectangleAnchor anchor,
    337                                              final double locationX,
    338                                              final double locationY) {
    339        if (shape == null) {
    340            throw new IllegalArgumentException("Null 'shape' argument.");
    341        }
    342        if (anchor == null) {
    343            throw new IllegalArgumentException("Null 'anchor' argument.");
    344        }
    345        Point2D anchorPoint = RectangleAnchor.coordinates(
    346                shape.getBounds2D(), anchor);
    347        final AffineTransform transform = AffineTransform.getTranslateInstance(
    348                locationX - anchorPoint.getX(), locationY - anchorPoint.getY());
    349        return transform.createTransformedShape(shape);
    350    }
    351
    352    /**
    353     * Rotates a shape about the specified coordinates.
    354     *
    355     * @param base  the shape (<code>null</code> permitted, returns
    356     *              <code>null</code>).
    357     * @param angle  the angle (in radians).
    358     * @param x  the x coordinate for the rotation point (in Java2D space).
    359     * @param y  the y coordinate for the rotation point (in Java2D space).
    360     *
    361     * @return the rotated shape.
    362     */
    363    public static Shape rotateShape(final Shape base, final double angle,
    364                                    final float x, final float y) {
    365        if (base == null) {
    366            return null;
    367        }
    368        final AffineTransform rotate = AffineTransform.getRotateInstance(
    369                angle, x, y);
    370        final Shape result = rotate.createTransformedShape(base);
    371        return result;
    372    }
    373
    374    /**
    375     * Draws a shape with the specified rotation about <code>(x, y)</code>.
    376     *
    377     * @param g2  the graphics device (<code>null</code> not permitted).
    378     * @param shape  the shape (<code>null</code> not permitted).
    379     * @param angle  the angle (in radians).
    380     * @param x  the x coordinate for the rotation point.
    381     * @param y  the y coordinate for the rotation point.
    382     */
    383    public static void drawRotatedShape(final Graphics2D g2, final Shape shape,
    384                                        final double angle,
    385                                        final float x, final float y) {
    386
    387        final AffineTransform saved = g2.getTransform();
    388        final AffineTransform rotate = AffineTransform.getRotateInstance(
    389                angle, x, y);
    390        g2.transform(rotate);
    391        g2.draw(shape);
    392        g2.setTransform(saved);
    393
    394    }
    395
    396    /** A useful constant used internally. */
    397    private static final float SQRT2 = (float) Math.pow(2.0, 0.5);
    398
    399    /**
    400     * Creates a diagonal cross shape.
    401     *
    402     * @param l  the length of each 'arm'.
    403     * @param t  the thickness.
    404     *
    405     * @return A diagonal cross shape.
    406     */
    407    public static Shape createDiagonalCross(final float l, final float t) {
    408        final GeneralPath p0 = new GeneralPath();
    409        p0.moveTo(-l - t, -l + t);
    410        p0.lineTo(-l + t, -l - t);
    411        p0.lineTo(0.0f, -t * SQRT2);
    412        p0.lineTo(l - t, -l - t);
    413        p0.lineTo(l + t, -l + t);
    414        p0.lineTo(t * SQRT2, 0.0f);
    415        p0.lineTo(l + t, l - t);
    416        p0.lineTo(l - t, l + t);
    417        p0.lineTo(0.0f, t * SQRT2);
    418        p0.lineTo(-l + t, l + t);
    419        p0.lineTo(-l - t, l - t);
    420        p0.lineTo(-t * SQRT2, 0.0f);
    421        p0.closePath();
    422        return p0;
    423    }
    424
    425    /**
    426     * Creates a diagonal cross shape.
    427     *
    428     * @param l  the length of each 'arm'.
    429     * @param t  the thickness.
    430     *
    431     * @return A diagonal cross shape.
    432     */
    433    public static Shape createRegularCross(final float l, final float t) {
    434        final GeneralPath p0 = new GeneralPath();
    435        p0.moveTo(-l, t);
    436        p0.lineTo(-t, t);
    437        p0.lineTo(-t, l);
    438        p0.lineTo(t, l);
    439        p0.lineTo(t, t);
    440        p0.lineTo(l, t);
    441        p0.lineTo(l, -t);
    442        p0.lineTo(t, -t);
    443        p0.lineTo(t, -l);
    444        p0.lineTo(-t, -l);
    445        p0.lineTo(-t, -t);
    446        p0.lineTo(-l, -t);
    447        p0.closePath();
    448        return p0;
    449    }
    450
    451    /**
    452     * Creates a diamond shape.
    453     *
    454     * @param s  the size factor (equal to half the height of the diamond).
    455     *
    456     * @return A diamond shape.
    457     */
    458    public static Shape createDiamond(final float s) {
    459        final GeneralPath p0 = new GeneralPath();
    460        p0.moveTo(0.0f, -s);
    461        p0.lineTo(s, 0.0f);
    462        p0.lineTo(0.0f, s);
    463        p0.lineTo(-s, 0.0f);
    464        p0.closePath();
    465        return p0;
    466    }
    467
    468    /**
    469     * Creates a triangle shape that points upwards.
    470     *
    471     * @param s  the size factor (equal to half the height of the triangle).
    472     *
    473     * @return A triangle shape.
    474     */
    475    public static Shape createUpTriangle(final float s) {
    476        final GeneralPath p0 = new GeneralPath();
    477        p0.moveTo(0.0f, -s);
    478        p0.lineTo(s, s);
    479        p0.lineTo(-s, s);
    480        p0.closePath();
    481        return p0;
    482    }
    483
    484    /**
    485     * Creates a triangle shape that points downwards.
    486     *
    487     * @param s  the size factor (equal to half the height of the triangle).
    488     *
    489     * @return A triangle shape.
    490     */
    491    public static Shape createDownTriangle(final float s) {
    492        final GeneralPath p0 = new GeneralPath();
    493        p0.moveTo(0.0f, s);
    494        p0.lineTo(s, -s);
    495        p0.lineTo(-s, -s);
    496        p0.closePath();
    497        return p0;
    498    }
    499
    500    /**
    501     * Creates a region surrounding a line segment by 'widening' the line
    502     * segment.  A typical use for this method is the creation of a
    503     * 'clickable' region for a line that is displayed on-screen.
    504     *
    505     * @param line  the line (<code>null</code> not permitted).
    506     * @param width  the width of the region.
    507     *
    508     * @return A region that surrounds the line.
    509     */
    510    public static Shape createLineRegion(final Line2D line, final float width) {
    511        final GeneralPath result = new GeneralPath();
    512        final float x1 = (float) line.getX1();
    513        final float x2 = (float) line.getX2();
    514        final float y1 = (float) line.getY1();
    515        final float y2 = (float) line.getY2();
    516        if ((x2 - x1) != 0.0) {
    517            final double theta = Math.atan((y2 - y1) / (x2 - x1));
    518            final float dx = (float) Math.sin(theta) * width;
    519            final float dy = (float) Math.cos(theta) * width;
    520            result.moveTo(x1 - dx, y1 + dy);
    521            result.lineTo(x1 + dx, y1 - dy);
    522            result.lineTo(x2 + dx, y2 - dy);
    523            result.lineTo(x2 - dx, y2 + dy);
    524            result.closePath();
    525        }
    526        else {
    527            // special case, vertical line
    528            result.moveTo(x1 - width / 2.0f, y1);
    529            result.lineTo(x1 + width / 2.0f, y1);
    530            result.lineTo(x2 + width / 2.0f, y2);
    531            result.lineTo(x2 - width / 2.0f, y2);
    532            result.closePath();
    533        }
    534        return result;
    535    }
    536
    537    /**
    538     * Returns a point based on (x, y) but constrained to be within the bounds
    539     * of a given rectangle.
    540     *
    541     * @param x  the x-coordinate.
    542     * @param y  the y-coordinate.
    543     * @param area  the constraining rectangle (<code>null</code> not
    544     *              permitted).
    545     *
    546     * @return A point within the rectangle.
    547     *
    548     * @throws NullPointerException if <code>area</code> is <code>null</code>.
    549     */
    550    public static Point2D getPointInRectangle(double x, double y,
    551                                              final Rectangle2D area) {
    552
    553        x = Math.max(area.getMinX(), Math.min(x, area.getMaxX()));
    554        y = Math.max(area.getMinY(), Math.min(y, area.getMaxY()));
    555        return new Point2D.Double(x, y);
    556
    557    }
    558
    559    /**
    560     * Checks, whether the given rectangle1 fully contains rectangle 2
    561     * (even if rectangle 2 has a height or width of zero!).
    562     *
    563     * @param rect1  the first rectangle.
    564     * @param rect2  the second rectangle.
    565     *
    566     * @return A boolean.
    567     */
    568    public static boolean contains(final Rectangle2D rect1,
    569                                   final Rectangle2D rect2) {
    570
    571        final double x0 = rect1.getX();
    572        final double y0 = rect1.getY();
    573        final double x = rect2.getX();
    574        final double y = rect2.getY();
    575        final double w = rect2.getWidth();
    576        final double h = rect2.getHeight();
    577
    578        return ((x >= x0) && (y >= y0)
    579                && ((x + w) <= (x0 + rect1.getWidth()))
    580                && ((y + h) <= (y0 + rect1.getHeight())));
    581
    582    }
    583
    584
    585    /**
    586     * Checks, whether the given rectangle1 fully contains rectangle 2
    587     * (even if rectangle 2 has a height or width of zero!).
    588     *
    589     * @param rect1  the first rectangle.
    590     * @param rect2  the second rectangle.
    591     *
    592     * @return A boolean.
    593     */
    594    public static boolean intersects (final Rectangle2D rect1,
    595                                      final Rectangle2D rect2) {
    596
    597      final double x0 = rect1.getX();
    598      final double y0 = rect1.getY();
    599
    600      final double x = rect2.getX();
    601      final double width = rect2.getWidth();
    602      final double y = rect2.getY();
    603      final double height = rect2.getHeight();
    604      return (x + width >= x0 && y + height >= y0 && x <= x0 + rect1.getWidth()
    605              && y <= y0 + rect1.getHeight());
    606    }
    607
    608}