/**
 **
 ** OrthoMapsParameters.java - Configuration parameters for orthomosaic map generation and export
 **
 ** Copyright (C) 2024 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  OrthoMapsParameters.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/>.
 ** -----------------------------------------------------------------------------**
 **
 */
package com.elphel.imagej.orthomosaic;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;

import com.elphel.imagej.calibration.CalibrationFileManagement;
import com.elphel.imagej.common.GenericJTabbedDialog;

import ij.Prefs;
import ij.text.TextWindow;

public class OrthoMapsParameters {
	public static String  RESULTS_SUFFIX = "-results.csv";
	public static String  LOG_SUFFIX = ".log";
//	public	boolean       save;
//	public	boolean       show;
	public  boolean       table_datetime;
	public  boolean       append_table;
	

	public	String        save_dir;
	public	String        object_type;
	public	double []     min_corrs;  // one per
	public	double []     phaseCoeff; // one per
	public	double []     lpf_sigma;  // one per; lpf_sigma
	public	double        min_corr_full_rel;
	public	double        min_corr_half_rel;
	public	double        full_preference;
	public	double        max_min_ratio;
	public	boolean[]     combine_full;
	public	int           corr_size;
	public	double        adv_radius; //
	public	double        search_radius; // look for other scene max within this distance from main
	public	double        scale_warp;
	public	int           extr_size;
	public	boolean       remove_dc;
	public	double        abs_edge_frac; //  =     0.25;
	public	double        abs_oversize; //  =      1.8;
	public	boolean       abs_force_round;
	public	int           abs_mode; //  =          1; // 0 - keep; 1 keep type and if half +/-1; 2 - keep type; 3 - any
	public	boolean       abs_invert; //  =        true; // center has lower value than around
	public	double        abs_outliers_frac;
	public	boolean       abs_obscure_warm; // =  true; // obscured can only be by warmer objects 
	public	double        abs_obscure_frac; //  0.25; // obscured threshold between center and outer 
	public	boolean       filt_keep;
	public	boolean       filt_keep_pre;
	public	boolean       filt_atonce;
	
	public	double        filt_frac_max;  // measure are at fraction of max			
	public	double [][]   filt_max_radius;
	public	double [][]   filt_elongation;
	public	double        filt_other_rad;  // should be no disconnected high peaks in this radius (only for halves multiplied)
	public	double []     filt_other_frac; // no pixels around max at filt_other_rad should be higher than this fraction of max
	public	double []     filt_dist;
	public	double        filt_height;
	public	double []     filt_full_half_frac;
	public	double []     filt_roundness;
	public	double []     filt_best;
	public	double        filt_abs_contrast; //  = 50.0;
	public	double        filt_abs_easepart;
	
	public	boolean       convolve_after; // true for old kernel tuning (kernel decimation before convolution)
	public	boolean       corr_centered; // re-correlate first scene centered at candidates
	public  int           debugLevel;
	public  String        save_top_dir;
	public  String        sub_dir;       // also the bas of the name to save combined results
	public	boolean       show_images;
	public	boolean       show_final_image;
	public	boolean       save_images;
	public	boolean       show_results;
	public  boolean       append_log;
	public  String        alt_mode;
	public  boolean       results_multi; // save multiple scenes result in the same file
	
	public OrthoMapsParameters() {
		setDefaults();
	}

	public void setDefaults() {
		min_corrs =           new double []{0.009, 0.0036}; // 0.0035}; // morning/evening
		phaseCoeff =          new double []{0.98,0.5};
		lpf_sigma =           new double []{0.0, 0.0};
		min_corr_full_rel =   0.85; // try 0.85
		min_corr_half_rel =   1.2;  // increase correlation threshold for half-patterns relative to full patterns;
		full_preference  =    1.4;
		max_min_ratio =       3.0;
		combine_full =        new boolean[] {true, true}; // try for other olso?
		corr_size =           128; // pix
		adv_radius =          30.0; // cm ?
		search_radius =       15;    // look for other scene max within this distance from main
		object_type =        "TM62";
		scale_warp =          1.0; // use 4?
		extr_size =           128;
		remove_dc =           true;
		abs_edge_frac =       0.25;
		abs_oversize =        1.8;
		abs_force_round =     false; // after selection best pattern, force full pattern
		abs_mode =            1; // 0 - keep, 1 keep type and if half +/-1, 2 - keep type, 3 - any, force full pattern
		abs_invert =          true; // center has lower value than around
		abs_outliers_frac =   0.3;
		abs_obscure_warm =    false; // true; // obscured can only be by warmer objects 
		abs_obscure_frac =    0.25; // obscured threshold between center and outer 
		filt_keep =           false; // list filtered out, but actually keep them
		filt_keep_pre =       false; // Keep pre first filter
		filt_atonce =         false; // apply main/second together, false - first filter main only
		filt_frac_max =       0.6;   // 0.5;  // measure are at fraction of max
		filt_max_radius =     new double[][] {{1.0,1.0},{1.2,1.2}}; // {1.0,1.0}}; // [second/main][full/half] - inactive now
		filt_elongation =     new double[][] {{1.7,1.7},{1.7,1.7}}; // [second/main][full/half] ellipse axis ratio
		filt_other_rad  =     25; // should be no disconnected high peaks in this radius (only for halves multiplied)
		filt_other_frac =     new double[] {0.8,0.68}; // -.8}; // no pixels around max at filt_other_rad should be higher than this fraction of max
		filt_dist =           new double[]{10.0,5.0};  // full offset and perpendicular to images offsets
		filt_height =         0.2; // allow extra negative offset parallel to image offset, corresponding to object above ground
		filt_full_half_frac = new double[]{0.675, 0.5}; // second probably not needed, it is for main
		filt_roundness =      new double[]{0.58, 0.0}; //{0.6, 0.0}; // second currently is not calculated/ used 215 mine1 0.059618
		filt_best =           new double[]{0.095, 0.0};// second currently is not calculated
		filt_abs_contrast =   80.0; // 50.0;
		filt_abs_easepart =   1.0; // 1.0-no ease for low AGL. Allow lower absolute contrast for partial patterns (high AGL)
		convolve_after =      false; // true for old kernel tuning (kernel decimation before convolution)
		corr_centered =       true; // re-correlate first scene centered at candidates
		save_top_dir =        "/media/elphel/NVME/lwir16-proc/ortho_videos/debug/sept12-13/pattern_match/";
		sub_dir =             "bicubic_01";
		show_images =         false;
		show_final_image =    true;
		save_images =         true;
		show_results =        false;
		table_datetime =      true;
		append_table =        true;
		append_log =          true;
		alt_mode=             "??m";		
		debugLevel =          0;
		results_multi =       true;
	}
	
