// VOTable viewer
// R. White, 2007 April 5

var minradius = 0.01;	// minimum radius for footprint view
var maxradius = 10.0;	// maximum radius for footprint view
var maxDSSradius = 5.0;	// maximum footprint radius to include DSS image

function getTextContent(el) {
	var txt = el.textContent;
	if (txt != undefined) {
		return txt;
	} else {
		return getTCRecurs(el);
	}
}

function getTCRecurs(el) {
	// recursive method to get text content of an element
	// used only if the textContent attribute is not defined (e.g., in Safari)
	var x = el.childNodes;
	var txt = '';
	for (var i=0, node; node=x[i]; i++) {
		if (3 == node.nodeType) {
			txt += node.data;
		} else if (1 == node.nodeType) {
			txt += getTCRecurs(node);
		}
	}
	return txt;
}

function getElementsByClass(searchClass,node,tag) {
	var classElements = new Array();
	if (node == undefined) node = document;
	if (tag == undefined) tag = '*';
	var els = node.getElementsByTagName(tag);
	var elsLen = els.length;
	var pattern = new RegExp("(^|\\s)"+searchClass+"(\\s|$)");
	for (var i = 0, j = 0; i < elsLen; i++) {
		if (pattern.test(els[i].className) ) {
			classElements[j] = els[i];
			j++;
		}
	}
	return classElements;
}

// image error callback

function imageerror(img, msg) {
	var width = img.width || 256;
	var height = img.height || width;
	msg = msg || "No image";
	// replace image with a message of the same size
	// use a table so vertical centering works
	var table = document.createElement("table");
	table.style.width = width+"px";
	table.style.height = height+"px";
	table.style.backgroundColor = "#eee";
	table.style.fontSize = "90%";
	var row = table.insertRow(-1);
	var td = document.createElement("td");
	td.style.verticalAlign = "middle";
	td.style.textAlign = "center";
	td.innerHTML = msg;
	row.appendChild(td);
	img.parentNode.replaceChild(table, img);
}

// remove a blank-delimited string sub from string s
// if sub occurs multiple times, all are removed
// also normalizes the string by removing blanks

function removeSubstring(s,sub) {
	var flist = s.split(' ');
	var glist = [];
	for (var i=0, f; f = flist[i]; i++) {
		if (f && f != sub) {
			glist.push(f);
		}
	}
	return glist.join(' ');
}

// hide shopping cart icons by modifying CSS

