//
//  Rocket Science 101 Version 1.0 - Copyright (C) 1996, 2000 by Lorenz Wiest
//
//  <APPLET CODE="RocketScience.class" WIDTH=641 HEIGHT=481></APPLET>
//
//
//  "Contrary to popular opinion, filename is not from the English terms
//   name and file. It is pronounced 'feh-LEE-nah-mee' and comes from the
//   Tibetan, meaning 'What it is ?'"
//                                         -- David Swift on CompuServe
//
//
//  "Three things are certain:
//   Death, taxes, and lost data.
//   Guess which has occurred."
//
//  "Chaos reigns within.
//   Reflect, repent, and reboot.
//   Order shall return."
//
//  "Yesterday it worked,
//   Today it is not working.
//   The web is like that."
//
//  "Serious error.
//   All shortcuts have disappeared.
//   Screen. Mind. Both are blank."
//
//  "To have no errors
//   Would be life without meaning.
//   No struggle, no joy."
//
//        -- Error Messages of the BeOS's web-browser (actually Japanese haiku!)
//
//
// GNU License
// =============================================================================
// This program is free software; you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation; either version 2 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program; if not, write to the Free Software Foundation, Inc., 675 Mass
// Ave, Cambridge, MA 02139, USA.
//
//
// Notes
// =============================================================================
// - Code is Java 1.0.2 compliant and should run on any Java-enabled web browser.
// - It is not possible to declare a final static variable local to a method.
//

import java.applet.*;
import java.awt.*;
import java.awt.image.*;

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                             RocketScience                                  //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

public final class RocketScience extends Applet implements Runnable, Sleeper {

    final static int   WIDTH   = 641;
    final static int   HEIGHT  = 481;
    final static int   MX      = WIDTH  / 2;
    final static int   MY      = HEIGHT / 2;

    final static int   DELTAT  = 20;  // timespan [s] per timestep
    final static int   TSTEPS  = 40;  // timesteps between frames

    final static float ZOOM    = (float)(150.0 / Spacecraft.GEOSTATIONARY_RADIUS);

    final static int   NOTE_TIMERID  = 1;
    final static int   DEMO_TIMERID  = 2;
    final static int   SOYUZ_TIMERID = 3;

    final static int   DEMO_TIME         = 120;  // in [s]
    final static int   SOYUZ_LAUNCH_TIME =   5;
    final static int   NOTE_TIME         =   2;

    Image     offScreen;
    Graphics  offScreenGC;
    Rectangle offScreenClipRect;

    Spacecraft    Atlantis;
    Spacecraft    Soyuz;
    MirSpacecraft Mir;

    Bob    AtlantisBob;
    MirBob MirBob;
    Bob    SoyuzBob;
    Bob    AtlantisJetBob;
    Bob    SoyuzJetBob;
    Bob    DotsBob;
    Bob    EarthBob;
    Bob    NoteBob;
    Bob    AtlantisLabelBob;
    Bob    MirLabelBob;
    Bob    SoyuzLabelBob;
    Bob    TachoBob;

    Image  stripMirImage;
    Image  stripAtlantisImage;
    Image  stripSoyuzImage;
    Image  stripJetImage;
    Image  stripDotsImage;
    Image  stripEarthImage;
    Image  stripNoteImage;
    Image  stripLabelsImage;
    Image  stripTachoImage;

    Gadget StartGadget;
    Gadget BrakeGadget;
    Gadget SpeedGadget;
    Gadget ClearGadget;

    Image stripStartGadget;
    Image stripBrakeGadget;
    Image stripSpeedGadget;
    Image stripClearGadget;

    Timer NoteTimer;
    Timer DemoTimer;
    Timer SoyuzTimer;

    boolean InDemoMode          = true;
    boolean DoResetGame         = true;
    boolean DoRejectMouseEvents = true;

    Autopilot Autopilot = new Autopilot();

