/*
 * @(#)Lissa.java
 *
 * Jan Hnátek
 * 
 */
import Pix;
//import DebugArea; all debug commented out ...
import java.awt.*;
import java.applet.*;
import java.awt.event.*;

/**
 * The <code>Lissa</code> class is an application (applet) which 
 * can draw Lissajous figures according to some user input.
 * <p>
 *
 * @version 	1.40, 29/05/00
 * @author	Jan Hnátek
 */
public class Lissa extends Applet implements ActionListener, TextListener, AdjustmentListener
//*****************************************************************************************//
{
private static DrawArea output;
//private static DebugArea debug;
private static Dimension defaultDimension = new Dimension (200+1,200+1);	// initial size of output

private	Label appletLabel;
private	Panel signalParams;
private		Panel frequencies;
private			Panel f1;
private				Scrollbar frsl_1;
private				TextField fr_1;
private			Panel f2;
private				Scrollbar frsl_2;
private				TextField fr_2;
private		Panel phases;
private			Panel p1;
private				Scrollbar phsl_1;
private				TextField ph_1;
private			Panel p2;
private				Scrollbar phsl_2;
private				TextField ph_2;
private		Panel amplitudes;
private			Panel a1;
private				Scrollbar amsl_1;
private				TextField am_1;
private			Panel a2;
private				Scrollbar amsl_2;
private				TextField am_2;
			
/*
Panel debugPanel;
	Panel debugControl;
		Button redraw;
		Button switchDebug;
		Button clearDebug;
		TextField d;
*/

// constructor
public Lissa () {}
	/**
	 * Applet start.
	 */
public void start ()		{ output.start (); }
	/**
	 * Applet stop.
	 */
public void stop ()		{ output.stop (); }

// applet info	
	/**
	 * Returns information about this applet.
	 * @return a string containing information about the author, and version of the applet
	 */
public String getAppletInfo ()	{ return "Lissajousovy obrazce, v.05/2000, by Jan Hnátek"; }

// applet init
	/**
	 * Applet initializator. Should be run by browsers.
	 */
public void init ()
	{ 
	setLayout (new BorderLayout());
	add ("East", appletLabel = new Label (" ", Label.CENTER));
	appletLabel.setBackground (Color.gray);
	add ("West", appletLabel = new Label (" ", Label.CENTER));
	appletLabel.setBackground (Color.gray);
	add ("North", appletLabel = new Label ("Lissajousovy obrazce", Label.CENTER));
	appletLabel.setBackground (Color.gray);
	appletLabel.setForeground (Color.white);
	
// debug
/*
	Frame f = new Frame ("Debug Window");
	f.add (debugPanel = new Panel ());
	debugPanel.setLayout (new BorderLayout ());
	debugPanel.add ("North", debugControl = new Panel ());
	debugPanel.add ("Center", debug = new DebugArea ());
	debug.setBackground (Color.lightGray);
	debug.setEditable (false);
	debugControl.add (redraw = new Button ("redraw"));
	debugControl.add (switchDebug = new Button ("stop"));
	debugControl.add (clearDebug = new Button ("clear"));
	debugControl.add (d = new TextField ("0", 10));
	debugControl.setBackground (Color.red);
	d.addTextListener (this);
	redraw.addActionListener (this);
	switchDebug.addActionListener (this);
	clearDebug.addActionListener (this);
	f.setSize (500, 600);
	f.setLocation (450, 50);
	f.setVisible (true);
	f.addWindowListener (new WindowAdapter()
		{ public void windowClosing(WindowEvent e) { System.exit(0); }} );
*/

// draw	
	add ("Center", output = new DrawArea (defaultDimension, null));
	//output.setBackground (Color.white);

// signals	
	add ("South", signalParams = new Panel ());
	signalParams.setBackground (Color.gray);
	signalParams.setLayout (new GridLayout (0,1));
	//signalParams.add (new Label ("Parametry signálu"));
	signalParams.add (amplitudes = new Panel ());
	signalParams.add (frequencies = new Panel ());
	signalParams.add (phases = new Panel ());
		
	// amplitudes
	amplitudes.setLayout (new GridLayout (0,1));
	amplitudes.add (a1 = new Panel ());
	amplitudes.add (a2 = new Panel ());

	a1.add (new Label ("Amplitudy"));
	a2.add (new Label ("         "));
	a1.setLayout (new GridLayout (1,0));
	a2.setLayout (new GridLayout (1,0));
	a1.add (amsl_1 = new Scrollbar (Scrollbar.HORIZONTAL, 100, 1, 0, 101));
	a2.add (amsl_2 = new Scrollbar (Scrollbar.HORIZONTAL, 100, 1, 0, 101));
	a1.add (am_1 = new TextField (""+output.getAmp ().x, 10));
	a2.add (am_2 = new TextField (""+output.getAmp ().y, 10));

	amsl_1.setUnitIncrement (1);
	amsl_1.setBlockIncrement (10);
	amsl_2.setUnitIncrement (1);
	amsl_2.setBlockIncrement (10);

	amsl_1.addAdjustmentListener (this);
	amsl_2.addAdjustmentListener (this);
	am_1.addTextListener (this);
	am_2.addTextListener (this);

	// frequencies
	frequencies.setLayout (new GridLayout (0,1));
	frequencies.add (f1 = new Panel ());
	frequencies.add (f2 = new Panel ());

	f1.add (new Label ("Frekvence"));
	f2.add (new Label ("         "));
	f1.setLayout (new GridLayout (1,0));
	f2.setLayout (new GridLayout (1,0));
	f1.add (frsl_1 = new Scrollbar (Scrollbar.HORIZONTAL, 200, 10, 0, 5010));
	f2.add (frsl_2 = new Scrollbar (Scrollbar.HORIZONTAL, 100, 10, 0, 5010));
	f1.add (fr_1 = new TextField (""+output.getFreq().x, 10));	
	f2.add (fr_2 = new TextField (""+output.getFreq().y, 10));

	frsl_1.setUnitIncrement (1);
	frsl_1.setBlockIncrement (100);
	frsl_2.setUnitIncrement (1);
	frsl_2.setBlockIncrement (100);

	frsl_1.addAdjustmentListener (this);
	frsl_2.addAdjustmentListener (this);
	fr_1.addTextListener (this);
	fr_2.addTextListener (this);

	// phases
	phases.setLayout (new GridLayout (0,1));
	phases.add (p1 = new Panel ());
	phases.add (p2 = new Panel ());
	
	p1.add (new Label ("Fáze"));
	p2.add (new Label ("    "));
	p1.setLayout (new GridLayout (1,0));
	p2.setLayout (new GridLayout (1,0));
	p1.add (phsl_1 = new Scrollbar (Scrollbar.HORIZONTAL, 0, 1, 0, 361));
	p1.add (ph_1 = new TextField (""+output.getPhase().x, 10));
	p2.add (phsl_2 = new Scrollbar (Scrollbar.HORIZONTAL, 0, 1, 0, 361));
	p2.add (ph_2 = new TextField (""+output.getPhase().y, 10));
	
	phsl_1.setUnitIncrement (1);
	phsl_1.setBlockIncrement (15);
	phsl_2.setUnitIncrement (1);
	phsl_2.setBlockIncrement (15);

	phsl_1.addAdjustmentListener (this);
	phsl_2.addAdjustmentListener (this);
	ph_1.addTextListener (this);
	ph_2.addTextListener (this);
	}

// aplication constructor
	/**
	 * Constructor for the case, when <code>Lissa</code> is run as an application.
	 * @param f is a <code>Frame</code> to run in
	 */
private Lissa (Frame f)
	{
	f.addWindowListener(new WindowAdapter ()
		{
		public void windowClosing (WindowEvent e)	{ System.exit (0); }
		public void windowOpened (WindowEvent e)	{ start (); }
		public void windowDeiconified (WindowEvent e)	{ start (); }
		public void windowIconified (WindowEvent e)	{ stop (); }
		});
	init ();
	}

// main, to be able to run as an application
	/**
	 * Main application code. Used when <code>Lissa</code> is run as an application.
	 */
public static void main (String args[])
	{
	Frame f = new Frame ("Lissajous figures");
	Lissa lissa = new Lissa (f);
	f.add (lissa);
	f.setSize (344+8, 471+37);
	f.setLocation (80, 50);
	f.setVisible (true);
	}
	
/*** Lissa ActionListener implementation ***/
	/**
	 * ActionListener implementation. Used by debug subsystem.
	 */
public void actionPerformed (ActionEvent e)
	{
/*
	Object src = e.getSource();

	if (src == switchDebug)
		{
		if (debug.getOutput ())
			{ debug.setOutput (false); switchDebug.setLabel ("start"); }
		else	{ debug.setOutput (true); switchDebug.setLabel ("stop"); }
		}
	else if (src == clearDebug)
		debug.clear ();
	else if (src == redraw)
		output.reset ();
*/
	}

private boolean adjust;

/*** Lissa TextListener implementation ***/
	/**
	 * TextListener implementation. Processes textboxes input.
	 */
public void textValueChanged (TextEvent e)
	{
	TextComponent src = (TextComponent) e.getSource ();
	Double tmp;
	
	try	{ tmp = new Double (src.getText ());
	} catch (NumberFormatException ex)
		{
//		debug.write (debug.HIGH, "Exception: "+ex.toString ()); //+src.toString ()+
		if ((src == fr_1) || (src == fr_2))
			tmp = new Double (1);
		else	tmp = new Double (0);		
		}

	if (src == fr_1)
		{
		if (adjust == true)
			{
			//debug.write ("adjusting from TEXT :"+(int) (tmp.doubleValue()*100)+"  "+adjust);
			frsl_1.setValue ((int) (tmp.doubleValue()*100));
			}
		else adjust = true;
		output.setFreq (tmp.doubleValue(), output.getFreq().y);
		}
	else if (src == fr_2)
		{
		if (adjust == true)
			frsl_2.setValue ((int) (tmp.doubleValue()*100));
		else adjust = true;
		output.setFreq (output.getFreq().x, tmp.doubleValue());
		}
	else if (src == ph_1)
		{
		if (adjust == true)
			phsl_1.setValue ((int) (tmp.doubleValue()*10));
		else adjust = true;
		output.setPhase (tmp.doubleValue()*Math.PI/180, output.getPhase().y);
//		debug.write (""+Math.toRadians (tmp.doubleValue())+";"+tmp.doubleValue());
		}
	else if (src == ph_2)
		{
		if (adjust == true)
			phsl_2.setValue ((int) (tmp.doubleValue()*10));
		else adjust = true;
		output.setPhase (output.getPhase().x, tmp.doubleValue()*Math.PI/180);
		}
	else if (src == am_1)
		{
		if (adjust == true)
			amsl_1.setValue ((int) (tmp.doubleValue()*100));
		else adjust = true;
		output.setAmp (tmp.doubleValue(), output.getAmp().y);
		}
	else if (src == am_2)
		{
		if (adjust == true)
			amsl_2.setValue ((int) (tmp.doubleValue()*100));
		else adjust = true;
		output.setAmp (output.getAmp().x, tmp.doubleValue());
		}
/*	else if (src == d)
		//output.setTotal (tmp.doubleValue());		
		output.setINC (tmp.doubleValue ());
*/
	}
	
/*** Lissa AdjustmentListener implementation ***/
	/**
	 * AdjustementListener implementation. Processes sliders input.
	 */
public void adjustmentValueChanged (AdjustmentEvent e)
	{
	Scrollbar src = (Scrollbar) e.getSource ();
	double v = src.getValue ();
		
	if (src == frsl_1)
		{
		adjust = false;
//		debug.write ("adjusting from SCROLLBAR: "+Double.toString (v/100)+"  "+adjust);
		fr_1.setText (Double.toString (v/100));
		}
	else if (src == frsl_2)
		{
		adjust = false;
		fr_2.setText (Double.toString (v/100));
		}
	else if (src == phsl_1)
		{
		adjust = false;
		ph_1.setText (Double.toString (v));
		}
	else if (src == phsl_2)
		{
		adjust = false;
		ph_2.setText (Double.toString (v));
		}
	else if (src == amsl_1)
		{
		adjust = false;
		am_1.setText (Double.toString (v/100));
		}
	else if (src == amsl_2)
		{
		adjust = false;
		am_2.setText (Double.toString (v/100));
		}
	}	
} // EO class Lissa



