001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
006 * 
007 * http://www.object-refinery.com/orsoncharts/index.html
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package com.orsoncharts.util;
034
035import java.awt.Font;
036import java.awt.FontMetrics;
037import java.awt.Graphics2D;
038import java.awt.Shape;
039import java.awt.font.FontRenderContext;
040import java.awt.font.LineMetrics;
041import java.awt.font.TextLayout;
042import java.awt.geom.AffineTransform;
043import java.awt.geom.Rectangle2D;
044import java.text.AttributedString;
045
046/**
047 * Utility methods for working with text.
048 */
049public class TextUtils {
050
051    private TextUtils() {
052        // no need to instantiate this
053    }
054    
055    /**
056     * Draws a string such that the specified anchor point is aligned to the
057     * given {@code (x, y)} location, and returns a bounding rectangle 
058     * for the text.
059     *
060     * @param text  the text.
061     * @param g2  the graphics device ({@code null} not permitted).
062     * @param x  the x coordinate (Java 2D).
063     * @param y  the y coordinate (Java 2D).
064     * @param anchor  the anchor location ({@code null} not permitted).
065     *
066     * @return The text bounds (adjusted for the text position).
067     */
068    public static Rectangle2D drawAlignedString(String text,
069            Graphics2D g2, float x, float y, TextAnchor anchor) {
070
071        Rectangle2D textBounds = new Rectangle2D.Double();
072        float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor,
073                textBounds);
074        // adjust text bounds to match string position
075        textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
076            textBounds.getWidth(), textBounds.getHeight());
077        g2.drawString(text, x + adjust[0], y + adjust[1]);
078        return textBounds;
079    }
080
081    /**
082     * Returns the bounds of an aligned string.
083     * 
084     * @param text  the string ({@code null} not permitted).
085     * @param g2  the graphics target ({@code null} not permitted).
086     * @param x  the x-coordinate.
087     * @param y  the y-coordinate.
088     * @param anchor  the anchor point on the text that will be aligned to 
089     *     {@code (x, y)} ({@code null} not permitted).
090     * 
091     * @return The text bounds (never {@code null}).
092     * 
093     * @since 1.3
094     */
095    public static Rectangle2D calcAlignedStringBounds(String text,
096            Graphics2D g2, float x, float y, TextAnchor anchor) {
097
098        Rectangle2D textBounds = new Rectangle2D.Double();
099        float[] adjust = deriveTextBoundsAnchorOffsets(g2, text, anchor,
100                textBounds);
101        // adjust text bounds to match string position
102        textBounds.setRect(x + adjust[0], y + adjust[1] + adjust[2],
103            textBounds.getWidth(), textBounds.getHeight());
104        return textBounds;
105    }
106    
107    /**
108     * A utility method that calculates the anchor offsets for a string.
109     * Normally, the {@code (x, y)} coordinate for drawing text is a point on 
110     * the baseline at the left of the text string.  If you add these offsets 
111     * to {@code (x, y)} and draw the string, then the anchor point should 
112     * coincide with the {@code (x, y)} point.
113     *
114     * @param g2  the graphics device (not {@code null}).
115     * @param text  the text.
116     * @param anchor  the anchor point ({@code null} not permitted).
117     *
118     * @return  The offsets.
119     */
120    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
121            String text, TextAnchor anchor) {
122
123        float[] result = new float[2];
124        FontRenderContext frc = g2.getFontRenderContext();
125        Font f = g2.getFont();
126        FontMetrics fm = g2.getFontMetrics(f);
127        Rectangle2D bounds = getTextBounds(text, fm);
128        LineMetrics metrics = f.getLineMetrics(text, frc);
129        float ascent = metrics.getAscent();
130        float halfAscent = ascent / 2.0f;
131        float descent = metrics.getDescent();
132        float leading = metrics.getLeading();
133        float xAdj = 0.0f;
134        float yAdj = 0.0f;
135
136        if (anchor.isHorizontalCenter()) {
137            xAdj = (float) -bounds.getWidth() / 2.0f;
138        } else if (anchor.isRight()) {
139            xAdj = (float) -bounds.getWidth();
140        }
141
142        if (anchor.isTop()) {
143            yAdj = -descent - leading + (float) bounds.getHeight();
144        } else if (anchor.isHalfAscent()) {
145            yAdj = halfAscent;
146        } else if (anchor.isHalfHeight()) {
147            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
148        } else if (anchor.isBaseline()) {
149            yAdj = 0.0f;
150        } else if (anchor.isBottom()) {
151            yAdj = -metrics.getDescent() - metrics.getLeading();
152        }
153        result[0] = xAdj;
154        result[1] = yAdj;
155        return result;
156    }
157
158    /**
159     * A utility method that calculates the anchor offsets for a string.
160     * Normally, the {@code (x, y)} coordinate for drawing text is a point on 
161     * the baseline at the left of the text string.  If you add these offsets 
162     * to {@code (x, y)} and draw the string, then the anchor point should 
163     * coincide with the {@code (x, y)} point.
164     *
165     * @param g2  the graphics device (not {@code null}).
166     * @param text  the text.
167     * @param anchor  the anchor point ({@code null} not permitted).
168     * @param textBounds  the text bounds (if not {@code null}, this
169     *                    object will be updated by this method to match the
170     *                    string bounds).
171     *
172     * @return  The offsets.
173     */
174    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
175            String text, TextAnchor anchor, Rectangle2D textBounds) {
176
177        float[] result = new float[3];
178        FontRenderContext frc = g2.getFontRenderContext();
179        Font f = g2.getFont();
180        FontMetrics fm = g2.getFontMetrics(f);
181        Rectangle2D bounds = getTextBounds(text, fm);
182        LineMetrics metrics = f.getLineMetrics(text, frc);
183        float ascent = metrics.getAscent();
184        result[2] = -ascent;
185        float halfAscent = ascent / 2.0f;
186        float descent = metrics.getDescent();
187        float leading = metrics.getLeading();
188        float xAdj = 0.0f;
189        float yAdj = 0.0f;
190
191        if (anchor.isHorizontalCenter()) {
192            xAdj = (float) -bounds.getWidth() / 2.0f;
193        } else if (anchor.isRight()) {
194            xAdj = (float) -bounds.getWidth();
195        }
196
197        if (anchor.isTop()) {
198            yAdj = -descent - leading + (float) bounds.getHeight();
199        } else if (anchor.isHalfAscent()) {
200            yAdj = halfAscent;
201        } else if (anchor.isHorizontalCenter()) {
202            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
203        } else if (anchor.isBaseline()) {
204            yAdj = 0.0f;
205        } else if (anchor.isBottom()) {
206            yAdj = -metrics.getDescent() - metrics.getLeading();
207        }
208        if (textBounds != null) {
209            textBounds.setRect(bounds);
210        }
211        result[0] = xAdj;
212        result[1] = yAdj;
213        return result;
214    }
215    
216    /**
217     * Returns the bounds for the specified text.  The supplied text is
218     * assumed to be on a single line (no carriage return or newline 
219     * characters).
220     *
221     * @param text  the text ({@code null} not permitted).
222     * @param fm  the font metrics ({@code null} not permitted).
223     *
224     * @return The text bounds.
225     */
226    public static Rectangle2D getTextBounds(String text, FontMetrics fm) {
227        return getTextBounds(text, 0.0, 0.0, fm);
228    }
229    
230    /**
231     * Returns the bounds for the specified text when it is drawn with the 
232     * left-baseline aligned to the point {@code (x, y)}.
233     * 
234     * @param text  the text ({@code null} not permitted).
235     * @param x  the x-coordinate.
236     * @param y  the y-coordinate.
237     * @param fm  the font metrics ({@code null} not permitted).
238     * 
239     * @return The bounding rectangle (never {@code null}). 
240     */
241    public static Rectangle2D getTextBounds(String text, double x, double y,
242            FontMetrics fm) {
243        ArgChecks.nullNotPermitted(text, "text");
244        ArgChecks.nullNotPermitted(fm, "fm");
245        double width = fm.stringWidth(text);
246        double height = fm.getHeight();
247        return new Rectangle2D.Double(x, y - fm.getAscent(), width, height);
248    }
249    
250    /**
251     * Draws a string that is aligned by one anchor point and rotated about
252     * another anchor point.
253     *
254     * @param text  the text ({@code null} not permitted).
255     * @param g2  the graphics target ({@code null} not permitted).
256     * @param x  the x-coordinate for positioning the text.
257     * @param y  the y-coordinate for positioning the text.
258     * @param textAnchor  the text anchor ({@code null} not permitted).
259     * @param angle  the rotation angle.
260     * @param rotationX  the x-coordinate for the rotation anchor point.
261     * @param rotationY  the y-coordinate for the rotation anchor point.
262     * 
263     * @return The text bounds (never {@code null}).
264     */
265    public static Shape drawRotatedString(String text, Graphics2D g2, float x,
266            float y, TextAnchor textAnchor, double angle,
267            float rotationX, float rotationY) {
268        ArgChecks.nullNotPermitted(text, "text");
269        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor);
270        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1], 
271                angle, rotationX, rotationY);
272    }
273
274    /**
275     * Draws a string that is aligned by one anchor point and rotated about
276     * another anchor point, and returns a bounding shape for the text.
277     *
278     * @param text  the text ({@code null} not permitted).
279     * @param g2  the graphics device ({@code null} not permitted).
280     * @param x  the x-coordinate for positioning the text.
281     * @param y  the y-coordinate for positioning the text.
282     * @param textAnchor  the text anchor ({@code null} not permitted).
283     * @param angle  the rotation angle (in radians).
284     * @param rotationAnchor  the rotation anchor ({@code null} not permitted).
285     * 
286     * @return A bounding shape for the text.
287     */
288    public static Shape drawRotatedString(String text, Graphics2D g2,
289            float x, float y, TextAnchor textAnchor,
290            double angle, TextAnchor rotationAnchor) {
291
292        ArgChecks.nullNotPermitted(text, "text");
293        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor);
294        float[] rotateAdj = deriveRotationAnchorOffsets(g2, text,
295                rotationAnchor);
296        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
297                angle, x + textAdj[0] + rotateAdj[0],
298                y + textAdj[1] + rotateAdj[1]);
299    }
300
301    /**
302     * A utility method that calculates the rotation anchor offsets for a
303     * string.  These offsets are relative to the text starting coordinate
304     * ({@code BASELINE_LEFT}).
305     *
306     * @param g2  the graphics device ({@code null} not permitted).
307     * @param text  the text ({@code null} not permitted).
308     * @param anchor  the anchor point ({@code null} not permitted).
309     *
310     * @return  The offsets.
311     */
312    private static float[] deriveRotationAnchorOffsets(Graphics2D g2,
313            String text, TextAnchor anchor) {
314
315        float[] result = new float[2];
316        FontRenderContext frc = g2.getFontRenderContext();
317        LineMetrics metrics = g2.getFont().getLineMetrics(text, frc);
318        FontMetrics fm = g2.getFontMetrics();
319        Rectangle2D bounds = TextUtils.getTextBounds(text, fm);
320        float ascent = metrics.getAscent();
321        float halfAscent = ascent / 2.0f;
322        float descent = metrics.getDescent();
323        float leading = metrics.getLeading();
324        float xAdj = 0.0f;
325        float yAdj = 0.0f;
326
327        if (anchor.isLeft()) {
328            xAdj = 0.0f;
329        } else if (anchor.isHorizontalCenter()) {
330            xAdj = (float) bounds.getWidth() / 2.0f;
331        } else if (anchor.isRight()) {
332            xAdj = (float) bounds.getWidth();
333        }
334
335        if (anchor.isTop()) {
336            yAdj = descent + leading - (float) bounds.getHeight();
337        } else if (anchor.isHalfHeight()) {
338            yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
339        } else if (anchor.isHalfAscent()) {
340            yAdj = -halfAscent;
341        } else if (anchor.isBaseline()) {
342            yAdj = 0.0f;
343        } else if (anchor.isBottom()) {
344            yAdj = metrics.getDescent() + metrics.getLeading();
345        }
346        result[0] = xAdj;
347        result[1] = yAdj;
348        return result;
349    }
350    
351    /**
352     * A utility method for drawing rotated text.
353     * <P>
354     * A common rotation is {@code -Math.PI/2} which draws text 'vertically' 
355     * (with the top of the characters on the left).
356     *
357     * @param text  the text ({@code null} not permitted)
358     * @param g2  the graphics target ({@code null} not permitted).
359     * @param angle  the angle of the (clockwise) rotation (in radians).
360     * @param x  the x-coordinate.
361     * @param y  the y-coordinate.
362     * 
363     * @return The text bounds.
364     */
365    public static Shape drawRotatedString(String text, Graphics2D g2,
366            double angle, float x, float y) {
367        return drawRotatedString(text, g2, x, y, angle, x, y);
368    }
369
370    /**
371     * A utility method for drawing rotated text.
372     * <P>
373     * A common rotation is {@code -Math.PI/2} which draws text 'vertically' 
374     * (with the top of the characters on the left).
375     *
376     * @param text  the text ({@code null} not permitted).
377     * @param g2  the graphics device ({@code null} not permitted).
378     * @param textX  the x-coordinate for the text (before rotation).
379     * @param textY  the y-coordinate for the text (before rotation).
380     * @param angle  the angle of the (clockwise) rotation (in radians).
381     * @param rotateX  the point about which the text is rotated.
382     * @param rotateY  the point about which the text is rotated.
383     * 
384     * @return The bounds for the rotated text (never {@code null}).
385     */
386    public static Shape drawRotatedString(String text, Graphics2D g2,
387            float textX, float textY, double angle,
388            float rotateX, float rotateY) {
389        ArgChecks.nullNotPermitted(text, "text");
390        AffineTransform saved = g2.getTransform();
391        Rectangle2D rect = TextUtils.getTextBounds(text, textX, textY, 
392                g2.getFontMetrics());
393        AffineTransform rotate = AffineTransform.getRotateInstance(
394                angle, rotateX, rotateY);
395        Shape bounds = rotate.createTransformedShape(rect);
396        g2.transform(rotate);
397        g2.drawString(text, textX, textY);
398        g2.setTransform(saved);
399        return bounds;
400    }
401    
402    /**
403     * Draws the attributed string at {@code (x, y)}, rotated by the 
404     * specified angle about {@code (x, y)}.
405     * 
406     * @param text  the attributed string ({@code null} not permitted).
407     * @param g2  the graphics output target ({@code null} not permitted).
408     * @param angle  the angle.
409     * @param x  the x-coordinate.
410     * @param y  the y-coordinate.
411     * 
412     * @return The text bounds (never {@code null}).
413     * 
414     * @since 1.2
415     */
416    public static Shape drawRotatedString(AttributedString text, Graphics2D g2, 
417            double angle, float x, float y) {
418        return drawRotatedString(text, g2, x, y, angle, x, y);
419    }
420    
421    /**
422     * Draws the attributed string at {@code (textX, textY)}, rotated by 
423     * the specified angle about {@code (rotateX, rotateY)}.
424     * 
425     * @param text  the attributed string ({@code null} not permitted).
426     * @param g2  the graphics output target ({@code null} not permitted).
427     * @param textX  the x-coordinate for the text alignment point.
428     * @param textY  the y-coordinate for the text alignment point.
429     * @param angle  the rotation angle (in radians).
430     * @param rotateX  the x-coordinate for the rotation point.
431     * @param rotateY  the y-coordinate for the rotation point.
432     * 
433     * @return The text bounds (never {@code null}).
434     * 
435     * @since 1.2
436     */
437    public static Shape drawRotatedString(AttributedString text, Graphics2D g2, 
438            float textX, float textY, double angle, float rotateX, 
439            float rotateY) {
440        
441        ArgChecks.nullNotPermitted(text, "text");
442        AffineTransform saved = g2.getTransform();
443        AffineTransform rotate = AffineTransform.getRotateInstance(angle, 
444                rotateX, rotateY);
445        g2.transform(rotate);
446        TextLayout tl = new TextLayout(text.getIterator(),
447                    g2.getFontRenderContext());
448        Rectangle2D rect = tl.getBounds();
449        tl.draw(g2, textX, textY);
450        g2.setTransform(saved);
451        return rotate.createTransformedShape(rect);
452    }
453
454    /**
455     * Draws the attributed string aligned to the point {@code (x, y)}, 
456     * rotated by the specified angle about {@code rotationAnchor}.
457     * 
458     * @param text  the attributed string ({@code null} not permitted).
459     * @param g2  the graphics target ({@code null} not permitted).
460     * @param x  the x-coordinate.
461     * @param y  the y-coordinate.
462     * @param textAnchor  the text anchor ({@code null} not permitted).
463     * @param angle  the rotation angle (in radians).
464     * @param rotationAnchor  the rotation anchor ({@code null} not 
465     *     permitted).
466     * @param nonRotatedBounds  if not {@code null} this rectangle will 
467     *     be updated with the non-rotated bounds of the text for the caller
468     *     to use.
469     * 
470     * @return The text bounds (never {@code null}).
471     * 
472     * @since 1.2
473     */
474    public static Shape drawRotatedString(AttributedString text, Graphics2D g2,
475            float x, float y, TextAnchor textAnchor,
476            double angle, TextAnchor rotationAnchor, 
477            Rectangle2D nonRotatedBounds) {
478        ArgChecks.nullNotPermitted(text, "text");
479        float[] textAdj = deriveTextBoundsAnchorOffsets(g2, text, textAnchor, 
480                nonRotatedBounds);
481        float[] rotateAdj = deriveRotationAnchorOffsets(g2, text, 
482                rotationAnchor);
483        return drawRotatedString(text, g2, x + textAdj[0], y + textAdj[1],
484                angle, x + textAdj[0] + rotateAdj[0],
485                y + textAdj[1] + rotateAdj[1]);
486    }
487
488    /**
489     * Calculates the x and y offsets required to align the text with the
490     * specified {@code anchor}.
491     * 
492     * @param g2  the graphics target ({@code null} not permitted).
493     * @param text  the text ({@code null} not permitted).
494     * @param anchor  the anchor ({@code null} not permitted).
495     * @param textBounds  if not {@code null}, this rectangle will be
496     *     updated with the bounds of the text (for the caller to use).
497     * 
498     * @return An array of two floats dx and dy.
499     */
500    private static float[] deriveTextBoundsAnchorOffsets(Graphics2D g2,
501            AttributedString text, TextAnchor anchor, Rectangle2D textBounds) {
502
503        TextLayout layout = new TextLayout(text.getIterator(), 
504                g2.getFontRenderContext());
505        Rectangle2D bounds = layout.getBounds();
506
507        float[] result = new float[3];
508        float ascent = layout.getAscent();
509        result[2] = -ascent;
510        float halfAscent = ascent / 2.0f;
511        float descent = layout.getDescent();
512        float leading = layout.getLeading();
513        float xAdj = 0.0f;
514        float yAdj = 0.0f;
515        
516        if (anchor.isHorizontalCenter()) {
517            xAdj = (float) -bounds.getWidth() / 2.0f;
518        } else if (anchor.isRight()) {
519            xAdj = (float) -bounds.getWidth();
520        }
521
522        if (anchor.isTop()) {
523            yAdj = -descent - leading + (float) bounds.getHeight();
524        } else if (anchor.isHalfAscent()) {
525            yAdj = halfAscent;
526        } else if (anchor.isHalfHeight()) {
527            yAdj = -descent - leading + (float) (bounds.getHeight() / 2.0);
528        } else if (anchor.isBaseline()) {
529            yAdj = 0.0f;
530        } else if (anchor.isBottom()) {
531            yAdj = -descent - leading;
532        }
533        if (textBounds != null) {
534            textBounds.setRect(bounds);
535        }
536        result[0] = xAdj;
537        result[1] = yAdj;
538        return result;
539    }
540    
541    /**
542     * A utility method that calculates the rotation anchor offsets for a
543     * string.  These offsets are relative to the text starting coordinate
544     * ({@code BASELINE_LEFT}).
545     *
546     * @param g2  the graphics device ({@code null} not permitted).
547     * @param text  the text ({@code null} not permitted).
548     * @param anchor  the anchor point ({@code null} not permitted).
549     *
550     * @return  The offsets.
551     */
552    private static float[] deriveRotationAnchorOffsets(Graphics2D g2, 
553            AttributedString text, TextAnchor anchor) {
554
555        float[] result = new float[2];
556        
557        TextLayout layout = new TextLayout(text.getIterator(), 
558                g2.getFontRenderContext());
559        Rectangle2D bounds = layout.getBounds();
560        float ascent = layout.getAscent();
561        float halfAscent = ascent / 2.0f;
562        float descent = layout.getDescent();
563        float leading = layout.getLeading();
564        float xAdj = 0.0f;
565        float yAdj = 0.0f;
566
567        if (anchor.isLeft()) {
568            xAdj = 0.0f;
569        } else if (anchor.isHorizontalCenter()) {
570            xAdj = (float) bounds.getWidth() / 2.0f;
571        } else if (anchor.isRight()) {
572            xAdj = (float) bounds.getWidth();
573        }
574
575        if (anchor.isTop()) {
576            yAdj = descent + leading - (float) bounds.getHeight();
577        } else if (anchor.isHalfHeight()) {
578            yAdj = descent + leading - (float) (bounds.getHeight() / 2.0);
579        } else if (anchor.isHalfAscent()) {
580            yAdj = -halfAscent;
581        } else if (anchor.isBaseline()) {
582            yAdj = 0.0f;
583        } else if (anchor.isBottom()) {
584            yAdj = descent + leading;
585        }
586        result[0] = xAdj;
587        result[1] = yAdj;
588        return result;
589    }
590
591}
592
593