/*
 * @(#)Path.java    2.4.0   15 September 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.util.*;

import java.awt.*;
import java.awt.geom.*;

/**
 * <p>Class <code>Path</code> encapsulates an interface <code>Strategy</code>
 * that defines the requirement for a strategy that can automatically build a
 * <code>GeneralPath</code> given vertex and tangent data.</p>
 *
 * <p>Specifically, the strategy requires:</p>
 * 
 * <UL>
 *   <LI>a vertex array,</LI>
 *   <LI>a tangent array,</LI>
 *   <LI>a choice of <code>ClosureMode</code>, and</LI>
 *   <LI>a choice of <code>WindingRule</code>.</LI>
 * </UL>
 *
 * <p>Class <code>Path</code> also provides some examples of Strategy objects
 * and some useful static methods.</p>
 *
 * <p>Class <code>Path</code> cannot be instantiated.</p>
 *
 * <p>In 2.4.0, the interface contract for <code>Path.Strategy</code> was changed
 * to permit the designer to specify what will happen if the tangent array that
 * is passed the method <code>makePath</code> happens to be <code>null</code>. In
 * prior versions of this class, the designer was forced to return an empty
 * <code>GeneralPath</code> and had no other choice.</p>
 *
 * <p>In 2.4.0, the methods to show shape structure were refactored to use the
 * tools in the class <code>PathList</code> and to follow the corresponding
 * conventions for the parameters and their defaults.</p>
 *
 * <p>Finally, in 2.4.0, certain static methods were moved to
 * <code>BaseShape</code> and renamed to be consistent with conventions there.</p>
 *
 * @see java.awt.Shape
 * @see java.awt.geom.GeneralPath
 * @author  Richard Rasala
 * @version 2.4.0
 * @since   2.3
 */
public class Path {

    /** Private constructor to prevent instantiation. */
    private Path() {}
    
    /** The standard dot size. */
    public static final int SIZE = 6;
    
    /** Half of the standard dot size. */
    public static final int HALF = SIZE / 2;
    
    /** The standard dot centered at (0,0). */
    public static final Rectangle2D.Double DOT =
        new Rectangle2D.Double(-HALF, -HALF, SIZE, SIZE);
    
    /** The standard stroke thickness. */
    public static final int THICK = 2;
    