	public void setDefaults50() {
		alt_mode="50m";
		lpf_sigma =           new double [] {0.25, 0.25};
		phaseCoeff =          new double [] {0.98, 0.5}; //  {0.98, 0.5};{.995,.995}; //
		full_preference  =    1.2; // 1.4;
//		min_corrs =   new double[] {0.008, 0.0036};
		min_corrs =           new double[] {0.007,0.0036}; // {0.007, 0.0036};
		min_corr_full_rel =   0.85; // try 0.85
		min_corr_half_rel =   1.2;  // increase correlation threshold for half-patterns relative to full patterns;
		abs_edge_frac =       0.25;
		abs_oversize =        1.8;
		abs_force_round =     false; // after selection best pattern, force full pattern
		filt_abs_contrast =   85; // 70;
		filt_roundness[0] =   0.36; //0.38; // 0.4; // 0.56;
		filt_best[0] =        0.0085; // 0.01;
		// mode == 1 seems slightly better for mine 5 (absolute contrast)
		abs_mode =            1; // 0; // 1; // 0 - keep, 1 keep type and if half +/-1, 2 - keep type, 3 - any, force full pattern
		abs_invert =          true; // center has lower value than around
		abs_outliers_frac =   0.3;
		abs_obscure_warm =    true; // false; // true; // obscured can only be by warmer objects 
		abs_obscure_frac =    0.25; // obscured threshold between center and outer 
		filt_max_radius =     new double [][] {{0.47,0.29},{1.2,1.2}}; // {1.0,1.0}}; // [second/main][full/half] - inactive now
		filt_elongation =     new double [][] {{1.45,1.46},{1.7,1.7}}; // [second/main][full/half] ellipse axis ratio
		filt_other_rad  =     25; // should be no disconnected high peaks in this radius (only for halves multiplied)
		filt_other_frac =     new double []{0.65,0.68}; // -.8}; // no pixels around max at filt_other_rad should be higher than this fraction of max
	}
	public void setDefaults75() {
		alt_mode="75m";
		full_preference  =  1.15; // 5 - 1.21
		lpf_sigma =  new double [] {0.25, 0.25};
		phaseCoeff = new double [] {0.95, 0.95};
		//				min_corrs[0] =      0.0119;
//		min_corrs =   new double[]{0.0090,0.008}; // A1: .0654
		min_corrs =   new double[]{0.0087,0.008}; // A1: .0654
		abs_edge_frac =     0.35;
		abs_oversize =      2.2;		
		filt_abs_contrast = 75; // 70;
		filt_abs_easepart = 0.85;
		abs_obscure_warm = true;

		// parameters for force round
		abs_force_round =   true;
		filt_roundness[0] = 0.56; // 0.55;
		filt_best[0] =      0.0087; // 0.009; //****** tune, this one is inactive
		filt_max_radius = new double [][] {{0.35,0.2},{0.35,0.2}}; // {1.0,1.0}}; // [second/main][full/half] - inactive now
		filt_elongation = new double [][] {{1.45,1.55},{1.45,1.55}}; // [second/main][full/half] ellipse axis ratio
		filt_other_rad  = 25; // should be no disconnected high peaks in this radius (only for halves multiplied)
		filt_other_frac = new double [] {0.6,0.6}; // -.8}; // no pixels around max at filt_other_rad should be higher than this fraction of max
	}
	public void setDefaults100() {
		alt_mode="100m";
		lpf_sigma = new double [] {0.25, 0.25};
		//				min_corrs =   new double[]{0.0068,0.006};
		min_corrs =   new double[]{0.0064,0.006}; // A1: .0654
		//				min_corrs =   new double[]{0.0066,0.0057};
		abs_edge_frac =     0.35;
		abs_oversize =      2.2;		
		filt_abs_contrast = 60;
		filt_abs_easepart = 0.85;
		phaseCoeff[0] =     0.95;
		phaseCoeff[1] =     0.95;
		abs_force_round =   true;
		filt_max_radius = new double [][] {{0.6,0.3}, {1.0,1.0}}; // {1.0,1.0}}; // [second/main][full/half] - inactive now
		//				filt_elongation = new double [][] {{1.4,1.5}, {1.7,1.7}};
		filt_elongation = new double [][] {{1.4,1.55}, {1.7,1.7}};
		filt_frac_max =     0.7;
		filt_dist = new double[]{5.0,2.5};  // full offset and perpendicular to images offsets
		filt_roundness[0] = 0.55; // 0.6;214-213_1694565491_890936-1694565488_589836 is 0.589
		filt_best[0] =      0.0064; // tune
		filt_other_frac = new double [] {0.75,0.68};// no pixels around max at filt_other_rad should be higher than this fraction of max
	}
	
