/**
 File:      NewsBannerApplet.java
 Author:    Angus McIntyre <angus@pobox.com>
 Date:      26.11.96
 Updated:   27.11.96 1:25 pm
 
 Simple Java applet to display a set of strings as a banner. The
 banner is refreshed at intervals.

    HISTORY
    -------
    
    26.11.96   SLAM  First version implemented
    
    27.11.96   SLAM  Added multi-directional scrolling. Did so at
                     first in the most baroque possible manner, then,
                     trying to work around a quirk with clipRect on
                     certain JVM's, suddenly realised that it could
                     be done much more cleanly and simply. I'm not
                     even going to tell you how I did it before - it's
                     too embarassing.
    
    TO-DO
    -----
    
    Write some decent file-parsing routines which will (a) handle
    commented lines and (b) accept line-initial '.' characters. The
    StreamTokenizer appears to munge these.
    
    Write a tweak to allow strings to be shown in random order.
    
    Write a version of this that uses images rather than strings.
    
    Write a version that allows multi-part banners along the lines
    of the applet used at 'http://www.cnn.com'.
          
    LEGAL
    -----
    
    This software is free. It can be used and modified in any way you 
    choose, but it may not be sold, either separately or as part of a 
    collection without explicit prior permission from the author. The 
    author assumes no liability for any loss, damage or mental or 
    physical trauma you may incur through use of or inability to use 
    this software. This disclaimer must appear on any modified or 
    unmodified version of the software in which the name of the author
    also appears.
    
**/

/* ----------------------------------------------------------------------
 *                              IMPORTS
 * ---------------------------------------------------------------------- */

import java.awt.*;
import java.net.*;
import java.io.*;
import ExtendedApplet;

/* ----------------------------------------------------------------------
 *                              CLASSES
 * ---------------------------------------------------------------------- */

// Class:   NewsBannerApplet
//
// An applet to display strings one after the other in a rectangular
// area on the screen.

public class NewsBannerApplet extends ExtendedApplet implements Runnable
{

   // Defaults
   
   static   final Font        default_font = new Font("TimesRoman",Font.BOLD,14);
   static   final Color       default_background_color = Color.blue;
   static   final Color       default_text_color = Color.white;
   static   final Color       default_frame_color = Color.black;
   static   final int         default_delay = 5;
   
   // Parameters
   
   static   final int         maximum_strings = 20;
   static   final int         frame_margin = 2;
   static   final int         text_h_margin = 2;
   static   final int         scroll_time = 500;
   
   // Constants
   
   static   final int         align_left = 0;
   static   final int         align_center = 1;
   static   final int         align_right = 2;
   
   static   final int         scroll_direction_up = 0;
   static   final int         scroll_direction_down = 1;
   static   final int         scroll_direction_left = 2;
   static   final int         scroll_direction_right = 3;
   static   final int         scroll_direction_random = 4;

   // Instance variables
   
   private     Thread      timer_thread = null;
   protected   String[]    strings;
   protected   Color       text_color;
   protected   Color       background_color;
   protected   Color       frame_color;
   protected   Font        font;
   protected   int         delay;
   protected   int         number_of_strings;
   protected   int         frame = 0;
   protected   Image       offscreen;
   protected   int         alignment = align_left;
   protected   int         scroll_direction = scroll_direction_up;
   protected   String      string_file_path;
   
   // Method:  getAppletInfo()
   //
   // Return information about the applet. 

   public String getAppletInfo() {
      return "NewsBannerApplet 1.0 by Angus McIntyre <angus@pobox.com> / <http://www.raingod.com/>";
   }
   
   // Method: getParameterInfo()
   //
   // Return information about the applet's parameter options. Read this
   // if you want to see what parameters this applet supports.
    
