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

/**
 * An applet to provide an interactive, 2D visual explanation of football's
 * 'offside rule'. The applet places a number of icons representing players
 * (attackers of one team and defenders of the other) on a football pitch,
 * seen from plan view.
 * <p>
 * The user has the ability to drag each of these players into any desired
 * position on the pitch, in order to create a mock-up of a situation that could
 * arise in a real football match. The onside/offside status of the play at
 * that point (were the ball to be played forward) can then be determined
 * by clicking a button. The applet marks the 'offside threshold' (the point
 * beyond which attackers must not advance if they are to remain onside)
 * and reports the status of the play.
 * <p>
 * The user also has the ability to draw arbitrary temporary shapes on the
 * screen (in the manner of Andy Gray on Sky Sports...).
 *
 * @author Michael Fitzmaurice, April 2003
 */
public class OffsideApplet extends Applet implements ActionListener
{
    // :TODO:
    //
    // - make number and properties of players configurable via <PARAM> tags
    // - factor out all hardcoded config & magic numbers (see above)
    // - highlight multiple offside players (return Player[] ?)
    // - use a canvas in the center of the applet so that image is not clipped
    // - add a ball to one of the attackers (option?) and incorporate the
    //   more complex rules concerning running from behind ball, etc
    // - add a colour key indicating which team are attacking

    private Player[]     m_defenders,
                         m_attackers;

    private Player       m_offsidePlayer;

    private boolean      m_drawOffsideThreshold;

    private Label        m_messageLabel;

    private int          m_mouseXLocation,
                         m_mouseYLocation;

    private int          m_canvasHeight,
                         m_canvasWidth;

    private Player       m_selectedPlayer;

    private Button       m_playBallButton;

    private Image        m_pitchImage;

    // note the use of the jdk 1.2 style color constants (lower case) -
    // in jdk 1.4 the more correct upper case constants have been added,
    // but the old style ones are still supported for backwards compatibility
    private static final Color PLAYER_OUTLINE_COLOUR    = Color.white;

    private static final Color OFFSIDE_BOUNDARY_COLOUR  = Color.red;

    private static final Color ATTACKERS_SHIRT_COLOR    = new Color(72, 100, 255);

    private static final Color DEFENDERS_SHIRT_COLOR    = new Color(255, 25, 16);

    private static final Color GOALKEEPER_SHIRT_COLOR   = new Color(10, 150, 10);

    private static final int PLAYER_HEIGHT              = 30;

    private static final int PLAYER_WIDTH               = 30;

    public void init()
    {
        setLayout( new BorderLayout() );

        m_canvasHeight  = getBounds().height;
        m_canvasWidth   = getBounds().width;

        // how wide is 1 / 10 of the applet? use this to position
        // players in different scetions around the pitch
        int tenPercentOfWidth = m_canvasWidth / 10;

        m_defenders     = new Player[5];
        m_defenders[0]  = new Player(   DEFENDERS_SHIRT_COLOR,
                                        5,
                                        tenPercentOfWidth,
                                        m_canvasHeight / 2);
        m_defenders[1]  = new Player(   DEFENDERS_SHIRT_COLOR,
                                        3,
                                        tenPercentOfWidth * 3,
                                        m_canvasHeight / 3);
        m_defenders[2]  = new Player(   DEFENDERS_SHIRT_COLOR,
                                        2,
                                        tenPercentOfWidth * 6,
                                        m_canvasHeight / 4);
        m_defenders[3]  = new Player(   DEFENDERS_SHIRT_COLOR,
                                        4,
                                        tenPercentOfWidth * 8,
                                        m_canvasHeight / 2);
        m_defenders[4]  = new Player(   GOALKEEPER_SHIRT_COLOR,
                                        1,
                                        (m_canvasWidth / 2) - PLAYER_WIDTH,
                                        PLAYER_HEIGHT);

        m_attackers     = new Player[2];
        m_attackers[0]  = new Player(   ATTACKERS_SHIRT_COLOR,
                                        9,
                                        tenPercentOfWidth * 4,
                                        m_canvasHeight / 2);
        m_attackers[1]  = new Player(   ATTACKERS_SHIRT_COLOR,
                                        7,
                                        (m_canvasWidth / 2),
                                        m_canvasHeight / 3);

        this.addMouseListener( new MouseClickHandler() );
        this.addMouseMotionListener( new MouseMotionHandler() );

        // load up the background image & scale it to
        // fit the applet height / width
        m_pitchImage = getImage(getDocumentBase(), "footballPitch.gif");
        m_pitchImage = m_pitchImage.getScaledInstance(  m_canvasWidth,
                                                        m_canvasHeight,
                                                        Image.SCALE_DEFAULT);

        // set up GUI components
        m_messageLabel = new Label();
        m_messageLabel.setAlignment(Label.CENTER);
        m_messageLabel.setBackground( new Color(230, 230, 10) );
        m_messageLabel.setForeground(OFFSIDE_BOUNDARY_COLOUR);

        m_playBallButton = new Button("CHECK FOR OFFSIDE");
        m_playBallButton.addActionListener(this);
        Panel buttonPanel = new Panel();
        buttonPanel.setBackground(Color.gray);
        buttonPanel.add(m_playBallButton);

        this.add(m_messageLabel, BorderLayout.NORTH);
        this.add(buttonPanel, BorderLayout.SOUTH);
    }

