/* ===========================================================

 * JFreeChart : a free chart library for the Java(tm) platform

 * ===========================================================

 *

 * (C) Copyright 2000-2004, by Object Refinery Limited and Contributors.

 *

 * Project Info:  http://www.jfree.org/jfreechart/index.html

 *

 * This library is free software; you can redistribute it and/or modify it under the terms

 * of the GNU Lesser General Public License as published by the Free Software Foundation;

 * either version 2.1 of the License, or (at your option) any later version.

 *

 * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;

 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 * See the GNU Lesser General Public License for more details.

 *

 * You should have received a copy of the GNU Lesser General Public License along with this

 * library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,

 * Boston, MA 02111-1307, USA.

 *

 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 

 * in the United States and other countries.]

 *

 * -----------------

 * CategoryAxis.java

 * -----------------

 * (C) Copyright 2000-2004, by Object Refinery Limited.

 *

 * Original Author:  David Gilbert;

 * Contributor(s):   -;

 *

 * $Id: CategoryAxis.java,v 1.2 2004/09/03 15:58:18 mungady Exp $

 *

 * Changes (from 21-Aug-2001)

 * --------------------------

 * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);

 * 18-Sep-2001 : Updated header (DG);

 * 04-Dec-2001 : Changed constructors to protected, and tidied up default values (DG);

 * 19-Apr-2002 : Updated import statements (DG);

 * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);

 * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);

 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);

 * 22-Jan-2002 : Removed monolithic constructor (DG);

 * 26-Mar-2003 : Implemented Serializable (DG);

 * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into this class (DG);

 * 13-Aug-2003 : Implemented Cloneable (DG);

 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);

 * 05-Nov-2003 : Fixed serialization bug (DG);

 * 26-Nov-2003 : Added category label offset (DG);

 * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised category label 

 *               position attributes (DG);

 * 07-Jan-2004 : Added new implementation for linewrapping of category labels (DG);

 * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);

 * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);

 * 16-Mar-2004 : Added support for tooltips on category labels (DG);

 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D because of 

 *               JDK bug 4976448 which persists on JDK 1.3.1 (DG);

 * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);

 *

 */



package org.jfree.chart.axis;



import java.awt.Graphics2D;

import java.awt.Insets;

import java.awt.Shape;

import java.awt.geom.Rectangle2D;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;



import org.jfree.chart.entity.EntityCollection;

import org.jfree.chart.entity.TickLabelEntity;

import org.jfree.chart.event.AxisChangeEvent;

import org.jfree.chart.plot.CategoryPlot;

import org.jfree.chart.plot.Plot;

import org.jfree.chart.plot.PlotRenderingInfo;

import org.jfree.text.G2TextMeasurer;

import org.jfree.text.TextBlock;

import org.jfree.text.TextUtilities;

import org.jfree.ui.RectangleAnchor;

import org.jfree.ui.RectangleEdge;

import org.jfree.ui.Size2D;

import org.jfree.util.Log;

import org.jfree.util.LogContext;

import org.jfree.util.ObjectUtils;

import org.jfree.util.ShapeUtils;



/**

 * An axis that displays categories.

 */

public class CategoryAxis extends Axis implements Cloneable, Serializable {



    /** The default margin for the axis (used for both lower and upper margins). */

    public static final double DEFAULT_AXIS_MARGIN = 0.05;



    /** The default margin between categories (a percentage of the overall axis length). */

    public static final double DEFAULT_CATEGORY_MARGIN = 0.20;



    /** The amount of space reserved at the start of the axis. */

    private double lowerMargin;



    /** The amount of space reserved at the end of the axis. */

    private double upperMargin;



    /** The amount of space reserved between categories. */

    private double categoryMargin;

    

    /** The maximum number of lines for category labels. */

    private int maxCategoryLabelLines;



    /** 

     * A ratio that is multiplied by the width of one category to determine the 

     * maximum label width. 

     */

    private float maxCategoryLabelWidthRatio;

    

    /** The category label offset. */

    private int categoryLabelPositionOffset; 

    

    /** A structure defining the category label positions for each axis location. */

    private CategoryLabelPositions categoryLabelPositions;

    

    /** Storage for the category label tooltips (if any). */

