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

// Freedom VR 2.0
// (C) 1997 Paul A. Houle (houle@msc.cornell.edu)
// http://www.msc.cornell.edu/~houle/vr/freedom/
// 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// (or see http://www.gnu.org/copyleft/gpl.html)

public class fvr2 extends Applet implements Runnable
{
   static final boolean debug = false;  // if false,  debug code is omitted
					// by javac since this variable is
					// final.

   boolean debugLoad = false;		// issue debug messages for image
					// loading.  Generates a lot of
					// data!

   boolean debugEvents = false;		// debug event handling
   boolean debugPaint = false;		// debug painting

   AMDProgressBarEmbed progbar;		// progress bar

   Image frame[][];			// VR model frames
   int status[][];			// status of frames

   boolean fatal;			// true if a fatal error happened
   boolean ready;			// true if ready to spin
   boolean stopped;			// true if we've been stopped
   boolean dragged;			// true if we're dragging
   boolean isMouseIn;			// true if mouse is in applet

   Image dBuff;				// the double buffer
   int direction = 1;			// 1 for cw photos, -1 for ccw
   int upsideDown = 1;			// make -1 to flip upside down

   Thread me;				// the active thread

   int loadFrameI,loadFrameJ;		// frame being loaded at the moment
   int FrameI, FrameJ;			// frame being displayed at the moment

   int oldx,oldy;			// x,y coordinates at drag start
   int zoneI,zoneJ;			// current dragPixel zone
   int mouseX, mouseY;			// x,y coordinates of mouse
   int dragPixels = 10;			// drag sensitivity,  pixels per frame

   int progX, progY;			// progress bar X and Y
   int marginX,marginY;			// X and Y margins

   boolean circleN,circleM;		// do we circle in horizontal and
					// vertical directions?
   int N, M;				// numbers of frames
					// N=horizontal
					// M=vertical

   int initialI, initialJ;		// initial frame to display
   String modelBase;			// URL base for loading frames
   String extension;			// extension of image files
   String statusText;			// loaded status Text
   String currentStatus;		// I remember currentStatus

   static int myCursor = Frame.MOVE_CURSOR; // the type of cursor I use

   Hashtable plugins;			// list of loaded plug-ins
   Vector hotspots;			// Vector of hotspots
   fvr2Hotspot overHotspot;		// The hotspot mouse is over
   int recall_x, recall_y;		// used to remember where mouse was
					// when 'x' is hit.

