/**
 * This file contains functions specific to the drop viewer ( /imagingsession/NNN ).
 */

/**********************************************************************************************************
 * KEYBOARD SHORTCUTS HANDLER
 * Note that the actual key mappings are defined in dropviewer_keymappings.js
 **********************************************************************************************************/

let KeyboardShortcuts={

		ctrlPressed:false,
		detectCtrlKey:function(evt){
			if((evt.keyCode && evt.keyCode===17) || (evt.key && "Control"===evt.key)){
				if(evt.type==="keydown"){
					KeyboardShortcuts.ctrlPressed=true;
				} else if(evt.type==="keyup"){
					KeyboardShortcuts.ctrlPressed=false;
				}
			}
		},
		
		shiftPressed:false,
		detectShiftKey:function(evt){
			if((evt.keyCode && evt.keyCode===16) || (evt.key && "Shift"===evt.key)){
				if(evt.type==="keydown"){
					KeyboardShortcuts.shiftPressed=true;
				} else if(evt.type==="keyup"){
					KeyboardShortcuts.shiftPressed=false;
				}
			}
		},
		
		keyMapping:new Map([]),
		
		init:function(){
			let mapping=UserConfig.get("dropviewer_keyboardMapping","British");
			KeyboardShortcuts.keyMapping=KeyboardShortcuts.keyMappings[mapping];
			window.addEventListener("keydown", KeyboardShortcuts.handleKeyDown);
			window.addEventListener("keyup", KeyboardShortcuts.handleKeyUp);
			window.addEventListener("keydown", KeyboardShortcuts.detectCtrlKey);
			window.addEventListener("keyup", KeyboardShortcuts.detectCtrlKey);
			window.addEventListener("keydown", KeyboardShortcuts.detectShiftKey);
			window.addEventListener("keyup", KeyboardShortcuts.detectShiftKey);
		},

		handleKeyUp:function(evt){
			KeyboardShortcuts.handleKey(evt,false);
		},
		handleKeyDown:function(evt) {
			KeyboardShortcuts.handleKey(evt,true);
		},
		handleKey:function(evt, isDown){
			if(document.activeElement){
				let type=document.activeElement.tagName.toLowerCase();
				if("input"===type||"textarea"===type||"select"===type){ return false;}
			}
			let keyPressed;
			let keyHandler;
			if(evt.key){
				keyPressed=evt.key;
			}
			keyHandler=KeyboardShortcuts.keyMapping[keyPressed];

			if(isDown) {
				if(keyHandler){
					keyHandler(keyPressed);
				}
			} else {
				//call the mirroring function - swap out images swapped in on keydown, for example
				if (keyHandler === DropViewer.swapInTimeCourseFirstImage) {
					DropViewer.swapOutTimeCourseFirstImage();
				} else if (keyHandler === DropViewer.swapInTimeCourseLastImage) {
					DropViewer.swapOutTimeCourseLastImage();
				}
			}
		},

		movieFirst:function(){
			if(DropNav.isOpen){
				DropNav.goToFirst();
			} else if(currentPanel.goToFirst){
				currentPanel.goToFirst();
			} else {
				DropViewer.goToFirst();
			}
		},
		moviePrevious:function(){
			if(DropNav.isOpen){
				DropNav.goToPrevious();
			} else if(currentPanel.goToPrevious){
				currentPanel.goToPrevious();
			} else {
				DropViewer.goToPrevious(true);
			}
		},
		movieNext:function(){
			if(DropNav.isOpen){
				DropNav.goToNext();
			} else if(currentPanel.goToNext){
				currentPanel.goToNext();
			} else {
				DropViewer.goToNext(true);
			}
		},
		movieLast:function(){
			if(DropNav.isOpen){
				DropNav.goToLast();
			} else if(currentPanel.goToLast){
				currentPanel.goToLast();
			} else {
				DropViewer.goToLast();
			}
		},
		moviePlayStop:function(){
			if(DropNav.isOpen){
				return false;
			} else if(currentPanel.playOrStop){
				currentPanel.playOrStop();
			} else {
				DropViewer.playOrStop();
			}
		},

		upOneRow:function(){
			if(DropNav.isOpen){
				DropNav.goUpOneRow();
			} else if(currentPanel.goUpOneRow){
				currentPanel.goUpOneRow();
			} else {
				console.log("No goUpOneRow in "+currentPanel.name)
			}
		},
		downOneRow:function(){
			if(DropNav.isOpen){
				DropNav.goDownOneRow();
			} else if(currentPanel.goDownOneRow){
				currentPanel.goDownOneRow();
			} else {
				console.log("No goUpOneRow in "+currentPanel.name)
			}
		},
		
		toggleTimeCourse:function(){
			if(currentPanel===TimeCourse){
				switchPanels(document.getElementById("viewbutton_dv"));
			} else {
				switchPanels(document.getElementById("viewbutton_tc"));
			}
		},
		
		olderImage:function(){
			if(currentPanel.olderImage){
				currentPanel.olderImage();
			}
		},
		
		newerImage:function(){
			if(currentPanel.newerImage){
				currentPanel.newerImage();
			}
		},
		
};



/**********************************************************************************************************
 * DROP VIEWER
 **********************************************************************************************************/

