// RoboGame.java, version 1.13, first created July 25, 2006, last edited July 22, 2009.
// Applet for robotic simulation.
// Copyright 2006-2009 by Rick Wagner, all rights reserved.
// No use without attribution.

import java.applet.*;
import java.awt.*;

// This is an educational example of object oriented design for a robot group simulation applet.
// Use of this source code is authorized for educational purposes only. No use without
// proper attribution to Rick Wagner (Richard.J.Wagner@gmail.com).

// 637 lines of code (including comments and white space). Compiled with the Sun J2SE (JDK 1.6).

// The prefix naming convention used here is a modified Hungarian notation.
// "s" is string, "sf" is single precision floating point, "i" is integer, "b" is boolean,
// "d" is dimension, and "c" is color.

// Uses the "over and under" curley braces style to make it easy to match open and close braces.
// Uses single-precision floating point arithmetic to maximize performance. Double precision should
// be reserved for only those applications requiring it (it cuts computation speed in half). Unfortunately,
// the Java language authors chose to use double precision as their math library default.

// This Java source code is for educational purposes only. Viewing or downloading the source implies your
// acceptance of the restrictions: 

//   1. Use the source code for educational purposes only. 
//   2. Give appropriate attribution in all executables and listings derived from this source. 
//   3. Reproduce these restrictions and conditions in any derived programs. 


// Applet class definition, must be public because the brower creates an instance:

public class RoboGame extends Applet                     // A public class must be named the same as the file
{                                                        // that defines it
  // Applet instance variables:
  private String sVerNum = "1.13";                       // Only constructors can run here ("" is a constructor)
  private String sCompileDate = "July 22, 2009";
  private Dimension dApplet = null;                      // For the applet panel size (read from the html file)
  private Image imOffScreen = null;                      // Offscreen image for double buffering
  private Graphics grOffScreen = null;                   // Offscreen graphics for double buffering
  private Timer AnimationTimer = null;                   // Timer for control of the animation rate
  private int iTimerInterval = 0;                        // Timer interval in milliseconds
  private Label lblMessage = null;                       // To inform the user of applet state
  private final int iNumRobots = 17;                     // Set the number of robots here
  private Robot robotArray[] = new Robot[iNumRobots];    // Array of Robots
  private int iToggleState = 0;                          // Semaphore for applet state
  private int iInitFlag = 0;

  // To allow browsers to get information about the applet:
  public String getAppletInfo()
  {
    return "Robot social group applet, version " + sVerNum +
           ", by Rick Wagner, copyright 2006-2009,\nall rights reserved.\n\n" +
           "This is an educational example of object oriented design \n" +
           "using a timer to control animation rate.\n\n" +
           "Compiled " + sCompileDate + ". Source code use authorized for\n" +
           "educational purposes only. No use without attribution.\n";
  }

  // Initialize the applet
  public void init()
  {
    int i = 0;
    float sfTheta = 0;
    float x = 0;
    float y = 0;
    String sTemp = null;

    // Applet parameter:
    sTemp = getParameter("InitFlag");
    if (sTemp != null)
    {
      iInitFlag = Integer.valueOf(sTemp).intValue();
    }

    this.setBackground(Color.lightGray);                    // For the 25% gray browser background (c0c0c0)
    dApplet = this.size();                                  // Size is set in the calling html file
    iTimerInterval = 33;                                    // 33 ms is 30 frames per sec (TV rate)
    AnimationTimer = new Timer(this, iTimerInterval);       // 40 ms is 25 frames per sec, 67 is 15
    lblMessage = new Label("Welcome to Robot Game Simulation. Click to start.");
    lblMessage.setAlignment(Label.CENTER);
    this.add(lblMessage);

    for (i = 0; i < iNumRobots; i++)
    {
      robotArray[i] = new Robot();
      robotArray[i].setColor(get10Color(i % 10));
    }

    switch(iInitFlag)
    {
      case 0:
      {
        setRobotRandom();
        break;
      }

      case 1:
      {
        setRobotCircular();
        break;
      }

      default:
      {
        break;
      }
    }
  }                                                         // End of init()

