/* Javascript for the Galex GIPS Tools site */

/* load a chunk of html from the url into the element with matching id */
function getHtmlProduct(id, url) {
  var elem = $(id+'_html');
  if ( elem ) {
    new Ajax.Request( url, {
      method: 'get',
      onSuccess: function(transport) { 
        var loading = $(id+'_loading');
        if ( loading ) {
          loading.hide();
        }
        elem.innerHTML = transport.responseText;
        elem.show();
      },
      onFailure: function(transport) {
        var loading = $(id+'_loading');
        if ( loading ) {
          loading.hide();
        }
        elem.innerHTML = 'Error Loading Product';
        elem.show();
      }
    });
  }
}

/* load an image from the url into the 'img' element with the matching id */
function getImgProduct(id, url) {
  var elem = $(id+'_img');
  if ( elem ) {
    var loading = $(id+'_loading');
    if ( loading ) {
      loading.hide();
    }
    elem.src = url;
    elem.show();
  }
}

/* toggle the visibility of table records based on array of class names */
function showTableRec(state, tbl_id, classes) {
  /* toggle the state of the buttons */
  for ( var i = 0, l = classes.length; i < l; ++i ) {
    var elem = $('show_'+tbl_id+'_'+classes[i]);
    if ( elem ) {
      if ( classes[i] == state ) {
        elem.style.display = 'inline';
      } else {
        elem.style.display = 'none';
      }
    }
  }
  /* apply the visibility changes to the table */
  if ( state == 'all' ) {
    $$('#'+tbl_id+' tr').each(Element.show);
  } else {
    /* show only the elments where state matches one of class names */
    for ( var i = 0, l = classes.length; i < l; ++i ) {
      /* skip the class name 'all' */
      if ( classes[i] == 'all' ) {
        continue;
      }
      if ( classes[i] == state ) {
        $$('#'+tbl_id+' .'+classes[i]).each(Element.show);
      } else {
        $$('#'+tbl_id+' .'+classes[i]).each(Element.hide);
      }
    }
  }
}

/* return closest enclosing parent element with matching tag name */
function getParentElementByTagName(node, tag) {
  while ( node != null ) {
    if ( node.nodeType == 1 ) { /* Node.ELEMENT_NODE = 1 */
      if ( node.tagName.toLowerCase() == tag ) {
        return node;
      }
    }
    node = node.parentNode;
  }
  return node; /* will be null if parent not found */
}

/* return all the observation numbers that are currently in use */
function getObservationNums() {
  var nums = [];
  var elems = $('observations_and_targets').immediateDescendants();
  for ( var i = 0, l = elems.length; i < l; ++i ) {
    if ( elems[i].id.substr(0,3) == 'obs' && $(elems[i]).hasClassName('obs') ){
      nums.push( parseInt(elems[i].id.substr(3)) );
    }
  }
  return nums;
}

/* renumber the observation elements */
function renumberObservation(div, old_num, new_num) {
  var old_id = 'obs'+old_num; var old_len = old_id.length;
  var new_id = 'obs'+new_num; var new_len = new_id.length;
  div.id = div.id.replace(old_id, new_id);
  var elems = div.getElementsByTagName('*');
  for ( var i = 0, l = elems.length; i < l; ++i ) {
    /* change any ids containing old number to new number */
    if ( elems[i].id && elems[i].id.substr(0,old_len) == old_id ) {
      elems[i].id = elems[i].id.replace(old_id, new_id);
    }
    /* change any names containing old number to new number */
    if ( elems[i].name && elems[i].name.substr(0,old_len) == old_id ) {
      elems[i].name = elems[i].name.replace(old_id, new_id);
    }
    /* change the number displayed to the user */
    if ( elems[i].id && elems[i].id == new_id+'_num' ) {
      elems[i].innerHTML = '#'+new_num;
    }
    /* hide any messages */
    if ( elems[i].id && elems[i].id.search(new_id+'_\\w+_msg') != -1 ) {
      elems[i].style.display = 'none';
    }
    /* unhide the ability to delete this observation */
    if ( new_num != 1 && elems[i].name && elems[i].name == 'remove_obs' ) {
      elems[i].style.display = 'inline';
    }
    /* clear out any user entered values */
    if ( elems[i].value ) {
      elems[i].value = '';
    }
    /* remove any class names used to display element as invalid */
    $(elems[i]).removeClassName('invalid');
  }
}

/* return the science target number for the row */
function getScienceTargetNum(row) {
  var num = null;
  var inputs = row.getElementsByTagName('input');
  if ( inputs.length && inputs[0].id ) {
    var tok = inputs[0].id.split('_');
    if ( tok.length > 1 && tok[1].substr(0,7) == 'scitarg' ) {
      num = parseInt(tok[1].substr(7));
    }
  }
  return num;
}

/* return the science target numbers that are in use for this observation */
function getScienceTargetNums(table) {
  var nums = [];
  for ( var i = 1, l = table.rows.length; i < l; ++i ) { /* skip first row */
    var n = getScienceTargetNum(table.rows[i]);
    if ( n != null ) {
      nums.push(n);
    }
  }
  return nums;
}

/* renumber the science target elements for this observation */
function renumberScienceTarget(row, old_num, new_num) {
  var old_id = 'scitarg'+old_num; var old_len = old_id.length;
  var new_id = 'scitarg'+new_num; var new_len = new_id.length;
  var re = new RegExp(old_id);
  var elems = row.getElementsByTagName('*');
  for ( var i = 0, l = elems.length; i < l; ++i ) {
    /* change any ids containing old number to new number */
    if ( elems[i].id && elems[i].id.search(re) != -1 ) {
      elems[i].id = elems[i].id.replace(re, new_id);
    }
    /* change any names containing old number to new number */
    if ( elems[i].name && elems[i].name.search(re) != -1 ) {
      elems[i].name = elems[i].name.replace(re, new_id);
    }
    /* change the number displayed to the user */
    if ( elems[i].id.search(new_id+'_num') != -1 ) {
      elems[i].innerHTML = '#'+new_num;
    }
    /* hide any messages */
    if ( elems[i].id.search('obs\\d+_'+new_id+'_\\w+_msg') != -1 ) {
      elems[i].style.display = 'none';
    }
    /* unhide the ability to delete this observation */
    if ( new_num != 1 && elems[i].name && elems[i].name == 'remove_scitarg' ) {
      elems[i].style.display = 'inline';
    }
    /* clear out any user entered values */
    if ( elems[i].value ) {
      elems[i].value = '';
    }
    /* remove any class names used to display element as invalid */
    $(elems[i]).removeClassName('invalid');
  }
}

/* add an observation */
function onClickAddObservation(src) {
  var div = getParentElementByTagName(src, 'div');
  if ( div != null ) {
    var nums = getObservationNums();
    if ( nums.length >= 999 ) {
      alert('The maximum number of observations is 999.');
      return;
    }
    var new_div = div.cloneNode(true);
      var old_num = parseInt(div.id.substr(3));
      var new_num = nums[nums.length-1] + 1;
      renumberObservation(new_div, old_num, new_num);
    $('observations_and_targets').insertBefore(new_div, $('obs_scitarg_msg'));
    /* remove every science target except the first */
    var scitarg_table = $('obs'+new_num+'_scitarg_table');
    for ( var i = scitarg_table.rows.length-1; i > 1; --i ) {
      scitarg_table.deleteRow(i);
    }
  }
}

/* remove an observation and any enclosing science targets */
function onClickRemoveObservation(src) {
  var div = getParentElementByTagName(src, 'div');
  if ( div != null ) {
    div.parentNode.removeChild(div);
  }
}

/* add a science target to an observation */
function onClickAddScienceTarget(src) {
  var table = getParentElementByTagName(src, 'table');
  if ( table != null && table.rows.length > 1 ) {
    var nums = getScienceTargetNums(table);
    if ( nums.length >= 10 ) {
      alert('The maximum number of science targets in an observation is 10.');
      return;
    }
    var new_row = table.rows[table.rows.length-1].cloneNode(true);
      var old_num = getScienceTargetNum(new_row);
      var new_num = nums[nums.length-1] + 1;
      renumberScienceTarget(new_row, old_num, new_num);
    table.rows[table.rows.length-1].parentNode.appendChild(new_row);
  }
}

/* remove a science target from an observation */
function onClickRemoveScienceTarget(src) {
  var row = getParentElementByTagName(src, 'tr');
  if ( row != null ) {
    row.parentNode.removeChild(row);
  }
}

