Parent Web Page
/** 
  * Bandscope Receiver Applet 1.0.0
  * @author Robert J Morton <robmorton@clara.net>
  * @version 13 March 2002
  * @copyright Robert J Morton (all rights reserved) */

/* This applet conforms to API 1.1  
   Contains a method computePlots() that is specific to the way the
   AOR AR8600 Communications Receiver provides bandscope data via 
   its RS232 port. This applet gets its bandscope data via data files
   acquired via a web server from an RS232 port server with an AR8600
   specific data handling method - all written entirely in Java. */


import java.applet.*;             //all the gubbins to make the applet work
import java.awt.*;                //for graphics operations (GUI)
import java.awt.event.*;          //for the new-fangled 1.1 event handling
import java.net.*;                //for downloading data from the remote server
import java.io.*;                 //for stream handling for the above
import java.util.Date;            //for bandscope data file date formatting



public class bs extends Applet implements Runnable {
   AppletContext ac;     //get details of HTML document this applet is running in
   Date D;
   String cb;            //code base URL - where this applet's class file came from
   URL url;              //url of current HTML file 
   InputStream I;        //input stream for downloading index or current HTML file
   int L = 0;            //length of the remote item being loaded
   int l = 0;            //number of bytes of the above successfully downloaded
   byte B[];             //gigantic byte array to hold the downloaded signal data
   int w;                //pixel counter used on horizontal axis of graph
   int fh;               //font height - total height area needed for a line of text
   int fa;               //font ascent - distance from text base line to top of text area
   Thread TH;            //reference for a separate Internet Data Transfer thread
   int lp = 1, LP = 1;   //indicates current and previous download phases
   String E;             //for Exception during downloading + method where it occurred
   int W = 400;          //width of applet panel
   int H = 235;          //height of applet panel
   int GW = 200;         //graph width
   int GH = 96;          //graph height
   int GL = 80;          //Graph Left: x-co-ordinate of left side of graph area
   int GC = GL + 100;    //horizontal centre of graph
   int GR = GL + GW;     //Graph Right: x-co-ordinate of right side of graph area
   int GB = 150;         //Graph Bottom: panel y-co-ordinate of bottom of graph area 
   int GT = GB - GH;     //Graph Top: panel y-co-ordinate of top of graph area
   int VC = GB - GH / 2; //vertical centre of graph
   int hS = 0;           //graph x co-ordinate of highlighted signal
   int mS = 0;           //S  co-ordinate within graph area
   int rS = 0;           //receiver's signal strength
   int rF = 0;           //receiver frequency in integral half-kHz
   int mF = 0;           //frequency of mouse position on graph
   int redraw = 0;       //redraw task switch
   int SCH = 145;        //height of top of span choice box above bottom of graph
   int ADH = -20;        //height of age data message relative to bottom of graph
   long lm = 0;          //when current data file last modified (seconds since 1970/01/01/00:00Z)
   long T = 0;           //last time's System time
   String LM = "";       //date when data file last modified
   String AgeMsg = "";   //age message for current graph data
   String oldAge = "";   //to preserve the part of the age string that changes
   Button EntBut;        //button to enter the centre frequency
   Button PulBut;        //button to pull or re-pull bandscope data from server
   Button UpBut;         //for inching receiver frequency up
   Button DnBut;         //for inching receiver frequency down
   Button HldBut;        //for the Hold button - causes scope data to accumulate
   Label FrqLab;         //label for the centre-frequency entry field
   TextField FrqFld;     //centre frequency entry field
   Label SpanLab;        //Scope Frequency Span
   Choice Span;          //choices of bandscope frequency span
   Choice BandTypes;     //choices of radio band types
   Choice Bands;         //choices of radio band
   Choice Update;        //how often you update the trace
   int sBT = 0;          //choice index number of selected band type choice
   int sbt = 0;          //array index number of selected band type
   int sB = 0;           //choice index number of selected band choice
   int sb = 0;           //array index number of selected band
   String FR = "50";     //Frequency Resolution of graph in kHz per pixel.

   boolean dataOK = false;             //data file is corrupt or contains wrong samples for selected band span
   String dataMsg = "";                //'data unavailable' message
   int Plots[] = new int[GW];          //to hold the graphical signal strength plots

   String fn = "bs";                   //default name for the bandscope data file
   String mFs = "--------------MHz";   //mouse pointer frequency
   String rFs;                         //receiving frequency
   String PHmsg = "Peak Hold off";     //Peak Hold message
   boolean peakHold = false;           //Peak hold off/on status
   boolean CFvalid = false;            //centre frequency valid flag
   boolean onGraph = true;             //indicates when mouse is within graph area
   boolean receiving = false;          //receiver has been tuned to a frequency by mouse click

   Color bg1 = new Color(210, 210, 210);   //main background colour
   Color bg2 = new Color(210, 195, 195);   //graph background colour
   Color bg3 = new Color(180, 180, 180);   //graticule colour
   Color amber  = new Color(255, 255, 0);  //for signals being monitored

   int KP[] = {  100,   50,   20,   10,   5,   2,   1 };   //twice kilohertz per pixel for each selectable band span
   int MC[] = {12000, 6000, 3000, 1200, 600, 220, 120 };   //minimum centre frequency (in half-kHz)
   int FG[] = { 4000, 2000, 2000, 1000, 200, 200, 100 };   //half-kHz per annotated graduation on the frequency scale for each span choice
   int FS[] = {10000, 4000, 2000, 1000, 400, 200, 100 };   //frequency to subtract to get first annotation (in half-kHz)
   int NG[] = {    6,    5,    3,    3,   5,   3,   3 };   //number of annotated graduations
   int SB[] = {    0,   20,    0,    0,  20,   0,   0 };   //starting bias in pixels
   int FJ[] = {   40,   40,  100,  100,  40, 100, 100 };   //pixels to jump between successive annotations
   int DP[] = {    0,    0,    1,    1,   2,   2,   2 };   //3 - number of required decimal places (including the decimal point itself)

