import nom.tam.fits.*;
import nom.tam.image.*;
import nom.tam.util.*;


import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.io.*;
import java.text.*;
import java.util.*;
import javax.swing.*;

// weight is pixel weight
// rms is beam rms.
// rms in output is written as pixel rms

public class ReducedData implements Cloneable {
    double[][] maps[], map, rms, clean, s2n, weight, count;
    int[][] flag;
    Header header;

    int sizeX, sizeY;
  
    String sourceName;
    double RA, DEC, coordinateEpoch;
    boolean altaz;
    double cosDEC;

    String[] options;

    double delta;
    double rangeRA, rangeDEC;

    double min, max;
    double defaultZoom = 1.0;
    
    double Jy = 5.43e-7 * Unit.VOLT; // sky corrected V at bolometer
    double fluxCorrectingFWHM = Double.NaN;

    double beamFWHM = ReductionTool.beamFWHM;
    double beamArea, effectiveResolution, pointsPerBeam = 1.0;    

    DisplayUnit V = new DisplayUnit(Unit.VOLT, "V");
    DisplayUnit mV = new DisplayUnit(1.0e-3 * V.value, "mV");
    DisplayUnit uV = new DisplayUnit(1.0e-6 * V.value, "uV");
    DisplayUnit nV = new DisplayUnit(1.0e-9 * V.value, "nV");
    DisplayUnit pV = new DisplayUnit(1.0e-12 * V.value, "pV");
       
    DisplayUnit janskyPerBeam = new DisplayUnit(0.0, "Jy/beam");
    DisplayUnit janskyPerSquareArcSec;
    DisplayUnit janskyPerSr;    
    DisplayUnit janskyPer9ArcSecBeam;

    DisplayUnit displayUnit, fitsUnit;
    
    int chooser = 0, useScheme = 0;

    Vector regions = new Vector();
    Vector colorSchemes = new Vector();
    Vector units = new Vector();
    // The Sharc Header with all the auxillary info
    SharcScan[] scan;
    ReductionTool reducer;

    String fileName;

    // The reference offsets and the corresponding map indexes
    double dRA0=0.0, dDEC0=0.0;
    double i0=0.0, j0=0.0;
    
    String creator = "CRUSH"; 
    String creatorVersion = Crush.version;

    boolean masked=false, circled=true;
    MapPanel display;
    
    private double timeResolution;
    
    public ReducedData() { }
    
    public ReducedData(int i, int j) { setSize(i, j); }
    
    public ReducedData(String name) throws Exception { read(name); }

    public void init() {	
	janskyPerSquareArcSec = new DisplayUnit(Jy * SharcHeader.pixelArea / (Unit.ArcSECOND * Unit.ArcSECOND), "Jy/arcsec**2");
	janskyPerSr = new DisplayUnit(Jy * SharcHeader.pixelArea, "Jy/sr");

	double beam9Area = 2.0 * Math.PI * 81.0 * Unit.ArcSECOND * Unit.ArcSECOND / 5.5452 / SharcHeader.pixelArea;
	janskyPer9ArcSecBeam = new DisplayUnit(Jy/beam9Area, "Jy/9\"beam");

	calcBeamUnits();

	colorSchemes.add(new CameraLike());
	colorSchemes.add(new GreyScale());
	colorSchemes.add(new Orangy());
	colorSchemes.add(new CoolBlue());
	
	units.add(uV);
	units.add(janskyPerBeam);
	units.add(janskyPerSquareArcSec);

	displayUnit = (DisplayUnit) units.get(0);

	cosDEC = Math.cos(DEC);
    }
    
    
    public Object clone() { 	
	try { 
	    ReducedData newReduced = (ReducedData) super.clone(); 
	    newReduced.init();
	    return newReduced;
	}
	catch(CloneNotSupportedException e) { e.printStackTrace(); }
	return null;
    }
    
    public ReducedData getReducedData() {
	ReducedData copy = (ReducedData) clone();
	
	// Make a copy of the fundamental data
	copy.clean = (double[][]) imageCopy(clean);
	copy.rms = (double[][]) imageCopy(rms);
	copy.weight = (double[][]) imageCopy(weight);
	copy.count = (double[][]) imageCopy(count);
	copy.flag = (int[][]) imageCopy(flag);
      
	// Calculate the dependent quantities;
	copy.s2n = new double[sizeX][sizeY];
	copy.calc();
	
	copy.colorSchemes = (Vector) colorSchemes.clone();
	copy.units = (Vector) units.clone();
	
	return copy;
    }
    
    public Object imageCopy(Object image) {

	if(image instanceof double[][]) {
	    double[][] orig = (double[][]) image;
	    double[][] copy = new double[sizeX][sizeY];
	    for(int i=0; i<sizeX; i++) System.arraycopy(orig[i], 0, copy[i], 0, sizeY);	
	    return copy;
	}	
	else if(image instanceof int[][]) {
	    int[][] orig = (int[][]) image;
	    int[][] copy = new int[sizeX][sizeY];
	    for(int i=0; i<sizeX; i++) System.arraycopy(orig[i], 0, copy[i], 0, sizeY);	
	    return copy;
	}
       
	return null;
    }
    
    ReducedData backup = null;
    public void backup(boolean multilevel) {
	backup = getReducedData();
	if(!multilevel) backup.backup = null;
    }
    
    public void backup() { backup(false); }    

    public void undo() {
	if(backup != null) {
	    ReducedData newBackup = getReducedData();
	    
	    clean = backup.clean;
	    rms = backup.rms;
	    weight = backup.weight;
	    count = backup.count;
	    s2n = backup.s2n;
	    flag = backup.flag;

	    calc();
	    backup = newBackup;
	}
    }
    
    public void calc() {
	maps = new double[][][] { clean, rms, s2n };
	map = maps[chooser];

	calcRMS();
	calcS2N();	
	calcBeamUnits();

	min=getMin();
	max=getMax();
	
	rangeRA = delta * sizeX;
	rangeDEC = delta * sizeY;

	ci = sizeX / 2.0;
	cj = sizeY / 2.0;
    }

    public void info() {
	DecimalFormat f1 = new DecimalFormat("0.0");
	DecimalFormat f2 = new DecimalFormat("0.00");
	DecimalFormat e3 = new DecimalFormat("0.000E0");

	double FWHM = Math.sqrt(beamFWHM * beamFWHM + effectiveResolution * effectiveResolution);

	String info = "\n";
	
	info += fileName == null ? "\n" : " Image File: " + fileName + ". ->" + "\n\n";

	info +=
	    "  [" + sourceName + "]" + "\n" +
	    "  RA = " + Util.HMS(RA * 12.0 * Unit.HOUR / Math.PI) + ", DEC = " + Util.DMS(DEC) + " (" + f1.format(coordinateEpoch) + ")\n" +
	    "  Map Size: " + sizeX + " x " + sizeY + " pixels. (" 
	    + f1.format(rangeRA / Unit.ArcMINUTE) + " x " + f1.format(rangeDEC / Unit.ArcMINUTE) + " arcmin.)" + "\n" +
	    "  Grid Spacing: " + f2.format(delta / Unit.ArcSECOND) + " arcsec.\n" +
	    "  Instrument Beam FWHM: " + f2.format(beamFWHM / Unit.ArcSECOND) + " arcsec." + "\n" +
	    "  Applied Smoothing: " + f2.format(effectiveResolution / Unit.ArcSECOND) + " arcsec." + "\n" +
	    "  Image Resolution (FWHM): " + f2.format(FWHM / Unit.ArcSECOND) + " arcsec. (includes smoothing)" + "\n" +
	    "  Orientation: " + (altaz ? "Alt/Az" : "RA/DEC (skyright)") + "\n" + 
	    "  Units & Conversion Factors: \n";
	
	for(int i=0; i<units.size(); i++) {
	    DisplayUnit aUnit = (DisplayUnit)units.get(i);
	    info += "\t" + aUnit.name + " = " + e3.format(aUnit.value) + "\n";
	}
	
	info += "  Created by " + creator + " version " + creatorVersion + "\n";
	info += "  Reduction Options: ";

	for(int i=0; i<options.length; i++) info += options[i] + " ";

	info += "\n";

	if(Double.isNaN(fluxCorrectingFWHM)) info += "  Fluxes are uncorrected."; 
	else info += "  Fluxes are corrected for " + f2.format(fluxCorrectingFWHM / Unit.ArcSECOND) + " arcsec FWHM sources. ";

	System.out.println(info);
    }
     
    public void calcBeamUnits() {
	double FWHM2 = beamFWHM * beamFWHM + effectiveResolution * effectiveResolution;
	beamArea = 2.0 * Math.PI * FWHM2 / 5.5452;
	janskyPerBeam.value = Jy * SharcHeader.pixelArea / beamArea;
	pointsPerBeam = beamArea / (delta*delta);
    }
   
    public void math(char op, ReducedData arg) { math(op, arg, true); }
    