    DirtyRect dirtyRect;
    Thread  gameThread = null;

//*** Applet ******************************************************************
    public String getAppletInfo() {
        return("RocketScience 101 Version 1.0 - Build 240300 - Copyright 1996, 2000 by Lorenz Wiest");
    }

//*** Applet ******************************************************************
    public void start() {

     // Initialize backbuffer

        if(offScreenGC==null) {
            offScreen         = this.createImage(WIDTH, HEIGHT);
            offScreenGC       = offScreen.getGraphics();
            offScreenClipRect = new Rectangle(0, 0, WIDTH, HEIGHT);
        }

     // Initialize spacecrafts

        Atlantis = new    Spacecraft(0, 0, 0, 0, true);
        Soyuz    = new    Spacecraft(0, 0, 0, 0, false);
        Mir      = new MirSpacecraft(0, 0, 0, 0, true);

     // Init Timer

        NoteTimer  = new Timer(this, NOTE_TIMERID );
        DemoTimer  = new Timer(this, DEMO_TIMERID );

        SoyuzTimer = new Timer(this, SOYUZ_TIMERID);

     // Init dirtyRect

        dirtyRect = new DirtyRect();

        InDemoMode          = true;
        DoResetGame         = true;
        DoRejectMouseEvents = true;

        if(gameThread==null) {
            gameThread = new Thread(this, "RocketScience 101 Game Thread");
            gameThread.start();
        }
    }

//*** Applet ******************************************************************
    public void stop() {

        if(gameThread!=null && gameThread.isAlive())
            gameThread.stop();
        gameThread = null;

        SoyuzTimer.stop();
        DemoTimer.stop();
        NoteTimer.stop();
    }

//*** Applet ******************************************************************
    public void run() {
        final int FRAME_SLEEP = 50;

        offScreenGC.setColor(Color.white);
        offScreenGC.fillRect(0, 0, WIDTH, HEIGHT);

     // Load image strips via MediaTracker

        stripMirImage      = this.getImage(this.getCodeBase(), "mir.gif");
        stripAtlantisImage = this.getImage(this.getCodeBase(), "atlantis.gif");
        stripSoyuzImage    = this.getImage(this.getCodeBase(), "soyuz.gif");
        stripJetImage      = this.getImage(this.getCodeBase(), "jet.gif");
        stripDotsImage     = this.getImage(this.getCodeBase(), "dots.gif");
        stripEarthImage    = this.getImage(this.getCodeBase(), "earth.gif");
        stripNoteImage     = this.getImage(this.getCodeBase(), "note.gif");
        stripLabelsImage   = this.getImage(this.getCodeBase(), "labels.gif");
        stripTachoImage    = this.getImage(this.getCodeBase(), "tacho.gif");
        stripStartGadget   = this.getImage(this.getCodeBase(), "reset.gif");
        stripBrakeGadget   = this.getImage(this.getCodeBase(), "brake.gif");
        stripSpeedGadget   = this.getImage(this.getCodeBase(), "speed.gif");
        stripClearGadget   = this.getImage(this.getCodeBase(), "clear.gif");

        MediaTracker tracker = new MediaTracker(this);

        tracker.addImage(stripMirImage,      0);
        tracker.addImage(stripAtlantisImage, 1);
        tracker.addImage(stripSoyuzImage,    2);
        tracker.addImage(stripJetImage,      3);
        tracker.addImage(stripDotsImage,     4);
        tracker.addImage(stripEarthImage,    5);
        tracker.addImage(stripNoteImage,     6);
        tracker.addImage(stripLabelsImage,   7);
        tracker.addImage(stripStartGadget,   8);
        tracker.addImage(stripBrakeGadget,   9);
        tracker.addImage(stripSpeedGadget,  10);
        tracker.addImage(stripClearGadget,  11);
        tracker.addImage(stripTachoImage,   12);

        for(int i=0; i<13; i++) {
            try { tracker.waitForID(i); } catch (InterruptedException e) {}
            paintTracker( 100 * i / (13-1));
        }

     // Initialize bobs

        initBobs();

        MirBob.setFrameRow(MIRBOB_ROW_1SOYUZ);
        Autopilot.setSoyuzLaunchCount(2);

     // Initialize gadgets

        StartGadget = new Gadget(155, 450, stripStartGadget, offScreen, this);
        BrakeGadget = new Gadget(255, 450, stripBrakeGadget, offScreen, this);
        SpeedGadget = new Gadget(328, 450, stripSpeedGadget, offScreen, this);
        ClearGadget = new Gadget(428, 450, stripClearGadget, offScreen, this);

     // Game loop starts here

        InDemoMode          = true;
        DoRejectMouseEvents = false;
        DoResetGame         = true;

        boolean saveDemoMode;

        while(Thread.currentThread()==gameThread) {
            if(DoResetGame)
                resetGame();
            DoResetGame = false;

            if(InDemoMode)
                enterDemoMode();
            else
                exitDemoMode();

            saveDemoMode = InDemoMode;

            while(!DoResetGame && saveDemoMode==InDemoMode) {
                Autopilot.nextManeuver(MirBob, SoyuzBob, Mir, Soyuz, DELTAT);

                if((!Mir.blockAtlantisDocking()) && (!InDemoMode) && (!Atlantis.hasDocked()))
                    handleAtlantisRendezvous();

                for(int j=0; j<TSTEPS; j++) {
                    Mir.newPosition(DELTAT);
                    if(!Atlantis.hasDocked()) {
                        if(Atlantis.r < EARTHBOB_RADIUS) {
                            InDemoMode  = true;
                            DoResetGame = true;
                            break;
                        }
                        float normThrust = Atlantis.thrust / Atlantis.v;
                        Atlantis.thrustX = normThrust * Atlantis.vx;
                        Atlantis.thrustY = normThrust * Atlantis.vy;
                        Atlantis.newPosition(DELTAT);
                    }
                    if(Soyuz.onScreen) {
                        Autopilot.navigate(MirBob, SoyuzBob, Mir, Soyuz);
                        Soyuz.newPosition(DELTAT);
                    }
                }

                updateBobPositions(Autopilot);
                drawBobs();

             // update(this.getGraphics());
                paint(this.getGraphics());

                System.gc(); // Synchronous garbage collection assures smooth animation
                try { Thread.sleep(FRAME_SLEEP); } catch ( InterruptedException e ){}
            }
        }
    }

//*** Applet ******************************************************************
    public void paint(Graphics g) {
        if(offScreenGC!=null)
            blit(g, 0, 0, offScreen, 0, 0, WIDTH, HEIGHT);
    };
//*** Applet ******************************************************************
    public void update(Graphics g) {
        if(offScreenGC!=null)
            blit(g, offScreenClipRect.x, offScreenClipRect.y, offScreen, 0, 0,
                offScreenClipRect.width, offScreenClipRect.height);
    };

//******************************************************************************
    public void paintTracker(int percent) {
        final int x = ( WIDTH - 300 ) / 2;
        final int y = 240;
        final int w = 300;
        final int h = 15;

        Font        ArialFont = new Font("Arial", Font.PLAIN, 12);
        FontMetrics metrics   = this.getFontMetrics(ArialFont);
        String      mylabel   = "Rocket Science 101 is loading. Please wait.";

        int labelOffset = (WIDTH - metrics.stringWidth(mylabel) ) / 2;

        Graphics g = offScreenGC;
        g.setFont(ArialFont);
        g.setColor(Color.black);
        g.drawString(mylabel, labelOffset, y - 6);

        int offsetX = (int) (w - 1) * percent / 100;
        g.setColor(Color.black);
        g.drawRect(0, 0, WIDTH-1, HEIGHT-1);
        g.drawRect(x, y, w, h);

        g.setColor(Color.blue);
        g.fillRect(x + 1, y + 1, offsetX , h - 1);

        g.setColor(Color.white);
        g.fillRect(x + 1 + offsetX, y + 1, w - 1 - offsetX - 1 , h - 1);

        paint(this.getGraphics());
   }

//******************************************************************************
   public boolean handleEvent(Event event) {

// To be compatible with old browsers I use the old Java 1.0 event model here

        if(DoRejectMouseEvents)
            return false;

        switch(event.id) {
            case Event.MOUSE_DOWN:
                if(StartGadget.inside(event.x, event.y)) {
                    StartGadget.down();
                    DemoTimer.setSnooze( DEMO_TIME );
                }

                if(BrakeGadget.inside(event.x, event.y)) {
                    BrakeGadget.down();
                    if(InDemoMode)
                        InDemoMode  = false;

                    DemoTimer.setSnooze( DEMO_TIME );
                    Atlantis.thrust = Spacecraft.ATLANTIS_BRAKE_THRUST;
                }
                if(SpeedGadget.inside(event.x, event.y)) {
                    SpeedGadget.down();
                    if(InDemoMode)
                        InDemoMode  = false;

                    DemoTimer.setSnooze( DEMO_TIME );
                    Atlantis.thrust = Spacecraft.ATLANTIS_SPEED_THRUST;
                }
                if(ClearGadget.inside(event.x, event.y)) {
                    ClearGadget.down();
                    DemoTimer.setSnooze( DEMO_TIME );
                }
                return false;

            case Event.MOUSE_UP:
                if(StartGadget.isPressed()) {
                    StartGadget.up();
                    InDemoMode  = false;
                    DoResetGame = true;
                }

                if(BrakeGadget.isPressed()) {
                    BrakeGadget.up();
                    Atlantis.thrust = Spacecraft.NO_THRUST;
                }
                if(SpeedGadget.isPressed()) {
                    SpeedGadget.up();
                    Atlantis.thrust = Spacecraft.NO_THRUST;
                }
                if(ClearGadget.isPressed()) {
                    ClearGadget.up();
                    clearScreen();
                }
                return false;

            case Event.MOUSE_DRAG:
                if(StartGadget.isPressed() && !StartGadget.inside(event.x, event.y) )
                    StartGadget.up();

                if(BrakeGadget.isPressed() && !BrakeGadget.inside(event.x, event.y) ) {
                    BrakeGadget.up();
                    Atlantis.thrust = Spacecraft.NO_THRUST;
                }
                if(SpeedGadget.isPressed() && !SpeedGadget.inside(event.x, event.y) ) {
                    SpeedGadget.up();
                    Atlantis.thrust = Spacecraft.NO_THRUST;
                }
                if(ClearGadget.isPressed() && !ClearGadget.inside(event.x, event.y) )
                    ClearGadget.up();

                return false;

            default:
                return true;
        }
    }

//******************************************************************************
    private void resetGame() {
        resetBobs();
        resetSpacecraft();
        Autopilot.reset(Mir, Soyuz);
        enableGadgets();
        clearScreen();
    }

//******************************************************************************
    private void resetBobs() {
        AtlantisBob.setInUse(false);
        AtlantisBob.setVisible(true);
        AtlantisBob.setFrameNr(4);

        MirBob.setInUse(false);
        MirBob.setVisible(true);
        MirBob.setFrameNr(4);

        SoyuzBob.setInUse(false);
        SoyuzBob.setVisible(false);
        SoyuzBob.setFrameNr(0);

        AtlantisJetBob.setInUse(false);
        AtlantisJetBob.setVisible(false);

        SoyuzJetBob.setInUse(false);
        SoyuzJetBob.setVisible(false);

        EarthBob.setInUse(false);
        EarthBob.setVisible(true);

        AtlantisLabelBob.setInUse(false);
        AtlantisLabelBob.setVisible(false);
        AtlantisLabelBob.setFrameRow(LABELBOB_ROW_ATLANTIS);

        MirLabelBob.setInUse(false);
        MirLabelBob.setVisible(false);
        MirLabelBob.setFrameRow(LABELBOB_ROW_MIR);

        SoyuzLabelBob.setInUse(false);
        SoyuzLabelBob.setVisible(false);

        NoteBob.setInUse(false);
        NoteBob.setVisible(false);

        TachoBob.setInUse(false);
        TachoBob.setVisible(false);

        DotsBob.setInUse(true);
        DotsBob.setVisible(true);
    }

//******************************************************************************
    private void resetSpacecraft() {
        Atlantis.x  = -Spacecraft.GEOSTATIONARY_RADIUS * 2 / 3;
        Atlantis.y  = 0;
        Atlantis.vx = 0;
        Atlantis.vy = (float)(-Math.sqrt(Spacecraft.GM / -Atlantis.x));
        Atlantis.v  =  Atlantis.vy;
        Atlantis.r  = -Atlantis.x;

        Atlantis.setDocked(false);
        Atlantis.onScreen  = true;
        Atlantis.thrust    = Spacecraft.NO_THRUST;

        Mir.x  = Spacecraft.GEOSTATIONARY_RADIUS;
        Mir.y  = 0;
        Mir.vx = 0;
        Mir.vy = (float)(Math.sqrt(Spacecraft.GM / Mir.x));

        Mir.resetTrail((int)(Spacecraft.ONE_DAY / (DELTAT * TSTEPS)));

        Mir.onScreen    = true;
        Mir.thrust      = Spacecraft.NO_THRUST;

        Soyuz.onScreen  = false;
        Soyuz.thrust    = Spacecraft.NO_THRUST;
    }

//******************************************************************************
    private synchronized void clearScreen() {
        AtlantisBob.setInUse(false);
        MirBob.setInUse(false);
        SoyuzBob.setInUse(false);
        AtlantisJetBob.setInUse(false);
        SoyuzJetBob.setInUse(false);
        DotsBob.setInUse(false);
        EarthBob.setInUse(false);
        NoteBob.setInUse(false);
        AtlantisLabelBob.setInUse(false);
        MirLabelBob.setInUse(false);
        SoyuzLabelBob.setInUse(false);
        TachoBob.setInUse(false);

        offScreenGC.setColor(Color.white);
        offScreenGC.fillRect(0, 0, WIDTH, HEIGHT);

        offScreenGC.setColor( new Color(192, 192, 255));
        for(int i=0; i<WIDTH; i+=10)
            offScreenGC.drawLine(i, 0, i, HEIGHT-1);
        for(int j=0; j<HEIGHT; j+=10)
            offScreenGC.drawLine(0, j, WIDTH-1, j);

        Mir.resetTrail((int)(Spacecraft.ONE_DAY / (DELTAT * TSTEPS)));
        paint(this.getGraphics());
    }

//******************************************************************************
    private void enableGadgets() {
        SpeedGadget.enable();
        BrakeGadget.enable();
    }

//******************************************************************************
    private void disableGadgets() {
        SpeedGadget.disable();
        BrakeGadget.disable();
    }

//*** Sleeper Interface ********************************************************
    public synchronized void wakeUp(int id) {
        switch(id) {
            case NOTE_TIMERID : killNote();
                                break;

            case DEMO_TIMERID : InDemoMode  = true;
                                DoResetGame = true;
                                break;

            case SOYUZ_TIMERID: Autopilot.triggerSoyuz();
                                SoyuzTimer.setSnooze(SOYUZ_LAUNCH_TIME);
                                break;
        }
    }

//******************************************************************************
    private void enterDemoMode() {
        InDemoMode = true;
        DemoTimer.clearSnooze();

        SoyuzTimer.setSnooze( SOYUZ_LAUNCH_TIME );
        killNote();

        AtlantisLabelBob.setVisible(true);
        MirLabelBob.setVisible(true);
        SoyuzLabelBob.setVisible(true);

        ClearGadget.disable();
    }

//******************************************************************************
    private void exitDemoMode() {
        InDemoMode = false;
        DemoTimer.setSnooze( DEMO_TIME );

        setNote(NOTEBOB_ROW_OK4DOCKING);

        AtlantisLabelBob.setVisible(false);
        MirLabelBob.setVisible(false);
        SoyuzLabelBob.setVisible(false);

        ClearGadget.enable();
    }

//******************************************************************************
    private void setNote(int id) {
        NoteBob.setVisible(true);
        NoteBob.setFrameRow(id);
        NoteTimer.setSnooze( NOTE_TIME );

        if((id!=NOTEBOB_ROW_TOOFAST) && (id!=NOTEBOB_ROW_LOOKSGOOD) && (TachoBob.isVisible()))
            TachoBob.setVisible(false);
    }

//******************************************************************************
    private void killNote() {
        NoteBob.setVisible(false);
        NoteTimer.clearSnooze();

        TachoBob.setVisible(false);
    }

/*******************************************************************************
*                                                                              *
*                           Simulation Graphics Engine                         *
*                                                                              *
*******************************************************************************/
    private void handleAtlantisRendezvous() {
        final float CONVERT_TO_PERCENT = 20; // scale velocitiy from 2000 m/s to 100%

        final float SENSERADIUS_SQR   = (100 * 100) / (ZOOM * ZOOM); // sense radius is  100 pixels
        final float DOCKINGRADIUS_SQR = ( 28 *  28) / (ZOOM * ZOOM); // docking radius is 28 pixels
        final float REJECTRADIUS_SQR  = ( 21 *  21) / (ZOOM * ZOOM); // reject radius is  21 pixels

        float dx         = Mir.x - Atlantis.x;
        float dy         = Mir.y - Atlantis.y;
        float distMA_sqr = dx * dx + dy * dy;

        if(distMA_sqr < REJECTRADIUS_SQR)
           setNote(NOTEBOB_ROW_TOOCLOSE);
        else
           if(distMA_sqr > SENSERADIUS_SQR) {
               if(NoteBob.isVisible() && NoteBob.getFrameRow()!=NOTEBOB_ROW_OK4DOCKING)
                   killNote();
           }
           else {
               float dvx = Mir.vx - Atlantis.vx; // dvx, dvy are in Atlantis-centered CS
               float dvy = Mir.vy - Atlantis.vy;
               float dot = dvx * dx + dvy * dy;

               if(dot < 0) {             // ATLANTIS moves *TOWARD* MIR
                   float approachVelocity  = (float)Math.sqrt(dvx * dvx + dvy * dvy);
                   int   percent           = (int)(approachVelocity / CONVERT_TO_PERCENT);
                   if(percent>100) percent = 100;

                   if(percent<20) {
                       setNote(NOTEBOB_ROW_LOOKSGOOD);
                       paintTachoBob(TACHOBOB_ROW_GREEN, percent);
                       TachoBob.setVisible(true);
                   }
                   else if(percent<40) {
                       setNote(NOTEBOB_ROW_LOOKSGOOD);
                       paintTachoBob(TACHOBOB_ROW_ORANGE, percent);
                       TachoBob.setVisible(true);
                   }
                   else {
                       setNote(NOTEBOB_ROW_TOOFAST);
                       paintTachoBob(TACHOBOB_ROW_RED, percent);
                       TachoBob.setVisible(true);
                   }

                   if(percent<20 && distMA_sqr < DOCKINGRADIUS_SQR) {
                       setNote(NOTEBOB_ROW_DOCKED);
                       Atlantis.setDocked(true);
                       disableGadgets();
                   }
               }
           }
    }

//******************************************************************************
    private synchronized void paintTachoBob(int rowIndex, int percent) {
        blit(TachoBob.stripImage, 0, 0, stripTachoImage,
             0, -TACHOBOB_ROW_MASTER * TachoBob.bitmapHeight,
             TachoBob.width, TachoBob.height);

        int pixelPercentage = (int)(percent * 0.01 * TachoBob.width);

        blit(TachoBob.stripImage, 0, 0, stripTachoImage,
             0, -rowIndex * TachoBob.bitmapHeight,
             pixelPercentage, TachoBob.height);

        int markerPos = pixelPercentage - TACHOBOB_MARKERWIDTH / 2 ;

        blit(TachoBob.stripImage, markerPos, 0, stripTachoImage,
             markerPos, -TACHOBOB_ROW_MARKER * TachoBob.bitmapHeight,
             TACHOBOB_MARKERWIDTH, TachoBob.height);
    }

//******************************************************************************
    public void updateBobPositions(Autopilot Autopilot) {
        final int PIXEL_SENSERADIUS_SQR = 100 * 100;

        int dx;
        int dy;
        int distAM_sqr;

        MirBob.x = (int)(MX + Mir.x * ZOOM + MirBob.offsetX);
        MirBob.y = (int)(MY - Mir.y * ZOOM + MirBob.offsetY);

     // Calculate Atlantis Bob position

        if(Atlantis.hasDocked()) {
            AtlantisBob.x = MirBob.x + MirBob.transX[MirBob.getFrameNr()];
            AtlantisBob.y = MirBob.y + MirBob.transY[MirBob.getFrameNr()];
            AtlantisJetBob.setVisible(false);
            if(MirBob.getFrameNr()!=AtlantisBob.getFrameNr())
                AtlantisBob.setFrameNr(MirBob.getFrameNr());
        }
        else {
           AtlantisBob.x = (int)(MX + Atlantis.x * ZOOM + AtlantisBob.offsetX);
           AtlantisBob.y = (int)(MY - Atlantis.y * ZOOM + AtlantisBob.offsetY);
           if(!Mir.blockAtlantisDocking()) {
               dx =   MirBob.x - MirBob.offsetX - (AtlantisBob.x - AtlantisBob.offsetX);
               dy = -(MirBob.y - MirBob.offsetY - (AtlantisBob.y - AtlantisBob.offsetY));
               if((dx * dx + dy * dy) < PIXEL_SENSERADIUS_SQR)
                   AtlantisBob.setFrameNr(-dx, -dy);
           }

     // Calc Atlantis Jet position

           if(Atlantis.thrust!= Spacecraft.NO_THRUST) {
               AtlantisJetBob.setVisible(true);
               AtlantisJetBob.x = AtlantisBob.x - AtlantisBob.offsetX + AtlantisJetBob.offsetX;
               AtlantisJetBob.y = AtlantisBob.y - AtlantisBob.offsetY + AtlantisJetBob.offsetY;
               AtlantisJetBob.setFrameNr(-Atlantis.thrustX, -Atlantis.thrustY);
           }
          else
               AtlantisJetBob.setVisible(false);
        }

     // Calc Soyuz position

        if(SoyuzBob.isVisible()) {
            SoyuzBob.x = (int)(MX + Soyuz.x * ZOOM + SoyuzBob.offsetX);
            SoyuzBob.y = (int)(MY - Soyuz.y * ZOOM + SoyuzBob.offsetY);

            dx =   MirBob.x - MirBob.offsetX - (SoyuzBob.x - SoyuzBob.offsetX);
            dy = -(MirBob.y - MirBob.offsetY - (SoyuzBob.y - SoyuzBob.offsetY));
            if((dx * dx + dy * dy) < PIXEL_SENSERADIUS_SQR)
                SoyuzBob.setFrameNr(dx, dy);

            if(Soyuz.thrust>Spacecraft.NO_THRUST)
                SoyuzBob.setFrameNr(-Autopilot.soyuzVelocityDirX, -Autopilot.soyuzVelocityDirY);
        }

     // Calc Soyuz Jets position

        if(Soyuz.thrust!=Spacecraft.NO_THRUST) {
            Soyuz.thrust--;
            SoyuzJetBob.setVisible(true);

            SoyuzJetBob.x = (int)(MX + Soyuz.x * ZOOM + SoyuzJetBob.offsetX);
            SoyuzJetBob.y = (int)(MY - Soyuz.y * ZOOM + SoyuzJetBob.offsetY);
            SoyuzJetBob.setFrameNr(Autopilot.soyuzVelocityDirX, Autopilot.soyuzVelocityDirY);
        }
        else
            SoyuzJetBob.setVisible(false);

     // Calc Mir position

        dx =   AtlantisBob.x - AtlantisBob.offsetX - (MirBob.x - MirBob.offsetX);
        dy = -(AtlantisBob.y - AtlantisBob.offsetY - (MirBob.y - MirBob.offsetX));
        distAM_sqr = dx * dx + dy * dy;
        if((!Mir.blockAtlantisDocking()) && (distAM_sqr < PIXEL_SENSERADIUS_SQR) && (!Atlantis.hasDocked()))
            MirBob.setFrameNr(dx, dy);
        else {
            if(SoyuzBob.isVisible()) {
                dx =   SoyuzBob.x - SoyuzBob.offsetX - (MirBob.x - MirBob.offsetX);
                dy = -(SoyuzBob.y - SoyuzBob.offsetY - (MirBob.y - MirBob.offsetY));
                if((dx * dx + dy * dy) < PIXEL_SENSERADIUS_SQR) {
                    if(Autopilot.getManeuverType()==Autopilot.OM_HIDESOYUZ) {
                        if(Autopilot.getManeuverClass()==Autopilot.OMC_LODOCK) {

                            MirBob.setFrameNr(dy, -dx);
                            if((distAM_sqr < PIXEL_SENSERADIUS_SQR) && (!InDemoMode))
                                setNote(NOTEBOB_ROW_SOYUZ28);
                        }
                        if(Autopilot.getManeuverClass()==Autopilot.OMC_HIDOCK) {
                           MirBob.setFrameNr(-dy, dx);
                           if((distAM_sqr < PIXEL_SENSERADIUS_SQR) && (!InDemoMode))
                              setNote(NOTEBOB_ROW_SOYUZ10);
                        }
                    }
                    if(Autopilot.getManeuverClass()==Autopilot.OMC_LOSEPR)  MirBob.setFrameNr( dy, -dx);
                    if(Autopilot.getManeuverClass()==Autopilot.OMC_HISEPR)  MirBob.setFrameNr(-dy,  dx);
                }
            }
        }

     // Calc Note position

        if(NoteBob.isVisible()) {
            if(AtlantisBob.x < MirBob.x) {
                NoteBob.x = MirBob.x + MirBob.width;
                NoteBob.y = MirBob.y - NOTEBOB_HALFHEIGHT;
                NoteBob.setFrameNr(NOTEBOB_FRM_RIGHT);
            }
            else {
                NoteBob.x = MirBob.x - NoteBob.width;
                NoteBob.y = MirBob.y - NOTEBOB_HALFHEIGHT;
                NoteBob.setFrameNr(NOTEBOB_FRM_LEFT);
            }
        }

     // Calc Tacho position

        if(TachoBob.isVisible()) {
            if(AtlantisBob.x < MirBob.x) {
                TachoBob.x = NoteBob.x + TACHOBOB_NOTEOFFSET_RIGHT;
                TachoBob.y = NoteBob.y + TACHOBOB_NOTEOFFSET_DOWN;
            }
            else {
                TachoBob.x = NoteBob.x + TACHOBOB_NOTEOFFSET_LEFT;
                TachoBob.y = NoteBob.y + TACHOBOB_NOTEOFFSET_DOWN;
            }
        }

     // Calc DemoMode labels position

        if(InDemoMode) {
            AtlantisLabelBob.x = AtlantisBob.x + AtlantisBob.width;
            AtlantisLabelBob.y = AtlantisBob.y - LABELBOB_HALFHEIGHT;
            MirLabelBob.x      = MirBob.x + MirBob.width;
            MirLabelBob.y      = MirBob.y - LABELBOB_HALFHEIGHT;
            if(SoyuzBob.isVisible()) {
                SoyuzLabelBob.x = SoyuzBob.x + SoyuzBob.width;
                SoyuzLabelBob.y = SoyuzBob.y - LABELBOB_HALFHEIGHT;
                if((Autopilot.getManeuverClass()==Autopilot.OMC_HIDOCK) ||
                   (Autopilot.getManeuverClass()==Autopilot.OMC_HISEPR) )
                    SoyuzLabelBob.setFrameRow(LABELBOB_ROW_SOYUZ28);
                else
                    SoyuzLabelBob.setFrameRow(LABELBOB_ROW_SOYUZ10);
            }
        }

        EarthBob.cycleFrameNr();
    }

/*******************************************************************************
*                                                                              *
*                            Core Render Engine                                *
*                                                                              *
*******************************************************************************/
    public final void blit(Image destImage, int destX, int destY, Image srcImage, int srcX, int srcY, int w, int h) {
        blit(destImage.getGraphics(), destX, destY, srcImage, srcX, srcY, w, h);
    }

