// sccsId = "@(#)FourDem.java 1.17 05/06/96";

import java.lang.*;
import java.awt.*;


public class FourDem extends java.applet.Applet implements Runnable
{
  private final int	sbHeight = 89;
  private final int	maxDelVal = 50;		// 5 Log[2, 5120ms / 5ms]
  private final int	initDelVal = 20;
  private final	int	initSamp = 200, minSamp = 40, maxSamp = 1000;
  private final double	tolerance = 0.8;	// For approximating Ellipses

  private Image		foreBuffer, backBuffer;
  private Thread	demoThread;
  private Scrollbar	delaySl, harmSl, sampSl;// for changing parameters
  private Rectangle	lastRect;

  int			delay = delayCurve(initDelVal);
  int			frameNumber = 0, framePainted = 0, frameBefore = -2;
  long			spareTime = 0;
  boolean		running = true;

  private Dimension	dim;

  private final double				// Fourier-Deskriptoren
    a[] = {0,
      1.000000e+00,  1.829451e-01,  1.585744e-01, -4.146237e-02,  3.487496e-02,
      1.075525e-01,  3.634476e-02, -2.373294e-02, -1.391077e-02,  4.323664e-03},
    b[] = {0,
      2.227347e-08,  6.604903e-02,  1.706410e-02, -5.249486e-02,  8.367436e-02,
     -2.367232e-03, -9.487874e-04, -2.526771e-02,  1.100689e-02, -4.641854e-02},
    c[] = {0,
     -1.389192e-08,  8.084710e-02, -1.130283e-01,  5.414879e-02,  2.957821e-02,
      1.457458e-01,  4.407634e-02,  2.858498e-02, -1.353993e-02, -5.606119e-02},
    d[] = {0,
     -6.687967e-01,  1.710615e-01,  8.602461e-03, -1.595088e-01,  3.802965e-02,
      4.743616e-02, -1.158317e-02, -1.420237e-02,  4.266163e-03,  1.822623e-02};
    // currently fixed to degrees 0 to 9 of the duck
  private final	Color	elliCol[]=
  {Color.yellow, Color.green, Color.cyan.darker(), new Color(0x7070ff),
   new Color(0xc000ff), Color.magenta, Color.orange.darker()};
  private final Color	markCol = Color.orange;
  private final Color	curveCol = Color.red;
  private final Color	backgroundCol = Color.gray.brighter();


  private	double 	scale;			// Skalierungsfaktor
  private final int	maxHarm = a.length-1;
  int			harm = maxHarm/2;
  int			samp = initSamp;
  private 	double 	sinu[], cosi[];		// tabulated Sine and Cosine
  private Polygon 	ellipse[] = null;
  private Rectangle	elliRect[];