    public void update(Graphics g)
    {
        // override update() to reduce flicker - no need to bother
        // redrawing the background on the on-screen Graphics object,
        // since the whole thing is copied from the off-screen Graphics
        // object once it is complete. To clear the on-screen background
        // first is the default behaviour - do not want this duplication
        paint(g);
    }

    /**
     * Paints the current scene onto the screen.
     *
     * @param g The Graphics context supplied by the applet's container
     */
    public void paint(Graphics g)
    {
        // use double buffering to reduce flicker - draw
        // the overall picture in stages offscreen
        Image offScreenImage        = createImage(m_canvasWidth, m_canvasHeight);
        Graphics offScreenGraphics  = offScreenImage.getGraphics();

        this.drawPitch(offScreenGraphics);

        for (int i = 0; i < m_defenders.length; i++)
        {
            m_defenders[i].drawSelf(offScreenGraphics);
        }

        for (int i = 0; i < m_attackers.length; i++)
        {
            m_attackers[i].drawSelf(offScreenGraphics);
        }

        if (m_drawOffsideThreshold)
        {
            this.drawOffsideBoundary(offScreenGraphics);
            m_drawOffsideThreshold = false;
        }

        // copy the finished off-screen image onto the canvas in one go
        g.drawImage (offScreenImage, 0, 0, this);
    }

    /**
     * Helper method to draw a line on screen marking the threshold of the
     * offside region, given the current scene.
     */
    private void drawOffsideBoundary(Graphics g)
    {
        // get the location of the last defender (used later
        // on to find the second-to-last defender)
        int lastDefenderPosition = m_canvasHeight;

        for (int i = 0; i < m_defenders.length; i++)
        {
            Player defender      = m_defenders[i];
            int defenderPosition = defender.getYPosition();

            if (defenderPosition < lastDefenderPosition)
            {
                lastDefenderPosition = defenderPosition;
            }
        }

        // offside threshold is the position of the second-to-last defender
        int offsideThreshold = m_canvasHeight;

        for (int i = 0; i < m_defenders.length; i++)
        {
            Player defender      = m_defenders[i];
            int defenderPosition = defender.getYPosition();

            // does this player represent the offside threshold?
            if (    (defenderPosition > lastDefenderPosition) &&
                    (defenderPosition < offsideThreshold) )
            {
                offsideThreshold = defenderPosition;
            }
        }

        g.setColor(OFFSIDE_BOUNDARY_COLOUR);
        g.drawLine(0, offsideThreshold, m_canvasWidth, offsideThreshold);
    }

