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