/*
 * @(#)XPoint2D.java    2.2  17 September 2002
 *
 * Copyright 2004
 * 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;

import edu.neu.ccs.codec.*;
import java.awt.geom.*;
import java.text.ParseException;
import java.util.*;
import java.beans.*;
import java.io.Serializable;
import javax.swing.event.SwingPropertyChangeSupport;

/**
 * <P>Data model representing a point in graphics, 
 * containing a <CODE>double</CODE> <I>x</I> coordinate 
 * and a <CODE>double</CODE> <I>y</I> coordinate.</P>
 *
 * @author  Viera K. Proulx
 * @author  Richard Rasala
 * @version 2.2
 * @since   1.1
 */
public final class XPoint2D 
	extends Point2D.Double
    implements Cloneable, Serializable, Stringable 
{
    /** Bound property name for the x property. */
    protected static final String X_PROPERTY = "x";

    /** Bound property name for the y property. */
    protected static final String Y_PROPERTY = "y";

    /////////////////
    // Member Data //
    /////////////////
    
    /** Helper object for property change API. */
    protected SwingPropertyChangeSupport changeAdapter =
        new SwingPropertyChangeSupport(this);
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs a wrapper for the default
     * <CODE>Point2D.Double</CODE> value.
     *
     * @see #XPoint2D(Point2D)
     * @see #XPoint2D(double, double)
     * @see #XPoint2D(String)
     */
    public XPoint2D() {}

    /**
     * Constructs a wrapper for the given
     * <CODE>Point2D</CODE> value.
     * 
     * @param p the value to be wrapped
     * @see #XPoint2D()
     * @see #XPoint2D(double, double)
     * @see #XPoint2D(String)
     * @see #setValue(Point2D)
     */
    public XPoint2D(Point2D p) {
    	setValue(p);
    }
    
    /**
     * Constructs a wrapper for the given
     * <CODE>double</CODE> values.
     * 
     * @param x the x value to be wrapped
     * @param y the y value to be wrapped
     * @see #XPoint2D()
     * @see #XPoint2D(Point2D)
     * @see #XPoint2D(String)
     * @see #setValue(double, double)
     */
    public XPoint2D(double x, double y) {
        setValue(x, y);
    }
    

    /**
     * Constructs an <CODE>XPoint2D</CODE> object from
     * a <CODE>String</CODE> representation of the data state.
     * 
     * @param data <CODE>String</CODE> representation of the desired value
     * @throws ParseException if the data is malformed
     * @see #XPoint2D()
     * @see #XPoint2D(double, double)
     * @see #XPoint2D(Point2D)
     * @see #toStringData()
     * @see #fromStringData(String)
     * @see #toString()
     */
    public XPoint2D(String data) throws ParseException {
        fromStringData(data);
    }
    
    ////////////////
    // Stringable //
    ////////////////
    
    /**
     * Returns a <CODE>String</CODE> representation of the data state
     * for this <CODE>XPoint2D</CODE> object.
     *
     * @see #XPoint2D(String)
     * @see #fromStringData(String)
     * @see #toString()
     */
    public String toStringData() {
        return toString();
    }
    
    /**
     * Extracts the data state for this <CODE>XPoint2D</CODE> object 
     * from a <CODE>String</CODE> representation of the data state.
     * 
     * @param data <CODE>String</CODE> representation 
     *      of the desired value
     * @throws ParseException if the data is malformed
     * @see #XPoint2D(String)
     * @see #toStringData()
     * @see #toString()
     * @see #decodeIntoPair(String)
     * @see #setValue(double, double)
     */
    public void fromStringData(String data)
        throws ParseException
    {
        if (data == null)
            throw new ParseException("Input was null.", -1);

        String[] pair = decodeIntoPair(data);
        
        double xx = XDouble.parseDouble(pair[0]);
        double yy = XDouble.parseDouble(pair[1]);
        
        setValue(xx, yy);
    }
    
    /**
     * Returns a human readable <CODE>String</CODE> representing the
     * data state of this <CODE>XPoint2D</CODE> as [x;y].
     *
     * @see #XPoint2D(String)
     * @see #toStringData()
     * @see #fromStringData(String)
     */
    public String toString() {
        return "[" + x + ";" + y + "]";
    }

    /**
     * Decode a pair of numeric strings encoded using a standard codec
     * or encoded using the format [x;y] or the format (x,y).
     *
     * @param data the String to decode into a String pair
     * @see #fromStringData(String)
     */
    public static String[] decodeIntoPair(String data)
        throws ParseException
    {   
        final String NO_DATA =
            "Data string is missing or empty";
        
        final String NO_PAIR =
            "Expected [x;y] or (x,y) with x and y of type double.";
        
        if (data == null)
            throw new ParseException(NO_DATA, 0);
        
        String trim = data.trim();
        
        if (trim.length() == 0)
            throw new ParseException(NO_DATA, 0);
        
        String[] pair = null;
        int last = trim.length() - 1;
        
        boolean isSimple
            =  (trim.charAt(0) == '[') && (trim.charAt(last) == ']')
            || (trim.charAt(0) == '(') && (trim.charAt(last) == ')');
        
        // handle data encoded as [x;y] or (x,y)
        if (isSimple) {
            String separator = (trim.charAt(0) == '[') ? ";" : ",";
            String XY = trim.substring(1, last);
            
            StringTokenizer tokenizer = new StringTokenizer(XY, separator);
            int tokencount = tokenizer.countTokens();
          
            if (tokencount == 2) {
                pair = new String[2];
                pair[0] = tokenizer.nextToken();
                pair[1] = tokenizer.nextToken();            
            }
            else {
                throw new ParseException(
                    NO_PAIR, 
                    data.length());
            }
        }
        // handle data encoded by a standard codec
        else {
            pair = CodecUtilities.decode(data);
        }
        
        return pair;
    }
    
    ////////////////
    // Public API //
    ////////////////
        
    /**
     * Sets the value wrapped by this object 
     * to the given value.
     *
     * @param p the value to be wrapped
     * @see #setValue(double, double)
     */
    public void setValue(Point2D p) {
    	if (p != null)
			setValue(p.getX(), p.getY());        
    }
    
    /**
     * Sets the values wrapped by this object to the given values.
     *
     * @param newX the x coordinate
     * @param newY the y coordinate
     * @see #setValue(Point2D)
     */
    public void setValue(double newX, double newY) {
        double oldX = x;
        double oldY = y;
        
        x = newX;
        y = newY;
        
        // if x has changed
        if (newX != oldX) {
        
            // notify listeners of property change
            changeAdapter.firePropertyChange(
                X_PROPERTY,
                new java.lang.Double(oldX),
                new java.lang.Double(newX));
        }
        
        // if y has changed
        if (newY != oldY) {
        
            // notify listeners of property change
            changeAdapter.firePropertyChange(
                Y_PROPERTY,
                new java.lang.Double(oldY),
                new java.lang.Double(newY));
        }
    }
    
    //////////////////////////
    // Mathematical Methods //
    //////////////////////////
    
    /**
     * Computes the distance of the point to the origin.
     *
     * @see #radiusSq()
     */
    public double radius() {
        return distance(0, 0);
    }
    
    /**
     * Computes the distance squared of the point to the origin.
     *
     * @see #radius()
     */
    public double radiusSq() {
        return distanceSq(0, 0);
    }
    
    /**
     * Computes the angle in radians measured counter-clockwise
     * from the positive x-axis.
     *
     * @see #angleInDegrees()
     */
    public double angleInRadians() {
        return angleInRadians(1, 0, x, y);
    }
    
    /**
     * Computes the angle in degrees measured counter-clockwise
     * from the positive x-axis.
     *
     * @see #angleInRadians()
     */
    public double angleInDegrees() {
        return Math.toDegrees(angleInRadians());
    }
    
    /////////////////////////////////
    // Static Mathematical Methods //
    /////////////////////////////////
    
    /**
     * Computes the angle in radians from
     * the point p to the point q
     * measured counter-clockwise.
     *
     * @param p point to start  angle measurement
     * @param q point to finish angle measurement
     * @see #angleInRadians(double, double, double, double)
     * @see #angleInDegrees(Point2D, Point2D)
     */
    public static double angleInRadians(Point2D p, Point2D q) {
        if ((p != null) && (q != null))
            return angleInRadians
                (p.getX(), p.getY(), q.getX(), q.getY());
        else
            return 0;
    }
    
    /**
     * Computes the angle in radians from the
     * the point (x1,y1) to the point (x2,y2)
     * measured counter-clockwise.
     *
     * @param x1 x-coordinate of point to start  angle measurement
     * @param y1 y-coordinate of point to start  angle measurement
     * @param x2 x-coordinate of point to finish angle measurement
     * @param y2 y-coordinate of point to finish angle measurement
     * @see #angleInRadians(Point2D, Point2D)
     * @see #angleInDegrees(double, double, double, double)
     */
    public static double angleInRadians
        (double x1, double y1, double x2, double y2)
    {
        double radians = Math.atan2(y2, x2) - Math.atan2(y1, x1);
        
        if (radians < 0)
            radians += 2 * Math.PI;
        
        return radians;
    }
    
    /**
     * Computes the angle in degrees from
     * the point p to the point q
     * measured counter-clockwise.
     *
     * @param p point to start  angle measurement
     * @param q point to finish angle measurement
     * @see #angleInDegrees(double, double, double, double)
     * @see #angleInRadians(Point2D, Point2D)
     */
    public static double angleInDegrees(Point2D p, Point2D q) {
        return Math.toDegrees(angleInRadians(p, q));
    }
    
    /**
     * Computes the angle in degrees from the
     * the point (x1,y1) to the point (x2,y2)
     * measured counter-clockwise.
     *
     * @param x1 x-coordinate of point to start  angle measurement
     * @param y1 y-coordinate of point to start  angle measurement
     * @param x2 x-coordinate of point to finish angle measurement
     * @param y2 y-coordinate of point to finish angle measurement
     * @see #angleInDegrees(Point2D, Point2D)
     * @see #angleInRadians(double, double, double, double)
     */
    public static double angleInDegrees
        (double x1, double y1, double x2, double y2)
    {
        return Math.toDegrees(angleInRadians(x1, y1, x2, y2));
    }
    
    /////////////////////////////
    // Property Change Support //
    /////////////////////////////
    
    /**
     * Registers the given object 
     * to listen for property change events 
     * generated by this object.
     *
     * @param listener the listener to be registered
     */
    public void addPropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(listener);
    }

    /**
     * Registers the given object 
     * to listen for property change events
     * generated by this object
     * with the given property name.
     *
     * @param propertyName the name of the desired property 
     * @param listener the listener to be registered
     */
    public void addPropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.addPropertyChangeListener(
            propertyName, 
            listener);
    }

    /**
     * Deregisters the given object 
     * from listening for property change events 
     * generated by this object.
     *
     * @param listener the listener to be deregistered
     */
    public void removePropertyChangeListener(
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(listener);
    }

    /**
     * Deregisters the given object 
     * from listening for property change events
     * generated by this object
     * with the given property name.
     *
     * @param propertyName the name of the desired property 
     * @param listener the listener to be deregistered
     */
    public void removePropertyChangeListener(
        String propertyName,
        PropertyChangeListener listener) 
    {
        changeAdapter.removePropertyChangeListener(
            propertyName, 
            listener);
    }
}
 
