ShapeTransform.java Source code

Java tutorial

Introduction

Here is the source code for ShapeTransform.java

Source

/**
 * 
 * JFreeReport : a free Java reporting library
 * 
 *
 * Project Info:  http://reporting.pentaho.org/
 *
 * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
 *
 * This library is free software; you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Foundation;
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 *
 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
 * in the United States and other countries.]
 *
 * ------------
 * ShapeTransform.java
 * ------------
 * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
 */

import java.awt.Dimension;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.Dimension2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;

/**
 * Utility class, which resizes or translates a Shape. The class contains
 * special handlers for Rectangles and Lines.
 * 
 * @author Thomas Morgner
 */
public final strictfp class ShapeTransform {
    // some constants for the cohenen-algorithmus
    /**
     * Flag for point lying left of clipping area.
     */
    public static final int LEFT = 0x01;

    /**
     * Flag for point lying between horizontal bounds of area.
     */
    public static final int H_CENTER = 0x02;

    /**
     * Flag for point lying right of clipping area.
     */
    public static final int RIGHT = 0x04;

    /**
     * Flag for point lying "below" clipping area.
     */
    public static final int BELOW = 0x10;

    /**
     * Flag for point lying between vertical bounds of clipping area.
     */
    public static final int V_CENTER = 0x20;

    /**
     * Flag for point lying "above" clipping area.
     */
    public static final int ABOVE = 0x40;

    /** A simple way to handle rounding errors. */
    private static final double DELTA = 0.000001;

    /**
     * Mask for points which are inside.
     */
    public static final int INSIDE = H_CENTER | V_CENTER;

    /**
     * Mask for points which are outside.
     */
    public static final int OUTSIDE = LEFT | RIGHT | BELOW | ABOVE;

    /**
     * Default constructor.
     */
    private ShapeTransform() {
    }

    /**
     * Resizes a line. Instead of creating a GeneralPath (as AffineTransform's
     * scale would do) we modify the line itself.
     * 
     * @param line
     *          the line that should be scaled
     * @param width
     *          the new width of the line bounds
     * @param height
     *          the new height of the line bounds
     * @return the scale Line2D object.
     */
    private static Line2D resizeLine(final Line2D line, final double width, final double height) {
        final Line2D newLine = getNormalizedLine(line);
        final Point2D p1 = newLine.getP1();
        final Point2D p2 = newLine.getP2();
        final double normPointX = (p1.getX() - p2.getX());
        final double normPointY = (p1.getY() - p2.getY());
        final double scaleX = (normPointX == 0) ? 1 : width / Math.abs(normPointX);
        final double scaleY = (normPointY == 0) ? 1 : height / Math.abs(normPointY);
        p2.setLocation((p2.getX() - p1.getX()) * scaleX + p1.getX(), (p2.getY() - p1.getY()) * scaleY + p1.getY());
        newLine.setLine(p1, p2);
        return newLine;
    }

    /**
     * Normalize the line; the point with the lowest X is the primary point, if
     * both points have the same X, that point with the lowest Y value wins.
     * 
     * @param line
     *          the original line
     * @return the normalized line
     */
    private static Line2D getNormalizedLine(final Line2D line) {
        final Line2D lineClone = (Line2D) line.clone();

        final Point2D p1 = line.getP1();
        final Point2D p2 = line.getP2();
        if (p1.getX() < p2.getX()) {
            return lineClone;
        }
        if (p1.getX() > p2.getX()) {
            lineClone.setLine(p2, p1);
            return lineClone;
        }
        if (p1.getY() < p2.getY()) {
            return lineClone;
        }
        lineClone.setLine(p2, p1);
        return lineClone;
    }

    /**
     * Resizes a shape, so that the shape has the given width and height, but the
     * origin of the shape does not change. <p/> Unlike the AffineTransform, this
     * method tries to preserve the Shape's Type.
     * 
     * @param s
     *          the shape
     * @param width
     *          the new width
     * @param height
     *          the new height
     * @return the resized shape.
     */
    public static Shape resizeShape(final Shape s, final float width, final float height) {
        if (s instanceof Line2D) {
            return resizeLine((Line2D) s, width, height);
        }
        if (s instanceof RectangularShape) {
            return resizeRect((RectangularShape) s, width, height);
        }
        return transformShape(s, true, false, new Dimension((int) width, (int) height));
    }

    /**
     * Resizes a rectangle. This works for real rectangles and produces funny
     * results for RoundRects etc ..
     * 
     * @param rectangularShape
     *          the rectangle
     * @param width
     *          the new width of the rectangle
     * @param height
     *          the new height of the rectangle.
     * @return the resized rectangle.
     */
    public static Shape resizeRect(final RectangularShape rectangularShape, final double width,
            final double height) {
        final RectangularShape retval = (RectangularShape) rectangularShape.clone();
        retval.setFrame(retval.getX(), retval.getY(), width, height);
        return retval;
    }

    /**
     * Translates the given shape. The shape is translated to the origin supplied
     * in <code>point</code>. If scaling is requested, the shape will also be
     * scaled using an AffineTransform.
     * 
     * @param s
     *          the shape that should be transformed
     * @param scale
     *          true, if the shape should be scaled, false otherwise
     * @param keepAR
     *          true, if the scaled shape should keep the aspect ratio
     * @param width
     *          the target width.
     * @param height
     *          the target height.
     * @return the transformed shape
     */
    public static Shape transformShape(final Shape s, final boolean scale, final boolean keepAR, final double width,
            final double height) {
        /**
         * Always scale to the maximum bounds ...
         */
        if (scale) {

            final Rectangle2D boundsShape = s.getBounds2D();
            final double w = boundsShape.getWidth();
            final double h = boundsShape.getHeight();
            double scaleX = 1;

            if (w != 0) {
                scaleX = width / w;
            }

            double scaleY = 1;
            if (h != 0) {
                scaleY = height / h;
            }

            if (scaleX != 1 || scaleY != 1) {
                if (s instanceof RectangularShape) {
                    return ShapeTransform.resizeRect((RectangularShape) s, w * scaleX, h * scaleY);
                }
                if (s instanceof Line2D) {
                    return ShapeTransform.resizeLine((Line2D) s, w * scaleX, h * scaleY);
                }

                if (keepAR) {
                    final double scaleFact = Math.min(scaleX, scaleY);
                    return performDefaultTransformation(s, scaleFact, scaleFact);
                } else {
                    return performDefaultTransformation(s, scaleX, scaleY);
                }
            }
        }
        return s;
    }

    /**
     * Translates the given shape. The shape is translated to the origin supplied
     * in <code>point</code>. If scaling is requested, the shape will also be
     * scaled using an AffineTransform.
     * 
     * @param s
     *          the shape that should be transformed
     * @param scale
     *          true, if the shape should be scaled, false otherwise
     * @param keepAR
     *          true, if the scaled shape should keep the aspect ratio
     * @param dim
     *          the target dimension.
     * @return the transformed shape
     */
    public static Shape transformShape(final Shape s, final boolean scale, final boolean keepAR,
            final Dimension2D dim) {
        return transformShape(s, scale, keepAR, dim.getWidth(), dim.getHeight());
    }

    /**
     * Clips the given shape to the given bounds. If the shape is a Line2D, manual
     * clipping is performed, as the built in Area does not handle lines.
     * 
     * @param s
     *          the shape to be clipped
     * @param bounds
     *          the bounds to which the shape should be clipped
     * @return the clipped shape.
     */
    public static Shape performCliping(final Shape s, final Rectangle2D bounds) {
        if (s instanceof Line2D) {
            final Line2D line = (Line2D) s;
            final Point2D[] clipped = getClipped(line.getX1(), line.getY1(), line.getX2(), line.getY2(), -DELTA,
                    DELTA + bounds.getWidth(), -DELTA, DELTA + bounds.getHeight());
            if (clipped == null) {
                return new GeneralPath();
            }
            return new Line2D.Float(clipped[0], clipped[1]);
        }

        final Rectangle2D boundsCorrected = bounds.getBounds2D();
        boundsCorrected.setRect(-DELTA, -DELTA, DELTA + boundsCorrected.getWidth(),
                DELTA + boundsCorrected.getHeight());
        final Area a = new Area(boundsCorrected);
        if (a.isEmpty()) {
            // don't clip ... Area does not like lines
            // operations with lines always result in an empty Bounds:(0,0,0,0) area
            return new GeneralPath();
        }

        final Area clipArea = new Area(s);
        a.intersect(clipArea);
        return a;

    }

    /**
     * Scales a given shape. The shape is first normalized, then scaled and
     * finally brought back into its original position.
     * 
     * @param shape
     *          the shape to be scaled
     * @param scaleX
     *          the horizontal scaling factor
     * @param scaleY
     *          the vertical scaling factor
     * @return the scaled shape
     */
    private static Shape performDefaultTransformation(final Shape shape, final double scaleX, final double scaleY) {
        /**
         * Apply the normalisation shape transform ... bring the shape to pos (0,0)
         */
        final Rectangle2D bounds = shape.getBounds2D();
        AffineTransform af = AffineTransform.getTranslateInstance(0 - bounds.getX(), 0 - bounds.getY());
        // apply normalisation translation ...
        Shape s = af.createTransformedShape(shape);

        af = AffineTransform.getScaleInstance(scaleX, scaleY);
        // apply scaling ...
        s = af.createTransformedShape(s);

        // now retranslate the shape to its original position ...
        af = AffineTransform.getTranslateInstance(bounds.getX(), bounds.getY());
        return af.createTransformedShape(s);
    }

    /**
     * Translates a given shape. Special care is taken to preserve the shape's
     * original class, if the shape is a rectangle or a line.
     * 
     * @param s
     *          the shape
     * @param x
     *          the x coordinate where the shape is translated to
     * @param y
     *          the y coordinate where the shape is translated to
     * @return the translated shape
     */
    public static Shape translateShape(final Shape s, final double x, final double y) {
        if (s instanceof RectangularShape) {
            final RectangularShape rect = (RectangularShape) s;
            final RectangularShape retval = (RectangularShape) rect.clone();
            retval.setFrame(retval.getX() + x, retval.getY() + y, retval.getWidth(), retval.getHeight());
            return retval;
        }
        if (s instanceof Line2D) {
            final Line2D line = (Line2D) s;
            final Line2D retval = (Line2D) line.clone();
            retval.setLine(retval.getX1() + x, retval.getY1() + y, retval.getX2() + x, retval.getY2() + y);
            return retval;
        }

        final AffineTransform af = AffineTransform.getTranslateInstance(x, y);
        return af.createTransformedShape(s);
    }

    /**
     * Calculate the clipping points of a line with a rectangle.
     * 
     * @param x1
     *          starting x of line
     * @param y1
     *          starting y of line
     * @param x2
     *          ending x of line
     * @param y2
     *          ending y of line
     * @param xmin
     *          lower left x of rectangle
     * @param xmax
     *          upper right x of rectangle
     * @param ymin
     *          lower left y of rectangle
     * @param ymax
     *          upper right y of rectangle
     * @return <code>null</code> (does not clip) or array of two points
     */
    public static Point2D[] getClipped(final double x1, final double y1, final double x2, final double y2,
            final double xmin, final double xmax, final double ymin, final double ymax) {
        int mask1 = 0; // position mask for first point
        if (x1 < xmin) {
            mask1 |= LEFT;
        } else if (x1 > xmax) {
            mask1 |= RIGHT;
        } else {
            mask1 |= H_CENTER;
        }
        if (y1 < ymin) {
            // btw: I know that in AWT y runs from down but I more used to
            // y pointing up and it makes no difference for the algorithms
            mask1 |= BELOW;
        } else if (y1 > ymax) {
            mask1 |= ABOVE;
        } else {
            mask1 |= V_CENTER;
        }

        int mask2 = 0; // position mask for second point
        if (x2 < xmin) {
            mask2 |= LEFT;
        } else if (x2 > xmax) {
            mask2 |= RIGHT;
        } else {
            mask2 |= H_CENTER;
        }
        if (y2 < ymin) {
            mask2 |= BELOW;
        } else if (y2 > ymax) {
            mask2 |= ABOVE;
        } else {
            mask2 |= V_CENTER;
        }

        final int mask = mask1 | mask2;

        if ((mask & OUTSIDE) == 0) {
            // fine. everything's internal
            final Point2D[] ret = new Point2D[2];
            ret[0] = new Point2D.Double(x1, y1);
            ret[1] = new Point2D.Double(x2, y2);
            return ret;
        } else if ((mask & (H_CENTER | LEFT)) == 0 || // everything's right
                (mask & (H_CENTER | RIGHT)) == 0 || // everything's left
                (mask & (V_CENTER | BELOW)) == 0 || // everything's above
                (mask & (V_CENTER | ABOVE)) == 0) { // everything's below
            // nothing to do
            return null;
        } else {
            // need clipping
            return getClipped(x1, y1, mask1, x2, y2, mask2, xmin, xmax, ymin, ymax);
        }
    }

    /**
     * Calculate the clipping points of a line with a rectangle.
     * 
     * @param x1
     *          starting x of line
     * @param y1
     *          starting y of line
     * @param mask1
     *          clipping info mask for starting point
     * @param x2
     *          ending x of line
     * @param y2
     *          ending y of line
     * @param mask2
     *          clipping info mask for ending point
     * @param xmin
     *          lower left x of rectangle
     * @param ymin
     *          lower left y of rectangle
     * @param xmax
     *          upper right x of rectangle
     * @param ymax
     *          upper right y of rectangle
     * @return <code>null</code> (does not clip) or array of two points
     */
    private static Point2D[] getClipped(final double x1, final double y1, final int mask1, final double x2,
            final double y2, final int mask2, final double xmin, final double xmax, final double ymin,
            final double ymax) {
        final int mask = mask1 ^ mask2;
        Point2D p1 = null;

        if (mask1 == INSIDE) {
            // point 1 is internal
            p1 = new Point2D.Double(x1, y1);
            if (mask == 0) {
                // both masks are the same, so the second point is inside, too
                final Point2D[] ret = new Point2D[2];
                ret[0] = p1;
                ret[1] = new Point2D.Double(x2, y2);
                return ret;
            }
        } else if (mask2 == INSIDE) {
            // point 2 is internal
            p1 = new Point2D.Double(x2, y2);
        }

        if ((mask & LEFT) != 0) {
            // System.out.println("Trying left");
            // try to calculate intersection with left line
            final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin, xmin, ymax);
            if (p != null) {
                if (p1 == null) {
                    p1 = p;
                } else {
                    final Point2D[] ret = new Point2D[2];
                    ret[0] = p1;
                    ret[1] = p;
                    return ret;
                }
            }
        }
        if ((mask & RIGHT) != 0) {
            // System.out.println("Trying right");
            // try to calculate intersection with left line
            final Point2D p = intersect(x1, y1, x2, y2, xmax, ymin, xmax, ymax);
            if (p != null) {
                if (p1 == null) {
                    p1 = p;
                } else {
                    final Point2D[] ret = new Point2D[2];
                    ret[0] = p1;
                    ret[1] = p;
                    return ret;
                }
            }
        }
        if (mask1 == (LEFT | BELOW) || mask1 == (RIGHT | BELOW)) {
            // for exactly these two special cases use different sequence!

            if ((mask & ABOVE) != 0) {
                // System.out.println("Trying top");
                // try to calculate intersection with lower line
                final Point2D p = intersect(x1, y1, x2, y2, xmin, ymax, xmax, ymax);
                if (p != null) {
                    if (p1 == null) {
                        p1 = p;
                    } else {
                        final Point2D[] ret = new Point2D[2];
                        ret[0] = p1;
                        ret[1] = p;
                        return ret;
                    }
                }
            }
            if ((mask & BELOW) != 0) {
                // System.out.println("Trying bottom");
                // try to calculate intersection with lower line
                final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin, xmax, ymin);
                if (p != null && p1 != null) {
                    final Point2D[] ret = new Point2D[2];
                    ret[0] = p1;
                    ret[1] = p;
                    return ret;
                }
            }
        } else {
            if ((mask & BELOW) != 0) {
                // System.out.println("Trying bottom");
                // try to calculate intersection with lower line
                final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin, xmax, ymin);
                if (p != null) {
                    if (p1 == null) {
                        p1 = p;
                    } else {
                        final Point2D[] ret = new Point2D[2];
                        ret[0] = p1;
                        ret[1] = p;
                        return ret;
                    }
                }
            }
            if ((mask & ABOVE) != 0) {
                // System.out.println("Trying top");
                // try to calculate intersection with lower line
                final Point2D p = intersect(x1, y1, x2, y2, xmin, ymax, xmax, ymax);
                if (p != null && p1 != null) {
                    final Point2D[] ret = new Point2D[2];
                    ret[0] = p1;
                    ret[1] = p;
                    return ret;
                }
            }
        }

        // no (or not enough) intersections found
        return null;
    }

    /**
     * Intersect two lines.
     * 
     * @param x11
     *          starting x of 1st line
     * @param y11
     *          starting y of 1st line
     * @param x12
     *          ending x of 1st line
     * @param y12
     *          ending y of 1st line
     * @param x21
     *          starting x of 2nd line
     * @param y21
     *          starting y of 2nd line
     * @param x22
     *          ending x of 2nd line
     * @param y22
     *          ending y of 2nd line
     * @return intersection point or <code>null</code>
     */
    private static Point2D intersect(final double x11, final double y11, final double x12, final double y12,
            final double x21, final double y21, final double x22, final double y22) {
        final double dx1 = x12 - x11;
        final double dy1 = y12 - y11;
        final double dx2 = x22 - x21;
        final double dy2 = y22 - y21;
        final double det = (dx2 * dy1 - dy2 * dx1);

        if (det != 0.0) {
            final double mu = ((x11 - x21) * dy1 - (y11 - y21) * dx1) / det;
            if (mu >= 0.0 && mu <= 1.0) {
                return new Point2D.Double(x21 + mu * dx2, y21 + mu * dy2);
            }
        }

        return null;
    }

}