   int fs = 0;                             //selected frequency span number 0 = 10MHz ... 6 = 100kHz
   int CF = 186000;                        //entered centre frequency in half-kHz
   String Smeter[] = {                     //S-meter scale
      "0", "", "2", "", "4", "", "6", 
      "", "8", "", "10", "", "12"
   };

   String bandNames[][] = {
      {"None", "Long Wave", "Medium Wave", "120 metre", "90 metre", "75 metre", "60 metre", "49 metre", "39 metre", "31 metre", "25 metre", "22 metre", "19 metre", "16 metre", "13 metre", "11 metre", "FM (Low half)", "FM (High half)"},
      {"None", "Top Band", "80 metre", "40 metre", "30 metre", "20 metre", "17 metre", "15 metre", "12 metre", "10 metre", "6 metre", "4 metre", "2 metre", "70 cm", "23 cm"},
      {"None", "LF Navigation", "3 Meg", "31/2 Meg", "41/2 Meg", "51/2 Meg", "61/2 Meg", "9 Meg", "10 Meg", "11 Meg", "13 Meg", "15 Meg", "18 Meg", "22 Meg", "23 Meg", "VHF Civil (Lower Half)", "VHF Civil (Upper Half)"},
      {"None", "2-3 Meg", "61/2 Meg", "81/2 Meg", "12-13 Meg", "18.8 Meg", "19.7 Meg", "22-23 Meg", "25 Meg", "Marine VHF"}
   };

   int bandCentres[][] = {
      {250, 1000, 2400, 3300, 3950, 4900, 6100, 7200, 9700, 11800, 13700, 15550, 17700, 21650, 25850, 93000, 103000},  //broadcast
      {1850, 3650, 7050, 10150, 14200, 18100, 21200, 24900, 29000, 51000, 70250, 145000, 435000, 1245000},  //amateur
      {550, 3000, 3450, 4700, 5600, 6600, 8900, 10050, 11300, 13300, 15050, 17950, 21950, 23250, 123000, 133000},  //aircraft
      {2500, 6350, 8500, 12800, 18850, 19750, 22600, 25300, 160000 }   //marine
   };

   int bandStarts[][] = {
      {150,  500, 2300, 3200, 3900, 4750, 5950, 7100, 9500, 11650, 13600, 15510, 17550, 21450, 25670, 88000, 98000},
      {1810, 3500, 7000, 10100, 14000, 18068, 21000, 24890, 28000, 50000, 70025, 144000, 430000, 1240000},
      {515, 2850, 3400, 4650, 5450, 6525, 8815, 10005, 11175, 13200, 15010, 17900, 21870, 23200, 118000, 128000},
      {2000, 6200, 8195, 12330, 18780, 19680, 22000, 25010, 156000}
   };

   int bandEnds[][] = {
      {350, 1500, 2498, 3400, 4000, 4995, 6200, 7300, 9900, 11975, 13800, 15600, 17900, 21850, 26100, 98000, 108000},
      {1850, 3800, 7100, 10150, 14350, 18168, 21450, 24990, 29700, 52000, 70500, 146000, 440000, 1250000},
      {540, 3150, 3500, 4750, 5730, 6765, 9040, 10100, 11400, 13360, 15100, 18030, 22000, 23350, 128000, 136950},
      {2850, 6525, 8815, 13200, 18900, 19800, 22855, 25550, 162050}
   };

   int bandSpans[][] = {
      { 5, 3, 5, 5, 6, 4, 4, 5, 4, 4, 5, 6, 4, 4, 4, 0, 0 }, //broadcast
      { 6, 4, 6, 6, 4, 5, 4, 5, 2, 2, 4, 2, 0, 0 },          //amateur
      { 6, 4, 6, 6, 4, 4, 4, 6, 4, 5, 6, 5, 5, 5, 0, 0 },    //aircraft
      { 3, 4, 3, 3, 5, 5, 3, 3, 0 }                          //marine
   };

   String bandType[]= {    //first 3 letters of bandscope data file names
      "brd",               //broadcast band
      "ham",               //amateur band
      "air",               //aeronautical band
      "mar"                //marine band
   };



   public void init() {
      ac = getAppletContext();                             //get details of HTML document this applet is running in
      bg1 = getBackground();                               //get host document's background colour
      setBackground(bg1);                                  //make this the applet panel's background colour
      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 (circa Nov 1999)
      }
      setLayout(null);                                     //to allow the control panels to be laid out manually
      mml ce = new mml(this);                              //set up a mouse motion listener to
      addMouseMotionListener(ce);                          //listen for mouse movements within this applet
      ml cj = new ml(this);                                //set up a mouse listener to
      addMouseListener(cj);                                //listen for mouse clicks etc within this applet

      FrqFld = new TextField("93.000"); add(FrqFld);       //set up centre frequency entry field
      FrqFld.setBounds(GL + 70, GB + 35, 60, 20);          //set its position and size on applet panel
      bl ff = new bl(bl.FRQFLD, this);                     //set up action listener to
      FrqFld.addActionListener(ff);                        //listen for c/r from text field

      EntBut = new Button("Enter"); add(EntBut);           //set up the Enter button 
      EntBut.setBounds(GL + 135, GB + 35, 40, 20);
      bl eb = new bl(bl.ENTBUT, this); 
      EntBut.addActionListener(eb);

      DnBut = new Button("-"); add(DnBut);                 //set up the Down button
      DnBut.setBounds(GL + 82, GB - 114, 16, 16);
      bl db = new bl(bl.DNBUT, this); 
      DnBut.addActionListener(db);

      UpBut = new Button("+"); add(UpBut);                 //set up the Up button
      UpBut.setBounds(GL + 102, GB - 114, 16, 16);
      bl ub = new bl(bl.UPBUT, this); 
      UpBut.addActionListener(ub);

      Span = new Choice();  //graph's frequency span choice box (located above the graph)
      Span.add("10 MHz");
      Span.add("5 MHz");
      Span.add("2 MHz");
      Span.add("1 MHz");
      Span.add("500 kHz");
      Span.add("200 kHz");
      Span.add("100 kHz");
      add(Span);
      Span.setBounds(GL + 65, GB - SCH, 70, 20);  //set its size and position on the applet panel
      chl span = new chl(chl.SPAN, this);         //set up listener for checkbox group
      Span.addItemListener(span);

