    001    /* ========================================================================
    002     * JCommon : a free general purpose class library for the Java(tm) platform
    003     * ========================================================================
    004     *
    005     * (C) Copyright 2000-2005, 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-2005, 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.17 2005/11/02 16:31:50 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     *
    059     */
    060
    061    package org.jfree.util;
    062
    063    import java.awt.Graphics2D;
    064    import java.awt.Polygon;
    065    import java.awt.Shape;
    066    import java.awt.geom.AffineTransform;
    067    import java.awt.geom.Arc2D;
    068    import java.awt.geom.Ellipse2D;
    069    import java.awt.geom.GeneralPath;
    070    import java.awt.geom.Line2D;
    071    import java.awt.geom.PathIterator;
    072    import java.awt.geom.Point2D;
    073    import java.awt.geom.Rectangle2D;
    074    import java.util.Arrays;
    075
    076    import org.jfree.ui.RectangleAnchor;
    077
    078    /**
    079     * Utility methods for {@link Shape} objects.
    080     *
    081     * @author David Gilbert
    082     */
    083    public class ShapeUtilities {
    084
    085        /**
    086         * Prevents instantiation.
    087         */
    088        private ShapeUtilities() {
    089        }
    090
    091        /**
    092         * Returns a clone of the specified shape, or <code>null</code>.  At the
    093         * current time, this method supports cloning for instances of
    094         * <code>Line2D</code>, <code>RectangularShape</code>, <code>Area</code>
    095         * and <code>GeneralPath</code>.
    096         * <p>
    097         * <code>RectangularShape</code> includes <code>Arc2D</code>,
    098         * <code>Ellipse2D</code>, <code>Rectangle2D</code>,
    099         * <code>RoundRectangle2D</code>.
    100         *
    101         * @param shape  the shape to clone (<code>null</code> permitted,
    102         *               returns <code>null</code>).
    103         *
    104         * @return A clone or <code>null</code>.
    105         */
    106        public static Shape clone(final Shape shape) {
    107
    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 = p1.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            );
    321            return transform.createTransformedShape(shape);
    322        }
    323
    324        /**
    325         * Translates a shape to a new location such that the anchor point
    326         * (relative to the rectangular bounds of the shape) aligns with the
    327         * specified (x, y) coordinate in Java2D space.
    328         *
    329         * @param shape  the shape (<code>null</code> not permitted).
    330         * @param anchor  the anchor (<code>null</code> not permitted).
    331         * @param locationX  the x-coordinate (in Java2D space).
    332         * @param locationY  the y-coordinate (in Java2D space).
    333         *
    334         * @return A new and translated shape.
    335         */
    336        public static Shape createTranslatedShape(final Shape shape,
    337                                                  final RectangleAnchor anchor,
    338                                                  final double locationX,
    339                                                  final double locationY) {
    340            if (shape == null) {
    341                throw new IllegalArgumentException("Null 'shape' argument.");
    342            }
    343            if (anchor == null) {
    344                throw new IllegalArgumentException("Null 'anchor' argument.");
    345            }
    346            Point2D anchorPoint = RectangleAnchor.coordinates(
    347                shape.getBounds2D(), anchor
    348            );
    349            final AffineTransform transform = AffineTransform.getTranslateInstance(
    350                locationX - anchorPoint.getX(), locationY - anchorPoint.getY()
    351            );
    352            return transform.createTransformedShape(shape);
    353        }
    354
    355        /**
    356         * Rotates a shape about the specified coordinates.
    357         *
    358         * @param base  the shape (<code>null</code> permitted, returns
    359         *              <code>null</code>).
    360         * @param angle  the angle (in radians).
    361         * @param x  the x coordinate for the rotation point (in Java2D space).
    362         * @param y  the y coordinate for the rotation point (in Java2D space).
    363         *
    364         * @return the rotated shape.
    365         */
    366        public static Shape rotateShape(final Shape base, final double angle,
    367                                        final float x, final float y) {
    368            if (base == null) {
    369                return null;
    370            }
    371            final AffineTransform rotate = AffineTransform.getRotateInstance(
    372                angle, x, y
    373            );
    374            final Shape result = rotate.createTransformedShape(base);
    375            return result;
    376        }
    377
    378        /**
    379         * Draws a shape with the specified rotation about <code>(x, y)</code>.
    380         *
    381         * @param g2  the graphics device (<code>null</code> not permitted).
    382         * @param shape  the shape (<code>null</code> not permitted).
    383         * @param angle  the angle (in radians).
    384         * @param x  the x coordinate for the rotation point.
    385         * @param y  the y coordinate for the rotation point.
    386         */
    387        public static void drawRotatedShape(final Graphics2D g2, final Shape shape,
    388                                            final double angle,
    389                                            final float x, final float y) {
    390
    391            final AffineTransform saved = g2.getTransform();
    392            final AffineTransform rotate = AffineTransform.getRotateInstance(
    393                angle, x, y
    394            );
    395            g2.transform(rotate);
    396            g2.draw(shape);
    397            g2.setTransform(saved);
    398
    399        }
    400
    401        /** A useful constant used internally. */
    402        private static final float SQRT2 = (float) Math.pow(2.0, 0.5);
    403
    404        /**
    405         * Creates a diagonal cross shape.
    406         *
    407         * @param l  the length of each 'arm'.
    408         * @param t  the thickness.
    409         *
    410         * @return A diagonal cross shape.
    411         */
    412        public static Shape createDiagonalCross(final float l, final float t) {
    413            final GeneralPath p0 = new GeneralPath();
    414            p0.moveTo(-l - t, -l + t);
    415            p0.lineTo(-l + t, -l - t);
    416            p0.lineTo(0.0f, -t * SQRT2);
    417            p0.lineTo(l - t, -l - t);
    418            p0.lineTo(l + t, -l + t);
    419            p0.lineTo(t * SQRT2, 0.0f);
    420            p0.lineTo(l + t, l - t);
    421            p0.lineTo(l - t, l + t);
    422            p0.lineTo(0.0f, t * SQRT2);
    423            p0.lineTo(-l + t, l + t);
    424            p0.lineTo(-l - t, l - t);
    425            p0.lineTo(-t * SQRT2, 0.0f);
    426            p0.closePath();
    427            return p0;
    428        }
    429
    430        /**
    431         * Creates a diagonal cross shape.
    432         *
    433         * @param l  the length of each 'arm'.
    434         * @param t  the thickness.
    435         *
    436         * @return A diagonal cross shape.
    437         */
    438        public static Shape createRegularCross(final float l, final float t) {
    439            final GeneralPath p0 = new GeneralPath();
    440            p0.moveTo(-l, t);
    441            p0.lineTo(-t, t);
    442            p0.lineTo(-t, l);
    443            p0.lineTo(t, l);
    444            p0.lineTo(t, t);
    445            p0.lineTo(l, t);
    446            p0.lineTo(l, -t);
    447            p0.lineTo(t, -t);
    448            p0.lineTo(t, -l);
    449            p0.lineTo(-t, -l);
    450            p0.lineTo(-t, -t);
    451            p0.lineTo(-l, -t);
    452            p0.closePath();
    453            return p0;
    454        }
    455
    456        /**
    457         * Creates a diamond shape.
    458         *
    459         * @param s  the size factor (equal to half the height of the diamond).
    460         *
    461         * @return A diamond shape.
    462         */
    463        public static Shape createDiamond(final float s) {
    464            final GeneralPath p0 = new GeneralPath();
    465            p0.moveTo(0.0f, -s);
    466            p0.lineTo(s, 0.0f);
    467            p0.lineTo(0.0f, s);
    468            p0.lineTo(-s, 0.0f);
    469            p0.closePath();
    470            return p0;
    471        }
    472
    473        /**
    474         * Creates a triangle shape that points upwards.
    475         *
    476         * @param s  the size factor (equal to half the height of the triangle).
    477         *
    478         * @return A triangle shape.
    479         */
    480        public static Shape createUpTriangle(final float s) {
    481            final GeneralPath p0 = new GeneralPath();
    482            p0.moveTo(0.0f, -s);
    483            p0.lineTo(s, s);
    484            p0.lineTo(-s, s);
    485            p0.closePath();
    486            return p0;
    487        }
    488
    489        /**
    490         * Creates a triangle shape that points downwards.
    491         *
    492         * @param s  the size factor (equal to half the height of the triangle).
    493         *
    494         * @return A triangle shape.
    495         */
    496        public static Shape createDownTriangle(final float s) {
    497            final GeneralPath p0 = new GeneralPath();
    498            p0.moveTo(0.0f, s);
    499            p0.lineTo(s, -s);
    500            p0.lineTo(-s, -s);
    501            p0.closePath();
    502            return p0;
    503        }
    504
    505        /**
    506         * Creates a region surrounding a line segment by 'widening' the line
    507         * segment.  A typical use for this method is the creation of a
    508         * 'clickable' region for a line that is displayed on-screen.
    509         *
    510         * @param line  the line (<code>null</code> not permitted).
    511         * @param width  the width of the region.
    512         *
    513         * @return A region that surrounds the line.
    514         */
    515        public static Shape createLineRegion(final Line2D line, final float width) {
    516            final GeneralPath result = new GeneralPath();
    517            final float x1 = (float) line.getX1();
    518            final float x2 = (float) line.getX2();
    519            final float y1 = (float) line.getY1();
    520            final float y2 = (float) line.getY2();
    521            if ((x2 - x1) != 0.0) {
    522                final double theta = Math.atan((y2 - y1) / (x2 - x1));
    523                final float dx = (float) Math.sin(theta) * width;
    524                final float dy = (float) Math.cos(theta) * width;
    525                result.moveTo(x1 - dx, y1 + dy);
    526                result.lineTo(x1 + dx, y1 - dy);
    527                result.lineTo(x2 + dx, y2 - dy);
    528                result.lineTo(x2 - dx, y2 + dy);
    529                result.closePath();
    530            }
    531            else {
    532                // special case, vertical line
    533                result.moveTo(x1 - width / 2.0f, y1);
    534                result.lineTo(x1 + width / 2.0f, y1);
    535                result.lineTo(x2 + width / 2.0f, y2);
    536                result.lineTo(x2 - width / 2.0f, y2);
    537                result.closePath();
    538            }
    539            return result;
    540        }
    541
    542        /**
    543         * Returns a point based on (x, y) but constrained to be within the bounds
    544         * of a given rectangle.
    545         *
    546         * @param x  the x-coordinate.
    547         * @param y  the y-coordinate.
    548         * @param area  the constraining rectangle (<code>null</code> not
    549         *              permitted).
    550         *
    551         * @return A point within the rectangle.
    552         *
    553         * @throws NullPointerException if <code>area</code> is <code>null</code>.
    554         */
    555        public static Point2D getPointInRectangle(double x, double y,
    556                                                  final Rectangle2D area) {
    557
    558            x = Math.max(area.getMinX(), Math.min(x, area.getMaxX()));
    559            y = Math.max(area.getMinY(), Math.min(y, area.getMaxY()));
    560            return new Point2D.Double(x, y);
    561
    562        }
    563
    564        /**
    565         * Checks, whether the given rectangle1 fully contains rectangle 2
    566         * (even if rectangle 2 has a height or width of zero!).
    567         *
    568         * @param rect1  the first rectangle.
    569         * @param rect2  the second rectangle.
    570         *
    571         * @return A boolean.
    572         */
    573        public static boolean contains(final Rectangle2D rect1,
    574                                       final Rectangle2D rect2) {
    575
    576            final double x0 = rect1.getX();
    577            final double y0 = rect1.getY();
    578            final double x = rect2.getX();
    579            final double y = rect2.getY();
    580            final double w = rect2.getWidth();
    581            final double h = rect2.getHeight();
    582
    583            return ((x >= x0) && (y >= y0)
    584                    && ((x + w) <= (x0 + rect1.getWidth()))
    585                    && ((y + h) <= (y0 + rect1.getHeight())));
    586
    587        }
    588
    589
    590        /**
    591         * Checks, whether the given rectangle1 fully contains rectangle 2
    592         * (even if rectangle 2 has a height or width of zero!).
    593         *
    594         * @param rect1  the first rectangle.
    595         * @param rect2  the second rectangle.
    596         *
    597         * @return A boolean.
    598         */
    599        public static boolean intersects (final Rectangle2D rect1,
    600                                          final Rectangle2D rect2) {
    601
    602          final double x0 = rect1.getX();
    603          final double y0 = rect1.getY();
    604
    605          final double x = rect2.getX();
    606          final double width = rect2.getWidth();
    607          final double y = rect2.getY();
    608          final double height = rect2.getHeight();
    609          return (x + width >= x0 && y + height >= y0 && x <= x0 + rect1.getWidth()
    610                  && y <= y0 + rect1.getHeight());
    611        }
    612    }