// LunarLander.java, version 1.10, July 7, 2008.
// Applet to demonstrate numerical integration in the simulation of a lunar lander.
// 
// Copyright 2000-2008 by Rick Wagner, all rights reserved.
//
// Created for use in the USC computer science course CS102. Use of this code
// is authorized for educational purposes only with proper attribution. Any
// derivative must carry this notice. No use without attribution.

import java.applet.*;
import java.awt.*;

public class LunarLander extends Applet implements Runnable
{
  // Instance variables (private where possible)
  private String sVerNum = "1.10";                       // Only constructors can run here.
  private Dimension dPanel;                              // The applet panel size.
  private Image imOffScreen = null;                      // Offscreen image for double buffering.
  private Graphics grOffScreen = null;                   // Offscreen graphics for double buffering.
  private Lander lunarLander;                            // A single Lander instance.
  private Thread tLander = null;                         // Thread for applet animation (repainting).
  private int iSleepInterval = 50;                       // Thread sleep interval.
  private float t = (float) 0.5;                         // Time slice for integration.
  private float throttle = (float) 0.5;                  // 0.6856 for constant-throttle soft landing.
  private Button startButton;
  private Button resetButton;
  private float gsfTotalTime = 0;                        // Realtime accumulator.

  // To allow browsers to get information about the applet:
  public String getAppletInfo()
  {
    return "LunarLander applet, version " + sVerNum +
           ", a lunar lander simulator program,\n" +
           "by Rick Wagner, copyright 2000-2008, all rights reserved.";
  }

  // Initialize the applet (primarily for building the GUI)
  public void init()
  {
    setBackground(Color.lightGray);
    dPanel = this.size();                               // The applet panel size (set in html code)

    // Create new Lander object (defined as a class below):
    lunarLander = new Lander();
    lunarLander.setThrottle(throttle);
    System.out.println(getAppletInfo());

    startButton = new Button("Start");
    startButton.setForeground(Color.black);
    startButton.setBackground(Color.lightGray);
    startButton.enable();
    resetButton = new Button("Reset");
    resetButton.setForeground(Color.black);
    resetButton.setBackground(Color.lightGray);
    resetButton.disable();

    this.add(startButton);
    this.add(resetButton);
  }

  public void run()
  {
    float v = 0;
    long lStartTime = System.currentTimeMillis();          // Get the current time in milliseconds.
    gsfTotalTime = 0;

    if (throttle > 0) lunarLander.setFlame(true);
    this.requestFocus();                                    // So we can get keyboard input.
    while (lunarLander.getAltitude() > 0)
    {
      try
      {
        tLander.sleep(iSleepInterval);                      // sleep() throws an exception.
      }                                              
      catch(InterruptedException e1)
      {
      }
      v = lunarLander.getSpeed();
      gsfTotalTime = ((float) (System.currentTimeMillis() - lStartTime)) / 1000;
      lunarLander.update(t);
      repaint();
    }

    lunarLander.setFlame(false);
    repaint();                                              // Repaint the applet frame.
    resetButton.enable();
    System.out.println("v = " + v);
    if (-v > 2.5)
    {
      this.showStatus("Crash! Velocity = " + Math.round(v) + " m/s.");
    }
    else
    {
      this.showStatus("Nice landing! Velocity = " + Math.round(v) + " m/s.");
    }

    if (tLander != null) tLander.stop();
    tLander = null;
  }

  public void start()
  {
    this.requestFocus();                                    // So we can get keyboard input
    this.showStatus("Up and down arrow keys to adjust throttle.");
  }

  // The applet runtime interpreter passes g to this applet frame painting function
  public void paint(Graphics g)
  {
    g.clearRect(0, 0, dPanel.width, dPanel.height);        // Necessary with double buffering.
    this.setBackground(Color.lightGray);

    g.drawString("Realtime: " + Float.toString(gsfTotalTime) + " seconds", 4, 51);

    // Ask the lander to paint itself:
    lunarLander.paint(g);

    // Surface of the moon:
    g.setColor(Color.gray);
    g.fillRect(1, dPanel.height - dPanel.height / 13, dPanel.width - 2, dPanel.height - 2);

    // Raised border for the applet:
    g.setColor(Color.white);
    g.drawLine(0, 0, dPanel.width - 1, 0);
    g.drawLine(0, 0, 0, dPanel.height);
    g.setColor(Color.black);
    g.drawLine(0, dPanel.height - 1, dPanel.width - 1, dPanel.height - 1);
    g.drawLine(dPanel.width - 1, 0, dPanel.width - 1, dPanel.height - 1);
  }