window.DropViewer={

	//array is zero-based, plate row numbers are 1-based, so empty item first.
	rowLabels:['','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T'],

	movieDelay:1.0, //seconds
	movieInterval:null,
	plateType:null,
	screen:null,
	currentIndex:0,
	maxIndex:0,
	images:[],

	warningImage:'<img alt="" style="height:1.5em;vertical-align:top;position:relative;top:-0.1em;margin-right:0.25em" src="/images/icons/'+skin["bodyIconTheme"]+'icons/warning.png" />',

	init:function(){
		document.title="Drop viewer: Plate "+data['platename'];
		if(parseInt(data["isarchived"])){
			let controls=document.getElementById("controls");
			ui.archivedProjectMessageBar(controls);
			controls.querySelectorAll(".controlpanel").forEach(function(panel){
				panel.style.marginTop="5em"
			});
		}
		setHandedness(UserConfig.get("dropviewer_handedness","right"));
		DropViewer.movieDelay=UserConfig.get("dropviewer_movieDelay", DropViewer.movieDelay);
		DropViewer.getScoringSystem();
		DropViewer.getPlateType();
		DropViewer.getScreen();
		let uri='/api/imagingsession/'+data['id']+'/dropimage?all=1';
		/* TODO if sort by score...
		uri+="&sortby=latestcrystalprobabilitypercent&sortdescending=true";
		 */
		new AjaxUtils.Request(uri,{
			method:'get',
			onSuccess:DropViewer.init_onSuccess,
			onFailure:DropViewer.init_onFailure
		});
		window.addEventListener("resize", DropViewer.fitImageToPane);
	},

	init_onSuccess:function(transport){
		let images=transport.responseJSON.rows;
		let img=document.getElementById("currentimage");
		img.src=images[0]["fullimageurl"];
		img.dataset.micronshigh=images[0]["micronsperpixely"]*images[0]["pixelheight"]+"";
		DropViewer.images=images;
		DropViewer.maxIndex=images.length-1;
		//Determine starting state from hash - drop and, if present, crystal. Example: #A03.2 #H7.2c4
		//Otherwise from user config, defaulting to measure/score
		let startImage=0;
		let startCrystal=-1;
		if(document.location.hash){
			let dropLabel=document.location.hash.substring(1);
			let startRow=DropViewer.rowLabels.indexOf(dropLabel.substr(0,1));
			let parts=dropLabel.substring(1).split(".");
			let startColumn=parseInt(parts[0]);
			let startDrop=parts[1];
			if(-1!==startDrop.indexOf("c")){
				let subParts=startDrop.split("c");
				startDrop=parseInt(subParts[0]);
				startCrystal=parseInt(subParts[1]);
				window.startMode="CrystalSelection";
			}
			let numImages=DropViewer.images.length;
			for(let i=0;i<numImages;i++){
				let img=DropViewer.images[i];
				let imgRow=parseInt(img.row);
				let imgCol=parseInt(img.col);
				let imgDrop=parseInt(img.dropnumber);
				if(startRow===imgRow && startColumn===imgCol && parseInt(startDrop)===imgDrop){
					startImage=i;
					break;
				}
			}
		}

		DropViewer.getCurrentImageTimeCourse();
		window.setTimeout(DropViewer.highlightCurrentImageScore,250);
		DropViewer.setCurrentImage(startImage);

		if(startCrystal>0){
			CrystalSelection.currentCrystalNumber=startCrystal;
			window.setTimeout(CrystalSelection.switchTo, 250);
		}
		window.setTimeout(Measure.setScale,250);

		DropViewer.waitForCachedMovieImages();
		DropViewer.preCacheMovieImages();
	},

	init_onFailure:function(){
		alert("Could not get images.");
	},

	waitForCachedMovieImages:function (){
		if(!document.getElementById("loadingpanel").classList.contains("preCacheDone")){
			window.setTimeout(DropViewer.waitForCachedMovieImages, 25);
			return;
		}
		let startMode=UserConfig.get("dropviewer_startMode","DropViewer");
		currentPanel=startMode;
		window.setTimeout(function(){
			if(window[startMode]){
				window[startMode].switchTo();
			} else {
				DropViewer.switchTo();
				UserConfig.set("dropviewer_startMode","DropViewer");
			}
		},25);
	},

	getScreen:function(){
		if(""===data["screenid"] || null==data["screenid"] || 0===data["screenid"]){ return false; }
		new AjaxUtils.Request('/api/screen/'+data["screenid"],{
			method:'get',
			onSuccess:DropViewer.getScreen_onSuccess,
			onFailure:DropViewer.getScreen_onFailure
		});
	},
	getScreen_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		window.setTimeout(function(){
			DropViewer.screen=transport.responseJSON;
			new AjaxUtils.Request('/api/screen/'+data["screenid"]+'/screencondition?all=1&sortby=name',{
				method:'get',
				onSuccess:function(transport){ 
					AjaxUtils.checkResponse(transport);
					DropViewer.screen.conditions=transport.responseJSON.rows;
				},
				onFailure:function(){
					/* do nothing, assume screen not set */
				}
			});
		},50);
	},
	getScreen_onFailure:function(){
		/* do nothing, assume screen not set */
	},

	getPlateType:function(){
		new AjaxUtils.Request('/api/platetype/'+data["platetypeid"],{
			method:'get',
			onSuccess:DropViewer.getPlateType_onSuccess,
			onFailure:DropViewer.getPlateType_onFailure
		});
	},
	getPlateType_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		window.setTimeout(function(){
			DropViewer.plateType=transport.responseJSON;
		},50);
	},
	getPlateType_onFailure:function(){
		alert("Could not retrieve plate type information");
	},
	
	
	getScoringSystem:function(){
		new AjaxUtils.Request('/api/crystalscoringsystem/'+data["crystalscoringsystemid"]+'/score?sortby=scoreindex',{
			method:'get',
			onSuccess:DropViewer.getScoringSystem_onSuccess,
			onFailure:DropViewer.getScoringSystem_onFailure
		});
	},
	getScoringSystem_onSuccess:function(transport){
		DropViewer.scores=transport.responseJSON.rows;
		AjaxUtils.checkResponse(transport);
		if(50>document.getElementById("dv_scores").offsetHeight){
			window.setTimeout(function(){DropViewer.getScoringSystem_onSuccess(transport)},50);
			return;
		}
		DropViewer.renderScoringSystem(transport);
	},
	
	/* If the score buttons would be below this height, render them double-height half-width.
	 * Intent: Make them more touchable on small tablets.
	 */
	scoreHeightCutoff:50,
	
	renderScoringSystem:function(transport){
		let scores=transport.responseJSON.rows;
		DropViewer.scores=scores;
		DropViewer.scoreIndicesByScoreId={};
		let total=transport.responseJSON.rows.length;
		let scoresDiv=document.getElementById("dv_scores");
		let scoreHeight=Math.floor(  scoresDiv.offsetHeight / (1+Math.round(total)) );
		let renderHalfWidth=false;
		if(scoreHeight<DropViewer.scoreHeightCutoff){
			renderHalfWidth=true;
		}
		scoresDiv.innerHTML="";
		scores.forEach(function(s){
			DropViewer.scoreIndicesByScoreId['s'+s.id]=1*s.scoreindex;
			let sc=document.createElement("div");
			sc.id="score"+s.id;
			sc.dataset.scoreid=s.id;
			sc.dataset.scoreindex=s.scoreindex;
			sc.dataset.hotkey=s.hotkey;
			sc.className="dv_score";
			sc.style.height=(scoreHeight-4)+"px";
			sc.style.backgroundColor="black";
			sc.style.borderColor="#"+s.color;
			sc.style.fontWeight="bold";
			sc.style.paddingLeft="1em";
			sc.style.textShadow="1px 1px 1px black";
			sc.innerHTML='['+s.hotkey+']&nbsp; '+s.label;
			scoresDiv.appendChild(sc);
			sc.style.borderColor="#"+s.color;
			if(renderHalfWidth){
				sc.style.lineHeight="auto";
				sc.style.height=(2*sc.offsetHeight-10)+"px";
				sc.style.width="40%";
				sc.style.cssFloat="left";
			} else {
				sc.style.lineHeight=(sc.offsetHeight-4)+"px";
				sc.style.height=(sc.offsetHeight-4)+"px";
			}
			if(canUpdate){
				sc.style.cursor="pointer";
				sc.onclick=function(){ DropViewer.setScore(sc); }
			}
		});
		if(canUpdate){
			let lbl=document.createElement("label");
			let cb=document.createElement("input");
			cb.id="dv_advanceonscore";
			cb.type="checkbox";
			lbl.appendChild(cb);
			lbl.innerHTML+=" Go to next drop after scoring";
			lbl.style.borderRadius="0";
			lbl.style.clear="both";
			scoresDiv.appendChild(lbl);
			lbl.style.lineHeight=lbl.previousElementSibling.offsetHeight+"px";
			lbl.style.textAlign="center";
			lbl.style.fontWeight="bold";
			window.setTimeout(function(){
				if(parseInt(UserConfig.get("dropviewer_advanceOnScore","1"))===1){
					document.getElementById("dv_advanceonscore").checked="checked";
				}
				document.getElementById("dv_advanceonscore").addEventListener("click",DropViewer.setAdvanceOnScore);
			},50);
		}
	},
	getScoringSystem_onFailure:function(transport){
		AjaxUtils.checkResponse(transport);
	},

	setAdvanceOnScore:function(){
		if(document.getElementById("dv_advanceonscore").checked){
			UserConfig.set("dropviewer_advanceOnScore",1);
		} else {
			UserConfig.set("dropviewer_advanceOnScore",0);
		}
	},

	setScoreByHotkey:function(hotkey){
		if(DropInfo===currentPanel){
			return DropInfo.setScoreByHotkey(hotkey);
		}
		document.querySelectorAll("#dv_scores .dv_score").forEach(function(s){
			if(s.dataset.hotkey===hotkey){
				DropViewer.setScore(s);
			}
		});
	},

	setScore:function(scoreElem){
		if(scoreElem.classList.contains("updating")){ return false; }
		if(!canUpdate){ 
			alert("You don't have permission to update this plate. Talk to your administrator.");
			return false; 
		}

		let imageToScore=DropViewer.images[DropViewer.currentIndex];
		let imageId=imageToScore.id;
		let scoreId;

		if(scoreElem.classList.contains("dv_currentscore")){
			//unscore the drop altogether
			if(!confirm("Remove the score for this image?")){
				return false;
			}			
			scoreId=0;
			scoreElem.classList.remove("dv_currentscore");
			imageToScore.latestcrystalscoreid="";
		} else {
			//set the new score
			scoreElem.style.backgroundColor=scoreElem.style["borderColorLeft"];
			scoreElem.classList.add("dv_currentscore");
			imageToScore.latestcrystalscoreid=scoreElem.dataset.scoreid;
			scoreId=scoreElem.dataset.scoreid;
		}
		//highlight it, unhighlight the rest
		scoreElem.classList.add("updating");
		new AjaxUtils.Request('/api/dropimage/'+imageId, {
			method:'patch',
			parameters:{
				csrfToken:csrfToken,
				latestcrystalscoreid:scoreId,
				humanscorerid:userId
			},
			onSuccess:DropViewer.setScore_onSuccess,
			onFailure:DropViewer.setScore_onFailure,
		});

		let aos=document.getElementById("dv_advanceonscore");
		if(aos && aos.checked){
			window.setTimeout(function(){ DropViewer.goToNext(true)},200);
		}
	},
	setScore_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		DropViewer.highlightCurrentImageScore();
	},
	setScore_onFailure:function(transport){
		if(transport.responseJSON && transport.responseJSON.error  && transport.responseJSON.error.indexOf("Duplicate entry") >-1){ 
			//Scored the same drop twice with same score in same second - just eat it. 
			return false; 
		}
		AjaxUtils.checkResponse(transport);
		DropViewer.highlightCurrentImageScore();
	},

	setMovieDelay:function(slider){
		DropViewer.changeMovieDelay(slider, 0);
	},
	increaseMovieDelay:function(btn){
		DropViewer.changeMovieDelay(btn, 1);
	},
	decreaseMovieDelay:function(btn){
		DropViewer.changeMovieDelay(btn, -1);
	},
	changeMovieDelay:function(elem, direction){
		let slider=elem.closest("div").querySelector("input");
		let md=parseFloat(slider.value)+(direction*slider.step);
		md=md.toFixed(1);
		UserConfig.set("dropviewer_movieDelay",md);
		document.getElementById("dv_delay").value=md;
		document.getElementById("tc_delay").value=md;
		DropViewer.movieDelay=md;
		if(DropViewer.movieInterval){
			DropViewer.stopMovie();
			DropViewer.startMovie();
		}
	},
	
	switchTo:function(){
		setCurrentModeButton(document.getElementById("viewbutton_dv"));
		if(DropViewer===currentPanel){ return false; }
		currentPanel=DropViewer;
		DropViewer.setCurrentImage(DropViewer.currentIndex);
		document.querySelectorAll(".controlpanel").forEach(function(p){
			p.style.display="none";
		});
		document.getElementById("dropviewerpanel").style.display="block";
		DropViewer.highlightCurrentImageScore();
		return true;
	},

	highlightCurrentImageScore:function(){
		let currentImageScore=DropViewer.images[DropViewer.currentIndex]['latestcrystalscoreid'];
		let scores=document.querySelectorAll("#dv_scores .dv_score");
		if(!scores.length){
			window.setTimeout(DropViewer.highlightCurrentImageScore,100);
			return false;
		}
		scores.forEach(function(s){
			s.classList.remove("updating");
			if(1*s.dataset.scoreid===1*currentImageScore){
				s.style.backgroundColor=s.style.borderColor;
				s.classList.add("dv_currentscore");
			} else {
				s.style.backgroundColor="black";
				s.classList.remove("dv_currentscore");
			}
		});
	},
	
	switchFrom:function(){
		DropViewer.stopMovie();
	},
	
	goToFirst:function(){
		DropViewer.stopMovie();
		DropViewer.setCurrentImage(0);
	},
	goToPrevious:function(byDrop){
		DropViewer.stopMovie();
		if(byDrop || KeyboardShortcuts.ctrlPressed){
			DropViewer.previousImage();
		} else if(KeyboardShortcuts.shiftPressed){
			DropViewer.previousImageAboveScoreThreshold();
		} else {
			DropViewer.goLeftOneColumn();
		}
	},
	goToNext:function(byDrop){
		DropViewer.stopMovie();
		if(byDrop || KeyboardShortcuts.ctrlPressed){
			DropViewer.nextImage();
		} else if(KeyboardShortcuts.shiftPressed){
			DropViewer.nextImageAboveScoreThreshold();
		} else {
			DropViewer.goRightOneColumn();
		}
	},
	goToLast:function(){
		DropViewer.stopMovie();
		DropViewer.setCurrentImage(DropViewer.images.length-1);
	},

	goLeftOneColumn:function(){
		let current=DropViewer.getCurrentImage();
		let newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row,1*current.col-1,1*current.dropnumber);
		if(-1!==newIndex){
			DropViewer.setCurrentImage(newIndex);
			return;
		}
		for(let c=DropViewer.plateType.cols;c>=1;c--){
			newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row-1,c,1*current.dropnumber);
			if(-1!==newIndex){
				DropViewer.setCurrentImage(newIndex); 
				return;
			}
		}
		
	},
	goRightOneColumn:function(){
		let current=DropViewer.getCurrentImage();
		let newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row,1*current.col+1,1*current.dropnumber);
		if(-1!==newIndex){
			DropViewer.setCurrentImage(newIndex); 
			return;
		}
		for(let c=1;c<=DropViewer.plateType.cols;c++){
			newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row+1,c,1*current.dropnumber);
			if(-1!==newIndex){
				DropViewer.setCurrentImage(newIndex); 
				return;
			}
		}
	},
	goUpOneRow:function(){
		let current=DropViewer.getCurrentImage();
		let newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row-1,1*current.col,1*current.dropnumber);
		if(-1!==newIndex){ DropViewer.setCurrentImage(newIndex); }
	},
	goDownOneRow:function(){
		let current=DropViewer.getCurrentImage();
		let newIndex=DropViewer.getImageIndexByRowColumnDrop(1*current.row+1,1*current.col,1*current.dropnumber);
		if(-1!==newIndex){ DropViewer.setCurrentImage(newIndex); }
	},
	
	getImageIndexByRowColumnDrop:function(rowNum,colNum,dropNum){
		let numImages=DropViewer.images.length;
		for(let i=0;i<numImages;i++){
			let img=DropViewer.images[i];
			if(1*img.row===1*rowNum && 1*img.col===1*colNum && 1*img.dropnumber===1*dropNum){
				return i;
			}
		}
		return -1;
	},
	
	playOrStop:function(){
		if(DropViewer.movieInterval){
			DropViewer.stopMovie();
		} else {
			DropViewer.startMovie();
		}
	},
	startMovie:function(){
		if(DropViewer.currentIndex>=DropViewer.maxIndex){
			DropViewer.setCurrentImage(0);
		}
		DropViewer.movieInterval=window.setInterval(DropViewer.nextImage,DropViewer.movieDelay*1000);
		let ps=document.getElementById("dv_playstop");
		ps.title="Stop";
		ps.classList.add("playing");
		document.getElementById("dv_timecourseends").style.display="none";
	},
	stopMovie:function(){
		window.clearInterval(DropViewer.movieInterval);
		DropViewer.movieInterval=null;
		let ps=document.getElementById("dv_playstop");
		ps.title="Play";
		ps.classList.remove("playing");
		DropViewer.getCurrentImageTimeCourse();
	},
	nextImage:function(){
		DropViewer.currentIndex++;
		if(!DropViewer.setCurrentImage(DropViewer.currentIndex)){
			DropViewer.currentIndex--;
			DropViewer.stopMovie();
		} else {
			DropViewer.preCacheMovieImages();
		}
	},
	previousImage:function(){
		DropViewer.currentIndex--;
		if(!DropViewer.setCurrentImage(DropViewer.currentIndex)){
			DropViewer.currentIndex++;
			DropViewer.stopMovie();
		}
	},
	nextImageAboveScoreThreshold:function(){
		DropViewer.nextOrPreviousAboveScoreThreshold(1);
	},
	previousImageAboveScoreThreshold:function(){
		DropViewer.nextOrPreviousAboveScoreThreshold(-1);
	},
	nextOrPreviousAboveScoreThreshold:function(direction){
		let scoringSystemId=DropViewer.scores[0]["crystalscoringsystemid"];
		let defaultThreshold=1*(DropViewer.scores[DropViewer.scores.length-1]["scoreIndex"]);
		let indexThreshold=1*(UserConfig.get("dropviewer_scorethreshold_ss"+scoringSystemId, defaultThreshold));
		let newIndex=-1;
		for(let i=DropViewer.currentIndex+direction;i<=DropViewer.maxIndex&&i>=0;i+=direction){
			let latestScoreId=DropViewer.images[i].latestcrystalscoreid;
			if(DropViewer.scoreIndicesByScoreId["s"+latestScoreId] >= indexThreshold){
				newIndex=i;
				break;
			}
		}
		DropViewer.setCurrentImage(newIndex);
	},

	preCacheMovieImages:function(){
		let cache=document.getElementById("imagecache");
		let panel=document.getElementById("loadingpanel");
		panel.style.textAlign="center";
		panel.style.color="#eee";
		panel.style.display="block";
		if(!DropViewer.images || !DropViewer.images.length){
			window.setTimeout(DropViewer.preCacheMovieImages, 50);
			return;
		}
		window.setTimeout(function(){
			DropViewer.images.forEach(function(img){
				if(img && !cache.querySelector("#cache"+img.id)){
					let i=document.createElement("img");
					i.className="notLoaded";
					i.id="cache"+img.id;
					i.src=img["thumbnailurl"];
					i.style.visibility="hidden";
					cache.appendChild(i);
				}
			});
		}, 10);
		window.setInterval(DropViewer.checkPreCachingStatus,50);
	},

	initOnPreCachePercent:80,
	checkPreCachingStatus:function(){
		let numImages=DropViewer.images.length;
		DropViewer.images.forEach(function(img){
			let elem=document.getElementById("cache"+img.id);
			if(0!==elem.clientWidth){
				elem.className="loaded";
			}
		});
		let loaded=document.getElementById("imagecache").querySelectorAll(".loaded");
		let percent=100*loaded.length/numImages;
		document.getElementById("cachebar").style.width=percent+"%";
		if(percent>=DropViewer.initOnPreCachePercent){
			window.clearInterval(DropViewer.checkPreCachingStatus);
			document.getElementById("loadingpanel").classList.add("preCacheDone");
		}
	},

	getCurrentImage:function(){
		return DropViewer.images[DropViewer.currentIndex];
	},
	
	setCurrentImage:function(index){
		index*=1;
		if(index>DropViewer.maxIndex){ return false; }
		if(index<0){ return false; }
		let img=DropViewer.images[index];
		let imageElement=document.getElementById("currentimage");
		imageElement.src=img["thumbnailurl"];
		window.setTimeout(function(){
			imageElement.src = img["fullimageurl"];
		},100);
		imageElement.dataset.micronshigh=img["pixelheight"]*img["micronsperpixely"]+"";
		DropViewer.fitImageToPane(img);
		Measure.setScale();
		DropViewer.setDropNameInTitle(img);
		DropViewer.currentIndex=index;
		DropViewer.highlightCurrentImageScore();
		DropViewer.getCurrentImageTimeCourse();
		DropInfo.refresh();
		return true;
	},

	fitImageToPane:function(){
		let img=DropViewer.images[DropViewer.currentIndex];
		let imageElement=document.getElementById("currentimage");
		let imagePane=document.getElementById("imagepane");
		if(img["pixelwidth"]/imagePane.offsetWidth > img["pixelheight"]/imagePane.offsetHeight){
			imageElement.style.width="100%";
			imageElement.style.height="auto";
		} else {
			imageElement.style.width="auto";
			imageElement.style.height="100%";
		}
		Measure.setScale();
		if(CrystalSelection===currentPanel){
			CrystalSelection.redrawCrosshairs();
		}
	},
	
	getWellNameForImage:function(img){
		let col=img.col*1;
		if(col<10){ col="0"+col; }
		return DropViewer.rowLabels[img.row] + col;
	},
	
	setDropNameInTitle:function(img){
		let dropName=DropViewer.getWellNameForImage(img)+"."+img.dropnumber;
		document.getElementById("dropname").innerHTML=dropName;
		document.location.hash=dropName;
	},
	
	/**
	 * Retrieves all the image records for the current drop, as JSON. 
	 * Success handler attaches them to the current drop and updates first/last image.
	 */
	getCurrentImageTimeCourse:function(){
		//use a short timeout to break the call chain, otherwise pre-nav image's time course is fetched
		window.setTimeout(DropViewer._doGetCurrentImageTimeCourse,25);
	},
	_doGetCurrentImageTimeCourse:function(){
		let index=DropViewer.currentIndex;
		let obj=DropViewer.images[index];
		new AjaxUtils.Request('/api/welldrop/'+obj.welldropid+'/timecourseimage?all=1',{
			method:'get',
			onSuccess:function(transport){ DropViewer.getCurrentImageTimeCourse_onSuccess(transport, DropViewer.currentIndex); },
			onFailure:function(){ DropViewer.getCurrentImageTimeCourse_onFailure(); },
		});
	},
	getCurrentImageTimeCourse_onSuccess:function(transport, imageIndex){
		let tc=transport.responseJSON.rows;
		DropViewer.images[imageIndex].timeCourseImages=tc;
		let first=document.getElementById("dv_timecoursefirst");
		let last=document.getElementById("dv_timecourselast");
		first.src=tc[0]["thumbnailurl"];
		first.dataset.fullsrc=tc[0]["fullimageurl"];
		first.onmouseover=DropViewer.swapInTimeCourseFirstImage;
		first.onmouseout=DropViewer.swapOutTimeCourseFirstImage;
		last.src=tc[tc.length - 1]["thumbnailurl"];
		last.dataset.fullsrc=tc[tc.length - 1]["fullimageurl"];
		last.onmouseover=DropViewer.swapInTimeCourseLastImage;
		last.onmouseout=DropViewer.swapOutTimeCourseLastImage;
		document.getElementById("dv_timecourseends").style.display="block";
	},
	getCurrentImageTimeCourse_onFailure:function(){
	},

	swapInTimeCourseFirstImage: function(){ DropViewer.swapInTimeCourseEndImage(document.getElementById("dv_timecoursefirst"));  },
	swapOutTimeCourseFirstImage:function(){ DropViewer.swapOutTimeCourseEndImage(document.getElementById("dv_timecoursefirst")); },
	swapInTimeCourseLastImage:  function(){ DropViewer.swapInTimeCourseEndImage(document.getElementById("dv_timecourselast"));   },
	swapOutTimeCourseLastImage: function(){ DropViewer.swapOutTimeCourseEndImage(document.getElementById("dv_timecourselast"));  },
	
	swapInTimeCourseEndImage:function(img){
		if(DropViewer!==currentPanel){ return false; }
		if(""!==img.dataset.oldsrc && undefined!==img.dataset.oldsrc){ return false; /* because this is a repeat onkeydown while the key is held */}
		img.dataset.oldsrc=document.getElementById("currentimage").src;
		document.getElementById("currentimage").src=img.dataset.fullsrc;
	},
	swapOutTimeCourseEndImage:function(img){
		if(DropViewer!==currentPanel){ return false; }
		document.getElementById("currentimage").src=img.dataset.oldsrc;
		img.dataset.oldsrc='';
	},
	
	olderImage:function(){
		DropViewer.olderOrNewerImage(-1);
	},
	newerImage:function(){
		DropViewer.olderOrNewerImage(1);
	},
	olderOrNewerImage:function(direction){
		if(DropViewer!==currentPanel){ return false; }
		let img=DropViewer.images[DropViewer.currentIndex];
		if(!img.timeCourseImages){ return false; }
		let timeCourseIndex=-1;
		let currentSrc=document.getElementById("currentimage").src;
		for(let i=0; i<img.timeCourseImages.length; i++){
			if(currentSrc.indexOf(img.timeCourseImages[i]["fullimageurl"])!==-1){
				timeCourseIndex=i;
				break;
			}
		}
		timeCourseIndex+=direction;
		if(timeCourseIndex>=0 && timeCourseIndex<img.timeCourseImages.length){
			document.getElementById("currentimage").src=img.timeCourseImages[timeCourseIndex]["fullimageurl"];
		}
	}
	
	
};




