package com.elphel.imagej.calibration;
/*
 **
 ** FittingStrategy.java
 **
 ** Copyright (C) 2011-2014 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  FittingStrategy.java is free software: you can redistribute it and/or modify
 **  it under the terms of the GNU General Public License as published by
 **  the Free Software Foundation, either version 3 of the License, or
 **  (at your option) any later version.
 **
 **  This program is distributed in the hope that it will be useful,
 **  but WITHOUT ANY WARRANTY; without even the implied warranty of
 **  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 **  GNU General Public License for more details.
 **
 **  You should have received a copy of the GNU General Public License
 **  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 ** -----------------------------------------------------------------------------**
 **
 */
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;

import com.elphel.imagej.common.WindowTools;

import ij.IJ;
import ij.gui.GenericDialog;
import ij.text.TextWindow;



    /**
     *
     * Specifies images to process and parameters to adjust
     * Each parameter cab be:
     * 0 - "fixed" - use individual, per-image parameters, do not modify them
     * 1 - "common" - parameters are common for all selected images.
     *                When saving - save to all selected images.
     *                When loading (if different) use "master image" (or closest to it in time)
     * 2 - "super common" - same as common, but save to all images, not just selected
     * 3 - "individual"
     * 4 - "per group"
     * 5 - "per-station"
     * 6 - "per-station" save to all (super)
     * 7 - "weak common" - like common, but enable small individual variations (for a price)
     * 8 - "weak station" - like per-station, but enable individual (for a price)
     *
+====================+===========+===========+
|                    |  Same TS  |  Diff TS  |
|                    +-----+-----+-----+-----+
|                    |  C  |  I  |  C  |  I  |
+============+=======+=====+=====+=====+=====+
| Same       |Eyesis |  X  |  X  |  C  |  I  |
| Subcamera  +-------+-----+-----+-----+-----+
|            |Subcam |  X  |  X  |  C  |  I* |
+============+=======+=====+=====+=====+=====+
| Different  |Eyesis |  C  |  C  |  C  |  I  |
| Subcameras +-------+-----+-----+-----+-----+
|            |Subcam |  I  |  I  |  I  |  I  |
+============+=======+=====+=====+=====+=====+
I* - special case when the subcamera is being adjusted/replaced. How to deal with it?

     *
     */
    public  class FittingStrategy{
    	public String pathName=null; // path to XML file this instance was created from
    	public DistortionCalibrationData distortionCalibrationData=null;// per-image parameters
    	private boolean [][] selectedImages=null; // images selected for each step (will be masked with enabled images)
    	private boolean [][] selectedValidImages=null; // images selected for each step same as selected, but only if number of weight>0.0 nodes > threshold
    	public boolean [] stopAfterThis=null;
    	final public static int modeFixed=0,modeCommon=1,modeSupercommon=2,modeIndividual=3,modeGroup=4;
    	final public static int modeStation=5,modeSuperStation=6,modeWeakCommon=7,modeWeakStation=8,modeTiltEqualize=9;
    	public String[] definedModes={
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all",     // modeSuperStation=6
    			"weak common, save to  all",     // modeWeakCommon=7
    			"weak per-station, save to  all" // modeWeakStation=8
    			};
    	public String[] definedModesNoWeak={     // show for parameters that can not use weak
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all"      // modeSuperStation=6
//    			"weak common",              // modeWeakCommon=7
//    			"weak per-station",         // modeWeakStation=8
    			};
    	public String[] definedModesTiltEq={      // show for parameters that can not use weak
    			"fixed",                         // modeFixed=0
    			"common",                        // modeCommon=1
    			"common, save to all",           // modeSupercommon=2
    			"individual",                    // modeIndividual=3
    			"per-group",                     // modeGroup=4
    			"per-station",                   // modeStation=5
    			"per-station, save to  all",     // modeSuperStation=6
    			"weak common, save to  all",     // modeWeakCommon=7
    			"weak per-station, save to  all",// modeWeakStation=8
    			"tilt equalize"                 // modeTiltEqualize
    			};
    	public String[] definedModesAll= definedModesTiltEq;
    	public int [][] parameterMode=null; // per series, per-parameter - see modeFixed...modeTiltEqualize. Higher numbers - mean "same as"
    	// and master subcamera index is calculated by subtracting this.definedModesAll.length

    	public int [][][] parameterGroups=null; // per series, per-parameter - null or array of group numbers (1 element per image)
    	public int [][] zGroups=null;
    	public boolean saveUnusedGroups=false; // purge groups for parameters when saving to XML, preserve if true
    	public double [] lambdas=null;   // LMA initial lambda for each step
    	public double defaultLambda=0.001;
    	public int [][] parameterList=null; // list of all parameters in the system, each has subcamera number and parameters index
    	// [][0] - subcamera (0 also for non-subcamera specific), [][1] - parameter "name"
    	public boolean [] parameterEnable=null; // select which parameters (of some 320 to display in selector)
    	public int [] masterImages=null;        // number of image no take "common" parameters from if they are different
    	public double defaultStepDone=1.0E-6;
    	public double [] stepDone=null;// delta_error to error ratio to consider series finished
//    	public double [] parameterVector;
    	public int [][] parameterMap=null;// ** valid for one strategy sequence *** for each parameter vector element hold image number/parameter number
    	public int [][] reverseParameterMap=null; // reversed map - for each [imageNumber][parameterNumber] -> vector index (-1 fixed)
    	public int currentSeriesNumber=-1;     // currently selected strategy step (for which parameterMap and reverseParameterMap)
    	public int debugLevel=2;
    	public int [] varianceModes=null;      // per-series: 0 - disabled, 1 - constant imageSets weight, 2 - variable imageSets weight
    	final public int varianceModeDisabled=0,varianceModeSameWeight=1,varianceModeVariableWeight=2;
    	public String [] definedVarianceModes={
    			"disabled",
    			"same weight",
    			"variable weight",
    	};

    	// next arrays will be initialized at buildVariancesMaps only if at least some parameters use variances, otherwise they will be null
    	public double [] variationsAverages=null; // holds per extrinsic parameter or per parameter/per station average values
    	public int    [] averageCellIndex=  null; // for each element in the parameters vector holds index in  variationsAverages array
    	public double [] weightOnAverage=   null; // how variation of this parameter influences average (for all or for currenrt station
    	public double [] weightVariance=    null; // weight for LMA over variance of parameters from average
    	public double [] varianceErrorsSquared=  null; // weighted squared errors

    	public String [] strategyComment = null;

    	public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData// per-image parameters
    			) {
    		this.distortionCalibrationData=distortionCalibrationData;
    		setDflt(0);
    	}
		public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData,// per-image parameters
    	    	int numSeries  // number of iteration series
    			){
    		this.distortionCalibrationData=distortionCalibrationData;
    		this.selectedImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];
    		this.selectedValidImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];

    		setDflt(numSeries);
    	}

	   	public FittingStrategy(
        		boolean smart,
        		String defaultPath,
    	    	DistortionCalibrationData distortionCalibrationData // per-image parameters
    			) {
			String [] extensions={".stg-xml","-strategy.xml"};
			MultipleExtensionsFileFilter parFilter = new MultipleExtensionsFileFilter("",extensions,"*.stg-xml files");
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					false,
					"Restore Fitting Strategy",
					"Restore",
					parFilter,
					defaultPath); //String defaultPath
			if ((pathname==null) || (pathname=="")) return;
	   		setFromXML(
	    	    	distortionCalibrationData, // per-image parameters
	    	    	pathname);
			System.out.println("Restored fitting strategy from "+pathname);
	   	}


    	/**
    	 * Reads FittingStrategy from an XML file
    	 * @param distortionCalibrationData should be defined before this class!
    	 * @param pathname pathe to the saved data
    	 */
	   	public FittingStrategy(
    	    	DistortionCalibrationData distortionCalibrationData, // per-image parameters
    	    	String pathname
    			) {
	   		setFromXML(
	    	    	distortionCalibrationData, // per-image parameters
	    	    	pathname);
	   	}
    	public void setFromXML(
    	    	DistortionCalibrationData distortionCalibrationData, // per-image parameters
    	    	String pathname
    			) {
    		this.distortionCalibrationData=distortionCalibrationData;
        	XMLConfiguration hConfig=null;
        	try {
				hConfig=new XMLConfiguration(pathname);
				hConfig.setListDelimiter((char) 0);
				hConfig.setDelimiterParsingDisabled(true);
				hConfig.setAttributeSplittingDisabled(true);

			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			hConfig.setThrowExceptionOnMissing(false); // default value, will return null on missing
// read parameterList
			int len=Integer.parseInt(hConfig.getString("parameterMap.length","0"));
			if (len<=0) {
				String msg="No parameterMap specified in "+ pathname;
				IJ.showMessage("Error",msg);
				throw new IllegalArgumentException (msg);
			}
	    	this.parameterList=new int [len][2];
	    	this.parameterEnable=new boolean[len];
    		for (int i=0; i<this.parameterList.length; i++){
    			this.parameterList[i][0]=Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".subcamera"));
    			this.parameterList[i][1]=Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".index"));
    			this.parameterEnable[i]= (Integer.parseInt(hConfig.getString("parameterMap.par_"+i+".visible"))>0);
    		}
			int nSer=Integer.parseInt(hConfig.getString("series.number","0"));
			if (nSer==0) return; // arrays will just be null
			this.selectedImages=new boolean[nSer][];
			this.selectedValidImages=new boolean[nSer][];
			this.masterImages=new int[nSer];
			this.stopAfterThis=new boolean[nSer];
			for (int i=0;i<nSer;i++) this.stopAfterThis[i]=true; // older configuration files did not have it
			this.parameterMode=new int[nSer][len];
			this.varianceModes=new int[nSer];
			this.zGroups=new int [nSer][];
			this.parameterGroups=new int[nSer][len][];
			this.lambdas=new double  [nSer];
			this.stepDone=new double [nSer];
			this.strategyComment=new String[nSer];
        	for (int i=0;i<nSer;i++){ // iterate through series
//this.stopAfterThis
// read selected images (no check here that it matches to the    distortionCalibrationData!
        		String sSeries="series.series_"+i;
        		String fs=hConfig.getString(sSeries+".selectedImages");
        		this.selectedImages[i]=new boolean[fs.length()];
        		this.selectedValidImages[i]=null;
        		for (int j=0;j<this.selectedImages[i].length;j++) this.selectedImages[i][j]=(fs.charAt(j)=='+');
    			this.masterImages[i]=Integer.parseInt(hConfig.getString(sSeries+".masterImage"));
    			if (hConfig.getString(sSeries+".varianceModes")!=null) {
    				this.varianceModes[i]=Integer.parseInt(hConfig.getString(sSeries+".varianceModes"));
    			} else {
    				this.varianceModes[i]=varianceModeDisabled;
    			}

        		for (int j=0;j<this.parameterList.length;j++){
        			int nPar=this.parameterList[j][1];
        			int nSub=this.parameterList[j][0];
        			String hconfigName=sSeries+".parameterMode."+
    				this.distortionCalibrationData.parameterDescriptions[nPar][0]+ // here intentionally left parameterDescriptions[][], not descrField
    				// so switching Cartesian on/off will not make make strategy invalid
    				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"");
//        			System.out.println("Setting this.parameterMode["+i+"]["+j+"] from " +hconfigName);
        			if (hConfig.getString(hconfigName)!=null)  this.parameterMode[i][j]=Integer.parseInt(hConfig.getString(hconfigName));
        			else System.out.println("Failed to set this.parameterMode["+i+"]["+j+"] from " +hconfigName+" - maybe it is a new parameter not present in the file "+pathname);
        			this.parameterGroups[i][j]=null;
//Try to read series
        			for (int ni=0;ni<this.selectedImages.length;ni++){
        				String sGroup=hConfig.getString(sSeries+".parameterMode."+
                				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
                				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"")+
                				"_group"+ni);
        				if (sGroup!=null){
        					if (this.parameterGroups[i][j]==null){
        						this.parameterGroups[i][j]=new int[this.selectedImages.length];
        						for (int ni1=0;ni1<this.selectedImages.length;ni1++)this.parameterGroups[i][j][ni1]=0;
        					}
        					this.parameterGroups[i][j][ni]=Integer.parseInt(sGroup);
        				}
        			}
        		}
        		this.lambdas[i]=Double.parseDouble(hConfig.getString(sSeries+".lambdas"));
        		this.stepDone[i]=Double.parseDouble(hConfig.getString(sSeries+".stepDone"));
        		if (hConfig.getString(sSeries+".stopAfterThis")!=null){
        			this.stopAfterThis[i]=Boolean.parseBoolean(hConfig.getString(sSeries+".stopAfterThis"));
        		}
    			String zGroupsName=sSeries+".zGroups";
    			if (hConfig.getString(zGroupsName)!=null) {
    				int numZgroups=Integer.parseInt(hConfig.getString(zGroupsName));
    				this.zGroups[i]=new int [numZgroups];
    				for (int nZGroup=0;nZGroup<numZgroups;nZGroup++){
    					this.zGroups[i][nZGroup]=Integer.parseInt(hConfig.getString(sSeries+".zGroup_"+nZGroup));
    				}
    			} else {
    				this.zGroups[i]=null;
    			}

    			if (hConfig.getString(sSeries+".strategyComment")!=null) {
    				this.strategyComment[i]=hConfig.getString(sSeries+".strategyComment");
    				if ((this.strategyComment[i].length()>10) && this.strategyComment[i].substring(0,9).equals("<![CDATA["))
    					this.strategyComment[i]=this.strategyComment[i].substring(9,this.strategyComment[i].length()-3);
    			}
        	}
//        	if (hConfig.getString("variances")!=null){
//            if (!hConfig.configurationAt("variances").isEmpty()){
       		if (hConfig.configurationsAt("variances").size()>0){
//        		System.out.println("hConfig.configurationAt(\"variances\").isEmpty()=false");
        		this.distortionCalibrationData.eyesisCameraParameters.getCostsPropertiesXML("variances.",hConfig);
        	}
        	this.pathName=pathname;
//        	distortionCalibrationData.readAllGrids();
    	}

/**
 * Adjust  selectedImages and parameterGroups after number of images may change, old number of images should be >0
 * If the new number is larger, the first images attributes will be copied, the extra ones - use those of the last of the old ones
 * @param newNumberOfImages new number of images used by this series
 */
    	public void adjustNumberOfImages(int newNumberOfImages){
    		if ((this.selectedImages == null) ||(this.selectedImages[0].length==0)) {
    			String msg="selectedImages array is "+((this.selectedImages == null)?"null":"empty");
    			IJ.showMessage("Error",msg);
    			throw new IllegalArgumentException (msg);
    		}
    		if (this.selectedImages[0].length==newNumberOfImages) return;
    		for (int nSer=0;nSer<this.selectedImages.length;nSer++){
    			boolean [] tmp=selectedImages[nSer].clone();
    			this.selectedImages[nSer]=new boolean[newNumberOfImages];
    			this.selectedValidImages[nSer]=null; // just invalidate
    			for (int i=0;i<newNumberOfImages;i++) this.selectedImages[nSer][i]=(i<tmp.length)?tmp[i]:tmp[tmp.length-1];
    			for (int nPar=0;nPar<this.parameterList.length;nPar++){
    				if (this.parameterGroups[nSer][nPar]!=null){
    					int [] tmpGroup=this.parameterGroups[nSer][nPar].clone();
    					this.parameterGroups[nSer][nPar]=new int[newNumberOfImages];
    	    			for (int i=0;i<newNumberOfImages;i++) this.parameterGroups[nSer][nPar][i]=(i<tmpGroup.length)?tmpGroup[i]:tmpGroup[tmpGroup.length-1];
    				}
    			}

    		}
    	}

    	public String selectAndSaveToXML(
    			boolean smart,
    			String defaultPath){
			String [] extensions={".stg-xml","-strategy.xml"};
			MultipleExtensionsFileFilter parFilter = new MultipleExtensionsFileFilter("",extensions,"*.stg-xml files");
			if ((defaultPath==null) || (defaultPath.length()==0)){
				defaultPath=this.pathName;
			}
			String pathname=CalibrationFileManagement.selectFile(
					smart,
					true,
					"Save Fitting Strategy",
					"Save",
					parFilter,
					defaultPath); //String defaultPath
			if (pathname!=null) saveToXML(pathname);
			return pathname;

    	}
    	  //http://commons.apache.org/configuration/userguide/howto_xml.html
        public boolean saveToXML(String pathname) {
        	if (pathname==null) return false;
        	XMLConfiguration hConfig=new XMLConfiguration();

        	hConfig.setListDelimiter((char) 0);
			hConfig.setDelimiterParsingDisabled(true);
			hConfig.setAttributeSplittingDisabled(true);

        	hConfig.setRootElementName("FittingStrategy");
//        	write which of the overall parameter correspond to each global/subcamera one, and which parameters are hidden/shown
    		hConfig.addProperty("parameterMap","");
    		hConfig.addProperty("parameterMap.length",this.parameterList.length);
    		for (int i=0; i<this.parameterList.length; i++){
        		hConfig.addProperty("parameterMap.par_"+i,"");
        		hConfig.addProperty("parameterMap.par_"+i+".subcamera",this.parameterList[i][0]);
        		hConfig.addProperty("parameterMap.par_"+i+".index",this.parameterList[i][1]);
        		hConfig.addProperty("parameterMap.par_"+i+".visible",this.parameterEnable[i]?"1":"0");
    		}

    		hConfig.addProperty("series","");
    		hConfig.addProperty("series.number",this.selectedImages.length);

        	for (int i=0;i<this.selectedImages.length;i++){ // iterate through series
        		String sSeries="series.series_"+i;
        		hConfig.addProperty(sSeries,"");
        		String fs="";
        		for (int j=0;j<this.selectedImages[i].length;j++)fs+=this.selectedImages[i][j]?"+":"-";
        		hConfig.addProperty(sSeries+".selectedImages",fs);
        		hConfig.addProperty(sSeries+".masterImage",this.masterImages[i]);
        		if (this.varianceModes!=null) hConfig.addProperty(sSeries+".varianceModes",this.varianceModes[i]);
        		hConfig.addProperty(sSeries+".parameterMode","");
        		for (int j=0;j<this.parameterList.length;j++){
        			int nPar=this.parameterList[j][1];
        			int nSub=this.parameterList[j][0];
            		hConfig.addProperty(sSeries+".parameterMode."+
            				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
            				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):""),
            				this.parameterMode[i][j]);
// cleaning up output - removing unused groups (may disable
            		if (((this.parameterMode[i][j]==modeGroup) || this.saveUnusedGroups ) &&  (this.parameterGroups[i][j]!=null)){
            			for (int ni=0;ni<this.parameterGroups[i][j].length;ni++){
                    		hConfig.addProperty(sSeries+".parameterMode."+
                    				this.distortionCalibrationData.parameterDescriptions[nPar][0]+
                    				(this.distortionCalibrationData.isSubcameraParameter(nPar)?("_sub"+nSub):"") +"_group"+ni,
                    				this.parameterGroups[i][j][ni]);
            			}
            		}
        		}
        		if (this.zGroups[i]!=null) {
        			hConfig.addProperty(sSeries+".zGroups", this.zGroups[i].length);
    				for (int nZGroup=0;nZGroup<this.zGroups[i].length;nZGroup++){
    					hConfig.addProperty(sSeries+".zGroup_"+nZGroup,this.zGroups[i][nZGroup]);
    				}
        		}
        		hConfig.addProperty(sSeries+".lambdas",lambdas[i]);
        		hConfig.addProperty(sSeries+".stepDone",stepDone[i]);
        		hConfig.addProperty(sSeries+".stopAfterThis",this.stopAfterThis[i]);

            	if (this.strategyComment[i]!=null){
            		String comment_esc=this.strategyComment[i].replace(",","\\,");
//            		comment_esc=comment_esc.replace("/","\\/");
            		//          hConfig.addProperty("comment","<![CDATA["+comment_esc+ "]]>");
            		hConfig.addProperty(sSeries+".strategyComment",comment_esc);
            	}
