// (c) MIT 2003.  All rights reserved.

////////////////////////////////////////////////////////////////////////////////
//
// FILE:	GArc.java
//
// AUTHOR:	Manoj D. Muzumdar
//		archon@ai.mit.edu
//
// DATE:	Jun 98
//
// COPYRIGHT:	Massachusetts Institute of Technology
//		All rights reserved.
//
// PURPOSE:	Geometric modeling
//
// HISTORY:     Jun 98  archon  created
//
//              Nov 98  archon
//              Made mutable.
//
//              Mar 99  archon
//              Fixed bugs.
//
////////////////////////////////////////////////////////////////////////////////

package rec.geo;

import java.awt.*;
import rec.util.*;

/**
 * GArc represents an arc.
 */
public class GArc extends GShape
{
  /**
   * Creates an arc with center center,
   *      x-axis length (2 * aLength), and
   *      y-axis length (2 * bLength) rotated angle radians.
   * The arc spans from beginRadians to endRadians.
   */
  public GArc( GPoint center, double radians, double aLength, double bLength,
	       double beginRadians, double endRadians )
  {
    Assert.check( aLength >= 0.0 && bLength >= 0.0,
		  "arcs cannot have negative axis lengths" );

    m_Center = new GPoint( center );
    m_Rotation = Radian.reduce( radians );
    m_ALength = aLength;
    m_BLength = bLength;
    m_BeginRadians = Radian.reduce( beginRadians );
    m_EndRadians = Radian.reduce( endRadians );
  }

  public GArc( GArc ga )
  {
    m_Center = new GPoint( ga.m_Center );
    m_Rotation = ga.m_Rotation;
    m_ALength = ga.m_ALength;
    m_BLength = ga.m_BLength;
    m_BeginRadians = ga.m_BeginRadians;
    m_EndRadians = ga.m_EndRadians;
  }

  //////////////////////////////////////////////////////////////////////////////

  public GShape copy()
  {
    return( new GArc( this ) );
  }

  public void reflect()
  {
    m_Center.reflect();
    m_Rotation = Radian.reduce( Radian.reduce( Math.PI / 4.0 - m_Rotation )
                                + Math.PI / 4.0 );
    m_BeginRadians = Radian.reduce(
      Radian.reduce( Math.PI / 4.0 - m_BeginRadians )
      + Math.PI / 4.0 );
    m_EndRadians = Radian.reduce(
      Radian.reduce( Math.PI / 4.0 - m_EndRadians )
      + Math.PI / 4.0 );
  }

  public void translate( double dx, double dy )
  {
    m_Center.translate( dx, dy );
  }

  public void rotate( GPoint gp, double radians )
  {
    m_Center.rotate( gp, radians );

    GPoint head = getPoint( m_BeginRadians );
    GPoint tail = getPoint( m_EndRadians );

    head.rotate( gp, radians );
    tail.rotate( gp, radians );

    m_Rotation = Geometry.angle( tail, head );
  }

  //////////////////////////////////////////////////////////////////////////////

  public double size()
  {
    // Adds the lengths of the A and B axes.
    return( m_ALength + m_BLength );
  }

  public void resize( double scaleFactor )
  {
    // archon: Does nothing right now!
  }

  //////////////////////////////////////////////////////////////////////////////

