import ij.*;
import ij.io.*;
import ij.process.*;
import ij.gui.*;
import ij.text.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;
import ij.plugin.frame.*;
import java.io.*;
import java.util.*;
import javax.swing.event.*;
import ij.plugin.*;
public class GM_Autofocus extends PlugInFrame implements ActionListener /*implements PlugInFilter */{	private ImagePlus imp;
	private ImageProcessor ip;  
	private ImageProcessor ipTmp;  
	private Button load;
	private Button search;
	private Label timeLabel;
	private ImageStack stack;
	private ImagePlus imgStartPoint = new ImagePlus();	
	private ImageStack stackStart;
	private ImagePlus imgStep = new ImagePlus();	
	private ImageStack stackStep;
	private ImagePlus imgAnim;	
	private ImageStack stackAnim;
	private StackWindow stackWin;
	private StackWindow stackWinStart;
	private StackWindow stackWinStep;
	private SyncSliceStackIndex startPointToInput;
	private SyncSliceStackLabel stepToInput;
	private ListenerRoi listenRoi;
	private Roi roi;
	private int totPixel;
	private double[] x;
	private double[] y;
	private PlotWindow plotWin;
	private Plot plot;
	private int imgType;
	private int startPoint;
	public int direzione = 1;
	public long time; 
	
	private Font sans = new Font("SansSerif", Font.BOLD, 12);
	
	public GM_Autofocus() {
		super("GM Autofocus System");
	}
		public void run(String arg) {
		
		//INTERFACCIA/////////////////////////////////////////////////////////////////////////////////
		this.setLayout(new BorderLayout());
		
		Panel buttonPanel = new Panel();
		buttonPanel.setLayout(new GridLayout(4, 1));
		
		// Panel title
                Panel titlePanel= new Panel();
                Label title= new Label("GM Autofocus System", Label.CENTER);
		title.setFont(sans);
		titlePanel.add(title);
		
		//Pannelo load
		Panel loadPanel = new Panel();
		load= new Button("Load");
		load.addActionListener(this);
		loadPanel.add(load);
		
		//Pannello serch
		Panel searchPanel = new Panel();
		search = new Button("Search");
		search.disable();
		search.addActionListener(this);
		searchPanel.add(search);
		
		// panel Info & Help
                Panel panelInfoHelp = new Panel();
                Button infoHelp = new Button("Help & Info");
                infoHelp.addActionListener(new InfoHelp());
                panelInfoHelp.add(infoHelp);
		
		//Panel secondi
		Panel timePanel = new Panel();
		//timePanel.setSize(new Dimension(500, 200));
		timeLabel = new Label("", Label.CENTER);
		//timeLabel.setFont(sans);
		timePanel.add(timeLabel);
		
		
		buttonPanel.add(loadPanel);
		buttonPanel.add(searchPanel);
		buttonPanel.add(panelInfoHelp);
		buttonPanel.add(timePanel);
		
		
		//Pannello Generale
		add(titlePanel, BorderLayout.NORTH);
		add(buttonPanel, BorderLayout.CENTER);
		//add(timePanel, BorderLayout.SOUTH);		
		
		pack();
		GUI.center(this);
		show();
		
		timePanel.resize(new Dimension(500, 200));
		timePanel.setSize(new Dimension(500, 200));
		
		Toolkit kit = Toolkit.getDefaultToolkit();
		Dimension dim = kit.getScreenSize();
		setLocation((dim.width-getWidth())/4,  (dim.height-getHeight())/4);	}
	public void roiChanged(){
		search.disable();
	}
	