      int x = 220;      //x-bias of buttons and choices on right of graph
      int dy = 48;      //y-increment between successive buttons/choices
      int y = 130;      //y-bias of top choice object
      int w = 95;       //width of these choices/buttons
      int d = 20;       //depth of these choices/buttons

      HldBut = new Button("Peak Hold");           //set up the HOLD button 
      add(HldBut);
      HldBut.setBounds(GL + x, GB - y, w, d);
      bl hb = new bl(bl.HLDBUT, this); 
      HldBut.addActionListener(hb);

      y -= dy;          //move down the panel to the position for the next Choice box

      BandTypes = new Choice();
      BandTypes.add("None");
      BandTypes.add("Broadcast");
      BandTypes.add("Amateur");
      BandTypes.add("Aircraft");
      BandTypes.add("Marine");
      add(BandTypes);
      BandTypes.setBounds(GL + x, GB - y, w, d);
      chl bandtypes = new chl(chl.BANDTYPES, this);     //set up listener for this choice menu
      BandTypes.addItemListener(bandtypes);

      y -= dy;          //move down the panel to the position for the next Choice box

      Bands = new Choice();
      Bands.add("None");  //this is absolutely vital to avoid hang-up at ###1
      add(Bands);
      Bands.setBounds(GL + x, GB - y, w, d);
      chl bands = new chl(chl.BANDS, this);   //set up listener for this choice menu
      Bands.addItemListener(bands);

      y -= dy;          //move down the panel to the position for the next Choice box

      Update = new Choice();
      Update.add("on demand");
      Update.add("continuous");
      Update.add("every minute");
      Update.add("every 5 mins");
      Update.add("every 10 mins");
      Update.add("every 15 mins");
      Update.add("every 20 mins");
      Update.add("every 30 mins");
      Update.add("every hour");
      add(Update);
      Update.setBounds(GL + x, GB - y, w, d);
      chl update = new chl(chl.UPDATE, this);
      Update.addItemListener(update);            //set up listener for this choice menu

      y = 60;          //how far next button is below the baseline of the graph area

      PulBut = new Button("Update Now");         //set up the Pull button
      add(PulBut);
      PulBut.setBounds(GL + x, GB + y, w, d);
      bl pb = new bl(bl.PULBUT, this); 
      PulBut.addActionListener(pb);