function hidecart() {
	if (! document.styleSheets) return;
	var sheet = document.styleSheets[0];
	if (! sheet) return;
	if (sheet.rules) {
		var cssRules = 'rules';
	} else if (sheet.cssRules) {
		cssRules = 'cssRules';
	} else {
		return;
	}
	for (var S = 0; S < document.styleSheets.length; S++) {
		var rules = document.styleSheets[S][cssRules];
		for (var R = 0; R < rules.length; R++) {
			var rule = rules[R];
			if (rule.selectorText && rule.selectorText.match(/\.cartitem($|[: ,])/)) {
				rule.style.backgroundImage = "";
				rule.style.paddingLeft = "";
			}
			if (rule.selectorText && rule.selectorText.match(/#hlacart($|[: ,])/)) {
				rule.style.backgroundImage = "url(/images/hcart_in.png)";
				rule.style.backgroundColor = "#eee";
				rule.style.color = "#aaa";
			}
		}
	}
}

// CSS rule acccess routines
// from http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript
// with some modifications by RLW

function getCSSRule(ruleName, doc, deleteFlag) {
   ruleName = ruleName.toLowerCase();
   if (! doc) doc = document;
   if (doc.styleSheets) {
      var styleSheet = doc.styleSheets[0];
      if (styleSheet.rules) {
         var cssRules = 'rules';
      } else if (styleSheet.cssRules) {
         cssRules = 'cssRules';
      } else {
         return;
      }
      for (var i=0; i<doc.styleSheets.length; i++) {
         styleSheet = doc.styleSheets[i];
		 var rules = styleSheet[cssRules];
         for (var ii=0; ii < rules.length; ii++) {
            var cssRule = rules[ii];
            if (cssRule.selectorText && cssRule.selectorText.toLowerCase()==ruleName) {
               if (deleteFlag=='delete') {
                  if (styleSheet.cssRules) {
                     styleSheet.deleteRule(ii);
                  } else {
                     styleSheet.removeRule(ii);
                  }
                  return true;
               } else {
                  return cssRule;
               }
            }
         }
      }
   }
   return false;
}

function killCSSRule(ruleName) {
   if (! doc) doc = document;
   return getCSSRule(ruleName,doc,'delete');
}

function addCSSRule(ruleName,doc) {
   if (! doc) doc = document;
   if (doc.styleSheets) {
      if (!getCSSRule(ruleName,doc)) {
         if (doc.styleSheets[0].addRule) {
            doc.styleSheets[0].addRule(ruleName, null,0);
         } else {
            doc.styleSheets[0].insertRule(ruleName+' { }', 0);
         }
      }
   }
   return getCSSRule(ruleName,doc);
}

// Generate a random string to attach to URL
function randomString() {
	return String((new Date()).getTime()).replace(/\D/gi,'');
}

// Validates that a string contains only valid numbers.
// Returns true if valid, otherwise false.

function validateNumeric(strValue) {
	var objRegExp  =  /^\s*(([-+]?\d\d*\.\d*$)|([-+]?\d\d*$)|([-+]?\.\d\d*))\s*$/;
	return objRegExp.test(strValue);
}

function validateInteger(strValue) {
	var objRegExp  =  /^\s*[-+]?\d\d*\s*$/;
	return objRegExp.test(strValue);
}

// Validates and parses a string containing a RA/Dec value (including
// colon-separated or space-separated sexagesimal format).
// Returns a float if value, undefined if not valid.
// Set hfactor to 15 for RA field (hh:mm:ss).  Assumes that a single float
// value is always degrees (so it does not use hfactor in that case.)

function parseCoordinate(strValue, hfactor) {
	if (! hfactor) hfactor = 1.0;
	if (validateNumeric(strValue)) {
		return parseFloat(strValue);
	}
	var flist = strValue.split(':');
	if (flist.length < 2) {
		flist = strValue.split(' ');
		if (flist.length < 2) {
			return undefined;
		}
	}
	// require 2 or 3 non-null fields for sexagesimal format
	var nonnull = [];
	for (var i=0; i < flist.length; i++) {
		var v = trim(flist[i]);
		if (v) {
			nonnull.push(v);
		}
	}
	flist = nonnull;
	if (flist.length > 3 || flist.length < 2) return undefined;
	i = flist.length - 1;
	if (validateNumeric(flist[i])) {
		var value = parseFloat(flist[i]);
	} else {
		return undefined;
	}
	var sign = 1;
	if (flist[0].charAt(0) == '-') sign = -1;
	flist[0] *= sign; 
	for (i=i-1; i >= 0; i--) {
		if (validateInteger(flist[i])) {
			value = parseInt(flist[i],10) + value/60.0;
		} else {
			return undefined;
		}
	}
	return sign*value*hfactor;
}

function sexagesimal(value, scale, ndigits) {
	if (scale == undefined) scale = 1;
	if (ndigits == undefined) {
		if (scale == 1) {
			ndigits = 2;
		} else {
			ndigits = 3;
		}
	}
	value = value/scale;
	if (value < 0) {
		value = Math.abs(value);
		var sign = "-";
	} else if (scale == 1) {
		sign = "+";
	} else {
		sign = "";
	}
	var dd = Math.floor(value);
	value = (value-dd)*60;
	var mm = Math.floor(value);
	var ss = (value-mm)*60;
	ss = ss.toFixed(ndigits);
	var v = parseFloat(ss);
	if (v >= 60) {
		v = v-60;
		ss = v.toFixed(ndigits);
		mm = mm+1;
		if (mm == 60) {
			mm = 0;
			dd = dd+1;
		}
	}
	if (ss.length == ndigits+2) {
		ss = "0"+ss;
	}
	mm = mm.toString();
	if (mm.length == 1) mm = "0"+mm;
	dd = dd.toString();
	if (dd.length == 1) dd = "0"+dd;
	return sign+dd+":"+mm+":"+ss;
}

// pack form parameters into a GET string

function getFormPars(formname) {
	if (typeof(formname) == "string") {
		var form = document.forms[formname];
	} else {
		form = formname;
	}
	var parlist = [];
	for (var i=0; i<form.elements.length; i++) {
		var el = form.elements[i];
		if (el.tagName == "INPUT") {
			var value = encodeURIComponent(el.value);
			if (el.type == "text" || el.type == "hidden") {
				parlist.push(el.name + "=" + value);
			} else if (el.type == "checkbox") {
				if (el.checked) {
					parlist.push(el.name + "=" + value);
				}
			} else if (el.type == "radio") {
				if (el.checked) {
					parlist.push(el.name + "=" + value);
				}
			}
		} else if (el.tagName == "SELECT") {
			parlist.push(el.name + "=" + encodeURIComponent(el.options[el.selectedIndex].value));
		}
	}
	return parlist.join("&");
}

// extract form parameters from a GET string and set form values

function setFormPars(formname,getstr) {
	if (typeof(formname) == "string") {
		var form = document.forms[formname];
	} else {
		form = formname;
	}
	// initialize all checkboxes to unchecked
	for (var j=0; j < form.elements.length; j++) {
		var el = form.elements[j];
		if (el.tagName == "INPUT" && el.type == "checkbox") {
			el.checked = false;
		}
	}
	var parlist = getstr.split("&");
	for (var i=0; i<parlist.length; i++) {
		var f = parlist[i].split("=");
		if (f.length < 2) {
			var name = parlist[i];
			var value = "";
		} else {
			// don't know if embedded '=' can happen, but might as well handle it
			name = f.shift();
			value = decodeURIComponent(f.join("="));
		}
		el = form[name];
		if (el != undefined) {
			if (el.tagName == "INPUT") {
				if (el.type == "checkbox") {
					if (value) {
						el.checked = true;
					} else {
						el.checked = false;
					}
				} else {
					// text or hidden element
					el.value = value;
				}
			} else if (el.tagName == "SELECT") {
				for (j=0; j < el.options.length; j++) {
					var option = el.options[j];
					if (option.value == value) {
						option.selected = true;
					} else {
						option.selected = false;
					}
				}
			} else if (el.length > 0) {
				if (el[0].type == "radio") {
					// radio buttons
					for (j=0; j < el.length; j++) {
						if (el[j].value == value) {
							el[j].checked = true;
						} else {
							el[j].checked = false;
						}
					}
				} else {
					// checkbox group
					for (j=0; j < el.length; j++) {
						if (el[j].value == value) {
							el[j].checked = true;
						}
					}
				}

			}
		}
	}
}

// pack hash table (dictionary) values into a URI-encoded string
// if omitkeys is given, it is a dictionary with names of keys to be omitted

function encodeHash(dict, omitkeys) {
	var s = [];
	omitkeys = omitkeys || {};
	for (var p in dict) {
		if (! omitkeys[p]) s.push(p + '=' + dict[p]);
	}
	return encodeURIComponent(s.join("$"));
}

// unpack hash table from URI-encoded string

function decodeHash(value) {
	var s = decodeURIComponent(value).split("$");
	var dict = {};
	for (var i=0; i<s.length; i++) {
		var p = s[i];
		var f = p.split("=");
		if (f.length == 1) {
			if (p) dict[p] = undefined;
		} else if (f.length == 2) {
			dict[f[0]] = f[1];
		} else {
			var field = f.shift();
			dict[field] = f.join("=");
		}
	}
	return dict;
}

// Couple two checkboxes so that they change in synchrony: when one
// is clicked, both change state
// Works for checkboxes, radio buttons, popup menus
// If oneway==1, only the link causing a change in input1 to set input2 is enabled.
// If oneway==2, only the link causing a change in input2 to set input1 is enabled.

function connectCheckboxes(input1, input2, oneway) {
	var p1 = input1;
	var p2 = input2;
	if (p1.selected != undefined) {
		var p1attr = "selected";
	} else {
		p1attr = "checked";
	}
	if (p2.selected != undefined) {
		var p2attr = "selected";
	} else {
		p2attr = "checked";
	}
	if ((! oneway) || (oneway == 2)) {
		p2.onclick = function() {
			p1[p1attr] = this[p2attr];
			return true;
		};
	}
	if ((! oneway) || (oneway == 1)) {
		p1.onclick = function() {
			p2[p2attr] = this[p1attr];
			return true;
		};
	}
}

function FSMCart(cform, output) {
	this.base = FSM;
	this.base("hlacart", {cform: cform, output: output});
	this.parameters = null;         // parameters for current frame
	var loaded = false;
	var me = this;

	// methods to send events

	this.enterView = function(ra,dec,radius,imagetype,ds,inst) {
   		this.handleEvent({type: "EnterView"});
	};

	this.leaveView = function() {
		this.handleEvent({type: "LeaveView"});
	};
	this._makeDiv= function() {
		this.div = document.createElement("div");
		
		// no existing frame, so create one
		this.div.name = "scart";
		this.div.id = "scart";
		this.div.className = "hidden";
		this.output.appendChild(this.div);
	};
	this._show = function() {
		if (this.div == null) {
			this._makeDiv();
			if (! hascookies) {
				// no cart function available, so div contains informative message
				var title = document.createElement('h2')
				title.innerHTML = "The shopping cart is not available because it uses cookies.  " +
					"Set your browser preferences to accept cookies from hla.stsci.edu in order to enable the cart.";
				this.div.appendChild(title);
			}
		}
		if (hascookies) {
			setHLACartFrame();
			rd.clearOutputPage();
			this.div.className="";
			showCart(this.div);
		} else {
			// no cart function available
			rd.clearOutputPage();
			this.div.className = "";
		}
	};

	this._hide = function() {
		if (this.div != null) this.div.className="hidden";
	};
}
FSMCart.prototype = new FSM;

// state table:
// States: Inactive, Loading, Current, LoadingHidden
// Events: EnterView, Loaded, Submitted, LeaveView

FSMCart.prototype.actionTransitionFunctions = {
	Inactive: {
		EnterView: function(event) {
			this._show();
			return this.currentState;
		},
		Loaded: function(event) { return this.currentState; },
		Submitted: function(event) { return this.currentState; },
		LeaveView: function(event) { 
			this._hide(); 
			return this.currentState; 
		},
		Cancel: function(event) { return this.currentState; }
	},

	Loading: {
		EnterView: function(event) {
			this._show();
			return this.currentState;
		},
		Loaded: function(event) { return this.currentState; },
		Submitted: function(event) { return this.currentState; },
		LeaveView: function(event) { 
			this._hide(); 
			return this.currentState; 
		},
		Cancel: function(event) { return this.currentState; }
	},

	Current: {
		EnterView: function(event) {
			this._show();
			return this.currentState;
		},
		Loaded: function(event) { return this.currentState; },
		Submitted: function(event) { return this.currentState; },
		LeaveView: function(event) { 
			this._hide(); 
			return this.currentState; 
		},
		Cancel: function(event) { return this.currentState; }
	},

	LoadingHidden: {
		EnterView: function(event) {
			this._show();
			return this.currentState;
		},
		Loaded: function(event) { return this.currentState; },
		Submitted: function(event) { return this.currentState; },
		LeaveView: function(event) { 
			this._hide(); 
			return this.currentState; 
		},
		Cancel: function(event) { return this.currentState; }
	}
};


// FSMFootprints: Finite-state machine for footprints

// Parameters:
// fform: HTML form element with the footprint parameters
// output: HTML element that holds the iframe

// Methods:
// enterView(ra,dec,radius,imagetype,ds,inst): Load footprints view
// leaveView(): Leave footprints view

// Internal methods:
// _makeFrame, _submitted, _loaded, _show, _hide, _load

function FSMFootprints(fform, output) {
	this.base = FSM;
	this.base("Footprints", {fform: fform, output: output});
	this.parameters = null;		// parameters for current frame

	var me = this;

	// methods to send events

	this.enterView = function(ra,dec,radius,imagetype,ds,inst) {
		if (imagetype == "exposure") {
			var level = "1";
		} else if (imagetype == "best") {
			level = "10";
		} else if (imagetype == "hlsp") {
			level = "5";
		} else if (imagetype == "combined") {
			level = "2";
		} else if (imagetype == "mosaic") {
			level = "3";
		} else {
			// treat color and all as level=10 (best), which
			// currently returns levels=2, 3, 5 and color
			level = "10";
		}
		if (radius > maxDSSradius) {
			var image = "off";
		} else {
			image = "on";
		}
		if (radius < minradius) {
			// minimum radius for footprint display
			radius = minradius;
		} else if (radius > maxradius) {
			// prevent extremely large area searches
			rd.errorMessage("The maximum search radius for the Footprints view is "+Math.round(maxradius)+" degrees");
			return;
		}
		radius = ""+(radius*60);

		// Insert the parameters into the (hidden) footprint form
		this.fform.ra.value = ra;
		this.fform.dec.value = dec;
		this.fform.sr.value = radius;
		this.fform.level.value = level;
		this.fform.image.value = image;
		this.fform.inst.value = inst.toUpperCase();
		// set main form instruments too
		setSelectedInstruments(inst);
		this.fform.ds.value = ds;
		var parameters = getFormPars(this.fform);
		this.handleEvent({type: "EnterView", parameters: parameters});
	};

	this.leaveView = function() {
		this.handleEvent({type: "LeaveView"});
	};

	// internal methods

	this._makeFrame = function() {
		if (this.iframe) {
			this.output.removeChild(this.iframe);
			this.iframe = undefined;
		}
		try {
			// crazy IE hack because IE does not set name of iframe
			// also include onload hack to work around inability to set onload in javascript
			this.iframe = document.createElement('<iframe name="'+this.fform.target+'" onload="rd.Footprints._loaded(true);">');
		} catch (e) {
			// sane browser
			this.iframe = document.createElement("iframe");
		}
		this.iframe.name = this.fform.target;
		this.iframe.id = this.fform.target;
		this.iframe.frameBorder = 0;
		this.iframe.className = "hidden";
		this.iframe.style.width = "900px";
		this.iframe.style.height = "1100px";
		this.iframe.style.overflowX = "auto";
		this.iframe.style.overflowX = "hidden";
		this.output.appendChild(this.iframe);
		// add onload after putting frame on page to avoid Safari
		// problem where onload immediately gets called for blank page
		// (Note that this has no effect for IE)
		this.iframe.onload = this._loaded;
	};

	this._submitted = function() {
		if (me.saveButtonPressed) {
			me.saveButtonPressed = false;
		} else {
			me.handleEvent({type: "Submitted"});
		}
		return true;
	};

	this._saveButton = function() {
		me.saveButtonPressed = true;
		return true;
	};

	this._loaded = function(useRandom) {
		// Called when frame is loaded (either after initial load or after reload)
		// list of selected footprints
		var selected = getSelectedFootprints(me.iframe);
		me.fform.ds.value = selected.join(",");
		// list of selected instruments
		var inst = getSelectedInstruments(me.iframe);
		me.fform.inst.value = inst.join(",");
		connectImageLevel(me.iframe);

		// Insert new CSS rule into the iframe stylesheet to suppress the Mozilla
		// table inheritance quirks mode that screws up font sizes. For details see
		// https://developer.mozilla.org/en/Fixing_Table_Inheritance_in_Quirks_Mode
		// Also see http://www.hunlock.com/blogs/Totally_Pwn_CSS_with_Javascript
		// for tips on adding CSS rules using Javascript.

		var doc = me.iframe.contentWindow.document;
		if (doc) {
			try {
				var rule = addCSSRule("table",doc);
				rule.style.fontSize = "inherit";
				rule.style.fontWeight = "inherit";
				rule.style.fontStyle = "inherit";
				rule.style.fontVariant = "inherit";
				rule = addCSSRule("caption",doc);
				rule.style.fontSize = "inherit";
				rule.style.fontWeight = "inherit";
				rule.style.fontStyle = "inherit";
				rule.style.fontVariant = "inherit";
			} catch (e) {};
		}

		if (me.currentState != "LoadingHidden") {
			// Update the advanced search form parameters too unless current state is hidden
			rd.setSelection(selected);
			setSelectedInstruments(me.fform.inst.value);
		}

		if (useRandom) {
			// For IE, append a random string to the main image name to prevent caching problems.
			// Yes, it's a hack.  What did you expect?
			if (doc) {
				var img = doc.getElementById("ctl00_ContentPlaceHolderMain_imgPlot");
				if (img && img.src) img.src = img.src + '?' + randomString();
			}
		}

		// set the submit button callback to start animation
		setSubmitCallback(me.iframe, me._submitted);
		// prevent the save button from hanging animation
		setSaveButtonCallback(me.iframe, me._saveButton);
		var parameters = getFormPars(me.fform);
		me.handleEvent({type: "Loaded", parameters: parameters});
	};

	this._show = function() {
		rd.clearOutputPage();
		me.iframe.className = "";
	};

	this._hide = function() {
		if (me.iframe) {
			me.iframe.className = "hidden";
		}
	};

	this._repaint = function() {
		// trying to work around Firefox repaint problem
		if (! me.iframe.className) {
			me.iframe.className = "hidden";
			setTimeout(function() {
				me.iframe.className = "";
				}, 1);
		}
	};

	this._load = function() {
		me._makeFrame();
		me._show();
		me.fform.submit();
	};

	this.cancelRequest = function() {
		// cancel current request
		deactivateLoading();
		// replace the old frame and hide the new one
		me._makeFrame();
		me._hide();
		me.parameters = null;
	};
}

FSMFootprints.prototype = new FSM;

// state table:
// States: Inactive, Loading, Current, LoadingHidden
// Events: EnterView, Loaded, Submitted, LeaveView

FSMFootprints.prototype.actionTransitionFunctions = {
	Inactive: {
		EnterView: function(event) {
			var newparameters = event.parameters || "";
			if (this.parameters == newparameters) {
				// show current page
				this._show();
				rd.saveState();
				return "Current";
			} else {
				this.parameters = newparameters;
				activateLoading();
				this._load();
				return "Loading";
			}
		},
		Loaded: function(event) { return this.currentState; },
		Submitted: function(event) { return this.currentState; },
		LeaveView: function(event) { return this.currentState; },
		Cancel: function(event) { return this.currentState; }
	},

	Loading: {
		EnterView: function(event) {
			var newparameters = event.parameters || "";
			if (this.parameters == newparameters) {
				// just continue loading
				return this.currentState;
			} else {
				deactivateLoading();
				return this.doActionTransition("Inactive", "EnterView", event);
			}
		},
		Loaded: function(event) {
			// loadedCallback returns new set of parameters (which may change)
			this.parameters = event.parameters;
			deactivateLoading();
			rd.saveState();
			return "Current";
		},
		Submitted: function(event) {
			// this probably should not happen (how can submit button be clicked when
			// page is not loaded), but if it does treat it as a no-op (since the
			// page is already loading and logo is animated)
			return this.currentState;
		},
		LeaveView: function(event) {
			deactivateLoading();
			this._hide();
			return "LoadingHidden";
		},
		Cancel: function(event) {
			this.cancelRequest();
			return "Inactive";
		}
	},

	Current: {
		EnterView: function(event) {
			var newparameters = event.parameters || "";
			if (this.parameters == newparameters) {
				// do nothing
				return this.currentState;
			} else {
				return this.doActionTransition("Inactive", "EnterView", event);
			}
		},
		Loaded: function(event) {
			this.parameters = event.parameters;
			rd.saveState();
			return this.currentState;
		},
		Submitted: function(event) {
			activateLoading();
			return 'Loading';
		},
		LeaveView: function(event) {
			this._hide();
			return "Inactive";
		},
		Cancel: function(event) { return this.currentState; }
	},

	LoadingHidden: {
		EnterView: function(event) {
			var newparameters = event.parameters || "";
			if (this.parameters == newparameters) {
				this._show();
				activateLoading();
				return "Loading";
			} else {
				return this.doActionTransition("Inactive", "EnterView", event);
			}
		},
		Loaded: function(event) {
			this.parameters = event.parameters;
			return "Inactive";
		},
		LeaveView: function(event) { return this.currentState; },
		Cancel: function(event) { return this.currentState; }
	}
};

// ----------------- end of FSM footprints -----------------

function NameResolver(form, paramname) {

	// Name resolver using Niall's OneBox interface

	this.form = document.forms[form];
	if (! this.form) alert("Search form named '"+form+"' was not found");
	this.paramname = paramname;
	this.name = null;
	this.posfilename = null;
	this.poslocalname = null;
	var me = this;

	// callback when resolver succeeds
	// extract position from the XML
	//XXX Probably should change this to use XPath for
	//XXX simplicity & performance

	this.resolverSuccess = function(xmldata) {
		var tab = xmldata.getElementsByTagName("resolvedCoordinate");
		if (!tab) {
			rd.errorMessage("No object matching "+me.name+" was found");
			return;
		}
		tab = tab[0];
		var ra = tab.getElementsByTagName("ra");
		var dec = tab.getElementsByTagName("dec");
		var equinox = tab.getElementsByTagName("equinox");
		var radius = tab.getElementsByTagName("radius");
		var source = tab.getElementsByTagName("resolver");
		var searchRadius = tab.getElementsByTagName("searchRadius");
		if (ra==undefined || dec==undefined || equinox==undefined ||
			radius==undefined || source==undefined || searchRadius==undefined) {
			rd.errorMessage("No object matching "+me.name+" was found");
			return;
		}
		source = getTextContent(source[0]);
		ra = getTextContent(ra[0]);
		dec = getTextContent(dec[0]);
		radius = getTextContent(radius[0]);
		searchRadius = getTextContent(searchRadius[0]);
		var fradius = parseFloat(radius);
		var fsearchRadius = parseFloat(searchRadius);
		if (fsearchRadius >= 0) fradius = fsearchRadius;
		//XXX if (fsearchRadius > fradius) fradius = fsearchRadius;
		// default radius for search is 0.2 degrees if no radius is found
		if (fradius < 0) {
			fradius = 0.2;
		} else if (fradius > 180) {
			alert("Radius value '"+fradius+"' must be less than 180 deg");
			me.form.sterm.focus();
			return;
		}
		radius = fradius.toFixed(6);
		var fra = parseFloat(ra);
		ra = fra.toFixed(6);
		var fdec = parseFloat(dec);
		dec = fdec.toFixed(6);
		// insert the parameters into the form
		if (me.form.RA) me.form.RA.value = ra;
		if (me.form.Dec) me.form.Dec.value = dec;
		if (me.form.Radius) me.form.Radius.value = radius;
		// trim leading and trailing blanks
		source = source.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
		me.setTitle();
		rd.loadData(ra,dec,radius);
	};

	this.resolverError = function(errmsg) {
		// handle Niall's name resolver exception
		if (errmsg.substring(0,10) == "Error 500:") {
			var msg = "Unknown target '"+me.name+"'";
		} else {
			msg = "Failed to resolve target name: "+me.name+"<br />\n"+errmsg;
		}
		me.setTitle(msg);
		rd.clearPageInfo();
		rd.saveState();
	};

	this.setTitle = function(msg) {
		if (msg == undefined) {
			if (me.posfilename) {
				msg = "Positions from <i>" + me.posfilename +
						"</i>&nbsp;&nbsp;r&nbsp;=&nbsp;" + this.form.Radius.value;
			} else {
				var ra = this.form.RA.value;
				var dec = this.form.Dec.value;
				msg = "<i>" + this.name + "</i>" +
						"&nbsp;&nbsp;RA&nbsp;=&nbsp;" + ra +
						"&nbsp;&nbsp;Dec&nbsp;=&nbsp;" + dec +
						"&nbsp;&nbsp;r&nbsp;=&nbsp;" + this.form.Radius.value +
						"&nbsp; [" + sexagesimal(parseFloat(ra), 15) + "&nbsp;" + sexagesimal(parseFloat(dec)) + "]";
			}
		}
		rd.setTitle(msg);
	};

	this.resolve = function() {
		// using Niall's OneBox name resolver, call the search function with
		// source position and search radius arguments
		// This keeps track of the last call parameters and uses the form's explicit
		// RA, Dec, and Radius fields if they have changed but the name has not.
		//XXX Ultimately this logic should probably go into FSM

		var name = me.form[me.paramname];
		if (! name) return rd.errorMessage("Unknown form parameter "+me.paramname);
		name = name.value;
		var ra = me.form.RA;
		if (ra) ra = ra.value;
		var dec = me.form.Dec;
		if (dec) dec = dec.value;
		var radius = me.form.Radius;
		if (radius) radius = radius.value;
		var posfilename = me.form.posfilename;
		if (posfilename) posfilename = posfilename.value;
		var poslocalname = me.form.poslocalname;
		if (poslocalname) poslocalname = poslocalname.value;

		var radefined = (ra!="" && dec!="");
		if (posfilename && (posfilename != me.posfilename || (name=="" && !radefined))) {
			// if upload filename is defined that takes precedence
			me.poslocalname = poslocalname;
			me.posfilename = posfilename;
			// default value for radius (0.02 for lists)
			if (!radius) {
				radius = "0.02";
				me.form.Radius.value = radius;
			} else {
				var fradius = parseFloat(radius);
				if (fradius > 0.2) {
					// clip at 0.2 deg max
					radius = "0.2";
					me.form.Radius.value = radius;
				}
			}
			// clear query, ra, dec
			me.form.RA.value = "";
			me.form.Dec.value = ""
			me.form[me.paramname].value = "";
			me.name = "";
			rd.clearOutput();
			me.setTitle();
			rd.loadData("","",radius,poslocalname,posfilename);
		} else if (((!name) || name == me.name) && radefined) {
			// name is unchanged (or null) and form values are defined, so use the
			// form ra & dec values for the search
			// clear upload filenames
			me.poslocalname = "";
			me.posfilename = "";
			me.form.poslocalname.value = "";
			me.form.posfilename.value = "";
			// default value for radius (0.2 for individual sources)
			if (!radius) {
				radius = "0.2";
				me.form.Radius.value = radius;
			}
			// parse & check for format errors
			var f = parseCoordinate(ra,15);
			if (f === undefined) {
				alert("Illegal RA value '"+ra+"'");
				me.form.RA.focus();
				return false;
			} else if (f < 0 || f > 360) {
				alert("RA value '"+ra+"' must be between 0 and 360 deg");
				me.form.RA.focus();
				return false;
			} else {
				ra = f.toFixed(6);
				me.form.RA.value = ra;
			}
			f = parseCoordinate(dec);
			if (f === undefined) {
				alert("Illegal Dec value '"+dec+"'");
				me.form.Dec.focus();
				return false;
			} else if (f < -90 || f > 90) {
				alert("Dec value '"+dec+"' must be between -90 and 90 deg");
				me.form.Dec.focus();
				return false;
			} else {
				dec = f.toFixed(6);
				me.form.Dec.value = dec;
			}
			if (! validateNumeric(radius)) {
				alert("Illegal Radius value '"+radius+"'");
				me.form.Radius.focus();
				return false;
			} else {
				f = parseFloat(radius);
				if (f < 0 || f > 180) {
					alert("Radius value '"+radius+"' must be between 0 and 180 deg");
					me.form.Radius.focus();
					return false;
				}
			}
			rd.clearOutput();
			me.setTitle();
			rd.loadData(ra,dec,radius);
		} else if (name) {
			// name is either new or ra/dec are not defined, so
			// resolve it to get ra & dec
			me.name = name;
			// clear upload filenames
			me.poslocalname = "";
			me.posfilename = "";
			me.form.poslocalname.value = "";
			me.form.posfilename.value = "";
			var params = encodeURIComponent(me.name);
			// clear out the form values so they can't be used
			me.form.RA.value = "";
			me.form.Dec.value = "";
			rd.clearOutput();
			me.setTitle("<i>"+name+"</i> ...");
			// have to delete saved XML until this request is resolved
			rd.xml = undefined;
			me.FSMresolver.makeRequest(params);
			// me.BrokenRequest(); // for name resolver testing
		} else if (rd.testQuery()) {
			// no position, but some query parameters are set
			// do an all-sky search by default
			me.form[me.paramname].value = "0 0 r=180";
			// call this function recursively now that position is defined
			me.resolve();
		} else {
			// name and position are null -- just create a message
			rd.errorMessage("Enter an object name or position to search");
		}
		return false;
	};

	// Substitute usable for testing when name resolver is broken
	this.BrokenRequest = function() {
		alert("HACK: fixed RA/Dec while resolver is broken");
		ra = 210.8025;
		dec = 54.3491;
		radius = 0.2;
		this.name = 'm101';
		// insert the parameters into the form
		if (this.form.RA) this.form.RA.value = ra;
		if (this.form.Dec) this.form.Dec.value = dec;
		if (this.form.Radius) this.form.Radius.value = radius.toFixed(6);
		if (this.form[this.paramname]) this.form[this.paramname].value = this.name;
		this.setTitle();
		rd.loadData(ra,dec,radius);
	};

	// create the finite-state machines
	// Name resolver
	this.FSMresolver = new FSMLoader("resolver", "/ags/TheOneBox-war/OneBoxServletLite?query=",
		this.resolverSuccess, this.resolverError);
}

function readdata(output, searchform, footprintform, cartform, paramname) {

	// Note all initialization is at the end (after the methods are defined)

	var me = this;

	this.clearOutput = function(el) {
		// clear both the table output area and footprints frame
		if (this.Footprints) this.Footprints.leaveView();
		this.clearOutputPage(el);
	};

	this.clearOutputPage = function(el) {
		// clear only the table output area (leaving footprints frame)
		while (this.output.hasChildNodes()) {
			this.output.removeChild(this.output.firstChild);
		}
		if (el) {
			this.output.appendChild(el);
		}
	};

	this.setTitle = function(innerHTML) {
		this.title.innerHTML = innerHTML;
	};

	this.setWindowTitle = function() {
		// set window title to include name
		var name = this.queryparam.value || this.form.posfilename.value;
		if (name) {
			window.document.title = "Hubble Legacy Archive (" + name + ")";
		} else {
			window.document.title = "Hubble Legacy Archive";
		}
	};

	this.clearPageInfo = function() {
		this.sortColumn = undefined;
		this.page = 1;
		// note that column order is not reset here
	};

	this.clearRequests = function() {
		// cancel active footprints and search requests
		this.SIAPloader.handleEvent({type: "Cancel"});
		this.XSLloader.handleEvent({type: "Cancel"});
		this.Footprints.handleEvent({type: "Cancel"});
	};

	this.clearForm = function(savelists) {
		// reset the forms and restore most things to default state

		if (savelists) {
			// save the current selection list
			var radeckey = '' + this.ra + ' ' + this.dec;
			if (this.selectedRows || this.savedSelections[radeckey]) {
				this.savedSelections[radeckey] = this.selectedRows;
			}
		}

		// start with a blank line and empty display
		this.setTitle("&nbsp;");

		this.view = this.defaultView;
		this.selectionDisplayMode = 'mixed';
		this.xml = undefined;
		// extra XSLT parameters
		this.xslParams = {};
		// previous state variables
		this.params = this.ra = this.dec = undefined;
		resolver.name = resolver.ra = resolver.dec = resolver.url = undefined;

		this.clearOutput();

		this.pageLength = 20;
		this.clearPageInfo();
		this.sortToggle = true;

		// could preserve this information by not clearing it here?
		this.selectedRows = [];

		this.form.reset();
		// reset does not work for some reason
		// this.fform.reset();
		this.fform.ra.value = "";
		this.fform.dec.value = "";
		this.fform.sr.value = "";
		this.fform.level.value = "";
		this.fform.image.value = "";
		this.fform.inst.value = this.getInstruments().join(",");
		this.fform.ds.value = "";

		// if iframe exists, synchronize the instrument selection there too
		var f = this.form.inst;
		for (var i=0; i<f.length; i++) {
			var el = f[i];
			if (el.onclick) el.onclick();
		}

		// set id="selected" for the default view
		el = document.getElementById("selected");
		if (el) el.removeAttribute("id");
		el = document.getElementById(this.defaultView);
		while (el && el.tagName != "LI") el= el.parentNode;
		if (el) el.id = "selected";
	};

	this.getInstruments = function() {
		// returns list of selected instruments
		var f = this.form.inst;
		var inst = [];
		for (var i=0; i<f.length; i++) {
			var el = f[i];
			if (el.checked) {
				inst.push(el.value);
			}
		}
		return inst;
	};

	this.resetInstruments = function() {
		// resets instruments to default
		var f = this.form.inst;
		for (var i=0; i<f.length; i++) {
			var el = f[i];
			el.checked = el.defaultChecked;
			if (el.onclick) el.onclick();
		}
	};

	this.testQuery = function() {
		// returns true if some of the query parameters are set
		return (this.form.prop_id.value || this.form.spectral_elt.value || this.form.moving_target.checked);
	};

	this.loadData = function(ra, dec, radius, poslocalname, posfilename) {
		var inst = this.getInstruments().join(",");
		var imagetype = this.form.imagetype.value;
		var format = "FITS";
		if (poslocalname != undefined && posfilename != undefined) {
			var params = 'posfilename=' + encodeURIComponent(poslocalname) +
				'&origfilename=' + encodeURIComponent(posfilename) +
				'&size=' + radius +
				'&imagetype=' + imagetype +
				'&inst=' + inst +
				'&hrcmatch=0' +
				'&format=' + format +
				'&listdelimiter=' + this.form.listdelimiter.value +
				'&listformat=' + this.form.listformat.value;
		} else {
			params = 'pos=' + ra +',' + dec +
				'&size=' + radius +
				'&imagetype=' + imagetype +
				'&inst=' + inst +
				'&hrcmatch=0' +
				'&format=' + format;
		}
		var prop_id = this.form.prop_id.value;
		if (prop_id) {
			params = params + '&prop_id=' + encodeURIComponent(prop_id);
		}
		var spectral_elt = this.form.spectral_elt.value;
		if (spectral_elt) {
			params = params + '&spectral_elt=' + encodeURIComponent(spectral_elt);
		}
		var moving_target = this.form.moving_target;
		if (moving_target && moving_target.checked) {
			params = params + '&moving_target=' + encodeURIComponent(moving_target.value);
		}

		if (params != this.params) {
			if (this.view != "Footprints") this.errorMessage("Searching...");
			// clear filter with new search
			if (! this.restoringState) this.filter.clearXSL();
			this.filter.setBaseDocument(null);
			// clear list of selected images with new search
			// unless ra and dec were unchanged
			if (ra != this.ra || dec != this.dec) {
				// save current selection list for last RA/Dec combo so we can get it
				// back later
				var radeckey = '' + this.ra + ' ' + this.dec;
				if (this.selectedRows || this.savedSelections[radeckey]) {
					this.savedSelections[radeckey] = this.selectedRows;
				}
				// see if new position is already saved
				radeckey = '' + ra + ' ' + dec;
				if (this.savedSelections[radeckey]) {
					this.selectedRows = this.savedSelections[radeckey];
				} else {
					this.clearSelection();
				}
			}

			// set global variables saving parameters for last search
			this.ra = ra;
			this.dec = dec;
			this.poslocalname = poslocalname;
			this.posfilename = posfilename;
			this.params = params;
			this.xml = undefined;
			// reset all sort/page info for new searches
			this.clearPageInfo();
			// Load XML (even if not needed right now)
			this.SIAPloader.makeRequest(params);
		} else if (! this.xml) {
			// Parameters are set but XML is not
			if (this.view != "Footprints") this.errorMessage("Searching...");
			this.SIAPloader.makeRequest(params);
		}
		this.loadXSL(); // Is this needed??

		// Call sort immediately if XML & XSL already exist or if they are not needed
		this.sortToggle = false;
		if ((!this.xsltfile) ||
			(this.xml != undefined &&
			 this.xslt != undefined)) {
			this.sort();
		}
	};

	this.setXSLBase = function(dir) {
		this.xslBase = dir;
	};

	this.checkXSL = function() {
		// clear saved XSL if it is not what we need
		this.view = this.view || this.defaultView;
		var xsltfile = this.view2xslt[this.view];
		if (this.xslBase) {
			xsltfile = this.xslBase + "/" + xsltfile;
		}
		if (xsltfile != this.xsltfile) {
			this.xsltfile = xsltfile;
			this.xslt = undefined;
			this.myXslProc = undefined;
		}
	};

	this.loadXSL = function() {
		this.checkXSL(); // Not needed?
		if (this.xsltfile && !this.xslt) {
			// don't toggle the sort order on next call
			this.sortToggle = false;
			this.XSLloader.makeRequest(this.xsltfile);
		}
	};

	this.getParameter = function(namespace, name) {
		// get XSLT parameter
		return me.xslParams[name];
	};

	this.setParameter = function(namespace, name, value) {
		// set XSLT parameter
		me.xslParams[name] = value;
	};

	this.saveState = function() {
		// Save current state
		if (this.sortColumn) {
			var sortOrder = this.sortOrder[this.sortColumn];
		} else {
			sortOrder = '';
		}
		// Small hack: always save the actual resolved source name in the state, even if
		// user has started typing another name but has not used it yet.
		var current_query = this.queryparam.value;
		this.queryparam.value = resolver.name || "";
		var pars = getFormPars(this.form);
		// Restore the current value
		this.queryparam.value = current_query;

		var ffpars = getFormPars(this.fform);

		var state = this.view + '|' +
					encodeHash(this.xslParams, {"selectedRows":1}) + '|' +
					pars + '|' +
					ffpars;
		StateManager.setState(state);
		// change window title just after state change so it shows up correctly
		// in page history
		// This works in Safari and Firefox2 but seems random in Firefox1.5.
		// Still seems like the right approach though.
		this.setWindowTitle();
	};

	this.restoreState = function(e) {
		// Restore current state
		// Called on a state change
		var state = e.id;
		me.restoringState = true;
		if (state == StateManager.defaultStateID) {
			// reset to default state
			// save selection list if it is not empty
			me.clearForm(true);
		} else {
			state = state.split('|');
			var newview = state[0];
			// don't toggle the sort order on first call
			me.sortToggle = false;
			if (state[2]) setFormPars(me.form, state[2]);
			if (state[3]) setFormPars(me.fform, state[3]);

			// update the dynamic label on cutout size
			updateScale("wfc_scale", me.form.cutout_size.value);
			// set visibility of cutout options
			var el = document.getElementById("CutoutOptions-show");
			if (el && el.checked) {
				toggleDisplay("CutoutOptions-show");
			} else {
				toggleDisplay("CutoutOptions-hide");
			}

			// these parameters have already been resolved, so update "last"
			// name value and make sure params does not match
			resolver.name = me.queryparam.value;
			me.params = undefined;

			me.filter.setParameters(decodeHash(state[1]));
			me.setView(newview);
		}
		me.restoringState = false;
	};

	this.clearState = function() {
		// Clear saved state
		// Set instruments manually to ensure footprint form gets updated too
		me.resetInstruments();
		// a little messy: clear all the cached filename values
		// XXX should clean this up
		me.poslocalname = "";
		me.posfilename = "";
		me.form.poslocalname.value = "";
		me.form.posfilename.value = "";
		resolver.poslocalname = null;
		resolver.posfilename = null;
		StateManager.setState(StateManager.defaultStateID);
		return true;
	};

	this.setView = function(current) {
		var oldview = me.view;
		if (!current) {
			me.view = me.view || me.defaultView;
			current = document.getElementById(me.view);
		} else if (typeof(current) == "string") {
			// current gives the name of the new view
			me.view = current || me.view || me.defaultView;
			current = document.getElementById(me.view);
		} else {
			// current is an HTML element whose id is
			// the name of the new view
			me.view = current.id || me.defaultView;
		}
		if (me.view != "Footprints") {
			me.Footprints.leaveView();
		}
		if (me.view != "hlacart") {	
			me.Cart.leaveView();
		}
		if (me.view != "Footprints" && me.view != "hlacart") {
			// clear XSL if it is not what is needed
			me.checkXSL();
		}

		// reset the currently selected element
		var el = document.getElementById("selected");
		if (el) el.removeAttribute("id");

		// set id="selected" for the currently selected view
		el = current;
		while (el && el.tagName != "LI") el= el.parentNode;
		if (el) el.id = "selected";

		// get rid of the dotted box for focus on this link
		// this works better than style-sheet approaches because it
		// still shows the box when tabbing is used to select the link
		// Checking queryparam focus works around Firefox bug that
		// generates annoying error message
		if ((! this.queryparam.hasFocus) && current.blur) {
			current.blur();
		}

		// Finally, do the search and load the XSL (if necessary) and
		// display the results

		if (me.view != "hlacart") {
			doSearch();
			me.loadXSL();
		} else {
			me.Cart.enterView();
		}
		return false;
	};

	this.setViewParams = function() {
		// set additional parameters specific to the current view
		if (this.form && this.view == "Images") {
			// only images have extra parameters
			var preview = getRadioValue(this.form.preview);
			var output_size = this.form.output_size.value;
			var hrcmatch = 1;
			this.myXslProc.setParameter(null, "output_size", ""+output_size);
			this.myXslProc.setParameter(null, "hrcmatch", ""+hrcmatch);
			if (preview!="1") {
				// cutout mode: specify additional parameters
				// cutout size in arcsec
				var size_arcsec = this.form.cutout_size.value;
				// convert size to WFC pixels
				var cutout_size = 256;
				if (validateNumeric(size_arcsec)) {
					cutout_size = Math.round(size_arcsec/0.05);
					if (cutout_size < 0) {
						cutout_size = 256;
					} else if (cutout_size > 4000) {
						cutout_size = 4000;
					}
				}
				var RA = this.form.RA.value;
				var Dec = this.form.Dec.value;
				this.myXslProc.setParameter(null, "cutout_size", ""+cutout_size);
				this.myXslProc.setParameter(null, "RA", ""+RA);
				this.myXslProc.setParameter(null, "Dec", ""+Dec);
			} else {
				// preview mode: restore the cutout parameters to their default values
				if (this.myXslProc.removeParameter) {
					this.myXslProc.removeParameter(null, "cutout_size");
					this.myXslProc.removeParameter(null, "RA");
					this.myXslProc.removeParameter(null, "Dec");
				} else {
					// IE doesn't have removeParameter
					this.myXslProc.setParameter(null, "cutout_size", null);
					this.myXslProc.setParameter(null, "RA", null);
					this.myXslProc.setParameter(null, "Dec", null);
				}
			}
		} else if (this.view == "Inventory") {
			if (this.maxColumns) {
				this.myXslProc.setParameter(null, "maxColumns", ""+this.maxColumns);
			} else {
				if (this.myXslProc.removeParameter) {
					this.myXslProc.removeParameter(null, "maxColumns");
				} else {
					// IE doesn't have removeParameter
					this.myXslProc.setParameter(null, "maxColumns", null);
				}
			}
			if (this.columnOrder) {
				this.myXslProc.setParameter(null, "columnOrder", (this.columnOrder.join(","))+",");
			} else {
				if (this.myXslProc.removeParameter) {
					this.myXslProc.removeParameter(null, "columnOrder");
				} else {
					this.myXslProc.setParameter(null, "columnOrder", null);
				}
			}
		}
	};

	this.setViewFootprints = function() {
		var ra = me.form.RA.value;
		var dec = me.form.Dec.value;
		var radius = me.form.Radius.value;
		if (ra && dec && radius) {
			// remove current table output (if any)
			me.clearOutput();
			var imagetype = me.form.imagetype.value;
			var ds = me.selectedRows.join(",");
			var inst = getSelectedInstruments(me.Footprints.iframe, me.restoringState);
			inst = inst.join(",");
			setSelectedInstruments(inst);
			connectImageLevel(me.Footprints.iframe);
			me.Footprints.enterView(ra,dec,radius,imagetype,ds,inst);
		}
	};

	this.xslLoaded = function(data) {
		me.xslt = data;
		if (me.xml) me.sort();
	};

	this.getXML = function() {
		return me.filter.getDocument();
	};

	this.xmlLoaded = function(data) {
		// If params is null, back button was presumably used to
		// return to a blank page.  Simply ignore the XML data in
		// that case.
		if (me.params) {
			me.xml = data;
			me.filter.setBaseDocument(data);
			if (me.xslt) me.sort();
		}
	};

	this.setMaxColumns = function(maxcolumns) {
		if (maxcolumns != me.maxColumns) {
			this.maxColumns = maxcolumns;
			this.sortToggle = false;
			this.sort();
		}
	};

	this.setColumnOrder = function(maxcolumns, order) {
		if (maxcolumns == me.maxColumns) {
			if (this.columnOrder) {
				if (this.columnOrder.length == order.length) {
					// just return if the order is unchanged
					var equals = true;
					for (var i=0; i<order.length; i++) {
						if (order[i] != this.columnOrder[i]) {
							equals = false;
							break;
						}
					}
					if (equals) return;
				}
			} else if (!order) {
				// both old and new order are undefined (default)
				return;
			}
		}
		this.columnOrder = order;
		this.maxColumns = maxcolumns;
		this.sortToggle = false;
		this.sort();
	};

	this.clearSelection = function(search) {
		if (me.selectedRows.length > 0) {
			me.selectedRows = [];
			if (search) {
				if (me.form) {
					doSearch();
				} else {
					me.sortToggle = false;
					return me.sort();
				}
			}
		}
		return true;
	};

	this.setSelection = function(selectors) {
		// set selection from a list or a comma-separated string of selectors
		this.clearSelection();
		return this.extendSelection(selectors);
	};

	this.extendSelection = function(selectors) {
		// extend current selection from a list or comma-separated string of selectors
		if (! selectors) return true;
		if (selectors.split) {
			// looks like a string
			me.selectedRows = me.selectedRows.concat(selectors.split(","));
		} else if (selectors.length) {
			// looks like a list
			me.selectedRows = me.selectedRows.concat(selectors);
		}
		// remove any duplicate selectors from the selection
		var uniq = [];
		var dict = {};
		for (var i=0, selector; i < me.selectedRows.length; i++) {
			selector = me.selectedRows[i];
			if (dict[selector] == undefined) {
				dict[selector] = 1;
				uniq.push(selector);
			}
		}
		// keep selectors in sorted order
		uniq.sort();
		me.selectedRows = uniq;
		me.setParameter(null, "selectedRows", me.selectedRows.join(","));
		return true;
	};

	this.addSelectedToCart = function() {
		if (! me.addhelper) {
			me.addhelper = new AddSelectedToCart();
		}
		me.addhelper.addSelectedToCart(me.getXML());
	};

	this.selectRow = function(el,dataset) {
		var cclass = el.className;
		for (var i=0, f; i < me.selectedRows.length; i++) {
			if (me.selectedRows[i] == dataset) {
				// second click disables selection
				me.selectedRows.splice(i,1);
				if (cclass) {
					el.className = removeSubstring(cclass,"selectedimage");
				}
				return;
			}
		}
		// not in current selection, so add this to selection
		me.selectedRows.push(dataset);
		me.selectedRows.sort();
		me.setParameter(null, "selectedRows", me.selectedRows.join(","));
		if (cclass) {
			el.className = cclass + " selectedimage"; 
		} else {
			el.className = "selectedimage"; 
		}
	};

	this.setSelectionMode = function(mode) {
		if (mode != 'first' && mode != 'only' && mode != 'not') {
			mode = 'mixed';
		}
		this.selectionDisplayMode = mode;
		this.sortToggle = false;
		this.page = 1;
		this.sort();
	};

	this.setPageLength = function(pageLength) {
		// change number of rows per page
		if ((!pageLength) || me.pageLength == pageLength) return;
		var start = me.pageLength*(me.page-1);
		me.pageLength = pageLength;
		me.sort(undefined, undefined, Math.floor(start/pageLength)+1);
	};

	this.sort = function(sortColumn, sortOrder, newpage) {
		if (me.view == "Footprints") {
			me.setViewFootprints();
			me.sortToggle = true;
			return false;
		}
		if (me.xml == undefined || me.xslt == undefined) return false;

		if (newpage != undefined) me.page = newpage;
		// sort direction gets toggled only if the page does not change
		var pchanged = newpage != undefined;

		if (!sortColumn) {
			sortColumn = me.sortColumn || "";
		}
		if (!sortOrder) {
			if (me.sortToggle && sortColumn == me.sortColumn && (! pchanged)) {
				// toggle sort order
				if (me.sortOrder[sortColumn] == "ascending") {
					sortOrder = "descending";
				} else {
					sortOrder = "ascending";
				}
			} else {
				// restore previous sort order or use default
				sortOrder = me.sortOrder[sortColumn] || "ascending";
			}
		}
		me.sortColumn = sortColumn;
		me.sortOrder[sortColumn] = sortOrder;
		me.sortToggle = true;
		// save state so back button works
		me.saveState();

		if (! me.myXslProc) {
			// Mozilla/IE XSLT processing using Sarissa
			if (!window.XSLTProcessor) return me.noXSLTMessage();

			me.myXslProc = new XSLTProcessor();
			if ((!me.myXslProc) || (!me.myXslProc.importStylesheet))
				return me.noXSLTMessage();
			// attach the stylesheet; the required format is a DOM object, and not a string
			me.myXslProc.importStylesheet(me.xslt);
		}

		// do the transform
		me.myXslProc.setParameter(null, "sortOrder", sortOrder);
		me.myXslProc.setParameter(null, "sortColumn", sortColumn);
		me.myXslProc.setParameter(null, "page", ""+me.page);
		me.myXslProc.setParameter(null, "pageLength", ""+me.pageLength);
		me.myXslProc.setParameter(null, "selectionMode", this.selectionDisplayMode);
		// set extra XSLT parameters
		me.setParameter(null, "selectedRows", me.selectedRows.join(","));
		for (var p in me.xslParams) {
			me.myXslProc.setParameter(null, p, me.xslParams[p]);
		}
		me.setViewParams();

		// create the HTML table and insert into document
		// wait for the activated image to load before executing the code
		activateLoading(
			function() {
				var finishedHTML = me.myXslProc.transformToFragment(me.getXML(), document);
				deactivateLoading();
				if (! finishedHTML) {
					alert('XSLT returned null HTML');
				} else {
					me.clearOutput();
					try {
						me.output.appendChild(document.adoptNode(finishedHTML));
					} catch (e) {
						try {
							me.output.appendChild(document.importNode(finishedHTML,true));
						} catch (e) {
							me.output.appendChild(finishedHTML);
						}
					}
					// make the fields table draggable if it exists
					var ftable = document.getElementById('fields');
					if (ftable) {
						var tablednd = new TableDnD.TableDnD();
						tablednd.onDrop = setColumnOrder;
						tablednd.init(ftable);
					}
				}
			}
		);
		return false;
	};

	this.errorMessage = function(msg) {
		var p = document.createElement('h2');
		p.innerHTML = msg;
		me.clearOutput(p);
		return false;
	};

	this.noXSLTMessage = function() {
		me.errorMessage("Sorry, your browser does not support XSLT -- try Firefox, Safari (version 3), Mozilla (version > 1.3), Internet Explorer, or other compatible browsers.");
		return false;
	};

	// *** State initialization ***

	this.defaultView = "Inventory";
	this.sortOrder = {};

	searchform = searchform || "searchForm";
	footprintform = footprintform || "footprintForm";
	cartform = cartform || "cartForm";
	paramname = paramname || "query_string";
	this.form = document.forms[searchform];
	this.fform = document.forms[footprintform];
	this.cform = document.forms[cartform];
	this.queryparam = this.form[paramname];

	this.xsltfile = undefined;
	this.xslt = undefined;
	this.filter = new XSLTFilter(null, this);
	this.xslBase = undefined;

	// mapping from view choices to XSLT files

	if (Sarissa._SARISSA_IS_SAFARI || Sarissa._SARISSA_IS_IE) {
		// work around bug in Safari that makes xsl:import fail
		this.view2xslt = {
				"Inventory": "/scripts/hlaview-safari.xsl",
				"Images": "/scripts/hlaview-images-safari.xsl",
				"Footprints": undefined,
				"Cart": undefined
				};
	} else {
		this.view2xslt = {
				"Inventory": "/scripts/hlaview.xsl",
				"Images": "/scripts/hlaview-images.xsl",
				"Footprints": undefined,
				"Cart": undefined
				};
	}

	// output has three parts, a title, a div for the XSL output and an (invisible) iframe
	while (output.hasChildNodes()) {
		output.removeChild(output.firstChild);
	}
	this.title = document.createElement("h3");
	// start with a blank line
	this.title.innerHTML = '&nbsp;';
	output.appendChild(this.title);

	this.output = document.createElement("div");
	output.appendChild(this.output);

	// saved list of selected rows associated with various targets
	this.savedSelections = {};

	// Finish the initialization using clearForm
	this.clearForm();

	// create the event-handling loaders
	this.SIAPloader = new FSMLoader("data", "/cgi-bin/acsSIAP.cgi?", this.xmlLoaded);
	this.XSLloader = new FSMLoader("xsl", "", this.xslLoaded);
	this.Footprints = new FSMFootprints(this.fform, output);
	this.Cart = new FSMCart(this.cform, output);
}

// functions used in XSL-generated code
function trim(str) {
	return str.replace(/^\s*(\S*(\s+\S+)*)\s*$/, "$1");
}

// other javascript

function getRadioValue(button) {
	// get the value for a radio button input
	for (var i=0, option; option = button[i]; i++) {
		if (option.checked) {
			return option.value;
		}
	}
	return undefined;
}

function toggleDisplay(control, label, hideprefix, showprefix) {
	// the id of controObj should be ID-control, ID-show, or ID-hide,
	// where ID is the name of the section to show/hide
	// control may also be a string
	// Target elements may be identified either with ID or (for
	// multiple elements) CLASS
	if (typeof(control) == "string") {
		var controlID = control;
		control = document.getElementById(controlID);
	} else {
		controlID = control.id;
	}
	if (! controlID) return false;

	var ctlist = ["-control", "-show", "-hide"];
	for (var ctype, j=0; ctype = ctlist[j]; j++) {
		var i = controlID.indexOf(ctype);
		if (i >= 0) break;
	}
	if (i<0) return false;

	var target = document.getElementById(controlID.substring(0,i));
	if (target) {
		target = [ target ];
	} else {
		target = getElementsByClass(controlID.substring(0,i));
	}
	if (target) {
		if (ctype == "-control") {
			// by default label gets changed only for "-control" types
			// because labels presumably are already correct if there are
			// separate "-show" and "-hide" controls
			if (hideprefix == undefined) hideprefix = "hide ";
			if (showprefix == undefined) showprefix = "";
			if (label == undefined) label = 'files';
			if (target[0].style.display == 'none') {
				ctype = "-show";
				if (control) control.firstChild.nodeValue = hideprefix+label;
			} else {
				ctype = "-hide";
				if (control) control.firstChild.nodeValue = showprefix+label;
			}
		} else {
			hideprefix = hideprefix || "";
			showprefix = showprefix || "";
		}
		if (ctype == "-show") {
			for (i=0; i<target.length; i++) {
				target[i].style.visibility = "inherit";
				target[i].style.display = "";
			}
		} else {
			for (i=0; i<target.length; i++) {
				target[i].style.visibility = "hidden";
				target[i].style.display = "none";
			}
		}
	}
	return false;
}

function checkall(el) {
	// handle "checkall" checkbox
	// find associated checkboxes and check or uncheck them
	var name = el.name;
	// ignore if name is not "something-control"
	if (name.length < 8 || name.substring(name.length-8) != "-control") return false;
	name = name.substring(0,name.length-8);
	var boxes = el.form[name];
	if (! boxes) return false;
	for (var i=0; i<boxes.length; i++) {
		boxes[i].checked = el.checked;
		if (boxes[i].onclick) boxes[i].onclick();
	}
	return true;
}

function doSearch() {
	// first see if there is a file waiting to be uploaded
	var doc = document.getElementById("fileupload").contentWindow.document;
	var form = doc.forms[0];
	var upload = form["filename"].value;
	if (upload) {
		// do the upload
		if (form.onsubmit) form.onsubmit();
		form.submit();
	} else {
		resolver.resolve();
	}
	return false;
}

// callbacks for selection list

function selectRow(el,dataset,event) {
	var ev = event || window.event;
	// don't select when links are clicked
	if (ev) {
		// srcElement is for IE
		var target = ev.target || ev.srcElement;
		if (target && target.tagName.toLowerCase() == "a") return;
	}
	rd.selectRow(el,dataset);
}

function clearSelection() {
	rd.clearSelection(true);
}

function setSelection(selectors) {
	rd.setSelection(selectors);
}

function extendSelection(selectors) {
	rd.extendSelection(selectors);
}

// insert a term into a source box

function insertTerm(el) {
	var sbox = document.getElementById('sterm');
	if (typeof(el) == "string") {
		sbox.value = el;
	} else {
		sbox.value = el.innerHTML;
	}
	sbox.focus();
	return false;
}


// start up fits2web image viewer

var displayWindow;

function openDisplay(url, target) {
	if ((! displayWindow) || displayWindow.closed) {
		if (target == undefined) target = "display";
		displayWindow = window.open(url, target, "resizable=yes,scrollbars=yes,status=no");
	} else {
		displayWindow.location = url;
	}
	displayWindow.focus();
}

function closeDisplay() {
	if (displayWindow) {
		if (! displayWindow.closed) displayWindow.close();
		displayWindow = null;
	}
}

function display(url, target) {
	openDisplay(url, target);
	return false;
}

function moveTo(x,y) {
	if (displayWindow && displayWindow.viewer) {
		displayWindow.viewer.moveTo(x,y);
	}
	return false;
}

function updateScale(target, size_arcsec) {
	// update the pixel cutout scale from the arcsec scale
	// convert size to WFC pixels
	var cutout_size = 256;
	if (validateNumeric(size_arcsec)) {
		cutout_size = Math.round(parseFloat(size_arcsec)/0.05);
		if (cutout_size < 0) {
			alert("Cutout size must be > 0");
			cutout_size = 256;
		} else if (cutout_size > 4000) {
			alert("Cutout maximum size is 200 arcsec\n(4000 ACS WFC pixels)");
			cutout_size = 4000;
		}
	} else {
		alert("Illegal cutout size value '"+size_arcsec+"'");
	}
	var output = document.getElementById(target);
	if (output) output.innerHTML = ""+cutout_size+"x"+cutout_size;
}

function clearAll() {
	if (confirm("Clear form and results?")) {
		// Reset form and clear saved state
		rd.clearRequests();
		rd.clearForm(true);
		rd.clearState();
	}
	return false;
}

function setSelectedInstruments(inst) {
	// Set selected instruments in main search form using 
	// comma-separated string of instruments
	var values = inst.split(',');
	// start with all values unchecked
	var checked = {};
	var f = rd.form.inst;
	for (var i=0; i < f.length; i++) {
		checked[f[i].value.toUpperCase()] = false;
	}
	for (i=0; i < values.length; i++) {
		var v = values[i].toUpperCase();
		checked[v] = true;
	}
	for (i=0; i < f.length; i++) {
		var el = f[i];
		var value = el.value.toUpperCase();
		if (!(checked[value] === undefined)) {
			el.checked = checked[value];
		}
	}
}

function getSelectedFootprints(iframe) {
	//XXX return the list of selected rows from the iframe
	//XXX this is a hack -- relies on internals of Gretchen's web page
	//XXX and also will not work if selected images extend beyond the
	//XXX first page
	var doc = iframe.contentWindow.document;
	var selected = [];

	var table = doc.getElementById("ctl00_ContentPlaceHolderMain_ScienceGrid");
	if (table) {
		var headings = table.getElementsByTagName("th");
		// find the Dataset heading
		for (var i=0; i < headings.length; i++) {
			var th = headings[i];
			if (getTextContent(th) == "Dataset") break;
		}
		if (!th) i = 1;

		// get a list of the selected rows
		var rowlist = table.getElementsByTagName("tr");
		selected = [];
		for (var j=0; j < rowlist.length; j++) {
			var row = rowlist[j];
			if (row.getAttribute('clicked') == 'true') {
				var tdlist = row.getElementsByTagName("td");
				var objid = tdlist[i];
				selected.push(getTextContent(objid));
			}
		}
	}
	return selected;
}

function getSelectedInstruments(iframe, useHidden) {
	// returns list of selected instruments from the iframe
	//XXX this is a hack -- relies on internals of Gretchen's web page

	if (useHidden || ! iframe) {
		// Use the readdata internal list if no iframe or
		// if useHidden is set
		return rd.getInstruments();
	}
	var inst = [];
	var found = {};
	var doc = iframe.contentWindow.document;

	var f = rd.form.inst;

	var table = doc.getElementById("Instrument");
	if (table) {
		var formdict = {};
		for (var j=0; j < f.length; j++) {
			// removed embedded dashes from the name
			var instname = f[j].value.replace(/-/g, "");
			formdict['ctl00_ContentPlaceHolderMain_cb'+instname] = f[j];
		}
		var inputlist = table.getElementsByTagName("input");
		for (var i=0; i < inputlist.length; i++) {
			var input = inputlist[i];
			var id = input.getAttribute("id");
			var input2 = formdict[id];
			if (input2) {
				if (input.checked) {
					inst.push(input2.value);
				}
				found[input2.value] = true;
				// tie the two checkboxes together
				connectCheckboxes(input, input2);
			}
		}
	}
	// add any instruments there were not found to the selected list if they
	// are currently checked on the main search form
	for (j=0; j<f.length; j++) {
		input2 = f[j];
		if (! found[input2.value]) {
			if (input2.checked) {
				inst.push(input2.value);
			}
		}
	}
	// get the unique values
	inst.sort();
	var uinst = [inst[0]];
	for (var i=1; i < inst.length; i++) {
		if (inst[i] != inst[i-1]) uinst.push(inst[i]);
	}
	return uinst;
}

function connectImageLevel(iframe) {
	// connect image-level selection widgets for footprints form and
	// main search form
	//XXX this is a hack -- relies on internals of Gretchen's web page

	if (!iframe) return;
	var doc = iframe.contentWindow.document;
	if (!doc) return;
	var table = doc.getElementById("ctl00_ContentPlaceHolderMain_rbtnListLevel");
	var options = rd.form.imagetype.options;
	if (table && options) {
		// build dictionary based on first word of footprint form value
		var inputlist = table.getElementsByTagName("input");
		var footdict = {};
		for (var i=0; i < inputlist.length; i++) {
			var input = inputlist[i];
			var value = input.value.toLowerCase().replace(/[ (].*$/, "");
			footdict[value] = input;
		}
		// find and link the form elements
		for (var j=0; j < options.length; j++) {
			value = options[j].value.toLowerCase();
			input = footdict[value];
			oneway = undefined;
			if (! input) {
				// try some alternate names
				if (value == "hlsp") {
					input = footdict["contributed"];
				} else if (value == "contributed") {
					input = footdict["hlsp"];
				} else if (value == "color") {
					input = footdict["best"];
					// only do link from color to best
					oneway = 2;
				}
			}
			if (input) {
				// tie the two checkboxes together
				connectCheckboxes(input, options[j], oneway);
			}
		}
	}
}

function setSubmitCallback(iframe, submitted) {
	//XXX set the onsubmit callback for the iframe's form
	//XXX this is a hack -- relies on internals of Gretchen's web page

	if (iframe) {
		var doc = iframe.contentWindow.document;
		var form = doc.getElementById("aspnetForm");
		if (form) {
			form.onsubmit = submitted;
		}
	}
}


function setSaveButtonCallback(iframe, pressed) {
	//XXX set the onclick callback for the iframe's save button
	//XXX this is a hack -- relies on internals of Gretchen's web page

	// This is needed because the save button generates a submit event
	// without an accompanying load event when it is done.  Have to
	// recognize this special case to avoid have the busy indicator
	// get stuck.

	if (iframe) {
		var doc = iframe.contentWindow.document;
		var button = doc.getElementById("ctl00_ContentPlaceHolderMain_btn_savetable", doc);
		if (button) {
			button.onclick = pressed;
		}
	}
}

// filtering hooks

function filterByColumn(form) {
	var changed = rd.filter.filterByColumn(form,rd.selectedRows.join(","));
	if (changed) {
		rd.sortToggle = false;
		rd.page = 1;
		rd.sort();
	}
	return false;
}

function resetFilter(form) {
	var changed = rd.filter.clear(form);
	if (changed) {
		rd.sortToggle = false;
		rd.page = 1;
		rd.sort();
	}
	return false;
}

function setMaxColumns(maxcolumns) {
	rd.setMaxColumns(maxcolumns);
}

function setColumnOrder(table, row) {
	// determine the column order from the table
	var rows = table.tBodies[0].rows;
	var maxcolumns = rows.length-1;
	var order = [];
	for (var i=0; i<rows.length; i++) {
		var classname = rows[i].className || "";
		if (classname.indexOf("separator") >= 0) {
			maxcolumns = i;
		} else {
			// ID for row is 'fieldrow_<number>'
			var f = rows[i].id.split('_');
			order.push(parseInt(f[f.length-1],10));
		}
	}
	rd.setColumnOrder(maxcolumns, order);
}

function resetColumnOrder() {
	rd.setColumnOrder();
}

function setSelectionMode(el) {
	var label = el.innerHTML.toLowerCase();
	rd.setSelectionMode(label);
}

// shopping cart hook
function xslAddCartItem(filename, url, label, naxis, event) {

	// if cart is not available, link leads to immediate download
	if (! hascookies) return true;

	label = decodeURIComponent(label);
	if (url.match(/stdads_mark=/)) {
		// DADS cart load
		addDADSItem(filename,label);
		popup_show(event);
		return false;
	}

	var flist = filename.split(',');
	if (flist.length == 1) {
		// simple filename
		var dataset = filename;
		if (url.match(/hstonline_mark=/)) {
			var fitsfile = dataset + '.tar';
		} else {
			// normal case: HLA FITS file
			var fitsfile = dataset + '_drz.fits';
		}
		var f1 = filename.split('_');
	} else {
		// color image -- construct filename
		// combine common prefix and suffix with trailing fields from each color
		var f1 = flist[0].split('_');
		var f2 = flist[1].split('_');
		if (flist.length == 2) {
			var f3 = f2;
		} else {
			f3 = flist[2].split('_');
		}
		var lmin = Math.min(f1.length,f2.length,f3.length);
		for (var i=0; i<lmin; i++) {
			if (f1[i] != f2[i] || f1[i] != f3[i]) break;
		}
		for (var j=0; j<lmin; j++) {
			if (f1[f1.length-j-1] != f2[f2.length-j-1] || f1[f1.length-j-1] != f3[f3.length-j-1]) break;
		}
		var j1 = f1.length-j;
		var j2 = f2.length-j;
		var j3 = f3.length-j;
		dataset = f1.slice(0,j1).join('_') + '_' + f2.slice(i,j2).join('_');
		if (flist.length > 2) dataset = dataset + '_' + f3.slice(i,j3).join('_');
		if (j > 0) dataset = dataset + '_' + f1.slice(j1).join('_');
		fitsfile = 'color_' + dataset + '_sci.fits';
	}
	// replace '&amp;' with '&' in url to get it to match values from
	// other sources
	var ulist = url.split('&amp;');
	if (ulist.length > 1) url = ulist.join('&');

	// convert naxis to estimate of file size
	var filesize = undefined;
	try {
		var a = naxis.split(',');
		if (flist.length <= 1) {
			filesize = 4;
		} else {
			// Note that 2-band color images lead to 3-plane RGB FITS file
			filesize = 12;
		}
		for (var i=0; i<a.length; i += 1) {
			filesize = filesize * parseInt(a[i],10);
		}
		if (dataset.substring(0,4).toLowerCase() == "hst_" && flist.length  == 1) {
			// assume 'hst_' files have error and quality extensions
			// except for exposures, which have a number at the end of the filename
			if (! validateInteger(f1[f1.length-1])) {
				// not an exposure
				filesize = filesize * 3;
			}
		}
		if (filesize==0 || filesize!=filesize) {
			// zero or NaN filesize
			if (label.match(/GRISM$/)) {
				// all grism spectra are the same size
				filesize = 20180.0;
			} else {
				filesize = undefined;
			}
		}
	} catch (e) {
		try {
			if (label.match(/GRISM$/)) {
				// all grism spectra are the same size
				filesize = 20180.0;
			} else {
				filesize = undefined;
			}
		} catch (e) {
		}
	}

	addCartItem(dataset,dataset,fitsfile,url,label,filesize);
	popup_show(event);
	return false;
}

function popup_show(event) {
	event = event || window.event;
	if (! event) return;
	var x = event.clientX;
	var y = event.clientY;
	var scrollAmount = window.pageYOffset ? window.pageYOffset :
		document[(document.compatMode == "CSS1Compat") ? "documentElement" : "body"].scrollTop;
	y += scrollAmount;

	var el = document.createElement('div');
	el.className = "popup_msg";
	el.innerHTML = "Added to cart";
	el.style.left = '' + (x-50) + 'px';
	el.style.top = '' + (y-12) + 'px';
	document.body.appendChild(el);
	setTimeout(function() {
			el.style.display = "none";
			document.body.removeChild(el);
		}, 1000);
}

// write debug output to div at top of page
function debug(innerHTML,clear) {
	var el = document.getElementById("debug");
	if (!el) {
		el = document.createElement("div");
		el.id = "debug";
		el.style.fontSize = "80%";
		el.innerHTML = '<a href="#" onclick="return debug(null,true)">Clear</a>';
		document.body.insertBefore(el, document.body.firstChild);
	}
	if (clear) {
		el.innerHTML = '<a href="#" onclick="return debug(null,true)">Clear</a>';
	} else {
		el.innerHTML += " "+innerHTML;
	}
	return false;
}

// Turn off autocomplete for all but the query string
// This works around a Firefox bug that generates annoying messages
// when blur method is called

function turnOffAutocomplete(paramname) {
	if (document.getElementsByTagName) {
		var inputElements = document.getElementsByTagName("input");
		var query;
		for (var i=0; i < inputElements.length; i++) {
			if (inputElements[i].name != paramname) {
				inputElements[i].setAttribute("autocomplete","off");
			} else {
				// save the query parameter
				query = inputElements[i]; 
			}
		}
		// add hasFocus flag to the query parameter
		if (query) {
			query.onfocus = function() {
				query.hasFocus = true;
			};
			query.onblur = function() {
				query.hasFocus = false;
			};
		}
	}
}