	public void actionPerformed(ActionEvent e) {
		Object source=e.getSource();
		
		if(source==load) {
			
			imp = WindowManager.getCurrentImage();
			String title = imp.getTitle();
		
			if ((imp==null) || (title.equals("Start Point")) || (title.equals("Steps")) || (title.equals("GM Autofocus System"))) {
				IJ.beep();
				IJ.showStatus("There are no images open or you have to select an image!");
				IJ.showMessage("No image", "There are no images open or you have to select an image!");
				search.disable();
				return;
			}
			
			int[] dimension =  imp.getDimensions();
			if (dimension[3]==1)  {
				IJ.beep();
				IJ.showStatus("The image is not a stack, the plugin needs a stack!");
				IJ.showMessage("No image stack", "The image is not a stack, the plugin needs a stack!");
				search.disable();
				return;
			}
			
			stackWin = (StackWindow)imp.getWindow();
			ip = imp.getProcessor();
			stack = imp.getStack();
			
			time=System.currentTimeMillis();
			imgType = imp.getType(); 
			if( (imgType!=imp.COLOR_RGB) && (imgType!=imp.GRAY8) ){
				IJ.beep();
				IJ.showStatus("GM Autofocus requires an image of type RGB color");
				IJ.error("Notice","GM Autofocus requires an image of type\n \n8-bit grayscale\nRGB color\n");
				search.disable();
				return;
			}			
			
			roi = imp.getRoi();
			if ( roi==null ) {
				IJ.beep();
				IJ.showStatus("You hate to implement a rectangular selection!");
				IJ.error("Notice","You hate to implement a rectangular selection!\n");
				search.disable();
				return;
			}
			
			if ( roi.getType() != Roi.RECTANGLE ) {
				IJ.beep();
				IJ.showStatus("The selection must be rectangular!");
				IJ.error("Notice","The selection must be rectangular!");
				search.disable();
				return;
			}
			
			Rectangle rect = roi.getBounds();
			totPixel = rect.width *rect.height;
			
			x = new double[stack.getSize()];
			y = new double[stack.getSize()];
			
			for (int i=1; i<=stack.getSize(); i++) {				
				ipTmp = stack.getProcessor(i);
				ipTmp.setRoi(roi);
				ipTmp = ipTmp.crop(); 
				ipTmp.findEdges();
				
				//calcolo la funzione di valutazione 
				double sum = 0;
				
				if( (imp.getType()==imp.COLOR_RGB) ){
					int[] pixels = (int[])(ipTmp.getPixels());	
					
					for (int j=0; j<pixels.length; j++) {						sum += 0xff & pixels[j];
						//sum += (int)(pixels[j] & 0x00ff00)>>8;					}
				}
				else if( (imgType==imp.GRAY8) ){
					byte[] pixels = (byte[])(ipTmp.getPixels());	
					
					for (int j=0; j<pixels.length; j++) {						sum += 0xff & pixels[j];
						//sum += (int)(pixels[j] & 0x00ff00)>>8;					}	
				}
				
				sum = sum/(totPixel);
				
				y[i-1] = sum;
				x[i-1] = i;
				
				IJ.showStatus("Loading image  "+i+"/"+stack.getSize()+"");
				IJ.showProgress((double)(i/stack.getSize()));			}
			
			stackStart = null;
			
			for (int i=1; i<= x.length ; i++) {
				double[] xCircle = new double[1];
				double[] yCircle = new double[1];
				xCircle[0] = x[i-1];
				yCircle[0] = y[i-1];
				
				plot = new Plot("Funzione valutazione", "Fotogramma", "Valore", x, y);
				plot.setColor(Color.blue);
				plot.addPoints(xCircle, yCircle, Plot.CIRCLE);
				plot.draw();
				
				ImagePlus grafico = plot.getImagePlus();
				
				if(stackStart==null){
					stackStart = grafico.createEmptyStack();
					stackStart.addSlice("Fotogramma "+i,grafico.getProcessor());
				}else{					stackStart.addSlice("Fotogramma "+i,grafico.getProcessor());
				}
				
				IJ.showStatus("Plotting Start Point  "+i+"/"+stack.getSize()+"");
			}
			
			imgStartPoint.setStack("Start Point",stackStart);
			if(WindowManager.getFrame("Start Point")==null){
				stackWinStart = new StackWindow(imgStartPoint);
				Point locationGM = getLocation();
				Dimension dim = getSize();
				stackWinStart.setLocation(locationGM.x+dim.width,locationGM.y);
				IJ.freeMemory();
			}
			else { }
			
			//Setto i listener 
			startPointToInput = new SyncSliceStackIndex(imgStartPoint, imp);
			stepToInput = new SyncSliceStackLabel(imgStep, imp);
			listenRoi = new ListenerRoi(imp, roi, this);
			
			time=System.currentTimeMillis()-time;
			timeLabel.setText("Load time: "+new Double(time/1000.0).toString()+" sec");
			repaint();
			search.enable();
			
			imp.setSlice(imgStartPoint.getCurrentSlice());
			
			IJ.showStatus("Load completed");
		}//FINE LOAD
		if(source==search) {
			IJ.showStatus("Searching.....");
			
			listenRoi.mouseExited(new MouseEvent(this, 1, (long)1.0,1,1,1,1,true));
			
			boolean lastStep = false;
			boolean firstStep = true;
			boolean exit = false;
			boolean noJump = true;
			boolean showMessage = false; //ponendo a true tale variabile vengono stampati alcuni messaggi che mostrando i calcoli che l'algotirmo effettua.
			boolean dirChanged = false;
			int currStep = 1;
			int badStep = 0;
			int totGoodStep = 0;
			int firstBadStep = 0;
			int totFirstBadStep = 1;
			//int direzione = 1; 	// 1==AVANTI   -1 ==INDIETRO
			double lastPendenza = 0; 
			int totOscill = 0;
			int MAXINCREMENTO = 5;
			int MAXSTEP = imgStartPoint.getStackSize();
			int MAXBADSTEP = 3;
			int MAXFIRSTBADSTEP = 2;
			int MINGOODSTEP = 4;
			double startValue = 0;
			double currValue = 0;
			double prevValue = 0;
			double maxValue = 0;
			int startPos = imgStartPoint.getCurrentSlice();
			int currPos = startPos;
			int prevPos = 0;
			int maxPos = 0;
			Vector xVect = new Vector();
			Vector yVect = new Vector();
			Vector xDeriv = new Vector();
			Vector yDeriv = new Vector();
			
			time=System.currentTimeMillis();
			stackStep = null;
			
			do{
				//IJ.wait(300); //potrebbe essere usato per vedere meglio i passi durante la ricerca
				if(lastStep) exit=true;	//ultimo ciplo e poi usciamo!
				
				//Trovo i bordi dell'immagine
				ipTmp = stack.getProcessor(currPos);
				ipTmp.setRoi(roi);
				ipTmp = ipTmp.crop(); 
				ipTmp.findEdges();
				
				//calcolo la funzione di valutazione 
				double sum = 0;
				
				if( (imp.getType()==imp.COLOR_RGB) ){
					int[] pixels = (int[])(ipTmp.getPixels());	
					
					for (int j=0; j<pixels.length; j++) {						sum += 0xff & pixels[j];
						//sum += (int)(pixels[j] & 0x00ff00)>>8;					}
				}
				else if( (imgType==imp.GRAY8) ){
					byte[] pixels = (byte[])(ipTmp.getPixels());	
					
					for (int j=0; j<pixels.length; j++) {						sum += 0xff & pixels[j];
						//sum += (int)(pixels[j] & 0x00ff00)>>8;					}	
				}
				
				sum = sum/(totPixel);
				
				xVect.add(new Double(currPos));
				yVect.add(new Double(sum));
				double[] xCircle = new double[xVect.size()];
				double[] yCircle = new double[yVect.size()];
				for(int i=0; i<xVect.size(); i++){
					Double x = (Double)xVect.elementAt(i);
					xCircle[i]=x.doubleValue();
					Double y = (Double)yVect.elementAt(i);
					yCircle[i]=y.doubleValue();
				}
				
				plot = new Plot("Passi algoritmo", "Fotogramma", "Valore", x, y);
				plot.setColor(Color.red);
				plot.addPoints(xCircle, yCircle, Plot.CIRCLE);
				plot.draw();
				
				ImagePlus grafico = plot.getImagePlus();
				
				if(stackStep==null){
					stackStep = grafico.createEmptyStack();
					stackStep.addSlice(""+currPos ,grafico.getProcessor());
				}else{
					stackStep.addSlice(""+currPos ,grafico.getProcessor());
				}
				
				currValue = sum;
				
				double derivata = currValue-prevValue; 
				xDeriv.add(new Double(currPos));
				yDeriv.add(new Double(derivata));
				//IJ.showStatus("derivata "+currPos+" valore "+derivata);
				
				//calcolo il coeff di accelerazione
				double pendenza = Math.atan(derivata);	
				
				//Aggiornameno MAXBADSTEP in funzione delle oscillazioni
				if( ((lastPendenza<0)&&(pendenza>0))||((lastPendenza>0)&&(pendenza<0)) )
					totOscill++;
				MAXBADSTEP = (int)Math.min(( Math.round(1.5*totOscill) ), 8);
				lastPendenza = pendenza;
			
				//ottengo l'incremento 
				int incremento; 
				incremento = (direzione)*(int)(Math.abs(Math.round(1.5/pendenza)));
				
				if(Math.abs(incremento) > MAXINCREMENTO) incremento = (direzione)*MAXINCREMENTO;	

				if(showMessage) IJ.showMessage("***\ncurrStep="+currStep+" incremento="+incremento+"  direzione="+direzione);
				
				if( ((currPos==1) || (currPos==MAXSTEP)) && (!exit)){ //dopo questo il max non si ferma corretto ma una meno o una pių		
					totGoodStep++;
					badStep = 0; //altrimenti antriamo in un ciclo perchč non riusciamo pių a fare passi
					direzione = (-1)*direzione;
					prevPos = startPos; //prev punta a startPos
					prevValue = startValue; //serve il startValue
					currPos = ((startPos+direzione)<=0 ? Math.max(1, startPos+direzione) : Math.min(MAXSTEP, startPos+direzione)); 
					
					if(showMessage) IJ.showMessage("FINE DISCESA - CAMBIO DIREZIONE\ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"  maxPos="+maxPos+"\npendenza="+pendenza+"  incremento="+incremento+"\ntotFirstBadStep="+totFirstBadStep);
				}
				else if (currValue > maxValue /*prevValue*/){ //ANDIAMO AVANTI NELLA RICERCA
					//aggiorno i valori
					if(maxValue!=0) { //si verifica quando abbiamo un apsso in salita che non č il primo passo
						firstStep = false; //lo abbiamo spostato nella parte della DISCESA
						startValue = currValue; 
						if(showMessage) IJ.showMessage("FISRT STEP == FALSE");
					}
					badStep = 0; //azzero il contatore dei passi in discesa
					totGoodStep++;
					prevPos = maxPos = currPos;
					currPos = ((currPos+incremento)<=0 ? Math.max(1, currPos+incremento) : Math.min(MAXSTEP, currPos+incremento)); 
					prevValue = maxValue = currValue;
					
					if(showMessage) IJ.showMessage("SALITA\ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"\npendenza="+pendenza+"  incremento="+incremento);
				}
				else{ //CI FERMIAMO DOPO 3 PASSI DI QUESTO TIPO
					if(firstStep){ //COMINCIAMO CON LA DISCESA
						if(firstBadStep == MAXFIRSTBADSTEP){ //CAMBIAMO DIREZIONE ==> INDIETRO
							dirChanged = true;
							direzione = (-1)*direzione; //cambiamo direzione
							firstStep = false;	
							prevPos = maxPos; //prev punta a max
							prevValue = maxValue;
							currPos = ((maxPos-1)<=0 ? Math.max(1, maxPos-1) : Math.min(MAXSTEP, maxPos-1)); 
							
							if(showMessage) IJ.showMessage("FINE DISCESA FIRST - CAMBIO DIREZIONE \ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"  maxPos="+maxPos+"\npendenza="+pendenza+"  incremento="+incremento+"\ntotFirstBadStep="+totFirstBadStep);
						}else{
							incremento = 2*incremento;
							firstBadStep++; 
							totFirstBadStep += incremento; //accumuliamo il numero dei passi fatti in discesa
							prevPos = currPos;
							currPos = ((currPos+incremento)<=0 ? Math.max(1, currPos+incremento) : Math.min(MAXSTEP, currPos+incremento)); 
							prevValue = currValue;
							if(showMessage) IJ.showMessage("DISCESA FIRST\ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"  maxPos="+maxPos+"\npendenza="+pendenza+"  incremento="+incremento+"\ntotFirstBadStep="+totFirstBadStep);
						}
					}
					else if(badStep == MAXBADSTEP){ //questo caso č simile a quello in cui si arriva alla fine o all'inizio dell'escursione 
						//se il num di passi cattivio supera il massimo consentito allora esco
						//e riporto l'obiettivo nella posizione max
						if((totGoodStep <= MINGOODSTEP) && (!dirChanged)){//se abbiamo fatto pochi passi in salita cambiamo direzione  e ricominciamo dal punto di partenza senza perdere il rif al max
							dirChanged = true;
							totGoodStep++;
							badStep = 0; //altrimenti antriamo in un ciclo perchč non riusciamo pių a fare passi
							direzione = (-1)*direzione;
							prevPos = startPos; //prev punta a startPos
							prevValue = startValue; //serve il startValue
							currPos = ((startPos+direzione)<=0 ? Math.max(1, startPos+direzione) : Math.min(MAXSTEP, startPos+direzione)); 
							
							if(showMessage) IJ.showMessage("FINE DISCESA - CAMBIO DIREZIONE\ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"  maxPos="+maxPos+"\npendenza="+pendenza+"  incremento="+incremento+"\ntotFirstBadStep="+totFirstBadStep);
						}
						else{ //num pass buoni sufficianti 
							lastStep=true;
							currPos = maxPos;
							if(showMessage) IJ.showMessage("EXIT\ncurrPos==maxPos=>"+currPos+"  badStep="+badStep);							
						}			
					}
					else{
						incremento = 2*incremento;
						//aggiorno i valori ma non aggiorno il max
						prevPos = currPos;
						currPos = ((currPos+incremento)<=0 ? Math.max(1, currPos+incremento) : Math.min(MAXSTEP, currPos+incremento)); 
						prevValue = currValue;
						badStep++; 
						firstStep = false; 
						totFirstBadStep = 0; //si usa solo la prima volta poi non serve pių
						
						if(showMessage) IJ.showMessage("DISCESA\ncurrStep="+currStep+"  currPos="+prevPos+"  nextPos="+currPos+"  maxValue="+maxValue+"  maxPos="+maxPos+"\npendenza="+pendenza+"  incremento="+incremento);
					}
				}
				currStep++;
				
				if(currStep >= (imp.getNSlices()*2)){ //superato numero massimo iterazioni
					lastStep=true;
					currPos = maxPos;
					if(showMessage) IJ.showMessage("EXIT\ncurrPos >> max Iterazioni");							
				}
				
				//Aggiorno la grafica
				stackWin.showSlice(currPos); 
				
			}while(!exit);//WHILE DI RICERCA
			
			imgStep.setStack("Steps",stackStep);
			if(WindowManager.getFrame("Steps")==null){
				stackWinStep = new StackWindow(imgStep);
				Point locationGM = getLocation();
				Dimension dim = getSize();
				Dimension dimStart = stackWinStart.getSize();
				stackWinStep.setLocation(locationGM.x+dim.width,locationGM.y+dimStart.height);
				IJ.freeMemory();
			}
			else { }
			
			imgStartPoint.setSlice(maxPos);
			imgStep.setSlice(imgStep.getNSlices());
			
			time=System.currentTimeMillis()-time;
			timeLabel.setText("Search time: "+new Double(time/1000.0).toString()+" sec");
			
			IJ.showStatus("Searching completed.");
		}//FINE START
	}//FINE ACTION PERFORMED
}
class ListenerRoi extends MouseAdapter{
	
	private Rectangle oldRect;
	private ImagePlus imp;
	private StackWindow stackWin;
	private GM_Autofocus gm;
		
	ListenerRoi(ImagePlus i, Roi r, GM_Autofocus gm){
		imp=i;
		oldRect = (imp.getRoi()).getBounds();
		stackWin = (StackWindow)imp.getWindow();
		stackWin.addMouseListener(this);
		this.gm = gm;
	}
	
	public void mouseExited(MouseEvent e) {
		Roi roi = imp.getRoi();
		if( !(oldRect.equals(roi.getBounds())  )){
			gm.roiChanged();
		}
		else{ }
		oldRect = roi.getBounds();
	}
}

class SyncSliceStackIndex implements ImageListener{
	
	private ImagePlus source;
	private ImagePlus dest;
	private int lastIndex = 0;
	
	SyncSliceStackIndex(ImagePlus s, ImagePlus d){
		source = s;
		dest = d;
		source.addImageListener(this);
	}
	
	public void imageClosed(ImagePlus imp) {}
	
	public void imageOpened(ImagePlus imp) {}
        
	public void imageUpdated(ImagePlus imp) {
		if(imp == source) {
			int currFoto = imp.getCurrentSlice();
			dest.setSlice(currFoto);
		}
		else{ }
	}         
}

class SyncSliceStackLabel implements ImageListener{
	
	private ImagePlus source;
	private ImagePlus dest;
	private int lastIndex = 0;
	
	SyncSliceStackLabel(ImagePlus s, ImagePlus d){
		source = s;
		dest = d;
		source.addImageListener(this);
	}
          
	public void imageClosed(ImagePlus imp) {}
	public void imageOpened(ImagePlus imp) {}
	public void imageUpdated(ImagePlus imp) {
		if(imp == source) {
			String strNum=(imp.getImageStack()).getSliceLabel(imp.getCurrentSlice()); 
			int currFoto = Integer.parseInt(strNum);
			dest.setSlice(currFoto);
		}
		else{ }
	}         
}

class InfoHelp implements ActionListener {
	
	private String description="HELP ABOUT \"GM Autofocus System\"\n\n"+
		"This plugin implements an autofocus system.\n"+
		"It allows to find the image with the best coefficient of focus in a sequence of images.\n"+
	
		"User have to implement a rectangular selection into the image. In this area we will calculate the coefficent of focus.\n"+
		"After the user have to push the button \"Load\", in this way the plugin will calculate the coefficent of focus into the selection for all the images of the sequence and will display the windows \"Start Point\"\n"+
		"With this window the user can select the start point of the search sliding the cursor.\n"+
		"Only now the user can start the search pushing the button \"Search\". When the search is finished the plugin will show the frame that have the best coefficient of focus.\n"+
		"At this point the user can:\n"+
		" - restart the search from the last point\n"+
		" - start the search from another start point\n"+
		" - change the rectangular selection, reload the coefficient of focus, select the start point and restart the search.\n"+
		"There is only one important constraint: every time you change the selection you have to load the coefficient of focus before start the search.\n"+
		
		"Here is a brief description of the steps to do:\n"+
		"1) Open an image stack or import a sequence of images;\n"+
		"2) Implement a rectangular selection;\n"+
		"3) Click on the button \"Load\" to load the coefficent of focus of all the sequence of images;\n"+
		"4) Select the start point of the search sliding the cursor in the window \"Start Point\";\n"+
		"5) Click on the button \"Search\" to start the search;\n\n"+
		
		"ABOUT THE AUTHOR\n\n"+
		"This plugin was implemented by Nicola Ciraulo under Professor Sebastiano Battiato's supervision.\n"+
		"It was a part of a project for the Multimedia Course (MSC Program in Computer Science) at University of Catania (ITALY)\n\n"+
		"The initial GM is due to the fact that I consecrate every my work to Jesus and Maria.\n\n"+
		
		"CONTACT INFO\n\n"+
		"For further information contact us:\n"+
		"Prof. Battiato Sebastiano, Ph.d. in Computer Science, e-mail: battiato@dmi.unict.it, web: http://www.dmi.unict.it/~battiato\n"+
		"Nicola Ciraulo, BS degree in Computer Science, e-mail: gmcola@gmail.com\n\n"+
		"LINKS\n\n"+
		"IPLab - Image Processing Laboratory - University of Catania (Italy), web: http://www.dmi.unict.it/~iplab\n"+
		"Department of Mathematics and Computer Science - University of Catania (Italy), web: http://www.dmi.unict.it/";
  
	public void actionPerformed(ActionEvent e) {
		StringBuffer sb = new StringBuffer();
		sb.append(description);
		TextWindow tw = new TextWindow("About \"GM Autofocus System\"", sb.toString(), 700, 500);
		tw.show();
	}
}













