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 * TextLine.java
029 * -------------
030 * (C) Copyright 2003-2005, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   -;
034 *
035 * $Id: TextLine.java,v 1.11 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 * 29-Jan-2004 : Added new constructor (DG);
042 * 22-Mar-2004 : Added equals() method and implemented Serializable (DG);
043 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D 
044 *               because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
045 * 03-Sep-2004 : Added a method to remove a fragment (DG);
046 * 08-Jul-2005 : Fixed bug in calculateBaselineOffset() (DG);
047 *
048 */
049
050package org.jfree.text;
051
052import java.awt.Font;
053import java.awt.Graphics2D;
054import java.awt.Paint;
055import java.io.Serializable;
056import java.util.Iterator;
057import java.util.List;
058
059import org.jfree.ui.Size2D;
060import org.jfree.ui.TextAnchor;
061import org.jfree.util.Log;
062import org.jfree.util.LogContext;
063
064/**
065 * A sequence of {@link TextFragment} objects that together form a line of 
066 * text.  A sequence of text lines is managed by the {@link TextBlock} class.
067 *
068 * @author David Gilbert
069 */
070public class TextLine implements Serializable {
071
072    /** For serialization. */
073    private static final long serialVersionUID = 7100085690160465444L;
074    
075    /** Storage for the text fragments that make up the line. */
076    private List fragments;
077    
078    /** Access to logging facilities. */
079    protected static final LogContext logger 
080        = Log.createContext(TextLine.class);
081
082    /**
083     * Creates a new empty line.
084     */
085    public TextLine() {
086        this.fragments = new java.util.ArrayList();
087    }
088    
089    /**
090     * Creates a new text line using the default font.
091     * 
092     * @param text  the text (<code>null</code> not permitted).
093     */
094    public TextLine(final String text) {
095        this(text, TextFragment.DEFAULT_FONT);   
096    }
097    
098    /**
099     * Creates a new text line.
100     * 
101     * @param text  the text (<code>null</code> not permitted).
102     * @param font  the text font (<code>null</code> not permitted).
103     */
104    public TextLine(final String text, final Font font) {
105        this.fragments = new java.util.ArrayList();
106        final TextFragment fragment = new TextFragment(text, font);
107        this.fragments.add(fragment);
108    }
109    
110    /**
111     * Creates a new text line.
112     * 
113     * @param text  the text (<code>null</code> not permitted).
114     * @param font  the text font (<code>null</code> not permitted).
115     * @param paint  the text color (<code>null</code> not permitted).
116     */
117    public TextLine(final String text, final Font font, final Paint paint) {
118        if (text == null) {
119            throw new IllegalArgumentException("Null 'text' argument.");   
120        }
121        if (font == null) {
122            throw new IllegalArgumentException("Null 'font' argument.");   
123        }
124        if (paint == null) {
125            throw new IllegalArgumentException("Null 'paint' argument.");   
126        }
127        this.fragments = new java.util.ArrayList();
128        final TextFragment fragment = new TextFragment(text, font, paint);
129        this.fragments.add(fragment);
130    }
131    
132    /**
133     * Adds a text fragment to the text line.
134     * 
135     * @param fragment  the text fragment (<code>null</code> not permitted).
136     */
137    public void addFragment(final TextFragment fragment) {
138        this.fragments.add(fragment);        
139    }
140    
141    /**
142     * Removes a fragment from the line.
143     * 
144     * @param fragment  the fragment to remove.
145     */
146    public void removeFragment(final TextFragment fragment) {
147        this.fragments.remove(fragment);
148    }
149    
150    /**
151     * Draws the text line.
152     * 
153     * @param g2  the graphics device.
154     * @param anchorX  the x-coordinate for the anchor point.
155     * @param anchorY  the y-coordinate for the anchor point.
156     * @param anchor  the point on the text line that is aligned to the anchor 
157     *                point.
158     * @param rotateX  the x-coordinate for the rotation point.
159     * @param rotateY  the y-coordinate for the rotation point.
160     * @param angle  the rotation angle (in radians).
161     */
162    public void draw(final Graphics2D g2,
163                     final float anchorX, final float anchorY, 
164                     final TextAnchor anchor,
165                     final float rotateX, final float rotateY, 
166                     final double angle) {
167    
168        float x = anchorX;
169        final float yOffset = calculateBaselineOffset(g2, anchor);
170        final Iterator iterator = this.fragments.iterator();
171        while (iterator.hasNext()) {
172            final TextFragment fragment = (TextFragment) iterator.next();
173            final Size2D d = fragment.calculateDimensions(g2);
174            fragment.draw(
175                g2, x, anchorY + yOffset, TextAnchor.BASELINE_LEFT, 
176                rotateX, rotateY, angle
177            );
178            x = x + (float) d.getWidth();
179        }
180    
181    }
182    
183    /**
184     * Calculates the width and height of the text line.
185     * 
186     * @param g2  the graphics device.
187     * 
188     * @return The width and height.
189     */
190    public Size2D calculateDimensions(final Graphics2D g2) {
191        double width = 0.0;
192        double height = 0.0;
193        final Iterator iterator = this.fragments.iterator();
194        while (iterator.hasNext()) {
195            final TextFragment fragment = (TextFragment) iterator.next();
196            final Size2D dimension = fragment.calculateDimensions(g2);
197            width = width + dimension.getWidth();
198            height = Math.max(height, dimension.getHeight());
199            if (logger.isDebugEnabled()) {
200                logger.debug("width = " + width + ", height = " + height);   
201            }
202        }
203        return new Size2D(width, height);
204    }
205    
206    /**
207     * Returns the first text fragment in the line.
208     * 
209     * @return The first text fragment in the line.
210     */
211    public TextFragment getFirstTextFragment() {
212        TextFragment result = null;
213        if (this.fragments.size() > 0) {
214            result = (TextFragment) this.fragments.get(0);
215        }    
216        return result;
217    }
218    
219    /**
220     * Returns the last text fragment in the line.
221     * 
222     * @return The last text fragment in the line.
223     */
224    public TextFragment getLastTextFragment() {
225        TextFragment result = null;
226        if (this.fragments.size() > 0) {
227            result = (TextFragment) this.fragments.get(this.fragments.size() 
228                    - 1);
229        }    
230        return result;
231    }
232    
233    /**
234     * Calculate the offsets required to translate from the specified anchor 
235     * position to the left baseline position.
236     * 
237     * @param g2  the graphics device.
238     * @param anchor  the anchor position.
239     * 
240     * @return The offsets.
241     */
242    private float calculateBaselineOffset(final Graphics2D g2, 
243                                          final TextAnchor anchor) {
244        float result = 0.0f;
245        Iterator iterator = this.fragments.iterator();
246        while (iterator.hasNext()) {
247            TextFragment fragment = (TextFragment) iterator.next();
248            result = Math.max(result, 
249                    fragment.calculateBaselineOffset(g2, anchor));
250        }
251        return result;
252    }
253    
254    /**
255     * Tests this object for equality with an arbitrary object.
256     * 
257     * @param obj  the object to test against (<code>null</code> permitted).
258     * 
259     * @return A boolean.
260     */
261    public boolean equals(final Object obj) {
262        if (obj == null) {
263            return false;
264        }
265        if (obj == this) {
266            return true;   
267        }
268        if (obj instanceof TextLine) {
269            final TextLine line = (TextLine) obj;
270            return this.fragments.equals(line.fragments);
271        }
272        return false;
273    }
274
275    /**
276     * Returns a hash code for this object.
277     * 
278     * @return A hash code.
279     */
280    public int hashCode() {
281        return (this.fragments != null ? this.fragments.hashCode() : 0);
282    }
283
284}