// Applet showing a mass-spring chain colliding with a block 
// J.W. Stumpel  <jstumpel@planet.nl>, November 2007    
// This is free software; no rights reserved 
// This is also my first Java program. The code is probably very clumsy; I'd
// like comments from Java experts.

import java.awt.*;
import java.applet.*;
import java.awt.event.*;

public class Bounce2 extends Applet implements ItemListener,
    ActionListener, Runnable
{
    final private static int STOPPED = 0;   /* Gawd, no #define in Java. */
    final private static int RUNNING = 1;
    final private static int PAUSED = 2;
    int state;
    Color blockColor = new Color (184, 134, 11);
    Color bgColor;
    CheckboxGroup massGroup = new CheckboxGroup ();
    Checkbox mass1 = new Checkbox ("1", massGroup, false);
    Checkbox mass2 = new Checkbox ("2", massGroup, false);
    Checkbox mass4 = new Checkbox ("4", massGroup, false);
    Checkbox mass8 = new Checkbox ("8", massGroup, true);
    Checkbox mass16 = new Checkbox ("16", massGroup, false);
    Checkbox mass50 = new Checkbox ("50", massGroup, false);
    CheckboxGroup springTypeGroup = new CheckboxGroup ();
    Checkbox spring1 = new Checkbox ("1", springTypeGroup, true);
    Checkbox spring2 = new Checkbox ("2", springTypeGroup, false);
    Checkbox spring3 = new Checkbox ("3", springTypeGroup, false);
    Checkbox attach = new Checkbox ("attach", false);
    Button runButton = new Button ("run");
    Button pauseButton = new Button ("pause");
    Label massLabel = new Label ("masses:");
    Label springLabel = new Label ("spring type:");
    MassSpringSystem springs;
    Thread animate;
    int displaytype;            /* 0: no springs drawn 
                                   1: springs are thin blue line
                                   2: thick springs, red when compressed, 
                                   green when relaxed/extended */
    private float timestep = 0.0075F;
    private int nMasses = 8;
    private int lineStart = 50, lineLength = 600, lineHeight = 40;
    private Dimension dim;
    private Image offScreen;
    private Graphics bufferGraphics;
    private boolean first = true, anchored = false;
    int speed = 1;              /* number of calculating steps per display step */
/*    int count = 0;*/

    public void init ()
    {
        String param = getParameter ("background");
          springs = new MassSpringSystem (nMasses, timestep);
          blockColor = new Color (184, 134, 11);
        if (param != null)
              bgColor = decodeColor (param);
        else
              bgColor = new Color (255, 255, 232);
          setBackground (bgColor);
          dim = getSize ();
          offScreen = createImage (dim.width, dim.height);
          bufferGraphics = offScreen.getGraphics ();
          setLayout (null);
          displaytype = 1;
          runButton.setBounds (700, 80, 50, 30);
          pauseButton.setBounds (750, 80, 50, 30);
          mass1.setBounds (80, 80, 40, 30);
          mass2.setBounds (120, 80, 40, 30);
          mass4.setBounds (160, 80, 40, 30);
          mass8.setBounds (200, 80, 40, 30);
          mass16.setBounds (240, 80, 40, 30);
          mass50.setBounds (280, 80, 45, 30);
          spring1.setBounds (475, 80, 35, 30);
          spring2.setBounds (510, 80, 35, 30);
          spring3.setBounds (545, 80, 35, 30);
          attach.setBounds (600, 80, 80, 30);
          massLabel.setBounds (0, 80, 80, 30);
          springLabel.setBounds (395, 80, 80, 30);
          add (runButton);
          add (pauseButton);
          add (mass1);
          add (mass2);
          add (mass4);
          add (mass8);
          add (mass16);
          add (mass50);
          add (spring1);
          add (spring2);
          add (spring3);
          add (massLabel);
          add (springLabel);
          add (attach);
          runButton.addActionListener (this);
          pauseButton.addActionListener (this);
          mass1.addItemListener (this);
          mass2.addItemListener (this);
          mass4.addItemListener (this);
          mass8.addItemListener (this);
          mass16.addItemListener (this);
          mass50.addItemListener (this);
          spring1.addItemListener (this);
          spring2.addItemListener (this);
          spring3.addItemListener (this);
          attach.addItemListener (this);
          setState (STOPPED);
    }

    void setState (int newState)
    {
        switch (newState)
        {
        case STOPPED:
            runButton.setLabel ("run");
            pauseButton.setVisible (false);
            attach.setEnabled (true);
            mass1.setEnabled (true);
            mass2.setEnabled (true);
            mass4.setEnabled (true);
            mass8.setEnabled (true);
            mass16.setEnabled (true);
            mass50.setEnabled (true);
            if (animate != null)
                animate = null;
            springs.anchor (anchored);
            springs.reset ();
            state = STOPPED;
            repaint ();
            break;
        case RUNNING:
            runButton.setLabel ("stop");
            pauseButton.setLabel ("pause");
            pauseButton.setVisible (true);
            attach.setEnabled (false);
            mass1.setEnabled (false);
            mass2.setEnabled (false);
            mass4.setEnabled (false);
            mass8.setEnabled (false);
            mass16.setEnabled (false);
            mass50.setEnabled (false);
            state = RUNNING;
            start ();
            break;
        case PAUSED:
            pauseButton.setLabel ("resume");
            if (animate != null)
                animate = null;
            state = PAUSED;
            break;
        }
    }

    public void update (Graphics g)
    {
        paint (g);
    }

    void putFixedElements (Graphics k)
    {
        k.setColor (blockColor);
        k.fillRect (10, 10, 40, 60);
    }

    void paintChain (Graphics k)
    {
        int j, n = springs.N;
        int x0 = springs.getCoords (lineLength);
        if (x0 > 600)           // stop simulation when about to run off the window 
        {
            setState (STOPPED);
            return;
        }

//  count++;
//  k.clearRect(200,0,100,12);
//  k.setColor(Color.black);
//  k.drawString("count: "+ count, 200,12);

        k.setColor (bgColor);
        k.clearRect (lineStart, lineHeight - 5, dim.width - lineStart, 10);
        switch (displaytype)
        {
        case 1:                // springs are thin blue line
            k.setColor (Color.blue);
            k.drawLine (lineStart + x0, lineHeight,
                        lineStart + springs.X[n - 1], lineHeight);
            break;
        case 2:                // springs are thick, change colour when compressed
            for (j = 0; j < n; j++)
            {
                if (springs.c[j] == true)
                    k.setColor (Color.red);
                else
                    k.setColor (Color.green);
                if (j == 0)
                    k.fillRect (lineStart + x0, lineHeight - 2,
                                springs.X[0] - x0 - 5, 4);
                else
                    k.fillRect (lineStart + springs.X[j - 1] + 5,
                                lineHeight - 2,
                                springs.X[j] - springs.X[j - 1] - 10, 4);
            }
            break;
        default:               // no springs shown
            break;
        }
        k.setColor (Color.black);
        for (j = 0; j < n; j++)
            k.fillRect (springs.X[j] + lineStart - 5, lineHeight - 5, 10, 10);
    }

    public void paint (Graphics g)
    {
        if (first == true)
        {
            putFixedElements (bufferGraphics);
            first = false;
        }
        paintChain (bufferGraphics);
        g.drawImage (offScreen, 0, 0, this);
        if (state == RUNNING)
            springs.step (speed);
    }

    public void actionPerformed (ActionEvent e)
    {
        if (e.getSource () == pauseButton)
        {
            if (state == RUNNING)
                setState (PAUSED);
            else
                setState (RUNNING);
        }
        else if (e.getSource () == runButton)
        {
            if (state == STOPPED)
                setState (RUNNING);
            else
                setState (STOPPED);
        }
    }

    public void itemStateChanged (ItemEvent e)
    {
        int oldtype = displaytype, oldmasses = nMasses;
        boolean oldanchor = anchored;

        if (mass1.getState ())
            nMasses = 1;
        else if (mass2.getState ())
            nMasses = 2;
        else if (mass4.getState ())
            nMasses = 4;
        else if (mass8.getState ())
            nMasses = 8;
        else if (mass16.getState ())
            nMasses = 16;
        else if (mass50.getState ())
            nMasses = 50;
        if (oldmasses != nMasses)
        {
            springs = new MassSpringSystem (nMasses, timestep);
            setState (STOPPED);
        }
        if (spring1.getState ())
            displaytype = 1;
        else if (spring2.getState ())
            displaytype = 2;
        else if (spring3.getState ())
            displaytype = 0;
        anchored = attach.getState ();
        if (oldanchor != anchored)
            setState (STOPPED); // state is already STOPPED, but this
        // forces a redraw 
        if (oldtype != displaytype)
            repaint ();
    }

    public void start ()
    {
        if (state == RUNNING)
        {
            if (animate == null)
            {
                animate = new Thread (this);
                animate.start ();
            }
        }
    }

// The next function was copied from a file on the Sun site, 
// http://java.sun.com/products/plugin/1.4/demos/plugin/applets/Animator/Animat
    private Color decodeColor (String s)
    {
        int val = 0;
        try
        {
            if (s.startsWith ("0x"))
                val = Integer.parseInt (s.substring (2), 16);
            else if (s.startsWith ("#"))
                val = Integer.parseInt (s.substring (1), 16);
            else if (s.startsWith ("0") && s.length () > 1)
                val = Integer.parseInt (s.substring (1), 8);
            else
                val = Integer.parseInt (s, 10);
            return new Color (val);
        }
        catch (NumberFormatException e)
        {
            return null;
        }
    }

    public void run ()
    {
        Thread thisThread = Thread.currentThread ();
        while ((animate == thisThread) && (state == RUNNING))
        {
            repaint ();
            try
            {
                Thread.sleep (20);
            }
            catch (InterruptedException e)
            {
                /* do nothing; don't know what this is anyway */
            }
        }
    }
}

