/* * This program is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software * Foundation. * * You should have received a copy of the GNU Lesser General Public License along with this * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html * or from the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. * * This program 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. * * Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved. */ package org.pentaho.reporting.engine.classic.core.util; 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; import org.pentaho.reporting.libraries.base.util.FloatDimension; /** * 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 FloatDimension( width, 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 Line2D ) { return ShapeTransform.resizeLine( (Line2D) s, w * scaleX, h * scaleY ); } if ( keepAR ) { final double scaleFact = Math.min( scaleX, scaleY ); if ( s instanceof RectangularShape ) { return ShapeTransform.resizeRect( (RectangularShape) s, w * scaleFact, h * scaleFact ); } return performDefaultTransformation( s, scaleFact, scaleFact ); } else { if ( s instanceof RectangularShape ) { return ShapeTransform.resizeRect( (RectangularShape) s, w * scaleX, h * scaleY ); } 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(); final AffineTransform translateTransform = AffineTransform.getTranslateInstance( 0 - bounds.getX(), 0 - bounds.getY() ); // apply normalisation translation ... final Shape translatedShape = translateTransform.createTransformedShape( shape ); final AffineTransform scaleTransform = AffineTransform.getScaleInstance( scaleX, scaleY ); // apply scaling ... final Shape scaledShape = scaleTransform.createTransformedShape( translatedShape ); // now retranslate the shape to its original position ... final AffineTransform translateBackTransform = AffineTransform.getTranslateInstance( bounds.getX(), bounds.getY() ); return translateBackTransform.createTransformedShape( scaledShape ); } /** * Translates a se. 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; } }