//        		hConfig.addProperty(sSeries+".strategyComment","<![CDATA["+this.strategyComment[i]+"]]>");
//        		hConfig.addPropertyDirect(sSeries+".strategyComment","<![CDATA["+this.strategyComment[i]+"]]>");
//        		hConfig.setProperty(sSeries+".strategyComment","<![CDATA["+this.strategyComment[i]+"]]>");
        	}
        	hConfig.addProperty("variances","");
       		this.distortionCalibrationData.eyesisCameraParameters.setCostsPropertiesXML("variances.",hConfig);
        	File file=new File (pathname);
        	BufferedWriter writer;
			try {
				writer = new BufferedWriter(new FileWriter(file));
	        	hConfig.save(writer);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			} catch (ConfigurationException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			this.pathName=pathname;
        	return true;
        }
        public int findLastValidSeries(int numSeries){
        	for (;numSeries>=0;numSeries--) if (isSeriesValid(numSeries)) return numSeries;
        	return -1; // none valid
        }
        public boolean copySeries(int sourceSeries, int numSeries){
     		if (isSeriesValid(sourceSeries)){
    				invalidateSelectedImages(numSeries); // just in case -= will be recalculated
    				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
    					this.selectedImages[numSeries][i]=this.selectedImages[sourceSeries][i]; // copy for all, not only enabled
    				}
    				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i]){
    					this.parameterMode[numSeries][i]=this.parameterMode[sourceSeries][i];
    					this.parameterGroups[numSeries][i]=(this.parameterGroups[sourceSeries][i]==null)?null:this.parameterGroups[sourceSeries][i].clone();
    				}
    				this.masterImages[numSeries]=this.masterImages[sourceSeries];
    				this.lambdas[numSeries]=this.lambdas[sourceSeries];
    				this.stepDone[numSeries]=this.stepDone[sourceSeries];
    				this.stopAfterThis[numSeries]=this.stopAfterThis[sourceSeries];
    				this.varianceModes[numSeries]=this.varianceModes[sourceSeries];
    				this.zGroups[numSeries]=(this.zGroups[sourceSeries]!=null)?this.zGroups[sourceSeries].clone():null;
    				this.strategyComment[numSeries] = this.strategyComment[sourceSeries]+" (copy)";
    				return true;
    		} else {
    			return false;
    		}
        }



        /**
         * Verifies that series exists and is not empty (valid for LMA)
         * @param num - series to verify
         * @return true if series is valid
         */
    	public boolean isSeriesValidVerbose(int num){
    		if ((num<0) || (num>=this.selectedImages.length)) return false;
    		boolean someImagesSelected=false;
    		for (int i=0;i<this.selectedImages[num].length;i++) if (this.selectedImages[num][i]){
    			someImagesSelected=true;
    			break;
    		}
    		if (!someImagesSelected) {
    			if (this.debugLevel>0) System.out.println("isSeriesValid("+num+"): no images selected");
    			return false;
    		}
    		boolean someParametersSelected=false;
    		for (int i=0;i<this.parameterMode[num].length;i++) if (this.parameterMode[num][i]>0){
    			someParametersSelected=true;
    			break;
    		}
    		if (!someParametersSelected) {
    			if (this.debugLevel>0) System.out.println("isSeriesValid("+num+"): no parameters selected");
    			return false;
    		}
    		return true;
    	}

    	public boolean isSeriesValid(int num){
    		if ((num<0) || (num>=this.selectedImages.length)) return false;
    		boolean someImagesSelected=false;
    		for (int i=0;i<this.selectedImages[num].length;i++) if (this.selectedImages[num][i]){
    			someImagesSelected=true;
    			break;
    		}
    		if (!someImagesSelected) {
    			return false;
    		}
    		boolean someParametersSelected=false;
    		for (int i=0;i<this.parameterMode[num].length;i++) if (this.parameterMode[num][i]>0){
    			someParametersSelected=true;
    			break;
    		}
    		if (!someParametersSelected) {
    			return false;
    		}
    		return true;
    	}
    	/**
    	 * Determines if LMA should stop after this series (either flag is set or next is invalid)
    	 * @param num number of series to test
    	 * @return true if LMA should stop
    	 */
    	public boolean isLastSeries(int num){
    		if (this.stopAfterThis[num]) return true;
    		if (!isSeriesValid(num+1)) return true;
    		return false;
    	}

        /**
         *
         * Specifies images to process and parameters to adjust
         * Each parameter cab be:
         * 0 - "fixed" - use individual, per-image parameters, do not modify them
         * 1 - "common" - parameters are common for all selected images.
         *                When saving - save to all selected images.
         *                When loading (if different) use "master image" (or closest to it in time)
         * 2 - "super common" - same as common, but save to all images, not just selected
         * 3 - "individual"
    +====================+===========+===========+
    |                    |  Same TS  |  Diff TS  |
    |                    +-----+-----+-----+-----+
    |                    |  C  |  I  |  C  |  I  |
    +============+=======+=====+=====+=====+=====+
    | Same       |Eyesis |  X  |  X  |  C  |  I  |
    | Subcamera  +-------+-----+-----+-----+-----+
    |            |Subcam |  X  |  X  |  C  |  I* |
    +============+=======+=====+=====+=====+=====+
    | Different  |Eyesis |  C  |  C  |  C  |  I  |
    | Subcameras +-------+-----+-----+-----+-----+
    |            |Subcam |  X  |  X  |  X  |  X  | subcameras for different subcameras are individual parameters
    +============+=======+=====+=====+=====+=====+
    I* - special case when the subcamera is being adjusted/replaced. How to deal with it?

         *
         */

 //    	public int [][] reverseParameterMap=null; // reversed map - for each [imageNumber][parameterNumber] -> vector index (-1 fixed)\
    	// selects all enabled images in the specified series (modifies strategy series!)
    	/**
    	 * selects all enabled images in the specified series (modifies strategy series!)
    	 * @param ser number of fitting strategy series to set
    	 * @return old value for series images selection
    	 */
    	public boolean [] selectAllImages(int ser){
 //   		this.selectedValidImages[ser]=null; // just invalidate - do nothing, so restore would work
    		boolean [] oldSelection=this.selectedImages[ser].clone();
			for (int i=0;i<this.selectedImages[ser].length;i++) this.selectedImages[ser][i]=true; // select all\
			return oldSelection;
    	}
    	/**
    	 * Sets image selection. Can be used to restore saved selection after selectAllImages(int ser)
    	 * @param ser number of fitting strategy series to set
    	 * @param selection array specifying selected images
    	 */
    	public void setImageSelection(int ser, boolean [] selection){
    		this.selectedImages[ser]=selection.clone();
    	}
    	public boolean [] selectedAllImages() {
//    		if (this.currentSeriesNumber<0) return null;
			return selectedAllImages(this.currentSeriesNumber);
		}
    	public boolean [] selectedAllImages(int ser) {
    		if ((ser<0) || (ser>=this.selectedImages.length)){
    			boolean [] allImages=new boolean[this.selectedImages[0].length];
    			for (int i=0;i<allImages.length;i++) allImages[i]=true; // select all
    			return allImages;
    		} else {
        		return this.selectedImages[ser];
    		}
		}
    	public boolean [] selectedImages() {
//    		if (this.currentSeriesNumber<0) return null;
			return selectedImages(this.currentSeriesNumber);
		}
    	public boolean [] selectedImagesNoBadKernels(int ser) {
    		boolean [] selected=selectedImages(ser);
    		for (int i=0;i<selected.length;i++) selected[i]&= !this.distortionCalibrationData.gIP[i].noUsefulPSFKernels;
    		return selected;
    	}
    	public void setNoUsefulPSFKernels(int i, boolean noUsefulPSFKernels){
    		this.distortionCalibrationData.gIP[i].noUsefulPSFKernels=noUsefulPSFKernels;
    	}
    	public void invalidateSelectedImages(int ser){
    		this.selectedValidImages[ser]=null;
    	}
    	public void initSelectedValidImages(int ser){
    		this.selectedValidImages[ser]=this.selectedImages[ser].clone();
    	}
    	public void invalidateSelectedImage(int ser,int nImg){
    		this.selectedValidImages[ser][nImg]=false;
    	}

    	//this.selectedValidImages
    	public boolean [] selectedImages(int ser) {
    		return selectedImages(ser,false);
    	}
    	public boolean [] selectedImages(int ser, boolean userSelection) {
    		boolean [] selectedMasked;
    		if ((ser<0) || (ser>=this.selectedImages.length)){
    			selectedMasked=new boolean[this.selectedImages[0].length];
    			for (int i=0;i<selectedMasked.length;i++) selectedMasked[i]=true; // select all
    		} else {
    			selectedMasked=this.selectedImages[ser].clone();
    		}
    		for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= this.distortionCalibrationData.gIP[i].enabled;
    		// unselect empty
    		// TODO: add minimal number of nodes?
			if ((ser>=0) && (this.selectedValidImages[ser]!=null) && !userSelection){
	    		for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= this.selectedValidImages[ser][i];
			} else {
				for (int i=0;i<selectedMasked.length;i++) selectedMasked[i] &= (this.distortionCalibrationData.gIP[i].pixelsXY.length>0);
			}
    		return selectedMasked; //this.selectedImages[ser];
    	}
    	public int getNumSeries(){
    		return this.selectedImages.length;
    	}

       	/**
    	 * Creates map from the parameter vector index to the {grid image number, parameter number}
    	 * When the parameter is shared by several images, the map points to the one which value will be used
    	 * (they might be different). Timestamp of the masterImages[] is used to determine which image to use.
    	 * Simultaneously creates this.reverseParameterMap that maps each of the image/parameter to the parameter vector
    	 * Needs to be run for each new strategy series
    	 * @param numSeries number of fitting strategy series
    	 * @return this.parameterMap
    	 */

    	public int  buildParameterMap (int numSeries){
    		return buildParameterMap (numSeries, true);
    	}
       	public int  buildParameterMap (int numSeries, boolean mark_disabled){
    		int numPars=this.distortionCalibrationData.getNumParameters();
    		int numImg=this.distortionCalibrationData.getNumImages();
    		int numTPars=this.parameterMode[numSeries].length;
    		this.reverseParameterMap=new int [numImg][numPars];
//			boolean [] selectedEnabledImagesAll=selectedImages(numSeries,true); // strictly as in series, including no valid points ones
			boolean [] selectedEnabledImages=selectedImages(numSeries);
// set defaults - -1 - "fixed", use individual parameter from this image
    		for (int i=0;i<numImg;i++) for (int j=0;j<numPars;j++) this.reverseParameterMap[i][j]=-1;
    		int vectorIndex=0;
//    		int [][] tmpMap=new int[numTPars][3]; // temporary array parameterMap[][] (will be truncated)
    		int [][] tmpMap=new int[numPars*numImg][3]; // temporary array parameterMap[][] (will be truncated)
			double masterTS=this.distortionCalibrationData.getImageTimestamp(this.masterImages[numSeries]); // timestamp of the master image
			int [] masterVectorIndices = new int [numTPars]; //[855] 
			// all but [4][21] are 0,[4][21] is 2 (modeSupercommon),
			// [4][63,105,147,189,231,273,315,357,399,441,483,525,567,609,651,]=10
			// [4][693]= 2 EO(==16)?
			// [4][733,777,819]= 26
// iterate through all global/subcamera parameters
    		for (int numTPar=0;numTPar<numTPars;numTPar++) if (this.parameterMode[numSeries][numTPar]!=modeFixed){ // skip "fixed"
    			boolean isCommon=
    				(this.parameterMode[numSeries][numTPar]==modeCommon) ||
    				(this.parameterMode[numSeries][numTPar]==modeSupercommon);
    			boolean isStation=
    				(this.parameterMode[numSeries][numTPar]==modeStation) ||
    				(this.parameterMode[numSeries][numTPar]==modeSuperStation);
    			boolean isGroup=(this.parameterMode[numSeries][numTPar]==modeGroup);
    			int numSub=this.parameterList[numTPar][0]; // number of sub-camera for this total parameter index
    			int numPar=this.parameterList[numTPar][1]; // number of per-image parameter for this total parameter index
    			boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(numPar);
    			// is it "same as" ?
    			int sameAs = this.parameterMode[numSeries][numTPar] - this.definedModesAll.length; // number of subcamera to use (this.definedModesAll.length==10)
    			int numTParMaster = -1;
    			int vectorIndexMaster = -1;
    			if (sameAs < 0  ) {
    				sameAs = -1;
    			} else {
    				for (numTParMaster = 0; numTParMaster < numTPar; numTParMaster++){
    					if ((this.parameterList[numTParMaster][0] == sameAs) && (this.parameterList[numTParMaster][1] == numPar)) break;
    				}
    				// find master index for this parameter
    				if (numTParMaster == numTPar) { // bug, report and treat as fixed
    					System.out.println("Bug in buildParameterMap("+numSeries+"): could not find master parameter for numTPar="+numTPar+", sameAs="+sameAs);
    					System.out.println("Treaing as fixed");
    					continue;
    				}
    				vectorIndexMaster = masterVectorIndices[numTParMaster]; // use as vectorIndex for slaves
    			}

    			if (this.debugLevel > 2) System.out.println("numTPa = "+numTPar+" numSub="+numSub+" numPar="+numPar+" numTParMaster="+numTParMaster);
// iterate through available images
    			for (int numThisImg=0;numThisImg<numImg;numThisImg++) {
//    				if ((selectedEnabledImages[numThisImg]) &&
//    						(!isSubCamera || (numSub==this.distortionCalibrationData.getImageSubcamera(numThisImg))) &&
//    						(this.reverseParameterMap[numThisImg][numPar]<0)){ // image used, this cell is not (yet) defined
    					
        			if((!isSubCamera || (numSub==this.distortionCalibrationData.getImageSubcamera(numThisImg))) &&
        						(this.reverseParameterMap[numThisImg][numPar]<0)){ // image used, this cell is not (yet) defined
    					if (this.debugLevel>2){
    						System.out.println("buildParameterMap("+numSeries+"): numThisImg="+numThisImg+", numPar="+numPar+", vectorIndex="+vectorIndex);
    					}
// is it "same as" ? // "same as" is only applicable to subcamera parameters
    					if (vectorIndexMaster >= 0){
    						if (mark_disabled || selectedEnabledImages[numThisImg]) {
    							this.reverseParameterMap[numThisImg][numPar] = vectorIndexMaster;
    						}
    						// does not look for same parameter in later images - leave it for the master?
    					} else {
    						if (selectedEnabledImages[numThisImg]) {
// assign it a new parameter
    							this.reverseParameterMap[numThisImg][numPar]=vectorIndex;
    							masterVectorIndices[numTPar] = vectorIndex;
    							double thisTS=this.distortionCalibrationData.getImageTimestamp(numThisImg);
    							int thisStation=this.distortionCalibrationData.gIP[numThisImg].getStationNumber();
// set pointer to this first image
    							tmpMap[vectorIndex][0]=numThisImg; // vectorindex==22 > tmpMap.length?
    							tmpMap[vectorIndex][1]=numPar;
    							tmpMap[vectorIndex][2]=this.parameterMode[numSeries][numTPar];
    							double minDist=Math.abs(this.distortionCalibrationData.getImageTimestamp(numThisImg)-masterTS);
    							if (this.debugLevel>2) System.out.println("vectorIndex="+vectorIndex+" numThisImg="+numThisImg);
// see if same parameter in some other (later) image(s) is shared
//    							for (int numOtherImg=numThisImg + 1; numOtherImg < numImg; numOtherImg++) {
    							boolean proc_disabled = mark_disabled && (isCommon || isStation || isGroup);
       							for (int numOtherImg=0; numOtherImg < numImg; numOtherImg++) {
       								// 06.03.2021: old way for enabled images - only assign to later images
       								// disabled or unselected images - assign to even earlier ones
   								    if ((selectedEnabledImages[numOtherImg] && (numOtherImg > numThisImg)) || (proc_disabled && !selectedEnabledImages[numOtherImg])) {
//   								    	if ((selectedEnabledImages[numOtherImg]) && // OOB 1
							    		if ((!isSubCamera || (numSub==this.distortionCalibrationData.getImageSubcamera(numOtherImg))) &&
   								    			(this.reverseParameterMap[numOtherImg][numPar]<0)){ // image used, this cell is not (yet) defined
   								    		if ((this.distortionCalibrationData.getImageTimestamp(numOtherImg)==thisTS) ||  // same parameter same timestamp - same group even if is set differently
   								    				(isStation && (this.distortionCalibrationData.gIP[numOtherImg].getStationNumber()==thisStation)) || // new
   								    				isCommon ||
   								    				(isGroup && (this.parameterGroups[numSeries][numTPar][numThisImg]==
   								    				this.parameterGroups[numSeries][numTPar][numOtherImg]))){
// assign it a the same parameter
   								    			this.reverseParameterMap[numOtherImg][numPar]=vectorIndex;
   								    			double thisDist=Math.abs(this.distortionCalibrationData.getImageTimestamp(numThisImg)-masterTS);
   								    			if (thisDist<minDist) {
   								    				minDist=thisDist;
   								    				tmpMap[vectorIndex][0]=numOtherImg;
   								    			}
   								    		}
   								    	}
   								    }
       							}
    							vectorIndex++;
    						}
    					}
    				}
    			}
// second pass    			
    			

    		}
    		// reverseParameterMap built,   vectorIndex equals to the total number of parameters needed for fitting
    		//truncate tmpMap into this.parameterMap[][]
    		this.parameterMap=new int[vectorIndex][];
    		for (int i=0;i<vectorIndex;i++) this.parameterMap[i] =tmpMap[i];
    		this.currentSeriesNumber=numSeries;
		   	if (this.debugLevel>2) System.out.println("this.parameterMap.length="+this.parameterMap.length);
    		return this.parameterMap.length;
    	}

    	/**
    	 * Prepare data for calculating additional LMA terms for parameter variances
    	 * @param numSeries fitting series to use
    	 * @return number of parameters for which variance is considered
    	 */
    	public int  buildVariancesMaps (
    			int numSeries){
    		if (this.debugLevel>0){
    			System.out.println("buildVariancesMaps("+numSeries+")");
    		}
    		if ((this.varianceModes==null) || (this.varianceModes[numSeries]==varianceModeDisabled)){
    				this.averageCellIndex=null;
    				this.variationsAverages=null;
    				this.weightVariance=null;
    				this.varianceErrorsSquared=null;
    				return 0;

    		}
    		int debugThreshold=2;
    		boolean useSetWeights=(this.varianceModes[numSeries]==varianceModeVariableWeight);
    		int numPars=this.distortionCalibrationData.getNumParameters();
    		int numStations=this.distortionCalibrationData.eyesisCameraParameters.getNumStations();
//    		int numTPars=this.parameterMode[numSeries].length;
    		int numSeriesPars=this.parameterMap.length;
			// count tilt positions per station
			List<Integer>  tiltList= new ArrayList<Integer>(1000);
			Integer iStationTilt;
			int tiltMotorIndex=2;
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				if (this.parameterMap[numSPar]==null){
					System.out.println("buildVariancesMaps() BUG - ,this.parameterMap["+numSPar+"]==null, numSeriesPars="+numSeriesPars);
					continue;
				}
				int imgNumber=this.parameterMap[numSPar][0]; // null pointer for triclops after adding variances for tilt
				int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
				int station=  this.distortionCalibrationData.gIP[imgNumber].getStationNumber();
				if (this.distortionCalibrationData.gIS[setNumber]==null){
					System.out.println("buildVariancesMaps() BUG - ,this.distortionCalibrationData.gIS["+setNumber+"]==null, numSeriesPars="+numSeriesPars+
							" numSPar="+numSPar+" imgNumber="+imgNumber+" station="+station);
					continue;
				}
				if (this.distortionCalibrationData.gIS[setNumber].motors==null){
					System.out.println("buildVariancesMaps() BUG - ,this.distortionCalibrationData.gIS["+setNumber+"].motors==null, numSeriesPars="+numSeriesPars+
							" numSPar="+numSPar+" imgNumber="+imgNumber+" station="+station);
					continue;
				}


				int tiltMotor=this.distortionCalibrationData.gIS[setNumber].motors[tiltMotorIndex];// null pointer for triclops after adding variances for tilt


				iStationTilt=station+numStations*tiltMotor;
				if (!tiltList.contains(iStationTilt)) tiltList.add(iStationTilt);
			}
			int numStationTilts=tiltList.size();

			int [] numGroups=new int [numPars];
			for (int i=0;i<numGroups.length;i++)numGroups[i]=0;
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int parIndex=this.parameterMap[numSPar][1];
//				if ((numGroups[parIndex]==0) && (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))){
				if (numGroups[parIndex]==0){
					boolean isSet=this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex);
					switch (this.parameterMap[numSPar][2]){
					case modeWeakCommon:
						if (isSet) numGroups[parIndex]=1;
						if (this.debugLevel>debugThreshold) System.out.println(">>>1 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					case modeWeakStation:
						if (isSet) numGroups[parIndex]=numStations;
						if (this.debugLevel>debugThreshold) System.out.println(">>>2 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					case modeTiltEqualize:
						if (isSet) numGroups[parIndex]=numStationTilts;
						if (this.debugLevel>debugThreshold) System.out.println(">>>3 "+numSPar+":"+parIndex +" - "+numGroups[parIndex]+" isSet="+isSet);
						break;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() numGroups:");
    			for (int i=0;i<numGroups.length;i++) if (numGroups[i]>0) System.out.println("--- "+i+":"+numGroups[i]);
    		}

			int startIndex=0;
			for (int i=0;i<numGroups.length;i++){
				if (numGroups[i]>0){
					int num=numGroups[i];
					numGroups[i]=startIndex;
					startIndex+=num;
				} else {
					numGroups[i]=-1;
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() start indices:");
    			for (int i=0;i<numGroups.length;i++) if (numGroups[i]>=0) System.out.println("--- "+i+":"+numGroups[i]);
    		}

    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() startIndex="+startIndex);
    		}

			if (startIndex==0){ // no variance parameters defined
				this.averageCellIndex=null;
				this.variationsAverages=null;
				this.weightVariance=null;
				this.varianceErrorsSquared=null;
				return 0;
			}

			// Assign average cell indices for each vector element, for each paramter it can be just 1 for global, numStations or number of tilt positions
			this.averageCellIndex=new int [numSeriesPars];
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				this.averageCellIndex[numSPar]=-1; // not applicable
				int parIndex=this.parameterMap[numSPar][1];
//			for (int numTPar=0;numTPar<numTPars;numTPar++) {
//				int parIndex=this.parameterList[numTPar][1];
				if ((numGroups[parIndex]>=0) && (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))){
					int imgNumber=this.parameterMap[numSPar][0];
					int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
					int station=  this.distortionCalibrationData.gIP[imgNumber].getStationNumber();

					switch (this.parameterMap[numSPar][2]){
					case modeWeakCommon:
						this.averageCellIndex[numSPar]=numGroups[parIndex];
						break;
					case modeWeakStation:
						this.averageCellIndex[numSPar]=numGroups[parIndex]+station;
						break;
					case modeTiltEqualize:
						int tiltMotor=this.distortionCalibrationData.gIS[setNumber].motors[tiltMotorIndex];
						iStationTilt=station+numStations*tiltMotor;
						this.averageCellIndex[numSPar]=numGroups[parIndex]+tiltList.indexOf(iStationTilt);
						break;
					}
				}
			}
    		if (this.debugLevel>1){
    			System.out.println("buildVariancesMaps() numStationTilts="+numStationTilts+" numSeriesPars="+numSeriesPars+ " useSetWeights="+useSetWeights+
    					" number of averages="+startIndex);
    		}

			this.variationsAverages=new double [startIndex]; // total number of different averages
			int [] numContributors=new int [startIndex];
			for (int i=0;i<this.variationsAverages.length;i++) {
				this.variationsAverages[i]=0.0;
				numContributors[i]=0;
			}
			// Calculate total weights for averages and number of contributors (to remove single-contributor items)
			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					double w;
					if (useSetWeights){
						int imgNumber=this.parameterMap[numSPar][0];
						int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
						w=this.distortionCalibrationData.gIS[setNumber].getSetWeight();
					} else {
						w=1.0;
					}
					if (w>0.0) { // do not count zero-contributors (are they possible?)
						this.variationsAverages[avIndex]+=w;
						numContributors[avIndex]++;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("buildVariancesMaps() numContributors:");
    			for (int i=0;i<numContributors.length;i++) if (numContributors[i]>0) System.out.println("--- "+i+":"+numContributors[i]);
    		}

			// calculate each parameter share of the average TODO: disable if share =100% (single contributor)
			int numVariancePars=0;
			this.weightOnAverage=new double [numSeriesPars];
			this.weightVariance= new double [numSeriesPars];
			this.varianceErrorsSquared= new double [numSeriesPars];

			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				this.weightOnAverage[numSPar]=0.0;
				this.weightVariance[numSPar]=0.0;
				this.varianceErrorsSquared[numSPar]=Double.NaN;
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					if (numContributors[avIndex]<2){
						this.averageCellIndex[numSPar]=-1; // single element - impossible to use
						if (this.debugLevel>0) System.out.println("buildVariancesMaps(): removed parameter #"+numSPar+" - single contributor to average");
					} else {
						numVariancePars++;
						double w;
						if (useSetWeights){
							int imgNumber=this.parameterMap[numSPar][0];
							int setNumber=this.distortionCalibrationData.gIP[imgNumber].getSetNumber();
							w=this.distortionCalibrationData.gIS[setNumber].getSetWeight();
						} else w=1.0;
						this.weightOnAverage[numSPar]=w/this.variationsAverages[avIndex];
						this.weightVariance[numSPar]= w;
					}
				}
			}
    		if (this.debugLevel>debugThreshold){
    			System.out.println("average indices/shares:");
    			for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
    				int avIndex=this.averageCellIndex[numSPar];
    				if (avIndex>=0){
    					int parIndex=this.parameterMap[numSPar][1];
    					System.out.println("==="+numSPar+" parIndex="+parIndex+" avIndex="+avIndex+
    							" weightOnAverage="+this.weightOnAverage[numSPar]+
    							" weightVariance="+this.weightVariance[numSPar]);
    				}
    			}
    		}
			if (numVariancePars==0){
				this.averageCellIndex=null;
				this.variationsAverages=null;
				this.weightVariance=null;
				this.varianceErrorsSquared=null;
				return 0;
			}
			// TODO this.variationsAverages needs to be reset to all zeros each calculations
			return numVariancePars;
    	}
    	/**
    	 * Ammend LMA arrays to pull individual values of the extrinsic parameters to their averages
    	 * Averages may be global, per-station or per same tilt motor position (same station)
    	 * The function to be minimized is
    	 * f(x) Scale*(1/p*Xeff+(1-1/p)*pow(Xeff,p)
    	 * Xeff=x/X0*(1-shareInAverage)
    	 * and x is difference between the poarameter and it's average over the specified set (all/station/tiltStation)
    	 * @param numSeries fitting series number
    	 * @param vector - parameters vector
    	 * @param jTByJ Jacobian transposed multiplied by Jacobian - some diagonal elements may be modified (weighted)  or null (will add to old values)
    	 * @param jTByDiff Jacobian transposed multiplied by difference vector (weighted)  or null (will add to old values)
    	 * @return true if parameter variances are applicable to the fitting series
    	 */
    	public boolean addVarianceToLMA(
    			int numSeries,
    			final double [] vector,
    	        double [][] jTByJ, // jacobian multiplied by Jacobian transposed
    	        double []   jTByDiff){ // jacobian multiplied difference vector
    		if (this.averageCellIndex==null) return false; // parameter varainces are not used
//    		int numTPars=this.parameterMode[numSeries].length;
    		int numSeriesPars=this.parameterMap.length;
    		int debugThreshold=2;

//    		double [] fX=   new double [numTPars];
//    		double [] diagJ=new double [numTPars]; // diagonal of Jacobian (here parameters only influence themselves)
    		double [] weights=new double [this.variationsAverages.length];
    		for (int i=0;i<this.variationsAverages.length;i++) {
    			this.variationsAverages[i]=0.0;
    			weights[i]=0.0;
    		}
    		// calculate appropriate averages
    		for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
    			this.varianceErrorsSquared[numSPar]=0.0;
				int avIndex=this.averageCellIndex[numSPar];
				if (avIndex>=0){
					this.variationsAverages[avIndex]+=this.weightOnAverage[numSPar]*vector[numSPar];
    			}
    		}
    		// now fX and diagJ
    		int numModded=0;
    		for (int numSPar=0;numSPar<numSeriesPars;numSPar++) {
				int avIndex=this.averageCellIndex[numSPar];
				int parIndex=this.parameterMap[numSPar][1];
//    			if ((avIndex>=0) &&  (this.distortionCalibrationData.eyesisCameraParameters.isVarianceCostSet(parIndex))) {
    			if (avIndex>=0) {
    				double scale=this.distortionCalibrationData.eyesisCameraParameters.varianceCostScale(parIndex);
    				double x0=   this.distortionCalibrationData.eyesisCameraParameters.varianceCostVariationAbs(parIndex);
    				double exp=  this.distortionCalibrationData.eyesisCameraParameters.varianceCostVariationExponent(parIndex);
    				double xEff=(vector[numSPar]-this.variationsAverages[avIndex])*(1.0-this.weightOnAverage[numSPar])/x0; //OOB-1
    				double axEff=Math.abs(xEff);
    				double sxEff=Math.signum(xEff);
    				double k=1.0/exp;
    				double fX=sxEff*scale*(k*axEff+(1.0-k)*Math.pow(axEff,exp));
    				double diagJ=scale*((1.0-this.weightOnAverage[numSPar])/x0)*(k+(exp-1)*Math.pow(axEff,exp-1.0));
    				this.varianceErrorsSquared[numSPar]=this.weightVariance[numSPar]*fX*fX;
    				if (jTByJ!=null) jTByJ[numSPar][numSPar]+=this.weightVariance[numSPar]*diagJ*diagJ;
    				if (jTByDiff!=null) jTByDiff[numSPar]-= this.weightVariance[numSPar]*fX*diagJ;
    				if (this.debugLevel>debugThreshold){
    					System.out.print  (numSPar+" "+parIndex+" "+avIndex+": "+" scale="+scale);
    					System.out.print  (" vector["+numSPar+"]="+vector[numSPar]+" ("+this.variationsAverages[avIndex]+") xEff="+xEff+" fX="+fX+" k="+k+" exp="+exp );
    					System.out.print  (" diagJ="+diagJ+" "+xEff+" jTByJ+"+ (this.weightVariance[numSPar]*diagJ*diagJ) );
    					System.out.print  (" jTByDiff+"+(-this.weightVariance[numSPar]*fX*diagJ)+" ves="+(this.weightVariance[numSPar]*fX*fX));
    					System.out.println(" weight="+this.weightVariance[numSPar]);
    				}
    				if (this.weightVariance[numSPar]!=0.0) numModded++;
    			}
    		}
			if (this.debugLevel>1)System.out.println ("addVarianceToLMA() - modified "+numModded+" elements");
    		return true;
    	}
    	public double [] getVarianceError2(){ return (this.averageCellIndex==null)?null:this.varianceErrorsSquared;}
    	public double [] getWeights(){ return (this.averageCellIndex==null)?null:this.weightVariance;}

    	/**
    	 *
    	 * @return number of the current fitting strategy series
    	 */
    	public int getCurrentSeries(){
    		return this.currentSeriesNumber;
    	}

    	/**
    	 * Calculate vector of the parameters used in LMA algorithm, extracted from the
    	 * individual data, using parameter map (calculated once after changing series)
    	 * @return vector of parameters used for the LMA
    	 */
    	public double [] getSeriesVector(){
        	if (this.parameterMap==null) {
        		String msg="Series parameters map is not calculated";
        		IJ.showMessage("Error",msg);
        		throw new IllegalArgumentException (msg);
        	}
    		double [] vector=new double[this.parameterMap.length];
    		for (int i=0;i<vector.length;i++) {
//    			vector[i]=this.distortionCalibrationData.pars[this.parameterMap[i][0]][this.parameterMap[i][1]];
    			vector[i]=this.distortionCalibrationData.getParameterValue(this.parameterMap[i][0],this.parameterMap[i][1]); // (numImg, numPar)
    		}
    		return vector;
    	}

    	/**
    	 * Saves data from the parameter vector to that of the images
    	 * @param vector vector of parameters (after LMA fitting)
    	 */
    	// TODO: Update the temporarily disabled images also, when possible (modify buildParameterMap also?
    	public void saveSeriesVector(double [] vector){
    		if ((this.parameterMap==null) ||(vector==null) || (vector.length!=this.parameterMap.length) ) {
    			String msg="Vector length does not match parameters length";
    			IJ.showMessage("Error",msg);
    			throw new IllegalArgumentException (msg);
    		}
    		// save for "individual" and "common" - and "station" (week - same as individual here)
    		boolean [] selectedEnabledImages=selectedImages(this.currentSeriesNumber);
    		for (int numImg=0;numImg<this.reverseParameterMap.length;numImg++)	if (selectedEnabledImages[numImg]){
    			for (int nPar=0;nPar<this.reverseParameterMap[numImg].length;nPar++) if (this.reverseParameterMap[numImg][nPar]>=0){
//    				this.distortionCalibrationData.pars[numImg][nPar]=vector[this.reverseParameterMap[numImg][nPar]];
    				this.distortionCalibrationData.setParameterValue(numImg,nPar,vector[this.reverseParameterMap[numImg][nPar]],true);

    				if (this.debugLevel>2){
    					System.out.println(" Updated image "+numImg+" "+distortionCalibrationData.getParameterName(nPar)+" = "+
    				//			this.distortionCalibrationData.pars[numImg][nPar]) ; //vector[this.reverseParameterMap[numImg][nPar]]);
    							this.distortionCalibrationData.getParameterValue(numImg,nPar));
    				}
    			}
    		}
    		// propagate to other (unselected) images for "super common" parameters
    		for (int vPar=0; vPar<this.parameterMap.length;vPar++) {
    			if (
    					(this.parameterMap[vPar][2]==modeSupercommon) ||
    					(this.parameterMap[vPar][2]==modeSuperStation)	){ // "super common" No "weak" here!

    				//        		int nSub=this.parameterMap[vPar][0]; // that's an image number, not a subcamera number
    				int nSub=this.distortionCalibrationData.gIP[this.parameterMap[vPar][0]].channel;
    				int nStation=this.distortionCalibrationData.gIP[this.parameterMap[vPar][0]].getStationNumber();
    				boolean superCommon=(this.parameterMap[vPar][2]==modeSupercommon);
    				int nPar=this.parameterMap[vPar][1];
    				//this.distortionCalibrationData.channels[i]
    				boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(nPar);
    				for (int numImg=0;numImg<this.reverseParameterMap.length;numImg++) {
    					if (superCommon || (this.distortionCalibrationData.gIP[numImg].getStationNumber()==nStation)) {

    						if (this.debugLevel>2){
    							System.out.println("saveSeriesVector(): numImg="+numImg+" nPar="+nPar+" isSubCamera="+isSubCamera+
    									" nSub="+nSub+" distortionCalibrationData.getImageSubcamera("+numImg+")="+distortionCalibrationData.getImageSubcamera(numImg)+
    									" vector["+vPar+"]="+vector[vPar]);
    						}
//    						if ((!isSubCamera) || (nSub==this.distortionCalibrationData.getImageSubcamera(numImg))) {
    						if ((!isSubCamera) || (nSub==this.distortionCalibrationData.getImageSubcamera(numImg)) ||
    								(this.reverseParameterMap[numImg][nPar] == vPar)) { // 06.03.2021
//    							this.distortionCalibrationData.pars[numImg][nPar]=vector[vPar];
    							this.distortionCalibrationData.setParameterValue(numImg,nPar,vector[vPar],true);
    						}
    					}
    				}
    			}
    		}
    	}

    	/**
    	 * Calculates current values of all parameters for the particular sensor - some ("fixed")
    	 * are taken from the data stored for this individual image, others - from the parameter
    	 * vector (used in fitting) UPDATE: works with null parameterVector
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return vector used for the current image (parameters influencing the acquired grid
    	 * on the sensor (common parameters and those of the sensor's subchannel)
    	 */
    	public double [] getImageParametersVector(int numImg, double [] parameterVector){
//    		double [] vector = this.distortionCalibrationData.pars[numImg].clone();
    		double [] vector = this.distortionCalibrationData.getParameters(numImg); // returns a copy, no clone() is needed
    		if ((parameterVector!=null) && (this.reverseParameterMap!=null) && (this.reverseParameterMap[numImg]!=null)){
    			for (int i=0;i<vector.length;i++){
    				if (this.reverseParameterMap[numImg][i]>=0) vector[i]=parameterVector[this.reverseParameterMap[numImg][i]];
    			}
    		}
    		return vector;
    	}
    	/**
    	 * Calculates which of all parameters for the particular sensor are to be adjusted (to reduce calculations)
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return mask vector to be used with the results of getImageParametersVector()
    	 */
    	public boolean [] getImageParametersVectorMask (int numImg){
    		if ((this.reverseParameterMap==null) || (this.reverseParameterMap[numImg]==null)) return null;
//    		boolean [] mask =new boolean [this.distortionCalibrationData.pars[numImg].length];
    		boolean [] mask =new boolean [this.distortionCalibrationData.getParametersLength(numImg)];
    		for (int i=0;i<mask.length;i++){
    			mask[i]=(this.reverseParameterMap[numImg][i]>=0);
    		}
    		return mask;
    	}
    	/**
    	 * Calculates index in the parameter vector corresponding to each image parameter vector element
    	 * @param numImg number of image
    	 * @param vector parameters vector
    	 * @return mask vector to be used with the results of getImageParametersVector()
    	 */
    	public int [] getImageParametersVectorReverseMap (int numImg){
    		if ((this.reverseParameterMap==null) || (this.reverseParameterMap[numImg]==null)) return null;
//    		int  [] map =new int [this.distortionCalibrationData.pars[numImg].length];
    		int  [] map =new int [this.distortionCalibrationData.getParametersLength(numImg)];
    		for (int i=0;i<map.length;i++){
    			map[i]=this.reverseParameterMap[numImg][i];
    		}
    		return map;
    	}
    	/**
    	 * Opens a text window with a table that shows map from each element of the parameters vector
    	 * to the image number used as a source of this parameter
    	 * @param title Window title
    	 */
    	public void showCurrentParameterMap(String title){
    	    String header="#\tMode\tName\tSubcamera\tImage Number\tMode";
    	    StringBuffer sb = new StringBuffer();
    	    for (int i=0; i<this.parameterMap.length;i++){
    	        sb.append(
    	        		i+"\t"+
    	        		this.definedModesAll[this.parameterMap[i][2]]+"\t"+
    	        		this.distortionCalibrationData.getParameterName(this.parameterMap[i][1])+"\t"+
    	        		this.distortionCalibrationData.getImageSubcamera(this.parameterMap[i][0])+"\t"+
    	        		this.parameterMap[i][0]+"\t"+
    	        		this.definedModesAll[this.parameterMap[i][2]]+"\n");
    	    }
    	    new TextWindow(title, header, sb.toString(), 800,600);
    	}
    	/**
    	 * Opens window with a table mapping each image (column) parameter (row) to parameter vector element
    	 * "-" (-1 in teh table) - fixed parameter, use one from the individual image
    	 * @param title Window title
    	 */
    	public void showCurrentReverseParameterMap(String title){
    	    String header="Image";
			boolean [] selectedEnabledImages=selectedImages(this.currentSeriesNumber);
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	header+="\t"+i;
    	    }
    	    StringBuffer sb = new StringBuffer();
    	    sb.append("timestmps");
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	sb.append("\t"+IJ.d2s(this.distortionCalibrationData.getImageTimestamp(i),6));
    	    }
    	    sb.append("\n");
    	    sb.append("subcamera");
    	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
    	    	sb.append("\t"+this.distortionCalibrationData.getImageSubcamera(i));
    	    }
    	    sb.append("\n");

    	    for (int k=0;k<this.reverseParameterMap[0].length;k++){
//    			boolean isSubCamera=this.distortionCalibrationData.isSubcameraParameter(k);
//        	    sb.append(this.distortionCalibrationData.getParameterName(this.parameterMap[k][1])); // name of parameter
        	    sb.append(this.distortionCalibrationData.getParameterName(k)); // name of parameter
        	    for (int i=0; i<this.reverseParameterMap.length;i++) if (selectedEnabledImages[i]){
        	    	int mode =this.reverseParameterMap[i][k];
        	    		sb.append("\t"+((mode>=0)?mode:"-"));

        	    }
        	    sb.append("\n");

    	    }
    	    new TextWindow(title, header, sb.toString(), 800,600);
    	}

    	/**
    	 * Add/reduce number of series in this fitting strategy
    	 * @param numSeries new number of series
    	 */
    	public void setLength(int numSeries){
    		if (numSeries<=0) {
    			setDflt(0);
    			return;
    		}
    		if ((this.selectedImages!=null) && (numSeries==this.selectedImages.length)) return;
    		if ((this.selectedImages==null) && (numSeries==0)) return;
        	boolean [][] oldSelectedImages=null; // images selected for each step
        	int [][] oldParameterMode=null;
        	int [] oldVarianceModes=null;
        	int [][] oldZGroups=null;
        	int [][][] oldParameterGroups=null;
        	double [] oldLambdas=null;   // LMA initial lambda for each step
    		int [] oldMasterImages=null;
        	double [] oldStepDone=null;
        	boolean [] oldStopAfterThis=null;
        	String [] oldStrategyComment=null;
            int oldNumSeries=(this.selectedImages==null)?0:this.selectedImages.length;
            int oldLength=0;
    		if (this.selectedImages!=null){
    			// deep clone this.* to old*;
    			oldSelectedImages=new boolean[oldNumSeries][];
    			oldParameterMode=new int[oldNumSeries][];
    			oldParameterGroups=new int[oldNumSeries][][];
    			oldZGroups=new int[oldNumSeries][];
        		oldStrategyComment = new String [oldNumSeries];
    			for (int i=0;i<oldNumSeries;i++){
    				oldSelectedImages[i]=this.selectedImages[i].clone();
        			oldParameterMode[i]= this.parameterMode[i].clone(); // out of bound - 2
        			oldParameterGroups[i]=new int[this.parameterGroups[i].length][];
        			for (int j=0;j<oldParameterGroups[i].length;j++)
        				oldParameterGroups[i][j]=(parameterGroups[i][j]==null)?null:parameterGroups[i][j].clone();
        			if (this.zGroups[i]!=null) oldZGroups[i]=this.zGroups[i].clone();
        			else this.zGroups[i]=oldZGroups[i];
        			oldStrategyComment[i]=this.strategyComment[i];
    			}
    			oldVarianceModes=(this.varianceModes==null)?null:this.varianceModes.clone();
    			oldLambdas=this.lambdas.clone();
    			oldLength=this.selectedImages.length;
        		oldMasterImages=this.masterImages.clone();
        		oldStepDone=this.stepDone.clone();
        		oldStopAfterThis=this.stopAfterThis.clone();
    		}
    		setDflt(numSeries);
    		if (this.selectedImages!=null) for (int i=0;(i<this.selectedImages.length) && (i<oldLength);i++){
    			this.selectedImages[i]=oldSelectedImages[i];
    			this.parameterMode[i]= oldParameterMode[i];
    			this.parameterGroups[i]=oldParameterGroups[i];
    			this.lambdas[i]=oldLambdas[i];
    			this.masterImages[i]=oldMasterImages[i];
        		this.stepDone[i]=oldStepDone[i];
        		this.stopAfterThis[i]=oldStopAfterThis[i];
    			this.varianceModes[i]=oldVarianceModes[i];
    			this.zGroups[i]=(oldZGroups[i]!=null)?oldZGroups[i].clone():null;
    			this.strategyComment[i] = oldStrategyComment[i];
    		}
    	}

    	public void updateNumberOfSubcameras(){  // can break "same as"?
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		initParameterList();
    		int numSubPars=0;
    		int [] subParIndex=new int[numPars];
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) subParIndex[numSubPars++]=i;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		int oldTotalPars=this.parameterEnable.length;
    		if (oldTotalPars==totalNumPars) return;
    		if (oldTotalPars>numPars) { // more than one subcamera, so last numSubPars are subcamera parameters
        		for (int i=0;i<numSubPars;i++)subParIndex[i]=(oldTotalPars-numSubPars)+i;
    		}