      TH = new Thread(this);     //create a thread for downloading data from server
   }


   public void paint(Graphics g) { 
      FontMetrics fm = g.getFontMetrics();  //get dimensions of current type face
      fh = fm.getHeight();                  //get the full interline height of font
      fa = fm.getAscent();                  //how far top of lettering can be above base line
      g.setColor(bg1);                      //applet's background colour
      g.fillRect(0, 0, W, H);               //clear the graph panel

      //PUT THE 3-D EFFECT WHITE & BLACK LINES ROUND APPLET PANEL AND GRAPH AREA
      g.setColor(Color.white);
      int x = W - 1, y = H - 1;             //x, y extents of applet panel
      g.drawLine(0, 0, x, 0); 
      g.drawLine(0, 0, 0, y);               //white top & left 3-D edges of applet panel
      int x1 = GR + 1;                      //1 pixel beyond right graph edge
      int x2 = GL - 1;                      //1 pixel before left graph edge
      int y1 = GB + 1;                      //1 pixel below graph bottom
      int y2 = GT - 1;                      //1 pixel above graph top
      g.drawLine(x2, y1, x1, y1);           //bottom white 3-D edge of graph area
      g.drawLine(x1, y2, x1, y1);           //right white 3-D edge of graph area
      g.setColor(Color.black);
      g.drawLine(0, y, x, y);
      g.drawLine(x, 1, x, y);               //black bottom & right 3-D edges of applet panel
      g.drawLine(x2, y2, x1, y2);           //black top 3-D edge of graph area
      g.drawLine(x2, y2, x2, y1);           //black right 3-D edge of graph area

      y1 = GB + 5; y2 = GB + 10;            //upper and lower extremities of graduation marks
      x = 0;                                //set pixel counter to start of graph
      for(int i = 0; i < 11; i++) {         //for each frequency graduation
         g.drawLine(GL + x, y1, GL + x, y2);//draw frequency graduation marks
         x += 20;                           //move to position of next frequency graduation mark
      }
      g.drawLine(GL, y1, GR, y1);           //horizontal axis (frequency)

      y = GB - SCH + 10;                    //SCH = Span Choice Height
      g.drawLine(GL, y, GL + 63, y);        //horizontal left-hand span arrow
      g.drawLine(GL, y, GL + 5, y - 3);
      g.drawLine(GL, y, GL + 5, y + 3);
      g.drawLine(GR - 63, y, GR, y);        //horizontal right-hand span arrow
      g.drawLine(GR - 5, y - 3, GR, y);
      g.drawLine(GR - 5, y + 3, GR, y);

      g.drawLine(GC, GB + 25, GC, GB + 40); //centre-line just above the centre frequency field

      x = GR + 20; y = GT + 9; int dy = 48;
      g.drawString("Select Band Type", x, y);
      g.drawString("Select Band", x, y += dy);
      g.drawString("Update trace...", x, y += dy);

      rightString(g, "Centre Frequency MHz", GL + 65, GB + 50);

      x1 = GL - 10; x2 = GL - 5;                 //DRAW THE S-METER SCALE AND LABELS
      int x3 = GL - 20;
      y = GB;
      dy = fh >> 1;                              //put text base-line half text height below S-graduation mark
      for(int i = 0; i < Smeter.length; i++) {   //for each S-unit graduation
         centreString(g, Smeter[i], x3, y + dy); //draw S-meter figure
         g.drawLine(x1, y, x2, y);               //draw graduation mark
         y -= 8;                                 //accumulated number of S-meter scale pixels
      }
      x = GL - 5;
      g.drawLine(x, GB, x, GT);                  //vertical (signal strength) axis
      x = GL - 55; y = GB - 51;
      centreString(g, "Signal", x, y);
      centreString(g, "Strength", x, y += fh);
      y = GB - 5;
      g.drawLine(x, GB, x, GB - 35);             //lower arrow line
      g.drawLine(x, GB, x - 3, y);               //left half of lower arrow head
      g.drawLine(x, GB, x + 3, y);               //right half of lower arrow head
      g.drawLine(x, GT, x, GT + 35);             //upper arrow line
      y = GT + 5;
      g.drawLine(x, GT, x - 3, y);               //left half of upper arrow head
      g.drawLine(x, GT, x + 3, y);               //right half of upper arrow head

      redraw = 7; update(g);                     //paint/repaint the graph bars etc.
   }



   public void update(Graphics g) {
      switch (redraw) {
         case 1:                                 //When mouse moves within graph area
            drawMouseFreq(g);                    //redraw mouse frequency and S-meter
            break;
         case 2:                                 //When mouse clicked on a signal
            drawGraph(g);                        //redraw the graph and signals
            break;
         case 3:                                 //When Span Choice or Band Choice is changed
            drawFreqRes(g);                      //redraw 'kHz/pixel' message
         case 4:                                 //When new centre frequency is entered
            drawFrequencyScale(g);               //redraw horizontal frequency scale and kHz/pixel message
            drawGraph(g);                        //redraw the graph and signals
            break;
         case 5:                                 //When Peak Hold button pressed
            drawPeakHoldMessage(g);              //redraw the Peak Hold message
            break;
         case 6:                                 //When new bandscope data is loaded
            showTimeStamp(g);                    //display bandscope data file's time stamp and age message
            break;
         case 7:                                 //full repaint
            drawMouseFreq(g);                    //redraw mouse frequency and S-meter
            drawFrequencyScale(g);               //redraw horizontal frequency scale and kHz/pixel message
            drawFreqRes(g);                      //redraw 'kHz/pixel' message
            drawGraph(g);                        //redraw the graph and signals
            drawPeakHoldMessage(g);              //redraw the Peak Hold message
            showTimeStamp(g);                    //display bandscope data file's time stamp and age message
      }
   }



   void showTimeStamp(Graphics g) {              //SHOW DATE AND AGE OF CURRENT BANDSCOPE DATA FOR SELECTED BAND
      g.setColor(bg1);
      int x1 = GL - 75;
      int y1 = GB + 75;                          //base line of time stamp
      int y2 = GB + 55;                          //base line of age message
      g.fillRect(x1, y1 - fa, 295, fh);          //clear the bandscope data file time stamp area
      g.fillRect(GR + 20, y2 - fa, 95, fh);      //clear the bandscope data age message above the Update Now button
      g.setColor(Color.black);
      g.drawString("Scanned: " + LM, x1, y1);    //display data file time stamp
      centreString(g, AgeMsg, GR + 67, y2);      //display data file age message
   }



   void drawPeakHoldMessage(Graphics g) {        //DRAW THE 'PEAK HOLD' MESSAGE ABOVE PEAK HOLD BUTTON
      int y = GB - 135;                          //base line bias of message text
      g.setColor(bg1);
      g.fillRect(GR + 20, y - fa, 95, fh);       //clear 'Peak Hold message' area 
      if(peakHold)
         g.setColor(Color.green);
      else
         g.setColor(Color.black);
      centreString(g, PHmsg, GL + 267, y);       //display Peak Hold message
   }



   void drawFrequencyScale(Graphics g) {                   //DRAW FREQUENCY SCALE & kHz/pixel MESSAGE
      int y = GB + 23;                                     //base line of frequency scale text
      g.setColor(bg1);                                     //applet's general background colour
      g.fillRect(GL - 20, y - fa, 240, fh);                //clear the frequency scale
      g.setColor(Color.black);
      if(CF < MC[fs])                                      //make sure Centre Frequency CF is not less than 
         CF = MC[fs];                                      //minimum centre frequency for this band
      FrqFld.setText(toMHz(CF, DP[fs]));                   //display corrected CF in entry field
      int ff = CF - FS[fs];                                //frequency of first annotation (in half-kHz)
      w = SB[fs];                                          //pixels from start of frequency scale of first annotation
      int dp = DP[fs];                                     //number of decimal places required for frequency annotations
      for(int i = 0; i < NG[fs]; i++) {                    //for each frequency graduation
         centreString(g, toMHz(ff, dp), GL + w, y);        //frequency annotation
         ff += FG[fs];                                     //increment to frequency of next annotation
         w += FJ[fs];                                      //move to position of next annotation
      }
   }



   void drawFreqRes(Graphics g) {
      int x = GL - 75;
      int y = GB - SCH + 10 + (fa >> 1);                   //base line of text
      g.setColor(bg1); g.fillRect(x, y - fa, 74, fh);      //clear 'kHz/pixel' area
      g.setColor(Color.black);
      g.drawString(FR + " kHz/pixel", x, y);               //display kHz/pixel message
   }



   void drawGraph(Graphics g) {                            //re-draw the bandscope graticule and signals
      if(sBT > 0 && sB > 0) {                              //if a specific band has been selected
         g.setColor(bg1);                                  //background colour of valid graph area
         g.fillRect(GL, GT, GW, GH);                       //clear graph 
         g.setColor(bg2);                                  //background colour of valid graph area
         int kp = KP[fs];                                  //half-kHz/pixel for selected frequency span
         int s = (CF - ((bandStarts[sbt][sb]) << 1)) / kp; //pixel distance of start of band from centre of graph
         int e = (((bandEnds[sbt][sb]) << 1 ) - CF) / kp;  //pixel distance of end of band from centre of graph     
         g.fillRect(GL + 100 - s, GT, s + e, GH);          //put in coloured background for selected band
      } else {                                             //else no band is selected - ie manual mode
         g.setColor(bg2);                                  //background colour of valid graph area
         g.fillRect(GL, GT, GW, GH);                       //put in coloured background for graph
      }
      g.setColor(bg3);                                     //colour for on-graph division marks
      int x = GL;                                          //VERTICAL IN-GRAPH FREQUENCY GRADUATION LINES
      for(int i = 0; i < 11; i++) {                        //for each frequency graduation
         g.drawLine(x, GB, x, GT);                         //vertical division line in graph area
         x += 20;                                          //move to position of next frequency graduation line
      }
      int y = GB;                                          //HORIZONTAL IN-GRAPH S-UNIT GRADUATION LINES
      for(int i = 0; i < Smeter.length; i++) {             //for each S-unit graduation
         g.drawLine(GL, y, GR, y);                         //horizontal annotation mark
         y -= 8;                                           //accumulated number of vertical pixels
      }

      if(dataOK) {
         g.setColor(Color.blue);                           //set trace colour to blue
         if(lp == 3) {                                     //provided data download completed
            x = GL;                                        //initial x-bias for week number
            int j = 0;
            for(int i = 0; i < GW; i++) {                  //display vertical bar on the chart
               g.drawLine(x, GB, x, GB - Plots[i]);
               x++;                                        //increment the week number
            }
         }
      } else {
         g.setColor(Color.black);                          //set to print in black
         centreString(g, dataMsg, GC, VC);                 //display unavailable data message
      }

      y = GB - 104;                                        //base line of message text
      g.setColor(bg1);
      g.fillRect(GL - 75, y - fa, 155, fh);                //clear 'Listening on' area 
      if(receiving) {
         g.setColor(amber);
         g.drawString("Listening on -->", GL - 75, y);
         rightString(g, rFs, GL + 79, y);                  //display receiver frequency
         g.drawLine(GL + hS, GB, GL + hS, GB - rS);        //display vertical bar on the chart
      }
   }



   void drawMouseFreq(Graphics g) {           //DISPLAY MOUSE FREQUENCY AND S-METER
      int y = GB - 104;                       //base line of mouse frequency
      g.setColor(bg1);                        //set to panel colour
      g.fillRect(GR - 80, y - fa, 80, fh);    //clear mouse frequency area
      g.setColor(Color.black);
      rightString(g, mFs, GR - 1, y);         //display mouse frequency
      int x = GL - 5; y = GB - mS; 
      g.drawLine(x, y, x, GT);                //upper black part of vertical (signal strength) axis
      g.setColor(amber); 
      g.drawLine(x, GB, x, y);                //lower amber part of vertical (signal strength) axis
   }



   public void start() { TH.start(); }        //resume execution of Internet data transfer thread 

   public void run() {                        //MANAGE INTERNET DATA TRANSFERS ON A SEPARATE THREAD
      while(TH.isAlive()) {                   //while this thread exists
         switch(lp) {                         //THE 2 DOWNLOADING PHASES
            case 1: fileConnect(); break;     //connect to bandscope data file on server
            case 2: fileLoad(); break;        //manage the downloading of its content
         }
         if(lp != LP) {                       //if loading of data has finished
            LP = lp;                          //latch the current load state
            redraw = 2; repaint();            //display all data
         }
         try {
            Thread.currentThread().sleep(250);//sleep to allow other things to take place
         } catch (InterruptedException e) {}  //catch interrupt from GUI or browser
         checkAgeOfBandscopeData();
      }
   }

   public void stop() { TH.stop(); }          //freeze execution of thread while away from host HTML page

   public void destroy() {                    //called after stop() before applet is removed from memory
      if(TH != null) {                        //if the Internet data transfer thread object still exists
         TH.stop();                           //stop it running
         TH = null;                           //and jettison its object into limbo
      }
   }


   void fileConnect() {                                    //CONNECT TO THE BANDSCOPE DATA FILE ON THE SERVER
      fn = "bs";                                           //default name for the bandscope data file
      if((sBT > 0) && (sB > 0)) {                          //if a specific band has been selected
         String s = "";                                    //pad band number out to two digits
         if(sB < 10) s = "0";                              //with a leading zero if required
         fn = bandType[sbt] + s + sB;                      //form the complete file name
      }
      fn += ".dat";
      showStatus("Loading " + fn);
      try {                                                //enable local exception-catching
         url = new URL(cb + fn);                           //form the url of the appropriate bandscope data file
         URLConnection u = url.openConnection();           //open a connection to the remote file
         LM = "" + new Date(lm = u.getLastModified());
         lm /= 1000;                                       //convert to whole seconds
         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) {
         lp = 0;                                           //set unrecoverable error status
         E = "fileConnect() " + e;                         //note the exception and where it occurred
         showStatus(E);                                    //show exception message in browser's status bar
      } 
   }


   void fileLoad() {                               //DOWNLOAD THE BANDSCOPE DATA
      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 big byte array
         if(l >= L) {                              //if the whole of the data has now been downloaded
            I.close();                             //close the URL Connection's input stream
            computePlots();                        //create graph plots from downloaded bandscope data
            lp = 3;                                //indicate file loading completed successfully
            showStatus("Data file: " + fn + " now loaded");
         }
      } catch(Exception e) { 
         lp = 0;                                   //set error condition
         E = "fileLoad() " + e + L + " " + l;      //note the exception and where it occurred
         showStatus(E);                            //show exception message in browser's status bar
      }
   }


   void checkAgeOfBandscopeData() {
      String Age;                                  //changeable part of age message
      long t = System.currentTimeMillis() / 1000;  //current System time
      if((t > T) && (lp == 3)) {                   //if more than a second has elapsed since last AgeMsg update
         long age = t - lm;                        //elapsed time (in seconds) since data file last modified
         if(age > 2592000)
            Age = "Over a month";
         else if(age > 604800)
            Age = "Over a week";
         else if(age > 86400)
            Age = "Over a day";
         else if (age > 3600)
            Age = "Over an hour";
         else if(age > 60)
            Age = "" + age / 60 + " min";
         else
            Age = "" + age + " sec";
         if(!Age.equals(oldAge)) {                 //if age message has changed since a second ago
            AgeMsg = Age + " old";                 //form the displayable age message
            redraw = 6; repaint();                 //display the age message
            oldAge = Age;                          //save age string for comparison next time through
         }
         T = t;                                    //note System time when AgeMsg last updated
      }
   }


   // USER-EVENT HANDLERS

   void selSpan() {                     //do when a new bandscope frequency span is selected
      sBT = 0; BandTypes.select(sBT);
      sB = 0;  Bands.select(sB);        //defaulting to manual selection ###1
      selSpan2();
      redraw = 3; repaint();
   }
   void selSpan2() {                    //do when a new bandscope frequency span is selected
      fs = Span.getSelectedIndex();
      if(fs == 6)                       //if 100 kHz span is selected
         FR = "1/2";                      //set straight to half kHz/pixel
      else {                            //for all other spans
         FR = "" + (KP[fs] >> 1);       //get the integral resolution in kHz/pixel
         if(fs == 4)                    //and if the 500 kHz span is selected
            FR += "1/2";                  //add the extra half sign for the 2.5 kHz/pixel
      }
      computePlots();
      receiving = false;
   }


   // THIS METHOD IS SPECIFIC TO THE WAY THE AOR AR8600 SCANNER PROVIDES BANDSCOPE DATA
   void computePlots() {                                   //create graph plots from bandscope data
      int D[] = { 0, 5, 1, 2, 4, 2, 4 };                   //multipliers and divisors
      int S[] = { 0, 249, 399, 449, 474, 0, 24 };          //starting points in bandscope samples array
      int E[] = {1000, 500, 200, 100, 50, 100, 50 };       //extents of bandscope samples within array
      int d = D[fs];
      int s = S[fs];                                       //starting point for currently selected scope width
      int e = s + E[fs];                                   //end point for currently selected scope width
      dataOK = true;
      if(L == 1000) {
         if(fs == 0) {                                     //if 10MHz span
            int j = 0, h = 0, plot = 0;                    //5 samples are averaged to form 1 pixel plot
            for(int i = s; i < e; i++) {                   //for each bandscope sample over the prescribed range
               h += (int)B[i] - 2;                         //sum next 5 data points in h (-2 takes off noise)
               if(++j == 5) {
                  Plots[plot++] = (h << 3) / 5;            //store average in next graphical signal plot
                  j = 0;
                  h = 0;
               }
            }
         } else if(fs == 1) {                              //if 5 MHz span
            boolean MustStepBack = true;                   //2.5 samples are averaged (sort of) to form 1 pixel plot
            int j = 0, h = 0, plot = 0;
            for(int i = s; i < e; i++) {                   //for each bandscope sample over the prescribed range
               h += (int)B[i] - 2;                         //in h (-2 takes off noise)
               if(++j == 3) {                              //sum the next 3 bandscope samples
                  Plots[plot++] = (h << 3) / 3;            //store average in next graphical signal plot
                  j = 0;
                  h = 0;
                  if(MustStepBack)                         //if done first 3 samples of current 5-sample group
                     i--;                                  //back up by one bandscope sample
                  MustStepBack = !MustStepBack;            //reverse the flag
               }
            }
         } else if(fs < 5) {                  //if fs = 2 (2 MHz span), 3 (1MHz span) or 4 (500kHz span)
            int plot = 0;                     //start at beginning of graph plots array
            for(int i = s; i < e; i++) {      //for each bandscope sample over the prescribed range
               int x = (int)B[i] - 2;         //get the sample and subtract the noise
               for(int j = 0; j < d; j++)     //store it in the next 1, 2 or 4 graph plots
                  Plots[plot++] = x << 3;     //according to currently selected frequency span
            }
         } else {
            dataMsg = "Loading 2kHz samples.";
            for(int i = 0; i < GW; i++) Plots[i] = 0;
            dataOK = false;
         }
      } else if(L == 100) {                   //assume first 100 bytes of B[] contain 2kHz samples
         if(fs > 4) {
            int plot = 0;                     //start at beginning of graph plots array
            for(int i = s; i < e; i++) {      //for each bandscope sample over the prescribed range
               int x = (int)B[i] - 2;         //get the sample and subtract the noise
               for(int j = 0; j < d; j++)     //store it in the next 1, 2 or 4 graph plots
                  Plots[plot++] = x << 3;     //according to currently selected frequency span
            }
         } else {
            dataMsg = "Loading 10kHz samples.";
            for(int i = 0; i < GW; i++) Plots[i] = 0;
            dataOK = false;
         }
      } else {
         dataMsg = "L = " + L + ", default data file corrupt.";
         for(int i = 0; i < GW; i++) Plots[i] = 0;
         dataOK = false;
      }
   }



   void pressFrqFld() {                //do when C/R received from centre frequency field
      sBT = 0; BandTypes.select(sBT);
      sB = 0;  Bands.select(sB);       //defaulting to manual selection ###1
      pressFrqFld2();                  //do when C/R received from centre frequency field
      if(CFvalid) {
         dataMsg = "Please update bandscope data.";
         for(int i = 0; i < GW; i++) Plots[i] = 0;
         dataOK = false;
         redraw = 4; repaint();        //so frequency scale will be re-drawn
      }
   }

   void pressFrqFld2() {                                   //do when C/R received from centre frequency field
      receiving = false;
      String s = FrqFld.getText();                         //get the text present in the field
      int x = s.indexOf(".");
      if(x == -1) {                                        //if text does not contain a decimal point
         s += ".0";                                        //append a decimal point with a trailing zero
         x = s.indexOf(".");                               //find index of inserted point
         FrqFld.setText(s);                                //display amended text in centre-frequency field
      }
      String k = s.substring(x, s.length());               //isolate the decimal point + digits
      s = s.substring(0, x);                               //isolate the integral MHz
      int l = k.length();
      if(l > 1) {                                          //if more than just the decimal point on its own
         if(l > 3) l = 3;                                  //limit to 2 decimal places of MHz
         k = k.substring(1, l);                            //isolate the decimal digits
         while(k.length() < 2) k += "0";                   //pad out with zeros to 3 digits
      } else k = "00";                                     //no decimal places, so add 3 zeros
      s += k;                                              //s now contains integral 10 kHz
      CFvalid = false;
      try {                                                //try to
         x = Integer.parseInt(s + "0");                    //parse string as an integer
         if(x < 2040000 && x > 100) {                      //if frequency within range of receiver scanner
            CF = x << 1;                                   //set new valid centre frequency (in integral half-kHz)
            CFvalid = true;                                //to trigger appropriate repaint mode
         } else
            showStatus("Frequency out of range!");         //show error message in browser's status bar.
      } catch(NumberFormatException e) {                   //if what was entered cannot be parsed into an integer
         showStatus("Invalid centre frequency!");          //show error message in browser's status bar.
      }
   }



   void selBand() {                        //action to be taken when new band is selected
      sB = Bands.getSelectedIndex();       //index number of band that has been selected
      sb = sB - 1;                         //corresponding array index
      if(sB > 0 && sBT > 0) {
         fs = bandSpans[sbt][sb];          //get frequency span for this band
         Span.select(fs);                  //select the appropriate span choice
         CF = (bandCentres[sbt][sb]) << 1; //get appropriate centre frequency for this band 
         FrqFld.setText(toMHz(CF, 4));     //set it in the CF entry field
         selSpan2();
         pressFrqFld2();                   //enter the centre frequency
         LP = 1; lp = 1;                   //trigger loading of the appropriate data file
         redraw = 3; repaint();
      }
   }



   void selBandType() {                                    //action to be taken when new bandscope span is selected
      sBT = BandTypes.getSelectedIndex();                  //index number of band type that has been selected
      sbt = sBT - 1;                                       //corresponding array index
      Bands.removeAll();                                   //remove band names of previously-selected band type
      if(sBT > 0)
         for(int i = 0; i < bandNames[sbt].length; i++)
            Bands.add(bandNames[sbt][i]);                  //load the appropriate bands into the band choice
      else Bands.add("None");                              //set up a null band
      receiving = false; selBand();
   }



   void scamper(int x, int y) {                     //MOUSE MOTION EVENT HANDLER
      x -= GL;                                      //frequency co-ordinate within graph area
      y = GB - y;                                   //signal co-ordinate within graph area
      if(x >= 0 && x <= GW && y >= 0 && y <= GH) {  //if mouse within graph area
         onGraph = true;                            //state that mouse is currently within graph area
         mF = CF + (x - 100) * KP[fs];              //true mouse frequency in integral half-kHz
         mFs = toMHz(mF, 4) + "MHz";                //to string 4 decimal places of MHz
         mS = Plots[x];                             //Signal strength at mouse
         redraw = 1; repaint();                     //set for a mouse-only repaint
      } else if(onGraph) {                          //if moved off graph area but not cleared frequency field
         mF = 0;
         mFs = "--------------MHz";
         mS = 0;
         redraw = 1; repaint();                     //set for a mouse-only repaint
         onGraph = false;                           //state that mouse is now outside graph area
      }                                             //so no more repaints of frequency display necessary
   }



   void click(int x, int y) {                       //MOUSE MOTION EVENT HANDLER
      x -= GL;                                      //mouse horizontal co-ordinate within graph area
      y = GB - y;                                   //mouse vertical co-ordinate within graph area
      if(x >= 0 && x <= GW && y >= 0 && y <= GH) {  //if mouse within graph area
         rF = CF + (x - 100) * KP[fs];              //receiver frequency in integral half-kHz
         rFs = toMHz(rF, 4) + "MHz";                //to string with 4 decimal places of MHz
         rS = Plots[x];                             //Signal strength at mouse (in pixels)
         hS = x;                                    //graph x-co-ordinate of highlighted signal
         receiving = true; redraw = 2; repaint();
         showStatus("RF" + rF * 500);               //send 'tune to' command to receiver
      }
   }



   void pressEntBut() { pressFrqFld(); }            //action to be taken when Enter button is pressed



   void pressUpBut() {                              //action to be taken when Up button is pressed
      rF++; adjustRXFrequency();
   }

   void pressDnBut() {                              //action to be taken when Down button is pressed
      rF--; adjustRXFrequency();
   }

   void adjustRXFrequency() {
      rFs = toMHz(rF, 4) + "MHz";                   //receiver frequency to string with 4 decimal places of MHz
      hS = 100 + (rF - CF) / KP[fs];                //horizontal pixel position of receiver frequency on graph
      if(hS < 0) hS = 0;
      if(hS > 199) hS = 199;
      rS = Plots[hS];                               //Signal strength at mouse (in pixels)
      receiving = true; redraw = 2; repaint();
      showStatus("RF" + rF * 500);                  //send 'tune to' command to receiver
   }



   void pressPulBut() {                             //action to be taken when Pull button is pressed
      if(false) {                                   //replace with server ack signal
         LP = 1; lp = 1;                            //trigger a reload of the data
      } else {
         showStatus("AR8600 port server currently unavailable.");
         getToolkit().beep();                       //beep
      }
   }



   void pressHldBut() {                  //action to be taken when Hold button is pressed
      if(peakHold) {
         peakHold = false;
         PHmsg = "off";
         showStatus("PH0");
      } else {
         peakHold = true;
         PHmsg = "ON";
         showStatus("PH1");
      }
      PHmsg = "Peak Hold " + PHmsg;
      redraw = 5; repaint();
   }
   void selUpdate() {                    //action to be taken when update choice made
      showStatus("new update frequency selected");
   }



   //STRING FORMATTING METHODS

   void centreString(Graphics g, String s, int x, int y) { //DRAW A STRING CENTRED AT A HORIZONTAL POSITION x
      FontMetrics fm = g.getFontMetrics();                 //dimensions of characters for current font
      int b = (fm.stringWidth(s)) >> 1;                    //half the width of the string to be displayed
      g.drawString(s, x - b, y);                           //display the string
   }

   void rightString(Graphics g, String s, int x, int y) {  //DRAW A RIGHT-JUSTIFIED STRING
      FontMetrics fm = g.getFontMetrics();                 //dimensions of characters for current font
      g.drawString(s, x - fm.stringWidth(s), y);           //display the string
   }

   String toMHz(                                           //INTEGRAL HALF-kHz to DECIMAL MHz STRING
      int x,                                               //frequency in integral half-kHz
      int d                                                //number of decimal places of MHz required
   ) {
      int D[] = { 5, 3, 2, 1, 0 };                         //to convert decimal places to string chop
      String s;
      if((x & 0x1) > 0)                                    //if there is an odd half-kHz
         s = "5";                                          //set 4th decimal place of MHz to '5'
      else                                                 //if it is a whole number of kHz
         s = "0";                                          //set 4th decimal place of MHz to '0'
      s = "" + (x >> 1) + s;                               //string of rF in form GL.XXXX MHz
      int l = s.length();                                  //insert the decimal point
      x = l - 4;                                           //to give MHz to 4 decimal places
      s = s.substring(0, x) + "." + s.substring(x, l);     //insert the decimal point
      return s.substring(0, s.length() - D[d]);            //chop to required number of decimal places
   }

}