  // Execute this code after initialization
  public void start()
  {
    this.requestFocus();                                    // So we can get keyboard input
    System.out.println("\n" + this.getAppletInfo());        // Identify self to the Java console-aware user
  }                                                         // End of start()

  // Execute this code when the browser leaves the page:
  public void stop()
  {
    AnimationTimer.stop();
  }                                                         // End of stop();

  public void onTimer()                                     // Called by the timer
  {
    int i;
    for (i = 0; i < iNumRobots; i++)
    {
      robotArray[i].update(i, iNumRobots, robotArray);      // Update the robots
    }
    repaint();
  }

  // The applet frame painting function
  public void paint(Graphics g)
  {
    // Code for displaying images or drawing in the applet frame
    int i = 0;

    g.clearRect(0, 0, dApplet.width, dApplet.height);
    this.setBackground(Color.lightGray);                    // Having been done in init(), one wouldn't think
                                                            // this is necessary (but it is).
    // Draw a recessed frame around the applet border. Designed for gray-on-gray browser background.
    g.setColor(Color.black);
    g.drawLine(0, 0, dApplet.width - 1, 0);
    g.drawLine(0, 0, 0, dApplet.height - 1);
    g.setColor(Color.white);
    g.drawLine(0, dApplet.height - 1, dApplet.width - 1, dApplet.height - 1);
    g.drawLine(dApplet.width - 1, 1, dApplet.width - 1, dApplet.height - 1);

    // Paint the robots:
    for (i = 0; i < iNumRobots; i++)
    {
      robotArray[i].paint(g, dApplet);
    }
  } // End of paint()

  // Implements double buffering for smooth animation
  public void update(Graphics g)
  {
    if (imOffScreen == null)
    {
      // Make sure the offscreen and graphics exist
      imOffScreen = this.createImage(dApplet.width, dApplet.height);
      grOffScreen = imOffScreen.getGraphics();
      grOffScreen.clearRect(0, 0, dApplet.width, dApplet.height);
    }
    this.paint(grOffScreen);
    g.drawImage(imOffScreen, 0, 0, null);
  }

  public boolean mouseDown(Event e, int x, int y)
  {
    switch (iToggleState)
    {
      case 0:
      {
        lblMessage.setText("Click to stop.");
        this.showStatus("Click to stop.");
        AnimationTimer.start();
        break;
      }
      case 1:
      {
        lblMessage.setText("Click to reset.");
        this.showStatus("Click to reset.");
        AnimationTimer.stop();
        break;
      }
      case 2:
      {
        lblMessage.setText("Click to start.");
        this.showStatus("Click to start.");
        switch(iInitFlag)
        {
          case 0:
          {
            setRobotRandom();
            break;
          }
          case 1:
          {
            setRobotCircular();
            break;
          }
          default:
          {
            break;
          }
        }
        repaint();
        break;
      }
    }
    iToggleState = (iToggleState + 1) % 3;
    this.requestFocus();                                         // Added July 4, 2008: works around bug added in
    return true;                                                 // Java 2SE. Enables keystroke reception.
  }

  public boolean keyDown(Event e, int k)
  {
    return true;
  }

  private Color get10Color(int i)
  {
    Color c = null;
    switch (i)
    {
      case 0:
      {
        c = Color.red;
        break;
      }
      case 1:
      {
        c = Color.blue;
        break;
      }
      case 2:
      {
        c = Color.white;
        break;
      }
      case 3:
      {
        c = Color.green;
        break;
      }
      case 4:
      {
        c = Color.yellow;
        break;
      }
      case 5:
      {
        c = Color.cyan;
        break;
      }
      case 6:
      {
        c = Color.orange;
        break;
      }
      case 7:
      {
        c = Color.pink;
        break;
      }
      case 8:
      {
        c = Color.magenta;
        break;
      }
      case 9:
      {
        c = Color.yellow.darker();
        break;
      }
    }
    return c;
  }                                                              // End of get10Color()

