////////////////////////////////////////////////////////////////////////////////
//																			  //
// AUTHOR:      Tevfik Metin Sezgin	                        				  //
//				Massachusetts Institute of Technology						  //
//				Department of Electrical Engineering and Computer Science	  //
//				Artificial Intelligence Laboratory							  //
//																			  //
// E-MAIL:		mtsezgin@ai.mit.edu, mtsezgin@mit.edu						  //
//																			  //
// COPYRIGHT:   Tevfik Metin Sezgin                    						  //
//              All rights reserved. This code can not be copied, modified,   //
//              or distributed in whole or partially without the written 	  //
//				permission of the author. Also see the COPYRIGHT file.		  //
//																			  //
////////////////////////////////////////////////////////////////////////////////

  /**
  *
  * See the end of the file for the log of changes.
  *
  * $Author: mtsezgin $
  * $Date: 2000/06/08 03:20:07 $
  * $Revision: 1.18 $
  * $Headers$
  * $Id: Tablet.java,v 1.18 2000/06/08 03:20:07 mtsezgin Exp $
  * $Name:  $
  * $Locker:  $
  * $Source: /mit/mtsezgin/CVS/Repository/REC/Tablet.java,v $
  *
  **/



import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;

/**
  *
  * This class is for reading mouse input and processing it.
  * Various fits are generated and stored here.
  * Acronyms: sdr -> StrokeDataReader, sd -> StrokeData
  *
  **/