class MassSpring
{
    float x;                    /* position */
    float v;                    /* speed    */
}

class MassSpringSystem
{
    int N;                      /* number of springs and masses */
    float delta, time;
    MassSpring m[], temp[], k1[], k2[], k3[], k4[];
    private int j;
    private boolean anchored = false;   /* First spring fixed to block. For test  */
    int X[];                    /* Integer version of coordinates         */
    boolean c[];                /* "true" when a spring is in compression */
      MassSpringSystem (int n, float interval)
    {
        N = n;
        m = new MassSpring[N];
        temp = new MassSpring[N];
        k1 = new MassSpring[N];
        k2 = new MassSpring[N];
        k3 = new MassSpring[N];
        k4 = new MassSpring[N];
        X = new int[N];
          c = new boolean[N];
        for (j = 0; j < N; j++)
        {
            m[j] = new MassSpring ();
            temp[j] = new MassSpring ();
            k1[j] = new MassSpring ();
            k2[j] = new MassSpring ();
            k3[j] = new MassSpring ();
            k4[j] = new MassSpring ();
        }
        delta = interval;
        time = 0.0F;
        reset ();
    }

    void anchor (boolean b)
    {
        anchored = b;
    }

    void reset ()
        /* set initial conditions for the system */
    {
        for (j = 0; j < N; j++)
        {
            if (anchored)
                m[j].x = 0;
            else
                m[j].x = 0.75F;
            m[j].v = -1.0F;
        }
    }