/**********************************************************************************************************
 * TIME COURSE 
 **********************************************************************************************************/

window.TimeCourse={

		//holds the src of the image showing before the time course was opened
		imageBefore:null,

		images:null,
		
		activeLightTypes:[],
		
		switchTo:function(){
			setCurrentModeButton(document.getElementById("viewbutton_tc"));
			TimeCourse.imageBefore=document.getElementById("currentimage").src.slice(0); //copy it
			if(TimeCourse===currentPanel){ return false; }
			document.querySelectorAll(".controlpanel").forEach(function(p){
				p.style.display="none";
			});
			document.getElementById("timecoursepanel").style.display="block";
			currentPanel=TimeCourse;
			let container=document.getElementById("tc_images");
			container.innerHTML="Waiting for images...";
			TimeCourse.writeTimeCourseImages();
			return true;
		},
		switchFrom:function(){
			document.getElementById("currentimage").src=TimeCourse.imageBefore;
			Measure.setScale();
		},

		writeTimeCourseImages:function(){
			let currentImage=DropViewer.getCurrentImage();
			while(!currentImage.timeCourseImages){
				//we wait for the DOM to catch up
			}
			let container=document.getElementById("tc_images");
			container.innerHTML="";
			TimeCourse.images=currentImage.timeCourseImages;
			if(TimeCourse.images.length===0){ return; }

			let regex=/[-:\s]/g;
			let parts=TimeCourse.images[0]["imageddatetime"].split(regex);
			let timeZero= new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], 0).getTime();
			timeZero=Math.floor(timeZero/1000);

			let scores=DropViewer.scores;
			let scoreIdsToScores=[];
			
			currentImage.timeCourseImages.forEach(function(img){
				let light=img["lighttype"];
				let d=document.createElement("div");
				d.onmouseover=function(){ TimeCourse.showTimePointImage(d); };
				d.onmouseout=function(){ TimeCourse.unshowTimePointImage(d); };
				d.onclick=function(){ TimeCourse.setCurrentImage(d, true); };
				d.image=img;
				d.className="timepoint tc_"+light;
				if(1*img.id===1*(DropViewer.images[DropViewer.currentIndex].id)){
					d.className+=" currenttimepoint";
				}
				let i=document.createElement("img");
				i.src=img["thumbnailurl"];

				if(""!==img.latestcrystalscoreid){
					let scoreId=img.latestcrystalscoreid;
					if(!scoreIdsToScores["s"+scoreId]){
						scores.forEach(function(s){
							scoreIdsToScores["s"+s.id]=s;
						});
					}
					i.style.borderColor="#"+scoreIdsToScores["s"+scoreId]["color"];
					i.title="Score: "+scoreIdsToScores["s"+scoreId]["label"];
				}
				d.appendChild(i);

				parts=img["imageddatetime"].split(regex);
				let offsetTime= new Date(parts[0], parts[1]-1, parts[2], parts[3], parts[4], 0).getTime();
				offsetTime=Math.floor(offsetTime/1000);
				let s=document.createElement("span");
				s.className="tc_dateoffset";
				s.innerText=ui.secondsToFriendlyUnits(offsetTime-timeZero);
				d.appendChild(s);
				let s2=document.createElement("span");
				s2.className="tc_actualdate";
				s2.innerText=ui.friendlyDate(img["imageddatetime"]);
				d.appendChild(s2);
				container.appendChild(d);
 				if(!document.getElementById("tc_"+light)){
					let btn=document.createElement("div");
					btn.className="tc_lighttype tc_lighttypeactive";
					btn.id="tc_"+light;
					btn.innerText=light;
					btn.dataset.light=light;
					btn.onclick=function(){ TimeCourse.toggleLightTypeImages(btn) };
					btn.style.backgroundImage='url(/images/icons/lighttypes/'+light+'.png)';
					document.getElementById("tc_lighttypes").appendChild(btn);
					TimeCourse.activeLightTypes.push(light);
				}
			});
		},

		toggleLightTypeImages:function(button){
			if(button.classList.contains("tc_lighttypeactive")){
				button.classList.remove("tc_lighttypeactive");
				document.querySelectorAll("#tc_images .tc_"+button.dataset.light).forEach(function(dv){
					//dv.style.display="none";
					dv.classList.add("suppressed");
				});
				TimeCourse.activeLightTypes=ui.arrayWithout(TimeCourse.activeLightTypes,button.dataset.light);
			} else {
				button.classList.add("tc_lighttypeactive");
				document.querySelectorAll("#tc_images .tc_"+button.dataset.light).forEach(function(dv){
					//dv.style.display="block";
					dv.classList.remove("suppressed");
				});
				TimeCourse.activeLightTypes.push(button.dataset.light);
			}
			let curr=document.getElementById("tc_images").querySelector(".currenttimepoint");
			if(curr && curr.classList.contains("suppressed")){
				if(!TimeCourse.goToPrevious()){
					TimeCourse.goToNext();
				}
			}
		},

		/**
		 * Mouseover/out image swap
		 */
		showTimePointImage:function(elem){
			if(!elem.image){ return false; }
			elem.previousImage=document.getElementById("currentimage").src;
			document.getElementById("currentimage").src=elem.image["fullimageurl"];
		},		
		unshowTimePointImage:function(elem){
			if(elem.classList.contains("currenttimepoint")){ return false; }
			if(!elem.previousImage){ return false; }
			document.getElementById("currentimage").src=elem.previousImage;
		},
		
		/**
		 * Movie/navigation controls
		 */
		goToFirst:function(){
			TimeCourse.stopMovie();
			let first=document.getElementById("tc_images").querySelector("div.timepoint:not(.suppressed)");
			if(!first){ return false; }
			TimeCourse.setCurrentImage(first);
		},
		goToPrevious:function(){
			TimeCourse.stopMovie();
			return TimeCourse.previousImage();
		},
		goToNext:function(){
			TimeCourse.stopMovie();
			return TimeCourse.nextImage();
		},
		goToLast:function(){
			let active=document.querySelectorAll("#tc_images div.timepoint:not(.suppressed)");
			if(!active || 0===active.length){ return false; }
			let last=active[active.length-1];
			TimeCourse.setCurrentImage(last);
		},
		playOrStop:function(){
			if(TimeCourse.movieInterval){
				TimeCourse.stopMovie();
			} else {
				TimeCourse.startMovie();
			}
		},
		startMovie:function(){
			let playStop=document.getElementById("tc_playstop");
			let current=document.getElementById("tc_images").querySelector(".currenttimepoint");
			let next=ui.nextElementSiblingMatchingSelector(current,"div.timepoint:not(.suppressed)");
			if(!next){
				TimeCourse.goToFirst();
			}
			TimeCourse.movieInterval=window.setInterval(TimeCourse.nextImage,DropViewer.movieDelay*1000);
			playStop.title="Stop";
			playStop.classList.add("playing");
		},
		stopMovie:function(){
			window.clearInterval(TimeCourse.movieInterval);
			TimeCourse.movieInterval=null;
			let playStop=document.getElementById("tc_playstop");
			playStop.title="Play";
			playStop.classList.remove("playing");
		},
		nextImage:function(){
			let current=document.getElementById("tc_images").querySelector(".currenttimepoint");
			let next=ui.nextElementSiblingMatchingSelector(current,"div.timepoint:not(.suppressed)");
			if(!next){
				TimeCourse.stopMovie();
				return false;
			}
			TimeCourse.setCurrentImage(next);
			return true;
		},
		previousImage:function(){
			let current=document.getElementById("tc_images").querySelector(".currenttimepoint");
			let prev=ui.previousElementSiblingMatchingSelector(current,"div.timepoint:not(.suppressed)");
			if(!prev){
				TimeCourse.stopMovie();
				return false;
			}
			TimeCourse.setCurrentImage(prev);
			return true;
		},
		olderImage:function(){
			TimeCourse.previousImage();
		},
		newerImage:function(){
			TimeCourse.nextImage();
		},
		
		getCurrentImage:function(){
			return TimeCourse.images[TimeCourse.currentIndex];
		},
		
		setCurrentImage:function(container, dontScroll){
			let tcImages=document.getElementById("tc_images");
			let currentImage=document.getElementById("currentimage");
			if(tcImages.querySelector(".currenttimepoint")){
				tcImages.querySelector(".currenttimepoint").classList.remove("currenttimepoint");
			}
			container.classList.add("currenttimepoint");
			currentImage.src=container.image["fullimageurl"];
			if(!dontScroll){
				tcImages.scrollTop=container.offsetTop;
			}
			currentImage.dataset.micronshigh=container.image["pixelheight"]*container.image["micronsperpixely"]+"";
			Measure.setScale();
			return true;
		},

};



/**********************************************************************************************************
 * DROP INFORMATION PANEL
 **********************************************************************************************************/

