let Fishing={
		
	//True if the notes box is being shown
	showingNotes:false,
	
	//Holds an array of item IDs created during this drag-drop, for undo
	currentFish:{},

	//Holds a list of all containers on the bench, including nested containers.
	//Used to prevent scanning of containers that IceBear thinks are nested.
	containersOnBench:[],
	
	//Holds a list of container types, in case unrecognised containers are scanned
	//and need to be created
	containerTypes:[],
	canCreateSomeContainers:false,
	
	//Holds the user's configuration of the fishing UI. Values below are defaults.
	config:{
        'autopintopuck':0,
        'shownotesboxafterfish':1
	},

	/***************************************************************************
	 * 
	 * INITIALIZATION
	 * 
	 ***************************************************************************/

	/**
	 * Initialises the UI.
	 */
	init:function(){
		
		document.getElementById("headersearch").style.display="none";
		
		Fishing.loadConfig();
		Fishing.getContainerTypes();
		document.getElementById("barcodeform").onsubmit=function(){ Fishing.handleScan(); return false; };
		document.getElementById("configbutton").addEventListener("click",Fishing.toggleConfig);
		window.setInterval(Fishing.focusBarcodeField,250);
		let qs=new URLSearchParams(document.location.search.substring(1));
		let barcodes=[];
		if(qs.has("barcodes")){
			barcodes=qs.get("barcodes").split(",");
		}
		let previousBarcodes=Fishing.getWorkspaceBarcodesFromCookie();
		if(previousBarcodes && 0!==previousBarcodes.length){
			let msg="IceBear found "+previousBarcodes.length+" barcodes from a previous session. Restore them to the workbench?";
			if(1===previousBarcodes.length){
				msg="IceBear found a barcode ("+previousBarcodes[0]+") from a previous session. Restore it to the workbench?";
			}
			if(confirm(msg)){
				barcodes=previousBarcodes;
			}
		}
		Fishing.restoreBarcodes(barcodes);
		Fishing.resetCurrentFish();
	},
	
	/**
	 * Takes a list of barcodes and calls findByBarcode on each, waiting for each item to
	 * render before continuing.
	 * 
	 * MAINTENANCE NOTE: If you work on this, make certain that it puts items onto the workbench
	 * in the order specified in the array. If a user is recovering from an error by reloading the
	 * page, they will expect their pins to be in the same order as before.
	 * 
	 * @param barcodes The array of barcodes to restore
	 * @param waitingFor The barcode currently restoring. Do not specify this.
	 * 
	 */
	restoreBarcodes:function(barcodes, waitingFor){
		if(!barcodes || 0===barcodes.length){ return; }
		if(waitingFor && !document.body.querySelector('[data-itemname="'+waitingFor+'"]')){
			window.setTimeout(function(){ Fishing.restoreBarcodes(barcodes, waitingFor) }, 50);
			return;
		}
		waitingFor=barcodes[0];
		barcodes.shift();
		Fishing.findByBarcode(waitingFor);
		window.setTimeout(function(){ Fishing.restoreBarcodes(barcodes, waitingFor) }, 150);
	},

	/**
	 * Returns focus to the barcode field, unless a drag operation is in progress or
	 * the notes box is being shown.
	 */
	focusBarcodeField:function(){
		if(Fishing.showingNotes){ return false; }
		document.getElementById("barcode").focus();
	},

	/**
	 * Fetches all container types ready for unknown container creation
	 */
	getContainerTypes:function(){
		new AjaxUtils.Request('/api/containertype',{
			method:'get',
			onSuccess:Fishing.getContainerTypes_onSuccess,
			onFailure:Fishing.getContainerTypes_onFailure
		});
	},
	getContainerTypes_onSuccess:function(transport){
		if(transport.responseJSON && transport.responseJSON.rows){
			Fishing.containerTypes=transport.responseJSON.rows;
			if(isAdmin || isTechnician){
				Fishing.canCreateSomeContainers=true;
			} else {
				Fishing.containerTypes.forEach(function(ct){
					if(1*ct["userscancreate"]){
						Fishing.canCreateSomeContainers=true;
					}
				});
			}
		}
	},
	getContainerTypes_onFailure:function(){
		//meh.
	},
	
	
	/***************************************************************************
	 * 
	 * MANAGING "CURRENT FISH" RECORD
	 * 
	 ***************************************************************************/
	
	resetCurrentFish:function(){
		Fishing.currentFish={ created:[] };
	},

	pushCreatedToCurrentFish:function(created){
		if(Array.isArray(created)){
			created.forEach(function(c){ Fishing.pushCreatedToCurrentFish(c); });
		} else if(created.welldropid){
			//it's a crystal
			Fishing.currentFish.created.push({
				"type":"crystal",
				"id":created.id,
				"welldropid":created.welldropid
			});
		} else if(created.position){
			//it's a containercontent
			Fishing.currentFish.created.push({ 
				"type":"containercontent",
				"id":created.id,
				"container":created.parent,
				"contained":created.child,
				"position":created.position,
			});
		} else if(created.containertypeid){
			//it's a dummy pin, we don't care
		}
	},

	/***************************************************************************
	 * 
	 * UNDO PREVIOUS FISH/NEST ACTION
	 * 
	 ***************************************************************************/
	
	showUndoButton:function(){
		let undoButton=document.getElementById("undobutton");
		if(!undoButton){
			undoButton=document.createElement("input");
			undoButton.type="button";
			undoButton.id="undobutton";
			undoButton.value="Undo last fish";
			undoButton.onclick=Fishing.undoFish;
			let headerSearchBox=document.getElementById("headersearch");
			headerSearchBox.parentNode.insertBefore(undoButton,headerSearchBox);
		}
		if(Fishing.currentFish.crystal){
			undoButton.value="Undo last fish";
		} else {
			undoButton.value="Undo last packing";
		}
		undoButton.style.display="inline";
	},

	hideUndoButton:function(){
		let undoButton=document.getElementById("undobutton");
		if(!undoButton){ return false; }
		undoButton.style.display="none";
	},
	
	undoFish:function(){
		Fishing.hideUndoButton();
		if(Fishing.currentFish.crystal){
			Fishing._unmarkCrystalFished(Fishing.currentFish.crystal.id);
		}
		Fishing.currentFish.created=Fishing.currentFish.created.reverse();
		Fishing._undoFish();
	},
	_undoFish:function(){
		if(!Fishing.currentFish.created || 0===Fishing.currentFish.created.length){
			window.setTimeout(Fishing.mapDragDropElements,100);
			return false; 
		}
		let undo=Fishing.currentFish.created.shift();
		if("crystal"===undo.type){
			Fishing._undoCreateCrystal(undo);
		} else if("containercontent"===undo.type){
			Fishing._undoItemIntoContainer(undo);
		} 
	},
	_undoItemIntoContainer:function(undo){
		let container=document.getElementById("container"+undo.container);
		let barcode="";
		if(container){
			let items=container.container.childitems;
			for(let i=1;i<items.length;i++){
				let ci=items[i];
				if(parseInt(ci.id)===parseInt(undo.contained)){
					if(ci.welldropid){
						Fishing._unmarkCrystalFished(ci.id,ci.welldropid);
					}
					if(!ci.welldropid && -1===ci["name"].indexOf("dummypin")){
						barcode=ci["name"];
					}
					Fishing.containersOnBench=ui.arrayWithout(Fishing.containersOnBench,barcode);
					if(ci.childitems){
						ci.childitems.forEach(function(cci){
							if(cci["name"] &&""!==cci["name"]){
								Fishing.containersOnBench=ui.arrayWithout(Fishing.containersOnBench,cci["name"]);
							}
						});
					}

					items[i]={
							controls:"",
							isEmpty:1,
							name:"",
							position:i,
							proteinacronym:"",
							rendername:"",
							samplename:"",
							shippingcomment:""
					};
					break;
				}
			}
		}
		new AjaxUtils.Request("/api/containercontent/"+undo.id,{
			method:'delete',
			parameters:{ csrfToken:csrfToken, },
			onSuccess:function(transport){ Fishing._undoItemIntoContainer_onSuccess(transport, barcode); },
			onFailure:Fishing._undoFailed
		});
		window.setTimeout(function(){ Fishing.updateContentCounter(container) },100);
	},
	_undoItemIntoContainer_onSuccess:function(transport, barcode){
		if(""!==barcode){
			Fishing.findByBarcode(barcode);
			Fishing._waitForBarcodeRescanAfterUndo(barcode);
		} else {
			Fishing._undoFish();
		}
		
	},
	
	_unmarkCrystalFished:function(crystalId,wellDropId){
		let wellDropDiv=document.getElementById("welldrop"+wellDropId);
		let crystalDiv=document.getElementById("crystal"+crystalId);
		if(wellDropDiv){
			wellDropDiv.crystals.forEach(function(c){
				if(1*c.id===1*crystalId){ c.isfished=0; }
			});
		}
		if(crystalDiv){
			crystalDiv.classList.remove("xtal_fished");
			crystalDiv.crystal.isfished=0;
			crystalDiv.title=null;
		}
		new AjaxUtils.Request("/api/crystal/"+crystalId,{
			method:'patch',
			parameters:{ csrfToken:csrfToken, isfished:0 }
		});
		
	},
	
	_waitForBarcodeRescanAfterUndo:function(barcode){
		let existsOnWorkbench=false;
		document.querySelectorAll(".fish_container").forEach(function(c){
			if(c.dataset.itemname===barcode){
				existsOnWorkbench=true;
			}
		});
		if(existsOnWorkbench){
			window.setTimeout(Fishing._undoFish, 100);
		} else {
			window.setTimeout(function(){ Fishing._waitForBarcodeRescanAfterUndo(barcode)}, 100);
		}
	},
	
	
	_undoCreateCrystal:function(undo){
		let wd=document.getElementById("welldrop"+undo.welldropid);
		for(let i=0; i<wd.crystals.length; i++){
			if(parseInt(wd.crystals[i].id)===parseInt(undo.id)){
				wd.crystals.splice(i,1);
				break;
			}
		}
		new AjaxUtils.Request("/api/crystal/"+undo.id,{
			method:'delete',
			parameters:{ csrfToken:csrfToken, },
			onSuccess:function(transport){ Fishing._undoCreateCrystal_onSuccess(transport, wd); },
			onFailure:Fishing._undoFailed
		});
	},
	_undoCreateCrystal_onSuccess:function(transport,wellDrop){
		window.setTimeout(function(){ 
			Fishing.renderPlateWidgetDrop(wellDrop); 
			Fishing.mapDragDropElements();
		}, 100);
		Fishing._undoFish();
	},
	
	_undoFailed:function(){
		alert("Some actions could not be undone.");
	},
	
	/***************************************************************************
	 * 
	 * FISHING UI CONFIGURATION
	 * 
	 ***************************************************************************/

	/**
	 * Writes the current configuration to UserConfig.
	 */
	saveConfig:function(){
		Object.keys(Fishing.config).forEach(function(item){
			UserConfig.set("fishing_"+item, 1*Fishing.config[item]);
			if(document.getElementById(item) && document.getElementById(item).closest("label")){
				document.getElementById(item).closest("label").classList.remove("updating");
			}
		});
	},
	
	/**
	 * Loads the previously-saved user configuration.
	 */
	loadConfig:function(){
		Object.keys(Fishing.config).forEach(function(item){
			let val=UserConfig.get("fishing_"+item, Fishing.config[item]);
			Fishing.config[item]=1*val;
		});
	},
		
	/**
	 * Shows the configuration options in a modal box.
	 */
	toggleConfig:function(){
		let bb=ui.modalBox({
			title:'Configure fishing',
			content:''
		});	
		let f=bb.form({ autosubmit:false });
		f.checkbox({
			name:'autopintopuck',
			label:'After fishing a crystal onto a pin, move the pin to first empty puck position',
			value:Fishing.config.autopintopuck
		});
		f.checkbox({
			name:'shownotesboxafterfish',
			label:'After fishing a crystal, edit its notes and crystallographic information',
			helpText:'If this is turned off, IceBear will add a note describing the fishing action.',
			value:Fishing.config.shownotesboxafterfish
		});
		
		f.querySelectorAll("label img").forEach(function(i){
			i.addEventListener("click",function(){ 
				let inp=i.closest("label").querySelector("input");
				inp.id=inp.name;
				if(""===inp.value || isNaN(inp.value)){ inp.value=0; }
				Fishing.config[inp.name]=inp.value;
				Fishing[inp.name]=inp.value;
				inp.closest("label").classList.add("updating");
				window.setTimeout(Fishing.saveConfig,50);
			});
		})
	},

	/***************************************************************************
	 * 
	 * SCANNING BARCODES AND RETRIEVING/RENDERING OBJECTS
	 * 
	 ***************************************************************************/
	
	/**
	 * Saves the barcodes of all items on the workbench to a cookie, to enable restoring the
	 * workbench after a page refresh. The cookie has a short expiry period.
	 */
	setWorkspaceBarcodesToCookie:function(){
		let barcodes=[];
		if(Fishing.plate){
			barcodes.push(Fishing.plate.name);
		}
		let containers=document.querySelectorAll(".fish_container");
		if(containers){
			containers.forEach(function(c){
				barcodes.push(c.dataset.itemname);
			});
		}
		Cookies.set("fishing_barcodesonbench", barcodes.join(","), 1); //last parameter is days
	},
	
	/**
	 * Retrieves barcodes from a previous session and returns them in an array.
	 * The expiry period is defined in setWorkspaceBarcodesToCookie.
	 */
	getWorkspaceBarcodesFromCookie:function(){
		let barcodes=Cookies.get("fishing_barcodesonbench");
		if(!barcodes){ return []; }
		return barcodes.split(",");
	},
	
    /**
     * Initiates the process of finding and rendering scanned items
     */
    handleScan:function(){
            let b=document.getElementById("barcode");
            let barcode=b.value.trim();
            Fishing.findByBarcode(barcode);
    },

    findByBarcode:function(barcode){
    	barcode=barcode.trim();
    	if(""===barcode){ return false; }
		let conflict=false;
		Fishing.containersOnBench.forEach(function(c){
			if(c===barcode){
				alert(barcode+" is already on the bench, perhaps in another container?");
				document.getElementById("barcode").value="";
				conflict=true;
			}
		});
		if(conflict){ return false; }
		new AjaxUtils.Request('/api/plate/name/'+barcode,{
			method:"get",
			onSuccess:function(transport){
				if(!transport.responseJSON || !transport.responseJSON.id){
					return AjaxUtils.checkResponse(transport);
				}
				Fishing.renderPlate(transport.responseJSON);
			},
			onFailure:function(){ 
				new AjaxUtils.Request('/api/container/name/'+barcode,{
					method:"get",
					onSuccess:function(transport){
						if(!transport.responseJSON || !transport.responseJSON.id){
							return AjaxUtils.checkResponse(transport);
						}
						let container=transport.responseJSON;
						Fishing.renderContainer(container);
					},
					onFailure:function(){
						Fishing.tryCreatingUnknownContainer(barcode);
					}
				});
			}
		});
		document.getElementById("barcode").value="";
	},
	
	renderPlate:function(plate){
		let plateBox=document.getElementById("platebox");
		plateBox.querySelector("h2").classList.add("updating");
		plateBox.querySelector(".boxbody").innerHTML="";
		Fishing.plate=plate;
		Fishing.plateDropsRetrieved=false;
		Fishing.plateScoresRetrieved=false;
		Fishing.plateCrystalsRetrieved=false;
		new AjaxUtils.Request('/api/plate/'+plate.id+'/welldrop',{
			method:"get",
			onSuccess:function(transport){
				Fishing.plate.welldrops=transport.responseJSON.rows;
				Fishing.plateDropsRetrieved=true;
			}
		});
		if(""!==plate.screenid){
			new AjaxUtils.Request('/api/screen/'+plate.screenid+'/screencondition?all=1',{
				method:"get",
				onSuccess:Fishing.attachScreenConditions
			});
		}
		new AjaxUtils.Request('/api/plate/'+plate.id+'/crystal?all=1',{
			method:'get',
			onSuccess:function(transport){
				Fishing.plate.crystals=transport.responseJSON.rows;
				Fishing.plateCrystalsRetrieved=true;
			},
			onFailure:function(){
				Fishing.plate.crystals=[];
				Fishing.plateCrystalsRetrieved=true;
			}
		});
		new AjaxUtils.Request('/api/plate/'+plate.id+'/dropbestscore?all=1',{
			method:'get',
			onSuccess:function(transport){
				Fishing.plate.scores=transport.responseJSON.rows;
				Fishing.plateScoresRetrieved=true;
			},
			onFailure:function(){
				Fishing.plate.scores=[];
				Fishing.plateScoresRetrieved=true;
			}
		});
		window.setTimeout(Fishing.doRenderPlate,100);
	},
	doRenderPlate:function(){
		let plate=Fishing.plate;
		if(!plate){ return false; }
		if(!Fishing.plateDropsRetrieved || !Fishing.plateScoresRetrieved || !Fishing.plateCrystalsRetrieved){ 
			window.setTimeout(Fishing.doRenderPlate,100);
			return false; 
		}
		let box=document.getElementById("platebox");
		if(!Fishing.plate || !Fishing.plate.welldrops){
			window.setTimeout(Fishing.renderPlate, 50);
			return false;
		}
		let header=box.querySelector("h2");
		let boxbody=box.querySelector(".boxbody");

		//whole boxbody goes up under header when platewidget is called. Correct for it.
		//20250220 - No evidence of this behaviour with below lines removed. Leaving in place for now but commented out.
		// boxbody.absolutize();
		// boxbody.style.top=header.offsetHeight+"px";

		boxbody.innerHTML="";
		let allWellsHaveConstructs=true;
		Fishing.plate.welldrops.forEach(function(wd){
			if(""===wd.constructid){
				allWellsHaveConstructs=false;
			}
		});
		if(!allWellsHaveConstructs){
			boxbody.innerHTML+='<div class="warning">Some drops have no protein/construct. This must be set before crystals are shipped.</div>';
		}
		let pw=PlateWidget.render({
			plateType:Fishing.plate,
			wellDrops:Fishing.plate.welldrops,
			dropPickerPosition:"left"
		}, boxbody);
		pw.plate=pw["plateType"];
		pw.dataset.itemname=plate.name;
		header.innerHTML="Plate: "+plate.name;
		let drops=pw.querySelector(".pw_plate").querySelectorAll(".pw_drop");
		drops.forEach(function(dropDiv){
			dropDiv.id="welldrop"+dropDiv.dataset.welldropid; //for direct targeting later
			dropDiv.crystals=[];
		});
		Fishing.plate.crystals.forEach(function(c){
			document.getElementById("welldrop"+c.welldropid).crystals.push(c);
		});
		Fishing.plate.scores.forEach(function(s){
			document.getElementById("welldrop"+s.id).score=s;
		});
		Fishing.plate.welldrops.forEach(function(wd){
			let dropDiv=document.getElementById("welldrop"+wd.id);
			dropDiv.wellDrop=wd;
			dropDiv.dataset.isImaged=wd["isimaged"];
		});
		drops.forEach(function(dropDiv){
			Fishing.renderPlateWidgetDrop(dropDiv);
		});
		header.classList.remove("updating");
		Fishing.mapDragDropElements();
		window.setTimeout(Fishing.setWorkspaceBarcodesToCookie,50);
	},
	
	renderPlateWidgetDrop:function(dropDiv){
		dropDiv.innerHTML="";
		dropDiv.id="welldrop"+dropDiv.dataset.welldropid; //for direct targeting later
		dropDiv.onclick=function(evt){ Fishing.showImageAndCrosshairsForDropDiv(evt.target); };
		let xtalcount=document.createElement("div");
		xtalcount.classList.add("fish_xtalcount","empty");
		let s=document.createElement("span");
		xtalcount.appendChild(s);
		dropDiv.appendChild(xtalcount);
		let xtaldrag=document.createElement("div");
		xtaldrag.classList.add("fish_xtaldrag");
		xtaldrag.dataset.welldropid=dropDiv.dataset.welldropid;
		dropDiv.appendChild(xtaldrag);
		if(dropDiv.score){
			dropDiv.style.backgroundColor="#"+dropDiv.score["bestscorecolor"];
		}
		let numCrystals=dropDiv.crystals.length;
		if(numCrystals>0){
			xtalcount.querySelector("span").innerHTML=numCrystals+"";
			xtalcount.classList.remove("empty");
		}
		let hasConstruct=(0!==1*dropDiv.wellDrop.constructid);
		let isImaged=parseInt(dropDiv.dataset.isImaged);
		let warning="";
		if(!hasConstruct){
			warning+="Drop has no protein/construct. ";
		}
		if(!isImaged){
			warning+="Drop has not been imaged and may be empty. ";
		}
		if(""!==warning){
			dropDiv.title=warning;
			dropDiv.classList.add("warning");
		}
	},
	
	attachScreenConditions:function(transport){
		if(!transport.responseJSON || !transport.responseJSON.rows){ return false; }
		let plate=document.body.querySelector(".pw_plate");
		if(!plate){
			window.setTimeout(function(){ Fishing.attachScreenConditions(transport)},100);
			return false;
		}
		transport.responseJSON.rows.forEach(function(condition){
			let well=plate.querySelector(".well"+condition["wellnumber"]);
			if(!well){ return; }
			well.dataset.condition=condition.description;
			well.title=Plate.getWellName(well.dataset.row,well.dataset.col)+": "+condition["description"];
		});
	},
	
	/**
	 * Initiates rendering of a pin, puck or dewar, fetching and attaching its contents.
	 * @param container A JSON object as returned by GET /api/container/1234.
	 * @param bypassDupeCheck If set, don't check whether this container is already on the bench (e.g., after nesting existing containers)
	 */
	renderContainer:function(container, bypassDupeCheck){
		new AjaxUtils.Request('/api/container/'+container.id+'/fullcontent',{
			method:'get',
			onSuccess:function(transport){
				let containers=transport.responseJSON.rows;
				let conflict=false;
				if(!bypassDupeCheck){
					containers.forEach(function(c){
						//TODO Recursively
						if(conflict || !c.name || !c["containertypename"]){ return; }
						if(-1!==Fishing.containersOnBench.indexOf(c.name) && 0!==c.name.indexOf("dummypin")){
							alert(container.name+" contains "+c.name+", which is already on the bench. Cannot add "+container.name);
							conflict=true;
						}
					});
				}
				if(conflict){ return false; }
				containers.forEach(function(c){
					//TODO Recursively
					if(!c.name || !c["containertypename"]){ return; }
					if(-1===Fishing.containersOnBench.indexOf(c.name)){
						Fishing.containersOnBench.push(c.name);
					}
				});
				if(-1===Fishing.containersOnBench.indexOf(container.name)){
					Fishing.containersOnBench.push(container.name);
				}
				Fishing.doRenderContainer(container, containers);
			},
			onFailure:function(transport){
				if(404!==transport.status){
					return AjaxUtils.checkResponse(transport);
				}
				Fishing.doRenderContainer(container, null);
			},
		});
	},
	
	/**
	 * Renders a pin, puck or dewar to the UI, with contents attached.
	 * @param container A JSON object as returned by GET /api/container/1234
	 * @param contents An array of the container's contents.
	 */
	doRenderContainer:function(container,contents){
		let elem=document.getElementById("container"+container.id);
		let category=container["containercategoryname"].toLowerCase();
		if(!elem){
			elem=document.createElement("div");
			elem.id="container"+container.id;
			elem.classList.add("fish_container","fish_"+category);
			let renderName=container.name;
			if(0===renderName.indexOf("dummypin")){
				renderName="Pin";
				elem.title="Pin has no barcode";
			}
			elem.dataset.itemname=container.name;
			elem.innerHTML='<span class="fish_contentcount"><span></span></span><span>'+renderName+'</span>';
			let box=document.getElementById(category+"sbox");
			box=box.querySelector(".boxbody")||box;
			box.appendChild(elem);
		} else {
			elem.classList.remove("fish_activedroppable");
		}
		elem.container=container;
		let contentCount=0;
		let positions=1*container.positions;
		if(contents){
			contents.forEach(function(c){
				if(c.childitems || c.welldropid){
					contentCount++;
				}
			});
		} else {
			contents=["dummy"];
			for(let i=0;i<positions;i++){
				contents.push("");
			}
		}
		elem.container.childitems=contents;
		Fishing.updateContentCounter(elem);
		window.setTimeout(Fishing.mapDragDropElements,50);
		window.setTimeout(Fishing.setWorkspaceBarcodesToCookie,50);
		return elem;
	},
	
	updateContentCounter:function(elem){
		if(!elem){ return false; }
		let category=elem.container["containercategoryname"].toLowerCase();
		let positions=1*elem.container.positions;
		let contentCount=0;
		let cc=elem.querySelector(".fish_contentcount");
		elem.container.childitems.forEach(function(c){
			if(c["childitems"] || c["welldropid"] || c["crystalhidden"]){
				contentCount++;
			}
		});
		if("pin"===category){
			if(0===contentCount){
				elem.classList.remove("hascrystal");
			} else {
				elem.classList.add("hascrystal");
			}
		} else if("puck"===category){
			cc.innerHTML=contentCount+"<span>/"+positions+"</span>";
		} else if("dewar"===category){
			cc.innerHTML=contentCount;
			window.setTimeout(Fishing.showShipDewarButton,50);
		}
		cc.onclick=Fishing.showClickedContainerContents;
		if(contentCount===positions){
			elem.classList.add("fish_full");
			elem.classList.remove("fish_notfull");
		} else {
			elem.classList.remove("fish_full");
			elem.classList.add("fish_notfull");
		}
	},
	
	showClickedContainerContents:function(evt){
		let elem=evt.target;
		while(!elem.container){
			elem=elem.closest("div");
		}
		Fishing.showContainerContents(elem);
	},
	showContainerContents:function(elem){
		Container.showAllContents(elem.container);
		if("puck"===elem.container["containercategoryname"].toLowerCase()){
			document.getElementById("modalBox").dataset.puckid=elem.container.id;
			Fishing.setUpPinInPuckDragDrop();
			Fishing.addUnpackButtons();
		} else if("dewar"===elem.container["containercategoryname"].toLowerCase()){
			Fishing.addUnpackButtons();
		} else if("pin"===elem.container["containercategoryname"].toLowerCase()){
			Fishing.addWashPinButton();
		}
	},
	
	/***************************************************************************
	 * 
	 * UNPACK TO BENCH
	 * 
	 ***************************************************************************/

	addWashPinButton:function(){
		let mb=document.getElementById("modalBox");
		if(!mb || -1!==mb.querySelector(".boxbody").innerHTML.indexOf("is empty")){
			return;
		}
		mb=mb.querySelector(".boxbody");
		let f=mb.form({});
		f.buttonField({
			label:"Wash pin",
			onclick:Fishing.washPin
		});
	},	

	washPin:function(evt){
		let btn=evt.target;
		let pin=btn.closest(".boxbody").pin;
		if(!confirm("Really wash the pin? This will remove the crystal from the pin.\n\nIf the crystal isn't yours, check first.")){
			return false;
		}
		btn.closest("label").classList.add("updating");
		new AjaxUtils.Request("/api/containercontent/"+pin.childitems[1].containercontentid,{
			method:"delete",
			parameters:{
				csrfToken:csrfToken
			},
			onSuccess:function(){
				//update pin inventory on bench and re-render.
				let pinElement=document.getElementById("container"+pin.id);
				pinElement.container.childitems[1]="";
				Fishing.updateContentCounter(pinElement);
				ui.closeModalBox();
			},
			onFailure:function(){
				btn.closest("label").classList.remove("updating");
				alert("Could not wash pin.");
			}
		});
	},
	

	/**
	 * TODO Request from TLV: Move pins between pucks. Implement as "unpack to bench"
	 * as a first attempt.
	 */
	addUnpackButtons:function(){

		let mb=document.getElementById("modalBox");
		let unpackIcon='<img style="min-height:2.5em; cursor:pointer" '+
		'src="/images/icons/'+skin["bodyIconTheme"]+'icons/unpack.gif" '+
		'title="Unpack and place onto workbench" onclick="Fishing.unpackToBench(this)"  alt=""/>';
		
		if(mb.querySelector(".treeitem")){
			// This is a dewar. Add the unpack icon to the right-hand end of each tree item
			// which has a puck.
			unpackIcon='<img style="cursor:pointer; height:2em; position:absolute; top:0.25em;right:0.5em; background-color:white;border-radius:3px" '+
			'src="/images/icons/'+skin["treeHeaderIconTheme"]+'icons/unpack.gif" '+
			'title="Unpack and place onto workbench" onclick="Fishing.unpackToBench(this)"  alt=""/>';
			
			mb.querySelectorAll(".treeitem").forEach(function(elem){
				if(elem.record && elem.record.name){
					elem.querySelector("h3").style.position="relative";
					elem.querySelector("h3").style.height="2.5em";
					elem.querySelector("h3").innerHTML+=unpackIcon;
				}
			});
		} else {
			// This is a puck. Add an extra cell to each table row, then add the unpack icon to
			// the new cell for any rows that have a pin.
			mb.querySelectorAll("tr").forEach(function(tr){
				tr.innerHTML+="<td></td>";
			});
			mb.querySelectorAll("tr.datarow").forEach(function(elem){
				if(elem.rowData && elem.rowData.name){
					let lastCell=elem.childNodes[elem.childNodes.length-1];
					lastCell.innerHTML=unpackIcon;
				}
			});
		}
	},
	
	unpackToBench:function(clickedIcon){
		Fishing.hideUndoButton();
		let item=clickedIcon.closest("tr, .treeitem");
		if(item.rowData){
			item.classList.add("updating");
			new AjaxUtils.Request('/api/containercontent/'+item.rowData.containercontentid,{
				method:'delete',
				parameters:{ csrfToken:csrfToken },
				onSuccess:function(){ Fishing.unpackPinToBench_onSuccess(item); },
				onFailure:AjaxUtils.checkResponse
			})
		} else if(item.record){
			item.querySelector(".treehead").classList.add("updating");
			new AjaxUtils.Request('/api/containercontent/'+item.record.containercontentid,{
				method:'delete',
				parameters:{ csrfToken:csrfToken },
				onSuccess:function(){ Fishing.unpackPuckToBench_onSuccess(item); },
				onFailure:AjaxUtils.checkResponse
			})
		}
	},
	
	unpackPinToBench_onSuccess:function(tr){
		let position=parseInt(tr.rowData['position']);
		//Re-render the pin
		Fishing.renderContainer({
			id:tr.rowData['id'],
			name:tr.rowData['name'],
			containercategoryname:"Pin",
			positions:1
		}, true);
		//Re-render the puck
		Fishing.renderContainer({
			id:tr.rowData['puckid'],
			name:tr.rowData['puckname'],
			containercategoryname:"Puck",
			positions:tr.rowData['puckpositions']
		}, true);
		
		//Update the HTML in the modal box
		tr.rowData={
			controls:"",name:"",proteinacronym:"",rendername:"",samplename:"",
			shippingcomment:"",spacegroup:"",unitcella:"",unitcellb:"",unitcellc:"",
			unitcellalpha:"",unitcellbeta:"",unitcellgamma:"",
			isEmpty:"1", position:position
		};
		tr.querySelectorAll("td").forEach(function(td){
			td.innerHTML='';
		});
		tr.querySelector("td").innerHTML=position;
		tr.classList.remove("updating");
	},

	unpackPuckToBench_onSuccess:function(treeItem){
		let unpacked=treeItem.record;
		let dewar=treeItem.dewar;
		//Re-render the puck
		Fishing.renderContainer({
			id:unpacked['id'],
			name:unpacked['name'],
			containercategoryname:"Puck",
			positions:unpacked['positions']
		}, true);
		//Re-render the dewar
		Fishing.renderContainer({
			id:dewar.id,
			name:dewar.name,
			containercategoryname:"Dewar",
			positions:dewar.name
		}, true);

		//update the copy of the dewar attached to the box
		dewar.childitems.forEach(function(pos){
			if(pos["id"] && parseInt(pos["id"])===parseInt(unpacked["id"])){
				pos="";
			}
		});
		
		//Update the HTML in the modal box
		treeItem.remove();
		
		if(!document.getElementById("modalBox").querySelector(".treeitem")){
			ui.closeModalBox();
		}
	},

	/***************************************************************************
	 * 
	 * RE-ARRANGE PINS WITHIN PUCK
	 * 
	 ***************************************************************************/
	
	/**
	 * Turns the pin barcodes in a puck into draggable buttons, if the puck is not full.
	 */
	setUpPinInPuckDragDrop:function(){
		window.setTimeout(function(){
			document.getElementById("modalBox").querySelectorAll("tr").forEach(function(tr){
				tr.querySelector("th,td").classList.add("fish_positionnumber");
				let td=tr.querySelector("td+td");
				if(!td){ return; }
				td.classList.add("pinbarcode");
				if(tr.rowData.isEmpty){
					td.classList.add("empty");
					td.classList.remove("haspin");
				} else {
					td.classList.add("haspin");
					td.classList.remove("empty");
				}
			});
			let pins=document.querySelectorAll("td.haspin");
			let slots=document.querySelectorAll("td.empty");
			//Nothing to drag if puck is full or empty
			if(0===pins.length||0===slots.length){ return; }
			pins.forEach(function(pin){
				pin.style.width="12em";
				let name=pin.closest("tr")["rowData"].name.trim();
				if(""===name || 0===name.indexOf("dummypin")){ name="(no barcode)"; }
				pin.innerHTML='<div class="fish_pininpuck" id="pininpuck'+pin.closest("tr")["rowData"].id+'">'+name+'</div>';
			});
			slots.forEach(function(slot){
				slot.innerHTML="";
			});
			
			let pinDraggables=document.querySelectorAll("#modalBox .haspin div");
			let pinDroppables=document.querySelectorAll("#modalBox .empty");
			Fishing.attachDroppables(pinDraggables, pinDroppables);
			pinDraggables.forEach(function(d){
				d.ondragstart=Fishing.dragPinInPuckStart; 
				d.ondragend=Fishing.dragPinInPuckEnd; 
			});
			pinDroppables.forEach(function(d){
				d.ondragover=Fishing.dropPinInPuckOver;
				d.ondragenter=Fishing.dropPinInPuckEnter;
				d.ondragleave=Fishing.dropPinInPuckLeave;
				d.ondrop=Fishing.dropPinInNewPuckSlot;
			});
		},50);
	},
		
	dragPinInPuckStart:function(evt){
		let dragged=evt.target;
		if(dragged.classList.contains("fish_dropped")){ return false; }
		Fishing.resetCurrentFish();
		Fishing.hideUndoButton();
		evt.dataTransfer.dropEffect="move";
		evt.dataTransfer.setData("text", dragged.id);
		dragged.classList.add("fish_draggingcontainer");
		dragged.droppables.forEach(function(d){ d.classList.add("fish_droppable"); });
	},
	dragPinInPuckEnd:function(evt){
		let dragged=evt.target;
		if(!dragged.classList.contains("fish_dropped")){
			dragged.classList.remove("fish_draggingcontainer");
		}
		document.querySelectorAll(".fish_droppable").forEach(function(d){ d.classList.remove("fish_droppable"); });
		return false;
	},
	dropPinInPuckOver:function(evt){
		if(evt.preventDefault){ evt.preventDefault(); }
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(droppable.classList.contains("fish_droppable")){
			evt.dataTransfer.dropEffect="move";
		} else {
			evt.dataTransfer.dropEffect="none";
		}
		return false;
	},
	dropPinInPuckEnter:function(evt){
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(droppable.classList.contains("fish_droppable")){
			droppable.classList.add("fish_activedroppable");
		}
		return false;
	},
	dropPinInPuckLeave:function(evt){
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(!droppable){ return false; }
		droppable.classList.remove("fish_activedroppable");
		return false;
	},
	
	dropPinInNewPuckSlot:function(evt){
		evt.preventDefault();
		let slot=evt.target;
		let pin=document.getElementById(evt.dataTransfer.getData("text"));
		if(!slot || !pin){ return false; }
		slot.classList.remove("fish_updated","fish_activedroppable");
		slot.classList.add("updating");
		let pinData=pin.closest("tr")["rowData"];
		let slotData=slot.closest("tr").rowData;
		new AjaxUtils.Request('/api/containercontent/'+pinData.containercontentid,{
			method:'patch',
			parameters:{
				csrfToken:csrfToken,
				position:slotData.position,
			},
			onSuccess:function(){ Fishing.afterMovePinInPuck(pin, slot) },
			onFailure:AjaxUtils.checkResponse
		});
	},
	
	afterMovePinInPuck:function(pin, slot){
		let puck=document.getElementById("container"+document.getElementById("modalBox").dataset.puckid);
		let pinPosition=pin.closest("tr").rowData.position;
		let slotPosition=slot.closest("tr").rowData.position;
		let tmp=puck.container.childitems[pinPosition];
		puck.container.childitems[pinPosition]=puck.container.childitems[slotPosition];
		puck.container.childitems[slotPosition]=tmp;
		Fishing.showContainerContents(puck);
	},
	
	
	/***************************************************************************
	 * 
	 * CREATING UNKNOWN CONTAINERS
	 * 
	 ***************************************************************************/
	
	/**
	 * Given a barcode, shows a modal box for selecting its container type. If a given containercategory
	 * has user create disabled, users will be encouraged to speak to the admin. If all containercategories
	 * have user create disabled, no containers can be created. 
	 * @param barcode The unrecognised barcode
	 */
	tryCreatingUnknownContainer:function(barcode){
		if(!Fishing.canCreateSomeContainers){
			alert("No plate or container with barcode "+barcode+".\n\nYou don't have permission to create containers.\n\nTalk to your IceBear administrator.");
			return false;
		}
		ui.modalBox({
			title:"IceBear has no container with barcode "+barcode+". What is it?",
			content:'<div id="fish_ct_pin"></div><div id="fish_ct_puck"></div><div id="fish_ct_dewar"></div>'
		});
		Fishing.containerTypes.forEach(function(ct){
			let catName=ct["containercategoryname"].toLowerCase();
			let categoryDiv=document.getElementById("fish_ct_"+catName);
			if(!categoryDiv){ return; } //Not interested in this containercategory
			let canCreate=true;
			if(!ct["userscancreate"] && !isAdmin && !isTechnician){
				canCreate=false;
				if(categoryDiv.querySelector(".fish_containertype")){
					//already wrote the "You can't create whatever(s)" div
					return;
				}
			}
			let html='';
			if(canCreate){
				html+='<div class="fish_containertype" data-containertypeid="'+ct.id+'" data-barcode="'+barcode+'" onclick="Fishing.createContainer(this)" style="cursor:pointer">';
				html+='<div class="fish_containertypename fish_'+catName+'">'+ct.name+'</div>';
				if("puck"===catName){
					html+='<div class="fish_containertypepositions">'+ct.positions+' positions</div>';
				}
				html+='</div>';
			} else {
				html+='<div class="fish_containertype" data-containertypeid="'+ct.id+'" data-barcode="'+barcode+'">';
				html+='<div class="fish_containertypename fish_'+catName+'">You can\'t create '+catName+'s.</div>';
				html+='<div class="fish_containertypepositions">Talk to your IceBear administrator.</div>';
				html+='</div>';
			}
			categoryDiv.innerHTML+=html;
		});
	},
	
	/**
	 * After user selection of container type, attempt to create a container. Determines the 
	 * barcode and container type from dataset attributes on the element.
	 * @param typeElement The clicked container type from the container creation modal box.
	 */
	createContainer:function(typeElement){
		typeElement.classList.add("updating");
		Container.create(typeElement.dataset.barcode, typeElement.dataset.containertypeid, Fishing.afterContainerCreate);
	},

	/**
	 * After creation of container, closes the type selection modal box and calls findByBarcode to add the
	 * newly-created container to the fishing workbench.
	 * @param container The created container 
	 */
	afterContainerCreate:function(container){
		ui.closeModalBox();
		Fishing.findByBarcode(container.name);
	},
	
	
	/***************************************************************************
	 * 
	 * SHOWING DROP IMAGE AND SHOWING/ADDING CROSSHAIRS
	 * 
	 ***************************************************************************/
	
	currentImageRow:null,
	currentImageCol:null,
	currentImageDrop:null,
	showImageAndCrosshairsForDropDiv:function(dropDiv){
		if(dropDiv.closest(".pw_drop")){ dropDiv=dropDiv.closest(".pw_drop"); }
		Fishing.showImageAndCrosshairs(dropDiv.dataset.row, dropDiv.dataset.col, dropDiv.dataset.dropnumber);
	},
	showImageAndCrosshairs:function(row,col,dropNum){
		Fishing.currentImageRow=row;
		Fishing.currentImageCol=col;
		Fishing.currentImageDrop=dropNum;
		let currentDrop=document.getElementById("platebox").querySelector(".platewidget").setCurrentDrop(row,col,dropNum);
		if(currentDrop.dropImages){
			Fishing.renderImageAndCrosshairs(currentDrop);
		} else {
			Fishing.findAndAttachImages(currentDrop);
		}
	},
	findAndAttachImages:function(dropDiv){
		new AjaxUtils.Request('/api/welldrop/'+dropDiv.dataset.welldropid+'/timecourseimage',{
			method:'get',
			onSuccess:function(transport){ Fishing.findAndAttachImages_onSuccess(transport,dropDiv) },
			onFailure:function(transport){ Fishing.findAndAttachImages_onFailure(transport,dropDiv) },
		});
	},
	findAndAttachImages_onSuccess:function(transport,dropDiv){
		let images=[];
		let rows=transport.responseJSON.rows;
		rows.forEach(function (img){
			if("visible"===img["lighttype"].toLowerCase()){
				images.push(img);
			}
		})
		if(!images.length){
			//No visible images. Just use the most recent (which is probably UV).
			images.push(rows[rows.length-1]);
		}
		dropDiv["dropImages"]=images;
		dropDiv["imageIndex"]=images.length-1;
		Fishing.renderImageAndCrosshairs(dropDiv);
	},
	findAndAttachImages_onFailure:function(transport){
		if(404!==transport.status){
			AjaxUtils.checkResponse(transport);
			return false;
		}
		alert("There are no images for this drop. Fish by dragging directly from the plate overview to the pin or puck.");
	},
	
	renderImageAndCrosshairs:function(dropDiv){
		let well=dropDiv.closest(".pw_well");
		let condition=well.dataset.condition;
		let box=document.getElementById("platebox");
		box.dropDiv=dropDiv;
		let boxbody=box.querySelector(".boxbody");
		let header=box.querySelector("h2");
		header.oldInnerHTML=header.innerHTML;
		header.innerHTML='<img src="/skins/default/images/icons/no.gif" class="closeicon" onclick="Fishing.closeImageAndCrosshairs()" alt="">' + header.oldInnerHTML +
			" well " + Plate.getWellName(dropDiv.dataset.row, dropDiv.dataset.col, dropDiv.dataset.dropnumber);
		let numImages=dropDiv.dropImages.length;
		let dropImage=dropDiv.dropImages[numImages-1];
		let imgDiv=document.createElement("div");
		imgDiv.classList.add("fish_dropimage");
		if(condition){
			imgDiv.classList.add("fish_hascondition");
		}
		let img=document.createElement("img");
		img.src=dropImage["fullimageurl"];
		img.id="fish_currentimage";
		img.dropImage=dropImage;
		img.addEventListener("click", Fishing.markCrystalOnImage);
		imgDiv.appendChild(img);
		boxbody.appendChild(imgDiv);
		boxbody.style.borderRadius=0;
		imgDiv.pixelScale=img.offsetHeight/dropImage["pixelheight"];
		dropDiv.crystals.forEach(function(xtal){
			Fishing.drawCrosshair(xtal);
		});
		Fishing.mapDragDropElements();
		Fishing.renderImageTimeControls(dropDiv);

		if(condition){
			let condDiv=document.createElement("div");
			condDiv.classList.add("fish_condition");
			condDiv.innerHTML="Condition: "+condition;
			boxbody.appendChild(condDiv);
		}

		window.onresize=function(){ 
			Fishing.closeImageAndCrosshairs();
			Fishing.renderImageAndCrosshairs(dropDiv);
		};
		Fishing.waitForCrosshairs(dropDiv.crystals.length);
	},

	waitForCrosshairs:function (crystalCount){
		if(!crystalCount){ return; }
		let rendered=document.getElementById("fish_currentimage").closest("div").querySelectorAll(".xtal_crosshair");
		if(rendered.length!==crystalCount){
			window.setTimeout(Fishing.waitForCrosshairs,250, crystalCount);
			return;
		}
		Fishing.mapDragDropElements();
	},

	renderImageTimeControls:function (dropDiv){
		let boxHeader=dropDiv.closest(".box").querySelector("h2");
		let wrapper=boxHeader.querySelector(".timeControls");
		if(wrapper){ wrapper.remove(); }
		let images=dropDiv["dropImages"];
		if(!images || 2>images.length){
			return;
		}
		wrapper=document.createElement("span");
		wrapper.classList.add("timeControls");
		wrapper.style.position="absolute";
		wrapper.style.right="4em";
		wrapper.style.top="0.2em";
		boxHeader.insertAdjacentElement("beforeend", wrapper);
		let backArrow=document.createElement("img");
		backArrow.addEventListener("click",Fishing.setPreviousImage);
		backArrow.style.filter="invert(1)";
		backArrow.src="/images/icons/darkicons/leftarrow.png";
		backArrow.id="imageNavForward";
		backArrow.style.height="1.5em"
		backArrow.title="Older image";
		wrapper.appendChild(backArrow);
		let clock=document.createElement("img");
		clock.style.filter="invert(1)";
		clock.style.height="1.5em";
		clock.style.margin="0 0.25em";
		clock.src="/images/icons/darkicons/clock.png";
		clock.id="imageNavForward";
		clock.title="Move back and forward in time";
		wrapper.appendChild(clock);
		let forwardArrow=document.createElement("img");
		forwardArrow.addEventListener("click",Fishing.setNextImage);
		forwardArrow.src="/images/icons/darkicons/rightarrow.png";
		forwardArrow.style.filter="invert(1)";
		forwardArrow.id="imageNavForward";
		forwardArrow.style.height="1.5em"
		forwardArrow.title="Newer image";
		wrapper.appendChild(forwardArrow);
		let imageIndex=dropDiv["imageIndex"];
		if(imageIndex>0){
			//attach event to back arrow, cursor:pointer
			backArrow.style.cursor="pointer";
			backArrow.style.opacity="1";
		} else {
			backArrow.style.cursor="default";
			backArrow.style.opacity="0.4";
		}
		if(imageIndex<images.length-1){
			//attach event to forward arrow, cursor:pointer
			forwardArrow.style.cursor="pointer";
			forwardArrow.style.opacity="1";
		} else {
			forwardArrow.style.cursor="default";
			forwardArrow.style.opacity="0.4";
		}
	},
	setPreviousImage:function (){
		let querySelector="tr.row"+Fishing.currentImageRow+" td.col"+Fishing.currentImageCol+" .drop"+Fishing.currentImageDrop;
		let dropDiv=document.getElementById("platebox").querySelector(querySelector);
		let imageIndex=dropDiv["imageIndex"];
		if(imageIndex<=0){ return false; }
		Fishing.setImage(dropDiv,imageIndex-1);
	},
	setNextImage:function (){
		let querySelector="tr.row"+Fishing.currentImageRow+" td.col"+Fishing.currentImageCol+" .drop"+Fishing.currentImageDrop;
		let dropDiv=document.getElementById("platebox").querySelector(querySelector);
		let imageIndex=dropDiv["imageIndex"];
		let images=dropDiv["dropImages"];
		if(imageIndex>=images.length-1){ return false; }
		Fishing.setImage(dropDiv,imageIndex+1);
	},
	setImage:function (dropDiv,imageIndex){
		dropDiv["imageIndex"]=imageIndex;
		let dropImage=dropDiv["dropImages"][imageIndex];
		let img=document.getElementById("fish_currentimage");
		let imgDiv=img.closest("div");
		imgDiv.pixelScale=img.offsetHeight/dropImage["pixelheight"];
		img.src=dropImage["fullimageurl"];
		img.dropImage=dropImage;
		imgDiv.querySelectorAll(".xtal_crosshair").forEach(function (crosshair){
			crosshair.remove();
		});
		dropDiv.crystals.forEach(function(xtal){
			Fishing.drawCrosshair(xtal);
		});
		Fishing.mapDragDropElements();
		Fishing.renderImageTimeControls(dropDiv);
	},

	drawCrosshair:function(xtal){
		let img=document.getElementById("fish_currentimage");
		if(!img.naturalHeight){
			window.setTimeout(Fishing.drawCrosshair, 250, xtal);
			return;
		}
		let xh=Crystal.drawCrosshair(xtal, img);
		let drag=document.createElement("div");
		drag.classList.add("fish_xtaldrag");
		drag.crystal=xtal;
		xh.appendChild(drag);
		if(xh.classList.contains("xtal_crosshairfromdifferentimage")){
			let querySelector="tr.row"+Fishing.currentImageRow+" td.col"+Fishing.currentImageCol+" .drop"+Fishing.currentImageDrop;
			let dropDiv=document.getElementById("platebox").querySelector(querySelector);
			let images=dropDiv["dropImages"];
			let index=-1;
			let found=false;
			images.forEach(function (img){
				if(found){ return; }
				index++;
				if(img["id"]===xtal["dropimageid"]){
					found=true;
					xh.addEventListener("click", function (){
						Fishing.setImage(dropDiv, index);
					});
				}
			});
		}
	},
	
	closeImageAndCrosshairs:function(){
		Fishing.currentImageRow=null;
		Fishing.currentImageCol=null;
		Fishing.currentImageDrop=null;
		let box=document.getElementById("platebox");
		let boxbody=box.querySelector(".boxbody");
		let header=box.querySelector("h2");
		header.innerHTML=header.oldInnerHTML;
		boxbody.querySelector(".fish_dropimage").remove();
		if(boxbody.querySelector(".fish_condition")){
			boxbody.querySelector(".fish_condition").remove();
		}
		window.onresize=null;
	},

	/***************************************************************************
	 * 
	 * CREATING CRYSTALS
	 * 
	 ***************************************************************************/
	
	/**
	 * Creates a new crystal, marked at the coordinates clicked on the image.
	 * @param evt The click event
	 */
	markCrystalOnImage:function(evt){
		let img=evt.target;
		//Crystal number should be the highest number in the drop, +1.
		let crystalNumber=0;
		let currentImage=document.getElementById("fish_currentimage");
		currentImage.closest(".fish_dropimage").querySelectorAll(".xtal_crosshair").forEach(function(c){
			crystalNumber=Math.max(crystalNumber,c.crystal.numberindrop);
		});
		crystalNumber++;
		//Coordinates
		let offset=ui.cumulativeOffset(currentImage);
		let scale=currentImage.offsetHeight / img.dropImage["pixelheight"];
		let crystalX=Math.round((evt.clientX-offset.left)/scale);
		let crystalY=Math.round((evt.clientY-offset.top)/scale);
		let pb=document.getElementById("platebox");
		pb.querySelector("h2").classList.add("updating");
		Fishing.createCrystal({
			isDummyCoordinate:false,
			isFished:false,
			crystalX:crystalX,
			crystalY:crystalY,
			crystalNumber:crystalNumber,
			projectId:currentImage.dropImage.projectid,
			wellDropId:currentImage.dropImage.welldropid,
			dropImageId:currentImage.dropImage.id
		});
	},
	
	createCrystal:function(crystal,container){
		new AjaxUtils.Request('/api/crystal',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				allowcreatedummyinspection:'yes',
				pixelx:crystal.crystalX,
				pixely:crystal.crystalY,
				isdummycoordinate:1*crystal.isDummyCoordinate,
				isfished:1*crystal.isFished,
				numberindrop:crystal.crystalNumber,
				projectid:crystal.projectId,
				welldropid:crystal.wellDropId,
				dropimageid:crystal.dropImageId,
				prefix:"dummy", //correct name generated server-side
				name:"dummy", //correct name generated server-side
			},
			onSuccess:function(transport){ Fishing.createCrystal_onSuccess(transport,container); },
			onFailure:function(transport){ Fishing.createCrystal_onFailure(transport,crystal,container); },
		});
	},
	createCrystal_onSuccess:function(transport,container){
		document.getElementById("platebox").querySelector("h2").classList.remove("updating");
		let crystal=transport.responseJSON.created;
		if(container){
			Fishing.addItemToContainer(crystal,container);
		}
		let drop=document.getElementById("welldrop"+crystal.welldropid);
		if(drop){
			drop.crystals.push(crystal);
			Fishing.renderPlateWidgetDrop(drop);
			if(document.getElementById("fish_currentimage")){
				Fishing.closeImageAndCrosshairs();
				Fishing.showImageAndCrosshairsForDropDiv(drop);
			}
		}
		Fishing.mapDragDropElements();
	},
	createCrystal_onFailure:function(transport,crystal){
		document.getElementById("platebox").querySelector("h2").classList.remove("updating");
		AjaxUtils.checkResponse(transport);
		PlateWidget.renderPlateWidgetDrop(document.getElementById("welldrop"+crystal.wellDropId));
	},


	/***************************************************************************
	 * 
	 * DRAG-DROP FUNCTIONALITY
	 * 
	 ***************************************************************************/

	dragImages:{},

	/**
	 * Defines which elements can be dragged onto which. For example, crystals into 
	 * pins or pucks but not dewars or other crystals; pucks into dewars only.
	 */
	mapDragDropElements:function(){
		let dragIcons=["crystal","pin","puck","dewar"];
		dragIcons.forEach(function(dragIcon){
			let img = document.createElement("img");
			img.src="/images/icons/"+skin["bodyIconTheme"]+"icons/fishdrag_"+dragIcon+".png";
			Fishing.dragImages[dragIcon]=img;
		});
		window.setTimeout(function(){
			let crystalDraggables=document.querySelectorAll(".fish_xtaldrag");
			let crystalDroppables=document.querySelectorAll(".fish_pin.fish_notfull, .fish_puck.fish_notfull");
			let pinDraggables=document.querySelectorAll(".fish_pin");
			let pinDroppables=document.querySelectorAll(".fish_puck.fish_notfull, #trashbox");
			let puckDraggables=document.querySelectorAll(".fish_puck");
			let puckDroppables=document.querySelectorAll(".fish_dewar.fish_notfull, #trashbox");
			let dewarDraggables=document.querySelectorAll(".fish_dewar");
			let dewarDroppables=document.querySelectorAll("#trashbox");
			Fishing.attachDroppables(crystalDraggables, crystalDroppables);
			Fishing.attachDroppables(pinDraggables, pinDroppables);
			Fishing.attachDroppables(puckDraggables, puckDroppables);
			Fishing.attachDroppables(dewarDraggables, dewarDroppables);
			crystalDraggables.forEach(function(d){
				d.ondragstart=Fishing.dragCrystalStart;
				d.ondragend=Fishing.dragCrystalEnd;
			});
			crystalDroppables.forEach(function(d){
				d.ondragover=Fishing.dropContainerOver;
				d.ondragenter=Fishing.dropContainerEnter;
				d.ondragleave=Fishing.dropContainerLeave;
				d.ondrop=Fishing.dropOntoContainer;
			});
			pinDraggables.forEach(function(d){
				d.ondragstart=Fishing.dragContainerStart; 
				d.ondragend=Fishing.dragContainerEnd; 
			});
			puckDraggables.forEach(function(d){
				d.ondragstart=Fishing.dragContainerStart; 
				d.ondragend=Fishing.dragContainerEnd; 
			});
			dewarDraggables.forEach(function(d){
				d.ondragstart=Fishing.dragContainerStart; 
				d.ondragend=Fishing.dragContainerEnd; 
			});
			pinDroppables.forEach(function(d){
				d.ondragover=Fishing.dropContainerOver;
				d.ondragenter=Fishing.dropContainerEnter;
				d.ondragleave=Fishing.dropContainerLeave;
				d.ondrop=Fishing.dropOntoContainer;
			});
			puckDroppables.forEach(function(d){
				d.ondragover=Fishing.dropContainerOver;
				d.ondragenter=Fishing.dropContainerEnter;
				d.ondragleave=Fishing.dropContainerLeave;
				d.ondrop=Fishing.dropOntoContainer;
			});
			dewarDroppables.forEach(function(d){
				d.ondragover=Fishing.dropContainerOver;
				d.ondragenter=Fishing.dropContainerEnter;
				d.ondragleave=Fishing.dropContainerLeave;
				d.ondrop=Fishing.dropOntoContainer;
			});
		},50);
	},

	/**
	 * Attaches an array of the suitable drop targets to each of the draggables.
	 */
	attachDroppables:function(draggables, droppables){
		if(!draggables || !droppables){ return false; }
		draggables.forEach(function(drag){
			drag.draggable=true;
			drag.droppables=droppables;
		});
	},
	
	dragCrystalStart:function(evt){
		let dragged=evt.target;
		if(dragged.closest(".xtal_fished")){
			alert("This crystal has already been fished.");
			return false;
		}
		Fishing.resetCurrentFish();
		dragged.classList.add("fish_draggingcrystal");
		evt.dataTransfer.dropEffect="move";
		evt.dataTransfer.setData("text", dragged.closest(".xtal_crosshair,.pw_drop").id);
		let img=Fishing.dragImages["crystal"];
		evt.dataTransfer.setDragImage(img,-10,50);
		dragged.droppables.forEach(function(d){ d.classList.add("fish_droppable"); });
		if(dragged.crystal){
			Fishing.currentFish.crystal=dragged.crystal;
			Fishing.currentFish.wellDrop=document.getElementById("welldrop"+dragged.crystal.welldropid).wellDrop;
		} else if(dragged.wellDrop) {
			Fishing.currentFish.wellDrop=dragged.wellDrop;
		}
	},
	dragCrystalEnd:function(){
		document.querySelectorAll(".fish_droppable").forEach(function(d){ d.classList.remove("fish_droppable"); });
		return false;
	},
	dragContainerStart:function(evt){
		let dragged=evt.target;
		if(dragged.classList.contains("fish_dropped")){ return false; }
		Fishing.resetCurrentFish();
		evt.dataTransfer.dropEffect="move";
		evt.dataTransfer.setData("text", dragged.id);
		dragged.classList.add("fish_draggingcontainer");
		let category=dragged.container["containercategoryname"].toLowerCase();
		let img=Fishing.dragImages[category];
		evt.dataTransfer.setDragImage(img,-10,50);
		dragged.droppables.forEach(function(d){ d.classList.add("fish_droppable"); });
	},
	dragContainerEnd:function(evt){
		let dragged=evt.target;
		if(!dragged.classList.contains("fish_dropped")){
			dragged.classList.remove("fish_draggingcontainer");
		}
		document.querySelectorAll(".fish_droppable").forEach(function(d){ d.classList.remove("fish_droppable"); });
		return false;
	},
	dropContainerOver:function(evt){
		if(evt.preventDefault){ evt.preventDefault(); }
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(droppable.closest(".fish_container")){ droppable=droppable.closest(".fish_container"); }
		if(droppable.classList.contains("fish_droppable")){
			evt.dataTransfer.dropEffect="move";
		} else {
			evt.dataTransfer.dropEffect="none";
		}
		return false;
	},
	dropContainerEnter:function(evt){
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(droppable.closest(".fish_container")){ droppable=droppable.closest(".fish_container"); }
		if(droppable.classList.contains("fish_droppable")){
			droppable.classList.add("fish_activedroppable");
		}
		return false;
	},
	dropContainerLeave:function(evt){
		let droppable=evt.target;
		if(!droppable){ return false; }
		if(!droppable){ return false; }
		droppable.classList.remove("fish_activedroppable");
		return false;
	},
	
	/**
	 * Handle a draggable item being dropped onto its destination.
	 */
	dropOntoContainer:function(evt){
		evt.preventDefault();
		let destination=evt.target;
		let dropped=document.getElementById(evt.dataTransfer.getData("text"));
		if(!destination || !dropped){ return false; }
		destination.classList.remove("fish_updated","fish_activedroppable");
		if(destination.closest("#trashbox") || destination.id==="trashbox"){
			return Fishing.removeContainerFromWorkbench(dropped);
		}
		if(!destination.container){ return false; }
		let container=destination.container;
		let contained=null;
		if(dropped.container){
			contained=dropped.container;
		} else if(dropped.crystal){
			contained=dropped.crystal;
		} else if(dropped.crystals){
			contained=dropped.wellDrop;
		}
		if(!contained){ return false; }
		Fishing.addItemToContainer(contained, container);
		window.setTimeout(Fishing.showUndoButton,100);
		return false;
	},
	
	/***************************************************************************
	 * 
	 * REMOVING CONTAINERS FROM WORKBENCH
	 * 
	 ***************************************************************************/

	/**
	 * Handle dropping a container onto the trash box
	 */
	removeContainerFromWorkbench:function(dropped){
		if(!dropped){ return false; }
		dropped.remove();
		let trash=document.getElementById("trashbox");
		trash.classList.remove("fish_activedroppable");
		trash.classList.remove("fish_updated");
		//We preserve the container's contents. Nothing to do there.
		//All we need to do is update the containers cookie so the container doesn't come back at reload.
		Fishing.containersOnBench=ui.arrayWithout(Fishing.containersOnBench,dropped.container.name);
		if(dropped.container.childitems){
			dropped.container.childitems.forEach(function(ci){
				Fishing.containersOnBench=ui.arrayWithout(Fishing.containersOnBench,ci.name);
				if(ci.childitems){
					ci.childitems.forEach(function(cci){
						Fishing.containersOnBench=ui.arrayWithout(Fishing.containersOnBench,cci.name);
					});
				}
			});
		}
		Fishing.setWorkspaceBarcodesToCookie();
		trash.querySelector(".boxbody").innerHTML=dropped.container.name+" removed.";
		let hasContents=false;
		if(dropped.container) {
			if (dropped.container.childitems) {
				dropped.container.childitems.forEach(function (ci) {
					if ("" !== ci && "dummy" !== ci) {
						hasContents = true;
					}
				});
			}
		}
		if(hasContents){
			trash.querySelector(".boxbody").innerHTML+="<br/>Contents were preserved.";
		}
		window.setTimeout(function(){
			trash.querySelector(".boxbody").innerHTML="Drag containers here to remove them from the bench";
		},1500);
		window.setTimeout(function(){
			if("dewar"===dropped.container["containercategoryname"].toLowerCase()){
				Fishing.showShipDewarButton();
			}
		},50);
	},
	
	/***************************************************************************
	 * 
	 * ADDING ITEMS TO CONTAINERS
	 * 
	 ***************************************************************************/

	/**
	 * Returns the number of the first empty position, or -1 if the container is full.
	 * @param container An object representing the container, including a childitems array.
	 * @return int|boolean The number of the first empty position, or -1 if full. This is 1-based, i.e., 7 is
	 * position 7 in the container. Returns false if the container does not exist or has no child items.
	 */
	getFirstEmptyPosition:function(container){
		if(!container.childitems){
			if(container.container){
				//Maybe got the element, not the object
				container=container.container;
			}
		}
		if(!container || !container.childitems){
			alert("No childitems on container in getFirstEmptyPosition"); 
			return false;
		}
		let firstEmpty=-1;
		let slots=container.childitems;
		for(let i=0;i<slots.length;i++){
			let slot=slots[i];
			if(""===slot || slot.isEmpty){
				slot="";
				firstEmpty=i;
				break;
			}
		}
		if(0<firstEmpty){
			Fishing.currentFish[container["containercategoryname"].toLowerCase()+"position"]=firstEmpty;
		}
		return firstEmpty;
	},


	/**
	 * Adds the contained item into the first empty position of the containing item.
	 * Depending on configuration settings, the containing item may itself be moved
	 * into another container, e.g., crystal to pin to puck.
	 * @param contained Object representing the child item
	 * @param container Object representing the parent item
	 */
	addItemToContainer:function(contained, container){
		let slot=Fishing.getFirstEmptyPosition(container);
		if(-1===slot){
			alert("Container is full.");
			//drag-drop setup should prevent. No other handling needed.
			return false;
		}
		new AjaxUtils.Request('/api/containercontent',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				parent:container.id,
				child:contained.id,
				position:slot
			}, 
			onSuccess:function(transport){ Fishing.addItemToContainer_onSuccess(transport, contained, container); },
			onFailure:Fishing.addItemToContainer_onFailure,
		});
	},
	
	addItemToContainer_onSuccess:function(transport, contained, container){
		let created=transport.responseJSON.created;
		Fishing.pushCreatedToCurrentFish(created);		

		let containerElement=document.getElementById("container"+contained.id);
		if(containerElement){
			//pin or puck was dragged
			containerElement.remove();
		} else if(document.getElementById("crystal"+contained.id)){
			//crystal crosshair was dragged
			let crystalCrosshair=document.getElementById("crystal"+contained.id);
			crystalCrosshair.crystal.isfished=1;
			crystalCrosshair.classList.add("xtal_fished");
			Fishing.renderPlateWidgetDrop(document.getElementById("welldrop"+contained.welldropid));
		} else if(document.getElementById("welldrop"+contained.id)){
			//well drop was dragged, should have created crystal
			created.forEach(function(obj){
				if("crystal"===obj["objecttype"]){
					document.getElementById("welldrop"+contained.id).crystals.push(obj);
					Fishing.currentFish.crystal=obj;
				} else if("container"===obj["objecttype"] && "pin"===obj["containercategoryname"].toLowerCase()){
					Fishing.currentFish.pin=obj;
				}
			});
			Fishing.renderPlateWidgetDrop(document.getElementById("welldrop"+contained.id));
		}
		Fishing.currentFish[container["containercategoryname"].toLowerCase()]=container;
		document.getElementById("container"+container.id).classList.add("fish_updated");
		window.setTimeout(function(){
			Fishing.renderContainer(container, true);
		},50);
		let autoPinToPuck=parseInt(Fishing.config.autopintopuck);
		let showNotesAfterFish=parseInt(Fishing.config.shownotesboxafterfish);
		if(autoPinToPuck && "pin"===container["containercategoryname"].toLowerCase()){
			let firstPuckWithSpace=document.getElementById("pucksbox").querySelector(".fish_notfull");
			if(firstPuckWithSpace){
				window.setTimeout(function(){
					Fishing.addItemToContainer(container, firstPuckWithSpace.container);
				},300);
			} else {
				Fishing.addDefaultNote();
				if(showNotesAfterFish){
					Fishing.showNotesBox();
				}
			}
		} else if(Fishing.currentFish.crystal){
			Fishing.addDefaultNote();
			if(showNotesAfterFish){
				Fishing.showNotesBox();
			}
		}
	},
	addItemToContainer_onFailure:function(transport){
		AjaxUtils.checkResponse(transport);
	},
	
	
	/***************************************************************************
	 * 
	 * CRYSTAL NOTES AND DETAILS
	 * 
	 ***************************************************************************/

	saveNoteOnLastFishedCrystal:function(noteText, successCallback){
		let crystal=Fishing.currentFish.crystal;
		if(""===noteText){ return false; }
		new AjaxUtils.Request('/api/note',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				parentid:crystal.id,
				projectid:crystal.projectid,
				text:noteText
			},
			onSuccess:function(transport){ if(successCallback){ successCallback(transport.responseJSON); } },
			onFailure:AjaxUtils.checkResponse
		})
	},

	addDefaultNote:function(){
		if(!Fishing.currentFish || !Fishing.currentFish.crystal){ return false; }
		if(!Fishing.currentFish.wellDrop){
			Fishing.currentFish.wellDrop=document.getElementById("welldrop"+Fishing.currentFish.crystal.welldropid).wellDrop;
		}
		let defaultNote='Crystal fished from plate '+Fishing.plate.name+' well '+Plate.getWellName(Fishing.currentFish.wellDrop.row, Fishing.currentFish.wellDrop.col, Fishing.currentFish.wellDrop.dropnumber);
		if(Fishing.currentFish.pin && 0!==Fishing.currentFish.pin.name.indexOf("dummy")){
			defaultNote+=' onto pin '+Fishing.currentFish.pin.name;
		}
		if(Fishing.currentFish.puck){
			defaultNote+=' and put into position '+Fishing.currentFish["puckposition"]+' of puck '+Fishing.currentFish.puck.name;
		}
		defaultNote+='.';
		Fishing.saveNoteOnLastFishedCrystal(defaultNote);
	},

	autoNoteSaveDelay:30, //seconds

	//Many crystals in a fishing session may have the same treatment. Make it possible
	//to re-use notes from this session (at least a small number).
	recentNotes:[],
	
	//The maximum number of recent notes to offer.
	recentNotesToOffer:5,
	
	addRecentNote:function(note){
		//clean up and escape
		note=note.trim();
		if(""===note){ return false; }
		let d=document.createElement("div");
	    d.innerText=note;
	    note=d.innerHTML;
	    //add to recents
		Fishing.recentNotes.forEach(function(n){
			if(n===note){
				Fishing.recentNotes=ui.arrayWithout(Fishing.recentNotes,n);
			}
		});
		Fishing.recentNotes.push(note);
	},
	
	showRecentNotes:function(){
		let mb=document.getElementById("modalBox");

		mb.querySelector(".boxbody").querySelector("form").style.display="none";

		mb.querySelector(".boxbody").form({
			action:'#',
			method:'post',
			id:'recentnotesform',
			autosubmit:false
		});
		let recentNotesForm=document.getElementById("recentnotesform");
		recentNotesForm.innerHTML='';
		let lbl=document.createElement("label");
		lbl.style.cursor="pointer";
		lbl.style.textAlign="center";
		lbl.style.fontWeight="bold";
		lbl.style.padding="1em";
		lbl.innerHTML='Click the note below to use it, or click here to cancel.';
		lbl.addEventListener("click", Fishing.cancelRecentNotes);
		recentNotesForm.appendChild(lbl);
		let notes=Fishing.recentNotes.slice(-1*Fishing.recentNotesToOffer).reverse();
		notes.forEach(function(n){
			lbl=document.createElement("label");
			lbl.style.cursor="pointer"
			lbl.style.textAlign="left";
			lbl.style.padding="0.5em";
			lbl.addEventListener("click", function(evt){
				let elem=evt.target;
				if(elem.closest("label")){ elem=elem.closest("label"); }
				Fishing.selectRecentNote(elem);
				return false;
			});
			lbl.innerHTML=n+"";
			document.getElementById("recentnotesform").appendChild(lbl);
		});
		return false;
	},

	cancelRecentNotes:function(){
		document.getElementById("fishingnote").closest("form").style.display="block";
		document.getElementById("recentnotesform").remove();
		return false;
	},
	
	selectRecentNote:function(elem){
		let noteText=elem.innerHTML.split("<br>").join("\n");
		let d=document.createElement("div");
	    d.innerHTML=noteText;
	    noteText=''+d.innerText;
		document.getElementById("fishingnote").value=noteText;
		Fishing.cancelRecentNotes();
		return false;
	},
	
	doNoteCountdown:function(){
		let saveDelaySpan=document.getElementById("autonotesavedelay");
		if(!document.getElementById("autonotesavetext")){ return false; }
		let secondsLeft=parseInt(saveDelaySpan.innerHTML);
		if(0>=secondsLeft){
			ui.closeModalBox();
		} else {
			saveDelaySpan.innerHTML=(secondsLeft-1)+"";
			window.setTimeout(Fishing.doNoteCountdown,1000);
		}
	},
	
	cancelAutoNoteSave:function(){
		let saveText=document.getElementById("autonotesavetext");
		if(saveText){
			saveText.closest("label").querySelector(".helptext").remove();
			saveText.remove();
		}
	},

	quickNotes:[
		{
			"label":"Good crystal",
			"note":"Good crystal"
		},
		{
			"label":"Cracked",
			"note":"Crystal cracked during fishing"
		},
	],

	showNotesBox:function(){
		Fishing.showingNotes=true;
		ui.modalBox({
			title:'Fishing complete',
			content:'',
			onclose:Fishing.hideNotesBox
		});
		let mb=document.getElementById("modalBox");
		let modalContent=document.getElementById("modalContent");
		let pinsBox=document.getElementById("pinsbox");
		modalContent.style.top="0";
		modalContent.style.left="0";
		modalContent.style.right="0";
		modalContent.style.bottom="0";
		modalContent.style.height="100%";
		modalContent.style.width="100%";
		mb.style.left=ui.cumulativeOffset(pinsBox).left+"px";
		mb.style.top=ui.cumulativeOffset(pinsBox).top+"px";
		let frm=mb.form({
			action:'#',
			method:'post',
			autosubmit:false
		});
		frm.onsubmit=function(){ ui.closeModalBox(); return false; };

		frm.innerHTML+='<label id="fish_quicknotes" style="text-align: center">Quick notes</label>';
		Note.writeQuickNoteButtons(document.getElementById("fish_quicknotes"), "fishingnote", Fishing.quickNotes, "width:90%; padding:0.25em; cursor:pointer; margin:.5em 0");

		let recentsButton='';
		if(0<Fishing.recentNotes.length){
			recentsButton=' <input type="button" onclick="Fishing.showRecentNotes();return false;" value="Recent notes..." />';
		}
		let ta=frm.textArea({
			label:'Crystal treatment'+recentsButton,
			name:'fishingnote',
			value:'',
			helpText:'Describe the cryoprotection, soaking, etc. This text is not sent to the synchrotron.'
		});

		ta.style.textAlign="center";
		ta.querySelector("textarea").style.width="95%";
		ta.querySelector("textarea").style.height="6em";
		ta.querySelector("textarea").addEventListener("focus", Fishing.cancelAutoNoteSave);
		ta.addEventListener("click", Fishing.cancelAutoNoteSave);

		let sub=frm.formField({
			label:'<span id="autonotesavetext">This box will close automatically in <span id="autonotesavedelay">'+(1+Fishing.autoNoteSaveDelay)+'...</span>',
			content:'<input type="submit" id="savenotes" name="savenotes" value="Save notes" onclick="Fishing.saveClicked=true" />',
		});
		ui.addSuppliedHelpText(sub, 'Editing anything in this box cancels the timer.');
		Fishing.saveClicked=false;

		window.setTimeout(function(){
			frm.querySelectorAll("button,input,textarea,select").forEach(function(inp){
				inp.addEventListener("click", Fishing.cancelAutoNoteSave);
				inp.addEventListener("focus", Fishing.cancelAutoNoteSave);
			});
			Fishing.doNoteCountdown();
		},10);
	},
	
	/**
	 * Callback for ui.closeModalBox.
	 */
	hideNotesBox:function(){
		//Force contents refresh from server, to ensure diffractionrequest updates are written to container elements
		if(Fishing.currentFish){
			if(Fishing.currentFish.puck && document.getElementById("container"+Fishing.currentFish.puck.id)){
				Fishing.renderContainer(Fishing.currentFish.puck, true);
			}
			if(Fishing.currentFish.pin && document.getElementById("container"+Fishing.currentFish.pin.id)){
				Fishing.renderContainer(Fishing.currentFish.pin, true);
			}
		}
		//No notes? Nothing to do. Bail.
		let noteText=document.getElementById("fishingnote").value.trim();
		if(""===noteText){
			Fishing.showingNotes=false;
			return true;
		}
		//Save button clicked? Save the note.
		if(Fishing.saveClicked){
			document.getElementById("savenotes").closest("label").classList.add("updating");
			Fishing.addRecentNote(noteText);
			Fishing.saveNoteOnLastFishedCrystal(noteText);
			Fishing.showingNotes=false;
			return true;
		}
		//Modal box closed? Confirm abandon note.
		if(!confirm("Your notes have not been saved.\n\nClose without saving?")){
			return false; //leave the box open
		}
		Fishing.showingNotes=false;
		return true;
	},

	/***************************************************************************
	 * 
	 * ONWARD SHIPPING
	 * 
	 ***************************************************************************/

	/**
	 * Shows a "Ship this|these dewar(s)" link if dewars are present and user can ship.
	 */
	showShipDewarButton:function(){
		let shipDewars=document.getElementById("fish_shipdewars");
		let dewarsBox=document.getElementById("dewarsbox");
		let dewars=document.querySelectorAll("#dewarsbox .fish_dewar");
		if(shipDewars){
			shipDewars.remove();
		}
		if(!dewars || 0===dewars.length || !canShip){
			return;
		}
		let sd=document.createElement("label");
		sd.id="fish_shipdewars";
		let a=document.createElement("a");
		a.innerHTML="Ship this dewar...";
		sd.appendChild(a);
		dewarsBox.appendChild(sd);
		shipDewars=sd;
		if(1!==dewars.length){
			shipDewars.querySelector("a").innerHTML="Ship these dewars...";
		}
		dewarsBox.appendChild(shipDewars);
		shipDewars.querySelector("a").onclick=Fishing.shipDewars;
	},

	
	/**
	 * Passes the names of the dewars to the shipment create UI.
	 */
	shipDewars:function(){
		if(!confirm("Leave this page and go to the shipping page?")){ return false; }
		let dewars=document.querySelectorAll(".fish_dewar");
		let dewarNames=[];
		dewars.forEach(function(d){
			dewarNames.push(encodeURIComponent(d.container.name));
		});
		document.location.href="/shipment/create?dewars="+dewarNames.join(",");
	},

	
};