//       public boolean isSubcameraParameter(int num){

    		int numSeries=this.parameterMode.length;
    		if (oldTotalPars<totalNumPars){ // grow, repeat last subcamera
    			if (this.debugLevel>1) System.out.println("Increasing total number of parameters in fitting strategy: "+oldTotalPars+" -> "+totalNumPars);
    			for (int nSer=0;nSer<numSeries;nSer++){
    				int [] parameterMode_tmp=this.parameterMode[nSer]; //.clone();
    				int [][] parameterGroups_tmp=this.parameterGroups[nSer]; //.clone();
            		this.parameterMode[nSer]=new int[totalNumPars];
            		this.parameterGroups[nSer]=new int[totalNumPars][];
            		for (int nTPar=0;nTPar<oldTotalPars;nTPar++){
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[nTPar];
            			this.parameterGroups[nSer][nTPar]=parameterGroups_tmp[nTPar];
            		}
            		for (int nTPar=oldTotalPars;nTPar<totalNumPars;nTPar++){
            			int index= subParIndex[(nTPar-oldTotalPars) % numSubPars];
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[index];
            			this.parameterGroups[nSer][nTPar]=(parameterGroups_tmp[index]==null)?null:parameterGroups_tmp[index].clone();
            		}
    			}
				boolean []  parameterEnable_tmp=this.parameterEnable; //.clone();
        		this.parameterEnable= new boolean [totalNumPars];
        		for (int nTPar=0;nTPar<oldTotalPars;nTPar++){
        			this.parameterEnable[nTPar]=parameterEnable_tmp[nTPar];
        		}
        		for (int nTPar=oldTotalPars;nTPar<totalNumPars;nTPar++){
        			int index= subParIndex[(nTPar-oldTotalPars) % numSubPars];
        			this.parameterEnable[nTPar]=parameterEnable_tmp[index];
        		}
    		} else { // shrink
    			if (this.debugLevel>1) System.out.println("Reducing total number of parameters in fitting strategy: "+oldTotalPars+" -> "+totalNumPars);
    			for (int nSer=0;nSer<numSeries;nSer++){
    				int [] parameterMode_tmp=this.parameterMode[nSer]; //.clone();
    				int [][] parameterGroups_tmp=this.parameterGroups[nSer]; //.clone();
            		this.parameterMode[nSer]=new int[totalNumPars];
            		this.parameterGroups[nSer]=new int[totalNumPars][];
            		for (int nTPar=0;nTPar<totalNumPars;nTPar++){
            			this.parameterMode[nSer][nTPar]=parameterMode_tmp[nTPar];
            			this.parameterGroups[nSer][nTPar]=parameterGroups_tmp[nTPar];
            		}
    			}
				boolean []  parameterEnable_tmp=this.parameterEnable; //.clone();
        		this.parameterEnable= new boolean [totalNumPars];
        		for (int nTPar=0;nTPar<totalNumPars;nTPar++){
        			this.parameterEnable[nTPar]=parameterEnable_tmp[nTPar];
        		}
    		}
    	}

    	private void setDflt(int numSeries){
    		if (numSeries==0) {
        		this.selectedImages=null;
        		this.lambdas=null;
        		this.stepDone=null;
        		this.parameterMode=null;
        		this.parameterGroups=null;
        		this.varianceModes=null;
        		this.zGroups=null;
        		this.strategyComment=null;
                return;
    		}
    		this.stopAfterThis=new boolean[numSeries];
    		for (int i=0;i<numSeries;i++) this.stopAfterThis[i]=true;

// calculate total (potential) number of parameters in the system
//TODO: modify for groups
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		int numSubPars=0;
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) numSubPars++;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		// number of parameters to adjust may be greater, if they were changing between the images
    		this.selectedImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];//
    		this.selectedValidImages=new boolean[numSeries][this.distortionCalibrationData.getNumImages()];//
    		this.masterImages=new int[numSeries];
    		this.lambdas=new double[numSeries];
    		this.stepDone=new double[numSeries];
    		this.parameterMode=new int[numSeries][totalNumPars];
    		this.varianceModes=new int[numSeries];
    		this.parameterGroups=new int[numSeries][totalNumPars][];
    		this.parameterEnable= new boolean [totalNumPars];
    		this.zGroups=new int[numSeries][];
    		this.strategyComment = new String [numSeries];
    		for (int i=0;i<this.selectedImages.length;i++){
    			invalidateSelectedImages(i);
    			for (int j=0;j<this.selectedImages[i].length;j++){
    				this.selectedImages[i][j]=false;
    			}
    			for (int j=0;j<this.parameterMode[i].length;j++){
    				this.parameterMode[i][j]=modeFixed; // fixed
    				this.parameterGroups[i][j]=null; // no groups yet defined
    			}
    			this.lambdas[i]=defaultLambda;
    			this.stepDone[i]=defaultStepDone;
    			this.masterImages[i]=0; // first image, supposingly the earliest timestamp
    			this.varianceModes[i]=varianceModeDisabled;
        		this.zGroups[i]=null;
        		this.strategyComment[i] = "";
    		}

    		initParameterList();

    		for (int i=0;i<numPars;i++){
    			this.parameterEnable[i]=!this.distortionCalibrationData.isNonRadial(i); // true; // initially enable all parameters for the first camera
    		}
    		for (int i=1;i<numSubCams;i++){
    			int i1=numPars+numSubPars*(i-1);
    			int j1=0;
    			for (int j=0;j<numPars;j++) if (this.distortionCalibrationData.isSubcameraParameter(j)){
    				this.parameterEnable[i1+j1]=
    						this.distortionCalibrationData.firstInGroup(i) &&
    						!this.distortionCalibrationData.isNonRadial(j); // false; // initially disable all other subcamera parameters
        			j1++;
    			}
    		}
    		// Improve - add main parameters (up to A8?) for each master channel


    	}

    	private void initParameterList(){
    		int numPars=   this.distortionCalibrationData.getNumParameters();
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		int numSubPars=0;
    		for (int i=0;i<numPars;i++){
    			if (this.distortionCalibrationData.isSubcameraParameter(i)) numSubPars++;
    		}
    		int totalNumPars=numPars+(numSubCams-1)*numSubPars; // maximal number of parametes per timestamp?
    		this.parameterList=new int[totalNumPars][2];

    		for (int i=0;i<numPars;i++){
    			this.parameterList[i][0]=0;
    			this.parameterList[i][1]=i;
//    			this.parameterEnable[i]=true; // initially enable all parameters for the first camera
    		}

    		for (int i=1;i<numSubCams;i++){
    			int i1=numPars+numSubPars*(i-1);
    			int j1=0;
    			for (int j=0;j<numPars;j++) if (this.distortionCalibrationData.isSubcameraParameter(j)){
    				this.parameterList[i1+j1][0]=i;
    				this.parameterList[i1+j1][1]=j;
 //   				this.parameterEnable[i1+j1]=false; // initially disable all other subcamera parameters
        			j1++;
    			}
    		}

    	}
    	/**
    	 * Find parameter number from subcamera and index
    	 * @param nSub number of subcamera (channel)
    	 * @param index index of parameter
    	 * @return number of parameter that has specified sub-camera and index. -1 if such does not exist
    	 */
    	private int getParameterNumber(int nSub, int index){
    		for (int i=0;i<this.parameterList.length;i++) if ((this.parameterList[i][0]==nSub) && (this.parameterList[i][1]==index)) return i;
    		return -1;
    	}
    	private int [] arrayToSortedInt(int [] srcArray){
    		Set<Integer> set = new HashSet<Integer>();
    		for (Integer data:srcArray) set.add(data);
    		int [] array = new int [set.size()];
    		int i=0;
    		for (Integer val:set) array[i++]= val;
    		Arrays.sort(array);
    		return array;
    	}

    	public boolean selectIndividualImages(
    			boolean [] selection,
    			boolean allImages,
    			int startIndex,
    			int perPage){
    		boolean [] enabled=  this.distortionCalibrationData.selectEnabled();
    		int [] hintedMatch=this.distortionCalibrationData.getHintedMatch();
    		if (selection.length!=enabled.length){
    			System.out.println("BUG: selectIndividualImages(): selection.length!=enabled.length!");
    			return false;
    		}
    		int endIndex=startIndex;
    		int numImg=0;
    		for (endIndex=startIndex; (endIndex<enabled.length) && (numImg<perPage);endIndex++) if (allImages || enabled[endIndex]) numImg++;
    		for (;(endIndex<enabled.length) && !allImages && !enabled[endIndex];endIndex++); // advance over disabled images
    		GenericDialog gd=new GenericDialog("Select images "+startIndex+"..."+(endIndex-1));
    		for (int i=startIndex;i<endIndex;i++) if (allImages || enabled[i]){
				gd.addCheckbox    (i+ " ("+this.distortionCalibrationData.gIP[i].getSetNumber()+
						"."+this.distortionCalibrationData.gIP[i].getChannel()+")"+
    		            " - "+(this.distortionCalibrationData.gIP[i].enabled?"":"(disabled) ")+
						IJ.d2s(this.distortionCalibrationData.gIP[i].timestamp,6)+
						" matched "+this.distortionCalibrationData.gIP[i].matchedPointers+" pointers"+
						", hinted state: "+((hintedMatch[i]<0)?"undefined":((hintedMatch[i]==0)?"failed":((hintedMatch[i]==1)?"orientation":"orientation and translation"))),
						selection[i]);
    		}
    		if (endIndex<enabled.length){
         		gd.enableYesNoCancel("Done", "Next");
    		}
			WindowTools.addScrollBars(gd);
			gd.showDialog();
			if (gd.wasCanceled()) return false;
    		for (int i=startIndex;i<endIndex;i++) if (allImages || enabled[i]){
    			selection[i]=gd.getNextBoolean();
    		}
			if (gd.wasOKed()) return true;
			return selectIndividualImages(
	    			selection,
	    			allImages,
	    			endIndex,
	    			perPage);
    	}

    	public boolean selectImageSets(
    			boolean [] selection,
    			boolean allImages,
    			int startIndex,
    			int perPage){
    		boolean [] enabled=  this.distortionCalibrationData.selectEnabled();
    		int [] imageStations=this.distortionCalibrationData.getStations();
    		int [] imageChannels=this.distortionCalibrationData.getChannels();

    		if (selection.length!=enabled.length){
    			System.out.println("BUG: selectIndividualImages(): selection.length!=enabled.length!");
    			return false;
    		}
    	    int [][] imageSets=this.distortionCalibrationData.listImages(
    	    		!allImages, // true - only enabled images
    	    		null); // do not filter by eo/lwir?
    	    
    	    boolean [] enabledSets=new boolean [imageSets.length];
    	    boolean [] selectedSets=new boolean [imageSets.length]; // at least one image selected in the series
    	    for (int i=0;i<imageSets.length;i++){
    	    	enabledSets[i]=false;
    	    	selectedSets[i]=false;
    	    	if (imageSets[i]!=null) for (int j=0;j<imageSets[i].length;j++){
    	    		enabledSets[i] |= enabled[imageSets[i][j]];
    	    		selectedSets[i] |= selection[imageSets[i][j]];
    	    	}
    	    }

    		int endIndex=startIndex;
    		int numSet=0;
    		for (endIndex=startIndex; (endIndex<enabledSets.length) && (numSet<perPage);endIndex++) if (enabledSets[endIndex]) numSet++;
    		for (;(endIndex<enabledSets.length) &&  !enabledSets[endIndex];endIndex++); // advance over disabled sets
    		GenericDialog gd=new GenericDialog("Select image Sets "+startIndex+"..."+(endIndex-1));
    		for (int i=startIndex;i<endIndex;i++) if (enabledSets[i]){
    			int station=-1;
    			String sImgList="";
    			int l=0;
    			for (int j=0;j<imageSets[i].length;j++) if (enabled[imageSets[i][j]]) {
    				station=imageStations[imageSets[i][j]];
    				int channel=imageChannels[imageSets[i][j]];
    				if (l > 0) sImgList+=", ";
    				sImgList+=imageSets[i][j]+" ["+channel+"] ";
    				l++;
    	    	}
				gd.addCheckbox    (i+" ("+sImgList+") === Station_"+(station+1), selectedSets[i]);
    		}
    		if (endIndex<enabledSets.length){
         		gd.enableYesNoCancel("Done", "Next");
    		}
			WindowTools.addScrollBars(gd);
			gd.showDialog();
			if (gd.wasCanceled()) return false;
//			Arrays.fill(selection, false);
    		for (int i=startIndex;i<endIndex;i++) if (enabledSets[i]){
    			selectedSets[i]=gd.getNextBoolean();
    			for (int j=0;j<imageSets[i].length;j++) {
    				selection[imageSets[i][j]]=selectedSets[i];
    			}
    		}
			if (gd.wasOKed()) return true;
			return selectImageSets(
	    			selection,
	    			allImages,
	    			endIndex,
	    			perPage);
    	}


    	/**
    	 * Manage image selection for the current series
    	 * @param numSeries series number to manage
    	 * @return -1 - cancel, 0 - repeat again, 1 -  Done,
    	 */
    	public int manageSelection(
    			int numSeries){
//    		String [] firstOperand= {"Current","Inverted current","None","All"};
//    		String [] SecondOperand={"Selection","Inverted selection","None","All"};
//    		String [] operation={"And","Or"};
    		String [] actions={
    				"Replace current with new selection",
    				"Add selection to current",
    				"Add inverted selection to current",
    				"And selection with current",
    				"Remove selection from current",
    				"Invert current selection, disregard other settings"};
    		String [] selectionType={
    				"Select from all enabled images",                 // 0
    				"Select from new enabled images",                 // 1
    				"Select from images with estimated orientation",  // 2
    				"Individual images, start from empty selection",  // 3
    				"Individual images, start from current selection",// 4
    				"Image sets, start from empty selection",         // 5
    				"Image sets, start from current selection"        // 6
    				};
    		String help = "<html>"+
    				"<h2>Use of buttons in this dialog:</h2>"+
    				"<ul>"+
    				"<li><b>More</b> will re-open dialog after performin selection modification actions,"+
    				" in case of selecting individual images that may happen after multiple selection "+
    				"screens</li>"+
    				"<li><b>Cancel</b> - keep current selection that you do not want to modify further"+
    				" (e.g. if you wrongly pressed 'More' in this dialog earlier)</li>"+
    				"<li><b>OK</b> - perform selection modification once (same as 'More'...'Cancel')</li>"+
    				"</ul><br/>";
    		int numStations=this.distortionCalibrationData.eyesisCameraParameters.getNumStations();
    		int numChannels=this.distortionCalibrationData.eyesisCameraParameters.getNumChannels(0); // for station 0
    		boolean [] selected=          this.selectedImages[numSeries];
    		boolean [] enabled=           this.distortionCalibrationData.selectEnabled();
            boolean [] newEnabled=        this.distortionCalibrationData.selectNewEnabled ();
            boolean [] estimated=         this.distortionCalibrationData.selectEstimated(true); //boolean enabledOnly);
            boolean [] estimatedAll=      this.distortionCalibrationData.selectEstimated(false); //boolean enabledOnly);

    		if (selected.length!=enabled.length){
    			System.out.println("WARNING: manageSelection(): lengths (strategy and images) mismatch selected.length="+selected.length+" available images: "+enabled.length);
   				boolean [] newSelection=new boolean[enabled.length];
				for (int i=0;i<newSelection.length;i++) newSelection[i]=(i<enabled.length)?enabled[i]:false;
				selected=newSelection;
				this.selectedImages[numSeries]=selected;
    		}
    		int numSelected=0;
    		int [] numSelectedPerStation=new int [numStations];
    		Arrays.fill(numSelectedPerStation, 0);
    		int numEnabled  = 0; // this.distortionCalibrationData.getNumEnabled();
    		int [] numEnabledPerStation=new int [numStations];
    		Arrays.fill(numEnabledPerStation, 0);
    		int [] totalPerStation=new int [numStations];
    		Arrays.fill(totalPerStation, 0);
    		int [] imageStations=this.distortionCalibrationData.getStations();
    		int [] imageChannels=this.distortionCalibrationData.getChannels();

    		int numNewEnabled=0;
    		int [] numNewEnabledPerStation=new int [numStations];
    		Arrays.fill(numNewEnabledPerStation, 0);
    		int numNewEnabledSelected=0;
    		int [] numNewEnabledSelectedPerStation=new int [numStations];
    		Arrays.fill(numNewEnabledSelectedPerStation, 0);

    		int numEstimatedSelected=0;
    		int [] numEstimatedSelectedPerStation=new int [numStations];
    		Arrays.fill(numEstimatedSelectedPerStation, 0);

    		int numEstimated=0;
    		int [] numEstimatedPerStation=new int [numStations];
    		Arrays.fill(numEstimatedPerStation, 0);

    		int numEstimatedAll=0;
    		int [] numEstimatedAllPerStation=new int [numStations];
    		Arrays.fill(numEstimatedAllPerStation, 0);

    		int [] matchedPointers=this.distortionCalibrationData.getMatchedPointers();
    		int [] matchedPointersIndex=arrayToSortedInt(matchedPointers);
    		int [] hintedMatch=this.distortionCalibrationData.getHintedMatch();
    		int [] hintedMatchIndex=arrayToSortedInt(hintedMatch);
    		Map <Integer,Integer> mapMP=new HashMap<Integer,Integer>();
    		Map <Integer,Integer> mapHM=new HashMap<Integer,Integer>();
    		for (Integer index=0;index<matchedPointersIndex.length;index++) mapMP.put(new Integer(matchedPointersIndex[index]),index);
    		for (Integer index=0;index<hintedMatchIndex.length;index++) mapHM.put(new Integer(hintedMatchIndex[index]),index);

    		int []   numMatchedPointers=        new int [matchedPointersIndex.length];
    		int []   numMatchedPointersSelected=new int [matchedPointersIndex.length];
    		int []   numMatchedPointersEnabled= new int [matchedPointersIndex.length];
    		int [][] numMatchedPointersPerStation=        new int [matchedPointersIndex.length][];
    		int [][] numMatchedPointersSelectedPerStation=new int [matchedPointersIndex.length][];
    		int [][] numMatchedPointersEnabledPerStation= new int [matchedPointersIndex.length][];
    		for (int n=0;n<numMatchedPointers.length;n++){
    			numMatchedPointers[n]=0;
    			numMatchedPointersSelected[n]=0;
    			numMatchedPointersEnabled[n]=0;
    			numMatchedPointersPerStation[n]=new int [numStations];
    			numMatchedPointersSelectedPerStation[n]=new int [numStations];
    			numMatchedPointersEnabledPerStation[n]=new int [numStations];
        		Arrays.fill(numMatchedPointersPerStation[n], 0);
        		Arrays.fill(numMatchedPointersSelectedPerStation[n], 0);
        		Arrays.fill(numMatchedPointersEnabledPerStation[n], 0);
    		}

    		int []   numHintedMatch=        new int [hintedMatchIndex.length];
    		int []   numHintedMatchSelected=new int [hintedMatchIndex.length];
    		int []   numHintedMatchEnabled= new int [hintedMatchIndex.length];
    		int [][] numHintedMatchPerStation=        new int [hintedMatchIndex.length][];
    		int [][] numHintedMatchSelectedPerStation=new int [hintedMatchIndex.length][];
    		int [][] numHintedMatchEnabledPerStation= new int [hintedMatchIndex.length][];
    		for (int n=0;n<numHintedMatch.length;n++){
    			numHintedMatch[n]=0;
    			numHintedMatchSelected[n]=0;
    			numHintedMatchEnabled[n]=0;
    			numHintedMatchPerStation[n]=new int [numStations];
    			numHintedMatchSelectedPerStation[n]=new int [numStations];
    			numHintedMatchEnabledPerStation[n]=new int [numStations];
        		Arrays.fill(numHintedMatchPerStation[n], 0);
        		Arrays.fill(numHintedMatchSelectedPerStation[n], 0);
        		Arrays.fill(numHintedMatchEnabledPerStation[n], 0);
    		}


    		for (int i=0;i<selected.length;i++) if (imageStations[i]>=0){
    			int mpi=mapMP.get(new Integer(matchedPointers[i]));
    			int hmi=mapHM.get(new Integer(hintedMatch[i]));
    			if (enabled[i]) {
    				numEnabledPerStation[imageStations[i]]++;
    				numEnabled++;
        			if (selected[i]) {
        				numSelectedPerStation[imageStations[i]]++;
        				numSelected++;
        				numMatchedPointersSelectedPerStation[mpi][imageStations[i]]++;
        				numMatchedPointersSelected[mpi]++;

        				numHintedMatchSelectedPerStation[hmi][imageStations[i]]++;
        				numHintedMatchSelected[hmi]++;

        			}
        			if (newEnabled[i]){
        				numNewEnabledPerStation[imageStations[i]]++;
        				numNewEnabled++;
            			if (selected[i]) {
            				numNewEnabledSelectedPerStation[imageStations[i]]++;
            				numNewEnabledSelected++;
            			}
        			}
        			if (estimated[i]){
        				numEstimatedPerStation[imageStations[i]]++;
        				numEstimated++;
            			if (selected[i]) {
            				numEstimatedSelectedPerStation[imageStations[i]]++;
            				numEstimatedSelected++;
            			}
        			}
        			numMatchedPointersEnabledPerStation[mpi][imageStations[i]]++;
    				numMatchedPointersEnabled[mpi]++;
    				if (hmi>=numHintedMatchEnabledPerStation.length) {
    					System.out.println("*** Bug: hmi>=numHintedMatchEnabledPerStation.length");
    				} else {
    					numHintedMatchEnabledPerStation[hmi][imageStations[i]]++;
    					numHintedMatchEnabled[hmi]++;
    				}


    			}
    			if (estimatedAll[i]){
    				numEstimatedAllPerStation[imageStations[i]]++;
    				numEstimatedAll++;
    			}
    			numMatchedPointersPerStation[mpi][imageStations[i]]++;
				numMatchedPointers[mpi]++;

				numHintedMatchPerStation[hmi][imageStations[i]]++;
				numHintedMatch[hmi]++;

				totalPerStation[imageStations[i]]++;

    		}
    		String sAvailable="["+numSelected+"/"+numEnabled+"/"+selected.length+"]   ";
    		String sNewEnabled="["+numNewEnabledSelected+"/"+numNewEnabled+"]   ";
    		String sEstimated="["+numEstimatedSelected+"/"+numEstimated+"/"+numEstimatedAll+"]   ";
    		String [] sMatchedPointers=new String [matchedPointersIndex.length];
    		String [] sHintedMatch=new String [hintedMatchIndex.length];
    		for (int n=0;n<sMatchedPointers.length;n++){
    			sMatchedPointers[n]="["+numMatchedPointersSelected[n]+"/"+numMatchedPointersEnabled[n]+"/"+numMatchedPointers[n]+"]   ";
    		}
    		for (int n=0;n<sHintedMatch.length;n++){
    			sHintedMatch[n]="["+numHintedMatchSelected[n]+" / "+numHintedMatchEnabled[n]+" / "+numHintedMatch[n]+"]   ";
    		}
    		if (numStations>1) for (int i=0;i<numStations;i++) {
    			sAvailable+= " station_"+(i+1)+": ["+numSelectedPerStation[i]+" / "+numEnabledPerStation[i]+" / "+totalPerStation[i]+"]";
    			sNewEnabled+=" station_"+(i+1)+": ["+numNewEnabledSelectedPerStation[i]+" / "+numNewEnabledPerStation[i]+"]";
    			sEstimated+=" station_"+(i+1)+": ["+numEstimatedSelectedPerStation[i]+" / "+numEstimatedPerStation[i]+" / "+numEstimatedAllPerStation[i]+"]";
    			for (int n=0;n<sMatchedPointers.length;n++){
    				sMatchedPointers[n]+=" station_"+(i+1)+": ["+numMatchedPointersSelectedPerStation[n][i]+" / "+
    						numMatchedPointersEnabledPerStation[n][i]+" / "+numMatchedPointersPerStation[n][i]+"]";
    			}
    			for (int n=0;n<sHintedMatch.length;n++){
    				sHintedMatch[n]+=" station_"+(i+1)+": ["+numHintedMatchSelectedPerStation[n][i]+" / "+
    						numHintedMatchEnabledPerStation[n][i]+" / "+numHintedMatchPerStation[n][i]+"]";
    			}

    		}

    		int operIndex=0,selectionTypeIndex=0;
//			boolean selectEstimated=false;
//			boolean selectNewEnabled=false;
    		boolean [] requiredMatchedPointers=new boolean [matchedPointersIndex.length];
    		boolean [] requiredHintedMatch=new boolean [hintedMatchIndex.length];
    		boolean [] requiredStations=new boolean [numStations];
    		boolean [] requiredChannels=new boolean [numChannels];
    		Arrays.fill(requiredMatchedPointers,true);
    		Arrays.fill(requiredHintedMatch,true);
    		Arrays.fill(requiredStations,true);
    		Arrays.fill(requiredChannels,true);
    		selectionType[0]+=" ("+numEnabled+")";
    		selectionType[1]+=" ("+numNewEnabled+")";
    		selectionType[2]+=" ("+numEstimated+")";
    		if (this.debugLevel>0){
    			System.out.println("Image statistics for series "+numSeries+": [currently selected/enabled/total]");
    			System.out.println("Grid images:"+sAvailable);
    			System.out.println("New enabled images: "+sNewEnabled);
    			System.out.println("Estimated orientation: "+sEstimated);
        		for (int n=0;n<sMatchedPointers.length;n++) System.out.println("Images with "+matchedPointersIndex[n]+" pointers: "+sMatchedPointers[n]);
        		for (int n=0;n<sHintedMatch.length;n++) System.out.println("Images with hinted match state=\""+hintedMatchIndex[n]+"\": "+sHintedMatch[n]);
        		System.out.println();
    		}

    		GenericDialog gd=new GenericDialog("Manage image selection for series "+numSeries);
    		gd.addMessage("Image statistics: [currently selected/enabled/total]");
    		gd.addMessage("Grid images:"+sAvailable);
    		gd.addMessage("New enabled images: "+sNewEnabled);
    		gd.addMessage("Estimated orientation: "+sEstimated);
    		for (int n=0;n<sMatchedPointers.length;n++) gd.addMessage("Images with "+matchedPointersIndex[n]+" pointers: "+sMatchedPointers[n]);
    		for (int n=0;n<sHintedMatch.length;n++) gd.addMessage("Images with hinted match state=\""+hintedMatchIndex[n]+"\": "+sHintedMatch[n]);
			gd.addChoice("Operation on selection", actions, actions[operIndex]);
			gd.addChoice("Selection type", selectionType, selectionType[selectionTypeIndex]);
    		//selectionType
//			gd.addCheckbox("Select images with estimated orientation", selectEstimated);
//			gd.addCheckbox("Select new enabled images", selectNewEnabled);
			if (this.distortionCalibrationData.hasSmallSensors()) {
				gd.addMessage("=== Filter selection by High/Low resolution sensors (such as EO/LWIR)");
				gd.addCheckbox("Select high-res sensors", true);
				gd.addCheckbox("Select low-res sensors", true);
			}
    		gd.addMessage("=== Filter selection by the number of matched laser pointers ===");
    		for (int i=0;i<matchedPointersIndex.length;i++){
    			gd.addCheckbox("Select images with "+matchedPointersIndex[i]+" pointers", requiredMatchedPointers[i]);
    		}
    		gd.addMessage("=== Filter selection by the hinted match state (-1 - none, 1 - orientation, 2 - position and orientation) ===");
    		for (int i=0;i<hintedMatchIndex.length;i++){
    			gd.addCheckbox("Select images hintedMatch="+hintedMatchIndex[i], requiredHintedMatch[i]);
    		}
    		gd.addMessage("=== Limit selection by the station ===");
    		for (int i=0;i<requiredStations.length;i++){
    			gd.addCheckbox("Select station "+(i+1), requiredStations[i]);
    		}
    		gd.addMessage("=== Limit selection by the channel ===");
    		for (int i=0;i<requiredChannels.length;i++){
    			gd.addCheckbox("Select channel "+i, requiredChannels[i]);
    		}
    		gd.enableYesNoCancel("OK", "More");
    		gd.addHelp(help);
			WindowTools.addScrollBars(gd);
			gd.showDialog();
			if (gd.wasCanceled()) return -1;
			boolean more=!gd.wasOKed();
			System.out.println("manageSelection(): more=!gd.wasOKed() = "+more);
			operIndex=gd.getNextChoiceIndex();
			selectionTypeIndex=gd.getNextChoiceIndex(); // TODO:Implement!

			boolean [] selectHiLowRes = null;
			if (this.distortionCalibrationData.hasSmallSensors()) {
				selectHiLowRes = new boolean[2];
				selectHiLowRes[0] = gd.getNextBoolean(); //	gd.addCheckbox("Select high-res sensors", true);
				selectHiLowRes[1] = gd.getNextBoolean(); //	gd.addCheckbox("Select low-res sensors", true);
			}
    		for (int i=0;i<matchedPointersIndex.length;i++) requiredMatchedPointers[i]=gd.getNextBoolean();
    		for (int i=0;i<hintedMatchIndex.length;i++)     requiredHintedMatch[i]=    gd.getNextBoolean();
    		for (int i=0;i<requiredStations.length;i++)     requiredStations[i]=gd.getNextBoolean();
    		for (int i=0;i<requiredChannels.length;i++)     requiredChannels[i]=gd.getNextBoolean();
    		boolean [] selection=new boolean [enabled.length];
    		Arrays.fill(selection,false);
    		switch (selectionTypeIndex){
    		case 0:
    			selection=enabled.clone();
    			break;
    		case 1:
    			selection=newEnabled.clone();
    			break;
    		case 2:
    			selection=estimated.clone();
    			break;
    		case 4: // start from current selection
    			selection=selected.clone();
    		case 3: // start from empty selection
    			if (!selectIndividualImages(
	    			selection,
	    			false, // allImages,
	    			0, // star iIndex
	    			500)) return -1; //perPage))
    			break;
    		case 6: // start from current selection
    			selection=selected.clone();
    		case 5: // start from empty selection
    			if (!selectImageSets(
    	    			selection,
    	    			false, // allImages,
    	    			0, // star iIndex
    	    			500)) return -1; //perPage))
    			break;
    		}
			for (int i=0;i<selection.length;i++) selection[i] &= requiredMatchedPointers[mapMP.get(new Integer(matchedPointers[i]))];
			for (int i=0;i<selection.length;i++) selection[i] &= requiredHintedMatch[mapHM.get(new Integer(hintedMatch[i]))];

			for (int i=0;i<selection.length;i++) if (imageStations[i]>=0) selection[i] &= requiredStations[imageStations[i]];
			else selection[i] = false;

			if (selectHiLowRes != null) {
				boolean [] lowres_channel = this.distortionCalibrationData.getSmallSensors();
				for (int i =0; i < requiredChannels.length;i++) {
					if ((lowres_channel[i] && !selectHiLowRes[1]) ||(!lowres_channel[i] && !selectHiLowRes[0])) {
						requiredChannels[i] = false;
					}
				}
			}

			for (int i=0;i<selection.length;i++) if (imageChannels[i]>=0) selection[i] &= requiredChannels[imageChannels[i]];
			else selection[i] = false;


			// now combine new/old selections
			switch (operIndex){
			case 0: // keep new selection
				break;
			case 1:
				for (int i=0;i<selection.length;i++) selection[i] |= selected[i]; // OR
				break;
			case 2:
				for (int i=0;i<selection.length;i++) selection[i] = selected[i] | !selection[i]; // OR-NOT
				break;
			case 3:
				for (int i=0;i<selection.length;i++) selection[i] = selected[i] && selection[i]; // AND
				break;
			case 4:
				for (int i=0;i<selection.length;i++) selection[i] = selected[i] && !selection[i]; // AND-NOT
				break;
			case 5:
				for (int i=0;i<selection.length;i++) selection[i]= !selected[i];
				break;
			}
			// Remove disabled
			for (int i=0;i<selection.length;i++) selection[i] &= enabled[i];
			// replace current selection
			this.selectedImages[numSeries]=selection;
			System.out.println("manageSelection(): on exit more = "+more);
			return more?0:1;
    	}
       	/**
   	 *
   	 * @param numSeries Number of series to edit
   	 * @param useImages Select images for this series
   	 * @param fromToImages - limit number of checkboxes, otherwise window does not show the bottom ones
   	 * @param useParameters Select parameters for this series
   	 * @param askNextSeries Ask for next series number
   	 * @param zeroAndOther use 2 channels 0 and "other", propagate settings for channel 1 to all the rest
   	 * For low/high res (LWIR/EO) - use first /other for each class
   	 * @return -2 - cancel, -1, done, otherwise - number of step to edit
   	 */

    	public void listStrategies() {
    		if (this.selectedImages == null) {
    			String msg="No strategies defined";
    			IJ.showMessage("Error",msg);
    			return;
    		}
    		int fromSer = 0;
    		int toSer = this.selectedImages.length;
			boolean zeroAndOther=    true;
    		GenericDialog gd = new GenericDialog("List Fitting Strategies");
    		gd.addNumericField("First number of series to list", fromSer, 0);
    		gd.addNumericField("Last number of series to list", toSer - 1, 0);
    		gd.addCheckbox    ("Use only channel 0 and \"all other channels\"",zeroAndOther);
    		gd.showDialog();
    		if (gd.wasCanceled()) return;
    		fromSer=          (int) gd.getNextNumber();
    		toSer=            (int) gd.getNextNumber();
    		zeroAndOther=           gd.getNextBoolean();
    		listStrategies(fromSer, toSer, zeroAndOther);
    	}

    	public void listStrategies(
        		int     fromSer,
        		int     toSer,
       			boolean zeroAndOther
    			) {
    		if (this.selectedImages == null) {
    			String msg="No strategies defined";
    			IJ.showMessage("Error",msg);
    			return;
    		}
    		int numSubCams=this.distortionCalibrationData.getNumSubCameras();
    		int [] choice_offsets = new int [this.parameterEnable.length];
    		//
    	    String header="Strategy #\tType";
    	    StringBuffer sb = new StringBuffer();
    		for (int i=fromSer;i<toSer;i++)  if (isSeriesValid(i)) header+="\t"+i;
			sb.append("Comment\t");
			for (int i=fromSer;i<toSer;i++) if (isSeriesValid(i)) sb.append("\t"+this.strategyComment[i]);sb.append("\n");
			sb.append("Number of selected images\t");
			for (int i=fromSer;i<toSer;i++) if (isSeriesValid(i)) {
				int ns = 0;
				for (int j=0; j<this.selectedImages[i].length;j++) if (this.selectedImages[i][j]) ns++;
				sb.append("\t"+ns);
			}
			sb.append("\n");
			// parameters
			for (int ipar =0; ipar<this.parameterEnable.length;ipar++) if (this.parameterEnable[ipar] &&
//					(!zeroAndOther || (this.parameterList[ipar][0] <= 1) || (this.parameterList[ipar][0] ==24))){ // in "zeroAndOther" mode do not show other subcameras
				    (!zeroAndOther || this.distortionCalibrationData.firstInGroup(this.parameterList[ipar][0]))){ // in "zeroAndOther" mode do not show other subcameras
				int parIndex=this.parameterList[ipar][1];
				int subCam=this.parameterList[ipar][0];
				boolean isSub=this.distortionCalibrationData.isSubcameraParameter(parIndex);
//				String sChn=(zeroAndOther && (subCam>=1)&& (subCam<24))?"-head-other":
//					((zeroAndOther && (subCam>=24))?"-bottom":("-"+subCam));
				String sChn=this.distortionCalibrationData.getSubName(subCam,false);
				boolean noWeak=!this.distortionCalibrationData.eyesisCameraParameters.isExtrinsic(parIndex);
				boolean isTilt=this.distortionCalibrationData.eyesisCameraParameters.isTilt(parIndex);
				// for non-subcamera or subcam 0 - no extra choices. For "zeroAndOther" - only "0". For other subCam - all less than this
				// if zeroAndOther:
				// non-sub: -1
				// sub0 : -1
				// sub:    0
				// not zeroAndOther:
				// non-sub - -1:
				// sub: subCam-1 (previous index)
				//int subMaxNum = (this.distortionCalibrationData.isSubcameraParameter(parIndex) && (subCam > 0)) ? (zeroAndOther? 0 : (subCam-1)):-1;
				//
				// new:
				// if zeroAndOther:
				// non-sub: -1
				// firstInGroup : -1
				// sub(other):    their group number - no, 0 - later add this.definedModesAll.length to master subcamera
				// not zeroAndOther:
				// non-sub - -1:
				// sub(>0): subCam-1 (previous index) - will suggest any with smaller index - may be useless with lwir < eo indices
//				int subMaxNum = (this.distortionCalibrationData.isSubcameraParameter(parIndex) && (subCam > 0)) ? (zeroAndOther? 0 : (subCam-1)):-1;
				int subMaxNum = -1;
				if (this.distortionCalibrationData.isSubcameraParameter(parIndex)) { // only for subcamera parameters
					if (zeroAndOther) {
//						subMaxNum = this.distortionCalibrationData.firstInGroup(subCam)?-1:0; //this.distortionCalibrationData.getSubGroup(subCam);
						subMaxNum = (this.distortionCalibrationData.sourceToCopy(subCam) < 0)?-1:0; //this.distortionCalibrationData.getSubGroup(subCam);
					} else {
						subMaxNum =  subCam-1; // 0 will be -1 - this may be wrong to align to higher index
					}
				}


	/*=========*/

				String [] commonChoices = (isTilt?this.definedModesTiltEq:(noWeak?this.definedModesNoWeak:this.definedModes));
				String [] theseChoices;
				String thisChoice = "";
				choice_offsets[ipar] = 0;

				sb.append(this.distortionCalibrationData.getParameterName(parIndex)+"\t"+(isSub?(" sub"+sChn):"com "));
				for (int numSeries=fromSer;numSeries<toSer;numSeries++) if (isSeriesValid(numSeries)) {
					if (subMaxNum < 0){
						thisChoice = this.definedModesAll[this.parameterMode[numSeries][ipar]];
					} else { // only can happen if (!isTilt) && (noWeak)
						choice_offsets[ipar] = commonChoices.length;
//						theseChoices = new String [commonChoices.length + subMaxNum + 1];
						theseChoices = new String [commonChoices.length];
						for (int ch = 0; ch < commonChoices.length; ch++) theseChoices[ch] = commonChoices[ch];
/*
						if (zeroAndOther) {
							if (subMaxNum >=0) { // can only be 0
//								theseChoices[commonChoices.length] = "same as "+this.distortionCalibrationData.masterSub(subCam);
								// TODO: Make human-readable channel name?
								theseChoices[commonChoices.length] = "same as "+this.distortionCalibrationData.sourceToCopy(subCam);
							}
						} else {
							for (int ch = 0; ch <= subMaxNum; ch++)  {
								theseChoices[ch + commonChoices.length] = "same as "+ch;
							}
						}
*/
						// choice index for "same as ..." starts with  this.definedModesAll.length, but in the listbox index is lower
						int indx = this.parameterMode[numSeries][ipar];

						if (indx >= this.definedModesAll.length) {
							indx -= (this.definedModesAll.length - choice_offsets[ipar]);
						}
						if (indx < theseChoices.length) {
							thisChoice = theseChoices[indx];
						} else {
							thisChoice = "same as "+ (indx - theseChoices.length);
						}
//						System.out.println("selectStrategyStep(): this.parameterMode["+numSeries+"]["+ipar+"]=" + this.parameterMode[numSeries][ipar]+" indx = "+indx);
					}
					if (thisChoice.equals("fixed")) thisChoice = "-";
					sb.append("\t"+thisChoice);
				}
				sb.append("\n");

			}


		    new TextWindow("Strategies_List", header, sb.toString(), 800,1000);
			System.out.println("** Do not save as .csv - it will replace tabs with commas! ***");
    	}


    	/**
    	 *
    	 * @param numSeries Number of series to edit
    	 * @param useImages Select images for this series
    	 * @param fromToImages - limit number of checkboxes, otherwise window does not show the bottom ones
    	 * @param useParameters Select parameters for this series
    	 * @param askNextSeries Ask for next series number
    	 * @param zeroAndOther use 2 channels 0 and "other", propagate settings for channel 1 to all the rest
    	 * Updated for more groups (up to 4 in LWIR/EO) - use 1 channel for each group, propagate to the rest of the group
    	 * @return -2 - cancel, -1, done, otherwise - number of step to edit
    	 */
    	public int selectStrategyStep(
    			int numSeries,
    			boolean useImages,
				int [] fromToImages,
    			boolean allImages,
    			boolean useParameters,
    			boolean askLambdas,
    			boolean askNextSeries,
    			boolean zeroAndOther
    			){
    		boolean showDirectMap=false;
    		boolean showReverseMap=false;
    		boolean showAdvancedImageSelection=false;
    		int [] choice_offsets = new int [this.parameterEnable.length];

    		// if current series is not valid (probably just started a new one) - look for the last valid (if any)
    		// and copy it;
    		int numEstimated=this.distortionCalibrationData.getNumberOfEstimated(true); //(boolean enabledOnly
    		int [] numEstimatedPerStation=this.distortionCalibrationData.getNumberOfEstimatedPerStation(true); //(boolean enabledOnly
    		String sNumEstimatedPerStation="";
    		for (int i=0;i<numEstimatedPerStation.length;i++){
    			if (i>0) sNumEstimatedPerStation+=", ";
    			sNumEstimatedPerStation+=numEstimatedPerStation[i];
    		}
    		int numNewEnabled=this.distortionCalibrationData.getNumNewEnabled();
    		int [] numNewEnabledPerStation=this.distortionCalibrationData.getNumNewEnabledPerStation();
    		String sNumNewEnabledPerStation="";
    		for (int i=0;i<numNewEnabledPerStation.length;i++){
    			if (i>0) sNumNewEnabledPerStation+=", ";
    			sNumNewEnabledPerStation+=numNewEnabledPerStation[i];
    		}
     		if (!isSeriesValid(numSeries)){
    			int sourceSeries= findLastValidSeries(numSeries);
    			if (sourceSeries>=0)  copySeries(sourceSeries, numSeries);
    		}

			GenericDialog gd = new GenericDialog("Fitting Strategy Step Configuration, step "+numSeries+
					                             " number of enabled images="+this.distortionCalibrationData.getNumEnabled());
			gd.addStringField  ("Comment",   this.strategyComment[numSeries],80);
			gd.addCheckbox("Advanced image selection (disregard other fields)", showAdvancedImageSelection);
			gd.addCheckbox("Copy all from the series below, ignore all other fields", false);
			gd.addNumericField("Source series to copy from", (numSeries>0)?(numSeries-1):(numSeries+1), 0, 3, "");
			gd.addCheckbox("Remove all (but first) images, reopen dialog", false); // remove all will be invalid, copied from the previous
			gd.addCheckbox("Select all images, reopen dialog", false);
			if (numEstimated>0){
				gd.addMessage("There are "+numEstimated+" ("+sNumEstimatedPerStation+") enabled images that have estimated orientation");
				gd.addCheckbox("Select them and only them (and re-open dialog)", false);
			} else {
				gd.addMessage("There are no enabled images with estimated (from neighbors) orientation");
			}
			if (numNewEnabled>0){
				gd.addMessage("There are "+numNewEnabled+" ("+sNumNewEnabledPerStation+" new enabled images");
				gd.addCheckbox("Select them and only them (and re-open dialog)", false);
			} else {
				gd.addMessage("There are no new enabled images");
			}
			int numStations=this.distortionCalibrationData.eyesisCameraParameters.getNumStations();
			boolean [] constrainByStation=new boolean[numStations];
			for (int i=0;i<constrainByStation.length;i++) constrainByStation[i]=true;
			if (this.distortionCalibrationData.eyesisCameraParameters.numStations>1){
				gd.addMessage("Constrain (select/remove all) by stations");
				for (int i=0;i<this.distortionCalibrationData.eyesisCameraParameters.numStations;i++) 	gd.addCheckbox("Station "+i, constrainByStation[i]);
			}
			if (this.distortionCalibrationData.hasSmallSensors()) {
				gd.addMessage("Constrain (select/remove all) by High/Low resolution sensors (such as EO/LWIR)");
				gd.addCheckbox("Select high-res sensors", true);
				gd.addCheckbox("Select low-res sensors", true);
			}



			if (useImages) {
	    		gd.addNumericField("Image selection range, from", fromToImages[0], 0);
	    		gd.addNumericField("Image selection range, up to (including)", fromToImages[1], 0);
				gd.addMessage("Select files to include");
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++)
					if ((allImages || this.distortionCalibrationData.gIP[i].enabled) && (i>=fromToImages[0]) && (i<=fromToImages[1])){
					int hm=this.distortionCalibrationData.gIP[i].hintedMatch;
					gd.addCheckbox    (i+" - "+(this.distortionCalibrationData.gIP[i].enabled?"":"(disabled) ")+
							IJ.d2s(this.distortionCalibrationData.gIP[i].timestamp,6)+
							": "+this.distortionCalibrationData.gIP[i].channel+
							" matched "+this.distortionCalibrationData.gIP[i].matchedPointers+" pointers"+
							", hinted state: "+((hm<0)?"undefined":((hm==0)?"failed":((hm==1)?"orientation":"orientation and translation"))),
							this.selectedImages[numSeries][i]);
				}
				if (allImages) gd.addCheckbox("Enable selected, disable deselected images", false);

				gd.addNumericField("The 'master' (used for common parameters)", this.masterImages[numSeries], 0);

			}
			if (useParameters) {
				gd.addMessage("Select parameters to fit");
				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i] &&