  private void setRobotRandom()
  {
    float x = 0;
    float y = 0;
    float sfTheta = 0;
    int i = 0;
    int j = 0;
    boolean bClear = false;
    float sfDistance = 0;

    for (i = 0; i < iNumRobots; i++)
    {
      bClear = false;
      while (!bClear)
      {
        bClear = true;
        x = (float) (30 * Math.random());    // 30 times the paint scale factor is 600 pixels
        y = (float) (30 * Math.random());

        // See if these coordinates are near any existing robot
        for (j = 0; j < iNumRobots; j++)
        {
          if (i != j)
          {
            // Compute the ith proposed coordinates distance to the jth robot
            sfDistance = ((x - robotArray[j].getX()) * (x - robotArray[j].getX())) +
                         ((y - robotArray[j].getY()) * (y - robotArray[j].getY()));
            if (sfDistance < 2)
            {
              bClear = false;
              break;                                             // Break out of for j
            }
          }
        }        
      }
      // bClear is true so pose the ith robot
      sfTheta = (float) (2 * Math.PI * Math.random() - Math.PI); // Normalized to +/- pi from east.
      robotArray[i].pose(x, y, sfTheta);
    }
    repaint();
  }                                                              // End of setRobotRandom();

  private void setRobotCircular()
  {
    float x = 0;
    float y = 0;
    float sfTheta = 0;
    float sfDeltaAngle = 0;
    float sfTempAngle = 0;
    int i = 0;
    int j = 0;
    float sfDistance = 0;

    sfDeltaAngle = (float) (2 * Math.PI / iNumRobots);                     // Pie slice for circular position

    for (i = 0; i < iNumRobots; i++)
    {
      sfTempAngle += sfDeltaAngle;
      sfTheta = (float) (sfTempAngle - Math.PI);      // Normalized to +/- pi from east.
      x = (float) (15 * Math.cos(sfTheta) + 15);      // 30 times the paint scale factor is 600 pixels
      y = (float) (15 * Math.sin(sfTheta) + 15);

      sfTheta += (float) Math.PI;

      robotArray[i].pose(x, y, sfTheta);
    }
  }                                                              // End of setRobotRandom();
}                                                                // End of Applet class


class Timer implements Runnable
{
  // Calls the onTimer() function in the calling applet every iInterval milliseconds.
  // Copyright 1998 and 1999 by Rick Wagner, all rights reserved. No use without attribution.
  // Note: a timer object is now included in the J2SE, (JDK 1.5, 1.6 development kit).

  private Thread TimerThread = null;
  private int iInterval = 0;
  private RoboGame app = null;                                  // The calling applet

  public Timer(RoboGame a, int i)                               // Interval constructor
  {
    iInterval = i;
    app = a;
  }

  public void start()
  {
    TimerThread = new Thread(this);
    TimerThread.start();
  }

  public void stop()
  {
    if (TimerThread != null) TimerThread.stop();
    TimerThread = null;
  }

  public void run()
  {
    long lStartTime = System.currentTimeMillis();
    long lTemp = 0;
    int iSleepTime = iInterval / 10;
    if (iSleepTime == 0) iSleepTime = 1;
    while (true)
    {
      try
      {
        TimerThread.sleep(iSleepTime);                           // Wake up every 1/10 interval and see
      }                                                          // if it's time yet.
      catch(InterruptedException e1)
      {
      }
      lTemp = System.currentTimeMillis();
      if (lTemp - lStartTime >= iInterval)
      {
        app.onTimer();                                           // Trigger the desired events
        lStartTime = lTemp;
      }
    }
  }
}                                                               // End of Timer class

class Robot
{
  private float sfDirection;          // Direction of travel in radians from east
  private float sfPx;                 // Position
  private float sfPy;
  private float sfX[] = new float[5]; // Float arrays for the robot outline polygon
  private float sfY[] = new float[5];
  private int iX[] = new int[5];      // Integer arrays for constructing the polygon for painting
  private int iY[] = new int[5];
  private float sfPaintScaleFactor;
  private Color cRobot;

  public Robot()                      // Default constructor
  {
    cRobot = Color.white;
    sfPx = 0;
    sfPy = 0;
    sfPaintScaleFactor = 20;          // Robot is painted 20 pixels long
    pose(0, 0, 0);
  }