class      TabletDataProcessor
extends    TabletGUI
implements MouseMotionListener, 
           MouseListener
{
	/**
	 * Stroke data reader used for reading and instantiating 
	 * StrokeData
	 **/
    public StrokeDataReader sdr;
	
	/**
	 * Stroke data 
	 **/
    public StrokeData data;
	
	public Vertex     final_fit[];
	public Vertex     speed_fit[];
	public Vertex     direction_fit[];
	
	public PolarPoint polar_points[];

	/**
	 * Parameters and constants.
	 **/
    public int        direction_window_width  = 5;
    public double     dd_dt_average_scale     = 0.6;
    public double     speed_average_scale     = 0.6;
    public double     test_line_scale         = 1.2;
    public double     combine_treshold        = 49;
    public double     orientation_angle       = Math.PI/180; // Was /24
    public double     turn_angle_treshold     = Math.PI/8;   // Was /6
	public double     circle_bias             = 0.5; // Multiply by circle_LSQE
    public Point      line[];

    public boolean  v_is_valid            = true;
    public boolean  a_is_valid            = true;
    public boolean  d_is_valid            = true;
    public boolean  dd_dt_is_valid        = true;
    public boolean  polar_points_is_valid = true;
    public boolean  in_recognition_mode   = true;
	
    public static boolean  platform_is_windows9x = false;
    public static boolean  platform_is_windowsNT = false;
    
    public static boolean		   debug;	 
    public static boolean		   debug2;    

	public static int    fit_method   = LinearFit.ROTATION_METHOD;
    public static double window_width = Math.PI/36;

    public static long    sequence_delay = 1500;
	public static double  LSQE_treshold  = 36.0;  // A max deviation of 6 pixels

    public ObjectManager  object_manager;
    public ControlModule  control_module;
        

    /**
    *
    * The constructor.
    *
    **/
    public TabletDataProcessor( int     max_data_points, 
            		 		    double  speed_average_scale,
            		 		    String  cursor_image_name,
            		 		    boolean platform_is_windows9x,
					 		 	boolean platform_is_windowsNT )
    {
        super( cursor_image_name );

        this.speed_average_scale   = speed_average_scale;
        this.platform_is_windows9x = platform_is_windows9x;
        this.platform_is_windowsNT = platform_is_windowsNT;

        object_manager = new ObjectManager( this );
        control_module = new ControlModule( this );

        control_module.setLocation( 500, 500 );
        
		sdr = new StrokeDataReader( max_data_points );
		addEventListeners();
    }

        
    /**
    *
    * add event listeners
    *
    **/
    public void
    addEventListeners()
    {
        addKeyListener( object_manager );
        addMouseListener( object_manager );
        addMouseMotionListener( object_manager );
		status_bar.addKeyListener( object_manager );
		
        addMouseListener( this );
        addMouseMotionListener( this );
	}


    /**
    *
    * Handle mouse events
    *
    **/
    public void
    mouseReleased( MouseEvent e )
    {
		Polygon    original_points;
		Vertex     black_board_result[];
		Vertex     points[];
		double     d[];
		
        boolean line_result;
        boolean circle_result;
        boolean polygon_result;
        
        double  speed_LSQ     = Double.MAX_VALUE;
        double  circle_LSQ    = Double.MAX_VALUE;
        double  final_fit_LSQ = Double.MAX_VALUE;

        
        if ( !in_recognition_mode || !sdr.hasSufficientData() )
            return;
			
		data = sdr.getStrokeData();
		data.fit_method = fit_method;
		data.deriveProperties();
        original_points = data.getDataPoints();
        line_result     = data.testLine( test_line_scale );
		points          = data.vertices;
		d               = data.d;

        if ( line_result ) {
            GeometricObject recognized_object = new 
				Line( points[0], points[points.length-1] );
            recognized_object.setDataPoints( original_points );
            recognized_object.setTimeStamp( getTimeStamp( e ) );
            object_manager.addObject( recognized_object ); 

            return;
        }
		
		speed_fit = data.getSpeedFit( speed_average_scale );
        
        circle_LSQ = data.leastSquaresForCircle();
		speed_LSQ  = Blackboard.leastSquaresForPolygon( speed_fit, points );
        
        if ( circle_LSQ*circle_bias > speed_LSQ ) {
            polygon_result = true;
            circle_result  = false;            
        } else {
            polygon_result = false;
            circle_result  = true;
        }
        
		direction_fit = data.getDirectionFit( dd_dt_average_scale );
		
        speed_fit     = simplifyPolygon( speed_fit );
        direction_fit = simplifyPolygon( direction_fit );
		
		
		if ( ( speed_fit     != null ) &&
			 ( direction_fit != null ) ) {
			black_board_result =  Blackboard.decide( speed_fit,
													 direction_fit,
													 points,
													 data );
			control_module.setHybridFitList( Blackboard.hybrid_fits );
			System.out.println( "Speed result      : " 
								+ speed_fit );
			System.out.println( "Direction result  : " 
								+ direction_fit );
			System.out.println( "Blackboard result : " 
								+ black_board_result );
								
			final_fit = black_board_result;
		} else {
			System.out.println( "Speed result or direction result is null" );
		}
		
		
        final_fit     = combineEndPoints( final_fit ); 
		final_fit_LSQ = Blackboard.leastSquaresForPolygon( final_fit, points );
        
        System.out.println( "LSQE for circle    : " + circle_LSQ    );
        System.out.println( "LSQE for speed fit : " + speed_LSQ     );
        System.out.println( "LSQE for final fit : " + final_fit_LSQ );
		
        if ( circle_result ) {
            GeometricObject recognized_object = data.getEllipse();
            recognized_object.setDataPoints( original_points );
            object_manager.addObject( recognized_object ); 
            recognized_object.setTimeStamp( getTimeStamp( e ) );
        }
        
        if ( polygon_result ) {
            GeometricObject recognized_object = new Polygon( final_fit );
            recognized_object.setDataPoints( original_points );
            recognized_object.setTimeStamp( getTimeStamp( e ) );
            object_manager.addObject( recognized_object ); 
        }
    }
    

    /**
    *
    * Set debugging mode
    *
    **/
    public void
    setDebugging( boolean d )
    {
        debug = d;
    }

	
    /**
    *
    * Handle mouse events
    *
    **/
    public void
    mouseClicked( MouseEvent e ) 
    {
    }
    

    /**
    *
    * Handle mouse events
    *
    **/
    public void
    mouseEntered( MouseEvent e ) 
    {
    }
    

    /**
    *
    * Handle mouse events
    *
    **/
    public void
    mouseExited( MouseEvent e ) 
    {
    }
    

    /**
    *
    * Handle mouse events
    *
    **/
    public void
    mousePressed( MouseEvent e ) 
    {
		reset();
        if ( in_recognition_mode ) {
	        addPoint( new Point( e.getPoint() ), getTimeStamp( e ) );
        }
    }

    
    /**
    *
    * Reset the arrays
    *
    **/
    public void
    reset() 
    {
        sdr.reset();
		speed_fit     = null;
        final_fit     = null;
        direction_fit = null;
    }


    /**
    *
    * Add a new point to sdr with t as the time stamp.
    *
    **/
    public void
    addPoint( Point p, long t ) 
    {
		Point point = new Point( p );
		point.setTimeStamp( t );
        sdr.addPoint( point );
    }


    /**
    *
    * Disable or enable recognition process by turning mouse event 
    * listening off or on. Set flag to be true if enabling is desired.
    * Set flag to be false if disabling is desired.
    *
    **/
    public void
    setRecognitionMode( boolean flag )
    {
        in_recognition_mode = flag;
    }


    /**
    *
    * Handle mouse motion events
    *
    **/
    public void
    mouseDragged( MouseEvent e ) 
    {
        if ( !in_recognition_mode )
            return;
			
		Point point = new Point( e.getPoint() );
        addPoint( point, getTimeStamp( e ) );
    }


    /**
    *
    * Handle mouse motion events
    *
    **/
    public void
    mouseMoved( MouseEvent e ) 
    {
    }
	
	
    /**
    * 
    * Combines the ends of the polygon if they are sufficiently close.
    * 
    **/
    public Vertex[]
    combineEndPoints( Vertex polygon[] )
    {
        if ( ( polygon == null ) || ( polygon.length < 2 ) ) {
            System.err.println("Error: combineEndPoints no polygon...");
            return null;
        }
        
        int l = polygon.length-1;
        
        if ( polygon[l].distance( polygon[0] ) < combine_treshold ) {
            polygon[0].x  = polygon[l].x;
            polygon[0].y  = polygon[l].y;
            System.out.println( "COMBINED ENDS OF THE POLYGON" );
        }
		
		return polygon;
    }


    /**
    * 
    * Simplify the polygon to get rid of consecutive lines which are
    * sufficiently parallel.
    *
    **/
    public Vertex[]
    simplifyPolygon( Vertex input_vertices[] ) 
    {
        double theta     = 0.0;
        double old_theta = 0.0;
        
        if ( (input_vertices == null) || (input_vertices.length < 2) ) {
            System.err.println("Error: simplifyPolygon no polygon detected.");
            return null;
        }
        
		double  intermediate_angles[] = 
			GeometryUtil.getIntermediateAngles( input_vertices );
        boolean include[]             = 
			new boolean[intermediate_angles.length+1];
        
        if ( debug2 ) {
            System.out.println( "Starting simplification" );
            System.out.println( "# of points = " + input_vertices.length );
            System.out.println( "intermediate_angles.length = " + 
								intermediate_angles.length );
        }

        include[0] = true;
        old_theta  = intermediate_angles[0];
        int deleted_points = 0;
        for ( int i=1; i<intermediate_angles.length; i++ ) {
            if ( Math.abs( old_theta - intermediate_angles[i] ) < 
				 turn_angle_treshold ) {
                if ( debug2 ) {
                    System.out.println( "Deleting point " + i );
                }
                deleted_points++;
                include[i] = false;
            } else {
                include[i] = true;
            }
            old_theta = intermediate_angles[i];
        }
        include[include.length-1] = true;
        System.out.println( "Simplification over" );
        System.out.println( "Deleted points = " +  deleted_points );

        Vertex new_polygon[] = new Vertex[include.length - deleted_points];
        System.out.println( "Created new polygon with " + 
                             new_polygon.length + " points");
        int index = 0;
        for ( int i=0; i<include.length; i++ ) {
            if ( include[i] ) {
                if ( debug2 ) {
                    System.out.println( "Included point " + i +
                                        " " + input_vertices[i] );
                }
                new_polygon[index] = new Vertex( input_vertices[i] );
                index++;
            } else {
                if ( debug2 ) {
                    System.out.println( "Excluded point " + i );
                }
            }
        }
		
		return new_polygon;
    }

    
    /**
    *
    * This method is needed only for supporting the rather 
    * silly windows9X platform, because it is still using the
    * dos clock.
    *
    **/
    public long
    getTimeStamp( MouseEvent e )    
    {
	    if ( platform_is_windows9x ) {
    	    return Timer.tics();
    	} else {
    	    return e.getWhen();
    	}
    }
}

/**
  *
  * $Log: Tablet.java,v $
  *
  *
  **/
