001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2006, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * --------------------
028     * SubCategoryAxis.java
029     * --------------------
030     * (C) Copyright 2004-2006, by Object Refinery Limited.
031     *
032     * Original Author:  David Gilbert;
033     * Contributor(s):   Adriaan Joubert;
034     *
035     * $Id: SubCategoryAxis.java,v 1.6.2.2 2006/08/18 14:48:31 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 12-May-2004 : Version 1 (DG);
040     * 30-Sep-2004 : Moved drawRotatedString() from RefineryUtilities 
041     *               --> TextUtilities (DG);
042     * 26-Apr-2005 : Removed logger (DG);
043     * ------------- JFREECHART 1.0.0 ---------------------------------------------
044     * 18-Aug-2006 : Fix for bug drawing category labels, thanks to Adriaan
045     *               Joubert (1277726) (DG);
046     *
047     */
048    
049    package org.jfree.chart.axis;
050    
051    import java.awt.Color;
052    import java.awt.Font;
053    import java.awt.FontMetrics;
054    import java.awt.Graphics2D;
055    import java.awt.Paint;
056    import java.awt.geom.Rectangle2D;
057    import java.io.IOException;
058    import java.io.ObjectInputStream;
059    import java.io.ObjectOutputStream;
060    import java.io.Serializable;
061    import java.util.Iterator;
062    import java.util.List;
063    
064    import org.jfree.chart.event.AxisChangeEvent;
065    import org.jfree.chart.plot.CategoryPlot;
066    import org.jfree.chart.plot.Plot;
067    import org.jfree.chart.plot.PlotRenderingInfo;
068    import org.jfree.data.category.CategoryDataset;
069    import org.jfree.io.SerialUtilities;
070    import org.jfree.text.TextUtilities;
071    import org.jfree.ui.RectangleEdge;
072    import org.jfree.ui.TextAnchor;
073    
074    /**
075     * A specialised category axis that can display sub-categories.
076     */
077    public class SubCategoryAxis extends CategoryAxis 
078                                 implements Cloneable, Serializable {
079        
080        /** For serialization. */
081        private static final long serialVersionUID = -1279463299793228344L;
082        
083        /** Storage for the sub-categories (these need to be set manually). */
084        private List subCategories;
085        
086        /** The font for the sub-category labels. */
087        private Font subLabelFont = new Font("SansSerif", Font.PLAIN, 10);
088        
089        /** The paint for the sub-category labels. */
090        private transient Paint subLabelPaint = Color.black;
091        
092        /**
093         * Creates a new axis.
094         * 
095         * @param label  the axis label.
096         */
097        public SubCategoryAxis(String label) {
098            super(label);
099            this.subCategories = new java.util.ArrayList();
100        }
101    
102        /**
103         * Adds a sub-category to the axis.
104         * 
105         * @param subCategory  the sub-category.
106         */
107        public void addSubCategory(Comparable subCategory) {
108            this.subCategories.add(subCategory);    
109        }
110        
111        /**
112         * Returns the font used to display the sub-category labels.
113         * 
114         * @return The font (never <code>null</code>).
115         */
116        public Font getSubLabelFont() {
117            return this.subLabelFont;   
118        }
119        
120        /**
121         * Sets the font used to display the sub-category labels and sends an 
122         * {@link AxisChangeEvent} to all registered listeners.
123         * 
124         * @param font  the font (<code>null</code> not permitted).
125         */
126        public void setSubLabelFont(Font font) {
127            if (font == null) {
128                throw new IllegalArgumentException("Null 'font' argument.");   
129            }
130            this.subLabelFont = font;
131            notifyListeners(new AxisChangeEvent(this));
132        }
133        
134        /**
135         * Returns the paint used to display the sub-category labels.
136         * 
137         * @return The paint (never <code>null</code>).
138         */
139        public Paint getSubLabelPaint() {
140            return this.subLabelPaint;   
141        }
142        
143        /**
144         * Sets the paint used to display the sub-category labels and sends an 
145         * {@link AxisChangeEvent} to all registered listeners.
146         * 
147         * @param paint  the paint (<code>null</code> not permitted).
148         */
149        public void setSubLabelPaint(Paint paint) {
150            if (paint == null) {
151                throw new IllegalArgumentException("Null 'paint' argument.");   
152            }
153            this.subLabelPaint = paint;
154            notifyListeners(new AxisChangeEvent(this));
155        }
156        
157        /**
158         * Estimates the space required for the axis, given a specific drawing area.
159         *
160         * @param g2  the graphics device (used to obtain font information).
161         * @param plot  the plot that the axis belongs to.
162         * @param plotArea  the area within which the axis should be drawn.
163         * @param edge  the axis location (top or bottom).
164         * @param space  the space already reserved.
165         *
166         * @return The space required to draw the axis.
167         */
168        public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 
169                                      Rectangle2D plotArea, 
170                                      RectangleEdge edge, AxisSpace space) {
171    
172            // create a new space object if one wasn't supplied...
173            if (space == null) {
174                space = new AxisSpace();
175            }
176            
177            // if the axis is not visible, no additional space is required...
178            if (!isVisible()) {
179                return space;
180            }
181    
182            space = super.reserveSpace(g2, plot, plotArea, edge, space);
183            double maxdim = getMaxDim(g2, edge);
184            if (RectangleEdge.isTopOrBottom(edge)) {
185                space.add(maxdim, edge);
186            }
187            else if (RectangleEdge.isLeftOrRight(edge)) {
188                space.add(maxdim, edge);
189            }
190            return space;
191        }
192        
193        /**
194         * Returns the maximum of the relevant dimension (height or width) of the 
195         * subcategory labels.
196         * 
197         * @param g2  the graphics device.
198         * @param edge  the edge.
199         * 
200         * @return The maximum dimension.
201         */
202        private double getMaxDim(Graphics2D g2, RectangleEdge edge) {
203            double result = 0.0;
204            g2.setFont(this.subLabelFont);
205            FontMetrics fm = g2.getFontMetrics();
206            Iterator iterator = this.subCategories.iterator();
207            while (iterator.hasNext()) {
208                Comparable subcategory = (Comparable) iterator.next();
209                String label = subcategory.toString();
210                Rectangle2D bounds = TextUtilities.getTextBounds(label, g2, fm);
211                double dim = 0.0;
212                if (RectangleEdge.isLeftOrRight(edge)) {
213                    dim = bounds.getWidth();   
214                }
215                else {  // must be top or bottom
216                    dim = bounds.getHeight();
217                }
218                result = Math.max(result, dim);
219            }   
220            return result;
221        }
222        
223        /**
224         * Draws the axis on a Java 2D graphics device (such as the screen or a 
225         * printer).
226         *
227         * @param g2  the graphics device (<code>null</code> not permitted).
228         * @param cursor  the cursor location.
229         * @param plotArea  the area within which the axis should be drawn 
230         *                  (<code>null</code> not permitted).
231         * @param dataArea  the area within which the plot is being drawn 
232         *                  (<code>null</code> not permitted).
233         * @param edge  the location of the axis (<code>null</code> not permitted).
234         * @param plotState  collects information about the plot 
235         *                   (<code>null</code> permitted).
236         * 
237         * @return The axis state (never <code>null</code>).
238         */
239        public AxisState draw(Graphics2D g2, 
240                              double cursor, 
241                              Rectangle2D plotArea, 
242                              Rectangle2D dataArea,
243                              RectangleEdge edge,
244                              PlotRenderingInfo plotState) {
245            
246            // if the axis is not visible, don't draw it...
247            if (!isVisible()) {
248                return new AxisState(cursor);
249            }
250            
251            if (isAxisLineVisible()) {
252                drawAxisLine(g2, cursor, dataArea, edge);
253            }
254    
255            // draw the category labels and axis label
256            AxisState state = new AxisState(cursor);
257            state = drawSubCategoryLabels(
258                g2, plotArea, dataArea, edge, state, plotState
259            );
260            state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 
261                    plotState);
262            state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
263        
264            return state;
265    
266        }
267        
268        /**
269         * Draws the category labels and returns the updated axis state.
270         *
271         * @param g2  the graphics device (<code>null</code> not permitted).
272         * @param plotArea  the plot area (<code>null</code> not permitted).
273         * @param dataArea  the area inside the axes (<code>null</code> not 
274         *                  permitted).
275         * @param edge  the axis location (<code>null</code> not permitted).
276         * @param state  the axis state (<code>null</code> not permitted).
277         * @param plotState  collects information about the plot (<code>null</code> 
278         *                   permitted).
279         * 
280         * @return The updated axis state (never <code>null</code>).
281         */
282        protected AxisState drawSubCategoryLabels(Graphics2D g2,
283                                                  Rectangle2D plotArea,
284                                                  Rectangle2D dataArea,
285                                                  RectangleEdge edge,
286                                                  AxisState state,
287                                                  PlotRenderingInfo plotState) {
288    
289            if (state == null) {
290                throw new IllegalArgumentException("Null 'state' argument.");
291            }
292    
293            g2.setFont(this.subLabelFont);
294            g2.setPaint(this.subLabelPaint);
295            CategoryPlot plot = (CategoryPlot) getPlot();
296            CategoryDataset dataset = plot.getDataset();
297            int categoryCount = dataset.getColumnCount();
298    
299            double maxdim = getMaxDim(g2, edge);
300            for (int categoryIndex = 0; categoryIndex < categoryCount; 
301                 categoryIndex++) {
302    
303                double x0 = 0.0;
304                double x1 = 0.0;
305                double y0 = 0.0;
306                double y1 = 0.0;
307                if (edge == RectangleEdge.TOP) {
308                    x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
309                            edge);
310                    x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
311                            edge);
312                    y1 = state.getCursor();
313                    y0 = y1 - maxdim;
314                }
315                else if (edge == RectangleEdge.BOTTOM) {
316                    x0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
317                            edge);
318                    x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
319                            edge); 
320                    y0 = state.getCursor();                   
321                    y1 = y0 + maxdim;
322                }
323                else if (edge == RectangleEdge.LEFT) {
324                    y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
325                            edge);
326                    y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
327                            edge);
328                    x1 = state.getCursor();
329                    x0 = x1 - maxdim;
330                }
331                else if (edge == RectangleEdge.RIGHT) {
332                    y0 = getCategoryStart(categoryIndex, categoryCount, dataArea, 
333                            edge);
334                    y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea, 
335                            edge);
336                    x0 = state.getCursor();
337                    x1 = x0 + maxdim;
338                }
339                Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 
340                        (y1 - y0));
341                int subCategoryCount = this.subCategories.size();
342                float width = (float) ((x1 - x0) / subCategoryCount);
343                float height = (float) ((y1 - y0) / subCategoryCount);
344                float xx = 0.0f;
345                float yy = 0.0f;
346                for (int i = 0; i < subCategoryCount; i++) {
347                    if (RectangleEdge.isTopOrBottom(edge)) {
348                        xx = (float) (x0 + (i + 0.5) * width);
349                        yy = (float) area.getCenterY();
350                    }
351                    else {
352                        xx = (float) area.getCenterX();
353                        yy = (float) (y0 + (i + 0.5) * height);                   
354                    }
355                    String label = this.subCategories.get(i).toString();
356                    TextUtilities.drawRotatedString(label, g2, xx, yy, 
357                            TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
358                }
359            }
360    
361            if (edge.equals(RectangleEdge.TOP)) {
362                double h = maxdim;
363                state.cursorUp(h);
364            }
365            else if (edge.equals(RectangleEdge.BOTTOM)) {
366                double h = maxdim;
367                state.cursorDown(h);
368            }
369            else if (edge == RectangleEdge.LEFT) {
370                double w = maxdim;
371                state.cursorLeft(w);
372            }
373            else if (edge == RectangleEdge.RIGHT) {
374                double w = maxdim;
375                state.cursorRight(w);
376            }
377            return state;
378        }
379        
380        /**
381         * Tests the axis for equality with an arbitrary object.
382         * 
383         * @param obj  the object (<code>null</code> permitted).
384         * 
385         * @return A boolean.
386         */
387        public boolean equals(Object obj) {
388            if (obj == this) {
389                return true;
390            }
391            if (obj instanceof SubCategoryAxis && super.equals(obj)) {
392                SubCategoryAxis axis = (SubCategoryAxis) obj;
393                if (!this.subCategories.equals(axis.subCategories)) {
394                    return false;
395                }
396                if (!this.subLabelFont.equals(axis.subLabelFont)) {
397                    return false;   
398                }
399                if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
400                    return false;   
401                }
402                return true;
403            }
404            return false;        
405        }
406        
407        /**
408         * Provides serialization support.
409         *
410         * @param stream  the output stream.
411         *
412         * @throws IOException  if there is an I/O error.
413         */
414        private void writeObject(ObjectOutputStream stream) throws IOException {
415            stream.defaultWriteObject();
416            SerialUtilities.writePaint(this.subLabelPaint, stream);
417        }
418    
419        /**
420         * Provides serialization support.
421         *
422         * @param stream  the input stream.
423         *
424         * @throws IOException  if there is an I/O error.
425         * @throws ClassNotFoundException  if there is a classpath problem.
426         */
427        private void readObject(ObjectInputStream stream) 
428            throws IOException, ClassNotFoundException {
429            stream.defaultReadObject();
430            this.subLabelPaint = SerialUtilities.readPaint(stream);
431        }
432      
433    }