    private Map categoryLabelToolTips;

    

    /** Access to logging facilities. */

    protected static final LogContext logger = Log.createContext(CategoryAxis.class);



    /**

     * Creates a new category axis with no label.

     */

    public CategoryAxis() {

        this(null);    

    }

    

    /**

     * Constructs a category axis, using default values where necessary.

     *

     * @param label  the axis label (<code>null</code> permitted).

     */

    public CategoryAxis(String label) {



        super(label);



        this.lowerMargin = DEFAULT_AXIS_MARGIN;

        this.upperMargin = DEFAULT_AXIS_MARGIN;

        this.categoryMargin = DEFAULT_CATEGORY_MARGIN;

        this.maxCategoryLabelLines = 1;

        this.maxCategoryLabelWidthRatio = 0.0f;

        

        setTickMarksVisible(false);  // not supported by this axis type yet

        

        this.categoryLabelPositionOffset = 4;

        this.categoryLabelPositions = CategoryLabelPositions.STANDARD;

        this.categoryLabelToolTips = new HashMap();

        

    }



    /**

     * Returns the lower margin for the axis.

     *

     * @return the margin.

     */

    public double getLowerMargin() {

        return this.lowerMargin;

    }



    /**

     * Sets the lower margin for the axis and sends an {@link AxisChangeEvent} to all registered

     * listeners.

     *

     * @param margin  the margin as a percentage of the axis length (for example, 0.05 is five 

     *                percent).

     */

    public void setLowerMargin(double margin) {

        this.lowerMargin = margin;

        notifyListeners(new AxisChangeEvent(this));

    }



    /**

     * Returns the upper margin for the axis.

     *

     * @return the margin.

     */

    public double getUpperMargin() {

        return this.upperMargin;

    }



    /**

     * Sets the upper margin for the axis and sends an {@link AxisChangeEvent} to all registered

     * listeners.

     *

     * @param margin  the margin as a percentage of the axis length (for example, 0.05 is five 

     *                percent).

     */

    public void setUpperMargin(double margin) {

        this.upperMargin = margin;

        notifyListeners(new AxisChangeEvent(this));

    }



    /**

     * Returns the category margin.

     *

     * @return the margin.

     */

    public double getCategoryMargin() {

        return this.categoryMargin;

    }



    /**

     * Sets the category margin and sends an {@link AxisChangeEvent} to all registered

     * listeners.

     * <P>

     * The overall category margin is distributed over N-1 gaps, where N is the number of 

     * categories on the axis.

     *

     * @param margin  the margin as a percentage of the axis length (for example, 0.05 is five 

     *                percent).

     */

    public void setCategoryMargin(double margin) {

        this.categoryMargin = margin;

        notifyListeners(new AxisChangeEvent(this));

    }



    /**

     * Returns the maximum number of lines to use for each category label.

     * 

     * @return The maximum number of lines.

     */

    public int getMaxCategoryLabelLines() {

        return this.maxCategoryLabelLines;

    }

    

    /**

     * Sets the maximum number of lines to use for each category label.

     * 

     * @param lines  the maximum number of lines.

     */