window.DropInfo={

	/**
	 * Which items are shown on the info panel.
	 * This is the default state, which will be overwritten by user's configuration
	 * (or become the user's configuration, if none exists).
	 */
	infoPanelItems:{
			'dropviewer_infopanel_show_Drop_scoring':0,
			'dropviewer_infopanel_show_Protein':1,
			'dropviewer_infopanel_show_Well_solution':1,
			'dropviewer_infopanel_show_Protein_solution':1,
			'dropviewer_infopanel_show_Drop_volumes':1,
			'dropviewer_infopanel_show_Plate_details':1,
			'dropviewer_infopanel_show_Incubation':1,
	},
	
	switchTo:function(){
		Object.keys(DropInfo.infoPanelItems).forEach(function(k){
			DropInfo.infoPanelItems[k]=parseInt(UserConfig.get(k, DropInfo.infoPanelItems[k]));
		});
		
		setCurrentModeButton(document.getElementById("viewbutton_di"));
		document.querySelectorAll(".controlpanel").forEach(function(p){
			p.style.display="none";
		});
		currentPanel=DropInfo;
		DropInfo.render();
		return true;
	},
	switchFrom:function(){
		document.getElementById("dropinfopanel").innerHTML='';
	},
	refresh:function(){
		if(!DropViewer.images[DropViewer.currentIndex].welldrop){
			DropInfo.getWellDropInfo(); //triggers render()
		} else {
			DropInfo.render();
		}
	},
	getWellDropInfo:function(){
		let wellDropId=DropViewer.images[DropViewer.currentIndex].welldropid;
		new AjaxUtils.Request("/api/welldrop/"+wellDropId,{
			method:"get",
			onSuccess:DropInfo.getWellDropInfo_onSuccess,
			onFailure:DropInfo.getWellDropInfo_onFailure,
		});
	},
	getWellDropInfo_onSuccess:function(transport){
		//set response to current drop's "welldrop" property
		if(!AjaxUtils.checkResponse(transport)){ return false; }
		DropViewer.images[DropViewer.currentIndex].welldrop=transport.responseJSON;
		DropInfo.render();
	},
	getWellDropInfo_onFailure:function(){
		DropInfo.render();
	},
	render:function(){
		if(DropInfo!==currentPanel){ return false; }
		if(!DropViewer.scores){
			window.setTimeout(DropInfo.render, 250);
			return false;
		}
		let wellDrop=DropViewer.images[DropViewer.currentIndex].welldrop;
		let img=DropViewer.images[DropViewer.currentIndex];
		let screen=DropViewer.screen;
		let out='';
		let currentScoreColor="";

		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Drop_scoring)){
			let currentScoreColor='';
			out+='<h2>Drop score</h2><div style="border-left:1.5em solid transparent">';
			if(canUpdate) {
				out+='<select id="dropinfo_scoring" onChange="DropInfo.setScoreByWidget()">';
					out+='<option value="0">(unscored)</option>';
					DropViewer.scores.forEach(function(sc){
						let selected='';
						if(1*sc.id===1*img.latestcrystalscoreid){
						selected='selected="selected"';
						currentScoreColor=sc["color"];
					}
						out+='<option '+selected+' value="'+sc.id+'" style="padding-left:0.25em;border-left:0.5em solid #'+sc["color"]+'">['+sc.hotkey+'] '+sc.label+'</option>';
					});
					out+='</select>';
				//highlight current
			} else {
				DropViewer.scores.forEach(function (sc) {
					if (1 * sc.id === 1 * img.latestcrystalscoreid) {
						currentScoreColor = sc["color"];
						out += '<div style="padding-left:0.25em;border-left:0.5em solid #' + sc["color"] + '">' + sc["label"] + '</div>'
					}
				});
			}
			out+='</div>';
		}
		
		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Protein)){
			out+='<br/><h2>Protein</h2>';
			if(wellDrop && wellDrop["proteinname"] && wellDrop["proteinname"]!==""){
				out+=DropViewer.images[DropViewer.currentIndex].welldrop["proteinname"];
				out+='<br/>Construct: '+DropViewer.images[DropViewer.currentIndex].welldrop["constructname"];
			} else {
				out+=DropViewer.warningImage+'Not set';
			}
		}

		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Well_solution)){
			out+='<br/><br/><h2>Well solution</h2>';
			if(null==screen || !screen.conditions){
				out+='Screen: (none chosen)';
			} else {
				let screenDescription='(Not found)';
				let screenRow=1*img.row+1*data["offsetinscreeny"];
				let screenCol=1*img.col+1*data["offsetinscreenx"];
				screen.conditions.forEach(function(c){
					if(1*c.col===screenCol && 1*c.row===screenRow){
						screenDescription=c.description;
					}
				});
				out+=screenDescription;
				out+='<br/>Screen: <a href="/screen/'+screen.id+'">'+screen.name+'</a>';
			}
		}
		
		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Protein_solution)){
			out+='<br/><br/><h2>Protein solution</h2>';
			if(""===wellDrop.proteinconcentrationamount || ""===wellDrop.proteinconcentrationunit){
				out+='Protein concentration: '+DropViewer.warningImage+'Not set';

			} else {
				let unit=wellDrop.proteinconcentrationunit;
				unit=unit.replace("uL","&#181;L");
				unit=unit.replace("ug","&#181;g");
				out+='Protein concentration: '+wellDrop.proteinconcentrationamount + unit;
			}
			out+='<br/>';
			if(""===wellDrop.proteinbuffer){
				out+='Protein buffer: '+DropViewer.warningImage+'Not set';
			} else {
				out+='Protein buffer: '+wellDrop.proteinbuffer;
			}
		}
		
		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Drop_volumes)){
			out+='<br/><br/><h2>Drop volumes</h2>';
			if(""===wellDrop["proteinsolutionamount"] || ""===wellDrop["proteinsolutionunit"]){
				out+='Protein solution: '+DropViewer.warningImage+'Not set';
			} else {
				let unit=wellDrop["proteinsolutionunit"];
				unit=unit.replace("uL","&#181;L");
				unit=unit.replace("ug","&#181;g");
				out+='Protein solution: '+wellDrop["proteinsolutionamount"] + unit;
			}
			out+='<br/>';
			if(""===wellDrop["wellsolutionamount"] || ""===wellDrop["wellsolutionunit"]){
				out+='Well solution: '+DropViewer.warningImage+'Not set';
			} else {
				let unit=wellDrop["wellsolutionunit"];
				unit=unit.replace("uL","&#181;L");
				unit=unit.replace("ug","&#181;g");
				out+='Well solution: '+wellDrop["wellsolutionamount"] + unit;
			}
		}
			
		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Plate_details)){
			out+='<br/><br/><h2>Plate</h2>';
			out+='Type: <a href="/platetype/'+DropViewer.plateType.id+'">'+DropViewer.plateType.name+'</a>';
			out+='<br/>Barcode: <a href="/plate/'+data.plateid+'">'+data["platename"]+'</a>';
			out+='<br/>Description: '+data["platedescription"];
		}
		
		if(1===parseInt(DropInfo.infoPanelItems.dropviewer_infopanel_show_Incubation)){
			out+='<br/><br/><h2>Incubation</h2>';
			out+='Imager: <a href="/imager/'+data["imagerid"]+'">'+data["imagerfriendlyname"]+'</a>';
			out+='<br/>Temperature: '+data["temperature"]+'&deg;C';
		}
		
		document.getElementById("dropinfopanel").innerHTML=out;
		document.getElementById("dropinfopanel").style.display="block";

		window.setTimeout(function(){
			if(document.getElementById("dropinfo_scoring") && ""!==currentScoreColor){
				document.getElementById("dropinfo_scoring").closest("div").style.borderColor="#"+currentScoreColor;
			}
		},50);
	},

	setScoreByHotkey:function(hotkey){
		if(DropInfo!==currentPanel){
			return false;
		}
		let scoreElem=document.getElementById("dropinfo_scoring");
		DropViewer.scores.forEach(function(s){
			if(s.hotkey===hotkey){
				if(scoreElem){
					scoreElem.value=s.id;
				}
				DropInfo.setScoreById(s.id);
			}
		});
	},
	setScoreByWidget:function(){
		let scoreElem=document.getElementById("dropinfo_scoring");
		if(scoreElem.classList.contains("updating")){ return false; }
		DropInfo.setScoreById(scoreElem.value);	
	},
	
	setScoreById:function(scoreId){
		if(!canUpdate){ 
			alert("You don't have permission to update this plate. Talk to your administrator.");
			return false; 
		}

		let imageToScore=DropViewer.images[DropViewer.currentIndex];
		let imageId=imageToScore.id;
		let scoreElem=document.getElementById("dropinfo_scoring");
		if(scoreElem){
			scoreElem.classList.add("updating");
			scoreElem.value=scoreId;
			let col='transparent';
			DropViewer.scores.forEach(function(sc){
				if(1*sc.id===1*scoreId){
					col="#"+sc.color;
				}
			});
			scoreElem.closest("div").style.borderColor=col;
		}
		new AjaxUtils.Request('/api/dropimage/'+imageId, {
			method:'patch',
			parameters:{
				csrfToken:csrfToken,
				latestcrystalscoreid:scoreId,
				humanscorerid:userId
			},
			onSuccess:DropInfo.setScore_onSuccess,
			onFailure:DropInfo.setScore_onFailure,
		});
		
	},
	setScore_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		let scoreElem=document.getElementById("dropinfo_scoring");
		if(scoreElem){
			scoreElem.classList.remove("updating");
			scoreElem.closest("div").classList.remove("updating");
		}
	},
	setScore_onFailure:function(transport){
		if(transport.responseJSON && transport.responseJSON.error  && transport.responseJSON.error.indexOf("Duplicate entry") >-1){ 
			//Scored the same drop twice with same score in same second - just eat it. 
			return false; 
		}
		AjaxUtils.checkResponse(transport);
		let scoreElem=document.getElementById("dropinfo_scoring");
		if(scoreElem){
			scoreElem.classList.remove("updating");
			scoreElem.closest("div").classList.remove("updating");
		}
	},

	
};

/**********************************************************************************************************
 * CRYSTAL SELECTION PANEL
 **********************************************************************************************************/

