001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://www.jfree.org/jfreechart/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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------
028 * AttrStringUtils.java
029 * --------------------
030 * (C) Copyright 2013-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 * 
035 */
036
037package org.jfree.chart.util;
038
039import org.jfree.chart.internal.Args;
040import java.awt.Graphics2D;
041import java.awt.font.TextLayout;
042import java.awt.geom.AffineTransform;
043import java.awt.geom.Rectangle2D;
044import java.text.AttributedString;
045import org.jfree.chart.text.TextAnchor;
046
047/**
048 * Some {@code AttributedString} utilities.
049 * 
050 * @since 1.0.16
051 */
052public class AttrStringUtils {
053   
054    private AttrStringUtils() {
055        // no need to instantiate this class   
056    }
057    
058    /**
059     * Returns the bounds for the attributed string.
060     * 
061     * @param text  the attributed string ({@code null} not permitted).
062     * @param g2  the graphics target ({@code null} not permitted).
063     * 
064     * @return The bounds (never {@code null}).
065     * 
066     * @since 1.0.18
067     */
068    public static Rectangle2D getTextBounds(AttributedString text, 
069            Graphics2D g2) {
070        TextLayout tl = new TextLayout(text.getIterator(), 
071                g2.getFontRenderContext());
072        return tl.getBounds();
073    }
074    
075    /**
076     * Draws the attributed string at {@code (x, y)}, rotated by the 
077     * specified angle about {@code (x, y)}.
078     * 
079     * @param text  the attributed string ({@code null} not permitted).
080     * @param g2  the graphics output target.
081     * @param angle  the angle.
082     * @param x  the x-coordinate.
083     * @param y  the y-coordinate.
084     * 
085     * @since 1.0.16
086     */
087    public static void drawRotatedString(AttributedString text, Graphics2D g2, 
088            double angle, float x, float y) {
089        drawRotatedString(text, g2, x, y, angle, x, y);
090    }
091    
092    /**
093     * Draws the attributed string at {@code (textX, textY)}, rotated by 
094     * the specified angle about {@code (rotateX, rotateY)}.
095     * 
096     * @param text  the attributed string ({@code null} not permitted).
097     * @param g2  the graphics output target.
098     * @param textX  the x-coordinate for the text.
099     * @param textY  the y-coordinate for the text.
100     * @param angle  the rotation angle (in radians).
101     * @param rotateX  the x-coordinate for the rotation point.
102     * @param rotateY  the y-coordinate for the rotation point.
103     * 
104     * @since 1.0.16
105     */
106    public static void drawRotatedString(AttributedString text, Graphics2D g2, 
107            float textX, float textY, double angle, float rotateX, 
108            float rotateY) {
109        Args.nullNotPermitted(text, "text");
110
111        AffineTransform saved = g2.getTransform();
112        AffineTransform rotate = AffineTransform.getRotateInstance(angle, 
113                rotateX, rotateY);
114        g2.transform(rotate);
115        TextLayout tl = new TextLayout(text.getIterator(),
116                    g2.getFontRenderContext());
117        tl.draw(g2, textX, textY);
118        
119        g2.setTransform(saved);        
120    }
121    
122    /**
123     * Draws the string anchored to {@code (x, y)}, rotated by the 
124     * specified angle about {@code (rotationX, rotationY)}.
125     * 
126     * @param text  the text ({@code null} not permitted).
127     * @param g2  the graphics target.
128     * @param x  the x-coordinate for the text location.
129     * @param y  the y-coordinate for the text location.
130     * @param textAnchor  the text anchor point.
131     * @param angle  the rotation (in radians).
132     * @param rotationX  the x-coordinate for the rotation point.
133     * @param rotationY  the y-coordinate for the rotation point.
134     * 
135     * @since 1.0.16
136     */
137    public static void drawRotatedString(AttributedString text, Graphics2D g2, 
138            float x, float y, TextAnchor textAnchor, 
139            double angle, float rotationX, float rotationY) {
140        Args.nullNotPermitted(text, "text");
141        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor, 
142                null);
143        drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], angle,
144                rotationX, rotationY);        
145    }
146
147    /**
148     * Draws a rotated string.
149     * 
150     * @param text  the text to draw.
151     * @param g2  the graphics target.
152     * @param x  the x-coordinate for the text location.
153     * @param y  the y-coordinate for the text location.
154     * @param textAnchor  the text anchor point.
155     * @param angle  the rotation (in radians).
156     * @param rotationAnchor  the rotation anchor point.
157     * 
158     * @since 1.0.16
159     */
160    public static void drawRotatedString(AttributedString text, Graphics2D g2,
161            float x, float y, TextAnchor textAnchor,
162            double angle, TextAnchor rotationAnchor) {
163        Args.nullNotPermitted(text, "text");
164        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor, 
165                null);
166        float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, 
167                rotationAnchor);
168        drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
169                angle, x + textAdj[0] + rotateAdj[0],
170                y + textAdj[1] + rotateAdj[1]);        
171    }
172        
173    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
174            AttributedString text, TextAnchor anchor, Rectangle2D textBounds) {
175
176        TextLayout layout = new TextLayout(text.getIterator(), g2.getFontRenderContext());
177        Rectangle2D bounds = layout.getBounds();
178
179        float[] result = new float[3];
180        float ascent = layout.getAscent();
181        result[2] = -ascent;
182        float halfAscent = ascent / 2.0f;
183        float descent = layout.getDescent();
184        float leading = layout.getLeading();
185        float xAdj = 0.0f;
186        float yAdj = 0.0f;
187        
188        if (isHorizontalCenter(anchor)) {
189            xAdj = (float) -bounds.getWidth() / 2.0f;
190        }
191        else if (isHorizontalRight(anchor)) {
192            xAdj = (float) -bounds.getWidth();
193        }
194
195        if (isTop(anchor)) {
196            //yAdj = -descent - leading + (float) bounds.getHeight();
197            yAdj = (float) bounds.getHeight();
198        }
199        else if (isHalfAscent(anchor)) {
200            yAdj = halfAscent;
201        }
202        else if (isHalfHeight(anchor)) {
203            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
204        }
205        else if (isBaseline(anchor)) {
206            yAdj = 0.0f;
207        }
208        else if (isBottom(anchor)) {
209            yAdj = -descent - leading;
210        }
211        if (textBounds != null) {
212            textBounds.setRect(bounds);
213        }
214        result[0] = xAdj;
215        result[1] = yAdj;
216        return result;
217    }
218    
219    /**
220     * A utility method that calculates the rotation anchor offsets for a
221     * string.  These offsets are relative to the text starting coordinate
222     * (BASELINE_LEFT).
223     *
224     * @param g2  the graphics device.
225     * @param text  the text.
226     * @param anchor  the anchor point.
227     *
228     * @return  The offsets.
229     */
230    private static float[] deriveRotationAnchorOffsets(Graphics2D g2, 
231            AttributedString text, TextAnchor anchor) {
232
233        float[] result = new float[2];
234        
235        TextLayout layout = new TextLayout(text.getIterator(), 
236                g2.getFontRenderContext());
237        Rectangle2D bounds = layout.getBounds();
238        float ascent = layout.getAscent();
239        float halfAscent = ascent / 2.0f;
240        float descent = layout.getDescent();
241        float leading = layout.getLeading();
242        float xAdj = 0.0f;
243        float yAdj = 0.0f;
244
245        if (isHorizontalLeft(anchor)) {
246            xAdj = 0.0f;
247        }
248        else if (isHorizontalCenter(anchor)) {
249            xAdj = (float) bounds.getWidth() / 2.0f;
250        }
251        else if (isHorizontalRight(anchor)) {
252            xAdj = (float) bounds.getWidth();
253        }
254
255        if (isTop(anchor)) {
256            yAdj = descent + leading - (float) bounds.getHeight();
257        }
258        else if (isHalfHeight(anchor)) {
259            yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
260        }
261        else if (isHalfAscent(anchor)) {
262            yAdj = -halfAscent;
263        }
264        else if (isBaseline(anchor)) {
265            yAdj = 0.0f;
266        }
267        else if (isBottom(anchor)) {
268            yAdj = descent + leading;
269        }
270        result[0] = xAdj;
271        result[1] = yAdj;
272        return result;
273
274    }
275    
276    private static boolean isTop(TextAnchor anchor) {
277        return anchor.equals(TextAnchor.TOP_LEFT) 
278                || anchor.equals(TextAnchor.TOP_CENTER) 
279                || anchor.equals(TextAnchor.TOP_RIGHT);
280    }
281
282    private static boolean isBaseline(TextAnchor anchor) {
283        return anchor.equals(TextAnchor.BASELINE_LEFT) 
284                || anchor.equals(TextAnchor.BASELINE_CENTER) 
285                || anchor.equals(TextAnchor.BASELINE_RIGHT);
286    }
287
288    private static boolean isHalfAscent(TextAnchor anchor) {
289        return anchor.equals(TextAnchor.HALF_ASCENT_LEFT) 
290                || anchor.equals(TextAnchor.HALF_ASCENT_CENTER)
291                || anchor.equals(TextAnchor.HALF_ASCENT_RIGHT);
292    }
293
294    private static boolean isHalfHeight(TextAnchor anchor) {
295        return anchor.equals(TextAnchor.CENTER_LEFT) 
296                || anchor.equals(TextAnchor.CENTER) 
297                || anchor.equals(TextAnchor.CENTER_RIGHT);
298    }
299
300    private static boolean isBottom(TextAnchor anchor) {
301        return anchor.equals(TextAnchor.BOTTOM_LEFT) 
302                || anchor.equals(TextAnchor.BOTTOM_CENTER) 
303                || anchor.equals(TextAnchor.BOTTOM_RIGHT);
304    }
305
306    private static boolean isHorizontalLeft(TextAnchor anchor) {
307        return anchor.equals(TextAnchor.TOP_LEFT) 
308                || anchor.equals(TextAnchor.CENTER_LEFT) 
309                || anchor.equals(TextAnchor.HALF_ASCENT_LEFT) 
310                || anchor.equals(TextAnchor.BASELINE_LEFT) 
311                || anchor.equals(TextAnchor.BOTTOM_LEFT);
312    }
313
314    private static boolean isHorizontalCenter(TextAnchor anchor) {
315        return anchor.equals(TextAnchor.TOP_CENTER) 
316                || anchor.equals(TextAnchor.CENTER) 
317                || anchor.equals(TextAnchor.HALF_ASCENT_CENTER) 
318                || anchor.equals(TextAnchor.BASELINE_CENTER) 
319                || anchor.equals(TextAnchor.BOTTOM_CENTER);
320    }
321
322    private static boolean isHorizontalRight(TextAnchor anchor) {
323        return anchor.equals(TextAnchor.TOP_RIGHT) 
324                || anchor.equals(TextAnchor.CENTER_RIGHT) 
325                || anchor.equals(TextAnchor.HALF_ASCENT_RIGHT) 
326                || anchor.equals(TextAnchor.BASELINE_RIGHT)
327                || anchor.equals(TextAnchor.BOTTOM_RIGHT);
328    }
329}