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