    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
    062    package org.jfree.util;
    063
    064    import java.awt.Graphics2D;
    065    import java.awt.Polygon;
    066    import java.awt.Shape;
    067    import java.awt.geom.AffineTransform;
    068    import java.awt.geom.Arc2D;
    069    import java.awt.geom.Ellipse2D;
    070    import java.awt.geom.GeneralPath;
    071    import java.awt.geom.Line2D;
    072    import java.awt.geom.PathIterator;
    073    import java.awt.geom.Point2D;
    074    import java.awt.geom.Rectangle2D;
    075    import java.util.Arrays;
    076
    077    import org.jfree.ui.RectangleAnchor;
    078
    079    /**
    080     * Utility methods for {@link Shape} objects.
    081     *
    082     * @author David Gilbert
    083     */
    084    public 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    }