  public Robot(float px, float py, float theta)                  // Position and direction constructor
  {
    cRobot = Color.white;
    sfPx = px;
    sfPy = py;
    sfPaintScaleFactor = 20;                                    // Robot is painted 20 pixels long
    pose(sfPx, sfPy, theta);
  }

  public void setColor(Color c)                                 // Mutator
  {
    cRobot = c;
  }

  public float getX()                                           // Accessor
  {
    return sfPx;
  }

  public float getY()                                           // Accessor
  {
    return sfPy;
  }

  public void pose(float x, float y, float a)
  {
    // Sets the position and orientation of the robot and computes the outline polygon:
    int i = 0;
    float sfTempX = 0;
    float sfTempY = 0;
    float sfSin = 0;
    float sfCos = 0;

    sfX[0] = (float) -0.5; sfY[0] = (float) -0.375;
    sfX[1] = (float) 0.5; sfY[1] = (float) -0.375;
    sfX[2] = (float) 0.75; sfY[2] = (float) 0.0;
    sfX[3] = (float) 0.5; sfY[3] = (float) 0.375;
    sfX[4] = (float) -0.5; sfY[4] = (float) 0.375;

    sfSin = (float) Math.sin(a);                               // Compute these just once and store
    sfCos = (float) Math.cos(a);

    for (i = 0; i < 5; i++)
    {
      sfTempX = sfX[i]; sfTempY = sfY[i];
      sfX[i] = x + sfTempX * sfCos - sfTempY * sfSin;
      sfY[i] = y + sfTempX * sfSin + sfTempY * sfCos;
    }
    sfPx = x;
    sfPy = y;
    sfDirection = a;
  }