    private void drawPitch(Graphics g)
    {
        g.drawImage(m_pitchImage, 0, 0, this);
    }

    /**
     * Helper method to return the (most) offside attacking player given
     * the current scene. If no players are offside, null is returned.
     */
    private Player getOffsidePlayer()
    {
        Player offsideAttacker = null;

        // relative position of the foremost attacker in the Y dimension
        // determines onside/offside status - record this position
        Player foremostAttacker          = m_attackers[0];
        int  foremostAttackerPosition    = foremostAttacker.getYPosition();

        for (int i = 1; i < m_attackers.length; i++)
        {
            Player attacker       = m_attackers[i];
            int attackerPosition  = attacker.getYPosition();

            // values of a lesser magnitude are further forward, since
            // the top of the screen is at 0 pixels in the Y plane, and
            // the goal line is at the top end of the scene
            if (attackerPosition < foremostAttackerPosition)
            {
                foremostAttacker          = attacker;
                foremostAttackerPosition  = attackerPosition;
            }
        }

        int numberOfGoalsideDefenders = 0;

        for (int i = 0; i < m_defenders.length; i++)
        {
            Player defender           = m_defenders[i];
            int defenderPosition      = defender.getYPosition();

            // level is considered onside (hence <=)
            if (defenderPosition <= foremostAttackerPosition)
                numberOfGoalsideDefenders++;
        }

        // there must be at least 2 defenders (goalkeeper counts) between the
        // foremost attacker and the goal-line in order for play to be onside
        if (numberOfGoalsideDefenders < 2)
            offsideAttacker = foremostAttacker;

        return offsideAttacker;
    }

    public void actionPerformed(ActionEvent e)
    {
        String message    = "PLAY IS ONSIDE!!!";

        m_offsidePlayer   = this.getOffsidePlayer();

        if (m_offsidePlayer != null)
        {
            message =
                m_offsidePlayer + " is offside - free kick to defending team";
        }

        m_messageLabel.setText(message);

        m_drawOffsideThreshold = true;

        repaint();
    }

    /////////////// INNER CLASSES FOR MOUSE EVENT HANDLING ///////////////

    private class MouseClickHandler extends MouseAdapter
    {
        public void mousePressed(MouseEvent e)
        {
            // the user has pressed the mouse button down but not released it -
            // this could be the start of an attempt to drag one of the player
            // icons to another location. In order to test whether or not this
            // is the case, the coordinates of the mouse pointer at the time of
            // the click must be compared to the coordinates of each player
            m_mouseXLocation = e.getX();
            m_mouseYLocation = e.getY();

            boolean foundSelection = false;

            for (int i = 0; i < m_defenders.length; i++)
            {
                Player player = m_defenders[i];

                if ( player.isSelected(m_mouseXLocation, m_mouseYLocation) )
                {
                    foundSelection    = true;
                    m_selectedPlayer  = player;
                    break;
                }
            }

            if (! foundSelection)
            {
                for (int i = 0; i < m_attackers.length; i++)
                {
                    Player player = m_attackers[i];

                    if ( player.isSelected(m_mouseXLocation, m_mouseYLocation) )
                    {
                        foundSelection    = true;
                        m_selectedPlayer  = player;
                        break;
                    }
                }
            }

            if (! foundSelection)
            {
                m_selectedPlayer = null;
                m_messageLabel.setText( "No player selected - switching to " +
                                        "drawing mode");
            }
            else
            {
                m_messageLabel.setText("Moving " + m_selectedPlayer);
            }
        }
    }

