Java tutorial
/** * * 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; } }