/**
 * The <code>DrawArea</code> class the part of the Lissa application 
 * draws the Lissajous figures.
 * <p>
 * It works as an animator with double buffering.
 *
 * @version 	1.40, 29/05/00
 * @author	Jan Hnátek
 */
class DrawArea extends Canvas implements Runnable
//*********************************************//
{
private Image buffer;
private Graphics graphics;
private Dimension size, scale;	// scale == size/2
private Thread animator;
//private DebugArea debug;		// set in constructor, instantiated in parent class

private double time=0, total=0;	// current time, end time
private Pix f,t, freq,phase,amp;	// f1,f2 (phase1,phase2) => freq.x,freq.y (phase.x,phase.y)
						// phases in radians; amplitudes in (0,1)

final private static double INITINC = 0.0001024;
private static double INC = INITINC;		// dynamic
private static int SPEED=2;

// precision enhancement : ldx - last dx, dx - current dx
static int ldx, ldy, dx=0, dy=0, maxDelta=0;

private static Color bgColor = Color.white;
private static Color fgColor = Color.blue;


/*** public functions ***/

// constructor
	/**
	 * Constructs a new <code>DrawArea</code> and sets its
	 * initial values to their defaults.
	 * @param daSize initial size of drawing area
	 * @param da <code>DebugArea</code> used to output debug information, 
	 * should be set up by parent object
	 */
public DrawArea (Dimension daSize, DebugArea da)
	{
	//debug = da;
	size = new Dimension ();
	scale = new Dimension ();
	setSize (daSize);
	setForeground (fgColor);
	setBackground (bgColor);
	freq = new Pix (2,1);		// implicit values, use setFreq, setPhase, setAmp
	phase = new Pix (0,0);
	amp = new Pix (1,1);
	setTotal ();
	f = count (0);
	//debug.setPriority (DebugArea.NONE);
	//debug.write ("Total: "+total);
	}

// frequency and phase handlers
	/**
	 * Set frequencies according to given <code>Pix</code>
	 * @param freq <code>Pix</code> containing new frequency values
	 */
public void setFreq (Pix freq)			{ this.freq = freq; reset (); }
	/**
	 * Set frequencies according to two given double values.
	 * @param f1 new x-axis frequency value
	 * @param f2 new y-axis frequency value
	 */
public void setFreq (double f1, double f2)	{ freq = new Pix (f1,f2); reset (); }
	/**
	 * Set phases according to given <code>Pix</code>
	 * @param phase <code>Pix</code> containing new phase values
	 */
public void setPhase (Pix phase)			{ this.phase = phase; reset (); }
	/**
	 * Set phases according to two given double values.
	 * @param p1 new x-axis phase value
	 * @param p2 new y-axis phase value
	 */
public void setPhase (double p1, double p2)	{ phase = new Pix (p1,p2); reset (); }
	/**
	 * Set amplitudes according to given <code>Pix</code>
	 * @param amp <code>Pix</code> containing new amplitude values
	 */
public void setAmp (Pix amp)				{ this.amp = amp; reset (); }
	/**
	 * Set amplitudes according to two given double values.
	 * @param a1 new x-axis amplitude value
	 * @param a2 new y-axis amplitude value
	 */
public void setAmp (double a1, double a2)	{ amp = new Pix (a1,a2); reset (); }

	/**
	 * Returns frequency values.
	 * @return current frequency values
	 */
public Pix getFreq ()		{ return freq; }
	/**
	 * Returns phase values.
	 * @return current phase values
	 */
public Pix getPhase () 	{ return phase; }
	/**
	 * Returns amplitude values.
	 * @return current amplitude values
	 */
public Pix getAmp ()	 	{ return amp; }
	
// size modifier
	/**
	 * Set size of this <code>DrawArea</code>
	 * @param daSize new <code>DrawArea</code> size
	 */
public void setSize (Dimension daSize)
	{
	super.setSize (daSize);
	size = daSize;
	scale.setSize (size.width/2, size.height/2);
	}
	
// restart drawing
	/**
	 * Reset the drawing with initial time constant.
	 */
public void reset ()	{ reset (INITINC); }
	/**
	 * Reset the drawing with given time constant. 
	 * Stop applet, reset all values and start the applet again.
	 * @param i new time constant
	 */
public void reset (double i)
	{
	stop ();
	time = 0;
	SPEED = 2;
	f = count (0);
	setTotal ();
	setINC (i);
	//debug.clear ();
//	debug.write ("total: "+total+"    \t timeINC: "+INC+"\t f: ["+freq.x+","+freq.y+"]");
	graphics.clearRect (0,0, size.width, size.height);
	start ();
	}


/*** debug ***/
/*
public void setTotal (double i)		{ total = i; }
*/
	/**
	 * Set time constant.
	 * @param i new time constant
	 */
public void setINC (double i)		{ INC = i; }


/*** private common functions ***/

// default (initial value)
	/**
	 * Set time constant to its initial value.
	 */
public void setINC ()				{ INC = INITINC; }

// total time counter
private void setTotal ()
	{
	int n = decimals (freq.x) >= decimals (freq.y) ? decimals (freq.x) : decimals (freq.y);
	long x = getTime (freq.x);
	long y = getTime (freq.y);
	if (x*y > Math.pow (10, n))
		total = Math.pow (10, n);
	else	total = x*y;
	//debug.write ("setTotal: ["+x+","+y+"]"+" => "+total);
	}
	
// counts time for given frequency
private long getTime (double x)
	{
	Double f = new Double (x);
	double q; // quotient; should be integer-type, else 10^n
	
	if (fractional (f) != 0)
		q = 1/fractional (f);
	else	{ return 1; }
	//debug.write ("getTime: f"+f+" => q ="+q);
	if (fractional (new Double (q)) != 0)
		{
		q = Math.pow (10, decimals (f.doubleValue ()));
		if (q >= 10000)
			q = 1000;
		}	// quotient is float, return 10^n ;;; else OK, integer-type
	return (long) q;	
	}

// returns only the fractional part of a number
private double fractional (Double x)
	{
	//debug.write ("fractional: "+x+" => "+(x.floatValue () - x.intValue ()));
	return x.floatValue () - x.intValue ();
	}

// counts number of digits in decimal part of double
private int decimals (double d)
	{
	Double x = new Double (d);
	String s = x.toString ().substring (x.toString ().indexOf (".")+1);
	if (s.compareTo ("0") == 0)
		return 0;
	else	return s.length ();
	}
	
// computes values from specified time 
private Pix count (double time)
	{
	return (new Pix (	amp.x*Math.sin (Math.PI*2*freq.x*time+phase.x),	// 1: freq.x, phase.x, amp.x
				amp.y*Math.sin (Math.PI*2*freq.y*time+phase.y) ));	// 2: freq.y, phase.y, amp.y
	}
	

/***  dynamic precision enhancing mechanism ***/

// returns suggested corrected value of INC 
private double testDelta (int delta)
	{
	if (delta > 50)		// brutal reset, somebody's trying, what it can stand (f ~ 1000+)
		reset (INC/10);
	switch (delta)		// switch between (un)tolerated delta differences
		{
		case 0:
		case 1:	return INC*2;	// too small delta changes, possible to increase
		case 2: //case 3: case 4: case 5: case 6: case 7: case 8: case 9: 
				return INC;		// optimal delta change
		default:	return INC/2;	// maximum delta change, decreasing
		}
	}

// draws line between two points, x taken negative; count maxDeltas
private void drawLine (Pix p1, Pix p2)
	{	
	int	px = (int)(-p1.x*scale.width+scale.width), py = (int)(p1.y*scale.height+scale.height),
		qx = (int)(-p2.x*scale.width+scale.width), qy = (int)(p2.y*scale.height+scale.height);
	graphics.drawLine (px, py, qx, qy);
	
	/* just for illustrational purposes
	graphics.drawLine (px, py, px, py);
	graphics.drawLine (qx, qy, qx, qy);
	*/
	
	/* Java + optimization ??
	ldx = dx;	dx = qx - px;
	ldy = dy;	dy = qy - py;
	int Delta = Math.abs (dx - ldx) >= Math.abs (dy - ldy) ? Math.abs (dx - ldx) : Math.abs (dy - ldy);
	*/
	int	a = Math.abs ((ldx = dx) - (dx = qx - px)),
		b = Math.abs ((ldy = dy) - (dy = qy - py));
	int	Delta = a >= b ? a : b;
	if (maxDelta < Delta)
		maxDelta = Delta;

	// DEBUG
	/*
	debug.write (time+" INC: "+INC+"\t [dx,dy]==["+dx+","+dy+"]"+"\t D>"+Delta+"\t MD> "+maxDelta);
	
	if ((long)(time*1000)%1000 == 0)
		debug.write (debug.LOW, "@"+time+"/"+total+":"+p2.toString (scale.width, scale.height));
	*/		
	}

// counts optimized INC
private double preciseINC ()
	{
	double nINC = testDelta (maxDelta);			
	//debug.write (time+"\tINC: "+INC+"  SPEED: "+SPEED+"\t MD> "+maxDelta+"\t nINC> "+nINC);
	if (SPEED < 250)
		if (SPEED >= 20)
			SPEED += 10;
		else	SPEED += 2;
	maxDelta = 0;	
	return nINC;
	}


/*** animator functions ***/

// paint, update
	/**
	 * Update override. Its not wanted to clear the output every frame.
	 */
public synchronized void update (Graphics g)	{ paint (g); }
	/**
	 * Painting function.
	 */
public synchronized void paint (Graphics g)
	{
	if (!size.equals (getSize ()))			// resize check
		{
		setSize (getSize ());
		time = 0;
		SPEED = 2;
		f = count (0);
		setINC ();
//		debug.write ("size: "+size.toString ());
//		debug.write (debug.LOW, "scale: "+scale.toString ());
		buffer = null;
		start ();		
		}
	if (buffer == null)
		{
		buffer = createImage (size.width, size.height);
		graphics = buffer.getGraphics ();
		}
	if (time < total)
		{
		for (int i=0; i<SPEED; i++)
			{
			t = count (time += INC);
			drawLine (f, t);
			f = t;
			}
		g.drawImage (buffer, 0, 0, this);
		}
	else stop ();

	INC = preciseINC ();		// dynamic precision enhancement
	
	if (animator == null)		// image mode .. just draw the result
		g.drawImage (buffer, 0, 0, this);
	}

// start, stop
	/**
	 * Animator stop function.
	 */
public void stop ()		{ animator = null; }
	/**
	 * Animator start function.
	 */
public void start ()
	{
	if (animator == null)
		{
		animator = new Thread (this);
		animator.start ();
		}
	}

// run
	/**
	 * Animator run function.
	 */
public void run ()
	{
	while (animator != null)
  		{
    		try { Thread.sleep (5);
    			} catch (InterruptedException e) { break; }
		repaint();
 		}
	}


/*** overall information fuction ***/
	/**
	 * Returns a <code>String</code> that represents the state of this <code>DrawArea</code>.
	 * @return a string containing relevant information of the current <code>DrawArea</code> state
	 */
public String toString ()
	{
	return (	"time: "+time+" of total: "+total+"  INC="+INC+" SPEED="+SPEED+
			"  AMP["+amp.x+","+amp.y+"]"+
			" FREQ["+freq.x+","+freq.y+"]"+
			" PHASE["+phase.x+","+phase.y+"]");	
	}
} // EO class DrawArea