/*
 * @(#)PlotTool.java    2.4.0   16 May 2005
 *
 * Copyright 2005
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.*;

import java.awt.*;
import java.awt.geom.*;
import java.io.*;

/**
 * <P>Encapsulates the transform between world and image coordinates
 * for use in plotting data graphs.  Provides functions to plot or
 * mark an array of Point2D data or an array of such Point2D arrays.
 * Automatically handles the painting of axes, grid lines, and tick
 * marks on the axes.</P>
 *
 * <P>The "world" coordinate space refers to the coordinate space
 * inhabited by the data arrays or functions that are to be plotted.
 *
 * The "image" coordinate space refers to the coordinate space of
 * the graphics context on which the plots are painted.</P>
 *
 * @author  Richard Rasala
 * @author  Jeff Raab
 * @version 2.2
 * @since   1.0
 */
public class PlotTool 
    implements Cloneable, Serializable 
{
    /** 
     * Default window size.  
     *
     * Used if <CODE>null</CODE> parameters are passed. 
     */
    private static final int DEFAULT_WINDOW_SIZE = 400;
    
    /** Default value for the preservesShape property. */
    private static final boolean DEFAULT_EQUALIZE = false;
    
    /** Default image inset. */
    private static final int DEFAULT_INSET = 0;
    
    /** Default width for each of the axes. */
    private static final int DEFAULT_AXES_SIZE = 3;
    
    /** Minimum pixel distance for automatic grid line painting. */
    private static final int MINIMUM_GRID_PIXELS = 20;
    
    /** Minimum pixel distance for automatic tick mark painting. */
    private static final int MINIMUM_TICK_PIXELS = 8;
    
    /** Default length for a tick mark. */
    private static final int DEFAULT_TICK_SIZE = 5;
    
    /** Default color for plots. */
    private static final Color DEFAULT_PLOT_COLOR = Color.black;
    
    /** Default color for each of the axes. */
    private static final Color DEFAULT_AXES_COLOR = Color.black;
    
    /** Default color for each of the grid lines. */
    private static final Color DEFAULT_GRID_COLOR = Color.gray;
    
    /** Default color for each of the tick marks. */
    private static final Color DEFAULT_TICK_COLOR = Color.red;
    
    /** Helper for strokes of thickness 1. */
    private static final BasicStroke DEFAULT_STROKE = new BasicStroke(1);
    
    
    // protected member data that is supplied
    // in the constructor or in setPlotTool
    
    /** Bounding rectangle of the world coordinate space. */
    protected Rectangle2D.Double worldBounds = null;
    
    /** Bounding rectangle of the image coordinate space. */
    protected Rectangle2D.Double imageBounds = null;
    
    /** Whether or not to preserve geometric shape. */
    protected boolean preservesShape = DEFAULT_EQUALIZE;

    /** 
     * Number of pixels in image coordinate units
     * to inset the transform of the world coordinates. 
     */
    protected int inset = DEFAULT_INSET;
    

    // protected member data that is computed
    // internally to specify transforms
    
    /** The world to image transform. */
    protected AffineTransform mapping = new AffineTransform();
    
    /** The x-component of the world to image transform. */
    protected Transform1D xMapping = new Transform1D();
    
    /** The y-component of the world to image transform. */
    protected Transform1D yMapping = new Transform1D();
    
    /** The inverse of the world to image transform. */
    protected AffineTransform inverseMapping = new AffineTransform();
    
    /** The inverse of the x-component of the world to image transform. */
    protected Transform1D inverseXMapping = new Transform1D();
    
    /** The inverse of the y-component of the world to image transform. */
    protected Transform1D inverseYMapping = new Transform1D();
    
    
    // protected hidden state data
    
    /** The x-interval in world coordinates. */
    protected XInterval worldXRange = new XInterval();
    
    /** The y-interval in world coordinates. */
    protected XInterval worldYRange = new XInterval();
    
    /** The x-interval in image coordinates. */
    protected XInterval imageXRange = new XInterval();
    
    /** The y-interval in image coordinates. */
    protected XInterval imageYRange = new XInterval();
    
    /** The absolute value of the x and y scale factors. */
    protected Point2D.Double scale = new Point2D.Double();
    
    
    // protected helpers to assist graphics operations
    
    /** Helper for point computation. */
    protected transient Point2D.Double P = new Point2D.Double();
    
    /** Helper for point computation. */
    protected transient Point2D.Double Q = new Point2D.Double();
    
    /** Helper for line computation. */
    protected transient Line2D.Double  L = new Line2D.Double();
        
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs a plot tool with the default parameters.
     */
    public PlotTool() {
        setPlotTool(null, null, DEFAULT_EQUALIZE, DEFAULT_INSET);
    }
    
    /**
     * Constructs a plot tool for use with the given
     * world and image coordinate spaces.
     *
     * @param w the desired world coordinate bounds
     * @param i the desired image coordinate bounds
     */     
    public PlotTool(
        Rectangle2D w,
        Rectangle2D i)
    {
        setPlotTool(w, i, DEFAULT_EQUALIZE, DEFAULT_INSET);    
    }
    
    /**
     * Constructs a plot tool for use with the given
     * world and image coordinate spaces,
     * with the given policy regarding the preservation 
     * of geometric shape.
     *
     * @param w the desired world coordinate bounds
     * @param i the desired image coordinate bounds
     * @param eq whether or not the transform preserves geometric shape
     */     
    public PlotTool(
        Rectangle2D w,
        Rectangle2D i,
        boolean eq)
    {
        setPlotTool(w, i, eq, DEFAULT_INSET);    
    }
    
    /**
     * Constructs a plot tool for use with the given
     * world and image coordinate spaces,
     * with the given policy regarding the preservation 
     * of geometric shape, and the given image inset.
     *
     * @param w the desired world coordinate bounds
     * @param i the desired image coordinate bounds
     * @param eq whether or not the transform preserves geometric shape
     * @param in the desired inset in image pixel units
     */     
    public PlotTool(
        Rectangle2D w,
        Rectangle2D i,
        boolean eq,
        int in)
    {
        setPlotTool(w, i, eq, in);    
    }

    //////////////////
    // Serializable //
    //////////////////
    
    /**
     * Extends the default deserialization process 
     * to restore helper data members upon instantiation.
     *
     * @param in the deserialization input stream
     */
    private void readObject(ObjectInputStream in) 
        throws IOException, ClassNotFoundException 
    {
        in.defaultReadObject();
        
        // restore transient fields
        P = new Point2D.Double();
        Q = new Point2D.Double();
        L = new Line2D.Double();
    }
    
    ////////////////
    // Public API //
    ////////////////
    
    //////////////////////////////////////////////
    // Functions to Set and Get Plot Tool State //
    //////////////////////////////////////////////
    
    /**
     * Sets the world coordinate space, the image coordinate space,
     * the shape preservation policy and the image inset
     * for this plot tool to the respective values
     * for the given plot tool.
     *
     * @param other the object to be copied
     * @return a reference to this plot tool
     */
    public PlotTool setPlotTool(PlotTool other) {
        if (other != null) {
            return setPlotTool(
                other.worldBounds,
                other.imageBounds,
                other.preservesShape,
                other.inset);
        }
        else
            return this;
    }
     
    /**
     * Sets the world coordinate space, the image coordinate space,
     * the shape preservation policy and the image inset
     * for this plot tool to the given values.
     *
     * @param w the desired world coordinate bounds
     * @param i the desired image coordinate bounds
     * @param eq whether or not the transform preserves geometric shape
     * @param in the desired inset in image pixel units
     * @return a reference to this plot tool
     */
    public PlotTool setPlotTool(
        Rectangle2D w,
        Rectangle2D i,
        boolean eq,
        int in)
    {
        // initialize with the user supplied parameters
        storeWorldBounds(w);
        storeImageBounds(i);
        storePreservesShape(eq);
        storeInset(in);
        
        // define the transform, the x,y component transforms,
        // and the inverses of these transforms
        return setTransforms();
    }
    
    /**
     * Sets the world coordinate bounds to the given bounds.
     *
     * @param w the new world coordinate bounds
     * @return a reference to this plot tool
     */
    public PlotTool setWorldBounds(Rectangle2D w) {
        storeWorldBounds(w);
        return setTransforms();
    }
    
    /**
     * Returns a clone of the world coordinate bounds.
     */
    public Rectangle2D getWorldBounds() {
        return (Rectangle2D)worldBounds.clone();
    }
    
    /**
     * Set the world coordinate bounds to the smallest rectangle
     * that contains the <CODE>Point2D</CODE> array data.
     *
     * @param data the array data to enclose in the bounds
     * @return a reference to this plot tool
     */
    public PlotTool setWorldBounds(Point2D[] data) {
        return setWorldBounds(makeBoundsRectangle2D(data));
    }
         
    /**
     * Set the world coordinate bounds to the smallest rectangle
     * that contains the <CODE>Point2D</CODE> array of array data.
     *
     * @param data the array of array data to enclose in the bounds
     * @return a reference to this plot tool
     */
    public PlotTool setWorldBounds(Point2D[][] data) {
        return setWorldBounds(makeBoundsRectangle2D(data));
    }
    
    /**
     * Return the smallest rectangle containing the data
     * in the given <CODE>Point2D</CODE> array
     * and return null if no data is supplied.
     *
     * @param data the Point2D array
     */
    public static Rectangle2D makeBoundsRectangle2D(Point2D[] data) {
        Rectangle2D.Double R = null;
        boolean first = true;
        
        if (data != null) {
            int size = data.length;
            
            for (int i = 0; i < size; i++) {
                if (data[i] != null) {
                    double x = data[i].getX();
                    double y = data[i].getY();
                    
                    if (first) {
                        R = new Rectangle2D.Double(x, y, 0.0, 0.0);
                        first = false;
                    }
                    else {
                        R.add(x, y);
                    }
                }
            }
        }
        
        return R;
    }
    
    /**
     * Return the smallest rectangle containing the data
     * in the given array of <CODE>Point2D</CODE> arrays
     * and return null if no data is supplied.
     *
     * @param data the Point2D array
     */
    public static Rectangle2D makeBoundsRectangle2D(Point2D[][] data) {
        Rectangle2D.Double R = null;
        boolean first = true;
        
        if (data != null) {
            int size = data.length;
            
            for (int i = 0; i < size; i++) {
                if (data[i] != null) {
                    int innersize = data[i].length;
                    
                    for (int j = 0; j < innersize; j++) {
                        if (data[i][j] != null) {
                            double x = data[i][j].getX();
                            double y = data[i][j].getY();
                    
                            if (first) {
                                R = new Rectangle2D.Double(x, y, 0.0, 0.0);
                                first = false;
                            }
                            else {
                                R.add(x, y);
                            }
                        }
                    }
                }
            }
        }
        
        return R;
    }
    
    /**
     * Sets the image coordinate bounds to the given bounds.
     *
     * @param i the new image coordinate bounds
     * @return a reference to this plot tool
     */
    public PlotTool setImageBounds(Rectangle2D i) {
        storeImageBounds(i);
        return setTransforms();
    }
    
    /**
     * Returns a clone of the image coordinate bounds.
     */
    public Rectangle2D getImageBounds() {
        return (Rectangle2D)imageBounds.clone();
    }
         
    /**
     * Sets the geometric shape preservation policy to the given value.
     *
     * @param eq whether or not this tool preserves geometric shape
     * @return a reference to this plot tool
     */
    public PlotTool setPreservesShape(boolean eq) {
        storePreservesShape(eq);
        return setTransforms();
    }
    
    /**
     * Returns whether or not this tool preserves geometric shape.
     */
    public boolean preservesShape() {
        return preservesShape;
    }
         
    /**
     * Sets the image inset to the given value in image pixel units.
     *
     * @param in the new image inset in image pixel units
     * @return a reference to this plot tool
     */
    public PlotTool setInset(int in) {
        storeInset(in);
        return setTransforms();
    }
    
    /**
     * Return the image inset.
     */
    public int getInset() {
        return inset;
    }
    
    /////////////////////////////////////////
    // Functions to Get Current Transforms //
    /////////////////////////////////////////
    
    /**
     * Returns a clone of the world bounds to image bounds
     * two-dimensional transform.
     */
    public AffineTransform getTransform() {
        return new AffineTransform(mapping);    
    }
    
    /**
     * Returns a clone of the <I>x</I> component 
     * of the world to image transform.
     */
    public Transform1D getXTransform() {
        return new Transform1D(
            xMapping.getFactor(), xMapping.getOffset());
    }
    
    /**
     * Return a clone of the <I>y</I> component 
     * of the world to image transform.
     */
    public Transform1D getYTransform() {
        return new Transform1D(
            yMapping.getFactor(), yMapping.getOffset());
    }
    
    /**
     * Returns a clone of the inverse of the world bounds to image bounds
     * two-dimensional transform.
     */
    public AffineTransform getInverseTransform() {
        return new AffineTransform(inverseMapping);    
    }
    
    /**
     * Returns a clone of the inverse of the <I>x</I> component 
     * of the world to image transform.
     */
    public Transform1D getInverseXTransform() {
        return new Transform1D(
            inverseXMapping.getFactor(), inverseXMapping.getOffset());
    }
    
    /**
     * Returns a clone of the inverse of the <I>y</I> component 
     * of the world to image transform.
     */
    public Transform1D getInverseYTransform() {
        return new Transform1D(
            inverseYMapping.getFactor(), inverseYMapping.getOffset());
    }
    
    //////////////////////////////////////////////////
    // Functions to Scale Points or x,y Coordinates //
    //////////////////////////////////////////////////
    
    /**
     * Scales the given source <CODE>Point2D</CODE>
     * using the world to image transform 
     * and sets the target <CODE>Point2D</CODE>
     * to the transformed value.
     *
     * This method signature follows the conventions 
     * for similar methods in the <CODE>AffineTransform</CODE> class.
     *
     * @param source the point to transform
     * @param target the point to store transformed point 
     *      or <CODE>null</CODE> if a new point should be created
     * @return the given target point, transformed, 
     *      or a new transformed point if the given target point
     *      was <CODE>null</CODE>
     */
    public Point2D scale
        (Point2D source, Point2D target)
    {
        return (Point2D) mapping.transform(source, target);
    }

    /**
     * Returns the point produced by transforming
     * the given point using the world to image transform.
     *
     * @param source the point to transform
     */
    public Point2D scale(Point2D source) {
        return scale(source, null);
    }
    
    /**
     * Returns the result of scaling the given value
     * using the <I>x</I> component of the world to image transform.
     */
    public double xScale(double x) {
        return xMapping.transform(x);
    }
    
    /**
     * Returns the result of scaling the given value
     * using the <I>y</I> component of the world to image transform.
     */
    public double yScale(double y) {
        return yMapping.transform(y);
    }
    
    /**
     * Scales the given source <CODE>Point2D</CODE>
     * using the inverse of the world to image transform 
     * and sets the target <CODE>Point2D</CODE>
     * to the transformed value.
     *
     * This method signature follows the conventions 
     * for similar methods in the <CODE>AffineTransform</CODE> class.
     *
     * @param source the point to transform
     * @param target the point to store transformed point 
     *      or <CODE>null</CODE> if a new point should be created
     * @return the given target point, transformed, 
     *      or a new transformed point if the given target point
     *      was <CODE>null</CODE>
     */
    public Point2D inverseScale
        (Point2D source, Point2D target)
    {
        return (Point2D) inverseMapping.transform(source, target);
    }

    /**
     * Returns the point produced by transforming
     * the given point using the inverse of the world to image transform.
     *
     * @param source the point to transform
     */
    public Point2D inverseScale(Point2D source) {
        return inverseScale(source, null);
    }
    
    /**
     * Returns the result of scaling the given value
     * using the <I>x</I> component 
     * of the inverse of the world to image transform.
     */
    public double inverseXScale(double x) {
        return inverseXMapping.transform(x);
    }
    
    /**
     * Returns the result of scaling the given value
     * using the <I>y</I> component 
     * of the inverse of the world to image transform.
     */
    public double inverseYScale(double y) {
        return inverseYMapping.transform(y);
    }
    
    /////////////////////////////
    // Plot and Mark Functions //
    /////////////////////////////
    
    /**
     * Transforms the points of the given array
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE> 
     * to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[] data, 
        Paint color)
    {
        plotData(g, data, color, DEFAULT_STROKE);
    }
    
    /**
     * Transforms the points of the given array
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE>
     * and the given <CODE>Stroke</CODE> to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[] data, 
        Paint color, 
        Stroke s)
    {
        if ((g == null) || (data == null))
            return;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_PLOT_COLOR);

        // set stroke width
        if (s != null)
            g.setStroke(s);
        else
            g.setStroke(DEFAULT_STROKE);
    
        // loop to plot successive line segments
        // ignore points in the data array that are null
        int size = data.length;
        boolean first = true;
        
        for (int i = 0; i < size; i++) {
            if (data[i] != null) {
                if (first) {
                    scale(data[i], P);      // scale first point to P
                    first = false;
                }
                else {
                    scale(data[i], Q);      // scale next point to Q
                    L.setLine(P, Q);        // set line to P,Q
                    g.draw(L);              // draw the line
                    P.setLocation(Q);       // update P to Q
                }
            }
        }
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE> 
     * to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[][] data, 
        Paint color)
    {
        plotData(g, data, color, DEFAULT_STROKE);
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE>
     * and the given <CODE>Stroke</CODE> to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[][] data, 
        Paint color, 
        Stroke s)
    {
        if ((g == null) || (data == null))
            return;
            
        // loop to plot successive arrays
        int size = data.length;
        
        for (int i = 0; i < size; i++)
            plotData(g, data[i], color, s);
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE> 
     * to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[][] data, 
        Paint[] color)
    {
        plotData(g, data, color, DEFAULT_STROKE);
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and plots a function of the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE>
     * and the given <CODE>Stroke</CODE> to draw the lines.
     */
    public void plotData(
        Graphics2D g, 
        Point2D[][] data, 
        Paint[] color, 
        Stroke s)
    {
        if ((g == null) || (data == null))
            return;
        
        if (color == null)
            color = new Paint[] { DEFAULT_PLOT_COLOR };
        
        // loop to plot successive arrays
        int size = data.length;
        int colorsize = color.length;
        
        for (int i = 0; i < size; i++) {
            int j = i % colorsize;
            plotData(g, data[i], color[j], s);
        }
    }
    
    /**
     * Transforms the points of the given array
     * using the world to image transform 
     * and marks the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE> 
     * to paint the marks.
     */
    public void markData
        (Graphics2D g, Point2D[] data, Paint color, PlotMark plotmark)
    {
        if ((g == null) || (data == null) || (plotmark == null))
            return;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_PLOT_COLOR);

        // set stroke width
        g.setStroke(DEFAULT_STROKE);
        
        // loop to mark successive data points
        int size = data.length;
        
        for (int i = 0; i < size; i++) {
            if (data[i] != null) {
                scale(data[i], P);      // scale point to P
                plotmark.mark(g, P);    // mark the point
            }
        }
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and marks the resulting points 
     * in the given graphics context 
     * using the given color or <CODE>Paint</CODE> 
     * to paint the marks.
     */
    public void markData
        (Graphics2D g, Point2D[][] data, Paint color, PlotMark plotmark)
    {
        if ((g == null) || (data == null) || (plotmark == null))
            return;
            
        // loop to mark successive arrays
        int size = data.length;
        
        for (int i = 0; i < size; i++)
            markData(g, data[i], color, plotmark);
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and marks the resulting points 
     * in the given graphics context 
     * using the given <CODE>Paint</CODE> array 
     * to paint the marks for the corresponding sub array
     * of the data.
     */
    public void markData
        (Graphics2D g, Point2D[][] data, Paint color[], PlotMark plotmark)
    {
        if ((g == null) || (data == null) || (plotmark == null))
            return;
            
        if (color == null)
            color = new Paint[] { DEFAULT_PLOT_COLOR };
        
        // loop to plot successive arrays
        int size = data.length;
        int colorsize = color.length;
        
        for (int i = 0; i < size; i++) {
            int j = i % colorsize;
            markData(g, data[i], color[j], plotmark);
        }
    }
    
    /**
     * Transforms the points of the given array of arrays
     * using the world to image transform 
     * and marks the resulting points 
     * in the given graphics context 
     * using the given <CODE>Paint</CODE> array 
     * and the given plot mark array
     * to paint the marks for the corresponding sub array
     * of the data.
     */
    public void markData
        (Graphics2D g, Point2D[][] data, Paint color[], PlotMark[] plotmarks)
    {
        if ((g == null) || (data == null) || (plotmarks == null))
            return;
            
        if (color == null)
            color = new Paint[] { DEFAULT_PLOT_COLOR };
        
        // loop to plot successive arrays
        int size = data.length;
        int colorsize = color.length;
        int marksize  = plotmarks.length;
        
        for (int i = 0; i < size; i++) {
            int j = i % colorsize;
            int k = i % marksize;
            
            markData(g, data[i], color[j], plotmarks[k]);
        }
    }
    
    ///////////////////////////////////////
    // Functions to Plot Grids and Ticks //
    ///////////////////////////////////////
    
    /**
     * Plots a vertical grid line corresponding to
     * the given <I>x</I> position in world coordinates,
     * in the given graphics context.
     * 
     * @param g the graphics context
     * @param xPosition the <I>x</I> position in world coordinates
     */
    public void plotVGridLine(Graphics2D g, double xPosition) {
        if (g == null)
            return;
        
        double x = xScale(xPosition);
        L.setLine(x, imageYRange.getMinimum(),
                  x, imageYRange.getMaximum());
        g.draw(L);
    }
    
    /**
     * Plots a horizontal grid line corresponding to
     * the given <I>y</I> position in world coordinates,
     * in the given graphics context.
     * 
     * @param g the graphics context
     * @param yPosition the <I>y</I> position in world coordinates
     */
    public void plotHGridLine(Graphics2D g, double yPosition) {
        if (g == null)
            return;
        
        double y = yScale(yPosition);
        L.setLine(imageXRange.getMinimum(), y,
                  imageXRange.getMaximum(), y);
        g.draw(L);
    }
    
    /**
     * Plots a vertical tick line corresponding to
     * the given <I>x</I> position in world coordinates,
     * in the given graphics context
     * using the given tick size.
     * 
     * @param g the graphics context
     * @param xPosition the <I>x</I> position in world coordinates
     * @param ticksize the length of the tick mark
     */
    public void plotVTickMark(
        Graphics2D g, 
        double xPosition, 
        int ticksize) 
    {
        if (g == null)
            return;
        
        double x = xScale(xPosition);
        double y = yScale(0.0);
        L.setLine(x, y - ticksize, x, y + ticksize);
        g.draw(L);
    }
    
    /**
     * Plots a horizontal tick line corresponding to
     * the given <I>y</I> position in world coordinates,
     * in the given graphics context
     * using the given tick size.
     * 
     * @param g the graphics context
     * @param yPosition the <I>y</I> position in world coordinates
     * @param ticksize the length of the tick mark
     */
    public void plotHTickMark(
        Graphics2D g, 
        double yPosition, 
        int ticksize) 
    {
        if (g == null)
            return;
        
        double x = xScale(0.0);
        double y = yScale(yPosition);
        L.setLine(x - ticksize, y, x + ticksize, y);
        g.draw(L);
    }
    
    /**
     * Returns the minimum index for vertical grid or tick lines
     * given the spacing along the <I>x</I> axis 
     * in world coordinates.
     *
     * The given spacing must be greater than or equal to 0.
     *
     * @param delta the spacing along the <I>x</I> axis 
     *      in world coordinates
     */
    public int xMinIndex(double delta) {

        // error return on zero values
        if ((scale.x == 0.0) || (delta <= 0.0))
            return 0;
        
        // compute the minimum world x value that maps into the image
        double limit = inverseXScale(imageXRange.getMinimum());
        
        // return index as the ratio of limit to delta rounded down
        return (int) Math.floor(limit / delta);    
    }
    
    /**
     * Returns the minimum index for horizontal grid or tick lines
     * given the spacing along the <I>y</I> axis
     * in world coordinates.
     *
     * The given spacing must be greater than or equal to 0.
     *
     * @param delta the spacing along the <I>y</I> axis 
     *      in world coordinates
     */
    public int yMinIndex(double delta) {

        // error return on zero values
        if ((scale.y == 0.0) || (delta <= 0.0))
            return 0;
        
        // compute the minimum world y value that maps into the image
        // keep in mind that y scaling reverses
        double limit = inverseYScale(imageYRange.getMaximum());
        
        // return index as the ratio of limit to delta rounded down
        return (int) Math.floor(limit / delta);
    }
    
    /**
     * Returns the maximum index for vertical grid or tick lines
     * given the spacing along the <I>x</I> axis
     * in world coordinates.
     *
     * The given spacing must be greater than or equal to 0.
     *
     * @param delta the spacing along the <I>x</I> axis 
     *      in world coordinates
     */
    public int xMaxIndex(double delta) {

        // error return on zero values
        if ((scale.x == 0.0) || (delta <= 0.0))
            return 0;
        
        // compute the maximum world x value that maps into the image
        double limit = inverseXScale(imageXRange.getMaximum());
        
        // return index as the ratio of limit to delta rounded up
        return (int) Math.ceil(limit / delta);    
    }
    
    /**
     * Returns the maximum index for horizontal grid or tick lines
     * given the spacing along the <I>y</I> axis
     * in world coordinates.
     *
     * The given spacing must be greater than or equal to 0.
     *
     * @param delta the spacing along the <I>y</I> axis 
     *      in world coordinates
     */
    public int yMaxIndex(double delta) {

        // error return on zero values
        if ((scale.y == 0.0) || (delta <= 0.0))
            return 0;
        
        // compute the maximum world y value that maps into the image
        // keep in mind that y scaling reverses
        double limit = inverseYScale(imageYRange.getMinimum());
        
        // return index as the ratio of limit to delta rounded up
        return (int) Math.ceil(limit / delta);
    }
    
    /**
     * Plots a sequence of vertical grid lines of thickness 1
     * using the given color or <CODE>Paint</CODE>
     * and the given <I>x</I> grid spacing value.
     *
     * @param g the graphics context
     * @param color the grid line color
     * @param delta the <I>x</I> grid spacing
     * @return a reference to this plot tool
     */
    public PlotTool plotVGridLines(
        Graphics2D g,
        Paint color,
        double delta)
    {
        if ((g == null) || (scale.x == 0.0) || (delta == 0.0))
            return this;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_GRID_COLOR);

        // set stroke width to 1
        g.setStroke(DEFAULT_STROKE);
    
        // make sure spacing is positive
        delta = Math.abs(delta);
    
        // draw vertical grids spaced in the x-direction
        int min = xMinIndex(delta);
        int max = xMaxIndex(delta);
    
        for (int k = min; k <= max; k++)
            plotVGridLine(g, k * delta);
    
        return this;
    }
    
    /**
     * Plots a sequence of horizontal grid lines of thickness 1
     * using the given color or <CODE>Paint</CODE>
     * and the given <I>y</I> grid spacing value.
     *
     * @param g the graphics context
     * @param color the grid line color
     * @param delta the <I>y</I> grid spacing
     * @return a reference to this plot tool
     */
    public PlotTool plotHGridLines(
        Graphics2D g,
        Paint color,
        double delta)
    {
        if ((g == null) || (scale.y == 0.0) || (delta == 0.0))
            return this;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_GRID_COLOR);

        // set stroke width to 1
        g.setStroke(DEFAULT_STROKE);
    
        // make sure spacing is positive
        delta = Math.abs(delta);
    
        // draw horizontal grids spaced in the y-direction
        int min = yMinIndex(delta);
        int max = yMaxIndex(delta);
    
        for (int k = min; k <= max; k++)
            plotHGridLine(g, k * delta);
    
        return this;
    }
    
    /**
     * Plots vertical and horizontal grid lines of thickness 1
     * using the given color or <CODE>Paint</CODE>
     * and the given grid spacing values
     * encapsulated as a <CODE>Point2D</CODE> object.
     *
     * @param g the graphics context
     * @param color the grid line color
     * @param delta a <CODE>Point2D</CODE> encapsulating 
     *      the grid spacing in the <I>x</I> and <I>y</I> directions
     * @return a reference to this plot tool
     */
    public PlotTool plotGridLines(
        Graphics2D g,
        Paint color,
        Point2D delta)
    {
        if ((g == null) || (delta == null))
            return this;
            
        plotVGridLines(g, color, delta.getX());
        plotHGridLines(g, color, delta.getY());
    
        return this;
    }
    
    /**
     * Plots a sequence of vertical tick marks of thickness 1
     * using the given color or <CODE>Paint</CODE>,
     * <I>x</I> grid spacing value, and tick mark length.
     *
     * @param g the graphics context
     * @param color the tick mark color
     * @param delta the <I>x</I> tick mark spacing
     * @param ticksize the length for each tick mark
     * @return a reference to this plot tool
     */
    public PlotTool plotVTickMarks(
        Graphics2D g,
        Paint color,
        double delta,
        int ticksize)
    {
        if ((g == null) || (scale.x == 0.0) || (delta == 0.0))
            return this;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_TICK_COLOR);

        // set stroke width to 1
        g.setStroke(DEFAULT_STROKE);
    
        // make sure spacing is positive
        delta = Math.abs(delta);
    
        // draw vertical ticks spaced in the x-direction
        int min = xMinIndex(delta);
        int max = xMaxIndex(delta);
    
        for (int k = min; k <= max; k++)
            plotVTickMark(g, k * delta, ticksize);
    
        return this;
    }
    
    /**
     * Plots a sequence of horizontal tick marks of thickness 1
     * using the given color or <CODE>Paint</CODE>,
     * <I>y</I> grid spacing value, and tick mark length.
     *
     * @param g the graphics context
     * @param color the tick mark color
     * @param delta the <I>y</I> tick mark spacing
     * @param ticksize the length for each tick mark
     * @return a reference to this plot tool
     */
    public PlotTool plotHTickMarks(
        Graphics2D g,
        Paint color,
        double delta,
        int ticksize)
    {
        if ((g == null) || (scale.y == 0.0) || (delta == 0.0))
            return this;
            
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_TICK_COLOR);

        // set stroke width to 1
        g.setStroke(DEFAULT_STROKE);
    
        // make sure spacing is positive
        delta = Math.abs(delta);
    
        // draw horizontal ticks spaced in the y-direction
        int min = yMinIndex(delta);
        int max = yMaxIndex(delta);
    
        for (int k = min; k <= max; k++)
            plotHTickMark(g, k * delta, ticksize);
    
        return this;
    }
    
    /**
     * Plots vertical and horizontal tick marks of thickness 1
     * using the given color or <CODE>Paint</CODE>,
     * grid spacing values encapsulated 
     * as a <CODE>Point2D</CODE> object, and tick mark length.
     *
     * @param g the graphics context
     * @param color the tick mark color
     * @param delta a <CODE>Point2D</CODE> encapsulating 
     *      the tick mark spacing 
     *      in the <I>x</I> and <I>y</I> directions
     * @param ticksize the length for each tick mark
     * @return a reference to this plot tool
     */
    public PlotTool plotTickMarks(
        Graphics2D g,
        Paint color,
        Point2D delta,
        int ticksize)
    {
        if ((g == null) || (delta == null))
            return this;
            
        plotVTickMarks(g, color, delta.getX(), ticksize);
        plotHTickMarks(g, color, delta.getY(), ticksize);
    
        return this;
    }
    
    /**
     * Returns the smallest double that has one of the
     * three forms 10<SUP><I>k</I></SUP>, 
     * 2*10<SUP><I>k</I></SUP>, or 5*10<SUP><I>k</I></SUP>, 
     * and is greater than or equal to the absolute value of
     * the given minimum.
     *
     * Returns 0.0 if the given minimum is equal to 0.0.
     *
     * @param minimumDelta the minimum value that could be returned
     */
    public static double findSpacing(double minimumDelta) {
        if (minimumDelta == 0.0)
            return 0.0;
            
        minimumDelta = Math.abs(minimumDelta);
        
        // invariant for computation: above = below * 10.0
        double below = 1.0;
        double above = 10.0;
        
        // shift above and below by powers of 10 to obtain
        // below < minimumDelta <= above
        
        // shift up if above is too small
        while (above <  minimumDelta) {
            below = above;
            above *= 10.0;
        }
        
        // shift down if below is too large
        while (below >= minimumDelta) {
            above = below;
            below /= 10.0;
        }
        
        // the return value will be 2*below, 5*below, or 10*below
        // (which is equal to above)
        
        if ((2.0 * below) >= minimumDelta)
            return 2.0 * below;
            
        if ((5.0 * below) >= minimumDelta)
            return 5.0 * below;
        
        return above;
    }
    
    /**
     * Returns a <CODE>Point2D</CODE> object encapsulating
     * an appropriate spacing for grid lines and tick marks
     * in world coordinates.
     *
     * The <I>x</I> and <I>y</I> values of the returned 
     * <CODE>Point2D</CODE> object will each have one of the
     * three numeric forms: 10<SUP><I>k</I></SUP>, 
     * 2*10<SUP><I>k</I></SUP>, or 5*10<SUP><I>k</I></SUP>.
     *
     * If this plot tool is set to preserve geometric shape 
     * then the <I>x</I> and <I>y</I> values of the returned
     * point will be equal.
     * 
     * @param minPixel the minimum spacing in image pixel units
     * @return a <CODE>Point2D</CODE> object encapsulating
     *      the appropriate spacing
     *      for grid lines and tick marks
     *      in world coordinates
     */
    public Point2D autoSpacing(int minPixel) {
        minPixel = Math.abs(minPixel);
        
        // compute the minimum delta.x and delta.y
        // in world coordinates
        Point2D.Double delta = new Point2D.Double();
        
        delta.x = (scale.x > 0.0) ? (minPixel / scale.x) : 0.0;
        delta.y = (scale.y > 0.0) ? (minPixel / scale.y) : 0.0;

        // modify delta.x and delta.y to have the desired form
        // 10^k, 2*10^k, or 5*10^k
        delta.x = findSpacing(delta.x);
        delta.y = findSpacing(delta.y);
    
        // equalize deltas if true geometric shape is being used
        // choose the larger of delta.x and delta.y in this case
        if (preservesShape && (delta.x != 0) && (delta.y != 0)) {
            if (delta.x < delta.y)
                delta.x = delta.y;
            else
                delta.y = delta.x;
        }
        
        // return the grid or tick spacing
        return delta;
    }
    
    /**
     * Plots grid lines appropriate for the world to image transform
     * in the given graphics context, using the default thickness
     * and the default color for grid lines.
     * 
     * @param g the graphics context on which to plot
     * @return a reference to this plot tool
     */
    public PlotTool autoGridLines(Graphics2D g) {
        return plotGridLines(
            g, 
            DEFAULT_GRID_COLOR, 
            autoSpacing(MINIMUM_GRID_PIXELS));
    }
    
    /**
     * Plots tick marks appropriate for the world to image transform
     * in the given graphics context, using the default thickness
     * and the default color for tick marks.
     * 
     * @param g the graphics context on which to plot
     * @return a reference to this plot tool
     */
    public PlotTool autoTickMarks(Graphics2D g) {
        return plotTickMarks(
            g, 
            DEFAULT_TICK_COLOR, 
            autoSpacing(MINIMUM_TICK_PIXELS), 
            DEFAULT_TICK_SIZE);
    }
    
    /**
     * Plots axes in the given graphics context
     * using the given color or <CODE>Paint</CODE>
     * and the given line thickness.
     *
     * @param g the graphics context on which to plot
     * @param color the desired axes color
     * @param thick the desired axes line thickness
     * @return a reference to this plot tool
     */
    public PlotTool plotAxes(
        Graphics2D g, 
        Paint color, 
        int thick) 
    {
        if (g == null)
            return this;
        
        // set paint color
        if (color != null)
            g.setPaint(color);
        else
            g.setPaint(DEFAULT_AXES_COLOR);

        // set stroke width to thick
        g.setStroke(new BasicStroke(Math.abs(thick)));
        
        // draw axes
        plotVGridLine(g, 0.0);
        plotHGridLine(g, 0.0);
    
        // set stroke width to 1
        g.setStroke(DEFAULT_STROKE);
    
        return this;
    }
    
    /**
     * Plots black axes of thickness 3 in given graphics context.
     *
     * @param g the graphics context on which to plot
     * @return a reference to this plot tool
     */
    public PlotTool autoAxes(Graphics2D g) {
        return plotAxes(g, DEFAULT_AXES_COLOR, DEFAULT_AXES_SIZE);
    }

    ///////////////////////
    // Protected methods //
    ///////////////////////
    
    /**
     * Stores the given world coordinate bounds.
     *
     * @param w the new world coordinate bounds
     */
    protected void storeWorldBounds(Rectangle2D w) {
        if (w != null) {
            worldBounds = new Rectangle2D.Double(
                w.getX(), 
                w.getY(), 
                w.getWidth(), 
                w.getHeight());
        }                
        else {
            worldBounds = new Rectangle2D.Double(0.0, 0.0, 1.0, 1.0);
        }
    }
    
    /**
     * Stores the given image coordinate bounds.
     *
     * @param i the new image coordinate bounds
     */
    public void storeImageBounds(Rectangle2D i) {
        if (i != null) {
            imageBounds = new Rectangle2D.Double(
                i.getX(), 
                i.getY(), 
                i.getWidth(), 
                i.getHeight());
        }
        else {
            imageBounds = new Rectangle2D.Double(
                0.0, 
                0.0, 
                DEFAULT_WINDOW_SIZE, 
                DEFAULT_WINDOW_SIZE);
        }
    }
    
    /**
     * Stores the given shape preservation policy.
     *
     * @param ps whether or not the transform 
     *      preserves geometric shape
     */
    protected void storePreservesShape(boolean ps) {
        preservesShape = ps;
    }
    
    /**
     * Stores the given inset value.
     *
     * @param in the new image inset in image pixel units
     */
    protected void storeInset(int in) {
        if (in > 0)
            inset = in;
        else
            inset = 0;
    }
    
    /**
     * <P>Creates an affine transform suitable for transforming points in
     * the world coordinate space to points in the image coordinate space, 
     * either by best fit or by preserving the shape of the geometry.
     * </P>
     *
     * <P>The affine transform will support the mathematician's
     * convention for world coodinates:
     * </P>
     * <UL>
     *     <LI>x increases to the right.
     *     <LI>y increases upward.
     * </UL>
     * <P>This means that in practice:
     * </P>
     * <UL>
     *     <LI>x-scaling will be positive.
     *     <LI>y-scaling will be negative.
     * </UL>
     *
     * <P>The image of the world will be centered, that is, 
     * the center of the bounds rectangle for the world coordinate space 
     * will map to
     * the center of the bounds rectangle for the image coordinate space.
     * </P>
     *
     * <P>If this plot tool is not set to preserve geometric shape,
     * then the transform will map
     * the bounds rectangle for the world coordinate space
     * directly to
     * the bounds rectangle for the image coordinate space.
     * </P>
     * 
     * <P>The inset parameter is used to adjust the transform so that 
     * the world coordinate bounds 
     * maps to a subrectangle of
     * the image coordinate bounds
     * inset on all sides by the given number of pixels.
     * </P>
     *
     * @return a reference to this plot tool
     */
    protected PlotTool setTransforms() {
        
        // find the intervals for the world and image
        worldXRange.setEndpoints
            (worldBounds.x, worldBounds.x + worldBounds.width);
        worldYRange.setEndpoints
            (worldBounds.y, worldBounds.y + worldBounds.height);
        
        imageXRange.setEndpoints
            (imageBounds.x, imageBounds.x + imageBounds.width);
        imageYRange.setEndpoints
            (imageBounds.y, imageBounds.y + imageBounds.height);
        
        // prepare to compute scale factors
        scale.x = 0.0;
        scale.y = 0.0;
        
        // first compute scale factors for best fit
        double image_range = 0.0;
        
        if (worldXRange.getSize() != 0) {
            image_range = imageXRange.getSize() - 2 * inset;
            
            if (image_range > 0.0)
                scale.x = image_range / worldXRange.getSize();
        }
        
        if (worldYRange.getSize() != 0) {
            image_range = imageYRange.getSize() - 2 * inset;
            
            if (image_range > 0.0)
                scale.y = image_range / worldYRange.getSize();
        }
        
        // equalize scale factors if true geometric shape is desired
        if (preservesShape && (scale.x != 0.0) && (scale.y != 0.0)) {
            if (scale.x > scale.y)
                scale.x = scale.y;
            else
                scale.y = scale.x;
        }
        
        // compute transform to map world center to image center
        // invert y scaling to support mathematician's viewpoint
        // use notation in java docs for AffineTransform coefficients
        
        // linear part
        double m00 =   scale.x;
        double m10 =   0.0;
        double m01 =   0.0;
        double m11 = - scale.y;    // notice inversion
        
        // affine part
        double m02 =   imageXRange.getMidpoint()
                       - m00 * worldXRange.getMidpoint();
                            
        double m12 =   imageYRange.getMidpoint()
                       - m11 * worldYRange.getMidpoint();
        
        // now define the transform and the component transforms
        mapping.setTransform(m00, m10, m01, m11, m02, m12);
        xMapping.setTransform(m00, m02);
        yMapping.setTransform(m11, m12);
        
        // now define the inverse transforms
        // if a scale factor is zero then use zero in its "inverse" also
        
        if (m00 != 0.0)
            m00 = 1.0/m00;
        
        if (m11 != 0.0)
            m11 = 1.0/m11;
        
        m02 = worldXRange.getMidpoint()
              - m00 * imageXRange.getMidpoint();

        m12 = worldYRange.getMidpoint()
              - m11 * imageYRange.getMidpoint();

        inverseMapping.setTransform(m00, m10, m01, m11, m02, m12);
        inverseXMapping.setTransform(m00, m02);
        inverseYMapping.setTransform(m11, m12);

        // return a reference to this plot tool object
        return this;
    }
}