  // Implements double buffering
  public void update(Graphics g)
  {
    if (imOffScreen == null)
    {
      // Make sure the offscreen and graphics exist
      imOffScreen = this.createImage(dPanel.width, dPanel.height);
      grOffScreen = imOffScreen.getGraphics();
      grOffScreen.clearRect(0, 0, dPanel.width, dPanel.height);
    }
    this.paint(grOffScreen);
    g.drawImage(imOffScreen, 0, 0, null);
  }

  public boolean mouseDown(Event e, int x, int y)          // For mouse events
  {
    this.requestFocus();                                    // So we can get keyboard input
    return true;
  }

  public boolean keyDown(Event e, int k)                    // For keyboard events
  {
    //System.out.println("KeyDown " + k + "\n");
    switch (k)
    {
      case 'a':                                             // Autopilot on;
      {
        lunarLander.setAutoPilot(true);
        lunarLander.setThrottle(0);
        this.showStatus("Autopilot on");
        break;
      }
      case 1004:                                            // Up arrow (increase throttle)
      {
        if (lunarLander.getAutoPilot())
        {
          this.showStatus("On autopilot");
        }
        else
        {
          throttle += (float) 0.05;
          if (throttle > 1) throttle = 1;
          lunarLander.setThrottle(throttle);
          if (lunarLander.getAltitude() > 0)
          {
            this.showStatus("Throttle increased to " + throttle);
          }
        }
        break;
      }
      case 1005:                                            // Down arrow (decrease throttle)
      {
        if (lunarLander.getAutoPilot())
        {
          this.showStatus("On autopilot");
        }
        else
        {
          throttle -= (float) 0.05;
          if (throttle < 0) throttle = 0;
          lunarLander.setThrottle(throttle);
          if (lunarLander.getAltitude() > 0)
          {
            this.showStatus("Throttle decreased to " + throttle);
          }
        }
        break;
      }
      case 32:                                             // Space bar;
      {
        if (startButton.isEnabled())
        {
          startButtonAction();
        }
        else
        {
          if (resetButton.isEnabled())
          {
            resetButtonAction();
          }
        }
        break;
      }
    }
    return true;
  }

  public boolean action(Event e, Object o)
  {
    if (e.target == startButton)
    {
      startButtonAction();
    }
    if (e.target == resetButton)
    {
      resetButtonAction();
      this.requestFocus();                                    // So we can get keyboard input
    }
    return false;
  }

  public void startButtonAction()
  {
    tLander = new Thread(this);
    tLander.start();
    startButton.disable();
  }

  public void resetButtonAction()
  {
    resetButton.disable();
    startButton.enable();
    tLander = null;
    lunarLander = null;
    lunarLander = new Lander();
    throttle = (float) 0.5;
    lunarLander.setThrottle(throttle);
    this.showStatus("Throttle reset to " + throttle);
    lunarLander.setFlame(false);
    gsfTotalTime = 0;
    repaint();
  }

} // End of applet LunarLander class

// Class for the lunar lander:
class Lander
{
  private float gsfGravity = (float) 1.62;

  private Color cBodyColor = Color.black;
  private Color cBodyFillColor = Color.white;
  private Color cFlameColor = Color.yellow;

  private float _sfThrottle;                      // Ranges from 0 to 1.0, default 0.
  private float _sfVerticalSpeed;                 // Default 0 meters per second.
  private float _sfAltitude;                      // Default 1000 meters.
  private float _sfFuelMass;                      // Default 1700 kg.
  private float _sfLanderMass;                    // Default 900
  private float _sfMaxBurnRate;                   // Default 10 kg per second.
  private float _sfMaxThrust;                     // Default 5000 newtons.

  private int iNumPoints = 0;
  private int iNumFlamePoints = 0;
  private int bodyX[];
  private int bodyY[];
  private int flameX[];
  private int flameY[];
  private int tempBodyY[];
  private int tempFlameY[];

  private Polygon body;
  private Polygon flame;

  private boolean _bFlame;
  private boolean _bAutoPilot;

  private float _sfTotalTime;

