/** 
  * HF Receiver Controller Applet 1.0.0
  * @author Robert J Morton <robmorton@clara.net>
  * @version 25 November 2001
  * @copyright Robert J Morton (all rights reserved) */

/* This applet conforms to API 1.1  
   It does the following:

   INITIALISATION SEQUENCE

   1. Loads a file full of the names of lots of HF broadcasting services and stations
      from a file located on the server, and places them into a stations Choice list.

   2. Selects the first one in the list and loads the list of frequencies on which
      this station broadcasts into a frequency Choice list.

   3. Sets up the standard set of squelch levels S0 to S9+ in a squelch Choice list
      and selects S5 as the default.

   4. Sends the default frequency and squelch level back to the server to be POSTed
      to a receiver whose command interface is connected to the server PC's AUX port.

   5. Awaits user input.


   USER-INITIATED ACTIONS

   1. User can select another station or broadcasting service from the list. 
      The new station's list of frequencies is then loaded from the server 
      into the frequencies Choice list. New station's first frequency is then
      sent automatically to the receiver as a re-tuning command.

   2. User can select a frequency from the frequency Choice list. This frequency
      is then sent automatically to the receiver as a re-tuning command.

   3. User can set the receiver scanning through the current station's frequencies
      at the rate of one per second. It stops scanning when the receiver detects
      a signal of a strength equal to or greater than the currently selected
      squelch level. The user can press the scan Start button again to resume 
      scanning round the frequencies list.

   4. User can step up and down the frequency list manually by pressing the Higher
      and Lower buttons. Each new frequency is sent as a tuning command to the 
      receiver.

   5. User can enter a word or phrase and search for it within all station names
      in the list of stations or braodcasting services. If found, the the first 
      station's name is selected and its broadcasting frequencies loaded. Pressing
      the Find/Next button again proceeds with the search onwards down the list
      of stations.

   Appropriate code has to be inserted into this applet where indicated to construct
   and POST receiver command messages of the type required for your receiver and its
   control interface software. Your server needs to know where to POST these messages.

*/


import java.awt.*;
import java.io.*;
import java.applet.*;
import java.awt.event.*;                  //for the new-fangled 1.1 event handling
import java.net.*;                        //for downloading data from the remote server


public class hfbrx extends Applet implements Runnable {
   AppletContext ac;                      //get details of HTML document this applet is running in
   String cb;                             //code base URL - where this applet's class file came from
   Choice stations, frequencies, squelch; //stations and frequencies choice lists
   Label Stn, Frq, Squ, ScanLab, NextLab, PrevLab, FindLab;
   Label TextLab1, TextLab2, TextLab3, TextLab4, TextLab5;
   Button ScanBut;                        //button to start/stop frequency scanning for current station
   Button NextBut, PrevBut, FindBut;      //button to go to the next frequency in the list
   BufferedReader r;                      //for reading in station and frequency lists from files
   long si = 1000;                        //frequency scan interval
   long T;                                //time at which to step to the next frequency
   Thread TH;                             //declare a thread reference variable
   int MaxFreq = 0;                       //max lisat number for frequencies this station transmits on
   int FreqNum = 0;                       //list number of the current listening frequency
   boolean ScanFlag = false;              //indicates whether or not automatic scanning in progress
   int lp = 0, LP = 1;                    //indicates current and previous download phases
   String loadFile = "stations.txt";      //name of the station names file
   int loadType = 0;                      //0 = Station names file, 1 = Frequencies file
   int L = 0;                             //length of the remote item being loaded
   int l = 0;                             //number of bytes of the above successfully downloaded
   InputStream I;                         //input stream for downloading index or current HTML file
   byte B[];                              //gigantic byte array to hold the downloaded index data
   boolean StationNamesLoaded = false;    //station names not yet loaded
   boolean FrequenciesLoaded = false;     //selected station's frequencies not yet loaded
   boolean PanelNotCompleted = true;      //to stop panel flicker as items are added to station names
   boolean NewFreq = false;               //new frequency must be displayed when true
   boolean NewStn = false;                //new station must be displayed when true
   boolean NewSquelch = false;            //new squelch level must be displayed when true
   String E;                              //for Exception during downloading + method where it occurred
   TextField FindStn;                     //keyword entry field on the applet panel 
   String SearchString = "";              //search string for finding a station name
   int SearchIndex = 0;                   //list number of station name we have searched up to

