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

 * 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.]

 *

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

 * StandardLegend.java

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

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

 *

 * Original Author:  David Gilbert (for Object Refinery Limited);

 * Contributor(s):   Andrzej Porebski;

 *                   Luke Quinane;

 *                   Barak Naveh;

 *

 * $Id: StandardLegend.java,v 1.1 2004/08/31 14:26:08 mungady Exp $

 *

 * Changes (from 20-Jun-2001)

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

 * 20-Jun-2001 : Modifications submitted by Andrzej Porebski for legend placement;

 * 18-Sep-2001 : Updated header and fixed DOS encoding problem (DG);

 * 16-Oct-2001 : Moved data source classes to com.jrefinery.data.* (DG);

 * 19-Oct-2001 : Moved some methods [getSeriesPaint(...) etc.] from JFreeChart to Plot (DG);

 * 22-Jan-2002 : Fixed bug correlating legend labels with pie data (DG);

 * 06-Feb-2002 : Bug fix for legends in small areas (DG);

 * 23-Apr-2002 : Legend item labels are now obtained from the plot, not the chart (DG);

 * 20-Jun-2002 : Added outline paint and stroke attributes for the key boxes (DG);

 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);

 * 23-Sep-2002 : Changed the name of LegendItem --> DrawableLegendItem (DG);

 * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG);

 * 16-Oct-2002 : Adjusted vertical text position in legend item (DG);

 * 17-Oct-2002 : Fixed bug where legend items are not using the font that has been set (DG);

 * 11-Feb-2003 : Added title code by Donald Mitchell, removed unnecessary constructor (DG);

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

 * 22-Sep-2003 : Added nullpointer checks (TM);

 * 23-Sep-2003 : Fixed bug in equals(...) method (DG);

 * 08-Oct-2003 : Applied patch for displaying series line style, contributed by Luke Quinane (DG);

 * 23-Dec-2003 : Added scale factors (x and y) for shapes displayed in legend (DG);

 * 26-Mar-2004 : Added option to control item order, contributed by Angel (DG);

 * 26-Mar-2004 : Added support for 8 more anchor points (BN);

 * 27-Mar-2004 : Added support for round corners of bounding box (BN);

 * 07-Apr-2004 : Changed text bounds calculation (DG);

 * 21-Apr-2004 : Barak Naveh has contributed word-wrapping for legend items (BN);

 * 

 */



package org.jfree.chart;



import java.awt.BasicStroke;

import java.awt.Color;

import java.awt.Font;

import java.awt.FontMetrics;

import java.awt.Graphics2D;

import java.awt.Paint;

import java.awt.Shape;

import java.awt.Stroke;

import java.awt.font.LineMetrics;

import java.awt.geom.AffineTransform;

import java.awt.geom.Line2D;

import java.awt.geom.Point2D;

import java.awt.geom.Rectangle2D;

import java.awt.geom.RectangularShape;

import java.awt.geom.RoundRectangle2D;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;



import org.jfree.chart.entity.EntityCollection;

import org.jfree.chart.entity.LegendItemEntity;

import org.jfree.chart.event.LegendChangeEvent;

import org.jfree.io.SerialUtilities;

import org.jfree.text.TextUtilities;

import org.jfree.ui.Spacer;

import org.jfree.ui.TextAnchor;

import org.jfree.util.Log;

import org.jfree.util.LogContext;

import org.jfree.util.ObjectUtils;



/**

 * A chart legend shows the names and visual representations of the series

 * that are plotted in a chart.

 */

public class StandardLegend extends Legend implements Serializable {



    /** The default outer gap. */

    public static final Spacer DEFAULT_OUTER_GAP = new Spacer(Spacer.ABSOLUTE, 3, 3, 3, 3);



    /** The default inner gap. */

    public static final Spacer DEFAULT_INNER_GAP = new Spacer(Spacer.ABSOLUTE, 2, 2, 2, 2);



    /** The default outline stroke. */

    public static final Stroke DEFAULT_OUTLINE_STROKE = new BasicStroke();



    /** The default outline paint. */

    public static final Paint DEFAULT_OUTLINE_PAINT = Color.gray;



    /** The default background paint. */

    public static final Paint DEFAULT_BACKGROUND_PAINT = Color.white;



    /** The default title font. */

    public static final Font DEFAULT_TITLE_FONT = new Font("SansSerif", Font.BOLD, 11);



    /** The default item font. */

    public static final Font DEFAULT_ITEM_FONT = new Font("SansSerif", Font.PLAIN, 10);



    /** 

     * Used with {@link #setPreferredWidth(double)} to indicate that no preferred 

     * width is desired and defaults are to be used.

     */

    public static final double NO_PREFERRED_WIDTH = Double.MAX_VALUE;

    

    /** Reported when illegal legend is unexpectedly found. */

    private static final String UNEXPECTED_LEGEND_ANCHOR = "Unexpected legend anchor";

    

    /** The amount of blank space around the legend. */

    private Spacer outerGap;



    /** The stroke used to draw the outline of the legend. */

    private transient Stroke outlineStroke;



    /** The paint used to draw the outline of the legend. */

    private transient Paint outlinePaint;



    /** The paint used to draw the background of the legend. */

    private transient Paint backgroundPaint;



    /** The blank space inside the legend box. */

    private Spacer innerGap;



    /** An optional title for the legend. */

    private String title;



    /** The font used to display the legend title. */