  // Default constructor:
  Lander()
  {
    int i = 0;                        // Loop index.

    _sfThrottle = 0;                  // Ranges from 0 to 1.0, default 0.
    _sfVerticalSpeed = 0;             // Default 0 meters per second.
    _sfAltitude = 1000;               // Default 1000 meters.
    _sfFuelMass = 1700;               // Default 1700 kg.
    _sfLanderMass = 900;              // Default 900 kg.
    _sfMaxBurnRate = 10;              // Default 10 kg per second.
    _sfMaxThrust = 5000;              // Default 5000 newtons.
    _bFlame = false;                  // Flag for flame state.
    _bAutoPilot = false;              // Flag for autopilot state.
    _sfTotalTime = 0;                 // Landing time accumulator.

    iNumPoints = 28;                  // Number of points in the lander body polygon.
    iNumFlamePoints = 7;              // Number of points in the rocket flame.

    bodyX = new int[iNumPoints];      // Arrays for the polygon constructor.
    bodyY = new int[iNumPoints];
    tempBodyY = new int[iNumPoints];  // Only the Y dimension changes in this simulation.

    // Here we define the shape of the lander body polygon:
    bodyX[0] = 4; bodyY[0] = -2;
    bodyX[1] = 2; bodyY[1] = -8;
    bodyX[2] = 6; bodyY[2] = -8;
    bodyX[3] = 10; bodyY[3] = -1;
    bodyX[4] = 8; bodyY[4] = -1;
    bodyX[5] = 8; bodyY[5] = 0;
    bodyX[6] = 14; bodyY[6] = 0;
    bodyX[7] = 14; bodyY[7] = -1;
    bodyX[8] = 12; bodyY[8] = -1;
    bodyX[9] = 8; bodyY[9] = -8;
    bodyX[10] = 10; bodyY[10] = -8;
    bodyX[11] = 10; bodyY[11] = -18;
    bodyX[12] = 6; bodyY[12] = -18;
    bodyX[13] = 4; bodyY[13] = -24;
    bodyX[14] = -4; bodyY[14] = -24;
    bodyX[15] = -6; bodyY[15] = -18;
    bodyX[16] = -10; bodyY[16] = -18;
    bodyX[17] = -10; bodyY[17] = -8;
    bodyX[18] = -8; bodyY[18] = -8;
    bodyX[19] = -12; bodyY[19] = -1;
    bodyX[20] = -14; bodyY[20] = -1;
    bodyX[21] = -14; bodyY[21] = 0;
    bodyX[22] = -8; bodyY[22] = 0;
    bodyX[23] = -8; bodyY[23] = -1;
    bodyX[24] = -10; bodyY[24] = -1;
    bodyX[25] = -6; bodyY[25] = -8;
    bodyX[26] = -2; bodyY[26] = -8;
    bodyX[27] = -4; bodyY[27] = -2;

    // Now we scale the polygon to the size we want:
    for (i = 0; i < iNumPoints; i++)
    {
      bodyX[i] = 2 * bodyX[i] + 100;
      bodyY[i] = 2 * bodyY[i] + 100;
    }
    body = new Polygon(bodyX, bodyY, iNumPoints);                   // Polygon object constructor

    flameX = new int[iNumFlamePoints];
    flameY = new int[iNumFlamePoints];
    tempFlameY = new int[iNumFlamePoints];

    // Here we define the shape of the flame:
    flameX[0] = 0; flameY[0] = 12;
    flameX[1] = 2; flameY[1] = 8;
    flameX[2] = 4; flameY[2] = 2;
    flameX[3] = 4; flameY[3] = -2;
    flameX[4] = -4; flameY[4] = -2;
    flameX[5] = -4; flameY[5] = 2;
    flameX[6] = -2; flameY[6] = 8;

    // Now we scale the polygon to the size we want:
    for (i = 0; i < iNumFlamePoints; i++)
    {
      flameX[i] = 2 * flameX[i] + 100;
      flameY[i] = 2 * flameY[i] + 100;
    }
    flame = new Polygon(flameX, flameY, iNumFlamePoints);          // Polygon object constructor
  } // End default constructor.

  float getSpeed()                        // Accessor.
  {
    return _sfVerticalSpeed;
  }

  float getAltitude()                     // Accessor.
  {
    return _sfAltitude;
  }

  float getFuelMass()                     // Accessor.
  {
    return _sfFuelMass;
  }

  boolean getAutoPilot()                  // Accessor.
  {
    return _bAutoPilot;
  }

  void setThrottle(float setting)         // Mutator.
  {
    _sfThrottle = setting;
  }

  void setFlame(boolean b)                // Mutator.
  {
    _bFlame = b;
  }