  public void update(int iMyIndex, int iNumRobots, Robot[] rArray)
  {
    int i = 0;

    float sfIncrement = (float) 0.07;                                         // Sets the default speed of forward
                                                                              // motion
    float sfDistance = 0;                                                     // Actually, the distance squared,
    float sfShortestDistance = 800 * 800;                                     // we don't take the square root
    float sfSecondShortest = 800 * 800;                                       // for performance reasons

    int iNearestRobotIndex = 0;
    int iSecondNearest = 0;

    float sfAngleToNearest = 0;
    float sfAngleToMidPoint = 0;
    float sfDistanceToMidPoint = 0;
    float sfSqrtDistance = 0;                                                 // Sometimes need the actual distance
    float sfDeltaAngle = 0;

    float sfDeltaX = 0;
    float sfDeltaY = 0;

    float sfMidPointX = 0;
    float sfMidPointY = 0;

    float sfTurnFactor = 0;

    // Find the distances to all the other robots and identify the shortest and next shortest
    for (i = 0; i < iNumRobots; i++)
    {
      if (i != iMyIndex)
      {
        sfDistance = ((sfPx - rArray[i].sfPx) * (sfPx - rArray[i].sfPx)) +
                     ((sfPy - rArray[i].sfPy) * (sfPy - rArray[i].sfPy));

        if (sfDistance < sfShortestDistance)
        {
          sfShortestDistance = sfDistance;
          iNearestRobotIndex = i;
        }
        else
        {
          if (sfDistance < sfSecondShortest)
          {
            sfSecondShortest = sfDistance;
            iSecondNearest = i;
          }
        }
      }
    }

    // Move away from nearest robot if too close
    if (sfShortestDistance < 3.1)
    {
      sfSqrtDistance = (float) Math.sqrt(sfDistanceToMidPoint);
      if (sfSqrtDistance < 0.25) sfSqrtDistance = (float) 0.25;

      // Find the direction *from* the nearest robot:
      sfAngleToNearest = (float) Math.atan2(sfPy - rArray[iNearestRobotIndex].sfPy,
                                            sfPx - rArray[iNearestRobotIndex].sfPx);

      // Find the delta angle to the nearest robot:
      sfDeltaAngle = normalizeAngle(sfAngleToNearest - sfDirection);

      // Change direction toward the delta angle by a degree if the delta is greater than a degree:
      if (sfDeltaAngle > 0.017 || sfDeltaAngle < -0.017)                      // There are 17 milliradians
      {                                                                       // in a degree of angle
        if (sfDeltaAngle > 0)
        {
          sfDirection += 0.017 * 2.5 / sfSqrtDistance;                        // Turn angle in radians
        }                                                                     // 2.5 is tuning
        if (sfDeltaAngle < 0)
        {
          sfDirection -= 0.017 * 2.5 / sfSqrtDistance;
        }
      }
      sfDeltaX = (float) (Math.cos(sfDirection) * sfIncrement * 0.3 / (sfSqrtDistance)); // 0.3 is  tuning
      sfDeltaY = (float) (Math.sin(sfDirection) * sfIncrement * 0.3 / (sfSqrtDistance));
      sfPx += sfDeltaX;
      sfPy += sfDeltaY;
      pose(sfPx, sfPy, normalizeAngle(sfDirection));
    }
    else
    {
      // Compute the point midway between the nearest and second nearest:
      sfMidPointX = (rArray[iNearestRobotIndex].sfPx + rArray[iSecondNearest].sfPx) / 2;
      sfMidPointY = (rArray[iNearestRobotIndex].sfPy + rArray[iSecondNearest].sfPy) / 2;

      // Find the distance to the midpoint:
      sfDistanceToMidPoint = ((sfPx - sfMidPointX) * (sfPx - sfMidPointX)) +
                             ((sfPy - sfMidPointY) * (sfPy - sfMidPointY));

      // Move toward the midpoint if not close enough:
      if (sfDistanceToMidPoint > 1)
      {
        // Find the angle to the midpoint:
        sfAngleToMidPoint = (float) Math.atan2(sfMidPointY - sfPy, sfMidPointX - sfPx);

        // Find the delta angle to the midpoint:
        sfDeltaAngle = normalizeAngle(sfAngleToMidPoint - sfDirection);

        sfSqrtDistance = (float) Math.sqrt(sfDistanceToMidPoint);
        sfTurnFactor = Math.abs(sfDeltaAngle) * sfSqrtDistance * ((float) 0.6); // 0.6 is tuning

        if (sfTurnFactor > 17)
        {
          sfTurnFactor = 17;
        }

        // Change direction toward the delta angle by a factor if the delta is greater than a degree:
        if (sfDeltaAngle > 0.017 || sfDeltaAngle < -0.017)                    // There are 17 milliradians
        {                                                                     // in a degree of angle
          if (sfDeltaAngle > 0)
          {
            sfDirection += 0.017 * sfTurnFactor;                              // sfTurnFactor degrees turn;
          }
          if (sfDeltaAngle < 0)
          {
            sfDirection -= 0.017 * sfTurnFactor;
          }
        }
        sfDeltaX = (float) (Math.cos(sfDirection) * sfIncrement * sfSqrtDistance * 0.9); // 0.9 is tuning
        sfDeltaY = (float) (Math.sin(sfDirection) * sfIncrement * sfSqrtDistance * 0.9);

        sfPx += sfDeltaX;
        sfPy += sfDeltaY;
        pose(sfPx, sfPy, normalizeAngle(sfDirection));
      }
    }
  }                                                                            // End of Robot.update()

  public void paint(Graphics g, Dimension d)
  {
    Polygon pgon = null;
    int i;

    for (i = 0; i < 5; i++)
    {
      iX[i] = 140 + Math.round(sfPaintScaleFactor * sfX[i]);                  // Set the polygon array for
      iY[i] = 720 - Math.round(sfPaintScaleFactor * sfY[i]);                  // painting
    }
    pgon = new Polygon(iX, iY, 5);
    g.setColor(cRobot);
    g.fillPolygon(pgon);
    g.setColor(Color.black);
    g.drawPolygon(pgon);
  }                                                                            // End of Robot.paint()

  private float normalizeAngle(float a)                                        // Normalize angle to +/- pi
  {                                                                            // radians
    float r = a;
    if (a > Math.PI) r -= (float) (Math.PI * 2.0);
    if (a <= -Math.PI) r += (float) (Math.PI * 2.0);
    return r;
  }                                                                            // End of Robot.normalizeAngle()
}                                                                              // End of Robot class, line 637