/*====================================================================
                             utilities.js
  This file contains useful functions that can be included into any
  course page.
  ====================================================================*/

function get_ccfs_r(theWindow, fs_name)
{
  // Returns a reference to the open window or frame whose name is given (case-insensitively)
  // by fs_name, if there is such a window in the parent/opener chain of theWindow, or if
  // theWindow itself is such a window.  Returns null otherwise.
  if (theWindow.name.toLowerCase()==fs_name.toLowerCase() && !theWindow.closed)
    return theWindow
  else
  {
    if (theWindow.parent != theWindow)
      var parent_ccfs = get_ccfs_r(theWindow.parent, fs_name)
    else
      parent_ccfs = null
    if (parent_ccfs)
      return parent_ccfs
    else
    {
      if (theWindow.opener==null)
        return null
      else
        return get_ccfs_r(theWindow.opener, fs_name)
    }
  }
}
//----------------------------------------------------------------------------------------
function get_ccfs(fs_name)
{
  // Returns a reference to the window or frame whose name is given by
  // fs_name, if such a window or frame is open.  Returns null otherwise.

  // Always look in this window's parent/opener chain before trying window.open.
  // This is to prevent certain odd problems that occur in some browsers when
  // doing a window.open that names a parent of the current frame.
  var theWindow = get_ccfs_r(self, fs_name)
  if (theWindow != null)
    return theWindow
  else
  {
    // It's not in startWindow's parent/opener chain.
    theWindow = window.open('javascript:void 0',fs_name)
    self.focus() // Moves newly opened window (if applicable) to the back
    if (theWindow.document.URL.length==0 || /^javascript:/i.test(theWindow.document.URL))
    {
      // This is a brand new window, which means there was none called fs_name.
      theWindow.close()
      return null
    }
    else
      return theWindow
  }
}
//----------------------------------------------------------------------------------------
function getWidthAndHeight(wRef)
{
  /*
  Given a reference to a window or a frame (wRef), this function
  returns a 2-element array in which the first element is the width
  of the window or frame, and the second element is its height
  */
  var width, height
  if (document.all || document.getElementByID)
  {
    // MSIE or NS7
    width = wRef.document.body.clientWidth
    height = wRef.document.body.clientHeight
  }
  else
  {
    // NS 6 or below
    width = wRef.innerWidth
    height = wRef.innerHeight
  }
  return [width, height]
}
//----------------------------------------------------------------------------------------
function inSafari()
{
	//Returns true if the user is using the Safari web browser.
	return navigator.userAgent.indexOf("Safari") != -1
	
}
//----------------------------------------------------------------------------------------
function setCookie(cookieName, cookieValue, path, not_escape)
{
  // Sets the cookie with the given name (cookieName) to the given string value (cookieValue).
  // The path parameter is optional.  If it's specified, it becomes the cookie's path, which
  // means that the cookie is accessible by all documents in the specified folder or deeper.
  // If path is not specified, the default path is used, which is the same as the path of the
  // document which includes this function.  For example, if this function is included in
  // frameset.htm, the default path is the course's root folder, so all cookies created with
  // the default path will be available to all documents in the course.
  //
  // Special features:
  // - Escapes cookieValue before storing it (so caller doesn't have to), provided 
  //   not_escape is not true.
  // - Letter case in cookieName is not significant (provided you use getCookie
  //   to retrieve the cookie).

  path = fixCookiePath(path)

  if (inSafari())
  {  //If we're in safari, the path for the cookie must be the current document or a directory 
  	 //containing the current document.
  	 var currentFolder = fixCookiePath(null)
  	 var pathFolderList = path.split("/"), currentFolderList = currentFolder.split("/")
  	 var newPath = ""
	 var min = Math.min(pathFolderList.length,currentFolderList.length),i
	 for (i=0;i<min;i++)
	 {
	 	if (pathFolderList[i] != currentFolderList[i]) break;
	 	newPath += pathFolderList[i]
	 	if (i<min-1 && pathFolderList[i + 1] == currentFolderList[i + 1]) newPath+="/"
	 }
	 if (newPath == "/") newPath = "/"
	 
	 path = newPath
  }
  
  var theCookie = cookieName.toLowerCase() + "=" + (not_escape ? cookieValue : escape(cookieValue))
  if (path != null) theCookie += ";path=" + path
  if (theCookie.length > 4095)
    alert("WARNING: We're setting a very large cookie ('" + cookieName.toLowerCase() + "', " + theCookie.length + " characters).  Some course features may not work right.")
  document.cookie = theCookie
}
//----------------------------------------------------------------------------------------
function setLongCookie(cookieName, cookieValue, path)
{
  /*
    This is just like setCookie except, if the cookie is very long, it splits it into multiple
    cookies if necessary.  If extra cookies are required, their names are like cookieName but
    with an underscore and a sequential number appended.  For example, a long cookie called
    "myCookie" may end up as 3 cookies called "myCookie", "myCookie_1" and "myCookie_2".
    The getLongCookie function will automatically join these together again when retrieving a cookie.
  */

  var maxSize = 3900 // Actually 4095; but leave plenty of room for name and path
  var maxPieces = 15 // Browsers are only required to store 20 cookies *per website*

  var isIE  = (window.navigator.appName.indexOf("Explorer") >= 0);
  var isWin  = (window.navigator.userAgent.indexOf("Win") >= 0);

  // =============================== EMERGENCY PATCH =======================================
  // Multiple long cookies are not appreciated by (at least) Apache web server.  Sending a 
  // very long set of cookies results in a "header too long" message from the server.  This
  // is a different problem than the long cookie problem with Windows IE (in which Windows IE
  // cannot store multiple long cookies on the client machine).  However, the solution is the
  // same--just limit the total cookie information being saved & transmitted.  Therefore, we
  // are, at least for the time being, changing the "if" condition from this:
  //     if (isWin && isIE)
  // ...to this:  
  if (true)
  // ============================= END EMERGENCY PATCH =====================================
  {
    // In Windows IE, the sum of ALL cookies from a server must not exceed 4k:
    var overflow = (escape(cookieValue).length + document.cookie.length > maxSize)
  }
  else
  {
    // In most other browsers, EACH cookie may be up to 4k:
    var numPieces = Math.ceil(escape(cookieValue).length/maxSize)
    overflow = numPieces > maxPieces
  }
  
  if (overflow)
  {
    /*
      For now, this alert may be too distracting and will probably occur quite often especially
      in Windows IE.  It is likely to occur far less often once we get rid of large course_state
      properties such as quiz_list, notebook_list, Assessment_HTML and notebook_content. Consider
      un-commenting the alert when we reach that state (e.g. when FRAMESET_CONTROLS and SAVE_DATA
      are false for all courses).
    */
    // alert("Cookie overflow: Some information may not be preserved if you refresh this page.")
  }
  else
  {
    killLongCookie(cookieName, path) // Get rid of all "parts" of previous incarnation of this cookie.
    path = fixCookiePath(path)
    var content = escape(cookieValue)
    for (var n=0; n*maxSize<content.length ;n++)
    {
      //var theCookie = cookieName.toLowerCase() + ((n>0)?("_"+n):("")) + "=" + content.substr(n*maxSize, maxSize)
      //if (path != null) theCookie += ";path=" + path
      setCookie(cookieName + ((n>0)?("_"+n):("")),content.substr(n*maxSize, maxSize),path,true)
      //document.cookie = theCookie
    }
  }
}
//----------------------------------------------------------------------------------------
function fixCookiePath(path)
{
  /*
    This function returns a "fixed" version of the specified cookie path, which (if not null)
    is suitable for use as the "path=" attribute when setting a cookie.  It accounts for
    certain ideosyncrasies in the way some browsers handle cookies.
  */
  var isIE  = (window.navigator.appName.indexOf("Explorer") >= 0);
  var isWin  = (window.navigator.userAgent.indexOf("Win") >= 0);

  if (self.location.protocol=="file:" && isIE && isWin)
  {
    /*
      Special handling: The "path" parameter does not work right in Windows IE for files on the
      client system (i.e. when the protocol is "file:")
    */
    path = "/"
  }
  else if (path==null)
  {
    /* In theory, if you fail to specify the path attribute, the cookie's path is supposed
       to default to the location of the setting document.  But in Mac IE (5.0, 5.1 at least),
       a missing path attribute defaults to "/"; that is, to the web root.  Therefore, for
       consistency, if the caller didn't specify the path we'll explicitly set it to the
       document's folder.
    */
    var pattern = /(.*)\/[^\/]+$/
    pattern.exec(location.pathname.replace(/\\/g, "/"))
    // RegExp.$1 now contains this document's path, excluding the actual document name.
    path = RegExp.$1
  }
  return path
}
//----------------------------------------------------------------------------------------
function getCookie(cookieName)
{
  // Returns the unescaped string value of the cookie whose name is given by cookieName,
  // if the cookie exists.  If the cookie doesn't exist, the function returns null.
  // (Note that this is different from a cookie which exists but has a blank string value.)
  // The cookieName is case insensitive, provided the cookie was set using setCookie.
  var lowerName = cookieName.toLowerCase()
  var allCookies = document.cookie
  var pairs = allCookies.split(";")
  for (var i=0; i<pairs.length; i++)
  {
    var pair = pairs[i]
    var eq = pair.indexOf("=")
    var name = pair.substring(0, eq)
    while (name.charAt(0)==" ") name = name.substring(1) // Strip leading spaces
    if (name==lowerName) return unescape(pair.substring(eq+1))
  }
  // If we got this far, cookie wasn't found:
  return null
}
//----------------------------------------------------------------------------------------
function getLongCookie(cookieName)
{
  /*
    This function is just like getCookie, but it retrieves the value of a "multipart"
    cookie that was created with the setLongCookie function.
  */
  var content = ""
  var allCookies = document.cookie
  var pairs = allCookies.split(";")
  var done = false
  for (var n=0; !done; n++)
  {
    var realCookieName = cookieName.toLowerCase() + ((n>0)?("_"+n):(""))
    var found = false
    for (var i=0; i<pairs.length && !found; i++)
    {
      var pair = pairs[i]
      var eq = pair.indexOf("=")
      var name = pair.substring(0, eq)
      while (name.charAt(0)==" ") name = name.substring(1) // Strip leading spaces
      if (name==realCookieName)
      {
        content += pair.substring(eq+1)
        found = true
      }
    } // next i
    if (!found)
    {
      // No more pieces
      done = true
      if (n==0)
      {
        return null // No pieces at all!
      }
    }
  } // next n
  return unescape(content)
}
//----------------------------------------------------------------------------------------
function killCookie(cookieName, path)
{
  // Removes the indicated cookie.  The cookieName parameter is case-insensitive,
  // provided the cookie was set using the the setCookie function.  The path parameter
  // is optional; by default, the path will be the same as the location of the calling
  // document.  The path must match that of original cookie, or the cookie won't be deleted.

  path = fixCookiePath(path)
  var theCookie = cookieName.toLowerCase() + "=dead"
  if (path != null) theCookie += ";path=" + path
  theCookie += ";expires=Fri, 02-Jan-1970 00:00:00 GMT"
  document.cookie = theCookie
}
//----------------------------------------------------------------------------------------
function killLongCookie(cookieName, path)
{
  // Just like killCookie, except it removes "long" cookies that were created with
  // the setLongCookie function.

  var maxPieces = 15

  // If path represents a folder that's deeper than the current document, getCookie can't
  // see the existing cookie (if any).  In that case, we will use the "shotgun" technique
  // to kill any existing pieces of the cookie.

  if (path==null)
    var shotgun = false // cookie's path equals this document's path
  else
  {
    var pattern = /(.*)\/[^\/]+$/
    pattern.exec(location.pathname.replace(/\\/g, "/"))
    // RegExp.$1 now contains this document's path, excluding the actual document name.
    var thisPath = RegExp.$1 + "/"
    var cookiePath = path.replace(/\\/g, "/") //(change backslashes to forward slashes, just in case)
    if (cookiePath.charAt(cookiePath.length-1) != "/") cookiePath += "/"
    // We can see the cookie if cookiePath is a (left-justified) substring of thisPath 
    shotgun = (thisPath.toLowerCase().indexOf(cookiePath.toLowerCase()) != 0)
  }

  if (shotgun)
  {
    // We can't see the pieces, so just call killCookie a maximum number of times
    // just be be sure we're hitting all the pieces.
    for (var n=0; n<maxPieces; n++)
    {
      var realCookieName = cookieName.toLowerCase() + ((n>0)?("_"+n):(""))
      killCookie(realCookieName, path)
    }
  }
  else
  {
    // We can see the pieces, so use getCookie to see if a piece exists
    // before calling killCookie.
    var done = false
    for (var n=0; !done; n++)
    {
      var realCookieName = cookieName.toLowerCase() + ((n>0)?("_"+n):(""))
      if (getCookie(realCookieName) != null)
       killCookie(realCookieName, path)
      else
       done = true
    } // next n
  } // not shotgun
}
//----------------------------------------------------------------------------------------
function js_literal(theValue, forDisplayOnly)
{
  /* Returns a string which is a literal representation, in JavaScript syntax,
     of the given value.  It is sort of the opposite of eval; however, eval(js_literal(theValue))
     won't quite always return theValue, mostly because if theValue is a string, js_literal puts
     double-quotes around it before returning the result.  But if you do this:
    
         eval("x = " + js_literal(theValue))
 
     then theValue will be correctly assigned to x.
  
     If theValue is an object, js_literal makes a good attempt to re-create the
     literal representation of it, but that only works for objects which have simple
     enumerable properties.

     The forDisplayOnly parameter is optional.  It's a boolean value; if it's true, it means
     that the caller intends to use the returned string in a display but won't try to evaluate
     it.  This makes a difference in case js_literal cannot turn theValue into a proper literal.
     In this case, js_literal will return some generic string like "*object*" if forDisplayOnly
     is true, but will display an error alert if forDisplayOnly is false.  If this parameter is
     omitted, it's treated as false.
     
     KNOWN PROBLEMS:
     * In Mac IE 5.0 (and higher?), regular expression objects do not have a readable
       "ignoreCase" property or "global" property.  Therefore, the "i" and/or "g"
       will be missing from the literal string which is returned.
  */
  var type, outString, i, propName, firstProp, pattern
  
  if (forDisplayOnly==null) forDisplayOnly = false
  if (theValue == null)
    type = "null"
  else if (typeof theValue=="object" || typeof theValue=="function") //(NS calls RegExp object a "function")
  {
    if (theValue.constructor==null)
      type = "unknown_object"
    else if (theValue.constructor==Array)
      type = "array"
    else if (theValue.constructor==Date)
      type = "date"
    else if (theValue.constructor==RegExp)
      type = "regexp"
    else if (theValue.constructor==Object)
      type = "object"
    else
    {
      // For unknown reasons, sometimes (theValue.constructor==Object) is false even though the object is
      // a user-defined object.  This usually only happens when the object is a property of a larger object.
      // In these cases, assuming the browswer is not safari, we can still recognize it as a user-defined 
      // object by parsing theValue.constructor.toString().  In the case that the browser is safari, we assume
      // that a user-defined object has been passed
      pattern = /function\s*(\w*)/
      pattern.exec(theValue.constructor.toString())
      if (RegExp.$1=="Object" || (inSafari() && theValue.constructor.toString() == "(Internal function)"))
        type = "object"
      else if (forDisplayOnly)
        type = "unknown_" + RegExp.$1
      else
      {
        alert("function js_literal: Can't interpret this kind of object:\n" + theValue.constructor.toString())
        return null
      }
    }
  }
  else
    type = typeof theValue
    
  switch (type)
  {
    case "string":
      // escape various special characters; then surround with double-quotes:
      outString = theValue.replace(/\\/g, "\\\\") // backslash
      outString = outString.replace(/"/g, "\\\"") // double-quote
      outString = outString.replace(/\n/g, "\\n") // newline (ASCII 10)
      outString = outString.replace(/\t/g, "\\t") // tab
      outString = outString.replace(/\r/g, "\\r") // CR (ASCII 13)
      outString = outString.replace(/\f/g, "\\f") // form feed
      outString = "\"" + outString + "\""
      break
    case "boolean":
      outString = (theValue?"true":"false")
      break
    case "number":
      outString = String(theValue)
      break
    case "null":
      outString = "null"
      break
    case "array":
      outString = "["
      for (i=0; i<theValue.length; i++)
      {
        if (i>0) outString += ","
        outString += js_literal(theValue[i])
      }
      outString += "]"
      break
    case "date":
      outString = 'new Date("' + String(theValue) + '")'
      break
    case "regexp":
      outString = "/" + theValue.source + "/"
      if (theValue.global) outString += "g"
      if (theValue.ignoreCase) outString += "i"
      break
    case "object":
      outString = "{"
      firstProp = true
      for (propName in theValue)
      {
        if (firstProp) firstProp = false; else outString += ","
        outString += propName + ":" + js_literal(theValue[propName])
      }
      outString += "}"
      break
    default:
      if (forDisplayOnly)
        outString = "*" + typeof theValue + "*"
      else
      {
        alert('function js_literal: Cannot handle value of type "' + type + '".')
        return null
      }
      break
  }
  return outString
}
//----------------------------------------------------------------------------------------
function getCourseState()
{
  /*
    If the "course_state" cookie exists, this function returns the contents of the
    cookie converted to object form.  If the cookie doesn't exist, the function
    returns null.  NOTE: Since the "course_state" cookie's scope is the course root
    folder, this function should not be called by any document outside of that folder.
  */
  var theCookie = getLongCookie("course_state")
  if (theCookie==null)
  {
  	alert("The course_state cookie does not exist")
    return null
  }
  else
  {
    var stateObj
    eval ("stateObj = " + theCookie) // stateObj = eval(theCookie) doesn't work, for some reason.
    return stateObj
  }
}
//----------------------------------------------------------------------------------------
function setCourseStateProp(propName, propValue)
{
  /*
    Updates the "course_state" cookie, setting the property named by propName
    to the value indicated by propValue.  NOTE: Since the "course_state" cookie's scope
    is the course root folder, this function should not be called by any document outside
    of that folder, nor by any document in a deeper folder.
  */
  var courseStateObj = getCourseState()
  if (courseStateObj == null) courseStateObj = new Object()
  courseStateObj[propName] = propValue
  setCourseState(courseStateObj)
}
//----------------------------------------------------------------------------------------
function setCourseState(courseStateObj, path)
{
  /*
    Replaces the "course_state" cookie with the contents of the courseStateObj object
    (converted to a string).  If the path parameter is null, the scope of the cookie
    will be set to the current document (the document that includes utilities.js).
    Otherwise, the cookie's scope will be set to the indicated path.  For consistency,
    you should always specify the path of the course root folder if the including document
    is not already in the course root folder.
  */
  setLongCookie("course_state", js_literal(courseStateObj),path)
}
//========================================================================================
//=============================== DEBUGGING UTILITIES ====================================
//========================================================================================
var DEBUGGING
var TRACE_WINDOW
var TRACE_OBJECTS = new Array()
//----------------------------------------------------------------------------------------
function debug_alert(theString, alsoTrace)
{
  /*
    Displays an alert only if the global DEBUGGING flag is true.  Parameters:
     theString - the string to display in the alert.
     alsoTrace - (optional) a boolean value.  If true, the string is also sent to
                 the Trace Window, in addition to the alert dialog.  If omitted,
                 this parameter is treated as false.
  */ 
  if (typeof DEBUGGING == "undefined" || DEBUGGING==null) DEBUGGING = false
  if (DEBUGGING)
  {
    alert(theString)
    if (alsoTrace) trace(theString)
  }
}
//----------------------------------------------------------------------------------------
function trace(theString, depthChange, forceTrace)
{
  /*
    This function adds the indicated debugging string to a special "trace window."  It
    creates the window if it doesn't already exist.  By default, the string is added
    only if the global DEBUGGING flag is true.  Parameters:
    * theString   - (required). The string to display in the window.
    * depthChange - (optional). Indicates whether to change the indentation level of
                    the displayed strings.  This is useful, for example, for providing
                    a visual cue of when a function was entered or exited.  Use one of
                    the following values:
                      +1 - Increment indentation level AFTER displaying this string.
                       0 - Don't change indentation level (default).
                      -1 - Decrement indentation level BEFORE displaying this string.
                      -2 - Set indentation level to zero BEFORE displaying this string.
                    If this parameter is omitted (or null), the indentation level is not changed.
                    If the message is not actually traced (i.e. if DEBUGGING is false and the
                    forceTrace parameter is omitted (or it's null or false)), the indentation
                    level is not changed.
    * forceTrace  - (optional). Set this to true if you want to force the message to be traced
                    regardless of the state of the DEBUGGING flag.  If this parameter is omitted,
                    or is null, or false, the message will be traced only if DEBUGGING is true.
  */
  if (depthChange==null) depthChange = 0
  if (forceTrace==null) forceTrace = false
  if (DEBUGGING || forceTrace)
  {
    // Create window if necessary
    if (TRACE_WINDOW==null || TRACE_WINDOW.closed)
    {
      var uri = "javascript:\"<html><head><title>Debugging Trace</title></head><frameset rows=*,50><frame name='main'><frame name='bottom' noresize src='javascript:\\\"<center><input type=\\\\\\\"button\\\\\\\" value=\\\\\\\"Clear\\\\\\\" onClick=\\\\\\\"parent.opener.clear_trace()\\\\\\\"></center>\\\"'></frameset></html>\""
      TRACE_WINDOW = window.open(uri, "", "width=600,height=300,scrollbars,resizable")
      // When first creating this window, it can take a little while for the frames to establish
      // themselves.  Therefore, quit the function and come back in 1/2 second.
    }
    // Create a new trace object and add it to the global TRACE_OBJECTS array
    var traceObj = new Object()
    traceObj.text = theString
    traceObj.depthChange = depthChange
    TRACE_OBJECTS[TRACE_OBJECTS.length] = traceObj
    if (TRACE_WINDOW.main)
      write_trace_objects()
    else
    {
      // This can happen if we've just now created the trace window and the frames haven't had
      // enough time to establish themselves.  In this case, wait a bit and then call write_trace_objects.
      var timer = setTimeout("write_trace_objects()",500)
    }
  } // if (DEBUGGING || forceTrace)
}
//------------------------------------------------------------------------
function write_trace_objects()
{
  // Writes the contents of the TRACE_OBJECTS array to the TRACE_WINDOW.
  var html = "<html>\n<head>\n<title>Debugging Trace</title>\n</head>\n<font face='courier' size=2>\n"
  var ul_depth = 0
  html += "<ul>\n"
  for (var i=0; i<TRACE_OBJECTS.length; i++)
  {
    traceObj = TRACE_OBJECTS[i]
    // Pre-decrement indentation level if necessary:
    if (traceObj.depthChange==-1)
    {
      if (ul_depth > 0)
      {
        html += "</ul>\n"
        ul_depth--
      }
    }
    else if (traceObj.depthChange==-2)
      while (ul_depth-- > 0) html += "</ul>\n"
    // Write the text:
    var theText = traceObj.text.replace(/&/g, "&amp;")
    theText = theText.replace(/</g, "&lt;")
    theText = theText.replace(/>/g, "&gt;")
    theText = theText.replace(/"/g, "&quot;")
    theText = theText.replace(/\n/g, "<br>")
    html += "<li>" + theText + "</li>\n"
    // Post-increment indendation level if necessary:
    if (traceObj.depthChange==1)
    {
      html += "<ul>\n"
      ul_depth++
    }
  } // next i (next traceObj)
  while (ul_depth-- > 0) html += "</ul>\n"
  html += "</ul>\n"
  html += "</font>\n</html>"
  TRACE_WINDOW.main.document.write(html)
  TRACE_WINDOW.main.document.close()
}
//--------------------------------------------------------------------------------
function trace_entry(showArguments, forceTrace)
{
  /*
    This function writes a message to the Trace Window, indicating that a function has
    been entered.  It automatically displays the name of the function that called trace_entry.
    If trace_entry is called from the top level of a program (i.e., not from within a function),
    it does nothing.
    
    showArguments is a boolean parameter.  If it's true, the message also lists the values
    of the arguments that were passed to the function (if they can be displayed as literals).

    forceTrace is an optional boolean parameter.  If it's omitted, the function writes
    the message only if the global DEBUGGING flag is true.  Set forceTrace to true if
    you want the message to be written regardless of the state of the DEBUGGING flag.

    This function should be used in conjunction with the trace_exit function, to make
    sure that the indentation levels are set correctly in the Trace Window.
  */
  var theCaller = trace_entry.caller
  if (showArguments==null) showArguments = false
  if (forceTrace==null) forceTrace = false
  if (typeof DEBUGGING == "undefined" || DEBUGGING==null) DEBUGGING = false
  if ((DEBUGGING || forceTrace) && theCaller != null)
  {
    var pattern = /function\s*(\w*)/
    pattern.exec(theCaller.toString())
    var callerName = RegExp.$1
    var msg = "Entering function " + callerName + "("
    if (showArguments)
    {
      var callerArgs = theCaller.arguments  // Deprecated, but not replaced by anything better!
      for (var i=0; i<callerArgs.length; i++)
        msg += (i>0?", ":"") + js_literal(callerArgs[i], true)
    }
    msg += ") ..."
    trace(msg, +1, forceTrace)
  }
}
//--------------------------------------------------------------------------------
function trace_exit(forceTrace)
{
  /*
    This function writes a message to the Trace Window, indicating that a function
    is about to be exited.  It automatically displays the name of the function that
    called trace_exit.  If trace_exit if called from the top level of a program
    (i.e., not from within a function), it does nothing.

    forceTrace is an optional boolean parameter.  If it's omitted, the function writes
    the message only if the global DEBUGGING flag is true.  Set forceTrace to true if
    you want the message to be written regardless of the state of the DEBUGGING flag.

    This function should be used in conjunction with the trace_entry function, to make
    sure that the indentation levels are set correctly in the Trace Window.
  */
  var theCaller = trace_exit.caller
  if (forceTrace==null) forceTrace = false
  if (typeof DEBUGGING == "undefined" || DEBUGGING==null) DEBUGGING = false
  if ((DEBUGGING || forceTrace) && theCaller != null)
  {
    var pattern = /function\s*(\w*)/
    pattern.exec(theCaller.toString())
    var callerName = RegExp.$1
    var msg = "Exiting function " + callerName + "()"
    trace(msg, -1, forceTrace)
  }
}
//--------------------------------------------------------------------------------
function trace_path(showArguments, forceTrace)
{
  /*
    This function writes a message to the Trace Window which shows the name of the function
    (if any) that called trace_function, and the function that called that one, etc, all the
    way out the main program.  If trace_path is called from the main program (i.e., not from
    any function), it does nothing.

    showArguments is a boolean parameter.  If it's true, the message also lists the values
    of the arguments that were passed to each function in the stack (if they can be displayed as literals).

    forceTrace is an optional boolean parameter.  If it's omitted, the function writes
    the message only if the global DEBUGGING flag is true.  Set forceTrace to true if
    you want the message to be written regardless of the state of the DEBUGGING flag.
  */
  var theCaller = trace_path.caller
  if (forceTrace==null) forceTrace = false
  if (typeof DEBUGGING == "undefined" || DEBUGGING==null) DEBUGGING = false
  if (DEBUGGING || forceTrace)
  {
    var prevFunctionName = null
    var pattern = /function\s*(\w*)/
    while (theCaller != null)
    {
      pattern.exec(theCaller.toString())
      var callerName = RegExp.$1
      if (prevFunctionName==null)
        var msg = "Now inside"
      else if (prevFunctionName=="") //(yes, there are nameless functions)
        msg += "\nThat function was called by"
      else
        msg += "\n" + prevFunctionName + " was called by"
      msg += " function " + callerName + "("
      if (showArguments)
      {
        var callerArgs = theCaller.arguments  // Deprecated, but not replaced by anything better!
        for (var i=0; i<callerArgs.length; i++)
          msg += (i>0?", ":"") + js_literal(callerArgs[i], true)
      }
      msg += ")."
      prevFunctionName = callerName
      theCaller = theCaller.caller
    }
    if (prevFunctionName=="") msg += "\nThat function"; else msg += "\n" + prevFunctionName
    msg += " was called by the main program."
    trace(msg, 0, forceTrace)
  }
}
//--------------------------------------------------------------------------------
function clear_trace()
{
  // Erases the current contents (if any) of the Trace Window
  TRACE_OBJECTS = new Array() // Erases the old array.
  if (TRACE_WINDOW != null && !TRACE_WINDOW.closed)
  {
    TRACE_WINDOW.main.document.write("<html></html>")
    TRACE_WINDOW.main.document.close()
  }
}
//---------------------------------------------------------------------------------
function htmlencode(theText)
{
  // Returns a version of theText in which certain characters have been replaced by HTML character entities.
  theText = theText.replace(/&/g, "&amp;")
  theText = theText.replace(/</g, "&lt;")
  theText = theText.replace(/>/g, "&gt;")
  theText = theText.replace(/"/g, "&quot;")
  theText = theText.replace(/\n/g, "<br>")
  return theText
}
//------------------------------------------------------------------------------------------------
function folderURL(theWindow)
{
  // Returns a string which is a URL pointing to the ENCLOSING FOLDER of the
  // location of theWindow.  The returned string includes the trailing "/".
  // DON'T use theWindow.location here.  There are known problems with trying
  // to read the location object in Windows IE version 6.
  var url = theWindow.document.URL.replace(/\\/g, "/")  // Change all "\" to "/"
  if (url.indexOf("#")!=-1) url = url.substring(0,url.lastIndexOf("#")) // Chop off hash (if any)
  if (url.indexOf("?")!=-1) url = url.substring(0,url.lastIndexOf("?")) // Chop off querystring (if any)
  return url.substring(0,url.lastIndexOf("/")+1)
}
//-------------------------------------------
function fullURL(url, wRef)
{
  /*
    Returns an absolute URL based on the url parameter.
    Parameters:
      url  - An absolute or relative URL.
      wRef - Optional.  Ignored if url is an absolute url.  If url is relative,
             it is assumed to be relative to the document in the window referenced
             by wRef.  If wRef is omitted, the url is assumed to be relative to the
             document that includes this function.
  */
  var theFullURL

  if (wRef == null) wRef = self
  var rex = /^\w+:/
  if (rex.test(url))
    theFullURL = url //URL has a protocol string; assume it's already absolute
  else if (url.charAt(0)=="/" || url.charAt(0) == "\\")
    theFullURL = wRef.location.protocol + url
  else
    theFullURL = URLglue(folderURL(wRef), url)

  // Windows IE is too stupid to understand an escaped URL if the protocol is "file":
  var isIE  = (window.navigator.appName.indexOf("Explorer") >= 0);
  var isWin  = (window.navigator.userAgent.indexOf("Win") >= 0);
  var isFile = (theFullURL.substr(0,5)=="file:")
  if (isWin && isIE && isFile) theFullURL = unescape(theFullURL)
  return theFullURL
}
//-------------------------------------------
function docWrite(theText, theWindow)
{
  /*
  This function calls document.write to write theText.  The second parameter, theWindow, is optional.  If it's
  specified, it should be a window object reference, and theText is written to the document in that window.
  If it's omitted, theText is written to the document that is including utilities.js.

  Now, before you say, "This is a really stupid function," the reason it exists is to work around the behavior
  of newer Windows IE browsers (v6 SP1 and higher), which display an annoying prompt whenever an <embed> or
  <object> tag appears in HTML.  The only general way to eliminate the prompt is to write such tags using Javascript
  that resides in an *external* ".js" file.  This means that simply calling document.write won't work, but calling
  a function in utilities.js that calls document.write, will.  What is STUPID is the lawsuit that forced MicroSoft
  to change its browser to behave in this STUPID way.
  */

  if (theWindow == null) theWindow = self
  theWindow.document.write(theText)
}
//-------------------------------------------
function IsArray(theItem)
{
  // Returns true if theItem is an array
  if (theItem!=null && typeof theItem=="object" && theItem.constructor!=null && (theItem.constructor==Array || theItem.constructor.toString().indexOf("function Array(")>-1))
    return true
  else if (inSafari())
    // In Safari, window1.Array != window2.Array, so the test (theItem.constructor==Array) might fail, even if theItem is an array.
    return IsProbablyArray(theItem)
  else
    return false
}
//------------------------------------------------------------------------------------------------
function IsProbablyArray(item)
{
  // Performs a very rough test to determine whether the passed item is an array.
  // To do this, it checks that the item is an object which has a numeric "length" property,
  // and may have numeric-indexed properties from 0 to length-1 (provided length > 0), and has
  // no other enumerable properties.
  //
  // The function returns true or false.  It returns a "false positive" if it receives
  // a non-array object that happens to have the abovementioned characteristics.  It
  // returns a "false negative" if it receives an array to which the user has also assigned
  // additional properties

  if (item==null || typeof item != "object" || typeof item.length != "number")
    return false
  else
  {
    for (var prop in item)
    {
      if (!isNaN(prop)) // It's numeric
        {if (parseInt(prop) < 0 || parseInt(prop) >= item.length) return false}
      else if (prop != "length")
        return false
    }
    // Passed all tests.
    return true
  }
}
//-------------------------------------------------------------------------------------------------
function BetterUnescape(theString)
{
  /*
    This function assumes that theString is in "escaped" format, and "unescapes" it.  It works
    better than the built-in Javascript unescape function, which does not always do the unescape
    properly when the escaped character has an ASCII code that's greater than 127.
  */
  var i, outString=""
  while ((i=theString.indexOf("%"))>-1)
  {
    outString += theString.substr(0,i) // copy everything up to (but not including) the "%"
    outString += String.fromCharCode(eval("0x" + theString.substr(i+1,2))) // convert the hex digits to a Unicode character
    theString = theString.substr(i+3) // chop off the beginning of the string, up to and including the "%XX"
  }
  outString += theString // Add whatever's left over in theString
  return outString
}
//------------------------------------------------------------
function UTF8toUnicode(theString) {
  // Returns a copy of theString in which UTF-8 byte sequences have been translated to Unicode.
  var rex = /[\xC2-\xDF][\x80-\xBF]|[\xE0-\xEF][\x80-\xBF]{2}|[\xF0-\xF7][\x80-\xBF]{3}/g //Matches UTF-8 sequences
  return theString.replace(rex, unicoder)
  function unicoder(utf8_sequence) {
    var n1 = utf8_sequence.charCodeAt(0), n2 = utf8_sequence.charCodeAt(1)
    if (utf8_sequence.length > 2) var n3 = utf8_sequence.charCodeAt(2)
    if (utf8_sequence.length > 3) var n4 = utf8_sequence.charCodeAt(3)
    switch (utf8_sequence.length) {
      case 2 :
        var codePoint = ((n1 & 31)<<6) + (n2 & 63)
        break;
      case 3 :
        codePoint = ((n1 & 15)<<12) + ((n2 & 63)<<6) + (n3 & 63)
        break;
      case 4 :
        codePoint = ((n1 & 7)<<18) + ((n2 & 63)<<12) + ((n3 & 63)<<6) + (n4 & 63)
        break;
    } // end switch
    return String.fromCharCode(codePoint)
  } // end (nested) function unicoder
} // end function UTF8toUnicode
//------------------------------------------------------------
function UTF8toISO8859(inString)
{
  /*
    ============ DEPRECATED.  Use UTF8toUnicode instead. =======================

    This function assumes that "inString" is in UTF-8 format, and that all its characters are in the
    Unicode range 0 through 255.  (In this case, characters 0-127 occupy 1 byte, and characaters 128-255
    occupy 2 bytes, according to the UTF-8 standard.)  The function returns a corresponding string in which
    every character occupies only 1 byte.  In other words, it does not alter the characters with codes 0-127;
    and it converts each of the 2-byte characters with codes 128-255 into their 1-byte equivalent.  This
    generally makes the string easier to display in most browsers.

    The process is complicated by the fact that some versions of Flash may automatically recognize UTF-8 and
    therefore the charCodeAt method may return the unicode value (extracted from 1 or 2 bytes) rather than the
    byte value.  Therefore we'll make an attempt to recognize whether the byte pattern looks characteristic of UTF-8.
    This method is not foolproof.
  */
    var outString = ""
    for (var i=0; i<inString.length; i++)
    {
      var n = inString.charCodeAt(i)
      if ((n==194 || n==195) && i<inString.length-1 && (inString.charCodeAt(i+1) & 192)==128)
      {
        // Looks like a 2-byte UTF-8 character representation
        outString += String.fromCharCode((n & 3)*64 + (inString.charCodeAt(i+1) & 63))
        i++
      }
      else
        outString += String.fromCharCode(n)
    }
    return outString
}
//----------------------------------------------------------------------------------------
function stripProps(theObject, propArray)
{
  /*
    Returns a copy of an object from which selected properties have been removed.
    Parameters:
      theObject - The original object from which the properties are to be stripped.
                  May also be null, in which case the function returns null.
      propArray - An array of strings representing the names of properties which are
                  to be removed.  The names are treated case-insensitively.  If a given
                  name is not represented in theObject, it's ignored.
    
    Note: if the resulting object has no properties, the function returns null (not an empty object).
  */
  var propName, propName_lc, found, i
  var hasProps = false
  if (theObject==null) return null
  var newObject = new Object()
  for (propName in theObject) {
    propName_lc = propName.toLowerCase()
    found = false
    for (i=0; i<propArray.length && !found; i++) {
      if (propArray[i].toLowerCase()==propName_lc) found=true
    }
    if (!found) {
      newObject[propName] = theObject[propName]
      hasProps = true
    }
  }
  if (hasProps)
    return newObject
  else
    return null
}
//----------------------------------------------------------------------------------------
function buildAttrString(attrObj)
{
  /*
    Uses the names and values of properties in attrObj to build a string of attribute/value pairs
    suitable for insertion into an HTML/XML tag.  If attrObj is null or has no enumarable properties,
    the function returns an empty string.  Otherwise, it returns a space-delimited list of attribute/value
    pairs, with a leading space as the string's first character.  Example: if attrObj is {x:120,y:'blue'},
    the output string will look like: ' x="120" y="blue"'
  */
  var outString = ""
  var propName
  if (attrObj!=null) {
    for (propName in attrObj) outString += ' ' + propName + '="' + htmlencode(String(attrObj[propName])) + '"'
  }
  return outString
}
//---------------------------------------------------------------------------------
function buildParamTags(attrObj, defaultValues)
{
  /*
    Uses the names and values of properties in attrObj and defaultValues to build a series of <param> tags suitable for
    insertion in (for example) an HTML <object> or <applet> element.
    Parameters:
      attrObj       - null, or an object representing name/value pairs that should go into the <param> elements.
      defaultValues - null, or an object representing name/value pairs that should go into the <param> elements.

    The resulting <param> tags will represent the UNION of the properties in attrObj and defaultValues. In cases
    where the same property (the same <param> name) is found in both attrObj and defaultValues, the value
    in attrObj is used.  (NOTE: Property names are treated case-insensitively for this purpose.)

    If both attrObj and defaultValue are null or neither has any enumerable properties, the function returns an
    empty string.  Otherwise, it returns a string consisting of a newline-delimited sequence of <param> elements,
    one per unique enumerable property.  Example, if attrObj is {x:120, y:'blue'} and defaultValues is {y:'green', z:false},
    the output string will look like:
      <param name="x" value="120">
      <param name="y" value="blue">
      <param name="z" value="false">
  */
  var outString = ""
  var propName, propName2, defaultClone
  if (attrObj==null && defaultValues==null) {
    return ""
  }
  else if (attrObj==null || defaultValues==null) {
    var oneObject = attrObj || defaultValues
    for (propName in oneObject) outString += '<param name="' + propName + '" value="' + htmlencode(String(oneObject[propName])) + '">\n'
  }
  else {
    defaultClone = new Object()
    for (propName2 in defaultValues) defaultClone[propName2] = defaultValues[propName2]
    for (propName in attrObj) {
      outString += '<param name="' + propName + '" value="' + htmlencode(String(attrObj[propName])) + '">\n'
      for (propName2 in defaultClone) {
        if (propName.toLowerCase()==propName2.toLowerCase()) {
          delete defaultClone[propName2]
          break
        }
      }
    }
    for (propName2 in defaultClone) {
      outString += '<param name="' + propName2 + '" value="' + htmlencode(String(defaultClone[propName2])) + '">\n'
    }
  }
  return outString
}