// Frets.java -- source file for the Lute Fret Calculator applet
//
import java.awt.*;

public class Frets extends java.applet.Applet
{

  String name[] = { " 1st (B)", " 2nd (C)", " 3rd (D)", " 4th (E)", " 5th (F)",
                    " 6th (G)", " 7th (H)", " 8th (I)", " 9th (K)", "10th (L)",
                    "11th (M)", "12th (N)" };  // Yeah, there's an extra one.

  double    CalcValue = 0, dowland[];
  Image     lutewood;
  TextField entryArea;
  Font      fntCour, fntHelv;


  //----------------------------------------------------------------------------
  // The MakeDowland() method calculates the fret placements according to the
  // formula given by John Dowland in his son Robert's 1610 book, "Varietie of
  // Lute-lessons".  It is a rather convoluted formula, to say the least.
  //
  void MakeDowland()
  {
    dowland[11] = CalcValue/2;
    dowland[ 6] = (dowland[11] / 3) * 2;
    dowland[ 0] = (dowland[6] / 11) * 2;
    dowland[ 1] = (dowland[6] / 3);
    dowland[ 4] = (dowland[11] / 2);
    dowland[ 5] = ((dowland[6] - dowland[4]) / 2) + dowland[4];
    dowland[ 2] = ((dowland[0] / 3) * 4.5) + dowland[0];   
    dowland[ 3] = ((dowland[4] - dowland[2]) / 2) + dowland[2];
    dowland[ 7] = ((CalcValue-dowland[0]) / 3) + dowland[0];
    dowland[ 8] = ((CalcValue-dowland[1]) / 3) + dowland[1];
    dowland[ 9] = ((CalcValue-dowland[2]) / 3) + dowland[2];
    dowland[10] = ((CalcValue-dowland[3]) / 3) + dowland[3];
  }


  //----------------------------------------------------------------------------
  // The createAppletLayout() method creates the controls for the input area.
  //
  void createAppletLayout()
  {
    int i, j;

    Label lb = new Label( "Unsupported string length (mm):" );
    lb.setAlignment( Label.RIGHT );
    add( lb );
    entryArea = new TextField( "600", 3 );
    add( entryArea );
    add( new Button( "Calculate" ));
  }


  //----------------------------------------------------------------------------
  // The makeFormattedString() method converts a double into a string with
  // three digits before the decimal point (blank-padded), and three digits
  // after it (zero-padded).  
  // 
  String makeFormattedString( double d )
  {
    String s = Double.toString( d );
    if (d < 100) s = " " + s;
    if (d < 10) s = " " + s;
    if (d == (int) d) s = s + ".";
    while (s.length() < 7) s = s + "0";
    if (s.length() > 7) s = s.substring( 0, 7 );
    return s;
  }