   /**
    *
    * Applet init method.  Do all of the initialization which can be
    * done quickly;  everything except for image loading.  Image loading
    * will be done in a separate thread so the AWT event handler won't
    * be hung up when we load images
    *
    */
 
  
   public void init()
   {
      fatal = false;
      ready = false;

      // if neither nFrames or mFrames is specified,  we'll
      // use this variable to trigger an error message

      boolean frames_specified = false;

      // nFrames number of frames in horizontal direction

      String s = getParameter("nFrames");

      if(s == null)
      {
         N = 1;
      }
      else
         try
         {
            N = Integer.parseInt(s);
            frames_specified = true;
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: nFrames not a number");
            fatal = true;
            repaint();
            return;
         }

      // mFrames number of frames in vertical direction

      s = getParameter("mFrames");
      if(s == null)
      {
         M = 1;
      }
      else
         try
         {
            M = Integer.parseInt(s);
            frames_specified = true;
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: mFrames not a number");
            fatal = true;
            repaint();
            return;
         }

      if(frames_specified == false)
      {
         System.err.println("fvr2: must specify nFrames or mFrames.");
         fatal = true;
         repaint();
         return;
      }

      // progX -- position progress bar in X coordinate

      progX = 20;
      s = getParameter("progX");
      if(s != null)
         try
         {
            progX = Integer.parseInt(s);
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: progX not a number");
            fatal = true;
            repaint();
            return;
         }

      // progY -- position progress bar in Y coordinate

      progY = 20;
      s = getParameter("progY");
      if(s != null)
         try
         {
            progY = Integer.parseInt(s);
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: progY not a number");
            fatal = true;
            repaint();
            return;
         }

      // marginX -- position of VR image X coordinate

      marginX = 0;
      marginY = 0;
      s = getParameter("marginX");
      if(s != null)
         try
         {
            marginX = Integer.parseInt(s);
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: marginX not a number");
            fatal = true;
            repaint();
            return;
         }

      // marginY -- position of VR image Y coordinate

      s = getParameter("marginY");
      if(s != null)
         try
         {
            marginY = Integer.parseInt(s);
         }
         catch(NumberFormatException nfe)
         {
            System.err.println("fvr2: marginY not a number");
            fatal = true;
            repaint();
            return;
         }

      // modelBase -- where to find VR image files

      s = getParameter("modelBase");
      if(s == null)
      {
         s = "model/";
      }

      modelBase = s;

      // imgExt -- what the filenames end in

      extension = ".jpg";
      s = getParameter("imgExt");
      if(s != null)
         extension = s;

      // noCircleN,  noCircleM  -- for making non 360 
      // degree models which don't close in a circle

      circleN = true;
      if(getParameter("noCircleN") != null)
         circleN = false;

      circleM = true;
      if(getParameter("noCircleM") != null)
         circleM = false;

      // ccw -- for a model where photos go in counter
      // clockwise order

      if(getParameter("ccw") != null)
         direction = -1;

      // upsideDown -- for a model where the photos are
      // reversed in the vertical direction

      if(getParameter("upsideDown") != null)
         upsideDown = -1;

      // initialI,  initialJ -- the first frame to display
      // default is 1

      initialI = 1;

      s = getParameter("initialI");
      if(s != null)
         initialI = Integer.parseInt(s);
      initialJ = 1;

      s = getParameter("initialJ");
      if(s != null)
         initialJ = Integer.parseInt(s);

      // bgColor -- set background color

      Color bgColor = getColorParameter("bgColor");
      if(bgColor != null)
         setBackground(bgColor);

      // configure status line text

      statusText = "Freedom VR ready";

      if(getParameter("statusText") != null)
         statusText = getParameter("statusText");

      // load plug-ins

      plugins = new Hashtable();
      s = getParameter("plugins");
      if(s != null)
      {
         for(StringTokenizer st = new StringTokenizer(s);st.hasMoreElements();)
         {
            Class pluginClass = null;
            String pluginName = (String)st.nextElement();
            try
            {
               pluginClass = Class.forName(pluginName);
            }
            catch(Exception e)
            {
               fatal = true;
               System.out.println("fvr2 fatal error:  couldn't load plugin " + pluginName);
               break;
            }
            plugins.put(pluginName,pluginClass);
         }
      }

      // initialize arrays

      frame = new Image[M][N];
      status = new int[M][N];

      // set up progress bar

      progbar = new AMDProgressBarEmbed(this);
      progbar.reshape(progX,progY,150,25);
      progbar.setText("Loading VR object");
      progbar.setPercent(0);

      // set up a wait cursor while we load

      setCursor(Frame.WAIT_CURSOR);
      FrameI = initialI;
      FrameJ = initialJ;

      // initialize Hotspot system

      hotspots = new Vector();
      overHotspot = null;
      isMouseIn = false;
      System.out.println(getAppletInfo());
   }

   // code stolen from "Java in a Nutshell" by D. Flanagan
   // parse a hexadecimal color #RRGGBB

   Color getColorParameter(String name)
   {
      String value = getParameter(name);
      int intvalue;
      try
      {
         intvalue = Integer.parseInt(value,16);
      }
      catch(NumberFormatException e)
      {
         return null;
      }
      return new Color(intvalue);
   }

   /**
    * @return applet info string
    **/

   public String getAppletInfo()
   {
      return "Freedom VR 2.0 (http://www.honeylocust.com/vr/)";
   }

