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