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    
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 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    }