window.CrystalSelection={
		
	crystals:[],
	currentCrystalNumber:null,
	originalImageSrc:"",

	switchTo:function(){
		setCurrentModeButton(document.getElementById("viewbutton_xs"));
		Measure.stop();
		document.querySelectorAll(".controlpanel").forEach(function(p){
			p.style.display="none";
		});
		document.getElementById("selectpanel").style.display="block";
		currentPanel=CrystalSelection;
		CrystalSelection.getCrystals();
		CrystalSelection.getPinCategoryAndTypes();
		if(canUpdate){
			document.getElementById("currentimage").addEventListener("click", CrystalSelection.addCrystal);
		}
		CrystalSelection.originalImageSrc=document.getElementById("currentimage").src;
		return true;
	},
	switchFrom:function(){
		document.querySelectorAll(".xtal_crosshair").forEach(function(ch){
			ch.remove();
		});
		document.getElementById("currentimage").removeEventListener("click", CrystalSelection.addCrystal);
		if(!Measure.suppressed){
			Measure.init();
		}
		document.getElementById("selectpanel").innerHTML='';
		CrystalSelection.crystals=null;
		CrystalSelection.currentCrystalNumber=null;
		document.getElementById("currentimage").src=CrystalSelection.originalImageSrc;
	},
	refresh:function(){
		if(CrystalSelection!==currentPanel){ return false; }
		let sp=document.getElementById("selectpanel");
		let out='';
		let currentCrystal=null;
		if(1===CrystalSelection.crystals.length){
			CrystalSelection.currentCrystalNumber=CrystalSelection.crystals[0].numberindrop;
		}
		CrystalSelection.crystals.forEach(function(c){
			if(parseInt(c.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
				currentCrystal=c;
				document.getElementById("selectpanel").currentCrystal=c;
			}
		});

		//No crystal selected
		if(null==CrystalSelection.currentCrystalNumber){
			out+='<h3>Crystals</h3>';
			if(!parseInt(data["isarchived"])){
				out+='<p>Click any of the crosshairs on the image to select it, or click anywhere on the image to select a new crystal.</p>';
			} else {
				out+='<p>Click any of the crosshairs on the image to select it.</p>';
			}
			sp.innerHTML=out;
			sp.style.display="block";
			CrystalSelection.redrawCrosshairs();
			return;
		}
	
		//Crystal selected, show its info
		let markedOnCurrentImage=!(document.getElementById("currentimage").src.indexOf(currentCrystal.dropimageid)<1);
		if(!markedOnCurrentImage){
			document.getElementById("currentimage").src="/api/dropimagefile/"+currentCrystal.dropimageid+"/full.jpg";
		}
		CrystalSelection.redrawCrosshairs();
		let crosshairs=document.getElementById("imagepane").querySelectorAll(".xtal_crosshair");
		if(crosshairs){
			crosshairs.forEach(function(xh){
				xh.classList.remove("xtal_currentcrosshair");
				let xtalNumber=parseInt(xh.querySelector(".xtal_number").innerHTML);
				if(xtalNumber===1*CrystalSelection.currentCrystalNumber){
					xh.classList.add("xtal_currentcrosshair");
				}
			});
		}

		out='<br/><h2>Crystal '+CrystalSelection.currentCrystalNumber+' overview - <a href="/crystal/'+currentCrystal.id+'">View full details</a></h2>';
		out+='<div id="selectpaneltabs">';
		
		out+='<h3 id="xtaldetails">Details</h3>';
		out+='<div id="xtaldetails_body" class="infopanel">';
			out+='<form action="#" id="xtaldetailsform"></form>';
			if(canDeleteCrystals){
				if(1===parseInt(currentCrystal.isfished)) {
					out += '<br/><hr/><p>This crystal has been fished.</p><p>It cannot be deleted.</p>';
				} else if(document.querySelector("#xtalnotes_body .crystalNote")){
					out += '<br/><hr/><p>This crystal has notes.</p><p>It cannot be deleted.</p>';
				} else if(document.querySelector("#xtalfiles_body .crystalFile")){
					out += '<br/><hr/><p>This crystal has files.</p><p>It cannot be deleted.</p>';
				} else {
					out+='<br/><hr/><br/><div><input type="button" id="xtaldelete" onclick="CrystalSelection.deleteCurrentCrystal()" value="Delete crystal" /></div>';
				}
			}
	
		out+='</div>';

		out+='<h3 id="xtalfiles">Files</h3>';
		out+='<div id="xtalfiles_body" class="infopanel">';
			out+='<div id="crystalfiles">Waiting for files...</div>';
		out+='</div>';
		
		out+='<h3 id="xtalnotes">Notes</h3>';
		out+='<div id="xtalnotes_body" class="infopanel">';
			out+='<div id="crystalnotes">Waiting for notes...</div>';
		out+='</div>';

		if(canUpdate && 0===parseInt(currentCrystal.isfished)){
			out+='<h3 id="xtalfish">QuickFish</h3>';
			out+='<div id="xtalfish_body" class="infopanel">';
			out+='<p>If you\'re just fishing the crystal onto a pin for testing, you can do that here. You will need a barcoded pin.</p>';
			out+='<p>If you\'re not using barcoded pins, or if you want to store the pin in a puck, you\'ll need to use the <a href="/containercontent/create">Crystal Fishing</a> page.</p>';
			out+='</div>';
		}
		
		sp.innerHTML=out;
		Crystal.writeDetailsFormFields(currentCrystal, canUpdate, document.getElementById("xtaldetailsform"));

		sp.style.display="block";
		
		if(document.getElementById("nameseparator") && (""===currentCrystal["suffix"] || !currentCrystal["suffix"])){
			document.getElementById("nameseparator").style.visibility="hidden";
		}

		if(canUpdate){
			if(document.getElementById("movetoimagectrl")){
				document.getElementById("movetoimagectrl").innerHTML='<input type="button" style="cursor:pointer" onclick="CrystalSelection.updateCrystalImageNumber()" id="movetocurrentimage" value="Move crystal mark to this image" title="Crosshair was marked on a different image of this drop."/>';
			}
		}
		
		//get all the crystals, plot them
		if(null!=CrystalSelection.currentCrystalNumber){
			CrystalSelection.crystals.forEach(function(c){
				if(parseInt(c.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
					CrystalSelection.getCrystalNotes(c);
					CrystalSelection.getCrystalFiles(c);
				}
			});
		}
		sp.querySelectorAll("h3").forEach(function(h){
			h.addEventListener("click",CrystalSelection.switchTab);
		});
		sp.querySelectorAll(".infopanel").forEach(function(p){
			p.style.position="absolute";
			p.style.bottom="6em";
		});
		if(canUpdate && 0===parseInt(currentCrystal.isfished)){
			let ff=ui.textField({ name:'quickfish_barcode', label:'Scan pin barcode', value:'', suppressAutoUpdate:true },document.getElementById("xtalfish_body"));
			ff.querySelector("input").addEventListener("keyup",CrystalSelection.startQuickFish);
			document.getElementById("xtalfish").addEventListener("click", function(){ff.querySelector("input").focus(); } )

		}
		window.setTimeout(function(){
			CrystalSelection.switchTab();
		},50);
	},

	
	switchTab:function(evt){
		let sp=document.getElementById("selectpanel");

		let clicked=sp.querySelector("h3");
		if(evt){
			clicked=evt.target;
		} else if(CrystalSelection.currentTab){
			clicked=CrystalSelection.currentTab;
		}
		if(!clicked){ return; }
		let id=clicked.id;
		CrystalSelection.currentTab=clicked;
		sp.querySelectorAll("h3").forEach(function(h){
			h.classList.remove("current");
		});
		sp.querySelectorAll(".infopanel").forEach(function(p){
			p.style.display="none";
		});
		document.getElementById(id).classList.add("current");
		document.getElementById(id+"_body").style.display="block";
	},
	
	redrawCrosshairs:function(){
		if(!document.getElementById("currentimage").naturalHeight){
			window.setTimeout(CrystalSelection.redrawCrosshairs, 100);
			return;
		}
		document.querySelectorAll(".xtal_crosshair").forEach(function(x){
			if("xtal_crosshair"===x.id){
				x.style.display="none";
			} else {
				x.remove();
			}
		});
		CrystalSelection.crystals.forEach(function(c){
			CrystalSelection.writeCrosshair(c);
		});
	},
	
	writeCrosshair: function(crystal){
		let imageElement=document.getElementById("currentimage");
		imageElement.dropImage=DropViewer.getCurrentImage();
		let crosshair=Crystal.drawCrosshair(crystal, imageElement);
		crosshair.addEventListener("click",CrystalSelection.showCrystal);
		return crosshair;
	},

	getPinCategoryAndTypes:function(){
		new AjaxUtils.Request('/api/containercategory/name/Pin',{
			method:'get',
			onSuccess:function(transport){
				if(transport.responseJSON.id){
					CrystalSelection.pinCategory=transport.responseJSON;
					if(!CrystalSelection.pinCategory["userscancreate"]){ return; }
					new AjaxUtils.Request('/api/containertype/containercategoryid/'+CrystalSelection.pinCategory.id, {
						method:'get',
						onSuccess:function(transport){
							if(transport.responseJSON.rows){
								CrystalSelection.pinTypes=transport.responseJSON.rows;
							}
						},
						onFailure:function(transport){
							console.log("Could not get the Pin container types. Error code: "+transport.status);
						}
					});
				}
			},
			onFailure:function(transport){
				console.log("Could not get the Pin container category. Error code: "+transport.status);
			}
		});
	},
	
	
	startQuickFish:function(evt){
		if("Enter"!==evt.key){ return; } //fire only on enter
		CrystalSelection.doStartQuickFish();
	},
	doStartQuickFish:function(){
		let barcodeField=document.getElementById("quickfish_barcode");
		let barcode=barcodeField.value.trim();
		if(""===barcode){ return false; }
		barcodeField.closest("label").classList.add("updating");
		new AjaxUtils.Request("/api/container/name/"+barcode,{
			method:"get",
			onSuccess:function(transport){ 
				if(!transport.responseJSON || !transport.responseJSON.id){
					return AjaxUtils.checkResponse(transport);
				}
				let pin=transport.responseJSON;
				if("pin"!==pin.containercategoryname.toLowerCase()){
					CrystalSelection.doQuickFishError('Container with barcode '+barcode+' is a '+pin.containercategoryname.toLowerCase()+', not a pin.');
					return false;
				}
				CrystalSelection.quickFishGetPinContents(pin);
			},
			onFailure:function(transport){ 
				if(404===transport.status){
					//Pin does not exist
					if(!CrystalSelection.pinCategory || 1!==parseInt(CrystalSelection.pinCategory["userscancreate"]) || !CrystalSelection.pinTypes){
						//Can't create pin - insufficient info
						CrystalSelection.doQuickFishError('No pin found with barcode '+barcode+'.');
						return false; 
					} else if(1===CrystalSelection.pinTypes.length){
						//Only one type of pin - create it
						CrystalSelection.quickFishCreatePin(barcode, CrystalSelection.pinTypes[0].id);
						return false; 
					} else {
						//More than one type of pin - user must choose
						CrystalSelection.showPinTypeSelectionDialog(barcode);
						return false; 
					}
				}
				AjaxUtils.checkResponse(transport);
			},
		});
	},
	
	quickFishCreatePin:function(barcode, containerTypeId){
		ui.closeModalBox();
		new AjaxUtils.Request('/api/container',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				name:barcode,
				containertypeid:containerTypeId
			},
			onSuccess:CrystalSelection.doStartQuickFish,
			onFailure:function(){ alert("Pin does not exist, and it could not be created."); }
		});
	},

	showPinTypeSelectionDialog:function(barcode){
		ui.modalBox({
			title:"IceBear has no pin with barcode "+barcode+". What kind of pin is it?",
			content:'<div id="fish_ct_pin"></div></div>'
		});
		CrystalSelection.pinTypes.forEach(function(ct){
			let catName=ct.containercategoryname.toLowerCase();
			let categoryDiv=document.getElementById("fish_ct_"+catName);
			if(!categoryDiv){ return; } //Not interested in this containercategory
			let html='';
			html+='<div class="fish_containertype" data-containertypeid="'+ct.id+'" data-barcode="'+barcode+
				'" onclick="CrystalSelection.quickFishCreatePin(this.dataset.barcode, this.dataset.containertypeid)" style="cursor:pointer">';
			html+='<div class="fish_containertypename fish_'+catName+'">'+ct.name+'</div>';
			html+='</div>';
			categoryDiv.innerHTML+=html;
		});
		
	},
	
	doQuickFishError:function(err){
		let bc=document.getElementById("quickfish_barcode");
		bc.closest("label").classList.remove("updating");
		bc.value="";
		alert(err);
		bc.focus();
	},

	quickFishWashPinAndReattempt:function(pin){
		if(!pin.containercontentid){
			CrystalSelection.doQuickFishError("Pin has no 'containercontentid' key. Cannot unlink pin and existing crystal.");
			return false;
		}
		new AjaxUtils.Request("/api/containercontent/"+pin.containercontentid,{
			method:"delete",
			postBody:"csrfToken="+csrfToken,
			onSuccess:function (transport){
				let currentCrystal=null;
				CrystalSelection.crystals.forEach(function(c){
					if(parseInt(c.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
						currentCrystal=c;
						document.getElementById("selectpanel").currentCrystal=c;
					}
				});
				CrystalSelection.quickFishDoFish(pin, currentCrystal);
			},
			onFailure:function (transport){
				CrystalSelection.doQuickFishError('Could not empty the pin.');
			}
		})

	},

	quickFishGetPinContents:function(pin){
		new AjaxUtils.Request("/api/container/"+pin.id+'/content',{
			method:"get",
			onSuccess:function(transport){
				//Success is failure! The pin is full.
				if(confirm(
					"IceBear thinks pin "+pin.name+" has a crystal in it. This could be because someone else didn't clean up properly after a shipment.\n\n"+
					"Click OK to use this pin for your crystal.\n"+
					"Click Cancel to abandon this QuickFish and scan another pin."
				)){
					let contents=transport.responseJSON["rows"];
					pin.containercontentid=contents[0]["containercontentid"];
					CrystalSelection.quickFishWashPinAndReattempt(pin);
				} else {
					let bc=document.getElementById("quickfish_barcode");
					bc.closest("label").classList.remove("updating");
					bc.value="";
					bc.focus();
				}
			},
			onFailure:function(transport){ 
				if(404===transport.status){
					//Failure is success! The pin is empty.
					let currentCrystal=null;
					CrystalSelection.crystals.forEach(function(c){
						if(parseInt(c.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
							currentCrystal=c;
							document.getElementById("selectpanel").currentCrystal=c;
						}
					});
					CrystalSelection.quickFishDoFish(pin, currentCrystal);
				} else {
					//Real, actual failure.
					CrystalSelection.doQuickFishError('Could not determine whether pin is full. Try another.');
				}
			}
		});
	},
	quickFishDoFish:function(pin,crystal){
		if(!pin||!crystal){
			CrystalSelection.doQuickFishError('Could not record crystal fishing.');
		}
		new AjaxUtils.Request('/api/containercontent/',{
			method:'post',
			parameters:{
				'csrfToken':csrfToken,
				'parent':pin.id,
				'child':crystal.id,
				'position':1
			},
			onSuccess:function(){
				crystal.isfished=1;
				new AjaxUtils.Request('/api/crystal/'+crystal.id,{
					method:'patch',
					parameters:{
						'csrfToken':csrfToken,
						'isfished':1
					},
					//assume worked, no further handling needed
					onSuccess:function (){}
				});
				let noteText="Crystal fished onto pin "+pin.name+".";
				let bc=document.getElementById("quickfish_barcode");
				bc.closest("label").classList.remove("updating");
				bc.style.display="none";
				let lbl=bc.closest("label").querySelector("span.label");
				lbl.style.cssFloat="none";
				lbl.innerHTML=noteText;
				window.setTimeout(function(){
					CrystalSelection.quickFishSaveNote(crystal.id, noteText);
				},1000);
			},
			onFailure:function(transport){
				AjaxUtils.checkResponse(transport);
			}
		});
	},
	
	quickFishSaveNote:function(crystalId, noteText){
		new AjaxUtils.Request("/api/note",{
			method:'post',
			parameters:{
				'csrfToken':csrfToken,
				'parentid':crystalId,
				'text':noteText
			},
			onSuccess:function(){
				CrystalSelection.quickFishAfterSaveNote(crystalId);
			},
			onFailure:function(){
				alert("Crystal was fished, but note describing this could not be saved.");
				CrystalSelection.quickFishAfterSaveNote(crystalId);
			}
		});
	},
	
	quickFishAfterSaveNote:function(crystalId){
		CrystalSelection.crystals.forEach(function(c){
			if(parseInt(c.id)===parseInt(crystalId)){
				c.isfished=true;
			}
		});
		CrystalSelection.currentTab=document.getElementById("selectpaneltabs").querySelector("h3");
		CrystalSelection.refresh();
	},
	
	
	updateCrystalImageNumber:function(){
		let crystalId=0;
		let imageId=DropViewer.images[DropViewer.currentIndex].id;
		CrystalSelection.crystals.forEach(function(xtal){
			if(parseInt(xtal.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
				crystalId=xtal.id;
			}
		});
		if(0===crystalId){ alert("Could not find crystal ID"); return false; }
		document.getElementById("xtaldetails").classList.add("updating");
		new AjaxUtils.Request('/api/crystal/'+crystalId,{
			method:'patch',
			parameters:{
				csrfToken:csrfToken,
				dropimageid:imageId
			},
			onSuccess:CrystalSelection.updateCrystalImageNumber_onSuccess,
			onFailure:CrystalSelection.updateCrystalImageNumber_onFailure,
		});
	},
	updateCrystalImageNumber_onSuccess:function(transport){
		let updated=transport.responseJSON["updated"];
		CrystalSelection.crystals.forEach(function(xtal){
			if(parseInt(xtal.id)===parseInt(updated.id)){
				xtal.dropimageid=updated.dropimageid;
			}
		});
		let m=document.getElementById("movetocurrentimage");
		m.previousElementSibling.remove();
		m.remove();
		document.getElementById("xtaldetails").classList.remove("updating");
	},
	updateCrystalImageNumber_onFailure:function(transport){
		document.getElementById("movetocurrentimage").closest("h2").classList.remove("updating");
		AjaxUtils.checkResponse(transport);
	},
	
	showCrystal:function(evt){
		let xh=evt.target;
		while(!xh.crystal){
			xh=xh.parentElement;
		}
		document.getElementById("imagepane").querySelectorAll(".xtal_crosshair").forEach(function(mark){
			mark.classList.remove("xtal_currentcrosshair");
		});
		xh.classList.add("xtal_currentcrosshair");
		CrystalSelection.currentCrystalNumber=xh.crystal.numberindrop;
		CrystalSelection.currentTab=null;
		CrystalSelection.refresh();
	},
	
	getCrystals:function(){
		let img=DropViewer.getCurrentImage();
		if(!img.crystals){
			new AjaxUtils.Request('/api/welldrop/'+img.welldropid+'/crystal',{
				method:'get',
				onSuccess:CrystalSelection.getCrystals_onSuccess,
				onFailure:CrystalSelection.getCrystals_onFailure,
			});
		} else {
			CrystalSelection.crystals=img.crystals;
			CrystalSelection.refresh();
		}
	},
	getCrystals_onSuccess:function(transport){
		let img=DropViewer.getCurrentImage();
		img.crystals=transport.responseJSON.rows;
		CrystalSelection.crystals=transport.responseJSON.rows;
		let numCrystals=CrystalSelection.crystals.length;
		if(0===numCrystals){
			CrystalSelection.refresh();
			return;
		}
		let counter=0;
		CrystalSelection.crystals.forEach(function(c){
			new AjaxUtils.Request('/api/crystal/'+c.id+'/diffractionrequest',{
				method:'get',
				onSuccess:function(transport){
					c.diffractionrequests=transport.responseJSON.rows;
					counter++;
					if(counter===numCrystals){ CrystalSelection.refresh(); }
				},
				onFailure:function(){
					counter++;
					if(counter===numCrystals){ CrystalSelection.refresh(); }
				}
			});
		});
		window.setTimeout(function(){
			if(!document.getElementById("selectpanel").querySelector("h3")){
				CrystalSelection.refresh();
			}
		},2000);
	},
	getCrystals_onFailure:function(transport){
		if(404===transport.status){
			CrystalSelection.crystals=[];
			CrystalSelection.refresh();
			return;
		}
		alert("Could not retrieve list of crystals for this well.");
	},


	getCrystalFiles:function(crystal){
		new AjaxUtils.Request('/api/crystal/'+crystal.id+'/file',{
			method:'get',
			onSuccess:CrystalSelection.getCrystalFiles_onSuccess,
			onFailure:CrystalSelection.getCrystalFiles_onFailure,
		})
	},
	getCrystalFiles_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		let fb=document.getElementById("xtalfiles_body");
		fb.innerHTML='';
		let out='';
		CrystalSelection.writeCrystalFilesForm();
		transport.responseJSON.rows.forEach(function(file){
			out+='<a class="crystalFiles" href="/api/file/'+file.id+'/'+file.filename+'" target="_blank">'+file.filename+'</a><br/>';
		});
		fb.innerHTML+=out;
	},
	getCrystalFiles_onFailure:function(transport){
		document.getElementById("xtalfiles_body").innerHTML='';
		if(404===transport.status){
			CrystalSelection.writeCrystalFilesForm();
			return;
		}
		alert("Could not retrieve files for this crystal.");
	},
	writeCrystalFilesForm:function(){
		let out='';
		if(!canUpdate){ return; }
		out+='<form id="crystalfilesadd" method="post" action="/api/file" onsubmit="alert(\'!!\');return false">';
		out+='<input type="hidden" value="'+csrfToken+'" name="csrfToken" />';
		out+='<input type="hidden" value="'+document.getElementById("selectpanel").currentCrystal.id+'" name="parentid" />';
		out+='<strong>Attach a file: </strong><input id="crystalfilesaddfile" type="file" name="file" onchange="ui.submitForm(this)" />';
		out+='</form>';
		out+='<hr/>';
		document.getElementById("xtalfiles_body").innerHTML+=out;
		window.setTimeout(function(){ 
			document.getElementById("crystalfilesaddfile").options={
				afterSuccess:function(){
					CrystalSelection.refresh();
				}
			}
		},50);
	},

	
	getCrystalNotes:function(crystal){
		new AjaxUtils.Request('/api/crystal/'+crystal.id+'/note',{
			method:'get',
			onSuccess:CrystalSelection.getCrystalNotes_onSuccess,
			onFailure:CrystalSelection.getCrystalNotes_onFailure,
		})
	},
	getCrystalNotes_onSuccess:function(transport){
		AjaxUtils.checkResponse(transport);
		let nb=document.getElementById("xtalnotes_body");
		nb.innerHTML='';
		let out='';
		CrystalSelection.writeCrystalNotesForm();
		transport.responseJSON.rows.forEach(function(note){
			out+='<h4 class="crystalNote"><a href="/user/'+note.userid+'">'+note.username+'</a>, '+ui.friendlyDate(note["createtime"])+'</h4>';
			out+=ui.urlifyAndBreakNoteText(note.text)+'<hr/><br/>';
		});
		nb.innerHTML+=out;
	},
	getCrystalNotes_onFailure:function(transport){
		document.getElementById("xtalnotes_body").innerHTML='';
		if(404===transport.status){
			CrystalSelection.writeCrystalNotesForm();
			return;
		}
		alert("Could not retrieve notes for this crystal.");
	},
	writeCrystalNotesForm:function(){
		let out='';
		if(!canUpdate){ return; }
		out+='<div id="crystalnotesadd">';
		out+='<textarea id="crystalnotesaddtext" placeholder="Add a note..."></textarea>';
		out+='<br/><input id="crystalnotesaddbutton" type="button" value="Save note" />';
		out+='</div><hr/>';
		document.getElementById("xtalnotes_body").innerHTML+=out;
		window.setTimeout(function(){ document.getElementById("crystalnotesaddbutton").addEventListener("click",CrystalSelection.addCrystalNote); },50);
	},

	addCrystalNote: function(){
		let cn=document.getElementById("crystalnotesaddtext");
		if(cn.classList.contains("updating")){ return false; }
		let parentId=0;
		CrystalSelection.crystals.forEach(function(c){
			if(parseInt(c.numberindrop)===parseInt(CrystalSelection.currentCrystalNumber)){
				parentId=c.id;
			}
		});
		if(0===parentId){
			alert("Could not determine crystal for note");
			return false;
		}
		let noteText=cn.value.trim();
		if(""===noteText){
			alert("Cannot add an empty note");
			return false;
		}
		cn.classList.add("updating");
		new AjaxUtils.Request('/api/note',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				parentid:parentId,
				text:noteText,
			},
			onSuccess:CrystalSelection.addCrystalNote_onSuccess,
			onFailure:CrystalSelection.addCrystalNote_onFailure,
		});
	},
	addCrystalNote_onSuccess: function(){
		CrystalSelection.refresh();
	},
	addCrystalNote_onFailure: function(transport){
		document.getElementById("crystalnotesaddtext").classList.remove("updating");
		let err=transport.responseText;
		if(transport.responseJSON && transport.responseJSON.error){
			err=transport.responseJSON.error;
		}
		alert("Could not add note. The server said:\n\n"+err);
	},

	deleteCurrentCrystal:function(){
		let sp=document.getElementById("selectpanel");
		if(!confirm("Really delete crystal "+sp.currentCrystal.numberindrop+"?")){ return false; }
		document.getElementById("xtaldelete").closest("div").classList.add("updating");
		Crystal.delete(sp.currentCrystal, CrystalSelection.deleteCurrentCrystal_afterSuccess, null);
	},
	deleteCurrentCrystal_afterSuccess:function(response){
		document.getElementById("selectpanel").currentCrystal=null;
		document.querySelectorAll(".xtal_crosshair").forEach(function(xh){
			if(1*xh.dataset.crystalid===1*response['deleted']){
				xh.remove();
			}
		});
		CrystalSelection.switchFrom();
		CrystalSelection.crystals=null;
		let img=DropViewer.getCurrentImage();
		img.crystals=null;
		CrystalSelection.switchTo();
	},
	
	addCrystal:function(evt){
		let crystalNumber=0;
		CrystalSelection.crystals.forEach(function(c){
			crystalNumber=Math.max(crystalNumber,c.numberindrop);
		});
		crystalNumber++;
		let img=DropViewer.getCurrentImage();
		let ci=document.getElementById("currentimage");
		let offset=ui.cumulativeOffset(ci);
		let scale=ci.offsetHeight / img["pixelheight"];
		let crystalX=Math.round((evt.clientX-offset.left)/scale);
		let crystalY=Math.round((evt.clientY-offset.top)/scale);
		let crystalName=DropViewer.getWellNameForImage(img)+'d'+img.dropnumber+'c'+crystalNumber;
		new AjaxUtils.Request('/api/crystal',{
			method:'post',
			parameters:{
				csrfToken:csrfToken,
				numberindrop:crystalNumber,
				name:crystalName,
				pixelx:crystalX,
				pixely:crystalY,
				welldropid:img.welldropid,
				dropimageid:img.id,
				projectid:img.projectid
			},
			onSuccess:CrystalSelection.addCrystal_onSuccess,
			onFailure:CrystalSelection.addCrystal_onFailure
		});
	},
	addCrystal_onSuccess: function(transport){
		if(!transport.responseJSON|| !transport.responseJSON.created){
			return CrystalSelection.addCrystal_onFailure(transport);
		}
		CrystalSelection.crystals.push(transport.responseJSON.created);
		CrystalSelection.currentCrystalNumber=transport.responseJSON.numberindrop;
		if(undefined!==transport.responseJSON["scoreId"]){
			DropViewer.images[DropViewer.currentIndex]['latestcrystalscoreid']=transport.responseJSON["scoreId"];
		}
		CrystalSelection.refresh();
	},
	addCrystal_onFailure: function(transport){
		let err=transport.responseText;
		if(transport.responseJSON && transport.responseJSON.error){
			err=transport.responseJSON.error;
		}
		alert("Could not add crystal. The server said:\n\n"+err);
	},
	
	
	/* Movie controls when in crystals view */
	goToFirst:function(){
		DropViewer.goToFirst();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},
	goToLast:function(){
		DropViewer.goToLast();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},
	goToNext:function(){
		DropViewer.goToNext();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},
	goToPrevious:function(){
		DropViewer.goToPrevious();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},

	playOrStop:function(){
		if(DropViewer.movieInterval){
			CrystalSelection.stopMovie();
		} else {
			CrystalSelection.startMovie();
		}
	},

	startMovie:function(){
		document.querySelectorAll(".xtal_crosshair").forEach(function(ch){
			if("xtal_crosshair"===ch.id){
				ch.style.display="none";
			} else {
				ch.remove(); 
			} 
		});
		DropViewer.startMovie();
	},

	stopMovie:function(){
		DropViewer.stopMovie();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},
	
	
	goUpOneRow:function(){
		DropViewer.goUpOneRow();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},
	goDownOneRow:function(){
		DropViewer.goDownOneRow();
		CrystalSelection.switchFrom();
		CrystalSelection.switchTo();
	},

	
};