  public Rectangle getBounds()
  {
    GPoint bp = getPoint( m_BeginRadians );
    GPoint ep = getPoint( m_EndRadians );

    int minX = Math.min( bp.getIntX(), ep.getIntX() ) - 1;
    int minY = Math.min( bp.getIntY(), ep.getIntY() ) - 1;

    int maxX = Math.max( bp.getIntX(), ep.getIntX() ) + 1;
    int maxY = Math.max( bp.getIntY(), ep.getIntY() ) + 1;

    double incr = 1.0 / (m_ALength + m_BLength);

    if( m_BeginRadians <= m_EndRadians ) {

      for( double d = m_BeginRadians + incr; d < m_EndRadians; d += incr ) {
	GPoint gp = getPoint( d );
	minX = Math.min( minX, gp.getIntX() - 1 );
	maxX = Math.max( maxX, gp.getIntX() + 1 );
	minY = Math.min( minY, gp.getIntY() - 1 );
	maxY = Math.max( maxY, gp.getIntY() + 1 );
      }
    }
    else {

      // The radian range is over the overlap from 2.0 * Math.PI to 0.0.

      for( double d = m_BeginRadians + incr; d < 2.0 * Math.PI; d += incr ) {
	GPoint gp = getPoint( d );
	minX = Math.min( minX, gp.getIntX() - 1 );
	maxX = Math.max( maxX, gp.getIntX() + 1 );
	minY = Math.min( minY, gp.getIntY() - 1 );
	maxY = Math.max( maxY, gp.getIntY() + 1 );
      }

      for( double d = 0.0; d < m_EndRadians; d += incr ) {
	GPoint gp = getPoint( d );
	minX = Math.min( minX, gp.getIntX() - 1 );
	maxX = Math.max( maxX, gp.getIntX() + 1 );
	minY = Math.min( minY, gp.getIntY() - 1 );
	maxY = Math.max( maxY, gp.getIntY() + 1 );      }
    }

    /*
    double CosR = Geometry.cos( m_Rotation );
    double SinR = Geometry.sin( m_Rotation );

    GPoint bp = getPoint( m_BeginRadians );
    GPoint ep = getPoint( m_EndRadians );

    int minX = Math.min( bp.getIntX(), ep.getIntX() ) - 1;
    int minY = Math.min( bp.getIntY(), ep.getIntY() ) - 1;

    int maxX = Math.max( bp.getIntX(), ep.getIntX() ) + 1;
    int maxY = Math.max( bp.getIntY(), ep.getIntY() ) + 1;

    double xT = Radian.reduce(
      Math.atan( -1.0 * m_BLength * SinR / m_ALength * CosR ) );
    double yT = Radian.reduce(
      Math.atan( m_BLength * CosR / m_ALength * SinR ) );

    if( isInRange( xT ) ) {
      GPoint gp = getPoint( xT );
      minX = Math.min( minX, gp.getIntX() - 1 );
      maxX = Math.max( maxX, gp.getIntX() + 1 );
    }

    if( isInRange( Radian.reduce( xT + Math.PI ) ) ) {
      GPoint gp = getPoint( Radian.reduce( xT + Math.PI ) );
      minX = Math.min( minX, gp.getIntX() - 1 );
      maxX = Math.max( maxX, gp.getIntX() + 1 );
    }

    if( isInRange( yT ) ) {
      GPoint gp = getPoint( yT );
      minY = Math.min( minY, gp.getIntY() - 1 );
      maxY = Math.max( maxY, gp.getIntY() + 1 );
    }

    if( isInRange( Radian.reduce( yT + Math.PI ) ) ) {
      GPoint gp = getPoint( Radian.reduce( yT + Math.PI ) );
      minY = Math.min( minX, gp.getIntY() - 1 );
      maxY = Math.max( maxX, gp.getIntY() + 1 );
    }
    */

    return( new Rectangle( minX, minY,
			   maxX - minX, maxY - minY ) );
  }

  //////////////////////////////////////////////////////////////////////////////

  public void display( Graphics g )
  {
    double incr = 1.0 / (m_ALength + m_BLength);

    if( m_BeginRadians <= m_EndRadians ) {

      GPoint prev = getPoint( m_BeginRadians );

      for( double d = m_BeginRadians + incr; d <= m_EndRadians; d += incr ) {
	GPoint curr = getPoint( d );
	g.drawLine( prev.getIntX(), prev.getIntY(),
		    curr.getIntX(), curr.getIntY() );
	prev = curr;
      }

      return;
    }

    // The radian range is over the overlap from 2.0 * Math.PI to 0.0.

    GPoint prev = getPoint( m_BeginRadians );

    for( double d = m_BeginRadians + incr; d < 2.0 * Math.PI; d += incr ) {
      GPoint curr = getPoint( d );
      g.drawLine( prev.getIntX(), prev.getIntY(),
		  curr.getIntX(), curr.getIntY() );
      prev = curr;
    }

    for( double d = 0.0; d <= m_EndRadians; d += incr ) {
      GPoint curr = getPoint( d );
      g.drawLine( prev.getIntX(), prev.getIntY(),
		  curr.getIntX(), curr.getIntY() );
      prev = curr;
    }
  }

  //////////////////////////////////////////////////////////////////////////////

  public GPoint getPoint( double radians )
  {
    double x = m_ALength * Geometry.cos( radians );
    double y = m_BLength * Geometry.sin( radians );

    double CosR = Geometry.cos( m_Rotation );
    double SinR = Geometry.sin( m_Rotation );

    return( new GPoint( x * CosR - y * SinR + m_Center.x,
			x * SinR + y * CosR + m_Center.y ) );
  }

  //////////////////////////////////////////////////////////////////////////////

  public boolean includes( GPoint gp )
  {
    return( includes( gp, 2.0 ) );

    /*
    if( !isInRange( Geometry.angle( m_Center, gp ) + m_Rotation ) )
      return( false );

    double xD = gp.x - m_Center.x;
    double yD = gp.y - m_Center.y;
    double cosT = Geometry.cos( m_Rotation );
    double sinT = Geometry.sin( m_Rotation );

    double x1 = Math.pow( xD * cosT + yD * sinT, 2 );
    double y1 = Math.pow( yD * cosT - xD * sinT, 2 );

    return( x1 / Math.pow( m_ALength, 2 )
            + y1 / Math.pow( m_BLength, 2 ) == 1  );
    */
  }

  public boolean contains( GPoint gp )
  {
    Assert.error( "Not implemented" );
    return false;
  }