	public boolean setupDialog(
			int []     indices,
			OrthoMap[] ortho_maps) {

		GenericJTabbedDialog gd = new GenericJTabbedDialog("Correlate "+((indices.length==1)? "one image":"two images")+" with patterns, altitude "+alt_mode,1200,1100);
		gd.addTab("General","General parameters");
		gd.addStringField ("Object type",                       object_type, 20,"Object type/name.");
		gd.addNumericField("Minimal correlation, first",        min_corrs[0],  5,7,"","Minimal correlation value to keep, first scene.");
		gd.addNumericField("Minimal correlation, second",       min_corrs[1],  5,7,"","Minimal correlation value to keep, second scene.");
		gd.addNumericField("Phase correlation main",            phaseCoeff[0],      3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
		gd.addNumericField("Phase correlation other",           phaseCoeff[1], 3,7,"","1.0 - pure phase correlation, 0.0 - regular correlation.");
		gd.addNumericField("Low-pass sigma main",               lpf_sigma[0], 3,7,"","Low-pass sigma for phase correlation. 0 - no filtering, 0.25 - 4 pix.");
		gd.addNumericField("Low-pass sigma other",              lpf_sigma[1], 3,7,"","Low-pass sigma for phase correlation. 0 - no filtering, 0.25 - 4 pix.");
		gd.addNumericField("Minimal relative full correlation", min_corr_full_rel,  5,7,"","Minimal relative correlation value with full circular pattern to keep.");
		gd.addNumericField("Minimal relative half correlation", min_corr_half_rel,  5,7,"","Increase correlation threshold for partial patterns relative to min_corrs.");
		gd.addNumericField("Prefer full pattern",               full_preference,  3,7,"","Prefer full pattern to partial; scale by this value.");
		gd.addNumericField("Max/min halves ratio",              max_min_ratio,  3,7,"","Ratio of halves best/worst to use half-pattern.");
		gd.addCheckbox    ("Combine with full pattern, main",   combine_full[0], "Multiply by normalized full pattern correlation maximum.");
		gd.addCheckbox    ("Combine with full pattern, other",  combine_full[1], "Multiply by normalized full pattern correlation maximum.");
		gd.addNumericField("Correlation size",                  corr_size,  0,4,"pix", "Should be power of 2.");
		gd.addNumericField("Adversarial distance",              adv_radius,  1,6,"pix", "Suppress weaker if they have closer strong ones.");
		gd.addNumericField("Search other scene max",            search_radius,  1,6,"pix", "Search for other scene max from the main one.");
		gd.addNumericField("Scale warp",                        scale_warp,  1,6,"x", "Scale second image warping.");
		gd.addNumericField("Extract object size",               extr_size,  0,4,"", "Size of square object image to extract.");
		gd.addCheckbox    ("Remove DC",                         remove_dc, "Remove DC in extracted object images.");
		gd.addMessage("Mitigating what seems to be a bug - kernels tuned to be convolved after decimation. Should be opposite?");
		gd.addCheckbox    ("Convolve after zoom out",           convolve_after, "Zoom patterns before convolving with resolution-matching kernels.");
		gd.addCheckbox   ("Re-correlate centered",              corr_centered, "Re-correlate main scene candidates cenetered.");
		gd.addTab("Absolute Contrast","Preparing absolute contrast measurements for the main scene");
		gd.addNumericField("Edge cutt-off fraction (<0.5)",     abs_edge_frac,  5,7,"","Used for extracting inner and outer zones from tha existing patterns.");		
		gd.addNumericField("Radius of outer ring to pattern radius",abs_oversize,  5,7,"","Outer ring external radius as a mupltile of the pattern radius.");
		gd.addCheckbox    ("Force round",                       abs_force_round, "Force round pattern and recheck \"best\" correlation value.");
		gd.addNumericField("Update subpattern mode",            abs_mode,  0,4,"", "0 - keep, 1 keep type and if half, only change by +/-1, 2 - keep type, 3 - any.");
		gd.addCheckbox    ("Cold center",                       abs_invert, "Center pattern values are lower than around.");
		gd.addNumericField("Outlier fraction when averaging",   abs_outliers_frac,  5,7,"","Discard this fraction of pixels in inner and outer zones when averaging.");
		
		gd.addCheckbox    ("May be obscured only by warmer",    abs_obscure_warm, "Half-pattern objects may be obscured by warmer (for cold objects).");
		gd.addNumericField("\"warmer\" threshold",              abs_obscure_frac,  5,7,"","Threshold fraction between average center and average around ring.");
		
		gd.addTab("Filters","Filtering results to reduce false positives");
		gd.addCheckbox    ("Keep filtered out",                 filt_keep, "List filtered out, but actaually keep them.");
		gd.addCheckbox    ("Keep pre-filter",                   filt_keep_pre, "Keep even before the first scene filter.");
		gd.addCheckbox    ("Apply filters at once",             filt_atonce, "If false, first filter by main scene only, do not process secondary scene if main failed.");
		gd.addNumericField("Peak hight fraction",               filt_frac_max,     5,7,"","Fraction of the peak hight for eigenvlues (radius and elongation).");
		gd.addNumericField("Max radius, main, full",            filt_max_radius[0][0],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, main, halves",          filt_max_radius[0][1],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, other, full",           filt_max_radius[1][0],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max radius, other, half",           filt_max_radius[1][1],  5,7,"pix","Maximal peak cross-section average radius at certain height.");		
		gd.addNumericField("Max elongation, main, full",        filt_elongation[0][0],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, main, halves",      filt_elongation[0][1],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, other, full",       filt_elongation[1][0],  5,7,"","Maximal peak ellipse axis ratio at certain height.");		
		gd.addNumericField("Max elongation, other, halves",     filt_elongation[1][1],  5,7,"","Maximal peak ellipse axis ratio at certain height.");
		
		gd.addNumericField("Other peak distance",               filt_other_rad,  5,7,"pix","Should be no disconnected peaks within this radius from the used one. Set to 0 to disable.");
		gd.addNumericField("Other peak fraction, main scene",   filt_other_frac[0],  5,7,"","Maximal near peak fraction of the used maximum. Only for halves.");
		gd.addNumericField("Other peak fraction, other scene",  filt_other_frac[1],  5,7,"","Maximal near peak fraction of the used maximum. Only for halves.");
		
		gd.addNumericField("Maximal offset",                    filt_dist[0],     5,7,"pix","Maximal other scene offset from the main.");
		gd.addNumericField("Maximal offset perpendicular",      filt_dist[1],     5,7,"pix","Maximal other scene offset perpendicular to offset between scenes.");
		gd.addNumericField("Maximal object height",             filt_height,      5,7,"m",  "Maximal object height to allow extra offset.");
		gd.addNumericField("Full/half corr minimal ratio, main",filt_full_half_frac[0],  5,7,"","Minimal correlation with full pattern strength ratio to correlation with a half-pattern.");
		gd.addNumericField("Full/half corr minimal ratio, other",filt_full_half_frac[1],  5,7,"","Minimal correlation with full pattern strength ratio to correlation with a half-pattern.");
		gd.addNumericField("Minimal roundness of round, main",  filt_roundness[0],  5,7,"","Minimal ratio of worst half-pattern correlation to the full-pattern ones. Only for full patterns.");
		gd.addNumericField("Minimal best corr , main",          filt_best[0],  5,7,"","Minimal best correlation for all patterns, even if selected is a full one.");
		
		gd.addNumericField("Minimal absolute contrast",         filt_abs_contrast, 5,7,"","Minimal absolute difference between center and peripheral areas of the main scene objects.");
		gd.addNumericField("Ease absolute contrast for halves", filt_abs_easepart, 5,7,"","Ease absolute contast requirements for high AGL (low resolution).");
		
		gd.addTab("Save/show","Save/show generated images");
		gd.addStringField ("Pattern match save directory",      save_top_dir, 120, "Top directory to save pattern patch results");
		gd.addStringField ("Save subdirectory",                 sub_dir, 80, "Subdirectory for versions of the same scene/pair of scenes");
		gd.addCheckbox    ("Show intermediate images",          show_images, "Display all intermediate images.");
		gd.addCheckbox    ("Show final image",                  show_final_image, "Display final image with marked objects.");
		gd.addCheckbox    ("Save generated images",             save_images, "Save generated image to the location defined by above.");
		gd.addCheckbox    ("Show result tables",                show_results, "Display results tables.");
		//
		gd.addCheckbox    ("Append results table",              append_table,"Append results table.");
		gd.addCheckbox    ("Date/time when appending",          table_datetime, "Include date/time when appending results.");
		gd.addCheckbox    ("Write to log",                      append_log, "Append log file.");
		gd.addCheckbox    ("Save multi-scene results",          results_multi, "Save multi-scene results to a single table.");
		//
		gd.addNumericField("Debug level",                       debugLevel,  0,4,"", "Debug level.");
		gd.showDialog();
		if (gd.wasCanceled()) return false;
		object_type=           gd.getNextString();		
		min_corrs[0]=          gd.getNextNumber();
		min_corrs[1]=          gd.getNextNumber();
		phaseCoeff[0]=         gd.getNextNumber();
		phaseCoeff[1]=         gd.getNextNumber();
		lpf_sigma[0]=          gd.getNextNumber();
		lpf_sigma[1]=          gd.getNextNumber();
		min_corr_full_rel=     gd.getNextNumber();
		min_corr_half_rel=     gd.getNextNumber();		
		full_preference=       gd.getNextNumber();
		max_min_ratio=         gd.getNextNumber();		
		combine_full[0] =      gd.getNextBoolean();
		combine_full[1] =      gd.getNextBoolean();
		corr_size=       (int) gd.getNextNumber();
		adv_radius=            gd.getNextNumber();
		search_radius=         gd.getNextNumber();
		scale_warp =           gd.getNextNumber();
		extr_size=       (int) gd.getNextNumber();
		remove_dc=             gd.getNextBoolean();
		convolve_after=        gd.getNextBoolean();
		corr_centered=         gd.getNextBoolean();
		abs_edge_frac =        gd.getNextNumber();
		abs_oversize =         gd.getNextNumber(); 
		abs_force_round=       gd.getNextBoolean();
		abs_mode =       (int) gd.getNextNumber();      
		abs_invert =           gd.getNextBoolean();    
		abs_outliers_frac =    gd.getNextNumber();		
		abs_obscure_warm=      gd.getNextBoolean();
		abs_obscure_frac =     gd.getNextNumber();
		filt_keep=             gd.getNextBoolean();
		filt_keep_pre=         gd.getNextBoolean();		
		filt_atonce=           gd.getNextBoolean();
		filt_frac_max=         gd.getNextNumber();
		filt_max_radius[0][0]= gd.getNextNumber();
		filt_max_radius[0][1]= gd.getNextNumber();
		filt_max_radius[1][0]= gd.getNextNumber();
		filt_max_radius[1][1]= gd.getNextNumber();
		filt_elongation[0][0]= gd.getNextNumber();
		filt_elongation[0][1]= gd.getNextNumber();
		filt_elongation[1][0]= gd.getNextNumber();
		filt_elongation[1][1]= gd.getNextNumber();
		
		filt_other_rad=        gd.getNextNumber();
		filt_other_frac[0]=    gd.getNextNumber();
		filt_other_frac[1]=    gd.getNextNumber();
		filt_dist[0] =         gd.getNextNumber();
		filt_dist[1] =         gd.getNextNumber();
		filt_height =          gd.getNextNumber();
		filt_full_half_frac[0]=gd.getNextNumber();
		filt_full_half_frac[1]=gd.getNextNumber();
		filt_roundness[0]=     gd.getNextNumber();
		filt_best[0]=          gd.getNextNumber();
		filt_abs_contrast=     gd.getNextNumber();
		filt_abs_easepart=     gd.getNextNumber();
		save_top_dir=          gd.getNextString();
		sub_dir=               gd.getNextString();
		show_images=           gd.getNextBoolean();
		show_final_image=      gd.getNextBoolean();
		save_images=           gd.getNextBoolean();
		show_results=          gd.getNextBoolean();
		append_table=          gd.getNextBoolean();
		table_datetime=        gd.getNextBoolean();
		append_log=            gd.getNextBoolean();
		results_multi =        gd.getNextBoolean();
		debugLevel=      (int) gd.getNextNumber();
		setSaveDir(
				indices,     // int [] indices,
				ortho_maps); // OrthoMap[] ortho_maps)
		return true;
	}
	
	public void setSaveDir(
			int [] indices,
			OrthoMap[] ortho_maps) {
		save_dir = null;
		if (save_images) {
			if (!save_top_dir.endsWith(Prefs.getFileSeparator())) {
				save_top_dir+= Prefs.getFileSeparator();
			}
			if (indices.length == 1) {
				save_dir=String.format("%s%03d-%s",
						save_top_dir,indices[0],ortho_maps[indices[0]].getName());
			} else if (indices.length == 2) {
				save_dir=String.format("%s%03d-%03d_%s-%s",
						save_top_dir,indices[0],indices[1],ortho_maps[indices[0]].getName(),ortho_maps[indices[1]].getName());
			}
			if (!sub_dir.equals("")) {
				save_dir+= Prefs.getFileSeparator()+sub_dir;
			}
			// create if it does not exist
			File  fsave_dir = new File(save_dir);
			fsave_dir.mkdirs();
			save_dir += Prefs.getFileSeparator();
		}
	}
	
	
	public void printReport(
			boolean               gen_results,
			boolean               gen_parameters,
			OrthoMap []           ortho_maps,			
			int []                indices,
			ArrayList<Integer>    match_sort,
			ArrayList <ItemMatch> matches_list,
			int                   zoom_level,
			double []             scene_xy_offset,
			double                scene_agl,
			int                   num_patterns, // corrs_out[scene_num].length
			int []                origin,
			double [][]           centers,
			GroundObjectPattern   gop,
			double                hi_lo_freq,
			int []                hi_freq_wh,
			double [][]           hi_freq_arr) {
		int sort_pattern_index = 0;
		boolean [] has_filter_data0 = new boolean[2];
		for (int i = 0; i < match_sort.size(); i++) {
			int indx =match_sort.get(i);
			ItemMatch match = matches_list.get(indx);
			for (int j = 0; j < 2; j++) {
				has_filter_data0[j] |= match.filter_data[0][j] != null;
			}
		}
		
		if (gen_parameters) {
			if (indices.length > 1) {
				System.out.println(String.format("Main scene:  (%03d) %17s",indices[0],ortho_maps[indices[0]].getName()));
				System.out.println(String.format("Other scene: (%03d) %17s",indices[1],ortho_maps[indices[1]].getName()));
			} else {
				System.out.println(String.format("Scene:  (%03d) %17s",indices[0],ortho_maps[indices[0]].getName()));
			}
			System.out.println("Searching for object type="+object_type+", found "+match_sort.size()+" of them."+
					" PhaseCoeff=["+phaseCoeff[0]+","+phaseCoeff[1]+"], lpf = ["+lpf_sigma[0]+","+lpf_sigma[1]+"], min_corrs=["+min_corrs[0]+","+min_corrs[1]+
					"], min_corr_full_rel="+min_corr_full_rel+",  min_corr_half_rel="+min_corr_half_rel+", frac_max="+filt_frac_max);
			System.out.println("filt_max_radius main =["+filt_max_radius[0][0]+","+filt_max_radius[0][1]+"], "+
					"filt_max_radius other=["+filt_max_radius[1][0]+","+filt_max_radius[1][1]+"], "+
					"filt_elongation main=["+filt_elongation[0][0]+","+filt_elongation[0][1]+"], "+
					"filt_elongation other=["+filt_elongation[1][0]+","+filt_elongation[1][1]+"]");
			System.out.println("filt_dist=["+filt_dist[0]+", "+filt_dist[1]+"] "+
					"(First value is the total offset in pixels, second accounts for parallax.) "+
					" filt_height="+filt_height+ " meters.");
			System.out.println("filt_other_rad="+filt_other_rad+"pix,  filt_other_frac=["+filt_other_frac[0]+", "+filt_other_frac[1]+"],"+
					" filt_full_half_frac=["+filt_full_half_frac[0]+", "+filt_full_half_frac[1]+"], filt_roundness="+filt_roundness[0]+
					", filt_best="+filt_best[0]+" .");
			System.out.println("filt_abs_contrast="+filt_abs_contrast+", filt_abs_easepart="+filt_abs_easepart);
			System.out.println("abs_edge_frac="+abs_edge_frac+", abs_oversize="+abs_oversize+
					", abs_force_round="+abs_force_round+", abs_mode="+abs_mode+", abs_invert="+abs_invert);
			System.out.println("abs_outliers_frac="+abs_outliers_frac+", abs_obscure_warm="+abs_obscure_warm+
					", abs_obscure_frac="+abs_obscure_frac + ", Average hi_lo_freq="+hi_lo_freq);
			if (indices.length > 1) {
				System.out.println("Second image is offset by dx="+scene_xy_offset[0]+", dy="+scene_xy_offset[1]+", dist="+
						Math.sqrt(scene_xy_offset[0]*scene_xy_offset[0]+scene_xy_offset[1]*scene_xy_offset[1])+" pix");
			}
		}
		if (gen_results) {
			//				System.out.println("Updated object list with main scene peak areas and secondary scene maximums, areas and maximum distances from the main");
			System.out.print(String.format("%4s: %9s %8s %8s %8s %8s %6s %3s %6s %6s",
					" # ", "  x/y  ", " hfreq ","sel-corr", " best ","contrast","round","sub"," dist ","  az  "));
			for (int j = 0; j < (num_patterns + 1); j++) if (j != sort_pattern_index) {
				System.out.print(String.format(" %8s ","match_"+j));
			}
			if (has_filter_data0[0]) {
			System.out.print(String.format("%6s %6s %6s %6s %6s %6s |",
					"mf-rad", "mf-eln","mh-rad", "mh-eln", "mh-ofr", "m_fhr"));
			} else {
				System.out.print(" |");
			}
			if (indices.length > 1) {
				System.out.print(String.format(" %8s %6s %6s %6s %7s %7s %7s %7s %7s | %8s %6s %6s %6s %6s %6s %7s %7s",
						"of-corr","of-rad","of-eln", "of-dst", "of-dx",  "of-dy","of-par","of-prp","elevat",
						"oh-corr","oh-rad","oh-eln", "oh-ofr", "o_fhr", "oh-dst", "oh-dx","oh-dy"));
				System.out.print(String.format("| %6s %6s"," dist2","  az2 "));
			}
			System.out.println(String.format(" %7s",
					"removed"));

			for (int i = 0; i < match_sort.size(); i++) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				double [] match_values = match.getMatchValues(gop);
				CorrelationPeakStats [][] filter_data = match.filter_data;
				int [] icenter_xy = match.getIntXY();
				double hfr = Double.NaN;
				if (hi_freq_arr != null) {
					int hfr_x = icenter_xy[0]/(corr_size/2);
					int hfr_y = icenter_xy[1]/(corr_size/2);
					hfr = hi_freq_arr[0][hfr_x + hfr_y * hi_freq_wh[0]];
				}
				double [] vert_offs = new double[2];
				for (int j = 0; j < vert_offs.length; j++) {
					vert_offs[j] = icenter_xy[j] - origin[j];
				}
				double vert_dist = Math.sqrt(vert_offs[0]*vert_offs[0]+vert_offs[1]*vert_offs[1]);
				double vert_az= Math.atan2(vert_offs[1],vert_offs[0])*180.0/Math.PI;
				System.out.print(String.format("%4d: %4d/%4d %8.5f %8.5f %8.5f %8.3f %6.3f %3d %6.1f %6.1f",
						i,
						icenter_xy[0],icenter_xy[1],
						hfr,
						match.getMatchValue(gop), // ,sort_pattern_index),
						match.getMatchBestValue(gop),
						match.getAbsoluteContrast(),
						match.getRoundness(gop),
						match.getPatternMatch(gop).getBestSub(), // 1-based
						vert_dist, vert_az));  // horizontal distance and azimuth from the camera ground projection
				for (int j = 0; j < match_values.length; j++) if (j != sort_pattern_index) {
					System.out.print(String.format(" %8.5f",match_values[j]));
					if (j != (match_values.length-1)) {
						System.out.print(",");
					} else {
						System.out.print(" ");
					}
				}
				boolean removed = match.isRemoved();
				String sremoved = removed?" removed":"";
				if (filter_data[0][0] != null) {
					if (filter_data[0][1] == null) {
						System.out.print(String.format(
								"\t%6.3f\t%6.3f\t---\t---\t---\t---",
								// main full and best half areas only, radius and elongation
								filter_data[0][0].eff_rad, filter_data[0][0].elong)); // main full:      radius and elongation
					} else {
						double m_fhr = filter_data[0][0].best_d/filter_data[0][1].best_d;
						System.out.print(String.format(
								"%6.3f %6.3f %6.3f %6.3f %6.3f %6.3f |",
								// main full and best half areas only, radius and elongation
								filter_data[0][0].eff_rad, filter_data[0][0].elong, // main full:      radius and elongation
								filter_data[0][1].eff_rad, filter_data[0][1].elong, // main best half: radius and elongation
								filter_data[0][1].max_other, // other max fraction of the main one
								m_fhr));
					}
				} else {
					System.out.print(" |");
				}
				if (indices.length > 1) {
					double [] vert_offs2 = new double[2];
					for (int j = 0; j < vert_offs.length; j++) {
						vert_offs2[j] = icenter_xy[j] - centers[1][j];
					}
					double vert_dist2 = Math.sqrt(vert_offs2[0]*vert_offs2[0]+vert_offs2[1]*vert_offs2[1]);
					double vert_az2= Math.atan2(vert_offs2[1],vert_offs2[0])*180.0/Math.PI;

					double [] second_xy_offsets = filter_data[1][0].cent_offs;
					// try elevation for second scene and full pattern only
					double [] scnd_xy_proj = OrthoMapsCollection.projectOffsetOnVector(
							second_xy_offsets, // double [] object_offset,
							scene_xy_offset); // double [] scene_offset)
					double  obj_elev =        OrthoMapsCollection.offsetToElevation(
							second_xy_offsets, //double [] object_offset,
							scene_xy_offset,   // double [] scene_offset,
							zoom_level,        // int       zoom_level,
							scene_agl);        // double    agl)
					//			System.out.println(String.format("best_d=%7.5f, rad=%6.3f, elong=%6.3f, dist=%6.3f, dx=%6.3f, dy=%6.3f",
					double o_fhr = filter_data[1][0].best_d/filter_data[1][1].best_d;
					System.out.print(String.format(
							" %8.5f %6.3f %6.3f %6.3f %7.3f %7.3f %7.3f %7.3f %7.3f | %8.5f %6.3f %6.3f %6.3f %6.3f %6.3f %7.3f %7.3f",
							// for second full - everything and parallel/perpendicular+elevation
							filter_data[1][0].best_d, filter_data[1][0].eff_rad, filter_data[1][0].elong, // full: corr, radius, elongation
							filter_data[1][0].dist, filter_data[1][0].cent_offs[0], filter_data[1][0].cent_offs[1], // full: dist, dx, dy
							scnd_xy_proj[0], scnd_xy_proj[1],obj_elev,                           // full: parallel, perpnd, elevation(m)

							filter_data[1][1].best_d, filter_data[1][1].eff_rad, filter_data[1][1].elong, // half: corr, radius, elongation
							filter_data[1][1].max_other, ///
							o_fhr,
							filter_data[1][1].dist, filter_data[1][1].cent_offs[0], filter_data[1][1].cent_offs[1]));  // half: dist, dx, dy
					System.out.print(String.format(
							"| %6.1f %6.1f", vert_dist2, vert_az2));
				}
				System.out.println(String.format(" %7s",
						sremoved));
			}
		}
	}

	public static String getBaseName(
			OrthoMap []           ortho_maps,			
			int []                indices){
		String basename = indices[0]+"-"+ortho_maps[indices[0]].getName();
		if (indices.length > 1) {
			basename = indices[0]+"-"+indices[1]+"_"+ortho_maps[indices[0]].getName()+"-"+ortho_maps[indices[1]].getName();
		}
		return basename;		
	}
	
	public void generateReport(
			String                log_line, // should end with \n or be null
			boolean               show,
			boolean               save,
			boolean               gen_results,
			boolean               gen_parameters,
			OrthoMap []           ortho_maps,			
			int []                indices,
			ArrayList<Integer>    match_sort,
			ArrayList <ItemMatch> matches_list,
			int                   zoom_level,
			double []             scene_xy_offset,
			double                scene_agl,
			int                   num_patterns, // corrs_out[scene_num].length
			int []                origin,
			double [][]           centers,
			GroundObjectPattern   gop,
			double                hi_lo_freq,
			int []                hi_freq_wh,
			double [][]           hi_freq_arr) {
		StringBuffer sb_head_res =    new StringBuffer();
		StringBuffer sb_body_res =    new StringBuffer();
		StringBuffer sb_head_params = new StringBuffer();
		StringBuffer sb_body_params = new StringBuffer();
		int sort_pattern_index = 0;
		// main table header
		boolean [] has_filter_data0 = new boolean[2];
		for (int i = 0; i < match_sort.size(); i++) {
			int indx =match_sort.get(i);
			ItemMatch match = matches_list.get(indx);
			for (int j = 0; j < 2; j++) {
				has_filter_data0[j] |= match.filter_data[0][j] != null;
			}
		}
		if (gen_results) {
			if (indices.length > 1) {
				sb_head_res.append("scene\tother");
			} else {
				sb_head_res.append("scene");
			}
			sb_head_res.append("\tobj\tx\ty\thfreq\tsel-corr\tbest\tcontrast\tround\tsub\tdist\taz");
			for (int j = 0; j < (num_patterns + 1); j++) if (j != sort_pattern_index) {
				sb_head_res.append("\tmatch_"+j);
			}
			if (has_filter_data0[0]) {
				sb_head_res.append("\tmf-rad\tmf-eln\tmh-rad\tmh-eln\tmh-ofr\tm_fhr");
			}
			if (indices.length > 1) {
				sb_head_res.append("\tof-corr\tof-rad\tof-eln\tof-dst\tof-dx\tof-dy\tof-par\tof-prp\televat");
				sb_head_res.append("\toh-corr\toh-rad\toh-eln\toh-ofr\to_fhr\toh-dst\toh-dx\toh-dy");
				sb_head_res.append("\tdist2\taz2");
			}
			sb_head_res.append("\tremoved");
			// main table lines
			for (int i = 0; i < match_sort.size(); i++) {
				int indx =match_sort.get(i);
				ItemMatch match = matches_list.get(indx);
				double [] match_values = match.getMatchValues(gop);
				CorrelationPeakStats [][] filter_data = match.filter_data;
				int [] icenter_xy = match.getIntXY();
				double hfr = Double.NaN;
				if (hi_freq_arr != null) {
					int hfr_x = icenter_xy[0]/(corr_size/2);
					int hfr_y = icenter_xy[1]/(corr_size/2);
					hfr = hi_freq_arr[0][hfr_x + hfr_y * hi_freq_wh[0]];
				}
				double [] vert_offs = new double[2];
				for (int j = 0; j < vert_offs.length; j++) {
					vert_offs[j] = icenter_xy[j] - origin[j];
				}
				double vert_dist = Math.sqrt(vert_offs[0]*vert_offs[0]+vert_offs[1]*vert_offs[1]);
				double vert_az= Math.atan2(vert_offs[1],vert_offs[0])*180.0/Math.PI;
				if (indices.length > 1) {
					sb_body_res.append(String.format("%4d\t%4d",indices[0],indices[1]));
				} else {
					sb_body_res.append(String.format("%4d",indices[0]));
				}

				sb_body_res.append(String.format("\t%4d\t%4d\t%4d\t%8.5f\t%8.5f\t%8.5f\t%8.3f\t%6.3f\t%3d\t%6.1f\t%6.1f",
						i,
						icenter_xy[0],icenter_xy[1],
						hfr,
						match.getMatchValue(gop), // ,sort_pattern_index),
						match.getMatchBestValue(gop),
						match.getAbsoluteContrast(),
						match.getRoundness(gop),
						match.getPatternMatch(gop).getBestSub(), // 1-based
						vert_dist, vert_az));  // horizontal distance and azimuth from the camera ground projection
				for (int j = 0; j < match_values.length; j++) if (j != sort_pattern_index) {
					sb_body_res.append(String.format("\t%8.5f",match_values[j]));
				}
				boolean removed = match.isRemoved();
				String sremoved = removed?" removed":"";
				// (filter_data == null) ||(filter_data[0] == null) || - always != null
				if (filter_data[0][0] != null) {
					if (filter_data[0][1] == null) {
						sb_body_res.append(String.format(
								"\t%6.3f\t%6.3f\t---\t---\t---\t---",
								// main full and best half areas only, radius and elongation
								filter_data[0][0].eff_rad, filter_data[0][0].elong)); // main full:      radius and elongation
					} else {
						double m_fhr = filter_data[0][0].best_d/filter_data[0][1].best_d;
						sb_body_res.append(String.format(
								"\t%6.3f\t%6.3f\t%6.3f\t%6.3f\t%6.3f\t%6.3f",
								// main full and best half areas only, radius and elongation
								filter_data[0][0].eff_rad, filter_data[0][0].elong, // main full:      radius and elongation
								filter_data[0][1].eff_rad, filter_data[0][1].elong, // main best half: radius and elongation
								filter_data[0][1].max_other, // other max fraction of the main one
								m_fhr));
					}
				}
				if (indices.length > 1) {
					double [] vert_offs2 = new double[2];
					for (int j = 0; j < vert_offs.length; j++) {
						vert_offs2[j] = icenter_xy[j] - centers[1][j];
					}
					double vert_dist2 = Math.sqrt(vert_offs2[0]*vert_offs2[0]+vert_offs2[1]*vert_offs2[1]);
					double vert_az2= Math.atan2(vert_offs2[1],vert_offs2[0])*180.0/Math.PI;

					double [] second_xy_offsets = filter_data[1][0].cent_offs;
					// try elevation for second scene and full pattern only
					double [] scnd_xy_proj = OrthoMapsCollection.projectOffsetOnVector(
							second_xy_offsets, // double [] object_offset,
							scene_xy_offset); // double [] scene_offset)
					double  obj_elev =        OrthoMapsCollection.offsetToElevation(
							second_xy_offsets, //double [] object_offset,
							scene_xy_offset,   // double [] scene_offset,
							zoom_level,        // int       zoom_level,
							scene_agl);        // double    agl)
					//			System.out.println(String.format("best_d=%7.5f, rad=%6.3f, elong=%6.3f, dist=%6.3f, dx=%6.3f, dy=%6.3f",
					double o_fhr = filter_data[1][0].best_d/filter_data[1][1].best_d;
					sb_body_res.append(String.format(
							"\t%8.5f\t%6.3f\t%6.3f\t%6.3f\t%7.3f\t%7.3f\t%7.3f\t%7.3f\t%7.3f\t%8.5f\t%6.3f\t%6.3f\t%6.3f\t%6.3f\t%6.3f\t%7.3f\t%7.3f",
							// for second full - everything and parallel/perpendicular+elevation
							filter_data[1][0].best_d, filter_data[1][0].eff_rad, filter_data[1][0].elong, // full: corr, radius, elongation
							filter_data[1][0].dist, filter_data[1][0].cent_offs[0], filter_data[1][0].cent_offs[1], // full: dist, dx, dy
							scnd_xy_proj[0], scnd_xy_proj[1],obj_elev,                           // full: parallel, perpnd, elevation(m)

							filter_data[1][1].best_d, filter_data[1][1].eff_rad, filter_data[1][1].elong, // half: corr, radius, elongation
							filter_data[1][1].max_other, ///
							o_fhr,
							filter_data[1][1].dist, filter_data[1][1].cent_offs[0], filter_data[1][1].cent_offs[1]));  // half: dist, dx, dy
					sb_body_res.append(String.format(
							"\t%6.1f\t%6.1f", vert_dist2, vert_az2));
				}
				sb_body_res.append(String.format("\t%7s\n",sremoved));
			}
		}
		if (gen_parameters) {
			// parameters header
			sb_head_params.append("\ntype\tscene\tname");
			if (indices.length > 1)	{
				sb_head_params.append("\tother\tother_name");
			}
			sb_head_params.append("\tph_corr\tlpf\tcorr\tbest\tcntrst\tround\trelfull\trelhalf\tfracmx");
			
			sb_head_params.append("\trad_mf\trad_mh\teln_mf\teln_mh");
			sb_head_params.append("\tdist\tprlx\theight");
			sb_head_params.append("\toth_rad\toth_frac\tfh_frac");
			sb_head_params.append("\teasepart\tedge\tovrsize\tf-rnd\tamode\tinvert\toutlier\twarm_obs\tfrac_obs");
			sb_head_params.append("\tavg_hifr");
			// parameters for the second scene
			if (indices.length > 1) {
				if (indices.length > 1)	sb_head_params.append("\tph.c_1\tlpf_1\tcorr_1\tbest_1");
				sb_head_params.append("\trad_of\trad_oh\teln_of\teln_oh");
				sb_head_params.append("\toffs\toffs_x\toffs_y");
			}
			// data row
			sb_body_params.append(String.format("%s\t%d\t%s",	object_type,indices[0], ortho_maps[indices[0]].getName()));
			if (indices.length > 1)	{
				sb_body_params.append(String.format("\t%d\t%s",	indices[1], ortho_maps[indices[1]].getName()));
			}
			sb_body_params.append(String.format("\t%6.2f\t%6.2f\t%8.6f\t%8.6f\t%6.2f\t%5.3f\t%7.5f\t%7.5f\t%7.5f",
					phaseCoeff[0],lpf_sigma[0],min_corrs[0],filt_best[0],filt_abs_contrast,filt_roundness[0], min_corr_full_rel,min_corr_half_rel,filt_frac_max));
			sb_body_params.append(String.format("\t%6.4f\t%6.4f\t%6.4f\t%6.4f",
					filt_max_radius[0][0],filt_max_radius[0][1],filt_elongation[0][0],filt_elongation[0][1]));
			sb_body_params.append(String.format("\t%6.3f\t%6.3f\t%6.3f", filt_dist[0],filt_dist[1],filt_height));
			sb_body_params.append(String.format("\t%5.1f\t%6.4f\t%6.4f", filt_other_rad,filt_other_frac[0],filt_full_half_frac[0]));
			sb_body_params.append(String.format("\t%5.3f\t%5.2f\t%5.2f\t%b\t%d\t%b\t%5.3f\t%b\t%5.3f",
					filt_abs_easepart,abs_edge_frac,abs_oversize,abs_force_round,
					abs_mode,abs_invert,abs_outliers_frac,abs_obscure_warm,abs_obscure_frac));
			sb_body_params.append(String.format("\t%8.6f", hi_lo_freq));
			// parameters for the second scene
			if (indices.length > 1) {
				sb_body_params.append(String.format("\t%6.2f\t%6.2f\t%8.6f\t8.6f",
						phaseCoeff[1],lpf_sigma[1],min_corrs[1],filt_best[1]));
				sb_body_params.append(String.format("\t%6.4f\t%6.4f\t%6.4f\t%6.4f",
						filt_max_radius[1][0],filt_max_radius[1][1],filt_elongation[1][0],filt_elongation[1][1]));
				sb_body_params.append(String.format("\t%6.1f\t%6.1f\t%6.1f",
						Math.sqrt(scene_xy_offset[0]*scene_xy_offset[0]+scene_xy_offset[1]*scene_xy_offset[1]),
						scene_xy_offset[0],scene_xy_offset[1]));
			}
			sb_body_params.append("\n");
		}
		String basename = getBaseName(
				ortho_maps, // OrthoMap []           ortho_maps,			
				indices);   // int []                indices) 
		
		if (show_results && show) {
			if (gen_results) {
				new TextWindow(basename+"-results", sb_head_res.toString(), sb_body_res.toString(), 1500,400);
			}
			if (gen_parameters) {
				new TextWindow(basename+"-parameters", sb_head_params.toString(), sb_body_params.toString(), 1200,400);
			}
		}
		StringBuffer sb =    new StringBuffer();
		if (gen_results) {
			sb.append(sb_head_res);
			sb.append("\n");
			sb.append(sb_body_res);
			if (gen_parameters) sb.append("\n"); // extra line
		}
		if (gen_parameters) {
			sb.append(sb_head_params);
			sb.append("\n");
			sb.append(sb_body_params);
		}
		if (save && results_multi) { // no date stamp
			String multi_path = save_top_dir;
			if (!multi_path.endsWith(Prefs.getFileSeparator())) {
				multi_path+= Prefs.getFileSeparator();
			}
			multi_path += sub_dir+RESULTS_SUFFIX; 
			CalibrationFileManagement.saveStringToFile (
					multi_path,          //String path,
					sb.toString(), // data,
					append_table); // boolean append)
			if (debugLevel > -4) {
				System.out.println ("Results appended to "+multi_path);
			}
		}
		
		if (save_images && save && (gen_results || gen_parameters)) {
			StringBuffer sb1 =    new StringBuffer();
			if (table_datetime && append_table) {
				sb1.append(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(Calendar.getInstance().getTime())+"\n");
			}
			sb1.append(sb);
			
			String path = save_dir+basename+RESULTS_SUFFIX;
			CalibrationFileManagement.saveStringToFile (
					path,          //String path,
					sb1.toString(), // data,
					append_table); // boolean append)
			if (debugLevel > -4) {
				System.out.println ("Results "+(append_table? "appended":"saved")+" to "+path);
			}
		}
		if (append_log) {
			StringBuffer sb1 =    new StringBuffer();
			if (log_line != null) {
				sb1.append(log_line);
			}
			sb1.append (sb);
			String log_path = save_dir+basename+LOG_SUFFIX;
			CalibrationFileManagement.saveStringToFile (
					log_path,      //String path,
					sb1.toString(), // data,
					true);         // boolean append)
		}
		System.out.println("generateReport() DONE");
	}
	
	
	
}