   public void init() {
      ac = getAppletContext();                      //get details of HTML document this applet is running in
      cb = getCodeBase().toString();                //URL (less file name) from where this applet came
      if(!cb.endsWith("/")) {                       //workaround re Hotjava re Microsoft intranet machine names
         int x = cb.lastIndexOf('/');               //where getCodeBase() wrongly seems to return the
         if(x != -1) cb = cb.substring(0, x + 1);   //document base (cira Nov 1999)
      }
      setLayout(null);      //to allow the control panels to be laid out manually

      Stn = new Label("Station or Broadcasting Service");
      add(Stn); Stn.setBounds(100, 6, 200, 15);
      stations = new Choice();
      add(stations); 
      stations.setBounds(100, 26, 200, 15);
      chlisten ws = new chlisten(chlisten.STATION, this); 
      stations.addItemListener(ws);

      Frq = new Label("Freq (kHz)");
      add(Frq); Frq.setBounds(315, 6, 60, 15);
      frequencies = new Choice();
      add(frequencies); 
      frequencies.setBounds(315, 26, 60, 15);
      chlisten bk = new chlisten(chlisten.FREQUENCY, this); 
      frequencies.addItemListener(bk);

      Squ = new Label("Squelch");
      add(Squ); 
      Squ.setBounds(390, 6, 50, 15);
      squelch = new Choice();
      add(squelch);
      squelch.setBounds(390, 26, 50, 15);
      chlisten sq = new chlisten(chlisten.SQUELCH, this); 
      squelch.addItemListener(sq);
      squelch.addItem("S0"); squelch.addItem("S1");
      squelch.addItem("S2"); squelch.addItem("S3");
      squelch.addItem("S4"); squelch.addItem("S5");
      squelch.addItem("S6"); squelch.addItem("S7");
      squelch.addItem("S8"); squelch.addItem("S9");
      squelch.addItem("S9+");
      squelch.select(5); NewSquelch = true;   //set default squench level to 'S5' signal strength

      FindLab = new Label("Station Search");  //set up the label for the station search field
      add(FindLab); 
      FindLab.setBounds(10, 6, 80, 15);
      FindStn = new TextField(); 
      add(FindStn);
      FindStn.setBounds(10, 26, 80, 20); 
      btlisten sf = new btlisten(btlisten.FINDSTN, this); 
      FindStn.addActionListener(sf);  //listen for c/r from text field

      lp = 1;                         //start the station names loading process
   }


   public void paint(Graphics g) {
      showStn(g);                    //re-display station name when an overlaid window removed
      showFreq(g);                   //re-display frequency when an overlaid window removed
      showSquelch(g);                //re-display the squelch setting
   }


   public void update(Graphics g) {
      if(PanelNotCompleted) {

      ScanLab = new Label("Automatic scanning of this station's frequency list.");
      add(ScanLab); 
      ScanLab.setBounds(155, 60, 290, 20);

      ScanBut = new Button("Start");
      add(ScanBut); 
      ScanBut.setBounds(100, 60, 50, 20);
      btlisten sb = new btlisten(btlisten.SCANBUT, this); 
      ScanBut.addActionListener(sb);

      NextLab = new Label("Listen on next higher frequency in this station's list.");
      add(NextLab); 
      NextLab.setBounds(155, 85, 290, 20);

      NextBut = new Button("Higher");
      add(NextBut); 
      NextBut.setBounds(100, 85, 50, 20);
      btlisten nb = new btlisten(btlisten.NEXTBUT, this); 
      NextBut.addActionListener(nb);

      PrevLab = new Label("Listen on next lower frequency in this station's list.");
      add(PrevLab); 
      PrevLab.setBounds(155, 110, 290, 20);

      PrevBut = new Button("Lower");
      add(PrevBut); 
      PrevBut.setBounds(100, 110, 50, 20);
      btlisten pb = new btlisten(btlisten.PREVBUT, this); 
      PrevBut.addActionListener(pb);

      FindBut = new Button("Find/Next");
      add(FindBut); 
      FindBut.setBounds(10, 135, 80, 20);
      btlisten fb = new btlisten(btlisten.FINDBUT, this); 
      FindBut.addActionListener(fb);

      TextLab1 = new Label("Find station(s)"); add(TextLab1); TextLab1.setBounds(10, 54, 80, 15);
      TextLab2 = new Label("whose names");     add(TextLab2); TextLab2.setBounds(10, 68, 80, 15);
      TextLab3 = new Label("contain the");     add(TextLab3); TextLab3.setBounds(10, 82, 80, 15);
      TextLab4 = new Label("entered word");    add(TextLab4); TextLab4.setBounds(10, 96, 80, 15);
      TextLab5 = new Label("or phrase.");      add(TextLab5); TextLab5.setBounds(10,110, 80, 15);

      PanelNotCompleted = false;
      }

      if(NewFreq) {            //if a new frequency has just been selected
         showFreq(g);          //re-display the frequency field
         NewFreq = false;      //note that this has been done
      }
      if(NewStn) {             //if a new station has just been selected
         showStn(g);           //re-display the station name
         NewStn = false;       //note that this has been done
      }
      if(NewSquelch) {         //if a new squelch level has just been selected
         showSquelch(g);       //re-display the squelch setting
         NewSquelch = false;   //note that this has been done
      }
   }