///						(!zeroAndOther || (this.parameterList[i][0] <= 1) || (this.parameterList[i][0] ==24))){ // in "zeroAndOther" mode do not show other subcameras
						(!zeroAndOther || this.distortionCalibrationData.firstInGroup(this.parameterList[i][0]))){ // in "zeroAndOther" mode do not show other subcameras

					int parIndex=this.parameterList[i][1];
					int subCam=this.parameterList[i][0];
					boolean isSub=this.distortionCalibrationData.isSubcameraParameter(parIndex);
					boolean defined=false;
					double min=0.0,max=0.0;
					int sub_group = this.distortionCalibrationData.getSubGroup(subCam);
					for (int imgNumber=0;imgNumber<this.distortionCalibrationData.getNumImages(); imgNumber++) {
						if (this.selectedImages[numSeries][imgNumber] && this.distortionCalibrationData.gIP[imgNumber].enabled){
							int sub=this.distortionCalibrationData.gIP[imgNumber].channel;
							if (!isSub || (sub==subCam) ||  // global or same subcamera
									//									(zeroAndOther && (subCam>=1) && (subCam<24) && (sub>=1) && (sub<24)) || // both head "other"
									//									(zeroAndOther && (subCam>=24) && (sub>=24) )
									(zeroAndOther && (sub_group == this.distortionCalibrationData.getSubGroup(sub)))
									) { // both subcameras are "other" subcameras
								double parValue=this.distortionCalibrationData.getParameterValue(imgNumber,parIndex);
								if (!defined) {
									min=parValue;
									max=min;
									defined=true;
								}
								if (parValue<min) min=parValue;
								if (parValue>max) max=parValue;
							}
						}
					}
					// undefined, min, max
					String sValue=(defined)?((min==max)?(min+""):(min+"..."+max)):"undefined";
///					String sChn=(zeroAndOther && (subCam>=1)&& (subCam<24))?"-head-other":
///						((zeroAndOther && (subCam>=24))?"-bottom":("-"+subCam));
					String sChn = this.distortionCalibrationData.getSubName(subCam,false);

					boolean noWeak=!this.distortionCalibrationData.eyesisCameraParameters.isExtrinsic(parIndex);
					boolean isTilt=this.distortionCalibrationData.eyesisCameraParameters.isTilt(parIndex);
					// for non-subcamera or subcam 0 - no extra choices. For "zeroAndOther" - only "0". For other subCam - all less than this

///					int subMaxNum = (this.distortionCalibrationData.isSubcameraParameter(parIndex) && (subCam > 0)) ? (zeroAndOther? 0 : (subCam-1)):-1;
					// for non-subcamera or subcam 0 - no extra choices. For "zeroAndOther" - only "0". For other subCam - all less than this
					// if zeroAndOther:
					// non-sub: -1
					// sub0 : -1
					// sub:    0
					// not zeroAndOther:
					// non-sub - -1:
					// sub: subCam-1 (previous index)
					//int subMaxNum = (this.distortionCalibrationData.isSubcameraParameter(parIndex) && (subCam > 0)) ? (zeroAndOther? 0 : (subCam-1)):-1;
					//
					// new:
					// if zeroAndOther:
					// non-sub: -1
					// firstInGroup : -1
					// sub(other):    their group number - no, 0
					// not zeroAndOther:
					// non-sub - -1:
					// sub(>0): subCam-1 (previous index) - will suggest any with smaller index - may be useless with lwir < eo indices
//					int subMaxNum = (this.distortionCalibrationData.isSubcameraParameter(parIndex) && (subCam > 0)) ? (zeroAndOther? 0 : (subCam-1)):-1;
					int subMaxNum = -1;
					if (this.distortionCalibrationData.isSubcameraParameter(parIndex)) { // only for subcamera parameters
						if (zeroAndOther) {
//							subMaxNum = this.distortionCalibrationData.firstInGroup(subCam)?-1:0; //this.distortionCalibrationData.getSubGroup(subCam);
							subMaxNum = (this.distortionCalibrationData.sourceToCopy(subCam) < 0)?-1:0; //this.distortionCalibrationData.getSubGroup(subCam);
						} else {
							subMaxNum =  subCam-1; // 0 will be -1 - this may be wrong to align to higher index
						}
					}

					String [] commonChoices = (isTilt?this.definedModesTiltEq:(noWeak?this.definedModesNoWeak:this.definedModes));
					String [] theseChoices;
					String thisChoice = "";
					choice_offsets[i] = 0;
					if (subMaxNum < 0){
						theseChoices = commonChoices;
						thisChoice = this.definedModesAll[this.parameterMode[numSeries][i]];
					} else { // only can happen if (!isTilt) && (noWeak)
						choice_offsets[i] = commonChoices.length;
						theseChoices = new String [commonChoices.length + subMaxNum + 1];
						for (int ch = 0; ch < commonChoices.length; ch++) theseChoices[ch] = commonChoices[ch];
						if (zeroAndOther) {
							if (subMaxNum >=0) {
//								theseChoices[commonChoices.length] = "same as "+this.distortionCalibrationData.masterSub(subCam);
								// TODO: Make human-readable channel name?
								theseChoices[commonChoices.length] = "same as "+this.distortionCalibrationData.sourceToCopy(subCam);
							}
						} else {
							for (int ch = 0; ch <= subMaxNum; ch++)  {
								theseChoices[ch + commonChoices.length] = "same as "+ch;
							}
						}
						// choice index for "same as ..." starts with  this.definedModesAll.length, but in the listbox index is lower
						int indx = this.parameterMode[numSeries][i];
						if (indx >= this.definedModesAll.length) {
							indx -= (this.definedModesAll.length - choice_offsets[i]);
						}
						// currently only out of limits can occur with a single choice, so
						if (indx >= theseChoices.length) {
							indx = theseChoices.length - 1;
						}


						thisChoice = theseChoices[indx];
//						System.out.println("selectStrategyStep(): this.parameterMode["+numSeries+"]["+i+"]=" + this.parameterMode[numSeries][i]+" indx = "+indx);
					}
					gd.addChoice( // ArrayIndexOutOfBoundsException: 9
							(this.distortionCalibrationData.isSubcameraParameter(parIndex)?"":"* ") +
							this.distortionCalibrationData.getParameterName(parIndex)+
							" ("+sValue+" "+
							this.distortionCalibrationData.getParameterUnits(parIndex)+")"+
//							(this.distortionCalibrationData.isSubcameraParameter(parIndex)?(" sub"+sChn):"com "),
							(this.distortionCalibrationData.isSubcameraParameter(parIndex)?("   "+sChn):"   common"),
							theseChoices,
							thisChoice);
				}
			}
			if (askLambdas) {
				gd.addNumericField("Initial lambda for the L-M algorithm", this.lambdas[numSeries], 6,8,"");
				gd.addStringField("Relative decrese in error to error ratio to consider series finished", ""+this.stepDone[numSeries], 8);
				gd.addCheckbox("Stop after this series", this.stopAfterThis[numSeries]);
			}
			if (this.varianceModes!=null) gd.addChoice(
					"Processing of selected parameters variances",
					this.definedVarianceModes,
					this.definedVarianceModes[this.varianceModes[numSeries]]);
			gd.addCheckbox("Edit variances costs", false);
			if (numStations>1){
				int oldZGroupsLength=(this.zGroups[numSeries]!=null)?this.zGroups[numSeries].length:0;
				int [] oldZGroups={};
				if (oldZGroupsLength>0) oldZGroups=this.zGroups[numSeries].clone();
				this.zGroups[numSeries]=new int [numStations];
				int nextZGroup=-1;
				for (int i=0;i<numStations;i++){
					if (i<oldZGroupsLength) {
						this.zGroups[numSeries][i]=oldZGroups[i];
						if (this.zGroups[numSeries][i]>nextZGroup) nextZGroup=this.zGroups[numSeries][i];
					} else {
						this.zGroups[numSeries][i]=++nextZGroup;
					}
				}
				String [] zGroupsChoices=new String [numStations+1];
				zGroupsChoices[0]="Not used";
				for (int i=0;i<numStations;i++) zGroupsChoices[i+1]="Group "+i;
				gd.addMessage ("Select groups of same pattern for different stations (pattern did not move between measurements). Used for pattern correction command");
				for (int i=0;i<numStations;i++){
					int zG=(this.zGroups[numSeries][i]<0)?0:(this.zGroups[numSeries][i]+1);
					gd.addChoice( // ArrayIndexOutOfBoundsException: 9
							"Same target group for station " +i,
							zGroupsChoices,
							zGroupsChoices[zG]); // definedModesAll - includes all others

				}
			} else {
				this.zGroups[numSeries]=new int [1];
				this.zGroups[numSeries][0]=0;
			}

			if (askNextSeries) {
				gd.addCheckbox("Rebuild/Show parameter Map", showDirectMap);
				gd.addCheckbox("Rebuild/Show reverse parameter Map", showReverseMap);
				gd.addNumericField("Next series to edit (<0 - done)", numSeries+1, 0);
			}

			if (askNextSeries) gd.enableYesNoCancel("OK", "Done");
			WindowTools.addScrollBars(gd);
			gd.showDialog();
			if (gd.wasCanceled()) return -2;
			this.strategyComment[numSeries]= gd.getNextString();
			showAdvancedImageSelection=gd.getNextBoolean();
			if (showAdvancedImageSelection){
				int rslt=0;
				while (rslt==0) {
					rslt=manageSelection(numSeries);
				}
				return numSeries; // cancel from manageSelection will just exit that mode with no changes
			}
			boolean copyFromPrevious=gd.getNextBoolean();
			int sourceStrategy= (int) gd.getNextNumber();
			boolean removeAllImages=gd.getNextBoolean();
			boolean selectAllImages=gd.getNextBoolean();
			boolean selectEstimated=false;
			if (numEstimated>0) selectEstimated=gd.getNextBoolean();
			boolean selectNewEnabled=false;
			if (numNewEnabled>0) selectNewEnabled=gd.getNextBoolean();
			if (this.distortionCalibrationData.eyesisCameraParameters.numStations>1){
				for (int i=0;i<constrainByStation.length; i++) constrainByStation[i]=gd.getNextBoolean();
			}
			boolean [] selectHiLowRes = null;
			if (this.distortionCalibrationData.hasSmallSensors()) {
				selectHiLowRes = new boolean[2];
				selectHiLowRes[0] = gd.getNextBoolean(); //	gd.addCheckbox("Select high-res sensors", true);
				selectHiLowRes[1] = gd.getNextBoolean(); //	gd.addCheckbox("Select low-res sensors", true);
			}

			if (selectNewEnabled) {
				this.selectedImages[numSeries]=this.distortionCalibrationData.selectNewEnabled();
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
					this.selectedImages[numSeries][i]&=constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
				}
				return numSeries; // caller will repeat with the same series
			}

			if (selectEstimated) {
				this.selectedImages[numSeries]=this.distortionCalibrationData.selectEstimated(true); //(boolean enabledOnly
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
					this.selectedImages[numSeries][i]&=constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
				}
				return numSeries; // caller will repeat with the same series
			}

			if (copyFromPrevious){
				int sourceSeries= findLastValidSeries(sourceStrategy);
				if (sourceSeries>=0) {
					copySeries(sourceSeries, numSeries);
				} else {
					System.out.println("Could not copy from invalid series "+sourceSeries);
				}
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
					this.selectedImages[numSeries][i]&=constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
				}
				return numSeries; // caller will repeat with the same series
			}
			if (removeAllImages || selectAllImages) {
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++){
					this.selectedImages[numSeries][i]=selectAllImages || ((i==0) && removeAllImages); // invalidate - all, regardless of .enabled
					this.selectedImages[numSeries][i] &= constrainByStation[this.distortionCalibrationData.gIP[i].getStationNumber()];
					if (selectHiLowRes != null) {
						int small_01 = this.distortionCalibrationData.isSmallSensor(i)?1:0;  //OK, i here is image number, not channel number
//						System.out.println(i+":"+small_01);
						this.selectedImages[numSeries][i] &= selectHiLowRes[small_01];
					}

				}
				return numSeries; // caller will repeat with the same series
			}
			boolean enableDisableSelected=false;
			if (useImages) {
				fromToImages[0]=    (int) gd.getNextNumber();
				fromToImages[1]=    (int) gd.getNextNumber();
				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) {
//					if ((allImages || this.distortionCalibrationData.gIP[i].enabled) && (i>=fromToImages[0]) && (i<=fromToImages[1])){
//						this.selectedImages[numSeries][i]=gd.getNextBoolean();
//					}
					if ((i>=fromToImages[0]) && (i<=fromToImages[1])){
						if (allImages || this.distortionCalibrationData.gIP[i].enabled) {
							this.selectedImages[numSeries][i]=gd.getNextBoolean();
						} else {
							this.selectedImages[numSeries][i]=false; // unselect stray non-shown images
						}



					}


				}
				if (allImages) enableDisableSelected=gd.getNextBoolean();
				this.masterImages[numSeries]=(int) gd.getNextNumber();
				if (this.masterImages[numSeries]<0)this.masterImages[numSeries]=0;
				if (this.masterImages[numSeries]>=this.selectedImages[numSeries].length)this.masterImages[numSeries]=this.selectedImages[numSeries].length;
			}
