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