    public final void blit(Graphics destGC, int destX, int destY, Image srcImage, int srcX, int srcY, int w, int h) {
        Graphics g = destGC.create();
        g.clipRect(destX, destY, w, h);
        g.drawImage(srcImage, srcX, srcY, null);
        g.dispose();
    }

/*******************************************************************************
*                                                                              *
*                               Bob Render Engine                              *
*                                                                              *
*******************************************************************************/

          final static int  ATLANTISBOB_WIDTH            =  39;
          final static int  ATLANTISBOB_HEIGHT           =  39;
          final static int  ATLANTISBOB_OFFSETX          = -19;
          final static int  ATLANTISBOB_OFFSETY          = -19;
          final static int  ATLANTISBOB_BITMAPWIDTH      =  40;
          final static int  ATLANTISBOB_BITMAPHEIGHT     =   0;
          final static int  ATLANTISBOB_COLSOFFRAMES     =  16;

          final static int  SOYUZBOB_WIDTH               =  21;
          final static int  SOYUZBOB_HEIGHT              =  21;
          final static int  SOYUZBOB_OFFSETX             = -10;
          final static int  SOYUZBOB_OFFSETY             = -10;
          final static int  SOYUZBOB_BITMAPWIDTH         =  22;
          final static int  SOYUZBOB_BITMAPHEIGHT        =   0;
          final static int  SOYUZBOB_COLSOFFRAMES        =  16;