    private Font titleFont;



    /** The font used to display the legend item names. */

    private Font itemFont;



    /** The paint used to display the legend item names. */

    private transient Paint itemPaint;



    /** A flag controlling whether or not outlines are drawn around shapes.*/

    private boolean outlineShapes;



    /** The stroke used to outline item shapes. */

    private transient Stroke shapeOutlineStroke = new BasicStroke(0.5f);



    /** The paint used to outline item shapes. */

    private transient Paint shapeOutlinePaint = Color.lightGray;



    /** A flag that controls whether the legend displays the series shapes. */

    private boolean displaySeriesShapes;

    

    /** The x scale factor for shapes displayed in the legend. */

    private double shapeScaleX = 1.0;

    

    /** The y scale factor for shapes displayed in the legend. */

    private double shapeScaleY = 1.0;



    /** A flag that controls whether the legend displays the series line */

    private boolean displaySeriesLines;



    /** The order of the legend items. */

    private LegendRenderingOrder renderingOrder = LegendRenderingOrder.STANDARD;

    

    /** The width of the arc used to round off the corners of the bounding box. */

    private int boundingBoxArcWidth = 0;



    /** The height of the arc used to round off the corners of the bounding box. */

    private int boundingBoxArcHeight = 0;



    /** The preferred width of the legend bounding box. */

    private double preferredWidth = NO_PREFERRED_WIDTH;



    /** Access to logging facilities. */

    private static final LogContext LOGGER = Log.createContext(StandardLegend.class);

    

    /**

     * Constructs a new legend with default settings.

     */

    public StandardLegend() {

        this.outerGap = DEFAULT_OUTER_GAP;

        this.innerGap = DEFAULT_INNER_GAP;

        this.backgroundPaint = DEFAULT_BACKGROUND_PAINT;

        this.outlineStroke = DEFAULT_OUTLINE_STROKE;

        this.outlinePaint = DEFAULT_OUTLINE_PAINT;

        this.title = null;

        this.titleFont = DEFAULT_TITLE_FONT;

        this.itemFont = DEFAULT_ITEM_FONT;

        this.itemPaint = Color.black;

        this.displaySeriesShapes = false;

        this.displaySeriesLines = false;

    }



    /**

     * Creates a new legend.

     *

     * @param chart  the chart that the legend belongs to.

     * 

     * @deprecated use the default constructor instead and let JFreeChart manage

     * the chart reference

     */

    public StandardLegend(JFreeChart chart) {

        this();

    }



    /**

     * Returns the outer gap for the legend.  This is the amount of blank space around the outside 

     * of the legend.

     *

     * @return The gap (never <code>null</code>).

     */

    public Spacer getOuterGap() {

        return this.outerGap;

    }



    /**

     * Sets the outer gap for the legend and sends a {@link LegendChangeEvent} to all

     * registered listeners.

     *

     * @param outerGap  the outer gap (<code>null</code> not permitted).

     */

    public void setOuterGap(Spacer outerGap) {

        if (outerGap == null) {

            throw new NullPointerException("Null 'outerGap' argument.");

        }

        this.outerGap = outerGap;

        notifyListeners(new LegendChangeEvent(this));

    }





    /**

     * Returns the inner gap for the legend.  This is the amount of blank space around the inside

     * of the legend.

     *

     * @return The gap (never <code>null</code>).

     */

    public Spacer getInnerGap() {

        return this.innerGap;

    }



    /**

     * Sets the inner gap for the legend and sends a {@link LegendChangeEvent} to all

     * registered listeners.

     *

     * @param innerGap  the inner gap (<code>null</code> not permitted).

     */