/* The following class creates instances of various event listeners.
   They are used by this applet to create and service events for 
   each of the control buttons and choices, and also mouse movements
   and clicks. The underlying system calls the actionPerformed() 
   method below every time a button is pushed. The method is invoked
   upon the instance of one of the following classes corresponding to
   that particular button, choice or mouse event. The value of the 
   variable id at the time is therefore that which corresponds to 
   the button, choice or mouse event concerned. This causes the 
   appropriate case statement to be executed, thus passing control to
   the appropriate handling method in the above applet. */


class bl implements ActionListener {
   static final int ENTBUT = 0;                   //'Enter button pressed' event
   static final int PULBUT = 1;                   //'Pull button pressed' event
   static final int FRQFLD = 2;                   //'C/R from centre frequency entry field' event
   static final int UPBUT  = 3;                   //'Up button pressed' event
   static final int DNBUT  = 4;                   //'Down button pressed' event
   static final int HLDBUT = 5;                   //'Hold button pressed' event

   int id;                                        //one of the above
   bs ap;                                         //the application that called: always the above applet!

   public bl(int id, bs 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 ENTBUT: ap.pressEntBut(); break;    //it was the 'Enter' button
         case PULBUT: ap.pressPulBut(); break;    //it was the 'Pull' button
         case FRQFLD: ap.pressFrqFld(); break;    //it was a 'C/R' from centre frequency entry field
         case UPBUT:  ap.pressUpBut();  break;    //it was the 'Up' button
         case DNBUT:  ap.pressDnBut();  break;    //it was the 'Down' button
         case HLDBUT: ap.pressHldBut(); break;    //it was the 'Hold' button
      }
   }
}



