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