  private	int	xoff, yoff;		// Translation
  private	int	x1, y1;			// previous point on curve
  
public synchronized void paint(Graphics g) {
  g.drawImage(foreBuffer, 0, 0, null);		// must be quick
  framePainted = frameNumber;
  // sketchClip(g, Color.blue);
}

public void sketchClip(Graphics g, Color col) {
   sketchRect(g, g.getClipRect(), col);
}

public void sketchRect(Graphics g, Rectangle r, Color col) {
  g.setColor(col);
  sketchRect(g, r);
}
public void sketchRect(Graphics g, Rectangle r) {
  g.drawLine(r.x, r.y, r.x+r.width-1, r.y+r.height-1);
  g.drawLine(r.x, r.y+r.height-1, r.x+r.width-1, r.y);
  g.drawRect(r.x, r.y, r.width-1, r.height-1);
}

  
public void update(Graphics g) {
  // Stop the default (Component) implementation of the update() method
  // from clearing the Component's background (drawing a rectangle over
  // the component's clipping area in the Component's background).
  // See http://www.ee.ethz.ch/isg/tardis/soft/java/ui/overview/drawing.html
  paint(g);
}

public void init() {
  dim		= size();
  dim.height	-= sbHeight;
  foreBuffer	= createImage(dim.width, dim.height);
  backBuffer	= createImage(dim.width, dim.height);

  //  setBackground(backgroundCol);
  ellipse  = new Polygon  [maxHarm+1];
  elliRect = new Rectangle[maxHarm+1];
  sinu = new double [maxSamp];			// Sinus und Cosi vorberechnen  
  cosi = new double [maxSamp];
  setupEllips();
  setupBg();
  
  setLayout(new BorderLayout());
  Panel sliderPanel = new Panel();
  sliderPanel.setLayout( new GridLayout(1,0));
  delaySl = new Scrollbar(Scrollbar.HORIZONTAL,
  		initDelVal, 5, 0, maxDelVal); // "time per frame"
  harmSl  = new Scrollbar(Scrollbar.HORIZONTAL,
  		maxHarm / 2, 1, 1, maxHarm); // "harmonics"
  sampSl  = new Scrollbar(Scrollbar.HORIZONTAL,
  		initSamp, 100, minSamp, maxSamp); // "samples"
  sliderPanel.add("time per frame", delaySl);
  sliderPanel.add("harmonics", harmSl );
  sliderPanel.add("samples", sampSl );
  add("South", sliderPanel);
}
  
public void start() {
  if (running) {
    if (demoThread == null)
      demoThread = new Thread(this, "Demo");
    demoThread.start();
  }
}

public void run() {
  long startTime = System.currentTimeMillis();

  while (demoThread != null) {
    if (framePainted == frameNumber) {
      frameNumber++;
      drawFrame();
      startTime += delay;
    }
    else {
      Graphics g = foreBuffer.getGraphics();
      g.setColor(Color.red.darker());
      g.drawString("slow down", xoff-30, yoff +5);
      startTime += 20;				// give a chance to catch up
      repaint(xoff-30, yoff -5, 61, 13);
      lastRect.add(xoff-30, yoff -5);
      lastRect.add(xoff+30, yoff +7);
    }
    spareTime = startTime - System.currentTimeMillis();
    try {
      demoThread.sleep(Math.max(0, spareTime));
    } catch (InterruptedException e){
      break;
    }
  }
}
public synchronized void stop() {
  if (demoThread != null) {
    demoThread.stop();
    demoThread = null;
  }
}

public boolean handleEvent(Event evt) {
  switch (evt.id) {
  case Event.SCROLL_LINE_UP: case Event.SCROLL_LINE_DOWN:
  case Event.SCROLL_PAGE_UP: case Event.SCROLL_PAGE_DOWN:
  case Event.SCROLL_ABSOLUTE:
    delay = delayCurve(delaySl.getValue());
    if (harm != harmSl.getValue()) {
      harm = harmSl.getValue();
      setupBg();				// don't need to set up ellipses
    }
    if (samp != sampSl.getValue()) {
      frameNumber = frameNumber % samp * sampSl.getValue() / samp;
      samp = sampSl.getValue();
      setupEllips();
      setupBg();
    }
    break;
  case Event.MOUSE_DOWN:
    if (evt.y >= dim.height)
      return false;				// else fall through
  case Event.KEY_ACTION:
    running = ! running;
    if (running) start();
    else         stop ();
    return true;
  default:
    break;
  } // switch

  return false;
} // handleEvent(Event)

private int delayCurve(int index) {
  return index % 5 + 5 << index / 5;
}
  
private synchronized void drawFrame() {
  Rectangle newRect = null;
  Graphics graph = foreBuffer.getGraphics();

  Graphics backGraph = graph.create();		// for selective repaint
  backGraph.clipRect(lastRect.x, lastRect.y, lastRect.width, lastRect.height);
  backGraph.drawImage(backBuffer, 0, 0, null);
  backGraph.dispose();

//   graph.setColor(Color.blue);
//   graph.drawString("delay = " + delay + " ms", 5, 10);
//   graph.setColor(Color.red);
//   graph.drawString("frame #" + frameNumber, 100, 10);
//   graph.setColor(Color.magenta);
//   graph.drawString("spare: "+ spareTime + " ms", 180, 10);

  //Graphics g = graph.create();
  double x = xoff;
  double y = yoff;
  int xRound = (int)x, yRound= (int)y;
  for (int j = 1; j <= harm; j++){
    graph.setColor(elliCol[j % elliCol.length]);
    if (j > 1) {
      graph.translate(xRound, yRound);
      graph.drawPolygon(ellipse[j]);
      graph.translate(-xRound, -yRound);

      elliRect[j].translate(xRound, yRound);
      newRect.add(elliRect[j]);
      elliRect[j].translate(-xRound, -yRound);
    }
    int index = frameNumber*j%samp;
    int xold= xRound;
    int yold= yRound;
    x+= a[j]*cosi[index] + b[j]*sinu[index];
    y+= c[j]*cosi[index] + d[j]*sinu[index];
    //    foreG.setColor(curveCol);
    xRound = (int)x;
    yRound = (int)y;
    graph.drawLine(xold, yold, xRound, yRound);

    if (j == 1) {				// Init BoundingBox
      newRect = new Rectangle(Math.min(xRound, xoff), Math.min(yRound, yoff),
			      Math.abs(xRound -xoff), Math.abs(yRound -yoff));
    }
  }
  
  graph.setColor(markCol);
  graph.fillRect(xRound-1, yRound-1, 3, 3);
  newRect.add(xRound-1, yRound-1);
  newRect.add(xRound+1, yRound+1);
  
  if (frameNumber == frameBefore + 1) {
    graph.setColor   (curveCol);		// for the foreground curve
    graph.drawLine(x1, y1, xRound, yRound);
    newRect.add(x1,y1);

    graph = backBuffer.getGraphics();
    graph.setColor   (curveCol);		// for the retained curve
    graph.drawLine(x1, y1, xRound, yRound);
  }
  x1 = xRound;
  y1 = yRound;
  frameBefore = frameNumber;

  newRect.grow(1,1);
  lastRect.add(newRect);
  repaint(lastRect.x, lastRect.y, lastRect.width, lastRect.height);
  lastRect = newRect;
}
  
private void setupEllips() {
  
  //  double  x, y, xold, yold;		// Hilfsvariable
  						// Vorbereitungen
  double  minx= 0, miny= 0,		// BoundingBox der rek. Kurve
	  maxx= 0, maxy= 0;
  int	borderWidth = dim.width / 32;
    
  //   fd = fopen (fname, "r");			// Fourier-Deskr. lesen
  //   if (!fd) {
  //     fprintf(stderr, "%s, opening \"%s\" for reading: %s.\n",
  //          progname, fname, sysErrlist[errno]);
  //     exit(errno);
  //   }
    
  //   fscanf (fd, "%d ", &n);
  //   if (maxHarm > n) maxHarm = n;
  //   for (j = 1; j <= maxHarm; j++){
  //     fscanf (fd, "%f %f %f %f", &a[j], &b[j], &c[j], &d[j]);
  //   }
  //   fclose (fd);
  //  printf ("%d  Koeffizienten eingelesen.\n", maxHarm * 4); 
    
  for (int i= 0; i < samp; i++) {
    sinu[i] = Math.sin(2.0 * Math.PI * i/samp);
    cosi[i] = Math.cos(2.0 * Math.PI * i/samp);
  }

  for (int i = 0; i < samp; i++){		// BoBox of the reconstr. curve
    double x = 0, y = 0;
    for (int j = 1; j <= maxHarm; j++){
      int index= i*j%samp;
      x += a[j] * cosi[index] + b[j] * sinu[index];
      y += c[j] * cosi[index] + d[j] * sinu[index];
    }
    if      (x < minx) minx= x;
    else if (x > maxx) maxx= x;
    if      (y < miny) miny= y;
    else if (y > maxy) maxy= y;
  }

  double scalex= (dim.width  - 2*borderWidth)/(maxx-minx),
         scaley= (dim.height - 2*borderWidth)/(maxy-miny);
  xoff = (int)(borderWidth - minx*scalex);
  yoff = (int)(borderWidth - miny*scaley);
  scale = Math.min(scalex, scaley);

  for (int i= 0; i < samp; i++) {		// Sinus und Cosinus skalieren
    sinu[i] *= scale;
    cosi[i] *= scale;
  }
    
  // Ellipsen vorbereiten
  Point   elliCurve[] = new Point[samp+1];

  for (int j= 1; j <= maxHarm; j++) {
    for (int i= 0; i < samp; i++) {
      elliCurve[i] = new Point((int)(a[j]*cosi[i] + b[j]*sinu[i]),
			       (int)(c[j]*cosi[i] + d[j]*sinu[i]));
    }
    elliCurve[samp] = elliCurve[0];		// close the Polygon
    ellipse [j]= polygonApprox(elliCurve, samp+1, tolerance);
    elliRect[j]= ellipse[j].getBoundingBox();
  }
} // setupEllips

private void setupBg() {
  Graphics graph = backBuffer.getGraphics();
  graph.setColor(getBackground());
  graph.fillRect(0, 0, dim.width, dim.height);
  lastRect = new Rectangle(0, 0, dim.width, dim.height);

  graph.setColor   (elliCol[1]);
  graph.translate  (xoff, yoff);
  graph.drawPolygon(ellipse[1]);
  frameBefore = -2;				// invalidate (x1,y1)
}


/* included C source: pgon-approx.c as of 23 April 1996.
 * Approximiert die als Punktliste der Laenge npt gegebene Kurve curve durch
 * einen Polygonzug  mit maximaler normaler Abweichung toleranz.
 * Gibt die Anzahl der Linien  zurueck.
 * 11.10.88; Christian Brechbuehler
 */

private Polygon polygonApprox(Point curve[], int npt, double toleranz)
/* An interface that makes sure the end of the last span, which is not the
 * start of any other span, will be written to polygon. It is the caller's
 * responsability to provide sufficient storage space in polygon.
 */
  {
    Polygon poly = new Polygon();
    polygonApproxI(curve, 0, npt-1, toleranz, poly);
    poly.addPoint(curve[npt-1].x, curve[npt-1].y);
    return poly;
  }


private void polygonApproxI(Point curve[], int start, int end, double toleranz,
			    Polygon poly)
/* Douglas & Peucker's Algorithm.
 * Recursive procedure for subdividing the curve into sections that are well
 * enough (i.e., within toleranz) represented by a straight line (a span of
 * a polygon). The starting points of all spans are written to polygon, and
 * the ending point is implicitly given as the starting point of the next
 * span. If some application (e.g., with closed curves) does not need the end
 * point of the last span, which is always the last point of curve, it may
 * call this function. Normally, "polygonApprox" should be used instead.
 */
{
  int pMax = 0;					// indices into array
  int dqMax;

  int nx= curve[end].y - curve[start].y;		// normal
  int ny= curve[start].x - curve[end].x;
  if (nx != 0 || ny != 0){			// open curve
    dqMax= (int)Math.floor((nx*nx + ny*ny) * toleranz*toleranz);
    int diff= curve[start].x*nx+ curve[start].y*ny;
    for (int p= start; p < end; p++){
      int d = curve[p].x*nx + curve[p].y*ny - diff;
      int dq= d*d;
      if (dq > dqMax) {dqMax= dq; pMax= p;}
    }
  }
  else {                                         // closed curve or sigle point 
    dqMax= (int)Math.floor(toleranz * toleranz);
    for (int p = start; p < end; p++) {
      int
	dx= curve[p].x - curve[start].x,
	dy= curve[p].y - curve[start].y,
	dq= dx*dx + dy*dy;
      if (dq > dqMax) { dqMax= dq; pMax= p;}
    }
  }

  if (pMax != 0){				// tolerance exceeded => split
    polygonApproxI(curve, start, pMax, toleranz, poly);
    polygonApproxI(curve, pMax , end , toleranz, poly); }
  else {                                    // tolerance met
    poly.addPoint(curve[start].x, curve[start].y);
  }
} /* END polygonApprox */
  
} // FourDem