class chl implements ItemListener {               //LISTENS FOR EVENTS FROM THE CHOICE MENU SELECTORS
   static final int SPAN = 0;                     //an event from the 'bandscope span selector' choice menu
   static final int BANDTYPES = 1;                //an event from the 'band type selector' choice menu
   static final int BANDS = 2;                    //an event from the 'band selector' choice menu
   static final int UPDATE = 3;                   //an event from the 'update frequency selector' choice menu
   int id;                                        //one of the above events
   bs ap;                                         //the application that called: always the above applet!

   public chl(int id, bs ap) {                    //constructor for a new Choice selection event
      this.id = id;                               //set the id number pertaining to this instance of 'cl'
      this.ap = ap;                               //set the reference to the instance of the 'bs' 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 SPAN: ap.selSpan(); break;          //Execute the method in the above applet which deals with this choice
         case BANDTYPES: ap.selBandType(); break; //Execute the method in the above applet which deals with this choice
         case BANDS: ap.selBand(); break;         //Execute the method in the above applet which deals with this choice
         case UPDATE: ap.selUpdate(); break;      //Execute the method in the above applet which deals with this choice
      }
   }
}



class mml implements MouseMotionListener {        //LISTENS FOR MOUSE MOVEMENTS
   bs ap;                                         //the application that called: always the above applet!

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