/* parse the contents of the bulk loader text into an array of objects */
function parseBulkLoaderText(bulk_text, errmsg) {
  var bulk_data = [ ];
  var lines = bulk_text.split('\n'), tok, have_first_obs = false;
  for ( var i = 0, l = lines.length; i < l; ++i ) {
    if ( trim(lines[i]) == '' ) {
      continue; /* skip empty lines */
    }
    tok = lines[i].split(',');
    if ( ! have_first_obs && tok.length != 3 ) {
      errmsg.push(
        "first line must be an observation, offending line: '"+lines[i]+"'");
      return bulk_data;
    }
    if ( tok.length == 3 ) { /* observation */
      bulk_data.push({
        name: trim(tok[0]),
        ra:   trim(tok[1]),
        dec:  trim(tok[2])
      });
      have_first_obs = true;
    } else if ( tok.length == 4 ) { /* science target */
      bulk_data.push({
        name: trim(tok[0]),
        ra:   trim(tok[1]),
        dec:  trim(tok[2]),
        diam: trim(tok[3]) ? trim(tok[3]) : 0.1
      });
    } else {
      if ( tok.length < 3 ) {
        errmsg.push(
          "line contains too few tokens, offending line: '"+lines[i]+"'");
      } else if ( tok.length > 4 ) {
        errmsg.push(
          "line contains too many tokens, offending line: '"+lines[i]+"'");
      }
      return bulk_data;
    }
  }
  return bulk_data;
}

/* return true when argument is an integer */
function is_integer(inp) {
  /*
    an integer is...
    [+-]?       leading optional pos or neg sign
    \d+         followed by one or more integers
  */
  var tok = inp.match(/^[+-]?\d+$/g);
  return tok && tok.length == 1;
}

/*
  return true when argument is a floating point number
  NB - integers are also floating point numbers
       things like 'NaN' and 'Inf' are not considered floats
*/
function is_float(inp) {
  /*
    a float is...
    [+-]?       leading optional pos or neg sign
                followed by one of the following
    \d+\.?\d*   one or more integers, optional decimal, zero or more integers
    \.\d+       leading decimal followed by one or more integers
    \d+\.?\d*[Ee][+-]?\d{1,3}   
    one or more integers, optional decimal, zero or more integers, 
    letter 'E' or 'e', optional pos or neg sign, one to three integers
  */
  var tok = inp.match(/^[+-]?(\d+\.?\d*|\.\d+|\d+\.?\d*[Ee][+-]?\d{1,3})$/g);
  return tok && tok.length == 1;
}

/* trim leading and trailing whitespace */
function trim(inp) {
  inp = inp.replace(/^\s+/g, '');
  inp = inp.replace(/\s+$/g, '');
  return inp;
}

/* check that val is in the range [low, high) */
function is_in_open_range(val, low, high, errmsg, context) {
  if ( val < low || val >= high ) {
    errmsg.push(context+" not in range ["+low+", "+high+")");
    return false;
  }
  return true;
}

/* check that val is in the range [low, high] */
function is_in_closed_range(val, low, high, errmsg, context) {
  if ( val < low || val > high ) {
    errmsg.push(context+" not in range ["+low+", "+high+"]");
    return false;
  }
  return true;
}

/* attempt to guess the coordinate format from input */
function guess_coord_fmt(inp) {
  /* split trimmed input into tokens */
  var tok = trim(inp).split(/:|\s+/g);
  if ( tok ) {
    if ( tok.length == 1 ) {
      if ( is_float(tok[0]) ) {
        return 'dd'; /* exactly 1 floating point number */
      }
    } else if ( tok.length == 3 ) {
      if ( is_integer(tok[0]) && is_integer(tok[1]) && is_float(tok[2]) ) {
        return 'sex'; /* exactly 3 numbers, first 2 are integers */
      }
    }
  }
  /* who knows what we got? */
  return 'undef';
}