  void setAutoPilot(boolean b)            // Mutator.
  {
    _bAutoPilot = b;
  }

  void update(float t)                    // Mutator.
  {
    // Update the lander's state after the passage of t seconds:

    int i = 0;
    int iTempFlameY = 0;

    // Intermediate variable declarations for code clarity:
    float sfDeltaV = 0;                                  // Velocity change.
    float sfDeltaY = 0;                                  // Altitude change.
    float sfDeltaM = 0;                                  // Fuel mass change.
    float sfForce = 0;                                   // Current thrust.
    float sfMass = 0;                                    // Total mass of the lander;

    // Autopilot feature:
    float sfThrottleAdjustment = (float) 0.2;

    // Fuel flow rate:
    if (_sfThrottle > 0 && _sfFuelMass <= 0) _sfThrottle = 0;

    // Update the accumulated time for this landing:
    _sfTotalTime += t;

    // Velocity change:
    sfForce = _sfThrottle * _sfMaxThrust;
    sfMass = _sfLanderMass + _sfFuelMass;
    if (sfMass == 0)                                     // Defensive programming.
    {
      System.out.println("Houston, we have a problem: can't divide by zero.");
    }
    else
    {
      sfDeltaV = t * ((sfForce / sfMass) - gsfGravity);  // Extra parentheses for clarity.
    }
    _sfVerticalSpeed += sfDeltaV;

    // Altitude change:
    sfDeltaY = t * _sfVerticalSpeed;                     // Negative speed is downward.
    _sfAltitude += sfDeltaY;

    // Fuel mass change:
    sfDeltaM = -t * _sfThrottle * _sfMaxBurnRate;        // Fuel mass can only decrease.
    _sfFuelMass += sfDeltaM;

    // Further adjustments:
    if (_sfAltitude < 0)
    {
      _sfAltitude = 0;
      _sfVerticalSpeed = 0;                              // Landed.
    }

    if (_sfFuelMass < 0)
    {
      _sfFuelMass = 0;                                   // Out of fuel;
      _bFlame = false;
    }
    else
    {
      if (_sfAltitude > 0)
      {
        if (_sfThrottle > 0)
        {
          _bFlame = true;
        }
      }
    }

    // Autopilot feature:
    if (_bAutoPilot)
    {
      if (_sfAltitude > 0)
      {
        if (_sfVerticalSpeed < -0.7 * Math.sqrt(_sfAltitude))  // Lucky hack.
        {
          _sfThrottle += sfThrottleAdjustment;
          if (_sfThrottle > 1) _sfThrottle = 1;
        }
        else
        {
          _sfThrottle -= sfThrottleAdjustment;                 // No deadband. Always adjusting.
          if (_sfThrottle < 0) _sfThrottle = 0;
        }
      }
    }

    // Move the lander according to altitude:
    for (i = 0; i < iNumPoints; i++)
    {
      tempBodyY[i] = bodyY[i] + (1000 - (int) _sfAltitude) / 2;
    }
    body = null;
    body = new Polygon(bodyX, tempBodyY, iNumPoints);                   // Polygon object constructor

    for (i = 0; i < iNumFlamePoints; i++)
    {
      iTempFlameY = flameY[i] - 98;
      iTempFlameY *= 4.0 * _sfThrottle;
      iTempFlameY += 98 + 4.0 * _sfThrottle;
      tempFlameY[i] = iTempFlameY + (1000 - (int) _sfAltitude) / 2;
    }
    flame = null;
    flame = new Polygon(flameX, tempFlameY, iNumFlamePoints);           // Polygon object constructor
  } // End of update() function.

  // The lander paint() function is called in the applet paint() function:
  public void paint(Graphics g)
  {
    g.drawString("Simtime: " + Float.toString(_sfTotalTime) + " seconds", 4, 40);

    if (_bFlame && _sfThrottle > 0)
    {
      g.setColor(cFlameColor);
      g.fillPolygon(flame);
    }

    g.setColor(cBodyFillColor);
    g.fillPolygon(body);
    g.setColor(cBodyColor);
    g.drawPolygon(body);
  }
} // End of Lander class

// Version History
//
// 1.08  October 30, 2001  Fixed the "zero-throttle start flame off bug" reported by "Captain"
//                         Hannes Mayer.
// 1.09  October 31, 2001  Added the feature for keyboard (space bar) control of the buttons for
//                         total "hands off the mouse" operation.
// 1.10  July 7, 2008      Adds finer granularity in the length of the flame graphic.