    void deriv (MassSpring[]in, MassSpring[]out)
        /* Take the positions and speeds from "in", and put their derivatives 
           (determined by the equations of motion of the system) in "out". 
           This is the "heart" of the simulation. */
    {
        float firstSpring;
        int j;
        if (anchored == true)
            firstSpring = 2 * in[0].x;
        else
            firstSpring = (in[0].x < 0.0) ? 2 * in[0].x : in[0].x;
        if (N == 1)
        {
            if (anchored)
                out[0].v = -in[0].x;
            else
                out[0].v = (in[0].x < 0) ? -in[0].x : 0;
        }
        else if (N == 2)
        {
            out[0].v = 4 * (in[1].x - firstSpring);
            out[1].v = 4 * (in[0].x - in[1].x);
        }
        else
        {
            out[0].v = N * N * (in[1].x - firstSpring);
            for (j = 1; j < N - 1; j++)
                out[j].v = N * N * (in[j - 1].x - 2 * in[j].x + in[j + 1].x);
            out[N - 1].v = N * N * (in[N - 2].x - in[N - 1].x);
        }
        for (j = 0; j < N; j++)
            out[j].x = in[j].v;
    }

    void step (int nSteps)
        /* Do some Runge-Kutta steps to find the positions and speeds 
           after time interval "delta". The accuracy of the Runge-Kutta 
           method is phenomenal. */
    {
        int i;
        for (i = 0; i < nSteps; i++)
        {
            deriv (m, k1);
            for (j = 0; j < N; j++)
            {
                temp[j].x = m[j].x + k1[j].x * delta / 2;
                temp[j].v = m[j].v + k1[j].v * delta / 2;
            }
            deriv (temp, k2);
            for (j = 0; j < N; j++)
            {
                temp[j].x = m[j].x + k2[j].x * delta / 2;
                temp[j].v = m[j].v + k2[j].v * delta / 2;
            }
            deriv (temp, k3);
            for (j = 0; j < N; j++)
            {
                temp[j].x = m[j].x + k3[j].x * delta;
                temp[j].v = m[j].v + k3[j].v * delta;
            }
            deriv (temp, k4);
            for (j = 0; j < N; j++)
            {
                m[j].x +=
                    (k1[j].x + 2 * k2[j].x + 2 * k3[j].x +
                     k4[j].x) * delta / 6;
                m[j].v +=
                    (k1[j].v + 2 * k2[j].v + 2 * k3[j].v +
                     k4[j].v) * delta / 6;
            }
            time += delta;
        }
    }

    int getCoords (int length)
        /* Convert coordinates of the masses to integer values in 
           the range [0..length]. User can retrieve them from 
           array X. 
           Array c holds booleans which indicate if the
           springs are compressed (true), or relaxed/extended (false). 
           Return value: coordinate of starting point of first spring */
    {
        int j;
        float scale = 0.25F, f;
        c[0] = m[0].x < 0;
        for (j = 1; j < N; j++)
            c[j] = m[j].x < m[j - 1].x;
        for (j = 0; j < N; j++)
        {
            f = (scale * N * m[j].x + j + 1) * length / N;
            X[j] = (int) f;
        }
        if (anchored)
            return 0;
        f = scale * m[0].x * length;
        if (f < 0)
            f = 0;
        return (int) f;
    }
}
