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