// (C)2007 Attila Kovacs <attila@submm.caltech.edu>

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



import java.io.*;
import java.text.*;
import java.util.*;

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

public class SkyMap implements Cloneable {
	int sizeX, sizeY;
	
	double[][] signal, weight, count, rms, s2n;
	int[][] flag;
	
	Header header;
	SphericalProjection projection;
	public HorizontalCoordinates horizontal;
	public EquatorialCoordinates equatorial;
	
	int generation = 0;
	double integrationTime = 0.0;
	
	String sourceName;
	double weightFactor = 1.0;

	String[] options;

	double delta;

	double beamFWHM = 18.0 * Unit.sec;
	double beamArea, smoothFWHM, pointsPerBeam = 1.0;    

	double extFilterFWHM = Double.NaN, fluxCorrectingFWHM = Double.NaN;
	double filterBlanking = Double.NaN;
	double clippingS2N = Double.NaN;
	
	Unit V = new Unit("V", Unit.V);
	Unit uV = new Unit("uV", 1e-6*Unit.V);
	Unit uVperBeam = new Unit("uV/beam", Double.NaN);
	Unit janskyPerBeam = new Unit("Jy/beam", Double.NaN);

	double janskyPerMicroV = Double.NaN;

	Unit fitsUnit = uV;

	String fileName;

	// The reference offsets and the corresponding map indexes
	double i0=0.0, j0=0.0;

	String creator = "MiniCRUSH"; 
	String creatorVersion;

	boolean isHorizontal = false;

	Vector<LabocaScan> scans = new Vector<LabocaScan>();
	private Vector2D reuseOffset = new Vector2D();

	Hashtable<String, String> configuration;
	
	public SkyMap() { }

	public SkyMap(int i, int j) { setSize(i, j); }

	public void init() {}


	public Object clone() { 	
		try { 
			SkyMap clone = (SkyMap) super.clone(); 
			clone.reuseOffset = new Vector2D();
			return clone;
		}
		catch(CloneNotSupportedException e) { return null; }
	}

	public SkyMap getSkyMap() {
		SkyMap copy = (SkyMap) clone();

		// Make a copy of the fundamental data
		copy.signal = (double[][]) imageCopy(signal);
		copy.rms = (double[][]) imageCopy(rms);
		copy.weight = (double[][]) imageCopy(weight);
		copy.count = (double[][]) imageCopy(count);
		copy.flag = (int[][]) imageCopy(flag);
		copy.s2n = (double[][]) imageCopy(s2n);
		
		// Make a copy of the beam dependent units as these get recalculated....
		copy.uVperBeam = (Unit) uVperBeam.clone();
		copy.janskyPerBeam = (Unit) janskyPerBeam.clone();
		copy.fitsUnit = uV;
		
		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;
	}
	

	public void calc() {
		calcRMS();
		calcS2N();	
		calcBeamUnits();
	}

	public void calcBeamUnits() {
		double FWHM2 = beamFWHM * beamFWHM + smoothFWHM * smoothFWHM;
		beamArea = 2.0 * Math.PI * FWHM2 / 5.5452;
		uVperBeam.value = uV.value * beamFWHM*beamFWHM / FWHM2;
		janskyPerBeam.value = uVperBeam.value / janskyPerMicroV;
		pointsPerBeam = beamArea / (delta*delta);	
	}

	public void shortInfo() {
		String info = "\n";
		
		info += fileName == null ? "\n" : " Image File: " + fileName + ". ->" + "\n\n";
	
		info +=
			"  [" + sourceName + "]" + "\n" +
			"  Position: " + projection.reference.toString() + "\n" +
			"  Map Size: " + sizeX + " x " + sizeY + " pixels. (" 
			+ Util.f1.format(sizeX * delta / Unit.arcmin) + " x " + Util.f1.format(sizeY * delta / Unit.arcmin) + " arcmin.)" + "\n" +
			"  Grid Spacing: " + Util.f2.format(delta / Unit.arcsec) + " arcsec.\n" +
			"  Projection: " + (isHorizontal ? "Horizontal" : "Equatorial") + " "
			+ projection.getFullName() + " (" + projection.getFitsID() + ")\n"; 
	
		info += "\n";
	
		System.out.println(info);
	}