          final static int  MIRBOB_WIDTH                 =  45;
          final static int  MIRBOB_HEIGHT                =  45;
          final static int  MIRBOB_OFFSETX               = -22;
          final static int  MIRBOB_OFFSETY               = -22;
          final static int  MIRBOB_BITMAPWIDTH           =  46;
          final static int  MIRBOB_BITMAPHEIGHT          =  46;
          final static int  MIRBOB_COLSOFFRAMES          =  16;

   public final static int  MIRBOB_ROW_0SOYUZ            =   0; // stored in MirBob.frameRow
   public final static int  MIRBOB_ROW_1SOYUZ            =   1;
   public final static int  MIRBOB_ROW_2SOYUZ            =   2;

          final static int  ATLANTISJETBOB_WIDTH         =  43;
          final static int  ATLANTISJETBOB_HEIGHT        =  43;
          final static int  ATLANTISJETBOB_OFFSETX       = -21;
          final static int  ATLANTISJETBOB_OFFSETY       = -21;
          final static int  ATLANTISJETBOB_BITMAPWIDTH   =  44;
          final static int  ATLANTISJETBOB_BITMAPHEIGHT  =   0;
          final static int  ATLANTISJETBOB_COLSOFFRAMES  =  16;

          final static int  SOYUZJETBOB_WIDTH            =  43;
          final static int  SOYUZJETBOB_HEIGHT           =  43;
          final static int  SOYUZJETBOB_OFFSETX          = -21;
          final static int  SOYUZJETBOB_OFFSETY          = -21;
          final static int  SOYUZJETBOB_BITMAPWIDTH      =  44;
          final static int  SOYUZJETBOB_BITMAPHEIGHT     =   0;
          final static int  SOYUZJETBOB_COLSOFFRAMES     =  16;

          final static int  DOTSBOB_WIDTH                =   6;
          final static int  DOTSBOB_HEIGHT               =   6;
          final static int  DOTSBOB_OFFSETX              =  -2; // Might be a little off
          final static int  DOTSBOB_OFFSETY              =  -2; // Might be a little off
          final static int  DOTSBOB_BITMAPWIDTH          =   7;
          final static int  DOTSBOB_BITMAPHEIGHT         =   7;
          final static int  DOTSBOB_COLSOFFRAMES         =   3;

   public final static int DOT_FRM_ATLANTIS              =   0;  // stored in DotBob.frameNr
   public final static int DOT_FRM_ATLANTIS_THRUSTS      =   1;
   public final static int DOT_FRM_MIR                   =   2;

          final static int  EARTHBOB_WIDTH               =  47;
          final static int  EARTHBOB_HEIGHT              =  47;
          final static int  EARTHBOB_OFFSETX             = -24;
          final static int  EARTHBOB_OFFSETY             = -24;
          final static int  EARTHBOB_BITMAPWIDTH         =  47;
          final static int  EARTHBOB_BITMAPHEIGHT        =   0;
          final static int  EARTHBOB_COLSOFFRAMES        =  16;

          final static float EARTHBOB_RADIUS = EARTHBOB_WIDTH / ( 2 * ZOOM ); // ???

          final static int  NOTEBOB_WIDTH                = 134;
          final static int  NOTEBOB_HEIGHT               =  17;
          final static int  NOTEBOB_OFFSETX              =   0;
          final static int  NOTEBOB_OFFSETY              =   0;
          final static int  NOTEBOB_BITMAPWIDTH          = 134;
          final static int  NOTEBOB_BITMAPHEIGHT         =  17;
          final static int  NOTEBOB_COLSOFFRAMES         =   2;

          final static int  NOTEBOB_HALFHEIGHT           =   9;

          final static int  NOTEBOB_FRM_RIGHT            =   0;  // stored in NoteBob.frameNr
          final static int  NOTEBOB_FRM_LEFT             =   1;

          final static int  NOTEBOB_ROW_OK4DOCKING       =   0;  // stored in NoteBob.frameRow
          final static int  NOTEBOB_ROW_SOYUZ10          =   1;
          final static int  NOTEBOB_ROW_SOYUZ28          =   2;
          final static int  NOTEBOB_ROW_TOOCLOSE         =   3;
          final static int  NOTEBOB_ROW_TOOFAST          =   4;
          final static int  NOTEBOB_ROW_DRIFTING         =   5;
          final static int  NOTEBOB_ROW_LOOKSGOOD        =   6;
          final static int  NOTEBOB_ROW_DOCKED           =   7;

          final static int  TACHOBOB_WIDTH                = 124;
          final static int  TACHOBOB_HEIGHT               =  10;
          final static int  TACHOBOB_OFFSETX              =   0;
          final static int  TACHOBOB_OFFSETY              =   0;
          final static int  TACHOBOB_BITMAPWIDTH          =   0;
          final static int  TACHOBOB_BITMAPHEIGHT         =  10;
          final static int  TACHOBOB_COLSOFFRAMES         =   1;

          final static int  TACHOBOB_ROW_MASTER           =   1;  // stored in TachoBob.frameRow
          final static int  TACHOBOB_ROW_RED              =   2;
          final static int  TACHOBOB_ROW_ORANGE           =   3;
          final static int  TACHOBOB_ROW_GREEN            =   4;
          final static int  TACHOBOB_ROW_MARKER           =   5;

          final static int  TACHOBOB_MARKERWIDTH          =   7;
          final static int  TACHOBOB_NOTEOFFSET_LEFT      =   0;  // offsets from NoteBob.x
          final static int  TACHOBOB_NOTEOFFSET_RIGHT     =  10;  // offsets from NoteBob.x
          final static int  TACHOBOB_NOTEOFFSET_DOWN      =  21;  // offsets from NoteBob.y

          final static int  LABELBOB_WIDTH               = 121;
          final static int  LABELBOB_HEIGHT              =  17;
          final static int  LABELBOB_OFFSETX             =   0;
          final static int  LABELBOB_OFFSETY             =   0;
          final static int  LABELBOB_BITMAPWIDTH         = 121;
          final static int  LABELBOB_BITMAPHEIGHT        =  17;
          final static int  LABELBOB_COLSOFFRAMES        =   2;

          final static int  LABELBOB_HALFHEIGHT          =   9;

