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