	public void setUnit(String name) {
		if(name.equalsIgnoreCase("uV/beam")) fitsUnit = uVperBeam;
		else if(name.equalsIgnoreCase("Jy/beam")) fitsUnit = janskyPerBeam;
		else System.err.println("ERROR Can't understand unit " + name);
	}


	public void info() {
		DecimalFormat f1 = new DecimalFormat("0.0");
		DecimalFormat f2 = new DecimalFormat("0.00");
	
		double FWHM = Math.sqrt(beamFWHM * beamFWHM + smoothFWHM * smoothFWHM);

		String info = "\n";

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

		info +=
			"  [" + sourceName + "]" + "\n" +
			"  Equatorial: " + equatorial.toString() + "\n" +
			"  Horizontal: " + horizontal.toString() + "\n" +
			"  Map Size: " + sizeX + " x " + sizeY + " pixels. (" 
			+ f1.format(sizeX * delta / Unit.arcmin) + " x " + f1.format(sizeY * delta / Unit.arcmin) + " arcmin.)" + "\n" +
			"  Grid Spacing: " + f2.format(delta / Unit.arcsec) + " arcsec.\n" +
			"  Instrument Beam FWHM: " + f2.format(beamFWHM / Unit.arcsec) + " arcsec." + "\n" +
			"  Applied Smoothing: " + f2.format(smoothFWHM / Unit.arcsec) + " arcsec." + "\n" +
			"  Image Resolution (FWHM): " + f2.format(FWHM / Unit.arcsec) + " arcsec. (includes smoothing)" + "\n" +
			"  Projection: " + (isHorizontal ? "Horizontal" : "Equatorial") + " " 
			+ projection.getFullName() + " (" + projection.getFitsID() + ")\n" + 
			"  Noise Estimate from: " + (weightFactor == 1.0 ? "data" : "image (" + f2.format(1.0 / weightFactor) + "x data)") + "\n"; 

		info += "\n";

		System.out.println(info);
	}


	public void setSize(int i, int j) {
		sizeX = i;
		sizeY = j;

		signal = new double[sizeX][sizeY];
		weight = new double[sizeX][sizeY];
		count = new double[sizeX][sizeY];
		
		rms = null;
		s2n = null;

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

		calc();
	}