   public String[][] getParameterInfo() {
      String[][] info = {
         {"string-file",         "String",   "file containing strings to show (required)"},
         {"font",                "String",   "specification of font used for display"},
         {"text-alignment",      "String",   "alignment of text - left, right, center"},
         {"scroll-direction",    "String",   "scroll direction - left, right, up, down, random"},
         {"text-color",          "Color",    "color used for display"},
         {"frame-color",         "Color",    "color used for frame of clock"},
         {"background-color",    "Color",    "color used for display background"},
         {"delay",               "int",      "time between each frame (seconds)"}
      };
      return info;
   }

   // Method:  init()
   //
   // Initialize the applet.
    
   public void init() {
      super.init();
      
      // Read in all the parameters
      
      font = parseFontParameter("font",default_font);
      text_color = parseColorParameter("text-color",default_text_color);
      frame_color = parseColorParameter("frame-color",default_frame_color);
      background_color = parseColorParameter("background-color",default_background_color);
      delay = parseIntegerParameter("delay",default_delay);
      string_file_path = this.getParameter("string-file");
        
      // Read the text alignment. Alignment defaults to left, but can be
      // set to center or right by passing the appropriate parameter.
        
      String align_text = this.getParameter("text-alignment");
      if (align_text != null)
         if (align_text.equalsIgnoreCase("right"))
            alignment = align_right;
         else if (align_text.equalsIgnoreCase("center"))
            alignment = align_center;
            
      // Read the scroll direction. There *must* be a better way to write
      // this expression - I hate nested 'if's.
      
      String scroll_text = this.getParameter("scroll-direction");
      if (scroll_text != null)
         if (scroll_text.equalsIgnoreCase("up"))
            scroll_direction = scroll_direction_up;
         else if (scroll_text.equalsIgnoreCase("down"))
            scroll_direction = scroll_direction_down;
              else if (scroll_text.equalsIgnoreCase("left"))
                  scroll_direction = scroll_direction_left;
                   else if (scroll_text.equalsIgnoreCase("right"))
                        scroll_direction = scroll_direction_right;
                        else if (scroll_text.equalsIgnoreCase("random"))
                             scroll_direction = scroll_direction_random;

      // Create an array to hold all the strings we need, up to the
      // permitted maximum, and then fetch them from the indicated file.
                    
      strings = new String[maximum_strings];
      fetchStringFile();
   }
   
   // Method:  start()
   //
   // The animation control thread is started by this method. If it's
   // null, a new thread is created and launched.
    
   public void start() {
      if (timer_thread == null) {
         timer_thread = new Thread(this);
         timer_thread.start();
      }
   }
    
   // Method:  stop()
   //
   // Stop the thread. If the user moves off to another window, this
   // method will get called to stop animation.
    
   public void stop() {
      if ((timer_thread != null) &&
          (timer_thread.isAlive()))
           timer_thread.stop();
         timer_thread = null;
   }
    
    // Method:  run()
    //
    // This method causes the applet to sleep repeatedly for a fixed
    // length of time, waking at intervals to update the display.
    // It will continue indefinitely.
    
   public void run() {
      while(true) {
      try { Thread.sleep(delay * 1000); }
         catch (InterruptedException e) { }
         updateBanner(this.getGraphics());
        }
    }

   // Method:  paint(Graphics)
   //
   // Called by the system to paint the applet. This in turn calls
   // the 'drawBanner()' method.
    
   public void paint(Graphics g) {
      drawBanner(g);
   }
   
   // Method:  updateBanner(Graphics)
   //
   // Update the banner. Increment the frame counter by one and, if
   // we reach the limit, wrap around. Call 'mergeBanner' to scroll
   // the new banner into place.
   
   public void updateBanner(Graphics g) {
      frame++;
      if (frame == number_of_strings)
         frame = 0;
      mergeBanner(g);
   }

   // Method:  mergeBanner(Graphics)
   //
   // Merge a banner into place. This writes the next string into an
   // offscreen buffer and then scrolls it into place. The direction
   // of the scroll is controlled by a parameter, and can be left,
   // right, up, down or random.
   
