001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, 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-2008, 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     * 13-Nov-2008 : Fix NullPointerException when dataset is null - see bug
047     *               report 2275695 (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(g2, plotArea, dataArea, edge, state, 
273                    plotState);
274            state = drawCategoryLabels(g2, plotArea, dataArea, edge, state,
275                    plotState);
276            state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
277    
278            return state;
279    
280        }
281    
282        /**
283         * Draws the category labels and returns the updated axis state.
284         *
285         * @param g2  the graphics device (<code>null</code> not permitted).
286         * @param plotArea  the plot area (<code>null</code> not permitted).
287         * @param dataArea  the area inside the axes (<code>null</code> not
288         *                  permitted).
289         * @param edge  the axis location (<code>null</code> not permitted).
290         * @param state  the axis state (<code>null</code> not permitted).
291         * @param plotState  collects information about the plot (<code>null</code>
292         *                   permitted).
293         *
294         * @return The updated axis state (never <code>null</code>).
295         */
296        protected AxisState drawSubCategoryLabels(Graphics2D g2,
297                                                  Rectangle2D plotArea,
298                                                  Rectangle2D dataArea,
299                                                  RectangleEdge edge,
300                                                  AxisState state,
301                                                  PlotRenderingInfo plotState) {
302    
303            if (state == null) {
304                throw new IllegalArgumentException("Null 'state' argument.");
305            }
306    
307            g2.setFont(this.subLabelFont);
308            g2.setPaint(this.subLabelPaint);
309            CategoryPlot plot = (CategoryPlot) getPlot();
310            int categoryCount = 0;
311            CategoryDataset dataset = plot.getDataset();
312            if (dataset != null) {
313                categoryCount = dataset.getColumnCount();
314            }
315    
316            double maxdim = getMaxDim(g2, edge);
317            for (int categoryIndex = 0; categoryIndex < categoryCount;
318                 categoryIndex++) {
319    
320                double x0 = 0.0;
321                double x1 = 0.0;
322                double y0 = 0.0;
323                double y1 = 0.0;
324                if (edge == RectangleEdge.TOP) {
325                    x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
326                            edge);
327                    x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
328                            edge);
329                    y1 = state.getCursor();
330                    y0 = y1 - maxdim;
331                }
332                else if (edge == RectangleEdge.BOTTOM) {
333                    x0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
334                            edge);
335                    x1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
336                            edge);
337                    y0 = state.getCursor();
338                    y1 = y0 + maxdim;
339                }
340                else if (edge == RectangleEdge.LEFT) {
341                    y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
342                            edge);
343                    y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
344                            edge);
345                    x1 = state.getCursor();
346                    x0 = x1 - maxdim;
347                }
348                else if (edge == RectangleEdge.RIGHT) {
349                    y0 = getCategoryStart(categoryIndex, categoryCount, dataArea,
350                            edge);
351                    y1 = getCategoryEnd(categoryIndex, categoryCount, dataArea,
352                            edge);
353                    x0 = state.getCursor();
354                    x1 = x0 + maxdim;
355                }
356                Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0),
357                        (y1 - y0));
358                int subCategoryCount = this.subCategories.size();
359                float width = (float) ((x1 - x0) / subCategoryCount);
360                float height = (float) ((y1 - y0) / subCategoryCount);
361                float xx = 0.0f;
362                float yy = 0.0f;
363                for (int i = 0; i < subCategoryCount; i++) {
364                    if (RectangleEdge.isTopOrBottom(edge)) {
365                        xx = (float) (x0 + (i + 0.5) * width);
366                        yy = (float) area.getCenterY();
367                    }
368                    else {
369                        xx = (float) area.getCenterX();
370                        yy = (float) (y0 + (i + 0.5) * height);
371                    }
372                    String label = this.subCategories.get(i).toString();
373                    TextUtilities.drawRotatedString(label, g2, xx, yy,
374                            TextAnchor.CENTER, 0.0, TextAnchor.CENTER);
375                }
376            }
377    
378            if (edge.equals(RectangleEdge.TOP)) {
379                double h = maxdim;
380                state.cursorUp(h);
381            }
382            else if (edge.equals(RectangleEdge.BOTTOM)) {
383                double h = maxdim;
384                state.cursorDown(h);
385            }
386            else if (edge == RectangleEdge.LEFT) {
387                double w = maxdim;
388                state.cursorLeft(w);
389            }
390            else if (edge == RectangleEdge.RIGHT) {
391                double w = maxdim;
392                state.cursorRight(w);
393            }
394            return state;
395        }
396    
397        /**
398         * Tests the axis for equality with an arbitrary object.
399         *
400         * @param obj  the object (<code>null</code> permitted).
401         *
402         * @return A boolean.
403         */
404        public boolean equals(Object obj) {
405            if (obj == this) {
406                return true;
407            }
408            if (obj instanceof SubCategoryAxis && super.equals(obj)) {
409                SubCategoryAxis axis = (SubCategoryAxis) obj;
410                if (!this.subCategories.equals(axis.subCategories)) {
411                    return false;
412                }
413                if (!this.subLabelFont.equals(axis.subLabelFont)) {
414                    return false;
415                }
416                if (!this.subLabelPaint.equals(axis.subLabelPaint)) {
417                    return false;
418                }
419                return true;
420            }
421            return false;
422        }
423    
424        /**
425         * Provides serialization support.
426         *
427         * @param stream  the output stream.
428         *
429         * @throws IOException  if there is an I/O error.
430         */
431        private void writeObject(ObjectOutputStream stream) throws IOException {
432            stream.defaultWriteObject();
433            SerialUtilities.writePaint(this.subLabelPaint, stream);
434        }
435    
436        /**
437         * Provides serialization support.
438         *
439         * @param stream  the input stream.
440         *
441         * @throws IOException  if there is an I/O error.
442         * @throws ClassNotFoundException  if there is a classpath problem.
443         */
444        private void readObject(ObjectInputStream stream)
445            throws IOException, ClassNotFoundException {
446            stream.defaultReadObject();
447            this.subLabelPaint = SerialUtilities.readPaint(stream);
448        }
449    
450    }