	public void calcS2N() {
		if(s2n == null) s2n = new double[sizeX][sizeY];
		
		calcRMS();
		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) 
			s2n[i][j] = rms[i][j] > 0.0 ? signal[i][j] / rms[i][j] : 0.0;
	}

	public void setResolution(double value) { delta = value; }

	public void setImage(double[][] image) { for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) signal[i][j] = image[i][j]; }

	public void addImage(double[][] image, double scale) {
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) signal[i][j] += scale * image[i][j];
	}

	public void addImage(double[][] image) { addImage(image, 1.0); }

	public void clear() {
		for(int i=0; i<sizeX; i++) {
			Arrays.fill(signal[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 clearElementaries() {
		for(int i=0; i<sizeX; i++) {
			Arrays.fill(signal[i], 0.0);
			Arrays.fill(weight[i], 0.0);
			Arrays.fill(count[i], 0.0);
			Arrays.fill(flag[i], 1);
		}		
	}

	public void reset() {
		clearElementaries();
		smoothFWHM = delta / 1.13;
		extFilterFWHM = Double.NaN;
		pointsPerBeam = 1.0;
		integrationTime = 0.0;
	}

	public double valueAtIndex(int i, int j) { return signal[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) * signal[i][j++];
		value += (1.0-ic) *      jc  * signal[i++][j];
		value +=      ic  *      jc  * signal[i][j--];
		value +=      ic  * (1.0-jc) * signal[i][j];

		return value;
	}
	
	public final void addPointAt(Vector2D mapOffset, final double value, final double g, final double w, final double time) {
		addPointAt(indexOfdX(mapOffset.x), indexOfdY(mapOffset.y), value, g, w, time);
	}
	
	public final void addPointAt(SphericalCoordinates coords, final double value, final double g, final double w, final double time) {
		projection.project(coords, reuseOffset);
		addPointAt(indexOfdX(reuseOffset.x), indexOfdY(reuseOffset.y), value, g, w, time);
	}

	public final void addPointAt(final int i, final int j, final double value, final double g, final double w, final double time) {
		final double wg = w * g;
		signal[i][j] += wg * value;
		weight[i][j] += wg * g;
		count[i][j] += time;
	}
	

	public synchronized void addDirect(SkyMap arg, double w) {
		for(int i=0; i<sizeX; i++) for(int j=0;j<sizeY; j++) if(arg.weight[i][j] > 0.0 && arg.flag[i][j] == 0) {
			signal[i][j] += w * arg.weight[i][j] * arg.signal[i][j];
			weight[i][j] += w * arg.weight[i][j];
			count[i][j] += arg.count[i][j];
			flag[i][j] = 0;
		}
		integrationTime += arg.integrationTime;
	}

	// 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) {
				signal[i][j] = 0.0;
				weight[i][j] = 0.0;
				flag[i][j] = 1;
			} 
			else {
				signal[i][j] /= weight[i][j];	
				flag[i][j] = 0;	    
			}	    
		}

		//calc();
	}

	public void sanitize() {
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(weight[i][j] <= 0.0 || flag[i][j] != 0) {
			weight[i][j] = 0.0;
			flag[i][j] |= 1;
			signal[i][j] = 0.0;
			s2n[i][j] = 0.0;
		}

	}
	
	public void applyCorrection(double filtering, double[][] significance) {		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(weight[i][j] > 0.0) if(significance[i][j] <= clippingS2N) {
			signal[i][j] /= filtering;
			weight[i][j] *= filtering * filtering;
		}		
	}

	// It's important to completely reset clipped points, otherwise they can be used...
	// One possibility for future is to raise flag only, then call sanitize()
	public void rmsClip(double maxRelativeRMS) {
		double maxRMS = maxRelativeRMS * getMin(rms);
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
			if(rms[i][j] > maxRMS) flag[i][j] = 1;
	}

	public void exposureClip(double minRelativeExposure) {
		double minIntTime = minRelativeExposure * getMax(count);
		directExposureClip(minIntTime);
	}
	
	public void directExposureClip(double minIntTime) {
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
			if(count[i][j] < minIntTime) flag[i][j] = 1;
	}

	public void s2nClip(double level) {
		calcS2N();

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
			if(s2n[i][j] < level) flag[i][j] = 1;

	}

	public void fluxClip(double level) {
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0)
			if(signal[i][j] < level) flag[i][j] = 1;

	}


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

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

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

		smoothFWHM = Math.sqrt(smoothFWHM * smoothFWHM + FWHM * FWHM);

		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) {
		signal = getConvolved(signal, 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];

		// Beam fitting: I' = C * sum(wBI) / sum(wB2)
		// I(x) = I -> I' = I -> C = sum(wB2) / sum(wB)
		// I' = sum(wBI)/sum(wB)
		// rms = Math.sqrt(1 / sum(wB))

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

			// ib = i + ic - i1
			final int fromib = Math.min(beam.length-1, i + ic);
			final int fromjb = Math.min(beam[0].length-1, j + jc);
			final int toib = Math.max(0, i + ic - sizeX);
			final int tojb = Math.max(0, j + jc - sizeY);
			
			for(int ib=fromib, i1=i+ic-fromib; ib>toib; ib--,i1++) for(int jb=fromjb, j1=j+jc-fromjb; jb>tojb; jb--,j1++) if(flag[i1][j1] == 0) {
				final double B = beam[ib][jb];
				if(B != 0.0) {
					convolved[i][j] += weight[i1][j1] * B * image[i1][j1];
					sumwB += weight[i1][j1] * Math.abs(B);		    
				}
			}

			if(sumwB > 0.0) convolved[i][j] /= sumwB;
		}

		return convolved;
	}

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

	// See rationale in getConvolved...
	public void calcBeamRMS() {
		
		if(smoothFWHM <= delta) {
			rms = getPointRMS();
			return;
		}
		
		if(rms == null) rms = new double[sizeX][sizeY];
	
		double[][] beam = getBeam(smoothFWHM);

		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 sumwB = 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)
						sumwB += B * weight[i1][j1];
				}
				// RMS array is normalized to beam rms for easy S/N calculations
				rms[i][j] = sumwB > 0.0 ? Math.sqrt(1.0/sumwB) : 0.0;
			}
		}
	}        

	public void filterCorrect(double FWHM) {
		if(!Double.isNaN(fluxCorrectingFWHM)) return;
		
		calcS2N();
		double filterC = getExtFilterCorrectionFactor(FWHM);
		double filterC2 = filterC * filterC;
		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			if(s2n[i][j] < filterBlanking) {
				signal[i][j] *= filterC;
				weight[i][j] /= filterC2;		
			}
		}
		
		fluxCorrectingFWHM = FWHM;
		
		calc();
	}
	
	
	public double getExtFilterCorrectionFactor(double FWHM) {
		double effectiveFilterFWHM2 = FWHM*FWHM + extFilterFWHM*extFilterFWHM;
		double effectiveFWHM2 = FWHM*FWHM + smoothFWHM*smoothFWHM;
		if(Double.isNaN(extFilterFWHM)) return 1.0;
		return 1.0 / (1.0 - effectiveFWHM2/effectiveFilterFWHM2);
	}
	
	public void filterAbove(double extendedFWHM) { filterAbove(extendedFWHM, Double.NaN); }


	public void filterAbove(double extendedFWHM, double blankingValue) {
		calcS2N();
		
		// Scale weights for filtering in the white noise assumption/
		double imageFWHM2 = beamFWHM * beamFWHM + smoothFWHM * smoothFWHM;
		double weightScale = 1.0 / (1.0 - imageFWHM2 / (extendedFWHM * extendedFWHM));

		SkyMap extended = getSkyMap();
		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(s2n[i][j] > blankingValue) extended.weight[i][j] = 0.0;
		
		extended.convolveTo(extendedFWHM);

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			signal[i][j] -= extended.signal[i][j];
			weight[i][j] *= weightScale;
		}

		if(Double.isNaN(extFilterFWHM)) extFilterFWHM = extendedFWHM;
		else extFilterFWHM = 1.0/Math.sqrt(1.0/(extFilterFWHM * extFilterFWHM) + 1.0/(extendedFWHM*extendedFWHM));

		filterCorrect(beamFWHM);
		
		calc();
	}


	
	
	//	 8/20/07 Changed to use blanking 
	//         Using Gaussian taper.
	//         Robust re-levelling at the end.
	public void fftFilterAbove(double FWHM, double blankingValue) {
		// sigma_x sigma_w = 1
		// FWHM_x sigma_w = 2.35
		// FWHM_x * 2Pi sigma_f = 2.35
		// sigma_f = 2.35/2Pi * 1.0/FWHM_x
		// delta_f = 1.0/(Nx * delta_x);
		// sigma_nf = sigma_f / delta_x = 2.35 * Nx * delta_x / (2Pi * FWHM_x)
		
		// Try to get an honest estimate of the extended structures using FFT (while blaning bright sources).
		// Then remove it from the original image...
		
		double[][] extended = new double[sizeX][sizeY];
		
		double avew = 0.0;
		int n=0;
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
			if(s2n[i][j] > blankingValue) extended[i][j] = 0.0;
			else {
				double w = 1.0 / rms[i][j] * rms[i][j];
				extended[i][j] = w * signal[i][j];
				avew += w;
				n++;
			}
		}
		if(n > 0) avew /= n;	
		
		Complex[][] cdata = ArrayUtil.loadFFT(extended);
		ArrayUtil.uncheckedFFT(cdata, true);

		int nx = cdata.length;
		int ny = cdata[0].length;

		double sigmax = 2.35 *  nx * delta / (2.0*Math.PI * FWHM);
		double ax = -0.5/(sigmax*sigmax);
		double sigmay = 2.35 *  ny * delta / (2.0*Math.PI * FWHM);
		double ay = -0.5/(sigmay*sigmay);

		for(int fx=1; fx<nx; fx++) for(int fy=1; fy<ny; fy++) {
			Double A = Math.exp(ax*fx*fx + ay*fy*fy);

			cdata[fx][fy].scale(A);
			cdata[nx-fx][fy].scale(A);
			cdata[fx][ny-fy].scale(A);
			cdata[nx-fx][ny-fy].scale(A);
		}
		cdata[0][0].zero();
		cdata[1][0].scale(Math.exp(ax));
		cdata[0][1].scale(Math.exp(ay));
		

		ArrayUtil.uncheckedFFT(cdata, false);
		ArrayUtil.unloadFFT(cdata, extended);
		
		
		// Scale weights for filtering in the white noise assumption/
		double imageFWHM2 = beamFWHM * beamFWHM + smoothFWHM * smoothFWHM;
		double weightScale = 1.0 / (1.0 - imageFWHM2 / (FWHM * FWHM));
		
		if(avew > 0.0) for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			signal[i][j] -= extended[i][j] / avew;
			weight[i][j] *= weightScale;
		}

		if(Double.isNaN(extFilterFWHM)) extFilterFWHM = FWHM;
		else extFilterFWHM = 1.0/Math.sqrt(1.0/(extFilterFWHM * extFilterFWHM) + 1.0/(FWHM*FWHM));

		robustLevel();
		calc();
	}
	

	// 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 = 3 * (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;
	}	

	
	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 double level() { 
		double level = robustLevel();
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(weight[i][j] > 0.0) signal[i][j] -= 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++) {
			signal[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 void weightScale(double scalar) {
		double rmsScalar = 1.0 / Math.sqrt(scalar);
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			weight[i][j] *= scalar;
			rms[i][j] *= rmsScalar;
		}
		calc();
	}


	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(signal);
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(weight[i][j] > 0.0) signal[i][j] -= level;
		calcRMS();
		calcS2N();
		return level;
	}

	// Adjust map weights to apparent map noise...
	public void robustWeight() { robustWeight(true); }

	public void robustWeight(boolean level) {
		double weightCorrection = getRobustWeight(level);
		weightScale(weightCorrection);
		weightFactor *= weightCorrection;
	}

	// Return to calculated weights...
	public void dataWeight() {
		weightScale(1.0/weightFactor);
		weightFactor = 1.0;
	}

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

	public double getRobustWeight(boolean level) {
		if(level) robustLevel(); 

		calcRMS();
		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];

		// median(x^2) = 0.454937 * sigma^2 
		return 0.454937 / ArrayUtil.median(chi2);	
	}

	public double getWeight(boolean level) {
		if(level) robustLevel(); 

		calcRMS();
		calcS2N();

		double chi2 = 0.0;
		int n=0;
		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
			chi2 += s2n[i][j] * s2n[i][j];
			n++;
		}

		return n / chi2;	
	}

	public double getMin() { return getMin(signal); }

	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(signal); }

	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 boolean containsNaN() {
		boolean hasNaN = false;
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			if(Double.isNaN(signal[i][j])) hasNaN = true;
			if(Double.isNaN(weight[i][j])) hasNaN = true;
		}
		return hasNaN;
	}


	// derive the MEM correction if MEM modeling
	
	public void MEM(double lambda, boolean forceNonNegative) {
		double[][] smoothed = getConvolvedTo(signal, 5.0*beamFWHM);
		if(forceNonNegative) for(int x=0; x<sizeX; x++) for(int y=0; y<sizeY; y++) if(smoothed[x][y] < 0.0) smoothed[x][y] = 0.0;
		MEM(smoothed, lambda);
	}
		
	public void MEM(double[][] model, double lambda) {
		for(int x=0; x<sizeX; x++) for(int y=0; y<sizeY; y++) if(flag[x][y] == 0) {
			double sigma = Math.sqrt(1.0/weight[x][y]);
			double memValue = Math.hypot(sigma, signal[x][y]) / Math.hypot(sigma, model[x][y]) ;
			signal[x][y] -= Math.signum(signal[x][y]) * lambda * sigma * Math.log(memValue);
		}
	}
	
	

	public void writeImage(double[][] image, String name, Unit 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 {
		FitsFactory.setUseHierarch(true);
		Fits fits = new Fits();	

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

		editMasterHeader(fits);

		return fits;
	}

	public void write() throws HeaderCardException, FitsException, IOException { write(getFits()); }

	public void write(Fits fits) throws HeaderCardException, FitsException, IOException {
		if(fileName == null) fileName = MiniCRUSH.workPath + File.separator + sourceName + ".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++) {
			if(flag[i][j] == 0.0) fitsImage[j][i] = (float) (image[i][j] / unitValue);
			else fitsImage[j][i] = Float.NaN;
		}
		ImageHDU hdu = (ImageHDU)Fits.makeHDU(fitsImage);


		hdu.addValue("EXTNAME", name, "The type of data contained in this HDU");
		addCoordinateInfo(hdu);

		hdu.addValue("DATAMIN", getMin(image) / unitValue, "");
		hdu.addValue("DATAMAX", 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 = isHorizontal ? "A" : "";

		hdu.addValue("ALTAZ", isHorizontal, "Map orientation.");

		if(isHorizontal) {
			hdu.addValue("CTYPE1", "ALON-" + projection.getFitsID(), "Azymuth Coordinate");
			hdu.addValue("CRPIX1", i0 + 1, "Reference grid position");
			hdu.addValue("CRVAL1", scans.get(0).horizontal.AZ()/Unit.deg, "AZ at reference position (deg)");
			hdu.addValue("CDELT1", delta/Unit.deg, "Grid spacing (deg)");	
			hdu.addValue("CROTA1", 0.0, "Axis rotation (deg)");

			hdu.addValue("CTYPE2", "ALAT-" + projection.getFitsID(), "Elevation Coordinate");
			hdu.addValue("CRPIX2", j0 + 1, "Reference grid position");
			hdu.addValue("CRVAL2", scans.get(0).horizontal.EL()/Unit.deg, "EL at reference position (deg)");
			hdu.addValue("CDELT2", delta/Unit.deg, "Grid spacing (deg)");
			hdu.addValue("CROTA2", 0.0, "Axis rotation (deg)");
		}

		// The RA axis is flipped cf the longitude x!!!
		// Images are written in increasing RA though...
		hdu.addValue("CTYPE1" + v, "RA---" + projection.getFitsID(), "Right Ascention Coordinate");
		hdu.addValue("CRPIX1" + v, i0 + 1, "Reference grid position");
		hdu.addValue("CRVAL1" + v, equatorial.RA()/Unit.deg, "RA at reference position (deg)");	
		hdu.addValue("CDELT1" + v, -delta/Unit.deg, "Grid spacing (deg)"); // this is the longitude offset
		hdu.addValue("CROTA1" + v, isHorizontal ? -scans.get(0).PA / Unit.deg : 0.0, "Axis rotation (deg)");	

		hdu.addValue("CTYPE2" + v, "DEC--" + projection.getFitsID(), "Declination Coordinate");
		hdu.addValue("CRPIX2" + v, j0 + 1, "Reference grid position");
		hdu.addValue("CRVAL2" + v, equatorial.DEC()/Unit.deg, "DEC at reference position (deg)");
		hdu.addValue("CDELT2" + v, delta/Unit.deg, "Grid spacing (deg)");
		hdu.addValue("CROTA2" + v, isHorizontal ? -scans.get(0).PA / Unit.deg : 0.0, "Axis rotation (deg)");

		hdu.addValue("EQUINOX", equatorial.epoch.year, "The catalog coordinate epoch");
		hdu.addValue("RADESYS", "FK5", "The Coordinate System Convention.");

	}

	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();

		double imageFWHM = Math.sqrt(beamFWHM * beamFWHM + smoothFWHM * smoothFWHM);


		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", "APEX", "California Institute of Technology"));
		cursor.add(new HeaderCard("TELESCOP", "APEX", "Caltech Submillimeter Observatory"));
		cursor.add(new HeaderCard("INSTRUME", "LABOCA", "Submillimeter High Angular Resolution Camera 2"));

		// The data descriptors
		cursor.add(new HeaderCard("BEAM", beamFWHM / Unit.arcsec, "The instrument FWHM (arcsec) of the beam."));
		cursor.add(new HeaderCard("BMAJ", imageFWHM, "The beam major axis (radians)"));
		cursor.add(new HeaderCard("BMIN", imageFWHM, "The beam minor axis (radians)."));
		cursor.add(new HeaderCard("BPA", 0.0, "The beam position angle (radians)."));

		cursor.add(new HeaderCard("SMOOTH", smoothFWHM / Unit.arcsec, "The FWHM (arcsec) of the smoothing applied."));	
		cursor.add(new HeaderCard("RESOLUTN", imageFWHM / Unit.arcsec, "The effective image FWHM (arcsec) resolution of the map."));	

		cursor.add(new HeaderCard("V2JY", (1e-6/janskyPerMicroV), "The default conversion from V to Jy."));
		
		cursor.add(new HeaderCard("INTEGRTN", integrationTime / Unit.s, "The total integration time in seconds."));
		
		if(!Double.isNaN(extFilterFWHM)) 
			cursor.add(new HeaderCard("EXTFLTR", extFilterFWHM / Unit.arcsec, "High pass filtering above this underlying FWHM."));
		if(!Double.isNaN(filterBlanking))
			cursor.add(new HeaderCard("FLTRBLNK", filterBlanking, "The S/N blanking of extended structure filters."));
		if(!Double.isNaN(clippingS2N))
			cursor.add(new HeaderCard("CLIPS2N", clippingS2N, "The S/N clipping level used in reduction."));

		
		cursor.add(new HeaderCard("CORRECTN", 
				(Double.isNaN(fluxCorrectingFWHM) ? "None" : (fluxCorrectingFWHM / Unit.arcsec) + ""),
		"Fluxes are corrected for sources of this size (FWHM) in arcsec."));
		
		cursor.add(new HeaderCard("SCANS", scans.size(), "The number of scans represented by the reduced data"));

		for(int i=1; i<=scans.size(); i++) 
			cursor.add(new HeaderCard("SCAN" + i, scans.get(i-1).serialNo, "APEX Scan number."));
		
		// Write the reduction configuration...
		if(configuration != null) {
			Set<String> keySet = configuration.keySet();
			for(String key : keySet) addLongHierarchKey(cursor, key, configuration.get(key));
		}

		// Copy over the remaining cards...
		if(header != null) {
			nom.tam.util.Cursor oldCards = header.iterator();
			oldCards.setKey("SCAN" + header.getIntValue("SCANS"));
			oldCards.next();
			while(oldCards.hasNext()) cursor.add((HeaderCard) oldCards.next());
		}
	}
	
	public void addLongHierarchKey(Cursor cursor, String key, String value) throws FitsException, HeaderCardException {
		addLongHierarchKey(cursor, key, 0, value);
	}

	public void addLongHierarchKey(Cursor cursor, String key, int part, String value) throws FitsException, HeaderCardException {
		if(value.length() == 0) value = "true";
		
		String alt = part > 0 ? "." + part : "";

		int available = 69 - (key.length() + alt.length() + 3);

		if(value.length() < available) cursor.add(new HeaderCard("HIERARCH." + key + alt, value, null));
		else { 
			if(alt.length() == 0) {
				part = 1;
				alt = "." + part;
				available -= 2;
			}

			cursor.add(new HeaderCard("HIERARCH." + key + alt, value.substring(0, available), null));
			addLongHierarchKey(cursor, key, (char)(part+1), value.substring(available)); 
		}
	}
	

	
	
	public void printHeader() {
		header.dumpHeader(System.out);
	}      


	
	public void despike(double significance) {
		double[][] neighbours = {{ 0, 1, 0 }, { 1, 0, 1 }, { 0, 1, 0 }};
		SkyMap diff = getSkyMap();
		diff.convolve(neighbours);
		
		WeightedPoint point = new WeightedPoint();
		WeightedPoint surrounding = new WeightedPoint();
		
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
			point.value = signal[i][j];
			point.weight = weight[i][j];
			surrounding.value = diff.signal[i][j];
			surrounding.weight = diff.weight[i][j];
			point.subtract(surrounding);
			if(point.significance() > significance) flag[i][j] = 1;			
		}	
	}
	
	
    public final double dXofIndex(double i) { return (i-i0)*delta; }
   
    public final double dYofIndex(double j) { return (j-j0)*delta; }
    
    public final int indexOfdX(double dX) { return (int) Math.round(fracIndexOfdX(dX)); }
    
    public final int indexOfdY(double dY) { return (int) Math.round(fracIndexOfdY(dY)); }

    public final double fracIndexOfdX(double dX) { return i0 + dX/delta; }
    
    public final double fracIndexOfdY(double dY) { return j0 + dY/delta; }



}