   void showStn(Graphics g) {
      if(StationNamesLoaded) {          //provided staion names download is complete
         g.setColor(getBackground());
         g.fillRect(100, 135, 200, 15);
         g.setColor(Color.black);
         g.drawString("Station: " + stations.getSelectedItem(), 100, 150);
      }
   }


   void showFreq(Graphics g) {
      if(FrequenciesLoaded) {           //provided frequencies download is complete
         g.setColor(getBackground());
         g.fillRect(315, 135, 60, 15);
         g.setColor(Color.black);
         g.drawString(frequencies.getSelectedItem() + " kHz", 315, 150);
      }
   }


   void showSquelch(Graphics g) {
      g.setColor(getBackground());
      g.fillRect(390, 135, 50, 15);
      g.setColor(Color.black);
      g.drawString(squelch.getSelectedItem(), 390, 150);
   }


   void selectStation() {                                  //broadcast selected event handler
      if(StationNamesLoaded) {
         NewStn = true;                                    //trigger display of new station name
         NewFreq = true;                                   //new frequency must be displayed
         frequencies.removeAll();                          //clear the frequencies list
         int x = stations.getSelectedIndex();              //number of the selected station within the choice list
         loadFile = "" + x;                                //form string version of station number
         if(x < 10)                                        //if it is less than 10
            loadFile = "00" + loadFile;                    //pad it out with 2 leading zeros
         else if(x < 100)                                  //else if it is less than 100
            loadFile = "0" + loadFile;                     //pad it with just 1 leading zero
         loadFile = "freqs" + loadFile + ".txt";           //form the name of the appropriate frequencies file
         loadType = 1;                                     //state type of file being loaded
         FrequenciesLoaded = false;                        //selected station's frequencies not yet loaded
         lp = 1;                                           //start the loading process
      }
   }



   void selectFrequency() {                                //frequency selection event handler
      FreqNum = frequencies.getSelectedIndex();            //note the list number of the frequency just selected
      NewFreq = true;                                      //trigger display of new selected frequency
      repaint();                                           //re-display new frequency

      /*    Here is where you construct and send an HTTP 'POST' message and send it 
            back to your HTTPD server, telling it to send a 'tune to this frequency' 
            command to your physical receiver. Your HTTPD server will have to know 
            where to POST this type of message on to. I wrote my web server in Java, 
            so I simply added a class file to construct and send receiver commands 
            via my PC's AUX port.  */
   }


   void selectSquelch() {                                  //SQUELCH SELECTION EVENT HANDLER
      String SquelchLevel = squelch.getSelectedItem();     //get the selected squelch level S0, S1, S2, ...
      NewSquelch = true;                                   //trigger display of new squelch level
      repaint();

      /*    Here is where you construct and send an HTTP 'POST' message and send it 
            back to your HTTPD server, telling it to send a 'set this squelch level' 
            command to your physical receiver. Your HTTPD server will have to know 
            where to POST this type of message on to. I wrote my web server in Java, 
            so I simply added a class file to construct and send receiver commands 
            via my PC's AUX port.  */
   }