    private class MouseMotionHandler extends MouseMotionAdapter
    {
        public void mouseDragged(MouseEvent e)
        {
            int newXLocation    = e.getX();
            int newYLocation    = e.getY();

            if (m_selectedPlayer != null)
            {
                // find out by how much the player should move
                int xMovement        = newXLocation - m_mouseXLocation;
                int yMovement        = newYLocation - m_mouseYLocation;
                int oldXPosition     = m_selectedPlayer.getXPosition();
                int oldYPosition     = m_selectedPlayer.getYPosition();

                m_selectedPlayer.setXPosition(oldXPosition + xMovement);
                m_selectedPlayer.setYPosition(oldYPosition + yMovement);

                // this calls update() automatically
                repaint();
            }
            else
            {
                // allow the user to draw (temporary) lines & shapes on
                // the screen if they have not selected a player to move
                Graphics g = getGraphics();
                g.setColor(Color.black);

                g.drawLine( m_mouseXLocation,
                            m_mouseYLocation,
                            newXLocation,
                            newYLocation);
            }

            m_mouseXLocation  = newXLocation;
            m_mouseYLocation  = newYLocation;
        }
    }
    ///////////////////////////////////////////////////////////////////////////

    /**
     * An inner class to represent a player - essentially just an object
     * that is aware of:
     * <ul>
     *     <li>the location on screen of the icon representing it
     *     <li>how to draw this icon onto the screen on demand
     * </ul>
     */
    private class Player
    {
        private int             m_xPosition,
                                m_yPosition,
                                m_height,
                                m_width;

        private int             m_shirtNumber;

        private Color           m_color;

        // the thickness of the outer circle around each player
        private static final int BORDER_SIZE = 6;

        /**
         * Creates a new instance of <code>Player</code> with the desired
         * configuration
         *
         * @param color The colour of this player's shirt
         * @param number The shirt number of this player
         * @param x The starting position of this player in the horizontal
         * plane of the enclosing scene
         * @param y The starting position of this player in the vertical
         * plane of the enclosing scene
         */
        Player(Color color, int number, int x, int y)
        {
            m_color         = color;
            m_shirtNumber   = number;
            m_xPosition     = x;
            m_yPosition     = y;
            m_height        = PLAYER_HEIGHT;
            m_width         = PLAYER_WIDTH;
        }

        /**
         * Paints this player onto the supplied Graphics context in
         * the player's current position within the scene
         *
         * @param g The Graphics context on which to paint
         */
        public void drawSelf(Graphics g)
        {
            // draw the enclosing circle (the coloured border)
            g.setColor(PLAYER_OUTLINE_COLOUR);
            g.fillOval( m_xPosition,
                        m_yPosition,
                        m_width + BORDER_SIZE,
                        m_height + BORDER_SIZE);

            // draw the inner circle in the player's shirt colour
            g.setColor(m_color);
            g.fillOval( m_xPosition + (BORDER_SIZE / 2),
                        m_yPosition + (BORDER_SIZE / 2),
                        m_width,
                        m_height);

            // draw the player's shirt number in the outline colour
            g.setColor(PLAYER_OUTLINE_COLOUR);
            g.drawString(   "" + m_shirtNumber,
                            m_xPosition + (m_width / 2),
                            m_yPosition + ( m_height - (m_height / 4) ) );
        }

        /**
         * Tests whether or not a given location within the current scene
         * falls inside the boundaries of this player's on-screen icon
         *
         * @param x The location within the x plane of the position to compare
         * @param y The location within the y plane of the position to compare
         *
         * @return True if the supplied on-screen location falls within
         * the boundaries of this player's on-screen icon
         */
        public boolean isSelected(int x, int y)
        {
            // both x & y coordinates must be within the on-screen
            // boundaries of the image representing this object
            boolean insideX = false;
            boolean insideY = false;

            insideX = (x >= m_xPosition) && ( x <= (m_xPosition + m_width) );

            if (insideX)
                insideY = (y >= m_yPosition) && ( y <= (m_yPosition + m_height) );

            return (insideX && insideY);
        }

        public int getXPosition()
        {
            return m_xPosition;
        }

        public void setXPosition(int x)
        {
            m_xPosition = x;
        }

        public int getYPosition()
        {
            return m_yPosition;
        }

        public void setYPosition(int y)
        {
            m_yPosition = y;
        }

        public String toString()
        {
            return "Player number " + m_shirtNumber;
        }
    }
}

OffsideApplet.java