   /**
    * The browser calls <CODE>start()</CODE> after calling
    * <CODE>init()</CODE>;  in Freedom VR,  this method starts a new
    * thread whose job is to load images.  The thread dies after the images
    * are loaded,  and for the rest of it's life,  FVR is "animated" by
    * the AWT event handling thread.  Execution picks up in the run()
    * method after the thread starts.
    **/

   public void start()
   {
      me = new Thread(this);
      me.start();
   }

   /**
    *
    * The browser calls <CODE>stop()</CODE> when an applet is being
    * stopped.  The stop could be temporary,  for instance,  the applet
    * might be stopped temporarily when the user visits another page,
    * and restarted when the user hits the back button and comes back
    * to the page with the applet.  In our case,  we shut down the image
    * loading thread is shut down if it's running.  Nothing else needs to
    * be done,  since no event handlers are going to be called when the
    * applet is stopped so the applet happily hibernates.
    *
    * <P>
    * Note that applets are responsible for stopping their own threads and
    * will eat up CPU time and hurt performance on the client machine unless
    * they stop themselves when requested in either the <CODE>stop()</CODE>
    * or <CODE>destroy()</CODE> methods.
    *
    **/

   public void stop()
   {
      me.stop();
      me = null;
   }

   /**
    *
    * When <CODE>destroy()</CODE> is called,  it's all over.  Time to die.
    * The applet will never get called back again.  Freedom VR trashes
    * any image objects it owns to free up memory.  <CODE>destroy()</CODE>
    * typically gets called a while after <CODE>stop()</CODE> does,
    * when the browser decides that the chance of the user going back to
    * the applet page is small enough that it isn't worth holding onto the
    * applet's code and data.
    * 
    */

   public void destroy()
   {
      System.out.println("Freedom VR destroying self.");
      // trash any image objects we own
      for(int i = 0;i < M;i++)
         for(int j = 0;j < N;j++)
            if(frame[i][j] != null)
	    {
               frame[i][j].flush();
               frame[i][j]=null;
	    };

      System.gc();
   }

   /**
    *
    * Ths method implements <CODE>ImageObserver.</CODE>  Java has a rather
    * complex system for loading images -- a method call which displays an
    * image returns immediately and causes another thread to display the
    * image.  This sounds complicated and it is,  but it has the advantage
    * that an applet isn't going to be hung up waiting for images to load.
    * <P>
    * The job of this method in Freedom VR is to control image loading:
    * it finds out when an image is completely loaded and then notifies
    * the image loading thread that it can go load the next image.
    *
    * @param img The image that's notifying us
    * @param infoflags the status of the image
    * @param x an x coordinate
    * @param y a y coordinate
    * @param width image width
    * @param height image height
    * @return <CODE>false</CODE> if we'll need more updates in the future,
    * <CODE>true</CODE> if we've been told everything we need to know.
    **/