/**********************************************************************************************************
 * MEASUREMENT 
 **********************************************************************************************************/

window.Measure={

	suppressed:true,
	
	init:function(){
		let ip=document.getElementById("imagepane");
		ip.addEventListener("mouseover", Measure.mouseOver);
		ip.addEventListener("mousemove", Measure.mouseMove);
		ip.addEventListener("mouseout",  Measure.mouseOut);
	},
			
	stop:function(){
		let ip=document.getElementById("imagepane");
		ip.removeEventListener("mouseover", Measure.mouseOver);
		ip.removeEventListener("mousemove", Measure.mouseMove);
		ip.removeEventListener("mouseout",  Measure.mouseOut);
	},
			
	setScale:function(){
		let m=document.getElementById("dv_measure");
		let ci=document.getElementById("currentimage");
		if(!ci){ return false; }
		let micronsHigh=ci.dataset.micronshigh;
		let pixelHeight=ci.offsetHeight;
		let screenMicronsPerPixel=micronsHigh/pixelHeight;
		m.style.height=Math.round(2000/screenMicronsPerPixel)+"px";
		m.style.width =Math.round(2000/screenMicronsPerPixel)+"px";
		if(screenMicronsPerPixel<=0){
			m.classList.add("dv_nomeasure");
		} else {
			m.classList.remove("dv_nomeasure");
		}
		if(screenMicronsPerPixel>3){
			m.classList.add("dv_smallmeasure");
		} else {
			m.classList.remove("dv_smallmeasure");
		}
	},
	mouseOver:function(){
		document.getElementById("dv_measure").style.display="block";
		Measure.mouseMove();

	},
	mouseMove:function(evt){
		let m=document.getElementById("dv_measure");
		if(!evt){ return false; }
		let absX=evt.pointer().x;
		let absY=evt.pointer().y;
		let imgPos=document.getElementById("currentimage").getBoundingClientRect();
		let h=m.offsetHeight;
		let x=absX-imgPos.left;
		let y=absY-imgPos.top;
		m.style.left=(x)-(h/2)+"px";
		m.style.top=(y)-(h/2)+"px";
	},
	mouseOut:function(){
		document.getElementById("dv_measure").style.display="none";
	},
	
	toggleVisibility:function(){
		if(Measure.suppressed){
			Measure.suppressed=false;
			Measure.init();
			Measure.mouseOver();
		} else {
			Measure.suppressed=true;
			Measure.mouseOut();
			Measure.stop();
		}
	},
	
};


/**********************************************************************************************************
 * DROP NAVIGATION
 **********************************************************************************************************/