   public void mouseMoved(MouseEvent e) {         //The mouse has moved
      int x = e.getX();                           //the mouse's GL co-ordinate 
      int y = e.getY();                           //the mouse's GB co-ordinate
      ap.scamper(x, y);                           //scamper() is in my applet.
   }                                              //It does the donkey work of processing mouse movements

   public void mouseDragged(MouseEvent e) {       //a Choice selection event has occurred from checkbox 'id'
   }                                              //I don't need to use this at the moment

}


class ml implements MouseListener {               //LISTENS FOR MOUSE CLICKS
   bs ap;                                         //the application that called: always the above applet!

   public ml(bs ap) {                             //constructor for a new cat's jaw
      this.ap = ap;                               //set the reference to the instance of the 'bs' applet
   }                                              //from which it came. (there will only be one instance anyway).

   public void mouseClicked(MouseEvent e) {       //Invoked when the mouse has been clicked on a component.
      int x = e.getX();
      int y = e.getY();
      ap.click(x, y);                             //handles the donkey work associated with a mouse click.
   }

   public void mousePressed(MouseEvent e) {}      //Invoked when a mouse button has been pressed on a component. 
   public void mouseReleased(MouseEvent e) {}     //Invoked when a mouse button has been released on a component.
   public void mouseEntered(MouseEvent e) {}      //Invoked when the mouse enters a component.
   public void mouseExited(MouseEvent e)  {}      //Invoked when the mouse exits a component. 

}



class ar8600 {     //ENCAPSULATES AR8600 COMMUNICATIONS RECEIVER COMMANDS

   /* AT	What is the attenuator status?
      AT1	Engage attanuator
      AT0	Disengage attenuator

      AM	Switch to bandscope mode
      CF	Set bandscope centre frequency in hertz
      DC	Set bandscope data centre frequency
      DS	Acquire current bandscope data
      MF	Bandscope: set marker frequency
      PH	Bandscope peak hold
      SW	Bandscope frequency span */



}





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

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