/* return true when coordinates are valid in decimal degrees */
function is_dd_ra_coord_valid(ra, errmsg) {
  /* ra format: DDD.DDD */
  var tok = trim(ra).split(/:|\s+/g);
  if ( ! tok || tok.length != 1 ) {
    errmsg.push("ra/lon not in DDD.DDD format");
    return false;
  }
  if ( ! is_float(tok[0]) ) {
    errmsg.push("ra/lon not floating point value");
    return false;
  }
  if ( ! is_in_closed_range(tok[0], 0.0, 360.0, errmsg, "ra/lon") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when coordinates are valid in decimal degrees */
function is_dd_dec_coord_valid(dec, errmsg) {
  /* dec format: [+/-]DD.DDD */
  var tok = trim(dec).split(/:|\s+/g);
  if ( ! tok || tok.length != 1 ) {
    errmsg.push("dec/lat not in [+/-]DD.DDD format");
    return false;
  }
  if ( ! is_float(tok[0]) ) {
    errmsg.push("dec/lat not floating point value");
    return false;
  }
  if ( ! is_in_closed_range(tok[0], -90.0, 90.0, errmsg, "dec/lat") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when coordinates are valid in sexagesimal */
function is_sex_ra_coord_valid(ra, errmsg) {
  /* ra format: HH:MM:SS.SSS */
  var tok = trim(ra).split(/:|\s+/g);
  if ( ! tok || tok.length != 3 ) {
    errmsg.push("ra not exactly 3 numbers");
    return false;
  }
  if ( ! is_integer(tok[0]) || ! is_integer(tok[1]) || ! is_float(tok[2]) ) {
    errmsg.push("ra not in HH:MM:SS.SSS format");
    return false;
  }
  if ( ! is_in_closed_range(tok[0], 0.0, 23.0, errmsg, "ra hours") ) {
    return false;
  }
  if ( ! is_in_closed_range(tok[1], 0.0, 59.0, errmsg, "ra minutes") ) {
    return false;
  }
  if ( ! is_in_open_range(tok[2], 0.0, 60.0, errmsg, "ra seconds") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when coordinates are valid in sexagesimal */
function is_sex_dec_coord_valid(dec, errmsg) {
  /* dec format: [+/-]DD:MM:SS.SSS */
  var tok = trim(dec).split(/:|\s+/g);
  if ( ! tok || tok.length != 3 ) {
    errmsg.push("dec not exactly 3 numbers, '"+dec+"'");
    return false;
  }
  if ( ! is_integer(tok[0]) || ! is_integer(tok[1]) || ! is_float(tok[2]) ) {
    errmsg.push("dec not in [+/-]DD:MM:SS.SSS format");
    return false;
  }
  if ( ! is_in_closed_range(tok[0], -90.0, 90.0, errmsg, "dec degrees") ) {
    return false;
  }
  if ( ! is_in_closed_range(tok[1], 0.0, 59.0, errmsg, "dec minutes") ) {
    return false;
  }
  if ( ! is_in_open_range(tok[2], 0.0, 60.0, errmsg, "dec seconds") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when coordinates are valid */
function is_ra_coord_valid(value, errmsg) {
  var fmt = guess_coord_fmt(value);
  if ( ! ['dd', 'sex'].any(function(f) { return fmt == f; }) ) {
    errmsg.push("unable to guess coordinate format");
    return false;
  }
  if ( fmt == 'dd' ) {
    return is_dd_ra_coord_valid(value, errmsg);
  }
  return is_sex_ra_coord_valid(value, errmsg);
}

function is_dec_coord_valid(value, errmsg) {
  var fmt = guess_coord_fmt(value);
  if ( ! ['dd', 'sex'].any(function(f) { return fmt == f; }) ) {
    errmsg.push("unable to guess coordinate format");
    return false;
  }
  if ( fmt == 'dd' ) {
    return is_dd_dec_coord_valid(value, errmsg);
  }
  return is_sex_dec_coord_valid(value, errmsg);
}

function is_lon_coord_valid(value, errmsg) {
  var fmt = guess_coord_fmt(value);
  if ( ! ['dd', 'sex'].any(function(f) { return fmt == f; }) ) {
    errmsg.push("unable to guess coordinate format");
    return false;
  }
  if ( fmt == 'dd' ) {
    return is_dd_ra_coord_valid(value, errmsg);
  }
  errmsg.push("sexagesimal format not supported for longitude");
  return false;
}

function is_lat_coord_valid(value, errmsg) {
  var fmt = guess_coord_fmt(value);
  if ( ! ['dd', 'sex'].any(function(f) { return fmt == f; }) ) {
    errmsg.push("unable to guess coordinate format");
    return false;
  }
  if ( fmt == 'dd' ) {
    return is_dd_dec_coord_valid(value, errmsg);
  }
  errmsg.push("sexagesimal format not supported for latitude");
  return false;
}

/* return true when time is valid gregorian date */
function is_greg_time_valid(greg, errmsg) {
  /* greg format: YYYY MM DD hh mm ss.s */
  var tok = trim(greg).split(/\s+/g);
  if ( ! tok || tok.length != 6 ) {
    errmsg.push("date and time not exactly 6 numbers");
    return false;
  }
  for ( var i = 0; i < 5; ++i ) {
    if ( ! is_integer(tok[i]) ) {
      errmsg.push("'"+tok[i]+"' is not an integer");
      return false;
    }
  }
  if ( ! is_float(tok[5]) ) {
    errmsg.push("seconds must be a number, '"+tok[5]+"'");
    return false;
  }
  if ( ! is_in_open_range(tok[0], 1980, 2050, errmsg, "year") )   return false;
  if ( ! is_in_closed_range(tok[1], 1, 12, errmsg, "month") )     return false;
  if ( ! is_in_closed_range(tok[2], 1, 31, errmsg, "day") )       return false;
  if ( ! is_in_closed_range(tok[3], 0, 23, errmsg, "hour") )      return false;
  if ( ! is_in_closed_range(tok[4], 0, 59, errmsg, "minutes") )   return false;
  if ( ! is_in_open_range(tok[5], 0.0, 60.0, errmsg, "seconds") ) return false;
  /* the Date class constructor attempts to adjust the date it is given when
     date is invalid, so we use that to detect invalid dates from leap years */
  var date = new Date(tok[0], tok[1]-1, tok[2], 0, 0, 0.0);
  if ( date.getMonth() != (tok[1]-1) || date.getDate() != tok[2] ) {
    errmsg.push("date is not valid");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when time is valid ccsds date */
function is_ccsds_time_valid(ccsds, errmsg) {
  /* ccsds format: YYYY-MM-DDThh:mm:ss.sZ */
  var tok = trim(ccsds).split(/-|T|:/g);
  if ( ! tok || tok.length != 6 ) {
    errmsg.push("date and time not exactly 6 numbers");
    return false;
  }
  for ( var i = 0; i < 5; ++i ) {
    if ( ! is_integer(tok[i]) ) {
      errmsg.push("'"+tok[i]+"' is not an integer");
      return false;
    }
  }
  if ( ! tok[5].match(/Z$/g) ) {
    errmsg.push("UTC (Coordinated Universal Time) requires terminating 'Z'");
    return false;
  }
  tok[5] = tok[5].replace(/Z$/g, ''); /* strip the terminating 'Z' */
  if ( ! is_float(tok[5]) ) {
    errmsg.push("seconds must be a number, '"+tok[5]+"'");
    return false;
  }
  if ( ! is_in_open_range(tok[0], 1980, 2050, errmsg, "year") )   return false;
  if ( ! is_in_closed_range(tok[1], 1, 12, errmsg, "month") )     return false;
  if ( ! is_in_closed_range(tok[2], 1, 31, errmsg, "day") )       return false;
  if ( ! is_in_closed_range(tok[3], 0, 23, errmsg, "hour") )      return false;
  if ( ! is_in_closed_range(tok[4], 0, 59, errmsg, "minutes") )   return false;
  if ( ! is_in_open_range(tok[5], 0.0, 60.0, errmsg, "seconds") ) return false;
  /* the Date class constructor attempts to adjust the date it is given when
     date is invalid, so we use that to detect invalid dates from leap years */
  var date = new Date(tok[0], tok[1]-1, tok[2], 0, 0, 0.0);
  if ( date.getMonth() != (tok[1]-1) || date.getDate() != tok[2] ) {
    errmsg.push("date is not valid");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when time is valid julian date */
function is_jd_time_valid(jd, errmsg) {
  /* jd format: any floating point value */
  jd = trim(jd);
  if ( ! is_float(jd) ) {
    errmsg.push("julian date must be a number");
    return false;
  }
  /* date range is [1980-01-06T00:00:00.0Z, 2050-01-01T00:00:00.0Z) */
  if ( ! is_in_open_range(jd, 2444244.5, 2469807.5, errmsg, "julian date") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when time is valid day of year */
function is_doy_time_valid(doy, errmsg) {
  /* doy format: YYYY DOY hh mm ss.s */
  var tok = trim(doy).split(/\s+/g);
  if ( ! tok || tok.length != 5 ) {
    errmsg.push("date and time not exactly 5 numbers");
    return false;
  }
  for ( var i = 0; i < 4; ++i ) {
    if ( ! is_integer(tok[i]) ) {
      errmsg.push("'"+tok[i]+"' is not an integer");
      return false;
    }
  }
  if ( ! is_float(tok[4]) ) {
    errmsg.push("seconds must be a number, '"+tok[5]+"'");
    return false;
  }
  if ( !is_in_open_range(tok[0], 1980, 2050, errmsg, "year") )     return false;
  if ( !is_in_closed_range(tok[1], 1, 366, errmsg, "day of year") )return false;
  if ( !is_in_closed_range(tok[2], 0, 23, errmsg, "hour") )        return false;
  if ( !is_in_closed_range(tok[3], 0, 59, errmsg, "minutes") )     return false;
  if ( !is_in_open_range(tok[4], 0.0, 60.0, errmsg, "seconds") )   return false;
  /* the Date class constructor attempts to adjust the date it is given when
     date is invalid, so we use that to detect invalid dates from leap years */
  if ( tok[1] == 366 ) {
    var date = new Date(tok[0], 1, 29, 0, 0, 0.0);
    if ( date.getMonth() != 1 || date.getDate() != 29 ) {
      errmsg.push("date is not a leap year, '"+tok[0]+"'");
      return false;
    }
  }
  return true; /* passed all tests, must be valid */
}

/* return true when time is valid Galex time */
function is_galex_time_valid(galex, errmsg) {
  /* galex format: seconds since Jan 6 1980 0:00 GMT */
  galex = trim(galex);
  if ( ! is_float(galex) ) {
    errmsg.push("Galex time must be a number");
    return false;
  }
  /* date range is [1980-01-06T00:00:00.0Z, 2050-01-01T00:00:00.0Z) */
  if ( ! is_in_open_range(galex, 0.00, 2208643200.00, errmsg, "Galex time") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when observation name is valid */
function is_observation_name_valid(name, errmsg) {
  name = trim(name);
  if ( name.length > 30 ) {
    errmsg.push("Observation name must be <= 30 characters.");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* observation name validation in GPW, is different from standalone tools */
function is_observation_name_valid_gpw(name, errmsg) {
  /* standalone validation */
  if ( ! is_observation_name_valid(name, errmsg) ) {
    return false;
  }
  /* some additional validation */
  name = trim(name);
  if ( name == '' ) {
    errmsg.push("missing required value");
    return false;
  } else if ( name.search(/[^\w-\+\._ ]/) != -1 ) {
    errmsg.push(
    "observation names can only contain spaces or 'a-z A-Z 0-9 _ - + .'");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when science target name is valid */
function is_science_target_name_valid(name, errmsg) {
  name = trim(name);
  if ( name.length > 30 ) {
    errmsg.push("Science target name must be <= 30 characters.");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when science target diameter is valid */
function is_science_target_diam_valid(diam, errmsg) {
  diam = trim(diam);
  if ( ! is_float(diam) ) {
    errmsg.push("Diameter must be a number");
    return false;
  }
  if ( ! is_in_closed_range(diam, 0.0, 62.0, errmsg, "Diameter") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when science target is completely within the useful fov */
function is_science_target_within_fov(ra1, dec1, ra2, dec2, diam, errmsg) {
  /* convert all inputs to decimal degrees */
  if ( guess_coord_fmt(ra1)   == 'sex' ) { ra1  = sex_ra_to_dd(ra1); }
  if ( guess_coord_fmt(dec1)  == 'sex' ) { dec1 = sex_dec_to_dd(dec1); }
  if ( guess_coord_fmt(ra2)   == 'sex' ) { ra2  = sex_ra_to_dd(ra2); }
  if ( guess_coord_fmt(dec2)  == 'sex' ) { dec2 = sex_dec_to_dd(dec2); }
  /* compute the angular separation between positions */
  var d2r = Math.PI / 180.0;
  ra1 *= d2r; dec1 *= d2r; ra2 *= d2r; dec2 *= d2r;
  var s = Math.sin(dec1) * Math.sin(dec2) + 
    Math.cos(dec1) * Math.cos(dec2) * Math.cos(ra1 - ra2);
  if      ( s > 1 )  { s = 1; }
  else if ( s < -1 ) { s = -1; }
  var angsep_deg = Math.acos(s) / d2r;
  /* compare the angular separation, in degrees, with the useful fov */
  var radius_deg = 0.5 * diam/60.0;
  var fov_deg = 0.6;
  var delta_deg = (fov_deg - radius_deg) - angsep_deg;
  if ( delta_deg < 0 ) {
    var delta_amin = Math.abs(delta_deg * 60.0);
    errmsg.push("Science target outside field of view by ~"+
      parseInt(Math.ceil(delta_amin))+"'.");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when gmap plot width is valid */
function is_gmap_plot_width_valid(width, errmsg) {
  width = trim(width);
  if ( ! is_float(width) ) {
    errmsg.push("Plot width must be a number");
    return false;
  }
  if ( ! is_in_closed_range(width, 3.0, 30.0, errmsg, "Plot width") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when toast point search fovr is valid */
function is_toast_point_fovr_valid(fovr, errmsg) {
  fovr = trim(fovr);
  if ( ! is_float(fovr) ) {
    errmsg.push("Point search FOVR must be a number");
    return false;
  }
  if ( ! is_in_closed_range(fovr, 0.1, 0.55, errmsg, "Point search FOVR") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when toast cone search fovr is valid */
function is_toast_cone_fovr_valid(fovr, errmsg) {
  fovr = trim(fovr);
  if ( ! is_float(fovr) ) {
    errmsg.push("Cone search radius must be a number");
    return false;
  }
  if ( ! is_in_closed_range(fovr, 0.1, 15.0, errmsg, "Cone search radius") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when redshift is valid */
function is_redshift_valid(rs, errmsg) {
  rs = trim(rs);
  if ( ! is_float(rs) ) {
    errmsg.push("Redshift must be a number");
    return false;
  }
  if ( ! is_in_open_range(rs, 0.0, 10.0, errmsg, "Redshift") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when wavelength is valid */
function is_wavelength_valid(wl, errmsg) {
  wl = trim(wl);
  if ( ! is_float(wl) ) {
    errmsg.push("Wavelength must be a number");
    return false;
  }
  if ( ! is_in_closed_range(wl, 500.0, 10000.0, errmsg, "Wavelength") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when zody flux is valid */
function is_zody_flux_valid(flux, errmsg) {
  flux = trim(flux);
  if ( ! is_float(flux) ) {
    errmsg.push("Count rate must be a number");
    return false;
  }
  if ( ! is_in_closed_range(flux, 0.0, 1000000.0, errmsg, "Count rate") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when requested exposure time is valid */
function is_req_exp_time_valid(exp_time, errmsg) {
  exp_time = trim(exp_time);
  if ( ! is_float(exp_time) ) {
    errmsg.push("Requested exposure time must be a number");
    return false;
  }
  if ( exp_time < Number.MIN_VALUE ) {
    errmsg.push("Requested exposure time must be a positive number");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when overlaps thresh is valid */
function is_overlaps_thresh_valid(thresh, errmsg) {
  thresh = trim(thresh);
  if ( ! is_float(thresh) ) {
    errmsg.push("Overlaps threshold must be a number");
    return false;
  }
  if ( ! is_in_closed_range(thresh, 0.01, 1.0, errmsg, "Overlaps threshold") ) {
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* return true when parameters needed for overlaps test are set */
function is_overlaps_test_valid(test, errmsg) {
  var test_cbox = $('overlaps_test');
  if ( test_cbox.checked == true ) {
    var search_type = getValueOfFirstCheckedRadio(
      $('point_search', 'box_search', 'cone_search')
    );
    if ( search_type != 'point_search' ) {
      errmsg.push("A <em>Point Search</em> must be used "+
        "when the overlap test is enabled");
      return false;
    }
    var checked_ow = $A($('tool')['ow_type']).findAll(
      function(e) { return e.checked; } );
    if ( checked_ow.length != 1 ) {
      errmsg.push("Exactly one <em>Optics Wheel Setting</em> must be chosen "+
        "when the overlap test is enabled");
      return false;
    }
    var req_exp_time = trim($('req_exp_time').value);
    if ( ! is_float(req_exp_time) ) {
      errmsg.push("A <em>Requested Exposure Time</em> must be specified "+
        "when the overlap test is enabled");
      return false;
    }
  }
  return true; /* passed all tests, must be valid */
}

/* return true when the value is a valid floating point number */
function is_float_value_valid(inp, errmsg) {
  inp = trim(inp);
  if ( ! is_float(inp) ) {
    errmsg.push("Value must be floating point number");
    return false;
  }
  return true; /* passed all tests, must be valid */
}

/* convert sexagesimal ra HH:MM:SS.SSS to decimal degrees */
function sex_ra_to_dd(ra) {
  var tok = trim(ra).split(/:|\s+/g); /* assume value is valid */
  return 15.0 * (parseFloat(tok[0]) + 
    parseFloat(tok[1])/60.0 + parseFloat(tok[2])/3600.0);
}

/* convert sexagesimal dec [+/-]DD:MM:SS.SSS to decimal degrees */
function sex_dec_to_dd(dec) {
  var tok = trim(dec).split(/:|\s+/g); /* assume value is valid */
  var sign = parseFloat(tok[0]) < 0.0 || tok[0].charAt(0) == '-' ? -1.0 : 1.0;
  return parseFloat(tok[0]) + 
    (sign * parseFloat(tok[1])/60.0) + (sign * parseFloat(tok[2])/3600.0);
}

/* display the error message element */
function showErrMsg(elem, msg) {
  if ( elem ) {
    elem.innerHTML = msg ? msg : 'value not valid';
    elem.removeClassName('status');
    elem.addClassName('error');
    elem.show();
  }
}

/* hide the error message element */
function hideErrMsg(elem) {
  if ( elem ) {
    elem.removeClassName('error');
    elem.hide();
  }
}

/* display the status message element */
function showStatusMsg(elem, msg) {
  if ( elem ) {
    elem.innerHTML = msg ? msg : 'validated!';
    elem.removeClassName('error');
    elem.addClassName('status');
    elem.show();
  }
}

/* hide the status message element */
function hideStatusMsg(elem) {
  if ( elem ) {
    elem.removeClassName('status');
    elem.hide();
  }
}

/* return true when a value is required for the element */
function isRequiredElem(elem) {
  var is_required = false;
  if ( elem ) {
    is_required = elem.hasClassName('required') ? true : false;
  }
  return is_required;
}

/* 'coordinates' component validation */
function checkCoordinatesValid() {
  var valid = true;
  var elem = $('coordinates');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    var cs_type = getValueOfFirstCheckedRadio(
      $( 'eqj2000', 'eqb1950', 'galactic' )
    );
    /* ignore coordinate checks when search method component exists
       and the user has selected a box search */
    var ignore_coords = $('search_method') != null &&
      getValueOfFirstCheckedRadio(
        $('point_search', 'box_search', 'cone_search') ) == 'box_search';
    if ( ! ignore_coords ) {
      if ( cs_type == 'eqj2000' || cs_type == 'eqb1950' ) {
        valid_funcs.push({ id: 'ra',       func: is_ra_coord_valid });
        valid_funcs.push({ id: 'dec',      func: is_dec_coord_valid });
      } else if ( cs_type == 'galactic' ) {
        valid_funcs.push({ id: 'lon',      func: is_lon_coord_valid });
        valid_funcs.push({ id: 'lat',      func: is_lat_coord_valid });
      }
      /* ignore science target diameter checks when search method component
         exists and the user has selected a cone search */
      var ignore_diam = $('search_method') != null &&
        getValueOfFirstCheckedRadio(
          $('point_search', 'box_search', 'cone_search') ) == 'cone_search';
      if ( ! ignore_diam ) {
        valid_funcs.push({id:'scitarg_diam',func:is_science_target_diam_valid});
      }
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'observations_and_targets' component validation */
function checkObservationsAndTargetsValid() {
  var valid = true;
  var elem = $('observations_and_targets');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var cs_type, valid_funcs = [];
    /* any leftover text in the bulk loader will result in failure 
       regardless of whether component is required */
    var bulk_elem = $('obs_scitarg_bulk_text');
    if ( bulk_elem ) {
      var bulk_text = trim(bulk_elem.value);
      if ( bulk_text != '' ) {
        valid = false;
        bulk_elem.addClassName('invalid');
        showErrMsg($('obs_scitarg_bulk_text_msg'), 
          'detected leftover text in the Coordinate Bulk Loader');
        if ( ! $('obs_scitarg_bulk').visible() ) {
          $('obs_scitarg_bulk').show();
        }
      } else { 
        bulk_elem.removeClassName('invalid');
        hideErrMsg($('obs_scitarg_bulk_text_msg'));
      }
    }
    /* determine the coordinate system type */
    var tool = $('tool');
    if ( tool ) {
      if ( tool['gpw_coord_sys_type'] ) {
        /* wizard uses a different element to obtain cs type info */
        cs_type = getValueOfFirstCheckedRadio(
          $( 'gpw_eqj2000', 'gpw_eqb1950', 'gpw_galactic' )
        );
      } else {
        cs_type = getValueOfFirstCheckedRadio(
          $( 'eqj2000', 'eqb1950', 'galactic' )
        );
      }
    }
    /* set validity funcs from coordinate system type */
    if ( cs_type == 'eqj2000' || cs_type == 'eqb1950' ) {
      valid_funcs = [
        { id: '',         func: is_observation_name_valid },
        { id: '',         func: is_ra_coord_valid },
        { id: '',         func: is_dec_coord_valid },
        { id: '',         func: is_science_target_diam_valid }
      ];
    } else if ( cs_type == 'galactic' ) {
      valid_funcs = [
        { id: '',         func: is_observation_name_valid },
        { id: '',         func: is_lon_coord_valid },
        { id: '',         func: is_lat_coord_valid },
        { id: '',         func: is_science_target_diam_valid }
      ];
    }
    var ra1, dec1, ra2, dec2, diam, obs_nums = getObservationNums();
    for ( var i = 0, l = obs_nums.length; i < l; ++i ) {
      var obs_id = 'obs'+obs_nums[i];
      valid_funcs[0].id   = obs_id+'_name';
      valid_funcs[0].func = is_observation_name_valid;
      valid_funcs[1].id   = obs_id+'_ra';
        ra1               = $(obs_id+'_ra').value;
      valid_funcs[2].id   = obs_id+'_dec';
        dec1              = $(obs_id+'_dec').value;
      if ( ! checkElemValid(valid_funcs, required) ) {
        valid = false;
      }
      var scitarg_nums = getScienceTargetNums($(obs_id+'_scitarg_table'));
      for ( var j = 0, ll = scitarg_nums.length; j < ll; ++j ) {
        var scitarg_id = 'scitarg'+scitarg_nums[j];
        valid_funcs[0].id   = obs_id+'_'+scitarg_id+'_name';
        valid_funcs[0].func = is_science_target_name_valid;
        valid_funcs[1].id   = obs_id+'_'+scitarg_id+'_ra';
        valid_funcs[2].id   = obs_id+'_'+scitarg_id+'_dec';
        valid_funcs[3].id   = obs_id+'_'+scitarg_id+'_diam';
        /* add default science target to observations without one */
        if ( required && j == 0 ) {
          var st_prefix = obs_id+'_'+scitarg_id;
          if ( $(st_prefix+'_name').value == "" ) {
            if ( $(obs_id+'_name').value != "" ) {
              $(st_prefix+'_name').value  = 
                $(obs_id+'_name').value.substring(0, 19)+' Sci. Targ.';
            }
          }
          if ( $(st_prefix+'_ra').value == "" ) {
            $(st_prefix+'_ra').value    = $(obs_id+'_ra').value;
          }
          if ( $(st_prefix+'_dec').value == "" ) {
            $(st_prefix+'_dec').value   = $(obs_id+'_dec').value;
          }
          if ( $(st_prefix+'_diam').value == "" ) {
            $(st_prefix+'_diam').value  = "0.1";
          }
        }
        if ( ! checkElemValid(valid_funcs, required) ) {
          valid = false;
        } else if ( required && ra1 != null && dec1 != null ) {
          /* compute angular separation between observation and targets */
          ra2   = $(obs_id+'_'+scitarg_id+'_ra').value;
          dec2  = $(obs_id+'_'+scitarg_id+'_dec').value;
          diam  = $(obs_id+'_'+scitarg_id+'_diam').value;
          if ( ra2 != null && dec2 != null ) {
            if ( diam == null ) { diam = 0.1; } /* default is point source */
            var errmsg = new Array();
            if ( ! is_science_target_within_fov(
              ra1,dec1,ra2,dec2,diam,errmsg) ) {
              valid = false;
              showErrMsg($(obs_id+'_'+scitarg_id+'_name_msg'), errmsg.pop());
              $(obs_id+'_'+scitarg_id+'_name').addClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_ra').addClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_dec').addClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_diam').addClassName('invalid');
            } else {
              showStatusMsg($(obs_id+'_'+scitarg_id+'_name_msg'));
              $(obs_id+'_'+scitarg_id+'_name').removeClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_ra').removeClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_dec').removeClassName('invalid');
              $(obs_id+'_'+scitarg_id+'_diam').removeClassName('invalid');
            }
          }
        }
      }
    }
    if ( ! valid ) {
      showErrMsg($('obs_scitarg_msg'), 'some of your entries are invalid');
    } else {
      showStatusMsg($('obs_scitarg_msg'));
    }
  }
  return valid;
}

/* 'reference_body' component validation */
function checkReferenceBodyValid() {
  var valid = true;
  var elem = $('reference_body');
  if ( elem ) {
    var form = $('tool');
    valid = checkCheckboxesValid(elem, form['ref_body'], 
      $('ref_body_msg'), 'reference body');
  }
  return valid;
}
 
/* 'optics_wheel' component validation */
function checkOpticsWheelValid() {
  var valid = true;
  var elem = $('optics_wheel');
  if ( elem ) {
    var form = $('tool');
    valid = checkCheckboxesValid(elem, form['ow_type'], 
      $('ow_type_msg'), 'optics wheel setting');
  }
  return valid;
}
 
/* 'surveys' component validation */
function checkSurveysValid() {
  var valid = true;
  var elem = $('surveys');
  if ( elem ) {
    var form = $('tool');
    valid = checkCheckboxesValid(elem, form['survey_type'], 
      $('survey_type_msg'), 'survey type');
  }
  return valid;
}
 
/* 'col_display' component validation */
function checkColDisplayValid() {
  var valid = true;
  var elem = $('col_display');
  if ( elem ) {
    var form = $('tool');
    valid = checkCheckboxesValid(elem, form['col_disp'], 
      $('col_disp_msg'), 'column for display');
  }
  return valid;
}

/* 'time' component validation */
function checkTimeValid() {
  var valid = true;
  var elem = $('time');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('greg_time').visible() ) {
      valid_funcs.push({ id: 'greg0',    func: is_greg_time_valid });
      valid_funcs.push({ id: 'greg1',    func: is_greg_time_valid });
    } else if ( $('ccsds_time').visible() ) {
      valid_funcs.push({ id: 'ccsds0',   func: is_ccsds_time_valid });
      valid_funcs.push({ id: 'ccsds1',   func: is_ccsds_time_valid });
    } else if ( $('jd_time').visible() ) {
      valid_funcs.push({ id: 'jd0',      func: is_jd_time_valid });
      valid_funcs.push({ id: 'jd1',      func: is_jd_time_valid });
    } else if ( $('doy_time').visible() ) {
      valid_funcs.push({ id: 'doy0',     func: is_doy_time_valid });
      valid_funcs.push({ id: 'doy1',     func: is_doy_time_valid });
    } else if ( $('galex_time').visible() ) {
      valid_funcs.push({ id: 'galex0',   func: is_galex_time_valid });
      valid_funcs.push({ id: 'galex1',   func: is_galex_time_valid });
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'search_method' component validation */
function checkSearchMethodValid() {
  var valid = true;
  var elem = $('search_method');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    var search_type = getValueOfFirstCheckedRadio(
      $('point_search', 'box_search', 'cone_search')
    );
    if ( search_type == 'point_search' ) {
      valid_funcs.push({ id: 'point_fovr', func: is_toast_point_fovr_valid });
    } else if ( search_type == 'box_search' ) {
        var cs_type = getValueOfFirstCheckedRadio(
          $( 'eqj2000', 'eqb1950', 'galactic' )
        );
        if ( cs_type == 'eqj2000' || cs_type == 'eqb1950' ) {
          valid_funcs.push({ id: 'p1ra',     func: is_ra_coord_valid });
          valid_funcs.push({ id: 'p1dec',    func: is_dec_coord_valid });
          valid_funcs.push({ id: 'p2ra',     func: is_ra_coord_valid });
          valid_funcs.push({ id: 'p2dec',    func: is_dec_coord_valid });
        } else if ( cs_type == 'galactic' ) {
          valid_funcs.push({ id: 'p1ra',     func: is_lon_coord_valid });
          valid_funcs.push({ id: 'p1dec',    func: is_lat_coord_valid });
          valid_funcs.push({ id: 'p2ra',     func: is_lon_coord_valid });
          valid_funcs.push({ id: 'p2dec',    func: is_lat_coord_valid });
        }
    } else if ( search_type == 'cone_search' ) {
      valid_funcs.push({ id: 'cone_fovr',  func: is_toast_cone_fovr_valid });
    }
    valid = checkElemValid(valid_funcs, required);
    /* for box searches that pass coordinate validation, also check width */
    if ( valid && search_type == 'box_search' ) {
      var ra1 = $('p1ra').value, dec1 = $('p1dec').value,
          ra2 = $('p2ra').value, dec2 = $('p2dec').value;
      /* convert all inputs to decimal degrees */
      if ( guess_coord_fmt(ra1)   == 'sex' ) { ra1  = sex_ra_to_dd(ra1); }
      if ( guess_coord_fmt(dec1)  == 'sex' ) { dec1 = sex_dec_to_dd(dec1); }
      if ( guess_coord_fmt(ra2)   == 'sex' ) { ra2  = sex_ra_to_dd(ra2); }
      if ( guess_coord_fmt(dec2)  == 'sex' ) { dec2 = sex_dec_to_dd(dec2); }
      var delta_ra = Math.abs(ra1 - ra2);
      if ( delta_ra > 180.0 ) { delta_ra = 360.0 - delta_ra; }
      if ( delta_ra > 20.0 ) { 
        showErrMsg($('box_search_method_msg'),
          'separation between points of '+delta_ra+'&deg; '+
          'exceeds 20&deg; limit for ra/lon');
        valid = false;
      } else {
        showStatusMsg($('box_search_method_msg'));
      }
      var delta_dec = Math.abs(dec1 - dec2);
      if ( delta_dec > 20.0 ) { 
        showErrMsg($('box_search_method_msg'),
          'separation between points of '+delta_dec+'&deg; '+
          'exceeds 20&deg; limit for dec/lat');
        valid = false;
      } else if ( valid ) { /* do not clear when previous tests failed */
        showStatusMsg($('box_search_method_msg'));
      }
      if ( delta_ra == 0.0 || delta_dec == 0.0 ) {
        showErrMsg($('box_search_method_msg'),
          'separation between points must be greater than 0.0&deg;');
        valid = false;
      } else if ( valid ) { /* do not clear when previous tests failed */
        showStatusMsg($('box_search_method_msg')); 
      }
    }
  }
  return valid;
}

/* 'tool_parameters' component validation */
function checkToolParametersValid() {
  var valid = true;
  var elem = $('tool_parameters');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [
      { id: 'gmap_plot_width', func: is_gmap_plot_width_valid },
      { id: 'req_exp_time', func: is_req_exp_time_valid },
      { id: 'overlaps_test', func: is_overlaps_test_valid }, 
      { id: 'overlaps_thresh', func: is_overlaps_thresh_valid }
    ];
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'etc_method' component validation */
function checkEtcMethodValid() {
  var valid = true;
  var elem = $('etc_method');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('exp_etc_method').visible() ) {
      valid_funcs.push( { id: 'exp_time', func: is_float_value_valid } );
    } else if ( $('snr_etc_method').visible() ) {
      valid_funcs.push( { id: 'snr_val',  func: is_float_value_valid } );
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'astro_object_properties' component validation */
function checkAstroObjectPropertiesValid() {
  var valid = true;
  var elem = $('astro_object_properties');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('star_object').visible() ) {
      valid_funcs.push( { id: 'star_temp',  func: is_float_value_valid } );
    } else if ( $('galaxy_object').visible() ) {
      valid_funcs.push( { id: 'gredshift',  func: is_redshift_valid } );
      valid_funcs.push( { id: 'extinction', func: is_float_value_valid } );
      valid_funcs.push( { id: 'escape',     func: is_float_value_valid } );
    } else if ( $('quasar_object').visible() ) {
      valid_funcs.push( { id: 'qredshift',  func: is_redshift_valid } );
    } else if ( $('white_dwarf_object').visible() ) {
      valid_funcs.push( { id: 'wd_temp',    func: is_float_value_valid } );
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'flux_method' component validation */
function checkFluxMethodValid() {
  var valid = true;
  var elem = $('flux_method');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('user_flux_method').visible() ) {
      valid_funcs.push( { id: 'wavelength',     func: is_wavelength_valid } );
      valid_funcs.push( { id: 'density',        func: is_float_value_valid } );
    } else if ( $('magnitude_method').visible() ) {
      valid_funcs.push( { id: 'app_magnitude',  func: is_float_value_valid } );
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'area_extinction' component validation */
function checkAreaExtinctionValid() {
  var valid = true;
  var elem = $('area_extinction');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('auto_extinct_method').visible() ) {
      valid_funcs.push({ id: 'auto_area', func: is_float_value_valid });
    } else if ( $('manual_extinct_method').visible() ) {
      valid_funcs.push({ id: 'manual_extinction', func: is_float_value_valid });
      valid_funcs.push({ id: 'manual_area', func: is_float_value_valid });
    } else if ( $('none_extinct_method').visible() ) {
      valid_funcs.push({ id: 'none_area', func: is_float_value_valid });
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'zody_calc' component validation */
function checkZodyCalcValid() {
  var valid = true;
  var elem = $('zody_calc');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [];
    if ( $('min_zody_method').visible() ) {
      /* do-nothing */
    } else if ( $('manual_zody_method').visible() ) {
      valid_funcs.push({ id: 'manual_nuv_zody', func: is_zody_flux_valid });
      valid_funcs.push({ id: 'manual_fuv_zody', func: is_zody_flux_valid });
    }
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* 'observation_information' component validation */
function checkObservationInformationValid() {
  var valid = true;
  var elem = $('observation_information');
  if ( elem ) {
    var required = isRequiredElem(elem);
    var valid_funcs = [
      { id: 'obs_name',       func: is_observation_name_valid },
      { id: 'scitarg_name',   func: is_science_target_name_valid }
    ];
    valid = checkElemValid(valid_funcs, required);
  }
  return valid;
}

/* return true when all the elements in the array are valid */
function checkElemValid(valid_funcs, required) {
  var valid = true;
  var errmsg = new Array();
  for ( var i = 0, l = valid_funcs.length; i < l; ++i ) {
    var elem = $(valid_funcs[i].id);
    var elem_msg = $(valid_funcs[i].id+'_msg');
    if ( elem ) {
      if ( elem.value ) {
        if ( ! valid_funcs[i].func(elem.value, errmsg) ) {
          valid = false;
          elem.addClassName('invalid');
          showErrMsg(elem_msg, errmsg.pop());
        } else {
          elem.removeClassName('invalid');
          showStatusMsg(elem_msg);
        }
      } else {
        if ( required ) {
          valid = false;
          elem.addClassName('invalid');
          showErrMsg(elem_msg, 'missing required value');
        } else {
          elem.removeClassName('invalid');
          hideErrMsg(elem_msg);
        }
      }
    }
    errmsg.clear();
  }
  return valid;
}

/* return true when at least one checkbox in the group is checked */
function checkCheckboxesValid(elem, all_cbox, msg_elem, msg) {
  var valid = true;
  if ( elem ) {
    var required = isRequiredElem(elem);
    var num_checked = 0;
    for ( var i = 0, l = all_cbox.length; i < l; ++i ) {
      if ( all_cbox[i].checked == true ) {
        ++num_checked;
      }
    }
    valid = ! required || (required && num_checked > 0);
    if ( ! valid ) {
      showErrMsg(msg_elem, 'at least one '+msg+' must be checked');
    } else {
      showStatusMsg(msg_elem);
    }
  }
  return valid;
}

/* return the synthesized validation status for the form */
function checkToolValid() {
  /* array of validation functions for form
     components that are not part of the form will return true */
  var valid_funcs = [
    { id: 'coordinates',              func: checkCoordinatesValid },
    { id: 'observations_and_targets', func: checkObservationsAndTargetsValid },
    { id: 'reference_body',           func: checkReferenceBodyValid },
    { id: 'optics_wheel',             func: checkOpticsWheelValid },
    { id: 'surveys',                  func: checkSurveysValid },
    { id: 'col_display',              func: checkColDisplayValid },
    { id: 'time',                     func: checkTimeValid },
    { id: 'search_method',            func: checkSearchMethodValid },
    { id: 'tool_parameters',          func: checkToolParametersValid },
    { id: 'etc_method',               func: checkEtcMethodValid },
    { id: 'astro_object_properties',  func: checkAstroObjectPropertiesValid },
    { id: 'flux_method',              func: checkFluxMethodValid },
    { id: 'area_extinction',          func: checkAreaExtinctionValid },
    { id: 'zody_calc',                func: checkZodyCalcValid },
    { id: 'observation_information',  func: checkObservationInformationValid }
  ];
  /* validate each component of the form */
  var num_valid = 0;
  for ( var i = 0, l = valid_funcs.length; i < l; ++i ) {
    if ( valid_funcs[i].func() ) {
      ++num_valid;
    } else {
      /* if there was a failure, make it visible to the user */
      if ( ! $(valid_funcs[i].id).visible() ) {
        $(valid_funcs[i].id).show();
      }
    }
  }
  /* display message when failures detected */
  if ( num_valid != valid_funcs.length ) {
    $('submit_msg').innerHTML =
      'Some of your inputs are invalid.<br />'+
      'Please fix the errors and try again.';
    $('submit_msg').removeClassName('pass');
    $('submit_msg').addClassName('fail');
    $('submit_msg').show();
  } else {
    $('submit_msg').innerHTML = 'All inputs are valid.';
    $('submit_msg').addClassName('pass');
    $('submit_msg').removeClassName('fail');
    $('submit_msg').show();
  }
  return num_valid == valid_funcs.length;
}

/* handle form submission */
function onSubmitTool(event) {
  if ( ! checkToolValid() ) {
    event.stop(); /* prevent the form submission from going through */
  }
  /* event.stop(); */ /* TODO: uncomment for debugging */
}

/* respond to click events for changing the appearance of the ui */
function onClickUi(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var form = $('tool');
    if ( form ) {
      var advanced_elem = form.getElementsByClassName('advanced');
      if ( elem == $('basic_ui') ) {
        /* hide all of the advanced elements in the form */
        for ( var i = 0, l = advanced_elem.length; i < l; ++i ) {
          if ( advanced_elem[i].visible() ) {
            advanced_elem[i].hide();
          }
        }
        /* disable the basic link and enable the advanced link */
        elem.removeClassName('clickable');
        $('advanced_ui').addClassName('clickable');
      } else if ( elem == $('advanced_ui') ) {
        /* show all of the advanced elements in the form */
        for ( var i = 0, l = advanced_elem.length; i < l; ++i ) {
          if ( ! advanced_elem[i].visible() ) {
            advanced_elem[i].show();
          }
        }
        /* disable the advanced link and enable the basic link */
        elem.removeClassName('clickable');
        $('basic_ui').addClassName('clickable');
      }
    }
  }
}

/* toggle visibility of the search expression */
function onClickToggleSearchExpr(event) {
  var elem = $('search_expr');
  if ( elem ) {
    if ( ! elem.visible() ) {
      elem.show();
    } else {
      elem.hide();
    }
  }
}

/* toggle the visibility of the bulk loader */
function onClickToggleBulkLoader(event) {
  var elem  = $('obs_scitarg_bulk');
  if ( elem ) {
    if ( ! elem.visible() ) {
      elem.show();
    } else {
      elem.hide();
    }
  }
}

/* get values from the table into the coordinate bulk loader */
function onClickGetBulkLoader(event) {
  var elem = $('observations_and_targets');
  if ( elem ) {
    var bulk_text = '';
    var obs_nums = getObservationNums();
    var name, ra, dec, diam;
    for ( var i = 0, l = obs_nums.length; i < l; ++i ) {
      var obs_id = 'obs'+obs_nums[i];
      name = $(obs_id+'_name').value;
      ra   = $(obs_id+'_ra').value;
      dec  = $(obs_id+'_dec').value;
      /* always write observation, regardless if it is empty */
      bulk_text += name+', '+ra+', '+dec+'\n';
      var scitarg_nums = getScienceTargetNums($(obs_id+'_scitarg_table'));
      for ( var j = 0, ll = scitarg_nums.length; j < ll; ++j ) {
        var scitarg_id = 'scitarg'+scitarg_nums[j];
        name = $(obs_id+'_'+scitarg_id+'_name').value;
        ra   = $(obs_id+'_'+scitarg_id+'_ra').value;
        dec  = $(obs_id+'_'+scitarg_id+'_dec').value;
        diam = $(obs_id+'_'+scitarg_id+'_diam').value;
        /* only write science targets that are not empty */
        if ( name || ra || dec || diam ) {
          bulk_text += name+', '+ra+', '+dec+', '+diam+'\n';
        }
      }
    }
    $('obs_scitarg_bulk_text').value = bulk_text;
  }
}

/* put values from the coordinate bulk loader into the table */
function onClickPutBulkLoader(event) {
  var elem = $('observations_and_targets');
  if ( elem ) {
    /* parse the bulk loader text into an array of objects */
    var errmsg = new Array();
    var bulk_text = $('obs_scitarg_bulk_text').value;
    var bulk_data = parseBulkLoaderText(bulk_text, errmsg);
    if ( errmsg.length > 0 ) {
      $('obs_scitarg_bulk_text').addClassName('invalid');
      showErrMsg($('obs_scitarg_bulk_text_msg'), errmsg.pop());
      return;
    } else if ( bulk_data.length == 0 ) {
      $('obs_scitarg_bulk_text').addClassName('invalid');
      showErrMsg($('obs_scitarg_bulk_text_msg'), 'nothing to copy');
      return;
    } else {
      $('obs_scitarg_bulk_text').removeClassName('invalid');
      hideErrMsg($('obs_scitarg_bulk_text_msg'));
      hideErrMsg($('obs_scitarg_msg'));
      $('obs_scitarg_bulk_text').value = '';
    }
    /* remove everything except observation # 1 */
    var obs_nums = getObservationNums();
    for ( var i = obs_nums.length-1; i > 0; --i ) {
      $('obs'+obs_nums[i]).remove();
    }
    renumberObservation($('obs1'), 1, 1);
    /* remove all science targets in observation # 1 except the first */
    var table = $('obs1_scitarg_table');
    for ( var j = table.rows.length-1; j > 1; --j ) {
      $(table.rows[j]).remove();
    }
    renumberScienceTarget($('obs1_scitarg_table').rows[1], 1, 1);
    /* rebuild the observations and targets */
    var obs_num = 0, scitarg_num = 0, prefix;
    var obs_div = $('obs1').cloneNode(true);
    var scitarg_row = $('obs1_scitarg_table').rows[1].cloneNode(true);
    for ( var k = 0, l = bulk_data.length; k < l; ++k ) {
      if ( ! bulk_data[k]['diam'] ) {   /* observation */
        ++obs_num;                      /* increment the observation count */
        scitarg_num = 0;                /* reset the science target count */
        if ( obs_num > 1 ) {            /* observation div does not exist */
          var new_div = obs_div.cloneNode(true);
          renumberObservation(new_div, 1, obs_num);
          elem.insertBefore(new_div, $('obs_scitarg_msg'));
        }
        prefix = 'obs'+obs_num;
        $(prefix+'_name').value  = bulk_data[k]['name'];
        $(prefix+'_ra').value    = bulk_data[k]['ra'];
        $(prefix+'_dec').value   = bulk_data[k]['dec'];
      } else {                          /* science target */
        ++scitarg_num;                  /* increment the science target count */
        if ( scitarg_num > 1 ) {        /* science target row does not exist */
          var new_row = scitarg_row.cloneNode(true);
          renumberObservation(new_row, 1, obs_num);
          renumberScienceTarget(new_row, 1, scitarg_num);
          table = $('obs'+obs_num+'_scitarg_table');
          table.rows[table.rows.length-1].parentNode.appendChild(new_row);
        }
        prefix = 'obs'+obs_num+'_scitarg'+scitarg_num;
        $(prefix+'_name').value  = bulk_data[k]['name'];
        $(prefix+'_ra').value    = bulk_data[k]['ra'];
        $(prefix+'_dec').value   = bulk_data[k]['dec'];
        $(prefix+'_diam').value  = bulk_data[k]['diam'];
      }
    }
    /* invoke the change event to synchronize form and table */
    $('obs1_ra', 'obs1_dec', 'obs1_name', 'obs1_scitarg1_name').each(
      syncFormAndTable
    );
  }
}

/* synchronize values for elements represented in the form and table */
function syncFormAndTable(elem) {
  if ( elem ) {
    /* map elements that need to synchronize */
    var sync_map = {
      ra:                 'obs1_ra',
      dec:                'obs1_dec',
      lat:                'obs1_dec',
      lon:                'obs1_ra',
      obs_name:           'obs1_name',
      scitarg_name:       'obs1_scitarg1_name',
      obs1_ra:            'ra',
      obs1_dec:           'dec',
      obs1_name:          'obs_name',
      obs1_scitarg1_name: 'scitarg_name'
    };
    var sync_elem = $(sync_map[elem.id]);
    if ( sync_elem ) {
      sync_elem.value = elem.value;
    }
  }
}

/* synchronize changes to the first observation between form and table */
function onChangeFirstObservation(event) {
  var elem = Event.element(event);
  syncFormAndTable(elem);
}

/*
** the functions that follow toggle the visibility of form elements
** based on various click events; look at the corresponding markup
** for a better understanding
*/

function onClickCoordSysType(event) {
  var elem = Event.element(event);
  if ( elem ) {
    switch ( $F(elem) ) {
      case 'eqj2000':
      case 'eqb1950':
        $('lonlat').hide();
        $('radec').show();
        break;
      case 'galactic':
        $('radec').hide();
        $('lonlat').show();
        break;
    }
  }
}

function onClickRefBody(event) {
  var element = Event.element(event);
  if ( element ) {
    var form = $('tool');
    toggleCheckboxes(form['ref_body'], $('all_ref_body'));
  }
}

function onClickOwType(event) {
  var element = Event.element(event);
  if ( element ) {
    var form = $('tool');
    toggleCheckboxes(form['ow_type'], $('all_ow'));
  }
}

function onClickSurveyType(event) {
  var element = Event.element(event);
  if ( element ) {
    var form = $('tool');
    toggleCheckboxes(form['survey_type'], $('all_surveys'));
  }
}

function onClickColDisp(event) {
  var element = Event.element(event);
  if ( element ) {
    var form = $('tool');
    toggleCheckboxes(form['col_disp'], $('all_cols'));
  }
}

function onClickShowArchObs(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var form = $('tool');
    if ( elem.checked ) {
      /* enable the other checkboxes */
      $A(form['ow_type']).each(Form.Element.enable);
      $A(form['survey_type']).each(Form.Element.enable);
    } else {
      /* disable the other checkboxes */
      $A(form['ow_type']).each(Form.Element.disable);
      $A(form['survey_type']).each(Form.Element.disable);
    } 
  }
}

function onClickTimeSysType(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['greg', 'ccsds', 'jd', 'doy', 'galex'];
    toggleRadio(elem, all_elem, '_time');
  }
}

function onClickEtcMethodType(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['exp', 'snr'];
    toggleRadio(elem, all_elem, '_etc_method');
  }
}

function onClickObjectType(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['star', 'galaxy', 'quasar', 'white_dwarf', 'sed'];
    toggleRadio(elem, all_elem, '_object');
  }
}

function onClickFluxMethod(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['user_flux', 'magnitude'];
    toggleRadio(elem, all_elem, '_method');
  }
}

function onClickExtinctMethod(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['none', 'auto', 'manual'];
    toggleRadio(elem, all_elem, '_extinct_method');
  }
}

function onClickZodyMethod(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['min', 'manual'];
    toggleRadio(elem, all_elem, '_zody_method');
  }
}

function onClickSearchMethod(event) {
  var elem = Event.element(event);
  if ( elem ) {
    var all_elem = ['point_search', 'box_search', 'cone_search'];
    toggleRadio(elem, all_elem, '_method');
    if ( elem.value == 'box_search' ) {
      $('coordinate_values').hide();
    } else if ( elem.value == 'cone_search' ) {
      $('scitarg_param').hide();
      $('coordinate_values').show();
    } else {
      $('scitarg_param').show();
      $('coordinate_values').show();
    }
  }
}

function toggleRadio(elem, all_elem, suffix) {
  for ( var i = 0, l = all_elem.length; i < l; ++i ) {
    if ( elem == $(all_elem[i]) ) {
      $(all_elem[i]+suffix).show();
    } else {
      $(all_elem[i]+suffix).hide();
    }
  }
}

function toggleCheckboxes(all_cbox, all) {
  if ( all_cbox != null && all != null ) {
    if ( all.checked == true ) {
      /* when 'all' is checked, disable others in group */
      for ( var i = 0, l = all_cbox.length; i < l; ++i ) {
        if ( all_cbox[i] != all ) {
          all_cbox[i].checked = true;
          all_cbox[i].disabled = true;
        }
      }
    } else {
      /* when 'all' is not checked, enable others in group */
      for ( var i = 0, l = all_cbox.length; i < l; ++i ) {
        if ( all_cbox[i] != all ) {
          all_cbox[i].disabled = false;
        }
      }
    }
  }
}

function setFormHandler(elem, eventName, handler) {
  if ( elem ) {
    for ( var i = 0, l = elem.length; i < l; ++i ) {
      Event.observe(elem[i], eventName, handler);
    }
  }
}

function getValueOfFirstCheckedRadio(all_elem) {
  var value, elem;
  for ( var i = 0, l = all_elem.length; i < l; ++i ) {
    elem = $(all_elem[i]); /* get prototype-extended element */
    if ( elem && elem.checked ) {
      value = elem.value;
      break;
    }
  }
  return value; 
}

/* intialize event handlers after page has loaded */
function initGIPS() {
  /* set interface control event handlers */
  if ( $('basic_ui') ) {
    Event.observe($('basic_ui'), 'click', onClickUi);
  }
  if ( $('advanced_ui') ) {
    Event.observe($('advanced_ui'), 'click', onClickUi);
  }
  if ( $('search_expr_show') ) {
    Event.observe($('search_expr_show'), 'click', onClickToggleSearchExpr);
  }
  /* set 'observations_and_targets' event handlers */
  var obs_scitarg = $('observations_and_targets');
  if ( obs_scitarg ) {
    if ( $('obs_scitarg_bulk_show') ) {
      Event.observe($('obs_scitarg_bulk_show'), 'click',
        onClickToggleBulkLoader);
    }
    if ( $('obs_scitarg_bulk_show2') ) {
      Event.observe($('obs_scitarg_bulk_show2'), 'click',
        onClickToggleBulkLoader);
    }
    if ( $('obs_scitarg_bulk_get') ) {
      Event.observe($('obs_scitarg_bulk_get'), 'click', onClickGetBulkLoader);
    }
    if ( $('obs_scitarg_bulk_put') ) {
      Event.observe($('obs_scitarg_bulk_put'), 'click', onClickPutBulkLoader);
    }
    /*
    ** If the 'observations_and_targets' component is being used
    ** as part of the wizard, then do not register the 'change' handler 
    ** linking the 'observations_and_targets' table with the 'coordinates' 
    ** and 'observation_information' components in the exposure time 
    ** calculator otherwise exposure time value inputs made in wizard 
    ** step3 might be mistakenly synchronized to the first observation 
    ** in step2.
    */
    if ( ! $('gpw_step2') && ! $('gpw_step3') ) {
      $('ra', 'dec', 'lat', 'lon', 'obs_name', 'scitarg_name',
        'obs1_ra', 'obs1_dec', 'obs1_name', 'obs1_scitarg1_name').each(
        function(id) {
        var elem = $(id);
        if ( elem ) {
          Event.observe(elem, 'change', onChangeFirstObservation);
        }
      });
    }
  }
  /* set 'tool' event handlers */
  var tool = $('tool');
  if ( tool ) {
    if ( tool.tagName.toLowerCase() == 'form' ) {
      tool.reset(); /* reset the display in case this was a page refresh */
      setFormHandler(tool['coord_sys_type'],    'click', onClickCoordSysType);
      setFormHandler(tool['ref_body'],          'click', onClickRefBody);
      setFormHandler(tool['ow_type'],           'click', onClickOwType);
      setFormHandler(tool['survey_type'],       'click', onClickSurveyType);
      setFormHandler(tool['col_disp'],          'click', onClickColDisp);
      setFormHandler(tool['time_sys_type'],     'click', onClickTimeSysType);
      setFormHandler(tool['etc_method_type'],   'click', onClickEtcMethodType);
      setFormHandler(tool['object_type'],       'click', onClickObjectType);
      setFormHandler(tool['flux_method_type'],  'click', onClickFluxMethod);
      setFormHandler(tool['extinct_method'],    'click', onClickExtinctMethod);
      setFormHandler(tool['zody_method'],       'click', onClickZodyMethod);
      setFormHandler(tool['search_method_type'],'click', onClickSearchMethod);
      if ( $('show_arch_obs') ) {
        Event.observe($('show_arch_obs'), 'click', onClickShowArchObs);
      }
      Event.observe(tool, 'submit', onSubmitTool);
    }
  }
  /* preload some images that are used with css effects */
  [ '../img/indicator_verybig.gif', 
    '../img/cross.png', '../img/tick.png', '../img/stop.png' ].each( 
    function(url) {
      var img = new Image();
      img.src = url;
    }
  );
}

Event.observe(window, "load", initGIPS);
