/**
 **
 ** CuasMultiSeries.java - cuas multi series
 **
 ** Copyright (C) 2025-2026 Elphel, Inc.
 **
 ** -----------------------------------------------------------------------------**
 **
 **  CuasMultiSeries.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.cuas;

import java.awt.Color;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;

import com.elphel.imagej.cameras.CLTParameters;
import com.elphel.imagej.gpu.GPUTileProcessor;
import com.elphel.imagej.ims.UasLogReader;
import com.elphel.imagej.tileprocessor.ErsCorrection;
import com.elphel.imagej.tileprocessor.GeometryCorrection;
import com.elphel.imagej.tileprocessor.ImageDtt;
import com.elphel.imagej.tileprocessor.QuadCLT;

import ij.ImagePlus;
import ij.io.FileSaver;
import ij.process.ColorProcessor;


public class CuasMultiSeries {
	public static final int TARGET_INDEX_NONE = 0;
	public static final int TARGET_INDEX_UAS =  1;
	public static final int AVG_RANGE =      0;
	public static final int AVG_TIMESTASMP = 1;
	public static final int AVG_KEYFRAME =   2;
	public static final int AVG_DISPARITY =  3;
	public static final int AVG_VELOCIY =    4;
	public static final int AVG_LENGTH = AVG_VELOCIY+1;
	
	
	final CLTParameters   clt_parameters;
	final File []         model_dirs;
	final String []       model_names;
	final double [][][][] targets_multi_series; // [nser][nseq][ntile][nfield]
	final int [][][][]    linked_targets_multi; // [nser][ntarg][nseq]{ntile,0} // was {ntile, alt}
	final int [][]        target_map; // [nser][ntarg] global target index, 1 for UAS
	int [][][]            target_rmap; // [target_id-1][num_pair]{nser,nltarget}
	final int [][]        local_map;  // [nser][ntarg]
	final int [][][]      local_imap;  // [nser][ntarg_grp]{ntarg1, ...}
	final int [][][]      targets_start_end; //[nser][ntarg]{start_nseq,end_nseq}
	final int [][]        uas_tiles;   // [nser][nseq]
	double [][][]         avg_range_ts; // [target_id][nser]{average_range, average_ts} 
	final String [][]     scene_titles; // keyframe scene titles/timestamps
	int                   global_targets = 0;
	QuadCLT               master_CLT = null;
	final UasLogReader    uasLogReader;
//	int             debugLevel = 0; 

	public 	CuasMultiSeries (
			CLTParameters     clt_parameters,
			boolean           video_mode,
			UasLogReader      uasLogReader,
			QuadCLT           quadCLT_main,
			String            path,
			String            version) {
		this.clt_parameters = clt_parameters;
		double infinity = clt_parameters.imp.cuas_infinity;
		File [] scene_dirs = (new File(path)).listFiles(); // may contain non-directories, will be filtered by filterScenes
		Arrays.sort(scene_dirs);
		int num_series = scene_dirs.length;
		targets_multi_series = new double [num_series][][][];
		scene_titles =         new String [num_series][];
		linked_targets_multi = new int [num_series][][][];
		target_map =           new int [num_series][];
		local_map =            new int [num_series][];
		local_imap=            new int [num_series][][];
		targets_start_end =    new int [num_series][][]; // [nser][ntarg]{start, end}
		uas_tiles =            new int [num_series][];
		master_CLT =           quadCLT_main;
		model_dirs =           new File[num_series];
		model_names =          new String[num_series];
		this.uasLogReader =    uasLogReader;
		int dbg_nser = 45;
		for (int nser = 0; nser < num_series; nser++) {
			model_names[nser] =  scene_dirs[nser].getName(); // 1747803230_276111-CENTER
			model_dirs[nser] = new File(scene_dirs[nser], version);  
			String target_file_name = model_names[nser]+ CuasMotion.getParametersSuffixRanging(clt_parameters,CuasRanging.TARGET_DISPARITIES_SUFFIX)+".tiff";
			File target_file=new File(model_dirs[nser], target_file_name);
			String [][] scen_titles_w = new String[1][];
			if (nser==dbg_nser) {
				System.out.println("-nser="+nser);
			}

			targets_multi_series[nser] = CuasMotion.getTargetsFromHyperAugment(
					null,          // String [][] pvf_top_titles,
					scen_titles_w, // String [][] pvf_titles,
					target_file.toString());
		
			scene_titles[nser] = scen_titles_w[0];
			linked_targets_multi[nser] = CuasMotion.getLinkedTargets(targets_multi_series[nser]); // [nser][ntarg(zero-based]{ntile,0}
			target_map[nser] = new int [linked_targets_multi[nser].length];
			for (int i = 0; i < target_map[nser].length; i++) {
				target_map[nser][i] = TARGET_INDEX_NONE;
			}
			uas_tiles[nser] = new int [targets_multi_series[nser].length]; // number of key frames
			Arrays.fill(uas_tiles[nser], -1);
			targets_start_end[nser] = new int [linked_targets_multi[nser].length][2];
			for (int ntarg = 0; ntarg < targets_start_end[nser].length; ntarg++) {
				int i = 0;
				for (; i < linked_targets_multi[nser][ntarg].length; i++) {
					if (linked_targets_multi[nser][ntarg][i] != null) {
						 targets_start_end[nser][ntarg][0] = i;
						break;
					}
				}
				if (i >= linked_targets_multi[nser][ntarg].length) {
					targets_start_end[nser][ntarg] = new int[0];
				} else {
					for ( i++; i < linked_targets_multi[nser][ntarg].length; i++) {
						if (linked_targets_multi[nser][ntarg][i] == null) {
							break;
						}
					}
					targets_start_end[nser][ntarg][1] = i-1;
					// targets_start_end[nser][ntarg][1] = i-1;
//					p.y = i-1;
//					tlist.add(p);
				}
			}
			
			
		}
		recalculateAllRanges(
				targets_multi_series,                // final double [][][][] targets_multi_series, // [nser][nseq][ntile][nfield]
				infinity,                            // final double new_infinity,
				master_CLT.getGeometryCorrection()); // final GeometryCorrection gc) {
		printAssignmentStats();
// show missing here		
		return;
	}

	public void saveUpdatedTargets() {
		int num_series =targets_multi_series.length;
		for (int nser = 0; nser < num_series; nser++) {
			String target_file_name = model_names[nser]+ CuasMotion.getParametersSuffixRanging(clt_parameters,CuasRanging.TARGET_GLOBALS_SUFFIX)+".tiff";
			File target_file=new File(model_dirs[nser], target_file_name);
			String file_path = target_file.toString(); 

			ImagePlus imp_with_globals = CuasMotion.showTargetSequence(
					targets_multi_series[nser],     // double [][][] vector_fields_sequence,
					scene_titles[nser] ,            // String []     titles, // all slices*frames titles or just slice titles or null
					target_file_name,
					false,          // boolean       show,
					master_CLT.getTilesX());   //  int           tilesX) {
			FileSaver fs=new FileSaver(imp_with_globals);
			fs.saveAsTiff(file_path); // image processor null?
			System.out.println("saveUpdatedTargets(): saved "+file_path);
		}		
	}
	
	public ImagePlus generateLegend(
			int    annot_mode,
			String title) {
		
		final int        icon_to_text = 5; // pixels
		final int        inter_column = 20; // pixels
		
		
		final boolean scale2x =           true;
		final double scale = scale2x ? 2 : 1;
		
//		final double ifov =               master_CLT.getGeometryCorrection().getIFOV();
//		final double radar_range =        clt_parameters.imp.cuas_radar_range;
		final String font_name =          clt_parameters.imp.cuas_font_name;
		final int    font_size_radar =    clt_parameters.imp.cuas_font_size; // 7; // 
//		final double font_ratio_radar =   clt_parameters.imp.cuas_font_spacing; // 1.2; // if 0 - will use default spacing ( ~=1.5)
		final int    font_type =          clt_parameters.imp.cuas_font_type;
		final Color   text_color =        clt_parameters.imp.cuas_text_color;
//		final Color   selected_color =    text_color; // clt_parameters.imp.cuas_selected_color;
//		final boolean transparent_other = clt_parameters.imp.cuas_transparent;
//		final boolean transparent_uas =   clt_parameters.imp.cuas_transparent_uas;
		final int     target_type =       clt_parameters.imp.cuas_target_type;     //  0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		final int     uas_type =          clt_parameters.imp.cuas_known_type;      //  2; // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
		final double uas_radius =         clt_parameters.imp.cuas_radar_uas_rad;// 4.0;
		final double target_radius =      clt_parameters.imp.cuas_radar_radius; // 2.5;
		final Color  uas_color =          clt_parameters.imp.cuas_radar_uas_color; // new Color(  0,100,140);
		final Color  target_color =       clt_parameters.imp.cuas_radar_color;  // new Color(  0,255,100);
		
		
//		final boolean legend_include =   clt_parameters.imp.cuas_legend_include;
		final Color      legend_title_color = clt_parameters.imp.cuas_legend_title_color;
		final int        legend_title_font =  clt_parameters.imp.cuas_legend_title_font;
		final int        legend_font =        clt_parameters.imp.cuas_legend_font;
		final int        legend_icons =       clt_parameters.imp.cuas_legend_icons;
		final double     legend_spacing =     clt_parameters.imp.cuas_legend_spacing;
		final Rectangle  rect_legend =     clt_parameters.imp.cuas_legend;
		final Color      legend_description_color = legend_title_color;
		final Color      legend_acronyms_color =    text_color;
		
//		final double [] camera_atr = uasLogReader.getCameraATR();
//		final ErsCorrection ersCorrection = master_CLT.getErsCorrection(); 
		boolean annotate_uas =   clt_parameters.imp.cuas_annotate_uas && (uasLogReader != null);    // false; // show circle around UAS position from the flight log
		// prepare UAS log icon (blue circle)
		ColorProcessor uaslog_cp =	annotate_uas? CuasMotion.getUasLogIconColorProcessor():null;

		// prepare target icons
		ColorProcessor cp_target_icon = CuasMotion.getIconColorProcessor(
				target_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		ColorProcessor cp_uas_icon =    CuasMotion.getIconColorProcessor(
				uas_type, // uas_type, // int     target_type, // Target location matching UAS flight log: 0; // 0 - unknown, 1 - known, 2 - friend, 3 - foe
				scale2x);    // boolean scale2x)
		final int uaslog_width = (uaslog_cp != null) ? uaslog_cp.getWidth() : 0; 
		final int target_icon_width = cp_target_icon.getWidth(); 
		final int uas_icon_width =    cp_uas_icon.getWidth();
		final int [] uaslog_pixels =      (int[]) uaslog_cp.getPixels();
		final int [] target_icon_pixels = (int[]) cp_target_icon.getPixels();
		final int [] uas_icon_pixels =    (int[]) cp_uas_icon.getPixels();
		final int [][] uaslog_rgba =      CuasMotion.splitColorIcon(uaslog_pixels);
		final int [][] target_icon_rgba = CuasMotion.splitColorIcon(target_icon_pixels);
		final int [][] uas_icon_rgba =    CuasMotion.splitColorIcon(uas_icon_pixels);
		final ColorProcessor cp_legend = new ColorProcessor(rect_legend.width, rect_legend.height);
		int    legend_width = rect_legend.width;
		int [] legend_pixels = (int[]) cp_legend.getPixels();
		int font_size_description =       legend_font; // font_size_radar; // separate
		
		Font font_title =       new Font(font_name, font_type, (int) Math.round (scale * legend_title_font));
		Font font_description = new Font(font_name, font_type, (int) Math.round (scale * font_size_description));
		Font font_acronyms =    new Font(font_name, font_type, (int) Math.round (scale * font_size_description)); // font_size_radar));
		
		int vspace_annot = (int) Math.round (Math.max(font_size_description, font_size_radar) * scale * legend_spacing);
		
		// Icons
		double icon_half_width = 0;
		icon_half_width = Math.max(uas_icon_width, 0.5*uaslog_width);
		icon_half_width = Math.max(uas_icon_width, 0.5*target_icon_width);
		icon_half_width = Math.max(uas_icon_width, 0.5*uas_icon_width);
		icon_half_width = Math.max(uas_icon_width, uas_radius);
		icon_half_width = Math.max(uas_icon_width, target_radius);
		// TODO - find offset from abbreviations
		cp_legend.setColor(legend_description_color);
		cp_legend.setFont(font_description);
		
		
		int x0 = rect_legend.x, y0 = rect_legend.y;
		int y0_acronyms_center = rect_legend.y + legend_icons; // align first lines
		
		int max_x = x0;
		for (int nicon = 0; nicon < CuasMotion.ICON_DESCRIPTIONS.length; nicon++) {
			y0 += legend_icons;
			switch (nicon) {
			case 0:
				CuasMotion.imprintPixelIcon(
						uas_icon_rgba,      // int [][] icon,
						uas_icon_pixels,    // int []   icon_pix,
						uas_icon_width,     // int      icon_width,
						legend_pixels,      // int []   image,
						legend_width,       // int      width,
						x0,                 // int      xc,
						y0);                // int      yc)
				break;
			case 1:
				CuasMotion.imprintPixelIcon(
						target_icon_rgba,      // int [][] icon,
						target_icon_pixels,    // int []   icon_pix,
						target_icon_width,     // int      icon_width,
						legend_pixels,      // int []   image,
						legend_width,       // int      width,
						x0,                 // int      xc,
						y0);                // int      yc)
				break;
			case 2:
				CuasMotion.imprintPixelIcon(
						uaslog_rgba,      // int [][] icon,
						uaslog_pixels,    // int []   icon_pix,
						uaslog_width,     // int      icon_width,
						legend_pixels,      // int []   image,
						legend_width,       // int      width,
						x0,                 // int      xc,
						y0);                // int      yc)
				break;
			case 3:
				CuasMotion.drawCircle(
						cp_legend, // ColorProcessor colorProcessor,
						uas_color,                // Color color, // or null
						x0,                        // double xc,
						y0,                        // double yc,
						uas_radius); // double radius)
				break;
			case 4:
				CuasMotion.drawCircle(
						cp_legend,      // ColorProcessor colorProcessor,
						target_color,   // Color color, // or null
						x0,             // double xc,
						y0,             // double yc,
						target_radius); // double radius)
				break;
			}
			// find the height
			String txt = CuasMotion.ICON_DESCRIPTIONS[nicon]; // single-line
			Rectangle txt_bound = cp_legend.getStringBounds(txt);
			int y_descr = y0 - (txt_bound.y + txt_bound.height/2);
			int x_descr = x0+((int) icon_half_width + icon_to_text);
			max_x = Math.max(max_x, x_descr + txt_bound.x + txt_bound.width);
			cp_legend.setColor(legend_description_color);
			cp_legend.drawString(txt, x_descr, y_descr); // transparent bg
		}
		// create text description
		// find longest short names
		int acronyms_max_end = 0;
		cp_legend.setFont(font_acronyms);
		cp_legend.setColor(legend_acronyms_color);
		int x0_acro = max_x + inter_column;
		int num_acro = 0;
		int [] acro_index = new int [CuasMotion.ANNOT_LABELS.length];
		int [] acro_pos_y = new int [CuasMotion.ANNOT_LABELS.length];
		Arrays.fill(acro_index, -1);
		for (int i = 0; i < CuasMotion.ANNOT_LABELS.length; i++) if ((annot_mode & (1 << i)) != 0){
			int i_base = i;
			int y0_acro = y0_acronyms_center + num_acro * vspace_annot; 
			String txt = CuasMotion.ANNOT_LABELS[i][0];
			if ((txt == null) && ((annot_mode & (1 << (i-1))) == 0)) { // if no elevation, but conditional_elevation is enabled
				i_base = i-1;
				txt = CuasMotion.ANNOT_LABELS[i_base][0]; // for conditional elevation
			}
			if (txt != null) {
				Rectangle txt_bound = cp_legend.getStringBounds(txt);
				int y_acro = y0_acro - (txt_bound.y + txt_bound.height/2);
				cp_legend.drawString(txt, x0_acro, y_acro); // transparent bg
				acronyms_max_end = Math.max(acronyms_max_end,  x0_acro + txt_bound.x + txt_bound.width);
				acro_index[i_base] = num_acro++;
				acro_pos_y[i_base] = y_acro; 
			}
		}
		int x0_acro_descr = acronyms_max_end + icon_to_text;
		cp_legend.setFont(font_description);
		cp_legend.setColor(legend_description_color);
		int acronyms_descr_end = x0_acro_descr;
		for (int i = 0; i < CuasMotion.ANNOT_LABELS.length; i++) if (acro_index[i] >=0) { // ((annot_mode & (1 << i)) != 0){
			String txt = CuasMotion.ANNOT_LABELS[i][2];
			Rectangle txt_bound = cp_legend.getStringBounds(txt);
			cp_legend.drawString(txt, x0_acro_descr, acro_pos_y[i]); // transparent bg
			acronyms_descr_end = Math.max(acronyms_descr_end,  x0_acro_descr + txt_bound.x + txt_bound.width);
		}
		
		// print title
//		int x0 = rect_legend.x
		{
			String txt = CuasMotion.LEGEND_TITLE;
			cp_legend.setColor(legend_title_color);
			cp_legend.setFont(font_title);
			Rectangle txt_bound = cp_legend.getStringBounds(txt);
			int offs = (acronyms_descr_end-txt_bound.width)/2;
			cp_legend.drawString(txt, rect_legend.x + offs, rect_legend.y); // transparent bg
		}
		
		ImagePlus imp_legend = new ImagePlus(title, cp_legend);
//		imp_legend.show();
		return imp_legend;
	}
	
	
	public void combineVideos() {
		String videoDirectory = master_CLT.correctionsParameters.selectVideoDirectory(true,true);
		
		String extra_suffix_with_radar = "-2";
//		double  video_fps =          clt_parameters.imp.video_fps;
		String  video_codec_combo =        clt_parameters.imp.video_codec.toLowerCase();
		int     video_crf_combo =          clt_parameters.imp.video_crf;
		double  video_bitrate_m =          clt_parameters.imp.video_bitrate_m;		
		int    corr_pairs =                clt_parameters.imp.cuas_corr_pairs;
		boolean ra_background =            clt_parameters.imp.cuas_ra_background;   // true;
		String ra_bg_suffix=(ra_background? ("-RABG"+corr_pairs):"");
		String clean_suffix =              "-CLEAN"; // CuasMotion.CLEAN_SUFFIX
		boolean legend_include =           clt_parameters.imp.cuas_legend_include;
		double  legend_duration =      clt_parameters.imp.cuas_legend_duration;
		
		
		if (videoDirectory == null) {
			System.out.println("No video directory selected");
			return;
		}
		File video_dir = new File (videoDirectory);
		video_dir.mkdirs(); // Should already exist
		
		int  full_annot_mode = clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_RADAR_PANE_INDX] | // both panes
				clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_IMAGE_PANE_INDX];
		int  clean_annot_mode = full_annot_mode &  clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_CLEAN_VIEW_INDX];
		ImagePlus [] imp_legends = new ImagePlus[2];
		String [] legend_paths = new String[2];
		for (int iclean = 0; iclean <2; iclean++) {
			boolean clean = iclean > 0;
			int annot_mode = clean ? clean_annot_mode : full_annot_mode;
			// use first series name
			String title = model_names[0]+"-LEGEND"+(clean?CuasMotion.CLEAN_SUFFIX: "")+".tiff";
			imp_legends[iclean] = generateLegend(
					annot_mode, // int    annot_mode,
					title);     // String title));
			legend_paths[iclean] = QuadCLT.saveImagePlusInDirectory(
					imp_legends[iclean], // ImagePlus imp,
					video_dir.toString()); // String    dir)
		}
		File legend_vfile = null;
		if (legend_include) { // only include clean legend
			double pts_scale = clt_parameters.imp.video_fps/clt_parameters.imp.sensor_fps;
			pts_scale = 1.0;
			String legend_path = legend_paths[1];
			String legend_vpath = legend_path;
			int ind_dot = legend_vpath.lastIndexOf(".");
			legend_vpath= legend_vpath.substring(0,ind_dot)+".webm";
			legend_vfile = new File(legend_vpath);
			String shell_legend_include = String.format("ffmpeg -y -loop 1 -i %s -r 60 -vf setpts=%f*PTS -b:v %fM -crf %d -c %s -t %f -pix_fmt yuv420p %s",
					legend_paths[1],
					pts_scale,
					video_bitrate_m,
					video_crf_combo,
					video_codec_combo,
					legend_duration,
					legend_vpath);
			boolean legend_ok = 	runShellCommand(
					shell_legend_include, // String shellCommand,
					video_dir, // File   work_dir,
					legend_vfile);
			if (!legend_ok) {
				legend_vfile = null;
			}
		}
		
		
		System.out.println();
		ArrayList<String> video_paths = new ArrayList<String>();
		if ((legend_vfile != null)&& legend_vfile.exists()) {
			video_paths.add(legend_vfile.toString());
		}
		
//		int    annot_mode = -1; // specify bits
		for (int nser = 0; nser < model_names.length; nser++) {
			String image_name = model_names[nser];
			String webm_title = image_name+CuasMotion.getParametersSuffixRslt(clt_parameters,"-RGB"+ra_bg_suffix+clean_suffix)+extra_suffix_with_radar+".webm";
			String webm_path = new File(model_dirs[nser], webm_title).toString();
			File webm_file = new File(webm_path);
			if (webm_file.exists()) {
				video_paths.add(webm_path);
			}
		}
		System.out.println("Combining "+video_paths.size()+" video files.");
		
		String combo_video_name = "COMBO"+CuasMotion.getParametersSuffixRslt(clt_parameters,"-RGB"+ra_bg_suffix+clean_suffix)+extra_suffix_with_radar+".webm";
		String concat_list_name = combo_video_name.substring(0, combo_video_name.lastIndexOf("."))+".list";
		File list_to_concat = new File (video_dir,concat_list_name);
		// delete if exists
		if (list_to_concat.exists()) {
			list_to_concat.delete();
		}
		PrintWriter writer = null;
		try {
			writer = new PrintWriter(list_to_concat, "UTF-8");
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if (writer == null) {
			return;
		}
//		int num_segments = 0;
		for (String path:video_paths) {
			writer.println("file '"+path+"'");
//			num_segments++;
		}
		writer.close();
		
		
		File video_out =  new File (video_dir,combo_video_name);
		if (video_out.exists()) {
			video_out.delete();
		}
		double pts_scale = clt_parameters.imp.video_fps/clt_parameters.imp.sensor_fps;
		pts_scale = 1.0;
		String shellCommand;
		//https://ffmpeg.org/ffmpeg-formats.html
		
		/*
		 when called from java, ffmpeg terminated in ~5min and 848MB, but java continued to wait 
		 
		 For now - just run command manually by copying the shellCommand
		 */
		

		shellCommand = String.format("ffmpeg -y -f concat -safe 0 -i %s -r 60 -vf setpts=%f*PTS -b:v %fM -crf %d -c %s %s",
				list_to_concat.toString(), pts_scale, video_bitrate_m,  video_crf_combo, video_codec_combo, video_out.toString());
		
		boolean shell_ok = 	runShellCommand(
				shellCommand, // String shellCommand,
				video_dir, // File   work_dir,
				video_out); // File   out_file) { // if not null will delete this file and verify its creation
        /*
		Process p = null;
		int exit_code = -1;
		System.out.println("Will run shell command: \""+shellCommand+"\"");
		System.out.println("This may take a while, please wait ...");
		try {
			p = Runtime.getRuntime().exec(
					shellCommand,
					null, // env
					video_dir // working dir - needed if "-report" is added to ffmpeg command
					);
		} catch (IOException e) {
			System.out.println("Failed shell command: \""+shellCommand+"\"");
		}

		if (p != null) {
			try {
				p.waitFor();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			exit_code = p.exitValue();
		}

		System.out.println("Ran shell command: \""+shellCommand+"\" -> ");
		if ((exit_code != 0) || !video_out.exists()) {
			System.out.println("Failed to create : \""+video_out.toString()+"\"");
		}
	   */
		System.out.println("All "+(shell_ok? "done":"failed"));
		return;
	}
	
	public boolean runShellCommand(
			String shellCommand,
			File   work_dir,
			File   out_file) { // if not null will delete this file and verify its creation
		if ((out_file != null) && out_file.exists()) {
			out_file.delete();
		}
		Process p = null;
		int exit_code = -1;
		System.out.println("Will run shell command: \""+shellCommand+"\"");
		System.out.println("This may take a while, please wait ...");
		try {
			p = Runtime.getRuntime().exec(
					shellCommand,
					null, // env
					work_dir // working dir - needed if "-report" is added to ffmpeg command
					);
		} catch (IOException e) {
			System.out.println("Failed shell command: \""+shellCommand+"\"");
		}

		if (p != null) {
			try {
				p.waitFor();
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			exit_code = p.exitValue();
		}

		System.out.println("Ran shell command: \""+shellCommand+"\" -> ");
		if ((exit_code != 0) || ((out_file != null) && !out_file.exists())) {
			System.out.println("Failed to create output in : \""+work_dir.toString()+"\"");
			return false;
		} else {
			//out_file
			if (out_file != null) {
				System.out.println("Created : \""+out_file.toString()+" in \""+work_dir.toString()+"\"");
			} else {
				System.out.println("Finished w/o errors in \""+work_dir.toString()+"\"");
			}
			return true;
		}

		
	}
	
	
	public void processGlobals() {
		int debugLevel = 0;
		if (uasLogReader != null) {
			int setup_uas = setupUasTiles();
			System.out.println("processGlobals(): setupUasTiles() -> "+setup_uas);
			int assign_uas_target = assignUasTarget();
			System.out.println("processGlobals(): assignUasTarget() -> "+assign_uas_target);
		} else {
			System.out.println("processGlobals(): no UAS flight log, skipping UAS-specific assignment");
		}
		printAssignmentStats();
		if (uasLogReader != null) {
			printUasStats();
		}
		printAssignments(); 
		combineLocalTargets(
				true,        // boolean skip_assigned, // if global ID is assigned, do not mess with that pair
				debugLevel); // int debugLevel)
		printLocalAssignments();
		combineGlobalTargets(
				true,        // boolean skip_assigned, // if global ID is assigned, do not mess wi that pair
				debugLevel); // int debugLevel)		
		printAssignmentStats();
		printAssignments();
		printReverseAssignments();
		double min_disparity_range =   0.02;  // make parameter? do not calculate range for too small disparity
		double min_disparity_velocity = 0.08; // make parameter? do not calculate axial velocity for too small disparity
		avg_range_ts = getAverageRangeTimestamp(
				min_disparity_range,     // double min_disparity_range){
				min_disparity_velocity); // double min_disparit_velocity);
		linearRangeInterpolation(); 
		printAverageRanges(avg_range_ts);
		if (uasLogReader != null) {
			printAverageVsUASRanges(avg_range_ts);
		}
		saveUpdatedTargets();
		//
		ImagePlus imp_radar = testGenerateRadarImage(
				clt_parameters, // CLTParameters       clt_parameters,
				15, // 45, // 3, // 15, // int                 nser,
				uasLogReader, // UasLogReader        uasLogReader, // contains camera orientation (getCameraATR())
				debugLevel); // int                 debugLevel) {
		return;
	}
	
	
	public void printUasStats() {
		System.out.println("printUasStats(): UAS detected in segments:");
		for (int nseg = 0; nseg < target_map.length; nseg++) {
			System.out.print(nseg+" ("+model_names[nseg]+"): ");
			for (int ntarg =0; ntarg < target_map[nseg].length; ntarg++) {
				if (target_map[nseg][ntarg] == TARGET_INDEX_UAS) {
					System.out.print(ntarg+" ");
				}
			}
			System.out.println();
		}
		return;
	}
	
	public void printAssignmentStats() {
		int [] stats = getAssignmentStatus();
		System.out.println("Number of global targets: "+stats[0]+", number of local targets: "+stats[1]+", of them assigned: "+stats[2]);
		return;
	}
	
	public void printLocalAssignments() {
		System.out.println("printLocalAssignments(): local target group assignments");
		for (int nseg = 0; nseg < local_map.length; nseg++) {
			System.out.print(nseg+ " ("+model_names[nseg]+")  ("+local_map[nseg].length+"): ");
			for (int i = 0; i < local_map[nseg].length; i++) {
				System.out.print(local_map[nseg][i]+"  ");
			}
			System.out.println();
		}
		return;
	}
	
	public void printAssignments() {
		System.out.println("printAssignments(): local-to-global target assignments");
		for (int nseg = 0; nseg < target_map.length; nseg++) {
			System.out.print(nseg+ " ("+model_names[nseg]+")  ("+target_map[nseg].length+"): ");
			for (int i = 0; i < target_map[nseg].length; i++) {
				System.out.print(target_map[nseg][i]+"  ");
			}
			System.out.println();
		}
		return;
	}
	
	public void printReverseAssignments() {
		System.out.println("printReverseAssignments(): global-to-local target assignments");
		for (int ngtarg = 0; ngtarg < target_rmap.length; ngtarg++) {
			int target_id = ngtarg+1; 
			System.out.print(target_id+" ("+target_rmap[ngtarg].length+"): ");
			for (int i = 0; i < target_rmap[ngtarg].length; i++) {
				System.out.print(target_rmap[ngtarg][i][0]+":"+target_rmap[ngtarg][i][1]+" ");
			}
			System.out.println();
		}
		return;
	}	
	
	public void printAverageRanges(double [][][] avg_range_ts) {
		System.out.println("printAverageRanges(): global targets ranges in different series");
		for (int ngtarg = 0; ngtarg < avg_range_ts.length; ngtarg++) {
			int target_id = ngtarg +1;
			System.out.print(target_id+": ");
			for (int nser = 0; nser < avg_range_ts[ngtarg].length; nser++) if (avg_range_ts[ngtarg][nser] != null) {
				System.out.print(nser+":"+avg_range_ts[ngtarg][nser][AVG_RANGE]+"("+avg_range_ts[ngtarg][nser][AVG_VELOCIY]+
						")["+avg_range_ts[ngtarg][nser][AVG_DISPARITY]+"] ");
			}
			System.out.println();
		}
		return;
	}

	public void printAverageVsUASRanges(double [][][] avg_range_ts) {
		if (uasLogReader == null) {
			System.out.println("printAverageVsUASRanges(): skipping, no UAS flight log");
			return;
		}
		System.out.println("printAverageVsUASRanges(): Compare average UAS range with flight log");
		int ngtarg = 0;
		System.out.println("name, timestamp,scene,range,fl_range,axial_velocity,disparity");
		for (int nser = 0; nser < avg_range_ts[ngtarg].length; nser++) if (avg_range_ts[ngtarg][nser] != null) {
			int mid_seq = (int) +avg_range_ts[ngtarg][nser][2];
			int fl_tile =	uas_tiles[nser][mid_seq];
			double fl_range = targets_multi_series[nser][mid_seq][fl_tile][CuasMotionLMA.RSLT_FL_RANGE];
			double ts =  QuadCLT.getTimeStamp(model_names[nser]);
			System.out.println(model_names[nser]+", "+ts+", "+nser+", "+avg_range_ts[ngtarg][nser][AVG_RANGE]+", "+fl_range+", "+avg_range_ts[ngtarg][nser][AVG_VELOCIY]+
					", "+avg_range_ts[ngtarg][nser][AVG_DISPARITY]);

		}
		return;
	}	
	
	/**
	 * Get total number of local targets and number of assigned ones
	 * Sets global_targets to the total number of global targets
	 * @return {global_targets, num_total, num_assigned}
	 */
	public int [] getAssignmentStatus() {
		global_targets= 0;
		int num_total = 0;
		int num_assigned = 0;
		for (int nser = 0; nser < target_map.length; nser++) {
			num_total += target_map[nser].length;
			for (int ntarg = 0; ntarg < target_map[nser].length; ntarg++) {
				global_targets = Math.max(global_targets,target_map[nser][ntarg]);
				if (target_map[nser][ntarg] > 0) {
					num_assigned++;
				}
			}
		}
		return new int [] {global_targets, num_total, num_assigned};
	}
	
	/**
	 * Setup sequences of tiles corresponding to the UAS movements
	 * @return number of keyframes with missing UAS log (should be 0)
	 */
	public int setupUasTiles() {
		if (uasLogReader == null) {
			return 0;
		}
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		final AtomicInteger amiss = new AtomicInteger(0);
		final int dbg_nser = 45;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSer = ai.getAndIncrement(); nSer < targets_multi_series.length; nSer = ai.getAndIncrement()) {
						if (nSer==dbg_nser) {
							System.out.println("-nser="+nSer);
						}
						
						for (int nseq = 0; nseq < targets_multi_series[nSer].length; nseq++) {
							for (int ntile = 0; ntile < targets_multi_series[nSer][nseq].length; ntile++) { 
								double [] target = targets_multi_series[nSer][nseq][ntile];
								if ((target != null) && !Double.isNaN(target[CuasMotionLMA.RSLT_FL_PX])) {
									uas_tiles[nSer][nseq] = ntile;
									break; // only one reference UAS in the air
								}
							}
							if (uas_tiles[nSer][nseq] < 0) {
								amiss.getAndIncrement();
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return amiss.get();
	}
	
	
	/**
	 * Find which of the local (per series targets correspond to the UAS with a flight log)
	 * @return number of assigned local targets
	 */
	public int assignUasTarget() {
		if (uasLogReader == null) {
			return 0;
		}
		final double tmtch_pix=           clt_parameters.imp.cuas_tmtch_pix;
		final double tmtch_frac =         clt_parameters.imp.cuas_tmtch_frac;
		final int tileSize = GPUTileProcessor.DTT_SIZE;		
		final double tmtch_pix2 = tmtch_pix * tmtch_pix;
		final int tilesX =          master_CLT.getTilesX();
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		final AtomicInteger aglob = new AtomicInteger(0);
		final int dbg_nser = 45;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSer = ai.getAndIncrement(); nSer < targets_multi_series.length; nSer = ai.getAndIncrement()) {
						if (nSer==dbg_nser) {
							System.out.println("-nser="+nSer);
						}
						int [][][] ltargets = linked_targets_multi[nSer];
						double [][][] mtargets = targets_multi_series[nSer];
						int [] utiles = uas_tiles[nSer];
						int num_targets = ltargets.length;
						int [] num_def =   new int [num_targets];
						int [] num_match = new int [num_targets];
						boolean [][] conflict = new boolean [num_targets][num_targets]; // both halves defined are defined
						for (int nseq = 0; nseq < utiles.length; nseq++)  {
							int utile = utiles[nseq];
							if (utile >= 0) {
								double [] utarget = mtargets[nseq][utile];
								double upx = utarget[CuasMotionLMA.RSLT_FL_PX];
								double upy = utarget[CuasMotionLMA.RSLT_FL_PY];
								for (int ntarg = 0; ntarg < num_targets; ntarg++) if (ltargets[ntarg][nseq] != null) {
									int ltile = ltargets[ntarg][nseq][0];
									double []   ltarget = mtargets[nseq][ltile];
									int tileX = ltile % tilesX;
									int tileY = ltile / tilesX;
									double px = tileSize * tileX + tileSize/2 + ltarget[CuasMotionLMA.RSLT_X];
									double py = tileSize * tileY + tileSize/2 + ltarget[CuasMotionLMA.RSLT_Y];
									double dx = px - upx;
									double dy = py - upy;
									double err2 = dx*dx + dy*dy;
									// match[ntarg][nseq] = (err2 <= known_err2);
									num_def[ntarg]++;
									if (err2 <= tmtch_pix2) {
										num_match[ntarg]++;
									}
									for (int ntarg1 = ntarg+1;ntarg1 < num_targets; ntarg1++) if (ltargets[ntarg1][nseq] != null){
										conflict[ntarg][ntarg1] = true;
										conflict[ntarg1][ntarg] = true;
									}
								}
							}
						}
						// keep only that have enough fraction, if conflict - keep best fraction
						int num_good = 0;
						for (int ntarg = 0; ntarg < num_targets; ntarg++) if (num_match[ntarg] > 0){
							if (num_match[ntarg] < num_def[ntarg] * tmtch_frac) {
								num_match[ntarg] = 0;
							} else {
								num_good++;
							}
						}
						if (num_good > 0) {
							if (num_good > 1) { // check/remove conflicting
								for (int ntarg = 0; ntarg < num_targets; ntarg++) if (num_match[ntarg] > 0){
									for (int ntarg1 = 0; ntarg1 < num_targets; ntarg1++) if ((ntarg1 != ntarg) && (num_match[ntarg1] > 0)){
										if (conflict[ntarg][ntarg1] && (num_match[ntarg1] > num_match[ntarg])) {
											num_match[ntarg] = 0;
										}
									}
								}								
							}
							// now all ntarg with num_match[ntarg] > 0 match UAS
							for (int ntarg = 0; ntarg < num_targets; ntarg++) if (num_match[ntarg] > 0){
								target_map[nSer][ntarg] = TARGET_INDEX_UAS;
								aglob.getAndIncrement();
								for (int nseq = 0; nseq< mtargets.length; nseq++) {
									if (ltargets[ntarg][nseq] != null) {
										double [] target = mtargets[nseq][ltargets[ntarg][nseq][0]];
										target[CuasMotionLMA.RSLT_TARGET_ID] = TARGET_INDEX_UAS;
									}
								}
							}
						}
					}
				}
			};
		}		      
		ImageDtt.startAndJoin(threads);
		return aglob.get();
	}
	
	/**
	 * Combine same-sequence targets that may correspond to the same actual target
	 * @return
	 */
	public int combineLocalTargets(
			boolean skip_assigned, // if global ID is assigned, do not mess wi that pair
			int debugLevel) {
		int debug_min = -2; // if >= then print
		boolean debug = debugLevel>= debug_min; 
		
		final int    tmtch_gaps =         clt_parameters.imp.cuas_tmtch_gaps;
		final double local_apix =         clt_parameters.imp.cuas_local_apix;
//		final double tmtch_axv =          clt_parameters.imp.cuas_tmtch_axv;
//		final double tmtch_axv_k =        clt_parameters.imp.cuas_tmtch_axv_k;
//		final double tmtch_disp =         clt_parameters.imp.cuas_tmtch_disp;
		final double local_disp_diff =    clt_parameters.imp.cuas_local_diff; // maximal disparity difference to match 3d (regardless of absolute disparity
//		final double  min_disp =           clt_parameters.imp.cuas_tmtch_disp + clt_parameters.imp.cuas_infinity; // minimal "raw" disparity (with infinity at cuas_infinity)
		final boolean shortest_gap =      clt_parameters.imp.cuas_tmtch_short; // if two merges conflict, use one with the shortest gap (false - longest combo). Pairwise only
		final double local_pix2 = local_apix * local_apix; 
//		final double velocity_scale = 1.0/clt_parameters.imp.cuas_corr_offset;
		final int tilesX =          master_CLT.getTilesX();
		final int tileSize = GPUTileProcessor.DTT_SIZE;		
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		final int dbg_nser = -3;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSer = ai.getAndIncrement(); nSer < targets_multi_series.length; nSer = ai.getAndIncrement()) {
						double fps = getFps(nSer);
						int num_targ = targets_start_end[nSer].length;
						int num_after = 0;
						boolean [][] after = new boolean [num_targ][num_targ];
						if (nSer == dbg_nser) {
							System.out.println("combineLocalTargets(): nSer="+nSer);
						}
						// skip pairs that have global target_id >0 (at least skip UAS 
						for (int ntarg = 0; ntarg < num_targ; ntarg++) if (!skip_assigned ||(target_map[nSer][ntarg] <= 0)) { // no globally assigned
							if (targets_start_end[nSer][ntarg].length > 0) { // now, start,end pair may be [] if all were NaN
								for (int ntarg1 = ntarg+1; ntarg1 < num_targ; ntarg1++)  if (!skip_assigned || (target_map[nSer][ntarg1] <= 0)) {  // no globally assigned
									if (targets_start_end[nSer][ntarg1].length > 0) {
										if (targets_start_end[nSer][ntarg][1] < targets_start_end[nSer][ntarg1][0]) {
											if ((targets_start_end[nSer][ntarg1][0] - targets_start_end[nSer][ntarg][1]) <= tmtch_gaps) {
												after[ntarg][ntarg1] = true; // ntarg1 is after ntarg
												num_after++;
											}
										}
										if (targets_start_end[nSer][ntarg1][1] < targets_start_end[nSer][ntarg][0]) {
											if ((targets_start_end[nSer][ntarg][0] - targets_start_end[nSer][ntarg1][1]) <= tmtch_gaps) {
												after[ntarg1][ntarg] = true; // ntarg1 is after ntarg 
												num_after++;
											}
										}
									}
								}
							}
						}
						local_map[nSer] = new int [num_targ];  // [nser][ntarg]
						local_imap[nSer]= new int [num_targ][1];  // [nser][ntarg]
						for (int i = 0; i < num_targ; i++) {
							local_map[nSer][i] =     i; // trivial mapping	
							local_imap[nSer][i][0] = i; // trivial mapping	
						}
						

						if (num_after > 0) {
							ArrayList<Point> pair_list = new ArrayList<Point>();
							for (int ntarg0 = 0; ntarg0 < num_targ; ntarg0++) {
								for (int ntarg1 = 0; ntarg1 < num_targ; ntarg1++)  if (after[ntarg0][ntarg1]){
									int nseq0= targets_start_end[nSer][ntarg0][1]; // end of 0
									int nseq1= targets_start_end[nSer][ntarg1][0]; // start of 1
									int ntile0 = linked_targets_multi[nSer][ntarg0][nseq0][0];
									int ntile1 = linked_targets_multi[nSer][ntarg1][nseq1][0];
									double [] target0 = targets_multi_series[nSer][nseq0][ntile0];
									double [] target1 = targets_multi_series[nSer][nseq1][ntile1];
									double err2 = 	getMispatch2(
											clt_parameters, // CLTParameters  clt_parameters,
											target0,  // double [] target0,
											target1,  // double [] target1,
											scene_titles[nSer][nseq0], // String    sts0,
											scene_titles[nSer][nseq1], // String    sts1,
											ntile0,                    // int       ntile0,
											ntile1,                    // int       ntile1,
											tilesX,                    // int       tilesX,
											tileSize,                  // int       tileSize,
											fps);                       // double    fps,
									// check gap size
									// check ranges/disparity
									if (err2 <= local_pix2) {
										double [] middle_target0 = getMiddleTarget(
												nSer, // int nser,
												ntarg0,   // int local_target)
												null);    // int [] seq_tile) {
										double [] middle_target1 = getMiddleTarget(
												nSer,     // int nser,
												ntarg1,   // int local_target)
												null);    // int [] seq_tile) {
										double md0 = (middle_target0 == null) ? Double.NaN:  middle_target0[CuasMotionLMA.RSLT_GDISPARITY];
										double md1 = (middle_target1 == null) ? Double.NaN:  middle_target1[CuasMotionLMA.RSLT_GDISPARITY];
										double md0z = Double.isNaN(md0)? 0 : md0; 
										double md1z = Double.isNaN(md1)? 0 : md1; 
										if (Math.abs(md1z-md0z) < local_disp_diff) { // false for NaN
											pair_list.add(new Point(ntarg0, ntarg1));
											if (debug) {
												System.out.println("combineLocalTargets(): nSer="+nSer+", ntarg0="+ntarg0+", ntarg1="+ntarg1+
														", disp_diff="+Math.abs(md1-md0));
											}
										} else {
											if (debug) {
												System.out.println("combineLocalTargets(): nSer="+nSer+", ntarg0="+ntarg0+", ntarg1="+ntarg1+
														", md0="+md0+", md1="+md1+", abs(md1-md0) ="+Math.abs(md1-md0)+"> "+local_disp_diff+" (or NaN");
											}
										}
									}
								}
 							}
							if (!pair_list.isEmpty()) {
								// check for conflicts
								removeConflictPairs(
										pair_list,     //  ArrayList<Point>  pair_list, // in each pair, .x is earlier than .y
										nSer,          // int               nser0,  // corresponds to .x, may be the same as nser1 (for local merging)
										nSer,          // int               nser1,  // corresponds to .y, may be the same as nser0 (for local merging)
										shortest_gap,  // boolean           shortest_gap,
										debugLevel);   // int               debugLevel) {
								// combine all the remaining pairs
								for (Point p:pair_list) {
									int mn = Math.min(local_map[nSer][p.x], local_map[nSer][p.y]);
									int mx = Math.max(local_map[nSer][p.x], local_map[nSer][p.y]);
									for (int i = 0; i < local_map[nSer].length; i++) {
										if (local_map[nSer][i] == mx) {
											local_map[nSer][i] = mn;
										} else if (local_map[nSer][i] > mx) {
											local_map[nSer][i] --;
										}
									}
								}
								local_imap[nSer] = new int[num_targ - pair_list.size()][];
								for (int i = 0; i < local_imap[nSer].length; i++) {
									int nt = 0;
									for (int j: local_map[nSer]) {
										if (j==i) nt++;
									}
									//local_imap[nSer][i] = new int [nt];
									Integer [] itargets = new Integer[nt];
									nt = 0;
									for (int j = 0; j < num_targ; j++) {
										if (local_map[nSer][j] == i) {
//											local_imap[nSer][i][nt++] = j;
											itargets[nt++] = j;
										}
									}
									// reorder local_imap[nSer][i] in the order of first nscene //local_imap[nSer][i]
									Arrays.sort(itargets, new Comparator<Integer>() { // 
										@Override
										public int compare(Integer lhs, Integer rhs) {
											return Integer.compare(lhs, rhs);
										}
									});
									local_imap[nSer][i] = Arrays.stream(itargets)
				                               .filter(ii -> ii != null) // Filter out nulls if present
				                               .mapToInt(Integer::intValue)
				                               .toArray();
									
								}
							} // if (!pair_list.isEmpty()) {
						} // if (num_after > 0) {
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return 0;
	}
	
	
	
	public int combineGlobalTargets(
			boolean skip_assigned, // if global ID is assigned, do not mess wi that pair
			int debugLevel) {
		final boolean old_mode = false; // true;
		int debug_min = -2; // if >= then print
		boolean debug = debugLevel>= debug_min; 
		final GeometryCorrection gc = master_CLT.getGeometryCorrection();		
		final int    tmtch_ends =         clt_parameters.imp.cuas_tmtch_ends;
//		final int    tmtch_gaps =         clt_parameters.imp.cuas_tmtch_gaps;
		final double tmtch_apix =         clt_parameters.imp.cuas_tmtch_apix;
		final double tmtch_rpix =         clt_parameters.imp.cuas_tmtch_rpix;
		final double tmtch_axv =          clt_parameters.imp.cuas_tmtch_axv;
		final double tmtch_axv_k =        clt_parameters.imp.cuas_tmtch_axv_k;
		final double tmtch_disp_diff =    clt_parameters.imp.cuas_tmtch_diff; // maximal disparity difference to match 3d (regardless of absolute disparity
		final double  min_disp =           clt_parameters.imp.cuas_tmtch_disp + clt_parameters.imp.cuas_infinity; // minimal "raw" disparity (with infinity at cuas_infinity)
		final boolean shortest_gap =      clt_parameters.imp.cuas_tmtch_short; // if two merges conflict, use one with the shortest gap (false - longest combo). Pairwise only

//		final double tmtch_pix2 = tmtch_apix * tmtch_apix; 
//		final double velocity_scale = 1.0/clt_parameters.imp.cuas_corr_offset;
		final int tilesX =          master_CLT.getTilesX();
		final int tileSize = GPUTileProcessor.DTT_SIZE;		
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(1);
		final int [][][] pairs = new int [local_map.length][][]; // [nSer][npair]{target_group_prev_nser,target_group_nser}
		if (pairs.length > 0) {
			pairs[0] = new int [0][];
		}
		// pair[0] is local target index, not the target group
		final int gbg_nser = -3;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSer = ai.getAndIncrement(); nSer < targets_multi_series.length; nSer = ai.getAndIncrement()) {
						double fps = getFps(nSer);
						int prev_ser = nSer -1;
						if (nSer==gbg_nser) {
							System.out.println("combineGlobalTargets(): nSer="+nSer);
						}
						ArrayList<Point> pair_list = new ArrayList<Point>();
						for (int ntgrp0 = 0; ntgrp0 < local_imap[prev_ser].length; ntgrp0++) {
							int [] targs0 = local_imap[prev_ser][ntgrp0]; 
							if (targs0.length > 0){
								int ntarg0 = targs0[targs0.length -1]; // last if several are connected
								if (!skip_assigned || (target_map[prev_ser][ntarg0] <= 0)) { // not globally assigned
									if (targets_start_end[prev_ser][ntarg0].length > 0) { // now, start,end pair may be [] if all were NaN
										int nseq0 =  targets_start_end[prev_ser][ntarg0][1]; // end of the last segment in previous series
										int end_gap = linked_targets_multi[prev_ser][ntarg0].length -1 - nseq0;
										if (end_gap <= tmtch_ends) {
											int ntile0 = linked_targets_multi[prev_ser][ntarg0][nseq0][0];
											double [] target0 = targets_multi_series[prev_ser][nseq0][ntile0];
											double ts0 = QuadCLT.getTimeStamp(scene_titles[prev_ser][nseq0]);
											for (int ntgrp1 = 0; ntgrp1 < local_imap[nSer].length; ntgrp1++) {
												int [] targs1 = local_imap[nSer][ntgrp1]; 
												if (targs1.length > 0){
													int ntarg1 = targs1[0]; // first if several are connected
													if (!skip_assigned || (target_map[nSer][ntarg1] <= 0)) { // no globally assigned
														if (targets_start_end[nSer][ntarg1].length > 0) { // now, start,end pair may be [] if all were NaN
															int nseq1 =  targets_start_end[nSer][ntarg1][0]; // start of the first segment in this series
															if (nseq1 <= tmtch_ends) {
																int ntile1 = linked_targets_multi[nSer][ntarg1][nseq1][0];
																double [] target1 = targets_multi_series[nSer][nseq1][ntile1];
																double ts1 = QuadCLT.getTimeStamp(scene_titles[nSer][nseq1]);
																double dt = ts1-ts0;
																double lat_err2 = 	getMispatch2(
																		clt_parameters, //CLTParameters  clt_parameters
																		target0,  // double [] target0,
																		target1,  // double [] target1,
																		scene_titles[prev_ser][nseq0], // String    sts0,
																		scene_titles[nSer][nseq1],     // String    sts1,
																		ntile0,                        // int       ntile0,
																		ntile1,                        // int       ntile1,
																		tilesX,                        // int       tilesX,
																		tileSize,                      // int       tileSize,
																		fps);                           // double    fps,
																double max_lat_err = tmtch_apix + tmtch_rpix * dt;
																// calculate maximal allowable error adding time difference
																double max_lat_err2 = max_lat_err * max_lat_err;
																if (lat_err2 <= max_lat_err2) {
																	// calculate axial error and verify, print debug
																	// TODO: average ranging if there are several local targets corresponding to the same object
																	double [] middle_target0 = getMiddleTarget(
																			prev_ser, // int nser,
																			ntarg0,   // int local_target)
																			null);    // int [] seq_tile) {

																	double [] middle_target1 = getMiddleTarget(
																			nSer,     // int nser,
																			ntarg1,   // int local_target)
																			null);    // int [] seq_tile) {

																	// only check matches if disparity is sufficient
																	boolean range_match = false;
																	double md0 = (middle_target0 == null) ? 0:  middle_target0[CuasMotionLMA.RSLT_GDISPARITY];
																	double md1 = (middle_target1 == null) ? 0:  middle_target1[CuasMotionLMA.RSLT_GDISPARITY];
																	if (Math.abs(md1-md0) < tmtch_disp_diff) { // false for NaN
																		range_match = true;
																		/*
																if ((middle_target0 != null) && (middle_target1 != null) && // do not have measured disparity at all  
																		(middle_target0[CuasMotionLMA.RSLT_GDISPARITY] > min_disp) &&
																		(middle_target1[CuasMotionLMA.RSLT_GDISPARITY] > min_disp)) {
																		 */
																		if ((md0 > min_disp) && (md1 > min_disp)) {
																			double range0 = middle_target0[CuasMotionLMA.RSLT_GRANGE];
																			double range1 = middle_target1[CuasMotionLMA.RSLT_GRANGE];
																			double avelocity = (range1 - range0)/dt;
																			double lvelocity = getLateralVelocity (
																					clt_parameters, //CLTParameters clt_parameters,
																					gc,             // GeometryCorrection gc,
																					middle_target0, // double []     target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
																					middle_target1, // double []     target1, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
																					fps);           // double        fps){ // velocity_scale times fps
																			double max_axial_velocity = Math.max(tmtch_axv, lvelocity * tmtch_axv_k);
																			if (Math.abs (avelocity) > max_axial_velocity) {
																				range_match = false;
																			}
																			if (debug) {
																				System.out.println("combineGlobalTargets(): nSer="+nSer+", ntarg0="+ntarg0+", ntarg1="+ntarg1+
																						", range0="+range0+", range1="+range1+", avelocity="+avelocity+"m/s, lvelocity="+
																						lvelocity+"m/s, disparity="+
																						middle_target0[CuasMotionLMA.RSLT_GDISPARITY]+
																						":"+middle_target1[CuasMotionLMA.RSLT_GDISPARITY] +
																						",  max_axial_velocity="+max_axial_velocity+"m/s, range_match="+range_match);
																			}
																		} else {
																			if (debug) {
																				//double disp0 = (middle_target0 == null)? Double.NaN: middle_target0[CuasMotionLMA.RSLT_GDISPARITY];
																				//double disp1 = (middle_target1 == null)? Double.NaN: middle_target1[CuasMotionLMA.RSLT_GDISPARITY];
																				System.out.println("combineGlobalTargets(): nSer="+nSer+", ntarg0="+ntarg0+", ntarg1="+ntarg1+
																						". Bypassing axial matching - disparity="+md0+
																						":"+md1+" < "+min_disp+" .");
																			}
																		}
																	} else {
																		if (debug) {
																			System.out.println("combineGlobalTargets(): nSer="+nSer+", ntarg0="+ntarg0+", ntarg1="+ntarg1+
																					", disparity0="+md0+", disparity1="+md1+
																					". Disparity difference="+Math.abs(md1-md0)+" is not less than "+tmtch_disp_diff+" .");
																		}
																	}
																	if (range_match) {
																		pair_list.add(new Point(ntarg0, ntarg1));
																	}
																}
															}
														}
													}
												}
											}
										}
									}
								}								
							}
						} // for (int ntgrp0 = 0; ntgrp0 < local_imap[prev_ser].length; ntgrp0++) {
						// find and resolve conflicts between pairs
						// pair.x is in previous segment (prev_seg), and in case of a chain - the last one
						// pair.y is the local index of a target in this (nSeg) segment, and in case of a chain - the first one
						// will compare only these 2 targets, not the total length for the chain
						if (!pair_list.isEmpty()) {
							// check for conflicts
							removeConflictPairs(
									pair_list,     //  ArrayList<Point>  pair_list, // in each pair, .x is earlier than .y
									prev_ser,      // int               nser0,  // corresponds to .x, may be the same as nser1 (for local merging)
									nSer,          // int               nser1,  // corresponds to .y, may be the same as nser0 (for local merging)
									shortest_gap,  // boolean           shortest_gap,
									debugLevel);   // int               debugLevel) {
							// combine all the remaining pairs
							// convert pair_list to array -> pairs[nSer]
							pairs[nSer] = new int [pair_list.size()][2];
							for (int i = 0; i < pairs[nSer].length; i++) {
								Point p = pair_list.get(i);
								pairs[nSer][i] = new int[] {p.x, p.y};
							}
						} else {
							pairs[nSer] = new int [0][];
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		// process pairs, assign global indices
		// non -threaded part
		int next_global_target_id = TARGET_INDEX_UAS + 1; // 2
		for (int nser = 0; nser < target_map.length; nser++) {
			int num_seq = targets_multi_series[nser].length;
			if (nser > 0) {
				// start with pairs
				for (int [] pair : pairs[nser]) { // null?
					int target_id = target_map[nser-1][pair[0]];
					int tgrp = local_map[nser][pair[1]];
					for (int i = 0; i < local_imap[nser][tgrp].length; i++) {
						int ntarg = local_imap[nser][tgrp][i];
						target_map[nser][ntarg] = target_id;

						for (int nseq = 0; nseq < num_seq; nseq++) {
							int [] ltm = linked_targets_multi[nser][ntarg][nseq];
							if (ltm != null) {
								int ntile = ltm[0];
								double [] target = targets_multi_series[nser][nseq][ntile];
								target[CuasMotionLMA.RSLT_TARGET_ID] = target_id;
							}
						}
					}					
				}
			}
			if (old_mode) {
				for (int tgrp = 0; tgrp < local_imap[nser].length; tgrp++) if (local_imap[nser][tgrp].length > 0){
					int target_id = -1;
					for (int i = 0; i < local_imap[nser][tgrp].length; i++) {
						int ntarg = local_imap[nser][tgrp][i];
						if (target_map[nser][ntarg] == TARGET_INDEX_NONE) { // 0, not yet assigned
							if (target_id <= 0) { // assign once
								target_id = next_global_target_id++;
							}
							target_map[nser][ntarg] = target_id;
							for (int nseq = 0; nseq < num_seq; nseq++) {
								int [] ltm = linked_targets_multi[nser][ntarg][nseq];
								if (ltm != null) {
									int ntile = ltm[0];
									double [] target = targets_multi_series[nser][nseq][ntile];
									target[CuasMotionLMA.RSLT_TARGET_ID] = target_id;
								}
							}
						}
					}
				}
			} else {
				/// Now new global target_id will start with the local target sequences (having most tiles).
				/// This is done for later annotations ordering - prefer longer targets (display over others)    
				ArrayList<Point> unassigned_list= new ArrayList<Point>(); // list of unassigned groups index:num_tiles
				for (int tgrp = 0; tgrp < local_imap[nser].length; tgrp++) if (local_imap[nser][tgrp].length > 0){
					int ntarg0 = local_imap[nser][tgrp][0]; // always exists as (local_imap[nser][tgrp].length > 0)
					if (target_map[nser][ntarg0] == TARGET_INDEX_NONE) { // already assign to all targets in a group
						int num_occur = 0;
						for (int i = 0; i < local_imap[nser][tgrp].length; i++) {
							int ntarg = local_imap[nser][tgrp][i];
							// count number of this target group keyframes
							for (int nseq = 0; nseq < num_seq; nseq++) {
								int [] ltm = linked_targets_multi[nser][ntarg][nseq];
								if (ltm != null) {
									num_occur++;
								}
							}
						}
						unassigned_list.add(new Point(tgrp, num_occur));
					}
				}
				// sort by decreasing length (longest - first)
				Collections.sort(unassigned_list, new Comparator<Point>() {
					@Override
					public int compare(Point lhs, Point rhs) {
						return Integer.compare(rhs.y, lhs.y);
					}
				});

				for (Point p:unassigned_list) {
					int tgrp = p.x;
					int target_id = next_global_target_id++;
					for (int i = 0; i < local_imap[nser][tgrp].length; i++) {
						int ntarg = local_imap[nser][tgrp][i];
						target_map[nser][ntarg] = target_id;
						for (int nseq = 0; nseq < num_seq; nseq++) {
							int [] ltm = linked_targets_multi[nser][ntarg][nseq];
							if (ltm != null) {
								int ntile = ltm[0];
								double [] target = targets_multi_series[nser][nseq][ntile];
								target[CuasMotionLMA.RSLT_TARGET_ID] = target_id;
							}
						}
					}				
				}
				if (debug) {
					System.out.println("combineGlobalTargets() nser="+nser+", unassigned_list.size()="+unassigned_list.size()+", next_global_target_id="+next_global_target_id);
				}
			}
			
		}

		
		// print more debug
		target_rmap = new int [next_global_target_id - 1][][];
		ai.set(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nGtarg = ai.getAndIncrement(); nGtarg < target_rmap.length; nGtarg = ai.getAndIncrement()) {
						int target_id = nGtarg+1;
						ArrayList<Point> local_list = new ArrayList<Point>(); // .x - nser, .y - ntarg(0-based)
						for (int nser = 0; nser < target_map.length; nser++) {
							for (int ntarg = 0; ntarg < target_map[nser].length; ntarg++) {
								if (target_map[nser][ntarg] == target_id) {
									local_list.add(new Point(nser,ntarg));
								}
							}
						}
						// sort list by the timestamps
						Collections.sort(local_list, new Comparator<Point>() {
							@Override
							public int compare(Point lhs, Point rhs) {
								return Double.compare(getLocalTargetStartTimestamp(lhs.x, lhs.y), getLocalTargetStartTimestamp(rhs.x, rhs.y));
							}
						});
						target_rmap[nGtarg]= new int [local_list.size()][];
						for (int i = 0; i <target_rmap[nGtarg].length; i++) {
							Point p = local_list.get(i);
							target_rmap[nGtarg][i] = new int [] {p.x, p.y};
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return next_global_target_id;
	}
	
	public void linearRangeInterpolation() {
		final GeometryCorrection gc = master_CLT.getGeometryCorrection();
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nGtarg = ai.getAndIncrement(); nGtarg < avg_range_ts.length; nGtarg = ai.getAndIncrement()) {
						int target_id = nGtarg +1;
						for (int nser = 0; nser < avg_range_ts[nGtarg].length; nser++) if (avg_range_ts[nGtarg][nser] != null) {
							double fps = getFps(nser);
							double avg_ts =    avg_range_ts[nGtarg][nser][AVG_TIMESTASMP];
							double avg_range = avg_range_ts[nGtarg][nser][AVG_RANGE];
							double avg_away =  avg_range_ts[nGtarg][nser][AVG_VELOCIY];
							for (int ntarg=0; ntarg < target_map[nser].length; ntarg++) if (target_map[nser][ntarg] == target_id){ // same global id
								int nseq_start = targets_start_end[nser][ntarg][0];
								int nseq_end =   targets_start_end[nser][ntarg][1];
								for (int nseq = nseq_start; nseq <=nseq_end; nseq++) {
									int ntile = 	linked_targets_multi[nser][ntarg][nseq][0];
									double ts = getKeyTimeStamp(nser, nseq);
									double [] target = targets_multi_series[nser][nseq][ntile];
									double range = avg_range;
									if (avg_away != 0) {
										range += avg_away * (ts - avg_ts); 
									}
									target[CuasMotionLMA.RSLT_RANGE_LIN] = range;
									double [] vlateral = 	getLateralVelocity (
											clt_parameters, // CLTParameters      clt_parameters,
											gc,             // GeometryCorrection gc,
											target,         // double []          target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
											range,          // double             range,
											fps);           // double             fps){ // velocity_scale times fps
									target[CuasMotionLMA.RSLT_VEL_RIGHT] = vlateral[0];
									target[CuasMotionLMA.RSLT_VEL_UP] =    vlateral[1];
									target[CuasMotionLMA.RSLT_VEL_AWAY] =  avg_away;
								}
							}
						}						
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	public double [][][] getAverageRangeTimestamp(
			double min_disparit_range,
			double min_disparit_velocity){
		final double [][][] avg_rts = new double [target_rmap.length][target_map.length][];
		final double infinity =     clt_parameters.imp.cuas_infinity;
		final GeometryCorrection gc = master_CLT.getGeometryCorrection();		
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		final int dbg_gtarg = -21-1;
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					int [] seq_tile = new int [2];
					for (int nGtarg = ai.getAndIncrement(); nGtarg < target_rmap.length; nGtarg = ai.getAndIncrement()) {
						int [][] rmap = target_rmap[nGtarg];
//						int num_occ = target_rmap[nGtarg].length; // number of local targets with the same target_id
						int target_id = nGtarg+1;
						int indx_first = 0;
						int indx_next =0;
						if (nGtarg == dbg_gtarg) {
							System.out.println("getAverageRangeTimestamp(): nGtarg="+nGtarg);
						}
						for (; indx_first < rmap.length; indx_first = indx_next) {
							int nser = rmap[indx_first][0];
							double ts_start =  getKeyTimeStamp(
									nser,      // int nser,
									0);       // int nseq)
							int last_in_ser = targets_multi_series[nser].length - 1;  
							double ts_end =  getKeyTimeStamp(
									nser,      // int nser,
									last_in_ser);       // int nseq)
							indx_next = indx_first+1;
							for (; (indx_next < rmap.length) && (rmap[indx_next][0] == nser);indx_next++);
							int indx_last = indx_next -1;
							// calculate average disparity if multiple
							double sumw = 0, sumw_disp = 0, sumw_ts = 0;
							for (int indx = indx_first; indx <= indx_last; indx++) {
								int ltarg = rmap[indx][1]; // OOB
								double [] middle_target =  getMiddleTarget(
										nser, // int nser,
										ltarg, // int local_target,
										seq_tile); // int [] seq_tile)
								if (middle_target != null) {
									double w =         middle_target[CuasMotionLMA.RSLT_GLENGTH];
									double disparity = middle_target[CuasMotionLMA.RSLT_GDISPARITY];
									double ts = getKeyTimeStamp(
													nser,         // int nser,
													seq_tile[0]); // int nseq)
									sumw += w;
									sumw_disp += w * disparity;
									sumw_ts += w * ts;
								}
							}
							if (sumw > 0) { // if 0, keep undefined (null)
								double avg_disp = sumw_disp/sumw - infinity;
								double avg_ts =   sumw_ts/sumw;
								double avg_range = (avg_disp >= min_disparit_range) ? gc.getZFromDisparity(avg_disp):Double.POSITIVE_INFINITY;
								int avg_key_frame = (int) Math.round(last_in_ser * (avg_ts - ts_start)/(ts_end - ts_start));
								avg_rts[nGtarg][nser] = new double [AVG_LENGTH];
								avg_rts[nGtarg][nser][AVG_RANGE] = avg_range;
								avg_rts[nGtarg][nser][AVG_TIMESTASMP] = avg_ts;
								avg_rts[nGtarg][nser][AVG_KEYFRAME] =   avg_key_frame;
								avg_rts[nGtarg][nser][AVG_DISPARITY] =  avg_disp;
								avg_rts[nGtarg][nser][AVG_VELOCIY] =    Double.NaN;
							}

						}
						if (nGtarg == dbg_gtarg) {
							System.out.println("getAverageRangeTimestamp(): nGtarg="+nGtarg);
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		// calculate derivatives, save as the last item in avg_rts[nGtarg][nser]
		int num_ser = targets_multi_series.length;
		for (int ngtarg = 0; ngtarg < target_rmap.length; ngtarg++) {
			for (int nser = 0; nser < num_ser; nser++) {
				double [] avg = avg_rts[ngtarg][nser];
				if ((avg != null) && (avg[AVG_DISPARITY] > min_disparit_velocity)) {
					double v_prev = Double.NaN;
					double v_next = Double.NaN;
					double [] avg_prev = (nser > 0) ? avg_rts[ngtarg][nser - 1]: null;  
					double [] avg_next = (nser < (num_ser-1)) ? avg_rts[ngtarg][nser + 1]: null;
					if ((avg_prev != null) && (avg_prev[AVG_DISPARITY] > min_disparit_velocity)) {
						double dt = avg[AVG_TIMESTASMP] - avg_prev[AVG_TIMESTASMP];
						double dr = avg[AVG_RANGE] -      avg_prev[AVG_RANGE];
						v_prev = dr / dt;
					}
					if ((avg_next != null) && (avg_next[AVG_DISPARITY] > min_disparit_velocity)) {
						double dt = avg_next[AVG_TIMESTASMP] - avg[AVG_TIMESTASMP];
						double dr = avg_next[AVG_RANGE] -      avg[AVG_RANGE];
						v_next = dr / dt;
					}
					avg[AVG_VELOCIY] = (Double.isNaN(v_prev)? 0 : v_prev) + (Double.isNaN(v_next)? 0 : v_next);
					if (!Double.isNaN(v_prev) && !Double.isNaN(v_next)) {
						avg[AVG_VELOCIY] /= 2;
					}
				}
			}
		}
		return avg_rts;
	}
	
	
	
	public double getFps(int nser) {
		double fps = (CuasMotion.getCorrInc(clt_parameters) * (scene_titles[nser].length - 1.0))/(QuadCLT.getTimeStamp(scene_titles[nser][scene_titles[nser].length-1]) -
				QuadCLT.getTimeStamp(scene_titles[nser][0]));
		return fps;
	}
	
	public double getLocalTargetStartTimestamp(
			int nser,
			int ltarg) {
		return getKeyTimeStamp(
				nser, // int nser,
				targets_start_end[nser][ltarg][0]); // int nseq)
	}
	
	public double getKeyTimeStamp(
			int nser,
			int nseq) {
		String sts = scene_titles[nser][nseq];
		double ts = QuadCLT.getTimeStamp(sts);
		return ts;
	}
	
	
	private void removeConflictPairs(
			ArrayList<Point>  pair_list, // in each pair, .x is earlier than .y
			int               nser0,  // corresponds to .x, may be the same as nser1 (for local merging)
			int               nser1,  // corresponds to .y, may be the same as nser0 (for local merging)
			boolean           shortest_gap,
			int               debugLevel) {
		int debug_min = -2; // if >= then print
		boolean debug = debugLevel>= debug_min; 
		boolean conflict = true;
		while (conflict) {
			conflict= false;
			check_for_conflicts: {
				for (int npair0 = 0; npair0< (pair_list.size()); npair0++) {
					Point p0 =  pair_list.get(npair0); // here: .x - local target index in the previous series (prev_ser), .y - in this one (nSer) 
					for (int npair1 = npair0 +1; npair1< (pair_list.size()); npair1++) {
						Point p1 =  pair_list.get(npair1);
						if (p1.x == p0.x) {
							if (shortest_gap) {
								int p0_start1 =   targets_start_end[nser1][p0.y][0]; // start of the second segment
								int p1_start1 =   targets_start_end[nser1][p1.y][0];
								if (p0_start1 < p1_start1) {
									if (debug) System.out.println("1 nser "+nser0+":"+nser1+" removing pair ["+p1.x+","+p1.y+"] as its gap is longer than ["+p0.x+","+p0.y+"]");
									pair_list.remove(npair1);
								} else {
									if (debug) System.out.println("2 nser "+nser0+":"+nser1+" removing pair ["+p0.x+","+p0.y+"] as its gap is longer than ["+p1.x+","+p1.y+"]");
									pair_list.remove(npair0);
								}
							} else {
								int p0_end =   targets_start_end[nser1][p0.y][1];
								int p1_end =   targets_start_end[nser1][p1.y][1];
								if (p0_end > p1_end) {
									if (debug) System.out.println("3 nser "+nser0+":"+nser1+" removing pair ["+p1.x+","+p1.y+"] as it is shorter than ["+p0.x+","+p0.y+"]");
									pair_list.remove(npair1);
								} else {
									if (debug) System.out.println("4 nser "+nser0+":"+nser1+" removing pair ["+p0.x+","+p0.y+"] as it is shorter than ["+p1.x+","+p1.y+"]");
									pair_list.remove(npair0);
								}
							}
							conflict = true;
							break check_for_conflicts;
						}

						if (p1.y == p0.y) {
							if (shortest_gap) {
								int p0_end0 =   targets_start_end[nser0][p0.x][1]; // end of the first segment
								int p1_end0 =   targets_start_end[nser0][p1.x][1];
								if (p0_end0 > p1_end0) {
									if (debug) System.out.println("removing pair ["+p1.x+","+p1.y+"] as its gap is longer than ["+p0.x+","+p0.y+"]");
									pair_list.remove(npair1);
								} else {
									if (debug) System.out.println("removing pair ["+p0.x+","+p0.y+"] as its gap is longer than ["+p1.x+","+p1.y+"]");
									pair_list.remove(npair0);
								}
							} else {
								int p0_start0 =   targets_start_end[nser0][p0.x][0];
								int p1_start0 =   targets_start_end[nser0][p1.x][0];
								if (p0_start0 < p1_start0) {
									if (debug) System.out.println("removing pair ["+p1.x+","+p1.y+"] as it is shorter than ["+p0.x+","+p0.y+"]");
									pair_list.remove(npair1);
								} else {
									if (debug) System.out.println("removing pair ["+p0.x+","+p0.y+"] as it is shorter than ["+p1.x+","+p1.y+"]");
									pair_list.remove(npair0);
								}
							}
							conflict = true;
							break check_for_conflicts;
						}
					}											
				}
			}
		}
		return;
	}
	
	
	
	/**
	 * Find the "middle" keyframe that contanis disparity/range for the specified local target index (0-based)  
	 * @param nser series number containing specified local target
	 * @param local_target local
	 * @param seq_tile - null or int[2] - will return [nseq_middle, ntile_middle]
	 * @return target array containing the longest averaged disparity and range for the specified target
	 */
	private double  [] getMiddleTarget(
			int nser,
			int local_target,
			int [] seq_tile) {
		int [][] lt = linked_targets_multi[nser][local_target]; // OOB
		int len_middle = -1;
		int nseq_middle = -1;
		int ntile_middle = -1;
		for (int nseq = 0; nseq < lt.length; nseq++) {
			if (lt[nseq] != null) {
				int ntile = lt[nseq][0]; // null pointer
				double [] target = targets_multi_series[nser][nseq][ntile];
				if (target != null) {
					double dlength = target[CuasMotionLMA.RSLT_GLENGTH];
					if (!Double.isNaN(dlength)) {
						int ilength = (int) dlength;
						if (ilength > len_middle) {
							len_middle = ilength;
							nseq_middle = nseq;
							ntile_middle = ntile;
						}
					}
				}
			}
		}
		if (len_middle >= 0) {
			if (seq_tile != null) {
				seq_tile[0] = nseq_middle;
				seq_tile[1] = ntile_middle;
			}
			return targets_multi_series[nser][nseq_middle][ntile_middle];
		} else {
			return null;
		}
	}

	
	
	
	
	private double getMispatch2(
			CLTParameters  clt_parameters,
			double [] target0, 
			double [] target1,
			String    sts0,
			String    sts1,
			int       ntile0,
			int       ntile1,
			int       tilesX,
			int       tileSize,
			double    fps) {
		// multiply Vx, VY to get pixel shift in one frame (@60Hz)
		double    velocity_scale = CuasMotion.getFrameVelocityScale(clt_parameters); 
		double ts0 = QuadCLT.getTimeStamp(sts0);			
		double ts1 = QuadCLT.getTimeStamp(sts1);
		double dscenes = fps * (ts1 - ts0); // number of scene between 
		int tileX0 = ntile0 % tilesX; 
		int tileY0 = ntile0 / tilesX;
		int tileX1 = ntile1 % tilesX; 
		int tileY1 = ntile1 / tilesX;
		double px0 = tileSize * tileX0 + tileSize/2 + target0[CuasMotionLMA.RSLT_X];
		double py0 = tileSize * tileY0 + tileSize/2 + target0[CuasMotionLMA.RSLT_Y];
		double px1 = tileSize * tileX1 + tileSize/2 + target1[CuasMotionLMA.RSLT_X];
		double py1 = tileSize * tileY1 + tileSize/2 + target1[CuasMotionLMA.RSLT_Y];
		double vx = 0.5*(target0[CuasMotionLMA.RSLT_VX] + target1[CuasMotionLMA.RSLT_VX]);
		double vy = 0.5*(target0[CuasMotionLMA.RSLT_VY] + target1[CuasMotionLMA.RSLT_VY]);
		double dpx = px1 - px0;
		double dpy = py1 - py0;
		dpx -= vx * velocity_scale * dscenes;
		dpy -= vy * velocity_scale * dscenes;
		double err2 = dpx*dpx + dpy * dpy;
		return err2;
	}
	
 
	/**
	 * Get lateral velocity in meters/s
	 * @param clt_parameters
	 * @param gc
	 * @param target0
	 * @param target1
	 * @param range
	 * @param fps
	 * @return
	 */
	
	private static double getLateralVelocity (
			CLTParameters      clt_parameters,
			GeometryCorrection gc,
			double []          target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double []          target1, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double             range,
			double             fps){ // velocity_scale times fps
		double ifov = gc.getIFOV();
		double velocity_scale =  CuasMotion.getFrameVelocityScale(clt_parameters); // == 1.0/clt_parameters.imp.cuas_corr_offset;
		double vpx = 0.5*(target0[CuasMotionLMA.RSLT_VX] + target1[CuasMotionLMA.RSLT_VX]) * velocity_scale * fps;
		double vpy = 0.5*(target0[CuasMotionLMA.RSLT_VY] + target1[CuasMotionLMA.RSLT_VY]) * velocity_scale * fps;
        double vxm = vpx * ifov * range;
        double vym = vpy * ifov * range;
        double vm = Math.sqrt(vxm*vxm+vym*vym);
        return vm;
	}
	
	public static double [] getLateralVelocity (
			CLTParameters      clt_parameters,
			GeometryCorrection gc,
			double []          target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double             fps){ // velocity_scale times fps
		return 	getLateralVelocity (
				clt_parameters,
				gc,
				target0,                            // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
				target0[CuasMotionLMA.RSLT_GRANGE], //double             range,
				fps);                               // velocity_scale times fps
	}
	
	/**
	 * Calculate lateral (horizontal - right, vertical up} velocities in m/s
	 * @param clt_parameters
	 * @param gc
	 * @param target0
	 * @param range
	 * @param fps
	 * @return
	 */
	public static double [] getLateralVelocity (
			CLTParameters      clt_parameters,
			GeometryCorrection gc,
			double []          target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double             range,
			double             fps){ // velocity_scale times fps
		double ifov = gc.getIFOV();
		double velocity_scale =  CuasMotion.getFrameVelocityScale(clt_parameters); // == 1.0/clt_parameters.imp.cuas_corr_offset;
		double vpx = target0[CuasMotionLMA.RSLT_VX] * velocity_scale * fps;
		double vpy = target0[CuasMotionLMA.RSLT_VY] * velocity_scale * fps;
        double vxm = vpx * ifov * range;
        double vym = vpy * ifov * range;
        double vm = Math.sqrt(vxm*vxm+vym*vym);
        return new double [] {vxm, -vym};
	}
	
	
	private static double getLateralVelocity (
			CLTParameters clt_parameters,
			GeometryCorrection gc,
			double []          target0, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double []          target1, // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
			double             fps){ // velocity_scale times fps
		double range = 0.5 * (target0[CuasMotionLMA.RSLT_GRANGE] + target0[CuasMotionLMA.RSLT_GRANGE]); // meters
		return getLateralVelocity (
				clt_parameters,
				gc,
				target0,     // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
				target1,     // should contain RSLT_VX, RSLT_VX, RSLT_GRANGE,
				range,
				fps);        // velocity_scale times fps
		}	
	
	
	
	public double recalculateRangeAfterInfinityChange(
			double old_range,
			double old_infinity,
			double new_infinity) {
		GeometryCorrection gc = master_CLT.getGeometryCorrection();
		return recalculateRangeAfterInfinityChange(
				old_range,
				old_infinity,
				new_infinity,
				gc);
	}

	
	public static double recalculateRangeAfterInfinityChange(
			double old_range,
			double old_infinity,
			double new_infinity,
			GeometryCorrection gc) {
		if (Double.isNaN(old_range)) {
			return Double.NaN;
		}
		if (Double.isInfinite(old_range)) {
			return Double.POSITIVE_INFINITY;
		}
		double disparity = gc.getDisparityFromZ(old_range) + old_infinity -new_infinity;
		if (disparity <= 0) {
			return Double.POSITIVE_INFINITY;
		}
		return gc.getZFromDisparity(disparity);
	}
	
	public static void recalculateAllRanges(
			final double [][][][] targets_multi_series, // [nser][nseq][ntile][nfield]
			final double new_infinity,
			final GeometryCorrection gc) {
		System.out.println("recalculateAllRanges(), new infinity="+new_infinity);
		System.out.println("updates RSLT_RANGE, RSLT_GRANGE, RSLT_VEL_AWAY, RSLT_VEL_RIGHT, RSLT_VEL_UP, RSLT_RANGE_LIN, RSLT_INFINITY");
		final Thread[] threads =    ImageDtt.newThreadArray();
		final AtomicInteger ai =    new AtomicInteger(0);
		for (int ithread = 0; ithread < threads.length; ithread++) {
			threads[ithread] = new Thread() {
				public void run() {
					for (int nSer = ai.getAndIncrement(); nSer < targets_multi_series.length; nSer = ai.getAndIncrement()) {
						for (int nseq = 0; nseq < targets_multi_series[nSer].length; nseq++) {
							for (int ntile = 0; ntile < targets_multi_series[nSer][nseq].length; ntile++) {
								double [] target = targets_multi_series[nSer][nseq][ntile];
								if (target != null) { //  && !Double.isNaN(target[CuasMotionLMA.RSLT_INFINITY])) {
									double old_infinity = target[CuasMotionLMA.RSLT_INFINITY];
									if (!Double.isNaN(old_infinity) && (old_infinity != new_infinity)) {
										target[CuasMotionLMA.RSLT_RANGE] = recalculateRangeAfterInfinityChange(
												target[CuasMotionLMA.RSLT_RANGE],
												old_infinity,
												new_infinity,
												gc);
										double old_range = target[CuasMotionLMA.RSLT_GRANGE]; 
										target[CuasMotionLMA.RSLT_GRANGE] = recalculateRangeAfterInfinityChange(
												target[CuasMotionLMA.RSLT_GRANGE],
												old_infinity,
												new_infinity,
												gc);
										double new_range = target[CuasMotionLMA.RSLT_GRANGE];
										double range_scale = new_range/old_range;
										if ((range_scale > 0.1) && (range_scale < 10)) { // to deal with NaN and infinities
											target[CuasMotionLMA.RSLT_VEL_AWAY] *=   range_scale; // could be NaN - OK
											target[CuasMotionLMA.RSLT_VEL_RIGHT] *=  range_scale; // could be NaN - OK
											target[CuasMotionLMA.RSLT_VEL_UP] *=     range_scale; // could be NaN - OK
										}
										target[CuasMotionLMA.RSLT_RANGE_LIN] = recalculateRangeAfterInfinityChange(
												target[CuasMotionLMA.RSLT_RANGE_LIN],
												old_infinity,
												new_infinity,
												gc);
										target[CuasMotionLMA.RSLT_INFINITY] = new_infinity;
									}
								}
							}
						}
					}
				}
			};
		}
		ImageDtt.startAndJoin(threads);
		return;
	}
	
	public ImagePlus testGenerateRadarImage(
			CLTParameters       clt_parameters,
			int                 nser,
			UasLogReader        uasLogReader, // contains camera orientation (getCameraATR())
			int                 debugLevel) {
//		int    annot_mode = 0xffffffbf; // -1; // specify bits
		int    annot_mode = clt_parameters.imp.cuas_annot_sel[CuasMotion.ANNOT_RADAR_PANE_INDX];
		
		String image_name = model_names[nser];
		int    corr_pairs =        clt_parameters.imp.cuas_corr_pairs;
		boolean ra_background = clt_parameters.imp.cuas_ra_background;   // true;
		String ra_bg_suffix=(ra_background? ("-RABG"+corr_pairs):"");
		String clean_suffix = ""; // or "-CLEAN"
		String lwir_image_title = image_name+CuasMotion.getParametersSuffixRslt(clt_parameters,"-RGB"+ra_bg_suffix+clean_suffix)+".tiff";
		String lwir_path = new File(model_dirs[nser], lwir_image_title).toString();
		ImagePlus imp_lwir = new ImagePlus(lwir_path);
		if (imp_lwir.getWidth()> 0) {
			System.out.println("Read : "+lwir_path);
			String title = image_name+"-RADAR";
			ImagePlus imp_radar=CuasMotion.generateRadarImage(
					clt_parameters,
					annot_mode,    // int           annot_mode, // specify bits
					master_CLT,                // QuadCLT            scene,
					targets_multi_series[nser],
					uasLogReader,              // contains camera orientation (getCameraATR())
					title,
					scene_titles[nser],        // corresponding to top targets dimension
					imp_lwir,                 // all titles, one per frame
					debugLevel);
			imp_radar.show();
			return imp_radar;
		} else {
			System.out.println("File not found: "+lwir_path);
		}
		// 	final String [][]     scene_titles; // keyframe scene titles/timestamps
		return null;
	}
	
} 