   void pressScanBut() {                  //WHEN THE SCAN BUTTON IS PRESSED
      if(ScanFlag) {                      //If is scanning in progress
         ScanFlag = false;                //kill scanning and re-display button labels
         FindBut.setLabel("Find/Next");
         NextBut.setLabel("Higher");
         PrevBut.setLabel("Lower");
         ScanBut.setLabel("Start");
      } else {                            //otherwise
         ScanFlag = true;                 //start scanning and kill button labels
         FindBut.setLabel("");
         NextBut.setLabel("");
         PrevBut.setLabel("");
         ScanBut.setLabel("Stop");
      }
   }


   void pressNextBut() {
      if(!ScanFlag) nextFreq();                  //do nothing if automatic scanning is on
   }


   void pressPrevBut() {
      if(!ScanFlag) {                            //do nothing if automatic scanning is on
         if(--FreqNum < 0) FreqNum = MaxFreq;    //decrement frequency number, loop back to max if overshot
         frequencies.select(FreqNum);            //select the new listening frequency
         NewFreq = true;                         //new frequency must be displayed
         repaint();                              //sets up a call to update()
      }
   }


   void nextFreq() {
      if(++FreqNum > MaxFreq) FreqNum = 0;       //increment frequency number, loop back to zero if overshot
      frequencies.select(FreqNum);               //select the new listening frequency
      NewFreq = true;                            //new frequency must be displayed
      repaint();                                 //sets up a call to update()
   }


   void findStation() {                             //x is the item number at which to start/resume search
      if(!ScanFlag) {                               //get search string currently in the text field
         String s = FindStn.getText().trim().toLowerCase();
         if(!SearchString.equals(s)) {              //if different from last time's search string
            SearchString = s;                       //update search string
            SearchIndex = 0;                        //reset search index to start of list
         }
         int I = stations.getItemCount();           //get the number of station names in the list
         for(int i = SearchIndex; i < I; i++) {     //for each station name in the list
            s = stations.getItem(i).toLowerCase();  //get the next station name
            if(s.indexOf(SearchString) != -1) {     //if search string found in this station name
               stations.select(i);                  //select the station name
               SearchIndex = i + 1;                 //mark where we have searched up to
               showStatus("");                      //clear possible 'not found' message from last time
               selectStation();                     //see above
               return;
            }
         }
         showStatus("Search string not found in any station name.");
      }
   }


   void fileConnect() {                            //CONNECT TO THE APPROPRIATE DATA FILE ON THE SERVER
      try {                                        //set to capture any exceptions locally
         URL url = new URL(cb + loadFile);         //form the url of the station data file
         URLConnection u = url.openConnection();   //open a connection to the remote file
         L = u.getContentLength();                 //length of the remote file (bytes)
         l = 0;                                    //number of bytes so far successfully downloaded
         I = u.getInputStream();                   //create an input stream object to access the file
         B = new byte[L];                          //create the gigantic buffer for the data
         lp = 2;                                   //advance to the index loading phase
      } catch(Exception e) {                       //if any exception at all occurs
         lp = 0;                                   //set unrecoverable error status
         E = "fileConnect() " + e;                 //note the exception and where it occurred
         showStatus(E);                            //display error message on browser's status line
      } 
   }


   void fileLoad() {                               //DOWNLOAD THE APPROPRIATE DATA FILE
      int k;                                       //current byte being read()
      try {                                        //if read() hasn't hit the current end of input stream
         while(l < L && (k = I.read()) != -1)      //and the entire file has not yet been downloaded
            B[l++] = (byte)k;                      //add its new byte to the array big byte array
         if(l >= L) {                              //if the whole of the index has now been downloaded
            I.close();                             //close the URL Connection's input stream
            lp = 3;                                //indicate file loading completed successfully
            unravel();
         }
      } catch(Exception e) { 
         lp = 0;                                   //set error condition
         E = "fileLoad() " + e + L + " " + l;      //note the exception and where it occurred
         showStatus(E);                            //display error message on browser's status line
      }
   }