  //----------------------------------------------------------------------------
  // The showGraph() method calculates the mathematical placement of the frets
  // and displays the result table.  The Dowland calculations are performed
  // elsewhere.
  //
  // This is an ugly bit of code.
  //
  void showGraph( Graphics g )
  {
    Font   oldFont = g.getFont();
    int    height = this.size().height, width = this.size().width;
    int    cellHeight = g.getFontMetrics().getHeight(), cellWidth = width/5;
    int    y = 60, w = 50 + cellHeight + (cellHeight>>1);
    double foo, bar, baz;
    String s;

    // First, draw the table's separator lines.
    //
    g.setColor( Color.black );
    for (int i = 1; i < 5; i++)
      g.drawLine( cellWidth*i, ((i & 1) == 1) ? 50 : w,
                  cellWidth*i, height );
    g.drawLine( cellWidth, w, width, w );
    g.drawLine( 0, w + (cellHeight * 3), width, w + (cellHeight * 3));

    // Now put in all of the headings.
    //
    g.setFont( fntHelv );
    w = ((cellWidth * 2) - g.getFontMetrics().stringWidth( "Mathematical Spacing" )) / 2;
    g.drawString( "Mathematical Spacing", cellWidth + w, y );
    w = ((cellWidth * 2) - g.getFontMetrics().stringWidth( "Dowland Spacing" )) / 2;
    g.drawString( "Dowland Spacing", (cellWidth*3) + w, y );
    y += cellHeight*2;

    w = (cellWidth - g.getFontMetrics().stringWidth( "Distance from" )) / 2;
    g.drawString( "Distance from", (cellWidth + w), y );
    g.drawString( "Distance from", (cellWidth*2 + w), y );
    g.drawString( "Distance from", (cellWidth*3 + w), y );
    g.drawString( "Distance from", (cellWidth*4 + w), y );
    y += cellHeight;

    g.drawString( "Fret #", 5, y );
    w = (cellWidth - g.getFontMetrics().stringWidth( "nut (mm)" )) / 2;
    g.drawString( "nut (mm)", (cellWidth + w), y );
    w = (cellWidth - g.getFontMetrics().stringWidth( "bridge (mm)" )) / 2;
    g.drawString( "bridge (mm)", (cellWidth*2 + w), y );
    w = (cellWidth - g.getFontMetrics().stringWidth( "nut (mm)" )) / 2;
    g.drawString( "nut (mm)", (cellWidth*3 + w), y );
    w = (cellWidth - g.getFontMetrics().stringWidth( "bridge (mm)" )) / 2;
    g.drawString( "bridge (mm)", (cellWidth*4 + w), y );
    y += cellHeight * 2;

    // We're now ready to begin.
    //
    foo = CalcValue;
    MakeDowland();

    g.setFont( fntCour );  // Fixed-width fonts are our friends.

    // Get the horizontal offset within each "cell" of the table.
    //
    int oV = (cellWidth - g.getFontMetrics().stringWidth( "000.000" )) >> 1;

    for (int i = 0; i < 12; i++)
    {
      bar = foo / 1.059461;           // Get mathematical placement of next fret
      foo = bar;                      // Save it for next calculation.
      baz = dowland[ i ];             // Get Dowland placement of next fret
      g.drawString( name[i], 5, y );  // Label this row.

      // Now display their distances, both from the nut and from the bridge.
      //
      s = makeFormattedString(CalcValue-foo); g.drawString(s,cellWidth  +oV,y);
      s = makeFormattedString(foo          ); g.drawString(s,cellWidth*2+oV,y);
      s = makeFormattedString(baz          ); g.drawString(s,cellWidth*3+oV,y);
      s = makeFormattedString(CalcValue-baz); g.drawString(s,cellWidth*4+oV,y);
      y += cellHeight;
    }
    g.setFont( oldFont );  // Go back to the previous font.
  }


  //----------------------------------------------------------------------------
  // The paint() method tiles the lutewood graphic onto the background, then
  // (if a value has been entered) calls the showGraph() method.
  //
  public void paint( Graphics g )
  {
    int height = this.size().height, width = this.size().width;
    int imgheight = 100, imgwidth=100;
    int i, j;

    for (i = 0; i <= (height/imgheight); i++)
      for (j = 0; j <= (width/imgwidth); j++)
        g.drawImage( lutewood, j*imgwidth, (i*imgheight)+40, 100, 100, this );
    if (CalcValue > 0) showGraph( g );
  }


  //----------------------------------------------------------------------------
  // The init() method doesn't do much.  I think each of the five statements
  // is sufficiently self-explanatory that I don't need to bother commenting
  // them.
  // 
  public void init()
  {
    dowland = new double[12];
    createAppletLayout();
    lutewood = getImage( getCodeBase(), "lutewood.jpg" );
    fntCour = new Font( "Courier", Font.PLAIN, 12 );
    fntHelv = new Font( "Helvetica", Font.PLAIN, 12 );
  }


  //----------------------------------------------------------------------------
  // The performAction() method converts the data in the input field to a
  // double value.  If the numeric format is invalid, negative or zero,
  // the input field will be reset to "600", and the graph will not be shown.
  //
  void performAction()
  {
    try { CalcValue = Double.valueOf( entryArea.getText()).doubleValue(); }
    catch ( NumberFormatException e ) { CalcValue = 0; }
    if (CalcValue <= 0) entryArea.setText( "600" );
    paint( getGraphics());
  }


  //----------------------------------------------------------------------------
  // This is really sloppy, but there's only one control which causes an action
  // to happen, and it's the only button.
  //
  public boolean action( Event evt, Object arg )
  {
    if (evt.target instanceof Button)
      performAction();
    return true;
  }
}