          final static int  LABELBOB_ROW_ATLANTIS        =   0;  // stored in AtlantisLabelBob.frameRow
          final static int  LABELBOB_ROW_MIR             =   1;  // stored in MirLabelBob.frameRow
          final static int  LABELBOB_ROW_SOYUZ10         =   2;  // stored in SoyuzLabelBob.frameRow
          final static int  LABELBOB_ROW_SOYUZ28         =   3;  // stored in SoyuzLabelBob.frameRow

//******************************************************************************
    public synchronized void drawBobs() {
        dirtyRect.beginClipRect();

        AtlantisLabelBob.lift(dirtyRect);
        MirLabelBob.lift(dirtyRect);

        SoyuzLabelBob.lift(dirtyRect);
        if(!SoyuzBob.isVisible())
            SoyuzLabelBob.setInUse(false);

        NoteBob.lift(dirtyRect);
        TachoBob.lift(dirtyRect);

        MirBob.lift(dirtyRect);
        MirBob.setInUse(true);

        AtlantisBob.lift(dirtyRect);
        AtlantisBob.setInUse(true);

        AtlantisJetBob.lift(dirtyRect);
        SoyuzBob.lift(dirtyRect);
        SoyuzJetBob.lift(dirtyRect);
        EarthBob.lift(dirtyRect);

        if(!InDemoMode) dropDots();

        EarthBob.save();
        SoyuzJetBob.save();
        SoyuzBob.save();
        AtlantisJetBob.save();
        AtlantisBob.save();
        MirBob.save();
        NoteBob.save();
        TachoBob.save();

        if(SoyuzBob.isVisible())
            SoyuzLabelBob.save();

        MirLabelBob.save();
        AtlantisLabelBob.save();

        EarthBob.drop(dirtyRect);
        SoyuzJetBob.drop(dirtyRect);
        SoyuzBob.drop(dirtyRect);
        AtlantisJetBob.drop(dirtyRect);
        AtlantisBob.drop(dirtyRect);
        MirBob.drop(dirtyRect);
        TachoBob.drop(dirtyRect);
        NoteBob.drop(dirtyRect);

        if(SoyuzBob.isVisible())
            SoyuzLabelBob.drop(dirtyRect);

        MirLabelBob.drop(dirtyRect);
        AtlantisLabelBob.drop(dirtyRect);

        StartGadget.draw();
        BrakeGadget.draw();
        SpeedGadget.draw();
        ClearGadget.draw();

        dirtyRect.addClipRect(StartGadget.getRectangle());
        dirtyRect.addClipRect(BrakeGadget.getRectangle());
        dirtyRect.addClipRect(SpeedGadget.getRectangle());
        dirtyRect.addClipRect(ClearGadget.getRectangle());

        offScreenClipRect = dirtyRect.endClipRect();
    }

//******************************************************************************
    public void dropDots() {

        if(!Atlantis.hasDocked()) {
            DotsBob.x = AtlantisBob.x - AtlantisBob.offsetX + DotsBob.offsetX;
            DotsBob.y = AtlantisBob.y - AtlantisBob.offsetY + DotsBob.offsetY;

            if(AtlantisJetBob.isVisible())
                DotsBob.drop(dirtyRect, DOT_FRM_ATLANTIS_THRUSTS);
            else
                DotsBob.drop(dirtyRect, DOT_FRM_ATLANTIS);
        }

        if(Mir.doTrail()) {
            DotsBob.x = MirBob.x - MirBob.offsetX + DotsBob.offsetX;
            DotsBob.y = MirBob.y - MirBob.offsetY + DotsBob.offsetY;
            DotsBob.drop(dirtyRect, DOT_FRM_MIR);
        }
    }

//******************************************************************************
   public synchronized void initBobs() {

       AtlantisBob      = new Bob( ATLANTISBOB_WIDTH,            // width
                                   ATLANTISBOB_HEIGHT,           // height
                                   ATLANTISBOB_COLSOFFRAMES,     // colsOfFrames
                                   stripAtlantisImage,           // stripImage
                                   ATLANTISBOB_OFFSETX,          // offsetX
                                   ATLANTISBOB_OFFSETY,          // offsetY
                                   ATLANTISBOB_BITMAPWIDTH,      // bitmapWidth
                                   ATLANTISBOB_BITMAPHEIGHT,     // bitmapheight
                                   true,
                                   offScreen, this);

       SoyuzBob         = new Bob( SOYUZBOB_WIDTH,               // width
                                   SOYUZBOB_HEIGHT,              // height
                                   SOYUZBOB_COLSOFFRAMES,        // colsOfFrames
                                   stripSoyuzImage,              // stripImage
                                   SOYUZBOB_OFFSETX,             // offsetX
                                   SOYUZBOB_OFFSETY,             // offsetY
                                   SOYUZBOB_BITMAPWIDTH,         // bitmapWidth
                                   SOYUZBOB_BITMAPHEIGHT,        // bitmapheight
                                   false,
                                   offScreen, this);

       MirBob        = new MirBob( MIRBOB_WIDTH,                 // width
                                   MIRBOB_HEIGHT,                // height
                                   MIRBOB_COLSOFFRAMES,          // colsOfFrames
                                   stripMirImage,                // stripImage
                                   MIRBOB_OFFSETX,               // offsetX
                                   MIRBOB_OFFSETY,               // offsetY
                                   MIRBOB_BITMAPWIDTH,           // bitmapWidth
                                   MIRBOB_BITMAPHEIGHT,          // bitmapheight
                                   true,
                                   offScreen, this);

       AtlantisJetBob   = new Bob( ATLANTISJETBOB_WIDTH,         // width
                                   ATLANTISJETBOB_HEIGHT,        // height
                                   ATLANTISJETBOB_COLSOFFRAMES,  // colsOfFrames
                                   stripJetImage,                // stripImage
                                   ATLANTISJETBOB_OFFSETX,       // offsetX
                                   ATLANTISJETBOB_OFFSETY,       // offsetY
                                   ATLANTISJETBOB_BITMAPWIDTH,   // bitmapWidth
                                   ATLANTISJETBOB_BITMAPHEIGHT,  // bitmapheight
                                   false,
                                   offScreen, this );

       SoyuzJetBob      = new Bob( SOYUZJETBOB_WIDTH,            // width
                                   SOYUZJETBOB_HEIGHT,           // height
                                   SOYUZJETBOB_COLSOFFRAMES,     // colsOfFrames
                                   stripJetImage,                // stripImage
                                   SOYUZJETBOB_OFFSETX,          // offsetX
                                   SOYUZJETBOB_OFFSETY,          // offsetY
                                   SOYUZJETBOB_BITMAPWIDTH,      // bitmapWidth
                                   SOYUZJETBOB_BITMAPHEIGHT,     // bitmapheight
                                   false,
                                   offScreen, this );

       DotsBob          = new Bob( DOTSBOB_WIDTH,                // width
                                   DOTSBOB_HEIGHT,               // height
                                   DOTSBOB_COLSOFFRAMES,         // colsOfFrames
                                   stripDotsImage,               // stripImage
                                   DOTSBOB_OFFSETX,              // offsetX
                                   DOTSBOB_OFFSETY,              // offsetY
                                   DOTSBOB_BITMAPWIDTH,          // bitmapWidth
                                   DOTSBOB_BITMAPHEIGHT,         // bitmapheight
                                   true,
                                   offScreen, this );

       EarthBob         = new Bob( EARTHBOB_WIDTH,               // width
                                   EARTHBOB_HEIGHT,              // height
                                   EARTHBOB_COLSOFFRAMES,        // colsOfFrames
                                   stripEarthImage,              // stripImage
                                   EARTHBOB_OFFSETX,             // offsetX
                                   EARTHBOB_OFFSETY,             // offsetY
                                   EARTHBOB_BITMAPWIDTH,         // bitmapWidth
                                   EARTHBOB_BITMAPHEIGHT,        // bitmapheight
                                   true,
                                   offScreen, this );

       EarthBob.x = MX + EARTHBOB_OFFSETX;
       EarthBob.y = MY + EARTHBOB_OFFSETY;
       EarthBob.setFrameNr(0);

       NoteBob          = new Bob( NOTEBOB_WIDTH,                // width
                                   NOTEBOB_HEIGHT,               // height
                                   NOTEBOB_COLSOFFRAMES,         // colsOfFrames
                                   stripNoteImage,               // stripImage
                                   NOTEBOB_OFFSETX,              // offsetX
                                   NOTEBOB_OFFSETY,              // offsetY
                                   NOTEBOB_BITMAPWIDTH,          // bitmapWidth
                                   NOTEBOB_BITMAPHEIGHT,         // bitmapheight
                                   false,
                                   offScreen, this );

       TachoBob         = new Bob( TACHOBOB_WIDTH,               // width
                                   TACHOBOB_HEIGHT,              // height
                                   TACHOBOB_COLSOFFRAMES,        // colsOfFrames
                                   this.createImage(TACHOBOB_WIDTH, TACHOBOB_HEIGHT),
                                   TACHOBOB_OFFSETX,             // offsetX
                                   TACHOBOB_OFFSETY,             // offsetY
                                   TACHOBOB_BITMAPWIDTH,         // bitmapWidth
                                   TACHOBOB_BITMAPHEIGHT,        // bitmapheight
                                   false,
                                   offScreen, this );

       AtlantisLabelBob = new Bob( LABELBOB_WIDTH,               // width
                                   LABELBOB_HEIGHT,              // height
                                   LABELBOB_COLSOFFRAMES,        // colsOfFrames
                                   stripLabelsImage,             // stripImage
                                   LABELBOB_OFFSETX,             // offsetX
                                   LABELBOB_OFFSETY,             // offsetY
                                   LABELBOB_WIDTH,               // bitmapWidth
                                   LABELBOB_HEIGHT,              // bitmapheight
                                   false,
                                   offScreen, this );

       SoyuzLabelBob    = new Bob( LABELBOB_WIDTH,               // width
                                   LABELBOB_HEIGHT,              // height
                                   LABELBOB_COLSOFFRAMES,        // colsOfFrames
                                   stripLabelsImage,             // stripImage
                                   LABELBOB_OFFSETX,             // offsetX
                                   LABELBOB_OFFSETY,             // offsetY
                                   LABELBOB_WIDTH,               // bitmapWidth
                                   LABELBOB_HEIGHT,              // bitmapheight
                                   false,
                                   offScreen, this );

       MirLabelBob      = new Bob( LABELBOB_WIDTH,               // width
                                   LABELBOB_HEIGHT,              // height
                                   LABELBOB_COLSOFFRAMES,        // colsOfFrames
                                   stripLabelsImage,             // stripImage
                                   LABELBOB_OFFSETX,             // offsetX
                                   LABELBOB_OFFSETY,             // offsetY
                                   LABELBOB_WIDTH,               // bitmapWidth
                                   LABELBOB_HEIGHT,              // bitmapheight
                                   false,
                                   offScreen, this );
   }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                              AutopilotTable                                //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

// A helper class for Autopilot

final class AutopilotTable {
    float alfa;
    float semiAxis;
    int   maneuverType;

    public AutopilotTable() {
    }