//			if (selectHiLowRes != null) {
//				for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) {
//					boolean low_res = this.distortionCalibrationData.isSmallSensor(i);
//					if ((low_res && !selectHiLowRes[1]) && (!low_res && !selectHiLowRes[0])){
//						this.selectedImages[numSeries][i]= false;
//					}
//				}
//			}

			if (useParameters) {
				int [] lastGroups=null;
				for (int i =0; i<this.parameterEnable.length;i++) if (this.parameterEnable[i] &&
//						(!zeroAndOther || (this.parameterList[i][0] <=1) || (this.parameterList[i][0]==24))){ // in "zeroAndOther" mode do not show other subcameras
				    (!zeroAndOther || this.distortionCalibrationData.firstInGroup(this.parameterList[i][0]))){ // in "zeroAndOther" mode do not show other subcameras

					this.parameterMode[numSeries][i]=gd.getNextChoiceIndex();

					// make adjustment for "same as (other lower numbered subcamera)"
					if ((choice_offsets[i] > 0) && (this.parameterMode[numSeries][i] >= choice_offsets[i])){
						if (zeroAndOther) { // add reference channel
							int ref_chn = this.distortionCalibrationData.sourceToCopy(this.parameterList[i][0]);
							this.parameterMode[numSeries][i] += ref_chn;
						}
						System.out.print("selectStrategyStep(): choice_offsets["+i+"]="+choice_offsets[i]+ " this.parameterMode["+numSeries+"]["+i+"]=" + this.parameterMode[numSeries][i]);
						this.parameterMode[numSeries][i] += (this.definedModesAll.length - choice_offsets[i]);
						System.out.println(", corrected=" + this.parameterMode[numSeries][i]);
					}

					if (this.parameterMode[numSeries][i]==modeGroup) {
						if (this.parameterGroups[numSeries][i]!=null) lastGroups=this.parameterGroups[numSeries][i]; // default groups
						else if (lastGroups!=null) this.parameterGroups[numSeries][i]=lastGroups.clone(); // may be null
						selectGroups(numSeries,i);
					}
				}
				if (zeroAndOther ){
					for (int i =0; i<this.parameterEnable.length;i++) {
//						if ((this.parameterList[i][0]>1) && (this.parameterList[i][0]!=24)){ // "other" subchannels - copy from subchannel1
						if (!this.distortionCalibrationData.firstInGroup(this.parameterList[i][0])){ // "other" subchannels - copy from subchannel1
///						if (this.distortionCalibrationData.sourceToCopy(this.parameterList[i][0]) >= 0){ // "other" subchannels - copy from subchannel1
//							int refChannel=(this.parameterList[i][0]<24)?1:24;
							int refChannel= this.distortionCalibrationData.masterSub(this.parameterList[i][0]);
///							int refChannel= this.distortionCalibrationData.sourceToCopy(this.parameterList[i][0]);

							int iSub1=getParameterNumber(refChannel, this.parameterList[i][1]);
							if (this.parameterEnable[iSub1]){
								//					System.out.println(	"parameter number="+i+" this.parameterList[i][0]="+this.parameterList[i][0]+" this.parameterList[i][1]="+this.parameterList[i][1]+" iSub1="+iSub1);
								this.parameterMode[numSeries][i]=this.parameterMode[numSeries][iSub1];
								if (this.parameterMode[numSeries][i]==modeGroup) { // copy groups from channel 1
									if (this.parameterGroups[numSeries][i]!=null) this.parameterGroups[numSeries][i]=this.parameterGroups[numSeries][iSub1].clone(); // may be null
									else this.parameterGroups[numSeries][i]=null;
								}
							}
						}
					}
				}
			}
			if (askLambdas) {
				this.lambdas[numSeries]=gd.getNextNumber();
				this.stepDone[numSeries]=Double.parseDouble(gd.getNextString());
				this.stopAfterThis[numSeries]=gd.getNextBoolean();
			}
			if (enableDisableSelected) {
				this.distortionCalibrationData.enableSelected(this.selectedImages[numSeries]);
			}

			if (this.varianceModes!=null) this.varianceModes[numSeries]=gd.getNextChoiceIndex();

			boolean editVariancesCosts=gd.getNextBoolean();
			if (editVariancesCosts){
				for (int i =0; i<this.parameterList.length;i++) {
					int parIndex=this.parameterList[i][1];
					if ((this.parameterMode[numSeries][i]==modeWeakCommon) ||
							(this.parameterMode[numSeries][i]==modeWeakStation) ||
							(this.parameterMode[numSeries][i]==modeTiltEqualize)
							){
						if (!this.distortionCalibrationData.eyesisCameraParameters.isExtrinsic(parIndex)){
							System.out.println("BUG: this.parameterMode["+numSeries+"]["+i+"]="+this.parameterMode[numSeries][i]+
									", but this parameter ("+this.distortionCalibrationData.getParameterName(parIndex)+" is not valid for variances");
							continue;
						}
						this.distortionCalibrationData.eyesisCameraParameters.editCostProperties(
								parIndex,
								this.distortionCalibrationData.getParameterName(parIndex),
								this.distortionCalibrationData.getParameterDescription(parIndex),
								this.distortionCalibrationData.getParameterUnits(parIndex));
					}
				}
			}

			if (numStations>1){
				for (int i=0;i<numStations;i++){
					this.zGroups[numSeries][i]=gd.getNextChoiceIndex()-1;
				}
			}

			if (!gd.wasOKed()) return -1; // pressed Done (no need to ask for the next number)

			if (askNextSeries) {
				showDirectMap=gd.getNextBoolean();
				showReverseMap=gd.getNextBoolean();
				if (showDirectMap || showReverseMap){
					buildParameterMap(numSeries);
					if (showDirectMap)  showCurrentParameterMap       ("Parameter map");
					if (showReverseMap) showCurrentReverseParameterMap("Reverse parameter map");
				}

				int nextSeries= (int) gd.getNextNumber();
				if (nextSeries<-1) nextSeries=-1;
				return nextSeries;
			}
			return -1;
    	}

    	private boolean selectGroups(
    			int numSeries,
    			int numPar){
    		//   		if (this.debugLevel>1){
    		//   			System.out.println("selectGroups("+numSeries+", "+numPar+")");
    		//   		}


    		int parIndex=this.parameterList[numPar][1];
    		int subCam=this.parameterList[numPar][0];
    		String name=this.distortionCalibrationData.getParameterName(parIndex)+
    				(this.distortionCalibrationData.isSubcameraParameter(parIndex)?(" s"+subCam):"com ");
    		GenericDialog gd = new GenericDialog("Select image groups for "+name);
    		gd.addMessage("Select which images share the same value of "+name);

    		if (this.parameterGroups[numSeries][numPar]==null) {
    			this.parameterGroups[numSeries][numPar]=new int [this.distortionCalibrationData.getNumImages()];
    			for (int i=0;i<this.parameterGroups[numSeries][numPar].length;i++)this.parameterGroups[numSeries][numPar][i]=0;
    		}
    		String [] choices=organizeGroups(this.parameterGroups[numSeries][numPar],false); // preserve group numbers
    		for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) if (this.selectedImages[numSeries][i]){
    			gd.addChoice (i+" - "+IJ.d2s(this.distortionCalibrationData.gIP[i].timestamp,6)+
    					": "+this.distortionCalibrationData.gIP[i].channel,
    					choices,
    					choices[this.parameterGroups[numSeries][numPar][i]]
    					);
    		}
    		WindowTools.addScrollBars(gd);

    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		for (int i =0; i<this.distortionCalibrationData.getNumImages();i++) if (this.selectedImages[numSeries][i]){
    			this.parameterGroups[numSeries][numPar][i]=gd.getNextChoiceIndex();
    		}
    		return true;

    	}
    	/**
    	 * Organizes list of groups and creates a list of selection choices. If all members fit in the range
    	 * of 0 (length-1) and (force==false), group numbers are preserved, otherwise they are renumbered
    	 * @param groups array of integers - group numbers
    	 * @param force force renumbering groups
    	 * @return list of selection choices
    	 */
    	private String [] organizeGroups(int []groups, boolean force) {
    		if ((groups==null) || (groups.length==0)){
    			String msg="Cannot organize mull or empty group";
    			IJ.showMessage("Error",msg);
    			throw new IllegalArgumentException (msg);
    		}
    		// See if 0.. length-1 groups is not enough to represent all numbers used
    		for (int i=0;!force && (i<groups.length);i++) if ((groups[i]<0) || (groups[i]>=(groups.length-1))) force = true;
    		if (force){
    			int [] tmp=groups.clone();
    			for (int i=0;i<groups.length;i++) groups[i]=-1;
    			int groupNumber=0;
    			int max=tmp[0]; for (int i=0;i<tmp.length;i++) if (max<tmp[i]) max=tmp[i];
    			for (boolean organized=false; !organized;){
    				int min=max+1;
    				for (int i=0;i<tmp.length;i++) if ((groups[i]<0) && (min>tmp[i])) min=tmp[i];
    				if (min>max) organized=true;
    				else {
    					for (int i=0;i<tmp.length;i++) if ((groups[i]<0) && (min == tmp[i])) {
    						groups[i]=groupNumber;
    					}
    					groupNumber++;
    				}
    			}
    		}
    		String [] rslt= new String [groups.length];
    		for (int i=0;i<rslt.length;i++) rslt[i]="Group "+(i+1);
    		return rslt;

    	}
    	public boolean selectStrategy(int startSerNumber){
    		int defaultLength=30;
    		boolean selectImages=    false;
    		boolean allImages=       false;
    		boolean selectParameters=true;
    		boolean askNextSeries=   true;
    		boolean askLambdas=      true;
    		boolean askParameterMask=false;
			boolean zeroAndOther=    true;
			int [] fromToImages={0,500};

    		int numSeries=startSerNumber;
    		int oldLength=(this.selectedImages==null)?0:this.selectedImages.length;

    		GenericDialog gd = new GenericDialog("Fitting Strategy Step Configuration");
    		if (oldLength<=0) gd.addNumericField("Number of series in this strategy", defaultLength, 0);
    		gd.addNumericField("Number of series to edit (<0 - none)", numSeries, 0);
//    		gd.addNumericField("Number of series in this strategy", (oldLength>0)?oldLength:1, 0); //
    		gd.addCheckbox    ("Select images",selectImages);
    		gd.addNumericField("Show image checkboxes from", fromToImages[0], 0);
    		gd.addNumericField("Show image checkboxes up to (including)", fromToImages[1], 0);
    		gd.addCheckbox    ("Select from all images (false - only enabled)",allImages);
    		gd.addCheckbox    ("Select parameters",selectParameters);
    		gd.addCheckbox    ("Ask for initial lambda",askLambdas);
    		gd.addCheckbox    ("Ask for parameter mask",askParameterMask);
    		gd.addCheckbox    ("Use only 'master' channels",zeroAndOther);
    		gd.addCheckbox    ("Ask for next series",askNextSeries);
    		if (oldLength>0)  gd.addNumericField("Increase number of series in this strategy", oldLength, 0);
   	        gd.enableYesNoCancel("OK", "Done");

    		gd.showDialog();
    		if (gd.wasCanceled()) return false;
    		int numberOfSeries=0;
    		if (oldLength<=0) numberOfSeries= (int) gd.getNextNumber();
    		numSeries=          (int) gd.getNextNumber();
    		selectImages=             gd.getNextBoolean();
    		fromToImages[0]=    (int) gd.getNextNumber();
    		fromToImages[1]=    (int) gd.getNextNumber();
    		allImages=                gd.getNextBoolean();
    		selectParameters=         gd.getNextBoolean();
    		askLambdas=               gd.getNextBoolean();
    		askParameterMask=         gd.getNextBoolean();
    		zeroAndOther=             gd.getNextBoolean();
    		askNextSeries=            gd.getNextBoolean();
    		if (oldLength>0) numberOfSeries= (int) gd.getNextNumber();
    		if (numberOfSeries!=oldLength)  setLength(numberOfSeries);
    		if (!gd.wasOKed()) return true;
    		if (askParameterMask) setParameterSelectionMask(zeroAndOther);
    		while ((numSeries>=0) && (numSeries<this.selectedImages.length)){
    			numSeries=selectStrategyStep(
    					numSeries,
    					selectImages,
    					fromToImages,
    					allImages,
    					selectParameters,
    					askLambdas,
    					askNextSeries,
    					zeroAndOther);
    		}
    		return true;
    	}