   public void mergeBanner (Graphics g) {
      int image_x,image_y, image_x_inc, image_y_inc;
      int lines_to_scroll, line, scroll_delay;
      Rectangle bounds = this.bounds();
      
      // Set up the clip rect so that we don't draw over the frame.
      
      g.clipRect(bounds.x + frame_margin,
                 bounds.y + frame_margin,
                 bounds.width - (2 * frame_margin),
                 bounds.height - (2 * frame_margin));
      
      // Draw the new image into an offscreen buffer
      
      this.drawOffscreen();
      
      // Find out which direction we need to scroll in. If the
      // parameter setting was "random", then pick a direction
      // at random.
      
      int scroll_dir = scroll_direction;
      if (scroll_dir == scroll_direction_random)
         scroll_dir = (int) (Math.random() * 4);
      
      // The big switch. This sets up the parameters for the scroll.
      // Note the use of 'default' cases throughout, in order to get
      // the compiler to shut up about uninitialized variables.
      
      switch (scroll_dir) {
      
         // Horizontal scroll
         
         case scroll_direction_left:
         case scroll_direction_right:
            image_y = frame_margin;
            image_y_inc = 0;
            lines_to_scroll = bounds.width - (frame_margin * 2);
            switch (scroll_dir) {
               case scroll_direction_right:
                  image_x_inc = 1;
                  image_x = (frame_margin - lines_to_scroll);
                  break;
               case scroll_direction_left:
               default:
                  image_x_inc = -1;
                  image_x = (bounds.width - frame_margin) - 1;
                  break;
            }
            break;
         
         // Vertical scroll
         
         case scroll_direction_up:
         case scroll_direction_down:
         default:
            image_x = frame_margin;
            image_x_inc = 0;
            lines_to_scroll = bounds.height - (frame_margin * 2);
            switch (scroll_dir) {
               case scroll_direction_down:
                  image_y_inc = 1;
                  image_y = (frame_margin - lines_to_scroll);
                  break;
               case scroll_direction_up:
               default:
                  image_y_inc = -1;
                  image_y = (bounds.height - frame_margin) - 1;
                  break;
            }
            break;
      }
      
      // Having set everything up, scroll the sucker. Basically,
      // just move the top-left corner of the image and draw it
      // repeatedly, with a timed pause so that it doesn't go to
      // fast. On slow machines, the overhead of executing the
      // timed pause may be such that the delay is longer than the
      // time alotted to pause in - in which case scrolling will
      // probably be jerky.
         
      scroll_delay = scroll_time / lines_to_scroll;
      for(line=0;line<lines_to_scroll;line++) {
         g.drawImage(offscreen,image_x,image_y,this);
         image_x += image_x_inc;
         image_y += image_y_inc;
         pauseScroll(scroll_delay);
      }

      // Restore the usual clipping regime
      
      g.clipRect(bounds.x,bounds.y,bounds.width,bounds.height);
   }
   
   // Method:  pauseScroll(long);
   //
   // Pause the scrolling for a specified number of milliseconds. This
   // is a little ugly because it will probably lock up the processor
   // while it waits - should we be using a thread? - but the total period
   // involved isn't that long.
   
   public void pauseScroll(long period) {
      long delay_start = System.currentTimeMillis();
      while(System.currentTimeMillis() < delay_start + period);
   }

   // Method:  drawBanner(Graphics)
   //
   // Draw the banner.
    
   public void drawBanner (Graphics g) {
      Rectangle bounds = this.bounds();
      
      g.setColor(frame_color);
      g.drawRect(0,0,bounds.width-1,bounds.height-1);
      this.drawOffscreen();
      g.drawImage(offscreen,frame_margin,frame_margin,this);
   }

   // Draw the current string into the offscreen buffer
   
