/*
 * @(#)XRect.java    2.4.0   24 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;

import edu.neu.ccs.util.*;

import java.awt.geom.*;
import java.util.*;
import java.text.ParseException;
import java.beans.PropertyChangeListener;
import javax.swing.event.SwingPropertyChangeSupport;
import java.io.Serializable;

/**
 * <p>Class <code>XRect</code> extends <code>Rectangle2D.Double</code> in
 * order to provide a much shorter name that does not explicitly refer to
 * an inner class, to add error checking that prevents the width and
 * height from being set to negative values,
 * and to support <code>Stringable</code>.</p>
 *
 * @author  Richard Rasala
 * @version 2.4.0
 * @since   2.4.0
 */
public class XRect extends Rectangle2D.Double
    implements Serializable, Stringable, JPTConstants
{
    /** The data array with blank names "", "", "", "". */
    private static String[] BLANK = { "", "", "", "" };
    
    /** The data array with names "x", "y", "w", "h". */
    private static String[] XYWH = { "x", "y", "w", "h" };
    
    /** The data array with names "xc", "yc", "r", "s". */
    private static String[] XYRS = { "xc", "yc", "r", "s" };
    
    /** The data array with names "x1", "y1", "x2", "y2". */
    private static String[] X1Y1X2Y2 = { "x1", "y1", "x2", "y2" };
    
    /** The standard error message for fromStringData. */
    public static final String standardMessage =
        "\nXRect Error: Data format must be\n"
        + "[...;...;...;...] or\n"
        + "[x=...;y=...;w=...;h=...]\n"
        + "where ... stands for\n"
        + "corner coordinate data x,y\n"
        + "and width-height data w,h\n"
        + "or must be\n"
        + "[xc=...;yc=...;r=...;s=...]\n"
        + "where ... stands for\n"
        + "center coordinate data xc,yc\n"
        + "and radius data r,s\n"
        + "or must be\n"
        + "[x1=...;y1=...;x2=...;y2=...]\n"
        + "where ... stands for\n"
        + "opposite`corner coordinate data\n"
        + "x1,y1 and x2,y2\n";
    
    
    /** Helper object for property change API. */
    protected SwingPropertyChangeSupport changeAdapter =
        new SwingPropertyChangeSupport(this);
    
    
    /**
     * Constructs a new <code>XRect</code> initialized to location (0, 0)
     * and size (0, 0).
     */
    public XRect() {}
    
    
    /**
     * Constructs and initializes a <code>XRect</code> from the specified
     * double coordinates.  Calls <code>setXYWH</code>.
     *
     * @param x the proposed top-left x-coordinate
     * @param y the proposed top-left y-coordinate
     * @param w the proposed width
     * @param h the proposed height
     */
    public XRect(double x, double y, double w, double h) {
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Constructs and initializes a <code>XRect</code> from the specified
     * Rectangle2D object.  Calls <code>setXYWH</code>.</p>
     *
     * <p>Initializes to location (0, 0) and size (0, 0) if the given rect
     * is <code>null</code>.</p>
     *
     * @param rect the Rectangle2D object to copy
     */
    public XRect(Rectangle2D rect) {
        setRect(rect);
    }
    
    
    /**
     * <p>Constructs a new <code>XRect</code> initialized to center (0, 0)
     * and with the given radii: r = half-width and s = half-height.
     * Calls <code>setXYRS</code></p>
     *
     * @param r the proposed x-radius or half-width
     * @param s the proposed y-radius or half-height
     */
    public XRect(double r, double s) {
        setXYRS(0, 0, r, s);
    }
    
    
    /**
     * <p>Returns a human readable <CODE>String</CODE> representing
     * the data state of this <CODE>XRect</CODE> as an annotated
     * string.</p>
     *
     * <p><code>XRect[x=...;y=...;w=...;h=...]</code></p>
     *
     * <p>where the dots stand for the x,y corner data and
     * the w,h dimension data.</p>
     */
    public String toString() {
        double x = getX();
        double y = getY();
        double w = getWidth();
        double h = getHeight();
        
        return "XRect[x=" + x + ";y=" + y + ";w=" + w + ";h=" + h + "]";
    }
    
    
    /**
     * <p>Returns a human readable <CODE>String</CODE> representing
     * the data state of this <CODE>XRect</CODE> as an annotated
     * string.</p>
     *
     * <p><code>XRect[xc=...;yc=...;r=...;s=...]</code></p>
     *
     * <p>where the dots stand for the xc,yc center data and
     * the r,s radius data.</p>
     */
    public String toStringAsXYRS() {
        double xc = getCenterX();
        double yc = getCenterY();
        double r  = getWidth()  / 2;
        double s  = getHeight() / 2;
        
        return "XRect[xc=" + xc + ";yc=" + yc + ";r=" + r + ";s=" + s + "]";
    }
    
    
    /**
     * <p>Returns a human readable <CODE>String</CODE> representing
     * the data state of this <CODE>XRect</CODE> as an annotated
     * string.</p>
     *
     * <p><code>XRect[x1=...;y1=...;x2=...;y2=...]</code></p>
     *
     * <p>where the dots stand for the x1,y1,x2,y2 corner data.</p>
     */
    public String toStringAsX1Y1X2Y2() {
        double x1 = getMinX();
        double y1 = getMinY();
        double x2 = getMaxX();
        double y2 = getMaxY();
        
        return "XRect[x1=" + x1 + ";y1=" + y1 + ";x2=" + x2 + ";y2=" + y2 + "]";
    }
    
    
    /**
     * <p>Returns a human readable <CODE>String</CODE> representing
     * the data state of this <CODE>XRect</CODE> as a simple
     * string.</p>
     *
     * <p><code>[...;...;...;...]</code></p>
     *
     * <p>where the dots stand for the x,y corner data and
     * the w,h dimension data.</p>
     */
    public String toStringData() {
        double x = getX();
        double y = getY();
        double w = getWidth();
        double h = getHeight();
        
        return "[" + x + ";" + y + ";" + w + ";" + h + "]";
    }
    
    
    /**
     * <p>Defines the data state for this <CODE>XRect</CODE> object 
     * from a <CODE>String</CODE> representation of the data state.</p>
     * 
     * <p>Fires property change VALUE.</p>
     *
     * @param data <CODE>String</CODE> representation of the data state
     * @throws ParseException if the data is malformed
     */
    public void fromStringData(String data)
        throws ParseException
    {
        if (data == null)
            throw new ParseException(standardMessage, -1);

        String[] strings = Strings.decode(data);
            
        if (strings == null)
            throw new ParseException(standardMessage, -1);
        
        if (strings.length != 4)
            throw new ParseException(standardMessage, -1);
        
        String[] names  = Strings.getNames (strings);
        String[] values = Strings.getValues(strings);
        
        // check for valid names before parsing double values
        // to permit specific error messages
        
        if ((Arrays.equals(names, BLANK)) || (Arrays.equals(names, XYWH)))
            names = XYWH;
        else
        if (Arrays.equals(names, XYRS))
            names = XYRS;
        else
        if (Arrays.equals(names, X1Y1X2Y2))
            names = X1Y1X2Y2;
        else
            throw new ParseException(standardMessage, -1);
        
        // parse double values
        
        double[] result = null;
        
        try {
            result = Strings.stringsToDoubles(values);
        }
        catch (ParseException ex) {
            throw Strings.makeAdjustedParseException(ex, "XRect", names); 
        }
        
        // store double values depending on names
        
        if (names == XYWH) {
            setXYWH(result[0], result[1], result[2], result[3]);
            return;
        }
        
        if (names == XYRS) {
            setXYRS(result[0], result[1], result[2], result[3]);
            return;
        }
        
        if (names == X1Y1X2Y2) {
            setX1Y1X2Y2(result[0], result[1], result[2], result[3]);
            return;
        }
    }
    
        
    /**
     * <p>Sets the parameters of the <code>XRect</code> with error checking.</p>
     *
     * <p>If the width w  is negative, then w is replaced by its absolute value
     * and then x is replaced by (x - w).</p>
     *
     * <p>If the height h is negative, then h is replaced by its absolute value
     * and then y is replaced by (y - h).</p>
     *
     * <p>This method guarantees that the internal width and height are always
     * greater than or equal to zero.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param x the proposed top-left x-coordinate
     * @param y the proposed top-left y-coordinate
     * @param w the proposed width
     * @param h the proposed height
     */
    public void setXYWH(double x, double y, double w, double h)
    {
        if (w < 0) {
            w = Math.abs(w);
            x -= w;
        }

        if (h < 0) {
            h = Math.abs(h);
            y -= h;
        }

        super.setRect(x, y, w, h);
        
        // notify listeners of property change
        changeAdapter.firePropertyChange(VALUE, null, null);
    }
    
    
    /**
     * <p>Sets the parameters of the <code>XRect</code> with error checking
     * using the center (x,y) and the inner radii: r = half-width and
     * s = half-height.</p>
     *
     * <p>If the radius r is negative, then r is replaced by its absolute value.</p>
     *
     * <p>If the radius s is negative, then s is replaced by its absolute value.</p>
     *
     * <p>This method guarantees that the internal width and height are always
     * greater than or equal to zero.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param x the proposed center x-coordinate
     * @param y the proposed center y-coordinate
     * @param r the proposed x-radius or half-width
     * @param s the proposed y-radius or half-height
     */
    public void setXYRS(double x, double y, double r, double s)
    {
        r = Math.abs(r);
        s = Math.abs(s);
        
        setXYWH(x-r, y-s, 2*r, 2*s);
    }
    
    
    /**
     * <p>Sets the parameters of the <code>XRect</code> with error checking
     * using the coordinates of any pair of opposite corners.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param x1 the x-coordinate of one corner
     * @param y1 the y-coordinate of one corner
     * @param x2 the x-coordinate of the opposite corner
     * @param y2 the y-coordinate of the opposite corner
     */
    public void setX1Y1X2Y2(double x1, double y1, double x2, double y2)
    {
        setXYWH(x1, y1, x2 - x1, y2 - y1);
    }
    
    
    /**
     * <p>Overrides the corresponding inherited method to use the method
     * <code>setXYWH</code>.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param x the proposed top-left x-coordinate
     * @param y the proposed top-left y-coordinate
     * @param w the proposed width
     * @param h the proposed height
     */
    public void setFrame(double x, double y, double w, double h) {
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Overrides the corresponding inherited method to use the method
     * <code>setXYWH</code>.</p>
     *
     * <p>Does nothing if the given rect is <code>null</code>.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param rect the Rectangle2D object to copy
     */
    public void setFrame(Rectangle2D rect) {
        if (rect == null)
            return;
        
        double x = rect.getX();
        double y = rect.getY();
        double w = rect.getWidth();
        double h = rect.getHeight();
        
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Overrides the corresponding inherited method to use the method
     * <code>setXYWH</code>.</p>
     *
     * <p>Does nothing if the given point or dimension is <code>null</code>.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param point the top-left corner
     * @param dimension the dimension with the width and height
     */
    public void setFrame(Point2D point, Dimension2D dimension) {
        if ((point == null) || (dimension == null))
            return;
        
        double x = point.getX();
        double y = point.getY();
        double w = dimension.getWidth();
        double h = dimension.getHeight();
        
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Overrides the corresponding inherited method to use the method
     * <code>setXYWH</code>.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param x the proposed top-left x-coordinate
     * @param y the proposed top-left y-coordinate
     * @param w the proposed width
     * @param h the proposed height
     */
    public void setRect(double x, double y, double w, double h) {
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Overrides the corresponding inherited method to use the method
     * <code>setXYWH</code>.</p>
     *
     * <p>Does nothing if the given rect is <code>null</code>.</p>
     *
     * <p>Fires property change VALUE.</p>
     *
     * @param rect the Rectangle2D object to copy
     */
    public void setRect(Rectangle2D rect) {
        if (rect == null)
            return;
        
        double x = rect.getX();
        double y = rect.getY();
        double w = rect.getWidth();
        double h = rect.getHeight();
        
        setXYWH(x, y, w, h);
    }
    
    
    /**
     * <p>Moves the rectangle by a translation using the data in the point
     * specified by coordinates.</p>
     *
     * @param dx the x-coordinate of the translation
     * @param dy the y-coordinate of the translation
     */
    public void move(double dx, double dy) {
        double x = getX();
        double y = getY();
        double w = getWidth();
        double h = getHeight();
        
        setXYWH(x + dx, y + dy, w, h);
    }
    
    
    /**
     * <p>Moves the rectangle by a translation using the data in the point.</p>
     *
     * @param p the translation vector
     */
    public void move(Point2D p) {
        if (p == null)
            return;
        
        move(p.getX(), p.getY());
    }
    
    
    /**
     * <p>Mimics the inherited method <code>createIntersection</code>
     * but makes the return type <code>XRect</code>.</p>
     *
     * @param rect the rectangle to intersect with this rectangle
     * @return returns the largest rectangle contained within r and
     *         this rectangle
     */
    public XRect createIntersectionRect(Rectangle2D rect) {
        return new XRect(createIntersection(rect));
    }
    
    
    /**
     * <p>Mimics the inherited method <code>createUnion</code>
     * but makes the return type <code>XRect</code>.</p>
     *
     * @param rect the rectangle to union with this rectangle
     * @return returns the smallest rectangle that contains r and
     *         this rectangle
     */
    public XRect createUnionRect(Rectangle2D rect) {
        return new XRect(createUnion(rect));
    }
    
    
    /////////////////////////////
    // 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);
    }
    
}