    public AutopilotTable(float alfa, float semiAxis, int maneuverType) {
        this.alfa         = alfa;
        this.semiAxis     = semiAxis;
        this.maneuverType = maneuverType;
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                Autopilot                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

// Autopilot as a table-driven Finite State Machine -- still pretty messy!

final class Autopilot {
    final static float DEG2RAD         = (float)(Math.PI / 180.0);
    final static float RAD2DEG         = (float)(180.0  / Math.PI);

    final static int OM_CLASSES        = 4; // number of orbital maneuver classes
    final static int OMC_LODOCK        = 0; // maneuver class IDs
    final static int OMC_LOSEPR        = 1;
    final static int OMC_HIDOCK        = 2;
    final static int OMC_HISEPR        = 3;

    final static int OM_STEPS          = 8; // number of steps of each orbital meneuver
    final static int OM_INITLAUNCH     = 0; // maneuver step IDs
    final static int OM_MANEUVER       = 1;
    final static int OM_RENDEZVOUS     = 2;
    final static int OM_HIDESOYUZ      = 3;
    final static int OM_DOCKING        = 4;
    final static int OM_SEPARATION     = 5;
    final static int OM_SHOWSOYUZ      = 6;
    final static int OM_CLEANUPDESCENT = 7;
    final static int OM_NOP            = 8;

    final static int NIL               = -1;

    final static int SOYUZTHRUST_NUM_FRAMES = 10; // soyuz jet is shown for this # of screen frames

    private int     OMIndex;
    private boolean OMPending;
    private int     OMClass;
    private float   TD;
    private float   deltaTD;
    private boolean readyJets;
    private int     soyuzLaunchCount;

    public  float   soyuzVelocityDirX;
    public  float   soyuzVelocityDirY;

    private AutopilotTable OM;           // the current orbital maneuver
    private AutopilotTable OMTable[][];

////////////////////////////////////////////////////////////////////////////////
    public Autopilot() {
        OM       = new AutopilotTable();
        OMTable  = new AutopilotTable[OM_CLASSES][OM_STEPS];

     // Fill tables for automatic high/low orbit docking/separation procedures

        float R                       = Spacecraft.GEOSTATIONARY_RADIUS;
        float GROUND_ORBIT            = (float)(1.0 / 2.0 * 2.0 / 7.0               * R);
        float TRANSFER_PARK_TO_GROUND = (float)(1.0 / 2.0 * (1.0 / 2.0 + 1.0 / 7.0) * R);
        float PARK_ORBIT              = (float)(1.0 / 2.0                           * R);
        float TRANSFER_HIGH_TO_PARK   = (float)(1.0 / 2.0 * (6.0 / 5.0 + 1.0 / 2.0) * R);
        float HIGH_ORBIT              = (float)(6.0 / 5.0                           * R);
        float TRANSFER_MIR_TO_HIGH    = (float)(1.0 / 2.0 * (6.0 / 5.0 + 1.0)       * R);
        float TRANSFER_LOW_TO_PARK    = (float)(1.0 / 2.0 * (4.0 / 5.0 + 1.0 / 2.0) * R);
        float TRANSFER_MIR_TO_LOW     = (float)(1.0 / 2.0 * (4.0 / 5.0 + 1.0)       * R);
        float LOW_ORBIT               = (float)(4.0 / 5.0                           * R);
        float MIR_ORBIT               = (float)(1.0                                 * R);
        float SPECIAL                 = (float)(1.0 / 7.0                           * R);

        OMTable[OMC_HISEPR][0] = new AutopilotTable(NIL, NIL,                     OM_CLEANUPDESCENT);
        OMTable[OMC_HISEPR][1] = new AutopilotTable(NIL, GROUND_ORBIT,            OM_MANEUVER      );
        OMTable[OMC_HISEPR][2] = new AutopilotTable(NIL, TRANSFER_PARK_TO_GROUND, OM_MANEUVER      );
        OMTable[OMC_HISEPR][3] = new AutopilotTable(NIL, PARK_ORBIT,              OM_MANEUVER      );
        OMTable[OMC_HISEPR][4] = new AutopilotTable(NIL, TRANSFER_HIGH_TO_PARK,   OM_MANEUVER      );
        OMTable[OMC_HISEPR][5] = new AutopilotTable(NIL, HIGH_ORBIT,              OM_MANEUVER      );
        OMTable[OMC_HISEPR][6] = new AutopilotTable(NIL, NIL,                     OM_SHOWSOYUZ     );
        OMTable[OMC_HISEPR][7] = new AutopilotTable(NIL, TRANSFER_MIR_TO_HIGH,    OM_SEPARATION    );

        OMTable[OMC_LOSEPR][0] = new AutopilotTable(NIL, NIL,                     OM_CLEANUPDESCENT);
        OMTable[OMC_LOSEPR][1] = new AutopilotTable(NIL, GROUND_ORBIT,            OM_MANEUVER      );
        OMTable[OMC_LOSEPR][2] = new AutopilotTable(NIL, TRANSFER_PARK_TO_GROUND, OM_MANEUVER      );
        OMTable[OMC_LOSEPR][3] = new AutopilotTable(NIL, PARK_ORBIT,              OM_MANEUVER      );
        OMTable[OMC_LOSEPR][4] = new AutopilotTable(NIL, TRANSFER_LOW_TO_PARK,    OM_MANEUVER      );
        OMTable[OMC_LOSEPR][5] = new AutopilotTable(NIL, LOW_ORBIT,               OM_MANEUVER      );
        OMTable[OMC_LOSEPR][6] = new AutopilotTable(NIL, NIL,                     OM_SHOWSOYUZ     );
        OMTable[OMC_LOSEPR][7] = new AutopilotTable(NIL, TRANSFER_MIR_TO_LOW,     OM_SEPARATION    );

        OMTable[OMC_HIDOCK][0] = new AutopilotTable(NIL,     NIL,                     OM_DOCKING   );
        OMTable[OMC_HIDOCK][1] = new AutopilotTable(NIL,     MIR_ORBIT,               OM_MANEUVER  );
        OMTable[OMC_HIDOCK][2] = new AutopilotTable(NIL,     NIL,                     OM_HIDESOYUZ );
        OMTable[OMC_HIDOCK][3] = new AutopilotTable(NIL,     TRANSFER_MIR_TO_HIGH,    OM_RENDEZVOUS);
        OMTable[OMC_HIDOCK][4] = new AutopilotTable(NIL,     HIGH_ORBIT,              OM_MANEUVER  );
        OMTable[OMC_HIDOCK][5] = new AutopilotTable(NIL,     TRANSFER_HIGH_TO_PARK,   OM_MANEUVER  );
        OMTable[OMC_HIDOCK][6] = new AutopilotTable(NIL,     PARK_ORBIT,              OM_MANEUVER  );
        OMTable[OMC_HIDOCK][7] = new AutopilotTable(SPECIAL, TRANSFER_PARK_TO_GROUND, OM_INITLAUNCH);

        OMTable[OMC_LODOCK][0] = new AutopilotTable(NIL,     NIL,                     OM_DOCKING   );
        OMTable[OMC_LODOCK][1] = new AutopilotTable(NIL,     MIR_ORBIT,               OM_MANEUVER  );
        OMTable[OMC_LODOCK][2] = new AutopilotTable(NIL,     NIL,                     OM_HIDESOYUZ );
        OMTable[OMC_LODOCK][3] = new AutopilotTable(NIL,     TRANSFER_MIR_TO_LOW,     OM_RENDEZVOUS);
        OMTable[OMC_LODOCK][4] = new AutopilotTable(NIL,     LOW_ORBIT,               OM_MANEUVER  );
        OMTable[OMC_LODOCK][5] = new AutopilotTable(NIL,     TRANSFER_LOW_TO_PARK,    OM_MANEUVER  );
        OMTable[OMC_LODOCK][6] = new AutopilotTable(NIL,     PARK_ORBIT,              OM_MANEUVER  );
        OMTable[OMC_LODOCK][7] = new AutopilotTable(SPECIAL, TRANSFER_PARK_TO_GROUND, OM_INITLAUNCH);
    }

////////////////////////////////////////////////////////////////////////////////
    public void reset(MirSpacecraft Mir, Spacecraft Soyuz) {
        Soyuz.thrust = Spacecraft.NO_THRUST;
        Mir.setBlockAtlantisDocking(false);

        OMIndex   = NIL;
        OMPending = false;

        for(int i=0; i<OM_STEPS - 1 ; i++)  // ### Mind the -1 !
            for(int j=0; j<OM_CLASSES; j++)
                OMTable[j][i].alfa = NIL;
    }

////////////////////////////////////////////////////////////////////////////////
    public void setSoyuzLaunchCount(int number) {
        this.soyuzLaunchCount = number;
    }

////////////////////////////////////////////////////////////////////////////////
    public int getManeuverType() {
       return OM.maneuverType;
    }

////////////////////////////////////////////////////////////////////////////////
    public int getManeuverClass() {
       return OMClass;
    }

////////////////////////////////////////////////////////////////////////////////
    public void nextManeuver(MirBob MirBob, Bob SoyuzBob, MirSpacecraft Mir, Spacecraft Soyuz, int deltaT) {
        float x;
        float y;
        float vh;
        float normdeltav;
        float alfa;
        float currentTD;
        float finalTD;

        if( (OMIndex==NIL) || (OMPending==true) )
            return;

        OMPending = true;

        OM.alfa         = OMTable[OMClass][OMIndex].alfa;
        OM.semiAxis     = OMTable[OMClass][OMIndex].semiAxis;
        OM.maneuverType = OMTable[OMClass][OMIndex].maneuverType;

        OMIndex--;
        if(OMIndex<0) OMIndex = NIL;

        switch(OM.maneuverType) {
            case OM_SEPARATION :
                if(soyuzLaunchCount==2)
                    MirBob.setFrameRow(RocketScience.MIRBOB_ROW_2SOYUZ);
                if(soyuzLaunchCount==3)
                    MirBob.setFrameRow(RocketScience.MIRBOB_ROW_1SOYUZ);

                Soyuz.x  = Mir.x;
                Soyuz.y  = Mir.y;
                Soyuz.vx = Mir.vx;
                Soyuz.vy = Mir.vy;

                alfa = atan360(Soyuz.x, Soyuz.y);
                OMTable[OMClass][5].alfa = trim360(alfa + 180);
                OMTable[OMClass][4].alfa = trim360(alfa + 180);
                OMTable[OMClass][3].alfa = alfa;
                OMTable[OMClass][2].alfa = alfa;
                OMTable[OMClass][1].alfa = trim360(alfa + 90);

                vh         = DeltaVHohmann(Soyuz, OM.semiAxis);
                Soyuz.v    = (float)Math.sqrt(Soyuz.vx * Soyuz.vx + Soyuz.vy * Soyuz.vy);
                normdeltav = (vh / Soyuz.v) - 1;

                Soyuz.thrust    = SOYUZTHRUST_NUM_FRAMES;
                soyuzVelocityDirX = (normdeltav < 0) ? Soyuz.vx : -Soyuz.vx;
                soyuzVelocityDirY = (normdeltav < 0) ? Soyuz.vy : -Soyuz.vy;

                Soyuz.vx += normdeltav * Soyuz.vx;
                Soyuz.vy += normdeltav * Soyuz.vy;

                Soyuz.onScreen = true;
                SoyuzBob.setVisible(false);
                OMPending      = false;
                break;

            case OM_CLEANUPDESCENT:
                Soyuz.thrust   = Spacecraft.NO_THRUST;
                Soyuz.onScreen = false;
                SoyuzBob.setVisible(false);
                OMPending      = false;
                break;

            case OM_INITLAUNCH:
                if(soyuzLaunchCount==0)
                    MirBob.setFrameRow(RocketScience.MIRBOB_ROW_0SOYUZ);
                if(soyuzLaunchCount==1)
                    MirBob.setFrameRow(RocketScience.MIRBOB_ROW_1SOYUZ);

                alfa = (float)(360.0 * Math.random());
                x    = (float)Math.cos(alfa * DEG2RAD);
                y    = (float)Math.sin(alfa * DEG2RAD);
                Soyuz.x = OM.alfa * x;  // only in this case OM.alfa is initial distance form Earth
                Soyuz.y = OM.alfa * y;  // only in this case OM.alfa is initial distance form Earth

                vh       = DeltaVHohmann(Soyuz, OM.semiAxis);
                Soyuz.vx = vh * -y;
                Soyuz.vy = vh * x;

                Soyuz.thrust      = SOYUZTHRUST_NUM_FRAMES;
                soyuzVelocityDirX =  y;
                soyuzVelocityDirY = -x;

                OMTable[OMClass][6].alfa = trim360(alfa + 180);
                OMTable[OMClass][5].alfa = trim360(alfa + 180);
                OMTable[OMClass][4].alfa = alfa;

                Soyuz.onScreen = true;
                SoyuzBob.setVisible(true);
                OMPending      = false;
                break;

           case OM_DOCKING:
                Soyuz.thrust   = Spacecraft.NO_THRUST;
                Soyuz.onScreen = false;
                Mir.setBlockAtlantisDocking(false);
                OMPending      = false;
                break;

           case OM_RENDEZVOUS:
                Soyuz.r = (float)Math.sqrt(Soyuz.x * Soyuz.x + Soyuz.y * Soyuz.y);
                Soyuz.v = (float)Math.sqrt(Soyuz.vx * Soyuz.vx + Soyuz.vy * Soyuz.vy);
                Mir.r   = (float)Math.sqrt(Mir.x * Mir.x + Mir.y * Mir.y);
                Mir.v   = (float)Math.sqrt(Mir.vx * Mir.vx + Mir.vy * Mir.vy);

                currentTD = atan360(Mir.x, Mir.y) - atan360(Soyuz.x, Soyuz.y);
                finalTD   = (float)(180 * (1.0 - Math.pow( 0.5 * (Soyuz.r + Mir.r) / Mir.r , 3.0 / 2.0 )) );

                if(OMClass==OMC_LODOCK) {
                    if(currentTD < 0 )       currentTD += 360;
                    if(currentTD < finalTD)   currentTD += 360;
                    deltaTD = (float)(Soyuz.v / Soyuz.r - Mir.v / Mir.r) * deltaT * RAD2DEG;
                }
                if(OMClass==OMC_HIDOCK) {
                    finalTD   = (float)-finalTD;
                    deltaTD   = (float)-(Soyuz.v / Soyuz.r - Mir.v / Mir.r) * deltaT * RAD2DEG;
                    currentTD = (float)-currentTD;
                    if(currentTD < 0 )       currentTD += 360;
                    if(currentTD < finalTD ) currentTD += 360;
                }
                TD = currentTD - finalTD;
                break;

           case OM_MANEUVER:
                alfa = OM.alfa - atan360(Soyuz.x, Soyuz.y);
                if(alfa < -180) alfa += 360;              //*** -180..0..180
                readyJets = (alfa > 0) ? true : false; // readyJets if soyuz 180 deg from OM Point
                break;

           case OM_NOP :
                break;
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void navigate(MirBob MirBob, Bob SoyuzBob, MirSpacecraft Mir, Spacecraft Soyuz) {
        final float CATCHRADIUS_SQR = (8 * 8) / (RocketScience.ZOOM * RocketScience.ZOOM);

        float vh;
        float normdeltav;
        float alfa;
        float dx;
        float dy;

        if(OMPending==true) {
            switch(OM.maneuverType) {
                case OM_SHOWSOYUZ:
                    dx = Mir.x - Soyuz.x;
                    dy = Mir.y - Soyuz.y;
                    if((dx * dx + dy * dy) > CATCHRADIUS_SQR) {
                        SoyuzBob.setVisible(true);
                        if(soyuzLaunchCount==2)
                            MirBob.setFrameRow(RocketScience.MIRBOB_ROW_1SOYUZ);
                        if(soyuzLaunchCount==3)
                            MirBob.setFrameRow(RocketScience.MIRBOB_ROW_0SOYUZ);
                        OMPending = false;
                    }
                    break;

                case OM_HIDESOYUZ:
                    dx = Mir.x - Soyuz.x;
                    dy = Mir.y - Soyuz.y;
                    if((dx * dx + dy * dy) < CATCHRADIUS_SQR) {
                        SoyuzBob.setVisible(false);
                        if(soyuzLaunchCount==0)
                            MirBob.setFrameRow(RocketScience.MIRBOB_ROW_1SOYUZ);
                        if(soyuzLaunchCount==1)
                            MirBob.setFrameRow(RocketScience.MIRBOB_ROW_2SOYUZ);
                        OMPending = false;
                    }
                    break;

                case OM_RENDEZVOUS:
                    TD = TD - deltaTD;
                    if(TD<=0) {
                        Mir.setBlockAtlantisDocking(true);
                        if(OMClass==OMC_LODOCK)
                            OMTable[OMC_LODOCK][1].alfa = trim360(atan360(Soyuz.x, Soyuz.y) + 180);
                        if(OMClass==OMC_HIDOCK)
                            OMTable[OMC_HIDOCK][1].alfa = trim360(atan360(Mir.x, Mir.y) + 180);

                        vh         = DeltaVHohmann(Soyuz, OM.semiAxis);
                        Soyuz.v    = (float)Math.sqrt(Soyuz.vx * Soyuz.vx + Soyuz.vy * Soyuz.vy);
                        normdeltav = (vh / Soyuz.v) - 1;

                        Soyuz.thrust    = SOYUZTHRUST_NUM_FRAMES;
                        soyuzVelocityDirX = (normdeltav < 0) ? Soyuz.vx : -Soyuz.vx;
                        soyuzVelocityDirY = (normdeltav < 0) ? Soyuz.vy : -Soyuz.vy;

                        Soyuz.vx += normdeltav * Soyuz.vx;
                        Soyuz.vy += normdeltav * Soyuz.vy;
                        OMPending = false;
                    }
                    break;

                case OM_MANEUVER:
                    alfa = OM.alfa - atan360(Soyuz.x, Soyuz.y);
                    if(alfa < -180) alfa += 360;
                    if( (alfa > 0) && (readyJets==false) )
                        readyJets = true;

                    if( (alfa < 0) && (readyJets==true) ) {
                        vh         = DeltaVHohmann(Soyuz, OM.semiAxis);
                        Soyuz.v    = (float)Math.sqrt(Soyuz.vx * Soyuz.vx + Soyuz.vy * Soyuz.vy);
                        normdeltav = (vh / Soyuz.v) - 1;

                        Soyuz.thrust = SOYUZTHRUST_NUM_FRAMES;
                        soyuzVelocityDirX = (normdeltav < 0) ? Soyuz.vx : -Soyuz.vx;
                        soyuzVelocityDirY = (normdeltav < 0) ? Soyuz.vy : -Soyuz.vy;

                        Soyuz.vx += normdeltav * Soyuz.vx;
                        Soyuz.vy += normdeltav * Soyuz.vy;
                        OMPending = false;
                        readyJets = false;
                    }
                    break;

                case OM_NOP:
                    break;
            }
        }
    }

/*** Propellerhead Info ********************************************************
*                                                                              *
*  How to calculate the Hohmann transfer velocity vh                           *
*                                                                              *
*  In order to transfer a spacecraft from a circular orbit with radius r1 to   *
*  reach an orbit of radius r2 you have to accelerate the spacecraft to the    *
*  Hohmann velocity vh. With v2 as the velocity at the target orbit, use the   *
*  conservation of moments                                                     *
*                                                                              *
*     vh * r1 = v2 * r2 ->  v2 = vh * ( r1 / r2 )                       (1)    *
*                                                                              *
*  and the conservation of energy                                              *
*                                                                              *
*    -g * m1 * m2 / r1 + 1/2 * m2 * vh^2 = -g * m1 * m2 / r2 + 1/2 * m2 * v2^2 *
*                                                                              *
*  where g is the gravitational constant, and m1 the mass of the Earth.        *
*  Plug in Equ.(1), set r2 = 2 * a - r1, where a is the major semi-axis of the *
*  (Hohmann) transfer orbit ellipse. After a little algebra we receive         *
*                                                                              *
*     vh^2 = 2 * g * m1 * ( 1 / r1 - 1 / a )                                   *
*                                                                              *
*  With the spacecraft already being on a circular orbit with velocity v1 the  *
*  necessary change deltaV to vh is                                            *
*                                                                              *
*      deltaV = vh - v1;                                                       *
*                                                                              *
*******************************************************************************/

////////////////////////////////////////////////////////////////////////////////
    private float DeltaVHohmann(Spacecraft s, float a) {
        float r1 = (float)Math.sqrt(s.x * s.x + s.y * s.y);
        float vh = (float)Math.sqrt(2 * Spacecraft.GM / r1 - Spacecraft.GM / a);
        return(vh);
    }

////////////////////////////////////////////////////////////////////////////////
    public void triggerSoyuz() {

        if(OMIndex==NIL) {
            soyuzLaunchCount++;
            if(soyuzLaunchCount==4) soyuzLaunchCount = 0;
            switch(soyuzLaunchCount) {
                case 0 : OMClass = OMC_LODOCK; break;
                case 1 : OMClass = OMC_HIDOCK; break;
                case 2 : OMClass = OMC_HISEPR; break;
                case 3 : OMClass = OMC_LOSEPR; break;
            }
            OMIndex = OM_STEPS - 1;
        }
    }

////////////////////////////////////////////////////////////////////////////////
    private float atan360(float x, float y) {
        double phi;

        phi = Math.atan2((double)y, (double)x) * RAD2DEG;
        if(phi<0) phi += 360;
        return((float)phi);
    }

////////////////////////////////////////////////////////////////////////////////
    private float trim360(float phi) {

        if(phi>360) phi -= 360;
        return(phi);
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                  Sleeper                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

interface Sleeper {
    public void wakeUp(int id);
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                   Timer                                    //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

final class Timer implements Runnable {
    final static int OFF = -1;

    private int      id;
    private long     snooze;
    private Thread   timerThread;

// This is an ugly clutch. Something like a callback function/method should be
// more elegant. To be implemented in release 2.0 :-)

    private RocketScience client;

////////////////////////////////////////////////////////////////////////////////
    public Timer(RocketScience client, int id) {
        this.client  = client;
        this.id      = id;
        this.snooze  = OFF;

        if(this.timerThread==null) {
            this.timerThread = new Thread(this, "RocketScience 101 Timer Thread " + id);
            this.timerThread.start();
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void clearSnooze() {
        if(this.timerThread!=null)
            this.snooze = OFF;
    }

////////////////////////////////////////////////////////////////////////////////
    public void setSnooze(long snooze) {
        if(this.timerThread!=null)
            this.snooze = snooze;
    }

////////////////////////////////////////////////////////////////////////////////
    public void run() {
        while(Thread.currentThread()==this.timerThread) {
            try {
                this.timerThread.sleep(1000);   // sleep 1 sec
            } catch(InterruptedException e) {}

            if(this.snooze!=OFF) {
                this.snooze--;
                if(this.snooze<1) {
                    this.snooze = OFF;
                    this.client.wakeUp(this.id);
                }
            }
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public synchronized void stop() {

        if(this.timerThread!=null && this.timerThread.isAlive())
            this.timerThread.stop();
        this.timerThread = null;
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                               DirtyRect                                    //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

// Implements the classic dirty rectangle algorithm. In DirtyRect the elegant
// Java allocation method ("new") for adding rectangles is traded with one that
// uses a static Rectangle. Since these methods here are called quite often,
// this avoids the necessity for frequent garbage collection.

final class DirtyRect {
    private Rectangle clipRect;  // global clipRectangle for all bobs
    private boolean   firstClip; // flags start of clipping
    private Rectangle tmpRect;   // static rect to avoid dynamic allocation

////////////////////////////////////////////////////////////////////////////////
    public DirtyRect() {
        this.firstClip = true;
        this.clipRect  = new Rectangle();
        this.tmpRect   = new Rectangle();
    }

////////////////////////////////////////////////////////////////////////////////
    public void beginClipRect() {
        this.firstClip = true;
    }

////////////////////////////////////////////////////////////////////////////////
    public Rectangle endClipRect() {
        return this.clipRect;
    }

////////////////////////////////////////////////////////////////////////////////
    public void addClipRect(Rectangle rect) {
        addClipRect(rect.x, rect.y, rect.width, rect.height);
    }

////////////////////////////////////////////////////////////////////////////////
    public void addClipRect(int x, int y, int width, int height) {
        if(this.firstClip) {
            this.clipRect.x      = x;
            this.clipRect.y      = y;
            this.clipRect.width  = width;
            this.clipRect.height = height;

            this.firstClip = false;
        }
        else {
            this.tmpRect.x      = x;
            this.tmpRect.y      = y;
            this.tmpRect.width  = width;
            this.tmpRect.height = height;

            this.clipRect = this.clipRect.union(tmpRect);
        }
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                   Bob                                      //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

// A "Bob" is AMIGA-lingo for a blitter object (=sprite).

class Bob {
            int     x;               // left coordinate
            int     y;               // top coordinate
            int     width;           // width
            int     height;          // height
    private int     saveX;           // top coordinate of bob area to save
    private int     saveY;           // left coordinate of bob area to save
            Image   saveImage;       // place for the bob area to save
    private int     frameNr;         // frame number (column)
    private int     frameRow;        // frame row
            int     colsOfFrames;    // # of frame columns
            Image   stripImage;      // actual image strip
            int     offsetX;         // x offset of the image to the bob's x coordinate
            int     offsetY;         // y offset of the image to the bob's x coordinate
            int     bitmapWidth;     // x offset to the next frame on the image strip
            int     bitmapHeight;    // y offset to the next frame on the image strip
    private boolean visible;         // is bob currently visible
    private boolean inUse;           // is save image used -> backbuffer must be restored
    private Image   destImage;       // destination image -> where the bob is rendered to

////////////////////////////////////////////////////////////////////////////////
    public Bob( int width, int height,  int colsOfFrames, Image stripImage,
        int offsetX, int offsetY, int bitmapWidth, int bitmapHeight,
        boolean visible, Image destImage, Component component) {

        this.x             = 0;
        this.y             = 0;
        this.width         = width;
        this.height        = height;
        this.saveX         = 0;
        this.saveY         = 0;
        this.saveImage     = component.createImage(this.width, this.height);
        this.frameNr       = 0;
        this.frameRow      = 0;
        this.colsOfFrames  = colsOfFrames;
        this.stripImage    = stripImage;
        this.offsetX       = offsetX;
        this.offsetY       = offsetY;
        this.bitmapWidth   = bitmapWidth;
        this.bitmapHeight  = bitmapHeight;
        this.visible       = visible;
        this.inUse         = false;
        this.destImage     = destImage;
    }

////////////////////////////////////////////////////////////////////////////////
    public void blit(Image destImage, int destX, int destY, Image srcImage, int srcX, int srcY, int w, int h) {
        Graphics g = destImage.getGraphics().create();
        g.clipRect(destX, destY, w, h);
        g.drawImage(srcImage, srcX, srcY, null);
        g.dispose();
    }

////////////////////////////////////////////////////////////////////////////////
    public void lift(DirtyRect dirtyRect) {  // Copies saveImage to screen
        if(this.isInUse()) {
            blit(this.destImage, this.saveX, this.saveY, this.saveImage, this.saveX, this.saveY, this.width, this.height);
            if(!this.visible) this.inUse = false;
            dirtyRect.addClipRect(this.saveX, this.saveY, this.width, this.height);
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void save() {  // Copies screen to saveImage
        if(this.visible) {
            blit(this.saveImage, 0, 0, this.destImage, -this.x, -this.y, this.width, this.height);
            this.saveX = this.x;
            this.saveY = this.y;
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void drop(DirtyRect dirtyRect) {  // Copies bob to screen
        if(this.visible) {
            blit(this.destImage, this.x, this.y, this.stripImage,
               -this.frameNr  * this.bitmapWidth  + this.x,
               -this.frameRow * this.bitmapHeight + this.y,
               this.width, this.height);

            this.inUse = true;
            dirtyRect.addClipRect(this.x, this.y, this.width, this.height);
        }
    }

    public void drop(DirtyRect dirtyRect, int frameNr) {
        this.frameNr = frameNr;
        this.drop(dirtyRect);
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public void setFrameNr(int frameNr) {
        this.frameNr = frameNr;
    }

    public void setFrameNr(float x, float y) {
        final float RAD2DEG = (float)(180.0 / Math.PI);

        double phi        = Math.atan2((double)y, (double)x) * RAD2DEG;
        int    imageAngle = (int)((360.0 / this.colsOfFrames ) + 1);
        int    frameNr;

        phi += 0.5 * imageAngle;
        if(phi<0)    phi += 360;
        if(phi>=360) phi -= 360;

        frameNr = (int)phi / imageAngle;
        setFrameNr(frameNr);
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public int getFrameNr() {
        return this.frameNr;
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public void setFrameRow(int frameRow) {
        this.frameRow = frameRow;
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public void cycleFrameNr() {
        this.frameNr++;
        if(this.frameNr>=this.colsOfFrames)
            this.frameNr = 0;
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public int getFrameRow() {
        return this.frameRow;
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public boolean isVisible() {
        return(this.visible);
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public void setVisible(boolean visible) {
        this.visible = visible;
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public boolean isInUse() {
        return(this.inUse);
    }

//////////////////////////////////////////////////////////////////////////////////////////////
    public void setInUse(boolean inUse) {
        this.inUse = inUse;
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                   MirBob                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

final class MirBob extends Bob {

 // Translation offsets AtlantisBob to MirBob for all frames

    int transX[] = {27, 26, 21, 13, 3, -8, -15, -19, -21, -19, -14, -7, 3, 13, 21, 26};
    int transY[] = { 3, -7, -15, -19, -21, -19, -14, -6, 3, 12, 20, 26, 27, 24, 20, 11};

    MirBob( int width, int height, int colsOfFrames, Image stripImage,
        int offsetX, int offsetY, int bitmapWidth, int bitmapHeight,
        boolean isVisible, Image destImage, Component component) {

        super( width, height, colsOfFrames, stripImage, offsetX, offsetY,
            bitmapWidth, bitmapHeight, isVisible, destImage, component);
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                               Spacecraft                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

class Spacecraft {
    public final static float GRAVITY      = (float)6.67E-11;             // [N kg2/m2]
    public final static float EARTH_MASS   = (float)6.00E+24;             // [kg]
    public final static float EARTH_RADIUS = (float)6.40E+06;             // [m]
    public final static float ONE_DAY      = (float)(24.0 * 60.0 * 60.0); // [s]
    public final static float GM           = GRAVITY * EARTH_MASS;
    public final static float GEOSTATIONARY_RADIUS = (float)Math.pow(GRAVITY * EARTH_MASS * Math.pow(ONE_DAY / (2 * Math.PI), 2), 1.0 / 3.0); // [m]

    public final static float NO_THRUST             =  0;
    public final static float ATLANTIS_SPEED_THRUST =  (float)(70 * 0.001);
    public final static float ATLANTIS_BRAKE_THRUST = -(float)(70 * 0.001);

            float   x;
            float   y;
            float   vx;
            float   vy;
            float   r;
            float   v;

            float   thrust;
            float   thrustX;
            float   thrustY;

            boolean onScreen;
    private boolean hasDocked;

////////////////////////////////////////////////////////////////////////////////
    public Spacecraft(float x, float y, float vx, float vy, boolean onScreen) {
        this.x  = x;
        this.y  = y;
        this.vx = vx;
        this.vy = vy;
        this.r  = (float)Math.sqrt( x * x + y * y );
        this.v  = (float)Math.sqrt( vx * vx + vy * vy );
        this.onScreen = onScreen;

        this.thrust    = NO_THRUST;
        this.thrustX   = 0;
        this.thrustY   = 0;
        this.hasDocked = false;
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean hasDocked() {
        return this.hasDocked;
    }

////////////////////////////////////////////////////////////////////////////////
    public void setDocked(boolean docked) {
        this.hasDocked = docked;
    }

////////////////////////////////////////////////////////////////////////////////
    public void newPosition(float deltaT) {

    // Uses a 2nd order Runge-Kutte algorithm to solve the equation of motion

       float d;
       float ax;
       float ay;
       float tmpX;
       float tmpY;

       d  = (float)Math.sqrt(x * x + y * y);
       d  = -GM / (d * d * d);

       ax = d * x + thrustX;
       ay = d * y + thrustY;

       tmpX = x + ( vx + (float)0.5 * ax * deltaT ) * deltaT;
       tmpY = y + ( vy + (float)0.5 * ay * deltaT ) * deltaT;

       d  = (float)Math.sqrt(tmpX * tmpX + tmpY * tmpY);
       d  = -GM / (d * d * d);

       ax = (float)0.5 * (ax + d * tmpX);
       ay = (float)0.5 * (ay + d * tmpY);

       x  += ( vx + (float)0.5 * ax * deltaT ) * deltaT;
       y  += ( vy + (float)0.5 * ay * deltaT ) * deltaT;
       vx += ax * deltaT;
       vy += ay * deltaT;

       r  = (float)Math.sqrt(x * x + y * y);
       v  = (float)Math.sqrt(vx * vx + vy * vy);
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                               MirSpacecraft                                //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

final class MirSpacecraft extends Spacecraft {
    private int     trailCnt;
    private int     trailMax;
    private boolean trailDoTrail;
    private boolean blockAtlantisDocking = false;

    MirSpacecraft(float x, float y, float vx, float vy, boolean onScreen) {
        super(x, y, vx, vy, onScreen);
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean blockAtlantisDocking() {
        return this.blockAtlantisDocking;
    }

////////////////////////////////////////////////////////////////////////////////
    public void setBlockAtlantisDocking(boolean isBlocked) {
        this.blockAtlantisDocking = isBlocked;
    }

////////////////////////////////////////////////////////////////////////////////
    public void resetTrail(int numTrailDots) {
        this.trailCnt      = 0;
        this.trailMax      = numTrailDots;
        this.trailDoTrail  = true;
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean doTrail() {
        this.trailCnt++;
        if(this.trailCnt>this.trailMax)
            this.trailDoTrail = false;
        return(this.trailDoTrail);
    }
}

////////////////////////////////////////////////////////////////////////////////
//                                                                            //
//                                   Gadget                                   //
//                                                                            //
////////////////////////////////////////////////////////////////////////////////

// A "Gadget" is AMIGA-lingo for a push button

final class Gadget {
    final static int GADGET_ROW_UP       = 0;
    final static int GADGET_ROW_DOWN     = 1;
    final static int GADGET_ROW_DISABLED = 2;

    private Rectangle rect;
    private boolean   pressed;
    private boolean   enabled;
    private Image     image;
    private Image     destImage;
    private Container container;

////////////////////////////////////////////////////////////////////////////////
    public Gadget(int x, int y, Image image, Image destImage, Container container) {
        this.rect      = new Rectangle(x, y, image.getWidth(container), image.getHeight(container) / 3);
        this.image     = image;
        this.pressed   = false;
        this.enabled   = true;
        this.destImage = destImage;
        this.container = container;
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean inside(int x, int y) {
        if(this.enabled)
            return this.rect.inside(x, y);
        else
            return false;
    }

////////////////////////////////////////////////////////////////////////////////
    public Rectangle getRectangle() {
        return this.rect;
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean isPressed() {
        return this.pressed;
    }

////////////////////////////////////////////////////////////////////////////////
    public boolean isEnabled() {
        return this.enabled;
    }

////////////////////////////////////////////////////////////////////////////////
    public void enable() {
        this.pressed = false;
        this.enabled = true;
        this.draw();
    }

////////////////////////////////////////////////////////////////////////////////
    public void disable() {
        this.pressed = false;
        this.enabled = false;
        this.draw();
    }

////////////////////////////////////////////////////////////////////////////////
    public void down() {
        if(this.enabled) {
            this.drawDown();
            this.pressed = true;
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void up() {
        if(this.enabled) {
            this.drawUp();
            this.pressed = false;
        }
    }

////////////////////////////////////////////////////////////////////////////////
    public void draw() {
        if(this.enabled) {
            if(this.pressed)
                this.drawDown();
            else
               this.drawUp();
        }
        else
            this.drawDisabled();
    }

    private synchronized void drawUp() {
        Graphics g = destImage.getGraphics().create();
        g.clipRect(this.rect.x, this.rect.y, this.rect.width, this.rect.height);
        g.drawImage(this.image, this.rect.x, -this.rect.height * GADGET_ROW_UP + this.rect.y, this.container);
        g.dispose();
    }

    private synchronized void drawDown() {
        Graphics g = this.destImage.getGraphics().create();
        g.clipRect(this.rect.x, this.rect.y, this.rect.width, this.rect.height);
        g.drawImage(this.image, this.rect.x, -this.rect.height * GADGET_ROW_DOWN + this.rect.y, this.container);
        g.dispose();
    }

    private synchronized void drawDisabled() {
        Graphics g = this.destImage.getGraphics().create();
        g.clipRect(this.rect.x, this.rect.y, this.rect.width, this.rect.height);
        g.drawImage(this.image, this.rect.x, -this.rect.height * GADGET_ROW_DISABLED + this.rect.y, this.container);
        g.dispose();
    }
}