window.DropNav={

	isOpen:false,
		
	onclose:function(){
		DropNav.isOpen=false;
		if(document.getElementById("modalBox")){
			document.getElementById("modalBox").classList.remove("dropviewer");
		}
		window.removeEventListener("keyup",DropNav.navigateToDropOnEnterKey);
		return true;
	},

	open:function(){
		let pt=DropViewer.plateType;
		if(!pt){ alert("No plate type info, cannot show navigation"); return false; }
		DropNav.isOpen=true;
		ui.modalBox({
			title:"Drop navigation",
			content:"Just a moment...",
			onclose:DropNav.onclose
		});
		window.setTimeout(DropNav._doOpen,50);
	},
	_doOpen:function(){
		let mb=document.getElementById("modalBox");
		mb.classList.add("dropviewer");
		let pt=DropViewer.plateType;
		let ptRows=1*pt["rows"];
		let ptCols=1*pt["cols"];
		let ptSubs=1*pt["subs"];
		let out='<table id="dropnav"><tr><th>Drop</th><td>&nbsp;</td>';
		for(let i=1;i<=ptCols;i++){
			out+="<th>"+i+"</th>";
		}
		out+="</tr>";
		let inBottomHalf=0;
		for(let r=1;r<=ptRows;r++){
			if(r>ptRows/2){
				inBottomHalf=1;
			}
			out+="<tr>";
			if(r<=ptSubs){
				out+='<th class="droppicker droppickerempty" id="droppicker'+r+'" data-dropnumber="'+r+'" onclick="DropNav.selectDropNumber('+r+')">'+r+'</th>';
			} else {
				out+="<th>&nbsp;</th>";
			}
			out+="<th>"+DropViewer["rowLabels"][r]+"</th>";
			for(let c=1;c<=ptCols;c++){
				out+='<td id="nav_'+r+'_'+c+'" data-in-bottom-half="'+inBottomHalf+'" data-row="'+r+'" data-col="'+c+'" class="dropnav_well empty" style="">';
				out+="</td>";
			}
			out+="</tr>";
		}
		out+="</tr></table>";
		
		mb.querySelector(".boxbody").innerHTML=out;
		mb.querySelector(".boxbody").querySelector("tr").querySelectorAll("td,th").forEach(function(cell){
			cell.style.width=cell.offsetHeight+"px";
		});
		let counter=0;
		DropViewer.images.forEach(function(img){
			let rowNumber=img["row"];
			let colNumber=img["col"];
			let dropNumber=img["dropnumber"];
			let thumbUrl=img["thumbnailurl"];
			let td=document.getElementById("nav_"+rowNumber+"_"+colNumber);
			td.classList.remove("empty");
			
			let dv=document.createElement("div");
			dv.id="dropnav_"+rowNumber+"_"+colNumber+"_"+dropNumber;
			dv.className="dropnav_dropimage dropnav_drop"+dropNumber;
			dv.style.display="none";
			dv.dataset.row=rowNumber;
			dv.dataset.col=colNumber;
			dv.dataset.dropnumber=dropNumber;
			dv.dataset.dropindex=counter+"";
			dv.dataset[img["lighttype"].toLowerCase()+'image']=thumbUrl;
			let score=document.getElementById("score"+img["latestcrystalscoreid"]);
			if(score){
				dv.style.backgroundColor=score.style.borderColor;
			}
			let im=document.createElement("img");
			im.src=thumbUrl;
			dv.appendChild(im);
			let lbl=document.createElement("div");
			lbl.classList.add("dropnav_comparelabel");
			lbl.innerHTML="Drop "+dropNumber;
			dv.appendChild(lbl);
			td.appendChild(dv);
			dv.addEventListener("click",function(){ DropNav.goToDrop(dv); });
			dv.addEventListener("mouseover",function(){ DropNav.startCompareDrops(dv); });
			dv.addEventListener("mouseout",function(){ DropNav.stopCompareDrops(dv); });
			document.getElementById("droppicker"+dropNumber).classList.remove("droppickerempty");
			counter++;
		});
		document.querySelectorAll(".droppickerempty").forEach(function(dp) {
			dp.onclick=null;
			dp.style.cursor="not-allowed";
		});
		let currentImage=DropViewer.images[DropViewer.currentIndex];
		document.getElementById("dropnav_"+currentImage["row"]+"_"+currentImage["col"]+"_"+currentImage["dropnumber"]).closest("td").classList.add("dropnav_wellcurrent");
		window.addEventListener("keyup", DropNav.navigateToDropOnEnterKey);
		window.setTimeout(function(){
			DropNav.selectDropNumber(currentImage["dropnumber"]);
			DropNav.setOtherLightTypeImages();
		},50);
	},
	
	otherImagingSession:null,
	setOtherLightTypeImages:function(){
		if(!DropNav["otherImagingSession"] || !DropNav["otherImagingSession"]["dropimages"]){
			window.setTimeout(DropNav.getOtherImagingSession,50);
			return;
		}
		let otherImages=DropNav["otherImagingSession"]["dropimages"];
		if(0===otherImages.length){ return false; }
		let lighttype=otherImages[0]["lighttype"].toLowerCase();
		otherImages.forEach(function(di){
			document.getElementById("dropnav_"+di.row+"_"+di.col+"_"+di.dropnumber).dataset[lighttype+"image"]=di["thumbnailurl"];
		});
		let th=document.getElementById("dropnav").querySelector("th");
		th.style.background="url(/images/icons/"+ ((skin["bodyIconTheme"]+"icons")||"darkicons") +"/switchlight.png) no-repeat center";
		th.style.cursor="pointer";
		th.innerHTML="";
		th.title="Switch between visible and UV images";
		th.onclick=DropNav.toggleLightTypeImages;
	},
	
	/**
	 * Examine the images currently shown in the drop nav pane, and retrieve an
	 * imaging session with the opposite light type (visible vs UV).
	 */
	getOtherImagingSession:function(){
		let dropNavBox=document.getElementById("dropnav");
		let otherLightType=false;
		let dv=dropNavBox.querySelector("[data-visibleimage]");
		if(dv){
			otherLightType="UV";
		} else {
			dv=dropNavBox.querySelector("[data-uvimage]");
			if(dv){
				otherLightType="Visible";
			}
		}
		if(!otherLightType){ return false; }
		new AjaxUtils.Request("/api/imagingsession/plateid/"+data['plateid']+"/lighttype/"+otherLightType+"?sortby=imageddatetime&sortdescending=yes",{
			method:"get",
			onSuccess: DropNav.getOtherImagingSession_onSuccess,
			onFailure: DropNav.getOtherImagingSession_onFailure,
		});
	},
	getOtherImagingSession_onSuccess:function(transport){
		//take the first one in transport.responseJSON.rows - was sorted latest first
		DropNav.otherImagingSession=transport.responseJSON.rows[0];
		DropNav.getOtherImagingSessionImages();
	},
	getOtherImagingSession_onFailure:function(){
		//nothing to do, just swallow it
	},
	getOtherImagingSessionImages:function(){
		new AjaxUtils.Request("/api/imagingsession/"+DropNav.otherImagingSession.id+"/dropimage?all=1",{
			method:"get",
			onSuccess: DropNav.getOtherImagingSessionImages_onSuccess,
			onFailure: DropNav.getOtherImagingSessionImages_onFailure,
		});
	},
	getOtherImagingSessionImages_onSuccess:function(transport){
		DropNav.otherImagingSession.dropimages=transport.responseJSON.rows;
		DropNav.setOtherLightTypeImages();
	},
	getOtherImagingSessionImages_onFailure:function(){
		//nothing to do, just swallow it
	},

	currentLightType:"Visible",
	toggleLightTypeImages:function(){
		DropNav.currentLightType= ("Visible"===DropNav.currentLightType) ? "UV" : "Visible";
		let lt=DropNav.currentLightType.toLowerCase();
		document.querySelectorAll(".dropnav_dropimage").forEach(function(dv){
			dv.querySelector("img").src=dv.dataset[lt+"image"];
		});
	},

	//TODO Remove Prototype dependency
	startCompareDrops:function(dropDiv){
		let mb=document.getElementById("modalBox");
		let tr=dropDiv.closest("tr");
		let td=dropDiv.closest("td");
		let compareDiv=document.createElement("div");
		compareDiv.id="dropnav_compare";
		compareDiv.innerHTML=""+dropDiv.closest("td").innerHTML.replace(' id="dropnav_', ' id="dropnavcompare_');
		compareDiv.style.position="absolute";
		mb.querySelector(".boxbody").appendChild(compareDiv);
		compareDiv.querySelectorAll(".dropnav_dropimage").forEach(function(di){
			di.style.display="inline-block";
			di.style.height="auto";
			di.style.width="auto";
		});
		Element.clonePosition(compareDiv,dropDiv, { setWidth:false, setHeight:false, offsetLeft:-1000, offsetTop:100 });
		let w1=compareDiv.offsetWidth;
		let h1=compareDiv.offsetHeight;
		let w2=tr.offsetWidth;
		let h2=tr.offsetHeight;
		let w3=dropDiv.offsetWidth;
		let l2=ui.cumulativeOffset(tr).left;
		let l3=ui.cumulativeOffset(dropDiv.closest("td").previousElementSibling).left;

		let offsetLeft=5+l3+w3-l2;
		offsetLeft=Math.min(offsetLeft, w2-w1);

		//Filthy user-agent detect, but this doesn't need to be reliable. Worst-case, it pushes the drop images a bit
		//further away from the well. Without doing this, Chrome puts them over the well, causing the mouseout to fire
		//and an annoying flicker.
		let isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);

		if(1===parseInt(td.dataset.inBottomHalf)){
			if(isChrome){
				Element.clonePosition(compareDiv,dropDiv.closest("tr"), { setWidth:false, setHeight:false, offsetLeft:offsetLeft, offsetTop:-(h1+h2+h2+15) });
			} else {
				Element.clonePosition(compareDiv,dropDiv.closest("tr"), { setWidth:false, setHeight:false, offsetLeft:offsetLeft, offsetTop:-(compareDiv.offsetHeight+3) });
			}
		} else {
			Element.clonePosition(compareDiv,dropDiv.closest("tr"), { setWidth:false, setHeight:false, offsetLeft:offsetLeft, offsetTop:dropDiv.offsetHeight+9 });
		}
	},
	
	stopCompareDrops:function(){
		let compare=document.getElementById("dropnav_compare");
		if(compare){ compare.remove(); }
	},
	
	selectDropNumber:function(dropNumber){
		let mb=document.getElementById("modalBox");
		mb.querySelectorAll("div.dropnav_dropimage").forEach(function(dv){
			dv.style.display="none";
		});
		mb.querySelectorAll("div.dropnav_drop"+dropNumber).forEach(function(dv){
			dv.style.display="block";
		});
		mb.querySelectorAll("th.droppicker").forEach(function(dv){
			dv.classList.remove("droppickercurrent");
		});
		document.getElementById("droppicker"+dropNumber).classList.add("droppickercurrent");
	},
	
	navigateToDropOnEnterKey:function(evt){
		let mb=document.getElementById("modalBox");
		if(13!==evt.keyCode || !mb){ return; }
		let selectedDrop=mb.querySelector("div.dropnav_wellcurrent");
		if(selectedDrop){ DropNav.goToDrop(selectedDrop); }
	},
	
	goToDrop:function(dropElement){
		switchPanels(document.getElementById("viewbutton_dv"));
		DropViewer.setCurrentImage(dropElement.dataset.dropindex);
		ui.closeModalBox();
	},
	
	goToFirst:function(){
		DropNav.goToIndexInNavWindow(0);
	},
	goToPrevious:function(byDrop){
		if(byDrop || KeyboardShortcuts.ctrlPressed){
			DropNav.moveInNavWindow(0,0,-1);
		} else {
			DropNav.goLeftOneColumn();
		}
	},
	goToNext:function(byDrop){
		if(byDrop || KeyboardShortcuts.ctrlPressed){
			DropNav.moveInNavWindow(0,0,1);
		} else {
			DropNav.goRightOneColumn();
		}
	},
	goToLast:function(){
		DropNav.goToIndexInNavWindow(DropViewer.images.length-1);
	},

	goLeftOneColumn:function(){
		DropNav.moveInNavWindow(0,-1,0);
	},
	goRightOneColumn:function(){
		DropNav.moveInNavWindow(0,1,0);
	},
	goUpOneRow:function(){
		DropNav.moveInNavWindow(-1,0,0);
	},
	goDownOneRow:function(){
		DropNav.moveInNavWindow(1,0,0);
	},

	moveInNavWindow:function(rowDelta, colDelta, dropDelta){
		//what's highlighted now?
		let mb=document.getElementById("modalBox");
		let highlightedWell=mb.querySelector("td.dropnav_wellcurrent");
		let highlightedDrop=mb.querySelector("th.droppickercurrent");
		if(!highlightedWell || !highlightedDrop){ return false; }
		let oldRow=parseInt(highlightedWell.dataset.row);
		let oldCol=parseInt(highlightedWell.dataset.col);
		let oldDrop=parseInt(highlightedDrop.dataset.dropnumber);
		let toHighlight=null;
		let newDrop=null;
		if(0!==dropDelta){
			let oldIndex=parseInt(document.getElementById("dropnav_"+oldRow+"_"+oldCol+"_"+oldDrop).dataset.dropindex);
			DropNav.goToIndexInNavWindow(oldIndex+dropDelta);
		} else if(0!==rowDelta){
			let newRow=oldRow+rowDelta;
			toHighlight=document.getElementById("dropnav_"+newRow+"_"+oldCol+"_"+oldDrop);
			if(!toHighlight){ return; }
			DropNav.goToIndexInNavWindow(toHighlight.dataset.dropindex);
		} else if(-1===colDelta){
			let prev=highlightedWell.previousElementSibling;
			if(!prev.classList.contains("dropnav_well")){
				 prev=null;
			}
			if(!prev){
				let prevRow=highlightedWell.closest("tr").previousElementSibling;
				if(prevRow && "tr"===prevRow.tagName.toLowerCase()){
					let cells=prevRow.querySelectorAll("td.dropnav_well");
					prev=cells[cells.length-1];
				}
			}
			if(prev){ 
				newDrop=prev.querySelector(".dropnav_drop"+oldDrop);
				if(newDrop){
					DropNav.goToIndexInNavWindow(newDrop.dataset.dropindex); 
				}
			}
		} else if(1===colDelta){
			let next=ui.nextElementSiblingMatchingSelector(highlightedWell,"td.dropnav_well");
			if(!next){
				let nextRow=ui.nextElementSiblingMatchingSelector(highlightedWell.closest("tr"),"tr");
				if(nextRow){ next=nextRow.querySelector("td.dropnav_well"); }
			}
			if(next){ 
				newDrop=next.querySelector(".dropnav_drop"+oldDrop);
				if(newDrop){
					DropNav.goToIndexInNavWindow(newDrop.dataset.dropindex); 
				}
			}
		}
	},

	goToIndexInNavWindow:function(index){
		let img=DropViewer.images[index];
		if(!img){ return; }
		let dropDiv=document.getElementById("dropnav_"+img.row+"_"+img.col+"_"+img.dropnumber);
		if(!dropDiv){ return; }
		document.querySelectorAll(".dropnav_wellcurrent").forEach(function(elem){ elem.classList.remove("dropnav_wellcurrent")});
		dropDiv.closest("td").classList.add("dropnav_wellcurrent");
		dropDiv.classList.add("dropnav_wellcurrent");
		DropNav.selectDropNumber(dropDiv.dataset.dropnumber);
		DropNav.stopCompareDrops();
		DropNav.startCompareDrops(dropDiv);
	},
	
};

/**********************************************************************************************************
 * SMALL SCREEN
 * If the screen width is below the threshold (see ui.js:isSmallScreen), render a stripped-down viewer.
 **********************************************************************************************************/