   void unravel() {
      try {
         r = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(B)));
         String s;
         while((s = r.readLine()) != null) {       //read in the next line of text
            if(loadType == 0)
               stations.addItem(s.trim());         //add station name to stations choice list
            else
               frequencies.addItem(s.trim());      //add frequency to current station's frequencies list
         }
         r.close();                                //close the byte array reader
      } catch(Exception e) {
      }
      B = null;                                    //garbage the byte array to release memory
      if(loadType == 0) {
         StationNamesLoaded = true;
         selectStation();                          //pre-select the default station name
      } else {
         FrequenciesLoaded = true;
         MaxFreq = frequencies.getItemCount() - 1; //get total number of frequencies for this station
         FreqNum = 0;                              //reset to first frequency in the list
         repaint();
      }
   }


   public void run() {                                     //RUN THE AUXILIARY THREAD
      while(TH.isAlive()) {                                //while this thread exists
         if(ScanFlag && FrequenciesLoaded) nextFreq();
         long s = T - System.currentTimeMillis();          //get time remaining in this scan interval
         if (s < 5) s = 5;                                 //in case host PC is too slow
         try {
            Thread.currentThread().sleep(s);               //sleep for remaining time
         } catch (InterruptedException e) {                //allow browser events to break the thread
         }                                                 //happens if you return to applet's HTML page
         T = System.currentTimeMillis() + si;              //set finish time of next time frame

         switch(lp) {                                      //THE 2 DOWNLOADING PHASES
            case 1: fileConnect(); break;                  //connect to index resource on server
            case 2: fileLoad(); break;                     //manage the downloading of its content
         }
         if(lp != LP) {                                    //if loading of station data has finished
            LP = lp;                                       //latch the current load state
         }
      }
   }


   public void start() { TH = new Thread(this); TH.start(); }   //Start program thread
   public void  stop() { TH.stop(); }                           //Stop program thread

}







class chlisten implements ItemListener {             //LISTENS FOR EVENTS FROM THE CHOICE MENU SELECTORS
   static final int STATION = 0;                           //an event from the 'Stations' Choice Menu
   static final int FREQUENCY = 1;                         //an event from the 'Frequency' Choice Menu
   static final int SQUELCH = 2;                           //an event from the 'Squelch' Choice Menu
   int id;                                                 //one of the above events
   hfbrx ap;                                               //the application that called: always the above applet!

   public chlisten(int id, hfbrx ap) {               //constructor for a new Choice selection event
      this.id = id;                                        //set the id number pertaining to this instance of 'chlisten'
      this.ap = ap;                                        //set the reference to the instance of the 'hfbrx' applet
   }                                                       //from which it came. (there will only be one instance anyway).

   public void itemStateChanged(ItemEvent e) {             //a Choice selection event has occurred from checkbox 'id'
      switch(id) {
         case STATION: ap.selectStation(); break;          //Execute the method in the above applet which deals with the
         case FREQUENCY: ap.selectFrequency(); break;      //Choice selection that invoked this instance of 'chlisten'
         case SQUELCH: ap.selectSquelch(); break;
      }
   }
}






/* The following class creates instances of ActionListener. It is used by this applet to create 
   and service an action event for each of the control buttons. The underlying system calls the 
   actionPerformed() method below every time a button is pushed. The method is invoked upon the 
   instance of the following class corresponding to that particular button. The value of the 
   variable id at the time is therefore that which corresponds to the button concerned. This 
   causes the appropriate case statement to be executed, thus passing control to the appropriate 
   handling method above. */


class btlisten implements ActionListener {
   static final int SCANBUT = 0;                           //'scan button pressed' event
   static final int NEXTBUT = 1;                           //'next button pressed' event
   static final int PREVBUT = 2;                           //'prev button pressed' event
   static final int FINDSTN = 3;                           //'station search text field' event
   static final int FINDBUT = 4;                           //'station search button pressed' event
   int id;                                                 //one of the above
   hfbrx ap;                                               //the application that called: always the above applet!

   public btlisten(int id, hfbrx ap) {               //constructor for a new command
      this.id = id;
      this.ap = ap;
   }

   public void actionPerformed(ActionEvent e) {            //one of the buttons has been clicked
      switch(id) {                                         //id of this instance of ActionEvent
         case SCANBUT: ap.pressScanBut(); break;           //it was the 'scan' button
         case NEXTBUT: ap.pressNextBut(); break;           //it was the 'next' button
         case PREVBUT: ap.pressPrevBut(); break;           //it was the 'prev' button
         case FINDSTN: ap.findStation();  break;           //it was the 'find' fiels C/R
         case FINDBUT: ap.findStation();  break;           //it was the 'find' button
      }
   }
}







/*  Robert J Morton, the author of this program, 
    is a poor but Right Honourable Fellow of the
    Ancient and Noble Order of the Long-term Unemployed.

    Offers of work please to: robmorton@clara.net  */