
// Conventions used by this object:
// - The DOM Elements that compose a display node have a very specific naming convention.
//   (TODO: elaborate!)

function TaxonomyTreeControl(parentUID, mode) {

    //--------------------------------------------------------------    
	// Define member variables.
	//--------------------------------------------------------------    
	
	// "Self" fixes loss-of-scope problem in inner functions.		
	var self = this; 
	
	// An Ajax object maintained by the taxonomy tree.
	this.ajax = null;
	
	this.asyncGenomeURL = "async_genomeData.asp";
	this.asyncLinkoutURL = "async_linkoutData.asp";
	
	this.classPrefix = "TaxTree";
	
	// Is Ajax enabled on this object?
	this.isAjaxEnabled = true;
	
	this.leftSideOffsetPerNode = "20";
	
	// The mode in which the tree operates.
	this.mode;
	
	this.nodeDepthOffset = "0";
	
	
	this.parentUID = "";
	this.parentElement = null;
	
	// Variables used to determine genome/strain/isolate grouping (color-coding BG according to group membership).
	this.previousStrainName = "";
	this.isAltGenomeRow = false;
	
	// If the number of rows of asynchronous data is greater than this number, the progress message will  
	// be displayed until the data has been loaded.
	this.progressMessageThreshold = 50;
	
	// When data is exported, it will be maintained in these two lists.
	this.selectedGenomeNodes = "";
	this.selectedTaxNodes = "";
	
	// Node attribute names (note that these need to match XML attributes generated by server-side code!).
	this.ATTR_ACCNUM = "an";
	this.ATTR_CHILDCOUNT = "cc";
	this.ATTR_CHILDDATAPOPULATED = "cdp";
	this.ATTR_CHILDTYPE1 = "ctl1"; 
	this.ATTR_CHILDTYPE2 = "ctl2";
	this.ATTR_GENOMECOUNT = "gc";  
	this.ATTR_GENOMEDBXREFCOUNT = "gdc";
	this.ATTR_GENOMEID = "gi";
	this.ATTR_DEPTH = "d";
	this.ATTR_DISPLAYNAME = "fn";  
	this.ATTR_ISEXPANDED = "ie";
	this.ATTR_ISSELECTED = "is";
	this.ATTR_NUMBASES = "nb";
	this.ATTR_NUMCHILDRENSELECTED = "ncs";
	this.ATTR_PARENTUID = "puid";  
	this.ATTR_STRAINNAME = "sn";
	this.ATTR_TAXNODEDBXREFCOUNT = "tndc";
	this.ATTR_TAXNODEID = "tni";
	this.ATTR_TYPE = "tl";  //"t";
	this.ATTR_UID = "uid"; 
	
	
	// Types of "deselection".
	this.DESELECT_CHILDREN = 0;
	this.DESELECT_NODE = 1;
	this.DESELECT_PARENTS = 2;
	
	// Images used by the control.
	this.IMAGE_ASYNC_DATA_LOADING = "images/ajax-loader.gif"; // Courtesy of http://www.ajaxload.info/#preview
    this.IMAGE_COLLAPSED = "images/treeContracted.gif";
    this.IMAGE_EXPANDED = "images/treeExpanded.gif"
    this.IMAGE_NOCHILDREN = "images/treeNoChildren.gif";

    
    // The different modes in which the tree can operate. The tree mode value is one of the following:
    //
    // value    "constant"            definition
    // -----    --------------------  --------------------
    //     0    MODE_GENOME_DETAIL    Display a detailed view of genome data (viruses.asp).
    //     1    MODE_SELECT_GENOMES   Display a checkbox for each genome to allow user selection.
    //     2    MODE_SELECT_TAXONOMY  Display only taxonomy nodes (no genomes) to allow user selection.
    //     3    MODE_SELECT_ALL       Display both taxonomy and genomes and allow user selection of any.
    
    this.MODE_GENOME_DETAIL = 0;
    this.MODE_SELECT_GENOMES = 1;
    this.MODE_SELECT_TAXONOMY = 2;
    this.MODE_SELECT_ALL = 3;

    // The non-displayable "root" taxNodeID.
    this.TAXNODEID_ROOT = "8";
    
    
	//--------------------------------------------------------------    
	// Define member methods.
	//--------------------------------------------------------------    
    
    // Automatically expand the nodes that correspond to the input parameter: a comma-delimited list of taxNodeID's.
    this.autoExpand = function (taxNodeIdList) {
    
        if (isEmpty(taxNodeIdList)) {return self.displayError("Invalid taxNodeIdList","autoExpand");}
        
        var tniArray = taxNodeIdList.split(",");
        for (var t=0; t<tniArray.length; t++) {
            self.expandNode(tniArray[t]);
        }
    };
    
    
    // Automatically select the nodes that correspond to the input parameter - a comma-delimited list of taxNodeID's.
    this.autoSelect = function (taxNodeIdList, expandBelow) {

        // Validate the input parameter.
        if (isEmpty(taxNodeIdList)) {return self.displayError("Invalid taxNodeIdList","autoSelect");}
        
        if (self.mode != self.MODE_SELECT_TAXONOMY && 
            self.mode != self.MODE_SELECT_ALL) {return self.displayError("This function should only be run when the tree is in 'select taxonomy' or 'select all' mode","autoSelect");}
        
        var tniArray = taxNodeIdList.split(",");
        for (var t=0; t<tniArray.length; t++) {
            self.selectNode(tniArray[t], expandBelow);
        }
    };
    
    
    // Collapse the node.
    this.collapse = function (uid, nodeElement) {
    
        var childrenElement;
        var dataElement;
        var imageElement;
        var tableElement;
        var type;
        
        if (isEmpty(uid)) {return self.displayError("Invalid uid","collapse");}
        if (nodeElement == null) {return self.displayError("Invalid nodeElement","collapse");}
        
        nodeElement.setAttribute(self.ATTR_ISEXPANDED, "false");
    
        imageElement = document.getElementById(uid + "__plusminus");
        if (imageElement == null) {return self.displayError("Invalid imageElement","collapse");}
        imageElement.setAttribute("src",self.IMAGE_COLLAPSED);
        
        childrenElement = document.getElementById(uid + "__children");
        if (childrenElement == null) {return self.displayError("Invalid childrenElement","collapse");}
        
        childrenElement.className = self.classPrefix + "_HiddenChildren";
        
        // Change the node's color.
        tableElement = document.getElementById(uid + "__table");
        if (tableElement == null) {return self.displayError("Invalid tableElement","collapse");}
        
        // Get the node's type.
        dataElement = document.getElementById(uid + "__data");
        if (dataElement == null) {return self.displayError("Invalid dataElement","collapse");}
        

        if (self.mode != self.MODE_SELECT_TAXONOMY && self.mode != self.MODE_SELECT_ALL) {
            type = dataElement.getAttribute(self.ATTR_TYPE);
            if (!isEmpty(type)) {
                tableElement.className = self.classPrefix + "_DisplayTableClass unselected" + type + "Class";
            }
        }
    };
    
    // TODO: at some point, move this to utilityFunctions.js.
    
    // Create an XMLHTTP object specific to the browser (from http://www.quirksmode.org/js/xmlhttp.html).
    this.createXMLHTTPObject = function () {
	    var xmlhttp = false;
	    for (var i=0; i<XMLHttpFactories.length; i++) {
		    try { xmlhttp = XMLHttpFactories[i](); }
		    catch (e) { continue; }
		    break;
	    }
	    return xmlhttp;
    };
    
    
    // Deselect the node that corresponds to a particular unique ID. "Deselection type" indicates whether the 
    // child nodes and parent nodes should also be deselected.
    this.deselect = function (uid, deselectionType) {
    
        var childNode;
        var childNodes;
        var childUID;
        var ctrlElement;
        var dataElement;
        var dataID;
        var parentUID;
        var tableElement;
        var type;
        
        if (isEmpty(uid)) {return self.displayError("Invalid uid","deselect");}
        
        // Get, validate, and update the UI ctrl Element.
        ctrlElement = document.getElementById(uid + "__ctrl");
        if (ctrlElement == null) {return false;}
        ctrlElement.checked = false;
        
        // Try to retrieve the data Element.
        dataID = uid + "__data";
        dataElement = document.getElementById(dataID);
        if (dataElement != null) {
        
            // Set the data Element to "deselected".
            dataElement.setAttribute(self.ATTR_ISSELECTED, "false");
        
            // Get the child nodes collection, parentUID, and type.
            childNodes = dataElement.childNodes; 
            parentUID = dataElement.getAttribute(self.ATTR_PARENTUID);
            type = dataElement.getAttribute(self.ATTR_TYPE);
            
        } else {
        
            childNodes = null;
            parentUID = ctrlElement.getAttribute(self.ATTR_PARENTUID);
            type = "Genome"; // dmd 102408 too much of a hack???
        }
        
        if (deselectionType == self.DESELECT_CHILDREN) {
        
            // Deselect child nodes.
            if (childNodes != null && childNodes.length > 0) {

                // Iterate thru the child nodes.
                for (var c=0; c<childNodes.length; c++) {
                    childNode = childNodes[c];
                    if (childNode == null || childNode.nodeType != 1) {continue;}
                    
                    childUID = childNode.getAttribute(self.ATTR_UID);
                    if (isEmpty(childUID)) {continue;}
                    
                    self.deselect(childUID, self.DESELECT_CHILDREN);
                }
            }
        } else if (deselectionType == self.DESELECT_PARENTS) {

            if (!isEmpty(parentUID)) {
                self.deselect(parentUID, self.DESELECT_PARENTS);
            }
        }
      
        // Change the node's color.
        tableElement = document.getElementById(uid + "__table");
        if (tableElement == null) {return self.displayError("Invalid tableElement","deselect");}
        
        // Update the table formatting based on the type.
        if (!isEmpty(type)) {
            tableElement.className = self.classPrefix + "_DisplayTableClass unselected" + type + "Class";
        }
            
        return true;
    };
    
    
    // Deselect all selected nodes.
    this.deselectAll = function () {
    
        var taxNodeIDList;
        var tniArray;
        
        taxNodeIDList = self.exportTaxNodeIdList();
        if (!isEmpty(taxNodeIDList) && taxNodeIDList != false) {
            tniArray = taxNodeIDList.split(",");
            for (var t=0; t<tniArray.length; t++) {
                self.deselect("tni" + tniArray[t] + "_iso_gnm_gne", self.DESELECT_NODE);
            }
        }
    };
    
    
    // Let's say we want to display a node with arbitrary depth in the data tree. In order to do so
    // we need to see if it's already displayed. If so, exit. If not, we need to traverse its parent
    // hierarchy until we find a parent that's displayed. Once we find that one, begin reversing the 
    // traversed (data) hierarchy and displaying nodes until the terminal node is reached.
    this.displayArbitraryNode = function (uid, childUidList, autoExpand) {
    
        var childUID;
        var newChildUidList;
        var nodeData;
        var nodeElement;
        var numChildUids;
        var parentUID;
        var taxNodeID;
  
        if (isEmpty(uid)) {return self.displayError("Invalid uid", "displayArbitraryNode");}
        
	    // Is this node displayed? Find out by trying to get and validate the node Element. 
        nodeElement = document.getElementById(uid);
	    if (nodeElement == null) {
	
	        // Is this requesting the (non-displayable) taxonomy root node?
	        taxNodeID = self.getTaxNodeID(uid);
	        if (taxNodeID == self.TAXNODEID_ROOT) {return true;}
	        
	        // Get the dataID for this uid.
	        dataID = uid + "__data";
	        
	        // Get a reference to the data Element.
            dataElement = document.getElementById(dataID);
            if (dataElement == null) {alert("can't find data id " + dataID); return false;}
	        
	        // Get the parentUID from this node.
            parentUID = dataElement.getAttribute(self.ATTR_PARENTUID);
	        if (isEmpty(parentUID)) {return self.displayError("Invalid parentUID", "displayArbitraryNode");}
	        
	        // Add this uid to the list.
	        if (childUidList == null) {childUidList = new Array();}
	        childUidList[childUidList.length] = uid;
	
	        // Recursively call with the parentUID and the updated childUidList.
	        return self.displayArbitraryNode(parentUID, childUidList, autoExpand);
	        
	    } else {
	        // Since the node is displayed we need to return (if appropriate) and
	        // begin displaying nodes from the childUidList.
	        self.expandOrCollapse(uid, autoExpand);
	               
	        if (childUidList == null || childUidList.length == 0) {
	            // No children to display.
	            return true;
	        } else {
	            numChildUids = childUidList.length;
	     
	            // Get the most recently added childUID and then remove it from the list.
	            childUID = childUidList[(numChildUids - 1)];
	            
	            newChildUidList = new Array();
	            for (var n=0; n<(numChildUids - 1); n++){
	               newChildUidList[n] = childUidList[n];
	            }
	            childUidList = null;
     
	            if (isEmpty(childUID)) { return true; }
	             
	            // Get the dataID for this uid.
	            dataID = childUID + "__data";
    	        
	            // Get a reference to the data Element.
                dataElement = document.getElementById(dataID);
                if (dataElement == null) {return self.displayError("Invalid dataElement " + dataID, "displayArbitraryNode");}
    	        
	            // Recursively call this function with the childUID and the shortened list.
	            return self.displayArbitraryNode(childUID, newChildUidList, autoExpand);
	        }
	    }
    };
    
    
    // An "overridden" version of the global display error function.
    this.displayError = function (message, functionName) {
        return global_displayError(message, functionName, "TaxonomyTree");
    };
    
   

    // Display a progress message under parentUID's node.   
    this.displayProgressMessage = function (hideMessage, parentDepth, parentUID) {
    
        var displayOffset;
        var imgElement;
        var message;
        var msgDepth;
        var msgElement;
        var msgLabelElement;
        var parentElement;
       
        // Validate the input parameters.
        if (isEmpty(parentUID)) { return self.displayError("Invalid parentUID", "displayProgressMessage"); } 
        
        // Get the parent Element.
        parentElement = document.getElementById(parentUID + "__children");
        if (parentElement == null) { return self.displayError("Invalid parentElement", "displayProgressMessage"); } 
            
        if (hideMessage) {
        
            //----------------------------------------------------------------------
            // Hide an existing progress message.
            //----------------------------------------------------------------------
        
            msgElement = document.getElementById(parentUID + "__progressMsg");
            if (msgElement == null) { /* TODO: exception? */ return true; }
            
            // Remove the message Element from the parent.
            parentElement.removeChild(msgElement);
        
        } else {
        
            //----------------------------------------------------------------------
            // Display a progress message.
            //----------------------------------------------------------------------
          
            message = "Loading isolate data...";
            
            // The Element that contains the message "node".
            msgElement = document.createElement("div");
            msgElement.className = self.classPrefix + "_ProgressMessage";
            msgElement.setAttribute("id", parentUID + "__progressMsg");
            
            // The Element that displays the message text.
            msgLabelElement = document.createElement("span");
            msgLabelElement.className = self.classPrefix + "_ProgressMessageLabel";
            msgLabelElement.innerHTML = message;
            
            // The "asynchronous data loading" image.
            imgElement = document.createElement("img");
            imgElement.setAttribute("src", self.IMAGE_ASYNC_DATA_LOADING);
            imgElement.className = self.classPrefix + "_ProgressMessageImage";
            
            // Calculate the left-side offset.
            if (isEmpty(parentDepth)) { parentDepth = "0"; }
            msgDepth = parseInt(parentDepth) + 1;   
            displayOffset = (parseInt(msgDepth) - parseInt(self.nodeDepthOffset))
                * parseInt(self.leftSideOffsetPerNode);
            msgElement.style.marginLeft = String(displayOffset) + "px";      
            
            // Append all Elements to the tree.
            msgElement.appendChild(msgLabelElement);
            msgElement.appendChild(imgElement);
            parentElement.appendChild(msgElement);
            
            // Make sure message is visible.
            parentElement.className = self.classPrefix + "_VisibleChildren";
        }
        
        return true;
    };
    
    
    // An "overridden" version of the global display-zero-as-error function.
    this.displayZeroAsError = function (message, functionName) {
        return global_displayZeroAsError(message, functionName, "TaxonomyTree");
    };
    
    
    // Expand the node.
    this.expand = function (uid, nodeElement) {
    
        var childCount;
        var childData;
        var childDataCollection;
        var childDataPopulated;
        var childGenomeCount;
        var childType;
        var dataID;
        var dataElement;
        var imageElement;
        var parentDepth;
        var tableElement;
        var type;
        
        // Validate input parameters.
        if (isEmpty(uid)) {return self.displayError("Invalid uid","expand");}
        if (nodeElement == null) {return self.displayError("Invalid nodeElement","expand");}
        
        // Update this node to be "expanded".
        nodeElement.setAttribute(self.ATTR_ISEXPANDED, "true");
        
        // Retrieve the corresponding data Element.
        dataID = uid + "__data";
        dataElement = document.getElementById(dataID);
        if (dataElement == null) { return self.displayError("Invalid data Element for " + dataID,"expand"); }
        
        childType = dataElement.getAttribute(self.ATTR_CHILDTYPE1);
        if (self.mode == self.MODE_SELECT_TAXONOMY && !isEmpty(childType) && 
            (childType.toUpperCase().indexOf("GENOME") >= 0)) {return true;}
            
        // Get the number of child genomes.
        childGenomeCount = dataElement.getAttribute(self.ATTR_GENOMECOUNT);
        if (isEmpty(childGenomeCount)) { childGenomeCount = "0"; }
          
        // If the child type is empty, assume it's a genome and add the genome count.
        if (isEmpty(childType)) {
            if (parseInt(childGenomeCount) < 0) { return self.displayError("Invalid child type", "expand"); }
            childType = "Genome:" + childGenomeCount;
        }
        
        childDataPopulated = dataElement.getAttribute(self.ATTR_CHILDDATAPOPULATED);
        if (isEmpty(childDataPopulated)) {childDataPopulated = "false";}
    
        if (childDataPopulated.toUpperCase() == "FALSE") {
        
            // Request genome data asynchronously (but only in certain modes). Otherwise, look in 
            // the XML "data island".
            if (childType.toUpperCase().indexOf("GENOME") >= 0 && (
                self.mode == self.MODE_GENOME_DETAIL || 
                self.mode == self.MODE_SELECT_GENOMES ||
                self.mode == self.MODE_SELECT_ALL)) {
              
                // Will the requested number of data records exceed the progress message threshold?
                if (parseInt(childGenomeCount) > parseInt(self.progressMessageThreshold)) {
               
                    var getAsyncData = false;
                    
                    // The parent depth is used for proper display of the message.
                    parentDepth = dataElement.getAttribute(self.ATTR_DEPTH);
                    if (isEmpty(parentDepth)) { parentDepth = "0"; }
                    
                    // Display a progress message. By assigning the result to getAsyncData, we can 
                    // force the asynchronous call to wait until the message has been successfully displayed.
                    getAsyncData = self.displayProgressMessage(false, parentDepth, uid);
                    
                    if (getAsyncData) { self.getAsyncGenomeData(uid); } 
                    else { return self.displayError("Unable to retrieve asynchronous data", "expand"); }
                    
                } else {
                    // Get the asynchronous genome data.
                    self.getAsyncGenomeData(uid);
                }
                
            } else {
                // Get child data collection.
                childDataCollection = self.getChildDataCollection(uid);
                if (childDataCollection != null) {
                    for (var c=0; c<childDataCollection.length; c++) {
                        childData = childDataCollection[c];
                        if (childData == null) { continue; }
                        
                        // Use the data to generate a display node.
                        self.generateDisplayNode(childData);
                    }
                }
            }
            
            // Ultimately, set childDataPopulated to "true".
            dataElement.setAttribute(self.ATTR_CHILDDATAPOPULATED, "true");
        }
        
        // Update image to expanded.
        imageElement = document.getElementById(uid + "__plusminus");
        if (imageElement == null) {return self.displayError("Invalid imageElement","expand");}
        imageElement.setAttribute("src",self.IMAGE_EXPANDED);
        
        childrenElement = document.getElementById(uid + "__children");
        if (childrenElement == null) { return self.displayError("Invalid childrenElement","expand"); }
        
        childrenElement.className = self.classPrefix + "_VisibleChildren";
        
        if (self.mode != self.MODE_SELECT_TAXONOMY && self.mode != self.MODE_SELECT_ALL) {
        
            // Change the node's color.
            tableElement = document.getElementById(uid + "__table");
            if (tableElement == null) {return self.displayError("Invalid tableElement","expand");}
            
            // Get the node's type.
            type = dataElement.getAttribute(self.ATTR_TYPE);
            if (!isEmpty(type)) {
                tableElement.className = self.classPrefix + "_DisplayTableClass selected" + type + "Class";
            }
        }
    };
    
    
    // Expand all parent nodes above the node represented by this taxNodeID.
    this.expandAboveNode = function (taxNodeID) {
    
        var uid;
        
        if (isEmpty(taxNodeID)) {return self.displayError("Invalid taxNodeID", "expandAboveNode");}
        if (taxNodeID == self.TAXNODEID_ROOT) {return true;}
        
        uid = "tni" + taxNodeID + "_iso_gnm_gne";
        
        self.displayArbitraryNode(uid, null, "true");
    };
    
    
    // Expand all parent nodes above the node represented by this taxNodeID AND the node itself.
    this.expandNode = function (taxNodeID) {
    
        var nodeElement;
        var uid;
        
        if (isEmpty(taxNodeID)) {return self.displayError("Invalid taxNodeID", "expandNode");}
        if (taxNodeID == self.TAXNODEID_ROOT) {return true;}
        
        uid = "tni" + taxNodeID + "_iso_gnm_gne";
        
        self.displayArbitraryNode(uid, null, "true");
        
        nodeElement = document.getElementById(uid);
        if (nodeElement == null) {return self.displayError("Invalid nodeElement", "expandNode");}
        self.expand(uid, nodeElement);
    };
    
    
    // Should we expand or collapse?
    this.expandOrCollapse = function (uid, forceExpand) {
  
        var isExpanded;
        var nodeElement;
 
        if (isEmpty(uid)) {return self.displayError("Invalid uid","expandOrCollapse");}
        
        if (isEmpty(forceExpand)) {forceExpand = "false";}
        
        nodeElement = document.getElementById(uid);
        if (nodeElement == null) {return self.displayError("Invalid node Element","expandOrCollapse");}
        
        if (forceExpand.toUpperCase() == "TRUE") {
            isExpanded = "false";
        } else {
            isExpanded = nodeElement.getAttribute(self.ATTR_ISEXPANDED);
            if (isEmpty(isExpanded)) {isExpanded = "false";}
        }
      
        if (isExpanded.toUpperCase() == "TRUE") {
        
            // Collapse the expanded Element.
            self.collapse(uid, nodeElement);
            
        } else {
            
            // Expand the collapsed Element.
            self.expand(uid, nodeElement);
        }
    };
    
    
    // Export lists of selected taxNodeID's and genomeID's.
    this.exportData = function () {
    
        var ctrlNode;
        var ctrlNodes;
        var sequenceID;
        var taxNodeID;
        var type;
        
        if (self.parentElement == null) {return self.displayError("Invalid parent Element","exportData");}
        
        // Initialize the member variables that maintain export results.
        self.selectedGenomes = "";
        self.selectedTaxNodes = "";
        
        // Get a collection of all input Elements in the tree.
        ctrlNodes = self.parentElement.getElementsByTagName("input");
        if (ctrlNodes == null || ctrlNodes.length < 1) {return self.displayError("No valid data to export","exportData");}
        for (var c=0; c<ctrlNodes.length; c++) {
            ctrlNode = ctrlNodes[c];
            if (ctrlNode == null || ctrlNode.nodeType != 1) {continue;}
            
            type = ctrlNode.getAttribute("type");
            if (isEmpty(type)) {continue;}
            
            // We're only interested in checkboxes that are checked.
            if (type.toUpperCase() == "CHECKBOX" && ctrlNode.checked) {
            
                // Depending on the tree's mode a taxNodeID list and genomeID List can be exported.
                if (self.mode != self.MODE_SELECT_GENOMES && self.mode != self.MODE_GENOME_DETAIL) {
                    taxNodeID = ctrlNode.getAttribute("taxnodeid");
                    if (!isEmpty(taxNodeID)) { self.selectedTaxNodes += taxNodeID + ","; }
                }
                if (self.mode != self.MODE_SELECT_TAXONOMY) {
                    sequenceID = ctrlNode.getAttribute("sequenceID");
                    if (!isEmpty(sequenceID)) { self.selectedGenomes += sequenceID + ","; }
                }
            }
        }
    };

    // Only export the taxNodeID list.
    this.exportTaxNodeIdList = function () {
    
        var ctrlNode;
        var ctrlNodes;
        var taxNodeID;
        var type;
        
        if (self.parentElement == null) {return self.displayError("Invalid parent Element","exportTaxNodeIdList");}
        
        // Initialize the member variables that maintain export results.
        self.selectedGenomes = "";
        self.selectedTaxNodes = "";
        
        // Get a collection of all input Elements in the tree.
        ctrlNodes = self.parentElement.getElementsByTagName("input");
        if (ctrlNodes == null || ctrlNodes.length < 1) {return self.displayError("No valid data to export","exportTaxNodeIdList");}
        
        for (var c=0; c<ctrlNodes.length; c++) {
            ctrlNode = ctrlNodes[c];
            if (ctrlNode == null || ctrlNode.nodeType != 1) {continue;}
            
            type = ctrlNode.getAttribute("type");
            if (isEmpty(type)) {continue;}
            
            // We're only interested in checkboxes that are checked.
            if (type.toUpperCase() == "CHECKBOX" && ctrlNode.checked) {
            
                taxNodeID = ctrlNode.getAttribute("taxnodeid");
                if (!isEmpty(taxNodeID)) { self.selectedTaxNodes += taxNodeID + ","; }
            }
        }
        
        return self.selectedTaxNodes;
    };
    
    
    // Generate a user-interface component for this node data.
    this.generateDisplayNode = function (data) {
      
        var childCount;
        var childType1;
        var childType2;
        var dataID;
        var depth;
        var displayName;
        var displayOffset;
        var nodeChildrenElement;
        var nodeCtrlElement;
        var nodeCtrlTDElement;
        var nodeDIVElement;
        var nodeTableElement;
        var nodeTbodyElement;
        var nodeTRElement;
        var parentElement;
        var parentUID;
        var taxNodeDbxrefCount;
        var taxNodeID;
        var type;
        var uid;
        
        var nodePlusMinusImgElement;
        var nodePlusMinusTDElement;
        var nodeTypeElement;
        var nodeLinkoutElement;
        var nodeNameElement;
        var nodeChildCountElement;
        
        // Validate input parameter.
        if (data == null) {return self.displayError("Invalid data","generateDisplayNode");}
        
        // Get the uid from the data.
        uid = data[self.ATTR_UID];
        if (isEmpty(uid)) {return self.displayError("Invalid uid in data","generateDisplayNode");}
       
        // Get the parent UID from the data.
        parentUID = data[self.ATTR_PARENTUID];
        if (isEmpty(parentUID)) {return self.displayError("Invalid parentUID","generateDisplayNode");}
        
        // Get the parent Element.
        parentElement = document.getElementById(parentUID + "__children");
        if (parentElement == null) {
            // Use the root parent Element instead.
            parentElement = self.parentElement;
            if (parentElement == null) {return self.displayError("Invalid parentElement","generateDisplayNode");}
        }
    
        // Create the Elements.
        nodeCtrlElement = document.createElement("input");
        nodeChildrenElement = document.createElement("div");
        nodeDIVElement = document.createElement("div");
        nodeTableElement = document.createElement("table");
        nodeTbodyElement = document.createElement("tbody");
        nodeTRElement = document.createElement("tr");
    
        nodeCtrlTDElement = document.createElement("td");
        nodePlusMinusTDElement = document.createElement("td");
        nodePlusMinusImgElement = document.createElement("img");
        nodeTypeElement = document.createElement("td");
        nodeLinkoutElement = document.createElement("td");
        nodeNameElement = document.createElement("td");
        nodeChildCountElement = document.createElement("td");
       
        //---------------------------------------------------------------
        // Use the data to determine how to populate the node.
        //---------------------------------------------------------------
        
        childCount = data[self.ATTR_CHILDCOUNT];
        if (isEmpty(childCount)) {childCount = "0";}
   
        displayName = data[self.ATTR_DISPLAYNAME];
        if (isEmpty(displayName)) {return self.displayError("Invalid displayName","generateDisplayNode");}
        nodeNameElement.innerHTML = displayName;
        
        // If the mode is "select taxonomy", don't generate any display nodes below taxonomy.
        if (self.mode == self.MODE_SELECT_TAXONOMY && !isEmpty(data[self.ATTR_TYPE]) && 
            data[self.ATTR_TYPE].toUpperCase() == "GENOME") {return false;}
        
        taxNodeID = data[self.ATTR_TAXNODEID];
        if (isEmpty(taxNodeID)) {return self.displayError("Invalid taxNodeID","generateDisplayNode");}
        
        type = data[self.ATTR_TYPE];
        if (isEmpty(type)) {return self.displayError("Invalid type","generateDisplayNode");}
        nodeTypeElement.innerHTML = type + ":";
        
        // Set attributes.
        nodeDIVElement.setAttribute("id", uid);
        nodeChildrenElement.setAttribute("id", uid + "__children");
        nodeCtrlElement.setAttribute("id", uid + "__ctrl");
        nodeTableElement.setAttribute("id", uid + "__table");
        nodePlusMinusImgElement.setAttribute("id", uid + "__plusminus");
        nodeChildCountElement.setAttribute("id", uid + "__childcount");
        nodeLinkoutElement.setAttribute("id", uid + "__linkouts");
    
        nodeCtrlElement.setAttribute("type","checkbox");
        eval("nodeCtrlElement.onclick = function () {self.handleNodeSelection(uid);}");
        
        // Provide default CSS class names.
        nodeCtrlElement.className = self.classPrefix + "_CtrlClass";
        nodeCtrlTDElement.className = self.classPrefix + "_CtrlTDClass";
        nodeDIVElement.className = self.classPrefix + "_NodeClass";
        nodeTableElement.className = self.classPrefix + "_DisplayTableClass unselected" + type + "Class";
        nodePlusMinusTDElement.className = self.classPrefix + "_PlusMinusImgClass";
        nodeTypeElement.className = self.classPrefix + "_TaxLevelClass";
        nodeLinkoutElement.className = self.classPrefix + "_LinkoutClass";
        nodeNameElement.className = self.classPrefix + "_TaxNameClass";
        nodeChildCountElement.className = self.classPrefix + "_ChildCountClass";
        nodeChildrenElement.className = self.classPrefix + "_HiddenChildren";
        
        
        // Genomes are handled in a different way.
        if (type.toUpperCase() == "GENOME") {
            
            var parentDataElement;
            var parentDataID;
            var parentDepth;
            
            // Use the "no children" image.
            nodePlusMinusImgElement.setAttribute("src", self.IMAGE_NOCHILDREN);
            
            // A genome needs to have its depth calculated.
            parentDataID = parentUID + "__data"; 
            parentDataElement = document.getElementById(parentDataID);
            if (parentDataElement != null) {
                parentDepth = parentDataElement.getAttribute(self.ATTR_DEPTH);
                if (isEmpty(parentDepth)) {parentDepth = "0";}
                depth = parseInt(parentDepth) + 1;
            }
            
            // For convenience, add the parentUID as an attribute on the checkbox.
            nodeCtrlElement.setAttribute(self.ATTR_PARENTUID, parentUID);
            
        } else {    
        
            // For a taxonomy node, if we're in "select taxonomy" mode, we don't want to display
            // "child" genomes. Other modes DO want to display genomes, however. 
            if (parseInt(childCount) == 0 && self.mode == self.MODE_SELECT_TAXONOMY) {
                nodePlusMinusImgElement.setAttribute("src", self.IMAGE_NOCHILDREN);
            } else {
                nodePlusMinusImgElement.setAttribute("src", self.IMAGE_COLLAPSED);
                eval("nodePlusMinusImgElement.onclick = function () {self.expandOrCollapse(uid,'false');};");
            }
            
            // If this is a leaf node of the taxonomy tree, use genome count for "child count".
            if (childCount == "0") {
                childCount = data[self.ATTR_GENOMECOUNT];
                childType1 = "Genome:" + String(childCount);
                childType2 = "";
            } else {
                // Get the child types.
                childType1 = data[self.ATTR_CHILDTYPE1];
                childType2 = data[self.ATTR_CHILDTYPE2];
            }
        
            // Update the child count display.
            self.updateChildCount(uid, childCount, nodeChildCountElement, childType1, childType2);
            
            // Get the node's depth.
            depth = data[self.ATTR_DEPTH];
            if (isEmpty(depth)) {depth = "0";}
            
            // Add the taxnodeID to the ctrl.
            nodeCtrlElement.setAttribute("taxnodeid",data[self.ATTR_TAXNODEID]);
            
            // Assign the handleNodeSelection function to handle the "on click" event.
            nodeCtrlElement.onclick = function () { self.handleNodeSelection(uid); }
        }
        
        // Calculate the left-side offset.
        displayOffset = (parseInt(depth) - parseInt(self.nodeDepthOffset))
            * parseInt(self.leftSideOffsetPerNode);
        nodeTableElement.style.marginLeft = String(displayOffset) + "px";      
        
        // Append the ctrl to its TD.
        nodeCtrlTDElement.appendChild(nodeCtrlElement);
        
        
        // Configure the "linkouts" control.
        taxNodeDbxrefCount = data[self.ATTR_TAXNODEDBXREFCOUNT];
        if (!isEmpty(taxNodeDbxrefCount) && parseInt(taxNodeDbxrefCount) > 0) {
        
            var linkoutText;
            
            // dmd 042409 - Elliot said he preferred "linkout" and to leave out the number.
            //if (parseInt(taxNodeDbxrefCount) == 1) { linkText = " link"; }
            //else { linkText = " links"; }
            //nodeLinkoutElement.innerHTML = taxNodeDbxrefCount + linkText;
            nodeLinkoutElement.innerHTML = "linkout";
        
            // Assign the handleLinksSelection function to handle the "on click" event.
            nodeLinkoutElement.onclick = function (e) { self.getAsyncLinkoutData(displayName, uid, e); }
            
        } else { nodeLinkoutElement.innerHTML = ""; }
        
        // Build the node hierarchy.
        nodePlusMinusTDElement.appendChild(nodePlusMinusImgElement);
        nodeTRElement.appendChild(nodePlusMinusTDElement);
        
        // The mode determines whether or not to include the ctrl Element.
        if ((self.mode == self.MODE_SELECT_GENOMES || self.mode == self.MODE_GENOME_DETAIL) 
        && type.toUpperCase() == "GENOME") {
            nodeTRElement.appendChild(nodeCtrlTDElement);
        } else if (self.mode == self.MODE_SELECT_TAXONOMY && type.toUpperCase() != "GENOME") {
            nodeTRElement.appendChild(nodeCtrlTDElement);
        } else if (self.mode == self.MODE_SELECT_ALL) {
            nodeTRElement.appendChild(nodeCtrlTDElement);
        }
    
        nodeTRElement.appendChild(nodeTypeElement);
        nodeTRElement.appendChild(nodeNameElement);
        nodeTRElement.appendChild(nodeLinkoutElement);
        nodeTRElement.appendChild(nodeChildCountElement);
        
        nodeTbodyElement.appendChild(nodeTRElement);
        nodeTableElement.appendChild(nodeTbodyElement);
        nodeDIVElement.appendChild(nodeTableElement);
        nodeDIVElement.appendChild(nodeChildrenElement);
        
        parentElement.appendChild(nodeDIVElement);  
    };
    
    
    // Generate a user-interface component for genome node data (specifically used by viruses.asp).
    this.generateGenomeDetailNode = function (data) {
      
        var accNum;
        var accNumLink;
        var dataID;
        var depth;
        var displayName;
        var displayOffset;
        var genomeDbxrefCount = 0;
        var genomeDbxrefCount_text = "";
        var genomeID;
        var nodeCtrlElement;
        var nodeCtrlTDElement;
        var nodeDIVElement;
        var nodeTableElement;
        var nodeTbodyElement;
        var nodeTRElement;
        var numBases;
        var parentDataElement;
        var parentDataID;
        var parentDepth;
        var parentElement;
        var parentUID;
        var strain;
        var strainLink;
        var strainName;
        var type;
        var uid;
        
        var accNumLinkElement;
        var accNumTDElement;
        var numBasesTDElement;
        var strainLinkElement;
        var strainTDElement;
        
        // Validate input parameter.
        if (data == null) {return self.displayError("Invalid data","generateGenomeDetailNode");}
        
        // Get the uid from the data.
        uid = data[self.ATTR_UID];
        if (isEmpty(uid)) {return self.displayError("Invalid uid in data","generateGenomeDetailNode");}
        
        // Get the parent UID from the data.
        parentUID = data[self.ATTR_PARENTUID];
        if (isEmpty(parentUID)) {return self.displayError("Invalid parentUID","generateGenomeDetailNode");}
        
        // Get the parent Element.
        parentElement = document.getElementById(parentUID + "__children");
        if (parentElement == null) {
            // Use the root parent Element instead.
            parentElement = self.parentElement;
            if (parentElement == null) {return self.displayError("Invalid parentElement","generateGenomeDetailNode");}
        }
        
 
        // Create the Elements.
        nodeCtrlElement = document.createElement("input");
        nodeDIVElement = document.createElement("div");
        nodeTableElement = document.createElement("table");
        nodeTbodyElement = document.createElement("tbody");
        nodeTRElement = document.createElement("tr");
    
        nodeCtrlTDElement = document.createElement("td");
        nodeLinkoutElement = document.createElement("td");
        
        accNumLinkElement = document.createElement("a");
        accNumTDElement = document.createElement("td");
        numBasesTDElement = document.createElement("td");
        strainLinkElement = document.createElement("a");
        strainTDElement = document.createElement("td");
        
        
        //---------------------------------------------------------------
        // Use the data to determine how to populate the node.
        //---------------------------------------------------------------
        
        accNum = data[self.ATTR_ACCNUM];
        if (isEmpty(accNum)) {return self.displayError("Invalid accNum","generateGenomeDetailNode");}
        
        displayName = data[self.ATTR_DISPLAYNAME];
        if (isEmpty(displayName)) {return self.displayError("Invalid displayName","generateGenomeDetailNode");}
        
        // dmd 042009
        genomeDbxrefCount_text = data[self.ATTR_GENOMEDBXREFCOUNT];
        if (genomeDbxrefCount_text == null || genomeDbxrefCount_text == "") { genomeDbxrefCount = 0; }
        else { genomeDbxrefCount = parseInt(genomeDbxrefCount_text); }
        
        genomeID = data[self.ATTR_GENOMEID];
        if (isEmpty(genomeID)) {return self.displayError("Invalid genomeID","generateGenomeDetailNode");}
        
        numBases = data[self.ATTR_NUMBASES];
        if (isEmpty(numBases)) {return self.displayError("Invalid numBases","generateGenomeDetailNode");}
        numBasesTDElement.innerHTML = numBases + " bases";
        
        strainName = data[self.ATTR_STRAINNAME];
        if (isEmpty(strainName)) {return self.displayError("Invalid strainName","generateGenomeDetailNode");}
        
        type = data[self.ATTR_TYPE];
        if (isEmpty(type)) {return self.displayError("Invalid type","generateGenomeDetailNode");}
        
        
        // Set attributes.
        nodeDIVElement.setAttribute("id", uid);
        nodeCtrlElement.setAttribute("id", uid + "__ctrl");
        nodeLinkoutElement.setAttribute("id", uid + "__linkouts");
        nodeTableElement.setAttribute("id", uid + "__table");
        
        nodeCtrlElement.setAttribute("type","checkbox");
        nodeCtrlElement.setAttribute("sequenceID", genomeID);  // Add the sequence ID so this can be used by the export tools.
        nodeCtrlElement.setAttribute(self.ATTR_PARENTUID, parentUID);
        eval("nodeCtrlElement.onclick = function () {self.handleNodeSelection(uid);}");
        
        // Provide default CSS class names.
        nodeCtrlElement.className = self.classPrefix + "_CtrlClass";
        nodeCtrlTDElement.className = self.classPrefix + "_CtrlTDClass";
        nodeDIVElement.className = self.classPrefix + "_NodeClass";
        nodeTableElement.className = self.classPrefix + "_DisplayTableClass unselected" + type + "Class";
        
        if (strainName != self.previousStrainName) {
            // Toggle the "is alt genome row" variable.
            self.isAltGenomeRow = !self.isAltGenomeRow;   
        } 
        self.previousStrainName = strainName;
        
        if (self.isAltGenomeRow) {
            accNumTDElement.className = self.classPrefix + "_AltAccNumClass";
            nodeLinkoutElement.className = self.classPrefix + "_AltGenomeLinkoutClass";
            numBasesTDElement.className = self.classPrefix + "_AltNumBasesClass";
            strainTDElement.className = self.classPrefix + "_AltStrainClass";
        } else {
            accNumTDElement.className = self.classPrefix + "_AccNumClass";
            nodeLinkoutElement.className = self.classPrefix + "_GenomeLinkoutClass";
            numBasesTDElement.className = self.classPrefix + "_NumBasesClass";
            strainTDElement.className = self.classPrefix + "_StrainClass";
        }
        
        
        nodeCtrlTDElement.align = "center";
        
        
        // Configure the "linkouts" control.
        if (genomeDbxrefCount > 0) {
        
            var linkoutText;
            
            // dmd 042409 - Elliot said he preferred "linkout" and to leave out the number.
            //if (parseInt(genomeDbxrefCount) == 1) { linkText = " link"; }
            //else { linkText = " links"; }
            //nodeLinkoutElement.innerHTML = String(genomeDbxrefCount) + linkText;
            nodeLinkoutElement.innerHTML = "linkout";
        
            // Assign the handleLinkoutSelection function to handle the "on click" event.
            nodeLinkoutElement.onclick = function (e) { self.getAsyncLinkoutData(displayName, uid, e); }
            
        } else { nodeLinkoutElement.innerHTML = ""; }
       
        
        // Generate the accession number link.
        accNumLink = "http://www.ncbi.nlm.nih.gov/entrez/viewer.fcgi?db=nuccore&id=" + accNum;
        accNumLinkElement.innerHTML = accNum;
        accNumLinkElement.setAttribute("href", accNumLink);
        accNumLinkElement.setAttribute("target","_blank");
        
        // Generate the strain (genomeID) link.
        strainLink = "map.asp?genome_id=" + genomeID;
        strainLinkElement.innerHTML = displayName;
        strainLinkElement.setAttribute("href", strainLink);
        
        
        // A genome needs to have its depth calculated.
        parentDataID = parentUID + "__data"; 
        parentDataElement = document.getElementById(parentDataID);
        if (parentDataElement != null) {
            parentDepth = parentDataElement.getAttribute(self.ATTR_DEPTH);
            if (isEmpty(parentDepth)) {parentDepth = "0";}
            depth = parseInt(parentDepth) + 1;
        }

        
        // Calculate the left-side offset.
        displayOffset = (parseInt(depth) - parseInt(self.nodeDepthOffset))
            * parseInt(self.leftSideOffsetPerNode);
        nodeTableElement.style.marginLeft = String(displayOffset) + "px";      
        
        // Append the ctrl to its TD.
        nodeCtrlTDElement.appendChild(nodeCtrlElement);
        
        // Build the node hierarchy.
        
        // The mode determines whether or not to include the ctrl Element.
        if (self.mode == self.MODE_SELECT_GENOMES || self.mode == self.MODE_GENOME_DETAIL ||
        self.mode == self.MODE_SELECT_ALL) {
            nodeTRElement.appendChild(nodeCtrlTDElement);
        } 
    
        // Add the links to their TD's.
        var accNumTextElement = document.createElement("span");
        accNumTextElement.innerHTML = "Accession #:";
        accNumTextElement.className = self.classPrefix + "_GenomeLabel";
        
        accNumTDElement.appendChild(accNumTextElement);
        accNumTDElement.appendChild(accNumLinkElement);
        
        var strainTextElement = document.createElement("span");
        strainTextElement.innerHTML = "Isolate:";
        strainTextElement.className = self.classPrefix + "_GenomeLabel";
        
        strainTDElement.appendChild(strainTextElement);
        strainTDElement.appendChild(strainLinkElement);
        
        // Add the TD's to the TR.
        nodeTRElement.appendChild(strainTDElement);
        nodeTRElement.appendChild(nodeLinkoutElement);
        nodeTRElement.appendChild(accNumTDElement);
        nodeTRElement.appendChild(numBasesTDElement);
        
        nodeTbodyElement.appendChild(nodeTRElement);
        nodeTableElement.appendChild(nodeTbodyElement);
        nodeDIVElement.appendChild(nodeTableElement);
        
        parentElement.appendChild(nodeDIVElement);  
    };
    
    
    // Request genome data asynchronously.
    this.getAsyncGenomeData = function (uid) {
    
        var taxNodeID;
        var url;
        
        // Extract the taxNodeID from the uid.
        taxNodeID = self.getTaxNodeID(uid);
            
        // Get the data using Ajax.
        url = self.asyncGenomeURL + "?taxnodeid=" + taxNodeID;
        self.sendAsyncRequest(url, self.processAsyncGenomeData, "");
    };
    
    
    // Request "linkout" data asynchronously. This is called when the user clicks on the "linkout" control.
    this.getAsyncLinkoutData = function (displayName, uid, e) {
    
        var genomeID;
        var taxNodeID;
        var url;
        var x;
        var y;
        
        // Validate the input parameters.
        if (isEmpty(uid)) { return self.displayError("Invalid uid","getAsyncLinkoutData"); }
        if (isEmpty(displayName)) { return self.displayError("Invalid displayName","getAsyncLinkoutData"); }
   
        x = getMouseX(e);
        y = getMouseY(e);
        
        // Get the taxNodeID from the uid.
        taxNodeID = self.getTaxNodeID(uid);
        if (isEmpty(taxNodeID)) { return self.displayError("Invalid taxNodeID","getAsyncLinkoutData"); }
        
        // Get the genomeID from the uid (if there is one).
        genomeID = self.getGenomeID(uid);
        
        
        
        // Parameters to async page: gene_id,genome_id,name,show_tax_parents,taxnode_id,x,y

        // Get the data using Ajax.
        url = self.asyncLinkoutURL + "?" +
            "name=" + displayName + "&" + 
            "taxnode_id=" + taxNodeID + "&" +
            "x=" + x + "&" + 
            "y=" + y;      
        // Add the genome ID if one was found in the uid.
        if (!isEmpty(genomeID)) { url += "&genome_id=" + genomeID; }

        // Send the request.
        self.sendAsyncRequest(url, processAsyncLinkoutData, "");
    };
    
    
    // dmd 040609 - heavy modifications...
    // Get a child data collection for a particular (parent) uid from XML on the page. 
    this.getChildDataCollection = function (uid) {
    
        var childData;
        var childDataCollection = new Array();
        var childNode;
        var childNodes;
        var dataElement;
        var dataID;
        var taxNodeID;
    
       // Validate parameter.
        if (isEmpty(uid)) {return self.displayError("Invalid uid","getChildDataCollection");}
        
        // Extract the taxNodeID from the uid.
        taxNodeID = self.getTaxNodeID(uid);
         
        // Generate a taxonomy data ID.
        dataID = uid + "__data";
        
        dataElement = document.getElementById(dataID);
        if (dataElement == null) {return self.displayError("Invalid data Element","getChildDataCollection");}
     
        // Get and validate the child nodes of the data Element.
        childNodes = dataElement.childNodes;
        if (childNodes == null || childNodes.length < 1) { /* TODO: anything to return or display??? */ return null; }
        
        for (var c=0; c<childNodes.length; c++) {
        
            childNode = childNodes[c];
            if (childNode == null || childNode.nodeType != 1) {continue;}
            
            childData = new Array();
            
            childData[self.ATTR_ACCNUM] = childNode.getAttribute(self.ATTR_ACCNUM);
            childData[self.ATTR_CHILDCOUNT] = childNode.getAttribute(self.ATTR_CHILDCOUNT);
            childData[self.ATTR_CHILDDATAPOPULATED] = childNode.getAttribute(self.ATTR_CHILDDATAPOPULATED);
            childData[self.ATTR_CHILDTYPE1] = childNode.getAttribute(self.ATTR_CHILDTYPE1);
            childData[self.ATTR_CHILDTYPE2] = childNode.getAttribute(self.ATTR_CHILDTYPE2);
            childData[self.ATTR_GENOMECOUNT] = childNode.getAttribute(self.ATTR_GENOMECOUNT);
            childData[self.ATTR_GENOMEDBXREFCOUNT] = childNode.getAttribute(self.ATTR_GENOMEDBXREFCOUNT);
            childData[self.ATTR_GENOMEID] = childNode.getAttribute(self.ATTR_GENOMEID);
            childData[self.ATTR_DEPTH] = childNode.getAttribute(self.ATTR_DEPTH);
            childData[self.ATTR_DISPLAYNAME] = childNode.getAttribute(self.ATTR_DISPLAYNAME);
            childData[self.ATTR_ISEXPANDED] = childNode.getAttribute(self.ATTR_ISEXPANDED);
            childData[self.ATTR_NUMBASES] = childNode.getAttribute(self.ATTR_NUMBASES);
            childData[self.ATTR_PARENTUID] = childNode.getAttribute(self.ATTR_PARENTUID);
            childData[self.ATTR_STRAINNAME] = childNode.getAttribute(self.ATTR_STRAINNAME);
            childData[self.ATTR_TAXNODEDBXREFCOUNT] = childNode.getAttribute(self.ATTR_TAXNODEDBXREFCOUNT);
            childData[self.ATTR_TAXNODEID] = childNode.getAttribute(self.ATTR_TAXNODEID);
            childData[self.ATTR_TYPE] = childNode.getAttribute(self.ATTR_TYPE);
            childData[self.ATTR_UID] = childNode.getAttribute(self.ATTR_UID);
            
            // Add the child data to the child data collection.
            childDataCollection[childDataCollection.length] = childData;
        }
   
        return childDataCollection;
    };
    
    
    // With an example uid of "tni1002_isoMARV-Ravn_Lofts_gnm4119_gne", genomeID is 4119.
    this.getGenomeID = function (uid) {
    
        var token;
        var tokens;
        
        tokens = uid.split("_");
        for (var t=0; t<tokens.length; t++) {
            token = tokens[t];
            if (isEmpty(token)) {continue;}
            if (token.indexOf("gnm") == 0) {
                return token.substr(3);
            }
        }
    };
    
    
    // With an example uid of "tni1002_isoMARV-Ravn_Lofts_gnm4119_gne", taxNodeID is 1002.
    this.getTaxNodeID = function (uid) {
    
        var token;
        var tokens;
        
        tokens = uid.split("_");
        for (var t=0; t<tokens.length; t++) {
            token = tokens[t];
            if (isEmpty(token)) {continue;}
            if (token.indexOf("tni") == 0) {
                return token.substr(3);
            }
        }
    };
    
   
    // This function is called when the user checks or unchecks a ctrlElement.
    this.handleNodeSelection = function (uid) {

        var childUID;
        var ctrlElement;
        var dataElement;
        var dataID;
        var nodeElement;
        var parentUID;
     
        // Get the input parameter.
        if (isEmpty(uid)) {return self.displayError("Invalid uid","handleNodeSelection");}
  
        // Get the ctrl Element (checkbox).
        ctrlElement = document.getElementById(uid + "__ctrl");
        if (ctrlElement != null) {
            if (ctrlElement.checked) {isSelected = true;}
            else {isSelected = false;}
        } else {
            isSelected = false;
        }
    
        // Generate a data ID and try to retrieve the Element from the XML data island.
        dataID = uid + "__data"; 
        dataElement = document.getElementById(dataID);
        if (dataElement != null) {
        
            // Get the parentUID and child nodes collection.
            parentUID = dataElement.getAttribute(self.ATTR_PARENTUID);
            childNodes = dataElement.childNodes;
            
        } else {
            // dmd 102408 - maybe kind of a hack, but the parentUID is added as an attribute on 
            // the ctrl Element for Genomes...
            parentUID = ctrlElement.getAttribute(self.ATTR_PARENTUID);
            childNodes = null;
        }
        
        // Should the node be selected or deselected?
        if (isSelected == true) {
    
            // Select the node.
            self.select(uid);
           
            // Deselect all children.
            if (childNodes != null && childNodes.length > 0) {
            
                for (var c=0; c<childNodes.length; c++) {
                    childNode = childNodes[c];
                    if (childNode == null || childNode.nodeType != 1) {continue;}
                    
                    childUID = childNode.getAttribute(self.ATTR_UID);
                    if (isEmpty(childUID)) {continue;}
                    
                    self.deselect(childUID, self.DESELECT_CHILDREN);
                }
            }
            
            // Deselect any parents.
            if (!isEmpty(parentUID)) {
                self.deselect(parentUID, self.DESELECT_PARENTS);
            } 
            
        } else {
     
            // Deselect the node and its children.
            self.deselect(uid, self.DESELECT_CHILDREN);
            
            // Here's the tricky part - should the parents be deselected, as well? That depends on whether
            // they have over child selections...
            
            // Deselect any parents.
            if (isEmpty(parentUID)) {return true;}
            
            self.deselect(parentUID, self.DESELECT_PARENTS);     
        }
    };
    
    
    // Display the tree UI using this taxNodeID as the parent.
    this.populate = function (taxNodeID) {
    
        var data = new Array();
        var dataElement;
        var dataID;
        
        dataID = "tni" + taxNodeID + "_iso_gnm_gne__data";
        dataElement = document.getElementById(dataID);
        if (dataElement == null) {return self.displayError("Invalid dataElement","populate");}
        
        data[self.ATTR_ACCNUM] = dataElement.getAttribute(self.ATTR_ACCNUM);
        data[self.ATTR_CHILDCOUNT] = dataElement.getAttribute(self.ATTR_CHILDCOUNT);
        data[self.ATTR_CHILDDATAPOPULATED] = dataElement.getAttribute(self.ATTR_CHILDDATAPOPULATED);
        data[self.ATTR_CHILDTYPE1] = dataElement.getAttribute(self.ATTR_CHILDTYPE1);
        data[self.ATTR_CHILDTYPE2] = dataElement.getAttribute(self.ATTR_CHILDTYPE2);
        data[self.ATTR_GENOMECOUNT] = dataElement.getAttribute(self.ATTR_GENOMECOUNT);
        data[self.ATTR_GENOMEDBXREFCOUNT] = dataElement.getAttribute(self.ATTR_GENOMEDBXREFCOUNT);
        data[self.ATTR_GENOMEID] = dataElement.getAttribute(self.ATTR_GENOMEID);
        data[self.ATTR_DEPTH] = dataElement.getAttribute(self.ATTR_DEPTH);
        data[self.ATTR_DISPLAYNAME] = dataElement.getAttribute(self.ATTR_DISPLAYNAME);
        data[self.ATTR_ISEXPANDED] = dataElement.getAttribute(self.ATTR_ISEXPANDED);
        data[self.ATTR_NUMBASES] = dataElement.getAttribute(self.ATTR_NUMBASES);
        data[self.ATTR_PARENTUID] = dataElement.getAttribute(self.ATTR_PARENTUID);
        data[self.ATTR_STRAINNAME] = dataElement.getAttribute(self.ATTR_STRAINNAME);
        data[self.ATTR_TAXNODEDBXREFCOUNT] = dataElement.getAttribute(self.ATTR_TAXNODEDBXREFCOUNT);
        data[self.ATTR_TAXNODEID] = dataElement.getAttribute(self.ATTR_TAXNODEID);
        data[self.ATTR_TYPE] = dataElement.getAttribute(self.ATTR_TYPE);
        data[self.ATTR_UID] = dataElement.getAttribute(self.ATTR_UID);
        
        self.generateDisplayNode(data);
    };
    
    
    // Populate the child nodes of the node with this taxNodeID.
    this.populateChildren = function (taxNodeID) {
        
        var childDataCollection;
        var childType1;
        var childType2;
        var data = new Array();
        var dataElement;
        var dataID;
        var uid;
        
        if (isEmpty(taxNodeID)) {
            // An empty taxNodeID defaults to the tree root.
            uid = "treeRoot__Taxonomy";
        } else {
            uid = "tni" + taxNodeID + "_iso_gnm_gne";
        }
        
        dataID = uid + "__data";
        dataElement = document.getElementById(dataID);
        if (dataElement == null) {return self.displayError("Invalid dataElement for uid " + uid,"populateChildren");}
        
        childType1 = dataElement.getAttribute(self.ATTR_CHILDTYPE1);
        
        // Get child data collection.
        childDataCollection = self.getChildDataCollection(uid);
        if (childDataCollection != null && childDataCollection.length > 0) {
        
            if (!isEmpty(childType1) && (childType1.toUpperCase().indexOf("GENOME") >= 0) && 
                (self.mode == self.MODE_GENOME_DETAIL || self.mode == self.MODE_SELECT_ALL)) { 
            
                for (var c=0; c<childDataCollection.length; c++) {
                    var childData = childDataCollection[c];
                    if (childData == null) {continue;}
                    
                    // Use the data to generate a (genome) display node.
                    self.generateGenomeDetailNode(childData);
                }
                
            } else {
            
                for (var c=0; c<childDataCollection.length; c++) {
                    var childData = childDataCollection[c];
                    if (childData == null) {continue;}
                    
                    // Use the data to generate a display node.
                    self.generateDisplayNode(childData);
                }
            }
            
            // Update the parent node's child count.
            self.updateChildCount(uid, childDataCollection.length, childType1, childType2);
        }
        
        // Ultimately, set childDataPopulated to "true".
        dataElement.setAttribute(self.ATTR_CHILDDATAPOPULATED, "true");
    };
    
    
    
    // Handle an asynchronous request for genome data.
    this.processAsyncGenomeData = function (result) {
    
        var childData;
        var dataID;
        var dataElement;
        var genomeNode;
        var genomeNodes;
        var parentUID;
        var taxNodeID;
        var xmlDoc;
        
        // Validate the input.
        if (result == null) {return self.displayError("Invalid result","processAsyncGenomeData");}
        
        // The responseXML attribute of the result should be an XML document.
        xmlDoc = result.responseXML;
        
        genomeNodes = xmlDoc.getElementsByTagName("genome"); 
        if (genomeNodes == null || genomeNodes.length < 1) {return self.displayError("Invalid genomeNodes.length","processAsyncGenomeData");}
    
        // Iterate thru the genome nodes.
        for (var c=0; c<genomeNodes.length; c++) {
        
            genomeNode = genomeNodes[c];
            if (genomeNode == null || genomeNode.nodeType != 1) {continue;}
   
            childData = new Array();
            
            childData[self.ATTR_ACCNUM] = genomeNode.getAttribute(self.ATTR_ACCNUM);
            childData[self.ATTR_CHILDCOUNT] = genomeNode.getAttribute(self.ATTR_CHILDCOUNT);
            childData[self.ATTR_CHILDDATAPOPULATED] = genomeNode.getAttribute(self.ATTR_CHILDDATAPOPULATED);
            childData[self.ATTR_CHILDTYPE1] = genomeNode.getAttribute(self.ATTR_CHILDTYPE1);
            childData[self.ATTR_CHILDTYPE2] = genomeNode.getAttribute(self.ATTR_CHILDTYPE2);
            childData[self.ATTR_GENOMECOUNT] = genomeNode.getAttribute(self.ATTR_GENOMECOUNT);
            childData[self.ATTR_GENOMEDBXREFCOUNT] = genomeNode.getAttribute(self.ATTR_GENOMEDBXREFCOUNT);
            childData[self.ATTR_GENOMEID] = genomeNode.getAttribute(self.ATTR_GENOMEID);
            childData[self.ATTR_DEPTH] = genomeNode.getAttribute(self.ATTR_DEPTH);
            childData[self.ATTR_DISPLAYNAME] = genomeNode.getAttribute(self.ATTR_DISPLAYNAME);
            childData[self.ATTR_ISEXPANDED] = genomeNode.getAttribute(self.ATTR_ISEXPANDED);
            childData[self.ATTR_NUMBASES] = genomeNode.getAttribute(self.ATTR_NUMBASES);
            childData[self.ATTR_PARENTUID] = genomeNode.getAttribute(self.ATTR_PARENTUID);
            childData[self.ATTR_STRAINNAME] = genomeNode.getAttribute(self.ATTR_STRAINNAME);
            childData[self.ATTR_TAXNODEDBXREFCOUNT] = genomeNode.getAttribute(self.ATTR_TAXNODEDBXREFCOUNT);
            childData[self.ATTR_TAXNODEID] = genomeNode.getAttribute(self.ATTR_TAXNODEID);
            childData[self.ATTR_TYPE] = genomeNode.getAttribute(self.ATTR_TYPE);
            childData[self.ATTR_UID] = genomeNode.getAttribute(self.ATTR_UID);
         
            // Try to retrieve the parent node's dataElement.
            if (isEmpty(dataID)) {
                taxNodeID = childData[self.ATTR_TAXNODEID];
                dataID = "tni" + taxNodeID + "_iso_gnm_gne__data";
                dataElement = document.getElementById(dataID);
            }
            
            if (dataElement != null) {
                var genomeDataNode = document.createElement("div");
                
                genomeDataNode.setAttribute(self.ATTR_ACCNUM, childData[self.ATTR_ACCNUM]);
                genomeDataNode.setAttribute(self.ATTR_CHILDCOUNT, childData[self.ATTR_CHILDCOUNT]);
                genomeDataNode.setAttribute(self.ATTR_CHILDDATAPOPULATED, childData[self.ATTR_CHILDDATAPOPULATED]);
                genomeDataNode.setAttribute(self.ATTR_CHILDTYPE1, childData[self.ATTR_CHILDTYPE1]);
                genomeDataNode.setAttribute(self.ATTR_CHILDTYPE2, childData[self.ATTR_CHILDTYPE2]);
                genomeDataNode.setAttribute(self.ATTR_GENOMECOUNT, childData[self.ATTR_GENOMECOUNT]);
                genomeDataNode.setAttribute(self.ATTR_GENOMEDBXREFCOUNT, childData[self.ATTR_GENOMEDBXREFCOUNT]);
                genomeDataNode.setAttribute(self.ATTR_GENOMEID, childData[self.ATTR_GENOMEID]);
                genomeDataNode.setAttribute(self.ATTR_DEPTH, childData[self.ATTR_DEPTH]);
                genomeDataNode.setAttribute(self.ATTR_DISPLAYNAME, childData[self.ATTR_DISPLAYNAME]);
                genomeDataNode.setAttribute(self.ATTR_ISEXPANDED, childData[self.ATTR_ISEXPANDED]);
                genomeDataNode.setAttribute(self.ATTR_NUMBASES, childData[self.ATTR_NUMBASES]);
                genomeDataNode.setAttribute(self.ATTR_PARENTUID, childData[self.ATTR_PARENTUID]);
                genomeDataNode.setAttribute(self.ATTR_STRAINNAME, childData[self.ATTR_STRAINNAME]);
                genomeDataNode.setAttribute(self.ATTR_TAXNODEDBXREFCOUNT, childData[self.ATTR_TAXNODEDBXREFCOUNT]);
                genomeDataNode.setAttribute(self.ATTR_TAXNODEID, childData[self.ATTR_TAXNODEID]);
                genomeDataNode.setAttribute(self.ATTR_TYPE, childData[self.ATTR_TYPE]);
                genomeDataNode.setAttribute(self.ATTR_UID, childData[self.ATTR_UID]);
            
                dataElement.appendChild(genomeDataNode);
            }
        }

        // Use the taxNodeID to generate a parent UID.
        parentUID = "tni" + taxNodeID + "_iso_gnm_gne";

        // Update the parent's data Element so that it's children are specified as Genomes.
        var parentDataElement = document.getElementById(parentUID + "__data");
        if (parentDataElement == null) { return self.displayError("Invalid parent data Element","processAsyncGenomeData"); }
        
        parentDataElement.setAttribute(self.ATTR_CHILDTYPE1, "Genome");
        
        // Populate the children of this parent taxNodeID.
        self.populateChildren(taxNodeID);
        
        // Hide/remove the progress message.
        self.displayProgressMessage(true, "0", parentUID);
    };
    
    
    
    //b-storming:
    // - we need x, y to position a dialog.
    // - dialog title determined client side (hard-coded)
    // - each of the data nodes determines an option in the dialog.
    // - each dialog option will have (at least) a label and a URL. 
    // - additionally, dialog options might display "interesting" text.
    
    // TODO: code to generate a dialog (don't I have this for things in KDB?)
    
    
    // dmd 042409 - moved to utilityFunctions.js.
    // Handle an asynchronous request for linkout data.
    //this.processAsyncLinkoutData = function (result) {
      
            
     
    // Select the node that corresponds to a particular unique ID.
    this.select = function (uid) {
    
        var childUID;
        var ctrlElement;
        var dataElement;
        var dataID;
        var nodeElement;
        var tableElement;
        var type;
        
        // Validate the input parameter.
        if (isEmpty(uid)) {return self.displayError("Invalid uid","select");}
        
        // Get the ctrl Element, validate it, and make sure it's updated.
        ctrlElement = document.getElementById(uid + "__ctrl");
        if (ctrlElement == null) {return false;}
        ctrlElement.checked = true;
        
        // Look for a corresponding data Element.
        dataID = uid + "__data";
        dataElement = document.getElementById(dataID);
        if (dataElement != null) {
        
            // Update the data Element to indicated that this node is selected.
            dataElement.setAttribute(self.ATTR_ISSELECTED, "true");
        
            // Get the node's type.
            type = dataElement.getAttribute(self.ATTR_TYPE);
        
        } else {
            // dmd 102408 - is this a hack???
            type = "Genome";
        }

        
        
        // Change the node's color.
        tableElement = document.getElementById(uid + "__table");
        if (tableElement == null) {return self.displayError("Invalid tableElement","deselect");}

        if (!isEmpty(type)) {
            tableElement.className = self.classPrefix + "_DisplayTableClass selected" + type + "Class";
        }
        
        return true;
    };
    
    
    // Expand all parent nodes above the node represented by this taxNodeID AND then select the node.
    this.selectNode = function (taxNodeID, expandBelow) {
    
        var childNode;
        var childNodes;
        var nodeElement;
        var uid;
        
        if (isEmpty(taxNodeID)) {return self.displayError("Invalid taxNodeID", "selectNode");}
        
        if (taxNodeID == self.TAXNODEID_ROOT) {
        
            // Get the taxNodeID of all families (8 refers to "all families").
            dataID = "treeRoot__Taxonomy__data";
        
            nodeElement = document.getElementById(dataID);
            if (nodeElement == null) {return self.displayError("Invalid nodeElement", "selectNode");}
            
            childNodes = nodeElement.childNodes;
            if (childNodes != null && childNodes.length > 0) {
                for (var c=0; c<childNodes.length; c++) {
                    childNode = childNodes[c];
                    if (childNode == null || childNode.nodeType != 1) {continue;}
                    
                    taxNodeID = childNode.getAttribute(self.ATTR_TAXNODEID);
                    if (!isEmpty(taxNodeID)) {
                        uid = "tni" + taxNodeID + "_iso_gnm_gne";
                        self.select(uid);
                    }
                }
            }
        
        } else {
        
            uid = "tni" + taxNodeID + "_iso_gnm_gne";
            
            self.displayArbitraryNode(uid, null, expandBelow);
            
            nodeElement = document.getElementById(uid);
            if (nodeElement == null) {return self.displayError("Invalid nodeElement", "selectNode");}
            self.select(uid);
        }
    };
    
    
    // Send an asynchronous request to the server (from http://www.quirksmode.org/js/xmlhttp.html).
    this.sendAsyncRequest = function (url, callback, postData) {
    
        var method;
	    var req;
	    var xmlHttpObject;
	    
	    // Instantiate the XMLHTTP object.
	    xmlHttpObject = self.createXMLHTTPObject();
	    if (xmlHttpObject == null) { return self.displayError("Invalid XML HTTP object", "sendAsyncRequest"); }
	    
	    method = (postData) ? "POST" : "GET";
	    
	    xmlHttpObject.open(method, url, true);
	    xmlHttpObject.setRequestHeader('User-Agent','XMLHTTP/1.0');
	    
	    if (postData) { xmlHttpObject.setRequestHeader('Content-type','application/x-www-form-urlencoded'); }
	    
	    xmlHttpObject.onreadystatechange = function () {
		    if (xmlHttpObject.readyState != 4) { return; }
		    if (xmlHttpObject.status != 200 && xmlHttpObject.status != 304) { return; }
		    
		    callback(xmlHttpObject);
	    }
	    
	    if (xmlHttpObject.readyState == 4) return;
	    xmlHttpObject.send(postData);
    };


// TODO: I think number can be removed from param list...
    // Update a parent node's child count.
    this.updateChildCount = function (uid, number, childCountElement, childType1, childType2) {
    
        var count = 0;
        var countAsString;
        var displayText = "";
        var taxLevel;
        var tokenArray;
        
        if (isEmpty(uid)) {return self.displayError("Invalid uid","updateChildCount");}
        if (isEmpty(childType1)) {return false;} 
        
        // If no child count Element was provided as an input parameter, look it up using the uid.
        if (childCountElement == null) {
            childCountElement = document.getElementById(uid + "__childcount");
            if (childCountElement == null) {return self.displayError("Invalid childCountElement","updateChildCount");}
        }
        
        // TODO: should we raise an exception if there aren't 2 tokens?
        // Tokenize the child type based on a colon (which should separate level and count).
        tokenArray = childType1.split(":");
        if (tokenArray.length == 2) {
            taxLevel = tokenArray[0];
            countAsString = tokenArray[1];
            count = parseInt(countAsString);
        } 
        
        // Determine the correct taxonomy level name (singular or plural based on count).
        displayText = self.determineNameFromCount(taxLevel, count);
        
        childCountElement.innerHTML = "(" + String(count) + " " + displayText; 
        
        if (!isEmpty(childType2)) {
        
            // TODO: should we raise an exception if there aren't 2 tokens?
            // Tokenize the child type based on a colon (which should separate level and count).
            tokenArray = childType2.split(":");
            if (tokenArray.length == 2) {
                taxLevel = tokenArray[0];
                countAsString = tokenArray[1];
                count = parseInt(countAsString);
            } 
            
            // Determine the correct taxonomy level name (singular or plural based on count).
            displayText = self.determineNameFromCount(taxLevel, count);
            
            if (!isEmpty(displayText)) { childCountElement.innerHTML += ", " + String(count) + " " + displayText; } 
        }
        
        childCountElement.innerHTML += ")";
    };
                
    
    // Determine the singular or plural name of a taxonomy level based on the count.
    this.determineNameFromCount = function (taxLevel, count) {
    
        var displayText = "";
        
        if (isEmpty(taxLevel)) { return null; }
        
        switch (taxLevel.toUpperCase()) {
        
            case "GENE":
                if (count == 1) {displayText = "gene";} 
                else {displayText = "genes";}
                break;
            
            case "GENOME":
                if (count == 1) {displayText = "isolate";} 
                else {displayText = "isolates";}
                break;
             
            case "GENUS":
                if (count == 1) {displayText = "genus";} 
                else {displayText = "genera";}
                break;
              
            case "ISOLATE":
                if (count == 1) {displayText = "isolate";} 
                else {displayText = "isolates";}
                break;
                   
            case "GENOTYPE":
                if (count == 1) {displayText = "genotype";} 
                else {displayText = "genotypes";}
                break;
                
            case "SUBFAMILY":
                if (count == 1) {displayText = "subfamily";} 
                else {displayText = "subfamilies";}
                break;
                
            case "SUBGENOTYPE":
                if (count == 1) {displayText = "subtype";} 
                else {displayText = "subtypes";}
                break;
            
            case "SUBTYPE":
                if (count == 1) {displayText = "subtype";} 
                else {displayText = "subtypes";}
                break;
                    
            case "SPECIES":
                displayText = "species";
                break;
                
            case "STRAIN":
                if (count == 1) {displayText = "strain";} 
                else {displayText = "strains";}
                break;  
                
            case "TYPE":
                if (count == 1) {displayText = "type";} 
                else {displayText = "types";}
                break;
            
            case "SEROTYPE":
                if (count == 1) {displayText = "serotype";} 
                else {displayText = "serotypes";}
                break; 
        }
        
        return displayText;
    };
              
    
    //---------------------------------------------------------------------------
    // This is executed when the object is instantiated.
    //---------------------------------------------------------------------------

    if (isEmpty(parentUID)) { return self.displayError("Invalid parent uid", ""); }
    
    self.parentUID = parentUID;
    self.parentElement = document.getElementById(parentUID);
    if (self.parentElement == null) { return self.displayError("Invalid parentElement", ""); }
    
    // Validate the mode parameter.
    if (typeof(mode) == "number") {self.mode = mode;}
    else if (typeof(mode) == "string") {
        try {
            self.mode = parseInt(mode);
        } catch (ex) { return self.displayError("Invalid mode", ""); }
    }

};