001/* ========================================================================
002 * JCommon : a free general purpose class library for the Java(tm) platform
003 * ========================================================================
004 *
005 * (C) Copyright 2000-2005, 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 * TextBlock.java
029 * --------------
030 * (C) Copyright 2003, 2004, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * $Id: TextBlock.java,v 1.13 2005/10/18 13:17:16 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 07-Nov-2003 : Version 1 (DG);
040 * 22-Dec-2003 : Added workaround for Java bug 4245442 (DG);
041 * 09-Jan-2004 : Added an extra draw() method for no rotation case (DG);
042 * 25-Feb-2004 : Added getLines() method (DG);
043 * 22-Mar-2004 : Added equals() method and implemented Serializable (DG);
044 * 24-Mar-2004 : Added 'paint' argument to addLine() method (DG);
045 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D 
046 *               because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
047 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
048 *
049 */
050 
051package org.jfree.text;
052
053import java.awt.Font;
054import java.awt.Graphics2D;
055import java.awt.Paint;
056import java.awt.Shape;
057import java.awt.geom.Rectangle2D;
058import java.io.Serializable;
059import java.util.Collections;
060import java.util.Iterator;
061import java.util.List;
062
063import org.jfree.ui.HorizontalAlignment;
064import org.jfree.ui.Size2D;
065import org.jfree.ui.TextAnchor;
066import org.jfree.util.Log;
067import org.jfree.util.LogContext;
068import org.jfree.util.ShapeUtilities;
069
070/**
071 * A list of {@link TextLine} objects that form a block of text.
072 * 
073 * @see TextUtilities#createTextBlock(String, Font, Paint)
074 *
075 * @author David Gilbert
076 */
077public class TextBlock implements Serializable {
078
079    /** For serialization. */
080    private static final long serialVersionUID = -4333175719424385526L;
081    
082    /** Storage for the lines of text. */
083    private List lines;
084    
085    /** The alignment of the lines. */
086    private HorizontalAlignment lineAlignment;
087    
088    /** Access to logging facilities. */
089    protected static final LogContext logger 
090        = Log.createContext(TextBlock.class);
091
092    /**
093     * Creates a new empty text block.
094     */
095    public TextBlock() {
096        this.lines = new java.util.ArrayList();
097        this.lineAlignment = HorizontalAlignment.CENTER;
098    }
099    
100    /**
101     * Returns the alignment of the lines of text within the block.
102     * 
103     * @return The alignment (never <code>null</code>).
104     */
105    public HorizontalAlignment getLineAlignment() {
106        return this.lineAlignment;   
107    }
108    
109    /**
110     * Sets the alignment of the lines of text within the block.
111     * 
112     * @param alignment  the alignment (<code>null</code> not permitted).
113     */
114    public void setLineAlignment(HorizontalAlignment alignment) {
115        if (alignment == null) {
116            throw new IllegalArgumentException("Null 'alignment' argument.");
117        }
118        this.lineAlignment = alignment;   
119    }
120    
121    /**
122     * Adds a line of text that will be displayed using the specified font.
123     * 
124     * @param text  the text.
125     * @param font  the font.
126     * @param paint  the paint.
127     */
128    public void addLine(final String text, final Font font, final Paint paint) {
129        addLine(new TextLine(text, font, paint));
130    }
131    
132    /**
133     * Adds a {@link TextLine} to the block.
134     * 
135     * @param line  the line.
136     */
137    public void addLine(final TextLine line) {
138        this.lines.add(line);    
139    }
140    
141    /**
142     * Returns the last line in the block.
143     * 
144     * @return The last line in the block.
145     */
146    public TextLine getLastLine() {
147        TextLine last = null;
148        final int index = this.lines.size() - 1;
149        if (index >= 0) {
150            last = (TextLine) this.lines.get(index);
151        }
152        return last;
153    }
154    
155    /**
156     * Returns an unmodifiable list containing the lines for the text block.
157     *
158     * @return A list of {@link TextLine} objects.
159     */
160    public List getLines() {
161        return Collections.unmodifiableList(this.lines);
162    }
163    
164    /**
165     * Returns the width and height of the text block.
166     * 
167     * @param g2  the graphics device.
168     * 
169     * @return The width and height.
170     */
171    public Size2D calculateDimensions(final Graphics2D g2) {
172        double width = 0.0;
173        double height = 0.0;
174        final Iterator iterator = this.lines.iterator();
175        while (iterator.hasNext()) {
176            final TextLine line = (TextLine) iterator.next();
177            final Size2D dimension = line.calculateDimensions(g2);
178            width = Math.max(width, dimension.getWidth());
179            height = height + dimension.getHeight();
180        }
181        if (logger.isDebugEnabled()) {
182            logger.debug("width = " + width + ", height = " + height);   
183        }
184        return new Size2D(width, height);
185    }
186    
187    /**
188     * Returns the bounds of the text block.
189     * 
190     * @param g2  the graphics device (<code>null</code> not permitted).
191     * @param anchorX  the x-coordinate for the anchor point.
192     * @param anchorY  the y-coordinate for the anchor point.
193     * @param anchor  the text block anchor (<code>null</code> not permitted).
194     * @param rotateX  the x-coordinate for the rotation point.
195     * @param rotateY  the y-coordinate for the rotation point.
196     * @param angle  the rotation angle.
197     * 
198     * @return The bounds.
199     */
200    public Shape calculateBounds(final Graphics2D g2,
201                                 final float anchorX, final float anchorY, 
202                                 final TextBlockAnchor anchor,
203                                 final float rotateX, final float rotateY, 
204                                 final double angle) {
205        
206        final Size2D d = calculateDimensions(g2);
207        final float[] offsets = calculateOffsets(
208            anchor, d.getWidth(), d.getHeight()
209        );
210        final Rectangle2D bounds = new Rectangle2D.Double(
211            anchorX + offsets[0], anchorY + offsets[1], 
212            d.getWidth(), d.getHeight()
213        );
214        final Shape rotatedBounds = ShapeUtilities.rotateShape(
215            bounds, angle, rotateX, rotateY
216        );
217        return rotatedBounds;   
218        
219    }
220    
221    /**
222     * Draws the text block at a specific location.
223     * 
224     * @param g2  the graphics device.
225     * @param x  the x-coordinate for the anchor point.
226     * @param y  the y-coordinate for the anchor point.
227     * @param anchor  the anchor point.
228     */
229    public void draw(final Graphics2D g2, final float x, final float y, 
230                     final TextBlockAnchor anchor) {
231        draw(g2, x, y, anchor, 0.0f, 0.0f, 0.0);
232    }
233    
234    /**
235     * Draws the text block, aligning it with the specified anchor point and 
236     * rotating it about the specified rotation point.
237     * 
238     * @param g2  the graphics device.
239     * @param anchorX  the x-coordinate for the anchor point.
240     * @param anchorY  the y-coordinate for the anchor point.
241     * @param anchor  the point on the text block that is aligned to the 
242     *                anchor point.
243     * @param rotateX  the x-coordinate for the rotation point.
244     * @param rotateY  the x-coordinate for the rotation point.
245     * @param angle  the rotation (in radians).
246     */
247    public void draw(final Graphics2D g2,
248                     final float anchorX, final float anchorY, 
249                     final TextBlockAnchor anchor,
250                     final float rotateX, final float rotateY, 
251                     final double angle) {
252    
253        final Size2D d = calculateDimensions(g2);
254        final float[] offsets = calculateOffsets(anchor, d.getWidth(), 
255                d.getHeight());
256        final Iterator iterator = this.lines.iterator();
257        float yCursor = 0.0f;
258        while (iterator.hasNext()) {
259            TextLine line = (TextLine) iterator.next();
260            Size2D dimension = line.calculateDimensions(g2);
261            float lineOffset = 0.0f;
262            if (this.lineAlignment == HorizontalAlignment.CENTER) {
263                lineOffset = (float) (d.getWidth() - dimension.getWidth()) 
264                    / 2.0f;   
265            }
266            else if (this.lineAlignment == HorizontalAlignment.RIGHT) {
267                lineOffset = (float) (d.getWidth() - dimension.getWidth());   
268            }
269            line.draw(
270                g2, anchorX + offsets[0] + lineOffset, anchorY + offsets[1] + yCursor,
271                TextAnchor.TOP_LEFT, rotateX, rotateY, angle
272            );
273            yCursor = yCursor + (float) dimension.getHeight();
274        }
275        
276    }
277 
278    /**
279     * Calculates the x and y offsets required to align the text block with the
280     * specified anchor point.  This assumes that the top left of the text 
281     * block is at (0.0, 0.0).
282     * 
283     * @param anchor  the anchor position.
284     * @param width  the width of the text block.
285     * @param height  the height of the text block.
286     * 
287     * @return The offsets (float[0] = x offset, float[1] = y offset).
288     */
289    private float[] calculateOffsets(final TextBlockAnchor anchor, 
290                                     final double width, final double height) {
291        final float[] result = new float[2];
292        float xAdj = 0.0f;
293        float yAdj = 0.0f;
294
295        if (anchor == TextBlockAnchor.TOP_CENTER
296                || anchor == TextBlockAnchor.CENTER
297                || anchor == TextBlockAnchor.BOTTOM_CENTER) {
298                    
299            xAdj = (float) -width / 2.0f;
300            
301        }
302        else if (anchor == TextBlockAnchor.TOP_RIGHT
303                || anchor == TextBlockAnchor.CENTER_RIGHT
304                || anchor == TextBlockAnchor.BOTTOM_RIGHT) {
305                    
306            xAdj = (float) -width;
307            
308        }
309
310        if (anchor == TextBlockAnchor.TOP_LEFT
311                || anchor == TextBlockAnchor.TOP_CENTER
312                || anchor == TextBlockAnchor.TOP_RIGHT) {
313                    
314            yAdj = 0.0f;
315            
316        }
317        else if (anchor == TextBlockAnchor.CENTER_LEFT
318                || anchor == TextBlockAnchor.CENTER
319                || anchor == TextBlockAnchor.CENTER_RIGHT) {
320                    
321            yAdj = (float) -height / 2.0f;
322            
323        }
324        else if (anchor == TextBlockAnchor.BOTTOM_LEFT
325                || anchor == TextBlockAnchor.BOTTOM_CENTER
326                || anchor == TextBlockAnchor.BOTTOM_RIGHT) {
327                    
328            yAdj = (float) -height;
329            
330        }
331        result[0] = xAdj;
332        result[1] = yAdj;
333        return result;
334    }   
335    
336    /**
337     * Tests this object for equality with an arbitrary object.
338     * 
339     * @param obj  the object to test against (<code>null</code> permitted).
340     * 
341     * @return A boolean.
342     */
343    public boolean equals(final Object obj) {
344        if (obj == this) {
345            return true;   
346        }
347        if (obj instanceof TextBlock) {
348            final TextBlock block = (TextBlock) obj;
349            return this.lines.equals(block.lines);
350        }
351        return false;
352    }
353
354    /**
355     * Returns a hash code for this object.
356     * 
357     * @return A hash code.
358     */
359    public int hashCode() {
360        return (this.lines != null ? this.lines.hashCode() : 0);
361    }
362}