Source for org.jfree.text.TextUtilities

   1: /* ========================================================================
   2:  * JCommon : a free general purpose class library for the Java(tm) platform
   3:  * ========================================================================
   4:  *
   5:  * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors.
   6:  * 
   7:  * Project Info:  http://www.jfree.org/jcommon/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  * 
  27:  * ------------------
  28:  * TextUtilities.java
  29:  * ------------------
  30:  * (C) Copyright 2004-2006, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * $Id: TextUtilities.java,v 1.21 2006/07/04 10:20:40 taqua Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 07-Jan-2004 : Version 1 (DG);
  40:  * 24-Mar-2004 : Added 'paint' argument to createTextBlock() method (DG);
  41:  * 07-Apr-2004 : Added getTextBounds() method and useFontMetricsGetStringBounds
  42:  *               flag (DG);
  43:  * 08-Apr-2004 : Changed word break iterator to line break iterator in the 
  44:  *               createTextBlock() method - see bug report 926074 (DG);
  45:  * 03-Sep-2004 : Updated createTextBlock() method to add ellipses when limit 
  46:  *               is reached (DG);
  47:  * 30-Sep-2004 : Modified bounds returned by drawAlignedString() method (DG);
  48:  * 10-Nov-2004 : Added new createTextBlock() method that works with 
  49:  *               newlines (DG);
  50:  * 19-Apr-2005 : Changed default value of useFontMetricsGetStringBounds (DG);
  51:  * 17-May-2005 : createTextBlock() now recognises '\n' (DG);
  52:  * 27-Jun-2005 : Added code to getTextBounds() method to work around Sun's bug 
  53:  *               parade item 6183356 (DG);
  54:  * 06-Jan-2006 : Reformatted (DG);
  55:  * 
  56:  */
  57: 
  58: package org.jfree.text;
  59: 
  60: import java.awt.Font;
  61: import java.awt.FontMetrics;
  62: import java.awt.Graphics2D;
  63: import java.awt.Paint;
  64: import java.awt.Shape;
  65: import java.awt.font.FontRenderContext;
  66: import java.awt.font.LineMetrics;
  67: import java.awt.font.TextLayout;
  68: import java.awt.geom.AffineTransform;
  69: import java.awt.geom.Rectangle2D;
  70: import java.text.BreakIterator;
  71: 
  72: import org.jfree.ui.TextAnchor;
  73: import org.jfree.util.Log;
  74: import org.jfree.util.LogContext;
  75: import org.jfree.util.ObjectUtilities;
  76: import org.jfree.base.BaseBoot;
  77: 
  78: /**
  79:  * Some utility methods for working with text.
  80:  *
  81:  * @author David Gilbert
  82:  */
  83: public class TextUtilities {
  84: 
  85:     /** Access to logging facilities. */
  86:     protected static final LogContext logger = Log.createContext(
  87:             TextUtilities.class);
  88: 
  89:     /**
  90:      * A flag that controls whether or not the rotated string workaround is
  91:      * used.
  92:      */
  93:     private static boolean useDrawRotatedStringWorkaround;
  94: 
  95:     /**
  96:      * A flag that controls whether the FontMetrics.getStringBounds() method
  97:      * is used or a workaround is applied.
  98:      */
  99:     private static boolean useFontMetricsGetStringBounds;
 100: 
 101:     static {
 102:         final boolean isJava14 = ObjectUtilities.isJDK14();
 103: 
 104:         final String configRotatedStringWorkaround =
 105:               BaseBoot.getInstance().getGlobalConfig().getConfigProperty(
 106:                       "org.jfree.text.UseDrawRotatedStringWorkaround", "auto");
 107:         if (configRotatedStringWorkaround.equals("auto")) {
 108:            useDrawRotatedStringWorkaround = (isJava14 == false);
 109:         } 
 110:         else {
 111:             useDrawRotatedStringWorkaround 
 112:                     = configRotatedStringWorkaround.equals("true");
 113:         }
 114: 
 115:         final String configFontMetricsStringBounds 
 116:                 = BaseBoot.getInstance().getGlobalConfig().getConfigProperty(
 117:                         "org.jfree.text.UseFontMetricsGetStringBounds", "auto");
 118:         if (configFontMetricsStringBounds.equals("auto")) {
 119:             useFontMetricsGetStringBounds = (isJava14 == true);
 120:         }
 121:         else {
 122:             useFontMetricsGetStringBounds 
 123:                     = configFontMetricsStringBounds.equals("true");
 124:         }
 125:     }
 126: 
 127:     /**
 128:      * Private constructor prevents object creation.
 129:      */
 130:     private TextUtilities() {
 131:     }
 132: 
 133:     /**
 134:      * Creates a {@link TextBlock} from a <code>String</code>.  Line breaks 
 135:      * are added where the <code>String</code> contains '\n' characters.
 136:      * 
 137:      * @param text  the text.
 138:      * @param font  the font.
 139:      * @param paint  the paint.
 140:      * 
 141:      * @return A text block.
 142:      */
 143:     public static TextBlock createTextBlock(final String text, final Font font,
 144:                                             final Paint paint) {
 145:         if (text == null) {
 146:             throw new IllegalArgumentException("Null 'text' argument.");
 147:         }
 148:         final TextBlock result = new TextBlock();
 149:         String input = text;
 150:         boolean moreInputToProcess = (text.length() > 0);
 151:         final int start = 0;
 152:         while (moreInputToProcess) {
 153:             final int index = input.indexOf("\n");
 154:             if (index > start) {
 155:                 final String line = input.substring(start, index);
 156:                 if (index < input.length() - 1) {
 157:                     result.addLine(line, font, paint);
 158:                     input = input.substring(index + 1);
 159:                 }
 160:                 else {
 161:                     moreInputToProcess = false;
 162:                 }
 163:             }
 164:             else if (index == start) {
 165:                 if (index < input.length() - 1) {
 166:                     input = input.substring(index + 1);
 167:                 }
 168:                 else {
 169:                     moreInputToProcess = false;
 170:                 }
 171:             }
 172:             else {
 173:                 result.addLine(input, font, paint);
 174:                 moreInputToProcess = false;
 175:             }
 176:         }
 177:         return result;
 178:     }
 179: 
 180:     /**
 181:      * Creates a new text block from the given string, breaking the
 182:      * text into lines so that the <code>maxWidth</code> value is
 183:      * respected.
 184:      * 
 185:      * @param text  the text.
 186:      * @param font  the font.
 187:      * @param paint  the paint.
 188:      * @param maxWidth  the maximum width for each line.
 189:      * @param measurer  the text measurer.
 190:      * 
 191:      * @return A text block.
 192:      */
 193:     public static TextBlock createTextBlock(final String text, final Font font,
 194:             final Paint paint, final float maxWidth, 
 195:             final TextMeasurer measurer) {
 196:         
 197:         return createTextBlock(text, font, paint, maxWidth, Integer.MAX_VALUE, 
 198:                 measurer);
 199:     }
 200: 
 201:     /**
 202:      * Creates a new text block from the given string, breaking the
 203:      * text into lines so that the <code>maxWidth</code> value is
 204:      * respected.
 205:      * 
 206:      * @param text  the text.
 207:      * @param font  the font.
 208:      * @param paint  the paint.
 209:      * @param maxWidth  the maximum width for each line.
 210:      * @param maxLines  the maximum number of lines.
 211:      * @param measurer  the text measurer.
 212:      * 
 213:      * @return A text block.
 214:      */
 215:     public static TextBlock createTextBlock(final String text, final Font font,
 216:             final Paint paint, final float maxWidth, final int maxLines,
 217:             final TextMeasurer measurer) {
 218:         
 219:         final TextBlock result = new TextBlock();
 220:         final BreakIterator iterator = BreakIterator.getLineInstance();
 221:         iterator.setText(text);
 222:         int current = 0;
 223:         int lines = 0;
 224:         final int length = text.length();
 225:         while (current < length && lines < maxLines) {
 226:             final int next = nextLineBreak(text, current, maxWidth, iterator, 
 227:                     measurer);
 228:             if (next == BreakIterator.DONE) {
 229:                 result.addLine(text.substring(current), font, paint);
 230:                 return result;
 231:             }
 232:             result.addLine(text.substring(current, next), font, paint);
 233:             lines++;
 234:             current = next;
 235:             while (current < text.length()&& text.charAt(current) == '\n') {
 236:                 current++;
 237:             }
 238:         }
 239:         if (current < length) {
 240:             final TextLine lastLine = result.getLastLine();
 241:             final TextFragment lastFragment = lastLine.getLastTextFragment();
 242:             final String oldStr = lastFragment.getText();
 243:             String newStr = "...";
 244:             if (oldStr.length() > 3) {
 245:                 newStr = oldStr.substring(0, oldStr.length() - 3) + "...";
 246:             }
 247: 
 248:             lastLine.removeFragment(lastFragment);
 249:             final TextFragment newFragment = new TextFragment(newStr, 
 250:                     lastFragment.getFont(), lastFragment.getPaint());
 251:             lastLine.addFragment(newFragment);
 252:         }
 253:         return result;
 254:     }
 255: 
 256:     /**
 257:      * Returns the character index of the next line break.
 258:      * 
 259:      * @param text  the text.
 260:      * @param start  the start index.
 261:      * @param width  the target display width.
 262:      * @param iterator  the word break iterator.
 263:      * @param measurer  the text measurer.
 264:      * 
 265:      * @return The index of the next line break.
 266:      */
 267:     private static int nextLineBreak(final String text, final int start,
 268:             final float width, final BreakIterator iterator,
 269:             final TextMeasurer measurer) {
 270:         
 271:         // this method is (loosely) based on code in JFreeReport's 
 272:         // TextParagraph class
 273:         int current = start;
 274:         int end;
 275:         float x = 0.0f;
 276:         boolean firstWord = true;
 277:         int newline = text.indexOf('\n', start);
 278:         if (newline < 0) {
 279:             newline = Integer.MAX_VALUE;
 280:         }
 281:         while (((end = iterator.next()) != BreakIterator.DONE)) {
 282:             if (end > newline) {
 283:                 return newline;
 284:             }
 285:             x += measurer.getStringWidth(text, current, end);
 286:             if (x > width) {
 287:                 if (firstWord) {
 288:                     while (measurer.getStringWidth(text, start, end) > width) {
 289:                         end--;
 290:                         if (end <= start) {
 291:                             return end;
 292:                         }
 293:                     }
 294:                     return end;
 295:                 }
 296:                 else {
 297:                     end = iterator.previous();
 298:                     return end;
 299:                 }
 300:             }
 301:             // we found at least one word that fits ...
 302:             firstWord = false;
 303:             current = end;
 304:         }
 305:         return BreakIterator.DONE;
 306:     }
 307: 
 308:     /**
 309:      * Returns the bounds for the specified text.
 310:      * 
 311:      * @param text  the text (<code>null</code> permitted).
 312:      * @param g2  the graphics context (not <code>null</code>).
 313:      * @param fm  the font metrics (not <code>null</code>).
 314:      * 
 315:      * @return The text bounds (<code>null</code> if the <code>text</code> 
 316:      *         argument is <code>null</code>).
 317:      */
 318:     public static Rectangle2D getTextBounds(final String text, 
 319:             final Graphics2D g2, final FontMetrics fm) {
 320:         
 321:         final Rectangle2D bounds;
 322:         if (TextUtilities.useFontMetricsGetStringBounds) {
 323:             bounds = fm.getStringBounds(text, g2);
 324:             // getStringBounds() can return incorrect height for some Unicode
 325:             // characters...see bug parade 6183356, let's replace it with 
 326:             // something correct
 327:             LineMetrics lm = fm.getFont().getLineMetrics(text,
 328:                     g2.getFontRenderContext());
 329:             bounds.setRect(bounds.getX(), bounds.getY(), bounds.getWidth(),
 330:                     lm.getHeight());
 331:         }
 332:         else {
 333:             final double width = fm.stringWidth(text);
 334:             final double height = fm.getHeight();
 335:             if (logger.isDebugEnabled()) {
 336:                 logger.debug("Height = " + height);
 337:             }
 338:             bounds = new Rectangle2D.Double(0.0, -fm.getAscent(), width, 
 339:                     height);
 340:         }
 341:         return bounds;
 342:     }
 343: 
 344:     /**
 345:      * Draws a string such that the specified anchor point is aligned to the 
 346:      * given (x, y) location.
 347:      *
 348:      * @param text  the text.
 349:      * @param g2  the graphics device.
 350:      * @param x  the x coordinate (Java 2D).
 351:      * @param y  the y coordinate (Java 2D).
 352:      * @param anchor  the anchor location.
 353:      * 
 354:      * @return The text bounds (adjusted for the text position).
 355:      */
 356:     public static Rectangle2D drawAlignedString(final String text,
 357:             final Graphics2D g2, final float x, final float y, 
 358:             final TextAnchor anchor) {
 359: 
 360:         final Rectangle2D textBounds = new Rectangle2D.Double();
 361:         final float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor, 
 362:                 textBounds);
 363:         // adjust text bounds to match string position
 364:         textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
 365:             textBounds.getWidth(), textBounds.getHeight());
 366:         g2.drawString(text, x + adjust[0], y + adjust[1]);
 367:         return textBounds;
 368:     }
 369: 
 370:     /**
 371:      * A utility method that calculates the anchor offsets for a string.  
 372:      * Normally, the (x, y) coordinate for drawing text is a point on the 
 373:      * baseline at the left of the text string.  If you add these offsets to 
 374:      * (x, y) and draw the string, then the anchor point should coincide with 
 375:      * the (x, y) point.
 376:      *
 377:      * @param g2  the graphics device (not <code>null</code>).
 378:      * @param text  the text.
 379:      * @param anchor  the anchor point.
 380:      * @param textBounds  the text bounds (if not <code>null</code>, this 
 381:      *                    object will be updated by this method to match the 
 382:      *                    string bounds).
 383:      * 
 384:      * @return  The offsets.
 385:      */
 386:     private static float[] deriveTextBoundsAnchorOffsets(final Graphics2D g2,
 387:             final String text, final TextAnchor anchor, 
 388:             final Rectangle2D textBounds) {
 389: 
 390:         final float[] result = new float[3];
 391:         final FontRenderContext frc = g2.getFontRenderContext();
 392:         final Font f = g2.getFont();
 393:         final FontMetrics fm = g2.getFontMetrics(f);
 394:         final Rectangle2D bounds = TextUtilities.getTextBounds(text, g2, fm);
 395:         final LineMetrics metrics = f.getLineMetrics(text, frc);
 396:         final float ascent = metrics.getAscent();
 397:         result[2] = -ascent;
 398:         final float halfAscent = ascent / 2.0f;
 399:         final float descent = metrics.getDescent();
 400:         final float leading = metrics.getLeading();
 401:         float xAdj = 0.0f;
 402:         float yAdj = 0.0f;
 403: 
 404:         if (anchor == TextAnchor.TOP_CENTER
 405:                 || anchor == TextAnchor.CENTER
 406:                 || anchor == TextAnchor.BOTTOM_CENTER
 407:                 || anchor == TextAnchor.BASELINE_CENTER
 408:                 || anchor == TextAnchor.HALF_ASCENT_CENTER) {
 409: 
 410:             xAdj = (float) -bounds.getWidth() / 2.0f;
 411: 
 412:         }
 413:         else if (anchor == TextAnchor.TOP_RIGHT
 414:                 || anchor == TextAnchor.CENTER_RIGHT
 415:                 || anchor == TextAnchor.BOTTOM_RIGHT
 416:                 || anchor == TextAnchor.BASELINE_RIGHT
 417:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 418: 
 419:             xAdj = (float) -bounds.getWidth();
 420: 
 421:         }
 422: 
 423:         if (anchor == TextAnchor.TOP_LEFT
 424:                 || anchor == TextAnchor.TOP_CENTER
 425:                 || anchor == TextAnchor.TOP_RIGHT) {
 426: 
 427:             yAdj = -descent - leading + (float) bounds.getHeight();
 428: 
 429:         }
 430:         else if (anchor == TextAnchor.HALF_ASCENT_LEFT
 431:                 || anchor == TextAnchor.HALF_ASCENT_CENTER
 432:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 433: 
 434:             yAdj = halfAscent;
 435: 
 436:         }
 437:         else if (anchor == TextAnchor.CENTER_LEFT
 438:                 || anchor == TextAnchor.CENTER
 439:                 || anchor == TextAnchor.CENTER_RIGHT) {
 440: 
 441:             yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
 442: 
 443:         }
 444:         else if (anchor == TextAnchor.BASELINE_LEFT
 445:                 || anchor == TextAnchor.BASELINE_CENTER
 446:                 || anchor == TextAnchor.BASELINE_RIGHT) {
 447: 
 448:             yAdj = 0.0f;
 449: 
 450:         }
 451:         else if (anchor == TextAnchor.BOTTOM_LEFT
 452:                 || anchor == TextAnchor.BOTTOM_CENTER
 453:                 || anchor == TextAnchor.BOTTOM_RIGHT) {
 454: 
 455:             yAdj = -metrics.getDescent() - metrics.getLeading();
 456: 
 457:         }
 458:         if (textBounds != null) {
 459:             textBounds.setRect(bounds);
 460:         }
 461:         result[0] = xAdj;
 462:         result[1] = yAdj;
 463:         return result;
 464: 
 465:     }
 466: 
 467:     /**
 468:      * Sets the flag that controls whether or not a workaround is used for
 469:      * drawing rotated strings.  The related bug is on Sun's bug parade 
 470:      * (id 4312117) and the workaround involves using a <code>TextLayout</code> 
 471:      * instance to draw the text instead of calling the 
 472:      * <code>drawString()</code> method in the <code>Graphics2D</code> class.
 473:      *
 474:      * @param use  the new flag value.
 475:      */
 476:     public static void setUseDrawRotatedStringWorkaround(final boolean use) {
 477:         useDrawRotatedStringWorkaround = use;
 478:     }
 479: 
 480:     /**
 481:      * A utility method for drawing rotated text.
 482:      * <P>
 483:      * A common rotation is -Math.PI/2 which draws text 'vertically' (with the 
 484:      * top of the characters on the left).
 485:      *
 486:      * @param text  the text.
 487:      * @param g2  the graphics device.
 488:      * @param angle  the angle of the (clockwise) rotation (in radians).
 489:      * @param x  the x-coordinate.
 490:      * @param y  the y-coordinate.
 491:      */
 492:     public static void drawRotatedString(final String text, final Graphics2D g2,
 493:             final double angle, final float x, final float y) {
 494:         drawRotatedString(text, g2, x, y, angle, x, y);
 495:     }
 496: 
 497:     /**
 498:      * A utility method for drawing rotated text.
 499:      * <P>
 500:      * A common rotation is -Math.PI/2 which draws text 'vertically' (with the 
 501:      * top of the characters on the left).
 502:      *
 503:      * @param text  the text.
 504:      * @param g2  the graphics device.
 505:      * @param textX  the x-coordinate for the text (before rotation).
 506:      * @param textY  the y-coordinate for the text (before rotation).
 507:      * @param angle  the angle of the (clockwise) rotation (in radians).
 508:      * @param rotateX  the point about which the text is rotated.
 509:      * @param rotateY  the point about which the text is rotated.
 510:      */
 511:     public static void drawRotatedString(final String text, final Graphics2D g2,
 512:             final float textX, final float textY, final double angle,
 513:             final float rotateX, final float rotateY) {
 514: 
 515:         if ((text == null) || (text.equals(""))) {
 516:             return;
 517:         }
 518: 
 519:         final AffineTransform saved = g2.getTransform();
 520: 
 521:         // apply the rotation...
 522:         final AffineTransform rotate = AffineTransform.getRotateInstance(
 523:                 angle, rotateX, rotateY);
 524:         g2.transform(rotate);
 525: 
 526:         if (useDrawRotatedStringWorkaround) {
 527:             // workaround for JDC bug ID 4312117 and others...
 528:             final TextLayout tl = new TextLayout(text, g2.getFont(), 
 529:                     g2.getFontRenderContext());
 530:             tl.draw(g2, textX, textY);
 531:         }
 532:         else {
 533:             // replaces this code...
 534:             g2.drawString(text, textX, textY);
 535:         }
 536:         g2.setTransform(saved);
 537: 
 538:     }
 539: 
 540:     /**
 541:      * Draws a string that is aligned by one anchor point and rotated about 
 542:      * another anchor point.
 543:      *
 544:      * @param text  the text.
 545:      * @param g2  the graphics device.
 546:      * @param x  the x-coordinate for positioning the text.
 547:      * @param y  the y-coordinate for positioning the text.
 548:      * @param textAnchor  the text anchor.
 549:      * @param angle  the rotation angle.
 550:      * @param rotationX  the x-coordinate for the rotation anchor point.
 551:      * @param rotationY  the y-coordinate for the rotation anchor point.
 552:      */
 553:     public static void drawRotatedString(final String text, 
 554:             final Graphics2D g2, final float x, final float y,
 555:             final TextAnchor textAnchor, final double angle,
 556:             final float rotationX, final float rotationY) {
 557: 
 558:         if (text == null || text.equals("")) {
 559:             return;
 560:         }
 561:         final float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, 
 562:                 textAnchor);
 563:         drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle, 
 564:                 rotationX, rotationY);
 565:     }
 566: 
 567:     /**
 568:      * Draws a string that is aligned by one anchor point and rotated about 
 569:      * another anchor point.
 570:      *
 571:      * @param text  the text.
 572:      * @param g2  the graphics device.
 573:      * @param x  the x-coordinate for positioning the text.
 574:      * @param y  the y-coordinate for positioning the text.
 575:      * @param textAnchor  the text anchor.
 576:      * @param angle  the rotation angle (in radians).
 577:      * @param rotationAnchor  the rotation anchor.
 578:      */
 579:     public static void drawRotatedString(final String text, final Graphics2D g2,
 580:             final float x, final float y, final TextAnchor textAnchor,
 581:             final double angle, final TextAnchor rotationAnchor) {
 582: 
 583:         if (text == null || text.equals("")) {
 584:             return;
 585:         }
 586:         final float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, 
 587:                 textAnchor);
 588:         final float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, 
 589:                 rotationAnchor);
 590:         drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
 591:                 angle, x + textAdj[0] + rotateAdj[0], 
 592:                 y + textAdj[1] + rotateAdj[1]);
 593: 
 594:     }
 595: 
 596:     /**
 597:      * Returns a shape that represents the bounds of the string after the 
 598:      * specified rotation has been applied.
 599:      * 
 600:      * @param text  the text (<code>null</code> permitted).
 601:      * @param g2  the graphics device.
 602:      * @param x  the x coordinate for the anchor point.
 603:      * @param y  the y coordinate for the anchor point.
 604:      * @param textAnchor  the text anchor.
 605:      * @param angle  the angle.
 606:      * @param rotationAnchor  the rotation anchor.
 607:      * 
 608:      * @return The bounds (possibly <code>null</code>).
 609:      */
 610:     public static Shape calculateRotatedStringBounds(final String text,
 611:             final Graphics2D g2, final float x, final float y,
 612:             final TextAnchor textAnchor, final double angle,
 613:             final TextAnchor rotationAnchor) {
 614: 
 615:         if (text == null || text.equals("")) {
 616:             return null;
 617:         }
 618:         final float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, 
 619:                 textAnchor);
 620:         if (logger.isDebugEnabled()) {
 621:             logger.debug("TextBoundsAnchorOffsets = " + textAdj[0] + ", " 
 622:                     + textAdj[1]);
 623:         }
 624:         final float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, 
 625:                 rotationAnchor);
 626:         if (logger.isDebugEnabled()) {
 627:             logger.debug("RotationAnchorOffsets = " + rotateAdj[0] + ", " 
 628:                     + rotateAdj[1]);
 629:         }
 630:         final Shape result = calculateRotatedStringBounds(text, g2, 
 631:                 x + textAdj[0], y + textAdj[1], angle, 
 632:                 x + textAdj[0] + rotateAdj[0], y + textAdj[1] + rotateAdj[1]);
 633:         return result;
 634: 
 635:     }
 636: 
 637:     /**
 638:      * A utility method that calculates the anchor offsets for a string.  
 639:      * Normally, the (x, y) coordinate for drawing text is a point on the 
 640:      * baseline at the left of the text string.  If you add these offsets to 
 641:      * (x, y) and draw the string, then the anchor point should coincide with 
 642:      * the (x, y) point.
 643:      *
 644:      * @param g2  the graphics device (not <code>null</code>).
 645:      * @param text  the text.
 646:      * @param anchor  the anchor point.
 647:      *
 648:      * @return  The offsets.
 649:      */
 650:     private static float[] deriveTextBoundsAnchorOffsets(final Graphics2D g2,
 651:             final String text, final TextAnchor anchor) {
 652: 
 653:         final float[] result = new float[2];
 654:         final FontRenderContext frc = g2.getFontRenderContext();
 655:         final Font f = g2.getFont();
 656:         final FontMetrics fm = g2.getFontMetrics(f);
 657:         final Rectangle2D bounds = TextUtilities.getTextBounds(text, g2, fm);
 658:         final LineMetrics metrics = f.getLineMetrics(text, frc);
 659:         final float ascent = metrics.getAscent();
 660:         final float halfAscent = ascent / 2.0f;
 661:         final float descent = metrics.getDescent();
 662:         final float leading = metrics.getLeading();
 663:         float xAdj = 0.0f;
 664:         float yAdj = 0.0f;
 665: 
 666:         if (anchor == TextAnchor.TOP_CENTER
 667:                 || anchor == TextAnchor.CENTER
 668:                 || anchor == TextAnchor.BOTTOM_CENTER
 669:                 || anchor == TextAnchor.BASELINE_CENTER
 670:                 || anchor == TextAnchor.HALF_ASCENT_CENTER) {
 671: 
 672:             xAdj = (float) -bounds.getWidth() / 2.0f;
 673: 
 674:         }
 675:         else if (anchor == TextAnchor.TOP_RIGHT
 676:                 || anchor == TextAnchor.CENTER_RIGHT
 677:                 || anchor == TextAnchor.BOTTOM_RIGHT
 678:                 || anchor == TextAnchor.BASELINE_RIGHT
 679:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 680: 
 681:             xAdj = (float) -bounds.getWidth();
 682: 
 683:         }
 684: 
 685:         if (anchor == TextAnchor.TOP_LEFT
 686:                 || anchor == TextAnchor.TOP_CENTER
 687:                 || anchor == TextAnchor.TOP_RIGHT) {
 688: 
 689:             yAdj = -descent - leading + (float) bounds.getHeight();
 690: 
 691:         }
 692:         else if (anchor == TextAnchor.HALF_ASCENT_LEFT
 693:                 || anchor == TextAnchor.HALF_ASCENT_CENTER
 694:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 695: 
 696:             yAdj = halfAscent;
 697: 
 698:         }
 699:         else if (anchor == TextAnchor.CENTER_LEFT
 700:                 || anchor == TextAnchor.CENTER
 701:                 || anchor == TextAnchor.CENTER_RIGHT) {
 702: 
 703:             yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
 704: 
 705:         }
 706:         else if (anchor == TextAnchor.BASELINE_LEFT
 707:                 || anchor == TextAnchor.BASELINE_CENTER
 708:                 || anchor == TextAnchor.BASELINE_RIGHT) {
 709: 
 710:             yAdj = 0.0f;
 711: 
 712:         }
 713:         else if (anchor == TextAnchor.BOTTOM_LEFT
 714:                 || anchor == TextAnchor.BOTTOM_CENTER
 715:                 || anchor == TextAnchor.BOTTOM_RIGHT) {
 716: 
 717:             yAdj = -metrics.getDescent() - metrics.getLeading();
 718: 
 719:         }
 720:         result[0] = xAdj;
 721:         result[1] = yAdj;
 722:         return result;
 723: 
 724:     }
 725: 
 726:     /**
 727:      * A utility method that calculates the rotation anchor offsets for a 
 728:      * string.  These offsets are relative to the text starting coordinate 
 729:      * (BASELINE_LEFT).
 730:      *
 731:      * @param g2  the graphics device.
 732:      * @param text  the text.
 733:      * @param anchor  the anchor point.
 734:      *
 735:      * @return  The offsets.
 736:      */
 737:     private static float[] deriveRotationAnchorOffsets(final Graphics2D g2,
 738:             final String text, final TextAnchor anchor) {
 739: 
 740:         final float[] result = new float[2];
 741:         final FontRenderContext frc = g2.getFontRenderContext();
 742:         final LineMetrics metrics = g2.getFont().getLineMetrics(text, frc);
 743:         final FontMetrics fm = g2.getFontMetrics();
 744:         final Rectangle2D bounds = TextUtilities.getTextBounds(text, g2, fm);
 745:         final float ascent = metrics.getAscent();
 746:         final float halfAscent = ascent / 2.0f;
 747:         final float descent = metrics.getDescent();
 748:         final float leading = metrics.getLeading();
 749:         float xAdj = 0.0f;
 750:         float yAdj = 0.0f;
 751: 
 752:         if (anchor == TextAnchor.TOP_LEFT
 753:                 || anchor == TextAnchor.CENTER_LEFT
 754:                 || anchor == TextAnchor.BOTTOM_LEFT
 755:                 || anchor == TextAnchor.BASELINE_LEFT
 756:                 || anchor == TextAnchor.HALF_ASCENT_LEFT) {
 757: 
 758:             xAdj = 0.0f;
 759: 
 760:         }
 761:         else if (anchor == TextAnchor.TOP_CENTER
 762:                 || anchor == TextAnchor.CENTER
 763:                 || anchor == TextAnchor.BOTTOM_CENTER
 764:                 || anchor == TextAnchor.BASELINE_CENTER
 765:                 || anchor == TextAnchor.HALF_ASCENT_CENTER) {
 766: 
 767:             xAdj = (float) bounds.getWidth() / 2.0f;
 768: 
 769:         }
 770:         else if (anchor == TextAnchor.TOP_RIGHT
 771:                 || anchor == TextAnchor.CENTER_RIGHT
 772:                 || anchor == TextAnchor.BOTTOM_RIGHT
 773:                 || anchor == TextAnchor.BASELINE_RIGHT
 774:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 775: 
 776:             xAdj = (float) bounds.getWidth();
 777: 
 778:         }
 779: 
 780:         if (anchor == TextAnchor.TOP_LEFT
 781:                 || anchor == TextAnchor.TOP_CENTER
 782:                 || anchor == TextAnchor.TOP_RIGHT) {
 783: 
 784:             yAdj = descent + leading - (float) bounds.getHeight();
 785: 
 786:         }
 787:         else if (anchor == TextAnchor.CENTER_LEFT
 788:                 || anchor == TextAnchor.CENTER
 789:                 || anchor == TextAnchor.CENTER_RIGHT) {
 790: 
 791:             yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
 792: 
 793:         }
 794:         else if (anchor == TextAnchor.HALF_ASCENT_LEFT
 795:                 || anchor == TextAnchor.HALF_ASCENT_CENTER
 796:                 || anchor == TextAnchor.HALF_ASCENT_RIGHT) {
 797: 
 798:             yAdj = -halfAscent;
 799: 
 800:         }
 801:         else if (anchor == TextAnchor.BASELINE_LEFT
 802:                 || anchor == TextAnchor.BASELINE_CENTER
 803:                 || anchor == TextAnchor.BASELINE_RIGHT) {
 804: 
 805:             yAdj = 0.0f;
 806: 
 807:         }
 808:         else if (anchor == TextAnchor.BOTTOM_LEFT
 809:                 || anchor == TextAnchor.BOTTOM_CENTER
 810:                 || anchor == TextAnchor.BOTTOM_RIGHT) {
 811: 
 812:             yAdj = metrics.getDescent() + metrics.getLeading();
 813: 
 814:         }
 815:         result[0] = xAdj;
 816:         result[1] = yAdj;
 817:         return result;
 818: 
 819:     }
 820: 
 821:     /**
 822:      * Returns a shape that represents the bounds of the string after the 
 823:      * specified rotation has been applied.
 824:      * 
 825:      * @param text  the text (<code>null</code> permitted).
 826:      * @param g2  the graphics device.
 827:      * @param textX  the x coordinate for the text.
 828:      * @param textY  the y coordinate for the text.
 829:      * @param angle  the angle.
 830:      * @param rotateX  the x coordinate for the rotation point.
 831:      * @param rotateY  the y coordinate for the rotation point.
 832:      * 
 833:      * @return The bounds (<code>null</code> if <code>text</code> is 
 834:      *         </code>null</code> or has zero length).
 835:      */
 836:     public static Shape calculateRotatedStringBounds(final String text,
 837:             final Graphics2D g2, final float textX, final float textY,
 838:             final double angle, final float rotateX, final float rotateY) {
 839: 
 840:         if ((text == null) || (text.equals(""))) {
 841:             return null;
 842:         }
 843:         final FontMetrics fm = g2.getFontMetrics();
 844:         final Rectangle2D bounds = TextUtilities.getTextBounds(text, g2, fm);
 845:         final AffineTransform translate = AffineTransform.getTranslateInstance(
 846:                 textX, textY);
 847:         final Shape translatedBounds = translate.createTransformedShape(bounds);
 848:         final AffineTransform rotate = AffineTransform.getRotateInstance(
 849:                 angle, rotateX, rotateY);
 850:         final Shape result = rotate.createTransformedShape(translatedBounds);
 851:         return result;
 852: 
 853:     }
 854: 
 855:     /**
 856:      * Returns the flag that controls whether the FontMetrics.getStringBounds() 
 857:      * method is used or not.  If you are having trouble with label alignment
 858:      * or positioning, try changing the value of this flag.
 859:      * 
 860:      * @return A boolean.
 861:      */
 862:     public static boolean getUseFontMetricsGetStringBounds() {
 863:         return useFontMetricsGetStringBounds;
 864:     }
 865: 
 866:     /**
 867:      * Sets the flag that controls whether the FontMetrics.getStringBounds() 
 868:      * method is used or not.  If you are having trouble with label alignment 
 869:      * or positioning, try changing the value of this flag.
 870:      * 
 871:      * @param use  the flag.
 872:      */
 873:     public static void setUseFontMetricsGetStringBounds(final boolean use) {
 874:         useFontMetricsGetStringBounds = use;
 875:     }
 876: 
 877:     public static boolean isUseDrawRotatedStringWorkaround() {
 878:         return useDrawRotatedStringWorkaround;
 879:     }
 880: }