    public void math(char op, ReducedData arg, boolean calcWeight) {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    switch(op) {
	    case '+': 
		clean[i][j] += arg.clean[i][j]; 
		if(calcWeight) weight[i][j] = 1.0 / (1.0/weight[i][j] + 1.0/arg.weight[i][j]);
		break;
	    case '-': 
		clean[i][j] -= arg.clean[i][j]; 
		if(calcWeight) weight[i][j] = 1.0 / (1.0/weight[i][j] + 1.0/arg.weight[i][j]);
		break;
	    default: 
		return; 
	    }	    	    
	}
	calc();
    }
    
    public void setSize(int i, int j) {
	sizeX = i;
	sizeY = j;
	
	rms = new double[sizeX][sizeY];
	clean = new double[sizeX][sizeY];
	s2n = new double[sizeX][sizeY];
	weight = new double[sizeX][sizeY];
	count = new double[sizeX][sizeY];

	flag = new int[sizeX][sizeY];
	for(int ii=0; ii<sizeX; ii++) Arrays.fill(flag[ii], 1);
	
	calc();
    }
    

    public void calcS2N() {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) 
	    s2n[i][j] = rms[i][j] > 0.0 ? clean[i][j] / rms[i][j] : 0.0;
    }

    public void setUnit(String unitName) throws IllegalArgumentException {
	if(unitName.equals(V.name)) fitsUnit = V;
	else if(unitName.equals(mV.name)) fitsUnit = mV;
	else if(unitName.equals(uV.name)) fitsUnit = uV;
	else if(unitName.equals(nV.name)) fitsUnit = nV;
	else if(unitName.equals(pV.name)) fitsUnit = pV;
	else if(unitName.equals(janskyPerBeam.name)) fitsUnit = janskyPerBeam;
	else if(unitName.equals(janskyPerSr.name)) fitsUnit = janskyPerSr;
	else if(unitName.equals(janskyPerSquareArcSec.name)) fitsUnit = janskyPerSquareArcSec;
	else throw new IllegalArgumentException("Cannot recognize unit: " + unitName);
	
	displayUnit = fitsUnit;
    }
    
    JFrame frame;
    public void show() {
	System.out.println();
	System.out.println(" Press 'h' or '?' for help with Active Display Options... ");
	System.out.println(); 
	
	frame = new MapFrame();		
	frame.show();
    }
    
    public void setDefaults() {
	display.zoom = defaultZoom;
	display.offset = 0.0;
	display.stretch = 1.0;
	display.inverted = false;
	useScheme = 0;
	
	displayUnit = (DisplayUnit) units.get(0);
	
	display.setDefaults();
    }
    
    public void setResolution(double value) { delta = value; }
    
    double[][] base;
    public void setBase() {
	base = new double[sizeX][sizeY];
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) base[i][j] = clean[i][j];
    }
    
    public void addBase() {
	if(base != null) for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] += base[i][j];
	base = null;
    }
    
    public float[][] getFloatImageClone() {
	float[][] image = new float[sizeX][sizeY];
	
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) image[i][j] = (float) clean[i][j];
	
	return image;
    }
    
    
    public void setImage(float[][] image) { for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] = image[i][j]; }

    public void addImage(float[][] image, double scale) {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] += scale * image[i][j];
    }
    
    public void addImage(float[][] image) { addImage(image, 1.0); }
    
    public void addRegion(double dRA, double dDEC, double radius) {
	regions.add(new Region(dRA, dDEC, radius));
    }
    
    public void fit() {
	Minimizer amoeba = new SourceFit();
	
	double value = amoeba.minimize();	    
	System.out.println();
	for(int k=0; k<regions.size(); k++) ((Region)regions.get(k)).print();
	System.out.println();
	
    }
    
    public void clear() {
	for(int i=0; i<sizeX; i++) {
	    Arrays.fill(clean[i], 0.0);
	    Arrays.fill(weight[i], 0.0);
	    Arrays.fill(count[i], 0.0);
	    Arrays.fill(rms[i], 0.0);
	    Arrays.fill(s2n[i], 0.0);
	    Arrays.fill(flag[i], 1);
	}
    }

    public void reset() {
	clear();
	effectiveResolution = delta;
	pointsPerBeam = 1.0;
    }
    
    public double valueAtOffset(double dRA, double dDEC) {
	return valueAtIndex(fracIndexOfdRA(dRA), fracIndexOfdDEC(dDEC));
    }

    public double valueAtIndex(int i, int j) { return clean[i][j]; }
    
    public double valueAtIndex(double ic, double jc) {	
	double value = 0.0;
	
	int i = (int) Math.floor(ic);
	int j = (int) Math.floor(jc);

	ic -= i;
	jc -= j;

	value += (1.0-ic) * (1.0-jc) * clean[i][j++];
	value += (1.0-ic) *      jc  * clean[i++][j];
	value +=      ic  *      jc  * clean[i][j--];
	value +=      ic  * (1.0-jc) * clean[i][j];
	
	return value;
    }
    
    public void addPointAt(double aRA, double aDEC, double value, double w, double time) {
	addPointAtIndex(fracIndexOfRA(aRA), fracIndexOfDEC(aDEC), value, w, time);
    }
    
    public void addPointAtOffset(double dRA, double dDEC, double value, double w, double time) {
	addPointAtIndex(fracIndexOfdRA(dRA), fracIndexOfdDEC(dDEC), value, w, time);
    }
    
    public void addPointAtIndex(int i, int j, double value, double w, double time) {
	clean[i][j] += w * value;
	weight[i][j] += w;
	count[i][j] += time;
    }
    
    
    public void addPointAtIndex(double ic, double jc, double value, double w, double time) { 
	
	int i = (int) Math.floor(ic);
	int j = (int) Math.floor(jc);
	
	ic -= i;
	jc -= j;

	double c00 = (1.0-ic) * (1.0-jc);
	double c01 = (1.0-ic) *   jc;
	double c10 =    ic    * (1.0-jc);
	double c11 =    ic    *   jc;

	double w00 = w * c00;
	double w01 = w * c01;
	double w10 = w * c10;
	double w11 = w * c11;

	clean[i][j] += w00 * value; count[i][j] += c00 * time; weight[i][j++] += w00;
	clean[i][j] += w01 * value; count[i][j] += c01 * time; weight[i++][j] += w01;
	clean[i][j] += w11 * value; count[i][j] += c11 * time; weight[i][j--] += w11;
	clean[i][j] += w10 * value; count[i][j] += c10 * time; weight[i][j]   += w10;
	
    }
    
    public void addDirect(ReducedData arg, double w) {
	for(int i=0; i<sizeX; i++) for(int j=0;j<sizeY; j++) if(arg.weight[i][j] > 0.0) {
	    clean[i][j] += w * arg.weight[i][j] * arg.clean[i][j];
	    weight[i][j] += w * arg.weight[i][j];
	    count[i][j] += arg.count[i][j];
	    flag[i][j] = 0;
	}
    }
   
    // Normalize assuming weighting was sigma weighting
    public void normalize() {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    if(weight[i][j] <= 0.0) {
		flag[i][j] = 1;
		clean[i][j] = 0.0;
	    } else {
		flag[i][j] = 0;
		clean[i][j] /= weight[i][j];	    
	    }	    
	}

	calc();
    }

    public void normalizeCount() {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) 
	    count[i][j] = weight[i][j] > 0.0 ? count[i][j] / weight[i][j] : 0.0;
    }

    public double[][] clip(double level) {
	calcS2N();
	int[] index = indexOfMax(s2n);

	level *= clean[index[0]][index[1]];
	double[][] clip = new double[sizeX][sizeY];

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
	    if(clean[i][j] < level) {
		clip[i][j] = clean[i][j] - level;
		clean[i][j] = level;
	    }
    
	return clip;
    }
    
    public void clip(double dRAmin, double dDECmin, double dRAmax, double dDECmax) {
	int imin = (int)Math.floor(fracIndexOfdRA(dRAmin));
	int jmin = (int)Math.floor(fracIndexOfdDEC(dDECmin));
	int imax = (int)Math.ceil(fracIndexOfdRA(dRAmax));
	int jmax = (int)Math.ceil(fracIndexOfdDEC(dDECmax));

	clip(imin, jmin, imax, jmax);
    }
    
    public void clip(int imin, int jmin, int imax, int jmax) {
	int oldsizeX = sizeX;
	int oldsizeY = sizeY;

	double[][] oldclean = clean;
	double[][] oldrms = rms;
	double[][] olds2n = s2n;
	double[][] oldweight = weight;
	double[][] oldcount = count;
	int[][] oldflag = flag;

	setSize(imax-imin+1, jmax-jmin+1);
	for(int i=imin; i<=imax; i++) for(int j=jmin; j<=jmax; j++) {
	    if(i>0 && i<oldsizeX && j>0 && j < oldsizeY) {
		clean[i-imin][j-jmin] = oldclean[i][j];
		rms[i-imin][j-jmin] = oldrms[i][j];
		s2n[i-imin][j-jmin] = olds2n[i][j];
		weight[i-imin][j-jmin] = oldweight[i][j];
		count[i-imin][j-jmin] = oldcount[i][j];
		flag[i-imin][j-jmin] = oldflag[i][j];
	    }
	    else flag[i-imin][j-jmin] = 1;
	}
	    
	dRA0 = dRAofIndex(imin);
	dDEC0 = dDECofIndex(jmin);
	i0=0;
	j0=0;
		
	calcS2N();
	min = getMin();
	max = getMax();
	
    }

    public boolean isInsideRegions(double dRA, double dDEC) {
	return isInsideRegions(indexOfdRA(dRA), indexOfdDEC(dDEC));
    }

    public boolean isInsideRegions(int i, int j) {
	boolean inside = false;

	for(int k=0; k<regions.size(); k++) 
	    if( ((Region)regions.get(k)).isInside(i, j) ) { inside = true; break; }
	
	return inside;
    }


    public void regrid(double newdelta) { 
	regrid(newdelta, newdelta < delta ? 1.41 * delta : newdelta);
    }

    public void regrid(double newdelta, double beamFWHM) {
	backup();

	delta = newdelta;
	
	double stretch = backup.delta/delta;
	int dIndex = (int)Math.ceil(stretch);

	sizeX = (int) Math.round(sizeX*stretch);
	sizeY = (int) Math.round(sizeY*stretch);
	
	i0 *= stretch;
	j0 *= stretch;
     
	rangeRA += backup.delta - delta;
	rangeDEC += backup.delta - delta;
	
	setSize(sizeX, sizeY);
	
	clear();
	
	for(int oldi=0; oldi<backup.sizeX; oldi++) {
	    double i = stretch * oldi;
	    if(i>0 && i<sizeX) for(int oldj=0; oldj<backup.sizeY; oldj++) if(backup.flag[oldi][oldj]==0) {
		double j = stretch * oldj;
		double w = backup.weight[oldi][oldj];

		if(j>0 && j<sizeY) for(int di=0, i1=(int)Math.floor(i); di<dIndex; di++, i1++) 
		    for(int dj=0, j1=(int)Math.floor(j); dj<dIndex; dj++,j1++) {
			// Avoid double placement on the new grid...
			if(i1 >= 0 && i1 < sizeX && j1 >= 0 && j1 < sizeY) if(weight[i1][j1] == 0.0) {
			    clean[i1][j1] += w * backup.clean[oldi][oldj];
			    count[i1][j1] += w * backup.count[oldi][oldj];
			    weight[i1][j1] += w;
			}
		    }
	    }
	}

	normalize();
	normalizeCount();
	
	if(beamFWHM > 0.0) convolve(beamFWHM);

	calc();
    }

    
    // Convolves image to the specified beam resolution
    // by a properly chosen convolving beam...
    public void convolveTo(double FWHM) {
	if(effectiveResolution >= FWHM) return;
	convolve(Math.sqrt(FWHM * FWHM - effectiveResolution * effectiveResolution));
    }	

    public double[][] getConvolvedTo(double[][] image, double FWHM) {
	if(effectiveResolution >= FWHM) return image; 
	return getConvolved(image, Math.sqrt((FWHM * FWHM - effectiveResolution * effectiveResolution)));
    }

    public void convolve(double FWHM) {
	clean = getConvolved(clean, FWHM);
	count = getConvolved(count, FWHM);

	effectiveResolution = Math.sqrt(effectiveResolution * effectiveResolution + FWHM * FWHM);
	double sigma = effectiveResolution / 2.35482;
	pointsPerBeam = 2.0 * Math.PI * sigma * sigma / SharcHeader.pixelArea;

	calc();
    }

    public double[][] getConvolved(double[][] image, double FWHM) {
	int size = (int)(FWHM/delta);
	size = 2*size + 1;
	
	return getConvolved(image, getBeam(FWHM, size));
    }   
       
    public void convolve(double[][] beam) {
	clean = getConvolved(clean, beam);
	count = getConvolved(count, beam);
	
	calc();
    }
 
    
    public double[][] getConvolved(double[][] image, double[][] beam) {
	final int ic = (beam.length-1) / 2;
	final int jc = (beam[0].length-1) / 2;

	double[][] convolved = new double[sizeX][sizeY];

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
	    double sumw = 0.0;

	    for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[ib].length; jb++) {
		final int i1 = i - ib + ic;
		final int j1 = j - jb + jc;
		
		final double B = beam[ib][jb];
		
		if(B != 0.0) if(i1 >= 0 && i1 < sizeX) if(j1 >= 0 && j1 < sizeY) if(flag[i1][j1] == 0) {
		    final double w = B * weight[i1][j1]; 
		    convolved[i][j] += w * image[i1][j1];
		    sumw += w;
		}
	    }
	    
	    if(sumw > 0.0) convolved[i][j] /= sumw;
	}
	
	return convolved;
    }
    

    public void FFT() {
	double maxWeight = getMax(weight);
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) clean[i][j] *= weight[i][j] / maxWeight;

	clean = FFT(clean);
	weight = FFT(weight);
	count = FFT(count);

	sizeX = clean.length;
	sizeY = clean[0].length / 2;

	rms = new double[sizeX][sizeY];
	s2n = new double[sizeX][sizeY];

	i0 = clean.length/2.0;
	j0 = 0.0;
	
	delta = 1.0 * Unit.ArcSECOND;
	
	dRA0 = 0.0;
	dDEC0 = 0.0;
	
	flag = new int[sizeX][sizeY];

	altaz = true;

	calc();
    }

    public double[][] FFT(double[][] image) {
	Complex[][] spectrum = ArrayUtil.loadFFT(image);
	ArrayUtil.unsecureFFT(spectrum, true);
	return ArrayUtil.amplitude(spectrum);
    }


    public void calcRMS() {
	if(effectiveResolution > delta) calcBeamRMS();
	else rms = getPointRMS();
    }

    public void calcBeamRMS() {
	if(effectiveResolution <= delta) return;

	double[][] beam = getBeam(effectiveResolution);

	final int ic = (beam.length-1) / 2;
	final int jc = (beam[0].length-1) / 2;
	
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    double beamWeight = 0.0;

	    if(flag[i][j] == 0) {
		for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[ib].length; jb++) {
		    final int i1 = i - ib + ic;
		    final int j1 = j - jb + jc;
		    
		    final double B = beam[ib][jb];

		    if(B != 0.0) if(i1 >= 0 && i1 < sizeX) if(j1 >= 0 && j1 < sizeY) if(flag[i1][j1] == 0)
			beamWeight += B * weight[i1][j1];
		}
		
		// RMS array is normalized to beam rms for easy S/N calculations
		rms[i][j] = beamWeight > 0.0 ? Math.sqrt(1.0 / beamWeight) : 0.0;
	    }
	}
    }        

    
    public void filterAbove(double extendedFWHM) {
	double[][] extended = getConvolved(clean, extendedFWHM);
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] -= extended[i][j];
    }
    
    public void killWavelengthsAbove(double wavelength) {
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(rms[i][j] > 0.0) clean[i][j] /= rms[i][j] * rms[i][j];

	Complex[][] cdata = ArrayUtil.loadFFT(clean);
	ArrayUtil.unsecureFFT(cdata, true);
	
	int nx = cdata.length;
	int ny = cdata[0].length;

	double fMaxX = nx * delta / wavelength;
	double fMaxY = ny * delta / wavelength;

	for(int fx=0; fx<fMaxX; fx++) for(int fy=0; fy<fMaxY; fy++) {
	    double wx = (double)fx/fMaxX;
	    double wy = (double)fy/fMaxY;

	    if(wx*wx + wy*wy < 1.0) if(!(fx == 0 && fy ==0)) {
		cdata[fx][fy].zero();
		if(fx > 0) cdata[nx-fx][fy].zero();
		if(fy > 0) cdata[fx][ny-fy].zero();
		if(fx > 0 && fy > 0) cdata[nx-fx][ny-fy].zero();
	    }
	}

	ArrayUtil.unsecureFFT(cdata, false);
	ArrayUtil.unloadFFT(cdata, clean);

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(rms[i][j] > 0.0) clean[i][j] *= rms[i][j] * rms[i][j];
	
    }

    // Get the point RMS image for writing.
    public double[][] getPointRMS() {
	double[][] pointRMS = new double[sizeX][sizeY];
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) pointRMS[i][j] = Math.sqrt(1.0 / weight[i][j]);
	return pointRMS;
    }

    public double[][] getPointWeight(double[][] pointRMS) {
	double[][] pointWeight = new double[sizeX][sizeY];
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) pointWeight[i][j] = 1.0 / (pointRMS[i][j] * pointRMS[i][j]);
	return pointWeight;
    }

    public double[][] getBeamWeight() {
	calcBeamRMS();
	double[][] beamWeight = new double[sizeX][sizeY];
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) beamWeight[i][j] = 1.0 / (rms[i][j] * rms[i][j]);
	return beamWeight;
    }

    public void beamWeight() {
	calcBeamRMS();
	weight = getBeamWeight();
    }

    public int countPoints() {
	int points = 0;
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) points++;
	return points;
    }
    
    public double countBeams() { return getArea() / beamArea; }

    public double getArea() { return countPoints() * delta * delta; }
    
    public double[][] getBeam(double FWHM) {
	int size = 2 * (int)Math.ceil(FWHM/delta) + 1;
	return getBeam(FWHM, size);
    }	

    public double[][] getBeam(double FWHM, int size) {
	final double[][] beam = new double[size][size];
	final double sigma = FWHM / 2.35482;
	final double sigma2 = sigma * sigma;
	final double center = (size-1) / 2.0;

	for(int i=0; i<size; i++) for(int j=0; j<size; j++) {
	    double dx = delta * (i - center);
	    double dy = delta * (j - center);
	    
	    beam[i][j] = Math.exp(-(dx*dx+dy*dy)/(2.0*sigma2));
	}
	return beam;
    }
    
    private double[][] getWeightingBeam(double FWHM) { 
	int size = 2 * (int)Math.ceil(FWHM/delta) + 1;
	return getWeightingBeam(FWHM, size);
    }	

    private double[][] getWeightingBeam(double FWHM, int size) {
	double[][] beam = getBeam(FWHM, size);
	for(int i=0; i<size; i++) for(int j=0; j<size; j++) beam[i][j] *= beam[i][j];
	return beam;
    }

    public void clean() {
	double FWHM = Math.sqrt(beamFWHM*beamFWHM + effectiveResolution*effectiveResolution);
	clean(FWHM, 0.1, 0.5*FWHM);
    }

    public void clean(double FWHM, double gain, double replacementFWHM) {
	int dIndex = (int) Math.round((3.0*FWHM/delta + 1)/2 );
	if(dIndex < 0) dIndex = 0;
	
	double[][] beam = getBeam(FWHM, 2*dIndex+1);
	
	clean(beam, gain, replacementFWHM); 
    }
    
    public void clean(double[][] beam, double gain, double replacementFWHM) {
	int ic = beam.length / 2;
	int jc = beam[0].length / 2;
	
	double[][] deconvolved = new double[sizeX][sizeY];
	double[][] var = new double[sizeX][sizeY];
	
	// Normalize beam to peak = 1
	double norm = 0.0;
	for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[0].length; jb++) 
	    if(beam[ib][jb] > norm) norm = beam[ib][jb];
	
	for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[0].length; jb++)
	    beam[ib][jb] /= norm;
	
	// Get the beam integral
	double beamInt = 0.0;
	for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[0].length; jb++)
	    beamInt += beam[ib][jb];
	
	int removed = 0;
	
	// Find the peak
	int[] index = indexOfMax(clean);
	
	double ave = s2n[index[0]][index[1]];
	
	do {			    
	    // Get the peak value	    
	    double peak = clean[index[0]][index[1]];            
	    double peakS2N = s2n[index[0]][index[1]];            
	    
	    // Pole is the peak value times the beam integral to conserve flux	    
	    deconvolved[index[0]][index[1]] += gain * peak * beamInt;
	    
	    double pvar = 0.0;
	    
	    for(int ib=0; ib<beam.length; ib++) for(int jb=0; jb<beam[0].length; jb++) {
		int i = index[0] + ib - ic;
		int j = index[1] + jb - jc;
		
		if(i >= 0 && i < sizeX && j >= 0 && j < sizeY) if(flag[i][j] == 0) {
		    // Remove the beam from the clean
		    clean[i][j] -= gain * beam[ib][jb] * peak;
		    // ..and from s2n
		    s2n[i][j] -= gain * beam[ib][jb] * peakS2N;
		    // The variance of the pole increases by the removed variance
		    pvar += beam[ib][jb] * rms[i][j] * rms[i][j];
		}
	    }
	    
	    var[index[0]][index[1]] += gain * pvar;
	    
	    removed++;
	    
	    DecimalFormat df = new DecimalFormat("####0.00");
	    
	    
	    System.out.print("\r\t" + removed + " components removed.");
	    System.out.print(" |S/N| = " + df.format(peakS2N) );
	    System.out.print(" ave = " + df.format(ave) + "   ");
	    System.out.flush();
	    
	    
	    index = indexOfMax(clean);
	    
	    ave *= Math.exp(-0.01);
	    ave += 0.01 * peakS2N;
	    
	} while(ave > 1.0);
	
	double[][] residual = new double[sizeX][sizeY];
	
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    if(var[i][j] > 0.0) weight[i][j] += 1.0 / var[i][j];
	    residual[i][j] = clean[i][j];
	    clean[i][j] = deconvolved[i][j];
	}
      
	convolve(replacementFWHM);
	
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] += residual[i][j];
	
	
	System.out.println();
    }
    
    public int[] indexOfMax(double[][] image) {
	int[] index = new int[2];
	
	double peak = 0.0;
       
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
	    if(image[i][j] > peak) {
		peak = image[i][j];
		index[0] = i;
		index[1] = j;
	    }

	return index;
    }
 
    public Vector2D getOffset(int k) {
	Region region = (Region) regions.get(k);
	return new Vector2D(region.dRA, region.dDEC);	    
    }


    public Vector2D getPointing() {
	if(regions.size() != 1) {
	    calcS2N();
	    int[] index = indexOfMax(s2n); 
	    regions = new Vector();
	    regions.add(new Region(dRAofIndex(index[0]), dDECofIndex(index[1]), 7.0*Unit.ArcSECOND));
	}

	fit();

	if(altaz) {
	    Vector2D pointing = getOffset(0);
	    double FAZO = scan[0].FAZO + pointing.x;
	    double FZAO = scan[0].FZAO - pointing.y;

	    return new Vector2D(FAZO, FZAO);
	}

	return new Vector2D(Double.NaN, Double.NaN);
    }

    public void point() {
	if(altaz) {
	    Vector2D pointing = getPointing();

	    DecimalFormat of = new DecimalFormat("0.0");
	    System.out.print("Suggested Fixed Offsets:   ");
	    System.out.println(" FAZO -> " + of.format(pointing.x / Unit.ArcSECOND) + "\t" +
			       " FZAO -> " + of.format(pointing.y / Unit.ArcSECOND));
	    System.out.println();
	}
	
	else System.out.println(" Map not in Alt/Az coordinates. Cannot suggest pointing.");
    }
		  
			
    public void removeGradient() {
	double y[] = new double[sizeX];
	double wy[] = new double[sizeX];
	double x[] = new double[sizeY];
	double wx[] = new double[sizeY];

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) { 
	    boolean inside = isInsideRegions(i, j);
	  
	    if(!inside && flag[i][j] == 0) {
		final double w = weight[i][j];

		y[i] += w * clean[i][j];
		wy[i] += w;

		x[j] += w * clean[i][j];
		wx[j] += w;
	    }
	}
	for(int i=0; i<sizeX; i++) if(wy[i] > 0.0) y[i] /= wy[i];
	for(int j=0; j<sizeY; j++) if(wx[j] > 0.0) x[j] /= wx[j]; 
	
	double[] xparms = Util.lineFit(y, wy);
	double[] yparms = Util.lineFit(x, wx);

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) 
	    clean[i][j] -= xparms[1] * (i-sizeX/2) + yparms[1] * (j-sizeY/2);
    }

    public double level() { 
	double level = getLevel();
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] -= level;
	min -= level;
	max -= level;
	calcS2N();
	return level;
    }

    public void scale(double scalar) {
	double scalar2 = scalar*scalar;
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    clean[i][j] *= scalar;
	    rms[i][j] *= scalar;
	    weight[i][j] /= scalar2;
	}
    }  

    public void rmsScale(double scalar) {
	double scalar2 = scalar*scalar;
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    rms[i][j] *= scalar;
	    weight[i][j] /= scalar2;
	}
    }
   
    public double median(double[][] image) {
	double[] point = new double[countPoints()];
	
	if(point.length == 0) return 0.0;

	for(int i=0, k=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) point[k++] = image[i][j];
	
	Arrays.sort(point);
	int n = point.length;

	return n % 2 == 0 ? (point[n/2-1] + point[n/2]) / 2.0 : point[(n - 1) / 2]; 
    }

    public double robustLevel() {
	double level = median(clean);
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) clean[i][j] -= level;
	min -= level;
	max -= level;
	calcRMS();
	calcS2N();
	return level;
    }

    public double getRobustWeight() { return getRobustWeight(true); }

    public double getRobustWeight(boolean level) {	
	if(level) robustLevel();
	calcS2N();
	
	double[] chi2 = new double[countPoints()];

	if(chi2.length == 0) return 0.0;
	
	for(int i=0, k=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) chi2[k++] = s2n[i][j] * s2n[i][j];
	
	Arrays.sort(chi2);
	int n = chi2.length;
     
	// median(x^2) = 0.454936 * sigma 
	return 0.454936 / (n % 2 == 0 ? (chi2[n/2-1] + chi2[n/2]) / 2.0 : chi2[(n - 1) / 2]);	
    }

    public void clip() {
	min = getMin();

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    map[i][j] -= display.level - display.offset * display.stretch;
	    if(map[i][j] < 0.0) map[i][j] = 0.0;
	}
	
	display.level = display.offset * display.stretch;
	display.offset = 0.0;
	min = getMin();
	max = getMax();
    }

    public double getLevel() {
	double outflux = 0.0, sumw = 0.0;
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++)
	    if(flag[i][j] == 0) if(!isInsideRegions(i, j)) { 
		outflux += weight[i][j] * clean[i][j];
		sumw += weight[i][j];
	    }

	return sumw > 0.0 ? outflux / sumw : 0.0;
    }

    public DataPoint getFlux(int k) {
	double flux = 0.0, var = 0.0;
	
	double norm = SharcHeader.pixelArea / (delta*delta) * Jy;
	double level = getLevel();

	Region region = (Region) regions.get(k);

	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++)
	    if(region.isInside(i, j)) if(flag[i][j] == 0) {
		flux += clean[i][j] - level;
		var += 1.0 / weight[i][j];
	    }
	
	return new DataPoint(flux / norm, Math.sqrt(var) / norm);
    }

    public PointingData getPointingData() {

	PointingData pointing = new PointingData();
	Vector2D fixedOffset = getPointing();

	Region region = (Region) regions.get(0);	

	pointing.FAZO.value = fixedOffset.x;
	pointing.FZAO.value = fixedOffset.y;
	pointing.FAZO.rms = region.dRAerror;
	pointing.FZAO.rms = region.dDECerror;
	
	pointing.FWHM.value = region.radius;
	pointing.FWHM.rms = region.dradius;
	
	double sigma = 8.0 * Unit.ArcSECOND / 2.35;
	double toJyPerBeam = 2.0 * Math.PI * sigma / SharcHeader.pixelSize / Jy;

	pointing.peak.value = region.peak * toJyPerBeam;
	pointing.peak.rms = region.dpeak * toJyPerBeam;

	pointing.max.value = region.max * toJyPerBeam;
	pointing.max.rms = region.dmax * toJyPerBeam;

	pointing.flux = getFlux(0);

	double origRadius = region.radius;
	region.radius += 10.0 * Unit.ArcSECOND;
	pointing.extFlux = getFlux(0);
	region.radius = origRadius;

	return pointing;
    }
	

    public void getFluxes() {	
	System.out.println();

	DecimalFormat df = new DecimalFormat("0.00E0");
	for(int i=0; i<regions.size(); i++) {
	    DataPoint flux = getFlux(i);

	    System.out.print("  " + ( (Region)regions.get(i)).id + "\t");
	    System.out.print("Flux = " + df.format(1e6 * flux.value * Jy));
	    System.out.print(" +- " + df.format(1e6 * flux.rms * Jy) + " uV/detector");
	    System.out.print("   [ " + df.format(flux.value));
	    System.out.print(" +- " + df.format(flux.rms) + " Jy ]");
	    System.out.println();
	}

	System.out.println();
    }

    public void autoScale() { getMin(); getMax(); }
		    
    public double getMin() { return getMin(map); }

    public double getMin(double[][] image) { 
	double min=0.0;
	for(int i=0; i<sizeX; i++)
	    for(int j=0; j<sizeY; j++) if(flag[i][j]==0) { min = image[i][j]; break; }

	for(int i=0; i<sizeX; i++)
	    for(int j=0; j<sizeY; j++) if(flag[i][j]==0) 
		if(image[i][j] < min) min = image[i][j];
	return min;
    }

    public double getMax() { return getMax(map); }
	
    public double getMax(double[][] image) {
	double max=0.0;
	for(int i=0; i<sizeX; i++)
	    for(int j=0; j<sizeY; j++) if(flag[i][j]==0) { max = image[i][j]; break; }

	for(int i=0; i<sizeX; i++)
	    for(int j=0; j<sizeY; j++) if(flag[i][j]==0) 
		if(image[i][j] > max) max = image[i][j];
      
	return max;
    }
		
    public void extract(double confidenceLevel) {
	double Q = (1.0-confidenceLevel) / (1.0 + countBeams());
	double FWHM = Math.sqrt(beamFWHM * beamFWHM + effectiveResolution * effectiveResolution);

	// Correct image for point sources
	correctFluxesFor(beamFWHM);

	// Uncorrect the regional correlation flux removal effect.
	// This will be more properly taken care of in this algorithm.
	double regionCorrection = getRegionCorrectionFactor(beamFWHM);
	scale(1.0 / getRegionCorrectionFactor(beamFWHM));

	double minDeviation = ConfidenceCalculator.getSigma(1.0-Q);
	
	System.out.println(" Searching for peaks at " + new DecimalFormat("0.00").format(minDeviation) + " sigma...");
	
	int iMap = header.getIntValue("MAP_TURN");
	int iRegion = header.getIntValue("REG_TURN", -1);

	boolean isFiltered = false;
	double[][] filterBeam = null; 
	double filterScale = 0.0, filterSigma2 = 0.0;
	int ic = 0;

	if(iRegion > 0) {
	    if(iRegion < iMap) isFiltered = true;
	    else if(iRegion == iMap) isFiltered = header.getIntValue("REG_INDX") < header.getIntValue("MAP_INDX");
	}

	if(isFiltered) {
	    double filterFWHM = header.getDoubleValue("REG_SIZE") * Unit.ArcSECOND;
	    filterSigma2 = filterFWHM / 2.35;
	    filterSigma2 *= filterSigma2;
	    filterBeam = getBeam(filterFWHM);
	    ic = (filterBeam.length - 1) / 2;
	    filterScale = regionCorrection - 1.0;
	    System.out.println(" Correcting for Spacial Filtering... ");
	}
	

	System.out.println();

	int[] index = indexOfMax(s2n);
	while(s2n[index[0]][index[1]] > minDeviation) {	    	    
	    Region source = new Region(dRAofIndex(index[0]), dDECofIndex(index[1]), FWHM);
	    source.peak = clean[index[0]][index[1]];
	    source.dpeak = rms[index[0]][index[1]];
	   	    
	    source.flag();

	    regions.add(source);

	    // Correct for filtering.
	    if(isFiltered) {
		for(int ib=0, i=ib-ic; ib < filterBeam.length; ib++,i++) 
		    for(int jb = 0, j=jb-ic; jb < filterBeam.length; jb++, j++)
			if(i >= 0 && i<sizeX && j >= 0 && j < sizeY) 
			    clean[i][j] += filterScale * source.peak * filterBeam[ib][jb];
		source.peak *= 1.0 * filterScale;
	    }

	    // Re-level after source removal.
	    double level = level();

	    // Adjust detections fluxes to level.
	    for(int i=0; i<regions.size(); i++) {
		Region region = (Region) regions.elementAt(i);
		region.peak -= level;
		
		if(region != source) {
		    double dx = region.dRA - source.dRA;
		    double dy = region.dDEC - source.dDEC;
		    double d2 = dx*dx + dy*dy;
		    region.peak += filterScale * source.peak * Math.exp(-0.5 * d2 / filterSigma2);
		}
	    }

	    // Re-calculate the S/N map.
	    calcS2N();
	    index = indexOfMax(s2n);
	}

	if(regions.size() == 0) {
	    System.out.println(" No Detections. " + "\n");	
	    return;
	}

	// Check if there are comparable negatives (should not be!)
	double mostNegative = getMin(s2n);
	if(-mostNegative > minDeviation) {
	    System.out.println(" WARNING! Deepest negative is below inverted search level at " +
			       new DecimalFormat("0.00").format(mostNegative) + " sigma.");
	    System.out.println(" Detections(?) below inverted negative significance will carry \"???\".");
	}
	else System.out.println(" Noise seems well-behaved. No negatives below search significance.");

	System.out.println();

	// Print out region list...
	for(int i=0; i<regions.size(); i++) {
	    Region region = (Region) regions.elementAt(i);
	    if(region.peak / region.dpeak < -mostNegative) region.id += "???";
	    System.out.println(region.toString());
	}

	System.out.println();	    
    }

    private double getAbsoluteCorrectionFactor(double FWHM) {
	double correction = 1.0;
	
	correction *= getRowCorrectionFactor(FWHM);
	correction *= getMUXCorrectionFactor(FWHM);
	correction *= getSkyCorrectionFactor(FWHM);
	correction *= getRegionCorrectionFactor(FWHM);

	return correction;
    }

    private double getRowCorrectionFactor(double FWHM) {
	double C = header.getDoubleValue("ROW_PCOR", 1.0);
	return 1.0 / (1.0 - (C-1.0)/C * FWHM/beamFWHM);
    }

    private double getMUXCorrectionFactor(double FWHM) {
	double C = header.getDoubleValue("MUX_PCOR", 1.0);
	return 1.0 / (1.0 - (C-1.0)/C * FWHM/beamFWHM);
    }

    private double getSkyCorrectionFactor(double FWHM) {
	double C = header.getDoubleValue("SKY_PCOR", 1.0);
	return 1.0 / (1.0 - (C-1.0)/C * FWHM/beamFWHM);
    }    

    private double getRegionCorrectionFactor(double FWHM) {
	if(header.getDoubleValue("REG_PCOR", 1.0) > 1.0) {
	    double regionSize = header.getDoubleValue("REG_SIZE") * Unit.ArcSECOND;
	    return 1.0 / (1.0 - (FWHM*FWHM+effectiveResolution*effectiveResolution)/(regionSize*regionSize));
	}
	return 1.0;
    }

    public double getSourceCorrectionFactorFor(double FWHM) {
	return (Double.isNaN(FWHM) ? 1.0 : getAbsoluteCorrectionFactor(FWHM)) / 
	    (Double.isNaN(fluxCorrectingFWHM) ? 1.0 : getAbsoluteCorrectionFactor(fluxCorrectingFWHM));
    }

    public void correctFluxesFor(double FWHM) {
	if(FWHM == fluxCorrectingFWHM) return;
	System.out.println(" *** Correcting by " + getSourceCorrectionFactorFor(FWHM));
	scale(getSourceCorrectionFactorFor(FWHM));
	fluxCorrectingFWHM = FWHM;
    }

    double aX=0.0, bX=0.0, aY=0.0, bY=0.0;
    double ci, cj;
    int baseOrder = 0;

    public boolean containsNaN() {
	boolean hasNaN = false;
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    if(Double.isNaN(clean[i][j])) hasNaN = true;
	    if(Double.isNaN(weight[i][j])) hasNaN = true;
	}
	return hasNaN;
    }

    protected double[][] createModel() {
	double[][] model= new double[sizeX][sizeY];
	
	// Construct a 2nd order baseline
	for(int i=0; i<sizeX; i++) {
	    double dx = i;
	    double y1 = aX * dx*dx + bX * dx;
	    for(int j=0; j<sizeY; j++) {
		double dy = j;
		double y2 = aY * dx*dx + bY * dx;
		model[i][j] += y1 + y2;
	    }
	}
	
	for(int k=0; k<regions.size(); k++) {	    
	    Region source = (Region)regions.get(k);
	    double sigma2 = source.radius/2.35482;
	    sigma2 *= sigma2;
	    
	    int di = (int)Math.ceil(source.radius/delta);

	    int xmin = indexOfdRA(source.dRA) - di;
	    int xmax = xmin + 2*di;
	    int ymin = indexOfdDEC(source.dDEC) - di;
	    int ymax = ymin + 2*di;

	    if(xmin < 0) xmin = 0;
	    if(xmax >= sizeX-1) xmax = sizeX-1;
	    if(ymin < 0) ymin = 0;
	    if(ymax >= sizeY-1) ymax = sizeY-1;

	    for(int i=xmin; i<=xmax; i++) for(int j=ymin; j<=ymax; j++) if(source.isInside(i,j))
		{
		    double r = source.distanceFrom(i, j);
		    model[i][j] += source.peak * Math.exp(-(r*r)/(2.0*sigma2));
		}
	}
	return model;    
    }
    
    // map is

    public double dRAofIndex(double i) { return dRA0 + (i-i0)*delta; }

    public double getRAofIndex(double i) { return RA + dRAofIndex(i) / cosDEC; }
    
    public double dDECofIndex(double j) { return dDEC0 + (j-j0)*delta; }

    public double getDECofIndex(double i) { return DEC + dDECofIndex(i); }
    
    public int indexOfdRA(double dRA) { return (int) Math.round(fracIndexOfdRA(dRA)); }
    
    public int indexOfdDEC(double dDEC) { return (int) Math.round(fracIndexOfdDEC(dDEC)); }

    public int indexOfRA(double aRA) { return (int) Math.round(fracIndexOfRA(aRA)); }
    
    public int indexOfDEC(double aDEC) { return (int) Math.round(fracIndexOfDEC(aDEC)); }
    
    public double fracIndexOfdRA(double dRA) { return i0 + (dRA-dRA0)/delta; }
    
    public double fracIndexOfdDEC(double dDEC) { return j0 + (dDEC-dDEC0)/delta; }

    public double fracIndexOfRA(double aRA) { return fracIndexOfdRA((aRA - RA)*cosDEC); }
    
    public double fracIndexOfDEC(double aDEC) { return fracIndexOfdDEC(aDEC-DEC); }



    public void readMask(String maskName) {
	try {
	    RandomAccessFile maskFile = new RandomAccessFile(maskName, "r");
	    
	    while(maskFile.getFilePointer() < maskFile.length()) 
		regions.add(new Region(maskFile.readLine()));
	}
	catch(IOException e) {
	    System.out.println(" ERROR parsing mask file: " + maskName);
	    System.out.println(e.getMessage());
	}
    }
	

    public void writeMask() {
	String maskName = Crush.workPath + sourceName + ".mask";
	try {
	    RandomAccessFile maskFile = new RandomAccessFile(maskName, "rw");
	    
	    for(int i=0; i<regions.size(); i++) 
		maskFile.writeBytes(((Region) regions.get(i)).toString() + "\n");
	    
	    maskFile.close();
	}
	catch(IOException e) { 
	    System.out.println(" ERROR writing maskfile " + maskName);
	}
	System.out.println("Written " + maskName);
    }

    public void writeImage(String id) throws HeaderCardException, FitsException, IOException {
	DisplayUnit unity = new DisplayUnit(1.0, "1");
	DisplayUnit second = new DisplayUnit(1.0*Unit.SECOND, "s");

	if(id.equalsIgnoreCase("flux")) writeImage(clean, Crush.workPath + sourceName + ".flux.fits", fitsUnit);
	else if(id.equalsIgnoreCase("rms")) writeImage(getPointRMS(), Crush.workPath + sourceName + ".rms.fits", fitsUnit);
	else if(id.equalsIgnoreCase("time")) writeImage(weight, Crush.workPath + sourceName + ".time.fits", second);	
	else if(id.equalsIgnoreCase("s2n")) writeImage(s2n, Crush.workPath + sourceName + ".s2n.fits", unity);	
	else System.out.println("WARNING! No image with id " + id + ".");
    }

    public void writeImage(double[][] image, String name, DisplayUnit unit) throws HeaderCardException, FitsException, IOException {
	Fits fits = new Fits();	

	fits.addHDU(makeHDU(image, unit.value, unit.name, "Image"));
	editMasterHeader(fits);
	
	BufferedDataOutputStream file = new BufferedDataOutputStream(new FileOutputStream(name));
	
	fits.write(file);	
	System.out.println(" Written Image to " + name);
    }

    public Fits getFits() throws HeaderCardException, FitsException, IOException {
	Fits fits = new Fits();	

	fits.addHDU(makeHDU(clean, fitsUnit.value, fitsUnit.name, "Intensity"));
	fits.addHDU(makeHDU(count, Unit.SECOND, "s", "Integration Time"));
	fits.addHDU(makeHDU(getPointRMS(), fitsUnit.value, fitsUnit.name, "RMS"));
	fits.addHDU(makeHDU(s2n, 1.0, "1", "Signal-to-Noise"));


	for(int i=0; i<scan.length; i++) fits.addHDU(scan[i].getHDU());

	editMasterHeader(fits);
	
	return fits;
    }

    public void write() throws HeaderCardException, FitsException, IOException { write(getFits()); }
    
    public void write(Fits fits) throws HeaderCardException, FitsException, IOException {
	String scanSpecifier = "." + scan[0].descriptor;
	for(int i=1; i<scan.length; i++) scanSpecifier += "." + scan[i].descriptor;

	if(fileName == null) fileName = Crush.workPath + sourceName + scanSpecifier + ".fits";  
	
	BufferedDataOutputStream file = new BufferedDataOutputStream(new FileOutputStream(fileName));
	
	fits.write(file);
	
	System.out.println(" Written " + fileName);
    }


    public ImageHDU makeHDU(double[][] image, double unitValue, String unit, String name) 
	throws HeaderCardException, FitsException {
	
	float[][] fitsImage = new float[sizeY][sizeX];
	for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
	    // Flip image if RA/DEC for graphic, not for Alt/AZ
	    int i1 = altaz ? i : sizeX - 1 - i; 
	    if(flag[i][j] == 0.0) fitsImage[j][i1] = (float) (image[i][j] / unitValue);
	    else fitsImage[j][i1] = Float.NaN;
	}
	ImageHDU hdu = (ImageHDU)Fits.makeHDU(fitsImage);
		    

	hdu.addValue("EXTNAME", name, "The type of data contained in this HDU");
	hdu.addValue("ALTAZ", reducer.altaz, "Specify whether Alt/AZ or RA/Dec coordinates.");

	addCoordinateInfo(hdu);

	hdu.addValue("DATAMIN", Util.fitsDouble(getMin(image) / unitValue), "");
	hdu.addValue("DATAMAX", Util.fitsDouble(getMax(image) / unitValue), "");

	hdu.addValue("BZERO", 0.0, "Zeroing Level of the image data");
	hdu.addValue("BSCALE", 1.0, "Scaling of the image data");
	hdu.addValue("BUNIT", unit, "The image data unit.");

	return hdu;
    }

    public void addCoordinateInfo(BasicHDU hdu) throws HeaderCardException {
	String v = altaz ? "A" : "";
	
	if(altaz) {
	    hdu.addValue("CTYPE1", "ALON-SIN", "Azymuth Coordinate");
	    hdu.addValue("CRPIX1", fracIndexOfdRA(0.0)+1, "Reference grid position");
	    hdu.addValue("CRVAL1", Util.fitsDouble(scan[0].AZ/Unit.DEGREE), "AZ at reference position (deg)");
	    hdu.addValue("CDELT1", Util.fitsDouble(delta/Unit.DEGREE), "Grid spacing (deg)");	
	    hdu.addValue("CROTA1", 0.0, "Axis rotation (deg)");
	
	    hdu.addValue("CTYPE2", "ALAT-SIN", "Elevation Coordinate");
	    hdu.addValue("CRPIX2", fracIndexOfdDEC(0.0)+1, "Reference grid position");
	    hdu.addValue("CRVAL2", Util.fitsDouble(scan[0].EL/Unit.DEGREE), "EL at reference position (deg)");
	    hdu.addValue("CDELT2", Util.fitsDouble(delta/Unit.DEGREE), "Grid spacing (deg)");
	    hdu.addValue("CROTA2", 0.0, "Axis rotation (deg)");
	}

	hdu.addValue("CTYPE1" + v, "RA---SIN", "Right Ascention Coordinate");
	hdu.addValue("CRPIX1" + v, altaz ? fracIndexOfdRA(0.0)+1 : sizeX-fracIndexOfdRA(0.0), "Reference grid position");
	hdu.addValue("CRVAL1" + v, Util.fitsDouble(RA/Unit.DEGREE), "RA at reference position (deg)");	
	hdu.addValue("CDELT1" + v, -Util.fitsDouble(delta/Unit.DEGREE), "Grid spacing (deg)");	
	hdu.addValue("CROTA1" + v, altaz ? -scan[0].PA / Unit.DEGREE : 0.0, "Axis rotation (deg)");
	
	hdu.addValue("CTYPE2" + v, "DEC--SIN", "Declination Coordinate");
	hdu.addValue("CRPIX2" + v, fracIndexOfdDEC(0.0)+1, "Reference grid position");
	hdu.addValue("CRVAL2" + v, Util.fitsDouble(DEC/Unit.DEGREE), "DEC at reference position (deg)");
	hdu.addValue("CDELT2" + v, Util.fitsDouble(delta/Unit.DEGREE), "Grid spacing (deg)");
	hdu.addValue("CROTA2" + v, altaz ? -scan[0].PA / Unit.DEGREE : 0.0, "Axis rotation (deg)");
	    
	hdu.addValue("EQUINOX" + v, coordinateEpoch, "The catalog coordinate epoch");
	hdu.addValue("RADESYS" + v, "FK5", "The Coordinate System Convention.");
    }

    public void getCoordinateInfo(Header header) throws HeaderCardException {
	altaz = header.getBooleanValue("ALTAZ");

	if(altaz) {
	    RA  = header.getFloatValue("CRVAL1A", header.getFloatValue("CRVAL1")) * Unit.DEGREE;
	    DEC = header.getFloatValue("CRVAL2A", header.getFloatValue("CRVAL2")) * Unit.DEGREE;
	    coordinateEpoch = header.getFloatValue("EQUINOXA", header.getFloatValue("EQUINOX"));
	    i0 = header.getDoubleValue("CRPIX1") - 1;
	}

	else {
	    RA  = header.getFloatValue("CRVAL1") * Unit.DEGREE;
	    DEC = header.getFloatValue("CRVAL2") * Unit.DEGREE;
	    coordinateEpoch = header.getFloatValue("EQUINOX");
	    i0 = sizeX - header.getDoubleValue("CRPIX1");
	}
    
	// Get the Index of the reference position

	j0 = header.getDoubleValue("CRPIX2") - 1;

	delta = header.getDoubleValue("CDELT2") * Unit.DEGREE;
    }
    
    public void read(String name) throws HeaderCardException, FitsException, IOException {
	// read the master header first
	fileName = name;
	
	Fits fits;
	
	try { fits = new Fits(new File(name)); }
	catch(Exception e) { fits = new Fits(new File(Crush.workPath + name)); }
	
	BasicHDU[] HDU = fits.read();
	
	header = HDU[0].getHeader();
	
	sourceName = header.getStringValue("OBJECT");
	
	Jy = header.getFloatValue("V2JY", (float)Jy) * Unit.VOLT;	
	
	String correction = header.getStringValue("CORRECTN");
	if(correction == null) correction = "None";
	try { fluxCorrectingFWHM = Double.parseDouble(correction) * Unit.ArcSECOND; }
	catch(NumberFormatException e) { fluxCorrectingFWHM = Double.NaN; }
	    
	creator = header.getStringValue("CREATOR");
       	creatorVersion = header.getStringValue("CRUSHVER");
	
	// Get the offset of the reference from the nominal coordinates
	dRA0 = 0.0;
	dDEC0 = 0.0;
	
	// Read in the option strings
	options = new String[header.getIntValue("ARGS")];
	for(int i=0; i<options.length; i++) options[i] = Util.getLongFitsKey(header, "ARG" + (i+1));
		
	// Get the scan list
	scan = new SharcScan[header.getIntValue("SCANS")];
	for(int i=0; i<scan.length; i++) {
	    scan[i] = new SharcScan();
	    scan[i].descriptor = header.getStringValue("SCAN" + (i+1));
	    if(HDU.length > 4+i) scan[i].read((BinaryTableHDU)HDU[4+i]);
	}
	
	// Get the map size
	float[][] data = (float[][]) HDU[0].getData().getData();
	sizeY = data.length;
	sizeX = data[0].length;
	
	// Get the coordinate system information
	getCoordinateInfo(header);

	// get the beam and calculate derived quantities
	double beamFWHM =  header.getDoubleValue("BEAM", ReductionTool.beamFWHM / Unit.ArcSECOND) * Unit.ArcSECOND;

	init();

	// get the map resolution
	effectiveResolution = header.getDoubleValue("RESOLUTN") * Unit.ArcSECOND;

	calcBeamUnits();

	if(effectiveResolution == 0.0) {
	    effectiveResolution = delta;
	    pointsPerBeam = 1.0;
	}	

	// Adjust the index to the determined pointing
	double di0 = (altaz ? -1 : 1) * header.getDoubleValue("RAP") * Unit.ArcSECOND / delta;
	double dj0 = header.getDoubleValue("DECP") * Unit.ArcSECOND / delta;

	i0 += di0;
	j0 += dj0;

	if(di0 != 0.0 || dj0 != 0.0)
	    System.out.println("Shifting -> (" + 
			       header.getDoubleValue("RAP") * Unit.ArcSECOND / delta +  "," +
			       header.getDoubleValue("DECP") * Unit.ArcSECOND / delta + ")" );

	double[][][] image = new double[3][][];

	setUnit(header.getStringValue("BUNIT"));
	
	for(int n=0; n<3; n++) {
	    double scale = 1.0;

	    if(n == 0 || n == 2) scale = fitsUnit.value;
	    else if(n == 1) scale = Unit.SECOND;

	    Header imageHeader = HDU[n].getHeader();

	    min = imageHeader.getDoubleValue("DATAMIN");
	    max = imageHeader.getDoubleValue("DATAMAX");

	    data = (float[][]) HDU[n].getData().getData();
	    	    	    	    
	    image[n] = new double[sizeX][sizeY];
	    flag = new int[sizeX][sizeY];
	    
	    for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
		int i1 = altaz ? i : sizeX - 1 - i;
		if(!Float.isNaN(data[j][i1]))
		    image[n][i][j] = data[j][i1] * scale;	    
		else flag[i][j] = 1;
	    }
	}
	
	for(int n=4,i=0; n<HDU.length; n++,i++) scan[i].parseHeader(HDU[n].getHeader());
	    
	rangeRA = sizeX * delta;
	rangeDEC = sizeY * delta;

	clean = image[0];
	count = image[1];
	
	// Calculate the derived images 
	rms = image[2];
	weight = getPointWeight(rms);
	if(effectiveResolution > delta) calcBeamRMS();

	// Calculate the signal-to-noise image
	s2n = new double[sizeX][sizeY];
	calcS2N();
       
	map = clean;
	
	maps = new double[3][][];
	maps[0] = clean;
	maps[1] = rms;
	maps[2] = s2n;
	
	getMin();
	getMax();

	info();
    }
    
    
    public void editMasterHeader(Fits fits) throws HeaderCardException, FitsException, IOException {
	
	nom.tam.util.Cursor cursor = fits.getHDU(0).getHeader().iterator();
	
	// Go to the end of the header cards...
	while(cursor.hasNext()) cursor.next();


	cursor.add(new HeaderCard
	    ("CREATOR", creator, "Comprehensive Reduction Utility for SHARC2"));
	cursor.add(new HeaderCard
	    ("CRUSHVER", creatorVersion, "Version Information."));
	cursor.add(new HeaderCard
	    ("DATE", FitsDate.getFitsDateString(), "The date of creation"));
	cursor.add(new HeaderCard
	    ("OBJECT", sourceName, "Source name as it appears in the UIP catalog"));	
	cursor.add(new HeaderCard
	    ("ORIGIN", "CALTECH", "California Institute of Technology"));
	cursor.add(new HeaderCard
	    ("TELESCOP", "CSO", "Caltech Submillimeter Observatory"));
	cursor.add(new HeaderCard
	    ("INSTRUME", "SHARC2", "Submillimeter High Angular Resolution Camera 2"));

	// The data descriptors
	cursor.add(new HeaderCard
	    ("BEAM", beamFWHM / Unit.ArcSECOND, "The instrument FWHM (arcsec) of the beam."));	
	cursor.add(new HeaderCard
	    ("RESOLUTN", effectiveResolution / Unit.ArcSECOND, "The effective FWHM resolution (arcsec) of the map."));	

	

	cursor.add(new HeaderCard
	    ("RAP", 0.0, "The apparent RA pointing offset (arcsec). Edit manually."));
	cursor.add(new HeaderCard
	    ("DECP", 0.0, "The apparent DEC pointing offset (arcsec). Edit manually."));
	
	cursor.add(new HeaderCard
	    ("V2JY", Jy/Unit.VOLT, "The conversion factor from V to Jy/Cassegrain-Pixel (4.77 x 4.93)."));

	cursor.add(new HeaderCard
		   ("CORRECTN", (Double.isNaN(fluxCorrectingFWHM) ? "None" : (fluxCorrectingFWHM / Unit.ArcSECOND) + ""),
		    "Fluxes are corrected for sources of this size (FWHM) in arcsec."));

	cursor.add(new HeaderCard
	    ("ARGS", options.length, "The number of option arguments passed to CRUSH in command line"));
	
	for(int i=0; i<options.length; i++) 
	    Util.addLongFitsKey(cursor, "ARG" + (i+1), options[i], "CRUSH Option");
	
	cursor.add(new HeaderCard
	    ("SCANS", scan.length, "The number of scans represented by the reduced data"));
	
	for(int i=1; i<=scan.length; i++) 
	    Util.addLongFitsKey(cursor, "SCAN" + i, scan[i-1].descriptor, "SHARC2 Scan number.");

	// Copy over the remaining cards...
	if(header != null) {
	    nom.tam.util.Cursor oldCards = header.iterator();
	    oldCards.setKey("SCAN" + header.getIntValue("SCANS"));	    
	    while(oldCards.hasNext()) cursor.add((HeaderCard) oldCards.next());
	}
    } 
    
    public void clearRegions() { regions = new Vector(); }

	
    // Regions
    class Region {
	public Region() { init(); }

	public Region(double offsetRA, double offsetDEC, double r) {
	    init(); dRA = offsetRA; dDEC=offsetDEC; radius = r; 
	}

	public Region(String line) { init(); parse(line); }

	public double getAverage() {
	    int fromi = indexOfdRA(dRA+radius);
	    int toi = indexOfdRA(dRA-radius);
	    int fromj = indexOfdDEC(dDEC+radius);
	    int toj = indexOfdDEC(dDEC-radius);

	    double ave = 0.0;
	    int n = 0;

	    for(int i=fromi; i<toi; i++)
		for(int j=fromj; j<toj; j++)
		    if(isInside(i, j) && flag[i][j] == 0) { ave += map[i][j]; n++; }
		       
	    ave /= n;
	    return ave;
	}

	public void init() { id = "[" + (regions.size() + 1) + "]"; guessPeak(); }

	public double distanceFrom(int i, int j) {
	    double temp = dRAofIndex(i) - dRA;
	    double r = temp*temp;
	    temp = dDECofIndex(j)-dDEC;
	    r += temp*temp;
	    return Math.sqrt(r);
	}
		    
	public boolean isInside(int i, int j) { return distanceFrom(i, j) <= radius; }
    
	public double guessPeak() {
	    int di = (int)Math.round(radius/delta);
	    int i0 = indexOfdRA(dRA);
	    int j0 = indexOfdDEC(dDEC);
	   
	    double level = getLevel();
	    peak = level; 
	    int[] index = new int[2];
	    
	    for(int i=i0-di; i<i0+di; i++) for(int j=j0-di; j<j0+di; j++) 
		if(i > 0 && i < sizeX) if(j > 0 && j < sizeY) {
		    if(isInside(i, j) && map[i][j] > peak && flag[i][j]==0) {
			peak = map[i][j];
			dmax = rms[i][j];
		    }
		}

	    peak -= level;

	    return peak;
	}

	public void flag() {
	    int di = (int)Math.round(radius/delta);
	    int i0 = indexOfdRA(dRA);
	    int j0 = indexOfdDEC(dDEC);
	   	    
	    for(int i=i0-di; i<i0+di; i++) for(int j=j0-di; j<j0+di; j++) 
		if(i > 0 && i < sizeX) if(j > 0 && j < sizeY) if(isInside(i, j)) flag[i][j] = 1;
	}

	public String toString() {
	    DecimalFormat f1 = new DecimalFormat("0.0");
	    DecimalFormat e3 = new DecimalFormat("0.000E0");
	    DecimalFormat e1 = new DecimalFormat("0.0E0");
	    
	    String line = id + "\t" + 
		Util.HMS(12.0*Unit.HOUR/Math.PI*(RA + dRA/Math.cos(DEC))) + "  " +
		Util.DMS(DEC + dDEC) + "  " +
		f1.format(coordinateEpoch) + "  " +
		f1.format(radius/Unit.ArcSECOND) + "\t" + e3.format(peak/displayUnit.value) + " +- " +
		e1.format(dpeak/displayUnit.value) + " " + displayUnit.name;
	    
	    
	    return line;
	}
	

	public void parse(String line) {
	    StringTokenizer tokens = new StringTokenizer(line);
	    
	    id = tokens.nextToken();
	    dRA = (Math.PI/(12.0*Unit.HOUR) * Util.parseTime(tokens.nextToken()) - RA) *
		Math.cos(DEC);
	    dDEC = Util.parseAngle(tokens.nextToken()) - DEC;
	    // Skip the coordinate epoch information for now...
	    tokens.nextToken();
	    radius = Double.parseDouble(tokens.nextToken()) * Unit.ArcSECOND;
	    if(tokens.hasMoreTokens()) peak = Double.parseDouble(tokens.nextToken());

	    String nextArg = tokens.hasMoreTokens() ? tokens.nextToken() : null;
	    if(nextArg != null) {
		if(nextArg.equals("+-")) nextArg = tokens.nextToken();
		try { 
		    dpeak = Double.parseDouble(nextArg);
		    nextArg = tokens.hasMoreTokens() ? tokens.nextToken() : null;
		}
		catch(NumberFormatException e) {}
	    }
	    
	    if(nextArg != null) setUnit(nextArg);
	    else setUnit("Jy/beam");

	    peak *= displayUnit.value;
	    dpeak *= displayUnit.value;
	}

	public void print() {
	    DecimalFormat cf = new DecimalFormat("0.00");
	    DecimalFormat df = new DecimalFormat("0.00E0");
	    
	    System.out.print("  " + id);
	    if(altaz) System.out.print("\tAZO:" + cf.format(dRA/Unit.ArcSECOND) + "s\t" +
				       "ELO:" + cf.format(dDEC/Unit.ArcSECOND) +"s\t");
	    else System.out.print("\tRAO:" + cf.format(dRA/Unit.ArcSECOND) + "s\t" +
				  "DECO:" + cf.format(dDEC/Unit.ArcSECOND) +"s\t");
	    System.out.print("FWHM:" + cf.format(radius/Unit.ArcSECOND) + "s\t" +
			      "Peak:" + df.format(peak/displayUnit.value) + " " + displayUnit.name);
	    System.out.println();
	}

	String id = "unspecified";
	double dRA, dDEC, radius, peak, max;
	double dRAerror, dDECerror, dradius, dpeak, dmax;
    }

    public void usage() {
	System.out.println();
	System.out.println("    Display Operations:");
	System.out.println("\tZ,z\tZoom In,Out");
   	System.out.println("\tB,b\tBrightness Increment,Decrement");
	System.out.println("\tC,c\tContrast Increment,Decrement");
	System.out.println("\tk\tToggle between color schemes");
	System.out.println("\ti\tInvert colors");
	System.out.println("\t<,>\tToggle between Source, RMS, and S/N maps");
	
	System.out.println();
	System.out.println("    Operations for Highlighted Regions:");
	System.out.println("\tmouse\tDragging with left button pressed defines a circular region");
	System.out.println("\tR\tRemove/Forget all circular regions defined so far");
	System.out.println("\tr\tCircle the defined regions (default)");
	System.out.println("\tm\tMask the defined regions by partially opaque disk");
	System.out.println("\tl\tLevel to zero as measured outside the defined regions");
	System.out.println("\tL\tForce zero entries at all points below color scale level (black).");
	System.out.println("\tg\tGradient removal as measured outside defined regions");
	System.out.println("\tf\tfit Gaussians to the defined source regions");
	System.out.println("\tF\tFluxes inside the apertures defined by the regions");
	
	System.out.println();
	System.out.println("    Miscellaneous Operations:");
	System.out.println("\t?\tDisplay image information summary.");
	System.out.println("\tK\tKonvolve to beam size.");
	System.out.println("\tq\tDeconvolve with a standard Gaussian beam");
	System.out.println("\ts\tsmooth. Resample onto a 1 arcsec grid");
	System.out.println("\tT\tFourier Transform.");
	System.out.println("\tp\tPerform a pointing fit.");
	System.out.println("\tE\tExtended Structure filter. May improve point source appearance.");
	System.out.println("\tu\tChange display units.");
	System.out.println("\tU\tUndo.");
	System.out.println("\tw\twrite file.");
	System.out.println("\tW\tWrite regions (mask) file.");
    }


    class MapFrame extends JFrame {  
     
	

	public MapFrame() {
	    super();

	    int zoom = 1;
	    while(zoom*sizeX < 150 || zoom*sizeY < 150) zoom *= 2;
	    
	    defaultZoom = zoom;
	    
	    int xsize = zoom*sizeX + 100;
	    int ysize = zoom*sizeY + 80;
	    
	    setSize(xsize, ysize);
	    
	    display = new MapPanel();	    
	    display.zoom = zoom;
	    display.setScale();


	    Container contentPane = getContentPane();
	    contentPane.add(display, "Center");
	    contentPane.add(display.scale, "East");
	    contentPane.add(display.infobar, "South");
       
	    setTitle(sourceName);
		
	    addWindowListener(new WindowAdapter() {
		public void windowClosing(WindowEvent e) {
		    System.exit(0);
		}
	    });	

	    addKeyListener(new KeyAdapter() {
		    public void keyTyped(KeyEvent evt) {
			if(evt.getKeyChar() == 'Z') { display.zoom *= 2.0; display.repaint(); }
			else if(evt.getKeyChar() == 'z') { display.zoom /= 2.0; display.repaint(); } 
			else if(evt.getKeyChar() == 'B') { display.offset += 0.05; display.repaint(); } 
			else if(evt.getKeyChar() == 'b') { display.offset -= 0.05; display.repaint(); } 
			else if(evt.getKeyChar() == 'C') 
			    { display.stretch /= Math.pow(2.0,0.2); display.repaint(); } 
			else if(evt.getKeyChar() == 'c') 
			    { display.stretch *= Math.pow(2.0,0.2); display.repaint(); }
			else if(evt.getKeyChar() == 'l') { backup(); robustLevel(); display.repaint(); }
			else if(evt.getKeyChar() == 'L') { backup(); clip(); display.repaint(); }
			else if(evt.getKeyChar() == 'k') { 
			    useScheme=(useScheme+1)%colorSchemes.size(); 
			    display.repaint(); 
			} 
			else if(evt.getKeyChar() == 'K') {
			    backup();
			    convolveTo(ReductionTool.beamFWHM); 
			    display.setDefaults();
			    display.repaint(); 
			}
			else if(evt.getKeyChar() == '<') { 
			    chooser--; chooser += maps.length; 
			    chooser %= maps.length; map = maps[chooser];
			    if(maps[chooser] == s2n) calcS2N();
			    display.setDefaults();
			    display.repaint();
			}
			else if(evt.getKeyChar() == '>') { 
			    chooser++; chooser += maps.length;
			    chooser %= maps.length; map = maps[chooser];
			    if(maps[chooser] == s2n) calcS2N();
			    display.setDefaults();
			    display.repaint();
			}
			else if(evt.getKeyChar() == 'd') { setDefaults(); display.repaint(); }
			else if(evt.getKeyChar() == 'i') { 
			    display.inverted = !display.inverted;
			    display.colorScale.invert();
			    display.repaint(); 
			}
			else if(evt.getKeyChar() == '?') { info(); }
			else if(evt.getKeyChar() == 'r') { circled = !circled; display.repaint(); }
			else if(evt.getKeyChar() == 'm') { masked = !masked; display.repaint(); }
			else if(evt.getKeyChar() == 'R') { 
			    regions = new Vector();
			    if(circled || masked) display.repaint();
			}		    
			else if(evt.getKeyChar() == 'f') 
			    { if(regions != null) fit(); circled=true; display.repaint();}
			else if(evt.getKeyChar() == 'F') getFluxes();
			else if(evt.getKeyChar() == 'T') { FFT(); setDefaults(); display.repaint(); }
			else if(evt.getKeyChar() == 'g') { removeGradient(); display.repaint(); } 
			else if(evt.getKeyChar() == 'w') { 
			    try { write(); }
			    catch(Exception e) { e.printStackTrace(); }
			}		    
			else if(evt.getKeyChar() == 'u') {
			    int index = units.indexOf(displayUnit);
			    index++;
			    if(index >= units.size()) index = 0;
			    displayUnit = (DisplayUnit) units.get(index);
			    display.repaint();
			}
			else if(evt.getKeyChar() == 'U') { undo(); display.repaint(); }
			else if(evt.getKeyChar() == 'W') { writeMask(); }
			else if(evt.getKeyChar() == 's') { 
			    backup();
			    if(delta > 1.0*Unit.ArcSECOND) regrid(1.0*Unit.ArcSECOND);
			    display.setDefaults();
			    display.repaint();
			}
			else if(evt.getKeyChar() == 'E') { backup(); filterAbove(60.0*Unit.ArcSECOND); display.setDefaults(); display.repaint(); }
			else if(evt.getKeyChar() == 'p') { point(); display.repaint(); }
			else if(evt.getKeyChar() == 'q') {
			    backup();
			    clean(); 
			    display.setDefaults();
			    display.repaint(); 
			}
			else if(evt.getKeyChar() == 'h') { usage(); }
			
			else if(evt.getKeyChar() == '?') { usage(); }
			
			
		    }		   
		});
	}	
	
    }




    

    // Display with value/coordinate redout
    class MapPanel extends JPanel {
	int height, width, imageHeight, imageWidth;
	double x0, y0, dx, dy;
	Graphics graphics;
	double offsetRA, offsetDEC;
	InfoBar infobar;
	ColorScalePanel colorScale;
	Ruler ruler;
	AxisLabel fluxLabel;
	Box scale;
	boolean inverted = false;
	double zoom=1.0, offset=0.0, stretch=1.0, level=0.0;
	ColorScheme colorScheme;
	
	public MapPanel() {
	    setMinimumSize(new Dimension(100, 100));
	    setSize(200, 100);
	    infobar = new InfoBar();
	    
	    colorScale = new ColorScalePanel(20);
	    colorScale.colorScheme = (ColorScheme) colorSchemes.get(useScheme);
	    ruler = new Ruler();
	    fluxLabel = new AxisLabel(displayUnit.name, AxisLabel.RIGHT);
	    
	    scale = Box.createHorizontalBox();
	    
	    scale.add(colorScale);
	    scale.add(ruler);
	    scale.add(fluxLabel);

	    setDefaults();

	    addMouseListener(new MouseAdapter() {
		    double centerRA, centerDEC;

		    public void mouseEntered(MouseEvent evt) {
			setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
		    }
		    public void mouseExited(MouseEvent evt) {
			setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
		    }
		    public void mousePressed(MouseEvent evt) {			
			if(circled || masked) {
			    centerRA = getRA(evt.getX());
			    centerDEC = getDEC(evt.getY());			
			}
		    }
		    public void mouseReleased(MouseEvent evt) {
			if(circled || masked) if(evt.getButton() == MouseEvent.BUTTON1) {
			    double radius = Math.pow(centerRA-getRA(evt.getX()), 2.0);
			    radius +=  Math.pow(centerDEC-getDEC(evt.getY()), 2.0);
			    radius = Math.sqrt(radius);
			    regions.add(new Region(centerRA, centerDEC, radius));
			    repaint();
			}
		    }

		    public void mouseClicked(MouseEvent evt) {
			if(circled || masked) {
			    if(evt.getButton() == MouseEvent.BUTTON2) 
				regions.add(new Region(centerRA, centerDEC, 6.4*Unit.ArcSECOND));
			    if(evt.getButton() == MouseEvent.BUTTON3) 
				regions.add(new Region(centerRA, centerDEC, 9.0*Unit.ArcSECOND));
			    repaint();
			}
		    }					    
		});

	    addMouseMotionListener(new MouseMotionAdapter() {
		    public void mouseMoved(MouseEvent evt) {
			int i = (int)Math.round(geti(evt.getX()));
			int j = (int)Math.round(getj(evt.getY()));
			
			offsetRA = getRA(evt.getX())/Unit.ArcSECOND;
			offsetDEC = getDEC(evt.getY())/Unit.ArcSECOND;

			if(i>=0 && i<sizeX && j>=0 && j<sizeY) {
			    DecimalFormat of = new DecimalFormat("0.0");
			    DecimalFormat df = new DecimalFormat("0.00E0");
			    
			    if(altaz)  infobar.infotext = "AZO:" + of.format(offsetRA) +
				"  ELO:"+ of.format(offsetDEC) + "  ";
			    else infobar.infotext = "RAO:" + of.format(offsetRA) +
				"  DECO:"+ of.format(offsetDEC) + "  ";
			    
			    if(map == clean) {
				infobar.infotext += "S:" + df.format(map[i][j]/displayUnit.value) + " " + displayUnit.name;
				infobar.infotext += " RMS: " + df.format(rms[i][j]/displayUnit.value) + " " + displayUnit.name;
			    }

			    if(map == s2n) infobar.infotext += "S/N:" + df.format(s2n[i][j]); 
			    
			    if(map == rms ) infobar.infotext += "RMS:" + df.format(rms[i][j]/displayUnit.value) + " " + displayUnit.name; 
			    
			} else infobar.infotext = "";

			infobar.repaint();
		    }
		    
		});

	    
			 
	}
	
	public void setDefaults() {
	    level = getMin();
	    stretch = getMax() - level;
	    if(stretch == 0.0) stretch = 1.0;
	}

	public void setScale() {
	    double minValue = (level - offset * stretch);
	    double maxValue = ((1-offset) * stretch + level);

	    if(map != s2n) {
		minValue /= displayUnit.value;
		maxValue /= displayUnit.value;
	    }

	    // Repaint the ruler
	    ruler.setScale(minValue, maxValue);
	}

	public Dimension getPreferredSize() {
	    return new Dimension(imageWidth, imageHeight);
	}

	public void paintComponent(Graphics g) {
	    super.paintComponent(g);

	    colorScheme = (ColorScheme) colorSchemes.get(useScheme);
	    colorScale.colorScheme = colorScheme;

	    setScale();

	    // Repaint the map itself
	    height=getHeight();
	    width=getWidth();

	    imageWidth = (int)Math.ceil(sizeX*zoom);
	    imageHeight = (int)Math.ceil(sizeY*zoom);

	    dx = delta/rangeRA * imageWidth;
	    dy = delta/rangeDEC * imageHeight;

	    x0 = (width  + imageWidth - dx)/2;
	    y0 = (height + imageHeight - dy)/2;

	    int top = (height - imageHeight)/2;
	    int left = (width - imageWidth)/2;

	    graphics=g.create(left, top, imageWidth, imageHeight);
	    
	    for(int j=0; j<sizeY; j++) for(int i=0; i<sizeX; i++) if(flag[i][j] == 0) drawPoint(i, j); 

	    if(masked) for(int i=0; i<regions.size(); i++) 
		maskRegion((Region)regions.get(i));
	    if(circled) for(int i=0; i<regions.size(); i++) 
		circleRegion((Region)regions.get(i));

	    // Repaint the scales
	    scale.repaint();
	    colorScale.repaint();
	    ruler.repaint();
	    
	    display.fluxLabel.label = map == s2n ? "S/N" : displayUnit.name;
	    display.fluxLabel.repaint();
	}
	    
	
	private void drawPoint(int i, int j) {	 
	    double value = (map[i][j]-level)/stretch+offset;
	    
	    value = inverted ? 1.0-value : value;
	    graphics.setColor(colorScheme.getColor(value));

	    int left = (int) Math.round(imageWidth-(i+1)*dx);
	    int top = (int) Math.round(imageHeight-(j+1)*dy);
	    int w = (int) Math.ceil(dx);
	    int h = (int) Math.ceil(dy);
	    
	    if(altaz) left = imageWidth - w - left;

	    graphics.fillRect(left, top, w, h);
	    
	}

	

	private void maskRegion(Region region) {
	    int x = (int) Math.round(imageWidth - dx * (0.5 + fracIndexOfdRA(region.dRA)));
	    int y = (int) Math.round(imageHeight - dy * (0.5 + fracIndexOfdDEC(region.dDEC)));
	    int a = (int) Math.round(2.0 * region.radius/rangeRA * imageWidth);
	    int b = (int) Math.round(2.0 * region.radius/rangeDEC * imageHeight);
	    
	    if(altaz) x = imageWidth - 1 - x;
	    
	    x-=a/2; y-=b/2;

	    graphics.setColor(new Color(1.0F, 1.0F, 1.0F, 0.5F));
	    graphics.fillOval(x, y, a, b);
	}

	private void circleRegion(Region region) {
	    graphics.setColor(((ColorScheme)colorSchemes.get(useScheme)).getHighlight());

	    int x = (int) Math.round(imageWidth - dx * (0.5 + fracIndexOfdRA(region.dRA)));
	    int y = (int) Math.round(imageHeight - dy * (0.5 + fracIndexOfdDEC(region.dDEC)));
	    int a = (int) Math.round(2.0 * region.radius/rangeRA*imageWidth);
	    int b = (int) Math.round(2.0 * region.radius/rangeDEC*imageHeight);
	    
	    if(altaz) x = imageWidth - 1 - x;

	    x-=a/2; y-=b/2;
	    

	    graphics.drawOval(x, y, a, b);

	    x+=a/2.0;
	    y+=b/2.0;

	    graphics.drawLine(x-5, y, x+5, y);
	    graphics.drawLine(x, y-5, x, y+5);
	}


	public double geti(int x) { 
	    if(altaz) return (1-(x0-x)/imageWidth) * sizeX - 1;
	    return (x0-x)/imageWidth * sizeX; 
	}
	
	public double getj(int y) { return (y0-y)/imageHeight * sizeY; } 

	public double getRA(int x) { return dRAofIndex(geti(x)); }

	public double getDEC(int y) { return dDECofIndex(getj(y)); }
	
	

    }

    class SourceFit extends AmoebaMinimizer {
	int baseparms = 0;

	public SourceFit() {
	    verbose = true;

	    double[] initparm = new double[4*regions.size()+2*baseOrder+1];

	    initparm[baseparms++] = getLevel();

	    if(baseOrder >= 1) {
		initparm[baseparms++] = bX;
		initparm[baseparms++] = bY;
	    }
	    if(baseOrder >= 2) {
		initparm[baseparms++] = aX;
		initparm[baseparms++] = aY;
	    }

	    for(int k=0; k<regions.size(); k++) {
		Region source = (Region)regions.get(k);
		int m = 4*k+baseparms;
		initparm[m] = source.dRA;
		initparm[m+1] = source.dDEC;
		initparm[m+2] = source.radius;
		initparm[m+3] = source.guessPeak();
	    }	    

	    init(initparm);

	    double range = getMax() - getMin();

	    p[1][0] = 0.01 * range;

	    if(baseOrder >= 1) {
		p[2][1] = bX + 0.1 * range/sizeX;
		p[3][2] = bY + 0.1 * range/sizeY;
	    }

	    if(baseOrder >= 2) {
		p[4][3] = aX + 0.1 * range/(sizeX*sizeX);
		p[5][4] = aY + 0.1 * range/(sizeY*sizeY);
	    }

	    for(int k=0; k<regions.size(); k++) {
		int m = 4*k+baseparms;
		p[m+1][m] = initparm[m++] + 3.0 * Unit.ArcSECOND;
		p[m+1][m] = initparm[m++] + 3.0 * Unit.ArcSECOND;
		p[m+1][m] = initparm[m++] + 1.0 * Unit.ArcSECOND;
		p[m+1][m] *= 1.1;		
	    }

	    for(int i=0; i<y.length; i++) y[i] = evaluate(p[i]);
	}

	public double minimize() {
	    double retval = super.minimize();
	    
	    for(int k=0; k<regions.size(); k++) {
		Region source = (Region)regions.get(k);
		int m = 4*k+baseparms;
		
		source.dRAerror = getError(m, 0.1*Unit.ArcSECOND);
		source.dDECerror = getError(m+1, 0.1*Unit.ArcSECOND);
		source.dradius = getError(m+2, 0.1*Unit.ArcSECOND);
		source.dpeak = getError(m+3, 1.0e-3 * source.peak);
	    }

	    return retval;
	}
	
	private double getError(int k, double eps) {
	    double value = p[ilo-1][k];
	    double chi0 = y[ilo-1];
	        
	    p[ilo-1][k] += eps;
	    
	    double dchi = evaluate(p[ilo-1]) - chi0;
	    
	    p[ilo-1][k] = value;
	    
	    return Math.sqrt(eps*eps / dchi);
	}
	    

	public void setParameters(double[] setparm) {
	    if(baseOrder >= 1) {
		bX = setparm[1];
		bY = setparm[2];
	    }
	    if(baseOrder >= 2) {
		bX = setparm[3];
		bY = setparm[4];
	    }
	    for(int k=0; k<regions.size(); k++) {
		Region source = (Region)regions.get(k);
		int m = 4*k+baseparms;
		source.dRA = setparm[m];
		source.dDEC = setparm[m+1];
		source.radius = setparm[m+2];
		source.peak = setparm[m+3];
	    }
	}
	
	public double evaluate(double[] tryparm) {
	    setParameters(tryparm);
	    double[][] model = createModel();
	    
	    double chiSquared = 0.0;

	    int n=0;
	    for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
		double temp = (map[i][j]-model[i][j]-parm[0])/rms[i][j];
		chiSquared += temp*temp;
		n++;
	    }

	    // Put effective constraints on the parameters.
	    // Sources have positive flux and have to be within map region.
	    // Also, diameter must be smaller than map span
	    for(int k=0; k<regions.size(); k++) {
		Region source = (Region)regions.get(k);	       

		if(source.peak <= 0.0) chiSquared *= 100.0;
		if((source.radius > rangeRA/2.0 && source.radius > rangeDEC/2.0) || source.radius < 0.0) chiSquared *= 100.0;
		if(source.dRA < dRAofIndex(0) || source.dRA > dRAofIndex(sizeX-1)) chiSquared *= 100.0;
		if(source.dDEC < dDECofIndex(0) || source.dDEC > dDECofIndex(sizeY-1)) chiSquared *= 100.0;
	    }

	    chiSquared /= n-parm.length; 
	    return chiSquared;
	}		 
	    
    }
    
}
    
	


  