//TODO: Use Tabbed dialog
    	public boolean setParameterSelectionMask(boolean zeroAndOther){
    		GenericDialog gd = new GenericDialog("Set Parameter Selection Mask");
//    		gd.addMessage("Common parameters and sub-camera 0 parameters");
//    		gd.addMessage("Common parameters");
    		int lastSub = -2;
    		boolean [] show_parameter = new boolean[this.parameterList.length];
    		for (int i=0;i<this.parameterList.length;i++){
    			show_parameter[i]=true;
    			int subCam = this.parameterList[i][0];
    			int parIndex=this.parameterList[i][1];
    			boolean isSub=this.distortionCalibrationData.isSubcameraParameter(parIndex);
    			if (!isSub) subCam = -1;
				show_parameter[i] = true; // show all non-subcamera parameters
    			if (subCam >= 0) {
    				if (zeroAndOther) {
    					if (this.distortionCalibrationData.firstInGroup(subCam)) {
    						if (subCam != lastSub) gd.addMessage(this.distortionCalibrationData.getSubName(subCam,true));
    					} else {
    						show_parameter[i] = false;
    					}
    				} else {
    					gd.addMessage("Sub-camera "+subCam+" parameters");
    				}
    			} else {
					if (subCam != lastSub) gd.addMessage("Common parameters");
    			}
    			lastSub = subCam;
    			if (show_parameter[i]) {
    				gd.addCheckbox (this.distortionCalibrationData.getParameterName(parameterList[i][1]),this.parameterEnable[i]);
    			}
    		}
      	     WindowTools.addScrollBars(gd);
//   	     gd.setBackground(Color.white);
			gd.showDialog();
			if (gd.wasCanceled()) return false;
			//May add disabling all other subcameras, but try keeping it for later running with zeroAndOther==false
    		for (int i=0;i<this.parameterList.length;i++) if(show_parameter[i]) {
    			this.parameterEnable[i]=gd.getNextBoolean();
    		}
    		return true;
    	}
//this.currentSeriesNumber
    	public double getLambda(){
    		return getLambda(this.currentSeriesNumber);
    	}
    	public double getLambda(int numSeries){
    		return this.lambdas[numSeries];

    	}
    	public double getStepDone(){
    		return getStepDone(this.currentSeriesNumber);
    	}
    	public double getStepDone(int numSeries){
    		return this.stepDone[numSeries];
    	}
    }
