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    
050    package org.jfree.text;
051    
052    import java.awt.Font;
053    import java.awt.Graphics2D;
054    import java.awt.Paint;
055    import java.io.Serializable;
056    import java.util.Iterator;
057    import java.util.List;
058    
059    import org.jfree.ui.Size2D;
060    import org.jfree.ui.TextAnchor;
061    import org.jfree.util.Log;
062    import 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     */
070    public 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    }