001: /**
002: * ===========================================
003: * JFreeReport : a free Java reporting library
004: * ===========================================
005: *
006: * Project Info: http://reporting.pentaho.org/
007: *
008: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
009: *
010: * This library is free software; you can redistribute it and/or modify it under the terms
011: * of the GNU Lesser General Public License as published by the Free Software Foundation;
012: * either version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
015: * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
016: * See the GNU Lesser General Public License for more details.
017: *
018: * You should have received a copy of the GNU Lesser General Public License along with this
019: * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
020: * Boston, MA 02111-1307, USA.
021: *
022: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
023: * in the United States and other countries.]
024: *
025: * ------------
026: * ShapeTransform.java
027: * ------------
028: * (C) Copyright 2001-2007, by Object Refinery Ltd, Pentaho Corporation and Contributors.
029: */package org.jfree.report.util;
030:
031: import java.awt.Shape;
032: import java.awt.geom.AffineTransform;
033: import java.awt.geom.Area;
034: import java.awt.geom.Dimension2D;
035: import java.awt.geom.GeneralPath;
036: import java.awt.geom.Line2D;
037: import java.awt.geom.Point2D;
038: import java.awt.geom.Rectangle2D;
039: import java.awt.geom.RectangularShape;
040:
041: import org.jfree.ui.FloatDimension;
042:
043: /**
044: * Utility class, which resizes or translates a Shape. The class contains special handlers
045: * for Rectangles and Lines.
046: *
047: * @author Thomas Morgner
048: */
049: public final strictfp class ShapeTransform {
050: // some constants for the cohenen-algorithmus
051: /**
052: * Flag for point lying left of clipping area.
053: */
054: public static final int LEFT = 0x01;
055: /**
056: * Flag for point lying between horizontal bounds of area.
057: */
058: public static final int H_CENTER = 0x02;
059: /**
060: * Flag for point lying right of clipping area.
061: */
062: public static final int RIGHT = 0x04;
063:
064: /**
065: * Flag for point lying "below" clipping area.
066: */
067: public static final int BELOW = 0x10;
068: /**
069: * Flag for point lying between vertical bounds of clipping area.
070: */
071: public static final int V_CENTER = 0x20;
072: /**
073: * Flag for point lying "above" clipping area.
074: */
075: public static final int ABOVE = 0x40;
076:
077: /** A simple way to handle rounding errors. */
078: private static final double DELTA = 0.000001;
079: /**
080: * Mask for points which are inside.
081: */
082: public static final int INSIDE = H_CENTER | V_CENTER;
083: /**
084: * Mask for points which are outside.
085: */
086: public static final int OUTSIDE = LEFT | RIGHT | BELOW | ABOVE;
087:
088: /**
089: * Default constructor.
090: */
091: private ShapeTransform() {
092: }
093:
094: /**
095: * Resizes a line. Instead of creating a GeneralPath (as AffineTransform's scale
096: * would do) we modify the line itself.
097: *
098: * @param line the line that should be scaled
099: * @param width the new width of the line bounds
100: * @param height the new height of the line bounds
101: * @return the scale Line2D object.
102: */
103: private static Line2D resizeLine(final Line2D line,
104: final double width, final double height) {
105: final Line2D newLine = getNormalizedLine(line);
106: final Point2D p1 = newLine.getP1();
107: final Point2D p2 = newLine.getP2();
108: final double normPointX = (p1.getX() - p2.getX());
109: final double normPointY = (p1.getY() - p2.getY());
110: final double scaleX = (normPointX == 0) ? 1 : width
111: / Math.abs(normPointX);
112: final double scaleY = (normPointY == 0) ? 1 : height
113: / Math.abs(normPointY);
114: p2.setLocation((p2.getX() - p1.getX()) * scaleX + p1.getX(),
115: (p2.getY() - p1.getY()) * scaleY + p1.getY());
116: newLine.setLine(p1, p2);
117: return newLine;
118: }
119:
120: /**
121: * Normalize the line; the point with the lowest X is the primary point, if both points
122: * have the same X, that point with the lowest Y value wins.
123: *
124: * @param line the original line
125: * @return the normalized line
126: */
127: private static Line2D getNormalizedLine(final Line2D line) {
128: final Line2D lineClone = (Line2D) line.clone();
129:
130: final Point2D p1 = line.getP1();
131: final Point2D p2 = line.getP2();
132: if (p1.getX() < p2.getX()) {
133: return lineClone;
134: }
135: if (p1.getX() > p2.getX()) {
136: lineClone.setLine(p2, p1);
137: return lineClone;
138: }
139: if (p1.getY() < p2.getY()) {
140: return lineClone;
141: }
142: lineClone.setLine(p2, p1);
143: return lineClone;
144: }
145:
146: /**
147: * Resizes a shape, so that the shape has the given width and height, but the origin of
148: * the shape does not change.
149: * <p/>
150: * Unlike the AffineTransform, this method tries to preserve the Shape's Type.
151: *
152: * @param s the shape
153: * @param width the new width
154: * @param height the new height
155: * @return the resized shape.
156: */
157: public static Shape resizeShape(final Shape s, final float width,
158: final float height) {
159: if (s instanceof Line2D) {
160: return resizeLine((Line2D) s, width, height);
161: }
162: if (s instanceof RectangularShape) {
163: return resizeRect((RectangularShape) s, width, height);
164: }
165: return transformShape(s, true, false, new FloatDimension(width,
166: height));
167: }
168:
169: /**
170: * Resizes a rectangle. This works for real rectangles and produces funny results
171: * for RoundRects etc ..
172: *
173: * @param rectangularShape the rectangle
174: * @param width the new width of the rectangle
175: * @param height the new height of the rectangle.
176: * @return the resized rectangle.
177: */
178: public static Shape resizeRect(
179: final RectangularShape rectangularShape,
180: final double width, final double height) {
181: final RectangularShape retval = (RectangularShape) rectangularShape
182: .clone();
183: retval.setFrame(retval.getX(), retval.getY(), width, height);
184: return retval;
185: }
186:
187: /**
188: * Translates the given shape. The shape is translated to the origin supplied in
189: * <code>point</code>. If scaling is requested, the shape will also be scaled using an
190: * AffineTransform.
191: *
192: * @param s the shape that should be transformed
193: * @param scale true, if the shape should be scaled, false otherwise
194: * @param keepAR true, if the scaled shape should keep the aspect ratio
195: * @param width the target width.
196: * @param height the target height.
197: * @return the transformed shape
198: */
199: public static Shape transformShape(final Shape s,
200: final boolean scale, final boolean keepAR,
201: final double width, final double height) {
202: /**
203: * Always scale to the maximum bounds ...
204: */
205: if (scale) {
206:
207: final Rectangle2D boundsShape = s.getBounds2D();
208: final double w = boundsShape.getWidth();
209: final double h = boundsShape.getHeight();
210: double scaleX = 1;
211:
212: if (w != 0) {
213: scaleX = width / w;
214: }
215:
216: double scaleY = 1;
217: if (h != 0) {
218: scaleY = height / h;
219: }
220:
221: if (scaleX != 1 || scaleY != 1) {
222: if (s instanceof RectangularShape) {
223: return ShapeTransform.resizeRect(
224: (RectangularShape) s, w * scaleX, h
225: * scaleY);
226: }
227: if (s instanceof Line2D) {
228: return ShapeTransform.resizeLine((Line2D) s, w
229: * scaleX, h * scaleY);
230: }
231:
232: if (keepAR) {
233: final double scaleFact = Math.min(scaleX, scaleY);
234: return performDefaultTransformation(s, scaleFact,
235: scaleFact);
236: } else {
237: return performDefaultTransformation(s, scaleX,
238: scaleY);
239: }
240: }
241: }
242: return s;
243: }
244:
245: /**
246: * Translates the given shape. The shape is translated to the origin supplied in
247: * <code>point</code>. If scaling is requested, the shape will also be scaled using an
248: * AffineTransform.
249: *
250: * @param s the shape that should be transformed
251: * @param scale true, if the shape should be scaled, false otherwise
252: * @param keepAR true, if the scaled shape should keep the aspect ratio
253: * @param dim the target dimension.
254: * @return the transformed shape
255: */
256: public static Shape transformShape(final Shape s,
257: final boolean scale, final boolean keepAR,
258: final Dimension2D dim) {
259: return transformShape(s, scale, keepAR, dim.getWidth(), dim
260: .getHeight());
261: }
262:
263: /**
264: * Clips the given shape to the given bounds. If the shape is a Line2D, manual
265: * clipping is performed, as the built in Area does not handle lines.
266: *
267: * @param s the shape to be clipped
268: * @param bounds the bounds to which the shape should be clipped
269: * @return the clipped shape.
270: */
271: public static Shape performCliping(final Shape s,
272: final Rectangle2D bounds) {
273: if (s instanceof Line2D) {
274: final Line2D line = (Line2D) s;
275: final Point2D[] clipped = getClipped(line.getX1(), line
276: .getY1(), line.getX2(), line.getY2(), -DELTA, DELTA
277: + bounds.getWidth(), -DELTA, DELTA
278: + bounds.getHeight());
279: if (clipped == null) {
280: return new GeneralPath();
281: }
282: return new Line2D.Float(clipped[0], clipped[1]);
283: }
284:
285: final Rectangle2D boundsCorrected = bounds.getBounds2D();
286: boundsCorrected.setRect(-DELTA, -DELTA, DELTA
287: + boundsCorrected.getWidth(), DELTA
288: + boundsCorrected.getHeight());
289: final Area a = new Area(boundsCorrected);
290: if (a.isEmpty()) {
291: // don't clip ... Area does not like lines
292: // operations with lines always result in an empty Bounds:(0,0,0,0) area
293: return new GeneralPath();
294: }
295:
296: final Area clipArea = new Area(s);
297: a.intersect(clipArea);
298: return a;
299:
300: }
301:
302: /**
303: * Scales a given shape. The shape is first normalized, then scaled and finally brought
304: * back into its original position.
305: *
306: * @param shape the shape to be scaled
307: * @param scaleX the horizontal scaling factor
308: * @param scaleY the vertical scaling factor
309: * @return the scaled shape
310: */
311: private static Shape performDefaultTransformation(
312: final Shape shape, final double scaleX, final double scaleY) {
313: /**
314: * Apply the normalisation shape transform ... bring the shape to pos (0,0)
315: */
316: final Rectangle2D bounds = shape.getBounds2D();
317: AffineTransform af = AffineTransform.getTranslateInstance(
318: 0 - bounds.getX(), 0 - bounds.getY());
319: // apply normalisation translation ...
320: Shape s = af.createTransformedShape(shape);
321:
322: af = AffineTransform.getScaleInstance(scaleX, scaleY);
323: // apply scaling ...
324: s = af.createTransformedShape(s);
325:
326: // now retranslate the shape to its original position ...
327: af = AffineTransform.getTranslateInstance(bounds.getX(), bounds
328: .getY());
329: return af.createTransformedShape(s);
330: }
331:
332: /**
333: * Translates a given shape. Special care is taken to preserve the shape's original
334: * class, if the shape is a rectangle or a line.
335: *
336: * @param s the shape
337: * @param x the x coordinate where the shape is translated to
338: * @param y the y coordinate where the shape is translated to
339: * @return the translated shape
340: */
341: public static Shape translateShape(final Shape s, final double x,
342: final double y) {
343: if (s instanceof RectangularShape) {
344: final RectangularShape rect = (RectangularShape) s;
345: final RectangularShape retval = (RectangularShape) rect
346: .clone();
347: retval.setFrame(retval.getX() + x, retval.getY() + y,
348: retval.getWidth(), retval.getHeight());
349: return retval;
350: }
351: if (s instanceof Line2D) {
352: final Line2D line = (Line2D) s;
353: final Line2D retval = (Line2D) line.clone();
354: retval.setLine(retval.getX1() + x, retval.getY1() + y,
355: retval.getX2() + x, retval.getY2() + y);
356: return retval;
357: }
358:
359: final AffineTransform af = AffineTransform
360: .getTranslateInstance(x, y);
361: return af.createTransformedShape(s);
362: }
363:
364: /**
365: * Calculate the clipping points of a line with a rectangle.
366: *
367: * @param x1 starting x of line
368: * @param y1 starting y of line
369: * @param x2 ending x of line
370: * @param y2 ending y of line
371: * @param xmin lower left x of rectangle
372: * @param xmax upper right x of rectangle
373: * @param ymin lower left y of rectangle
374: * @param ymax upper right y of rectangle
375: * @return <code>null</code> (does not clip) or array of two points
376: */
377: public static Point2D[] getClipped(final double x1,
378: final double y1, final double x2, final double y2,
379: final double xmin, final double xmax, final double ymin,
380: final double ymax) {
381: int mask1 = 0; // position mask for first point
382: if (x1 < xmin) {
383: mask1 |= LEFT;
384: } else if (x1 > xmax) {
385: mask1 |= RIGHT;
386: } else {
387: mask1 |= H_CENTER;
388: }
389: if (y1 < ymin) {
390: // btw: I know that in AWT y runs from down but I more used to
391: // y pointing up and it makes no difference for the algorithms
392: mask1 |= BELOW;
393: } else if (y1 > ymax) {
394: mask1 |= ABOVE;
395: } else {
396: mask1 |= V_CENTER;
397: }
398:
399: int mask2 = 0; // position mask for second point
400: if (x2 < xmin) {
401: mask2 |= LEFT;
402: } else if (x2 > xmax) {
403: mask2 |= RIGHT;
404: } else {
405: mask2 |= H_CENTER;
406: }
407: if (y2 < ymin) {
408: mask2 |= BELOW;
409: } else if (y2 > ymax) {
410: mask2 |= ABOVE;
411: } else {
412: mask2 |= V_CENTER;
413: }
414:
415: final int mask = mask1 | mask2;
416:
417: if ((mask & OUTSIDE) == 0) {
418: // fine. everything's internal
419: final Point2D[] ret = new Point2D[2];
420: ret[0] = new Point2D.Double(x1, y1);
421: ret[1] = new Point2D.Double(x2, y2);
422: return ret;
423: } else if ((mask & (H_CENTER | LEFT)) == 0 || // everything's right
424: (mask & (H_CENTER | RIGHT)) == 0 || // everything's left
425: (mask & (V_CENTER | BELOW)) == 0 || // everything's above
426: (mask & (V_CENTER | ABOVE)) == 0) { // everything's below
427: // nothing to do
428: return null;
429: } else {
430: // need clipping
431: return getClipped(x1, y1, mask1, x2, y2, mask2, xmin, xmax,
432: ymin, ymax);
433: }
434: }
435:
436: /**
437: * Calculate the clipping points of a line with a rectangle.
438: *
439: * @param x1 starting x of line
440: * @param y1 starting y of line
441: * @param mask1 clipping info mask for starting point
442: * @param x2 ending x of line
443: * @param y2 ending y of line
444: * @param mask2 clipping info mask for ending point
445: * @param xmin lower left x of rectangle
446: * @param ymin lower left y of rectangle
447: * @param xmax upper right x of rectangle
448: * @param ymax upper right y of rectangle
449: * @return <code>null</code> (does not clip) or array of two points
450: */
451: private static Point2D[] getClipped(final double x1,
452: final double y1, final int mask1, final double x2,
453: final double y2, final int mask2, final double xmin,
454: final double xmax, final double ymin, final double ymax) {
455: final int mask = mask1 ^ mask2;
456: Point2D p1 = null;
457:
458: if (mask1 == INSIDE) {
459: // point 1 is internal
460: p1 = new Point2D.Double(x1, y1);
461: if (mask == 0) {
462: // both masks are the same, so the second point is inside, too
463: final Point2D[] ret = new Point2D[2];
464: ret[0] = p1;
465: ret[1] = new Point2D.Double(x2, y2);
466: return ret;
467: }
468: } else if (mask2 == INSIDE) {
469: // point 2 is internal
470: p1 = new Point2D.Double(x2, y2);
471: }
472:
473: if ((mask & LEFT) != 0) {
474: // System.out.println("Trying left");
475: // try to calculate intersection with left line
476: final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin,
477: xmin, ymax);
478: if (p != null) {
479: if (p1 == null) {
480: p1 = p;
481: } else {
482: final Point2D[] ret = new Point2D[2];
483: ret[0] = p1;
484: ret[1] = p;
485: return ret;
486: }
487: }
488: }
489: if ((mask & RIGHT) != 0) {
490: // System.out.println("Trying right");
491: // try to calculate intersection with left line
492: final Point2D p = intersect(x1, y1, x2, y2, xmax, ymin,
493: xmax, ymax);
494: if (p != null) {
495: if (p1 == null) {
496: p1 = p;
497: } else {
498: final Point2D[] ret = new Point2D[2];
499: ret[0] = p1;
500: ret[1] = p;
501: return ret;
502: }
503: }
504: }
505: if (mask1 == (LEFT | BELOW) || mask1 == (RIGHT | BELOW)) {
506: // for exactly these two special cases use different sequence!
507:
508: if ((mask & ABOVE) != 0) {
509: // System.out.println("Trying top");
510: // try to calculate intersection with lower line
511: final Point2D p = intersect(x1, y1, x2, y2, xmin, ymax,
512: xmax, ymax);
513: if (p != null) {
514: if (p1 == null) {
515: p1 = p;
516: } else {
517: final Point2D[] ret = new Point2D[2];
518: ret[0] = p1;
519: ret[1] = p;
520: return ret;
521: }
522: }
523: }
524: if ((mask & BELOW) != 0) {
525: // System.out.println("Trying bottom");
526: // try to calculate intersection with lower line
527: final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin,
528: xmax, ymin);
529: if (p != null && p1 != null) {
530: final Point2D[] ret = new Point2D[2];
531: ret[0] = p1;
532: ret[1] = p;
533: return ret;
534: }
535: }
536: } else {
537: if ((mask & BELOW) != 0) {
538: // System.out.println("Trying bottom");
539: // try to calculate intersection with lower line
540: final Point2D p = intersect(x1, y1, x2, y2, xmin, ymin,
541: xmax, ymin);
542: if (p != null) {
543: if (p1 == null) {
544: p1 = p;
545: } else {
546: final Point2D[] ret = new Point2D[2];
547: ret[0] = p1;
548: ret[1] = p;
549: return ret;
550: }
551: }
552: }
553: if ((mask & ABOVE) != 0) {
554: // System.out.println("Trying top");
555: // try to calculate intersection with lower line
556: final Point2D p = intersect(x1, y1, x2, y2, xmin, ymax,
557: xmax, ymax);
558: if (p != null && p1 != null) {
559: final Point2D[] ret = new Point2D[2];
560: ret[0] = p1;
561: ret[1] = p;
562: return ret;
563: }
564: }
565: }
566:
567: // no (or not enough) intersections found
568: return null;
569: }
570:
571: /**
572: * Intersect two lines.
573: *
574: * @param x11 starting x of 1st line
575: * @param y11 starting y of 1st line
576: * @param x12 ending x of 1st line
577: * @param y12 ending y of 1st line
578: * @param x21 starting x of 2nd line
579: * @param y21 starting y of 2nd line
580: * @param x22 ending x of 2nd line
581: * @param y22 ending y of 2nd line
582: * @return intersection point or <code>null</code>
583: */
584: private static Point2D intersect(final double x11,
585: final double y11, final double x12, final double y12,
586: final double x21, final double y21, final double x22,
587: final double y22) {
588: final double dx1 = x12 - x11;
589: final double dy1 = y12 - y11;
590: final double dx2 = x22 - x21;
591: final double dy2 = y22 - y21;
592: final double det = (dx2 * dy1 - dy2 * dx1);
593:
594: if (det != 0.0) {
595: final double mu = ((x11 - x21) * dy1 - (y11 - y21) * dx1)
596: / det;
597: if (mu >= 0.0 && mu <= 1.0) {
598: return new Point2D.Double(x21 + mu * dx2, y21 + mu
599: * dy2);
600: }
601: }
602:
603: return null;
604: }
605:
606: }
|