    /** The standard stroke. */
    public static final BasicStroke STROKE = new BasicStroke(THICK);
    
    
    /**
     * The <code>Strategy</code> interface requires one method that returns a
     * <code>GeneralPath</code> given its input parameters. 
     */
    public interface Strategy {
        /**
         * <p>The method <code>makePath</code> must return a
         * non-<code>null</code> <code>GeneralPath</code> given
         * a vertex array,
         * a tangent array,
         * a choice of <code>ClosureMode</code>,
         * and a choice of <code>WindingRule</code>.</p>
         *
         * <p>The <code>GeneralPath</code> returned may be empty, that is, it
         * may have no segments.</p>
         *
         * <p>The vertex array parameter must have the form float[N][2] or
         * else an empty <code>GeneralPath</code> must be returned.</p>
         *
         * <p>If the method uses the tangent array then the tangent array must
         * have the form float[N][2] for the same N as the vertex array or be
         * <code>null</code>.  The specification for the strategy must state
         * what will happen in the <code>null</code> case.  In the case of a
         * <code>null</code> tangent array, it is acceptable to simply return
         * an empty path but the designer may specify other behavior if there
         * is a better default action.</p>
         *
         * <p>If the method does not use the tangent array, this fact must be
         * documented.  In this case, it must be acceptable to pass any
         * tangent parameter including <code>null</code> with no change in
         * the path returned.</p>
         *
         * <p>If the closure mode is <code>null</code>, the default
         * <code>ClosureMode.CLOSED</code> should be used.</p>
         *
         * <p>If the winding rule is <code>null</code>, the default
         * <code>WindingRule.WIND_NON_ZERO</code> should be used.</p>
         *
         * <p>This method should not throw an exception.  It should always
         * return an empty path as a last resort.</p>
         *
         * @param vertex      the array of vertex data
         * @param tangent     the array of corresponding tangent data
         * @param closuremode the closure mode
         * @param windingrule the winding rule
         */
        public GeneralPath makePath
            (float[][]   vertex,
             float[][]   tangent,
             ClosureMode mode,
             WindingRule windingrule);
    }
    
    
    /** 
     * <p><code>POLYGON</code> is a <code>Strategy</code> that constructs a
     * <code>GeneralPath</code> as the polygon defined by the given vertex
     * array.</p>
     *
     * <p>The given tangent array is not used.</p>
     */
    public static final Path.Strategy POLYGON
        = new Path.Strategy() {
            public GeneralPath makePath
                (float[][]   vertex,
                 float[][]   tangent,
                 ClosureMode closuremode,
                 WindingRule windingrule)
            {
                if (closuremode == null)
                    closuremode = ClosureMode.CLOSED;
                
                if (windingrule == null)
                    windingrule = WindingRule.WIND_NON_ZERO;
                
                GeneralPath path = new GeneralPath(windingrule.rule());
                
                if (FloatArray.checkArray(vertex, 2)) {
                    int N = vertex.length;
                    
                    if (N > 0) {
                        path.moveTo(vertex[0][0], vertex[0][1]);
                        
                        for (int i = 1; i < N; i++)
                            path.lineTo(vertex[i][0], vertex[i][1]);
                        
                        if (closuremode == ClosureMode.CLOSED)
                            path.closePath();
                    }
                }
                
                return path;
            }
        };
    
    
    /** 
     * <p><code>POLYGON_DOTS</code> is a <code>Strategy</code> that constructs
     * a <code>GeneralPath</code> using a sequence of <code>moveTo</code>,
     * <code>lineTo</code> pairs, one for each vertex in the vertex array.</p>
     *
     * <p>The given tangent array is not used.</p>
     *
     * <p>The given closuremode is not used.  The path is geometrically open.</p>
     */
    public static final Path.Strategy POLYGON_DOTS
        = new Path.Strategy() {
            public GeneralPath makePath
                (float[][]   vertex,
                 float[][]   tangent,
                 ClosureMode closuremode,
                 WindingRule windingrule)
            {
                if (windingrule == null)
                    windingrule = WindingRule.WIND_NON_ZERO;
                
                GeneralPath path = new GeneralPath(windingrule.rule());
                
                if (FloatArray.checkArray(vertex, 2)) {
                    int N = vertex.length;
                    
                    for (int i = 0; i < N; i++) {
                        path.moveTo(vertex[i][0], vertex[i][1]);
                        path.lineTo(vertex[i][0], vertex[i][1]);
                    }
                }
                
                return path;
            }
        };
    
    
    /** 
     * <p><code>BEZIER_CUBIC</code> is a <code>Strategy</code> that constructs a
     * <code>GeneralPath</code> as the Bezier cubic defined by the given vertex
     * and tangent arrays.</p>
     *
     * <p>This strategy uses the tangent array but if the given tangent array is
     * <code>null</code> then the strategy <code>Path.POLYGON</code> is used on
     * the vertex array instead.</p> 
     */
    public static final Path.Strategy BEZIER_CUBIC
        = new Path.Strategy() {
            public GeneralPath makePath
                (float[][]   vertex,
                 float[][]   tangent,
                 ClosureMode closuremode,
                 WindingRule windingrule)
            {
                if (closuremode == null)
                    closuremode = ClosureMode.CLOSED;
                
                if (windingrule == null)
                    windingrule = WindingRule.WIND_NON_ZERO;
                
                if (tangent == null)
                    return POLYGON.makePath
                        (vertex, null, closuremode, windingrule);
                
                GeneralPath path = new GeneralPath(windingrule.rule());
                
                if (FloatArray.checkArrayPair(vertex, tangent, 2)) {
                    int N = vertex.length;
                    
                    if (N > 0) {
                        path.moveTo(vertex[0][0], vertex[0][1]);
                        
                        int limit = closuremode.limit(N);
                        
                        for (int i = 1; i <= limit; i++) {
                            int j = i - 1;
                            int k = (i < N) ? i : 0;
                            
                            float x1 = vertex[j][0] + tangent[j][0];
                            float y1 = vertex[j][1] + tangent[j][1];
                            
                            float x2 = vertex[k][0] - tangent[k][0];
                            float y2 = vertex[k][1] - tangent[k][1];
                            
                            float x3 = vertex[k][0];
                            float y3 = vertex[k][1];
                            
                            path.curveTo(x1, y1, x2, y2, x3, y3);
                        }
                        
                        if (closuremode == ClosureMode.CLOSED)
                            path.closePath();
                    }
                }
                
                return path;
            }
        };
    
    
    /** 
     * <p><code>BEZIER_FRAME</code> is a <code>Strategy</code> that constructs a
     * <code>GeneralPath</code> as the Bezier polygonal frame associated with the
     * given vertex and tangent arrays.</p>
     *
     * <p>The closuremode is used to determine whether the Bezier frame is closed
     * or open.</p>
     *
     * <p>This strategy uses the tangent array but if the given tangent array is
     * <code>null</code> then the strategy <code>Path.POLYGON</code> is used on
     * the vertex array instead.</p> 
     */
    public static final Path.Strategy BEZIER_FRAME
        = new Path.Strategy() {
            public GeneralPath makePath
                (float[][]   vertex,
                 float[][]   tangent,
                 ClosureMode closuremode,
                 WindingRule windingrule)
            {
                if (closuremode == null)
                    closuremode = ClosureMode.CLOSED;
                
                if (windingrule == null)
                    windingrule = WindingRule.WIND_NON_ZERO;
                
                if (tangent == null)
                    return POLYGON.makePath
                        (vertex, null, closuremode, windingrule);
                else
                if (closuremode == ClosureMode.CLOSED)
                    return POLYGON.makePath(
                        BaseShape.closedBezierFramePoints(vertex, tangent),
                        null,
                        closuremode,
                        windingrule);
                else
                    return POLYGON.makePath(
                        BaseShape.openBezierFramePoints(vertex, tangent),
                        null,
                        closuremode,
                        windingrule);
            }
        };
    
    
    /** 
     * <p><code>BEZIER_TANGENT_SEGMENTS</code> is a <code>Strategy</code> that
     * constructs a <code>GeneralPath</code> as a disjoint sequence of tangent
     * segments.</p>
     *
     * <p>The tangent segments have the form:</p>
     *
     * <UL>
     *   <LI><code>vertex[i] - tangent[i]</code></LI>
     *   <LI><code>vertex[i] + tangent[i]</code></LI>
     * </UL>
     *
     * <p>using the given vertex and tangent arrays.</p>
     *
     * <p>The given closuremode is not used.  The path is OPEN.</p>
     *
     * <p>This strategy uses the tangent array but if the given tangent array is
     * <code>null</code> then the strategy <code>Path.POLYGON_DOTS</code> is used
     * on the vertex array instead.</p> 
     */
    public static final Path.Strategy BEZIER_TANGENT_SEGMENTS
        = new Path.Strategy() {
            public GeneralPath makePath
                (float[][]   vertex,
                 float[][]   tangent,
                 ClosureMode closuremode,
                 WindingRule windingrule)
            {
                if (windingrule == null)
                    windingrule = WindingRule.WIND_NON_ZERO;
                
                if (tangent == null)
                    return POLYGON_DOTS.makePath
                        (vertex, null, closuremode, windingrule);
                
                GeneralPath path = new GeneralPath(windingrule.rule());
                
                if (FloatArray.checkArrayPair(vertex, tangent, 2)) {
                    int N = vertex.length;
                    
                    if (N > 0) {
                        float[][] segments =
                            BaseShape.bezierTangentSegmentPoints(vertex, tangent);
                        
                        for (int i = 0; i < N; i++) {
                            int r = 2 * i;
                            int s = r + 1;
                            
                            path.moveTo(segments[r][0], segments[r][1]);
                            path.lineTo(segments[s][0], segments[s][1]);
                        }
                    }
                }
                
                return path;
            }
        };
    
    
    /**
     * <p>Returns the <code>GeneralPath</code> obtained by appending the
     * given sequence of <code>Shape</code> objects to the given
     * <code>GeneralPath</code> using the given sequence of
     * <code>boolean</code> values to determine the connection at each
     * stage.</p>
     *
     * <p>There is one more <code>boolean</code> value than there is a
     * <code>Shape</code> object.  The final <code>boolean</code> value
     * determines whether or not the <code>GeneralPath</code> will be
     * terminated by a <code>closePath</code> operation.
     *
     * <p>Precondition: For some integer <code>N</code>:</p>
     * <UL>
     *   <LI>shape   is <code>Shape[N]</code>
     *   <LI>connect is <code>boolean[N+1]</code> or is <code>null</code>.
     * </UL>
     *
     * <p>If connect is <code>null</code>
     * then it is replaced by <code>new boolean[N+1]</code>.</p>
     *
     * <p>If the precondition fails, no append actions will be taken.</p>
     *
     * <p>If the path parameter is <code>null</code> then a new
     * <code>GeneralPath</code> will be created, modified, and returned.
     * Since a default <code>GeneralPath</code> uses the winding rule
     * <code>GeneralPath.WIND_NON_ZERO</code>, you are forced to accept
     * this choice if you pass a <code>null</code> argument for path.
     * To get the opposite winding rule with an empty path, you must
     * pass instead:</p>
     *
     * <pre>new GeneralPath(GeneralPath.WIND_EVEN_ODD)</pre>
     *
     * @param path    the path to extend or <code>null</code>
     * @param shape   the array of shapes to append
     * @param connect the array of connection specifications or <code>null</code>
     * @return the path as modified
     */
    public static GeneralPath append(
        GeneralPath path,
        Shape[]     shape,
        boolean[]   connect)
    {
        if (path == null)
            path = new GeneralPath();
        
        if (shape == null)
            return path;
        
        int N = shape.length;
        
        if (connect == null) {
            connect = new boolean[N+1];
        }
        else {
            if (connect.length != (N+1))
                return path;
        }
        
        for (int i = 0; i < N; i++)
            if (shape[i] != null)
                path.append(shape[i], connect[i]);
        
        if (connect[N])
            path.closePath();
        
        return path;
    }
    
    
    /**
     * <p>Returns the <code>GeneralPath</code> obtained by appending the
     * given sequence of <code>Shape</code> objects to the given
     * <code>GeneralPath</code> using the given <code>boolean</code>
     * value to determine the connection at every stage.</p>
     *
     * <p>Precondition: For some integer N, shape is Shape[N].</p>
     *
     * <p>If the precondition fails, no append actions will be taken.</p>
     *
     * <p>If the path parameter is <code>null</code> then a new
     * <code>GeneralPath</code> will be created, modified, and returned.
     * Since a default <code>GeneralPath</code> uses the winding rule
     * <code>GeneralPath.WIND_NON_ZERO</code>, you are forced to accept
     * this choice if you pass a <code>null</code> argument for path.
     * To get the opposite winding rule with an empty path, you must
     * pass instead:</p>
     *
     * <pre>new GeneralPath(GeneralPath.WIND_EVEN_ODD)</pre>
     *
     * @param path    the path to extend or <code>null</code>
     * @param shape   the array of shapes to append
     * @param connect the array of connection specifications or <code>null</code>
     * @return the path as modified
     */
    public static GeneralPath append(
        GeneralPath path,
        Shape[]     shape,
        boolean     connect)
    {
        if (shape == null)
            return path;
        
        int N = shape.length;
        
        boolean[] array = new boolean[N+1];
        
        for (int i = 0; i <= N; i++)
            array[i] = connect;
        
        return append(path, shape, array);
    }
    
    
    /**
     * <p>Shows the shape structure in the given graphics context.</p>
     * 
     * <p>If the graphics context or the shape is <code>null</code>,
     * then does nothing.</p>
     *
     * <p>Uses <code>PathList.makeStructurePaintable</code> and follows
     * the conventions described in that class.</p>
     *
     * @param g                the graphics context
     * @param shape            the shape
     * @param fillColor        the fill color for the shape
     * @param drawColor        the draw color for the shape
     * @param vertexDotsColor  the color for the vertex dots
     * @param controlDotsColor the color for the control dots
     * @param vertexFrameColor the color for the vertex frame
     * @param bezierFrameColor the color for the bezier frame
     * @param thickness        the stroke thickness
     */
    public static void showShapeStructure
        (Graphics g,
         Shape shape,
         Color fillColor,
         Color drawColor,
         Color vertexDotsColor,
         Color controlDotsColor,
         Color vertexFrameColor,
         Color bezierFrameColor,
         int   thickness)
    {
        if ((g == null) || (shape == null))
            return;
        
        PathList pathlist = new PathList(shape);
        
        Paintable paintable =
            pathlist.makeStructurePaintable
                (fillColor, drawColor, vertexDotsColor, controlDotsColor,
                 vertexFrameColor, bezierFrameColor, thickness);
        
        paintable.paint(g);
    }
    
    
    /**
     * <p>Shows the shape structure in the given graphics context.</p>
     * 
     * <p>If the graphics context or the shape is <code>null</code>,
     * then does nothing.</p>
     *
     * <p>Uses <code>PathList.makeStructurePaintable</code> and follows
     * the conventions described in that class.</p>
     *
     * @param g                the graphics context
     * @param shape            the shape
     * @param fillColor        the fill color for the shape
     * @param drawColor        the draw color for the shape
     * @param thickness        the stroke thickness
     */
    public static void showShapeStructure
        (Graphics g,
         Shape shape,
         Color fillColor,
         Color drawColor,
         int   thickness)
    {
        if ((g == null) || (shape == null))
            return;
        
        PathList pathlist = new PathList(shape);
        
        Paintable paintable =
            pathlist.makeStructurePaintable
                (fillColor, drawColor, thickness);
        
        paintable.paint(g);
    }
    
    
    /**
     * <p>Shows the shape structure in the given graphics context.</p>
     * 
     * <p>If the graphics context or the shape is <code>null</code>,
     * then does nothing.</p>
     *
     * <p>Uses <code>PathList.makeStructurePaintable</code> and follows
     * the conventions described in that class.</p>
     *
     * @param g                the graphics context
     * @param shape            the shape
     * @param thickness        the stroke thickness
     */
    public static void showShapeStructure
        (Graphics g, Shape shape, int thickness)
    {
        if ((g == null) || (shape == null))
            return;
        
        PathList pathlist = new PathList(shape);
        
        Paintable paintable = pathlist.makeStructurePaintable(thickness);
        
        paintable.paint(g);
    }
    
    
    /**
     * <p>Shows the shape structure in the given graphics context.</p>
     * 
     * <p>If the graphics context or the shape is <code>null</code>,
     * then does nothing.</p>
     *
     * <p>Uses <code>PathList.makeStructurePaintable</code> and follows
     * the conventions described in that class.</p>
     *
     * @param g                the graphics context
     * @param shape            the shape
     */
    public static void showShapeStructure
        (Graphics g, Shape shape)
    {
        if ((g == null) || (shape == null))
            return;
        
        PathList pathlist = new PathList(shape);
        
        Paintable paintable = pathlist.makeStructurePaintable();
        
        paintable.paint(g);
    }
    
    
    /**
     * <p>Returns the structural information about a <code>Shape</code>
     * as a large, multi-line <code>String</code> that contains all of
     * the information in the <code>PathIterator</code> for the shape.</p>
     *
     * <p>If the given shape is <code>null</code> returns "Null shape\n".</p>
     *
     * <p>If the given shape is non-<code>null</code> then, as of 2.4.0,
     * returns the <code>String</code>:</p>
     *
     * <pre>  new PathList(shape).toString()</pre>
     *
     * @return the shape information as a string
     */
    public static String shapeToString(Shape shape) {
        if (shape == null)
            return "Null shape\n";
        else
            return new PathList(shape).toString();
    }
    
}