window.SmallScreenDropViewer={

		init:function(){
			DropViewer.getPlateType();
			document.getElementById("imagepane").remove();
			document.getElementById("controls").remove();
			let h1=document.querySelector("header h1");
			h1.innerHTML=h1.innerHTML.replace("Trial drop viewer: ","").replace(" drop "," ");

			let cont=document.getElementById("container");
			cont.style.position="absolute"
			cont.style.top="0";
			cont.style.bottom="0";
			cont.style.right="0";
			cont.style.left="0";
			cont.style.height=window.height+"px";

			let content=document.getElementById("content");
			content.style.position="absolute";
			content.style.bottom="0";
			content.style.right="0";
			content.style.left="0";
			content.style.top=document.body.querySelector("header").offsetHeight+"px";
			content.style.height=window.height+"px";

			content.classList.add("smallscreenviewer");
			content.innerHTML='<div id="imagecache" style="position:absolute;left:-1000px"></div><div id="ssimagepane">Getting images...</div><div id="sscontrols"></div>';
			new AjaxUtils.Request('/api/imagingsession/'+data['id']+'/dropimage?all=1',{
				method:'get',
				onSuccess:SmallScreenDropViewer.init_onSuccess,
				onFailure:SmallScreenDropViewer.init_onFailure
			});
		},
		init_onFailure:function(){
			document.getElementById("ssimagepane").innerHTML="Could not get images.";
		},
		init_onSuccess:function(transport){
			document.getElementById("imagecache").innerHTML='';
			DropViewer.images=transport.responseJSON.rows;
			DropViewer.maxIndex=DropViewer.images.length-1;
			let startImage=0;
			if(document.location.hash){
				let dropLabel=document.location.hash.substring(1);
				let row=DropViewer.rowLabels.indexOf(dropLabel.substr(0,1));
				let parts=dropLabel.substring(1).split(".");
				let col=parseInt(parts[0]);
				let drop=parts[1];
				if(-1!==drop.indexOf("c")){
					let subParts=drop.split("c");
					drop=subParts[0];
				}
				let numImages=DropViewer.images.length;
				for(let i=0;i<numImages;i++){
					let img=DropViewer.images[i];
					if(row===parseInt(img.row) && col===parseInt(img.col) && parseInt(drop)===parseInt(img.dropnumber)){
						startImage=i;
						DropViewer.currentIndex=i;
						break;
					}
				}
			}
			SmallScreenDropViewer.renderControls();
			SmallScreenDropViewer.setCurrentImage(startImage);
		},
		setCurrentImage:function(index){
			DropViewer.currentIndex=index;
			let img=DropViewer.images[index];
			if(document.getElementById("cached"+img.id)){
				document.getElementById("ssimagepane").innerHTML='<img src="/api/dropimagethumb/'+img.id+'" alt=""/>';
			} else {
				document.getElementById("ssimagepane").innerHTML='Getting image...';
				window.setTimeout(function(){
					document.getElementById("ssimagepane").innerHTML='<img src="/api/dropimagethumb/'+img.id+'" alt=""/>';
				},50);
			}
			//update the page title
			DropViewer.setDropNameInTitle(img);
			//update the controls
			//cache adjacent images
			//			transport.responseJSON.rows.forEach(function(img){
			//				document.getElementById("imagecache").innerHTML+='<img src="/api/dropimagethumb/'+img.id+'"/>';
			//			});
			SmallScreenDropViewer.updateControls();
		},

		renderControls:function(){
			let ssImagePane=document.getElementById("ssimagepane");
			let ssControls=document.getElementById("sscontrols");
			let content=document.getElementById("content");
			if(ssImagePane.offsetHeight<100 || !ssImagePane.querySelector("img")){
				window.setTimeout(SmallScreenDropViewer.renderControls, 100);
				return false;
			}
			ssControls.innerHTML="";
			//This mess is because Unicode has a pile of poo symbol but no 4-way arrow symbol. We overlay N-S and E-W arrow.
			ssControls.innerHTML+='<div id="ssnav" class="sscontrol" onclick="SmallScreenDropViewer.showNav();return false">'+
				'<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:table-cell;text-align:center">&#x2194;</div>'+
				'<div style="position:absolute;top:0;left:0;right:0;bottom:0;display:table-cell;text-align:center">&#x2195;</div>'+
			'</div>';

			//disable scoring control
			//ssControls.innerHTML+='<div id="ssscore" class="sscontrol" href="#" onclick="SmallScreenDropViewer.showScoring();return false">&#x2714;</div>';
			
			ssControls.innerHTML+='<div id="ssprev"  class="sscontrol" onclick="SmallScreenDropViewer.prevDrop();return false">&lt;</div>';
			ssControls.innerHTML+='<div id="ssnext"  class="sscontrol" onclick="SmallScreenDropViewer.nextDrop();return false">&gt;</div>';
			ssControls.innerHTML+='<div id="ssfirst" class="sscontrol" onclick="SmallScreenDropViewer.firstDrop();return false">&#x23EA;</div>';
			ssControls.innerHTML+='<div id="sslast"  class="sscontrol" onclick="SmallScreenDropViewer.lastDrop();return false">&#x23E9;</div>';
			ssControls.innerHTML+='<div id="ssplay"  class="sscontrol" onclick="SmallScreenDropViewer.playStop();return false">&#x25B6;</div>';
			if(content.offsetHeight>content.offsetWidth){
				//Portrait
				//We assume that images are landscape, and that the phone is tall enough to fit both image and min-height controls,
				//so we don't need to scale the image down. Instead, bring the top of the controls up to meet the image.
				ssImagePane.querySelector("img").style.width="100%";
				ssImagePane.style.height=ssImagePane.querySelector("img").offsetHeight+"px";
				ssControls.style.height=(content.offsetHeight-ssImagePane.offsetHeight)+"px";
				ssControls.style.width="100%";
			} else {
				//Landscape
				//Assuming that the image is landscape, and that the controls must be a minimum width to be usable, we may end up with:
				ssControls.style.height="100%";
				ssControls.style.width="4cm";
				ssControls.style.right="0";
				ssControls.style.left="auto";
				ssImagePane.style.right="4cm";
				ssImagePane.querySelector("img").style.height="20%";
				ssImagePane.querySelector("img").style.width="auto";
				let widthFraction=ssImagePane.querySelector("img").offsetWidth/ssImagePane.offsetWidth;
				let heightFraction=ssImagePane.querySelector("img").offsetHeight/ssImagePane.offsetHeight;
				if(widthFraction<heightFraction){
					//width-limited. Set image height 100%.
					ssImagePane.querySelector("img").style.height="100%";
					ssImagePane.querySelector("img").style.width="auto";
				} else {
					//height-limited. Set image width 100%.
					ssImagePane.querySelector("img").style.height="auto";
					ssImagePane.querySelector("img").style.width="100%";
				}
				ssImagePane.style.width=ssImagePane.querySelector("img").offsetWidth+"px";
				ssControls.style.width=(content.offsetWidth-ssImagePane.offsetWidth)+"px";
			}
			SmallScreenDropViewer.updateControls();
		},
		
		updateControls:function(){
			
			//if playing movie, cache image 5 out and cull first in cache (on start movie, cache first five)
			//else wipe cache and cache adjacent in X,Y,drop axes
			if(!SmallScreenDropViewer.isPlaying){
				document.querySelectorAll(".sscontrol").forEach(function(elem){
					elem.style.opacity="1";
				});
				if(0===DropViewer.currentIndex){
					document.getElementById("ssprev").style.opacity="0.4";
					document.getElementById("ssfirst").style.opacity="0.4";
				} else if(DropViewer.maxIndex===DropViewer.currentIndex){
					document.getElementById("ssnext").style.opacity="0.4";
					document.getElementById("sslast").style.opacity="0.4";
					document.getElementById("ssplay").style.opacity="0.4";
				}
			}
		},
		
		movieInterval:null,
		movieDelay:0.8, //seconds
		isPlaying:false,
		playStop:function(){
			if(SmallScreenDropViewer.isPlaying){
				SmallScreenDropViewer.stopMovie();
			} else {
				SmallScreenDropViewer.startMovie();
			}
		},

		startMovie:function(){
			let play=document.getElementById("ssplay");
			if(SmallScreenDropViewer.isPlaying || DropViewer.currentIndex===DropViewer.maxIndex){ return false; }
			play.innerHTML="&#x25FC;";
			play.style.color="#f00";
			document.querySelectorAll(".sscontrol").forEach(function(elem){
				elem.style.opacity="0.4";
			});
			play.style.opacity="1";
			SmallScreenDropViewer.isPlaying=true;
			//grey out all controls except Stop
			//cache next five images
			SmallScreenDropViewer.movieInterval=window.setInterval(SmallScreenDropViewer.advanceMovie, SmallScreenDropViewer.movieDelay*1000);
		},
		
		stopMovie:function(){
			let play=document.getElementById("ssplay");
			play.innerHTML="&#x25B6;";
			play.style.color="#0f0";
			document.querySelectorAll(".sscontrol").forEach(function(elem){
				elem.style.opacity="1";
			});
			window.clearInterval(SmallScreenDropViewer.movieInterval);
			SmallScreenDropViewer.isPlaying=false;
			SmallScreenDropViewer.updateControls();

		},

		advanceMovie:function(){
			DropViewer.currentIndex++;
			SmallScreenDropViewer.setCurrentImage(DropViewer.currentIndex);
			if(DropViewer.currentIndex===DropViewer.maxIndex){
				SmallScreenDropViewer.stopMovie();
			}
		},

		nextDrop:function(){
			if(SmallScreenDropViewer.isPlaying || DropViewer.currentIndex===DropViewer.maxIndex){ return false; }
			SmallScreenDropViewer.setCurrentImage(DropViewer.currentIndex+1);
		},
		
		prevDrop:function(){
			if(SmallScreenDropViewer.isPlaying || DropViewer.currentIndex===0){ return false; }
			SmallScreenDropViewer.setCurrentImage(DropViewer.currentIndex-1);
		},

		firstDrop:function(){
			if(SmallScreenDropViewer.isPlaying || DropViewer.currentIndex===0){ return false; }
			SmallScreenDropViewer.setCurrentImage(0);
		},

		lastDrop:function(){
			if(SmallScreenDropViewer.isPlaying || DropViewer.currentIndex===DropViewer.maxIndex){ return false; }
			SmallScreenDropViewer.setCurrentImage(DropViewer.maxIndex);
		},

		showNav:function(row,col){
			ui.closeModalBox();
			let pt=DropViewer.plateType;
			let items=[];
			if(!row){
				//choosing row
				for(let i=1;i<=pt.rows;i++){
					items.push({
						"label":DropViewer.rowLabels[i],
						"row":i,
						"col":null,
						"hasImages":false,
						"bestScoreIndex":null
					});
				}
				DropViewer.images.forEach(function(img){
					items[img.row-1].hasImages=true;
				});
			} else if(!col){
				//choosing col
				let rowLabel=DropViewer.rowLabels[row];
				for(let i=1;i<=pt.cols;i++){
					items.push({
						"label":rowLabel+i,
						"row":row,
						"col":i,
						"hasImages":false,
						"bestScoreIndex":null
					});
				}
				DropViewer.images.forEach(function(img){
					items[img.col-1].hasImages=true;
				});
			} else {
				//choosing drop.
				let colLabel=(Plate.getWellName(row,col,1).split("."))[0];
				for(let i=1; i<=pt["subs"]; i++){
					items.push({
						"label":colLabel+"."+i,
						"row":row,
						"col":col,
						"drop":i,
						"hasImages":false,
						"bestScoreIndex":null,
						"dropImageIndex":null
					});
				}
				for(let i=0;i<=DropViewer.maxIndex;i++){
					let img=DropViewer.images[i];
					if(!img){ break; }
					if(row===parseInt(img.row) && col===parseInt(img.col)){
						items[img.dropnumber-1].hasImages=true;
						items[img.dropnumber-1].dropImageIndex=i;
					}
				}
				let imageCount=0;
				let imageIndex=-1;
				items.forEach(function(i){
					if(i.hasImages){ 
						imageCount++;
						imageIndex=i.dropImageIndex;
					}
				});
				if(1===imageCount && -1!==imageIndex){
					SmallScreenDropViewer.setCurrentImage(imageIndex);
					return false;
				}
			}

			//Later, maybe show best score in row/col?
			
			//render the nav buttons
			ui.modalBox({
				title:'Choose row, column, drop',
				content:''
			});
			let bb=document.getElementById("modalBox").querySelector(".boxbody");
			bb.style.padding="0";
			bb.style.borderRadius="0";
			bb.style.backgroundColor="black";
			bb.style.overflow="hidden";
			let isPortrait=(bb.offsetHeight>bb.offsetWidth);
			let numButtons=items.length;
			let halfButtons=Math.max(numButtons/2);
			let xPercent=100/halfButtons;
			let yPercent=50;
			let breakpoint=halfButtons;
			if(isPortrait){
				breakpoint=2;
				yPercent=100/halfButtons;
				xPercent=50;
			}
			if(numButtons<5){
				if(isPortrait){
					breakpoint=1;
					xPercent=100;
					yPercent=100/numButtons;
				} else {
					breakpoint=numButtons;
					yPercent=100;
					xPercent=100/numButtons;
				}

			} 
			let xCount=0;
			let yCount=0;
			items.forEach(function(i){
				let btn=document.createElement("div");
				btn.className="ssnavitem";
				btn.style.top=(yCount*yPercent)+"%";
				btn.style.left=(xCount*xPercent)+"%";
				btn.style.height=(yPercent-2)+"%";
				btn.style.width=(xPercent-2)+"%";
				if(i.dropImageIndex){
					btn.onclick=function(){ 
						SmallScreenDropViewer.setCurrentImage(i.dropImageIndex);
						ui.closeModalBox();
					};
				} else if(i.hasImages){
					btn.onclick=function(){ SmallScreenDropViewer.showNav(i.row, i.col); };
				} else {
					btn.style.opacity="0.4";
				}
				btn.innerHTML=i.label;
				bb.appendChild(btn);
				xCount++;
				if(xCount>=breakpoint){ xCount=0; yCount++; }
			});
		}
		
};


/**********************************************************************************************************
 * OTHER FUNCTIONS
 **********************************************************************************************************/

function togglePageConfig(){
	
	let handedness=UserConfig.get("dropviewer_handedness","right");
	let bb=ui.modalBox({
		title:'Configure the drop viewer',
		content:''
	});
	
	let f=bb.form({ readonly:false });
	f.style.marginTop="0";
	f.style.position='absolute';
	f.style.top="1%";
	f.style.left="1%";
	f.style.width='65%';

	let handednessDiv=document.createElement("div");
	handednessDiv.id="dv_config_handedness";
	
	let lh=document.createElement("div");
	lh.id="dv_config_lh";
	let leftImage=document.createElement("img");
	leftImage.src='/images/icons/dropviewer/controlsleft.png';
	leftImage.alt='Controls on the left';
	leftImage.style.height="auto";
	lh.appendChild(leftImage);
	lh.onclick=function(){ setHandedness("left"); };
	if("left"===handedness){ lh.style.borderColor='#666'; }
	handednessDiv.appendChild(lh);
	
	let rh=document.createElement("div");
	rh.id="dv_config_rh";
	let rightImage=document.createElement("img");
	rightImage.src='/images/icons/dropviewer/controlsright.png';
	rightImage.alt='Controls on the right';
	rightImage.style.height="auto";
	rh.appendChild(rightImage);
	rh.onclick=function(){ setHandedness("right"); };
	if("right"===handedness){ rh.style.borderColor='#666'; }
	handednessDiv.appendChild(rh);

	let lbl1=f.formField({
		"label":"Layout - controls on left or right",
		"content":""
	});
	lbl1.appendChild(handednessDiv);

	let lbl2=f.formField({
		label:"Minimum score for Shift-left/right navigation",
		content:""
	});
	let scoresDiv=document.createElement("div");
	scoresDiv.id="dv_configscorethresholdwrapper";
	scoresDiv.style.maxWidth="70%";
	scoresDiv.style.cssFloat="right";
	lbl2.appendChild(scoresDiv);
	writeScoreThresholdButtons(scoresDiv);
	lbl2.innerHTML+='<div style="clear:both;max-height:0.25em;">&nbsp;</div>';

	let f2=bb.form({ readonly: false });
	f2.style.position='absolute';
	f2.style.top="1%";
	f2.style.right="1%";
	f2.style.width="32%";

	//SELECT for panel to show first
	let opts=[
		{ value:'DropViewer', label:'Scoring' },
		{ value:'TimeCourse', label:'Imaging history' },
		{ value:'DropInfo', label:'Drop info' },
		{ value:'CrystalSelection', label:'Crystals' },
	];
	f2.dropdown({
		label:'Show this at start',
		name:'dropviewer_startMode',
		options:opts,
		value:UserConfig.get('dropviewer_startMode'),
		apiUrl:'/api/userconfig/dropviewer_startMode'
	});
	
	let lbl=f2.formField({label:'Items to show on info panel', content:'&nbsp;',name:'bogus' });
	lbl.classList.add("radiohead");

	Object.keys(DropInfo.infoPanelItems).forEach(function(k){
		let label=k.split("_show_");
		if(label[1]){ label=label[1].replace("_"," ")}
		f2.checkbox({ label:label, name:k, handler:setInfoPanelItemVisibility, checked:1*DropInfo.infoPanelItems[k]});
	})	
	
}
function writeScoreThresholdButtons(wrapper,currentThreshold){
	let scoringSystemId=DropViewer.scores[0]["crystalscoringsystemid"];
	wrapper.innerHTML="";
	if(undefined===currentThreshold){
		currentThreshold=1*UserConfig.get("dropviewer_scorethreshold_ss"+scoringSystemId,0);
	}
	DropViewer.scores.forEach(function(sc){
		let scoreIndex=sc["scoreindex"];
		let scoreColor=sc["color"];
		let out='<div data-scoreindex="'+scoreIndex+'" onclick="setShiftArrowScoreThreshold('+scoringSystemId+',this.dataset.scoreindex)" style="';
		out+='display:inline-block;width:14em;margin:0.25em;pointer:cursor;color:black;textShadow:1px 1px 1px white;';
		out+='padding=left:0.5em;text-align:left;border:4px solid #'+scoreColor+';';
		if((1*scoreIndex)>=(1*currentThreshold)){
			out+='background-color:#'+scoreColor+';';
			out+='font-weight:bold;';
			if((1*scoreIndex)===(1*currentThreshold)){
				out+='border:4px solid black;';
			}
		}
		out+='">'+sc["label"]+'</div>';
		wrapper.innerHTML+=out;
	});
}

/**
 * Click handler for info panel item checkboxes.
 * Not intended to be called directly, instead passed as a handler when
 * writing the buttons in the config box
 * @param inp The hidden input associated with the checkbox image
 * @returns
 */
function setInfoPanelItemVisibility(inp){
	let lbl=inp.closest("label");
	lbl.classList.add("updating");
	DropInfo.infoPanelItems[inp.name]=parseInt(inp.value);
	UserConfig.set(inp.name, inp.value);
	window.setTimeout(function(){ 
		DropInfo.refresh();
		lbl.classList.remove("updating");
	}, 500);
}

function setHandedness(hand){
	if("left"===hand){
		document.body.classList.add("lefthanded");
		UserConfig.set("dropviewer_handedness","left");
		if(document.getElementById("dv_config_handedness")){
			document.getElementById("dv_config_lh").style.borderColor="#666";
			document.getElementById("dv_config_rh").style.borderColor="transparent";
		}
	} else {
		document.body.classList.remove("lefthanded");
		UserConfig.set("dropviewer_handedness","right");
		if(document.getElementById("dv_config_handedness")){
			document.getElementById("dv_config_lh").style.borderColor="transparent";
			document.getElementById("dv_config_rh").style.borderColor="#666";
		}	
	}	
}

function setShiftArrowScoreThreshold(scoringSystemId,scoreIndex){
	if(isNaN(scoreIndex)){ scoreIndex=0; }
	scoreIndex=parseInt(scoreIndex);
	writeScoreThresholdButtons(document.getElementById("dv_configscorethresholdwrapper"),scoreIndex);
	UserConfig.set("dropviewer_scorethreshold_ss"+scoringSystemId, scoreIndex);
}

let currentPanel;
function switchPanels(button){
	let id=button.id;
	let newObj;
	if(id==="viewbutton_dv"){ newObj=DropViewer; }
	if(id==="viewbutton_tc"){ newObj=TimeCourse; }
	if(id==="viewbutton_di"){ newObj=DropInfo; }
	if(id==="viewbutton_xs"){ newObj=CrystalSelection; }
	if(newObj===currentPanel){ return false; }
	currentPanel.switchFrom();
	newObj.switchTo();
	currentPanel=newObj;
}
function setCurrentModeButton(button){
	document.querySelectorAll(".viewbutton").forEach(function(vb){
		vb.classList.remove("current");
		vb.querySelector("img").src=vb.querySelector("img").src.replace("_current.png",".png");
	});
	button.classList.add("current");
	button.querySelector("img").src=button.querySelector("img").src.replace(".png","_current.png");
}