  public boolean includes( GPoint gp, double offset )
  {
    Assert.check( offset >= 0.0, "offset cannot be negative" );

    double incr = 1.0 / (m_ALength + m_BLength);

    if( m_BeginRadians <= m_EndRadians ) {

      GPoint prev = getPoint( m_BeginRadians );

      for( double d = m_BeginRadians; d <= m_EndRadians; d += incr ) {
	GPoint p = getPoint( d );
	if( Geometry.distance( gp, p ) <= offset )
	  return( true );
      }
    }
    else {

      // The radian range is over the overlap from 2.0 * Math.PI to 0.0.

      for( double d = m_BeginRadians; d < 2.0 * Math.PI; d += incr ) {
	GPoint p = getPoint( d );
	if( Geometry.distance( gp, p ) <= offset )
	  return( true );
      }

      for( double d = 0.0; d <= m_EndRadians; d += incr ) {
	GPoint p = getPoint( d );
	if( Geometry.distance( gp, p ) <= offset )
	  return( true );
      }
    }

    return( false );

    /*
    if( Geometry.distance( getPoint( m_BeginRadians ), gp ) <= offset )
      return( true );

    if( Geometry.distance( getPoint( m_EndRadians ), gp ) <= offset )
      return( true );

    if( !isInRange( Geometry.angle( m_Center, gp ) + m_Rotation ) )
      return( false );

    double xD = gp.x - m_Center.x;
    double yD = gp.y - m_Center.y;
    double cosT = Geometry.cos( m_Rotation );
    double sinT = Geometry.sin( m_Rotation );

    double x1 = Math.pow( xD * cosT + yD * sinT, 2 );
    double y1 = Math.pow( yD * cosT - xD * sinT, 2 );

    if( x1 / Math.pow( m_ALength + offset, 2 )
        + y1 / Math.pow( m_BLength + offset, 2 ) > 1 )
      return( false );

    return( x1 / Math.pow( m_ALength - offset, 2 )
            + y1 / Math.pow( m_BLength - offset, 2 ) >= 1 );
    */
  }

  //////////////////////////////////////////////////////////////////////////////

  public GPoint getCenter()
  {
    return( new GPoint( m_Center ) );
  }

  public GSegment getMajorAxis()
  {
    if( m_ALength >= m_BLength )
      return( new GSegment( getPoint( Radian.reduce( m_Rotation + Math.PI ) ),
			    getPoint( m_Rotation ) ) );

    return( new GSegment(
      getPoint( Radian.reduce( m_Rotation + Math.PI / 2.0 ) ),
      getPoint( m_Rotation + 3.0 / 2.0 * Math.PI ) ) );
  }

  public GSegment getMinorAxis()
  {
    if( m_ALength < m_BLength )
      return( new GSegment( getPoint( Radian.reduce( m_Rotation + Math.PI ) ),
			    getPoint( m_Rotation ) ) );

    return( new GSegment(
      getPoint( Radian.reduce( m_Rotation + Math.PI / 2.0 ) ),
      getPoint( m_Rotation + 3.0 / 2.0 * Math.PI ) ) );
  }

  public boolean isInRange( double radians )
  {
    double reducedRadians = Radian.reduce( radians );

    if( m_EndRadians >= m_BeginRadians )
      return( reducedRadians >= m_BeginRadians
	      && reducedRadians <= m_EndRadians );

    // The radian range is over the overlap from
    //      2.0 * Math.PI to 0.0.
    return( reducedRadians <= m_BeginRadians
	    || reducedRadians >= m_EndRadians );
  }

  //////////////////////////////////////////////////////////////////////////////

  public double getRotation()
  {
    return( m_Rotation );
  }

  public double getALength()
  {
    return( m_ALength );
  }

  public double getBLength()
  {
    return( m_BLength );
  }

  public double getBeginRadians()
  {
    return( m_BeginRadians );
  }

  public double getEndRadians()
  {
    return( m_EndRadians );
  }

  //////////////////////////////////////////////////////////////////////////////

  public GPoint getHead()
  {
    return( getPoint( m_BeginRadians ) );
  }

  public GPoint getTail()
  {
    return( getPoint( m_EndRadians ) );
  }

  public GPoint getMidpoint()
  {
    if( m_BeginRadians <= m_EndRadians )
      return( getPoint( (m_BeginRadians + m_EndRadians) / 2.0 ) );

    // CASE: m_BeginRadians > m_EndRadians.

    return( getPoint(
	      Radian.reduce( (m_BeginRadians + m_EndRadians + 2.0 * Math.PI)
			     / 2.0 ) ) );
  }

  //////////////////////////////////////////////////////////////////////////////

  private GPoint m_Center;
  private double m_Rotation;
  private double m_ALength;
  private double m_BLength;
  private double m_BeginRadians;
  private double m_EndRadians;
}

////////////////////////////////////////////////////////////////////////////////