    public void setInnerGap(Spacer innerGap) {

        if (innerGap == null) {

            throw new NullPointerException("Null 'innerGap' argument.");

        }

        this.innerGap = innerGap;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the background paint for the legend.

     *

     * @return The background paint (never <code>null</code>).

     */

    public Paint getBackgroundPaint() {

        return this.backgroundPaint;

    }



    /**

     * Sets the background paint for the legend and sends a {@link LegendChangeEvent} to all 

     * registered listeners.

     *

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

     */

    public void setBackgroundPaint(Paint paint) {

        if (paint == null) {

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

        }

        this.backgroundPaint = paint;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the outline stroke.

     *

     * @return The outline stroke (never <code>null</code>).

     */

    public Stroke getOutlineStroke() {

        return this.outlineStroke;

    }



    /**

     * Sets the outline stroke and sends a {@link LegendChangeEvent} to all registered 

     * listeners.

     *

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

     */

    public void setOutlineStroke(Stroke stroke) {

        if (stroke == null) {

            throw new NullPointerException("Null 'stroke' argument.");

        }

        this.outlineStroke = stroke;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the outline paint.

     *

     * @return The outline paint (never <code>null</code>).

     */

    public Paint getOutlinePaint() {

        return this.outlinePaint;

    }



    /**

     * Sets the outline paint and sends a {@link LegendChangeEvent} to all registered listeners.

     *

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

     */

    public void setOutlinePaint(Paint paint) {

        if (paint == null) {

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

        }

        this.outlinePaint = paint;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Gets the title for the legend.

     *

     * @return The title (possibly <code>null</code>).

     */

    public String getTitle() {

        return this.title;

    }



    /**

     * Sets the title of the legend and sends a {@link LegendChangeEvent} to all registered 

     * listeners.

     *

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

     */

    public void setTitle(String title) {

        this.title = title;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the title font.

     *

     * @return The font (never <code>null</code>).

     */

    public Font getTitleFont() {

        return this.titleFont;

    }



    /**

     * Sets the title font and sends a {@link LegendChangeEvent} to all registered listeners.

     *

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

     */

    public void setTitleFont(Font font) {

        if (font == null) {

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

        }

        this.titleFont = font;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the series label font.

     *

     * @return The font (never <code>null</code>).

     */

    public Font getItemFont() {

        return this.itemFont;

    }



    /**

     * Sets the series label font and sends a {@link LegendChangeEvent} to all registered 

     * listeners.

     *

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

     */

    public void setItemFont(Font font) {

        if (font == null) {

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

        }

        this.itemFont = font;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the series label paint.

     *

     * @return The paint (never <code>null</code>).

     */

    public Paint getItemPaint() {

        return this.itemPaint;

    }



    /**

     * Sets the series label paint and sends a {@link LegendChangeEvent} to all registered 

     * listeners.

     *

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

     */

    public void setItemPaint(Paint paint) {

        if (paint == null) {

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

        }

        this.itemPaint = paint;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the flag that indicates whether or not outlines are drawn around shapes.

     *

     * @return The flag.

     */

    public boolean getOutlineShapes() {

        return this.outlineShapes;

    }



    /**

     * Sets the flag that controls whether or not outlines are drawn around shapes, and sends a

     * {@link LegendChangeEvent} to all registered listeners.

     *

     * @param flag  the flag.

     */

    public void setOutlineShapes(boolean flag) {

        this.outlineShapes = flag;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the stroke used to outline shapes.

     *

     * @return The stroke (never <code>null</code>).

     */

    public Stroke getShapeOutlineStroke() {

        return this.shapeOutlineStroke;

    }



    /**

     * Sets the stroke used to outline shapes and sends a {@link LegendChangeEvent} to all 

     * registered listeners.

     *

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

     */

    public void setShapeOutlineStroke(Stroke stroke) {

        if (stroke == null) {

            throw new NullPointerException("Null 'stroke' argument");

        }

        this.shapeOutlineStroke = stroke;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the paint used to outline shapes.

     *

     * @return The paint.

     */

    public Paint getShapeOutlinePaint() {

        return this.shapeOutlinePaint;

    }



    /**

     * Sets the paint used to outline shapes.  A {@link LegendChangeEvent} is sent to all

     * registered listeners.

     *

     * @param paint  the paint.

     */

    public void setShapeOutlinePaint(Paint paint) {

        this.shapeOutlinePaint = paint;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Sets a flag that controls whether or not the legend displays the series shapes.

     *

     * @param flag  the new value of the flag.

     */

    public void setDisplaySeriesShapes(boolean flag) {

        this.displaySeriesShapes = flag;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns a flag that controls whether or not the legend displays the series shapes.

     *

     * @return <code>true</code> if the series shapes should be displayed, <code>false</code> 

     *         otherwise.

     */

    public boolean getDisplaySeriesShapes() {

        return this.displaySeriesShapes;

    }

    

    /**

     * Returns the x scale factor for shapes displayed in the legend.

     * 

     * @return the x scale factor.

     */

    public double getShapeScaleX() {

        return this.shapeScaleX;

    }

    

    /**

     * Sets the x scale factor for shapes displayed in the legend and sends a

     * {@link LegendChangeEvent} to all registered listeners.

     * 

     * @param factor  the factor.

     */

    public void setShapeScaleX(double factor) {

        this.shapeScaleX = factor;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the y scale factor for shapes displayed in the legend.

     * 

     * @return the y scale factor.

     */

    public double getShapeScaleY() {

        return this.shapeScaleY;

    }

    

    /**

     * Sets the y scale factor for shapes displayed in the legend and sends a

     * {@link LegendChangeEvent} to all registered listeners.

     * 

     * @param factor  the factor.

     */

    public void setShapeScaleY(double factor) {

        this.shapeScaleY = factor;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Sets a flag that controls whether or not the legend displays the series line stroke.

     *

     * @param flag  the new value of the flag.

     */

    public void setDisplaySeriesLines(boolean flag) {

        this.displaySeriesLines = flag;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns a flag that controls whether or not the legend displays the series line stroke.

     *

     * @return <code>true</code> if the series lines should be displayed, <code>false</code> 

     *         otherwise.

     */

    public boolean getDisplaySeriesLines() {

        return this.displaySeriesLines;

    }



    /**

     * Returns the legend rendering order. 

     * 

     * @return The order (never <code>null</code>).

     */

    public LegendRenderingOrder getRenderingOrder() {

        return this.renderingOrder;

    }



    /**

     * Sets the legend rendering order and sends a {@link LegendChangeEvent} to all registered

     * listeners.

     * 

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

     */

    public void setRenderingOrder(LegendRenderingOrder order) {

        if (order == null) {

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

        }

        this.renderingOrder = order;

        notifyListeners(new LegendChangeEvent(this));

    }



    

    /**

     * Returns the width of the arc used to round off the corners of the

     * bounding box. 

     * 

     * @return the width of the arc used to round off the corners of the

     * bounding box.

     */

    public int getBoundingBoxArcWidth() {

        return this.boundingBoxArcWidth;

    }

    

    /**

     * Sets the width of the arc used to round off the corners of the

     * bounding box. 

     * A {@link LegendChangeEvent} is sent to all registered listeners.

     * 

     * @param arcWidth the new arc width.

     */

    public void setBoundingBoxArcWidth(int arcWidth) {

        this.boundingBoxArcWidth = arcWidth;

        notifyListeners(new LegendChangeEvent(this));

    }

    

    /**

     * Returns the height of the arc used to round off the corners of the

     * bounding box. 

     * 

     * @return the height of the arc used to round off the corners of the

     * bounding box.

     */

    public int getBoundingBoxArcHeight() {

        return this.boundingBoxArcHeight;

    }

    

    /**

     * Sets the height of the arc used to round off the corners of the

     * bounding box. 

     * A {@link LegendChangeEvent} is sent to all registered listeners.

     * 

     * @param arcHeight the new arc height.

     */

    public void setBoundingBoxArcHeight(int arcHeight) {

        this.boundingBoxArcHeight = arcHeight;

        notifyListeners(new LegendChangeEvent(this));

    }



    /**

     * Returns the preferred width of the legend bounding box if such width

     * has been defined; otherwise returns <code>NO_PREFERRED_WIDTH</code>.

     * 

     * @return the preferred width of the legend bounding box if such width

     * has been defined; otherwise returns <code>NO_PREFERRED_WIDTH</code>.

     */

    public double getPreferredWidth() {

        return this.preferredWidth;

    }



    /**

     * Sets the preferred width of the legend bounding box. If a preferred

     * width is set, the legend text is word-wrapped in an attempt to fulfill 

     * the preferred width. If the preferred width cannot be fulfilled, the 

     * legend would be wider to the extent necessary.

     * 

     * <p>

     * The preferred width takes effect only when the legend's anchor is set to 

     * one of the three EAST anchors or to one of the three WEST anchors.

     *   

     * <p>  

     * A {@link LegendChangeEvent} is sent to all registered listeners.

     * 

     * @param width the new width.

     */

    public void setPreferredWidth(double width) {

        this.preferredWidth = width;

        notifyListeners(new LegendChangeEvent(this));

    }

    

    /**

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

     *

     * @param g2  the graphics device.

     * @param available  the area within which the legend, and afterwards the plot, should be

     *                   drawn.

     * @param info  collects rendering info (optional).

     *

     * @return The area NOT used by the legend.

     */

    public Rectangle2D draw(Graphics2D g2, Rectangle2D available, ChartRenderingInfo info) {



        return draw(

            g2, available, (getAnchor() & HORIZONTAL) != 0, (getAnchor() & INVERTED) != 0, info

        );



    }



    /**

     * Draws the legend.

     *

     * @param g2  the graphics device.

     * @param available  the area available for drawing the chart.

     * @param horizontal  a flag indicating whether the legend items are laid out horizontally.

     * @param inverted ???

     * @param info  collects rendering info (optional).

     *

     * @return The remaining available drawing area.

     */

    protected Rectangle2D draw(Graphics2D g2, Rectangle2D available,

                               boolean horizontal, boolean inverted,

                               ChartRenderingInfo info) {



        LegendItemCollection legendItems = getChart().getPlot().getLegendItems();



        if (legendItems == null || legendItems.getItemCount() == 0) {

            return available; 

        }

        // else...



        DrawableLegendItem legendTitle = null;

        LegendItem titleItem = null; 



        if (this.title != null && !this.title.equals("")) {

            titleItem = new LegendItem(

                this.title, this.title, null, true, Color.black,

                new BasicStroke(1.0f), Color.black, new BasicStroke(1.0f)

            );

        }



        RectangularShape legendArea;

        double availableWidth = available.getWidth();

        // the translation point for the origin of the drawing system

        Point2D translation;



        // Create buffer for individual items within the legend

        List items = new ArrayList();



        // Compute individual rectangles in the legend, translation point as well

        // as the bounding box for the legend.

        if (horizontal) {

            double xstart = available.getX() + getOuterGap().getLeftSpace(availableWidth);

            double xlimit = available.getMaxX() - getOuterGap().getRightSpace(availableWidth);

            double maxRowWidth = 0;

            double xoffset = 0;

            double rowHeight = 0;

            double totalHeight = 0;

            boolean wrappingAllowed = true;



            if (titleItem != null) {

                g2.setFont(getTitleFont());



                legendTitle = createDrawableLegendItem(

                    g2, titleItem, xoffset, totalHeight

                );



                rowHeight = Math.max(0, legendTitle.getHeight());

                xoffset += legendTitle.getWidth();

            }



            g2.setFont(this.itemFont);

            for (int i = 0; i < legendItems.getItemCount(); i++) {

                DrawableLegendItem item;

                

                if (this.renderingOrder == LegendRenderingOrder.STANDARD) {

                    item = createDrawableLegendItem(

                        g2, legendItems.get(i), xoffset, totalHeight

                    );

                }

                else if (this.renderingOrder == LegendRenderingOrder.REVERSE) {

                    item = createDrawableLegendItem(

                        g2, legendItems.get(legendItems.getItemCount() - i - 1), xoffset, 

                        totalHeight

                    );                        

                }

                else {

                    // we're not supposed to get here, will cause NullPointerException

                    item = null;

                }



                if (item.getMaxX() + xstart > xlimit && wrappingAllowed) {

                    // start a new row

                    maxRowWidth = Math.max(maxRowWidth, xoffset);

                    xoffset = 0;

                    totalHeight += rowHeight;

                    i--; // redo this item in the next row

                    // if item to big to fit, we dont want to attempt wrapping endlessly. 

                    // we therefore disable wrapping for at least one item.

                    wrappingAllowed = false;  

                }

                else {

                    // continue current row

                    rowHeight = Math.max(rowHeight, item.getHeight());

                    xoffset += item.getWidth();

                    // we placed an item in this row, re-allow wrapping for next item.

                    wrappingAllowed = true; 

                    items.add(item);

                }

            }



            maxRowWidth = Math.max(maxRowWidth, xoffset);

            totalHeight += rowHeight;



            // Create the bounding box

            legendArea = new RoundRectangle2D.Double(

                0, 0, maxRowWidth, totalHeight, this.boundingBoxArcWidth, this.boundingBoxArcHeight

            );



            translation = createTranslationPointForHorizontalDraw(

                available, inverted, maxRowWidth, totalHeight 

            );

        }

        else {  // vertical...

            double totalHeight = 0;

            double maxWidth = (this.preferredWidth == NO_PREFERRED_WIDTH) ? 0 : this.preferredWidth;

            

            if (titleItem != null) {

                g2.setFont(getTitleFont());



                legendTitle = createDrawableLegendItem(g2, titleItem, 0, totalHeight);



                totalHeight += legendTitle.getHeight();

                maxWidth = Math.max(maxWidth, legendTitle.getWidth());

            }



            g2.setFont(this.itemFont);

            

            int legendItemsLength = legendItems.getItemCount();

            for (int i = 0; i < legendItemsLength; i++) {

                List drawableParts;

                

                if (this.renderingOrder == LegendRenderingOrder.STANDARD) {

                    drawableParts = createAllDrawableLinesForItem(g2, 

                            legendItems.get(i), 0, totalHeight, maxWidth);

                }

                else if (this.renderingOrder == LegendRenderingOrder.REVERSE) {

                    drawableParts = createAllDrawableLinesForItem(

                        g2, legendItems.get(legendItemsLength - i - 1), 0, totalHeight, maxWidth

                    );

                }

                else {

                    // we're not supposed to get here, will cause NullPointerException

                    drawableParts = null;

                }

                

                for (Iterator j = drawableParts.iterator(); j.hasNext();) {

                    DrawableLegendItem item = (DrawableLegendItem) j.next();

                    

                    totalHeight += item.getHeight();

                    maxWidth = Math.max(maxWidth, item.getWidth());

                    

                    items.add(item);

                }

            }



            // Create the bounding box

            legendArea = new RoundRectangle2D.Float(

                0, 0, (float) maxWidth, (float) totalHeight, 

                this.boundingBoxArcWidth, this.boundingBoxArcHeight

            );



            translation = createTranslationPointForVerticalDraw(

                available, inverted, totalHeight, maxWidth 

            );

        }



        // Move the origin of the drawing to the appropriate location

        g2.translate(translation.getX(), translation.getY());



        LOGGER.debug("legendArea = " + legendArea.getWidth() + ", " + legendArea.getHeight());

        drawLegendBox(g2, legendArea);

        drawLegendTitle(g2, legendTitle);

        drawSeriesElements(g2, items, translation, info);



        // translate the origin back to what it was prior to drawing the legend

        g2.translate(-translation.getX(), -translation.getY());



        return calcRemainingDrawingArea(available, horizontal, inverted, legendArea);

    }



    /**

     * ???

     * 

     * @param available  the available area.

     * @param inverted  inverted?

     * @param maxRowWidth  the maximum row width.

     * @param totalHeight  the total height.

     * 

     * @return The translation point.

     */

    private Point2D createTranslationPointForHorizontalDraw(Rectangle2D available, 

            boolean inverted, double maxRowWidth, double totalHeight) {

        // The yloc point is the variable part of the translation point

        // for horizontal legends xloc can be: left, center or right.

        double yloc = (inverted)

            ? available.getMaxY() - totalHeight

            - getOuterGap().getBottomSpace(available.getHeight())

            : available.getY() + getOuterGap().getTopSpace(available.getHeight());

        double xloc;

        if (isAnchoredToLeft()) {

            xloc = available.getX() + getChart().getPlot().getInsets().left;

        }

        else if (isAnchoredToCenter()) {

            xloc = available.getX() + available.getWidth() / 2 - maxRowWidth / 2;

        }

        else if (isAnchoredToRight()) {

            xloc = available.getX() + available.getWidth()

                   - maxRowWidth - getChart().getPlot().getInsets().left;

        }

        else {

            throw new IllegalStateException(UNEXPECTED_LEGEND_ANCHOR);

        }

        

        // Create the translation point

        return new Point2D.Double(xloc, yloc);

    }



    /**

     * ???

     * 

     * @param available  the available area.

     * @param inverted  inverted?

     * @param totalHeight  the total height.

     * @param maxWidth  the maximum width.

     * 

     * @return The translation point.

     */

    private Point2D createTranslationPointForVerticalDraw(Rectangle2D available, 

            boolean inverted, double totalHeight, double maxWidth) {

        // The xloc point is the variable part of the translation point

        // for vertical legends yloc can be: top, middle or bottom.

        double xloc = (inverted)

            ? available.getMaxX() - maxWidth - getOuterGap().getRightSpace(available.getWidth())

            : available.getX() + getOuterGap().getLeftSpace(available.getWidth());

        double yloc;

        if (isAnchoredToTop()) {

            yloc = available.getY() + getChart().getPlot().getInsets().top;

        }

        else if (isAnchoredToMiddle()) {

            yloc = available.getY() + (available.getHeight() / 2) - (totalHeight / 2);

        }

        else if (isAnchoredToBottom()) {

            yloc = available.getY() + available.getHeight() 

                   - getChart().getPlot().getInsets().bottom - totalHeight;

        }

        else {

            throw new IllegalStateException(UNEXPECTED_LEGEND_ANCHOR);

        }

        // Create the translation point

        return new Point2D.Double(xloc, yloc);

    }



    /**

     * Draws the legend title.

     * 

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

     * @param legendTitle  the title (<code>null</code> permitted, in which case the method 

     *                     does nothing).

     */

    private void drawLegendTitle(Graphics2D g2, DrawableLegendItem legendTitle) {

        if (legendTitle != null) {

            // XXX dsm - make title bold?

            g2.setPaint(legendTitle.getItem().getPaint());

            g2.setPaint(this.itemPaint);

            g2.setFont(getTitleFont());

            TextUtilities.drawAlignedString(

                legendTitle.getItem().getLabel(), g2,

                (float) legendTitle.getLabelPosition().getX(),

                (float) legendTitle.getLabelPosition().getY(), TextAnchor.CENTER_LEFT

            );

            LOGGER.debug("Title x = " + legendTitle.getLabelPosition().getX());

            LOGGER.debug("Title y = " + legendTitle.getLabelPosition().getY());

        }

    }



    /**

     * Draws the bounding box for the legend.

     * 

     * @param g2  the graphics device.

     * @param legendArea  the legend area.

     */

    private void drawLegendBox(Graphics2D g2, RectangularShape legendArea) {

        // Draw the legend's bounding box

        g2.setPaint(this.backgroundPaint);

        g2.fill(legendArea);

        g2.setPaint(this.outlinePaint);

        g2.setStroke(this.outlineStroke);

        g2.draw(legendArea);

    }



    /**

     * Draws the series elements.

     * 

     * @param g2  the graphics device.

     * @param items  the items.

     * @param translation  the translation point.

     * @param info  optional carrier for rendering info.

     */

    private void drawSeriesElements(Graphics2D g2, List items, Point2D translation, 

                                    ChartRenderingInfo info) {

        EntityCollection entities = null;

        if (info != null) {

            entities = info.getEntityCollection();

        }

        // Draw individual series elements

        for (int i = 0; i < items.size(); i++) {

            DrawableLegendItem item = (DrawableLegendItem) items.get(i);

            g2.setPaint(item.getItem().getPaint());

            Shape keyBox = item.getMarker();

            if (this.displaySeriesLines) {

                g2.setStroke(item.getLineStroke());

                g2.draw(item.getLine());



                if (this.displaySeriesShapes) {

                    if (item.isMarkerFilled()) {

                        g2.fill(keyBox);

                    }

                    else {

                        g2.draw(keyBox);

                    }

                }

            } 

            else {

                if (item.isMarkerFilled()) {

                    g2.fill(keyBox);

                }

                else {

                    g2.draw(keyBox);

                }

            }

            if (getOutlineShapes()) {

                g2.setPaint(this.shapeOutlinePaint);

                g2.setStroke(this.shapeOutlineStroke);

                g2.draw(keyBox);

            }

            g2.setPaint(this.itemPaint);

            g2.setFont(this.itemFont);

            TextUtilities.drawAlignedString(

                item.getItem().getLabel(), g2,

                (float) item.getLabelPosition().getX(), (float) item.getLabelPosition().getY(),

                TextAnchor.CENTER_LEFT

            );

            LOGGER.debug("Item x = " + item.getLabelPosition().getX());

            LOGGER.debug("Item y = " + item.getLabelPosition().getY());



            if (entities != null) {

                Rectangle2D area = new Rectangle2D.Double(

                    translation.getX() + item.getX(),

                    translation.getY() + item.getY(),

                    item.getWidth(), item.getHeight()

                );

                LegendItemEntity entity = new LegendItemEntity(area);

                entity.setSeriesIndex(i);

                entities.addEntity(entity);

            }

        }

    }



    /**

     * Calculates the remaining drawing area.

     * 

     * @param available  the available area.

     * @param horizontal  horizontal?

     * @param inverted  inverted?

     * @param legendArea  the legend area.

     * 

     * @return The remaining drawing area.

     */

    private Rectangle2D calcRemainingDrawingArea(Rectangle2D available, 

            boolean horizontal, boolean inverted, RectangularShape legendArea) {

        if (horizontal) {

            // The remaining drawing area bounding box will have the same

            // x origin, width and height independent of the anchor's

            // location. The variable is the y coordinate. If the anchor is

            // SOUTH, the y coordinate is simply the original y coordinate

            // of the available area. If it is NORTH, we adjust original y

            // by the total height of the legend and the initial gap.

            double yy = available.getY();

            double yloc = (inverted) ? yy

                : yy + legendArea.getHeight()

                + getOuterGap().getBottomSpace(available.getHeight());



            // return the remaining available drawing area

            return new Rectangle2D.Double(available.getX(), yloc, available.getWidth(),

                available.getHeight() - legendArea.getHeight()

                - getOuterGap().getTopSpace(available.getHeight())

                - getOuterGap().getBottomSpace(available.getHeight()));

        }

        else {

            // The remaining drawing area bounding box will have the same

            // y  origin, width and height independent of the anchor's

            // location. The variable is the x coordinate. If the anchor is

            // EAST, the x coordinate is simply the original x coordinate

            // of the available area. If it is WEST, we adjust original x

            // by the total width of the legend and the initial gap.

            double xloc = (inverted) ? available.getX()

                : available.getX()

                + legendArea.getWidth()

                + getOuterGap().getLeftSpace(available.getWidth())

                + getOuterGap().getRightSpace(available.getWidth());





            // return the remaining available drawing area

            return new Rectangle2D.Double(xloc, available.getY(),

                available.getWidth() - legendArea.getWidth()

                - getOuterGap().getLeftSpace(available.getWidth())

                - getOuterGap().getRightSpace(available.getWidth()),

                available.getHeight());

        }

    }



    /**

     * Returns a list of drawable legend items for the specified legend item.

     * Word-wrapping is applied to the specified legend item and it is broken

     * into a few lines in order to fit into the specified 

     * <code>wordWrapWidth</code>.

     *

     * @param g2 the graphics context.

     * @param legendItem  the legend item.

     * @param x  the upper left x coordinate for the bounding box.

     * @param y  the upper left y coordinate for the bounding box.

     * @param wordWrapWidth  the word wrap width.

     *

     * @return A list of drawable legend items for the specified legend item.

     * 

     * @see #setPreferredWidth(double)

     */

    private List createAllDrawableLinesForItem(Graphics2D g2,

            LegendItem legendItem, double x, double y, double wordWrapWidth) {

        

        List drawableParts = new ArrayList();



        DrawableLegendItem line = createDrawableLegendItem(g2, legendItem, x, y);



        if (line.getWidth() < wordWrapWidth) {

            // we don't need word-wrapping, return just a single line.

            drawableParts.add(line);

            return drawableParts;

        }

        

        // we need word-wrapping. start laying out the lines. add words to 

        // every line until it's full.

        

        boolean firstLine = true;

        double totalHeight = y;

        String prefix = "";

        String suffix = legendItem.getLabel().trim();

        

        LegendItem tmpItem = new LegendItem(

            prefix.trim(), 

            legendItem.getLabel(), 

            legendItem.getShape(),

            legendItem.isShapeFilled(), 

            legendItem.getPaint(),

            legendItem.getStroke(),

            legendItem.getOutlinePaint(), 

            legendItem.getOutlineStroke()

        );



        line = createDrawableLegendItem(g2, tmpItem, x, totalHeight);

        

        DrawableLegendItem goodLine = null; // no good known line yet.



        do {

            // save the suffix, we might need to restore it.

            String prevSuffix = suffix; 



            // try to extend the prefix.

            int spacePos = suffix.indexOf(" ");

            if (spacePos < 0) {

                // no space found, append all the suffix to the prefix.

                prefix += suffix;

                suffix = "";

            }

            else {

                // move a word from suffix to prefix.

                prefix += suffix.substring(0, spacePos + 1);

                suffix = suffix.substring(spacePos + 1);

            }

            

            // Create a temporary legend item for the extended prefix.

            // If first line, make its marker visible. Otherwise, paint marker

            // in background paint.

            Paint background = getBackgroundPaint();

            tmpItem = new LegendItem(

                prefix.trim(),

                legendItem.getLabel(), 

                legendItem.getShape(),

                legendItem.isShapeFilled(), 

                firstLine ? legendItem.getPaint() : background,

                legendItem.getStroke(),

                firstLine ? legendItem.getOutlinePaint() : background, 

                legendItem.getOutlineStroke());

            

            // and create a line for it as well.

            line = createDrawableLegendItem(g2, tmpItem, x, totalHeight);



            // now check if line fits in width.

            if (line.getWidth() < wordWrapWidth) {

                // fits! save it as the last good known line.

                goodLine = line;

            }

            else {

                // doesn't fit. do we have a saved good line? 

                if (goodLine == null) {

                    // nope. this means we will have to add it anyway and exceed 

                    // the desired wordWrapWidth. life is tough sometimes...

                    drawableParts.add(line);

                    totalHeight += line.getHeight();

                }

                else {

                    // yep, we have a saved good line, and we intend to use it...

                    drawableParts.add(goodLine);

                    totalHeight += goodLine.getHeight();

                    // restore previous suffix.

                    suffix = prevSuffix;

                }

                // prepare to start a new line.

                firstLine = false;

                prefix = "";

                suffix = suffix.trim();

                line = null; // mark as used to avoid using twice.

                goodLine = null; // mark as used to avoid using twice.

            }

        } 

        while (!suffix.equals(""));

        

        // make sure not to forget last line.

        if (line != null) {

            drawableParts.add(line);

        }

        

        return drawableParts;

    }

    

    /**

     * Creates a drawable legend item.

     * <P>

     * The marker box for each entry will be positioned next to the name of the

     * specified series within the legend area.  The marker box will be square

     * and 70% of the height of current font.

     *

     * @param graphics  the graphics context (supplies font metrics etc.).

     * @param legendItem  the legend item.

     * @param x  the upper left x coordinate for the bounding box.

     * @param y  the upper left y coordinate for the bounding box.

     *

     * @return A legend item encapsulating all necessary info for drawing.

     */

    private DrawableLegendItem createDrawableLegendItem(Graphics2D graphics,

                                                        LegendItem legendItem,

                                                        double x, double y) {



        LOGGER.debug("In createDrawableLegendItem(x = " + x + ", y = " + y);

        int insideGap = 2;

        FontMetrics fm = graphics.getFontMetrics();

        LineMetrics lm = fm.getLineMetrics(legendItem.getLabel(), graphics);

        float textAscent = lm.getAscent();

        float lineHeight = textAscent + lm.getDescent() + lm.getLeading();



        DrawableLegendItem item = new DrawableLegendItem(legendItem);



        float xLabelLoc = (float) (x + insideGap + 1.15f * lineHeight);

        float yLabelLoc = (float) (y + insideGap + 0.5f * lineHeight);



        item.setLabelPosition(new Point2D.Float(xLabelLoc, yLabelLoc));



        float width = (float) (item.getLabelPosition().getX() - x

            + fm.stringWidth(legendItem.getLabel()) + 0.5 * textAscent);



        float height = (2 * insideGap + lineHeight);

        item.setBounds(x, y, width, height);

        float boxDim = lineHeight * 0.70f;

        float xloc = (float) (x + insideGap + 0.15f * lineHeight);

        float yloc = (float) (y + insideGap + 0.15f * lineHeight);

        if (this.displaySeriesLines) {

            Line2D line = new Line2D.Float(

                xloc, yloc + boxDim / 2, xloc + boxDim * 3, yloc + boxDim / 2

            );

            item.setLineStroke(legendItem.getStroke());

            item.setLine(line);

            // lengthen the bounds to accomodate the longer item

            item.setBounds(

                item.getX(), item.getY(), item.getWidth() + boxDim * 2, item.getHeight()

            );

            item.setLabelPosition(new Point2D.Float(xLabelLoc + boxDim * 2, yLabelLoc));

            if (this.displaySeriesShapes) {

                Shape marker = legendItem.getShape();

                AffineTransform t1 = AffineTransform.getScaleInstance(

                    this.shapeScaleX, this.shapeScaleY

                );

                Shape s1 = t1.createTransformedShape(marker);

                AffineTransform transformer = AffineTransform.getTranslateInstance(

                    xloc + (boxDim * 1.5), yloc + boxDim / 2);

                Shape s2 = transformer.createTransformedShape(s1);

                item.setMarker(s2);

           }



        } 

        else {

            if (this.displaySeriesShapes) {

                Shape marker = legendItem.getShape();

                AffineTransform t1 = AffineTransform.getScaleInstance(

                        this.shapeScaleX, this.shapeScaleY

                );

                Shape s1 = t1.createTransformedShape(marker);

                AffineTransform transformer = AffineTransform.getTranslateInstance(

                    xloc + boxDim / 2, yloc + boxDim / 2);

                Shape s2 = transformer.createTransformedShape(s1);

                item.setMarker(s2);

            }

            else {

                item.setMarker(new Rectangle2D.Float(xloc, yloc, boxDim, boxDim));

            }

        }

        item.setMarkerFilled(legendItem.isShapeFilled());

        return item;



    }



    /**

     * Tests an object for equality with this legend.

     *

     * @param obj  the object.

     *

     * @return <code>true</code> or <code>false</code>.

     */

    public boolean equals(Object obj) {



        if (obj == null) {

            return false;

        }



        if (obj == this) {

            return true;

        }



        if (obj instanceof StandardLegend) {

            StandardLegend l = (StandardLegend) obj;

            if (super.equals(obj)) {



                if (!ObjectUtils.equal(this.outerGap, l.outerGap)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.outlineStroke, l.outlineStroke)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.outlinePaint, l.outlinePaint)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.backgroundPaint, l.backgroundPaint)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.innerGap, l.innerGap)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.title, l.title)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.titleFont, l.titleFont)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.itemFont, l.itemFont)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.itemPaint, l.itemPaint)) {

                    return false;

                }

                if (this.outlineShapes != l.outlineShapes) {

                    return false;

                }

                if (!ObjectUtils.equal(this.shapeOutlineStroke, l.shapeOutlineStroke)) {

                    return false;

                }

                if (!ObjectUtils.equal(this.shapeOutlinePaint, l.shapeOutlinePaint)) {

                    return false;

                }

                if (this.displaySeriesShapes == l.displaySeriesShapes) {

                    return true;

                }

            }

        }



        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();

        SerialUtilities.writeStroke(this.outlineStroke, stream);

        SerialUtilities.writePaint(this.outlinePaint, stream);

        SerialUtilities.writePaint(this.backgroundPaint, stream);

        SerialUtilities.writePaint(this.itemPaint, stream);

        SerialUtilities.writeStroke(this.shapeOutlineStroke, stream);

        SerialUtilities.writePaint(this.shapeOutlinePaint, stream);

    }



    /**

     * Provides serialization support.

     *

     * @param stream  the output 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();

        this.outlineStroke = SerialUtilities.readStroke(stream);

        this.outlinePaint = SerialUtilities.readPaint(stream);

        this.backgroundPaint = SerialUtilities.readPaint(stream);

        this.itemPaint = SerialUtilities.readPaint(stream);

        this.shapeOutlineStroke = SerialUtilities.readStroke(stream);

        this.shapeOutlinePaint = SerialUtilities.readPaint(stream);

    }



}