   public void drawOffscreen() {
   
      // Measure the size of the area we have available for drawing.
      
      Dimension image_size = this.size();
      image_size.height -= (2 * frame_margin);
      image_size.width -= (2 * frame_margin);

      // Create the offscreen buffer if we need to.
      
      if (offscreen == null) {
         offscreen = this.createImage(image_size.width,image_size.height);
      }
         
      // Work out how to position the string. We need to get some 
      // information about the font and then calculate the X and Y
      // offsets at which the string will be placed. The string is
      // centered vertically in the ribbon, while horizontal placement
      // is determined based on the specified text alignment.
      
      Graphics g = offscreen.getGraphics();
      FontMetrics metrics = g.getFontMetrics(font);

      int   y_offset = ((image_size.height - metrics.getHeight()) / 2) +
                         metrics.getAscent() - 1;
      int   x_offset = image_size.width - 
                       (metrics.stringWidth(strings[frame]) + 
                        (2 * text_h_margin));
      switch(alignment) {
         case align_left:
            x_offset = text_h_margin; 
            break;
         case align_right:
            x_offset += text_h_margin; 
            break;
         case align_center:
            x_offset = (x_offset/2) + text_h_margin; 
            break;
      }
      
      // Fill the area with color and then draw the text on it.
      
      g.setColor(background_color);
      g.fillRect(0,0,image_size.width,image_size.height);
      g.setColor(text_color);
      g.setFont(font);
      g.drawString(strings[frame],x_offset,y_offset);
   }
   
   // Method: fetchStringFile
   //
   // Fetch a file of strings
   
   public void fetchStringFile() {
      try {
      
         // Get a URL to the file containing the strings to be shown. This
         // is assumed to be relative to the document in which it appears.

         URL      document_url = this.getDocumentBase();
         String   document_path = document_url.getFile();
         String   separator = System.getProperty("file.separator");
         String   fetch_path = 
                     document_path.substring(0,
                                             document_path.lastIndexOf(separator))
                     + separator
                     + string_file_path;
         URL      file_url = new URL(document_url.getProtocol(),
                                     document_url.getHost(),
                                     document_url.getPort(),
                                     fetch_path);

         // Because I'm lazy, I'm going to use StreamTokenizer to split
         // the file up into a set of lines. Note that we don't use the
         // 'getContent' method to slurp in the file, which might be the
         // simplest approach; 'getContent' appears to be broken for
         // text files and throws up content handler errors. Conveniently,
         // however, we can use 'openStream' on a URL, and then pass that
         // stream as an initialization argument to the tokenizer, which
         // is almost as easy, if not easier.
         //
         // 27.11.96 SLAM  Needless to say, there's a catch. Something
         //                about this doesn't seem to like line-initial
         //                '.' and will eat them up. I need to fix this.
         
         StreamTokenizer tokenizer = 
            new StreamTokenizer(file_url.openStream());
         int strings_read = 0;
         int token_read;
         
         // Any ASCII characters below CR are taken as whitespace,
         // because we want to read whole lines at a time. Hack the
         // tokenizer so that it splits on linebreaks rather than
         // on spaces.
         
         tokenizer.whitespaceChars(0,13);
         tokenizer.wordChars(32,126);
         
         // Loop through the file, grabbing the tokens (i.e. lines) one 
         // at a time. If the tokenizer reports having read a word (i.e.
         // a line) and the line is not empty, then we can add it to the
         // list of strings to display. Stop when we hit the end of file
         // or we've read more strings than we have space allocated for.
         
         do {
            token_read = tokenizer.nextToken();
            if ((token_read == StreamTokenizer.TT_WORD) &&
                (tokenizer.sval.length() > 0)) {
               strings[strings_read++] = tokenizer.sval;
            }
         }
         while ((token_read != StreamTokenizer.TT_EOF) && 
                (strings_read < maximum_strings));
         
         // Note how many strings we read.
         
         number_of_strings = strings_read;
      }
      
      // Catch exceptions. We report the error on the console, and
      // set up a single default string to let the user know that
      // it didn't work.
      
      catch (IOException e) {
            System.err.println("Strings couldn't be loaded because " 
                               + " the error " + e + " occurred");
            strings[0] = "No strings loaded";
            number_of_strings = 1;
      }
   }
}
