/**
 * atHome.js
 * dynamic docform page generation
 * 
 * @author Tom Humphrey <t.humphrey@bigfoot.com>
 * @copyright Tom Humphrey 2008
 * @version 0.06f
 * @package docform
 */

/*
 * recent changes:
 * support for GOD accession numbers
 * new url for tiles
 * drop warning messages about Chrome
 * fix Bolton accession numbers (relax length restrictions)
 */
 
var globalVersion='006f';

var chromeBrowser=false;
var standardBrowser= (navigator.appName!='Microsoft Internet Explorer');
if (!standardBrowser) {
	var IEversion = navigator.appVersion;
	IEversion = parseInt(IEversion.substr(IEversion.indexOf("MSIE")+4),10);
	
	try {
  		document.execCommand("BackgroundImageCache", false, true);
	} catch(err) {}

} else {
    var IEversion=0;
    //if (navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
    //    alert('You appear to be using Google Chrome, unfortunately this web browser is not yet fully compatible with herb@home and you may experience problems. I hope to fully support Chrome soon, until then, the use of Firefox, IE7 or Safari is recommended.');
    //    chromeBrowser=true;
    //}
}

var iconSuffix=(standardBrowser || IEversion>=7)?'':'_ie'; // suffix for png's without alpha transparency
var globalEventStack=[];
var globalEventMethodName=[];
var globalEventTarget=[];
var globalEventCapturing=[];
var globalEventType=[];
var globalBlockSave;
var globalContainer;
var globalFocusedRowId=null;
var globalDropboxTimeoutHandle=null;
var thumbPage;
var specimens;
var loadRequest; // used by initial specimen load XML request;

/*
if (chromeBrowser) {
    // Chrome does over-zeallous garbage collection
    globalStaticObjectList=[];
}
*/

var help=new FormHelp('');

/*global scriptUrl,formLeafName,atHomeKey,slowConnection,phpSession,globalUserName*/
//var scriptUrl='http://herbariaunited.org.uk/';

var uid;
var tmpUser;

var globalBackupId; // set interval id of backup process
var backupSaveRequest=null;
var errorReportRequest=null;

var firstIdNumber=null; // global record of the first sheet photo number

var currentSheet=null;

var globalSaveTimeoutHandle=null;
var globalSaveCount=0;

var globalPauseImgCache=false; // suspend background caching of images during foreground activity
var globalImageCacheDelay=(standardBrowser || IEversion>=7)?20:250; // delay (ms) between sending image requests, old versions of IE seem to crash if too aggressive

var globalModifiedSinceLastBackup=false;
var registrationForm=null;

var trimRegex=/^\s+|\s+$/g;
var tutorial=false;
var currentBubble=null;

var globalMessages=[];

//var globalIFrameFlag=(window.parent!=window);
var globalIFrameFlag=(window.top.location != window.location);

var todayJD=current_date_JD();

var imageLoadTimeoutHandle=null;

/**
 * collection of form manifests (field layout descriptors)
 */
var manifests={};

try
{
	var tr=document.createElement("TR");
	tr.focus=false;
	tr=null;
}
catch (err)
{
	window.location.href=scriptUrl+"content/incompat/";
}

/**
 * generate a unique id, by appending an integer to the specified prefix
 * 
 * @param prefix {string} id prefix
 * @return id string
 */
function uniqueId(prefix) {
	return prefix+(uniqueId.counter++);
}
uniqueId.counter=0;

/**
 * container for allocated sheet
 * 
 * @constructor
 * @param {string} sheetExternalId
 * @param {int} sheetId internal (hu) sheet id
 * @param {domelement} container parent container DIV element
 * @param {xmlnode} XMLdesc xml sheet node 
 */
function AllocatedSheet(sheetExternalId,sheetId,container,XMLdesc) {
	var main=document.getElementById('main');
	
	this.sheetExternalId=sheetExternalId;
	this.sheetId=sheetId;

	this.data=new SpecimenData();
	this.data.load(this.sheetExternalId,this.sheetId,XMLdesc);

	this.specSheet=new SheetPage(container);
	this.specSheet.set_content(this.data);

	this.thumb=new SpecimenThumb(this.data);
}

/**
 * destructor 
 */
AllocatedSheet.prototype.destroy =function()
{
	this.data.destroy();
	this.thumb.destroy();
	this.specSheet.destroy();
};

/**
 * resize main doc window
 * @param {bool} fullWidth
 * @todo support remembering doc window config, + find out what fullWidth parameter is for!!
 */
function set_width(fullWidth)
{
	var page=window.document.getElementById('main');

	if (fullWidth)
	{
		page.className="fullwidth";

		// IE is buggy
		page.style.margin="0% 2% 0% 6px";
		//page.style.maxWidth="100%";
	}
	else
	{
		page.className="nosidebar";

		// IE is buggy
		page.style.margin="0% 2% 0% 6px";
		//page.style.maxWidth="45em";
	}
}

/**
 * container for the sheet picker page
 * @constructor
 * @param {domelement} container parent container DIV element
 */
function ThumbPage(container) {
	this.container=container;

	this.base=document.createElement("div");
	this.base.id=uniqueId("thumbbase");
	this.thumbSection=document.createElement("div");
	this.thumbSection.id=uniqueId("thumbsec");

	this.thumbSection.className="thumbsheet";
	this.sheets=[];

	this.base.appendChild(globalMessages.thumbblurb.cloneNode(true));
	this.base.appendChild(this.thumbSection);
	this.thumbFooterMessage=document.createElement('div');
}

/**
 * register a set of sheets
 * @param {AllocatedSheet[]} sheetArray array of AllocatedSheet
 */
ThumbPage.prototype.add_sheet_list = function (sheetArray) {	
	for (var n in sheetArray) {
		var sheet=sheetArray[n];
		this.sheets[sheet.sheetId]=sheet.thumb;
		this.thumbSection.appendChild(sheet.thumb.table);
	}
};

/**
 * display sheet picker page
 * scan sheets for state (i.e. documented/saved/reviewed etc)
 * @todo support indication of sheets which are saved as partially documented
 */
ThumbPage.prototype.display = function() {
	var unsaved=0;
	var documented=0;
	var skipped=0;
	var faulty=0;
	var saved=0;
	var reviewed=0;
	this.review=0;
	var sheets=0;

	var p,n;

	for (n in this.sheets) {
		this.sheets[n].update_fields();
		if (this.sheets[n].data.modified) {unsaved++;}
		if (this.sheets[n].data.documented) {documented++;}
		if (this.sheets[n].data.faulty) {faulty++;}
		if (this.sheets[n].data.saved) {saved++;}
		if (this.sheets[n].data.reviewed) {reviewed++;}
		if (this.sheets[n].data.review=="peer" || this.sheets[n].data.review=="expert") {this.review++;}
		sheets++;
	}
	set_width(true);

	if (this.allocSizeChangeHandle) {this.allocSizeChangeHandle=remove_event_handler(this.allocSizeChangeHandle);}
	if (this.thumbFooterMessage.parentNode) {this.thumbFooterMessage.parentNode.removeChild(this.thumbFooterMessage);}
	this.thumbFooterMessage=document.createElement('div');
	this.thumbFooterMessage.className="thumbpgfoot";

	if (unsaved)
	{
		p=document.createElement("p");
		p.appendChild(document.createTextNode(unsaved+" sheet"+((unsaved!=1)?'s have':' has')+" been modified but not yet saved."));
		p.appendChild(document.createElement('br'));
		
		this.loadNextLink=document.createElement("a");
		this.loadNextLink.href="/atHomeForm/"; // nop to make pointer change over link
		this.loadNextLink.appendChild(document.createTextNode("load next set"));
		p.appendChild(this.loadNextLink);
		p.appendChild(document.createTextNode(' (discard any unsaved changes)'));
		this.loadNextClickHandle=register_event_handler(this.loadNextLink,'click',this,'load_next_click_handler',0);
		p.appendChild(document.createElement('br'));

		this.finishLink=document.createElement("a");
		this.finishLink.href="/atHome/"; // nop to make pointer change over link
		this.finishLink.appendChild(document.createTextNode("finish"));
		p.appendChild(this.finishLink);
		this.finishClickHandle=register_event_handler(this.finishLink,'click',this,'finish_click_handler',0);
		p.appendChild(document.createTextNode(' (discard any unsaved changes)'));
		p.appendChild(document.createElement('br'));
		
		this.closeLink=document.createElement("a");
		this.closeLink.appendChild(document.createTextNode("close window"));
		p.appendChild(this.closeLink);
		this.closeClickHandle=register_event_handler(this.closeLink,'click',this,'close_click_handler',0);
		p.appendChild(document.createTextNode(' (unsaved changes will be remembered when you next vist)'));
		p.appendChild(document.createElement('br'));
		p.appendChild(document.createTextNode('or click on an image to continue editing.'));
	
		this.thumbFooterMessage.appendChild(p);
	}
	else
	{
		if (this.review===0) {
			if ((saved==sheets) || (documented==sheets)) {
				// All documented!
				p=document.createElement("p");
				p.appendChild(document.createTextNode("Done! Thankyou!"));
				p.style.fontSize="1.4em";
				this.thumbFooterMessage.appendChild(p);
			}
		}
		else
		{
			if (reviewed==sheets) {
				// All documented!
				p=document.createElement("p");
				p.appendChild(document.createTextNode("Done! Thankyou!"));
				p.style.fontSize="1.4em";
				this.thumbFooterMessage.appendChild(p);
			}
		}

		p=document.createElement("p");
		this.loadNextLink=document.createElement("a");
		this.loadNextLink.href="/atHomeForm/"; //nop to make pointer change etc
		this.loadNextLink.appendChild(document.createTextNode("load next set"));

		p.appendChild(this.loadNextLink);
		this.loadNextClickHandle=register_event_handler(this.loadNextLink,'click',this,'load_next_click_handler',0);
		p.appendChild(document.createTextNode(" | "));


		this.finishLink=document.createElement("a");
		this.finishLink.appendChild(document.createTextNode("finish"));
		this.finishLink.href="/atHome/"; //nop to make pointer change etc
		p.appendChild(this.finishLink);
		this.finishClickHandle=register_event_handler(this.finishLink,'click',this,'finish_click_handler',0);

		this.thumbFooterMessage.appendChild(p);
	}
	
	if (!tmpUser) {this.thumbFooterMessage.appendChild(this.sheet_choice_menu());}

	this.thumbSection.appendChild(this.thumbFooterMessage);

	this.container.appendChild(this.base);
	
	var maxHeight=0;
	for (n in this.sheets) {
		maxHeight=(maxHeight>this.sheets[n].table.firstChild.offsetHeight)?maxHeight:this.sheets[n].table.firstChild.offsetHeight;
	}

	for (n in this.sheets) {
		var table=this.sheets[n].table;
		//table.rows[table.rows.length-1].cells[0].firstChild.style.backgroundColor="red";
		table.rows[table.rows.length-1].cells[0].firstChild.style.marginBottom=((maxHeight-table.firstChild.offsetHeight)*1)+"px";
	}
	
	set_title("herbaria@home allocated sheets","herbaria@home allocated sheets");
	if (document.getElementById('footermenu')) {document.getElementById('footermenu').style.display="none";}
};

/**
 * Menu to pick how many sheets to allocate in a batch
 * registers an onchange event handler for the menu
 * @return htmlfragment
 */
ThumbPage.prototype.sheet_choice_menu = function() {
	var fragment=document.createDocumentFragment();
	var p=document.createElement('p');
	p.appendChild(document.createTextNode("Number of sheets to allocate at a time "));
	var select=document.createElement('select');
	var option;
	var allocNumber;
	
	var cookie=cookie_read_value('sheetalloc');
	if (cookie) {
		allocNumber=parseInt(cookie,10);
	} else {
		allocNumber=slowConnection?1:4; // default to 4
	}
	
	for (var n=1; n<=10;n++) {
		option=select.appendChild(document.createElement('option'));
		option.value=n;
		option.appendChild(document.createTextNode(n+' sheet'+(n>1?'s':'')));
		if (n==allocNumber) {option.selected=true;}
		select.appendChild(option);
	}
	
	this.allocSizeChangeHandle=register_event_handler(select,'change',this,'alloc_size_change_handler','');
	
	p.appendChild(select);
	p.appendChild(document.createTextNode(". If you have a slow network connection or an older computer then allocating a single sheet at a time is recommended."));
	
	fragment.appendChild(p);
	
	return fragment;
};

/**
 * update cookie to reflect menu choice
 */
ThumbPage.prototype.alloc_size_change_handler = function(event,element,param) {
	cookie_set_value('sheetalloc',element.options[element.selectedIndex].value,64);
	slowConnection=(element.options[element.selectedIndex].value==1);
};

/**
 * 'finished' button click handler
 * release sheets
 * close window
 */
ThumbPage.prototype.finish_click_handler =function(event,element) {
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
	
	globalModifiedSinceLastBackup=false;

	// send message to release specimens
	this.finished_message();

	try {
		if (window.opener && window.opener.closed===false && window.opener.allocation && window.opener.allocation.setSheetListSynchronous) {
			window.opener.allocation.setSheetListSynchronous();
		}
	} catch (err) {}
	
	window.close();
};

/**
 * similar to {@link ThumbPage#finish_click_handler} but do not release sheets
 */
ThumbPage.prototype.close_click_handler =function(event,element) {
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
	
	try {
		if (window.opener && window.opener.closed===false && window.opener.allocation && window.opener.allocation.setSheetList) {
			//window.opener.location.reload();
			window.opener.allocation.setSheetListSynchronous();
		}
	} catch (err) {}

	window.close();
};

/**
 * release allocated sheets by invoking XMLAllocatedSpecimens (synchronously)
 * clears globalModifiedSinceLastBackup as backups are nolonger relevant and there have been contention problems between release and backup.
 */
ThumbPage.prototype.finished_message =function () {
	globalModifiedSinceLastBackup=false;
	this.finishedRequest=agnostic_XMLHttpRequest(); // create the request object
	synchronous_xml_request(this.finishedRequest,scriptUrl+'XMLAllocatedSpecimens?release=true&seed='+(Math.random())+phpSession,null);
	this.finishedRequest=null;
};

/**
 * load next sheets click handler
 * currently fairly kludgy, acts by reloading entirely with new url paramters. Historically this has proved necessary to minimize memory leaks.
 * Leaks are probably resolved now, so should move to just throwing away old sheets and loading new, without page refresh.
 * @todo non-reload referesh
 */
ThumbPage.prototype.load_next_click_handler =function(event,element,param) {
	var reviewParam='';
	globalModifiedSinceLastBackup=false;

	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE

	if (this.review>0) {
		// was previously reviewing sheets
		reviewParam="&review=1";
	}

	window.location=scriptUrl+formLeafName+"/?key="+atHomeKey+"&release="+atHomeReleaseCode+(slowConnection?"&slow=1":"")+"&inst="+globalInstitution+"&tag="+globalTag+"&allocate="+allocateParam+reviewParam+(reviewUID?("&revuid="+reviewUID):"")+phpSession;
};

/**
 * stop displaying the sheet selector page, ditch button event handlers
 */
ThumbPage.prototype.hide = function() {
	this.base.parentNode.removeChild(this.base);

	if (this.loadNextClickHandle) {	
		this.loadNextClickHandle=remove_event_handler(this.loadNextClickHandle);
	}
	
	if (this.finishClickHandle) {
		this.finishClickHandle=remove_event_handler(this.finishClickHandle);
	}
	
	if (this.closeClickHandle) {
		this.closeClickHandle=remove_event_handler(this.closeClickHandle);
	}
	
	if (this.allocSizeChangeHandle) {this.allocSizeChangeHandle=remove_event_handler(this.allocSizeChangeHandle);}
};

/**
 * destructor
 */
ThumbPage.prototype.destroy = function() {
	this.sheets=null;
	this.container=null;
	this.base=null;
	this.thumbSection=null;
	this.thumbFooterMessage=null;

	if (this.loadNextClickHandle) {
		this.loadNextClickHandle=remove_event_handler(this.loadNextClickHandle);
	}
	
	if (this.finishClickHandle) {
		this.finishClickHandle=remove_event_handler(this.finishClickHandle);
	}
	
	if (this.closeClickHandle) {
		this.closeClickHandle=remove_event_handler(this.closeClickHandle);
	}
};

/**
 * single thumbnail, manifest as a floating table, with some info below
 * @constructor
 */
function SpecimenThumb(specimenData)
{
	this.table=document.createElement("table");

	// over-come inline table non-support
	this.table.style.display='inline';
	try {
	    this.table.style.display = 'inline-table';
	} catch(e) {}

	this.data=specimenData;
	
	this.thumbImage=document.createElement("img");
	this.thumbImage.src="http://"+specimenData.image.host+"/"+specimenData.image.path+"/"+specimenData.image.leafName+"/thumb_"+specimenData.image.leafName+".jpg";
	this.thumbImage.width=192;
	this.thumbImage.height=Math.floor((this.thumbImage.width/specimenData.image.sourceInitialWidth)*specimenData.image.sourceInitialHeight);
	
	this.table.insertRow(0).insertCell(0).appendChild(this.thumbImage);
	this.handler=register_event_handler(this.thumbImage,'click',this,'image_click_handler',0);
}

/**
 * click handler invoked when sheet thumbnail clicked,
 * hide the sheet selector ({@link ThumbPage#hide})
 * display specimen doc form ({@link SheetPage#display})
 */
SpecimenThumb.prototype.image_click_handler = function (event,element,param) {
	thumbPage.hide();

	specimens[this.data.sheetId].specSheet.display();
};

/**
 * update summary information
 */
SpecimenThumb.prototype.update_fields = function () {	
	//while (this.table.rows.length > 1) {this.table.deleteRow(1);} // delete existing rows
    for (var n=this.table.rows.length-1;n>0;n--) {this.table.deleteRow(n);}
	
	if (this.docStateElement && this.docStateElement.parentNode) {
		this.docStateElement.parentNode.removeChild(this.docStateElement);
		this.docStateElement=null;
	}

	var specimenHtml=this.data.get_html_array();

	var cell,row;
	var docStateMsg=this.data.get_doc_state();
	
	if (docStateMsg) {
		this.docStateElement=this.table.rows[0].cells[0].appendChild(document.createElement('p'));
		this.docStateElement.appendChild(document.createTextNode(docStateMsg));
		this.docStateElement.style.color="red";
	}

	var l=specimenHtml.length;
	
	for (var n=0;n<l;n++) {
		row=this.table.insertRow(n+1);
		cell=row.insertCell(0);
		if (docStateMsg==='undocumented') {cell.className='undocthumb';}
		cell.appendChild(specimenHtml[n]);
	}
};

/**
 * destructor
 */
SpecimenThumb.prototype.destroy = function () {
	if (this.handler) {this.handler=remove_event_handler(this.handler);}

	if (this.docStateElement && this.docStateElement.parentNode) {
		this.docStateElement.parentNode.removeChild(this.docStateElement);
	}
	this.docStateElement=null;

	this.thumbImage.parentNode.removeChild(this.thumbImage);
	this.thumbImage=null;

	this.table=null;
	this.data=null;
};

/**
 * Manifests configure the doc form structure 
 */
function Manifest(xml) {
	this.xml=xml;
}

/**
 * @return list of manifested fields {className: string, manifestXml: xml node}
 */
Manifest.prototype.get_fields = function () {
	if (!this.fields) {
		var xmlFields=this.xml.getElementsByTagName('field');
		this.fields={};
	
		if (xmlFields.length===0) {
			throw new Error('herb@home error:No fields in sheet Manifest.');
		} else {
			for (var n=0,l=xmlFields.length;n<l;n++) {
				this.fields[xmlFields[n].getAttribute('name')]={
					className: xmlFields[n].getAttribute('class'),
					manifestXml: xmlFields[n]
				};
			}
		}
	}
	return this.fields;
};

/**
 * return xml description
 * @param doc xml root element
 * @return manifest xml
 */
Manifest.prototype.get_xml = function (doc) {
	var xml;
	
	if (!doc.importNode) {
		// circumvent broken Internet Explorer
		xml=import_node(doc,this.xml,true);
	} else {
		xml=doc.importNode(this.xml,true);
	}
	
	return xml;
};

/**
 * destructor
 */
Manifest.prototype.destroy = function () {
	this.xml=null;
	if (this.fields) {
		for (var n in this.fields) {
			this.fields[n]=null;
		}
		this.fields=null;
	}
};

/**
 * container class for entire form
 * @constructor
 * @param {domelement} container DIV in which this will be contained
 */
function SheetPage(container) {
	this.container=container;
	this.base=document.createElement("div");
	this.base.id=uniqueId("sheetbase");
	this.narrowForm=false;
}

/**
 * display the documentation form
 */
SheetPage.prototype.display=function () {
	set_width(false);
	this.data.image.set_sheet_images();
	this.container.appendChild(this.base); // container must be attached before any attempt to open detached window
	if (SpecimenImage.pinned) {
		this.data.image.display_attached();
	} else {
		this.data.image.open_detached_window();
	}

	set_title("herbaria@home specimen"+((this.data.modified) ?' (unsaved)':''),this.data.title);

	// check if this is first specimen
	var n=this.data.sheetId;
	while (--n >= 0 && specimens[n]===undefined) {}
	//if (!specimens[n] && this.minusButton.parentNode) {this.minusButton.setAttribute("disabled","disabled");}
	//else if (this.data.sheetId==(specimens.length-1)) {this.plusButton.setAttribute("disabled","disabled");}
	if (!specimens[n] && this.minusButton.parentNode) {
		this.minusButton.className='sheetnav disabled';
		this.minusButton.href="";
		this.minusButton.setAttribute("title",'');
	}
	else if (this.data.sheetId==(specimens.length-1)) {
		this.plusButton.className='sheetnav disabled';
		this.plusButton.href="";
		this.plusButton.setAttribute("title",'');
	}


	currentSheet=this;
	
	var footer=document.getElementById('footermenu');
	if (footer && !globalIFrameFlag) {footer.style.display="block";}
	
	this.blurb.style.display=globalShowExpandedHelp?'block':'none';
	this.data.formFooter.apply_state(this);
	
	if (!(standardBrowser || IEversion >= 7)) {
		if (!this.onResizeHandle) {this.onResizeHandle=register_event_handler(window,'resize',this,'resize_handler','');}
	}
};

/**
 * used to implement min-width for doc form for browsers that do not support this
 */
SheetPage.prototype.resize_handler=function () {
	if (this.inResize) {return;}
	this.inResize=true;
	
	var width=document.documentElement.clientWidth; // document.body.offsetWidth; // clientWidth
	
	if (width<700) {
		if (!this.narrowForm) {
			this.data.formContainer.style.width="540px";
			this.data.comments.container.style.width="540px";
			this.narrowForm=true;
		}
	} else {
		if (this.narrowForm) {
			this.data.formContainer.style.width="84%";
			this.data.comments.container.style.width="84%";
			this.narrowForm=false;
		}
	}
	this.inResize=false;
};

/**
 * hide documentation form, ditching associated sheet images 
 */
SheetPage.prototype.hide=function () {
	if (this.onResizeHandle) {this.onResizeHandle=remove_event_handler(this.onResizeHandle);}
	
	if (currentBubble) {
		currentBubble.hide();
		currentBubble=null;
	}
	
	if (globalFocusedRowId && document.getElementById(globalFocusedRowId)) {document.getElementById(globalFocusedRowId).style.display="none";}
	globalFocusedRowId=null;
	this.data.image.clear_sheet_images();
	this.base.parentNode.removeChild(this.base);

	currentSheet=null;
};

/**
 * initialize SheetPage with {@link SpecimenData}
 * @param {SpecimenData} specimenData
 */
SheetPage.prototype.set_content=function (specimenData) {
	this.data=specimenData;
	specimenData.parentSheet=this;

	var ctrlButton;
	var controls=document.createElement("span");
	controls.id=uniqueId("sheetcontrols");

	this.minusButton=document.createElement("a");
	this.minusButton.className='sheetnav';
	this.minusButton.appendChild(document.createTextNode('<'));
	this.minusButton.title='previous sheet';
	this.minusButton.href='#';
	this.minusButtonHandle=register_event_handler(this.minusButton,'click',this,'sheet_control_click_handler',-1); // previous button click handler
	controls.appendChild(this.minusButton);

	ctrlButton=document.createElement("a");
	ctrlButton.className='sheetnav';
	ctrlButton.appendChild(document.createTextNode('list'));
	ctrlButton.title='list allocated sheets';
	ctrlButton.href='#';
	this.listButtonHandle=register_event_handler(ctrlButton,'click',this,'sheet_control_click_handler',0); // home button click handler
	controls.appendChild(ctrlButton);

	this.plusButton=document.createElement("a");
	this.plusButton.className='sheetnav';
	this.plusButton.appendChild(document.createTextNode('>'));
	this.plusButton.title='next sheet';
	this.plusButton.href='#';
	this.plusButtonHandle=register_event_handler(this.plusButton,'click',this,'sheet_control_click_handler',1); // home button click handler
	controls.appendChild(this.plusButton);

	controls.appendChild(this.data.formAdmin.skip);
	controls.appendChild(this.data.formAdmin.illegible);
	controls.appendChild(this.data.formAdmin.noInfo);
	controls.appendChild(this.data.formAdmin.faulty);

	this.data.set_sheet_name();
	this.data.formFooter.set_institution_text(this.data.institutionAbbr);
	this.data.formFooter.set_sheet_help_text(this.data.sheetHelp);
	
	this.skipButtonHandle=register_event_handler(this.data.formAdmin.skip,'click',this,'sheet_skip_click_handler',0);
	this.faultyButtonHandle=register_event_handler(this.data.formAdmin.faulty,'click',this,'sheet_faulty_click_handler',0);
	this.illegibleButtonHandle=register_event_handler(this.data.formAdmin.illegible,'click',this,'sheet_illegible_click_handler',0);
	this.noInfoButtonHandle=register_event_handler(this.data.formAdmin.noInfo,'click',this,'sheet_no_info_click_handler',0);
	
	this.blurb=this.base.appendChild(document.createElement('div'));
	//this.blurb.id=uniqueId('formblurb');
	//this.data.formFooter.formBlurbId="testing"; ///this.blurb.id;
	//alert('initial id='+this.data.formFooter.formBlurbId);
	this.blurb.appendChild(globalMessages.formblurb.cloneNode(true));

	this.base.appendChild(controls);

	this.base.appendChild(this.data.imageContainer);
	this.base.appendChild(this.data.formContainer);
	this.base.appendChild(this.data.comments.container);
	this.base.appendChild(this.data.formFooter.container);
	this.footerSkipButtonHandle=register_event_handler(this.data.formFooter.skip,'click',this,'sheet_skip_click_handler',0); // home button click handler
	this.helpToggleClickHandle=register_event_handler(this.data.formFooter.extendedHelp,'click',this,'toggle_help_click_handler',0);
};

/**
 * change display state of bubble help text
 */
SheetPage.prototype.toggle_help_click_handler = function (event,element) {
    if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE

	if (globalShowExpandedHelp) {
		element.firstChild.data="Show expanded help messages";
		if (globalFocusedRowId && document.getElementById(globalFocusedRowId)) {
			document.getElementById(globalFocusedRowId).style.display="none";
		}
		globalFocusedRowId=null;
		
		this.blurb.style.display="none"; // hide header help blurb
		
		// there's a strange bug where globalFocusedRowId can still be set, but the element is absent
	} else {
		element.firstChild.data="Hide expanded help messages";
		this.blurb.style.display="block"; // show header help blurb
	}
	
	globalShowExpandedHelp=!globalShowExpandedHelp;
	cookie_set_value('expandedhelp',globalShowExpandedHelp?'show':'hide',30);
};

/**
 * click handler mediating switch between sheets/return to tile overview
 * 
 * @param event
 * @param element
 * @param {int} direction previous=-1, home=0, next=+1
 */
SheetPage.prototype.sheet_control_click_handler= function (event,element,direction) {
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE

    switch (direction)
	{
		case 0:
		update_window_position_cookie();
		this.hide();
		thumbPage.display();
		break;

		case 1:
		var n=this.data.sheetId;
		while (++n < specimens.length && specimens[n]===undefined) {}
		if (specimens[n]) {
			this.hide();
			specimens[n].specSheet.display();
		}
		break;

		case -1:
		var n=this.data.sheetId;
		while (--n >= 0 && specimens[n]===undefined) {}
		if (specimens[n]) {
			this.hide();
			specimens[n].specSheet.display();
		}
		break;
	}
};

/**
 * skip sheet click handler
 */
SheetPage.prototype.sheet_skip_click_handler = function (event,element,param) {
	this.data.skipped=true;
	this.report_fault('skipped');

	this.next_new();
};

/**
 * 'bad photo' button click handler
 */
SheetPage.prototype.sheet_faulty_click_handler = function (event,element,param) {
	this.data.faulty=true;
	this.report_fault('photo');

	alert('Thank you for your feedback, the sheet has been marked as faulty.');

	this.next_new();
};

/**
 * 'no info' button click handler
 */
SheetPage.prototype.sheet_no_info_click_handler = function (event,element,param) {
	this.data.faulty=true;
	this.report_fault('noinfo');

	alert('Thank you for your feedback, the sheet has been marked as faulty.');

	this.next_new();
};

/**
 * 'illegible' button click handler
 */
SheetPage.prototype.sheet_illegible_click_handler = function (event,element,param) {
	this.data.illegible=true;
	this.report_fault('illegible');

	alert('Thank you for your feedback, the sheet has been marked as illegible.');

	this.next_new();
};

/**
 * generate an XML doc with the sheet id and error code and post back
 * reponse is a minimalistic document with the assigned specimen ids
 * 
 * @param fault 'illegible','noinfo','photo','skipped'
 */
SheetPage.prototype.report_fault =function (fault) {
	if (!globalBlockSave) {
		// globalBlockSave is used as flag to indecate that have logged in registered user (non-registered users cannot report faults)
		
		var doc=create_xml_doc();
		doc.documentElement.setAttribute('uid',uid); // currently reading from global uid, not certain if this compromises security - on server session uid should take precedence
	
		var sheet=doc.createElement('sheet');
		sheet.setAttribute('p',this.data.sheetExternalId);
		sheet.setAttribute('id',this.data.sheetId);
	
		var error=doc.createElement('error');
		error.appendChild(doc.createTextNode(fault));
		sheet.appendChild(error);
	
		doc.documentElement.appendChild(sheet);
	
		if (this.faultRequest) {
			this.faultRequest.abort(); // stop outstandijng previous request
		}
	
		this.faultRequest=agnostic_XMLHttpRequest(); // create the request object
		xml_save(this.faultRequest,scriptUrl+'atHomeMarkInvalid/?x=x'+phpSession,associateObjectWithXMLRequest(this, "fault_xml_handler", ''),doc);
	}
	/* respose is of the form
<response uid="n">
	<sheet id="n" p="n">
		<error> [none|photo|illegible|skipped]
		</error>
	</sheet>
</response>
	*/
};

/**
 * {@link SheetPage#report_fault} response handler
 */
SheetPage.prototype.fault_xml_handler = function () {
	if (this.faultRequest.readyState == 4 && this.faultRequest.status == 200)
    {
    	var response  = this.faultRequest.responseXML.documentElement;

		var errors = response.getElementsByTagName('error');
		if (errors.length>0) {alert(errors[0].firstChild.data);}
		
		this.faultRequest=null;
    }
};

/**
 * move to the next undocumented sheet (called after save)
 */
SheetPage.prototype.next_new =function () {
	var n=this.data.sheetId;
	while (++n < specimens.length && specimens[n]===undefined) {}

	window.scrollTo(0,0);
	if (specimens[n]) {
		this.hide();
		specimens[n].specSheet.display();
	}
	else
	{
		this.hide();
		thumbPage.display();
	}
};

/**
 * move to first undocumented sheet (after initial load)
 */
SheetPage.prototype.first_new =function () {
	var n=this.data.sheetId;
	
	while (n < specimens.length && (specimens[n]===undefined || ((specimens[n].data.review!="peer" && (specimens[n].data.documented===true && specimens[n].data.partial===false)) || (specimens[n].data.review=="peer" && specimens[n].data.reviewed===true) ))) {n++;}

	if (specimens[n]) {
		specimens[n].specSheet.display();
	} else {
		thumbPage.display();
	}
};

/**
 * destructor
 */
SheetPage.prototype.destroy = function () {
	
	if (this.faultRequest) {
		this.faultRequest.abort(); // stop outstanding request
		this.faultRequest=null;
	}
	
	this.blurb=null;
	
	if (this.minusButtonHandle) {this.minusButtonHandle=remove_event_handler(this.minusButtonHandle);}
	this.listButtonHandle=remove_event_handler(this.listButtonHandle);
	if (this.plusButtonHandle) {this.plusButtonHandle=remove_event_handler(this.plusButtonHandle);}

	this.skipButtonHandle=remove_event_handler(this.skipButtonHandle);
	this.faultyButtonHandle=remove_event_handler(this.faultyButtonHandle);
	this.illegibleButtonHandle=remove_event_handler(this.illegibleButtonHandle);
	this.noInfoButtonHandle=remove_event_handler(this.noInfoButtonHandle);
	this.footerSkipButtonHandle=remove_event_handler(this.footerSkipButtonHandle);
	this.helpToggleClickHandle=remove_event_handler(this.helpToggleClickHandle);
	if (this.onResizeHandle) {this.onResizeHandle=remove_event_handler(this.onResizeHandle);}

	this.container=null;
	this.base=null;
	this.minusButton=null;
	this.plusbutton=null;
};

/**
 * with early versions of IE a place holder handler function must be given to XMLrequest or the request fails silently
 */
function placeholder_handler()
{
	//nop
}

/**
 * save backup of current state of all sheets
 * @param {bool} synchronous true to make synchronous backup
 */
function send_backup(synchronous) {
	if (globalModifiedSinceLastBackup) {
		globalModifiedSinceLastBackup=false; // change flag here to reduce likelyhood of possible backup collision
	
		var doc=create_xml_doc();	
		var logString=[];
		var date=new Date();
		
		for (var sheet in specimens) {
			specimens[sheet].data.output_xml(doc);
			if (specimens[sheet].data.modifiedSinceBackup) {
				logString[logString.length]='time['+specimens[sheet].data.sheetId+']='+(date.getTime()-specimens[sheet].data.modifiedStampSinceBackup.getTime());
				specimens[sheet].data.modifiedSinceBackup=false;
			}
		}
		
		logString[logString.length]='client='+globalVersion;
		logString=(logString.length>0)?('?'+logString.join('&')):'';
		
		backupSaveRequest=agnostic_XMLHttpRequest(); // create the request object
		if (synchronous) {
			doc.documentElement.setAttribute('synch','true'); // for debuggin purposes
			xml_synchronous_save(backupSaveRequest,scriptUrl+'atHomeTemporaryBackup.php'+logString+phpSession,placeholder_handler,doc);
		}
		else {
			xml_save(backupSaveRequest,scriptUrl+'atHomeTemporaryBackup.php'+logString+phpSession,placeholder_handler,doc);
		}
	}
}

/**
 * reports a javascript error
 */
function log_error(message,url,line) {	
	window.onerror=null;
	
	var doc=create_xml_doc();
	var error=doc.createElement('error');
	
	if (line!==null) {error.setAttribute('line',line);}
	if (url!==null) {error.setAttribute('url',url);}
	//error.setAttribute('caller',window.onerror.caller||window.onerror.arguments.caller);
	if (uid!==null) {error.setAttribute('userid',uid);}
	
	error.setAttribute('version',globalVersion);
	error.setAttribute('browser',navigator.appName);
	error.setAttribute('browserv',navigator.appVersion);

	error.appendChild(doc.createTextNode(message));

	doc.documentElement.appendChild(error);

	errorReportRequest=agnostic_XMLHttpRequest(); // create the request object
	xml_save(errorReportRequest,scriptUrl+'javascriptErrorLog.php?x=x'+phpSession,placeholder_handler,doc);

	window.onerror=log_error; // turn on error handling again
	return true; // suppress normal error reporting
}

/**
 * container for all data related to sheet, (images and forms)
 * @constructor
 */
function SpecimenData() {
	this.parentSheet = null;

	this.imageContainer=document.createElement("div");
	this.imageContainer.id=uniqueId("imageContainer");
	this.imageContainer.style.position="relative";
	this.formContainer=document.createElement("div");
	this.formContainer.id=uniqueId("formContainer");
	
	this.formContainer.style.position="relative";
	this.formContainer.style.padding="0 18% 0 0"; //"150px";
	this.formContainer.style.minWidth="600px";

	this.forms=[];

	this.formAdmin=new FormAdmin();
	this.formFooter=new FormFooter();

	this.saveHandle=register_event_handler(this.formFooter.save,'click',this,'save_button_click_handler',false); // 'false' for save
	this.partialSaveHandle=register_event_handler(this.formFooter.partialSave,'click',this,'save_button_click_handler',true); // 'true' for partial

	this.defaultTaxonName='taxon';
	this.defaultTaxonId='';
	this.defaultTaxonAuthority='';
	this.defaultTaxonCommonName='';
	
	this.previousTaxonName='';
	this.previousTaxonId='';
	this.previousTaxonAuthority='';
	this.previousTaxonCommonName='';
	
	this.sheetHelp=''; // sheet specific help text

	this.modified=null;
	this.modifiedSinceBackup=false;
	this.set_modified(false);
	this.saved=false; // has been modified && saved
	this.documented=false;
	this.skipped=false;
}

/**
 * doc form title
 */
SpecimenData.prototype.title='Herbarium sheet form';

/**
 * click handler to initiate specimen save
 * @param event
 * @param element
 * @param {bool} partial set to indicate partial save (i.e. save, marking as only partially documented)
 */
SpecimenData.prototype.save_button_click_handler= function (event,element,partial) {

	if (globalSaveTimeoutHandle!==null) {
		clearTimeout(globalSaveTimeoutHandle);
		globalSaveTimeoutHandle=null;
	}
	globalSaveCount=0;
	this.partial=partial;
	this.save();
};

/**
 * output XML representation of sheet data and all forms for this specimen
 * @param {xmlnode} doc XML document root element
 */
SpecimenData.prototype.output_xml =function (doc) {
	doc.documentElement.setAttribute('uid',uid); // currently reading from global uid, not certain if this compromises security - on server session uid should take precedence
	doc.documentElement.setAttribute('client',globalVersion);
	if (tmpUser) {
		doc.documentElement.setAttribute('tmpuser','tmpuser');
	}

	var showExpandedHelp=doc.createElement('expandedhelp');
	showExpandedHelp.appendChild(doc.createTextNode(globalShowExpandedHelp?'true':'false'));
	doc.documentElement.appendChild(showExpandedHelp);

	var sheet=doc.createElement('sheet');
	sheet.setAttribute('p',this.sheetExternalId);
	sheet.setAttribute('manifest',this.manifestName);
	sheet.setAttribute('id',this.sheetId);
	if (this.modified) {sheet.setAttribute('modified','1');} // used for backup purposes
	if (this.saved || this.documented) {sheet.setAttribute('saved','1');} // used for backup purposes

	if (this.minorEditButton && (this.minorEditButton.checked.toString() == 'true')) {
		sheet.setAttribute('minoredit',1);
	}
	
	if (this.partial) {
		sheet.setAttribute('partial','1');
		sheet.setAttribute('reviewed',''); // partially compleated sheets cannot be reviewed
		this.reviewed=false;
	} else {
		if ((this.review=="peer" || this.review=="expert") && (this.reviwedButton.checked.toString() == 'true')) {
			sheet.setAttribute('reviewed',this.review);
			this.reviewed=true;
		}
		else {
			sheet.setAttribute('reviewed','');
			this.reviewed=false;
		}
	}
	
	var institution=doc.createElement('inst');
	institution.setAttribute('abbr',this.institutionAbbr);
	institution.setAttribute('id',this.institutionId);
	institution.appendChild(doc.createTextNode(this.institutionName));
	sheet.appendChild(institution);
	
	if (this.sheetHelp != '') {
		var sheetHelp=doc.createElement('sheethelp');
		sheetHelp.appendChild(doc.createTextNode(this.sheetHelp));
		sheet.appendChild(sheetHelp);
	}
	
	//output manifests
	for (var manifestName in manifests) {
		doc.documentElement.appendChild(manifests[manifestName].get_xml(doc));
	}
	
	// output images
	for (var i=0,n=this.images.length;i<n;i++) {
		var imageElement=doc.createElement('image');
		imageElement.setAttribute('leaf',this.images[i].leafName);
		imageElement.setAttribute('width',this.images[i].sourceInitialWidth);
		imageElement.setAttribute('height',this.images[i].sourceInitialHeight);
		imageElement.setAttribute('xtiles',this.images[i].sourceXTileNumber);
		imageElement.setAttribute('ytiles',this.images[i].sourceYTileNumber);
		imageElement.setAttribute('path', this.images[i].path);
		imageElement.setAttribute('host', this.images[i].originalHost);
		if (this.images[i].startPosition) {imageElement.setAttribute('origin', this.images[i].startPosition);}
	
		sheet.appendChild(imageElement);
	}

	var defaultTaxon=doc.createElement('defaulttaxon');
	defaultTaxon.setAttribute('id',this.defaultTaxonId?this.defaultTaxonId:'');
	defaultTaxon.setAttribute('authority',this.defaultTaxonAuthority?this.defaultTaxonAuthority:'');
	defaultTaxon.setAttribute('vulgar',this.defaultTaxonCommonName?this.defaultTaxonCommonName:'');
	defaultTaxon.setAttribute('warn',this.taxonWarning?'true':'false');
	
	if (this.defaultTaxonName) {defaultTaxon.appendChild(doc.createTextNode(this.defaultTaxonName));}
	sheet.appendChild(defaultTaxon);
	
	if (this.previousTaxonId >0) {
		var previousTaxon=doc.createElement('previoustaxon');
		previousTaxon.setAttribute('id',this.previousTaxonId?this.previousTaxonId:'');
		previousTaxon.setAttribute('authority',this.previousTaxonAuthority?this.previousTaxonAuthority:'');
		previousTaxon.setAttribute('vulgar',this.previousTaxonCommonName?this.previousTaxonCommonName:'');
		if (this.previousTaxonName) {previousTaxon.appendChild(doc.createTextNode(this.previousTaxonName));}
		sheet.appendChild(previousTaxon);
	}

	sheet.setAttribute('review',this.review); // so that save format mirrors load format

	if (this.fileNote) {
		var fileNoteElement=doc.createElement('filenote');
		fileNoteElement.appendChild(doc.createTextNode(this.fileNote));
		sheet.appendChild(fileNoteElement);
	}
	
	if (this.international) {
		sheet.appendChild(doc.createElement('international'));
	}
	
	sheet.appendChild(this.comments.get_xml(doc));

	// for each form output <specimen> object
	for (var n in this.forms) {
		sheet.appendChild(this.forms[n].get_xml(doc));
	}
	doc.documentElement.appendChild(sheet);
};

/**
 * generate an XML doc with the sheet settings and post back
 * reponse is a minimalistic document with the assigned specimen ids
 *
 * forthcomming backup should reflect future *saved* state of this specimen 
 * should consider what happens if subsequent save fails (ie. saved flag should be false in backup, but is incorrectly true)
 * praps should call send_backup *after* save, but this may cause problems with nested asynchronous calls, with broken IE6 ??
 */
SpecimenData.prototype.save =function () {
	this.modified=null;
	this.saved=true;   // probably should flag this later, after confirmed save (but will create problems for backup)
	this.skipped=false;
	
	send_backup(); // general backup should synchronise with save state

	if (this.progress && this.progress.parentNode) {this.progress.parentNode.removeChild(this.progress);}
	this.progress=document.createElement('img');
	this.progress.src="http://static.herbariumathome.org/snake_transparent.gif";
	this.progress.width=16;
	this.progress.height=16;
	this.formFooter.save.parentNode.insertBefore(this.progress,this.formFooter.save.nextSibling);

	var doc=create_xml_doc();
	this.output_xml(doc);

	globalSaveErrorText="Failed to save specimen, please check your internet connection and try again. If this does not fix the problem then please copy the data below and email it to support@herbariumathome.org and we will manually enter your results.\n"+xml_serialize(doc);

	var timeoutString="specimens["+this.sheetId+"].data.saveRequest.abort();globalSaveCount++;if (globalSaveCount<3) {specimens["+this.sheetId+"].data.save()}else{specimens["+this.sheetId+"].data.formFooter.status.firstChild.data=globalSaveErrorText;alert(globalSaveErrorText);specimens["+this.sheetId+"].data.progress.parentNode.removeChild(specimens["+this.sheetId+"].data.progress);}";

	globalSaveTimeoutHandle=window.setTimeout(timeoutString,60000);

	this.saveRequest=agnostic_XMLHttpRequest(); // create the request object
	xml_save(this.saveRequest,scriptUrl+'atHomeSaveSpecimen005t.php?x=x'+phpSession,associateObjectWithXMLRequest(this, "save_response_xml_handler", ''),doc);
};

/**
 * reponse handler for {@link SpecimenData#save}
 * on successful save regenerates all the form fields (to reflect any ammendments sent back following the save)
 */
SpecimenData.prototype.save_response_xml_handler = function () {
	if (this.saveRequest && this.saveRequest.readyState == 4 && this.saveRequest.status == 200)
    {  
    	if (globalSaveTimeoutHandle !=null) {
    		clearTimeout(globalSaveTimeoutHandle);
    		globalSaveTimeoutHandle=null;
    	}

    	globalSaveCount=0; // count of retries

    	if (this.saveRequest.responseXML) {
	    	var response  = this.saveRequest.responseXML.documentElement;
			
			var specimens = response.getElementsByTagName('specimen');
			if (specimens.length>0) {
				// have valid specimens, so delete the old form and replace with new data
	
				if (currentBubble) {
					currentBubble.hide();
					currentBubble=null;
				}
	
				//delete all forms
				for (var n in this.forms) {
					this.forms[n].formBase.parentNode.removeChild(this.forms[n].formBase); // delete current row
					this.forms[n].destroy();
	
					delete this.forms[n];
				}
	
				//recreate forms
				var l=specimens.length;
				for (n=0;n<l;n++) {
					var specimenXML=specimens[n];
					this.load_specimen_form(specimenXML);
				}
				
				//delete comments
				this.comments.clear();
				
				//recreate comments
				var comments=response.getElementsByTagName('comments');
				if (comments.length===0) {
					this.comments.add_blank();
					this.comments.add_feedback_option();
				}
				else {this.comments.add_from_xml(comments[0]);}
	
				this.set_modified(false);
				this.documented=true;
				this.saved=true;
	
				this.set_sheet_name();// message may have been changed to save failure previously
				this.refresh_specimen_labels();
	
				alert('Sheet saved successfully.\n\nThank you!');
				this.parentSheet.next_new();
			} else {		
				var loginRequest = response.getElementsByTagName('login');
				if (loginRequest.length>0) {
					tmpUser=true;
					globalBlockSave=true;
					
					if (registrationForm) {
						registrationForm.destroy(); // get rid of any residual form
					}
					
					registrationForm=new RegistrationForm();
					this.formFooter.apply_state(this.parentSheet);
					registrationForm.message.firstChild.data=loginRequest[0].firstChild.data;
				} else {
					var errorResponse = response.getElementsByTagName('error');
					
					if (errorResponse.length>0) {
						this.formFooter.status.firstChild.data=globalSaveErrorText; // xml text sent
						alert(errorResponse[0].firstChild.data);
					}
				}
			}
    	} else {
    		// failed to receive an XML response
    		this.formFooter.status.firstChild.data=globalSaveErrorText;
    		alert(globalSaveErrorText);
    	}
   
   		this.saveRequest=null;
		if (this.progress && this.progress.parentNode) {
			this.progress.parentNode.removeChild(this.progress);
		}
		this.progress=null;
	}
};

/**
 * sets the sheet name and link to the high resolution version of the primary image
 * (sheet name and link displayed near the bottom of the form)
 */
SpecimenData.prototype.set_sheet_name = function () {
	this.formFooter.status.firstChild.data="Sheet "+this.institutionAbbr+"."+this.sheetExternalId+" ("+this.institutionName+")";
	this.formFooter.status.href="http://static.herbariumathome.org/sheets/"+this.institutionAbbr+"/"+this.image.leafName+".jpg";
};

/**
 * update the specimens modified state and change window title to reflect this
 * @param <bool> state
 */
SpecimenData.prototype.set_modified = function (state) {
	
	if (state && !this.modifiedSinceBackup) {
		// first change since last backup
		this.modifiedStampSinceBackup=new Date();
		this.modifiedSinceBackup=true;
	}
	
	if (this.modified != state) {	
		this.modified=state;
		set_title("herbaria@home specimen"+(state ?' (unsaved)':''),this.title);
	}

	globalModifiedSinceLastBackup=globalModifiedSinceLastBackup || state;

	if (!state) {this.saved=false;} // saved flag indicates modified and saved (i.e. updated)
};

/**
 * initialize SpecimenData from xml descriptor
 * loads sheet description and associated specimen forms
 * @param {string} sheetExternalId external sheet identifier, e.g. P number or unique acc code
 * @param {string} sheetId internal (hu) database id
 * @param {xmlfragment} XMLdesc xml sheet element
 * @todo increase modularisation of sheet components - e.g. to support easy addition of new field elements (e.g. direct notes field ref when adding ocr text)
 */
SpecimenData.prototype.load =function (sheetExternalId,sheetId,XMLdesc) {
	this.sheetExternalId=sheetExternalId; // external unique sheet accession code or P number 
	this.sheetId=sheetId;
	var n;
	
	var images=XMLdesc.getElementsByTagName('image');
	n=images.length;
	
	this.manifestName=XMLdesc.getAttribute('manifest');
	if (!this.manifestName) {throw new Error('herb@home error: specimen data missing manifest name');}
	
	if (manifests[this.manifestName].xml.getAttribute('title')) {
		this.title=manifests[this.manifestName].xml.getAttribute('title');
	}
	
	this.images=[];
	
	for (var i=0; i<n; i++) {
		this.images[i]=new SpecimenImage(XMLdesc.getElementsByTagName('image')[i]);
	}
	
	if (n>1) {
		this.imageList=document.createElement('div');
		this.imageListClickHandles=[];
		for (i=0; i<n; i++) {
			var image=document.createElement('a');
			image.style.paddingRight="0.3em";
			image.appendChild(document.createTextNode('image '+(i+1)));
			this.imageList.appendChild(image);
			this.imageListClickHandles[i]=register_event_handler(image,'click',this,'image_select_handler',i);
		}
		this.imageContainer.appendChild(this.imageList);
	} else {
		this.imageList=null;
	}
	
	this.add_image(this.images[0]);

	var taxon=XMLdesc.getElementsByTagName('defaulttaxon');
	
	if (taxon.length && taxon[0].hasChildNodes()) {
		this.defaultTaxonName=taxon[0].firstChild.data;
		this.defaultTaxonId=taxon[0].getAttribute('id');
		this.defaultTaxonAuthority=taxon[0].getAttribute('authority');
		this.defaultTaxonCommonName=taxon[0].getAttribute('vulgar');
		this.taxonWarning=(taxon[0].getAttribute('warn')=='true');
		if (this.taxonWarning) {
			this.add_taxon_warning("Please check the default taxon name given ('"+this.defaultTaxonName+"') as it may be incomplete or inaccurate. If you are uncertain of the specimen\'s identity then you may prefer to skip the sheet, partially complete the documentation or request feedback from other users.");
		}
	} else {
		this.defaultTaxonName='';
		this.defaultTaxonId='';
		this.defaultTaxonAuthority='';
		this.defaultTaxonCommonName='';
		this.taxonWarning=true;
		this.add_taxon_warning('Please fill in the taxon name field for this specimen. If you are uncertain of the identification then you may prefer to skip the sheet, partially complete the documentation or request feedback from other users.');
	}
	
	var filenoteElement=XMLdesc.getElementsByTagName('filenote');
	if (filenoteElement.length && filenoteElement[0].hasChildNodes()) {
		this.fileNote=filenoteElement[0].firstChild.data;
		this.add_file_note(this.fileNote);
	} else {
		this.fileNote='';
	}
	
	if (tutorial) {
		this.tutorialId=XMLdesc.getAttribute('tutorialid');
	} else {
		this.tutorialId='';
	}
	
	taxon=XMLdesc.getElementsByTagName('previoustaxon');
	if (taxon.length >0 && taxon[0].hasChildNodes()) {
		this.previousTaxonName=taxon[0].firstChild.data;
		this.previousTaxonId=taxon[0].getAttribute('id');
		this.previousTaxonAuthority=taxon[0].getAttribute('authority');
		this.previousTaxonCommonName=taxon[0].getAttribute('vulgar');
	} else {
		this.previousTaxonName='';
		this.previousTaxonId='';
		this.previousTaxonAuthority='';
		this.previousTaxonCommonName='';
	}
	
	var institution=XMLdesc.getElementsByTagName('inst');
	this.institutionName=institution[0].firstChild.data;
	this.institutionAbbr=institution[0].getAttribute('abbr');
	this.institutionId=institution[0].getAttribute('id');
	
	var sheetHelpElement=XMLdesc.getElementsByTagName('sheethelp');
	if (sheetHelpElement.length && sheetHelpElement[0].hasChildNodes()) {
		this.sheetHelp=sheetHelpElement[0].firstChild.data;
	}
	
	flagElement=XMLdesc.getElementsByTagName('international');
	this.international=flagElement.length >0;
	
	var specimens=XMLdesc.getElementsByTagName('specimen');

	this.comments=new UserNotes(this);

	if (specimens.length===0) {
		//no specimens associated with sheet
		
		var ocrTextElements=XMLdesc.getElementsByTagName('ocrtext');
		
		if (ocrTextElements.length) {
			var l=ocrTextElements.length;
			for (var n=0;n<l;n++)
			{
				var form=this.add_blank();
				
				if (ocrTextElements[n].hasChildNodes()) {
					if (form.field.ocrnotes) {
						form.field.ocrnotes.notesField.value=ocrTextElements[n].firstChild.data;
						//form.field.ocrnotes.increase_size(form.field.ocrnotes.notesField);
						//form.field.ocrnotes.notesField.rows=0;
					} else {
						if (form.field.notes) {
							form.field.notes.notesField.value=ocrTextElements[n].firstChild.data;
							//form.field.notes.increase_size(form.field.notes.notesField);
							//form.field.notes.notesField.rows=0;
						}
					} 
				}
			}
		} else {
			this.add_blank();
		}
		
		this.comments.add_blank();
		this.comments.add_feedback_option();
		
		this.documented=false; // no sheets
		this.modified=false;
		this.saved=false;
		this.review=false;
		this.reviewed=false; // mark as not yet reviewed
		
		this.partial=false; // specimen has not been partially completed
	} else {
		this.reviewed=false; // mark as not yet reviewed

		this.partial=XMLdesc.getAttribute('partial');
		if (this.partial) {
			this.add_partial_message();
			this.partial=true;
			this.review=false; // cannot be partial and up for review
		} else {
			this.partial=false;
			
			this.review=XMLdesc.getAttribute('review'); // '' or 'peer' or 'expert'
			if (this.review=='peer' || this.review=='expert') {this.add_review_message();}
			else {this.review=false;}
		}

		var l=specimens.length;
		for (var n=0;n<l;n++) {
			var specimenXML=specimens[n];
			this.load_specimen_form(specimenXML);
		}

		this.modified=null;
		this.saved=(XMLdesc.getAttribute('saved')=="1" || XMLdesc.getAttribute('saved')=="true");

		if (XMLdesc.getAttribute('modified')=="1" || XMLdesc.getAttribute('modified')=="true") {
			this.set_modified(true); // may be reloading part documented but unsaved sheet (attribute is 1 or 'undefined' - so can't use directly as boolean param for set_modified
			this.documented=false;
		} else {
			this.documented=this.saved;
			this.set_modified(false);
		}
		
		var comments=XMLdesc.getElementsByTagName('comments');
		if (comments.length===0) {
			this.comments.add_blank();
			this.comments.add_feedback_option();
		} else {
			this.comments.add_from_xml(comments[0]);
		}
	}
	this.refresh_specimen_labels();
};

/**
 * add blank specimen form, prepopulated with default values
 */
SpecimenData.prototype.add_blank = function()
{
	var form= new SpecimenForm(this,this.manifestName);
	form.international=this.international;
	
	form.create_rows();
	
	form.set_default_taxon(this.defaultTaxonName,this.defaultTaxonId,this.defaultTaxonAuthority,this.defaultTaxonCommonName,this.previousTaxonName,this.previousTaxonId,this.previousTaxonAuthority,this.previousTaxonCommonName);
	form.populate_default();
	
	this.add_form(form);
	return form;
};

/**
 * populate and associate {@link SpecimenForm} from xml
 * @param {xmlspecimen} specimenXML
 * @todo consider how to generalize and better encapsulate taxa related initialization
 */
SpecimenData.prototype.load_specimen_form = function(specimenXML)
{
	var form= new SpecimenForm(this,this.manifestName);
	form.international=this.international;
	
	form.create_rows();
	form.set_default_taxon(this.defaultTaxonName,this.defaultTaxonId,this.defaultTaxonAuthority,this.defaultTaxonCommonName,this.previousTaxonName,this.previousTaxonId,this.previousTaxonAuthority,this.previousTaxonCommonName);
	
	form.add_from_xml(specimenXML);
	this.add_form(form);
	form.verify();
};

/**
 * returns form object given form id
 * @param <string> form id
 * @return SpecimenForm or null
 */
SpecimenData.prototype.get_form_by_id=function(id) {
	for (var n in this.forms) {
		if (this.forms[n].unique===id) {
			return this.forms[n];
		}
	}
	return null;
};

/**
 * remove specimen form (denoted by uniqueId)
 * following deletion if no forms remain then add a new one
 * @param event
 * @param element
 * @param {string} uniqueId
 */
SpecimenData.prototype.delete_specimen_click=function (event,element,uniqueId) {
	var form;
	// search form array and remove relevent entry
	for (var n in this.forms) {
		if (this.forms[n].unique===uniqueId) {
			form=this.forms[n];
			delete this.forms[n];
			break;
		}
	}

	if (!(standardBrowser || IEversion >= 7) && this.count_forms()===2) {
		// kludge to avoid IE 6 redraw problem, must be set before form destruction
		this.formContainer.style.height="auto";
	}

	// do deletion stuff first
	form.destroy();
	form=null;

	// recreate if necessary
	if (this.count_forms()===0) {
		form=new SpecimenForm(this,this.manifestName); // recreate blank specimen form
		form.international=this.international;
		form.create_rows();
		form.set_default_taxon(this.defaultTaxonName,this.defaultTaxonId,this.defaultTaxonAuthority,this.defaultTaxonCommonName,this.previousTaxonName,this.previousTaxonId,this.previousTaxonAuthority,this.previousTaxonCommonName);
		form.populate_default();
		this.add_form(form);	
	} else {
		this.set_form_scroll_state();
	}
	
	this.refresh_specimen_labels();
	this.set_modified(true);	
};

/**
 * associate form with sheet
 * @param {SpecimenForm} form
 */
SpecimenData.prototype.add_form=function (form) {
	this.forms.push(form);
	this.formContainer.appendChild(form.formBase);
	this.set_form_scroll_state();
	
	form.deleteButton.onclick=associate_obj_with_event(this, "delete_specimen_click",form.unique);
	form.addButton.onclick=associate_obj_with_event(this, "add_form_click",null);
};

SpecimenData.prototype.set_form_scroll_state=function() {
	if (this.count_forms()>1) {
		this.formContainer.style.overflow="auto";
		this.formContainer.style.overflowX="hidden";
		
		if (standardBrowser || IEversion >= 7) {
			this.formContainer.style.maxHeight="400px";
			//this.formContainer.style.width="84%";
		} else {
			this.formContainer.style.height="400px";
		}
		
		this.formContainer.style.borderBottomStyle="inset";
		this.formContainer.style.borderBottomWidth="thin";
		this.formContainer.style.borderBottomColor="gray";
		this.formContainer.scrollTop = this.formContainer.scrollHeight; // scroll to bottom (to ensure new form is visible)
	} else {
		this.formContainer.style.overflow="visible";
		this.formContainer.style.maxHeight="none";
		this.formContainer.style.borderBottomStyle="none";
		if (!(standardBrowser || IEversion >= 7)) {this.formContainer.style.height="auto";}
	}
};

/**
 * refresh specimen numbering (i.e. specimen x of n label)
 */
SpecimenData.prototype.refresh_specimen_labels=function () {
	var divs=this.formContainer.getElementsByTagName('div');
	var forms=[];
	var form;
	
	for (var i=0, l=divs.length; i<l; i++) {
		if (divs[i].className === "formbase") {
			forms.push(divs[i]);
		}
	}
	
	l=forms.length;
	if (l>1) {
		for (var n=0;n<l;n++) {
			var legend=forms[n].getElementsByTagName('div')[1];
			var legendLabel=legend.getElementsByTagName('span')[0];	
			legendLabel.replaceChild(document.createTextNode('specimen '+(n+1)+' of '+l),legendLabel.firstChild);
			
			if (n<(l-1)) {
				form=this.get_form_by_id(forms[n].id);
				form.addButton.style.display="none";
				if (n>0) {
					form.previousButton.style.display="inline";
					form.previousButton.href="#"+forms[n-1].id;
				} else {
					form.previousButton.style.display="none";
				}
				form.nextButton.style.display="inline";
				form.nextButton.href="#"+forms[n+1].id;
			}
		}
		form=this.get_form_by_id(forms[l-1].id);
		form.previousButton.href="#"+forms[l-2].id;
	} else {
		var legend=forms[0].getElementsByTagName('div')[1];
		var legendLabel=legend.getElementsByTagName('span')[0];
		legendLabel.replaceChild(document.createTextNode('specimen'),legendLabel.firstChild);
		form=this.get_form_by_id(forms[0].id);
		form.addButton.style.display="none";
		form.previousButton.style.display="none";
	}
	form.addButton.style.display="inline";
	form.nextButton.style.display="none";
};

/**
 * associate image with a sheet and attache
 * (there may be multiple images in {@link SpecimenData#images}, only one of which is visible at a time)
 * @param {SpecimenImage} image
 */
SpecimenData.prototype.add_image=function (image) {
	image.infoContainer.data=this;
	this.info=image.infoContainer;
	this.imageContainer.appendChild(image.infoContainer.div);
	this.imageContainer.appendChild(image.navContainer);
	this.imageContainer.appendChild(image.base);
	this.image=image;
};

/**
 * detach current image elements
 */
SpecimenData.prototype.remove_current_image=function () {
	this.image.clear_sheet_images();
	this.info=null;
	if (this.image.infoContainer.div.parentNode) {this.imageContainer.removeChild(this.image.infoContainer.div);}
	if (this.image.navContainer.parentNode) {this.imageContainer.removeChild(this.image.navContainer);}
	if (this.image.base.parentNode) {this.imageContainer.removeChild(this.image.base);}
	this.image=null;
};

/**
 * change currently displayed image
 * @param event
 * @param element
 * @param imageNumber
 */
SpecimenData.prototype.image_select_handler=function (event,element,imageNumber) {
	this.remove_current_image();
	this.add_image(this.images[imageNumber]);
	this.image.set_sheet_images();
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};

/**
 * add message indicating that sheet is available for review and add review tick box to {@link FormFooter}
 */
SpecimenData.prototype.add_review_message=function () {
	this.reviewMessage=document.createElement('p');
	this.reviewMessage.appendChild(document.createTextNode('This sheet has been documented by another user and is now waiting for review. Please make any corrections necessary, then tick the \'reviewed\' box and submit the sheet.'));
	this.reviewMessage.style.fontWeight="bold";
	this.formContainer.appendChild(this.reviewMessage);

	this.reviewedParagraph=document.createElement('p');
	this.reviwedButton=document.createElement('input');
	this.reviwedButton.type="checkbox";
	this.reviewedParagraph.appendChild(this.reviwedButton);

	var text=document.createElement('span');
	text.appendChild(document.createTextNode('mark as reviewed'));
	text.style.fontWeight="bold";

	this.reviewedParagraph.appendChild(text);
	this.reviewedParagraph.appendChild(document.createTextNode(' (tick this only if there are no further corrections to make)'));
	this.formFooter.reviewBox.appendChild(this.reviewedParagraph);
	this.formFooter.reviewBox.style.display="block";
};

/**
 * add message indicating that changes made to the sheet are minor and credit should go to the original documenter
 */
SpecimenData.prototype.add_minor_edit_checkbox=function () {
	this.minorEditParagraph=document.createElement('p');
	this.minorEditButton=document.createElement('input');
	this.minorEditButton.type="checkbox";
	this.minorEditParagraph.appendChild(this.minorEditButton);

	var text=document.createElement('span');
	text.appendChild(document.createTextNode('minor edit (or no changes needed)'));
	text.style.fontWeight="bold";

	this.minorEditParagraph.appendChild(text);
	//this.minorEditParagraph.appendChild(document.createTextNode(' (if ticked then sheet will be credited to original worker)'));
	this.formFooter.reviewBox.appendChild(this.minorEditParagraph);
	this.formFooter.reviewBox.style.display="block";
};

/**
 * add message indicating that sheet has been partially documented
 */
SpecimenData.prototype.add_partial_message=function () {
	this.reviewMessage=document.createElement('p');
	this.reviewMessage.appendChild(document.createTextNode('This sheet has already been partially documented by another user.'));
	this.reviewMessage.style.fontWeight="bold";
	this.formContainer.appendChild(this.reviewMessage);
	
	this.add_minor_edit_checkbox();
};

/**
 * add and display filing note, (used to indicate anomalies or to informally list a previous taxon name - though ideally previous names should be formally designated in the db athomesheet.previoustaxon column
 * @param {string} message
 */
SpecimenData.prototype.add_file_note=function (message) {
	this.fileMessage=document.createElement('p');
	this.fileMessage.appendChild(document.createTextNode('filing note: '+message));
	this.formContainer.appendChild(this.fileMessage);
};

/**
 * @return textual description of save/review status
 */
SpecimenData.prototype.get_doc_state=function () {
	if (this.modified) {return "modified";}
	else if (!this.documented) {return "undocumented";}
	else if (this.review=="peer" && !this.reviewed) {return "awaiting peer review";}
	else if (this.reviewed) {return "reviewed";}
	return '';
};

/**
 * add and display taxon warning message, typically encouraging user to check / ammend currently specified taxon name
 * @param {string} message
 */
SpecimenData.prototype.add_taxon_warning=function (message) {
	this.taxonMessage=document.createElement('p');
	this.taxonMessage.appendChild(document.createTextNode(message));
	this.formContainer.appendChild(this.taxonMessage);
};

/**
 * click handler called when add form (green cross) button clicked
 */
SpecimenData.prototype.add_form_click=function (event,element,param) {
	var form=new SpecimenForm(this,this.manifestName);
	form.international=this.international;
	form.showAccField=this.showAccField;
	form.showProvenanceField=this.showProvenanceField;
	
	form.create_rows();
	
	form.set_default_taxon(this.defaultTaxonName,this.defaultTaxonId,this.defaultTaxonAuthority,this.defaultTaxonCommonName,this.previousTaxonName,this.previousTaxonId,this.previousTaxonAuthority,this.previousTaxonCommonName);
	form.populate_default();
	this.add_form(form);
	this.refresh_specimen_labels();
	this.set_modified(true);
};

/**
 * return number of forms currently displayed
 * implemented by counting formContainer's child fieldset elements
 */
SpecimenData.prototype.count_forms=function () {
	var c=0;
	var divs=this.formContainer.getElementsByTagName('DIV');
	
	for (var i=0, l=divs.length;i<l;i++) {
		if (divs[i].className==='formbase') {c++;}
	}
	return c;
};

/**
 * get html summary of specimen
 * @return array of html fragments (one entry per form)
 */
SpecimenData.prototype.get_html_array = function () {
	var list=[];
	for (var formIndex in this.forms) {
		list.push(this.forms[formIndex].get_html());
	}
	return list;
};

/**
 * destructor
 */
SpecimenData.prototype.destroy = function () {
	if (this.saveHandle) {this.saveHandle=remove_event_handler(this.saveHandle);}
	if (this.partialSaveHandle) {this.partialSaveHandle=remove_event_handler(this.partialSaveHandle);}

	//delete all forms
	for (var n in this.forms) {
		this.forms[n].formBase.parentNode.removeChild(this.forms[n].formBase); // delete current row
		this.forms[n].destroy();
		delete this.forms[n];
	}
	this.forms=null;
	
	this.comments.destroy();
	this.comments=null;

	//delete images
	if (this.imageList) {
		this.imageContainer.removeChild(this.imageList);
		this.imageList=null;
		
		for (var n in this.imageListClickHandles) {
			this.imageListClickHandles[n]=remove_event_handler(this.imageListClickHandles[n]);
		}
		this.imageListClickHandles=null;
	}
	
	if (this.image.base) {this.imageContainer.removeChild(this.image.base);}
	this.imageContainer.parentNode.removeChild(this.imageContainer);
	
	var n=this.images.length;
	
	for (var i=0; i<n; i++) {
		this.images[i].infoContainer.destroy();
		this.images[i].infoContainer=null;
		this.images[i].destroy();
	}
	
	if (this.reviewMessage && this.reviewMessage.parentNode) {
		if (this.reviewedButton && this.reviewedButton.parentNode) {
			this.reviewedButton.parentNode.removeChild(this.reviewedButton);
		}
		
		if (this.minorEditButton && this.minorEditButton.parentNode) {
			this.minorEditButton.parentNode.removeChild(this.minorEditButton);
		}
		
		this.reviewedButton = null;
		this.minorEditButton = null;
		this.reviewedParagraph = null;
		this.formContainer.removeChild(this.reviewMessage);
	}
	this.reviewMessage= null;
	
	if (this.fileMessage && this.fileMessage.parentNode) {
		this.formContainer.removeChild(this.fileMessage);
	}
	this.fileMessage=null;
	
	if (this.taxonMessage && this.taxonMessage.parentNode) {
		this.formContainer.removeChild(this.taxonMessage);
	}
	this.taxonMessage=null;
	
	this.images=null;	
	this.image=null;
	this.info=null;
	this.imageContainer=null;

	//misc
	this.parentSheet=null; //posible circular ref

	//this.formContainer.parentNode.removeChild(this.formContainer);
	this.formContainer=null;

	this.formFooter.destroy();
	this.formFooter=null;
	this.formAdmin=null;
	
	this.institutionName=null;
	this.institutionAbbr=null;
	this.institutionId=null;
};

/**
 * represents the sheet's header controls
 * @constructor
 */
function FormAdmin() {
	this.skip=document.createElement("button");
	this.skip.appendChild(document.createTextNode('Skip this sheet'));
	this.skip.setAttribute('type','button');
	this.skip.style.marginLeft="1em";

	this.illegible=document.createElement("button");
	this.illegible.appendChild(document.createTextNode('Can\'t read label'));
	this.illegible.setAttribute('type','button');
	this.illegible.setAttribute('title','Use this option if the label is illegible or obscured.');
	
	this.noInfo=document.createElement("button");
	this.noInfo.appendChild(document.createTextNode('No information'));
	this.noInfo.setAttribute('type','button');
	this.noInfo.setAttribute('title','Use this option if the sheet is unlabelled.');

	this.faulty=document.createElement("button");
	this.faulty.appendChild(document.createTextNode('Photo is faulty'));
	this.faulty.setAttribute('type','button');
	this.faulty.setAttribute('title','Please click if the image is blurred or a critical part of the sheet has been cropped.');
}

/**
 * container for form footer controls and warning message
 * @constructor
 */
function FormFooter() {
	this.container=document.createElement("div");
	this.container.id=uniqueId("ff");

	this.reviewBox=document.createElement("div");
	this.reviewBox.style.display="none";
	this.container.appendChild(this.reviewBox);

	this.warningBox=document.createElement("div");
	var warnP=document.createElement("p");
	warnP.appendChild(document.createTextNode('Please check the form for possible errors before saving.'));
	this.warningBox.appendChild(warnP);
	this.container.appendChild(this.warningBox);
	this.warningBox.className="noWarn"; //warnSave

	this.footerButtonContainer=document.createElement('span');
	this.save=document.createElement("button");
	this.save.title='save completed work';
	this.save.style.display="inline";
	this.save.appendChild(document.createTextNode('Save'));
	
	this.partialSave=document.createElement("button");
	this.partialSave.style.display="inline";
	this.partialSave.title='send back partially completed, for reissue to another user';
	this.partialSave.appendChild(document.createTextNode('send back partially completed'));
	
	this.skip=document.createElement("button");
	this.skip.title='skip this sheet without saving';
	this.skip.style.display="inline";
	this.skip.appendChild(document.createTextNode('skip'));
	
	this.footerButtonContainer.appendChild(this.save);
	
	if (true) {
		this.footerButtonContainer.appendChild(document.createTextNode(', '));
		this.footerButtonContainer.appendChild(this.partialSave);
	}
	
	this.footerButtonContainer.appendChild(document.createTextNode(' or '));
	this.footerButtonContainer.appendChild(this.skip);
	this.warningBox.appendChild(this.footerButtonContainer);
	
	this.regFormPlaceholder=this.container.appendChild(document.createElement("div"));

	this.status=this.container.appendChild(document.createElement("a"));
	this.status.target="_blank";
	this.status.style.display="block";
	this.status.style.fontSize="0.8em";
	this.status.appendChild(document.createTextNode('status'));
	
	this.extendedHelp=this.container.appendChild(document.createElement("a"));
	this.extendedHelp.href='#';
	this.extendedHelpLinkText=this.extendedHelp.appendChild(document.createTextNode(''));
	this.institutionText=this.container.appendChild(document.createElement("div"));
}

/**
 * updates footer to show save buttons or registration/login form
 * configure extended help (bubble help) toggle link
 * @param {SheetPage} sheetPage
 */
FormFooter.prototype.apply_state = function(sheetPage) {
	if (tmpUser) {
		registrationForm.parentSheet=sheetPage;
		this.footerButtonContainer.style.display="none";
		this.regFormPlaceholder.appendChild(registrationForm.container);
	} else {
		this.footerButtonContainer.style.display="block";
	}
	
	this.extendedHelpLinkText.data=(globalShowExpandedHelp?'Hide':'Show')+' extended help text';
};

/**
 * displays institution specific blurb at bottom of doc form window
 * @param {string} institution abbreviated name used as key to look up message 
 */
FormFooter.prototype.set_institution_text = function(institution) {
	if (globalMessages[institution]) {
		this.institutionText.appendChild(globalMessages[institution].cloneNode(true));
	}
};

/**
 * append sheet-specific help text
 * @param {string} text
 */
FormFooter.prototype.set_sheet_help_text = function(text) {
	if (text!=='') {
		var p=document.createElement('P');
		p.appendChild(document.createTextNode(text));
		this.institutionText.appendChild(p);
	}
};

/**
 * set css style of warningBox to reflect error state
 * @param {bool} true to show warning
 * @todo support varying levels of warning severity
 */
FormFooter.prototype.show_warning = function (state) {
	if (state) {
		this.warningBox.className="warnSave";
	} else {
		this.warningBox.className="noWarn";
	}
};

/**
 * destructor
 */
FormFooter.prototype.destroy = function () {
	this.warningBox=null;
	this.container=null;
	this.extendedHelp=null;
	this.extendedHelpLinkText=null;
	this.institutionText=null;
	this.regFormPlaceholder=null;
	this.reviewBox=null;
	this.save=null;
	this.partialSave=null;
	this.skip=null;
	this.footerButtonContainer=null;
	this.status=null;
};

/**
 * represents a sheet image, its zoom controls and current state
 * @constructor
 * @param {xmlimage} imageXML
 */
function SpecimenImage(imageXML) {
	this.imageXML=imageXML;
	this.leafName=imageXML.getAttribute('leaf');
	this.sourceInitialWidth=parseInt(imageXML.getAttribute('width'),10);
	this.sourceInitialHeight=parseInt(imageXML.getAttribute('height'),10);
	
	this.path=imageXML.getAttribute('path'); // i.e. 'sheets/INST/imgcache/N' (previously 'sheets/INST')
	// N.B. this is still *not* a complete path, need to append leafname (which changes when sheet is rotated)
	this.host=imageXML.getAttribute('host');
	
	this.sourceXTileNumber=this.xTileNumber=parseInt(imageXML.getAttribute('xtiles'),10); // default 8 tiles wide
	this.sourceYTileNumber=this.yTileNumber=parseInt(imageXML.getAttribute('ytiles'),10); // default 12 tiles high
	
	this.originalHost=this.host; // backup copy of host (as if img is rotated then host is changed to static.herbariumathome.org)
	this.dynamicHost='static.herbariumathome.org'; // host used for rotated images created on-the-fly
	
	this.initialWidth=this.sourceInitialWidth; // 'initial' dimensions are subject to exchange when 90 rotated, ('sourceInitial' aren't)
	this.initialHeight=this.sourceInitialHeight;
	
	this.navContainerWidth=192;
	this.navContainerHeight=Math.floor((this.navContainerWidth/this.initialWidth)*this.initialHeight);
	
	this.base=document.createElement("div");
	this.base.id='imgbase';
	this.base.style.MozUserSelect="none";

	this.base.style.position="relative"; // act as positioned element, so as to accomodate children
	this.base.style.zIndex=1;
	
	SpecimenImage.pinned=true;
	SpecimenImage.set_viewport_size(this.navContainerWidth);
	
	this.base.style.width=SpecimenImage.viewportWidth+"px";
	this.base.style.height=SpecimenImage.viewportHeight+"px";
	this.base.style.backgroundColor="#fcfcfc";
	this.base.style.textAlign="center";
	this.base.style.overflow="hidden";
	this.base.specImg=this;
	
	this.tileWidth=Math.floor(this.initialWidth/this.xTileNumber);
	this.tileHeight=Math.floor(this.initialHeight/this.yTileNumber);

	this.loadMessage=document.createElement('p');
	this.loadMessage.appendChild(document.createTextNode('Loading...'));

	this.orientation='';

	this.image=document.createElement("img");
	this.imageLoadHandle=register_event_handler(this.image,'load',this,'img_load_handler','');
	
	this.image.width=SpecimenImage.viewportWidth;
	this.image.height=Math.floor(this.initialHeight*(SpecimenImage.viewportWidth/this.initialWidth));

	this.imageWidth=SpecimenImage.viewportWidth;
	this.imageHeight=Math.floor(this.initialHeight*(SpecimenImage.viewportWidth/this.initialWidth));

	this.xZoomStep=Math.ceil(this.initialWidth/8);
	this.yZoomStep=Math.ceil(this.initialHeight/8);

	this.image.style.position='absolute';

	this.imgLeft=0; //integer representation of image left;
	
	this.startPosition=imageXML.getAttribute('origin');
	if (this.startPosition=='top') {
		this.imgTop=0;
	} else {
		this.imgTop=this.navContainerHeight-this.imageHeight; //integer representation of image top
	}

	this.tile=[];

	this.overlay=document.createElement("img");
	this.overlay.style.position='absolute';
	this.overlay.style.zIndex=2;
	this.overlay.style.backgroundColor='transparent';
	this.overlay.style.width=this.imageWidth+"px";
	this.overlay.style.height=this.imageHeight+"px";
	this.overlay.style.left=this.image.style.left;
	this.overlay.style.top=this.image.style.top;
	this.overlay.src='http://static.herbariumathome.org/pixel.gif';
	this.overlay.className='zoomable';

	if (this.loadMessage) {this.base.appendChild(this.loadMessage);} // need to check existence to avoid destruction race condition
	this.base.appendChild(this.image);
	this.base.appendChild(this.overlay);

	this.mouseDownHandle=register_event_handler(this.overlay,'mousedown',this,'mouse_down_handler','');

	this.pinButton=document.createElement("a");
	this.pinButton.className='pin';
	this.pinButton.appendChild(document.createElement('img'));
	this.pinButton.setAttribute('title',SpecimenImage.pinDetachMsg);
	this.set_pin_button(true);
	this.base.appendChild(this.pinButton);
	this.pinButton.style.position='absolute';
	this.pinButton.style.left='0px';

	this.controlsPosition='left'; // buttons are at the left-hand side of the window
	this.magButton=document.createElement("a");
	this.magButton.className='pin';
	
	this.magButton.appendChild(document.createElement('img'));
	this.magButton.style.left='70px';
	this.magButton.firstChild.src="http://static.herbariumathome.org/magIn.gif";
	this.magButton.setAttribute('title','zoom in');
	this.base.appendChild(this.magButton);
	this.magButtonHandle=register_event_handler(this.magButton,'click',this,'magnifier_click_handler',1);

	this.magOutButton=document.createElement("a");
	this.magOutButton.className='pin';
	
	this.magOutButton.style.left='120px';
	this.magOutButton.appendChild(document.createElement('img'));
	this.magOutButton.firstChild.src="http://static.herbariumathome.org/magOut.gif";
	this.magOutButton.setAttribute('title','zoom out');
	this.base.appendChild(this.magOutButton);
	this.magOutButtonHandle=register_event_handler(this.magOutButton,'click',this,'magnifier_click_handler',-1);

	this.flipYButton=document.createElement("a");
	this.flipYButton.className='pin';
	
	this.flipYButton.style.left='175px';
	this.flipYButton.appendChild(document.createElement('img'));
	this.flipYButton.firstChild.src='http://static.herbariumathome.org/flipY'+iconSuffix+'.png';
	this.flipYButton.setAttribute('title','rotate 180 degrees');
	this.base.appendChild(this.flipYButton);
	this.flipYButtonHandle=register_event_handler(this.flipYButton,'click',this,'flip_rot_click_handler','rev');

	this.rot90Button=document.createElement("a");
	this.rot90Button.className='pin';
	
	this.rot90Button.style.left='215px';
	this.rot90Button.appendChild(document.createElement('img'));
	this.rot90Button.firstChild.src='http://static.herbariumathome.org/rot90'+iconSuffix+'.png';
	this.rot90Button.setAttribute('title','rotate 90 degrees');
	this.base.appendChild(this.rot90Button);
	this.rot90ButtonHandle=register_event_handler(this.rot90Button,'click',this,'flip_rot_click_handler','r90');

	this.navContainer=document.createElement("div");
	this.navContainer.style.position='absolute';
	this.navContainer.style.left=(SpecimenImage.viewportWidth+20)+'px';
	this.navContainer.style.width=this.navContainerWidth+'px';
	this.navContainer.style.height=this.navContainerHeight+'px';
	this.navContainer.style.padding="0";
	this.navContainer.style.margin="0";
	this.navContainer.style.MozUserSelect="none";

	this.infoContainer=new InfoContainer();

	this.navThumb=document.createElement("img");
	this.navContainer.appendChild(this.navThumb);

	this.navCursor=document.createElement("div");
	//this.navCursor.style.MozUserSelect="none";
	
	this.navCursor.style.backgroundImage="url('http://static.herbariumathome.org/shaded."+(standardBrowser||(IEversion>=7)?"png":"gif")+"')";
	this.navCursor.style.backgroundColor="transparent";
	this.navCursor.style.backgroundRepeat="repeat";
	this.navCursor.style.position='absolute';
	this.navContainer.appendChild(this.navCursor);
	
	this.navMouseDownHandle=register_event_handler(this.navCursor,'mousedown',this,'nav_cursor_mouse_down_handler','');

	this.navClickHandle=register_event_handler(this.navContainer,'click',this,'nav_click_handler','');
	this.reattachClickHandle=null;
}

/**
 * width (pixels) of viewport (excluding thumbnail panel),
 * default 600, min 400
 */
SpecimenImage.viewportWidth=600;

/**
 * height (pixels) of viewport (excluding thumbnail panel),
 * default 600
 */
SpecimenImage.viewportHeight=600;

SpecimenImage.detachedWindowWidth=600;
SpecimenImage.detachedWindowMinWidth=400;
SpecimenImage.detachedWindowDefaultWidth=600;
SpecimenImage.detachedWindowHeight=600;
SpecimenImage.detachedWindowMinHeight=200;
SpecimenImage.detachedWindowDefaultHeight=600;
SpecimenImage.detachedWindowX=null;
SpecimenImage.detachedWindowY=null;

/**
 * trigger loading of main low res sheet image and navigation thumbnail
 * set image positions
 * register resize handler
 */
SpecimenImage.prototype.set_sheet_images = function () {
	if (!SpecimenImage.pinned) {
		this.base.style.display='none'; // hide attached view while populating
		this.infoContainer.div.style.display='none';
		this.navContainer.style.display='none';
	}
	
	this.navThumb.src="http://"+this.host+"/"+this.path+"/"+this.leafName+this.orientation+"/thumb_"+this.leafName+this.orientation+".jpg";
	this.image.src="http://"+this.host+"/"+this.path+"/"+this.leafName+this.orientation+"/low_"+this.leafName+this.orientation+".jpg";
	this.resize_handler();
	this.adjust_position();
	this.register_resize_handler();
};

/**
 * register resize handler
 * called to either intially register the handler, or to reregister it following image reattachment
 */
SpecimenImage.prototype.register_resize_handler = function () {
	if (!this.onResizeHandle) {this.onResizeHandle=register_event_handler(window,'resize',this,'resize_handler','');}
};

/**
 * called following window resize
 */
SpecimenImage.prototype.resize_handler = function () {
	if (this.inResize) {return;}
	this.inResize=true;
	
	SpecimenImage.set_viewport_size(this.navContainerWidth);
	
	this.image.height=SpecimenImage.viewportHeight;
	this.base.style.height=SpecimenImage.viewportHeight+"px";
	
	if (this.image.width!=SpecimenImage.viewportWidth) {
		this.image.width=SpecimenImage.viewportWidth;
		this.base.style.width=SpecimenImage.viewportWidth+"px";
		
		if (SpecimenImage.pinned) {
			this.navContainer.style.left=(SpecimenImage.viewportWidth+20)+'px';
			this.infoContainer.set_position();
		} else {
			this.navContainer.style.left=(SpecimenImage.viewportWidth+15)+'px';
			document.getElementById('infocontainer').style.left=(SpecimenImage.viewportWidth+15)+'px'; // must be referenced by id rather via object hierachy to avoid crashing IE7
		}
	}

	this.adjust_position();
	this.inResize=false;
};

/**
 * remove images (hopefully memory saving)
 */
SpecimenImage.prototype.clear_sheet_images = function () {
	this.navThumb.src="";
	this.image.src="";
	
	for (var n in this.tile) {
		if (this.tile[n] && this.tile[n].style) {
			this.tile[n].quality=null;
			this.tile[n].parentNode.removeChild(this.tile[n]);
			this.tile[n]=null;
		}
	}
	
	if (this.onResizeHandle) {this.onResizeHandle=remove_event_handler(this.onResizeHandle);}
	if (!SpecimenImage.pinned) {this.hide_detached_window();}
};

/**
 * called to initialize container and following window resizing
 * (static method)
 */
SpecimenImage.set_viewport_size =function (navContainerWidth) {
	if (SpecimenImage.pinned) {
		SpecimenImage.viewportWidth=Math.floor(document.body.clientWidth*(2/3));
		if (SpecimenImage.viewportWidth < 550) { SpecimenImage.viewportWidth=550;}
		SpecimenImage.viewportHeight=600;
	} else {
		SpecimenImage.viewportWidth=document.body.clientWidth-(navContainerWidth+30); // half usual border width (30 not 60)
		if (SpecimenImage.viewportWidth < 400) { SpecimenImage.viewportWidth=400;}
		
		SpecimenImage.viewportHeight=(window.innerHeight)?window.innerHeight:document.documentElement.clientHeight; // cope with IE inconsistency
	}
};

/**
 * destructor
 */
SpecimenImage.prototype.destroy =function() {
	if (this.mouseDownHandle !==null) {this.mouseDownHandle=remove_event_handler(this.mouseDownHandle);}
	if (this.magButtonHandle !==null) {this.magButtonHandle=remove_event_handler(this.magButtonHandle);}
	if (this.magOutButtonHandle !==null) {this.magOutButtonHandle=remove_event_handler(this.magOutButtonHandle);}
	if (this.flipYButtonHandle !==null) {this.flipYButtonHandle=remove_event_handler(this.flipYButtonHandle);}
	if (this.rot90ButtonHandle !==null) {this.rot90ButtonHandle=remove_event_handler(this.rot90ButtonHandle);}
	if (this.navClickHandle !==null) {this.navClickHandle=remove_event_handler(this.navClickHandle);}
	if (this.navMouseDownHandle !==null) {this.navMouseDownHandle=remove_event_handler(this.navMouseDownHandle);}
	if (this.imageLoadHandle!==null) {this.imageLoadHandle=remove_event_handler(this.imageLoadHandle);}
	if (this.reattachClickHandle !==null) {this.reattachClickHandle=remove_event_handler(this.reattachClickHandle);}
	if (this.onResizeHandle) {this.onResizeHandle=remove_event_handler(this.onResizeHandle);}
	if (this.detachedImageWindowUnloadHandle) {this.detachedImageWindowUnloadHandle=remove_event_handler(this.detachedImageWindowUnloadHandle);}

	if (this.imageWindow) {this.hide_detached_window();}

	this.base.id=null;
	this.base.specImg=null;

	this.remove_move_handlers();
	if (this.rotatedImgOnloadHandle) {this.rotatedImgOnloadHandle=remove_event_handler(this.rotatedImgOnloadHandle);}

	if (this.pinHandler !==null) {this.pinHandler=remove_event_handler(this.pinHandler);}

	for (var n in this.tile) {
		if (this.tile[n] && this.tile[n].style) {
			this.tile[n].quality=null;
			this.tile[n].parentNode.removeChild(this.tile[n]);
			this.tile[n]=null;
		}
	}
	this.tile=null;

	this.loadMessage=null;
	this.pinButton.parentNode.removeChild(this.pinButton);
	this.pinButton=null;
	this.pinMessage=null;
	this.repinButton=null;
	this.magButton.parentNode.removeChild(this.magButton);
	this.magButton=null;
	this.magOutButton.parentNode.removeChild(this.magOutButton);
	this.magOutButton=null;
	this.flipYButton.parentNode.removeChild(this.flipYButton);
	this.flipYButton=null;
	this.rot90Button.parentNode.removeChild(this.rot90Button);
	this.rot90Button=null;
	this.base.removeChild(this.overlay);
	this.overlay=null;
	this.base.removeChild(this.image);
	this.image=null;
	if (this.base.parentNode) {this.base.parentNode.removeChild(this.base);}
	this.base=null;
	this.navCursor=null;
	this.navContainer=null;
	this.imageXML=null;
};

/**
 * clear image loading message once load complete
 */
SpecimenImage.prototype.img_load_handler=function (event,element,param) {
	if (this.loadMessage) {this.loadMessage.parentNode.removeChild(this.loadMessage);}
	this.loadMessage=null;
	if (this.imageLoadHandle!==null) {this.imageLoadHandle=remove_event_handler(this.imageLoadHandle);}
};

/**
 * update state of pinned image button and register event handler
 * @param {bool} state true if pinned to doc form
 */
SpecimenImage.prototype.set_pin_button=function (state) {
	this.pinButton.firstChild.src=(state)?"http://static.herbariumathome.org/pinin.gif":"http://static.herbariumathome.org/pinout.gif";
	this.pinButton.setAttribute('title',(state)?SpecimenImage.pinDetachMsg:SpecimenImage.pinReattachMsg);

	SpecimenImage.pinned=state;
	if (!this.pinHandler) {this.pinHandler=register_event_handler(this.pinButton,'click',this,'pin_click_handler','');}
};

/**
 * called when pin clicked to detach or reattach specimen from doc form
 * detachment involves opening and populating a new window
 * reattachment only involves closing the popup window as the remaining reattachment work is done by a separate onclose handler.
 */
SpecimenImage.prototype.pin_click_handler=function (event,element,param) {

	if (event.button<2) {
		if (SpecimenImage.pinned) {
			// currently attached to specimen window so detach to new window

			// don't allow event to bubble
			if (event.stopPropagation) {event.stopPropagation();} // DOM 2
			else {event.cancelBubble=true;} // IE

			// stop any default action
			if (event.preventDefault) {event.preventDefault();} // DOM 2
			else {event.returnValue=false;} // IE
			
			SpecimenImage.pinned=false;
			this.open_detached_window();
		} else {
			// read current window location
			if (window.screenX) {
				window.opener.SpecimenImage.detachedWindowX=window.screenX;
				window.opener.SpecimenImage.detachedWindowY=window.screenY;
			} else {
				window.opener.SpecimenImage.detachedWindowX=window.screenLeft;
				window.opener.SpecimenImage.detachedWindowY=window.screenTop;
			}
			
			window.close();
		}
	}
};

SpecimenImage.pinReattachMsg="Reattach specimen image window.";
SpecimenImage.pinDetachMsg="Detach image, to view in a new window";

/**
 * open (or reopen) and populate detached specimen window
 */
SpecimenImage.prototype.open_detached_window=function () {
	SpecimenImage.doNotRepinImage=false; // flag affects subsequent repin activity
	this.base.style.display='none'; // hide attached view
	this.infoContainer.div.style.display='none';
	this.navContainer.style.display='none';
	
	// remove resize event handler from parent, to prevent confusion from spurious resize events firing
	if (this.onResizeHandle) {this.onResizeHandle=remove_event_handler(this.onResizeHandle);}
	
	if (!this.pinMessage) {
		// this sheet wasn't previously unpinned
		
		this.pinMessage=document.createElement('div'); // message box about repinning specimen
		this.repinButton=document.createElement("button");
		this.repinButton.className='repin';
		this.repinButton.setAttribute('title',SpecimenImage.pinReattachMsg);
		this.repinButton.appendChild(document.createElement('img'));
		this.repinButton.firstChild.src="http://static.herbariumathome.org/pinout.gif";
		this.reattachClickHandle=register_event_handler(this.repinButton,'click',this,'reattach_click_handler','');

		this.pinMessage.appendChild(this.repinButton);
		//this.repinButton.appendChild(document.createTextNode('reattach specimen window'));
		this.base.parentNode.insertBefore(this.pinMessage,this.base);
	}
	
	if (SpecimenImage.detachedWindowX ==null) {
		// look for cookie
		var cookie=cookie_read_value('detachedparam');
		
		if (cookie !==null) {
			var param=cookie.split(',',4); // expect x,y,width,height
				
			SpecimenImage.detachedWindowX=param[0];
			SpecimenImage.detachedWindowY=param[1];
			SpecimenImage.detachedWindowWidth=param[2];
			SpecimenImage.detachedWindowHeight=param[3];
		}
	}
	
	var position=this.detached_window_position_string();
	
	this.imageWindow=window.open('','','width='+SpecimenImage.detachedWindowWidth+',height='+SpecimenImage.detachedWindowHeight+',resizable=yes,scrollbars=no,status=no,toolbar=no,menubar=no'+position);

	this.imageWindow.document.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"><html><head><title>Herbarium sheet</title><base href="'+scriptUrl+'"><link rel="STYLESHEET" href="'+scriptUrl+'directory.css" type="text/css"><link rel="STYLESHEET" href="'+scriptUrl+'atHome.01.css?f" type="text/css"><script type="text/javascript">var scriptUrl="'+scriptUrl+'";</script></head><body onload="init_detached_page()"><p id="detachedloader">Loading... please wait</p><SCRIPT type="text/javascript" charset="UTF-8" src="'+scriptUrl+'atHome.'+globalVersion+'.js"></SCRIPT><script type="text/javascript" src="'+scriptUrl+'atHomeSpecimen.002x.js"></script></body></html>');
	this.imageWindow.document.close();
};

/**
 * validate window position and width
 * @return string to pass position info to window.open() (or empty string)
 */
SpecimenImage.prototype.detached_window_position_string=function () {
	if ((SpecimenImage.detachedWindowX+SpecimenImage.detachedWindowWidth) < 128 ||
		SpecimenImage.detachedWindowX > window.screen.availWidth ||
		SpecimenImage.detachedWindowWidth < SpecimenImage.detachedWindowMinWidth ||
		SpecimenImage.detachedWindowWidth > window.screen.availWidth ||
		SpecimenImage.detachedWindowY < 0 || 
		SpecimenImage.detachedWindowY > window.screen.availHeight || 
		SpecimenImage.detachedWindowHeight < SpecimenImage.detachedWindowMinHeight ||
		SpecimenImage.detachedWindowHeight > window.screen.availHeight
		) {
		// window dimensions or position not valid
		SpecimenImage.detachedWindowX=null;
		SpecimenImage.detachedWindowY=null;
		SpecimenImage.detachedWindowWidth=SpecimenImage.detachedWindowDefaultWidth;
		SpecimenImage.detachedWindowHeight=SpecimenImage.detachedWindowDefaultHeight;
		
		return "";
	} 
	
	return ',left='+SpecimenImage.detachedWindowX+',screenX='+SpecimenImage.detachedWindowX+',top='+SpecimenImage.detachedWindowY+',screenY='+SpecimenImage.detachedWindowY;
};

/**
 * reattachment only involves closing the popup window as the remaining reattachment work is done by a separate onclose handler.
 */
SpecimenImage.prototype.reattach_click_handler=function (event,element,param) {
	if (event.button<2) {
		// read current window location
		if (this.imageWindow.screenX) {
			SpecimenImage.detachedWindowX=this.imageWindow.screenX;
			SpecimenImage.detachedWindowY=this.imageWindow.screenY;
		} else {
			SpecimenImage.detachedWindowX=this.imageWindow.screenLeft;
			SpecimenImage.detachedWindowY=this.imageWindow.screenTop;
		}
	
		this.imageWindow.close();
		this.imageWindow=null;
	}
};

/**
 * called (in the context of the detached window) when detached specimen window is closed
 * reattaches image to doc form parent
 * saves position and size of detached window
 */
SpecimenImage.prototype.detached_image_window_unload_handler=function (event,element,param) {
	//parentImg=window.opener.document.getElementById("imgbase").specImg;
	
	if (!standardBrowser) {
        document.body.onselectstart = function () { return false; };
    }
	
	try {
		if (window.opener && window.opener.SpecimenImage) {
			var parentImg=document.parentImg.specImg;
			
			// update frame size record
			if (!window.innerWidth) {
				//ie
	        	window.opener.SpecimenImage.detachedWindowWidth = document.body.clientWidth;
	        	window.opener.SpecimenImage.detachedWindowHeight = document.body.clientHeight;
	    	} else {
	    		//mozilla
	    		window.opener.SpecimenImage.detachedWindowWidth = window.innerWidth;
	        	window.opener.SpecimenImage.detachedWindowHeight = window.innerHeight;
	    	}
	    	
			cookie_set_value('detachedparam',window.opener.SpecimenImage.detachedWindowX+","+window.opener.SpecimenImage.detachedWindowY+","+window.opener.SpecimenImage.detachedWindowWidth+","+window.opener.SpecimenImage.detachedWindowHeight,14);
			
			if (!window.opener.SpecimenImage.doNotRepinImage) {
				parentImg.infoContainer.clear();
				parentImg.infoContainer.div=parentImg.infoContainer.div_original;
				parentImg.infoContainer.div_original=null;
				parentImg.infoContainer.document=window.opener.document;
				parentImg.infoContainer.div.style.display="block";
				parentImg.infoContainer.set_top_position();
				
				parentImg.display_attached();
				
				parentImg.imageWidth=this.imageWidth;
				parentImg.imageHeight=this.imageHeight;
				parentImg.imgLeft=this.imgLeft;
				parentImg.imgTop=this.imgTop;
			
				window.opener.SpecimenImage.pinned=true;
				window.opener.SpecimenImage.set_viewport_size(parentImg.navContainerWidth);
				parentImg.resize_handler();
				parentImg.register_resize_handler();
			} else {
				// just save the current cursor position
				parentImg.imageWidth=this.imageWidth;
				parentImg.imageHeight=this.imageHeight;
				parentImg.imgLeft=this.imgLeft;
				parentImg.imgTop=this.imgTop;
			}
			parentImg.imageWindow=null;
		}
	} catch(err) {}
	
	document.parentImg=null;
	this.destroy();
};

/**
 * make visible the image (attached to the doc form)
 * hide any remaining unpinned button
 */
SpecimenImage.prototype.display_attached = function () {
	if (this.pinMessage) {
		this.pinMessage.parentNode.removeChild(this.pinMessage);
		this.pinMessage=null;
		this.repinButton=null;
	}
	
	this.base.style.display='block';
	this.navContainer.style.display='block';
	this.infoContainer.div.style.display='block';
};

/**
 * hide detached specimen window, without invoking repin
 * called when window is currently unpinned and thumb page is about to be displayed instead of specimen window
 */
SpecimenImage.prototype.hide_detached_window=function () {
	SpecimenImage.doNotRepinImage=true;
	
	if (this.imageWindow) {
		// read current window location
		if (this.imageWindow.screenX) {
			SpecimenImage.detachedWindowX=this.imageWindow.screenX;
			SpecimenImage.detachedWindowY=this.imageWindow.screenY;
		} else {
			SpecimenImage.detachedWindowX=this.imageWindow.screenLeft;
			SpecimenImage.detachedWindowY=this.imageWindow.screenTop;
		}
		
		this.imageWindow.close();
		this.imageWindow=null;
	}
};

/**
 * called at start of click or drag on nav cursor
 * registers mouse handlers to follow dragging and release
 */
SpecimenImage.prototype.nav_cursor_mouse_down_handler=function (event,element,param) {
	if (event.button<2) {
		// only respond to left button
		
		if (this.dragging) {
			this.remove_move_handlers();
		} else {
			//register move and up handlers
			this.moveHandler=register_event_handler(document,'mousemove',this,'nav_mouse_move_handler','');
			this.upHandler=register_event_handler(document,'mouseup',this,'nav_mouse_up_handler','');
			this.outHandler=register_event_handler(this.navContainer,'mouseout',this,'nav_drag_mouse_out_handler','');
			
			this.dragging=true;
			
			/*
			// don't allow event to bubble
			if (event.stopPropagation) {event.stopPropagation();} // DOM 2
			else {event.cancelBubble=true;} // IE

			// stop any default action
			if (event.preventDefault) {event.preventDefault();} // DOM 2
			else {event.returnValue=false;} // IE
			*/
		}
	}
};

/**
 * called at start of click or drag on sheet image
 * registers mouse handlers to follow dragging and release
 */
SpecimenImage.prototype.mouse_down_handler=function (event,element,param) {
	if (event.button<2) {
		// only respond to left button
		
		if (this.dragging) {
			this.remove_move_handlers();
		} else {
			// compute distance between upper left corner and mouse click
			this.mouseInitialX=event.clientX; // - parseInt(this.image.style.left);
			this.mouseInitialY=event.clientY; // - parseInt(this.image.style.top);

			this.imageInitialX=parseInt(this.overlay.style.left,10);
			this.imageInitialY=parseInt(this.overlay.style.top,10);

			if (event.layerX) {
				this.imgMouseX=event.layerX;
				this.imgMouseY=event.layerY;
			} else {
				this.imgMouseX=event.offsetX;
				this.imgMouseY=event.offsetY;
			}

			//register move and up handlers
			this.moveHandler=register_event_handler(document,'mousemove',this,'mouse_move_handler','');
			this.upHandler=register_event_handler(document,'mouseup',this,'mouse_up_handler','');
			this.outHandler=register_event_handler(window,'mouseout',this,'img_mouse_out_handler','');

			this.dragging=true;

			// don't allow event to bubble
			if (event.stopPropagation) {event.stopPropagation();} // DOM 2
			else {event.cancelBubble=true;} // IE

			// stop any default action
			if (event.preventDefault) {event.preventDefault();} // DOM 2
			else {event.returnValue=false;} // IE
		}
	}
};

/**
 * stop dragging if mouse outside window bounds
 */
SpecimenImage.prototype.img_mouse_out_handler=function (event,element,param) {
	if (!event.relatedTarget) {
		this.remove_move_handlers();
	}
};

/**
 * stop dragging if mouse outside window bounds
 */
SpecimenImage.prototype.nav_drag_mouse_out_handler=function (event,element,param) {
	this.remove_move_handlers();
};

/**
 * while dragging, update x and y coords
 */
 if (document.implementation.hasFeature("Events", "2.0")) {
 	// DOM 2
	SpecimenImage.prototype.mouse_move_handler=function (event,element,param) {
		this.overlay.className='dragging';
		document.body.style.cursor='move';
	
		this.imgLeft=(this.imageInitialX+event.clientX - this.mouseInitialX);
		this.imgTop=(this.imageInitialY+event.clientY - this.mouseInitialY);
		this.adjust_position();
	
		event.stopPropagation(); // don't allow event to bubble
		event.preventDefault(); // stop any default action
	};
	
	SpecimenImage.prototype.nav_mouse_move_handler=function (event,element,param) {
		var baseWidth=parseInt(this.base.style.width,10);
		var baseHeight=parseInt(this.base.style.height,10);
		var x,y;
		
		//this.overlay.className='dragging';
		document.body.style.cursor='move';
		
		x=event.layerX/this.navContainerWidth;
		y=event.layerY/this.navContainerHeight;
	
		x+=parseInt(this.navCursor.style.left,10)/this.navContainerWidth;
		y+=parseInt(this.navCursor.style.top,10)/this.navContainerHeight;	
	
		this.imgLeft=-(this.imageWidth*x)+(baseWidth/2);
		this.imgTop=-(this.imageHeight*y)+(baseHeight/2);
		
		this.adjust_position();
	
		event.stopPropagation(); // don't allow event to bubble
		event.preventDefault(); // stop any default action
	};
 } else {
 	//IE
 	SpecimenImage.prototype.mouse_move_handler=function (event,element,param) {
 		this.overlay.className='dragging';
		document.body.style.cursor='move';
	
		this.imgLeft=(this.imageInitialX+event.clientX - this.mouseInitialX);
		this.imgTop=(this.imageInitialY+event.clientY - this.mouseInitialY);
		this.adjust_position();
	
		event.cancelBubble=true; // don't allow event to bubble
		event.returnValue=false; // stop any default action
	};
	
	SpecimenImage.prototype.nav_mouse_move_handler=function (event,element,param) {
		var baseWidth=parseInt(this.base.style.width,10);
		var baseHeight=parseInt(this.base.style.height,10);
		var x,y;
		
		this.overlay.className='dragging';
		document.body.style.cursor='move';
			
		x=event.offsetX/this.navContainerWidth;
		y=event.offsetY/this.navContainerHeight;
	
		x+=parseInt(this.navCursor.style.left,10)/this.navContainerWidth;
		y+=parseInt(this.navCursor.style.top,10)/this.navContainerHeight;	
	
		this.imgLeft=-(this.imageWidth*x)+(baseWidth/2);
		this.imgTop=-(this.imageHeight*y)+(baseHeight/2);
		
		this.adjust_position();
	
		event.cancelBubble=true; // don't allow event to bubble
		event.returnValue=false; // stop any default action
	};
 }

/**
 * handle clicks on navigation window (thumbnail panel)
 */
SpecimenImage.prototype.nav_click_handler=function (event,element,param) {
	var baseWidth=parseInt(this.base.style.width,10);
	var baseHeight=parseInt(this.base.style.height,10);
	var x,y;

	if (event.layerX) {
		x=event.layerX/this.navContainerWidth;
		y=event.layerY/this.navContainerHeight;
	} else {
		x=event.offsetX/this.navContainerWidth;
		y=event.offsetY/this.navContainerHeight;
	}

	if ((event.target && event.target==this.navCursor) || (event.srcElement && event.srcElement==this.navCursor)) {
		x+=parseInt(this.navCursor.style.left,10)/this.navContainerWidth;
		y+=parseInt(this.navCursor.style.top,10)/this.navContainerHeight;
	}
	
	if (this.imageWidth===400) {
		//not sure this happens any more, now that image widths are flexible
		
		// image at minimum size, so zoom as well
		this.imageWidth += (this.xZoomStep*3);
		this.imageHeight += (this.yZoomStep*3);
			
	} else if (this.imageWidth<=SpecimenImage.viewportWidth) {
		// image at default starting size, so zoom slightly  as well
		
		this.imageWidth += (this.xZoomStep);
		this.imageHeight += (this.yZoomStep);
	}

	this.imgLeft=-(this.imageWidth*x)+(baseWidth/2);
	this.imgTop=-(this.imageHeight*y)+(baseHeight/2);

	this.adjust_position();
};

/**
 * ensure that the image is not off-screen or detached from edge
 */
SpecimenImage.prototype.adjust_position=function () {
	var baseWidth=parseInt(this.base.style.width,10);
	var baseHeight=parseInt(this.base.style.height,10);

	if (this.imageHeight < baseHeight) {
		this.imageHeight=baseHeight;
		this.imageWidth=Math.floor((this.initialWidth/this.initialHeight)*baseHeight);
	}

	if (this.imgLeft >0) {this.imgLeft=0;}
	if ((this.imgLeft+this.imageWidth) < baseWidth) {this.imgLeft=baseWidth-this.imageWidth;}

	if (this.imgTop >0) {this.imgTop=0;}
	if ((this.imgTop+this.imageHeight) < baseHeight) {this.imgTop=baseHeight-this.imageHeight;}

	if (this.imgTop > -32 && this.scale > 0.40 && ((this.controlsPosition=='left' && this.imgLeft >-32) || (this.controlsPosition=='right' && this.imgLeft < (baseWidth-this.imageWidth+32)))) {
		// switch position of magnifer controls to avoid obscuring image
		
		if (this.controlsPosition=='left') {
			this.controlsPosition='right';
			this.magButton.style.right=this.magButton.style.left;
			this.magButton.style.left="auto";
			this.magOutButton.style.right=this.magOutButton.style.left;
			this.magOutButton.style.left="auto";
			this.flipYButton.style.right=this.flipYButton.style.left;
			this.flipYButton.style.left="auto";
			this.rot90Button.style.right=this.rot90Button.style.left;
			this.rot90Button.style.left="auto";
			this.pinButton.style.right=this.pinButton.style.left;
			this.pinButton.style.left="auto";
		} else {
			this.controlsPosition='left';
			this.magButton.style.left=this.magButton.style.right;
			this.magButton.style.right="auto";
			this.magOutButton.style.left=this.magOutButton.style.right;
			this.magOutButton.style.right="auto";
			this.flipYButton.style.left=this.flipYButton.style.right;
			this.flipYButton.style.right="auto";
			this.rot90Button.style.left=this.rot90Button.style.right;
			this.rot90Button.style.right="auto";
			this.pinButton.style.left=this.pinButton.style.right;
			this.pinButton.style.right="auto";
		}
	}

	var oldScale=this.scale;
	this.scale=this.imageHeight/this.initialHeight;

	if (this.scale <= 0.60) {
		if (oldScale > 0.60) {this.image.style.display="block";}

		this.image.style.left=this.imgLeft+'px';
		this.image.style.top=this.imgTop+'px';
		this.image.style.width=this.imageWidth+'px';
		this.image.style.height=this.imageHeight+'px';
	}
	else if (oldScale <= 0.60) {this.image.style.display="none";}

	this.check_tiles();

	this.overlay.style.left=this.imgLeft+'px';
	this.overlay.style.top=this.imgTop+'px';

	this.overlay.style.width=this.imageWidth+'px';
	this.overlay.style.height=this.imageHeight+'px';


	if (baseWidth<=this.imageWidth) {
		this.navCursor.style.width=(this.navContainerWidth*(baseWidth/this.imageWidth))+'px';
		this.navCursor.style.left=((-this.imgLeft/this.imageWidth)*this.navContainerWidth)+'px';
	} else {
		this.navCursor.style.width=this.navContainerWidth+'px';
		this.navCursor.style.left='0';
	}

	this.navCursor.style.height=(this.navContainerHeight*(baseHeight/this.imageHeight))+'px';
	this.navCursor.style.top=((-this.imgTop/this.imageHeight)*this.navContainerHeight)+'px';
};

/**
 * create/delete/reposition image hi-res tiles
 *
 * tiles form xTileNumber x yTileNumber grid
 * show tiles if scale > 0.5
 * @todo currently poorly optimised
 */
SpecimenImage.prototype.check_tiles=function () {
	var x,y,leftPos,tile;

	var lxTile=Math.floor(Math.abs(this.imgLeft/this.scale)/this.tileWidth);
	var lyTile=Math.floor(Math.abs(this.imgTop/this.scale)/this.tileHeight);

	var hxTile=Math.round((((-this.imgLeft+parseInt(this.base.style.width,10))/this.scale))/this.tileWidth);
	if (hxTile >= this.xTileNumber) {hxTile=this.xTileNumber-1;}

	var hyTile=Math.round((((-this.imgTop+parseInt(this.base.style.height,10))/this.scale))/this.tileHeight);
	if (hyTile >= this.yTileNumber) {hyTile=this.yTileNumber-1;}

	//if (this.scale >= 1.2) var quality=100;
	//else var quality=80;
	var quality=80;

	if (this.scale > 0.50) {
		
		var xScale256=Math.floor(this.scale*this.tileWidth);
		var yScale256=Math.floor(this.scale*this.tileHeight);
		var tileURL='http://'+this.host+'/'+this.path+'/'+this.leafName+this.orientation+'/tiles/';

		globalPauseImgCache=true; //stop background image caching
		
		// hide offscreen low numbered rows
		for (var n=(lxTile*this.yTileNumber)-1; n>=0; n--) {
			if (this.tile[n]) {this.tile[n].style.display='none';}
		}
		
		// hide offscreen high numbered rows
		for (n=(this.xTileNumber*this.yTileNumber)-1; n>=((hxTile+1)*this.yTileNumber); n--) {
			if (this.tile[n]) {this.tile[n].style.display='none';}
		}
		
		for (x=hxTile; x>=lxTile;x--) {	
			leftPos=((x*xScale256)+this.imgLeft)+'px';
			for (y=this.yTileNumber-1; y>=0; y--) {
				if (y<lyTile || y>hyTile) {
					if (this.tile[(x*this.yTileNumber)+y]) {this.tile[(x*this.yTileNumber)+y].style.display='none';}
				} else {
					if ((tile=this.tile[(x*this.yTileNumber)+y])) {
						tile.style.left=leftPos;
						tile.style.top=((y*yScale256)+this.imgTop)+'px';
						tile.width=xScale256;
						tile.height=yScale256;
		
						/* KEEP THIS - MAY NEED LATER IF REINSTATE QUALITY LEVELS
						 * if (this.tile[(x*this.yTileNumber)+y].quality < quality) {
							this.tile[(x*this.yTileNumber)+y].src=tileURL+Math.floor(x*this.tileWidth)+'_'+Math.floor(y*this.tileHeight)+'_'+this.tileWidth+'_'+this.tileHeight+'_'+quality+'.jpg';
							this.tile[(x*this.yTileNumber)+y].quality=quality;
						} */
		
						tile.style.display='block';
					} else {
						tile=document.createElement('img');
						tile.alt='';
						tile.style.margin="0";
						tile.style.padding="0";
						tile.style.border="none";
						tile.src=tileURL+Math.floor(x*this.tileWidth)+'_'+Math.floor(y*this.tileHeight)+'_'+this.tileWidth+'_'+this.tileHeight+'_'+quality+'.jpg';
						tile.style.position='absolute';
						tile.style.display='block';
						tile.style.left=leftPos;
						tile.style.top=((y*yScale256)+this.imgTop)+'px';
						tile.width=xScale256;
						tile.height=yScale256;
						
						this.tile[(x*this.yTileNumber)+y]=this.base.appendChild(tile);
						/* KEEP THIS - MAY NEED LATER IF REINSTATE QUALITY LEVELS
						 * this.tile[(x*this.yTileNumber)+y].quality=quality;
						 */
					}
				}	
			}
		}
		
		globalPauseImgCache=false; //restart background image caching
	} else {
		// not displaying tiles
		for (var n in this.tile) {
			if (this.tile[n] && this.tile[n].style) {this.tile[n].style.display='none';}
		}
	}
};

/**
 * handler for mouse up events (either part of click or end of drag) on main sheet image view
 */
SpecimenImage.prototype.mouse_up_handler=function (event,element,param) {
	// check if was click rather than drag in which case zoom (if spec not zoomed (i.e. width is default) then alays zoom)
	if ((this.imageWidth===400) || ((Math.abs(event.clientX - this.mouseInitialX) < 16) && (Math.abs(event.clientY - this.mouseInitialY) < 16))) {

		// centre the image on the click
		// first calculate document X and Y

		if (event.altKey) {
			var xPointerStep=-this.xZoomStep*(this.imgMouseX/this.imageWidth);
			var yPointerStep=-this.yZoomStep*(this.imgMouseY/this.imageHeight);

			this.imageWidth -= this.xZoomStep;
			this.imageHeight -= this.yZoomStep;
		} else {
			if (this.imageWidth < 2500) {
				var xPointerStep=this.xZoomStep*(this.imgMouseX/this.imageWidth);
				var yPointerStep=this.yZoomStep*(this.imgMouseY/this.imageHeight);

				this.imageWidth += this.xZoomStep;
				this.imageHeight += this.yZoomStep;
			} else {
				var xPointerStep=this.imgMouseX/this.imageWidth;
				var yPointerStep=this.imgMouseY/this.imageHeight;
			}
		}

		this.imgLeft=Math.floor((parseInt(this.base.style.width,10)/2)-this.imgMouseX-xPointerStep);
		this.imgTop=Math.floor((parseInt(this.base.style.height,10)/2)-this.imgMouseY-yPointerStep);

		this.adjust_position();
	}

	// don't allow event to bubble
	if (event.stopPropagation) {event.stopPropagation();} // DOM 2
	else {event.cancelBubble=true;} // IE

	this.remove_move_handlers();
};

/**
 * 
 */
SpecimenImage.prototype.nav_mouse_up_handler=function (event,element) {
	// don't allow event to bubble
	if (event.stopPropagation) {event.stopPropagation();} // DOM 2
	else {event.cancelBubble=true;} // IE

	this.remove_move_handlers();
};

/**
 * magnifier buttons click handler
 * @param event
 * @param element
 * @param {int} direction 1 is zoom in -1 is zoom out
 */
SpecimenImage.prototype.magnifier_click_handler=function (event,element,direction) {
	if ((direction ===1 && this.imageWidth >= 2500) || (direction ===-1 && this.imageWidth <= 400)) {
		return;
	}

	this.imageWidth += this.xZoomStep*direction;
	this.imageHeight += this.yZoomStep*direction;

	this.imgLeft -=(this.xZoomStep/2)*direction;
	this.imgTop -=(this.yZoomStep/2)*direction;

	this.adjust_position();
};

/**
 * exchange image dimensions (used for 90 degree rotation)
 */
SpecimenImage.prototype.swap_x_y =function () {
	var t=this.imageWidth;
	this.imageWidth=this.imageHeight;
	this.imageHeight=t;
	
	t=this.xZoomStep;
	this.xZoomStep=this.yZoomStep;
	this.yZoomStep=t;
	
	t=this.navContainerWidth;
	this.navContainerWidth=this.navContainerHeight;
	this.navContainerHeight=t;
	
	this.navContainer.style.width=this.navContainerWidth+"px";
	this.navContainer.style.height=this.navContainerHeight+"px";
	
	t=this.xTileNumber;
	this.xTileNumber=this.yTileNumber;
	this.yTileNumber=t;
	
	t=this.initialWidth;
	this.initialWidth=this.initialHeight;
	this.initialHeight=t;
	
	t=this.tileWidth;
	this.tileWidth=this.tileHeight;
	this.tileHeight=t;
	
	t=this.base.style.width;
	this.base.style.width=this.base.style.height;
	this.base.style.height=t;
	
	t=this.overlay.style.width;
	this.overlay.style.width=this.overlay.style.height;
	this.overlay.style.height=t;
	
	this.adjust_position();
};

/**
 * rotation button click handler
 * @param event
 * @param element
 * @param {string} angle 'rev' for 180, 'r90' for 90 degree 
 */
SpecimenImage.prototype.flip_rot_click_handler=function (event,element,angle) {	
	if (angle=='rev' && this.orientation != 'rev') {
		 if (this.orientation=='r90') {
		 	this.swap_x_y();
		 }
		 this.orientation='rev';
		 this.host=this.dynamicHost;
		 this.flipYButton.firstChild.src="http://static.herbariumathome.org/flip0"+iconSuffix+".png";
		 this.rot90Button.firstChild.src="http://static.herbariumathome.org/rot90"+iconSuffix+".png";
	} else {
		if (angle=='rev' && this.orientation == 'rev') {
			this.orientation='';
			this.host=this.originalHost;
			this.flipYButton.firstChild.src="http://static.herbariumathome.org/flipY"+iconSuffix+".png";
			this.rot90Button.firstChild.src="http://static.herbariumathome.org/rot90"+iconSuffix+".png";
		} else {
			// rotate 90
			if (this.orientation!='r90') {
				this.orientation ='r90';
				this.host=this.dynamicHost;
				this.flipYButton.firstChild.src="http://static.herbariumathome.org/flipY"+iconSuffix+".png";
				this.rot90Button.firstChild.src="http://static.herbariumathome.org/flip0"+iconSuffix+".png";
			} else {
				this.orientation ='';
				this.host=this.originalHost;
				this.flipYButton.firstChild.src="http://static.herbariumathome.org/flipY"+iconSuffix+".png";
				this.rot90Button.firstChild.src="http://static.herbariumathome.org/rot90"+iconSuffix+".png";
			}
			this.swap_x_y();
		}
	}
	
	this.rotatedImgOnloadHandle=register_event_handler(this.image,'load',this,'rotation_img_on_load_handler','');
	this.image.src="http://"+this.host+"/"+this.path+"/"+this.leafName+this.orientation+"/low_"+this.leafName+this.orientation+".jpg";
	
	for (var n in this.tile) {
		if (this.tile[n] && this.tile[n].style) {
			this.tile[n].quality=null;
			this.tile[n].parentNode.removeChild(this.tile[n]);
			this.tile[n]=null;
		}
	}
};

/**
 * update display and thumbnail following loading of rotated image
 */
SpecimenImage.prototype.rotation_img_on_load_handler=function() {
	this.check_tiles();
	this.navThumb.src="http://"+this.host+"/"+this.path+"/"+this.leafName+this.orientation+"/thumb_"+this.leafName+this.orientation+".jpg";
	if (this.rotatedImgOnloadHandle) {this.rotatedImgOnloadHandle=remove_event_handler(this.rotatedImgOnloadHandle);}
};

/**
 * remove image mouse handlers
 */
SpecimenImage.prototype.remove_move_handlers = function () {
	// remove handlers

	if (this.upHandler) {remove_event_handler(this.upHandler);}
	if (this.moveHandler) {remove_event_handler(this.moveHandler);}
	if (this.outHandler) {remove_event_handler(this.outHandler);}

	delete this.upHandler;
	delete this.moveHandler;
	delete this.outHandler;
	
	this.overlay.className='zoomable';
	document.body.style.cursor='auto';
	this.dragging=false;
};

/**
 * InfoContainer is the mid-top right panel used for suggestion lists and collector info
 * @constructor
 */
function InfoContainer() {
	this.data=null; // back ref to SpecimenData
	this.document=document; // IE is intolerant of moving html between document roots

	this.div=document.createElement("div");
	this.div.id="infocontainer";
	this.div.style.position='absolute';
	this.div.style.top=InfoContainer.defaultTop;
	this.div.style.width='200px';
	this.div.style.height="260px";
	this.div.style.padding="0";
	this.div.style.margin="0";
	this.div.style.overflow="hidden";
	this.set_position();

	this.content=null; // DOM content

	this.clear=this.clear_default;
	this.clearIntervalHandle=null;
	
	this.form=null;
	this.nameBox=null;
}

InfoContainer.defaultTop='320px';// was 520px
InfoContainer.detachedTop='308px';

/**
 * destructor
 */
InfoContainer.prototype.destroy =function() {
	this.xmlRequest=null;
	this.form=null;
	this.nameBox=null;
	this.data=null;
	this.document=null;

	this.clear(); // delete child elements
	if (this.div && this.div.parentNode) {this.div.parentNode.removeChild(this.div);}
	this.div=null;
	this.content=null;
	this.clear=null;
};

/**
 * set left position of the info box relative to the sheet image container
 */
InfoContainer.prototype.set_position = function() {
	this.div.style.left=(SpecimenImage.viewportWidth+30)+'px';
};

/**
 * set top position of the info box relative to the sheet image container
 * called following reattachment of container to doc form (after closure of detached window)
 */
InfoContainer.prototype.set_top_position = function() {
	this.div.style.top=InfoContainer.defaultTop;
};

/**
 * default placeholder function to clear child elements (NOP) 
 */
InfoContainer.prototype.clear_default = function() {
	if (this.clearIntervalHandle) {this.stop_timeout();}
};

/**
 * discard timer
 * (timer is used to postpone destruction, to allow click events to catch up)
 */
InfoContainer.prototype.stop_timeout = function () {
	if (this.clearIntervalHandle) {clearTimeout(this.clearIntervalHandle);}
	this.clearIntervalHandle=null;
};

/**
 * delete contents of suggestions box (when in collector list incarnation)
 */
InfoContainer.prototype.clear_collector =function() {
	if (this.clearIntervalHandle) {this.stop_timeout();}

	var links=this.div.getElementsByTagName('A');
	var l=links.length;

	for (var i=l-1; i>=0; i--) {
		links[i].onclick=null;
		
		links[i].params=null;
		links[i].flagChange=null;
		links[i].parentNode.removeChild(links[i]);
	}

	var node;
	while ((node=this.div.firstChild)) {this.div.removeChild(node);}
	this.clear=this.clear_default;
	
	this.nameBox=null;
};

/**
 * delete contents of suggestions box (when in place list incarnation)
 */
InfoContainer.prototype.clear_place =function() {
	if (this.clearIntervalHandle) {this.stop_timeout();}

	var links=this.div.getElementsByTagName('A');
	var l=links.length;

	for (var i=l-1; i>=0; i--) {
		links[i].onclick=null;
		
		//links[i].params=null;
		//links[i].flagChange=null;
		links[i].parentNode.removeChild(links[i]);
	}

	var node;
	while ((node = this.div.firstChild)) {this.div.removeChild(node);}
	this.clear = this.clear_default;
	
	//this.nameBox=null;
};

/**
 * @param <SpecimenForm> form parent specimen form
 * @param <string> type is 'col','prov' or 'det'
 * @param <htmlelement> element html input element containing query name
 * @param <NameBox> nameBox
 * @param <string> rowId
 * @param <NameList> nameList
 * @todo generalize field references
 */
InfoContainer.prototype.seek_provenance =function(form,type,element,nameBox,rowId,nameList) {
	this.clear();
	this.form=form; // make horribly circular reference
	this.nameBox=nameBox;
	this.nameBox.rowId=rowId;
	this.nameBox.parentNameList=nameList;

	var nameString=element.value.replace(trimRegex,''); // trim
	var fromDate=form.field.colDate.fromJdMinimum;
	var toDate=form.field.colDate.toJdMaximum;
	var taxon=form.field.taxa.defaultTaxonId;
	var instId=form.parentData.institutionId;
	
	var provIds=form.field.provenances?form.field.provenances.id_list():[];
	var colIds=form.field.collectors?form.field.collectors.id_list():[];
	var personId=(nameBox.personId)?"&personid="+nameBox.personId:"";
	
	var url=scriptUrl+"XMLcolsuggest.php?query="+encodeURIComponent(nameString.toLowerCase())+"&fromdate="+fromDate+"&todate="+toDate+"&taxon="+taxon+"&field="+type+"&instid="+instId+"&prov="+provIds.toString()+"&col="+colIds.toString()+personId;

	if (this.xmlRequest==null || this.xmlRequest.readyState===4 || this.requestURL!=url) {
		this.xmlRequest=agnostic_XMLHttpRequest(); // create the request object
		this.requestURL=url;
		xml_request(this.xmlRequest,url,associateObjectWithXMLRequest(this, "prov_xml_handler", ''));
	}
};

/**
 * @param <SpecimenForm> form parent specimen form
 * @param <PlaceSet> placeSet
 * @param <string> rowId
 * @param <PlaceList> placeList
 */
InfoContainer.prototype.seek_place_links =function(form, placeSet, rowId, placeList) {
	this.clear();
	this.form=form; // make horribly circular reference
	//this.nameBox=nameBox;
	//this.nameBox.rowId=rowId;
	//this.nameBox.parentNameList=nameList;

	var placeString = placeSet.placeName.input.value.replace(trimRegex,'').toLowerCase(); // trim
	var fromJD = form.field.colDate.fromJdMinimum;
	var toJD = form.field.colDate.toJdMaximum;
	//var taxon=form.field.taxa.defaultTaxonId;
	//var instId=form.parentData.institutionId;
	
	var provIds=form.field.provenances?form.field.provenances.id_list():[];
	var colIds=form.field.collectors?form.field.collectors.id_list():[];
	//var personId=(nameBox.personId)?"&personid="+nameBox.personId:"";
	
	var url=scriptUrl+'XMLplacesuggest.php?query='+encodeURIComponent(placeString)+
		"&fromjd="+fromJD+"&tojd="+toJD+
		"&fromdate="+form.field.colDate.fromString+"&todate="+form.field.colDate.toString+
		"&prov="+provIds.toString()+"&col="+colIds.toString();

	if (this.xmlRequest==null || this.xmlRequest.readyState===4 || this.requestURL!=url) {
		this.xmlRequest=agnostic_XMLHttpRequest(); // create the request object
		this.requestURL=url;
		xml_request(this.xmlRequest,url,associateObjectWithXMLRequest(this, 'place_xml_handler', ''));
	}
};

/**
 * display collectors name and dates and an info link
 * (formated name and link at top of info box, shown when focus on namebox with id'ed collector)
 */
InfoContainer.prototype.prov_name_info=function () {
	if (this.nameBox && this.nameBox.personId) {
		var infoLink=info_link_doc(this.document);
		infoLink.style.cssFloat='right';
		infoLink.href=scriptUrl+"collector/"+this.nameBox.personId+"/";
		
		var p=this.document.createElement('p');
		p.appendChild(infoLink);
		var a=this.document.createElement('a');
		a.appendChild(this.document.createTextNode(this.nameBox.input.value));
		a.href=scriptUrl+"collector/"+this.nameBox.personId+"/";
		a.setAttribute('target','_blank');
		
		p.appendChild(a);
		p.appendChild(this.document.createElement('br'));
		if (this.nameBox.dateString) {
			p.appendChild(this.document.createTextNode(this.nameBox.dateString+" "));
		}
		
		if (this.nameBox.wikiName) {
			a=this.document.createElement('a');
			a.appendChild(this.document.createTextNode('more info (wiki)'));
			a.href=scriptUrl+"wiki/"+this.nameBox.wikiName;
			a.setAttribute('target','_blank');
			
			p.appendChild(a);
		}
		
		this.div.appendChild(p);
		this.clear=this.clear_collector;
	}
};

/**
 * response handler for collector suggestions request by {@link InfoContainer#seek_provenance}
 */
InfoContainer.prototype.prov_xml_handler=function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4)
    {
		var response  = this.xmlRequest.responseXML.documentElement;
		var nameId,dob,dod,dateString,linkText,nameText,nameLink;

		this.clear();
		this.prov_name_info();
		
		if (response !==null) {
			var names = response.getElementsByTagName('name');
			
			// collector name
			if (names.length >0) {
				var p=this.document.createElement('P');
				p.appendChild(this.document.createTextNode('common names'));
				p.appendChild(this.document.createElement("br"));
	
				var l=names.length;
				for (var i=0; i<l;i++) {
					nameId=names[i].getAttribute('id');
					dob=names[i].getAttribute('dob');
					dod=names[i].getAttribute('dod');
					dateString='';
	
					if (names[i].hasChildNodes()) {
						linkText=names[i].firstChild.data;
						nameText=names[i].firstChild.data;
					} else {
						linkText=''; // odd error with null name, upsets IE
						nameText='';
					}
	
					if (dob || dod) {
						dateString='('+dob+'-'+dod+')';
					}
	
					if (nameId) {
						nameLink=this.document.createElement("a");
						nameLink.onclick=associate_obj_with_event(this.nameBox, "click_set_value",nameText);
						
						nameLink.params={
							name: nameText,
							externalId: nameId,
							dob: dob,
							dobjd: parseInt(names[i].getAttribute('dobjd'),10),
							dod: dod,
							dodjd: parseInt(names[i].getAttribute('dodjd'),10),
							dateString: dateString,
							wikiName: (names[i].getAttribute('wikiname'))?names[i].getAttribute('wikiname'):null
						};
						
						nameLink.appendChild(this.document.createTextNode(linkText));
						nameLink.setAttribute("title",dateString);
						nameLink.className="picklist";
						nameLink.flagChange=true; // kludge to force refresh of boxes (e.g. addition of extra collector/provenance fields)
						p.appendChild(nameLink);
						p.appendChild(this.document.createElement("br"));
					}
				}
	
				this.div.appendChild(p);
				this.clear=this.clear_collector;
			}
			
			var map = response.getElementsByTagName('map');
			var mapUrl;
			if (map.length && (mapUrl=map[0].getAttribute('url'))) {
				var img=this.document.createElement('img');
				img.src=mapUrl;
				img.height=150;
				this.div.appendChild(img);
				this.clear=this.clear_collector;
			}
		}
	}
};

/**
 * response handler for locality suggestions request by {@link InfoContainer#seek_place_links}
 */
InfoContainer.prototype.place_xml_handler = function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4)
    {
		var response  = this.xmlRequest.responseXML.documentElement;

		this.clear();
		//this.prov_name_info();
		
		if (response !==null) {
			// get link info (typically links to collector search results)
			// <link><anchor>anchor text</anchor><url>url</url></link>
			var links = response.getElementsByTagName('link');
			
			if (links.length >0) {
				var l = links.length;
				
				var p=this.document.createElement('P');
				//p.appendChild(this.document.createTextNode('common names'));
				//p.appendChild(this.document.createElement("br"));
				
				var anchor, url, el;
	
				for (var i=0; i<l; i++) {
					anchor = links[i].getElementsByTagName('anchor')[0].firstChild.data;
					url = links[i].getElementsByTagName('url')[0].firstChild.data;
					
					el = this.document.createElement("a");
					el.appendChild(this.document.createTextNode(anchor));
					//el.setAttribute("title",'search for specimens');
					el.href = url;
					el.setAttribute('target','_blank');
					//el.className="picklist";
						
					p.appendChild(el);
					p.appendChild(this.document.createElement("br"));
				}
	
				this.div.appendChild(p);
				this.clear=this.clear_place;
			}
		}
	}
};

/**
 * DOM form and associated data
 * @constructor
 * @param <SpecimenData> parentData
 */
function SpecimenForm(parentData,manifestName) {
	this.manifestName=manifestName;
	this.unique=uniqueId('form'); // globally unique ID (for internal use, never exported)
	this.parentData=parentData;
	this.field=null;
	
	this.huId=''; // HU internal id number
	this.externalId=''; // e.g. EM number
	this.international=false; // UK and IE only
}

/**
 * add the fieldset and table header and unpopulated rows
 * @todo improve support for user-specified rows and extensions
 */
SpecimenForm.prototype.create_rows = function () {
	this.formBase=document.createElement('div');
	this.formBase.className="formbase";
	this.formBase.id=this.unique;
	if (!standardBrowser) {
		this.formBase.onselectstart = function () { window.event.cancelBubble = true; }; //allow selection within the doc form
	}

	this.formBase.style.margin="0";
	this.formBase.style.padding="4px 0 4px 0";
	this.formBase.style.width="100%";

	var legend=document.createElement('div');
	legend.className="formlegend";
	
	this.deleteButton=legend.appendChild(deleteButton('delete specimen'));
	this.addButton=legend.appendChild(plusButton('add another specimen'));
	legend.appendChild(document.createElement('span')).appendChild(document.createTextNode('specimen'));
	
	var img=document.createElement('img');
	img.src="http://static.herbariumathome.org/down"+iconSuffix+".png";
	this.nextButton=legend.appendChild(document.createElement('a'));
	this.nextButton.setAttribute('title','next specimen (scroll down)');
	this.nextButton.appendChild(img);
	this.nextButton.onclick=associate_obj_with_event(this, "scroll_form_click_handler");
	
	img=document.createElement('img');
	img.src="http://static.herbariumathome.org/up"+iconSuffix+".png";
	this.previousButton=legend.appendChild(document.createElement('a'));
	this.previousButton.setAttribute('title','previous specimen (scroll up)');
	this.previousButton.appendChild(img);
	this.previousButton.onclick=associate_obj_with_event(this, "scroll_form_click_handler");

	this.iconContainer=document.createElement('div');
	this.iconContainer.className="formiconcontainer";
	if (!(standardBrowser || IEversion >= 7)) {
		this.iconContainer.style.right="20%";
		//this.iconContainer.style.width="1px";
		this.iconContainer.style.position="absolute";
	}
	this.formBase.appendChild(this.iconContainer);
	this.formBase.appendChild(legend);
	
	var table=document.createElement("table");
	table.style.width="100%";
	table.style.padding="0";
	table.style.margin="0";
	table.cellPadding="0";
	table.cellSpacing="0";
	
	this.formTable=table.appendChild(document.createElement("tbody"));
	
	// get form descriptor list from named manifest
	var fieldList=manifests[this.manifestName].get_fields();
	
	/**
	 * container object for form field rows
	 */
	this.field = {};
	
	// create field objects, passing reference to the container form and the field manifest xml
	for (var fieldName in fieldList) {
		try {
			this.field[fieldName]=new field[fieldList[fieldName].className](this,fieldList[fieldName].manifestXml);
			this.field[fieldName].iconify=!!fieldList[fieldName].manifestXml.getAttribute('iconify');
			if (fieldList[fieldName].manifestXml.getAttribute('iconname')) {this.field[fieldName].iconName=fieldList[fieldName].manifestXml.getAttribute('iconname');}
			if (fieldList[fieldName].manifestXml.getAttribute('iconlabel')) {this.field[fieldName].iconLabel=fieldList[fieldName].manifestXml.getAttribute('iconlabel');}
		} catch (err) {
			throw new Error('herb@home: failed to create field "'+fieldName+'", class "'+fieldList[fieldName].className+'" (JS error: '+err.message+')');
		}
	}
	
	for (fieldName in this.field) {
		if (this.field[fieldName]) {this.formTable.appendChild(this.field[fieldName].containerRow);}
	}

	this.formBase.appendChild(table);
	this.changeHandler=register_event_handler(this.formTable,'change',this,"change",0);
};

SpecimenForm.prototype.scroll_form_click_handler = function (event,element) {
	var id=element.href.substr(element.href.lastIndexOf('#')+1);
	var form=document.getElementById(id);
	this.parentData.formContainer.scrollTop=form.offsetTop;
	return false; //prevent default;
};

/**
 * display form row minimised (as icon)
 * @param field Field to minimise
 */
SpecimenForm.prototype.iconify = function (field) {
	field.containerRow.style.display="none";
	
	var icon=this.add_icon(field.iconName,field.iconLabel?field.iconLabel:field.labelName);
	icon.onclick=associate_obj_with_event(this, "icon_click",field.containerRow.id);
};

/**
 * click handler for minimised field icon
 * redisplay table row (identified by fieldRowId) and remove icon
 */
SpecimenForm.prototype.icon_click=function (event,element,fieldRowId) {
	row=document.getElementById(fieldRowId);

	try {
		row.style.display='table-row';
	} catch(e) {
		row.style.display = 'block';
	}

	element.onclick=null;
	this.iconContainer.removeChild(element);
};

SpecimenForm.prototype.add_icon = function (src,labelText) {
	var icon=document.createElement('img');
	icon.src=src;
	icon.setAttribute('title',labelText);
	this.iconContainer.appendChild(icon);
	return icon;
};

/**
 * populates a form in the default state for a new specimen
 * rows must have been created before this is called (see {@link SpecimenForm#create_rows})
 */
SpecimenForm.prototype.populate_default =function () {
	for (var item in this.field) {
		if (this.field[item]) {this.field[item].populate_default();}
		if (this.field[item].iconify) {this.iconify(this.field[item]);}
	}
};

/**
 * onchange event handler
 * sets SpecimenData modified state (escalates change up the object hierarchy)  
 */
SpecimenForm.prototype.change =function (event, element) {
	this.parentData.set_modified(true);
};

/**
 * populates a form with data from an XML specimen object
 * @param <XML> specimenXML xml specimen node
 */
SpecimenForm.prototype.add_from_xml = function (specimenXML) {
	this.externalId=specimenXML.getAttribute('externalid');
	this.huId=specimenXML.getAttribute('id');

	for (var item in this.field) {
		if (this.field[item] && this.field[item].xmlTagName) {
			this.field[item].add_from_xml(specimenXML.getElementsByTagName(this.field[item].xmlTagName));
			if (this.field[item].iconify) {this.iconify(this.field[item]);}
		}
	}
};

/**
 * sets default settings for taxon name (current and previous)
 * current should be currently accepted name, which may not neccessarily be listed on the sheet
 * previous taxon is typically an obsolete name under which the sheet is currently filed (or the name from the sheet)
 * These defaults are used to populate new copies of the taxon field (current name is also highlight on the taxon dropdown list)
 * @param <string> defaultTaxonName genus and species (subsp and var) no authority, or empty string
 * @param <int> defaultTaxonId numeric id of default taxon (internal HU db ref) or 0
 * @param <string> defaultTaxonAuthority default taxon's author(s)
 * @param <string> defaultTaxonCommonName vulgar name (English language - maybe need to consider internationalization extensions)
 * @param <string> previousTaxonName genus and species (subsp and var) no authority, or empty string
 * @param <int> previousTaxonId numeric id of default taxon (internal HU db ref) or 0
 * @param <string> previousTaxonAuthority default taxon's author(s)
 * @param <string> previousTaxonCommonName vulgar name (English language - maybe need to consider internationalization extensions)
 * @todo consider how this can be done differently, this is not the right place for this
 */
SpecimenForm.prototype.set_default_taxon = function (defaultTaxonName,defaultTaxonId,defaultTaxonAuthority,defaultTaxonCommonName,previousTaxonName,previousTaxonId,previousTaxonAuthority,previousTaxonCommonName) {
	this.field.taxa.defaultTaxonName=defaultTaxonName;
	this.field.taxa.defaultTaxonId=defaultTaxonId;
	this.field.taxa.defaultTaxonAuthority=defaultTaxonAuthority;
	this.field.taxa.defaultTaxonCommonName=defaultTaxonCommonName;
	this.field.taxa.previousTaxonName=previousTaxonName;
	this.field.taxa.previousTaxonId=previousTaxonId;
	this.field.taxa.previousTaxonAuthority=previousTaxonAuthority;
	this.field.taxa.previousTaxonCommonName=previousTaxonCommonName;
};

/**
 * XML serialization of the specimen, appended as child of sheet 
 * @param <xmlnode> doc xml document root node
 * @return xml <specimen> node
 * @todo remove accNumber as attribute once this version is mainstream
 */
SpecimenForm.prototype.get_xml =function (doc) {
	var specimen=doc.createElement('specimen');

	specimen.setAttribute('externalid',this.externalId);
	specimen.setAttribute('id',this.huId);
	
	// special treatment of accNumber as attribute is deprecated
	if (this.field.accNumber && this.field.accNumber.accField.value != this.field.accNumber.accField.defaultValue) {
		specimen.setAttribute('accno',this.field.accNumber.accField.value);
	}
	else {specimen.setAttribute('accno','');}
	
	for (var item in this.field) {
		if (this.field[item]) {specimen.appendChild(this.field[item].get_xml(doc));}
	}
	return specimen;
};

/**
 * returns summary of specimen as html
 * @return htmlfragment
 */
SpecimenForm.prototype.get_html =function () {
	var htmlFragment=document.createDocumentFragment();
	var html;
	
	/*
	var tBody=htmlFragment.appendChild(document.createElement('table')).appendChild(document.createElement('tbody'));
	
	for (var item in this.field) {
		if (this.field[item]) {
			html=this.field[item].get_html_tr();
			if (html) {
				tBody.appendChild(html);
			}
		}
	}
	*/
	
	var ul=htmlFragment.appendChild(document.createElement('ul'));
	
	for (var item in this.field) {
		if (this.field[item]) {
			html=this.field[item].get_html_div();
			if (html) {
				ul.appendChild(document.createElement('li')).appendChild(html);
			}
		}
	}
	
	return htmlFragment;
};

/**
 * destructor
 * many javascript implementations have poor garbage collection, so be (over)zealous about breaking references
 */
SpecimenForm.prototype.destroy = function () {
	for (var item in this.field) {
		if (this.field[item]) {
			this.field[item].destroy();
			this.field[item]=null; // probably unnecessary
		}
	}
	this.field=null;

	// ditch misc buttons and event handlers
	if (!standardBrowser) {this.formBase.onselectstart =null;}
	
	this.deleteButton.onclick=null;
	this.deleteButton.parentNode.removeChild(this.deleteButton);
	this.deleteButton=null;
	this.addButton.parentNode.removeChild(this.addButton);
	this.addButton=null;
	this.nextButton.onclick=null;
	this.nextButton=null;
	this.previousButton.onclick=null;
	this.previousButton=null;

	icons=this.iconContainer.getElementsByTagName('button');
	for (var i,l=icons.length; i<l;i++) {
		icons[i].onclick=null;
		this.iconContainer.removeChild(icons[i]);
	}
	this.iconContainer.parentNode.removeChild(this.iconContainer);
	this.iconContainer=null;

	this.formTable.parentNode.removeChild(this.formTable);
	this.formTable=null;

	if (this.changeHandler!==null) {
		this.changeHandler=remove_event_handler(this.changeHandler);
	}

	if (this.formBase.parentNode) {this.formBase.parentNode.removeChild(this.formBase);} // delete current row
	this.formBase=null;
	this.parentData=null;
};

/**
 * verify fields and set warning messages
 * @todo generalise this to support modular field plugins
 */
SpecimenForm.prototype.verify = function () {
	var result=false;

	this.field.colDate.verify();

	var fromJD = this.field.colDate.fromJdMinimum;
	var toJD = this.field.colDate.toJdMaximum;

	var colDOB;
	var colDOD;
	
	var minAge = (10 * 365); // assume under 10's aren't collectors

	//result|=this.field.collectors.verify()|this.fields.provenances.verify();

	if (this.field.collectors) {
		result|=this.field.collectors.verify();
		// checks dates against collectors
		for (var n=this.field.collectors.nameTable.rows.length-1;n>=0; n--) {
			colDOB = parseInt(this.field.collectors.nameTable.rows[n].nameBox.dob,10);
			colDOD = parseInt(this.field.collectors.nameTable.rows[n].nameBox.dod,10);
	
			if (colDOB >0 && fromJD && (colDOB + minAge) > fromJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with collector DOB: "+this.field.collectors.nameTable.rows[n].nameBox.input.value+" ("+this.field.collectors.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.fromError=true;
				this.field.colDate.containerRow.error=true;
			}
	
			if (colDOD >0 && fromJD && colDOD <= fromJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with collector DOD: "+this.field.collectors.nameTable.rows[n].nameBox.input.value+" ("+this.field.collectors.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.fromError=true;
				this.field.colDate.containerRow.error=true;
			}
	
			if (colDOB >0 && toJD && (colDOB + minAge) > toJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with collector DOB: "+this.field.collectors.nameTable.rows[n].nameBox.input.value+" ("+this.field.collectors.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.toError=true;
				this.field.colDate.containerRow.error=true;
			}
	
			if (colDOD >0 && toJD && colDOD <= toJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with collector DOD: "+this.field.collectors.nameTable.rows[n].nameBox.input.value+" ("+this.field.collectors.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.toError=true;
				this.field.colDate.containerRow.error=true;
			}
		}
	}

	if (this.field.provenances) {
		result|=this.field.provenances.verify();
		
		for (n=this.field.provenances.nameTable.rows.length-1;n>=0; n--) {
			colDOB=this.field.provenances.nameTable.rows[n].nameBox.dob;
			colDOD=this.field.provenances.nameTable.rows[n].nameBox.dod;
	
			if (colDOD >0 && fromJD && colDOD <= fromJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with provenance DOD: "+this.field.provenances.nameTable.rows[n].nameBox.input.value+" ("+this.field.provenances.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.fromError=true;
				this.field.colDate.containerRow.error=true;
			}
	
			if (colDOD >0 && toJD && colDOD <= toJD) {
				this.field.colDate.containerRow.errorMsg[this.field.colDate.containerRow.errorMsg.length]="Conflict with provenance DOD: "+this.field.provenances.nameTable.rows[n].nameBox.input.value+" ("+this.field.provenances.nameTable.rows[n].nameBox.dateString+")";
				this.field.colDate.toError=true;
				this.field.colDate.containerRow.error=true;
			}
		}
	}

	result|=this.field.taxa.verify_dates()|this.field.colDate.apply_error_state()|this.field.site.verify()
		|(this.field.accNumber?this.field.accNumber.verify():0)|(this.field.bonAccNumber?this.field.bonAccNumber.verify():0)|(this.field.godAccNumber?this.field.godAccNumber.verify():0);
	
	this.parentData.formFooter.show_warning(result);

	return result;
};

/**
 * parent table row container for doc form fields
 * includes the field label and help text
 */
function Row () {
	
}

Row.prototype.labelName=''; // row label
Row.prototype.helpText=''; // short summary help text
Row.prototype.longHelp=''; // message index key to lookup verbose help message

/**
 * initial setup of row
 * defaults to adding a 'blank' row using {@link Row#add_blank}.
 * populate_default is functionally distinct from {@link Row#add_blank} where the initial configuration involves multiple rows (e.g. a current and a previous taxon name)
 */
Row.prototype.populate_default = function () {
	this.add_blank();
};

/**
 * adds a blank entry
 * can ignore for items which only contain a single item and need no initialisation
 */
Row.prototype.add_blank = function () {
	// default NOP
};

/**
 * returns summary of content as html fragment
 * @return html fragment|null if no content 
 */
Row.prototype.get_html = function () {
	// default NOP
	return null;
};

/**
 * returns summary of content as html div block (with row label as title attribute)
 * @return html tr|null if no content
 */
Row.prototype.get_html_div = function () {
	var html=this.get_html();
	
	if (html) {
		var div=document.createElement('div');
		div.title=this.labelName;
		div.className='thumblist';
		div.appendChild(document.createElement('h5')).appendChild(document.createTextNode(this.labelName));
		div.appendChild(html);
		return div;
	} else {return null;}
};

/**
 * creates the table row, labels and help
 * registers focus-related event handlers
 * 
 * @param <string> labelName
 * @param <string> helpText short text used for help in righthand panel
 * @param <string> longHelp verbose help used for popup bubble
 */
Row.prototype.construct_row = function () {
	this.containerRow=document.createElement('tr');

	var tmpCell=this.containerRow.appendChild(document.createElement('td'));
	this.helpPlaceHolder=tmpCell.appendChild(document.createElement('span'));

	this.containerTable=tmpCell.appendChild(document.createElement('table'));
	this.containerTable.style.width="100%";
	this.containerTable.style.padding="0";
	this.containerTable.style.margin="0";
	this.containerTable.style.tableLayout="fixed";

	this.containerTable.cellPadding="0";
	this.containerTable.cellSpacing="0";
	this.row=this.containerTable.appendChild(document.createElement('tbody')).appendChild(document.createElement('tr'));
	
	this.containerRow.id=uniqueId("row"+this.labelName.replace(/\w+/,"_"));

	this.containerRow.style.borderTopWidth="5px";
	this.containerRow.style.borderTopStyle="solid";
	this.containerRow.style.borderTopColor="white";
	this.containerRow.style.backgroundPosition="left";
	this.containerRow.style.backgroundRepeat="repeat-y";
	this.focus=false;
	this.containerRow.errorMsg=[];
	this.containerRow.error=false;
	
	this.set_style();
		
	this.containerMouseOverHandler=register_event_handler(this.containerRow,'mouseover',this,'row_mouse_over','');
	this.containerMouseOutHandler=register_event_handler(this.containerRow,'mouseout',this,'row_mouse_out','');
	
	var td=this.row.appendChild(document.createElement("td"));
	this.helpPlaceHolder.style.position="absolute";
	this.helpPlaceHolder.style.left=0;
	
	this.helpPlaceHolder.style.margin="0";
	this.helpPlaceHolder.style.padding="0";
	
	td.appendChild(document.createTextNode(this.labelName));
	td.style.fontWeight="bold";
	td.style.textAlign="right";
	td.style.width="8em";
	td.style.paddingRight="5px";
	
	this.bubble=new BubbleHelp();

	if (true || standardBrowser) {
		this.bubble.bubble.style.fontSize="0.8em";
		document.body.appendChild(this.bubble.bubble);
	} else {	
		globalContainer.appendChild(this.bubble.bubble);
	}
	
	if (this.longHelp && globalMessages[this.longHelp]) {this.bubble.set_content(this.longHelp,this.labelName);}
	this.bubbleMoveHandler=register_event_handler(this.containerRow,'mousemove',this.bubble,'locate','');
};

/**
 * sets the style of the row to reflect focus and error state
 * @todo should use external css classes rather than setting specific styling here
 */
Row.prototype.set_style = function () {
	if (this.focus) {
		help.textBlock.style.display=globalShowExpandedHelp?'none':'block';

		this.containerRow.style.backgroundImage='';
		if (!this.containerRow.error) {
			this.containerRow.style.backgroundColor="#ffffcc";
			help.errorText.data='';
			help.errorBlock.style.display='none';
		} else {
			this.containerRow.style.backgroundColor="#FF9999";
			if (this.containerRow.errorMsg.length) {
				// set error reports
				var text=this.containerRow.errorMsg.join("\r\n");
				help.errorText.data=text;
				help.errorBlock.style.display='block';
			}
		}
	} else {
		this.containerRow.style.backgroundColor="white"; //"#fcfcfc"
		this.containerRow.style.backgroundImage=this.containerRow.error?'url("/redbar.png")':'url("/beigebar.png")';
	}
};

/**
 * remove focus and help related event handlers
 */
Row.prototype.remove_row_event_handlers =function () {
	this.helpText=null;
	this.containerRow.error=null;
	this.containerRow.errorMsg=null;
	this.helpPlaceHolder=null;

	if (this.bubbleMoveHandler) {this.bubbleMoveHandler=remove_event_handler(this.bubbleMoveHandler);}
	if (this.containerMouseOverHandler) {this.containerMouseOverHandler=remove_event_handler(this.containerMouseOverHandler);}
	if (this.containerMouseOutHandler) {this.containerMouseOutHandler=remove_event_handler(this.containerMouseOutHandler);}

	// as extra IE-orientated garbage collection, delete all the rows cells
	for (var n=this.row.cells.length-1; n>=0; n--) {
		this.row.deleteCell(n);
	}

	this.row.parentNode.removeChild(this.row);
	if (this.bubble) {
		this.bubble.destroy();
		this.bubble=null;
	}
};

/**
 * doc form row container mouse-over event handler
 * sets row style and display bubble help
 */
Row.prototype.row_mouse_over = function (event,element,param) {
	if (!this.focus) {
		this.focus=true;
		this.set_style();
		
		if (Row.focusedRow && Row.focusedRow!=this) {
			Row.focusedRow.row_mouse_out();
		}
		Row.focusedRow=this;
		
		if (globalShowExpandedHelp) {
			//this.bubble.display(event);
			this.bubble.display(null);
			currentBubble=this.bubble;
		}
	
		if (!globalShowExpandedHelp || this.containerRow.error) {
			// only display short help if not displaying long help or when error msg to be displayed 
			
			if (help.helpBox.parentNode) {help.helpBox.parentNode.removeChild(help.helpBox);}
			help.text.data=this.helpText; // set text content
		
			if (this.containerRow.offsetWidth) { 	
				help.helpBox.style.left=(this.containerRow.offsetLeft+this.containerRow.offsetParent.offsetWidth+2)+"px";
				help.helpBox.style.paddingLeft="4px";
				help.helpContent.style.top="-0px";
			} else {
				help.helpBox.style.right="8px";
			}
			
			this.helpPlaceHolder.appendChild(help.helpBox);
		}
	}
};

/**
 * set row styling and hide bubble help
 * may be called outside of mouse out event, so event parameters are not valid
 */
Row.prototype.row_mouse_out = function () {
	if (this.focus) {
		this.focus=false;
		Row.focusedRow=null;
		if (help.helpBox.parentNode) {help.helpBox.parentNode.removeChild(help.helpBox);}
		this.set_style();
		help.text.data=null;
		
		if (globalShowExpandedHelp) {
			if (this.bubble) {this.bubble.hide();}
			currentBubble=null;
		}
	}
};

/**
 * brief help text displayed at right-hand side when doc form row is highlighted
 */
function FormHelp (helpText) {
	this.helpBox=document.createElement("div");
	this.helpContent=document.createElement("div");
	this.textBlock=document.createElement("P");
	this.textBlock.style.fontSize="1em";
	this.text=this.textBlock.appendChild(document.createTextNode(helpText));
	this.errorText=document.createTextNode("");
	this.helpBox.appendChild(this.helpContent);
	this.helpBox.style.position="absolute";
	this.helpContent.style.padding="3px";
	this.helpContent.style.width="10em";
	this.helpContent.style.position="relative";
	this.helpContent.style.top="-1em";
	this.helpContent.style.backgroundColor="#FFFF99";
	this.helpContent.style.backgroundPosition="left";
	this.helpContent.style.backgroundRepeat="repeat-y";
	this.helpContent.style.backgroundImage='url("/yellowbar.png")';
	this.helpContent.style.fontWeight="normal";
	this.helpContent.style.textAlign="left";
	this.helpContent.appendChild(this.textBlock);

	this.errorBlock=document.createElement("P");
	this.errorBlock.className="helperror";
	this.errorBlock.appendChild(this.errorText);
	this.errorBlock.style.display="none";
	this.errorBlock.style.fontSize="1em";
	this.helpContent.appendChild(this.errorBlock);
}

FormHelp.prototype.destroy = function ()
{
	this.text=null;
	this.errorText=null;
	this.textBlock=null;

	if (this.helpBox.parentNode) {this.helpBox.parentNode.removeChild(this.helpBox);}
	this.helpBox=null;
	if (this.helpContent.parentNode) {this.helpContent.parentNode.removeChild(this.helpContent);}
	this.helpContent=null;

	if (this.errorBlock.parentNode) {this.errorBlock.parentNode.removeChild(this.errorBlock);}
	this.errorBlock=null;
};

/**
 * Container for field classes
 */
var field = {};

/**
 * Collection date field
 * consists of one or more 'from', 'to' and 'notes' entries
 * @param <SpecimenForm> form parent form object
 * @param <string> rowLabel
 */
field.ColDate = function (form) {
	this.form=form;
	this.construct_row();
	
	var dateCell=document.createElement("TD");

	this.dateTable=dateCell.appendChild(document.createElement("table"));
	this.dateTable.appendChild(document.createElement("tbody"));

	this.row.appendChild(dateCell);
	this.notesShown=false;
};

field.ColDate.prototype =new Row();
field.ColDate.prototype.constructor=field.ColDate;
field.ColDate.prototype.xmlTagName='coldate';

field.ColDate.prototype.labelName='collection date'; // row label
field.ColDate.prototype.helpText='Field collection date, expressed as dd/mm/yyyy or mm/yyyy or yyyy'; // short summary help text
field.ColDate.prototype.longHelp='datehelp'; // message index key to lookup verbose help message

// only allow 0-9 and /
if (standardBrowser)
{
	field.ColDate.prototype.key =function (event, element, param) {
		if (!((event.keyCode <=58) || (event.keyCode == 191)  || event.ctrlKey || (event.keyCode >=96 && event.keyCode <=105) || (event.keyCode == 111)) || (event.keyCode == 32)) {event.preventDefault();}
	};
} else {
	field.ColDate.prototype.key =function (event, element, param) {
		event.returnValue = ((event.keyCode <=58) || (event.keyCode == 191) || event.ctrlKey || (event.keyCode >=96 && event.keyCode <=105) || (event.keyCode == 111)) && (event.keyCode !=32);
	};
}

/**
 * add default state (blank) collection date field
 * creates html elements and registers event handlers
 */
field.ColDate.prototype.add_blank = function () {
	var row=this.dateTable.insertRow(this.dateTable.rows.length);
	row.id=uniqueId('date');
	
	var dateCell=row.insertCell(0);
	
	row.fromField=document.createElement("input");

	dateCell.appendChild(row.fromField);
	dateCell.appendChild(document.createTextNode(' (to '));

	row.toField=document.createElement("input");

	dateCell.appendChild(row.toField);
	dateCell.appendChild(document.createTextNode(')'));
	
	row.notesField=document.createElement('input');
	row.notesField.size=12;
	row.notesField.maxLength=64;
	row.notesField.defaultValue='notes';
	input_set_default(row.notesField);
	dateCell.appendChild(row.notesField);
	row.notesField.onfocus=input_default_focus;
	row.notesField.onblur=input_default_blur;
	
	row.fromKeyHandler=register_event_handler(row.fromField,'keydown',this,"key",'');
	row.toKeyHandler=register_event_handler(row.toField,'keydown',this,"key",'');
	row.fromChangeHandler=register_event_handler(row.fromField,'change',this,"change",'from');
	row.toChangeHandler=register_event_handler(row.toField,'change',this,"change",'to');

	var button=deleteButton('delete/reset dates');
	button.style.verticalAlign='top';
	dateCell.appendChild(button);
	row.deleteClickHandler=register_event_handler(button,'click',this,"delete_click",row.id);

	row.plusButton=plusButton('add another collection date');
	row.plusButton.style.verticalAlign='top';
	dateCell.appendChild(row.plusButton);
	row.addClickHandler=register_event_handler(row.plusButton,'click',this,"add_click",0);
	
	if (!this.notesShown) {
		row.moreButton=more_button('show date notes fields');
		row.moreButton.style.verticalAlign='top';
		dateCell.appendChild(row.moreButton);
		row.moreClickHandler=register_event_handler(row.moreButton,'click',this,"more_click",0);
		row.notesField.style.display="none";
	} else {
		row.moreClickHandler=null; //placeholder
	}

	row.fromField.jd=null;
	row.toField.jd=null;
	return row;
};

/**
 * date change event handler
 * check if format is valid
 * check if date is valid (in absolute terms and relative to col/prov/det)
 * @todo check if this is doing the error checking that it's meant to
 */
field.ColDate.prototype.change =function (event, element, field) {
	//var error=false;

	element.jd=null;

	if (element.value) {
		element.jd=date(element.value);
	}

	this.form.verify(); // check dates across form
	this.form.parentData.set_modified(true);
};

var dateRegex=/^(\d{1,2})?\/?(\d{1,2})?\/?(\d{4})$/;
/**
 * convert date string to Julian Day number (JD)
 * @param <string> dateString
 * @return <int> JD or null
 */
function date(dateString)
{
	var jd=null;

	dateString=dateString.match(dateRegex);

	if (dateString) {
		var year;
		var month=0;
		var day=0;
		var error=false;

		if (dateString[1] && !dateString[2]) {
			dateString[2]=dateString[1]; // month in day field
			dateString[1]=undefined; 
		}
		
		if (dateString[1]) {
			// nn/nn/nnnn
			dateString[2]=parseInt(dateString[2],10);
			switch (dateString[2])
			{
				case 4:
				case 6:
				case 9:
				case 11:
				var maxDays=30;
				break;

				case 1:
				case 3:
				case 5:
				case 7:
				case 8:
				case 10:
				case 12:
				var maxDays=31;
				break;

				case 2:
				var maxDays=29;
				break;

				default:
				var maxDays=-1; // invalid max days will fail at next step
			}

			dateString[1]=parseInt(dateString[1],10);
			if (dateString[1] >0 && dateString[1]<=maxDays) {
				day=dateString[1];
				month=dateString[2];
				dateString[3]=parseInt(dateString[3],10);
				year=dateString[3];


			}
			else {error=true;} //invalid month or day
		} else {
			dateString[3]=parseInt(dateString[3],10);
			year=dateString[3];

			if (dateString[2]) {
				// nn/nnnn
				dateString[2]=parseInt(dateString[2],10);
				if (dateString[2]>0 && dateString[2]<13) {month=dateString[2];}
				else {error=true;} //invalid month
			}
		}

		if (!error) {
			var today=new Date();
			var currentYear=today.getFullYear();
			if (currentYear < 1000) {currentYear+=1900;} // IE and firefox are not consistent

			if ((year >1500) && (year <= currentYear) ) {
				jd=to_JD(year,month,day);
			}
		}
	}

	return jd;
}

/**
 * clear date error state, prior to re-verifying
 */
field.ColDate.prototype.reset_error_state = function ()
{
	this.containerRow.errorMsg=[];
	this.containerRow.error=false;

	this.fromError=false;
	this.toError=false;
	
	this.fromJdMinimum = 99999999; // becomes null once set properly if there's no 'from' date
	this.fromString = ''; // verbose date string (passed to placename suggest - which needs date precision)
	this.toJdMaximum = 0;
	this.toString = ''; // verbose date string (passed to placename suggest - which needs date precision)
	
	var l=this.dateTable.rows.length;
	for (var n=0; n<l;n++) {
		this.dateTable.rows[n].fromField.error=false;
		this.dateTable.rows[n].toField.error=false;
	}
};

/**
 * clear any existing error state,
 * check internal consistency (calls {@link field.ColDate#verify_row})
 * set date min max bounds (parent form-level verification checks these bounds against collector constraints)
 */
field.ColDate.prototype.verify = function ()
{
	this.reset_error_state();
	
	var l=this.dateTable.rows.length;
	for (var n=0; n<l; n++) {
		var dateRow=this.dateTable.rows[n];	
		this.verify_row(dateRow);
		
		if (dateRow.fromField.jd && dateRow.fromField.jd < this.fromJdMinimum) {
			this.fromJdMinimum = dateRow.fromField.jd;
			if (dateRow.fromField.value) {
				this.fromString = dateRow.fromField.value; 
			} else {
				this.fromString = dateRow.toField.value;
			}
		}
		
		if (dateRow.toField.jd && dateRow.toField.jd > this.toJdMaximum) {
			this.toJdMaximum = dateRow.toField.jd;
			
			if (dateRow.toField.value) {
				this.toString = dateRow.toField.value;
			} else {
				this.toString = dateRow.fromField.value;
			}
		}
	}
	
	if (this.fromJdMinimum === 99999999) {
		this.fromJdMinimum=null;
	}
};

/**
 * check date format and internal consistency
 * @param <tablerow> dateRow
 */
field.ColDate.prototype.verify_row = function (dateRow)
{
	if (dateRow.fromField.value && !dateRow.fromField.jd) {
		this.containerRow.errorMsg[this.containerRow.errorMsg.length]="'From' date is not valid";
		this.fromError=true;
		dateRow.fromField.error=true;
	}

	if (dateRow.toField.value && !dateRow.toField.jd) {
		this.containerRow.errorMsg[this.containerRow.errorMsg.length]="'To' date is not valid";
		this.toError=true;
		dateRow.toField.error=true;
	}

	if (dateRow.toField.jd && (dateRow.fromField.jd > dateRow.toField.jd)) {
		this.containerRow.errorMsg[this.containerRow.errorMsg.length]="'to' date is older than 'from' date";
		this.fromError=true;
		this.toError=true;
		dateRow.fromField.error=true;
		dateRow.toField.error=true;
	}
	
	if (dateRow.fromField.jd > todayJD) {
		this.containerRow.errorMsg[this.containerRow.errorMsg.length]="'from' date is futuristic";
		this.fromError=true;
		dateRow.fromField.error=true;
	}
	
	if (dateRow.toField.jd > todayJD) {
		this.containerRow.errorMsg[this.containerRow.errorMsg.length]="'to' date is futuristic";
		this.toError=true;
		dateRow.toField.error=true;
	}
};

/**
 * sets field styling to highlight problem dates, based on previously set error flag
 */
field.ColDate.prototype.apply_error_state =function () { 
	var error=this.fromError||this.toError;
	var l=this.dateTable.rows.length;
	for (var n=0; n<l;n++) {
		(this.dateTable.rows[n].fromField.error)?this.flag_error(this.dateTable.rows[n].fromField):this.clear_error(this.dateTable.rows[n].fromField);
		(this.dateTable.rows[n].toField.error)?this.flag_error(this.dateTable.rows[n].toField):this.clear_error(this.dateTable.rows[n].toField);
	
		error =error || this.dateTable.rows[n].fromField.error || this.dateTable.rows[n].toField.error;
	}
	return error;
};

/**
 * highlight date input element as erroneous
 * @todo should do this with classes rather than by direct style meddling 
 */
field.ColDate.prototype.flag_error =function (element) {
	element.style.backgroundColor="#ff9999";
	this.containerRow.error=true;
	this.set_style();
};

/**
 * @todo need to fix this - currently row error state may not be correctly updated
 */
field.ColDate.prototype.clear_error =function (element)
{
	element.style.backgroundColor="white";
	//this.containerRow.error=this.fromError || this.toError;
	
	/* NEED TO FIX */
	
	this.set_style();
};

/**
 * add a date entry
 * @param <xml coldate> colDateXML
 */
field.ColDate.prototype.add_from_xml =function (colDateXML) {
	/*
	 <coldate><from value="" jd=""/><to value="" jd=""/><datenotes></datenotes></coldate> 
	*/
	
	if (colDateXML.length ===0) {
		this.add_blank(); // create a default entry
	} else {
		var l=colDateXML.length;
		var row,dateXML,noteXML;
		for (var n=0; n<l; n++) {
			row=this.add_blank();
			dateXML=colDateXML[n].getElementsByTagName('from');
			
			if (dateXML.length) {
				row.fromField.value=dateXML[0].getAttribute('value');
				row.fromField.jd=dateXML[0].getAttribute('jd');
			}
			
			dateXML=colDateXML[n].getElementsByTagName('to');
			
			if (dateXML.length) {
				row.toField.value=dateXML[0].getAttribute('value');
				row.toField.jd=dateXML[0].getAttribute('jd');
			}
			
			noteXML=colDateXML[n].getElementsByTagName('datenotes');
			if (noteXML.length && noteXML[0].hasChildNodes() && noteXML[0].firstChild.data) {
				row.notesField.value=noteXML[0].firstChild.data;
				row.notesField.style.color='black';
				this.notesShown=true;
			}
		}
		
		if (l >1) {this.notesShown=true;}
		
		if (this.notesShown) {this.show_notes();}
		this.hide_superfluous_buttons();
	}
};

/**
 * generate textual representations of dates for thumbnail summary 
 * @return html fragment, or null if no dates
 */
field.ColDate.prototype.get_html = function () {
	var l=this.dateTable.rows.length;
	var ul=document.createElement('ul');
	var liCount=0;

	for (var n=0; n<l;n++) {
		var dateString='';
		if (this.dateTable.rows[n].fromField.value) {
			dateString=this.dateTable.rows[n].fromField.value;
		}
		
		if (this.dateTable.rows[n].toField.value) {
			dateString+=' - '+this.dateTable.rows[n].fromField.value;
		}
		var dateString=dateString.replace(trimRegex,''); // trim leading/trailing spaces
		
		if (this.dateTable.rows[n].notesField.value && this.dateTable.rows[n].notesField.value!=this.dateTable.rows[n].notesField.defaultValue) {
			dateString+=' "'+this.dateTable.rows[n].notesField.value+'"';
		}
		
		if (dateString) {
			liCount++;
			ul.appendChild(document.createElement('li')).appendChild(document.createTextNode(dateString));
		}
	}
	
	if (ul.hasChildNodes()) {
		if (liCount===1) {ul.className='inlineul';}
		return ul;
	} else {
		return null;
	}
};

/**
 * generate XML serialization of dates
 * @param <xml root node> doc
 * @return <xml fragment>
 */
field.ColDate.prototype.get_xml = function (doc) {
	var l=this.dateTable.rows.length;
	var colDate,dateElement;

	var dateSet=doc.createDocumentFragment();
	
	for (var n=0; n<l;n++) {
		colDate=dateSet.appendChild(doc.createElement('coldate'));
		
		if (this.dateTable.rows[n].fromField.value) {
			dateElement=colDate.appendChild(doc.createElement('from'));
			dateElement.setAttribute('value',this.dateTable.rows[n].fromField.value);
			
			if (this.dateTable.rows[n].fromField.jd) {
				dateElement.setAttribute('jd',this.dateTable.rows[n].fromField.jd);
			}
		}
		
		if (this.dateTable.rows[n].toField.value) {
			dateElement=colDate.appendChild(doc.createElement('to'));
			dateElement.setAttribute('value',this.dateTable.rows[n].toField.value);
			
			if (this.dateTable.rows[n].toField.jd) {
				dateElement.setAttribute('jd',this.dateTable.rows[n].toField.jd);
			}
		}
		
		if (this.dateTable.rows[n].notesField.value && this.dateTable.rows[n].notesField.value!=this.dateTable.rows[n].notesField.defaultValue) {
			dateElement=colDate.appendChild(doc.createElement('datenotes'));
			dateElement.appendChild(doc.createTextNode(this.dateTable.rows[n].notesField.value));
		}
	}
	
	return dateSet;
};

field.ColDate.prototype.destroy = function () {
	this.form=null;

	var l=this.dateTable.rows.length;
	for (var n=l-1; n>=0; n--) {
		this.destroy_date_row(this.dateTable.rows[n]);
	}
	
	this.dateTable.parentNode.removeChild(this.dateTable);
	this.dateTable=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
};

/**
 * destruct the given date row (from/to/notes triplet)
 * @param <date table row> row
 */
field.ColDate.prototype.destroy_date_row = function (row) {
	if (row.fromChangeHandler!==null) {row.fromChangeHandler=remove_event_handler(row.fromChangeHandler);}
	if (row.toChangeHandler!==null) {row.toChangeHandler=remove_event_handler(row.toChangeHandler);}
	if (row.addClickHandler!==null) {row.addClickHandler=remove_event_handler(row.addClickHandler);}
	if (row.deleteClickHandler!==null) {row.deleteClickHandler=remove_event_handler(row.deleteClickHandler);}
	if (row.moreClickHandler!==null) {row.moreClickHandler=remove_event_handler(row.moreClickHandler);}
	
	row.fromField.parentNode.removeChild(row.fromField);
	row.toField.parentNode.removeChild(row.toField);
	
	if (row.plusButton) {row.plusButton.parentNode.removeChild(row.plusButton);
		row.plusButton=null;
	}
	if (row.moreButton) {row.moreButton.parentNode.removeChild(row.moreButton);
		row.moreButton=null;
	}
	
	row.fromField.jd=null;
	row.toField.jd=null;
		
	row.fromField.error=null;
	row.toField.error=null;

	row.notesField.defaultValue=null;
	row.notesField.onfocus=null;
	row.notesField.onblur=null;
	row.notesField.parentNode.removeChild(row.notesField);

	row.fromField=null;
	row.toField=null;
	row.notesField=null;
	
	row.fromKeyHandler=remove_event_handler(row.fromKeyHandler);
	row.toKeyHandler=remove_event_handler(row.toKeyHandler);
	
	row.parentNode.removeChild(row);
};

/**
 * click handler, called to delete (and if none left, recreate) a data row, specified by ID
 * @param <event> event
 * @param <element> element
 * @param <string> rowId unique id of row to delete
 */
field.ColDate.prototype.delete_click = function (event, element, rowId) {
	this.destroy_date_row(document.getElementById(rowId));

	if (this.dateTable.rows.length===0) {this.add_blank();}
	this.hide_superfluous_buttons();
	this.form.verify(); // if deleted row had error then need to clear this
};

field.ColDate.prototype.add_click = function (event, element, param) {
	this.add_blank();
	this.hide_superfluous_buttons();
};

/**
 * click handler, called to expand the date row, to show the 'notes' field
 */
field.ColDate.prototype.more_click = function (event, element) {
	this.notesShown=true;
	this.show_notes();
};

field.ColDate.prototype.show_notes = function () {
	var l=this.dateTable.rows.length;	
	for (var n=0; n<l;n++) {
		if (this.dateTable.rows[n].moreButton) {
			this.dateTable.rows[n].moreButton.style.display='none';
		}
		this.dateTable.rows[n].notesField.style.display='inline';
	}
	
	this.hide_superfluous_buttons();
};

/**
 * hide the 'add row' button for all but the last row
 */
field.ColDate.prototype.hide_superfluous_buttons = function (){
	for (var n=this.dateTable.rows.length-2; n>=0;n--) {
		this.dateTable.rows[n].plusButton.style.display='none';
	}
	this.dateTable.rows[this.dateTable.rows.length-1].plusButton.style.display='inline';
};

/**
 * returns the current date as a Julian Day Number (JD)
 * @return JD
 */
function current_date_JD()
{
	var date=new Date();
	return to_JD(date.getFullYear(),date.getMonth()+1,date.getDate());
}

/**
 * calculate Julian day number from specified year, month, day
 * @param <int> year
 * @param [int] month
 * @param [int] day
 * @return JD
 */
function to_JD(year,month,day)
{
	if (!month) {month=1;}
	if (!day) {day=1;}

	var a = Math.floor((14-month)/12);
	var y = year+4800-a;
	var m = month + (12*a) - 3;

	return day + Math.floor((153*m+2)/5) + (y*365) + Math.floor(y/4) - Math.floor(y/100) + Math.floor(y/400) - 32045;
}

/**
 * calculate year from specified Julian day number
 * @param <int> jd
 * @return year or empty string
 */
function year_from_JD(jd)
{
/*
	$a =  $JD + 32044;
	$b = floor((4*$a+3)/146097);
	$c = $a - floor(($b*146097)/4);
	$d = floor((4*$c+3)/1461);
	$e = $c - floor((1461*$d)/4);
	$m = floor((5*$e+2)/153);
	$month = $m + 3 - (12*floor($m/10));
	$year = ($b*100) + $d - 4800 + floor($m/10);
*/
	
	if (jd >0) {
		var a = parseInt(jd,10) + 32044;
		var b = Math.floor((4*a+3)/146097);
		var c = a - Math.floor((b*146097)/4);
		var d = Math.floor((4*c+3)/1461);
		var e = c - Math.floor((1461*d)/4);
		var m = Math.floor((5*e +2)/153);
		return ((b*100) + d - 4800 + Math.floor(m/10));
	}
	else {
		return '';
	}
}

/**
 * if element is empty or default then set to it's default value and shade text
 * @param <html input element> element
 * @todo should use css class rather than direct style manipulation
 */
function input_set_default(element)
{
	if (!element.value || element.value==element.defaultValue) {
		element.value=element.defaultValue;
		element.style.color="gray";
	}
}

/**
 * called as focus event handler, with 'this' set to the focused element
 * clear any default value and set text colour
 */
function input_default_focus()
{
	if (this.value==this.defaultValue || this.value==='') {
		this.value='';
		this.style.color="black";
	}
}

/**
 * called as blur event handler, with 'this' set to the blurred element
 * if value is empty then substitute default value and set text colour
 */
function input_default_blur()
{
	if (this.value==='') {
		this.value=this.defaultValue;
		this.style.color="gray";
	}
}

/**
 * generic menu field
 * @constructor
 */
field.MenuField = function (form,manifestXml) {
	this.form=form;
	
	this.rowName=manifestXml.getAttribute('name');
	this.labelName=manifestXml.getAttribute('rowlabel');
	this.helpText=manifestXml.getAttribute('helptext');
	this.longHelp=manifestXml.getAttribute('longhelp');
	
	this.construct_row();
	
	var cell=document.createElement("td");
	
	this.menuField=cell.appendChild(document.createElement("select"));
	this.menuField.style.verticalAlign="top";
	
	var menuElement = manifestXml.getElementsByTagName('menu');
	
	if (menuElement.length === 0) {throw new Error('herb@home: missing manifest menu element');}
	else {
		this.menuField.multiple = !!menuElement[0].getAttribute('multiple');
		
		var options=menuElement[0].getElementsByTagName('option');
		for (var i=0, l=options.length; i<l; i++) {
			this.menuField.options[i]=new Option(options[i].hasChildNodes()?options[i].firstChild.data:'',options[i].getAttribute('value'),options[i].getAttribute('default'),options[i].getAttribute('default'));
		}
		this.row.appendChild(cell);
		this.row.changeHandler=register_event_handler(this.menuField,'change',this.form,"change",0);
	}
};

field.MenuField.prototype = new Row();
field.MenuField.prototype.constructor=field.MenuField;
field.MenuField.prototype.xmlTagName='attribute';

/**
 * destructor
 */
field.MenuField.prototype.destroy =function() {
	if (this.row.changeHandler) {this.row.changeHandler=remove_event_handler(this.row.changeHandler);}
	this.menuField=null;
	
	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};

/**
 * create and populate menu form field from xml object
 * the value (or comma-separated values) of menuXML are used to set the menu selection
 * @param <xml menu selection> menuXML
 */
field.MenuField.prototype.add_from_xml = function(menuXML) {
	for (var m=0, t=menuXML.length;m<t;m++) {
		if (menuXML[m].getAttribute('name') === this.rowName) {		
			// menu only allows a single selection at a time
			if (this.menuField.selectedIndex!==null && this.menuField.selectedIndex>-1) {
				// clear previous selection (typically the default)
				this.menuField.options[this.menuField.selectedIndex].selected=false;
			}
		
			if (menuXML[m].hasChildNodes()) {
				var values=menuXML[m].firstChild.data.split(',');
				for (var i=0, l=values.length;i<l;i++) {
					for (var n=0, ml=this.menuField.options.length;n<ml;n++) {
						if (values[i] == this.menuField.options[n].value) {
							this.menuField.options[n].selected = true;
							if (values[i]) {this.iconify = false;} // have non-default so de-iconify
							break;
						}
					}
				}
			}	
			break;
		}
	}	
};

/**
 * return menu state, serialized as xml
 * @param <xml document element> doc
 */
field.MenuField.prototype.get_xml = function(doc) {
	var menu=doc.createElement(this.xmlTagName);
	menu.setAttribute('class','menu');
	menu.setAttribute('name',this.rowName);
	var value=[];
	
	for (var n=0, ml=this.menuField.options.length;n<ml;n++) {
		if (this.menuField.options[n].selected) {
			value.push(this.menuField.options[n].value);
		}
	}
	
	menu.appendChild(doc.createTextNode(value.join(',')));	
	return (menu);
};

/**
 * generate html representation of flower state for thumbnail summary 
 * @return html fragment, or null if undefined
 */
field.MenuField.prototype.get_html = function () {
	var value=[];
	
	for (var n=0, ml=this.menuField.options.length;n<ml;n++) {
		if (this.menuField.options[n].selected && this.menuField.options[n].value) {
			value.push(this.menuField.options[n].text);
		}
	}
	
	var fragment=document.createDocumentFragment();
	if (value.length) {
		fragment.appendChild(document.createTextNode(value.join(',')));
		return fragment;
	}
	return null;
};

/**
 * 'accession number' doc form field
 * currently this is MANCH specific - needs to be generalised
 * @todo make this less specific to MANCH
 * @param <SpecimenForm> form parent doc form
 * @constructor
 */
field.AccNumber =function (form) {
	this.form=form;
	this.construct_row();
	var accCell=document.createElement("td");
	this.accField=document.createElement("input");
	this.accField.defaultValue="e.g. Kk000";
	accCell.appendChild(this.accField);

	input_set_default(this.accField);
	this.accField.onfocus=input_default_focus;
	this.accField.onblur=input_default_blur;

	this.row.appendChild(accCell);
	this.row.keyHandler=register_event_handler(this.accField,'keydown',this,"key",0);
	this.row.changeHandler=register_event_handler(this.accField,'change',this.form,"change",0);
};

field.AccNumber.prototype = new Row();
field.AccNumber.prototype.constructor = field.AccNumber;
field.AccNumber.prototype.xmlTagName='accno';

field.AccNumber.prototype.labelName='accession num'; // row label
field.AccNumber.prototype.helpText="Musuem accession number starting with k or kk, e.g .'Kk803'. Missing from many sheets."; // short summary help text
field.AccNumber.prototype.longHelp='acchelp'; // message index key to lookup verbose help message

/**
 * create and populate 'accession number' doc form field from xml object
 * @param <xml acc no> accXML
 */
field.AccNumber.prototype.add_from_xml = function(accXML) {	
	if (accXML.length===1 && accXML[0].hasChildNodes()) {
		this.accField.value=accXML[0].firstChild.data;
		this.accField.style.color="black";
	}
};

/**
 * return acc no, serialized as xml
 * @param <xml root element> doc
 * @return xml accno
 */
field.AccNumber.prototype.get_xml = function(doc) {
	var element=doc.createElement(this.xmlTagName);
	if (this.accField.value !== this.accField.defaultValue) {
		element.appendChild(doc.createTextNode(this.accField.value));
	} else {
		element.appendChild(doc.createTextNode(""));
	}
	
	return (element);
};

/**
 * return accession number as html fragment
 * @return html fragment or null
 */
field.AccNumber.prototype.get_html = function() {
	if (this.accField.value !== this.accField.defaultValue) {
		var fragment=document.createDocumentFragment();
		fragment.appendChild(document.createTextNode(this.accField.value));
		return fragment;
	} else {
		return null;
	}
};

/**
 * 
 * @return <boolean> true if error
 */
field.AccNumber.prototype.verify = function() {
	return 0; // no error;
};

/**
 * destructor
 */
field.AccNumber.prototype.destroy = function () {
	this.accField.onfocus=null;
	this.accField.onblur=null;

	this.accField.parentNode.removeChild(this.accField);
	this.accField.defaultValue=null;
	this.accField=null;

	this.row.keyHandler=remove_event_handler(this.row.keyHandler);
	this.row.changeHandler=remove_event_handler(this.row.changeHandler);

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};


// only allow 0-9 and K
if (standardBrowser)
{
	field.AccNumber.prototype.key =function (event, element, param) {
		if (!((event.keyCode <=58) || (event.keyCode == 75) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105) )) {event.preventDefault();}
	};
}
else
{
	field.AccNumber.prototype.key =function (event, element, param) {
		event.returnValue = (event.keyCode <=58) || (event.keyCode == 75) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105);
	};
}


/**
 * 'accession number' doc form field
 * BON version (acc code formated as nn-nn-nnnnn) display in three input boxes
 * @todo make this inherit from accField
 * @param <SpecimenForm> form parent doc form
 * @constructor
 */
field.BonAccNumber = function (form) {
	this.form=form;
	this.construct_row();
	var accCell=document.createElement("td");
	this.accField1=document.createElement("input");
	this.accField1.defaultValue="";
	this.accField1.maxLength = 4;
	this.accField1.style.width = "3em";
	this.accField2=document.createElement("input");
	this.accField2.defaultValue="";
	this.accField2.maxLength=4;
	this.accField2.style.width = "3em";
	this.accField3=document.createElement("input");
	this.accField3.defaultValue="";
	this.accField3.maxLength=6;
	this.accField3.style.width = "5em";
	accCell.appendChild(this.accField1);
	accCell.appendChild(this.accField2);
	accCell.appendChild(this.accField3);

	input_set_default(this.accField1);
	input_set_default(this.accField2);
	input_set_default(this.accField3);
	this.accField1.onfocus=input_default_focus;
	this.accField1.onblur=input_default_blur;
	this.accField2.onfocus=input_default_focus;
	this.accField2.onblur=input_default_blur;
	this.accField3.onfocus=input_default_focus;
	this.accField3.onblur=input_default_blur;

	this.row.appendChild(accCell);
	this.row.keyHandler=register_event_handler(this.accField1,'keydown',this,"key",0);
	this.row.changeHandler=register_event_handler(this.accField1,'change',this,"change",0);
	this.row.keyHandler=register_event_handler(this.accField2,'keydown',this,"key",0);
	this.row.changeHandler=register_event_handler(this.accField2,'change',this,"change",0);
	this.row.keyHandler=register_event_handler(this.accField3,'keydown',this,"key",0);
	this.row.changeHandler=register_event_handler(this.accField3,'change',this,"change",0);
};

field.BonAccNumber.prototype = new Row();
field.BonAccNumber.prototype.constructor = field.BonAccNumber;
field.BonAccNumber.prototype.xmlTagName='accno';

field.BonAccNumber.prototype.labelName='accession num'; // row label
field.BonAccNumber.prototype.helpText="Museum accession number e.g. 00 00 00000 or 000 0000 000"; // short summary help text
field.BonAccNumber.prototype.longHelp='bonacchelp'; // message index key to lookup verbose help message

/**
 * create and populate 'accession number' doc form field from xml object
 * @param <xml acc no> accXML
 */
field.BonAccNumber.prototype.add_from_xml = function(accXML) {	
	if (accXML.length===1 && accXML[0].hasChildNodes()) {
		// BON acc consists of a hyphen-separated sequence
		var acc=accXML[0].firstChild.data.split('-', 3);
		
		if (acc[0]) {
			this.accField1.value=acc[0];
			this.accField1.style.color="black";
		}
		
		if (acc[1]) {
			this.accField2.value=acc[1];
			this.accField2.style.color="black";
		}
		
		if (acc[2]) {
			this.accField3.value=acc[2];
			this.accField3.style.color="black";
		}
	}
};

field.BonAccNumber.prototype.change =function (event, element, field) {
	this.form.verify(); // check dates across form
	this.form.parentData.set_modified(true);
};

field.BonAccNumber.prototype.to_separated_string = function() {
	var formattedString = '';
	
	if (this.accField3.value != this.accField3.defaultValue) {
		formattedString ='-'+this.accField3.value;
	}
	
	if (this.accField2.value != this.accField2.defaultValue) {
		formattedString ='-'+this.accField2.value+formattedString;
	}
	
	if (this.accField1.value != this.accField1.defaultValue) {
		formattedString =this.accField1.value+formattedString;
	}
	
	return formattedString;
};

/**
 * return acc no, serialized as xml
 * @param <xml root element> doc
 * @return xml accno
 */
field.BonAccNumber.prototype.get_xml = function(doc) {
	var element=doc.createElement(this.xmlTagName);
	
	element.appendChild(doc.createTextNode(this.to_separated_string()));
	return (element);
};

/**
 * return accession number as html fragment
 * @return html fragment or null
 */
field.BonAccNumber.prototype.get_html = function() {
	var accString = this.to_separated_string();
	
	if (accString != "") {
		var fragment=document.createDocumentFragment();
		fragment.appendChild(document.createTextNode(accString));
		return fragment;
	} else {
		return null;
	}
};

/**
 * 
 * @return TRUE on error
 */
field.BonAccNumber.prototype.verify = function () {
	
	if (this.accField1.value == this.accField1.defaultValue || this.accField2.value == this.accField2.defaultValue || this.accField3.value == this.accField3.defaultValue) {
		this.containerRow.error = true;
		this.containerRow.errorMsg[0] = "Please fill in the accession number, or 00 00 0000 if this is missing from the sheet.";
	} else {
		this.containerRow.error = false;
		this.containerRow.errorMsg = [];
	}

	this.set_style(); // force immediate styling update
	return this.containerRow.error;
};

/**
 * destructor
 */
field.BonAccNumber.prototype.destroy = function () {
	this.accField1.onfocus=null;
	this.accField1.onblur=null;
	
	this.accField2.onfocus=null;
	this.accField2.onblur=null;
	
	this.accField3.onfocus=null;
	this.accField3.onblur=null;

	this.accField1.parentNode.removeChild(this.accField1);
	this.accField1.defaultValue=null;
	this.accField1=null;
	
	this.accField2.parentNode.removeChild(this.accField2);
	this.accField2.defaultValue=null;
	this.accField2=null;
	
	this.accField3.parentNode.removeChild(this.accField3);
	this.accField3.defaultValue=null;
	this.accField3=null;

	this.row.keyHandler=remove_event_handler(this.row.keyHandler);
	this.row.changeHandler=remove_event_handler(this.row.changeHandler);

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};


// only allow 0-9
if (standardBrowser)
{
	field.BonAccNumber.prototype.key =function (event, element, param) {
		if (!((event.keyCode <=58) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105) )) {event.preventDefault();}
	};
}
else
{
	field.BonAccNumber.prototype.key =function (event, element, param) {
		event.returnValue = (event.keyCode <=58) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105);
	};
}

/**
 * 'accession number' doc form field
 * GOD version (acc code formated as nnnnnn)
 * @todo make this inherit from accField
 * @param <SpecimenForm> form parent doc form
 * @constructor
 */
field.GodAccNumber = function (form) {
	this.form=form;
	this.construct_row();
	var accCell=document.createElement("td");
	this.accField=document.createElement("input");
	this.accField.defaultValue = "nnnn";
	this.accField.maxLength = 6;
	this.accField.style.width = "5em";
	accCell.appendChild(this.accField);

	input_set_default(this.accField);
	this.accField.onfocus=input_default_focus;
	this.accField.onblur=input_default_blur;

	this.row.appendChild(accCell);
	this.row.keyHandler = register_event_handler(this.accField,'keydown',this,"key",0);
	this.row.changeHandler = register_event_handler(this.accField,'change',this,"change",0);
};

field.GodAccNumber.prototype = new Row();
field.GodAccNumber.prototype.constructor = field.GodAccNumber;
field.GodAccNumber.prototype.xmlTagName='accno';

field.GodAccNumber.prototype.labelName='barcode num'; // row label
field.GodAccNumber.prototype.helpText="Barcode number e.g. 1234"; // short summary help text
field.GodAccNumber.prototype.longHelp='godacchelp'; // message index key to lookup verbose help message

/**
 * create and populate 'accession number' doc form field from xml object
 * @param <xml acc no> accXML
 */
field.GodAccNumber.prototype.add_from_xml = function(accXML) {	
	if (accXML.length === 1 && accXML[0].hasChildNodes()) {
		this.accField.value = accXML[0].firstChild.data;
		this.accField.style.color = "black";
	}
};

field.GodAccNumber.prototype.change =function (event, element, field) {
	this.form.verify(); // check dates across form
	this.form.parentData.set_modified(true);
};

/**
 * return acc no, serialized as xml
 * @param <xml root element> doc
 * @return xml accno
 */
field.GodAccNumber.prototype.get_xml = function(doc) {
	var element=doc.createElement(this.xmlTagName);
	
	if (this.accField.value != this.accField.defaultValue) {
		element.appendChild(doc.createTextNode(this.accField.value));
	}
	
	return (element);
};

/**
 * return accession number as html fragment
 * @return html fragment or null
 */
field.GodAccNumber.prototype.get_html = function() {
	if (this.accField.value != this.accField.defaultValue) {
		var fragment=document.createDocumentFragment();
		fragment.appendChild(document.createTextNode(this.accField.value));
		return fragment;
	} else {
		return null;
	}
};

/**
 * 
 * @return TRUE on error
 */
field.GodAccNumber.prototype.verify = function () {
	
	if (this.accField.value == this.accField.defaultValue || this.accField.value == '') {
		this.containerRow.error = true;
		this.containerRow.errorMsg[0] = "Please fill in the barcode number.";
	} else {
		this.containerRow.error = false;
		this.containerRow.errorMsg = [];
	}

	this.set_style(); // force immediate styling update
	return this.containerRow.error;
};

/**
 * destructor
 */
field.GodAccNumber.prototype.destroy = function () {
	this.accField.onfocus=null;
	this.accField.onblur=null;

	this.accField.parentNode.removeChild(this.accField);
	this.accField.defaultValue=null;
	this.accField = null;

	this.row.keyHandler=remove_event_handler(this.row.keyHandler);
	this.row.changeHandler=remove_event_handler(this.row.changeHandler);

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};


// only allow 0-9
if (standardBrowser)
{
	field.GodAccNumber.prototype.key =function (event, element, param) {
		if (!((event.keyCode <=58) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105) )) {event.preventDefault();}
	};
}
else
{
	field.GodAccNumber.prototype.key =function (event, element, param) {
		event.returnValue = (event.keyCode <=58) || (event.keyCode == 107) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105);
	};
}



/**
 * 'determination' doc form field
 * @todo support multiple determiners; add notes field; consider authority field
 * @param <SpecimenForm> form parent doc form
 * @constructor
 */
field.Taxon = function (form,manifestXml) {	
	this.form=form;
	this.expanded=false; 
	this.construct_row();

	this.preferredGroupName=uniqueId('preftax'); // common 'name' shared by preferred radio group

	var taxonCell=document.createElement("td");
	this.button=document.createElement("input");
	this.button.type="checkbox";

	this.defaultTaxonName='';
	this.defaultTaxonId='';
	this.defaultTaxonAuthority='';

	// list of alternative dets (each pair is a table row)
	this.taxonTable=taxonCell.appendChild(document.createElement("table"));
	this.taxonTable.appendChild(document.createElement("tbody"));

	this.row.appendChild(taxonCell);
	
	if (manifestXml.getAttribute('searchtype')) {
		this.taxonSearchType=manifestXml.getAttribute('searchtype');
	} else {
		this.taxonSearchType='kent'; // either 'kent' or 'broad' match
	}
};

field.Taxon.prototype = new Row();
field.Taxon.prototype.constructor = field.Taxon;
field.Taxon.prototype.xmlTagName='determined';

field.Taxon.prototype.labelName='taxon'; // row label
field.Taxon.prototype.helpText="Please accept the default taxon name unless you can determine that this identification is incorrect. N.B. the specimen's label may well show a different obsolete name, which you may ignore."; // short summary help text
field.Taxon.prototype.longHelp='taxonhelp'; // message index key to lookup verbose help message

field.Taxon.prototype.populate_default = function () {
	var row=this.add_blank(true);
    row.detList.add_blank();
	
	if (this.previousTaxonName) {
		row=this.add_blank(false);
        row.detList.add_blank();
		row.taxonBox.taxonId=this.previousTaxonId;
		row.taxonBox.input.value=this.previousTaxonName;
		row.taxonBox.authority=this.previousTaxonAuthority;
		row.taxonBox.commonName=this.previousTaxonCommonName;
		row.taxonBox.input.style.color='green';
		row.taxonBox.input.setAttribute('title',this.previousTaxonName+" "+this.previousTaxonAuthority+" "+this.previousTaxonCommonName);
	}
	this.hide_superfluous_buttons();
};

/**
 * create and populate default determination field
 * @param <bool> preferred state of the 'preferred name' tick box
 */
field.Taxon.prototype.add_blank = function (preferred) {
	var row=this.taxonTable.insertRow(this.taxonTable.rows.length);
	row.id=uniqueId('tax');
    row.detList=new DetList(row,this.form);

	row.taxonBox= new TaxonBox(this.defaultTaxonName,this.defaultTaxonId,this.defaultTaxonAuthority,this.defaultTaxonCommonName);
	row.taxonBox.dropbox.style.width="28em";
	
	if (this.taxonSearchType=='broad') {
		row.taxonBox.searchURL='XMLbroadtaxonsearchAtHome';
	}
	row.taxonBox.input.style.color='green'; // default value is valid so should be green

	var cell=row.insertCell(0);
	cell.appendChild(row.taxonBox.container);

	row.preferredLabel=document.createElement('label');
	row.preferredLabel.innerHTML='<input type="radio" name="'+this.preferredGroupName+'" value="preferred"'+(preferred?' checked':'')+'>preferred';
	cell.appendChild(row.preferredLabel);
	row.preferred=row.preferredLabel.getElementsByTagName('input')[0];
	
	var button=deleteButton('delete determination');
	button.style.verticalAlign='top';
	cell.appendChild(button);
	row.deleteClickHandler=register_event_handler(button,'click',this,"delete_click",row.id);

	row.addButton=plusButton('add determination');
	row.addButton.style.verticalAlign='top';
	cell.appendChild(row.addButton);
	row.addClickHandler=register_event_handler(row.addButton,'click',this,"add_click",0);
	row.jd=null;
	row.taxonChangeHandler=register_event_handler(row.taxonBox.input,'change',this.form,"change",0);
	
	row.table=document.createElement('table');
	row.table.appendChild(document.createElement("tbody"));
	detRow=row.table.insertRow(0);
	detRow.insertCell(0).appendChild(document.createTextNode("identified by"));
    detRow.insertCell(1).appendChild(row.detList.table);
	
	var dateRow=row.table.insertRow(1);
	dateRow.insertCell(0).appendChild(document.createTextNode("identification date"));
	var dateCell=dateRow.insertCell(1);
	
	row.date=document.createElement("input");
	row.date.style.width="6em";
	dateCell.appendChild(row.date);
	
	var notesRow=row.table.insertRow(2);
	notesRow.insertCell(0).appendChild(document.createTextNode("notes"));
	var noteCell=notesRow.insertCell(1);
	
	row.notes=document.createElement("input");
	row.notes.style.width="20em";
	noteCell.appendChild(row.notes);	
	
	if (!this.expanded) {
		row.moreButton=more_button('show all fields');
		row.moreButton.style.verticalAlign='top';
		cell.appendChild(row.moreButton);
		row.moreClickHandler=register_event_handler(row.moreButton,'click',this,"more_click",0);
		row.table.style.display="none";
	} else {
		row.moreClickHandler=null; //placeholder
	}
	
	cell.appendChild(document.createElement('br'));
	cell.appendChild(row.table);
	
	row.dateChangeHandler=register_event_handler(row.date,'change',this,"date_change",row.id);
	row.dateKeyHandler=register_event_handler(row.date,'keydown',this,"key",'');

	return(row);
};

/**
 * click handler, called to expand the taxon row to show determiner name(s), date and notes fields 
 */
field.Taxon.prototype.more_click = function (event, element) {
	this.expanded=true;
	this.show_expanded();
};

field.Taxon.prototype.show_expanded = function () {
	var l=this.taxonTable.rows.length;	
	for (var n=0; n<l;n++) {
		if (this.taxonTable.rows[n].moreButton) {
			this.taxonTable.rows[n].moreButton.style.display='none';
		}
		
		try {
			this.taxonTable.rows[n].table.style.display='table';
		} catch(e) {
			this.taxonTable.rows[n].table.style.display='block';
		}
	}
};

/**
 * create and populate determination entries from xml fragment
 * @param <taxa XML> taxaXML
 */
field.Taxon.prototype.add_from_xml = function (taxaXML) {
	// given a (potentially empty) array of taxa add these to the form
	
	var row;
	if (taxaXML.length ===0) {
		row=this.add_blank(true); // create a default entry
		row.detList.add_blank();
		
		if (this.previousTaxonName) {
			row=this.add_blank(false);
			row.detList.add_blank();
			
			row.taxonBox.taxonId=this.previousTaxonId;
			row.taxonBox.input.value=this.previousTaxonName;
			row.taxonBox.authority=this.previousTaxonAuthority;
			row.taxonBox.commonName=this.previousTaxonCommonName;
			row.taxonBox.input.style.color='green';
			row.preferred.checked=false;
			row.taxonBox.show_info_link();
			row.taxonBox.set_tool_tip();
		}
	} else {	
		var l=taxaXML.length;
		var taxon,person,dateElement,notesElement,determiners;
		
		for (var n=0;n<l;n++) {
			taxon=taxaXML[n].getElementsByTagName('taxon')[0];
			row=this.add_blank(taxon.getAttribute('preferred')=='true'); // create a blank entry

			row.taxonBox.taxonId=taxon.getAttribute('id');
			row.taxonBox.authority=taxon.getAttribute('authority');
			row.taxonBox.commonName=taxon.getAttribute('vulgar');
			
			if (row.taxonBox.authority===null) {row.taxonBox.authority="";}
			if (row.taxonBox.commonName===null) {row.taxonBox.commonName="";}
			
			if (taxon.hasChildNodes()) {
				row.taxonBox.input.value=taxon.firstChild.data;
				
				if (row.taxonBox.taxonId) {
					row.taxonBox.input.style.color='green';
					row.taxonBox.show_info_link();
				} else {
					row.taxonBox.input.style.color='black';
					row.taxonBox.hide_info_link();
					row.taxonBox.show_search_links();
				}
			}
			
			row.taxonBox.set_tool_tip();

			dateElement=taxaXML[n].getElementsByTagName('date');
			if (dateElement.length >0 && dateElement[0].hasChildNodes()) {
				this.expanded=true;
				
				row.date.value=dateElement[0].firstChild.data;
				row.jd=dateElement[0].getAttribute('jd');
			}

			determiners=taxaXML[n].getElementsByTagName('determiner');
			if (determiners.length >0) {
				this.expanded=true;
				
				var detL=determiners.length;
				for (var detN=0; detN<detL; detN++) {
					var detRow=row.detList.add_blank();
					detRow.nameBox.illegibleButton.checked=(determiners[detN].getAttribute('illegible')==1 || determiners[detN].getAttribute('illegible')=="true");
					
					person=determiners[detN].getElementsByTagName('person');
					if (person.length) {
						detRow.nameBox.populate_from_xml(person[0]);
					}
					detRow.deleteButton.style.display="inline";
				}
                row.detList.hide_superfluous_buttons();
			} else {
				row.detList.add_blank();
			}
			
			notesElement=taxaXML[n].getElementsByTagName('detnotes');
			if (notesElement.length && notesElement[0].hasChildNodes()) {
				row.notes.value=notesElement[0].firstChild.data;
                this.expanded=true;
			}
		}
	}
	this.hide_superfluous_buttons();
    if (this.expanded) {
        this.show_expanded();
    }
};

/**
 * generate html representation of determinations for thumbnail summary
 * @return html fragment, or null if no entries
 * @todo need determinations and preferred status
 */
field.Taxon.prototype.get_html = function () {
	var fragment=document.createElement('ul');
	var l=this.taxonTable.rows.length;

	for (var n=0; n<l;n++) {
        var taxonRow=this.taxonTable.rows[n];
		var taxonString=taxonRow.taxonBox.input.value;
		if (taxonRow.taxonBox.authority) {taxonString+=" "+taxonRow.taxonBox.authority;}
		
		if (taxonRow.taxonBox.commonName.length >0) {
			taxonString+=' "'+taxonRow.taxonBox.commonName+'"';
		}
		
		if (taxonString) {
            var determiners=taxonRow.detList.get_text();
            if (determiners) {
                taxonString+=" by "+determiners;
            }

			fragment.appendChild(document.createElement('li')).appendChild(document.createTextNode(taxonString));
		}
	}

	if (fragment.hasChildNodes()) {
		return fragment;
	} else {
		return null;
	}
};

/**
 * returns summary of content as html div block (with row label as title attribute)
 * @return html tr|null if no content
 */
field.Taxon.prototype.get_html_div = function () {
	var html=this.get_html();
	
	if (html) {
		var div=document.createElement('div');
		div.className='thumblist';
		div.title=this.labelName;
		div.appendChild(html);
		return div;
	} else {return null;}
};

/**
 * @param <xml document element> doc
 * @return list of taxa (as XML)
 */
field.Taxon.prototype.get_xml = function (doc) {
	var determined=doc.createDocumentFragment();
	var l=this.taxonTable.rows.length;

	for (var n=0; n<l;n++) {
		var row=this.taxonTable.rows[n];
		var taxonDet=doc.createElement('determined');
		var taxon=doc.createElement('taxon');
		taxon.setAttribute('id',row.taxonBox.taxonId);
		taxon.setAttribute('authority',row.taxonBox.authority);
		taxon.setAttribute('vulgar',row.taxonBox.commonName);
		taxon.setAttribute('preferred',row.preferred.checked?'true':'false');
		taxon.appendChild(doc.createTextNode(row.taxonBox.input.value));
		taxonDet.appendChild(taxon);
		
		taxonDet.appendChild(row.detList.get_xml(doc));
		var detNotesElement=doc.createElement('detnotes');
		detNotesElement.appendChild(doc.createTextNode(row.notes.value));
		taxonDet.appendChild(detNotesElement);
		var dateElement=doc.createElement('date');
		if (this.taxonTable.rows[n].jd) {
			dateElement.setAttribute('jd',row.jd);
		}
		dateElement.appendChild(doc.createTextNode(row.date.value));
		taxonDet.appendChild(dateElement);

		determined.appendChild(taxonDet);
	}
	return determined;
};

// only allow 0-9 and /
if (standardBrowser)
{
	field.Taxon.prototype.key =function (event, element, param) {
		if (!((event.keyCode <=58) || (event.keyCode == 191) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105) || (event.keyCode == 111)) || (event.keyCode == 32)) {event.preventDefault();}
	};
}
else
{
	field.Taxon.prototype.key =function (event, element, param) {
		event.returnValue = ((event.keyCode <=58) || (event.keyCode == 191) || event.ctrlKey || (event.keyCode >=96 && event.keyCode<=105) || event.keyCode == 111) && (event.keyCode !=32);
	};
}

/**
 * Date change event handler
 * validate date then verify form
 * @param <event> event
 * @param <element> element
 * @param <string> rowId unique id of parent taxon table row
 */
field.Taxon.prototype.date_change = function (event, element, rowId) {
	var row=document.getElementById(rowId);

	row.jd=(element.value)?date(element.value):null;

	this.form.change();
	this.form.verify();
};

/**
 * check determination dates
 * @return boolean error state
 */
field.Taxon.prototype.verify_dates =function () {
	this.containerRow.errorMsg=[];
	this.containerRow.error=false;

	for (var n=this.taxonTable.rows.length-1; n >=0; n--) {
		var taxonRow=this.taxonTable.rows[n];
		
		if (taxonRow.date.value && !taxonRow.jd) {
			this.containerRow.errorMsg.push("date is not valid");
			this.containerRow.error=true;
			taxonRow.date.style.backgroundColor="#ff9999";
		} else {
			taxonRow.date.style.backgroundColor="white";
		}
	}

	this.set_style();
	return this.containerRow.error;
};

/**
 * remove taxon row (delete row button event handler)
 * if no rows left then add a new blank row
 * if preferred taxon deleted then set first taxon row to 'preferred'
 * @param <event> event
 * @param <element> element
 * @param <string> rowId unique id of taxon row to be deleted
 */
field.Taxon.prototype.delete_click = function (event, element, rowId) {
	var row=document.getElementById(rowId);
	var preferred=row.preferred.checked;
	this.destroy_taxon_row(row);

	if (this.taxonTable.rows.length===0) {this.add_blank(true);}
	else {
		if (preferred) {
			// need to set first remaining taxon as preferred
			this.taxonTable.rows[0].preferred.checked=true; // naff IE will probably balk at this
		}
	}
	this.hide_superfluous_buttons();
};

/**
 * add taxon click event handler
 */
field.Taxon.prototype.add_click = function (event, element, param) {
	this.add_blank(false).detList.add_blank(); // add blank row and det name
	this.hide_superfluous_buttons();
};

/**
 * set visibility of add taxon buttons and preferred state
 * (only single add button should be visible, preferred checkbox should only appear if there are multiple taxa)
 */
field.Taxon.prototype.hide_superfluous_buttons = function () {
	for (var n=this.taxonTable.rows.length-2; n>=0;n--) {
		this.taxonTable.rows[n].addButton.style.display='none';
	}
	this.taxonTable.rows[this.taxonTable.rows.length-1].addButton.style.display='inline';
	
	var prefShow=(this.taxonTable.rows.length===1)?'none':'inline';	
	for (n=this.taxonTable.rows.length-1; n>=0;n--) {
		this.taxonTable.rows[n].preferredLabel.style.display=prefShow;
	}
};

/**
 * destructor
 */
field.Taxon.prototype.destroy = function() {
	this.button=null;
	this.form=null;

	for (var n=this.taxonTable.rows.length-1; n>=0;n--) {this.destroy_taxon_row(this.taxonTable.rows[n]);}

	this.taxonTable=null;
	this.xmlRequest=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
};

/**
 * remove taxon row from taxon table
 * @param <taxon table row element> row
 */
field.Taxon.prototype.destroy_taxon_row = function (row) {
	row.addClickHandler=remove_event_handler(row.addClickHandler);
	row.addButton=null;

	row.deleteClickHandler=remove_event_handler(row.deleteClickHandler);
	
	row.taxonChangeHandler=remove_event_handler(row.taxonChangeHandler);
	//if (row.detChangeHandler) {row.detChangeHandler=remove_event_handler(row.detChangeHandler);}
	row.dateChangeHandler=remove_event_handler(row.dateChangeHandler);
	row.dateKeyHandler=remove_event_handler(row.dateKeyHandler);
	
	row.preferred=null;
	row.preferredLabel=null;

	row.taxonBox.destroy();
	row.taxonBox=null;

	row.detList.destroy();
	row.detList=null;
	
	row.date.parentNode.removeChild(row.date);
	row.date=null;

	row.parentNode.removeChild(row); // delete current row
	for (var n=row.cells.length-1;n>=0;n--) {row.deleteCell(n);}

	row.jd=null;
	row.error=null;
};

/**
 * @constructor
 */
function DetList(taxonRow,form)
{
	this.parentRow=taxonRow;
	this.parentForm=form;

	this.table=document.createElement('table');
	this.table.appendChild(document.createElement("tbody"));
}

/**
 * get list of determiners' names
 */
DetList.prototype.get_xml = function(doc) {
	var determiners=doc.createDocumentFragment();
	var l=this.table.rows.length;

	for (var n=0; n<l;n++) {
		var nameBox=this.table.rows[n].nameBox;
		if (nameBox.input.value != nameBox.input.defaultValue) {
			var nameString=nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
			if (nameString) {
				var determiner=doc.createElement('determiner');
				determiner.setAttribute('illegible',nameBox.illegibleButton.checked.toString());

				determiner.appendChild(nameBox.get_person_xml(doc)); // 'person' element
				determiners.appendChild(determiner);
			}
		}
	}
	return determiners;
};

/**
 * generate textual representations of collectors for thumbnail summary
 * @return text string, or '' if no determiners
 */
DetList.prototype.get_text = function () {
	var l=this.table.rows.length;
    var names=[];

	for (var n=0; n<l;n++) {
        var nameBox=this.table.rows[n].nameBox;
		var nameString=nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces

		var lump=(nameString && nameString!=nameBox.input.defaultValue)?nameString:'';

		if (nameBox.illegibleButton.checked) {lump+=" [illegible]";}
		if (lump) {
            names.push(lump);
		}
	}

	if (names.length) {
        if (names.length >1) {
            var lastDeterminer=names.pop();
            return names.join(', ')+' & '+lastDeterminer;
        } else {
            return names[0];
        }
	} else {
		return '';
	}
};

/**
 * add blank determiner name to det list
 */
DetList.prototype.add_blank = function () {
	var row=this.table.insertRow(this.table.rows.length);
	//row=document.createElement('tr');
    //this.table.tBodies[0].appendChild(row);

    row.id=uniqueId('detrow');
	row.nameBox=new NameBox(this.parentForm,'det');
	row.nameBox.input.style.width="20em";
	row.nameBox.input.onblur=input_default_blur;
	row.changeEventHandle=register_event_handler(row.nameBox.input,'change',this,'input_change',row.id);

	var cell=row.insertCell(0);
	cell.appendChild(row.nameBox.container);

	row.deleteButton=deleteButton('delete name');
	row.deleteButton.style.verticalAlign='top';
	row.deleteButton.style.display='none';

	cell.appendChild(row.nameBox.illegible);
	cell.appendChild(row.deleteButton);

	row.plusButton=plusButton('add another determiner');
	row.plusButton.style.verticalAlign='top';
	row.plusButton.style.display='none';
	cell.appendChild(row.plusButton);

	row.deleteButton.onclick=associate_obj_with_event(this, "delete_click",row.id);
	row.plusButton.onclick=associate_obj_with_event(this, "add_click",'');

	return row;
};

/**
 *
 */
DetList.prototype.auto_row_check = function (row) {

	// count number of blank rows
	var blanks=0;
	for (var rowIndex=this.table.rows.length-1; rowIndex >=0; rowIndex--) {
    	if (!this.table.rows[rowIndex].nameBox.input.value || this.table.rows[rowIndex].nameBox.input.value==this.table.rows[rowIndex].nameBox.input.defaultValue) {blanks++;}
  	}

	// rationalize rows and buttons
	if (row.nameBox.input.value=='' || row.nameBox.input.value == row.nameBox.input.defaultValue) {
		if (blanks > 1) {this.destroy_row(row);} // delete current row
		else {
			row.nameBox.input.value=row.nameBox.input.defaultValue;
			row.nameBox.input.style.color="gray";
			row.deleteButton.style.display="none";
			row.plusButton.style.display="none";
		}
	} else {
		row.deleteButton.style.display="inline";
		if (this.table.rows[this.table.rows.length-1].nameBox.input.value!==this.table.rows[this.table.rows.length-1].nameBox.input.defaultValue) {
			this.table.rows[this.table.rows.length-1].plusButton.style.display='inline';
		}
	}
};

/**
 *
 */
DetList.prototype.input_change=function (event,element,rowId) {
	var row=document.getElementById(rowId);

	this.auto_row_check(row);

	this.parentForm.verify(); // check dates across form
	if (event) {
		this.parentForm.change();
	}
};

/**
 *
 */
DetList.prototype.add_click = function (event, element) {
	this.add_blank();
	this.hide_superfluous_buttons();
};

/**
 *
 */
DetList.prototype.delete_click=function (event,element,rowId) {
	this.destroy_row(document.getElementById(rowId)); // delete current row
	if (this.table.rows.length===0) {this.add_blank();}
	this.hide_superfluous_buttons(); // hide un-needed add row buttons (only pertains to collector list)

	this.parentForm.verify(); // if deleted row had error then need to clear this
};

/**
 * hide the 'add row' button for all but the last row
 */
DetList.prototype.hide_superfluous_buttons = function (){
	for (var n=this.table.rows.length-2; n>=0;n--) {
		this.table.rows[n].plusButton.style.display='none';
	}

	if (this.table.rows[this.table.rows.length-1].nameBox.input.value!==this.table.rows[this.table.rows.length-1].nameBox.input.defaultValue) {
		this.table.rows[this.table.rows.length-1].plusButton.style.display='inline';
	}
};

/**
 *
 */
DetList.prototype.destroy_row = function (row) {
	row.nameBox.destroy();
	row.nameBox=null;
	row.parentNode.removeChild(row);
};

/**
 *
 */
DetList.prototype.destroy = function () {
	for (var n=this.table.rows.length-1; n>=0;n--) {
		this.destroy_row(this.table.rows[n]);
	}

	this.table=null;
	this.parentForm=null;
	this.parentRow=null;
};

/**
 * Drop down list
 * @constructor
 */
function Dropbox() {
	
}

/**
 * construct dropbox container and input box
 */
Dropbox.prototype.construct_dropbox = function () {
	this.input=document.createElement("input");
	this.searchLinks=[];

	this.container=document.createElement("div");
	this.container.className='namecontainer';
	this.container.id=uniqueId("dbcontainer");
	this.dropbox=document.createElement("div");
	this.dropbox.className='dropbox';

	if (!(standardBrowser || IEversion >=7)) {
		this.iframe=document.createElement('iframe');
		//this.iframe.setAttribute('src','/blank.php');
		this.iframe.style.position="absolute";
		this.iframe.style.border="none";
		this.iframe.frameBorder=0;
		this.iframe.style.display="none";
		this.container.appendChild(this.iframe);
	} else {
		this.iframe=null;	
	}
	this.container.appendChild(this.input);

	if (standardBrowser) {
		this.dropbox.style.zIndex='auto';
	} else {
		this.container.style.position="static";
		this.container.style.verticalAlign="top";
	}
	this.container.appendChild(this.dropbox);
	
	this.closeButton=deleteButton('close');
	
	this.container.appendChild(this.closeButton);
	this.closeButton.style.position="absolute";
	this.closeButton.style.top="1.5em";
	//this.closeButton.style.right="2px";
	this.closeButton.style.zIndex="10";
	this.closeButton.style.display="none";
	
	this.closeButton.style.filter="alpha(opacity:40)";
	this.closeButton.style.KHTMLOpacity="0.40";
	this.closeButton.style.MozOpacity="0.40";
	this.closeButton.style.opacity="0.40";
	this.closeButtonClickHandle=register_event_handler(this.closeButton,'click',this,'dropbox_close_now','');
	
	this.mouseOverEventHandler=register_event_handler(this.container,'mouseover',this,'mouse_over','');
	this.mouseOutEventHandler=register_event_handler(this.container,'mouseout',this,'mouse_out','');
	this.blurEventHandle=register_event_handler(this.input,'blur',this,'hide_drop','');
	
	this.suspendBlur=false;
	this.xmlRequest=null;
	this.requestURL=null;
	this.callIntervalHandle=null;
	this.pending=0;
	
	this.dropbox.selectedIndex=0;
	
	this.progress=document.createElement('img');
	this.progress.src="http://static.herbariumathome.org/snake_transparent.gif";
	this.progress.width=16;
	this.progress.height=16;
};

/**
 * wipe contents of drop down list box
 * @param <html div element> dropbox
 */
Dropbox.prototype.clear_box =function () {
	var links=this.dropbox.getElementsByTagName('a');
	for (var n=links.length-1;n>=0;n--) {
		var link=links[n];
		link.onclick=null;
		link.onmouseover=null;
		link.onmouseout=null;
		link.flagChange=null;
		link.params=null;
	}

	// the first element of the dropbox is the close button, hence n>=1 rater than 0
	for (n=this.dropbox.childNodes.length-1;n>=0;n--) {this.dropbox.removeChild(this.dropbox.lastChild);}
	this.dropbox.scrollTop=0;
};

/**
 * destructor
 */
Dropbox.prototype.destruct_dropbox = function () {
	this.clear_box(); // not sure if this is needed
	
	if (this.iframe) {
		this.iframe.parentNode.removeChild(this.iframe);
		this.iframe=null;
	}
	
	if (this.searchLinks) {
		for (var link in this.searchLinks) {
			if (this.searchLinks[link].destruct) {this.searchLinks[link].destruct();}
		}
	}
	
	if (this.mouseOverEventHandler) {this.mouseOverEventHandler=remove_event_handler(this.mouseOverEventHandler);}
	if (this.mouseOutEventHandler) {this.mouseOutEventHandler=remove_event_handler(this.mouseOutEventHandler);}
	if (this.blurEventHandle) {this.blurEventHandle=remove_event_handler(this.blurEventHandle);}
	if (this.closeButtonClickHandle) {this.closeButtonClickHandle=remove_event_handler(this.closeButtonClickHandle);}

	if (this.closeButton.parentNode) {this.closeButton.parentNode.removeChild(this.closeButton);}
	this.closeButton=null;

	this.progress=null;

	this.container.removeChild(this.dropbox);
	this.container.removeChild(this.input);
	this.dropbox.selectedIndex=null;
	this.dropbox=null;
	this.input.defaultValue=null;
	this.input=null;
	this.container.parentNode.removeChild(this.container);
	this.container=null;
};

/**
 * dropbox mouse-over event
 * delay closing box (suspendblur)
 */
Dropbox.prototype.mouse_over=function (event,element,param) {
	this.suspendBlur=true;
};

Dropbox.prototype.mouse_out=function (event,element,param) {
	this.suspendBlur=false;
};

Dropbox.prototype.show_progress = function () {
	this.container.style.position="relative";
	this.container.appendChild(this.progress);
	this.progress.style.position="absolute";
	this.progress.style.top="2px";
	this.progress.style.left=(this.input.offsetWidth-21)+"px";
};

Dropbox.prototype.hide_progress = function () {
	if (this.progress && this.progress.parentNode) {this.progress.parentNode.removeChild(this.progress);}
};

Dropbox.prototype.abort_xml_request = function () {
	if (this.callIntervalHandle!==null) {
		window.clearTimeout(this.callIntervalHandle);
		this.callIntervalHandle=null;
		this.pending=0;
	}

	if (this.xmlRequest) {
		this.xmlRequest.abort();
		this.xmlRequest=null;
		this.hide_progress();
	}
	this.requestURL=null;
};

Dropbox.prototype.droplist_mouse_over=function (event,element,index) {
	this.set_selection(index,this.dropbox.getElementsByTagName('a'));
};

Dropbox.prototype.droplist_mouse_out=function (event,element,index) {
	if (this.dropbox.selectedIndex == index) {
		element.style.backgroundColor="white";
		element.style.color="green";
		this.dropbox.selectedIndex=null;
	}
};

/**
 * issue queued xml request
 */
Dropbox.prototype.call_back= function () {
	if (this.pending==1) {
		this.issue_xml_request();
	}
	this.callIntervalHandle=null;
};

/**
 * hide dropbox immediately
 */
Dropbox.prototype.dropbox_close_now = function() {
	this.suspendBlur=false;
	
	if (globalDropboxTimeoutHandle) {
		window.clearTimeout(globalDropboxTimeoutHandle);
		globalDropboxTimeoutHandle=null;
		globalDropboxCloseRef=null;
	}

	if (this.dropbox) {
		this.clear_box();
		this.dropbox.style.display='none';
		this.dropbox.style.filter="alpha(opacity:95)";
		this.dropbox.style.KHTMLOpacity="0.95";
		this.dropbox.style.MozOpacity="0.95";
		this.dropbox.style.opacity="0.95";
	}

	if (!standardBrowser) {
		// clean up IE mess
		if (IEversion <7 && this.input) {this.input.style.marginBottom='';}
		if (this.container) {this.container.style.position="static";}
	}
	
	if (this.iframe) {this.iframe.style.display='none';}
	
	if (this.closeButton) {this.closeButton.style.display="none";}
	this.hide_progress();
};

/**
 * hide dropbox after a delay for clicks to complete
 */
Dropbox.prototype.hide_drop = function (event,element,param) {
	if (this.suspendBlur===false) {
		
		/*
		if (this.dropbox) {
			// fade gimmick
			this.dropbox.style.filter="alpha(opacity:40)";
			this.dropbox.style.KHTMLOpacity="0.40";
			this.dropbox.style.MozOpacity="0.40";
			this.dropbox.style.opacity="0.40";
		}
		*/
		
		this.abort_xml_request(); // stop any requests which may not have come back yet
		globalDropboxCloseRef=this;

		globalDropboxTimeoutHandle=setTimeout(Dropbox.commit_hidedrop, 500); // needs to be long enough or links don't get followed
	} else {
		if (this.input) {this.input.focus();}
	}
};

/**
 * static method used by dropbox close call back
 */
Dropbox.commit_hidedrop = function()
{
	globalDropboxTimeoutHandle=null;
	if (globalDropboxCloseRef) {
		globalDropboxCloseRef.dropbox_close_now();
		globalDropboxCloseRef=null;
	}
};

/**
 * signal that request should be issued at next available opportunity
 */
Dropbox.prototype.queue_xml_request = function (shortQuery) {
	if (this.callIntervalHandle===null) {
		if (!shortQuery) {this.issue_xml_request();} // issue request immediately unless little typed, so expecting more
		else {this.pending=1;}

		this.callIntervalHandle=window.setTimeout(associateObjectWithXMLRequest(this,"call_back",null),300);
	}
	else {this.pending=1;}
};

/**
 * immediately issue xml request
 */
Dropbox.prototype.issue_xml_request= function () {
	this.pending=0;
	var url=this.get_request_url();

	if (this.xmlRequest==null || this.xmlRequest.readyState==4 || this.requestURL!=url) {
		this.xmlRequest=agnostic_XMLHttpRequest(); // create the request object
		this.requestURL=url;
		this.show_progress();
		xml_request(this.xmlRequest,url,associateObjectWithXMLRequest(this, "xml_handler", ''));
	}
};

/**
 * change higlight item in dropdown list
 */
Dropbox.prototype.set_selection =function (newIndex,links) {
	if (this.dropbox.selectedIndex && this.dropbox.selectedIndex!=newIndex) {
		// unhighlight previous selection
		links[this.dropbox.selectedIndex-1].style.backgroundColor="white";
		links[this.dropbox.selectedIndex-1].style.color="green";
	}

	if (links[newIndex-1] && links[newIndex-1].style) {
		// TODO IE8 seems to baulk at style setting sometimes - ought to be investigated further
		links[newIndex-1].style.backgroundColor="green";
		links[newIndex-1].style.color="white";
	}
	this.dropbox.selectedIndex=newIndex;
};

/**
 * 
 */
Dropbox.prototype.register_search_link = function (searchLink)
{
	this.searchLinks.push(searchLink);
	searchLink.register_icon(this);
};

/**
 * 
 */
Dropbox.prototype.show_search_links = function ()
{
	for (var link in this.searchLinks) {
		if (this.searchLinks[link].show) {this.searchLinks[link].show();}
	}
};

/**
 * 
 */
Dropbox.prototype.hide_search_links = function ()
{
	for (var link in this.searchLinks) {
		if (this.searchLinks[link].hide) {this.searchLinks[link].hide();}
	}
};

/**
 * virtual class for providing search links to augment drop boxes.
 * Search providers typically use an external window (or potentially could integrate more closely)
 * in cases where the external provider returns data to relevent dropboxes then the provider should implement a singleton
 * shared between dropboxes
 *
 * @constructor
 */
function ExternalSearchLink ()
{
}

/**
 * uri of 16x16 image
 */
ExternalSearchLink.prototype.iconUrl='/uksearch.png';

/**
 * dropbox object which currently has focus (and 'owns' this search link)
 */
ExternalSearchLink.prototype.dropBox=null;

/**
 * dropbox object which currently has focus (and 'owns' this search link)
 */
ExternalSearchLink.prototype.defaultUrl='#';

/**
 */
ExternalSearchLink.prototype.destruct_external_link= function ()
{
	if (this.clickHandle) {this.clickHandle=remove_event_handler(this.clickHandle);}
	if (this.button && this.button.parentNode) {this.button.parentNode.removeChild(this.button);}
	this.button=null;
};

/**
 * generic destructor, typically replaced in child class
 */
ExternalSearchLink.prototype.destruct = function ()
{
	this.destruct_external_link();
};

/**
 * create icon and register event handler
 */
ExternalSearchLink.prototype.register_icon= function (dropBox)
{
	this.dropBox=dropBox;
	this.button=document.createElement('a');
	this.button.className="img";
	this.button.href=this.defaultUrl;
	this.button.style.display="none";
	var img=document.createElement('img');
	img.src=this.iconUrl;
	img.width=16;
	img.height=16;
	this.button.appendChild(img);
	if (this.linkTitle) {this.button.title=this.linkTitle;}
	this.clickHandle=register_event_handler(this.button,'click',this,'click_handler');
	this.dropBox.container.insertBefore(this.button,this.dropBox.dropbox);
};

/**
 * 
 */
ExternalSearchLink.prototype.hide= function () {
	this.button.style.display="none";
};

/**
 * 
 */
ExternalSearchLink.prototype.show= function () {
	this.button.style.display="inline";
};

/**
 * link to open a Google search window, which is populated with the placename and region fields
 *
 * @constructor
 */
function GooglePlaceSearch ()
{
}

GooglePlaceSearch.prototype = new ExternalSearchLink();
GooglePlaceSearch.prototype.constructor = GooglePlaceSearch;
GooglePlaceSearch.prototype.iconUrl='/Gicon.png';
GooglePlaceSearch.prototype.linkTitle='search Google';
GooglePlaceSearch.prototype.defaultUrl='http://www.google.com/';

/**
 * search query is populated with placename and region
 */
GooglePlaceSearch.prototype.click_handler = function (event,element) {
	this.dropBox.dropbox_close_now();
	var q;
	var parentPlaceSet=this.dropBox.parentPlaceSet;
	
	var placename=(this.dropBox.input.value != this.dropBox.input.defaultValue)?this.dropBox.input.value:'';
	
	if (parentPlaceSet.region.input.value != parentPlaceSet.region.input.defaultValue) {
		if (parentPlaceSet.region.input.value=='Channel Islands') {
			q=parentPlaceSet.region.input.value+(placename?", "+placename:'');
		} else {
			q=parentPlaceSet.region.input.value.match(/[^\s\-]*$/);
			q=q[0]?q[0]+(placename?", "+placename:''):placename;
		}
	} else {
		q=placename;
	}

	var url="http://www.google.com/custom?cof=S:http://herbariaunited.org/atHome/;AH:left;LH:57;L:http://herbariaunited.org/atHomeBanner.jpg;LW:859;AWFID:b3d90866d6b13843;&q="+encodeURIComponent(q);
	window.open(url);
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};

/**
 * link to open an OS leisure mapping search window, which is populated with the placename
 */
function OSPlaceSearch ()
{
}

OSPlaceSearch.prototype = new ExternalSearchLink();
OSPlaceSearch.prototype.constructor = OSPlaceSearch;
OSPlaceSearch.prototype.iconUrl='/osicon.png';
OSPlaceSearch.prototype.linkTitle='search Ordnance Survey leisure mapshop';
OSPlaceSearch.prototype.defaultUrl='http://www.ordnancesurvey.co.uk/oswebsite/freefun/didyouknow/';

/**
 * 
 */
OSPlaceSearch.prototype.click_handler = function (event,element)
{
	this.dropBox.dropbox_close_now();
	var value=(this.dropBox.input.value != this.dropBox.input.defaultValue)?this.dropBox.input.value:'';
	window.open(value?"http://leisure.ordnancesurvey.co.uk/search/all/"+encodeURIComponent(value)+"/1":"http://www.ordnancesurvey.co.uk/oswebsite/freefun/didyouknow/");
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};

/**
 * link to open a multimap search window, which is populated with the placename
 */
function MultimapSearch ()
{
}

MultimapSearch.prototype = new ExternalSearchLink();
MultimapSearch.prototype.constructor = MultimapSearch;
MultimapSearch.prototype.iconUrl='/multimapicon.png';
MultimapSearch.prototype.linkTitle='search Multimap';
MultimapSearch.prototype.defaultUrl='http://www.multimap.com/';

/**
 * 
 */
MultimapSearch.prototype.click_handler = function (event,element)
{
	this.dropBox.dropbox_close_now();
	var value=(this.dropBox.input.value != this.dropBox.input.defaultValue)?this.dropBox.input.value:'';
	window.open(value?"http://www.multimap.com/maps/?hloc=GB|"+encodeURIComponent(value):"http://www.multimap.com/");
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};


/**
 * link to open an IPNI search window, which is populated with the taxon name
 */
function IPNISearch ()
{
}

IPNISearch.prototype = new ExternalSearchLink();
IPNISearch.prototype.constructor = IPNISearch;
IPNISearch.prototype.iconUrl='/IPNIlogo.png';
IPNISearch.prototype.linkTitle='search the International Plant Name Index (IPNI)';
IPNISearch.prototype.defaultUrl='http://www.ipni.org/ipni/plantnamesearchpage.do';

/**
 * Internation Plant Name Index search
 */
IPNISearch.prototype.click_handler = function (event,element) {
	var value,url;
	this.dropBox.dropbox_close_now();
	value=(this.dropBox.input.value !== this.dropBox.input.defaultValue)?this.dropBox.input.value:'';
	window.open(value?"http://www.ipni.org/ipni/simplePlantNameSearch.do?output_format=normal&find_wholeName="+encodeURIComponent(value+"*"):"http://www.ipni.org/ipni/plantnamesearchpage.do");
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};

/**
 * link to open an uBio search window, which is populated with the taxon name
 */
function UBioSearch ()
{
}

UBioSearch.prototype = new ExternalSearchLink();
UBioSearch.prototype.constructor = IPNISearch;
UBioSearch.prototype.iconUrl='/uBioIcon.png';
UBioSearch.prototype.linkTitle='search uBio Namebank';
UBioSearch.prototype.defaultUrl='http://www.ubio.org/';

/**
 * 
 */
UBioSearch.prototype.click_handler = function (event,element) {
	var value;
	this.dropBox.dropbox_close_now();
	value=(this.dropBox.input.value !== this.dropBox.input.defaultValue)?this.dropBox.input.value:'';
	window.open(value?"http://www.ubio.org/browser/search.php?search_all="+encodeURIComponent(value):"http://www.ubio.org/");
	if (event.preventDefault) {event.preventDefault();} // DOM 2
	else {event.returnValue=false;} // IE
};

/**
 * Input field for taxon names, including dropbox
 * @constructor
 * @param <string> defaultTaxonName or empty string
 * @param <int> defaultTaxonId
 * @param <string> defaultTaxonAuthority
 * @param <string> defaultTaxonCommonName
 */
function TaxonBox (defaultTaxonName,defaultTaxonId,defaultTaxonAuthority,defaultTaxonCommonName) {
	this.taxonId=defaultTaxonId;
	
	this.defaultTaxonName=defaultTaxonName;
	this.defaultTaxonId=defaultTaxonId;
	this.defaultTaxonAuthority=defaultTaxonAuthority;
	this.defaultTaxonCommonName=defaultTaxonCommonName;
	this.authority=defaultTaxonAuthority;
	this.commonName=defaultTaxonCommonName;

	this.construct_dropbox();
	this.register_search_link(new IPNISearch());
	this.register_search_link(new UBioSearch());

	this.infoLink=info_link();
	
	this.searchURL='XMLtaxonsearchAtHome';

	this.input.style.width="24em";
	this.input.value=this.defaultTaxonName;
	this.inputUpHandler=register_event_handler(this.input,'keyup',this,'input_key_up','');
	
	this.set_tool_tip();
	this.show_info_link();
}

TaxonBox.prototype=new Dropbox();
TaxonBox.prototype.constructor=TaxonBox;

/**
 * destructor
 */
TaxonBox.prototype.destroy=function () {
	this.inputUpHandler=remove_event_handler(this.inputUpHandler);
	
	this.abort_xml_request();
	this.destruct_dropbox();

	this.taxonId=null;
	this.authority=null;
	this.commonName=null;
	this.defaultTaxonAuthority=null;
	this.defaultTaxonCommonName=null;

	this.infoLink=null;
};

/**
 * 
 */
TaxonBox.prototype.set_tool_tip=function () {
	this.input.setAttribute("title",this.taxonId?this.input.value+" "+this.authority+" "+this.commonName:'');
};

/**
 * key up event
 * if valid character then send lookup request
 * else if cursor key then change selection
 */
TaxonBox.prototype.input_key_up=function (event,element,param) {
	if (valid_key(event)) {
		this.taxonId=''; // reset due to key press
		
		if (element.value) {
			this.queue_xml_request(element.value.length <2);
			this.show_search_links();
		} else {
			this.abort_xml_request();
			this.dropbox_close_now();
		}
		
		this.input.style.color='black';
		this.hide_info_link();
		this.set_tool_tip();
	} else {
		if (event.keyCode===38) {this.change_selection(-1);} // up arrow
		else if (event.keyCode===40) {this.change_selection(1);} // up arrow
		else if (event.keyCode===13) {
			// return key
			this.abort_xml_request();
			this.dropbox_close_now();
		}
	}
};

/**
 * return xml request url and query parameters
 */
TaxonBox.prototype.get_request_url= function () {
	var taxonString=this.input.value.replace(trimRegex,''); // trim
	return scriptUrl+this.searchURL+"/"+encodeURIComponent(taxonString.toLowerCase())+"/";
};

/**
 * change dropbox selection
 * @param <int> direction -1|+1
 */
TaxonBox.prototype.change_selection =function (direction)
{
	var links=this.dropbox.getElementsByTagName('a');

	if (links.length && ((direction > 0 && this.dropbox.selectedIndex < links.length) || (direction <0 && this.dropbox.selectedIndex >1))) {
		this.set_selection(this.dropbox.selectedIndex+direction,links);
		this.import_params(links[this.dropbox.selectedIndex-1].params);
		
		this.input.style.color='green';
		this.show_info_link();
		if (this.input.onchange) {this.input.onchange();} // artificial change event required for IE
	}
};

TaxonBox.prototype.import_params = function (params)
{
	this.taxonId=params.externalId;
	this.input.value=params.name;
	this.authority=params.authority;
	this.commonName=params.commonName;
	this.set_tool_tip();
	this.hide_search_links();
};

/**
 * displays 'info' link icon
 */
TaxonBox.prototype.show_info_link = function () {
	if (this.infoLink.parentNode) {this.infoLink.parentNode.removeChild(this.infoLink);}

	if (this.taxonId) {
		this.infoLink.href=scriptUrl+"taxon/"+this.taxonId+"/";
		this.container.insertBefore(this.infoLink,this.dropbox);
	}
};

/**
 * hides 'info' link icon
 */
TaxonBox.prototype.hide_info_link = function () {
	if (this.infoLink.parentNode) {this.infoLink.parentNode.removeChild(this.infoLink);}
};

TaxonBox.prototype.xml_handler=function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4 && this.xmlRequest.status == 200 && this.xmlRequest.responseXML) {
		this.hide_progress();
    	var response  = this.xmlRequest.responseXML.documentElement;
		var names = response.getElementsByTagName('taxon');

		this.clear_box();
		if (!standardBrowser) {this.container.style.position="relative";} // for IE only make position relative at last moment

		this.dropbox.selectedIndex=0; //nothing selected

		var ul=document.createElement('ul'),li;
		var authorityElement,vulgarElement;
		
		var offset=1;

		if (this.defaultTaxonName) {
			// append the default option
			li=document.createElement('li');
			var nameLink=document.createElement("a");
			nameLink.onclick=associate_obj_with_event(this, "click_set_value",this.defaultTaxonName);
			nameLink.onmouseout=associate_obj_with_event(this, "droplist_mouse_out",1);
			nameLink.onmouseover=associate_obj_with_event(this, "droplist_mouse_over",1);
			
			nameLink.params={
				externalId: this.defaultTaxonId,
				name: this.defaultTaxonName,
				authority: this.defaultTaxonAuthority,
				commonName: this.defaultTaxonCommonName
			};
	
			var defaultTextNode=document.createElement("strong");
			defaultTextNode.style.fontStyle="italic";
			defaultTextNode.appendChild(document.createTextNode(this.defaultTaxonName));
			nameLink.appendChild(defaultTextNode);
			
			if (this.defaultTaxonAuthority) {
				authorityElement=document.createElement('span');
				authorityElement.style.color="gray";
				authorityElement.style.backgroundColor="transparent";
				nameLink.appendChild(authorityElement).appendChild(document.createTextNode(' '+this.defaultTaxonAuthority));
			}
			
			if (this.defaultTaxonCommonName) {
				vulgarElement=document.createElement('span');
				vulgarElement.style.backgroundColor="transparent";
				nameLink.appendChild(vulgarElement).appendChild(document.createTextNode(' '+this.defaultTaxonCommonName));
			}
			
			li.appendChild(nameLink);
			li.appendChild(document.createTextNode(' (default)'));
			ul.appendChild(li);
			
			offset+=1;
		}

		if (names && names.length >0) {
			var l=names.length;
			var nameNode;
			this.dropbox.selectedIndex=0; //nothing selected

			for (var i=0; i<l;i++) {
				if (names[i].hasChildNodes() && names[i].getAttribute('id')!=this.defaultTaxonId) {
					li=document.createElement('li');
					var nameLink=document.createElement("a");
					nameLink.onclick=associate_obj_with_event(this, "click_set_value",names[i].firstChild.data);
					nameLink.onmouseout=associate_obj_with_event(this, "droplist_mouse_out",offset);
					nameLink.onmouseover=associate_obj_with_event(this, "droplist_mouse_over",offset++);
					
					nameLink.params={
						externalId: names[i].getAttribute('id'),
						name: names[i].firstChild.data,
						authority: names[i].getAttribute('authority'),
						commonName: names[i].getAttribute('vulgar')
					};
					
					nameNode=document.createElement('i');
					nameLink.appendChild(nameNode).appendChild(document.createTextNode(nameLink.params.name));
					
					if (nameLink.params.authority) {
						authorityElement=document.createElement('span');
						authorityElement.style.color="gray";
						authorityElement.style.backgroundColor="transparent";
						nameLink.appendChild(authorityElement).appendChild(document.createTextNode(' '+nameLink.params.authority));
					}
					
					if (nameLink.params.commonName) {
						vulgarElement=document.createElement('span');
						vulgarElement.style.backgroundColor="transparent";
						nameLink.appendChild(vulgarElement).appendChild(document.createTextNode(' '+nameLink.params.commonName));
					}
					else {
						nameLink.params.commonName=''; // avoid spurious null values
					}
	
					li.appendChild(nameLink);
					ul.appendChild(li);
				} 
			}	
		} 
		
		if (ul.hasChildNodes()) {
			this.dropbox.appendChild(ul);
			
			this.dropbox.style.width=(this.input.offsetWidth*1.5)+"px";
			this.dropbox.style.display='block';
			this.dropbox.style.zIndex=8;
			this.closeButton.style.left=((this.input.offsetWidth*1.5)-36)+"px";
			this.closeButton.style.display='block';
			
			if (!(standardBrowser || IEversion >=7)) {
				this.iframe.style.top=(this.input.offsetTop+this.input.offsetHeight)+"px";
				this.iframe.style.width=this.dropbox.offsetWidth+"px";
				this.iframe.style.height=this.dropbox.offsetHeight+"px";
				this.iframe.style.display="block";
				this.dropbox.style.top=this.input.offsetHeight+"px";
			}
		} else {
			// nothing to show (no default name and no suggestions)
			this.closeButton.style.display="none";
			this.dropbox.style.display='none';
			if (this.iframe) {this.iframe.style.display="none";}
			this.clear_box();
		}
    }
};

TaxonBox.prototype.click_set_value = function (event,element,value) {
    // stop the default link click action
	if (event.preventDefault) {event.preventDefault();}
	else {event.returnValue=false;}

	this.xmlRequest.abort(); // stop any requests which may not have come back yet
	
	this.import_params(element.params);
	this.input.style.color="green";
	this.show_info_link();

	this.dropbox_close_now();
};

/**
 *
 */
function RegistrationForm() {
	this.parentSheet=null; // will be set when attached
	this.container=document.createElement("div");
	this.container.id=uniqueId("registrationContainer");
	
	this.container.className="formregbox";
	this.container.style.padding="8px";
	var table=document.createElement("table");
	
	this.container.appendChild(document.createElement('p')).appendChild(document.createTextNode('You are not currently logged in.  To save your work you will need to login or register.'));
	this.container.appendChild(document.createElement('p')).appendChild(document.createTextNode('If you are a new user then please enter your email address and click \'join\' to enable saving.  Please leave the password blank as we will email you a password later.'));
	this.container.appendChild(document.createElement('p')).appendChild(document.createTextNode('If you have already registered then please enter your username (or email address) and your password and click \'login/join\'.'));
	
	table.style.fontSize="0.8em";
	
	this.regTable=table.appendChild(document.createElement("tbody"));
	this.container.appendChild(table);
	
	var row=this.regTable.appendChild(document.createElement("tr"));
	row.appendChild(document.createElement("td")).appendChild(document.createTextNode("username or email"));
	this.username=row.appendChild(document.createElement("td")).appendChild(document.createElement("input"));
	if (globalUserName) {this.username.value=globalUserName;}
	
	row=this.regTable.appendChild(document.createElement("tr"));
	row.appendChild(document.createElement("td")).appendChild(document.createTextNode("password"));
	this.password=document.createElement("input");
	this.password.type="password"; // in IE6 type must be set before being appended to the document
	row.appendChild(document.createElement("td")).appendChild(this.password);

	this.joinButton=this.container.appendChild(document.createElement('button'));
	this.joinButton.appendChild(document.createTextNode('login / join'));
	this.loginClickHandler=register_event_handler(this.joinButton,'click',this,'login_click');
	this.message=this.container.appendChild(document.createElement('p'));
	this.message.appendChild(document.createTextNode(''));
	this.message.style.color="red";
}

RegistrationForm.prototype.login_click= function (event,element,param) {
	this.send_request();
};

/**
 * generate an XML doc with username/email and password, seeking login/registration
 */
RegistrationForm.prototype.send_request =function () {
	var doc=create_xml_doc();
	
	doc.documentElement.setAttribute('uid',uid);
	
	doc.documentElement.appendChild(doc.createElement('username')).appendChild(doc.createTextNode(this.username.value));
	doc.documentElement.appendChild(doc.createElement('password')).appendChild(doc.createTextNode(this.password.value));

	this.loginRequest=agnostic_XMLHttpRequest(); // create the request object
	
	xml_save(this.loginRequest,scriptUrl+'XMLregister/?x=x'+phpSession,associateObjectWithXMLRequest(this, "login_xml_handler", ''),doc);

	/* respose is of the form
<response>
	<userid>nnn</userid>
	<message>welcome etc.</message>
</response>

<response>
	<error>error msg</error>
</response>
	*/
};

RegistrationForm.prototype.login_xml_handler = function () {
	if (this.loginRequest.readyState == 4 && this.loginRequest.status == 200)
    {
    	var response  = this.loginRequest.responseXML.documentElement;
		var userId = response.getElementsByTagName('userid'); // if no names then IE can return null doc element
	
		if (userId.length===1) {
			this.message.firstChild.data='';
			
			uid=userId[0].firstChild.data;
			
			tmpUser=false;
			globalBlockSave=false;
			
			if (globalShowExpandedHelp) {
				var helpState=response.getElementsByTagName('help');
				if (helpState.length===1 && helpState[0].firstChild.data==='false') {
					this.parentSheet.data.formFooter.extendedHelpLinkText.data="Show expanded help messages";
					if (globalFocusedRowId && document.getElementById(globalFocusedRowId)) {document.getElementById(globalFocusedRowId).style.display="none";}
					globalFocusedRowId=null;
					globalShowExpandedHelp=false;
				}
			}
			
			alert(response.getElementsByTagName('message')[0].firstChild.data);
			
			this.parentSheet.data.formFooter.apply_state(null);
			this.destroy();
		} else {
			this.message.firstChild.data=response.getElementsByTagName('error')[0].firstChild.data;
		}
		
        this.loginRequest=null;
    }
};

RegistrationForm.prototype.destroy= function () {
	if (this.loginClickHandler) {this.loginClickHandler=remove_event_handler(this.loginClickHandler);}
	this.loginRequest=null;
	if (this.container && this.container.parentNode) {this.container.parentNode.removeChild(this.container);}
	
	this.password=null;
	this.username=null;
	this.message=null;
	this.joinButton=null;
	this.regTable=null;
	this.container=null;
	this.parentSheet=null;
};

/**
 * @todo rethink how auto-request-feedback works (too many spurious requests being generated)
 */
function UserNotes(specimenData) {
	this.parentData=specimenData;
	this.container=document.createElement("div");
	this.container.id=uniqueId("commentContainer");
	this.container.style.padding="6px 18% 8px 4px";
	this.container.style.minWidth="600px";

	var table=document.createElement("table");
	table.style.width="100%";

	table.style.padding="0";
	table.style.margin="0";

	table.cellPadding="0";
	table.cellSpacing="0";
	table.style.fontSize="0.8em";
	this.commentTable=table.appendChild(document.createElement("tbody"));
	this.container.appendChild(table);
	
	this.notesRows=[];
	
	this.row=new Row();
	this.row.labelName='user comments'; // row label
	this.row.helpText="Your comments as a herbaria@home user. You can use this section to request feedback, or to discuss the sheet.";
	this.row.longHelp='commentshelp';
	this.row.construct_row();
	
	var cell=document.createElement("td");
	this.notesTable=cell.appendChild(document.createElement("table"));
	this.notesTable.cellSpacing="0";
	this.notesTable.style.width="100%";
	
	this.notesTable.appendChild(document.createElement("tbody"));
	
	if (!standardBrowser) {
		this.notesTable.onselectstart = function () { window.event.cancelBubble = true; }; //allow selection within the notes table
	}
	
	this.row.row.appendChild(cell);
	
	this.commentTable.appendChild(this.row.containerRow);
	this.url='';
	
	this.allowAutoFeedback=true; // when notes field first written, tick the feedback box
}

/*
 * Add a read only user comment (e.g. a comment entered by another user)
 */
UserNotes.prototype.add_readonly = function (userName,message) {
	var row=this.notesTable.insertRow(this.notesTable.rows.length);
	row.id=uniqueId('notelist');

	var cell=row.insertCell(0);
	cell.style.verticalAlign='top';
	cell.style.fontWeight='bold';
	cell.style.width="auto";
	cell.appendChild(document.createTextNode(userName));

	cell=row.insertCell(1);
	cell.appendChild(document.createTextNode(message));

	row.readOnly=true;
	row.message=message;
	row.userName=userName;
	row.userId='';
	row.notesChangeHandler=null;
	this.notesRows.push(row);
};

UserNotes.prototype.add_writeable =function (userName,message,userId) {
	var row=this.notesTable.insertRow(this.notesTable.rows.length);
	row.id=uniqueId('notelist');
	var cell=row.insertCell(0);
	cell.colSpan=2;
	row.notesField=cell.appendChild(document.createElement("textarea"));
	
	row.notesChangeHandler=register_event_handler(row.notesField,'change',this,'notes_change');
	
	row.notesField.style.width="85%";
	row.notesField.rows="2";
	
	row.notesField.value=message;
	
	row.readOnly=false;
	row.userId=userId;
	row.userName=userName;
	this.notesRows.push(row);
};

/**
 * 
 */
UserNotes.prototype.add_feedback_option =function () {
	var row=this.notesTable.insertRow(this.notesTable.rows.length);
	row.id=uniqueId('notefeedback');
	var cell=row.insertCell(0);
	
	this.feedback=document.createElement("input");
	this.feedback.type="checkbox";
	
	var feedback=document.createElement('label');
	feedback.appendChild(this.feedback);
	
	feedback.appendChild(document.createTextNode("request feedback about this sheet"));
	
	cell.setAttribute('colspan','2');
	cell.appendChild(feedback);
};

UserNotes.prototype.add_forum_link =function (url) {
	this.url=url;
	
	var row=this.notesTable.insertRow(this.notesTable.rows.length);
	row.id=uniqueId('notefeedback');
	var cell=row.insertCell(0);
	
	var link=document.createElement("a");
	link.href=url;
	link.target='_blank';
	link.appendChild(document.createTextNode('View the message board post about this sheet.'));
	
	cell.setAttribute('colspan','2');
	cell.appendChild(link);
};

UserNotes.prototype.add_from_xml =function(commentsXML) {
	var notes=commentsXML.getElementsByTagName('usernote');
	var n=notes.length;
	
	if (n>0) {
		for (var i=0; i<n;i++) {
			var note=notes[i];
			var message;
			
			message=(note.getElementsByTagName('usercomment')[0].firstChild)?note.getElementsByTagName('usercomment')[0].firstChild.data:'';
			
			var user=note.getElementsByTagName('user')[0];
			var userName;
			
			userName=(user.firstChild)?user.firstChild.data:'';
			
			if (note.getAttribute('readonly')=='readonly') {		
				this.add_readonly(userName,message);
			} else {
				this.add_writeable(userName,message,user.getAttribute('userid'));
			}
		}
	}
	else {this.add_blank();}
	
	if (commentsXML.getAttribute('linkurl')) {
		this.add_forum_link(commentsXML.getAttribute('linkurl'));
	} else {
		this.add_feedback_option();
	}
};

UserNotes.prototype.add_blank =function() {
	this.add_writeable('','',uid); //uid is global user id
};

UserNotes.prototype.clear =function () {
	for (var n=this.notesRows.length-1;n>=0;n--) {
		var row=this.notesRows[n];
		row.userName=null;
		row.notesField=null;
		row.readOnly=null;
		row.userId=null;
		row.message=null;
		if (row.notesChangeHandler!==null) {
			row.notesChangeHandler=remove_event_handler(row.notesChangeHandler);
		}
	}
	
	this.notesRows.length=0;
	while (this.notesTable.rows.length >0) {this.notesTable.deleteRow(0);}
};

UserNotes.prototype.destroy =function () {
	this.clear();
	
	this.parentData=null;
	this.feedback=null;
	this.notesRows.length=0;
	if (!standardBrowser) {
		this.notesTable.onselectstart = null;
	}
	this.notesTable=null;
	this.commentTable.parentNode.removeChild(this.commentTable);
	this.commentTable=null;
	
	this.row.remove_row_event_handlers();
	
	this.container.parentNode.removeChild(this.container);
	this.container=null; // cannot destroy the container here as this function is used during refresh after save
};

UserNotes.prototype.notes_change= function (event,element,param) {
	if (element.value && this.allowAutoFeedback && this.feedback && this.feedback.checked===false) {
		this.allowAutoFeedback=false; // only tick the box automatically once
		
		this.feedback.checked=true;
	}
	this.parentData.set_modified(true);
};

UserNotes.prototype.get_xml = function (doc)
{
	var userNotes=doc.createElement('comments');
	
	for (var n in this.notesRows) {
		var note=doc.createElement('usernote');
		var userName=doc.createElement('user');
		var userComment=doc.createElement('usercomment');
		
		userName.appendChild(doc.createTextNode(this.notesRows[n].userName));
		
		if (this.notesRows[n].readOnly) {
			userComment.appendChild(doc.createTextNode(this.notesRows[n].message));
			note.setAttribute('readonly','readonly');	
		} else {
			//userName.setAttribute('userid',this.notesRows[n].userId);
			userName.setAttribute('userid',uid); // currently use global uid (not comment uid as this is causing problems with transition from temporary to full login)	
			userComment.appendChild(doc.createTextNode(this.notesRows[n].notesField.value));
		}
		
		note.appendChild(userName);
		note.appendChild(userComment);
		userNotes.appendChild(note);
	}
	if (this.url) {userNotes.setAttribute('linkurl',this.url);}
	else {
		userNotes.setAttribute('requestfeedback',this.feedback.checked.toString());
	}
	
	return userNotes;
};

/**
 *
 */
field.Notes =function (form,manifestXml) {
	this.form=form;
	this.rowName=manifestXml.getAttribute('name');
	this.labelName=manifestXml.getAttribute('rowlabel')?manifestXml.getAttribute('rowlabel'):'notes';
	this.helpText=manifestXml.getAttribute('helptext')?manifestXml.getAttribute('helptext'):"Any other information from the sheet; details which do not fit in other fields";
	this.longHelp=manifestXml.getAttribute('longhelp')?manifestXml.getAttribute('longhelp'):'noteshelp';
	
	this.construct_row();
	var notesCell=document.createElement("td");
	this.notesField=document.createElement("textarea");
	this.notesField.style.width="85%";
	this.notesField.rows="2";
	this.notesField.style.overflow="visible";

	notesCell.appendChild(this.notesField);

	this.row.appendChild(notesCell);
	this.notesField.onchange=associate_obj_with_event(this, "notes_change",'');
};

field.Notes.prototype=new Row();
field.Notes.prototype.constructor = field.Notes;
field.Notes.prototype.xmlTagName='notes';

field.Notes.prototype.destroy = function () {
	this.notesField.onchange=null;

	if (this.notesField.parentNode) {this.notesField.parentNode.removeChild(this.notesField);}
	this.notesField=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};

/**
 * notes change event handler
 * flag the form as changed and attempt to resize the notes field appropriately
 */
field.Notes.prototype.notes_change= function (event,element) {
	this.resize(element);
	this.form.change();
};

field.Notes.prototype.resize = function (field) {
	if (field.scrollTop===0) {
		while ((field.scrollHeight == field.clientHeight) && (field.rows > 1)) {field.rows--;}
		field.rows++;
	} else {
		while (field.scrollHeight > field.clientHeight) {field.rows++;}
	}
};

field.Notes.prototype.increase_size = function (field) {
	field.rows=1;
	
	// scrollHeight is total height
	// clientHeight is height on screen

	while (field.scrollHeight > field.clientHeight) {field.rows++;}
};

field.Notes.prototype.add_from_xml =function (notesXML) {
	// need to find notes field with matching row name (as can be multiple notes rows)
	for (var n=0, l=notesXML.length;n<l;n++) {
		if (notesXML[n].getAttribute('name')==this.rowName && notesXML[n].hasChildNodes()) {
			this.notesField.value=notesXML[n].firstChild.data;
			return;
		}
	}
};

field.Notes.prototype.get_xml = function (doc) {
	if (this.notesField.value) {
		var notes=doc.createElement('notes');
		notes.setAttribute('name',this.rowName);
		notes.appendChild(doc.createTextNode(this.notesField.value));
		return notes;
	}
	return doc.createDocumentFragment();
};

/**
 * generate html representation of notes for summary 
 * @return html fragment, or null if no notes
 * @todo should parse line breaks (or use whitespace preserving html)
 */
field.Notes.prototype.get_html = function () {
	if (this.notesField.value) {
		var notes=document.createElement('p');
		try {
			notes.style.whiteSpace="pre-line";
		} catch (err) {}
		notes.appendChild(document.createTextNode(this.notesField.value));
		return notes;
	} else {
		return null;
	}
};

/**
 * a name list is a table row, with a label followed by a nested table of input boxes
 * used as parent class for CollectorList and ProvenanceList
 */
function NameList () {
	//
}

NameList.prototype = new Row();
NameList.prototype.constructor = NameList;

NameList.prototype.construct =function () {
	this.construct_row(); // base element is table row
	var cell=this.row.appendChild(document.createElement("td"));
	
	this.nameTable=cell.appendChild(document.createElement("table"));

	this.nameTable.cellSpacing="0";
	this.nameTable.style.paddingTop="3px";
	this.nameTable.style.paddingBottom="3px";

	this.nameTable.appendChild(document.createElement("tbody"));
};

/**
 * destruct NameList and constituent rows
 */
NameList.prototype.destroy=function () {
	for (var n=this.nameTable.rows.length-1; n>=0;n--) {this.destroy_name_row(this.nameTable.rows[n]);}

	this.add=null;
	this.form=null;

	if (this.nameTable.parentNode) {this.nameTable.parentNode.removeChild(this.nameTable);}
	this.nameTable=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
};

/**
 * destruct specified name entry
 * @param row name table row (TR element)
 */
NameList.prototype.destroy_name_row=function (row) {
	row.parentNode.removeChild(row); // delete current row

	if (row.deleteButton.parentNode) {row.deleteButton.parentNode.removeChild(row.deleteButton);}
	row.deleteButton.onclick=null;
	row.deleteButton=null;
	
	if (row.plusButton) {
		if (row.plusButton.parentNode) {row.plusButton.parentNode.removeChild(row.plusButton);}
		row.plusButton.onclick=null;
		row.plusButton=null;
	}

	row.nameBox.destroy();
	row.nameBox=null;
	row.assoc=null;

	if (row.assocChangeHandler!==null) {row.assocChangeHandler=remove_event_handler(row.assocChangeHandler);}

	for (var n=row.cells.length-1;n>=0;n--) {row.deleteCell(n);}
};

/**
 * namelist lost focus so need to clear info panel,
 * do so after a delay (in case blur was due to click on list)
 */
NameList.prototype.input_blur=function (event,element,rowId) {
	this.form.parentData.info.clearIntervalHandle=window.setTimeout(associateObjectWithXMLRequest(this.form.parentData.info,"clear",null),200);
	this.input_change(null,null,rowId);
};

/**
 * this is called both by the change event handler and also artificially in which case event and element are not valid
 */ 
NameList.prototype.input_change=function (event,element,rowId) {
	var blanks=0;
	var row=document.getElementById(rowId);

	for (var rowIndex=this.nameTable.rows.length-1; rowIndex >=0; rowIndex--) {
    	if (!this.nameTable.rows[rowIndex].nameBox.input.value || this.nameTable.rows[rowIndex].nameBox.input.value==this.nameTable.rows[rowIndex].nameBox.input.defaultValue) {blanks++;}
  	}

	this.auto_row_check(row,blanks);

	this.form.verify(); // check dates across form
	if (event) {
		this.form.change();
	}
};

/**
 * if no blank rows left then auto-add another
 * if multiple blanks then remove one
 * @param row name row
 * @param blanks number of blank rows
 */
NameList.prototype.auto_row_check = function (row,blanks) {
	if (row.nameBox.input.value && row.nameBox.input.value != row.nameBox.input.defaultValue) {
		row.deleteButton.style.display="inline";
		// if no blanks left then add another

		if (blanks===0) {this.add_blank();}
	} else {
		if (blanks > 1) {this.destroy_name_row(row);} // delete current row
		else {
			row.nameBox.input.value=row.nameBox.input.defaultValue;
			row.nameBox.input.style.color="gray";
			row.deleteButton.style.display="none";
		}
	}
};

var invalidNamePattern=/^[^a-z]+$|\d+\/\d+/i; // names must contain some alphabetic chars and must not contain date-like components (n/n) 

/**
 * check if any of the names look like dates
 */
NameList.prototype.verify=function () {
	var l=this.nameTable.rows.length;
	var nameString;
	
	this.containerRow.errorMsg=[];

	for (var n=0; n<l;n++) {
		if ((nameString=this.nameTable.rows[n].nameBox.input.value)) {
			if (nameString.search(invalidNamePattern)!==-1) {
				this.containerRow.errorMsg[this.containerRow.errorMsg.length]='Not a valid name';
				this.containerRow.error=true;
				this.set_style();
				return true;
			}
		}
	}
	this.containerRow.error=false;
	this.set_style();
	return false;
};

NameList.prototype.id_list = function () {
	var id;
	var ids=[];

	for (var n=0,l=this.nameTable.rows.length; n<l;n++) {
		if ((id=this.nameTable.rows[n].nameBox.personId)>0) {
			ids.push(id);
		}
	}
	return ids;
};

/**
 * @constructor
 */
field.CollectorList = function (form) {
	this.form=form;
	this.construct();
};

field.CollectorList.prototype =new NameList();
field.CollectorList.prototype.constructor =field.CollectorList;
field.CollectorList.prototype.xmlTagName='collector';

field.CollectorList.prototype.labelName='collected by'; // row label
field.CollectorList.prototype.helpText='Field collector of the specimen.'; // short summary help text
field.CollectorList.prototype.longHelp='colhelp'; // message index key to lookup verbose help message

/**
 * @class field.CollectorList
 */
field.CollectorList.prototype.add_blank = function () {
	var row=this.nameTable.insertRow(this.nameTable.rows.length);
	row.id=uniqueId('colnamelist');

	var cell=row.insertCell(0);
	cell.style.verticalAlign='top';

	row.nameBox=new NameBox(this.form,'col');
	row.nameBox.input.style.width="24em";
	row.nameBox.dropbox.style.width="28em";
	
	row.deleteButton=deleteButton('delete name');
	row.deleteButton.style.verticalAlign='top';
	row.deleteButton.style.display='none';

	cell.appendChild(row.nameBox.container);
	cell.appendChild(row.nameBox.illegible);
	cell.appendChild(row.deleteButton);
	
	row.plusButton=plusButton('add another collector');
	row.plusButton.style.verticalAlign='top';
	row.plusButton.style.display='none';
	cell.appendChild(row.plusButton);

	row.deleteButton.onclick=associate_obj_with_event(this, "delete_click",row.id);
	row.plusButton.onclick=associate_obj_with_event(this, "add_click",row.id);

	row.nameBox.focusInfoEventHandle=register_event_handler(row.nameBox.input,'focus',this,'input_focus',row.id);
	row.nameBox.blurInfoEventHandle=register_event_handler(row.nameBox.input,'blur',this,'input_blur',row.id);

	row.assocChangeHandler=null; // not used for collectors
	return (row);
};

/**
 * auto row addition doesn't apply to collectors
 */
field.CollectorList.prototype.auto_row_check = function (row,blanks) {
	if (row.nameBox.input.value=='' || row.nameBox.input.value == row.nameBox.input.defaultValue) {
		if (blanks > 1) {this.destroy_name_row(row);} // delete current row
		else {
			row.nameBox.input.value=row.nameBox.input.defaultValue;
			row.nameBox.input.style.color="gray";
			row.deleteButton.style.display="none";
			row.plusButton.style.display="none";
		}
	} else {
		row.deleteButton.style.display="inline";
		if (this.nameTable.rows[this.nameTable.rows.length-1].nameBox.input.value!==this.nameTable.rows[this.nameTable.rows.length-1].nameBox.input.defaultValue) {
			this.nameTable.rows[this.nameTable.rows.length-1].plusButton.style.display='inline';
		}
	}
};

/**
 * 
 */
field.CollectorList.prototype.add_click = function (event, element, param) {
	this.add_blank();
	this.hide_superfluous_buttons();
};

/**
 * 
 */
field.CollectorList.prototype.delete_click=function (event,element,rowId) {
	this.destroy_name_row(document.getElementById(rowId)); // delete current row
	if (this.nameTable.rows.length===0) {this.add_blank();}
	this.hide_superfluous_buttons(); // hide un-needed add row buttons (only pertains to collector list)
	this.form.verify(); // if deleted row had error then need to clear this
};

/**
 * hide the 'add row' button for all but the last row
 */
field.CollectorList.prototype.hide_superfluous_buttons = function (){
	for (var n=this.nameTable.rows.length-2; n>=0;n--) {
		this.nameTable.rows[n].plusButton.style.display='none';
	}

	if (this.nameTable.rows[this.nameTable.rows.length-1].nameBox.input.value!==this.nameTable.rows[this.nameTable.rows.length-1].nameBox.input.defaultValue) {
		this.nameTable.rows[this.nameTable.rows.length-1].plusButton.style.display='inline';
	}
};

/**
 * generate textual representations of collectors for thumbnail summary 
 * @return html fragment, or null if no collectors
 */
field.CollectorList.prototype.get_html = function () {
	var l=this.nameTable.rows.length;
	var ul=document.createElement('ul');
	var liCount=0,lump;

	for (var n=0; n<l;n++) {
		var nameString=this.nameTable.rows[n].nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
	
		lump=(nameString && nameString!=this.nameTable.rows[n].nameBox.input.defaultValue)?nameString:'';
		
		if (this.nameTable.rows[n].nameBox.illegibleButton.checked) {lump+=" [illegible]";}
		if (lump) {
			ul.appendChild(document.createElement('li')).appendChild(document.createTextNode(lump));
			liCount++;
		}
	}
	
	if (ul.hasChildNodes()) {
		if (liCount===1) {ul.className='inlineul';}
		return ul;
	} else {
		return null;
	}
};

/**
 * given a (potentially empty) set of collectors as XML add these to the form
 */
field.CollectorList.prototype.add_from_xml =function (colXML) {
	if (colXML.length ===0) {
		this.add_blank(); // create a default entry
	} else {
		var l=colXML.length;
		for (var n=0; n<l; n++) {
			var row=this.add_blank(); // create a blank entry

			row.nameBox.illegibleButton.checked=(colXML[n].getAttribute('illegible')==1 || colXML[n].getAttribute('illegible')=="true");
			var person=colXML[n].getElementsByTagName('person');
			row.nameBox.populate_from_xml(person[0]);
			
			row.deleteButton.style.display="inline";
		}
	}
	this.hide_superfluous_buttons();
};

field.CollectorList.prototype.get_xml = function (doc) {
	var collectors=doc.createDocumentFragment();
	var l=this.nameTable.rows.length;

	for (var n=0; n<l;n++) {
		var nameBox=this.nameTable.rows[n].nameBox;
		if (nameBox.input.value != nameBox.input.defaultValue) {
			var nameString=nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
			if (nameString) {
				var collector=doc.createElement('collector');
				collector.setAttribute('illegible',nameBox.illegibleButton.checked.toString());
				
				collector.appendChild(nameBox.get_person_xml(doc)); // 'person' element
				collectors.appendChild(collector);
			}
		}
	}

	return collectors;
};

field.CollectorList.prototype.input_focus=function (event,element,rowId) {
	var row=document.getElementById(rowId);
		
	if (row.nameBox.dropbox.style.display!='block') {
		this.form.parentData.info.seek_provenance(this.form,'col',element,row.nameBox,row.id,this);
		if (row.nameBox.input.value=='') {row.nameBox.issue_suggestion_xml_request(this.form,'col');}
		else {
			if (!row.nameBox.personId) {
				row.nameBox.queue_xml_request(row.nameBox.input.value.length <2);
			}
		}
	}
};

/**
 *
 */
field.ProvenanceList = function (form) {
	this.form=form;
	this.construct();
};

field.ProvenanceList.prototype =new NameList();
field.ProvenanceList.prototype.constructor =field.ProvenanceList;
field.ProvenanceList.prototype.xmlTagName='provenance';

field.ProvenanceList.prototype.labelName='herbarium'; // row label
field.ProvenanceList.prototype.helpText='People or collections that have stored or handled the specimen.'; // short summary help text
field.ProvenanceList.prototype.longHelp='herbhelp'; // message index key to lookup verbose help message

field.ProvenanceList.prototype.add_blank = function () {
	var row=this.nameTable.insertRow(this.nameTable.rows.length);
	row.id=uniqueId('provnamelist');

	row.style.verticalAlign="top";
	var cell=row.insertCell(0);
	cell.style.verticalAlign='top';

	row.nameBox =new NameBox(this.form,'prov');
	row.nameBox.input.style.width="24em";
	row.nameBox.dropbox.style.width="28em";
	row.nameBox.input.defaultValue="person or institution";
	row.nameBox.input.value=row.nameBox.input.defaultValue;
	cell.appendChild(row.nameBox.container);

	cell=row.insertCell(1);
	
	row.assoc=document.createElement("select");
	row.assoc.style.verticalAlign="top";
	row.assoc.options[0]=new Option('','',true,false);
	row.assoc.options[1]=new Option('ex herb','exherb',false,true);
	row.assoc.options[2]=new Option('comm','comm',false,false);
	row.assoc.options[3]=new Option('per','comm',false,false);
	row.assoc.options[4]=new Option('from','comm',false,false);
	row.assoc.selectedIndex=1; // unneccessary but praps will force IE to make correct choice

	cell.appendChild(row.assoc);
	cell.appendChild(row.nameBox.illegible);
	
	row.deleteButton=deleteButton('delete name');
	row.deleteButton.style.verticalAlign='top';
	row.deleteButton.style.display='none';
	
	cell.appendChild(row.deleteButton);

	row.assocChangeHandler=register_event_handler(row.assoc,'change',this.form,"change",0);

	row.deleteButton.onclick=associate_obj_with_event(this, "delete_click",row.id);

	row.nameBox.focusInfoEventHandle=register_event_handler(row.nameBox.input,'focus',this,'input_focus',row.id);
	row.nameBox.blurInfoEventHandle=register_event_handler(row.nameBox.input,'blur',this,'input_blur',row.id);

	return (row);
};

/**
 * 
 */
field.ProvenanceList.prototype.delete_click=function (event,element,rowId) {
	this.destroy_name_row(document.getElementById(rowId)); // delete current row
	if (this.nameTable.rows.length===0) {this.add_blank();}
	//if (this.hide_superfluous_buttons) {this.hide_superfluous_buttons();} // hide un-needed add row buttons (only pertains to collector list)
	this.form.verify(); // if deleted row had error then need to clear this
};

/**
 * generate textual representations of provenance for thumbnail summary 
 * @return html fragment, or null if no entries
 */
field.ProvenanceList.prototype.get_html = function () {
	var l=this.nameTable.rows.length;
	var fragment=document.createElement('ul');
	var lump;

	for (var n=0; n<l;n++) {
		var nameString=this.nameTable.rows[n].nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
		
		lump=(nameString && nameString!=this.nameTable.rows[n].nameBox.input.defaultValue)?nameString:'';
		if (this.nameTable.rows[n].nameBox.illegibleButton.checked) {lump+=" [illegible]";}
		
		if (lump) {
			var li=fragment.appendChild(document.createElement('li'));
			li.appendChild(document.createElement('i')).appendChild(document.createTextNode(this.nameTable.rows[n].assoc.options[this.nameTable.rows[n].assoc.selectedIndex].text));
			li.appendChild(document.createTextNode(' '+lump));
		}	
	}
	
	if (fragment.hasChildNodes()) {
		return fragment;
	} else {
		return null;
	}
};

/**
 * given a (potentially empty) array of collectors add these to the form
 */
field.ProvenanceList.prototype.add_from_xml =function (colXML) {
	if (colXML.length ===0) {
		this.add_blank(); // create a default entry
	} else {
		var l=colXML.length;
		for (var n=0;n<l;n++) {
			var row=this.add_blank(); // create a blank entry
			row.nameBox.illegibleButton.checked=(colXML[n].getAttribute('illegible')=="1" || colXML[n].getAttribute('illegible')=="true");
			var person=colXML[n].getElementsByTagName('person')[0];
			row.nameBox.populate_from_xml(person);
			var assoc=colXML[n].getElementsByTagName('assoc')[0];

			if (assoc.hasChildNodes()) {
				switch (assoc.firstChild.data)
				{
					case '':
					row.assoc.selectedIndex=0; // nothing selected
					break;

					case 'exherb':
					row.assoc.selectedIndex=1;
					break;
					
					case 'comm':
					row.assoc.selectedIndex=2;
					break;
				}
			}
			else {row.assoc.selectedIndex=0;} // nothing selected
			row.deleteButton.style.display="inline";
		}

		this.add_blank(); // create a spare blank entry
	}
};

field.ProvenanceList.prototype.get_xml = function (doc) {
	var provenances=doc.createDocumentFragment();
	var l=this.nameTable.rows.length;

	for (var n=0; n<l;n++) {
		var nameBox=this.nameTable.rows[n].nameBox;
		if (nameBox.input.value != nameBox.input.defaultValue) {
			var nameString=nameBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
			if (nameString) {
				var provenance=doc.createElement('provenance');
				provenance.setAttribute('illegible',nameBox.illegibleButton.checked.toString());
				provenance.appendChild(nameBox.get_person_xml(doc)); // 'person' element
				
				var assoc=doc.createElement('assoc');
				assoc.appendChild(doc.createTextNode(this.nameTable.rows[n].assoc.options[this.nameTable.rows[n].assoc.selectedIndex].value));
				provenance.appendChild(assoc);
				provenances.appendChild(provenance);
			}
		}
	}

	return provenances;
};

field.ProvenanceList.prototype.input_focus=function (event,element,rowId) {
	var row=document.getElementById(rowId);
	if (row.nameBox.dropbox.style.display!='block') {
		this.form.parentData.info.seek_provenance(this.form,'prov',element,row.nameBox,row.id,this);
		if (row.nameBox.input.value=='') {row.nameBox.issue_suggestion_xml_request(this.form,'prov');}
		else {
			if (!row.nameBox.personId) {
				row.nameBox.queue_xml_request(row.nameBox.input.value.length <2);
			}
		}
	}
};

/**
 * User defined tag/value pairs (value is optional)
 */
field.AdlibTagList =function (form) {
	this.form=form;
	this.construct_row(); // base element is table row
	var cell=document.createElement("td");
	this.tagTable=cell.appendChild(document.createElement("table"));
	this.tagTable.cellSpacing="0";
	this.tagTable.style.width="100%";

	this.tagTable.appendChild(document.createElement("tbody"));
	this.row.appendChild(cell);
};

field.AdlibTagList.prototype = new Row();
field.AdlibTagList.constructor=field.AdlibTagList;
field.AdlibTagList.prototype.xmlTagName='adlibtag';

field.AdlibTagList.prototype.labelName='tag or attribute';
field.AdlibTagList.prototype.iconLabel='add a tag or attribute to the specimen';
field.AdlibTagList.prototype.helpText="Tags can be used to record or categorise details that don't fit in the other fields.";
field.AdlibTagList.prototype.longHelp="adlibtaghelp";
field.AdlibTagList.prototype.iconName='/tagsm'+iconSuffix+'.png';

field.AdlibTagList.prototype.destroy=function () {
	for (var n=this.tagTable.rows.length-1; n>=0;n--) {this.destroy_tag_row(this.tagTable.rows[n]);}

	if (this.tagTable.parentNode) {this.tagTable.parentNode.removeChild(this.tagTable);}
	this.tagTable=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};

/**
 * given a (potentially empty) set of tags as XML add these to the form
 */
field.AdlibTagList.prototype.add_from_xml =function (tagXml) {
	if (tagXml.length ===0) {
		this.add_blank(); // create a default entry
	} else {
		var l=tagXml.length;
		for (var n=0; n<l; n++) {
			var row=this.add_blank(); // create a blank entry

			row.tagBox.input.value=tagXml[n].getAttribute('tagname');
			row.tagBox.input.style.color="black";
			if (tagXml[n].hasChildNodes()) {
				row.valueField.value=tagXml[n].firstChild.data;
				row.valueField.style.color="black";
			}
			
			row.deleteButton.style.display="inline";
		}
		this.iconify=false;
	}
	this.hide_superfluous_buttons();
};

/**
 * generate textual representations of tags for thumbnail summary 
 * @return html fragment, or null if no collectors
 */
field.AdlibTagList.prototype.get_html = function () {
	var l=this.tagTable.rows.length;
	var ul=document.createElement('ul');
	var liCount=0,nameString;

	for (var n=0; n<l;n++) {
		nameString=this.tagTable.rows[n].tagBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces	
		
		if (nameString && nameString!==this.tagTable.rows[n].tagBox.input.defaultValue) {
			var value=this.tagTable.rows[n].valueField.value;
			if (value!==this.tagTable.rows[n].valueField.defaultValue) {
				nameString+=": "+value;
			}
			
			ul.appendChild(document.createElement('li')).appendChild(document.createTextNode(nameString));
			liCount++;
		}
	}
	
	if (ul.hasChildNodes()) {
		if (liCount===1) {ul.className='inlineul';}
		return ul;
	} else {
		return null;
	}
};

field.AdlibTagList.prototype.get_xml = function (doc) {
	var tags=doc.createDocumentFragment();
	var l=this.tagTable.rows.length;

	for (var n=0; n<l;n++) {
		var tagBox=this.tagTable.rows[n].tagBox;
		if (tagBox.input.value != tagBox.input.defaultValue) {
			var nameString=tagBox.input.value.replace(trimRegex,''); // trim leading/trailing spaces
			if (nameString) {
				var tag=tags.appendChild(doc.createElement('adlibtag'));
				tag.setAttribute('tagname',nameString);
				var value=this.tagTable.rows[n].valueField.value.replace(trimRegex,'');
				if (value!==this.tagTable.rows[n].valueField.defaultValue) {
					tag.appendChild(doc.createTextNode(value));
				}
			}
		}
	}

	return tags;
};

/**
 * 
 */
field.AdlibTagList.prototype.add_blank = function () {
	var row=this.tagTable.insertRow(this.tagTable.rows.length);
	row.id=uniqueId('taglist');

	var cell=row.insertCell(0);
	cell.style.verticalAlign='top';

	row.tagBox=new TagBox(this.form,this);
	row.tagBox.input.style.width="16em";
	row.tagBox.dropbox.style.width="20em";
	
	row.valueField=document.createElement('input');
	row.valueField.style.width="16em";
	row.valueField.defaultValue="value";
	input_set_default(row.valueField);
	row.valueField.onfocus=input_default_focus;
	row.valueField.onblur=input_default_blur;
	
	row.deleteButton=deleteButton('delete tag');
	row.deleteButton.style.verticalAlign='top';
	row.deleteButton.style.display='none';

	cell.appendChild(row.tagBox.container);
	cell.appendChild(row.valueField);
	cell.appendChild(row.deleteButton);
	
	row.plusButton=plusButton('add another tag');
	row.plusButton.style.verticalAlign='top';
	row.plusButton.style.display='none';
	cell.appendChild(row.plusButton);

	row.deleteButton.onclick=associate_obj_with_event(this, "delete_click",row.id);
	row.plusButton.onclick=associate_obj_with_event(this, "add_click",row.id);

	row.tagChangeHandler=register_event_handler(row.tagBox.input,'change',this,'input_change','');
	return (row);
};

/**
 * 
 */
field.AdlibTagList.prototype.add_click = function (event, element) {
	this.add_blank();
	this.hide_superfluous_buttons();
};

/**
 * 
 */
field.AdlibTagList.prototype.delete_click=function (event,element,rowId) {
	this.destroy_tag_row(document.getElementById(rowId)); // delete current row
	if (this.tagTable.rows.length===0) {this.add_blank();}
	this.hide_superfluous_buttons(); // hide un-needed add row buttons (only pertains to collector list)
	this.form.verify(); // if deleted row had error then need to clear this
};

field.AdlibTagList.prototype.input_blur=function (event,element,rowId) {
	this.input_change(null,null,rowId);
};

/**
 * this is called both by the change event handler and also artificially in which case event and element are not valid
 */ 
field.AdlibTagList.prototype.input_change=function (event,element,rowId) {
	/*
	var blanks=0;
	var row=document.getElementById(rowId);

	for (var rowIndex=this.tagTable.rows.length-1; rowIndex >=0; rowIndex--) {
    	if (!this.tagTable.rows[rowIndex].tagBox.input.value || this.tagTable.rows[rowIndex].tagBox.input.value==this.tagTable.rows[rowIndex].tagBox.input.defaultValue) {blanks++;}
  	}

	this.auto_row_check(row,blanks);	
	*/
	this.hide_superfluous_buttons();
	if (event) {
		this.form.change();
	}
};

/**
 * hide the 'add row' button for all but the last row
 */
field.AdlibTagList.prototype.hide_superfluous_buttons = function (){
	for (var n=this.tagTable.rows.length-2; n>=0;n--) {
		this.tagTable.rows[n].plusButton.style.display='none';
		if (this.tagTable.rows[n].tagBox.input.value!==this.tagTable.rows[n].tagBox.input.defaultValue) {
			this.tagTable.rows[n].deleteButton.style.display='inline';
		} else {
			this.tagTable.rows[n].deleteButton.style.display='none';
		}
	}

	if (this.tagTable.rows[this.tagTable.rows.length-1].tagBox.input.value!==this.tagTable.rows[this.tagTable.rows.length-1].tagBox.input.defaultValue) {
		this.tagTable.rows[this.tagTable.rows.length-1].plusButton.style.display='inline';
		this.tagTable.rows[this.tagTable.rows.length-1].deleteButton.style.display='inline';
	}
};

/**
 * 
 */
field.AdlibTagList.prototype.destroy_tag_row=function (row) {
	row.parentNode.removeChild(row); // delete current row

	if (row.deleteButton.parentNode) {row.deleteButton.parentNode.removeChild(row.deleteButton);}
	row.deleteButton.onclick=null;
	row.deleteButton=null;
	
	if (row.plusButton) {
		if (row.plusButton.parentNode) {row.plusButton.parentNode.removeChild(row.plusButton);}
		row.plusButton.onclick=null;
		row.plusButton=null;
	}

	if (row.tagChangeHandler) {row.tagChangeHandler=remove_event_handler(row.tagChangeHandler);}

	row.tagBox.destroy();
	row.tagBox=null;
	row.valueField=null;

	for (var n=row.cells.length-1;n>=0;n--) {row.deleteCell(n);}
};


/**
 * writeable person name field box and dropbox
 * @constructor
 */
function TagBox (form,parentTagList) {
	this.parentForm=form;
	this.parentTagList=parentTagList;
	this.construct_dropbox();

	this.input.onkeyup=associate_obj_with_event(this, "input_key_up",'');

	this.input.defaultValue='tag name';
	input_set_default(this.input);
	this.input.onfocus=input_default_focus;

	this.infoLink=info_link();
}

TagBox.prototype =new Dropbox();
TagBox.prototype.constructor=TagBox;

TagBox.prototype.destroy=function () {
	this.input.onchange=null;
	this.input.onkeyup=null;
	this.input.onblur=null;
	this.input.onfocus=null;

	this.input.defaultValue=null;
	
	this.abort_xml_request();
	this.destruct_dropbox();

	this.button=null;
	this.requestURL=null;
	this.parentForm=null;
	this.parentTagList=null;
	
	this.rowId=null;
};

/**
 * @return url and query parameters for xml request
 */
TagBox.prototype.get_request_url= function () {
	var nameString=this.input.value.replace(trimRegex,''); // trim
	return scriptUrl+"XMLtagsearch/"+encodeURIComponent(nameString.toLowerCase());
};

TagBox.prototype.input_key_up=function (event,element,param) {
	if (valid_key(event)) {
		var nameString=element.value.replace(trimRegex,''); // trim

		if (nameString.length >0) {
			this.queue_xml_request(nameString.length <2);
		} else {
			this.abort_xml_request();
			this.dropbox_close_now();
		}
		element.style.color='black';
	} else {
		if (event.keyCode===38) {
			this.change_selection(-1); // up arrow
		} else {
			if (event.keyCode===40) {
				this.change_selection(1); // down arrow
			} else {
				if (event.keyCode===13) {
					// return key
					this.abort_xml_request();
					this.dropbox_close_now();
				}
			}
		}
	}
};

TagBox.prototype.change_selection =function (direction)
{
	var links=this.dropbox.getElementsByTagName('a');

	if (links.length && ((direction > 0 && this.dropbox.selectedIndex < links.length) || (direction <0 && this.dropbox.selectedIndex >1))) {
		this.set_selection(this.dropbox.selectedIndex+direction,links);
		this.input.value=links[this.dropbox.selectedIndex-1].firstChild.data;	
	
		this.input.style.color='green';
		if (this.input.onchange) {this.input.onchange();} // artificial change event required for IE
	}
	this.parentForm.change(); // new addition
};

TagBox.prototype.xml_handler=function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4) {
    	this.hide_progress();
		var response  = (this.xmlRequest.responseXML)?this.xmlRequest.responseXML.documentElement:null;

		if (response !==null) {var names = response.getElementsByTagName('name');} // if no names then IE can return null doc element
		else {var names=[];}

		var l=names.length;

		if (l) {
			this.clear_box();
			if (!standardBrowser) {this.container.style.position="relative";} // for IE ony make position relative at last moment

			this.dropbox.selectedIndex=0; //nothing selected
			
			var ul=document.createElement('ul');
			var li;
			
			for (var i=0; i<l;i++) {
				var nameText=names[i].hasChildNodes()?names[i].firstChild.data:'';
				
				if (nameText) {
					li=ul.appendChild(document.createElement('li'));
					var nameLink=li.appendChild(document.createElement("a"));
					nameLink.onclick=associate_obj_with_event(this, "click_set_value",nameText);
					nameLink.onmouseover=associate_obj_with_event(this, "droplist_mouse_over",i+1);
					nameLink.onmouseout=associate_obj_with_event(this, "droplist_mouse_out",i+1);
					nameLink.appendChild(document.createTextNode(nameText));
				}
			}
			this.dropbox.appendChild(ul);
			this.dropbox.style.width=this.input.offsetWidth+"px";
			this.closeButton.style.left=(this.input.offsetWidth-36)+"px";
			this.closeButton.style.display='block';
			this.dropbox.style.display='block';
			this.dropbox.style.zIndex=8;
			
			if (!(standardBrowser || IEversion >=7)) {
				this.iframe.style.top=(this.input.offsetTop+this.input.offsetHeight)+"px";
				this.iframe.style.width=this.dropbox.offsetWidth+"px";
				this.iframe.style.height=this.dropbox.offsetHeight+"px";
				this.iframe.style.display="block";
				this.dropbox.style.top=this.input.offsetHeight+"px";
			}
		} else {
			this.dropbox.style.display='none';
			this.closeButton.style.display="none";
			if (this.iframe) {this.iframe.style.display="none";}
			this.clear_box();
		}
    }
};

TagBox.prototype.click_set_value = function (event,element,value) {
    // stop the default link click action
	if (event.preventDefault) {
		event.preventDefault();
	} else {
		event.returnValue=false;
	}

	this.abort_xml_request(); // stop any requests which may not have come back yet
		
	this.input.value=element.firstChild.data;
	this.input.style.color='green';
	this.dropbox_close_now();
	
	this.parentTagList.input_change(null,null,this.rowId);
	this.parentForm.change(); // new addition
};

/**
 *
 */
field.PlaceList =function (form) {
	this.form=form;
	this.construct_row(); // base element is table row
	var cell=document.createElement("td");
	this.placeTable=cell.appendChild(document.createElement("table"));
	this.placeTable.cellSpacing="0";
	this.placeTable.style.width="100%";

	this.placeTable.appendChild(document.createElement("tbody"));
	this.row.appendChild(cell);
	//this.international=false; // default to UK and IE only
	this.international=this.form.international;
};
field.PlaceList.prototype =new Row();
field.PlaceList.prototype.constructor=field.PlaceList;
field.PlaceList.prototype.xmlTagName='locality';

field.PlaceList.prototype.labelName='site'; // row label
field.PlaceList.prototype.helpText="This is the field collection site. Please tick 'cultivated' if the specimen is not from the wild."; // short summary help text
field.PlaceList.prototype.longHelp='sitehelp'; // message index key to lookup verbose help message

field.PlaceList.prototype.destroy=function () {
	for (var n=this.placeTable.rows.length-1; n>=0;n--) {this.destroy_place_row(this.placeTable.rows[n]);}

	if (this.placeTable.parentNode) {this.placeTable.parentNode.removeChild(this.placeTable);}
	this.placeTable=null;

	this.remove_row_event_handlers(); // help related hover handlers
	this.row=null;
	this.form=null;
};

field.PlaceList.prototype.destroy_place_row=function (row) {
	row.parentNode.removeChild(row); // delete current row

	row.place.destroy();
	row.place=null;

	row.deleteClickHandler=remove_event_handler(row.deleteClickHandler);
	row.addClickHandler=remove_event_handler(row.addClickHandler);

	if (row.regionChangeHandler) {
		row.regionChangeHandler=remove_event_handler(row.regionChangeHandler);
	}
	row.cultChangeHandler=remove_event_handler(row.cultChangeHandler);
	row.placeChangeHandler=remove_event_handler(row.placeChangeHandler);
	row.illegibleChangeHandler=remove_event_handler(row.illegibleChangeHandler);
	row.notesChangeHandler=remove_event_handler(row.notesChangeHandler);
	row.gridRefChangeHandler=remove_event_handler(row.gridRefChangeHandler);
	
	row.focusInfoEventHandle = remove_event_handler(row.focusInfoEventHandle);
	row.blurInfoEventHandle = remove_event_handler(row.blurInfoEventHandle);

	row.illegibleButton.parentNode.removeChild(row.illegibleButton);
	row.illegibleButton=null;
	row.externalId=null;

	for (var n=row.cells.length-1;n>=0;n--) {row.deleteCell(n);}
};

field.PlaceList.prototype.verify = function () {
	this.containerRow.error=false;
	this.containerRow.errorMsg=[];

	for (var n=this.placeTable.rows.length-1; n>=0;n--) {
		var place=this.placeTable.rows[n].place;
		
		if (!place.region.regionCode && (place.region.input.value && place.region.input.value != place.region.input.defaultValue)) {
			this.containerRow.error=true;
			this.containerRow.errorMsg[this.containerRow.errorMsg.length]="Not a valid region name.";
			break;
		}
		
		place.match_vc_and_gridref(false);
		
		if (place.gridRefError) {
			this.containerRow.error=true;
			this.containerRow.errorMsg[this.containerRow.errorMsg.length]=place.gridRefError;
		}
		
		if (place.altitudeError) {
			this.containerRow.error=true;
			this.containerRow.errorMsg[this.containerRow.errorMsg.length]=place.altitudeError;
		}
		
		if (place.precisionError) {
			this.containerRow.error=true;
			this.containerRow.errorMsg[this.containerRow.errorMsg.length]=place.precisionError;
		}
	}
	
	this.set_style(); // force immediate styling update
	return this.containerRow.error;
};

field.PlaceList.prototype.add_blank = function () {
	var row=this.placeTable.insertRow(this.placeTable.rows.length);
	row.id=uniqueId('pr');

	row.place=new PlaceSet(this.form,this.international);
	var cell=row.insertCell(0);

	cell.appendChild(row.place.placeName.container);
	cell.appendChild(row.place.region.container);
	
	cell.appendChild(row.place.country.menu);
	cell.appendChild(document.createElement('br'));
	row.place.geoDiv.appendChild(row.place.refTypeContainer);
	row.place.geoDiv.appendChild(row.place.gridRefField);
	row.place.geoDiv.appendChild(row.place.gridRefMoreButton);
	row.place.geoDiv.appendChild(row.place.latLongContainer);
	row.place.geoDiv.appendChild(row.place.precisionContainer);
	row.place.geoDiv.appendChild(row.place.refBasisContainer);
	row.place.geoDiv.appendChild(row.place.altitudeField);
	cell.appendChild(row.place.geoDiv);

	row.cultChangeHandler=register_event_handler(row.place.cultivated,'change',this.form,'change');
	row.countryChangeHandler=register_event_handler(row.place.country.menu,'change',this.form,'change');
	row.regionChangeHandler=register_event_handler(row.place.region.input,'change',this.form,'change');
	row.placeChangeHandler=register_event_handler(row.place.placeName.input,'change',this.form,'change');
	row.gridRefChangeHandler=register_event_handler(row.place.gridRefField,'change',this.form,'change');
	
	row.focusInfoEventHandle = register_event_handler(row.place.placeName.input, 'focus', this, 'place_focus', row.id);
	row.blurInfoEventHandle = register_event_handler(row.place.placeName.input, 'blur',  this, 'place_blur', row.id);

	var illegible=document.createElement("label");
	illegible.appendChild(document.createTextNode("("));
	row.illegibleButton=document.createElement("input");
	row.illegibleButton.type="checkbox";
	illegible.appendChild(row.illegibleButton);
	illegible.appendChild(document.createTextNode("illegible)"));

	cell.appendChild(illegible);
	cell.appendChild(row.place.cultivatedContainer);
	cell.appendChild(document.createElement('br'));
	cell.appendChild(row.place.notes);

	row.illegibleChangeHandler=register_event_handler(row.illegibleButton,'change',this.form,'change');
	row.notesChangeHandler=register_event_handler(row.place.notes,'change',this.form,'change');

	var button=deleteButton('delete site');
	button.style.verticalAlign='top';
	cell.appendChild(button);
	row.deleteClickHandler=register_event_handler(button,'click',this,"delete_click",row.id);

	button=plusButton('add another site');
	button.style.verticalAlign='top';
	cell.appendChild(button);
	row.addClickHandler=register_event_handler(button,'click',this,"add_click",0);
	return row;
};

/**
 * Event handler called when placename is focused - used to invoke right-hand (suggestions) info links and suggestions
 * @param event
 * @param element
 * @param rowId
 * @return
 */
field.PlaceList.prototype.place_focus=function (event,element,rowId) {
	var row = document.getElementById(rowId);
	
	this.form.parentData.info.seek_place_links(this.form, row.place, rowId, this);
	
	/*
	if (row.nameBox.dropbox.style.display!='block') {
		this.form.parentData.info.seek_provenance(this.form,'col',element,row.nameBox,row.id,this);
		if (row.nameBox.input.value=='') {row.nameBox.issue_suggestion_xml_request(this.form,'col');}
		else {
			if (!row.nameBox.personId) {
				row.nameBox.queue_xml_request(row.nameBox.input.value.length <2);
			}
		}
	}
	*/
};

/**
 * placelist lost focus so need to clear info panel,
 * do so after a delay (in case blur was due to click on list)
 */
field.PlaceList.prototype.place_blur = function (event,element,rowId) {
	this.form.parentData.info.clearIntervalHandle = window.setTimeout(associateObjectWithXMLRequest(this.form.parentData.info,"clear",null),200);
	//this.input_change(null,null,rowId);
};

/**
 * given a (potentially empty) array of localities (as xml) add these to the form
 */
field.PlaceList.prototype.add_from_xml = function (localities) {
	if (localities.length ===0) {
		this.add_blank(); // create a default entry
	} else {
		var l=localities.length;
		for (var n=0,locality=localities[0];n<l;locality=localities[++n]) {
			var row=this.add_blank(); // create a blank entry
			var expandGeoFieldsFlag=false; // need to expand if any non-default extended settings

			row.illegibleButton.checked=(locality.getAttribute('illegible')=="1" || locality.getAttribute('illegible')=="true");
			row.place.cultivated.checked=(locality.getAttribute('cultivated')=="1" || locality.getAttribute('cultivated')=="true");

			if (locality.getAttribute('id') >0) {row.place.externalId=locality.getAttribute('id');}

			var country=locality.getElementsByTagName('country');
			if (country.length ===1) {
				row.place.country.set(country[0].getAttribute('code'));

				var region=locality.getElementsByTagName('region');
				if (region.length ===1) {
					row.place.region.get_regions(country[0].getAttribute('code'),region[0].getAttribute('code'));
				}
				else {row.place.region.get_regions(country[0].getAttribute('code'),'');}
				
				row.place.region.country=country[0].getAttribute('code');
			} else {
				row.place.country.set('');
			}
			
			var preciseGridRef=locality.getElementsByTagName('precisegr');
			if (preciseGridRef.length===1 && preciseGridRef[0].hasChildNodes()) {
				row.place.gridRefField.style.color="black";
				row.place.gridRefField.value=preciseGridRef[0].firstChild.data;
				row.place.verifiedGR=preciseGridRef[0].getAttribute('verified');
				row.place.gridRefVcs=preciseGridRef[0].getAttribute('vc');
			}
			
			var preciseLatLngElement=locality.getElementsByTagName('preciselatlng');
			if (preciseLatLngElement.length===1 && preciseLatLngElement[0].hasChildNodes()) {
				var preciseLatLng=new LatLng();
				preciseLatLng.from_xml(preciseLatLngElement[0]);
				
				row.place.verifiedLat=preciseLatLng.lat;
				row.place.verifiedLng=preciseLatLng.lng;
				
				row.place.latDField.value=preciseLatLng.latD;
				row.place.latMField.value=preciseLatLng.latM;
				row.place.latSField.value=preciseLatLng.latS;
				row.place.longDField.value=preciseLatLng.lngD;
				row.place.longMField.value=preciseLatLng.lngM;
				row.place.longSField.value=preciseLatLng.lngS;
				row.place.longEWField.selectedIndex=(preciseLatLng.hemisphere==='E')?0:1;
				
				row.place.latDField.style.color="black";
				row.place.latMField.style.color="black";
				row.place.latSField.style.color="black";
				row.place.longDField.style.color="black";
				row.place.longMField.style.color="black";
				row.place.longSField.style.color="black";
			}

			var placename=locality.getElementsByTagName('placename');
			if (placename.length && placename[0].hasChildNodes()) {
				row.place.placeName.input.value=placename[0].firstChild.data;

				if (row.place.externalId) {
					row.place.placeName.input.style.color="green";

					var gridRef=locality.getElementsByTagName('gridref');
					if (gridRef.length ===1 && gridRef[0].hasChildNodes()) {
						
						if (gridRef[0].getAttribute('centre')) {
							row.place.pointGR=gridRef[0].getAttribute('centre');
						}
						
						if (gridRef[0].getAttribute('precision')) {
							row.place.defaultPrecision=gridRef[0].getAttribute('precision'); // precision (metres)
							
							if (row.place.defaultPrecision !=10000) {expandGeoFieldsFlag=true;}
							row.place.pointGR=gridRef[0].getAttribute('centre');
						}
						
						row.place.gridref=gridRef[0].firstChild.data;
						row.place.show_info_link(); // only shown if gridref exists
					}
					
					var baseLatLngElement=locality.getElementsByTagName('baselatlng');
					if (baseLatLngElement.length ===1) {
						row.place.pointLatLng.from_xml(baseLatLngElement[0]);
					}
				}
				else {
					row.place.placeName.input.style.color="black";
					row.place.placeName.show_search_links();
				}
			}
			
			var precisionElement=locality.getElementsByTagName('precision');
			if (precisionElement.length ===1 && precisionElement[0].hasChildNodes()) {
				row.place.precision.value=precisionElement[0].firstChild.data;
				row.place.precisionMetres=precisionElement[0].getAttribute('m');
				row.place.precision.style.color="black";
			}

			var locnotes=locality.getElementsByTagName('locnotes');
			if (locnotes.length===1 && locnotes[0].hasChildNodes()) {
				row.place.notes.value=locnotes[0].firstChild.data;
				row.place.notes.style.color='black';
			}
			
			var altitudeElements=locality.getElementsByTagName('altitude');
			if (altitudeElements.length===1 && altitudeElements[0].hasChildNodes()) {
				row.place.altitudeField.value=altitudeElements[0].firstChild.data;
				row.place.altitudeMetres=altitudeElements[0].getAttribute('m');
				row.place.altitudeField.style.color='black';
				expandGeoFieldsFlag=true;
			}
			
			var refType=locality.getAttribute('reftype');
			
			if (refType=='point') {
				expandGeoFieldsFlag=true;
				row.place.refType.selectedIndex=2;
				row.place.update_ref_type_visibility();
			} else {
				row.place.refType.selectedIndex=(refType=='gridsquare')?0:1;
			}
			
			if (expandGeoFieldsFlag) {
				row.place.show_expanded_geo_fields();
			}
			
			var refBasis=locality.getAttribute('refbasis');
			row.place.refBasis.selectedIndex=field.PlaceList.refBasisKeys[refBasis];
		}
	}
};

field.PlaceList.refBasisKeys= {
	col: 0,
	place: 1,
	vector: 2,
	unknown: 3,
	other: 4
};

field.PlaceList.prototype.get_xml = function (doc) {
	var places=doc.createDocumentFragment();
	var l=this.placeTable.rows.length;
	var place;
	
	for (var n=0; n<l;n++) {
		place=this.placeTable.rows[n].place;
		var locality=doc.createElement('locality');

		if (place.externalId >0) {locality.setAttribute('id',place.externalId);}
		locality.setAttribute('illegible',this.placeTable.rows[n].illegibleButton.checked.toString());
		locality.setAttribute('cultivated',place.cultivated.checked.toString());
		locality.setAttribute('reftype',place.refType.options[place.refType.selectedIndex].value);
		locality.setAttribute('refbasis',place.refBasis.options[place.refBasis.selectedIndex].value);

		var regionCountry=place.region.regionCode.split('_');

		
		if (place.country.menu.selectedIndex >=0) {
			var country=doc.createElement('country');
			country.setAttribute('code',place.country.menu.options[place.country.menu.selectedIndex].value);
			country.appendChild(doc.createTextNode(place.country.menu.options[place.country.menu.selectedIndex].text));
			locality.appendChild(country);
		}

		var region=doc.createElement('region');
		region.setAttribute('code',regionCountry[0]);
		if (place.region.input.value != place.region.input.defaultValue) {region.appendChild(doc.createTextNode(place.region.input.value));}
		locality.appendChild(region);

		var placename=doc.createElement('placename');
		//placename.setAttribute('id',place.placeName.placeId);
		if (place.placeName.input.value !== place.placeName.input.defaultValue) {placename.appendChild(doc.createTextNode(place.placeName.input.value.replace(trimRegex,'')));}
		locality.appendChild(placename);
		
		if (place.gridRefField.value !== place.gridRefField.defaultValue) {
			var preciseGRElement=doc.createElement('precisegr');
			preciseGRElement.appendChild(doc.createTextNode(place.gridRefField.value));
			if (place.gridRefVcs && place.gridRefVcs.length) {preciseGRElement.setAttribute('vc',place.gridRefVcs);}
			if (place.verifiedGR && place.verifiedGR.length) {preciseGRElement.setAttribute('verified',place.verifiedGR);}
			locality.appendChild(preciseGRElement);
		}
		
		if (place.pointLatLng.is_set()) {
			locality.appendChild(doc.createElement('baselatlng')).appendChild(place.pointLatLng.to_xml(doc));
		}
		
		if (place.latlng_is_modified()) {
			var latLng=new LatLng();
			
			if (place.latDField.value !== place.latDField.defaultValue) {latLng.latD=place.latDField.value;}
			if (place.latMField.value !== place.latMField.defaultValue) {latLng.latM=place.latMField.value;}
			if (place.latSField.value !== place.latSField.defaultValue) {latLng.latS=place.latSField.value;}
		
			if (place.longDField.value !== place.longDField.defaultValue) {latLng.lngD=place.longDField.value;}
			if (place.longMField.value !== place.longMField.defaultValue) {latLng.lngM=place.longMField.value;}
			if (place.longSField.value !== place.longSField.defaultValue) {latLng.lngS=place.longSField.value;}
			latLng.hemisphere=place.longEWField.options[place.longEWField.selectedIndex].value;
			
			latLng.lat=place.verifiedLat;
			latLng.lng=place.verifiedLng;
			locality.appendChild(doc.createElement('preciselatlng')).appendChild(latLng.to_xml(doc));
		}
		
		if (place.altitudeField.value !== place.altitudeField.defaultValue) {
			var altitudeElement=doc.createElement('altitude');
			altitudeElement.setAttribute('m',place.altitudeMetres);
			altitudeElement.appendChild(doc.createTextNode(place.altitudeField.value));
			locality.appendChild(altitudeElement);
		}
		
		if (place.precision.value !== place.precision.defaultValue) {
			var precisionElement=doc.createElement('precision');
			precisionElement.setAttribute('m',place.precisionMetres);
			precisionElement.appendChild(doc.createTextNode(place.precision.value));
			locality.appendChild(precisionElement);
		}

		var notes=doc.createElement('locnotes');
		if (place.notes.value != place.notes.defaultValue) {notes.appendChild(doc.createTextNode(place.notes.value));}
		locality.appendChild(notes);

		if (place.gridref) {
			var gridRef=doc.createElement('gridref');
			if (place.pointGR) {
				gridRef.setAttribute('centre',place.pointGR);
				gridRef.setAttribute('precision',place.defaultPrecision);
			}
			gridRef.appendChild(doc.createTextNode(place.gridref));
			locality.appendChild(gridRef);
		}

		places.appendChild(locality);
	}
	return places;
};

field.PlaceList.prototype.delete_click = function (event, element, rowId) {
	this.destroy_place_row(document.getElementById(rowId));

	if (this.placeTable.rows.length===0) {this.add_blank();}
	this.form.verify(); // if deleted row had error then need to clear this
};

field.PlaceList.prototype.add_click = function (event, element, param) {
	this.add_blank();
};

/**
 * return localties as html fragment
 * @return html fragment or null
 */
field.PlaceList.prototype.get_html =function () {
	var lump;
	var l=this.placeTable.rows.length;
	var fragment=document.createElement('ul');

	for (var n=0; n<l;n++) {
		var placeString=[];

		lump=this.placeTable.rows[n].place.country.menu.options[this.placeTable.rows[n].place.country.menu.selectedIndex].text;
		if (lump.length && lump!='country') {placeString.push(lump);}

		lump=this.placeTable.rows[n].place.region.input.value;
		if (lump.length && lump!=this.placeTable.rows[n].place.region.input.defaultValue) {placeString.push(lump);}

		lump=(this.placeTable.rows[n].place.placeName.input.value!=this.placeTable.rows[n].place.placeName.input.defaultValue)?lump=this.placeTable.rows[n].place.placeName.input.value.replace(trimRegex,''):'';
		if (this.placeTable.rows[n].illegibleButton.checked) {lump+=" [illegible]";}
		if (lump.length) {placeString.push(lump);}

		if (placeString.length) {
			fragment.appendChild(document.createElement('li')).appendChild(document.createTextNode(placeString.join(", ")));
		}
	}

	if (fragment.hasChildNodes()) {
		return fragment;
	} else {
		return null;
	}
};

/**
 *
 */
function LatLng() {
	this.lat=''; // complete (real number) latitude for centroid, for external linkage etc.
	this.latD=''; // integer degrees latitude
	this.latM=''; // integer minutes latitude
	this.latS=''; // real seconds latitude
	this.lng=''; // complete (real number) longitude for centroid, for external linkage etc.
	this.lngD=''; // integer degrees longitude
	this.lngM=''; // integer minutes longitude
	this.lngS=''; // real seconds longitude
	this.hemisphere=''; // hemisphere 'E' | 'W' (caps)
}

LatLng.prototype.from_xml = function (xml) {
	// <latitude d="" m="" s="">nnn</latitude>
	// <longitude d="" m="" s="" hemisphere="E|W">nnn</longitude>
	if (xml.hasChildNodes()) {
		var latElement=xml.getElementsByTagName('latitude')[0];
		var lngElement=xml.getElementsByTagName('longitude')[0];
		
		this.lat=(latElement.hasChildNodes() && latElement.firstChild.data)?latElement.firstChild.data:''; // need to avoid potential null
		this.latD=latElement.getAttribute('d');
		this.latM=latElement.getAttribute('m');
		this.latS=latElement.getAttribute('s');
		
		this.lng=(lngElement.hasChildNodes() && lngElement.firstChild.data)?lngElement.firstChild.data:''; // need to avoid potential null
		this.lngD=lngElement.getAttribute('d');
		this.lngM=lngElement.getAttribute('m');
		this.lngS=lngElement.getAttribute('s');
		this.hemisphere=lngElement.getAttribute('hemisphere');
	}
};

LatLng.prototype.to_xml = function(doc) {
	var fragment=doc.createDocumentFragment();
	var lat=doc.createElement('latitude');
	lat.setAttribute('d',this.latD);
	lat.setAttribute('m',this.latM);
	lat.setAttribute('s',this.latS);
	lat.appendChild(doc.createTextNode(this.lat));
	
	var lng=doc.createElement('longitude');
	lng.setAttribute('d',this.lngD);
	lng.setAttribute('m',this.lngM);
	lng.setAttribute('s',this.lngS);
	lng.setAttribute('hemisphere',this.hemisphere);
	lng.appendChild(doc.createTextNode(this.lng));
	
	fragment.appendChild(lat);
	fragment.appendChild(lng);
	return fragment;
};

LatLng.prototype.to_string = function() {
	
};

LatLng.prototype.is_set = function() {
	return (this.lat || this.lng);
};

/**
 * package of placename, region and country fields, grid ref related gunk and locality notes
 * @param <SpecimenForm> form parent SpecimenForm
 */
function PlaceSet(form) {
	this.form=form;
	this.cultivated=document.createElement("input");
	this.cultivated.type="checkbox";

	this.cultivatedContainer=document.createElement("label");
	this.cultivatedContainer.appendChild(document.createTextNode(' ('));
	this.cultivatedContainer.style.margin="0";
	this.cultivatedContainer.style.padding="0";
	this.cultivatedContainer.appendChild(this.cultivated);
	this.cultivatedContainer.appendChild(document.createTextNode('cultivated)'));

	this.country=new CountryList(this.international);

	this.suspendBlur=false;

	this.countryChangeHandler=register_event_handler(this.country.menu,'change',this,'country_change','');

	this.region=new RegionList();
	this.region.get_regions('gb');
    this.region.get_regions('ie');

	this.notes=document.createElement("textarea");
	this.notes.title="specific locality, habitat etc";
	this.notes.style.width="85%";
	this.notes.rows="1";

	this.notes.defaultValue='precise site or habitat information';
	input_set_default(this.notes);
	this.notes.onfocus=input_default_focus;
	this.notes.onblur=input_default_blur;

	this.placeName=new PlaceName(this);
	
	this.region.input.onkeyup=associate_obj_with_event(this, "region_key_up",'');
	this.regionChangeHandler=register_event_handler(this.region.input,'change',this,'region_change',''); //either region or place flags id as invalid
	
	this.placeChangeHandler=register_event_handler(this.placeName.input,'change',this,'place_change','');

	this.infoLink=document.createElement('a');
	var OSicon=document.createElement('img');
	OSicon.src='http://static.herbariumathome.org/osicon.png';
	OSicon.width=16;
	OSicon.height=16;
	OSicon.alt="map";
	this.infoLink.appendChild(OSicon);
	this.infoLink.setAttribute('target','_blank');
	
	this.geoDiv=document.createElement('div');
	this.geoDiv.id=uniqueId('geodiv');
	this.geoDiv.style.display='inline';
	this.geoDiv.style.MozBorderRadius="4px";
	this.geoDiv.style.webkitBorderRadius="4px";
	
	this.gridRefField=document.createElement("input");
	this.gridRefField.setAttribute('title','Ordnance survey grid reference, e.g. 34/123234 or SD2367');
	this.gridRefField.defaultValue=PlaceSet.staticText.defaultGridRefField;
	input_set_default(this.gridRefField);
	this.gridRefField.onfocus=input_default_focus;
	this.gridRefField.onblur=input_default_blur;
	
	this.altitudeField=this.geo_input_field('Altitude, e.g. 35m or 115\' or 115ft','altitude','none',5);
	this.altitudeChangeHandler=register_event_handler(this.altitudeField,'blur',this,'altitude_change','');
	
	this.latLongContainer=document.createElement('span');
	this.latLongContainer.style.display='none';
	this.longDField=this.geo_input_field('degrees longitude',PlaceSet.staticText.defaultLngD,'inline',5);
	this.longMField=this.geo_input_field('minutes longitude',PlaceSet.staticText.defaultLngM,'inline',2);
	this.longSField=this.geo_input_field('seconds longitude',PlaceSet.staticText.defaultLngS,'inline',4);
	this.longitude=document.createElement('span');
	this.longitude.style.marginLeft='4px';
	this.longitude.appendChild(document.createTextNode('long'));
	this.longitude.appendChild(this.longDField);
	this.longitude.appendChild(this.longMField);
	this.longitude.appendChild(this.longSField);
	this.longEWField=document.createElement('select');
	this.longEWField.options[0]=new Option('E','E',true,true);
	this.longEWField.options[1]=new Option('W','W',false,false);
	this.longitude.appendChild(this.longEWField);
	
	this.latDField=this.geo_input_field('degrees latitude',PlaceSet.staticText.defaultLatD,'inline',5);
	this.latMField=this.geo_input_field('minutes latitude',PlaceSet.staticText.defaultLatM,'inline',2);
	this.latSField=this.geo_input_field('seconds latitude',PlaceSet.staticText.defaultLatS,'inline',4);
	this.latitude=document.createElement('span');
	this.latitude.style.marginLeft='4px';
	this.latitude.appendChild(document.createTextNode('lat'));
	this.latitude.appendChild(this.latDField);
	this.latitude.appendChild(this.latMField);
	this.latitude.appendChild(this.latSField);
	this.latitude.appendChild(document.createElement('br'));
	this.latLongContainer.appendChild(this.longitude);
	this.latLongContainer.appendChild(this.latitude);	
	
	this.refTypeContainer=document.createElement('span');
	this.refTypeContainer.appendChild(document.createTextNode('georeference type'));	
	this.refType=document.createElement('select');
	this.refType.options[0]=new Option('grid square','gridsquare',true,true);
	this.refType.options[1]=new Option('grid point','gridpoint',false,false);
	this.refType.options[2]=new Option('latitude/longitude','point',false,false);
	this.refTypeContainer.appendChild(this.refType);
	this.refTypeContainer.style.display='none';
	
	this.refBasisContainer=document.createElement('span');
	this.refBasisContainer.appendChild(document.createElement('br'));
	this.refBasisContainer.appendChild(document.createTextNode('geo-ref. source'));	
	this.refBasis=document.createElement('select');
	this.refBasis.options[0]=new Option('collector','col',false,false);
	this.refBasis.options[1]=new Option('nearest named place','place',true,true);
	this.refBasis.options[2]=new Option('calculated','vector',false,false);
	this.refBasis.options[3]=new Option('not known','unknown',false,false);
	this.refBasis.options[4]=new Option('other (please state)','other',false,false);
	this.refBasisContainer.appendChild(this.refBasis);
	this.refBasisContainer.style.display='none';
	
	this.precisionContainer=document.createElement('span');
	this.precisionContainer.appendChild(document.createTextNode('precision'));
	this.precision=this.geo_input_field('precision','m or ft','inline',5);
	this.precisionContainer.appendChild(this.precision);
	this.precisionContainer.style.display='none';
	
	this.precisionChangeHandler=register_event_handler(this.precision,'blur',this,'precision_change','');
	
	this.gridRefMoreButton=more_button('show additional geolocation fields');
	this.moreButtonHandler=register_event_handler(this.gridRefMoreButton,'click',this,'more_click','');
	
	this.gridRefChangeHandler=register_event_handler(this.gridRefField,'blur',this,'gridref_change','');
	
	this.latLongChangeHandlerLatD=register_event_handler(this.latDField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLatM=register_event_handler(this.latMField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLatS=register_event_handler(this.latSField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLongD=register_event_handler(this.longDField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLongM=register_event_handler(this.longMField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLongS=register_event_handler(this.longSField,'blur',this,'latlong_change','');
	this.latLongChangeHandlerLongEW=register_event_handler(this.longEWField,'change',this,'latlong_change','');
	
	this.refBasisChangeHandler=register_event_handler(this.refBasis,'change',this,'ref_basis_change','');
	this.refTypeChangeHandler=register_event_handler(this.refType,'change',this,'ref_type_change','');

	this.reset_place();
	this.reset_user_geo_settings();

	this.callIntervalHandle=null;
	this.pending=0;
	this.showExpandedGeoFields=false;
}

PlaceSet.staticText = {
	defaultLatD : '° lat',
	defaultLatM : "'",
	defaultLatS : '"',
	defaultLngD : '° long',
	defaultLngM : "'",
	defaultLngS : '"',
	defaultGridRefField : 'OS grid ref'
};

PlaceSet.prototype.geo_input_field = function (title,defaultValue,display,maxLength) {
	var field=document.createElement("input");
	
	field.setAttribute('title',title);
	field.defaultValue=defaultValue;
	input_set_default(field);
	field.onfocus=input_default_focus;
	field.onblur=input_default_blur;
	field.style.display=display;
	field.style.width=maxLength+"em";
	
	return field;
};

if (standardBrowser)
{
	PlaceSet.prototype.pos_number_key =function (event, element, param) {
		if (!((event.keyCode>=48 && event.keyCode<=57) || (event.keyCode == 46) || (event.keyCode == 107) || (event.keyCode >=96 && event.keyCode<=105) )) {event.preventDefault();}
	};
}
else
{
	PlaceSet.prototype.pos_number_key =function (event, element, param) {
		//event.returnValue = (event.keyCode <=58) || (event.keyCode == 75) || (event.keyCode == 107) || (event.keyCode >=96 && event.keyCode<=105);
	};
}

/*
 * var valid_key=function(event) {
		return (event.keyCode >= 46 || event.keyCode==32 || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
		// see http://www.js-examples.com/page/tutorials__key_codes.html
	};
	
	var valid_pos_num_key=function(event) {
		return ((event.keyCode>=48 && event.keyCode<=57) || event.keyCode == 46 || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
	
	var valid_pos_int_key=function(event) {
		return ((event.keyCode>=48 && event.keyCode<=57) || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
}
else
{
	var valid_key=function(event) {
		return (event.keyCode >= 46 || event.keyCode==32 || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
		//event.returnValue = ((event.keyCode <=58) || (event.keyCode == 191)) && (event.keyCode !=32);
	};
	
	var valid_pos_num_key=function(event) {
		return ((event.keyCode >= 48 && event.keyCode <= 57) || event.keyCode==46 || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
	
	var valid_pos_int_key=function(event) {
		return ((event.keyCode >= 48 && event.keyCode <= 57) || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
 * 
 */

PlaceSet.units=[
	{	name: "m",
		regex: /^(\d+)\s*m$/,
	 	metres: 1
	},
	{	name: "ft",
		regex: /^(\d+)\s*ft$/,
	 	metres: 0.3048
	},
	{	name: "ft",
		regex: /^(\d+)\s*'$/,
	 	metres: 0.3048
	},
	{	name: "km",
		regex: /^(\d+)\s*km$/,
	 	metres: 1000
	},
	{	name: "mile",
		regex: /^(\d+)\s*miles?$/,
	 	metres: 1609.344
	},
	{	name: "yd",
		regex: /^(\d+)\s*yds?$/,
	 	metres: 0.9144
	}
];

/**
 * parse string, returning value in metres and tidying string
 */
PlaceSet.prototype.parse_linear_unit = function (unitString) {
	unitString.replace(/\s+/g,''); // dump spaces
	var matches;
	
	for (var i in PlaceSet.units) {
		var template=PlaceSet.units[i];
		
		matches=unitString.match(template.regex);
		
		if (matches!==null) {
			return {error: false, m: Math.round(matches[1]*template.metres), clean: (matches[1]+template.name)};
		}
	}
	
	return {error: true};
};

PlaceSet.prototype.precision_change = function (event,element) {
	element.value=element.value.replace(trimRegex,''); // trim
	
	if (element.value!=='') {
		var parsed=this.parse_linear_unit(element.value);
		
		if (!parsed.error) {
			element.value=parsed.clean;
			this.precisionMetres=parsed.m;
			this.precisionError='';
		} else {
			this.precisionMetres='';
			this.precisionError='Precision value not valid or missing units (e.g. m, ft)';
		}
	}
	this.form.verify();
};

PlaceSet.prototype.altitude_change = function (event,element) {
	element.value=element.value.replace(trimRegex,''); // trim
	
	if (element.value!=='' && element.value!=element.defaultValue) {
		var parsed=this.parse_linear_unit(element.value);
		if (!parsed.error) {
			element.value=parsed.clean;
			this.altitudeMetres=parsed.m;
			this.altitudeError='';
		} else {
			this.altitudeMetres='';
			this.altitudeError='Altitude value not valid or missing units (e.g. m, ft)';
		}
	} else {
		this.altitudeMetres='';
		this.altitudeError='';
		input_set_default(element);
	}
	this.form.verify();
};

PlaceSet.prototype.more_click = function () {
	this.show_expanded_geo_fields();
};

PlaceSet.prototype.show_expanded_geo_fields = function () {
	if (!this.showExpandedGeoFields) {
		this.showExpandedGeoFields=true;
		
		this.geoDiv.style.display='block';
		this.geoDiv.style.backgroundColor='#ffcc99';
		this.geoDiv.style.padding="4px";
		this.geoDiv.style.margin="3px 0 3px 0";
		this.geoDiv.style.width="83%";
		this.gridRefMoreButton.style.display="none";
		
		this.altitudeField.style.display='block';
		this.refTypeContainer.style.display='inline';
		this.refBasisContainer.style.display='inline';
		
		this.precisionContainer.style.display='inline';
		this.update_ref_type_visibility();
	}
};

PlaceSet.prototype.update_ref_type_visibility = function () {
	if (this.refType.selectedIndex < 2) {
		// show GR fields
		this.latLongContainer.style.display='none';
		this.gridRefField.style.display='inline';
		this.gridRefField.style.display='inline';
		this.gridRefField.style.marginLeft="1em";
		
		if (this.refType.selectedIndex===1) {
			this.precisionContainer.style.display='inline';
			this.precisionContainer.style.marginLeft="1em";
		} else {
			this.precisionContainer.style.display='none';
		}
		
		/*
		if (this.gridRefField.value=='' || this.gridRefField.value == this.gridRefField.defaultValue) {
				// currently showing default value -- change this to be appropriate precision
			
		
			if (this.refType.selectedIndex ===0) {
				// grid square
				
				
			} else {
				// grid point
				
			}
		}
		*/
		
	} else {
		this.latLongContainer.style.display='block';
		this.precisionContainer.style.display='inline';
		this.precisionContainer.style.marginLeft="0em";
		this.gridRefField.style.display='none';
		this.gridRefField.style.display='none';
	}
	this.show_info_link(); // refresh precision of grid ref field default value
};

PlaceSet.prototype.destroy = function () {
	this.form=null;

	if (this.countryChangeHandler!==null) {this.countryChangeHandler=remove_event_handler(this.countryChangeHandler);}

	if (!this.international) {
		if (this.regionChangeHandler) {this.regionChangeHandler=remove_event_handler(this.regionChangeHandler);}
		
		this.region.destroy();
		this.region=null;
	}
	
	if (this.moreButtonHandler) {
		this.moreButtonHandler=remove_event_handler(this.moreButtonHandler);
	}
	this.gridRefMoreButton.parentNode.removeChild(this.gridRefMoreButton);
	this.gridRefMoreButton=null;

	this.placeChangeHandler=remove_event_handler(this.placeChangeHandler);
	this.gridRefChangeHandler=remove_event_handler(this.gridRefChangeHandler);
	
	this.latLongChangeHandlerLatD=remove_event_handler(this.latLongChangeHandlerLatD);
	this.latLongChangeHandlerLatM=remove_event_handler(this.latLongChangeHandlerLatM);
	this.latLongChangeHandlerLatS=remove_event_handler(this.latLongChangeHandlerLatS);
	this.latLongChangeHandlerLongD=remove_event_handler(this.latLongChangeHandlerLongD);
	this.latLongChangeHandlerLongM=remove_event_handler(this.latLongChangeHandlerLongM);
	this.latLongChangeHandlerLongS=remove_event_handler(this.latLongChangeHandlerLongS);
	this.latLongChangeHandlerLongEW=remove_event_handler(this.latLongChangeHandlerLongEW);
	
	this.precisionChangeHandler=remove_event_handler(this.precisionChangeHandler);
	this.altitudeChangeHandler=remove_event_handler(this.altitudeChangeHandler);
	
	this.refBasisChangeHandler=remove_event_handler(this.refBasisChangeHandler);
	this.refTypeChangeHandler=remove_event_handler(this.refTypeChangeHandler);

	this.placeName.destroy();
	this.placeName=null;

	this.notes.defaultValue=null;
	this.notes.onfocus=null;
	this.notes.onblur=null;
	
	this.gridRefField.defaultValue=null;
	this.gridRefField.onfocus=null;
	this.gridRefField.onblur=null;

	this.cultivatedContainer.removeChild(this.cultivated);
	this.cultivated=null;
	this.cultivatedContainer.parentNode.removeChild(this.cultivatedContainer);
	this.cultivatedContainer=null;

	this.notes.parentNode.removeChild(this.notes);
	this.notes=null;
	
	this.gridRefField.parentNode.removeChild(this.gridRefField);
	this.gridRefField=null;

	this.country=null;
	
	this.infoLink=null;

	this.externalId=null;
	this.gridref=null;
};

PlaceSet.prototype.ref_basis_change = function (event,element) {
	
};

PlaceSet.prototype.ref_type_change = function (event,element) {
	this.update_ref_type_visibility();
};

PlaceSet.prototype.country_change = function (event,element,param) {
	var countryCode=element.options[element.selectedIndex].value;
	
	var oldCountry=(this.region.country)?this.region.country.toLowerCase():'';
	//var oldRegionName=this.region.input.value;

	if (countryCode=='more') {
		this.country.countries.length=0; // currently this is too destructive, need parallel international and local lists
		this.country.reset_list(true);
		
		return;
	}

	if (countryCode!='all') { 
		this.region.get_regions(countryCode);

		if (RegionList.regions[element.options[element.selectedIndex].value] && RegionList.regions[element.options[element.selectedIndex].value].count == 1) {
			// single region
			//this.region.regionCode=RegionList.regions[element.options[element.selectedIndex].value]
			for (var code in RegionList.regions[element.options[element.selectedIndex].value]) {
				if (code != 'count') {this.region.regionCode=code;}
			}

			this.region.input.value=RegionList.regions[element.options[element.selectedIndex].value][this.region.regionCode];
			this.region.input.setAttribute('disabled','disabled');
		}
		else {this.region.input.removeAttribute('disabled');}
		
		if (countryCode.substr(0,2) == oldCountry.substr(0,2)) {
			if (countryCode.substr(3)) {
				// have new province so need to test for region match
				
				this.region.country=countryCode;
				
				if (!this.region.match_input()) {
					// region name is nolonger valid, so invalid placename and region
					
					this.reset_place();
					if (this.placeName.input.value && this.placeName.input.value != this.placeName.input.defaultValue) {this.placeName.input.style.color="black";}
					if (this.region.input.value && this.region.input.value != this.region.input.defaultValue) {this.region.input.style.color="black";}
				}
			}
			
		} else {
			// country has changed
			
			this.reset_place();
			if (this.placeName.input.value && this.placeName.input.value != this.placeName.input.defaultValue) {this.placeName.input.style.color="black";}
			if (this.region.input.value && this.region.input.value != this.region.input.defaultValue) {this.region.input.style.color="black";}
		}
	} else {
		this.region.get_regions('gb');
		this.region.get_regions('ie');

		this.region.input.removeAttribute('disabled');
	}
};

/**
 * reset variables related to the place id (but not user-specified grid refs)
 */
PlaceSet.prototype.reset_place = function () {
	this.externalId='';
	this.gridref=''; // displayed ref associated with this place id
	this.pointGR=''; // point ref (OSGB associated with named place id)
	
	this.pointLatLng=new LatLng(); // create/reset lat/lng  
	this.defaultRefType='gridpoint'; // 'gridpoint','gridsquare','point'
	this.defaultPrecision=10000;
};

/**
 * reset everything (i.e. including precision and altitude)
 */
PlaceSet.prototype.reset_user_geo_settings = function () {
	this.reset_user_gridref();
	this.altitudeMetres=''; // altitude in metres, (parsed version of altitude string)
	this.precisionMetres=''; // precision in metres, (parsed version of user precision string)
};

/**
 * reset user-specified grid refs
 */
PlaceSet.prototype.reset_user_gridref = function () {
	this.gridRefError=''; // error message string
	this.altitudeError='';
	this.gridRefVcs=''; // Vice-counties associated with grid ref
	this.verifiedGR=''; // ref as return from verification routine (parsed string)
	this.verifiedLat=''; // as returned from verification routine
	this.verifiedLng=''; // as returned from verification routine
	this.geoRefCountry=''; // country code derrived from georef
	
	// need to clear the alternative version of the reference not currently displayed
	if (this.refType.selectedIndex < 2) {
		// clear lat lng
		this.latDField.value=this.latDField.defaultValue;
		this.latMField.value=this.latMField.defaultValue;
		this.latSField.value=this.latSField.defaultValue;
		this.longDField.value=this.longDField.defaultValue;
		this.longMField.value=this.longMField.defaultValue;
		this.longSField.value=this.longSField.defaultValue;
		
	} else {
		// clear GR
		this.gridRefField.value='';
	}
};

PlaceSet.prototype.place_change = function (event,element) {
	this.reset_place();
	if (this.placeName.input.value && this.placeName.input.value != this.placeName.input.defaultValue) {this.placeName.input.style.color="black";}
};

PlaceSet.prototype.gridref_change = function (event,element) {
	this.reset_user_gridref(); // nolonger have confidence in grid ref
	
	if (this.gridRefField.value && this.gridRefField.value != this.gridRefField.defaultValue) {
		var url=scriptUrl+"XMLparsegeoref.php?ref="+this.gridRefField.value.replace(/\s/g,'')+"&country="+this.country.menu.options[this.country.menu.selectedIndex].value+"&reftype="+this.refType.options[this.refType.selectedIndex].value;
		
		this.gridrefXMLrequest=agnostic_XMLHttpRequest(); // create the request object
		xml_request(this.gridrefXMLrequest,url,associateObjectWithXMLRequest(this, "georef_xml_handler", this.gridRefField.value));
	}
	
	this.form.verify(); // verify in case need to clear errors (may be overkill to do whole form though)
	this.show_info_link(); // refresh (and possibly hide) info link
};

/**
 * test if user specified latlng has other than default values
 */
PlaceSet.prototype.latlng_is_modified = function() {
	return ((this.latDField.value !=="" && this.latDField.value != this.latDField.defaultValue) ||
		(this.latMField.value !=="" && this.latMField.value != this.latMField.defaultValue) ||
		(this.latSField.value !=="" && this.latSField.value != this.latSField.defaultValue) ||
		(this.longDField.value !=="" && this.longDField.value != this.longDField.defaultValue) ||
		(this.longMField.value !=="" && this.longMField.value != this.longMField.defaultValue) ||
		(this.longSField.value !=="" && this.longSField.value != this.longSField.defaultValue) ||
		(this.pointLatLng.hemisphere !='' && this.longEWField.options[this.longEWField.selectedIndex].value != this.pointLatLng.hemisphere));
};

PlaceSet.prototype.latlong_change = function (event,element) {
	this.reset_user_gridref(); // nolonger have confidence in grid ref
	this.show_info_link(); // refresh (and possibly hide) info link
	
	if (this.latlng_is_modified()) {
		var latD=(this.latDField.value !== this.latDField.defaultValue && this.latDField.value!=='')?this.latDField.value:this.pointLatLng.latD;
		var latM=(this.latMField.value !== this.latMField.defaultValue || this.latMField.defaultValue==='')?this.latMField.value:this.pointLatLng.latM;
		var latS=(this.latSField.value !== this.latSField.defaultValue || this.latSField.defaultValue==='')?this.latSField.value:this.pointLatLng.latS;
		
		var longD=(this.longDField.value !== this.longDField.defaultValue && this.longDField.value!=='')?this.longDField.value:this.pointLatLng.lngD;
		var longM=(this.longMField.value !== this.longMField.defaultValue || this.longMField.defaultValue==='')?this.longMField.value:this.pointLatLng.lngM;
		var longS=(this.longSField.value !== this.longSField.defaultValue || this.longSField.defaultValue==='')?this.longSField.value:this.pointLatLng.lngS;
		
		var url=scriptUrl+"XMLparsegeoref.php?lat="+latD+"&latm="+latM+"&lats="+latS+"&long="+longD+"&longm="+longM+"&longs="+longS+"&hemisphere="+this.longEWField.options[this.longEWField.selectedIndex].value+"&country="+this.country.menu.options[this.country.menu.selectedIndex].value+"&reftype="+this.refType.options[this.refType.selectedIndex].value;
		
		this.gridrefXMLrequest=agnostic_XMLHttpRequest(); // create the request object
		
		xml_request(this.gridrefXMLrequest,url,associateObjectWithXMLRequest(this, "georef_xml_handler", this.gridRefField.value));
	}
	
	this.form.verify(); // verify in case need to clear errors (may be overkill to do whole form though)
};

PlaceSet.prototype.region_change = function (event,element) {
	if (element.value && element.value!=element.defaultValue) {
		if (!this.region.match_input()) { // check if region value matches region name
			this.region.input.style.color="black";
			element.style.backgroundColor="#ff9999";
		} else {
			if (this.placeName.input.value && this.placeName.input.value != this.placeName.input.defaultValue) {this.placeName.input.style.color="black";}
			element.style.backgroundColor="white";
			this.country.set(this.region.country);
		}
	} else {
		element.style.backgroundColor="white";
	}

	this.reset_place();
	this.hide_info_link();
	this.form.verify();
};

PlaceSet.prototype.region_key_up=function (event,element) {
	if (valid_key(event)) {
		this.reset_place(); // edited entry so id now not valid
		this.region.regionCode='';
		if (this.placeName.input.value && this.placeName.input.value != this.placeName.input.defaultValue) {this.placeName.input.style.color="black";}
		this.region.input.style.color="black";
			
		this.set_region_dropbox(this.country.menu.options[this.country.menu.selectedIndex].value);
	} else {
		if (element.value.length ===0) {
			this.region.dropbox_close_now();
		} else {
			if (event.keyCode===38) {this.change_region_selection(-1);} // up arrow
			else if (event.keyCode===40) {this.change_region_selection(1);} // down arrow
			else if (event.keyCode===13) {
				// return key

				this.region.dropbox_close_now();
			}
		}
	}
};

PlaceSet.prototype.change_region_selection =function (direction)
{
	var links=this.region.dropbox.getElementsByTagName('a');

	if (links.length && ((direction > 0 && this.region.dropbox.selectedIndex < links.length) || (direction <0 && this.region.dropbox.selectedIndex >1))) {
		this.region.set_selection(this.region.dropbox.selectedIndex+direction,links);
		this.region.input.value=links[this.region.dropbox.selectedIndex-1].params.name;
		this.region.regionCode=links[this.region.dropbox.selectedIndex-1].params.regionCode;

		this.region.input.style.color="green";

		var regionCountry=this.region.regionCode.split('_');
		this.country.set(regionCountry[1]);
	}
};

PlaceSet.prototype.apply_change = function (params) {
	this.placeName.input.value=params.name;
	this.placeName.input.style.color="green";
	this.placeName.hide_search_links();
	
	this.externalId=params.externalId;
	this.gridref=params.gridref;
	this.pointGR=params.pointGR;
	this.country.set(params.country);
	this.region.get_regions(params.country,params.region);
	this.region.country=params.country;
	
	this.pointLatLng=params.pointLatLng;
	
	this.defaultRefType=params.defaultRefType; // 'gridpoint','gridsquare','point'
	this.defaultPrecision=params.defaultPrecision;
	
	this.show_info_link();
};

PlaceSet.prototype.georef_xml_handler=function (rawGridRefValue) {
	if (this.gridrefXMLrequest && this.gridrefXMLrequest.readyState == 4 && this.gridrefXMLrequest.status == 200) {
    	var response  = this.gridrefXMLrequest.responseXML.documentElement;
    	
    	if (response) {
    		if (response.getElementsByTagName('error').length) {
    			this.gridRefError=response.getElementsByTagName('error')[0].firstChild.data;
    			this.form.verify();
    		} else {
    			var gridrefElement=response.getElementsByTagName('gridref')[0];
    			var sourceRefElement=response.getElementsByTagName('sourceref')[0];
    			var latElement=response.getElementsByTagName('latitude')[0];
    			var lngElement=response.getElementsByTagName('longitude')[0];
    			
    			this.verifiedGR=gridrefElement.firstChild.data; // have error here with missing data, need to check circumstances
    			this.verifiedLat=latElement.firstChild.data;
    			this.verifiedLng=lngElement.firstChild.data;
    			
    			if (sourceRefElement.getAttribute('type')!=='point') {
	    			if (this.gridRefField.value===rawGridRefValue && this.gridRefField.value!=this.verifiedGR) {
	    				this.gridRefField.value=this.verifiedGR;
	    			}
	    				
    				this.latDField.value=latElement.getAttribute('d');
    				this.latMField.value=latElement.getAttribute('m');
    				this.latSField.value=latElement.getAttribute('s');
    				this.longDField.value=lngElement.getAttribute('d');
    				this.longMField.value=lngElement.getAttribute('m');
    				this.longSField.value=lngElement.getAttribute('s');
    				this.longEWField.selectedIndex=(lngElement.getAttribute('hemisphere')==='E')?0:1;
	    			
    			} else {
    				this.gridRefField.value=this.verifiedGR;
    			}
    			
    			var countryElement=response.getElementsByTagName('country')[0];
    			if (countryElement.hasChildNodes()) {
    				this.geoRefCountry=countryElement.firstChild.data.toUpperCase();
    				
    				var vcElement=response.getElementsByTagName('vc')[0];
    				if (vcElement.hasChildNodes() && vcElement.firstChild.data) {
    					this.gridRefVcs=vcElement.firstChild.data;
    				}
    				this.match_vc_and_gridref(true);
    			}
    			
    			this.show_info_link();
    			this.form.verify();
    		}
    	}
		this.gridrefXMLrequest=null;
    }
};

PlaceSet.prototype.match_vc_and_gridref =function(apply) {
	if (this.gridRefVcs && (this.region.country=='all' || this.region.country=='al' || this.region.country.substr(0,2)==this.geoRefCountry || this.region.country.substr(0,2)==this.geoRefCountry.toLowerCase())) {
		
		if (this.region.regionCode=='' || this.region.regionCode.split('_')[0]=='') {
			
			if (apply) {
				this.region.get_regions(this.geoRefCountry,this.gridRefVcs); // set selected vc
				this.country.set(this.geoRefCountry);
			} else {
				this.gridRefError='Grid reference implies county '+this.gridRefVcs+' but region is not set.';
			}
		} else {
			// check for vc mismatch
			var gridRefVcsArray=this.gridRefVcs.split(',');
			var currentVcs=this.region.regionCode.split('_')[0].split(','); // region codes end with '_GB'
			var output=[];
			
			for(var testVC in gridRefVcsArray) {		
				for(var test2VC in currentVcs) {
					if (gridRefVcsArray[testVC]==currentVcs[test2VC]) {
						output[output.length]=gridRefVcsArray[testVC];
					}
				}
			}
			
			if (output.length) {
				// matched intersecting VCs
				if (apply) {
					this.region.get_regions(this.geoRefCountry,output.join()); // set selected vc
					this.country.set(this.geoRefCountry);
				} 
				this.gridRefError='';
			} else {
				this.gridRefError='Grid reference does not match specifed county, (grid ref '+this.verifiedGR+' lies in '+this.gridRefVcs+' but the county specified is '+this.region.regionCode.split('_')[0]+')';
			}
		}
	} 
};

PlaceSet.prototype.set_region_dropbox = function (country)
{
	this.region.country=country;

	var regionString=this.region.input.value.replace(trimRegex,''); // trim
	var list=this.region.search_list(regionString);

	this.region.clear_box();

	this.region.dropbox.selectedIndex=0; //nothing selected
	
	if (!(standardBrowser)) {this.region.container.style.position="relative";}
	var i=1;
	
	var ul=document.createElement('ul');

	for (var regionCode in list) {
		var li=ul.appendChild(document.createElement('li'));
		var regionCountry=regionCode.split('_');

		var regionName=list[regionCode];
		//var linkText=regionName+' ('+regionCountry[0]+')';
		
		var regionLink=li.appendChild(document.createElement("a"));
		regionLink.params={
			country: this.country,
			regionCode: regionCode,
			name: regionName};

		regionLink.onclick=associate_obj_with_event(this, "region_click_set_value",regionName);
		regionLink.onmouseout=associate_obj_with_event(this.region, "droplist_mouse_out",i);
		regionLink.onmouseover=associate_obj_with_event(this.region, "droplist_mouse_over",i++);

		regionLink.appendChild(document.createTextNode(regionName));
		vcSpan=document.createElement('span');
		vcSpan.style.color="gray";
		vcSpan.style.backgroundColor="transparent";
		regionLink.appendChild(vcSpan).appendChild(document.createTextNode(' ('+regionCountry[0]+')'));
	}
	
	if (i >1) {
		// atleast one region
		this.region.dropbox.appendChild(ul);
		this.region.dropbox.style.display='block';
		this.region.dropbox.style.zIndex=9;
		this.region.closeButton.style.left=(this.region.dropbox.offsetWidth-36)+"px";
		this.region.closeButton.style.display='block';
	
		if (!(standardBrowser || IEversion >=7)) {
			//this.region.input.style.marginBottom=this.region.dropbox.offsetHeight+2;
		
			this.region.iframe.style.top=(this.region.input.offsetTop+this.region.input.offsetHeight)+"px";
			this.region.iframe.style.width=this.region.dropbox.offsetWidth+"px";
			this.region.iframe.style.height=this.region.dropbox.offsetHeight+"px";
			this.region.iframe.style.display="block";
			this.region.dropbox.style.top=this.region.input.offsetHeight+"px";
		}
	} else {
		this.region.dropbox.style.display='none';
		this.region.closeButton.style.display='none';
		if (this.region.iframe) {this.region.iframe.style.display="none";}
	}
};

/**
 * add/remove map link
 * called when grid ref value changes
 */
PlaceSet.prototype.show_info_link = function () {
	if (this.verifiedGR) {
		this.infoLink.href="http://getamap.ordnancesurvey.co.uk/getamap/frames.htm?mapAction=gaz&gazName=g&gazString="+this.verifiedGR;
		this.gridRefField.parentNode.insertBefore(this.infoLink,this.gridRefField.nextSibling);
	}
	else if (this.gridref) {
		var ref=this.pointGR?this.pointGR:this.gridref;
		
		this.infoLink.href="http://getamap.ordnancesurvey.co.uk/getamap/frames.htm?mapAction=gaz&gazName=g&gazString="+ref;
		this.gridRefField.parentNode.insertBefore(this.infoLink,this.gridRefField.nextSibling);
	} else {
		if (this.infoLink.parentNode!==null) {this.infoLink.parentNode.removeChild(this.infoLink);}
	}

	if (this.pointLatLng.is_set()) {	
		if (this.latDField.value===this.latDField.defaultValue || this.latDField.value==='') {
			this.latDField.defaultValue='('+this.pointLatLng.latD+')';
			//alert('setting latD default to '+this.latDField.defaultValue+' current value is '+this.latDField.value+' colour is '+this.latDField.style.color);
			
			if (this.latDField.value!==this.latDField.defaultValue && this.latDField.style.color=="gray") {
				// assume that we have don't have input focus so can safely reset default value
				this.latDField.value=this.latDField.defaultValue;
			}
			//this.latDField.style.color="gray";
			
			if (this.latMField.value===this.latMField.defaultValue || this.latMField.value==='') {
				this.latMField.defaultValue='('+this.pointLatLng.latM+')';
				if (this.latMField.value!==this.latMField.defaultValue) {this.latMField.value=this.latMField.defaultValue;}
				if (this.latMField.defaultValue!=='') {this.latMField.style.color="gray";} 
				
				if (this.latSField.value===this.latSField.defaultValue || this.latSField.value==='') {
					this.latSField.defaultValue='('+this.pointLatLng.latS+')';
		
					if (this.latSField.value!==this.latSField.defaultValue) {this.latSField.value=this.latSField.defaultValue;}
					if (this.latSField.defaultValue!=='') {this.latSField.style.color="gray";}
				} else {
					this.latSField.defaultValue='('+this.pointLatLng.latS+')';
				}
			} else {
				this.latMField.defaultValue='('+this.pointLatLng.latM+')';
				
				if (this.latSField.value===this.latSField.defaultValue) {
					this.latSField.defaultValue='';
					
					if (this.latSField.value!=='') {this.latSField.value='';}
				} else {
					this.latSField.defaultValue='';
				}
			}
		} else {
			this.latDField.defaultValue='('+this.pointLatLng.latD+')';
			
			if (this.latMField.value===this.latMField.defaultValue) {
				this.latMField.defaultValue='';
				if (this.latMField.value!=='') {this.latMField.value='';}
			} else {
				this.latMField.defaultValue='';
			}
			
			if (this.latSField.value===this.latSField.defaultValue) {
				this.latSField.defaultValue='';
				if (this.latSField.value!=='') {this.latSField.value='';}
			} else {
				this.latSField.defaultValue='';
			}
		}
		
		if (this.longDField.value===this.longDField.defaultValue || this.longDField.value==='') {
			this.longDField.defaultValue='('+this.pointLatLng.lngD+')';
			if (this.longDField.value!==this.longDField.defaultValue) {this.longDField.value=this.longDField.defaultValue;}
			this.longDField.style.color="gray";
			
			this.longEWField.selectedIndex=(this.pointLatLng.hemisphere==='E')?0:1;
			
			if (this.longMField.value===this.longMField.defaultValue || this.longMField.value==='') {
				this.longMField.defaultValue='('+this.pointLatLng.lngM+')';
				if (this.longMField.value!==this.longMField.defaultValue) {this.longMField.value=this.longMField.defaultValue;}
				if (this.longMField.defaultValue!=='') {this.longMField.style.color="gray";} 
				
				if (this.longSField.value===this.longSField.defaultValue || this.longSField.value==='') {
					this.longSField.defaultValue='('+this.pointLatLng.lngS+')';
		
					if (this.longSField.value!==this.longSField.defaultValue) {this.longSField.value=this.longSField.defaultValue;}
					if (this.longSField.defaultValue!=='') {this.longSField.style.color="gray";}
				} else {
					this.longSField.defaultValue='('+this.pointLatLng.lngS+')';
				}
			} else {
				this.longMField.defaultValue='('+this.pointLatLng.lngM+')';
				
				if (this.longSField.value===this.longSField.defaultValue) {
					this.longSField.defaultValue='';
					
					if (this.longSField.value!=='') {this.longSField.value='';}
				} else {
					this.longSField.defaultValue='';
				}
			}
		} else {
			this.longDField.defaultValue='('+this.pointLatLng.lngD+')';
			
			if (this.longMField.value===this.longMField.defaultValue) {
				this.longMField.defaultValue='';
				if (this.longMField.value!=='') {this.longMField.value='';}
			} else {
				this.longMField.defaultValue='';
			}
			
			if (this.longSField.value===this.longSField.defaultValue) {
				this.longSField.defaultValue='';
				if (this.longSField.value!=='') {this.longSField.value='';}
			} else {
				this.longSField.defaultValue='';
			}
		}
	}
	
	if (this.gridref) {
		if (this.refType.selectedIndex===1 && this.pointGR) {
			// grid point
			if (this.gridRefField.value===this.gridRefField.defaultValue) {
				this.gridRefField.defaultValue='OS grid ref ('+this.pointGR+')';
				this.gridRefField.value=this.gridRefField.defaultValue;
			} else {
				this.gridRefField.defaultValue='OS grid ref ('+this.pointGR+')';
			}
		} else {	
			// grid square	
			if (this.gridRefField.value===this.gridRefField.defaultValue) {
				this.gridRefField.defaultValue='OS grid ref ('+this.gridref+')';
				this.gridRefField.value=this.gridRefField.defaultValue;
			} else {
				this.gridRefField.defaultValue='OS grid ref ('+this.gridref+')';
			}
		}
	}
	
	if (this.defaultPrecision) {
		var precisionString=(this.defaultPrecision < 1000)?this.defaultPrecision+'m':(this.defaultPrecision/1000)+'km';

		if (this.precision.value===this.precision.defaultValue) {
			this.precision.value=precisionString;
		}
		this.precision.defaultValue=precisionString;
	}
};

PlaceSet.prototype.hide_info_link = function () {
	if (this.infoLink.parentNode) {this.infoLink.parentNode.removeChild(this.infoLink);}

	this.apply_default(this.gridRefField,PlaceSet.staticText.defaultGridRefField);
	this.apply_default(this.longDField,PlaceSet.staticText.defaultLngD);
	this.apply_default(this.longMField,PlaceSet.staticText.defaultLngM);
	this.apply_default(this.longSField,PlaceSet.staticText.defaultLngS);
	this.apply_default(this.latDField,PlaceSet.staticText.defaultLatD);
	this.apply_default(this.latMField,PlaceSet.staticText.defaultLatM);
	this.apply_default(this.latSField,PlaceSet.staticText.defaultLatS);
};

PlaceSet.prototype.apply_default = function (field,defaultValue) {
	if (field.value===field.defaultValue) {
		field.value=defaultValue;
	}
	field.defaultValue=defaultValue;
};

PlaceSet.prototype.region_click_set_value = function (event,element,value) {
    // stop the default link click action
	if (event.preventDefault) {
		event.preventDefault();
	} else {
		event.returnValue=false;
	}

	this.region.input.value=value;
	this.region.input.style.color="green";
	this.region.input.style.backgroundColor="white";

	this.region.regionCode=element.params.regionCode;

	var regionCountry=this.region.regionCode.split('_');
	this.country.set(regionCountry[1]);
	this.region.country=regionCountry[1];

	this.region.dropbox_close_now(); //hide dropbox immediately

	this.form.verify();
};

// ignore all ctrl+key combinations except ctrl+V and ctrl+X
if (standardBrowser)
{
	var valid_key=function(event) {
		return (event.keyCode >= 46 || event.keyCode==32 || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
		// see http://www.js-examples.com/page/tutorials__key_codes.html
	};
	
	var valid_pos_num_key=function(event) {
		return ((event.keyCode>=48 && event.keyCode<=57) || event.keyCode == 46 || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
	
	var valid_pos_int_key=function(event) {
		return ((event.keyCode>=48 && event.keyCode<=57) || event.keyCode==8 || event.keyCode==0) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
}
else
{
	var valid_key=function(event) {
		return (event.keyCode >= 46 || event.keyCode==32 || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
		//event.returnValue = ((event.keyCode <=58) || (event.keyCode == 191)) && (event.keyCode !=32);
	};
	
	var valid_pos_num_key=function(event) {
		return ((event.keyCode >= 48 && event.keyCode <= 57) || event.keyCode==46 || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
	
	var valid_pos_int_key=function(event) {
		return ((event.keyCode >= 48 && event.keyCode <= 57) || event.keyCode==8) && !(event.ctrlKey && !(event.keyCode==86 || event.keyCode==88));
	};
}

/**
 * writeable place name field box and dropbox
 * @constructor
 */
function PlaceName (parentPlaceSet) {
	this.parentPlaceSet=parentPlaceSet;

	this.construct_dropbox();
	this.register_search_link(new GooglePlaceSearch());
	this.register_search_link(new OSPlaceSearch());
	this.register_search_link(new MultimapSearch());
	this.input.setAttribute('title','Place name, e.g. town/village');
	this.input.defaultValue='placename';
	input_set_default(this.input);
	this.input.onfocus=input_default_focus;
	this.input.onblur=input_default_blur;
	this.input.onkeyup=associate_obj_with_event(this, "name_key_up",'');
}

PlaceName.prototype=new Dropbox();
PlaceName.constructor=PlaceName;

PlaceName.prototype.destroy = function () {
	this.parentPlaceSet=null;
	this.input.onkeyup=null;
	this.input.onblur=null;
	this.input.defaultValue=null;
	
	this.abort_xml_request();
	this.destruct_dropbox();
};

PlaceName.prototype.get_request_url= function () {
	var countryRegion=this.parentPlaceSet.region.regionCode.split('_');
	var country,region;
	
	if (countryRegion[1]) {
		country=countryRegion[1];
		region=countryRegion[0];
	} else {
		country=this.parentPlaceSet.country.menu.options[this.parentPlaceSet.country.menu.selectedIndex].value;
		region=this.parentPlaceSet.region.regionCode;
	}

	var placeString=this.input.value.replace(trimRegex,''); // trim
	return scriptUrl+"XMLplacesearchAtHome/p"+encodeURIComponent(placeString.toLowerCase())+"/c"+country+"/r"+region+"/";
};

PlaceName.prototype.xml_handler=function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4) {
    	this.hide_progress();
    	
        // only if "OK"
		if (this.xmlRequest.status == 200 && this.xmlRequest.responseXML) {
        	var response  = this.xmlRequest.responseXML.documentElement;
			var places = response?response.getElementsByTagName('place'):[];

			if (places.length) {
				this.clear_box();
				
				this.dropbox.selectedIndex=0; //nothing selected
				if (!standardBrowser) {this.container.style.position="relative";} // for IE ony make position relative at last moment

				var placeLink,ul,li;
				var gridRef;
				var place;
				
				ul=document.createElement('ul');
				
				for (var i=0; (place=places[i]);i++) {
					var placeNames=place.getElementsByTagName('placename');
					if (placeNames.length && placeNames[0].hasChildNodes()) {
						li=ul.appendChild(document.createElement('li'));
						placeLink=li.appendChild(document.createElement("a"));
						
						var element=place.getElementsByTagName('country');	
						var vcElement=place.getElementsByTagName('vc');
						var latLongElement=place.getElementsByTagName('baselatlng');
											
						placeLink.params={
							externalId: place.getAttribute('id'),
							name: place.getElementsByTagName('placename')[0].firstChild.data,
							country: (element.length >0 && element[0].hasChildNodes())?element[0].firstChild.data:'',
							region: (vcElement.length >0 && vcElement[0].hasChildNodes())?vcElement[0].firstChild.data:'',
							pointLatLng: new LatLng()
						};
						
						if (latLongElement.length) {
							placeLink.params.pointLatLng.from_xml(latLongElement[0]);
						}
						
						gridRef=place.getElementsByTagName('gridref');
						if (gridRef.length && gridRef[0].hasChildNodes()) {
							gridRef=gridRef[0];
							placeLink.params.gridref=gridRef.firstChild.data;
							placeLink.params.pointGR=gridRef.getAttribute('centre');
							placeLink.params.defaultPrecision=gridRef.getAttribute('precision');
							placeLink.params.defaultRefType=gridRef.getAttribute('reftype'); // 'gridpoint','gridsquare','point'
						}
						
						placeLink.onclick=associate_obj_with_event(this, "place_click_set_value",placeLink.name);
						placeLink.onmouseout=associate_obj_with_event(this, "droplist_mouse_out",i+1);
						placeLink.onmouseover=associate_obj_with_event(this, "droplist_mouse_over",i+1);

						placeLink.appendChild(document.createTextNode(placeLink.params.name));
						
						element=place.getElementsByTagName('regname');
						if (element.length >0 && element[0].hasChildNodes()) {
							var regionSpan=document.createElement('span');
							regionSpan.style.color=(placeLink.params.region==="AMBIGUOUS")?"red":"gray";
							regionSpan.style.backgroundColor="transparent";
							placeLink.appendChild(regionSpan).appendChild(document.createTextNode(' '+element[0].firstChild.data));
						}
					} 
				}
				
				this.dropbox.appendChild(ul);
				this.dropbox.style.width=(this.input.offsetWidth*3)+"px";
				this.dropbox.style.zIndex=8;
				
				this.closeButton.style.left=((this.input.offsetWidth*3)-32)+"px";
				this.closeButton.style.display="block";
							
				this.dropbox.style.display='block';
			
				if (!(standardBrowser || IEversion >=7)) {
					this.iframe.style.top=(this.input.offsetTop+this.input.offsetHeight)+"px";
					this.iframe.style.width=this.dropbox.offsetWidth+"px";
					this.iframe.style.height=this.dropbox.offsetHeight+"px";
					this.iframe.style.display="block";
					this.dropbox.style.top=this.input.offsetHeight+"px";
				}
				
			} else {
				this.clear_box();
				this.dropbox.style.height="auto";
				this.dropbox.style.display='none';
				if (this.iframe) {this.iframe.style.display="none";}
				this.closeButton.style.display="none";
			}
		} 
    } 
};

PlaceName.prototype.place_click_set_value = function (event,element,value) {
    // stop the default link click action
	if (event.preventDefault) {event.preventDefault();}
	else {event.returnValue=false;}

	this.abort_xml_request(); // stop any requests which may not have come back yet
	this.parentPlaceSet.apply_change(element.params);
	this.dropbox_close_now();
};

PlaceName.prototype.name_key_up=function (event,element,param) {
	if (valid_key(event)) {
		var placeString=element.value.replace(trimRegex,''); // trim
		this.parentPlaceSet.reset_place(); // edited entry so id now not valid
		this.parentPlaceSet.hide_info_link();
		if (placeString.length >0) {	
			this.input.style.color="black";
			this.queue_xml_request(placeString.length <2);
			this.show_search_links();
		} else {
			this.dropbox_close_now();
		}
	} else {
		if (element.value.length ===0) {	
			this.dropbox_close_now();
		} else {
			if (event.keyCode==38) {this.change_selection(-1);} // up arrow
			else if (event.keyCode===40) {this.change_selection(1);} // down arrow
			else if (event.keyCode===13) {
				// return key
				
				this.dropbox_close_now();
			}
		}
	}
};

PlaceName.prototype.change_selection =function (direction) {
	var links=this.dropbox.getElementsByTagName('a');
	var linksLength=links.length;

	if (linksLength && ((direction > 0 && this.dropbox.selectedIndex < linksLength) || (direction <0 && this.dropbox.selectedIndex >1))) {
		this.set_selection(this.dropbox.selectedIndex+direction,links);
		this.parentPlaceSet.apply_change(links[this.dropbox.selectedIndex-1].params);
	}
};

/**
 * writeable person name field box and dropbox
 * @constructor
 */
function NameBox (form,context) {
	this.parentForm=form;
	
	this.context=context; // 'col, det or prov'

	this.illegibleButton=document.createElement("input");
	this.illegibleButton.type="checkbox";
	
	this.dob=null;
	this.dod=null;
	this.dateString=null;
	this.wikiName=null;

	this.personId='';
	this.institutionId=this.parentForm.parentData.institutionId;
	
	this.construct_dropbox();

	this.illegible=document.createElement("label");
	this.illegible.appendChild(document.createTextNode("("));
	this.illegible.appendChild(this.illegibleButton);
	this.illegible.appendChild(document.createTextNode("illegible)"));

	this.illegible.style.verticalAlign='top';

	this.input.onkeyup=associate_obj_with_event(this, "input_key_up",'');

	this.input.defaultValue='name';
	input_set_default(this.input);
	this.input.onfocus=input_default_focus;
	//this.input.onblur=input_default_blur;

	this.infoLink=info_link();

    /*
    if (chromeBrowser) {
        //this.UID=uniqueId('namebox');
        this.UID=globalStaticObjectList.push(this)-1;
        alert("length="+globalStaticObjectList.length);
    }
    */
}

NameBox.prototype =new Dropbox();
NameBox.prototype.constructor=NameBox;

NameBox.prototype.destroy=function () {
	this.input.onchange=null;
	this.input.onkeyup=null;
	this.input.onblur=null;
	this.input.onfocus=null;

	this.input.defaultValue=null;

	this.dob=null;
	this.dod=null;
	this.dateString=null;
	this.wikiName=null;

	this.illegible=null;
	
	if (this.focusInfoEventHandle) {this.focusInfoEventHandle=remove_event_handler(this.focusInfoEventHandle);} // handler used to update info pane
	if (this.blurInfoEventHandle) {this.blurInfoEventHandle=remove_event_handler(this.blurInfoEventHandle);} // handler used to update info pane

	this.abort_xml_request();
	this.destruct_dropbox();

	this.button=null;
	this.illegibleButton=null;
	this.requestURL=null;
	this.parentForm=null;
	this.infoLink=null;
	this.institutionId=null;
	this.context=null;
	
	this.parentNameList=null;
	this.rowId=null;

    /*
    if (chromeBrowser) {
        globalStaticObjectList[this.UID]=null;
        this.UID=null;
    }
    */
};

NameBox.prototype.show_info_link = function () {
	if (this.infoLink.parentNode) {this.infoLink.parentNode.removeChild(this.infoLink);}

	this.infoLink.href=scriptUrl+"collector/"+this.personId+"/";
	this.container.insertBefore(this.infoLink,this.dropbox);
};

NameBox.prototype.hide_info_link = function () {
	if (this.infoLink.parentNode) {this.infoLink.parentNode.removeChild(this.infoLink);}
};

/**
 * @return url and query parameters for xml request
 */
NameBox.prototype.get_request_url= function () {
	var nameString=this.input.value.replace(trimRegex,''); // trim
	return scriptUrl+"XMLcolsearchAtHome/"+encodeURIComponent(nameString.toLowerCase())+"/"+this.institutionId+"/"+this.context+"/";
};

/**
 * send request to suggest mechanism instead of normal query route
 * used when query field is blank
 * @param form parent SpecimenForm
 * @param type 'col','prov' or 'det'
 * @todo consider how to generalize date and col/prov reference (form.field.colDate)
 */
NameBox.prototype.issue_suggestion_xml_request= function (form,type) {
	var fromDate=form.field.colDate.fromJdMinimum;
	var toDate=form.field.colDate.toJdMaximum;
	var taxon=form.field.taxa.defaultTaxonId;
	var instId=form.parentData.institutionId;
	
	var provIds=(form.field.provenances)?form.field.provenances.id_list():'';
	var colIds=(form.field.collectors)?form.field.collectors.id_list():'';

	this.pending=0;

	var nameString=this.input.value.replace(trimRegex,''); // trim
	var url=scriptUrl+"XMLcolsuggest.php?query="+encodeURIComponent(nameString.toLowerCase())+"&fromdate="+fromDate+"&todate="+toDate+"&taxon="+taxon+"&field="+type+"&instid="+instId+"&prov="+provIds.toString()+"&col="+colIds.toString();

	if (this.xmlRequest==null || this.xmlRequest.readyState==4 || this.requestURL!=url) {
		this.xmlRequest=agnostic_XMLHttpRequest(); // create the request object
		this.requestURL=url;
		this.show_progress();
		xml_request(this.xmlRequest,url,associateObjectWithXMLRequest(this, "xml_handler", ''));
	}
};

NameBox.prototype.input_key_up=function (event,element,param) {
	if (valid_key(event)) {
		if (this.personId) {
			// refresh suggestion list, now that name has been edited
			
			this.personId=''; // edited entry so id now not valid
			this.dob=null;
			this.dod=null;
			this.dateString=null;
			this.wikiName=null;
			this.parentForm.parentData.info.seek_provenance(this.parentForm,this.context,element,this,this.rowId,this.parentNameList);
		
			this.hide_info_link();
			element.style.color='black';
		}

		var nameString=element.value.replace(trimRegex,''); // trim

		if (nameString.length >0) {
			this.queue_xml_request(nameString.length <2);
		} else {
			this.abort_xml_request();
			this.dropbox_close_now();
		}	
	} else {
		if (event.keyCode===38) {
			this.change_selection(-1); // up arrow
		} else {
			if (event.keyCode===40) {
				this.change_selection(1); // down arrow
			} else {
				if (event.keyCode===13) {
					// return key
					this.abort_xml_request();
					this.dropbox_close_now();
				}
			}
		}
	}
};

NameBox.prototype.import_params = function (params)
{	
	this.personId=params.externalId;
	this.input.value=params.name;
	this.dob=params.dobjd;
	this.dod=params.dodjd;
	this.dateString=params.dateString;
	this.wikiName=params.wikiName;
};

NameBox.prototype.change_selection =function (direction)
{
	var links=this.dropbox.getElementsByTagName('a');

	if (links.length && ((direction > 0 && this.dropbox.selectedIndex < links.length) || (direction <0 && this.dropbox.selectedIndex >1))) {
		this.set_selection(this.dropbox.selectedIndex+direction,links);
		this.import_params(links[this.dropbox.selectedIndex-1].params);	
	
		this.input.style.color='green';
		this.show_info_link();
		if (this.input.onchange) {this.input.onchange();} // artificial change event required for IE
	}
	this.parentForm.change(); // new addition
};


NameBox.prototype.xml_handler=function (param) {
	if (this.xmlRequest && this.xmlRequest.readyState == 4) {
    	this.hide_progress();
		var response  = (this.xmlRequest.responseXML)?this.xmlRequest.responseXML.documentElement:null;

		if (response !==null) {var names = response.getElementsByTagName('name');} // if no names then IE can return null doc element
		else {var names=[];}

		var l=names.length;

		if (l) {
			var suggest=response.getAttribute('suggest');
			
			this.clear_box();
			if (!standardBrowser) {this.container.style.position="relative";} // for IE ony make position relative at last moment

			this.dropbox.selectedIndex=0; //nothing selected
			
			var ul=document.createElement('ul');
			var li;
			
			for (var i=0; i<l;i++) {
				var name=names[i];
				
				var nameId=name.getAttribute('id');
				var dob=name.getAttribute('dob');
				var dod=name.getAttribute('dod');
				var dateString='';

				if (name.hasChildNodes()) {
					var linkText=name.firstChild.data;
					var nameText=name.firstChild.data;
				} else {
					var linkText=''; // odd error with null name, upsets IE
					var nameText='';
				}

				if (dob!='' || dod!='') {
					dateString=dob+'-'+dod;
					//linkText+=' ('+dateString+')';
				}

			if (nameId) {
					li=ul.appendChild(document.createElement('li'));
					var nameLink=li.appendChild(document.createElement("a"));
					nameLink.onclick=associate_obj_with_event(this, "click_set_value",nameText);
					nameLink.onmouseover=associate_obj_with_event(this, "droplist_mouse_over",i+1);
					nameLink.onmouseout=associate_obj_with_event(this, "droplist_mouse_out",i+1);
					if (suggest) { 
						nameLink.flagChange=true;
					}

					nameLink.params={
						name: nameText,
						externalId: nameId,
						dob: dob,
						dobjd: parseInt(name.getAttribute('dobjd'),10),
						dod: dod,
						dodjd: parseInt(name.getAttribute('dodjd'),10),
						dateString: dateString,
						wikiName: (name.getAttribute('wikiname'))?name.getAttribute('wikiname'):null
					};
					
					nameLink.appendChild(document.createTextNode(linkText));
					
					if (dateString) {
						var dateSpan=document.createElement('span');
						dateSpan.style.color="gray";
						dateSpan.style.backgroundColor="transparent";
						nameLink.appendChild(dateSpan).appendChild(document.createTextNode(' ('+dateString+')'));
					}
				}
			}
			this.dropbox.appendChild(ul);
			this.dropbox.style.width=this.input.offsetWidth+"px";
			this.closeButton.style.left=(this.input.offsetWidth-36)+"px";
			this.closeButton.style.display='block';
			this.dropbox.style.display='block';
			this.dropbox.style.zIndex=8;
			
			if (!(standardBrowser || IEversion >=7)) {
				this.iframe.style.top=(this.input.offsetTop+this.input.offsetHeight)+"px";
				this.iframe.style.width=this.dropbox.offsetWidth+"px";
				this.iframe.style.height=this.dropbox.offsetHeight+"px";
				this.iframe.style.display="block";
				this.dropbox.style.top=this.input.offsetHeight+"px";
			}
		} else {
			this.dropbox.style.display='none';
			this.closeButton.style.display="none";
			if (this.iframe) {this.iframe.style.display="none";}
			this.clear_box();
		}
    }
};

/**
 * @return person xml element
 */
NameBox.prototype.get_person_xml = function (doc) {
	var name=doc.createElement('person');
	if (this.personId >0) {name.setAttribute('id',this.personId);}
	name.appendChild(doc.createTextNode(this.input.value.replace(trimRegex,'')));

	if (this.dob >0) {
		name.setAttribute('dob',year_from_JD(this.dob));
		name.setAttribute('dobjd',this.dob);
	}

	if (this.dod >0) {
		name.setAttribute('dod',year_from_JD(this.dod));
		name.setAttribute('dodjd',this.dod);
	}
	
	return name;
};

NameBox.prototype.populate_from_xml = function (personXML) {
	this.personId=personXML.getAttribute('id');

	this.dateString='';
	if (personXML.getAttribute('dobjd')!=null) {
		this.dob = parseInt(personXML.getAttribute('dobjd'),10);
		this.dateString=personXML.getAttribute('dob');
	}
	this.dateString+='-';
	
	if (personXML.getAttribute('dodjd')!=null) {
		this.dod = parseInt(personXML.getAttribute('dodjd'),10);
		this.dateString+=personXML.getAttribute('dod');
	}
	
	if (personXML.getAttribute('wikiname')!=null) {
		this.wikiName=personXML.getAttribute('wikiname');
	}

	this.input.value=personXML.firstChild.data;
	if (this.personId >0) {
		this.show_info_link();
		this.input.style.color='green';
	}
	else if (this.input.value.length) {this.input.style.color='black';}
};

NameBox.prototype.click_set_value = function (event,element,value) {
    // stop the default link click action
	if (event.preventDefault) {
		event.preventDefault();
	} else {
		event.returnValue=false;
	}

	var flag=element.flagChange;
	this.abort_xml_request(); // stop any requests which may not have come back yet
	
	if (element.params) { // allow for weird IE bug where params may not be set
		this.import_params(element.params);
		this.input.style.color='green';
		this.show_info_link();
		this.dropbox_close_now();
	
		if (flag) {
			this.parentNameList.input_change(null,null,this.rowId);
		} else {
			this.parentForm.verify();
		}
		this.parentForm.change(); // new addition
		
		if (this.parentNameList) {
			this.parentForm.parentData.info.seek_provenance(this.parentForm,'col',this.input,this,this.rowId,this.parentNameList);
		}
	}
};

/**
 *
 */
function deleteButton(buttonTitle) {
	// returns a delete (x) button
	var button=document.createElement("button");
	var img=button.appendChild(document.createElement("img"));
	img.src="http://static.herbariumathome.org/Xbutton.png";
	button.className="img";
	button.setAttribute('type','button');
	button.style.background='transparent';
	if (standardBrowser) {img.alt="delete";}
	button.title=buttonTitle;
	return button;
}

/**
 *
 */
function plusButton(buttonTitle) {
	// returns an add (+) button
	var button=document.createElement("button");
	var img=button.appendChild(document.createElement("img"));
	img.src="http://static.herbariumathome.org/plusbutton.png";
	button.className="img";
	button.setAttribute('type','button');
	button.style.background='transparent';
	if (standardBrowser) {img.alt="add";}
	button.title=buttonTitle;
	return button;
}

/**
 *
 */
function more_button(buttonTitle) {
	// returns a 'more' (...) button
	var button=document.createElement("button");
	var img=button.appendChild(document.createElement("img"));
	img.src="http://static.herbariumathome.org/morebutton.png";
	button.className="img";
	button.setAttribute('type','button');
	button.style.background='transparent';
	if (standardBrowser) {img.alt="expand details";}
	button.title=buttonTitle;
	return button;
}

/**
 * Country list containing Great Britain and Ireland (and provinces)
 */
CountryList.localList=[['gb','Great Britain'],['gb-ci','Channel Islands'],['gb-eng','England'],['gb-man','Isle of Man'],['gb-sco','Scotland'],['gb-wal','Wales'],['ie','Ireland'],['more','More countries']];

/**
 * Country list of all countries
 */
CountryList.fullList=null;

/**
 *
 */
function CountryList (international) {
	this.menu=document.createElement("select");
	this.countries=[];
	this.reset_list(international);	
}

CountryList.prototype.reset_list = function (international) {
	this.menu.options.length=0;
	this.menu.options[0]=new Option('country','all',true,true);
	this.menu.options[0].style.color='gray';
	
	this.international=international;
	
	if (this.countries.length===0) {	
		if (this.international) {
			if (CountryList.fullList===null) {
				this.fetch_countries();
			}
			this.countries=CountryList.fullList;
		} else {
			this.countries=CountryList.localList;
			//CountryList.countries=[['gb','Great Britain'],['gb-ci','Channel Islands'],['gb-eng','England'],['gb-man','Isle of Man'],['gb-sco','Scotland'],['gb-wal','Wales'],['ie','Ireland'],['more','More countries']];
		}
	}
			
	var countryLength=this.countries.length;
	for (var c=0; c<countryLength;c++) {
		this.menu.options[c+1]=new Option(this.countries[c][1],this.countries[c][0],false,false);
	}
};

CountryList.prototype.fetch_countries = function () {
	CountryList.countryXMLrequest=agnostic_XMLHttpRequest(); // create the request object
	synchronous_xml_request(CountryList.countryXMLrequest,scriptUrl+'XMLcountrylist/',this.xml_handler);
	CountryList.countryXMLrequest=null;
};

CountryList.prototype.xml_handler = function(param) {
	if (CountryList.countryXMLrequest.readyState === 4 && CountryList.countryXMLrequest.status === 200) {
		var response = CountryList.countryXMLrequest.responseXML.documentElement, countries = response.getElementsByTagName('country');
		var countryLength=countries.length;
		CountryList.fullList=[];
		
		for (var c=0; c<countryLength;c++) {
			CountryList.fullList[c]=[countries[c].getAttribute('code').toLowerCase(),countries[c].firstChild.data];	
		}
		
		//CountryList.internationalList=true;
	}
};

CountryList.prototype.set = function(countryCode) {
	if (countryCode) {
		countryCode=countryCode.toLowerCase();
		var countryLength=this.countries.length; // may get strange error here with unrecognised country
		for (var c=0; c<countryLength;c++) {
			if (this.countries[c][0]==countryCode) {
				this.menu.selectedIndex=c+1; // first menu item is 'all'
				return;
			}
		}
	}
	this.menu.selectedIndex=0; // no country match so default to 'all'
};
	
/**
 * drop-down list of regions
 * @constructor
 */
function RegionList () {
	this.country='all'; // was null

	this.input=document.createElement("input");
	this.input.setAttribute('title','region name/vice-county');

	this.construct_dropbox();

	this.input.defaultValue='county';
	input_set_default(this.input);
	this.input.onfocus=input_default_focus;
	this.input.onblur=input_default_blur;
}

RegionList.prototype=new Dropbox();
RegionList.prototype.constructor=RegionList;

RegionList.regions=[];
RegionList.synonyms=[];

RegionList.prototype.get_regions = function (country,regSelection) {
	if (country) {
		country=country.toLowerCase();
		if (RegionList.regions[country]==null) {
			// haven't got regions for country yet
	
			RegionList.regionXMLrequest=agnostic_XMLHttpRequest(); // create the request object
			synchronous_xml_request(RegionList.regionXMLrequest,scriptUrl+'BetaXMLregionlist/'+country+'/',this.xml_handler);
			RegionList.regionXMLrequest=null;
		}
	
		if (regSelection !='' && regSelection != null) {
			// must test against '' (empty string), not certain that null test needed also
			this.regionCode=regSelection+"_"+country;
	
			if (RegionList.regions[country][regSelection]) {
				this.input.value=RegionList.regions[country][regSelection];
			} else {this.input.value=regSelection;} // not a matched region code
	
			this.input.style.color="green";
			this.input.style.backgroundColor="white";
		} else {
			this.regionCode='';
			input_set_default(this.input);
		}
	}
};

RegionList.prototype.xml_handler = function(param) {
	if (RegionList.regionXMLrequest.readyState == 4 && RegionList.regionXMLrequest.status == 200) {
		var response = RegionList.regionXMLrequest.responseXML.documentElement, countries = response.getElementsByTagName('country');
		var countryLength=countries.length;

		for (var c=0; c<countryLength;c++) {
			var regions = countries[c].getElementsByTagName('region');
			var countryCode = countries[c].getAttribute('code').toLowerCase(),regCode,regionNames;

			RegionList.regions[countryCode]=[];
			RegionList.synonyms[countryCode]=[];

			var regionsLength=regions.length;

			for (var i=0; i<regionsLength;i++) {
				regCode=regions[i].getAttribute('id');
				regionNames=regions[i].getElementsByTagName('p'); // preferred name
				RegionList.regions[countryCode][regCode]=regionNames[0].firstChild.data;

				regionNames=regions[i].getElementsByTagName('s'); // synonyms

				var synonymsLength=regionNames.length;

				for (var j=0; j<synonymsLength;j++) {
					RegionList.synonyms[countryCode].push([regionNames[j].firstChild.data,regCode]);
				}
			}
			RegionList.regions[countryCode].count=regionsLength;
		}
	}
};

RegionList.prototype.search_list = function (searchString) {
	var list=[];

	searchString=searchString.toLowerCase();
	if (this.country!='all') {
		this.search_list_add(searchString,this.country,list);
	} else {
		this.search_list_add(searchString,'gb',list);
		this.search_list_add(searchString,'ie',list);
	}

	return list;
};

RegionList.prototype.vchRegex=/^vch?/i;

RegionList.prototype.search_list_add = function (searchString, country,list) {
    var index;
	for (var regCode in RegionList.regions[country]) {
		if (regCode !== 'count') { // kludge to avoid 'count' member
			index= (RegionList.regions[country][regCode].toLowerCase()).indexOf(searchString);

			if ((index==0) || (index > 0 && searchString.length >=3)) {
                list[regCode+'_'+country]=RegionList.regions[country][regCode];
            } else {
				// test against regcode
				var code=regCode.replace(this.vchRegex,'');
				var stripedSearchString=searchString.replace(this.vchRegex,'');
				if ((stripedSearchString!=='') && (code.indexOf(searchString) >=0 || code.indexOf(stripedSearchString) >=0)) {
					list[regCode+'_'+country]=RegionList.regions[country][regCode];
				}
			}
		}
	}

	// scan synonyms
	if (RegionList.synonyms[country]) {
		for (var i=RegionList.synonyms[country].length-1;i>=0;i--) {
			var entry=RegionList.synonyms[country][i];
	
			index= (entry[0].toLowerCase()).indexOf(searchString);
			if ((index==0) || (index > 0 && searchString.length >=3)) {
				list[entry[1]+'_'+country]=RegionList.regions[country][entry[1]];
			}
		}
	}
};

RegionList.prototype.match_input = function () {
	var searchString=this.input.value.toLowerCase();
	this.country=this.country.toLowerCase();

	if (this.country!=='all') {
		return this.match_input_against_country(searchString,this.country);
	} else {
		if (this.match_input_against_country(searchString,'gb')) {
			this.country='gb';
			return true;
		} else {
			if (this.match_input_against_country(searchString,'ie')) {
				this.country='ie';
				return true;
			}
		}
	}
 	return false;
};

RegionList.prototype.match_input_against_country = function (searchString,country) {
	if (RegionList.regions[country]) {
		for (var regCode in RegionList.regions[country]) {
			if (regCode != "count") {
				if (RegionList.regions[country][regCode].toLowerCase() == searchString) {
					this.input.value=RegionList.regions[country][regCode];
					this.regionCode=regCode;
					this.input.style.color="green";
					return true;
				} else {
					// test against regcode
					var code=regCode.replace(this.vchRegex,'');
					var stripedSearchString=searchString.replace(this.vchRegex,'');
					if ((stripedSearchString!=='') && code.indexOf(' ')==-1 && (code.indexOf(searchString) >=0 || code.indexOf(stripedSearchString) >=0)) {
						this.input.value=RegionList.regions[country][regCode];
						this.regionCode=regCode;
						this.input.style.color="green";
						return true;
					}
				}
			}
	 	}
	 	
	 	// scan synonyms
		for (var i=RegionList.synonyms[country].length-1;i>=0;i--) {
			var entry=RegionList.synonyms[country][i];
	
			if (entry[0].toLowerCase() == searchString) {
				this.input.value=RegionList.regions[country][entry[1]];
				this.regionCode=entry[1];
				this.input.style.color="green";
				return true;
			}
		}
	}

	return false;
};

RegionList.prototype.destroy = function () {
	this.xmlRequest=null;
	this.destruct_dropbox();
};

/**
 *
 */
function load_xml_specimens() {
	if (loadRequest.readyState == 4 && loadRequest.status == 200) {
		var response  = loadRequest.responseXML.documentElement;

		var manifestElements=response.getElementsByTagName('manifest');
		for (var n=0,manifestElement; (manifestElement=manifestElements[n]); n++) {
			manifests[manifestElement.getAttribute('name')]=new Manifest(manifestElement);
		}
		
		var sheets=response.getElementsByTagName('sheet');

		if (sheets.length===0) {
			// something has gone wrong, no sheets
			var error=response.getElementsByTagName('error');
			if (error.length >0) {
				// report error
				globalContainer.appendChild(document.createTextNode(error[0].firstChild.data));
			} else {
				globalContainer.appendChild(document.createTextNode('An error has occurred: no sheets were allocated.'));
			}
		} else {
			uid=response.getAttribute('uid'); // set global user id
			tmpUser=(response.getAttribute('tmpuser')=='tmpuser');	

			if (tmpUser) {
				registrationForm=new RegistrationForm();	
				globalBlockSave=true;
				
				// if have unregistered user then user session cookie to determine whether to show expanded (bubble) help
				var cookie=cookie_read_value('expandedhelp');
				if (cookie) {
					globalShowExpandedHelp=(cookie=='show');
				}
			}

			var sheetExternalId,sheetId;
			firstIdNumber=null; // global record of the first sheet photo number
			
			for (var n=0,sheet; (sheet=sheets[n]); n++) {
				sheetId=sheet.getAttribute('id');
				sheetExternalId=sheet.getAttribute('p'); // of the form Pnn	
				if (firstIdNumber===null) {firstIdNumber=sheetId;}
				
				specimens[sheetId]=new AllocatedSheet(sheetExternalId,sheetId,globalContainer,sheet);
			}
			
			loadRequest=null;
		}
	}
}

/**
 *
 */
function assimilate_HTML_fragments() {
	var container=document.getElementById('fragmentcontainer');
	var fragments=container.getElementsByTagName('div');
	var i;
	
	if (document.createRange) {
		var range=document.createRange();
		
		for (i=fragments.length-1; i>=0;i--) {
			range.selectNodeContents(fragments[i]);
			globalMessages[fragments[i].id]=range.extractContents();
			fragments[i].parentNode.removeChild(fragments[i]);
		}
	} else {
		var j,l;
		for (i=fragments.length-1; i>=0;i--) {
			globalMessages[fragments[i].id]=document.createDocumentFragment();
			
			l=fragments[i].childNodes.length;
			
			for (j=0; j<l;j++) {
				globalMessages[fragments[i].id].appendChild(fragments[i].childNodes[j].cloneNode(true));
			}
		}
	}
}

/**
 * make a synchronous XMLrequest to XMLathomeloadspecimens.php
 * which returns a list of specimens allocated to the current user
 */
function load_specimens() {
	loadRequest=agnostic_XMLHttpRequest(); // create the request object
	synchronous_xml_request(loadRequest,scriptUrl+'XMLathomeloadspecimens005v.php?client='+globalVersion+'&seed='+(Math.random())+(tutorial?'&tutorial=true':'')+phpSession,load_xml_specimens);
	loadRequest=null;

	if (specimens.length) {
		globalBackupId=window.setInterval("send_backup()",60000);
		return true;
	}
	return false;
}

/**
 * 
 */
function update_window_position_cookie() {
	// update frame size record
	if (!window.innerWidth) {
		//ie
    	var width = document.body.clientWidth;
    	var height = document.body.clientHeight;
	} else {
		//mozilla
		var width = window.innerWidth;
    	var height = window.innerHeight;
	}
	
	// read current window location
	if (window.screenX) {
		var x=window.screenX;
		var y=window.screenY;
	} else {
		var x=window.screenLeft;
		var y=window.screenTop;
	}
	
	cookie_set_value('docformcoords',x+","+y+","+width+","+height,14);
}

/**
 *
 */
function init_container() {
	globalContainer=document.getElementById('container');
	
	var footer=document.getElementById('footermenu');
	if (footer) {
		footer.style.display="none";	
	}
	
	if (!standardBrowser) {
		// suppress drag-selecting of text (interferes with images)
		document.body.onselectstart = function () {return false;}; // block on unspecified background
		footer.onselectstart = function () {window.event.cancelBubble = true;}; // allow on the footer
	} 

	var cookie=cookie_read_value('docformcoords');
	if (cookie !==null) {
		var param=cookie.split(',',4); // expect x,y,width,height
		try {
			if (param[2] < window.screen.availWidth && param[3] < window.screen.availHeight) {
				window.resizeTo(param[2],param[3]);
				window.moveTo(param[0],param[1]);
			} 
		} catch (err) {}
	} else {	
		try {
			if (window.screen.availWidth > 800 && window.screen.availHeight >600) {
				window.resizeTo(900,window.screen.availHeight-100);
				window.moveTo(100,50);
			}
			else
			{
				window.moveTo(0,0);
				window.resizeTo(window.screen.availWidth,window.screen.availHeight);
			}
		} catch (err) {}
	}

	assimilate_HTML_fragments();

	specimens=[];
	if (load_specimens()) {
		thumbPage=new ThumbPage(globalContainer);
		thumbPage.add_sheet_list(specimens);
	
		specimens[firstIdNumber].specSheet.first_new(); // firstIdNumber is the internal id of the first sheet loaded
	
		cache_images();
	
		window.onunload=window_unload_handler;
		window.onerror=log_error;
	
		var loadingMessage=document.getElementById('loading');
		if (loadingMessage) {loadingMessage.parentNode.removeChild(loadingMessage);}
	}
}

var imageList;
var preloadImg;
/**
 * start systematic image preloading
 */
function cache_images() {
	imageList=[]; // global array of images to load systematically

	for (var sheet in specimens) {
		for (var imageNumber in specimens[sheet].data.images) {
			var image=specimens[sheet].data.images[imageNumber];
			
			var leafName=image.leafName;
			var tileWidth=image.tileWidth;
			var tileHeight=image.tileHeight;
			var path=image.path;
			var host=image.host;
			
			imageList.push('http://'+host+'/'+path+'/'+leafName+'/low_'+leafName+'.jpg');
			imageList.push('http://'+host+'/'+path+'/'+leafName+'/thumb_'+leafName+'.jpg');
			
			var tilePath='http://'+host+'/'+path+'/'+leafName+'/tiles/';
			for (var x=image.sourceXTileNumber-1; x>=0; x--) {
				for (var y=image.sourceYTileNumber-1; y>=0; y--) {
			 		imageList.push(tilePath+Math.floor(x*tileWidth)+'_'+Math.floor(y*tileHeight)+'_'+tileWidth+'_'+tileHeight+'_80.jpg');
		 		}
			}
		}
	}

	// grab the first image
	preloadImg=new Image(); // global container for images
	preloadImg.onload=image_onload_handler;
	preloadImg.onerror=image_onloaderror_handler;
	preloadImg.src=imageList.shift();
}

// set timeout is used to give IE time to garbage collect (overwise get stack overflows) + will also hammer server less

/**
 *
 */
function image_onload_handler() {
	if (imageList && imageList.length >0) {
		imageLoadTimeoutHandle=window.setTimeout(next_image,globalImageCacheDelay);
	} else {
		preloadImg.onload=null;
		preloadImg.onerror=null;
		preloadImg=null;
		imageList=null;
	}
}

/**
 * treat the same as onLoad, but add longer delay
 * possibly should log this error occurrence
 */
function image_onloaderror_handler() {
	if (imageList && imageList.length >0) {
		imageLoadTimeoutHandle=window.setTimeout(next_image,globalImageCacheDelay*3);
	} else {
		preloadImg.onload=null;
		preloadImg.onerror=null;
		preloadImg=null;
		imageList=null;
	}
}

/**
 *
 */
function next_image() {
	imageLoadTimeoutHandle=null;
	if (globalPauseImgCache) {
		imageLoadTimeoutHandle=window.setTimeout(next_image,250);
	} else {
		preloadImg.src=imageList.shift();
	}
}

/**
 *
 */
function window_unload_handler()
{
	window.onunload=null;
	globalContainer.onselectstart=null;
	Row.focusedRow=null;
	
	
	if (footer) {
		footer.style.display="none";	
	}
	
	if (!standardBrowser) {
		var footer = document.getElementById('footermenu');
		if (footer) {
			footer.onselectstart = null;
		}
	}
	
	if (globalBackupId) {
		window.clearInterval(globalBackupId);
		globalBackupId=null;
	}
	
	if (globalModifiedSinceLastBackup) {
		send_backup(true); // send synchronous request
	}
	
	if (imageLoadTimeoutHandle) {
		window.clearTimeout(imageLoadTimeoutHandle);
		imageLoadTimeoutHandle=null;
	}
	
	backupSaveRequest=null;
	
	window.onerror=null;
	errorReportRequest=null;
	
	if (preloadImg && preloadImg.onload) {
		preloadImg.onload=null;
		imageList=null;
		preloadImg=null;
	}

	if (registrationForm) {
		registrationForm.destroy(); // get rid of any residual form
		registrationForm=null;
	}

	for (var sheetNumber in specimens) {
		specimens[sheetNumber].destroy();
		specimens[sheetNumber]=null;
	}
	specimens=null;
	
	for (var n in manifests) {
		manifests[n].destroy();
	}
	manifests=null;

	thumbPage.destroy();

	/*
	for (var n in globalE)
	{
		globalE[n]=null;
	}
	globalE=null;
	
	for(var e in globalEventMethodName) {
		alert(e+" "+globalEventMethodName[e]);
	}
	
	for(var e in globalEventStack) {
		alert(e+" (stack) "+globalEventStack[e]);
	}
	*/

	help.destroy();
	currentBubble=null;

	globalContainer.parentNode.removeChild(globalContainer);
	globalContainer=null;
	currentSheet=null;
	
	globalMessages=null;
}

if (standardBrowser)
{
	var stdAssociateObjWithEvent = function(target,obj, methodName,param) {
		return (function(e){
			return obj[methodName](e, target, param);
		});
	};

	var register_event_handler = function(target,eventName,obj,methodName,param,capturing) {
		var stackElement=globalEventStack.length;
		target.addEventListener(eventName,globalEventStack[stackElement]=stdAssociateObjWithEvent(target,obj, methodName,param),capturing==true);

		globalEventTarget[stackElement]=target;
		globalEventMethodName[stackElement]=methodName;
		globalEventType[stackElement]=eventName;
		globalEventCapturing[stackElement]=(capturing==true);
		
		return stackElement; // ref to attached function, which is returned (in case needed for removal later)
	};


	var associate_obj_with_event = function(obj, methodName,param) {
	    /* The returned inner function is intended to act as an event
	       handler for a DOM element:-
	    */
	    return (function(e){ 
	        return obj[methodName](e, this, param);
	    });
	};
}
else
{
	var ie_associate_obj_with_event = function(obj, methodName,param){
		return (function(){
			return obj[methodName](window.event, this.event.srcElement, param);
		});
	};

	var register_event_handler = function(target,eventName,obj,methodName,param) {
		var stackElement=globalEventStack.length;
		target.attachEvent('on'+eventName,globalEventStack[stackElement]=ie_associate_obj_with_event(obj, methodName,param));

		globalEventTarget[stackElement]=target;
		globalEventMethodName[stackElement]=methodName;
		globalEventType[stackElement]=eventName;
		return stackElement; // ref to attached function, which is returned (in case needed for removal later)
	};
	
	// from http://jibbering.com/faq/faq_notes/closures.html
	var associate_obj_with_event = function (obj, methodName,param) {
	    // The returned inner function is intended to act as an event handler for a DOM element
	    return (function() {
        	return obj[methodName](window.event, this, param);
    	});
	};
}

if (document.removeEventListener) { // DOM event model
	var remove_event_handler = function(handler) {		
		if (globalEventStack[handler]) {
			globalEventTarget[handler].removeEventListener(globalEventType[handler],globalEventStack[handler], globalEventCapturing[handler]);
			//var idString=globalEventMethodName[handler]+" "+globalEventTarget[handler]+" "+globalEventType[handler];
			delete globalEventStack[handler];
			delete globalEventMethodName[handler];
			delete globalEventType[handler];
			delete globalEventTarget[handler];
			delete globalEventCapturing[handler];
		} else {
			throw new Error('herb@home: event remove failure');
		}
		return null;
	};
} else {
	var remove_event_handler = function(handler) {

		if (globalEventStack[handler]) {
			globalEventTarget[handler].detachEvent("on"+globalEventType[handler],globalEventStack[handler]);
			delete globalEventStack[handler];
			delete globalEventMethodName[handler];
			delete globalEventTarget[handler];
			delete globalEventType[handler];	
		} else {
			throw new Error('herb@home: event remove failure');
		}
		return null;
	};
}

/**
 * returns an XML http request object
 */
function associateObjectWithXMLRequest(obj, methodName, param) {
	return (function(){
		return obj[methodName](param);
	});
}

if (document.implementation && document.implementation.createDocument) {
	var create_xml_doc = function () {
		doc=document.implementation.createDocument("","response",null); // create blank XML response document

		if (doc.documentElement === null) { // Safari 1.2 bug
			// add documentElement
			doc.appendChild(doc.createElement("response"));
		}
		return doc;
	};
}
else if (window.ActiveXObject) {
	var create_xml_doc = function () {
		doc = new ActiveXObject("Microsoft.XMLDOM");
		doc.documentElement=doc.createElement('response');
		return doc;
	};
}
else {
	alert ('Unfortunately your web browser does not have some capabilites needed to run the documentation form.');
}

if (window.XMLHttpRequest) {
	var agnostic_XMLHttpRequest = function () {
		return new XMLHttpRequest();
	};

	var load_xml_doc=function(url,handler)
	{
		// native XMLHttpRequest object
		var req = new XMLHttpRequest();
		req.onreadystatechange = handler;
		req.open("GET", url, true);
		req.send(null);
		return req;
	};
} else {
	var agnostic_XMLHttpRequest = function () {
		return new ActiveXObject("Microsoft.XMLHTTP");
	};

	var load_xml_doc=function(url,handler) {
		var req = new ActiveXObject("Microsoft.XMLHTTP");
		req.onreadystatechange = handler;
		req.open("GET", url, true);
		req.send();
		return req;
	};
}

/**
 *
 */
function xml_request(req,url,handler)
{
	req.onreadystatechange = handler;
	req.open("GET", url, true);
	req.send(null);
}

/**
 *
 */
function xml_save(req,url,handler,data)
{
	req.onreadystatechange = handler;
	req.open("POST", url, true);
	req.send(data);
}

/**
 *
 */
function xml_synchronous_save(req,url,handler,data)
{
	req.open("POST", url, false);
	req.send(data);
	handler();
}

/**
 *
 */
function synchronous_xml_request(req,url,handler)
{
	req.open("GET", url, false);
	req.send(null);
	if (handler) {handler();}
}

/**
 * Set windows title and document h1 tag
 * @param <string> text window title
 * @param <string> header h1 heading
 */
function set_title(text,header) {
	document.title=text;
	var title=document.getElementsByTagName('title');
	title[0].text=text;

	var h1=document.getElementsByTagName('h1')[0];
	h1.firstChild.data=header;
}

/**
 *
 */
function xml_serialize(xmlNode) {
  var text = false;
  try {
    // Gecko-based browsers, Safari, Opera.
    var serializer = new XMLSerializer();
    text = serializer.serializeToString(xmlNode);
  }
  catch (e) {
    try {
      // Internet Explorer.
      text = xmlNode.xml;
    }
    catch (e) {}
  }
  return text;
}

/**
 *
 */
function BubbleHelp () {
	this.bubble=document.createElement('div');
	this.bubble.style.display='none';
	this.bubble.style.position='absolute';
	this.bubble.style.width="400px";
	this.bubble.style.backgroundColor="yellow";
	this.bubble.style.zLayer="99";
	this.bubble.style.filter="alpha(opacity:95)";
	this.bubble.style.KHTMLOpacity="0.95";
	this.bubble.style.MozOpacity="0.95";
	this.bubble.style.opacity="0.95";
	this.bubble.style.MozBorderRadius="10px";
	this.bubble.style.webkitBorderRadius="10px";
	this.bubble.style.padding="0.2em 0.4em 0.2em 0.4em";
	this.bubble.style.margin="0px";
	
	if (!(standardBrowser || IEversion >=7)) {
		this.iframe=document.createElement('iframe');
		this.iframe.setAttribute('src',scriptUrl+'blank.php');
		this.iframe.style.zLayer="98";
		this.iframe.style.position="absolute";
		this.iframe.style.borderStyle="none";
		this.iframe.style.borderWidth="0px";
		this.iframe.style.display="none";
		this.iframe.style.width=this.bubble.offsetWidth+"px";
		this.iframe.style.backgroundColor="transparent";
		this.iframe.frameBorder=0;
		this.iframe.setAttribute('frameborder','0');
		this.iframe.setAttribute('marginwidth','0');
		this.iframe.setAttribute('marginheight','0');
		//globalContainer.appendChild(this.iframe);
		document.body.appendChild(this.iframe);
	} else {
		this.iframe=null;
	}
}

BubbleHelp.prototype.set_content = function(tag,labelName) {
	var header=document.createElement('p');
	header.appendChild(document.createTextNode(labelName));
	header.style.fontWeight="bold";
	
	this.bubble.appendChild(header);
	this.bubble.appendChild(globalMessages[tag].cloneNode(true));
};

BubbleHelp.prototype.display = function(event) {
	if (this.iframe) {this.iframe.style.display="block";}
	this.bubble.style.display="block";
	if (this.iframe) {
		this.iframe.style.width=this.bubble.offsetWidth+"px";
		this.iframe.style.height=this.bubble.offsetHeight+"px";
	}
	
	if (event) {this.locate(event);}
};

BubbleHelp.prototype.hide = function() {
	this.bubble.style.display="none";
	if (this.iframe) {this.iframe.style.display="none";}
};

BubbleHelp.prototype.locate = function(e)
{
	if (globalShowExpandedHelp) {
		var posx=0,posy=0,docWidth;
		
		if(e.pageX || e.pageY){
	    	posx=e.pageX; 
	    	posy=e.pageY;
	    	docWidth=window.innerWidth;
	    }
		else if(e.clientX || e.clientY){
			if(document.documentElement.scrollTop){
	        	posx=e.clientX+document.documentElement.scrollLeft;
	        	posy=e.clientY+document.documentElement.scrollTop;
	        	docWidth=document.documentElement.offsetWidth;
	        } else {
	        	posx=e.clientX+document.body.scrollLeft;
	        	posy=e.clientY+document.body.scrollTop;
	        	docWidth=document.body.offsetWidth;
	        }	
	    }
	    if ((posx+this.bubble.offsetWidth) > docWidth) {
	    	posx-=(posx+this.bubble.offsetWidth) - docWidth;
	    }
	    
	    this.bubble.style.zLayer="99";
		this.bubble.style.top=(posy+10)+"px";
		this.bubble.style.left=(posx-20)+"px";
		
		if (this.iframe) {
			this.iframe.style.top=(posy+10)+"px";
			this.iframe.style.left=(posx-20)+"px";
			this.iframe.style.height=this.bubble.offsetHeight+"px";
		}
	}
};

BubbleHelp.prototype.destroy = function()
{
	if (this.bubble.parentNode) {
		this.bubble.parentNode.removeChild(this.bubble);
	}
	
	if (this.iframe) {
		this.iframe.parentNode.removeChild(this.iframe);
	} 
	this.bubble=null;
	this.iframe=null;
};

/**
 *
 */
function info_link() {
	var infoLink=document.createElement('a');
	infoLink.style.fontStyle="italic";
	infoLink.style.textDecoration="none";
	infoLink.style.fontWeight="bold";
	infoLink.style.fontSize="1.3em";
	infoLink.style.fontFamily="serif";
	infoLink.appendChild(document.createTextNode(' i '));
	infoLink.setAttribute('target','_blank');
	return infoLink;
}

/**
 *
 */
function info_link_doc(localDoc) {
	var infoLink=localDoc.createElement('a');
	infoLink.style.fontStyle="italic";
	infoLink.style.textDecoration="none";
	infoLink.style.fontWeight="bold";
	infoLink.style.fontSize="1.3em";
	infoLink.style.fontFamily="serif";
	infoLink.appendChild(localDoc.createTextNode(' i '));
	infoLink.setAttribute('target','_blank');
	return infoLink;
}

/**
 * read value from cookie
 * @return value or NULL
 */
function cookie_read_value(key) {
	if (document.cookie.length) {
		var start = document.cookie.indexOf(key+'=');
		if (start >-1) {
			var end=document.cookie.indexOf(';',start);
			return unescape(document.cookie.substring(start+key.length+1,end>0?end:1024));
		}
	}
	return null;
}

/**
 * set cookie value
 * @param {string} key
 * @param {string} value
 * @param {int} expiry (days into the future)
 * @return value or NULL
 */
function cookie_set_value(key,value,days,path) {
	var string=key+'='+escape(value);
	if (days) {
		var today=new Date();
	
		var offsetDate=new Date(today.getTime()+(days*24*60*60*1000));
		string+='; expires='+offsetDate.toUTCString();
	}
	
	string+='; path='+(path?path:'/');
	document.cookie=string;
}

// Internet explorer doesn't natively define xml node type labels
// from http://www.alistapart.com/articles/crossbrowserscripting
if (document.ELEMENT_NODE == null) {
	document.ELEMENT_NODE = 1;
	document.ATTRIBUTE_NODE = 2;
	document.TEXT_NODE = 3;
	document.CDATA_SECTION_NODE = 4;
  	document.ENTITY_REFERENCE_NODE = 5;
	document.ENTITY_NODE = 6;
	document.PROCESSING_INSTRUCTION_NODE = 7;
	document.COMMENT_NODE = 8;
	document.DOCUMENT_NODE = 9;
	document.DOCUMENT_TYPE_NODE = 10;
	document.DOCUMENT_FRAGMENT_NODE = 11;
	document.NOTATION_NODE = 12;
}

if (!document.importNode) {
	/**
	 * Internet Explorer doesn't implement Document.importNode
	 * from http://www.alistapart.com/articles/crossbrowserscripting
	 */
	 
	var import_node = function(doc,node, allChildren) {
		switch (node.nodeType) {
			case document.ELEMENT_NODE:
			var newNode = doc.createElement(node.nodeName);
			// does the node have any attributes to add?
			if (node.attributes && node.attributes.length > 0) {
				for (var i = 0, il = node.attributes.length; i < il;i++) {
					newNode.setAttribute(node.attributes[i].nodeName, node.getAttribute(node.attributes[i].nodeName));
				}
			}
			//are we going after children too, and does the node have any?
			if (allChildren && node.childNodes && node.childNodes.length > 0) {
				for (var i = 0, il = node.childNodes.length; i < il;i++) {
					newNode.appendChild(import_node(doc,node.childNodes[i], allChildren));
				}
			}
			return newNode;
			break;
			
			case document.TEXT_NODE:
			case document.CDATA_SECTION_NODE:
			case document.COMMENT_NODE:
			return doc.createTextNode(node.nodeValue);
			break;
		}
	};
}