    public void setMaxCategoryLabelLines(int lines) {

        this.maxCategoryLabelLines = lines;

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the category label width ratio.

     * 

     * @return the ratio.

     */

    public float getMaxCategoryLabelWidthRatio() {

        return this.maxCategoryLabelWidthRatio;

    }

    

    /**

     * Sets the maximum category label width ratio and sends an {@link AxisChangeEvent} to

     * all registered listeners.

     * 

     * @param ratio  the ratio.

     */

    public void setMaxCategoryLabelWidthRatio(float ratio) {

        this.maxCategoryLabelWidthRatio = ratio;

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the offset between the axis and the category labels (before label positioning 

     * is taken into account).

     * 

     * @return the offset (in Java2D units).

     */

    public int getCategoryLabelPositionOffset() {

        return this.categoryLabelPositionOffset;

    }

    

    /**

     * Sets the offset between the axis and the category labels (before label positioning 

     * is taken into account).

     * 

     * @param offset  the offset (in Java2D units).

     */

    public void setCategoryLabelPositionOffset(int offset) {

        this.categoryLabelPositionOffset = offset;

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the category label position specification (this contains label positioning info 

     * for all four possible axis locations).

     * 

     * @return the positions (never <code>null</code>).

     */

    public CategoryLabelPositions getCategoryLabelPositions() {

        return this.categoryLabelPositions;

    }

    

    /**

     * Sets the category label position specification for the axis and sends an 

     * {@link AxisChangeEvent} to all registered listeners.

     * 

     * @param positions  the positions (<code>null</code> not permitted).

     */

    public void setCategoryLabelPositions(CategoryLabelPositions positions) {

        if (positions == null) {

            throw new IllegalArgumentException("Null 'positions' argument.");   

        }

        this.categoryLabelPositions = positions;

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Adds a tooltip to the specified category and sends an {@link AxisChangeEvent} to 

     * all registered listeners.

     * 

     * @param category  the category (<code>null<code> not permitted).

     * @param tooltip  the tooltip text (<code>null</code> permitted).

     */

    public void addCategoryLabelToolTip(Comparable category, String tooltip) {

        if (category == null) {

            throw new IllegalArgumentException("Null 'category' argument.");   

        }

        this.categoryLabelToolTips.put(category, tooltip);

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Removes the tooltip for the specified category and sends an {@link AxisChangeEvent} to 

     * all registered listeners.

     * 

     * @param category  the category (<code>null<code> not permitted).

     */

    public void removeCategoryLabelToolTip(Comparable category) {

        if (category == null) {

            throw new IllegalArgumentException("Null 'category' argument.");   

        }

        this.categoryLabelToolTips.remove(category);   

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Clears the category label tooltips and sends an {@link AxisChangeEvent} to all registered

     * listeners.

     */

    public void clearCategoryLabelToolTips() {

        this.categoryLabelToolTips.clear();

        notifyListeners(new AxisChangeEvent(this));

    }

    

    /**

     * Returns the Java 2D coordinate for a category.

     * 

     * @param anchor  the anchor point.

     * @param category  the category index.

     * @param categoryCount  the category count.

     * @param area  the data area.

     * @param edge  the location of the axis.

     * 

     * @return the coordinate.

     */

    public double getCategoryJava2DCoordinate(CategoryAnchor anchor, 

                                              int category, 

                                              int categoryCount, 

                                              Rectangle2D area,

                                              RectangleEdge edge) {

    

        double result = 0.0;

        if (anchor == CategoryAnchor.START) {

            result = getCategoryStart(category, categoryCount, area, edge);

        }

        else if (anchor == CategoryAnchor.MIDDLE) {

            result = getCategoryMiddle(category, categoryCount, area, edge);

        }

        else if (anchor == CategoryAnchor.END) {

            result = getCategoryEnd(category, categoryCount, area, edge);

        }

        return result;

                                                      

    }

                                              

    /**

     * Returns the starting coordinate for the specified category.

     *

     * @param category  the category.

     * @param categoryCount  the number of categories.

     * @param area  the data area.

     * @param edge  the axis location.

     *

     * @return the coordinate.

     */

    public double getCategoryStart(int category, int categoryCount, Rectangle2D area,

                                   RectangleEdge edge) {



        double result = 0.0;

        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {

            result = area.getX() + area.getWidth() * getLowerMargin();

        }

        else if ((edge == RectangleEdge.LEFT) || (edge == RectangleEdge.RIGHT)) {

            result = area.getMinY() + area.getHeight() * getLowerMargin();

        }



        double categorySize = calculateCategorySize(categoryCount, area, edge);

        double categoryGapWidth = calculateCategoryGapSize(categoryCount, area, edge);



        result = result + category * (categorySize + categoryGapWidth);



        return result;

    }



    /**

     * Returns the middle coordinate for the specified category.

     *

     * @param category  the category.

     * @param categoryCount  the number of categories.

     * @param area  the data area.

     * @param edge  the axis location.

     *

     * @return the coordinate.

     */

    public double getCategoryMiddle(int category, int categoryCount, Rectangle2D area,

                                    RectangleEdge edge) {



        return getCategoryStart(category, categoryCount, area, edge)

               + calculateCategorySize(categoryCount, area, edge) / 2;



    }



    /**

     * Returns the end coordinate for the specified category.

     *

     * @param category  the category.

     * @param categoryCount  the number of categories.

     * @param area  the data area.

     * @param edge  the axis location.

     *

     * @return the coordinate.

     */

    public double getCategoryEnd(int category, int categoryCount, Rectangle2D area,

                                 RectangleEdge edge) {



        return getCategoryStart(category, categoryCount, area, edge)

               + calculateCategorySize(categoryCount, area, edge);



    }



    /**

     * Calculates the size (width or height, depending on the location of the axis) of a category.

     *

     * @param categoryCount  the number of categories.

     * @param area  the area within which the categories will be drawn.

     * @param edge  the axis location.

     *

     * @return the category size.

     */

    protected double calculateCategorySize(int categoryCount, Rectangle2D area,

                                           RectangleEdge edge) {



        double result = 0.0;

        double available = 0.0;



        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {

            available = area.getWidth();

        }

        else if ((edge == RectangleEdge.LEFT) || (edge == RectangleEdge.RIGHT)) {

            available = area.getHeight();

        }

        if (categoryCount > 1) {

            result = available * (1 - getLowerMargin() - getUpperMargin() - getCategoryMargin());

            result = result / categoryCount;

        }

        else {

            result = available * (1 - getLowerMargin() - getUpperMargin());

        }

        return result;



    }



    /**

     * Calculates the size (width or height, depending on the location of the axis) of a category

     * gap.

     *

     * @param categoryCount  the number of categories.

     * @param area  the area within which the categories will be drawn.

     * @param edge  the axis location.

     *

     * @return the category gap width.

     */

    protected double calculateCategoryGapSize(int categoryCount, Rectangle2D area,

                                              RectangleEdge edge) {



        double result = 0.0;

        double available = 0.0;



        if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {

            available = area.getWidth();

        }

        else if ((edge == RectangleEdge.LEFT) || (edge == RectangleEdge.RIGHT)) {

            available = area.getHeight();

        }



        if (categoryCount > 1) {

            result = available * getCategoryMargin() / (categoryCount - 1);

        }



        return result;



    }



    /**

     * Estimates the space required for the axis, given a specific drawing area.

     *

     * @param g2  the graphics device (used to obtain font information).

     * @param plot  the plot that the axis belongs to.

     * @param plotArea  the area within which the axis should be drawn.

     * @param edge  the axis location (top or bottom).

     * @param space  the space already reserved.

     *

     * @return The space required to draw the axis.

     */

    public AxisSpace reserveSpace(Graphics2D g2, Plot plot, Rectangle2D plotArea, 

                                  RectangleEdge edge, AxisSpace space) {



        // create a new space object if one wasn't supplied...

        if (space == null) {

            space = new AxisSpace();

        }

        

        // if the axis is not visible, no additional space is required...

        if (!isVisible()) {

            return space;

        }



        // calculate the max size of the tick labels (if visible)...

        double tickLabelHeight = 0.0;

        double tickLabelWidth = 0.0;

        if (isTickLabelsVisible()) {

            g2.setFont(getTickLabelFont());

            AxisState state = new AxisState();

            refreshTicks(g2, state, plotArea, plotArea, edge);

            if (edge == RectangleEdge.TOP) {

                tickLabelHeight = state.getMax();

            }

            else if (edge == RectangleEdge.BOTTOM) {

                tickLabelHeight = state.getMax();

            }

            else if (edge == RectangleEdge.LEFT) {

                tickLabelWidth = state.getMax(); 

            }

            else if (edge == RectangleEdge.RIGHT) {

                tickLabelWidth = state.getMax(); 

            }

        }

        

        // get the axis label size and update the space object...

        Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);

        double labelHeight = 0.0;

        double labelWidth = 0.0;

        if (RectangleEdge.isTopOrBottom(edge)) {

            labelHeight = labelEnclosure.getHeight();

            space.add(labelHeight + tickLabelHeight + this.categoryLabelPositionOffset, edge);

        }

        else if (RectangleEdge.isLeftOrRight(edge)) {

            labelWidth = labelEnclosure.getWidth();

            space.add(labelWidth + tickLabelWidth + this.categoryLabelPositionOffset, edge);

        }



        return space;



    }



    /**

     * Configures the axis against the current plot.

     */

    public void configure() {

        // nothing required

    }



    /**

     * Draws the axis on a Java 2D graphics device (such as the screen or a printer).

     *

     * @param g2  the graphics device (<code>null</code> not permitted).

     * @param cursor  the cursor location.

     * @param plotArea  the area within which the axis should be drawn (<code>null</code> not 

     *                  permitted).

     * @param dataArea  the area within which the plot is being drawn (<code>null</code> not 

     *                  permitted).

     * @param edge  the location of the axis (<code>null</code> not permitted).

     * @param plotState  collects information about the plot (<code>null</code> permitted).

     * 

     * @return the axis state (never <code>null</code>).

     */

    public AxisState draw(Graphics2D g2, 

                          double cursor, 

                          Rectangle2D plotArea, 

                          Rectangle2D dataArea,

                          RectangleEdge edge,

                          PlotRenderingInfo plotState) {

        

        if (logger.isDebugEnabled()) {

            logger.debug("Entering draw() method, cursor = " + cursor);

        }

        

        // if the axis is not visible, don't draw it...

        if (!isVisible()) {

            return new AxisState(cursor);

        }

        

        if (isAxisLineVisible()) {

            drawAxisLine(g2, cursor, dataArea, edge);

        }



        // draw the category labels and axis label

        AxisState state = new AxisState(cursor);

        state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, plotState);

        state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);

    

        return state;



    }



    /**

     * Draws the category labels and returns the updated axis state.

     *

     * @param g2  the graphics device (<code>null</code> not permitted).

     * @param plotArea  the plot area (<code>null</code> not permitted).

     * @param dataArea  the area inside the axes (<code>null</code> not permitted).

     * @param edge  the axis location (<code>null</code> not permitted).

     * @param state  the axis state (<code>null</code> not permitted).

     * @param plotState  collects information about the plot (<code>null</code> permitted).

     * 

     * @return the updated axis state (never <code>null</code>).

     */

    protected AxisState drawCategoryLabels(Graphics2D g2,

                                           Rectangle2D plotArea,

                                           Rectangle2D dataArea,

                                           RectangleEdge edge,

                                           AxisState state,

                                           PlotRenderingInfo plotState) {



        if (logger.isDebugEnabled()) {

            logger.debug("Entering drawCategoryLabels() method, cursor = " + state.getCursor());

        }

        if (state == null) {

            throw new IllegalArgumentException("Null 'state' argument.");

        }



        if (isTickLabelsVisible()) {

            g2.setFont(getTickLabelFont());

            g2.setPaint(getTickLabelPaint());

            List ticks = refreshTicks(g2, state, plotArea, dataArea, edge);

            state.setTicks(ticks);        

          

            int categoryIndex = 0;

            Iterator iterator = ticks.iterator();

            while (iterator.hasNext()) {

                

                CategoryTick tick = (CategoryTick) iterator.next();

                g2.setPaint(getTickLabelPaint());



                CategoryLabelPosition position = this.categoryLabelPositions.getLabelPosition(edge);

                double x0 = 0.0;

                double x1 = 0.0;

                double y0 = 0.0;

                double y1 = 0.0;

                if (edge == RectangleEdge.TOP) {

                    x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);

                    x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);

                    y1 = state.getCursor() - this.categoryLabelPositionOffset;

                    y0 = y1 - state.getMax();

                }

                else if (edge == RectangleEdge.BOTTOM) {

                    x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);

                    x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge); 

                    y0 = state.getCursor() + this.categoryLabelPositionOffset;                   

                    y1 = y0 + state.getMax();

                }

                else if (edge == RectangleEdge.LEFT) {

                    y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);

                    y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);

                    x1 = state.getCursor() - this.categoryLabelPositionOffset;

                    x0 = x1 - state.getMax();

                }