   synchronized public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height)
   {
      // make sure locks are released so thread can stop in an
      // orderly manner
      if(!isActive())
      {
         notify();
         return false;
      }
      ;
      if(debug && debugLoad)
         System.out.println("Got into image update,  status=" + infoflags);
      if(img != frame[loadFrameI - 1][loadFrameJ - 1])
      {
         System.err.println("fvr2: image update out of sequence.");
         fatal = true;
         notify();
         return false;
      }
      ;
      status[loadFrameI - 1][loadFrameJ - 1] = infoflags;
      notify();
      return true;
   }

   /**
    *
    * Map frame number to the name of the frame;  return the relative
    * URL for frame i,j given i,j
    *
    * @param i vertical frame number
    * @param j horizontal   frame number
    * @returns string with relative URL for frame i,j
    */

   String frameName(int i, int j)
   {
      //	if M==1 the movie is a horizontal strip and names are
      //      just modelBase+i+extension,  if N==1 the movie is a vertical
      //      strip and names are still just modelBase+j+extension,  if
      //      both are nonzero then names are
      //      modelBase+i+"_"+j+extension
      if(M == 1)
      {
         return modelBase + j + extension;
      }
      else
         if(N == 1)
         {
            return modelBase + i + extension;
         }

      return modelBase + i + '_' + j + extension;
   }

   /**
    *
    * Given the i and j number of the frame,  return the number giving
    * the serial order in which it will be loaded.  Used for the progress
    * bar.
    *
    * @param i vertical frame number
    * @param j horizontal frame number
    * @return serial number of frame
    *
    **/

   int frameNumber(int i, int j)
   {
      int number=((i - 1) * N + j);
      if (number<(initialI-1)*N+initialJ)
         number++;

      return number;
   }

   /**
    *
    * Load the frame indexed by i and j
    *
    * @param i vertical frame number
    * @param j horizontal frame number
    * @return <CODE>true</CODE> if everything worked,  <CODE>false</CODE>
    * if it didn't.
    *
    **/

   boolean loadFrame(int i, int j)
   {
      // if the image is already loaded,  we're done!

      if(status[i - 1][j - 1] == ImageObserver.ALLBITS)
         return true;

      try
      {
         URL url = new URL(getDocumentBase(),frameName(i,j));
         if(debug)
            System.out.println("URL = " + url.toString());
         frame[i - 1][j - 1] = getImage(url);
         status[i - 1][j - 1] = 0;
      }
      catch(MalformedURLException mue)
      {
         fatal = true;
         System.err.println("fvr2: URL " + frameName(i,j) + " ill-formed");
         repaint();
         return false;
      }

      loadFrameI = i;
      loadFrameJ = j;

      // make sure we die in an orderly manner when the
      // applet (and thread) is stopped

      if(!isActive())
         return false;

      if(debug && debugLoad)
         System.out.println("fvr2: preparing image " + i + "," + j);

      synchronized(this)
      {
         if(!prepareImage(frame[i - 1][j - 1],this))
         {
            do
            {
               try
               {
                  if(debug && debugLoad)
                     System.out.println("fvr2: wait() on image " + i + "," + j);
                  wait();
                  if(debug && debugLoad)
                     System.out.println("fvr2: got out of wait.");
               }
               catch(InterruptedException e)
               {
		// The really weird thing is that neither JDK 1.0.2 nor
		// JDK 1.1 ever throw an InterruptedException!
               }

               if(!isActive())
                  return false;

               if((status[loadFrameI - 1][loadFrameJ - 1] & ImageObserver.ABORT) != 0)
               {
                  fatal = true;
                  repaint();
                  return false;
               }

               if(((status[loadFrameI - 1][loadFrameJ - 1] & ImageObserver.SOMEBITS) != 0) && (i == 0))
                  synchronousRepaint();
            }
            while((status[loadFrameI - 1][loadFrameJ - 1] & ImageObserver.ALLBITS) == 0);
         }
         else
            if(debug && debugLoad)
               System.out.println("Image " + i + "," + j + " already ready!");

         // the image decoders seem to leave a lot of garbage behind
         // in memory when they are done.  If we clean up after loading
         // a large image,  reliability on MacOS JVMs
         // improves.  I think this is also good for performance
         // on Unix JVMs;  another advantage of loading images only one
	 // at a time.

         System.gc();

         // clean up after image loader

         status[i - 1][j - 1] = ImageObserver.ALLBITS;
      }
      return true;
   }

   /**
    * The job of the run method is to run in a thread which loads the
    * images.
    *
    * <P>
    * Loading images one at a time saves a lot of grief in early Java
    * implementations -- since image loading requires bursts of CPU activity
    * as well as use of the network,  in theory one can improve performance
    * by loading more than one image at once.  In practice many
    * implementations of Java somehow trip up on themselves when loading
    * images and you're really best off loading one image at a time.  Mac
    * JVMs can hang up entirely when abused,  while Netscape on AIX loads
    * images incredibly slowly when more than one is being loaded at a
    * time.  The following code has been tested through several revisions
    * and seems to give consistently good to excellent performance on
    * all platforms,  unlike <CODE>MediaTracker().</CODE>  Prehaps I
    * should make it reusable,  but Freedom VR is deliberately designed to
    * have a minimum number of classes so it loads lightning fast.
    *
    **/

   public void run()
   {
      // the repaint thread appears to run at priority 4 in all
      // implementations of Java.  Setting this thread to priority 3 
      // ensures that the repaint thread can override this thread and
      // repaint the screen at any time on all platforms 
      // we've tested on (Unix,  Mac and Windows)

      me.setPriority(3);

      // Image loading eats memory,  collect garbage to make sure we
      // get as much as we can.
      System.gc();

      setDefaultStatus("Freedom VR: Loading poster frame");
      loadFrame(initialI,initialJ);

      for(int i = 1;i <= M;i++)
      {
         for(int j = 1;j <= N;j++)
         {
            setDefaultStatus("Freedom VR: Loading frame " + frameNumber(i,j) + " of " + (N * M));
            if(!loadFrame(i,j))
               return;
            progbar.setPercent(frameNumber(i,j) * 1.0 / (N * M));
         }
      }

      if(debug)
         System.out.println("fvr2: Image loading complete.");

      ready = true;
      setDefaultStatus(statusText);
      setCursor(myCursor);
      repaint();
   }

   /**
    * <B>The following group of methods are event handlers and override
    * methods in <I>java.awt.Component</I> of which <I>java.applet.Applet</I>
    * is a subclass.</B>  Freedom VR 2.0 uses the JDK 1.0.2 event model,
    * but may be upgraded to JDK 1.1 in the future when most users are using
    * JDK 1.1 AWT compatible browsers.
    * <P>
    * I get called when the mouse button goes down over the applet.  This
    * warns me that the user may be about to drag the object.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/

   public boolean mouseDown(Event evt, int x, int y)
   {
      if(overHotspot != null)
         overHotspot.mouseDown();
      mouseX = x;
      // continuously remember mouse location
      mouseY = y;
      oldx = x;
      oldy = y;
      zoneI = 0;
      zoneJ = 0;
      return true;
   }

   /**
    * I get called when the mouse is dragged (moved with the button down)
    * over the applet.  This code is responsible for rotating objects.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/

   public boolean mouseDrag(Event evt, int x, int y)
   {
      mouseX = x;               // continuously remember mouse location
      mouseY = y;

      if(debug && debugEvents)
         System.out.println("Mouse drag at " + x + "," + y);

      if(overHotspot != null)
         return super.mouseDrag(evt,x,y);

      dragged = true;

      int newzoneI = (y - oldy) / dragPixels;
      int newzoneJ = (x - oldx) / dragPixels;

      if(newzoneJ != zoneJ && N != 1)
      {
         panRight(newzoneJ - zoneJ);
         zoneJ = newzoneJ;
      }

      if(newzoneI != zoneI && M != 1)
      {
         tiltDown(newzoneI - zoneI);
         zoneI = newzoneI;
      }

      return true;
   }

   /**
    * This method is called when the mouse is moved,  that is,  when the
    * button is up.  This method alerts Hotspots when the mouse is moved
    * in or out of them.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/

   public boolean mouseMove(Event evt, int x, int y)
   {
      mouseX = x;	// keep track of mouse coordinates
      mouseY = y;

      if(overHotspot == null)
      {
         fvr2Hotspot hs = searchHotspots(FrameI,FrameJ,x,y);
         if(hs == null)
            return true;
         overHotspot = hs;
         hs.mouseOver();
      }
      else
      {
         fvr2Hotspot hs = overHotspot;
         if(!hs.isMouseOver(FrameI,FrameJ,x,y))
         {
            overHotspot = null;
            hs.mouseOut();
            setCursor(myCursor);
            showStatus(statusText);
         }
      }
      return true;
   }

   /**
    * This method is called when the mouse button is released.  This
    * method does two things.  It drops us out of drag mode if we're
    * dragging,  and it also triggers the Hotspot if the user let the button
    * go over a Hotspot.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/

   public boolean mouseUp(Event evt, int x, int y)
   {
      mouseX = x;		// keep track of mouse coordinates
      mouseY = y;
      if(debug && debugEvents)
      {
         System.out.println("fvr2: mouseUp event received");
         System.out.println("overHotspot = " + overHotspot);
      }

      if(dragged)
      {
         dragged = false;
         paintHotspots(getGraphics());
         return true;
      }

      fvr2Hotspot hs = overHotspot;
      if(hs != null)
         hs.mouseUp();
      return true;
   }

   /**
    * if the mouse enters the applet,  we show our status line,  
    * tell ourselves the mouse is in,  request the focus so we can get
    * keyboard input,  and repaint.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/

   public boolean mouseEnter(Event evt, int x, int y)
   {
      isMouseIn = true;
      showStatus(currentStatus);
      requestFocus();
      repaint();
      return true;
   }

   /**
    * if the mouse leaves the applet,  we deactivate any hotspots and
    * tell ourselves the mouse is out and repaint.
    *
    * @param evt Event object
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return <CODE>true</CODE> if event has been handled,  <CODE>false</CODE>
    * if it must be passed further up the hierarchy.
    **/


   public boolean mouseExit(Event evt, int x, int y)
   {
      isMouseIn = false;
      if(overHotspot != null)
      {
         overHotspot.mouseOut();
         overHotspot = null;
      }

      repaint();
      return true;
   }

   /**
    *
    * And if somebody hits a key we display frame number and cursor x,y
    * just a little bit of GUI for authoring ;-)
    * <P>
    * Also,  per request,  added keyboard navigation,  you can spin the
    * object with the arrow keys
    *
    * @param evt Event object
    * @param key the integer value of the key that was hit
    *
    */

   public boolean keyUp(Event evt, int key)
   {
      if(debug && debugEvents)
         System.out.println("fvr2: Captured key up event,  key=" + key);

      if(key == Event.LEFT)
      {
         panLeft(1);
         return true;
      }

      if(key == Event.RIGHT)
      {
         panRight(1);
         return true;
      }

      if(key == Event.DOWN)
      {
         tiltDown(1);
         return true;
      }

      if(key == Event.UP)
      {
         tiltUp(1);
         return true;
      }

      // if the user hits the 'x' button we display the frame
      // numbers and the (x,y) coordinates to the screen

      if(key == (int)'X' || key == (int)'x')
      {
         recall_x = mouseX;
         recall_y = mouseY;
         System.out.println("Frame [" + FrameI + "," + FrameJ + "] + (x,y)=(" + recall_x + "," + recall_y + ")");
         return true;
      }

      // if the user hits the 's' button,  we display the size
      // of the rectangle between the place where 'x' was last hit
      // and where the cursor is.

      if(key == (int)'s' || key == (int)'S')
      {
         System.out.println("(width,height)=(" + (mouseX - recall_x) + "," + (mouseY - recall_y) + ")");
         return true;
      }

      if(key == (int)'\n')
      {
         if(overHotspot != null)
            overHotspot.mouseUp();
      }

      return true;
   }

   /**
    * <B>The following group of methods are used for moving the view.  This
    * may be of interest to Javascript programmers who would like to script
    * Freedom VR,  so they've been made public;  the terms pan and tilt
    * come from cinematography and are probably technically right only for
    * the panorama case.</B>
    *
    * Pan the view to the right.
    *
    * @param steps the number of frames to pan
    * @return <CODE>true</CODE> if pan succeeded,  <CODE>false</CODE> if
    * it didn't.
    *
    */

   public boolean panRight(int steps)
   {
      steps *= direction;
      FrameJ += steps;

      if(!circleN)
      {
         if(FrameJ < 1)
         {
            FrameJ = 1;
            repaint();
            return false;
         }

         if(FrameJ >= N)
         {
            FrameJ = N;
            repaint();
            return false;
         }

      }

      while(FrameJ < 1)
         FrameJ += N;

      FrameJ = ((FrameJ - 1) % N) + 1;
      repaint();
      return true;
   }

   /**
    *
    * Pan view to the left.
    *
    * @param steps the number of frames to pan
    * @return <CODE>true</CODE> if pan succeeded,  <CODE>false</CODE> if
    * it didn't.
    *    
    */

   public boolean panLeft(int steps)
   {
      return panRight(-steps);
   }

   /**
    *
    * Tilt view down.
    *
    * @param steps the number of frames to tilt
    * @return <CODE>true</CODE> if tilt succeeded,  <CODE>false</CODE> if
    * it didn't.
    *    
    */

   public boolean tiltDown(int steps)
   {
      steps *= upsideDown;
      FrameI += steps;
      if(!circleM)
      {
         if(FrameI < 1)
         {
            FrameI = 1;
            repaint();
            return false;
         }
         ;
         if(FrameI >= M)
         {
            FrameI = M;
            repaint();
            return false;
         }
         ;
      }
      ;
      while(FrameI < 1)
         FrameI += M;
      FrameI = ((FrameI - 1) % M) + 1;
      repaint();
      return true;
   }

   /**
    *
    * Tilt view up.
    *
    * @param steps the number of frames to tilt
    * @return <CODE>true</CODE> if tilt succeeded,  <CODE>false</CODE> if
    * it didn't.
    *    
    */

   public boolean tiltUp(int steps)
   {
      return tiltDown(-steps);
   }

   /**
    * <B>The next bunch of methods are preoccupied with graphical display
    * </B>
    *
    * <CODE>update()</CODE> is overridden to prevent flicker;  by default
    * <CODE>update()</CODE> fills in the background color before calling
    * paint
    *
    * @param g the graphics context from the AWT
    *
    */

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

   /**
    *
    * The <CODE>paint()</CODE> method.  Draws the appropriate frame,
    * superimposing progress bar if necessary,  double buffering if
    * prudent.
    *
    * @param g the graphics context from the AWT
    *
    */

   public void paint(Graphics g)
   {
      if(debug && debugPaint)
         System.out.println("paint called!");

      // if we've had a fatal error,  we display a red screen.  Error
      // reporting for image loading appears to work poorly and many
      // error conditions (such as OutOfMemory) while loading never
      // notify us.

      if(fatal)
      {
         g.setColor(Color.red);
         g.fillRect(0,0,size().width,size().height);
         return;
      }

      // avoid null pointer errors

      if(frame == null)
         return;
      if(frame[FrameI - 1][FrameJ - 1] == null)
         return;
      if(status[FrameI - 1][FrameJ - 1] == 0)
         return;

      // draw background -- draw only parts that do not overlap
      // image to increase speed and eliminate flicker without
      // the (very heavy on some platforms,  especially MacOS)
      // speed penalty of double buffering.  (DB is cheap on Unix,
      // expensive on Windows,  and unacceptable slow on MacOS)
      // we ~still~ however,  use double buffering when !ready
      // so the progress bar doesn't flicker.  Performance isn't
      // so critical here,  we think

      Graphics g1 = g;

      if(!ready)
      {
         if(dBuff == null)
            dBuff = createImage(size().width,size().height);
         g1 = dBuff.getGraphics();
         g1.setColor(getBackground());
         g1.fillRect(0,0,size().width,size().height);
      }
      else
      {
         dBuff = null;         // throw it out when we're done with it
         if(marginX != 0)
            g1.clearRect(0,0,marginX,size().height);
         int imgWidth = frame[FrameI - 1][FrameJ - 1].getWidth(null);
         if(marginX + imgWidth < size().width)
            g1.clearRect(marginX + imgWidth,0,size().width - marginX - imgWidth,size().height);
         if(marginY != 0)
            g1.clearRect(0,0,size().width,marginY);
         int imgHeight = frame[FrameI - 1][FrameJ - 1].getHeight(null);
         if(marginY + imgHeight < size().width)
            g1.clearRect(0,marginY + imgHeight,size().width,size().height - marginY - imgHeight);
      }

      // draw image

      g1.drawImage(frame[FrameI - 1][FrameJ - 1],marginX,marginY,null);

      // if we're still loading paint progress bar,  using
      // double-buffering to eliminate flicker.

      if(!ready)
      {
         progbar.paint(g1);
         g.drawImage(dBuff,0,0,null);
      }

      paintHotspots(g);
   }

   /**
    *
    * This method loops through the hotspots and gets the ones that are
    * visible to paint themselves.  Called by <CODE>paint()</CODE>
    *
    * @param g graphics context from the AWT
    *
    **/


   public void paintHotspots(Graphics g)
   {
      if(ready && !dragged && isMouseIn)
         for(Enumeration e = hotspots.elements();e.hasMoreElements();)
         {
            fvr2Hotspot hs = (fvr2Hotspot)e.nextElement();
            if(hs.isInFrame(FrameI,FrameJ))
            {
               hs.paint(g);
            }
         }
   }

   /**
    *
    * When this method is called,  the applet does a synchronous Repaint;  it
    * repaints itself immediately and does not return until the repaint is
    * completed,  in contrast to the behavior of <CODE>repaint().</CODE>
    * <CODE>repaint()</CODE> is best used in event handlers (unless you only
    * need to make small changes in the graphics,  such as when you're
    * drawing rubber band lines) while <CODE>synchronousRepaint</CODE> is
    * best used in a thread which is doing other things.  (Such as
    * loading images or computation)
    *
    */

   public void synchronousRepaint()
   {
      paint(getGraphics());
   }

   /**
    * This bit of code can set the type of cursour in Netscape.  It
    * doesn't work in appletviewer,  but the try/catch prevents this
    * from crashing the applet.
    *
    * @param type cursor type,  defined in <CODE>java.awt.Frame</CODE>
    *
    */

   void setCursor(int type)
   {
      try
      {
         ((Frame)getParent()).setCursor(type);
      }
      catch(Exception e)
      {
      }
   }



   /**
    * This is a control binding for javascript:  it's job is to make new
    * Hotspot objects and interconnect them with the applet
    *
    * @param name, Hotspot name
    * @param frame_i, Hotspot vertical frame #
    * @param frame_j, Hotspot horizontal frame #
    * @param x Hotspot x coordinate
    * @param y Hotspot y coordinate
    * @param height Hotspot height
    * @param width Hotspot width
    *
    */

   public fvr2Hotspot newHotspot(String kind) throws ClassNotFoundException, InstantiationException, IllegalAccessException
   {
      Class hsClass = (Class)plugins.get(kind);
      if(hsClass == null)
      {
         fatal = true;
         System.out.println("Hotspot plugin " + kind + " not loaded");
         repaint();
         return null;
      }

      fvr2Hotspot spot = (fvr2Hotspot)hsClass.newInstance();
      spot.attachApplet(this);
      hotspots.addElement(spot);
      return spot;
   }

   // set the status that's displayed when the mouse moves over the
   // applet.
 
   void setDefaultStatus(String s)
   {
      currentStatus = s;
      showStatus(s);
   }

   /**
    *
    * Search all the Hotspots for the first (topmost) one that is at the
    * coordinates "frame_i", "frame_j", "x" and "y"
    *
    * @param frame_i vertical frame index
    * @param frame_j horizontal frame index 
    * @param x x coordinate of mouse
    * @param y y coordinate of mouse
    * @return the Hotspot if there is one,  return null if there isn't
    *
    */

   fvr2Hotspot searchHotspots(int frame_i, int frame_j, int x, int y)
   {
      // java.util.Vector uses synchronize which improves
      // reliability but practically slows things down.  Bracketing
      // all of our accesses to hotspot within a synchronized
      // statement can reduce synchronization overhead if a
      // JVM is smart enough to take advantage of it.
      synchronized(hotspots)
      {
         for(int i = 0;i < hotspots.size();i++)
         {
            fvr2Hotspot hs = (fvr2Hotspot)hotspots.elementAt(i);
            if(hs.isMouseOver(FrameI,FrameJ,x,y))
               return hs;
         }
      }

      return null;

      // return null if we're not over one
   }


}