                else if (edge == RectangleEdge.RIGHT) {

                    y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, edge);

                    y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, edge);

                    x0 = state.getCursor() + this.categoryLabelPositionOffset;

                    x1 = x0 - state.getMax();

                }

                Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), (y1 - y0));

                double[] anchorPoint = RectangleAnchor.coordinates(

                    area, position.getCategoryAnchor()

                );

                TextBlock block = tick.getLabel();

                block.draw(

                    g2, 

                    (float) anchorPoint[0], (float) anchorPoint[1], position.getLabelAnchor(), 

                    (float) anchorPoint[0], (float) anchorPoint[1], position.getAngle()

                );

                Shape bounds = block.calculateBounds(

                    g2, (float) anchorPoint[0], (float) anchorPoint[1], position.getLabelAnchor(), 

                    (float) anchorPoint[0], (float) anchorPoint[1], position.getAngle()

                );

                if (plotState != null) {

                    EntityCollection entities = plotState.getOwner().getEntityCollection();

                    if (entities != null) {

                        String tooltip = (String) this.categoryLabelToolTips.get(

                            tick.getCategory()

                        );

                        entities.addEntity(new TickLabelEntity(bounds, tooltip, null));   

                    }

                }

                categoryIndex++;

            }



            if (edge.equals(RectangleEdge.TOP)) {

                double h = state.getMax();

                state.cursorUp(h);

            }

            else if (edge.equals(RectangleEdge.BOTTOM)) {

                double h = state.getMax();

                state.cursorDown(h);

            }

            else if (edge == RectangleEdge.LEFT) {

                double w = state.getMax();

                state.cursorLeft(w);

            }

            else if (edge == RectangleEdge.RIGHT) {

                double w = state.getMax();

                state.cursorRight(w);

            }

        }

        return state;

    }



    /**

     * Creates a temporary list of ticks that can be used when drawing the axis.

     *

     * @param g2  the graphics device (used to get font measurements).

     * @param state  the axis state.

     * @param plotArea  the area where the plot and axes will be drawn.

     * @param dataArea  the area inside the axes.

     * @param edge  the location of the axis.

     * 

     * @return A list of ticks.

     */

    public List refreshTicks(Graphics2D g2, 

                             AxisState state,

                             Rectangle2D plotArea, 

                             Rectangle2D dataArea,

                             RectangleEdge edge) {



        List ticks = new java.util.ArrayList();

        

        // sanity check for data area...

        if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {

            return ticks;

        }



        CategoryPlot plot = (CategoryPlot) getPlot();

        List categories = plot.getCategories();

        double max = 0.0;

                

        if (categories != null) {

            CategoryLabelPosition position = this.categoryLabelPositions.getLabelPosition(edge);

            float r = this.maxCategoryLabelWidthRatio;

            if (r <= 0.0) {

                r = position.getWidthRatio();   

            }

                  

            float l = 0.0f;

            if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {

                l = (float) calculateCategorySize(categories.size(), dataArea, edge);  

            }

            else {

                if (RectangleEdge.isLeftOrRight(edge)) {

                    l = (float) dataArea.getWidth();   

                }

                else {

                    l = (float) dataArea.getHeight();   

                }

            }

            int categoryIndex = 0;

            Iterator iterator = categories.iterator();

            while (iterator.hasNext()) {

                Comparable category = (Comparable) iterator.next();

                TextBlock label = createLabel(category, l * r, edge, g2);

                if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {

                    max = Math.max(max, calculateTextBlockHeight(label, position, g2));

                }

                else if (edge == RectangleEdge.LEFT || edge == RectangleEdge.RIGHT) {

                    max = Math.max(max, calculateTextBlockWidth(label, position, g2));

                }

                Tick tick = new CategoryTick(

                    category, label, 

                    position.getLabelAnchor(), position.getRotationAnchor(), position.getAngle()

                );

                ticks.add(tick);

                categoryIndex = categoryIndex + 1;

            }

        }

        state.setMax(max);

        return ticks;

        

    }



    /**

     * Creates a label.

     *

     * @param category  the category.

     * @param width  the available width. 

     * @param edge  the edge on which the axis appears.

     * @param g2  the graphics device.

     *

     * @return a label.

     */

    protected TextBlock createLabel(Comparable category, float width, 

                                    RectangleEdge edge, Graphics2D g2) {

        TextBlock label = TextUtilities.createTextBlock(

            category.toString(), getTickLabelFont(), getTickLabelPaint(), 

            width, this.maxCategoryLabelLines, new G2TextMeasurer(g2)

        );  

        return label; 

    }

    

    /**

     * A utility method for determining the width of a text block.

     *

     * @param block  the text block.

     * @param position  the position.

     * @param g2  the graphics device.

     *

     * @return the width.

     */

    protected double calculateTextBlockWidth(TextBlock block, 

                                             CategoryLabelPosition position, 

                                             Graphics2D g2) {

                                                    

        Insets insets = getTickLabelInsets();

        Size2D size = block.calculateDimensions(g2);

        Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), size.getHeight());

        Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 0.0f, 0.0f);

        double w = rotatedBox.getBounds2D().getWidth() + insets.top + insets.bottom;

        return w;

        

    }



    /**

     * A utility method for determining the height of a text block.

     *

     * @param block  the text block.

     * @param position  the label position.

     * @param g2  the graphics device.

     *

     * @return the height.

     */

    protected double calculateTextBlockHeight(TextBlock block, 

                                              CategoryLabelPosition position, 

                                              Graphics2D g2) {

                                                    

        Insets insets = getTickLabelInsets();

        Size2D size = block.calculateDimensions(g2);

        Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), size.getHeight());

        Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 0.0f, 0.0f);

        double h = rotatedBox.getBounds2D().getHeight() + insets.top + insets.bottom;

        return h;

        

    }



    /**

     * Creates a clone of the axis.

     * 

     * @return a clone.

     * 

     * @throws CloneNotSupportedException if some component of the axis does not support cloning.

     */

    public Object clone() throws CloneNotSupportedException {

    

        Object clone = super.clone();

        return clone;

            

    }

    

    /**

     * Tests this axis for equality with an arbitrary object.

     *

     * @param obj  the object (<code>null</code> permitted).

     *

     * @return a boolean.

     */

    public boolean equals(Object obj) {

        if (obj == this) {

            return true;

        }

        if (obj instanceof CategoryAxis && super.equals(obj)) {

            CategoryAxis axis = (CategoryAxis) obj;

            boolean b0 = (axis.lowerMargin == this.lowerMargin);

            boolean b1 = (axis.upperMargin == this.upperMargin);

            boolean b2 = (axis.categoryMargin == this.categoryMargin);

            boolean b3 = (axis.maxCategoryLabelWidthRatio == this.maxCategoryLabelWidthRatio);

            boolean b4 = (axis.categoryLabelPositionOffset == this.categoryLabelPositionOffset);

            boolean b5 = ObjectUtils.equal(

                axis.categoryLabelPositions, this.categoryLabelPositions

            );

            boolean b6 = ObjectUtils.equal(

                axis.categoryLabelToolTips, this.categoryLabelToolTips

            );

            return b0 && b1 && b2 && b3 && b4 && b5 && b6;

        }

        return false;

    }



    /**

     * Provides serialization support.

     *

     * @param stream  the output stream.

     *

     * @throws IOException  if there is an I/O error.

     */

    private void writeObject(ObjectOutputStream stream) throws IOException {

        stream.defaultWriteObject();

    }



    /**

     * Provides serialization support.

     *

     * @param stream  the input stream.

     *

     * @throws IOException  if there is an I/O error.

     * @throws ClassNotFoundException  if there is a classpath problem.

     */

    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {

        stream.defaultReadObject();

    }

    

    //// DEPRECATED CODE ////////////////////////////////////////////////////////////



    /**

     * Returns a flag indicating whether the category labels are rotated to vertical.

     *

     * @return The flag.

     * 

     * @deprecated Use the get/setXXXCategoryLabelPosition methods.

     */

    public boolean isVerticalCategoryLabels() {

        return false;

    }



    /**

     * Sets the flag that determines whether the category labels are rotated to vertical.

     *

     * @param flag  the flag.

     * 

     * @deprecated Use the get/setXXXCategoryLabelPosition methods.

     */

    public void setVerticalCategoryLabels(boolean flag) {



        if (flag) {

            this.categoryLabelPositions = CategoryLabelPositions.UP_90;

        }

        else {

            this.categoryLabelPositions = CategoryLabelPositions.STANDARD;

        }

        notifyListeners(new AxisChangeEvent(this));



    }



    /**

     * Returns the flag that determines whether the category labels are to be

     * skipped to avoid overlapping.

     *

     * @return The flag.

     * 

     * @deprecated No longer supported.

     */

    public boolean getSkipCategoryLabelsToFit() {

        return false;

    }



    /**

     * Sets the flag that determines whether the category labels are to be

     * skipped to avoid overlapping.

     *

     * @param flag  the new value of the flag.

     * 

     * @deprecated No longer supported.

     */

    public void setSkipCategoryLabelsToFit(boolean flag) {

        // not supported any more

    }



    /**

     * Returns the category label positioning info that applies when the axis is displayed

     * at the top of the plot area.

     * 

     * @return the position info.

     * 

     * @deprecated Use getCategoryLabelPositions().getLabelPosition(RectangleEdge.TOP).

     */

    public CategoryLabelPosition getTopCategoryLabelPosition() {

        return getCategoryLabelPositions().getLabelPosition(RectangleEdge.TOP);

    }

    

    /**

     * Sets the position info that applies when the axis is displayed at the top of the 

     * plot area.

     * 

     * @param position  the position info.

     * 

     * @deprecated Use setCategoryLabelPositions(...).

     */

    public void setTopCategoryLabelPosition(CategoryLabelPosition position) {

        setCategoryLabelPositions(new CategoryLabelPositions(

                position,

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.BOTTOM),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.LEFT),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.RIGHT)

        )

        );

    }



    /**

     * Returns the category label positioning info that applies when the axis is displayed

     * at the bottom of the plot area.

     * 

     * @return the position info.

     * 

     * @deprecated Use getCategoryLabelPositions().getLabelPosition(RectangleEdge.BOTTOM).

     */

    public CategoryLabelPosition getBottomCategoryLabelPosition() {

        return getCategoryLabelPositions().getLabelPosition(RectangleEdge.BOTTOM);

    }

    

    /**

     * Sets the position info that applies when the axis is displayed at the bottom of the 

     * plot area.

     * 

     * @param position  the position info.

     * 

     * @deprecated Use setCategoryLabelPositions(...).

     */

    public void setBottomCategoryLabelPosition(CategoryLabelPosition position) {

        setCategoryLabelPositions(new CategoryLabelPositions(

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.TOP),

                position,

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.LEFT),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.RIGHT)

        )

        );

    }



    /**

     * Returns the category label positioning info that applies when the axis is displayed

     * at the left of the plot area.

     * 

     * @return the position info.

     * 

     * @deprecated Use getCategoryLabelPositions().getLabelPosition(RectangleEdge.LEFT).

     */

    public CategoryLabelPosition getLeftCategoryLabelPosition() {

        return getCategoryLabelPositions().getLabelPosition(RectangleEdge.LEFT);

    }

    

    /**

     * Sets the position info that applies when the axis is displayed at the left of the 

     * plot area.

     * 

     * @param position  the position info.

     * 

     * @deprecated Use setCategoryLabelPositions(...).

     */

    public void setLeftCategoryLabelPosition(CategoryLabelPosition position) {

        setCategoryLabelPositions(new CategoryLabelPositions(

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.TOP),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.BOTTOM),

                position,

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.RIGHT)

        )

        );

    }



    /**

     * Returns the category label positioning info that applies when the axis is displayed

     * at the right of the plot area.

     * 

     * @return the position info.

     * 

     * @deprecated Use getCategoryLabelPositions().getLabelPosition(RectangleEdge.RIGHT).

     */

    public CategoryLabelPosition getRightCategoryLabelPosition() {

        return getCategoryLabelPositions().getLabelPosition(RectangleEdge.RIGHT);

    }

    

    /**

     * Sets the position info that applies when the axis is displayed at the right of the 

     * plot area.

     * 

     * @param position  the position info.

     * 

     * @deprecated Use setLabelPositions(...).

     */

    public void setRightCategoryLabelPosition(CategoryLabelPosition position) {

        setCategoryLabelPositions(new CategoryLabelPositions(

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.TOP),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.BOTTOM),

                this.categoryLabelPositions.getLabelPosition(RectangleEdge.LEFT),

                position

        )

        );

    }



}

