package crushtools;
/*******************************************************************************
 * Copyright (c) 2017 Attila Kovacs <attila[AT]sigmyne.com>.
 * All rights reserved. 
 * 
 * This file is part of crushtools.
 * 
 *     crushtools is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * 
 *     crushtools is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 * 
 *     You should have received a copy of the GNU General Public License
 *     along with crushtools.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Contributors:
 *     Attila Kovacs <attila[AT]sigmyne.com> - initial API and implementation
 ******************************************************************************/

import crushtools.plot.AxisLabel;
import crushtools.plot.ColorScalePanel;
import crushtools.plot.InfoPanel;
import crushtools.plot.Ruler;
import crushtools.util.AmoebaMinimizer;
import crushtools.util.Minimizer;

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

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.*;
import java.text.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;

import jnum.Constant;
import jnum.ExtraMath;
import jnum.parallel.ParallelTask;
import jnum.Symbol;
import jnum.Unit;
import jnum.Util;
import jnum.astro.*;
import jnum.data.*;
import jnum.fft.MultiFFT;
import jnum.fits.*;
import jnum.math.*;
import jnum.math.specialfunctions.CumulativeNormalDistribution;
import jnum.plot.*;
import jnum.plot.colorscheme.*;
import jnum.projection.*;
import jnum.text.*;
import jnum.util.PrefixedUnit;



public class SkyMap implements Cloneable {
    
	public static String version = "1.06-3";
	public static String workPath = "./";
	public static boolean isHQRendering = true;

	static {
	    FitsFactory.setLongStringsEnabled(true);
	    FitsFactory.setUseHierarch(true);
	    // Suppress messages about trimming long header card comments...
	    Logger.getLogger(HeaderCard.class.getName()).setLevel(Level.WARNING);
	}
	
	String creator = "crushtools"; 
	String creatorVersion = version;

	public int sizeX, sizeY;
	public double[][] rms, signal, s2n, weight, count;
	public int[][] flag;
	public SphericalProjection projection;

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


	public boolean smoothedWeights = false;
	public String sourceName, origin, telescope, instrument;
	public double weightFactor = 1.0;
	public double integrationTime = Double.NaN;

	public double instrumentFWHM = defaultBeamFWHM;
	public double smoothFWHM, pointsPerBeam = 1.0;
	
	public double extFilterFWHM = Double.NaN;
	public double fluxCorrectingFWHM = Double.NaN;

	public double filterBlanking = Double.NaN;
	public double clippingS2N = Double.NaN;

	public Header header;
	public String[] options;
	


	public double Jy = Double.NaN;
	public Unit nativeUnit = new Unit("counts", 1.0);
	public Unit janskyPerBeam = new Unit("Jy/beam", Double.NaN);
	public Unit milliJanskyPerBeam = new Unit("mJy/beam", Double.NaN);
	public Unit megaJanskyPerSr = new Unit("MJy/sr", Double.NaN);   

	public Unit displayUnit;

	int useScheme = 0;

	Vector<Region> regions = new Vector<Region>();
	Vector<Polygon> polygons = new Vector<Polygon>();
	Vector<ColorScheme> colorSchemes = new Vector<ColorScheme>();
	Vector<Unit> units = new Vector<Unit>();
	Vector<BasicHDU<?>> extraHDUs = new Vector<BasicHDU<?>>();

	public String fileName;

	public double defaultZoom = 1.0;

	boolean circled=true, names=true;
	MapPanel display;

	public boolean verbose = true;

	public static double fwhm2size = Math.sqrt(2.0 * Math.PI) / Constant.sigmasInFWHM;
	
	public Vector<String> history = new Vector<String>();

	public SkyMap() { 
		colorSchemes.add(new Colorful());

		colorSchemes.add(new GreyScale());

		//colorSchemes.add(new NightTime());
		colorSchemes.add(new Glacier());
		colorSchemes.add(new CoolBlue());

		colorSchemes.add(new BBLike());
		colorSchemes.add(new DayTime());
		//colorSchemes.add(new Doppler());

		colorSchemes.add(new Temperature());

		//colorSchemes.add(new Orangy());	
		//colorSchemes.add(new Rainbow());

		units.add(nativeUnit);
		units.add(janskyPerBeam);
		units.add(milliJanskyPerBeam);
		units.add(megaJanskyPerSr);

		displayUnit = nativeUnit;

		Locale.setDefault(Locale.US);

		projection = new SansonFlamsteed();
		projection.setReference(new EquatorialCoordinates());
	}

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

	public SkyMap(String name) throws Exception { this(); read(name); }
	
	public void addHistory(Header header) throws HeaderCardException {
	    Cursor<String, HeaderCard> c = FitsToolkit.endOf(header);
		for(int i=0; i<history.size(); i++) c.add(new HeaderCard("HISTORY", history.get(i), false));
	}
	
	public void addProcessingHistory(String entry) {
		history.add(" " + entry);
	}
	
	public void parseHistory(Header header) {
		history.clear();
		
		Cursor<String, HeaderCard> cursor = header.iterator();
		
		while(cursor.hasNext()) {
			HeaderCard card = cursor.next();
			if(card.getKey().equalsIgnoreCase("HISTORY")) {
				String comment = card.getComment();
				if(comment != null) history.add(comment);
			}
		}

		if(!history.isEmpty()) {
			System.err.println(" Processing History: " + history.size() + " entries found.");
			System.err.println("   --> Last: " + history.get(history.size() - 1));
		}
			
		//for(int i=0; i<history.size(); i++) System.err.println("#  " + history.get(i));
	}
	
	
	public void emptyMap() {
		setSize(100, 100);
	}

	@Override
	public Object clone() { 	
		try { return super.clone(); }
		catch(CloneNotSupportedException e) { return null; }
	}

	@SuppressWarnings("unchecked")
	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);

		// Calculate the dependent quantities;
		copy.s2n = (double[][]) imageCopy(s2n);
		copy.updateBeamQuantities();

		copy.colorSchemes = (Vector<ColorScheme>) colorSchemes.clone();
		copy.units = (Vector<Unit>) units.clone();
		
		if(history != null) {
			copy.history = new Vector<String>(history.size());
			for(int i=0; i<history.size(); i++) copy.history.add(history.get(i));
		}

		if(nativeUnit != null) copy.nativeUnit = nativeUnit.clone();
		if(janskyPerBeam != null) copy.janskyPerBeam = janskyPerBeam.clone();
		if(milliJanskyPerBeam != null) copy.milliJanskyPerBeam = milliJanskyPerBeam.clone();
		if(megaJanskyPerSr != null) copy.megaJanskyPerSr = megaJanskyPerSr.clone();		

		return copy;
	}

	public void empty() {
		backup = null;
		signal = null;
		weight = null;
		count = null;
		rms = null;
		s2n = null;
		flag = null;
		header = null;
		sizeX = -1;
		sizeY = -1;		
	}

	public Object imageCopy(Object image) {

		if(image instanceof double[][]) {
			double[][] orig = (double[][]) image;
			double[][] copy = new double[sizeX][sizeY];
			for(int i=sizeX; --i >= 0; ) System.arraycopy(orig[i], 0, copy[i], 0, orig[i].length);
			return copy;
		}	
		else if(image instanceof int[][]) {
			int[][] orig = (int[][]) image;
			int[][] copy = new int[sizeX][sizeY];
			for(int i=sizeX; --i >= 0; ) System.arraycopy(orig[i], 0, copy[i], 0, orig[i].length);
			return copy;
		}

		return null;
	}

	SkyMap backup = null;
	public void backup(boolean multilevel) {
		backup = getSkyMap();
		if(!multilevel) backup.backup = null;
	}

	public void backup() { backup(false); }    

	public void undo() {
		if(backup != null) {
			SkyMap newBackup = getSkyMap();

			signal = backup.signal;
			rms = backup.rms;
			weight = backup.weight;
			count = backup.count;
			s2n = backup.s2n;
			flag = backup.flag;
			
			history = backup.history;

			updateBeamQuantities();
			backup = newBackup;
		}
	}

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

	public void info() {
		if(fileName == null) {
			System.out.println("\n  No current image.\n"); 
			return;
		}


		DecimalFormat f1 = new DecimalFormat("0.0");
		DecimalFormat f2 = new DecimalFormat("0.00");
		DecimalFormat e3 = new DecimalFormat("0.000E0");

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

		String info = "\n";

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

		String projectionName = projection.getReference() instanceof HorizontalCoordinates ? "Horizontal" : "Equatorial";

		info +=
				"  [" + sourceName + "]" + "\n" +
						"  " + projectionName + ": " + projection.getReference().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(instrumentFWHM / 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" +
						"  Filtering: " + (extFilterFWHM > 0.0 ? f2.format(extFilterFWHM / Unit.arcsec) + " arcsec, " +
								(Double.isNaN(filterBlanking) ? "everywhere." : "when S/N < " + f1.format(filterBlanking)) 
								: "none.") + "\n" +
								"  Projection: " + projection.getFullName() + " (" + projection.getFitsID() + ")\n" +
								"  Noise Estimate Type: " + (weightFactor == 1.0 ? "RAW" : "IMAGE (" + f2.format(Math.sqrt(1.0 / weightFactor)) + "x RAW)") + "\n" + 
								"  Units & Conversion Factors from map flux unit (MFU): \n";

		for(int i=0; i<units.size(); i++) {
			Unit aUnit = units.get(i);
			info += "\t1 " + aUnit.name() + " = " + e3.format(aUnit.value()/nativeUnit.value()) + " MFU\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.arcsec) + " arcsec FWHM sources. ";

		System.out.println(info + "\n");
	}


	public void setJansky(double value) {
		Jy = value;
		double instrumentBeamArea = getInstrumentBeamArea();
		megaJanskyPerSr = new Unit("mJy/sr", 1e6 * Jy * instrumentBeamArea / Unit.sr);
		janskyPerBeam = new Unit("Jy", Jy * instrumentBeamArea / getImageBeamArea());
		milliJanskyPerBeam = new Unit("mJy/beam", 1e-3 * janskyPerBeam.value());
	}

	public double getJansky() {
		return Jy;
	}

	public void updateBeamQuantities() {
		double imageBeamArea = getImageBeamArea();
		
		if(!Double.isNaN(Jy)) {
			janskyPerBeam = new Unit("Jy/beam", Jy * getInstrumentBeamArea() / imageBeamArea);
			milliJanskyPerBeam = new Unit("mJy/beam", 1e-3 * janskyPerBeam.value());
		}

		pointsPerBeam = imageBeamArea / (delta*delta);	
	}


	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 = new double[sizeX][sizeY];
		s2n = new double[sizeX][sizeY];

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


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

	public void setUnit(String unitName) throws IllegalArgumentException {		
		displayUnit = nativeUnit = getUnit(unitName);
		

		if(verbose) System.err.println("Set map units to " + nativeUnit.name());
	}

	public Unit getUnit(String unitName) throws IllegalArgumentException {	
		Unit unit = null;

		if(unitName.equalsIgnoreCase(nativeUnit.name())) unit = nativeUnit;
		else if(unitName.equalsIgnoreCase(janskyPerBeam.name())) unit = janskyPerBeam;
		else if(unitName.equalsIgnoreCase(milliJanskyPerBeam.name())) unit = milliJanskyPerBeam;
		else if(unitName.equalsIgnoreCase(megaJanskyPerSr.name())) unit = megaJanskyPerSr;
		else unit = Unit.get(unitName);

		return unit;
	}

	MapFrame 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.setVisible(true);
	}

	public void setDefaults() {
		display.zoom = defaultZoom;
		display.inverted = false;
		useScheme = 0;

		displayUnit = units.get(0);

		display.setDefaults();
	}

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


	public void addRegion(double dX, double dY, double radius) {
		Region region = new Region(dX, dY, radius);
		regions.add(region);
	}


	public void addRegion(double dX, double dY, double radius, double peak) {
		Region region = new Region(dX, dY, radius);
		region.peak = peak;
		regions.add(region);
	}


	public void fit() {
		if(verbose) System.err.println("Fitting " + regions.size() + " source(s).");

		Minimizer amoeba = new SourceFit();
		amoeba.minimize();	    

		System.out.println();


		SphericalCoordinates coords = projection.getReference();
		CoordinateSystem offsetSystem = coords.getLocalCoordinateSystem();


		System.out.println(" ID         " + offsetSystem.get(0).getShortLabel() + " (\")\t" +  offsetSystem.get(1).getShortLabel() + " (\")\t" +
				"FWHM (\")\t" + "Peak (" + displayUnit.name() + ")");

		System.out.println(" ============================================================================");

		for(int k=0; k<regions.size(); k++) {
			(regions.get(k)).print();
		}

		System.out.println(" ----------------------------------------------------------------------------");

		System.out.println();

	}

	public void clear() {
		//if(verbose) System.err.println("Clearing map data.");

		for(int i=sizeX; --i >= 0; ) {
			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);
		}
		
		history.clear();
		addProcessingHistory("cleared.");
	}



	public double valueAtOffset(double dX, double dY) {
		return valueAtIndex(signal, dX, dY);
	}

	public double valueAtOffset(double[][] image, double dX, double dY) {
		return valueAtIndex(image, fracIndexOfdX(dX), fracIndexOfdY(dY));
	}

	public double valueAtIndex(int i, int j) { return signal[i][j]; }

	public double valueAtIndex(double ic, double jc) {
		return valueAtIndex(signal, ic, jc);
	}

	private double[] ax, ay;
	private AtomicBoolean interpolating = new AtomicBoolean(false);
	// Performs a bicubic spline interpolation...
	public double valueAtIndex(double[][] data, double ic, double jc) {

		final int i0 = (int)Math.floor(ic-1.0);
		final int j0 = (int)Math.floor(jc-1.0);

		final int fromi = Math.max(0, i0);
		final int toi = Math.min(sizeX, i0+4);

		final int fromj = Math.max(0, j0);
		final int toj = Math.min(sizeY, j0+4);


		// Check to make sure the coefficients aren't being used by another call...
		// Get an exclusive lock if possible...
		if(!interpolating.compareAndSet(false, true)) 
			throw new ConcurrentModificationException("concurrent interpolation conflict.");

		if(ax == null) ax = new double[4];
		if(ay == null) ay = new double[4];

		// Calculate the spline coefficients....
		for(int i=fromi; i<toi; i++) {
			final double dx = Math.abs(i - ic);
			ax[i-i0] = dx > 1 ? 
					((-0.5 * dx + 2.5) * dx - 4.0) * dx + 2.0 : (1.5 * dx - 2.5) * dx * dx + 1;
		}

		for(int j=fromj; j<toj; j++) {
			final double dy = Math.abs(j - jc);
			ay[j-j0] = dy > 1 ? 
					((-0.5 * dy + 2.5) * dy - 4.0) * dy + 2.0 : (1.5 * dy - 2.5) * dy * dy + 1;
		}

		// Do the spline convolution...
		double sum = 0.0, sumw = 0.0;
		for(int i=fromi; i<toi; i++) for(int j=fromj; j<toj; j++) if(flag[i][j] == 0) {
			final double w = ax[i-i0]*ay[j-j0];
			sum += w * data[i][j];
			sumw += w;
		}

		// Release the lock on the interpolation. Others may call it now...
		interpolating.set(false);

		return sum / sumw;
	}




	public final void addPointAtOffset(final double dX, final double dY, final double value, double w, final double time) {
		addPointAtIndex(indexOfdX(dY), indexOfdY(dY), value, w, time);
	}

	public final void addPointAtIndex(final double i, final double j, final double value, final double w, final double time) {
		addPointAtIndex(Math.round(i), Math.round(j), value, w, time);
	}

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


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



	// Normalize assuming weighting was sigma weighting
	public void normalize() {
		if(verbose) System.err.println("Normalizing.");

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			if(weight[i][j] <= 0.0) {
				flag[i][j] = 1;
				signal[i][j] = 0.0;
				weight[i][j] = 0.0;
				count[i][j] = 0.0;
			} else {
				flag[i][j] = 0;
				signal[i][j] /= weight[i][j];	    
			}	    
		}

		calc();
	}


	public void sanitize() {
		//if(verbose) System.err.println("Sanitizing.");

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(weight[i][j] <= 0.0 || flag[i][j] != 0 || Double.isNaN(signal[i][j])) {
			weight[i][j] = 0.0;
			flag[i][j] |= 1;
			signal[i][j] = 0.0;
			s2n[i][j] = 0.0;
		}
	}


	// 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) {
		if(verbose) System.err.println("Noise clipping at " + maxRelativeRMS + "x min noise.");

		final double maxRMS = maxRelativeRMS * getMin(rms);
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(rms[i][j] > maxRMS) flag[i][j] = 1;
		
		addProcessingHistory("noise flagged above " + maxRelativeRMS + "x min rms.");
	}

	public void exposureClip(double minRelativeExposure) {
		if(verbose) System.err.println("Exposure clipping at " + minRelativeExposure + "x max exposure.");

		final double minIntTime = minRelativeExposure * getMax(count);
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(count[i][j] < minIntTime) flag[i][j] = 1;
		
		addProcessingHistory("exposure flagged below " + minRelativeExposure + "x max exp.");
	}

	public void s2nClip(final double level) {
		if(verbose) System.err.println("S/N clipping at " + level + "-sigma.");

		calcS2N();

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

		clippingS2N = level;
		
		addProcessingHistory("S/N flagged below " + level + ".");
	}

	public void fluxClip(final double level) {
		if(verbose) System.err.println("Flux clipping at " + Util.s3.format(level/displayUnit.value()) + " " + displayUnit.name());

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(signal[i][j] < level) flag[i][j] = 1;
		
		addProcessingHistory("signal flagged below " + Util.S3.format(level/displayUnit.value())  + " " + displayUnit.value() + ".");

	}


	public void crop(double dXmin, double dYmin, double dXmax, double dYmax) {		
		if(dXmin > dXmax) { double temp = dXmin; dXmin = dXmax; dXmax=temp; }
		if(dYmin > dYmax) { double temp = dYmin; dYmin = dYmax; dYmax=temp; }

		int imin = (int)Math.floor(fracIndexOfdX(dXmin));
		int jmin = (int)Math.floor(fracIndexOfdY(dYmin));
		int imax = (int)Math.ceil(fracIndexOfdX(dXmax));
		int jmax = (int)Math.ceil(fracIndexOfdY(dYmax));

		crop(imin, jmin, imax, jmax);
	}

	public void crop(int imin, int jmin, int imax, int jmax) {
		if(verbose) System.err.println("Cropping.");

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

		imin = Math.max(imin, 0);
		jmin = Math.max(jmin, 0);
		imax = Math.min(imax, sizeX-1);
		jmax = Math.min(jmax, sizeY-1);

		addProcessingHistory("cropped: " + imin + ":" + imax + ", " + jmin + ":" + jmax);
		
		setSize(imax-imin+1, jmax-jmin+1);

		for(int i=imin, i1=0; i<=imax; i++, i1++) for(int j=jmin, j1=0; j<=jmax; j++, j1++) {
			signal[i1][j1] = oldclean[i][j];
			rms[i1][j1] = oldrms[i][j];
			s2n[i1][j1] = olds2n[i][j];
			weight[i1][j1] = oldweight[i][j];
			count[i1][j1] = oldcount[i][j];
			flag[i1][j1] = oldflag[i][j];
		}

		i0 -= imin;
		j0 -= jmin;
	}


	public void growFlags(double radius, int pattern) {
		if(verbose) System.err.println("Growing flagged areas.");

		addProcessingHistory("grown flagged area by " + Util.S3.format(radius / Unit.arcsec) + " arcsec (0x" + Integer.toHexString(pattern) + ".");
		
		radius /= delta;
		int di = (int)Math.ceil(radius);
		

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if((flag[i][j] & pattern) != 0) {

			final int fromi1 = Math.max(0, i-di);
			final int fromj1 = Math.max(0, j-di);
			final int toi1 = Math.max(sizeX, i+di+1);
			final int toj1 = Math.max(sizeY, j+di+1);
			final int matchPattern = flag[i][j] & pattern;

			for(int i1 = fromi1; i1<toi1; i1++) for(int j1 = fromj1; j1<toj1; j1++) 
				if(ExtraMath.hypot(i-i1, j-j1) <= radius) flag[i1][j1] |= matchPattern;
		}
		
		
	}

	public boolean isInsideRegions(double dX, double dY) {
		return isInsideRegions(indexOfdX(dX), indexOfdY(dY));
	}

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

		for(Region region : regions) if(region.isInside(i, j)) { inside = true; break; }

		return inside;
	}


	public void regrid(double newdelta) {
		double stretch = delta / newdelta;	
		backup();

		i0 *= stretch;
		j0 *= stretch;

		setSize((int)Math.round(stretch * sizeX), (int)Math.round(stretch * sizeY));
		delta = newdelta;

		backup.regridTo(this);

	}

	public void regridTo(SkyMap regrid) {
		if(verbose) System.err.println("Regridding to " + Util.f1.format(regrid.delta/Unit.arcsec) + " arcsec pixelization.");

		// replace pixels with Gaussians whose FWHM is the diagonal of the pixel (i.e. enclosing)...
		double FWHM = delta * Math.sqrt(2.0);

		// Add in the smoothing of the regrid algorithm...
		regrid.smoothFWHM = ExtraMath.hypot(smoothFWHM, FWHM);

		regrid.clear();
		float renorm[][] = new float[regrid.sizeX][regrid.sizeY];

		EquatorialCoordinates coords = new EquatorialCoordinates();
		Vector2D offset = new Vector2D();

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j]==0) {
			offset.set(dXofIndex(i), dYofIndex(j));		
			projection.deproject(offset, coords);
			regrid.projection.project(coords, offset);
			regrid.addGaussianAt(signal[i][j], weight[i][j], count[i][j], offset.x(), offset.y(), FWHM, 2.0, renorm);
		}

		regrid.normalize();

		// recalculate the flags and weights;
		// The new weights should be smoothed approproately.
		double wscale = regrid.smoothFWHM * regrid.smoothFWHM / (smoothFWHM * smoothFWHM);

		for(int i=regrid.sizeX; --i >= 0; ) {
			Arrays.fill(regrid.flag[i], 1);
			for(int j=regrid.sizeY; --j >= 0; ) {
				if(renorm[i][j] > 0.0) regrid.weight[i][j] *= wscale / renorm[i][j];
				else regrid.weight[i][j] = 0.0;
			}
		}

		for(int i=sizeX; --i >= 0; ) {
			final int mini = Math.max(0, regrid.indexOfdX(dXofIndex(i-0.5)));
			final int maxi = Math.min(regrid.sizeX-1, regrid.indexOfdX(dXofIndex(i+0.5)));

			for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {	
				final int minj = Math.max(0, regrid.indexOfdY(dYofIndex(j-0.5)));
				final int maxj = Math.min(regrid.sizeY-1, regrid.indexOfdY(dYofIndex(j+0.5)));

				for(int i1=mini; i1<=maxi; i1++) for(int j1=minj; j1<=maxj; j1++) regrid.flag[i1][j1] = 0;
			}

		}

		regrid.sanitize();
		regrid.calc();
		
		regrid.addProcessingHistory("Regridded to " + Util.F3.format(regrid.delta/Unit.arcsec) + " arcsec resolution.");
	}


	public void addGaussianAt(double flux, double w, double intTime, double dx, double dy, double FWHM, double beamRadius, float[][] renorm) {
		FWHM /= delta;

		final double sigma = FWHM / Constant.sigmasInFWHM;
		final double A = -0.5 / (sigma*sigma);
		final double i0 = indexOfdX(dx);
		final double j0 = indexOfdY(dy);
		final int di = (int)Math.ceil(beamRadius * FWHM);

		final int imin = Math.max(0, (int)Math.floor(i0-di));
		final int imax = Math.min(sizeX-1, (int)Math.ceil(i0+di));
		final int jmin = Math.max(0, (int)Math.floor(j0-di));
		final int jmax = Math.min(sizeY-1, (int)Math.ceil(j0+di));

		for(int i=imin; i<=imax; i++) {
			double di2 = i - i0;
			di2 *= di2;

			for(int j=jmin; j<=jmax; j++) {
				double dj2 = j - j0;
				dj2 *= dj2;

				final double a = Math.exp(A * (di2 + dj2));

				signal[i][j] += a * w * flux;
				weight[i][j] += a * w;
				count[i][j] += a * intTime;
				renorm[i][j] += (float) a;
			}
		}
	}



	// Convolves image to the specified beam resolution
	// by a properly chosen convolving beam...
	public void convolveTo(double FWHM) {	
		if(verbose) System.err.print("Set Smoothing to " + Util.f1.format(FWHM/Unit.arcsec) + " arcsec FWHM: ");

		if(smoothFWHM >= FWHM) {
			if(verbose) System.err.println("Nothing to do.");
			return;
		}

		convolve(Math.sqrt(FWHM * FWHM - smoothFWHM * smoothFWHM), false);
		addProcessingHistory("convolved to " + Util.F3.format(FWHM / Unit.arcsec) + " arcsec FWHM.");
	}	

	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) { convolve(FWHM, true); }

	public void convolve(double FWHM, boolean addHistory) {		
		if(FWHM <= 0.01*delta || Double.isNaN(FWHM)) {
			if(verbose) System.err.println("Already smoothed.");
			return;
		}

		if(verbose) System.err.println("Smoothing by " + Util.f2.format(FWHM/Unit.arcsec) + " arcsec.");


		double[][] image = getConvolved(signal, FWHM);
		for(int i=sizeX; --i >= 0; ) System.arraycopy(image[i], 0, signal[i], 0, sizeY);

		image = getConvolved(count, FWHM);
		for(int i=sizeX; --i >= 0; ) System.arraycopy(image[i], 0, count[i], 0, sizeY);

		image = getSmoothWeight(FWHM);
		for(int i=sizeX; --i >= 0; ) System.arraycopy(image[i], 0, weight[i], 0, sizeY);

		filterBlanking *= getImageFWHM() * getImageFWHM() / smoothFWHM;
		smoothFWHM = Math.sqrt(smoothFWHM * smoothFWHM + FWHM * FWHM);
		filterBlanking *= smoothFWHM / (getImageFWHM() * getImageFWHM()); 

		if(addHistory) addProcessingHistory("convolved to " + Util.F3.format(FWHM / Unit.arcsec) + " arcsec FWHM.");
		
		calc();
	}

	public double[][] getConvolved(double[][] image, double FWHM) {
		int size = (int)Math.ceil(2.0*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(final double[][] image, final double[][] beam) {
		final int ic = (beam.length-1) / 2;
		final int jc = (beam[0].length-1) / 2;

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

		// Beam fitting: I' = C * sum(wBI) / sum(w|B|2)
		// I(x) = I -> I' = I -> C = sum(w|B|2) / sum(w|B|)
		// I' = sum(wBI)/sum(w|B|)
		// rms = Math.sqrt(1 / sum(w|B|))

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) 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 double[][] getSmoothWeight(final double FWHM) {	
		int size = (int)Math.ceil(2.0*FWHM/delta);
		size = 2*size + 1;

		return getSmoothWeight(getBeam(FWHM, size));
	}

	public double[][] getSmoothWeight(final double[][] beam) {
		final int ic = (beam.length-1) / 2;
		final int jc = (beam[0].length-1) / 2;

		final double[][] convolved = new double[sizeX][sizeY];
		final double A = smoothFWHM * fwhm2size / delta;
		final double renorm = 1.0 / (A*A);
		
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) 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) 
				sumwB += weight[i1][j1] * Math.abs(beam[ib][jb]);
			
			convolved[i][j] = sumwB * renorm;
		}

		return convolved;
	}

	public int[] getHorizontalIndexRange() {
		int min = sizeX-1, max = 0;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			if(i < min) min = i;
			if(i > max) max = i;
			break;
		}
		return max > min ? new int[] { min, max } : null;
	}

	public int[] getVerticalIndexRange() {
		int min = sizeY-1, max = 0;
		for(int j=sizeY; --j >= 0; ) for(int i=sizeX; --i >= 0; ) if(flag[i][j] == 0) {
			if(j < min) min = j;
			if(j > max) max = j;
			break;
		}
		return max > min ? new int[] { min, max } : null;
	}

	public void autoCrop() {
		int[] hRange = getHorizontalIndexRange();
		int[] vRange = getVerticalIndexRange();
		if(vRange == null || hRange == null) return;
		
		if(verbose) System.err.print("Auto-cropping. ");
		if(verbose) System.err.println((hRange[1] - hRange[0] + 1) + "x" + (vRange[1] - vRange[0] + 1));
		this.crop(hRange[0], vRange[0], hRange[1], vRange[1]);
	}

	public double[][] getUniformConvolved(double[][] image, double FWHM) {
		int size = (int)Math.abs(FWHM/delta);
		size = 2*size + 1;

		return getUniformConvolved(image, getBeam(FWHM, size));
	}

	public double[][] getUniformConvolved(final double[][] image, final double[][] beam) {
		final int ic = (beam.length-1) / 2;
		final int jc = (beam[0].length-1) / 2;

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

		// Beam fitting: I' = C * sum(wBI) / sum(w|B|2)
		// I(x) = I -> I' = I -> C = sum(w|B|2) / sum(w|B|)
		// I' = sum(wBI)/sum(w|B|)
		// rms = Math.sqrt(1 / sum(w|B|))

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			double sumwB = 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) {
					convolved[i][j] += B * image[i1][j1];
					sumwB += Math.abs(B);		    
				}
			}

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

		return convolved;
	}

	public void FFT() {
		if(verbose) System.err.println("Fourier Transforming...");

		int size = ExtraMath.pow2ceil(Math.max(sizeX, sizeY));

		// Make it square size...
		crop(0, 0, size, size);

		double maxWeight = getMax(weight);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			if(flag[i][j] == 0) signal[i][j] *= weight[i][j] / maxWeight;
			else signal[i][j] = 0.0;
		}

		signal = FFT(signal);
		weight = FFT(weight);
		count = FFT(count);

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

		reorderFFTImage(signal);
		reorderFFTImage(weight);
		reorderFFTImage(count);

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

		i0 = signal.length/2.0-1;
		j0 = 0.0;

		projection.setReference(new SphericalCoordinates(0.0, 0.0));

		delta = 1.0 / (delta * size);

		flag = new int[sizeX][sizeY];


		display.setMap(signal);

		calc();
		
		addProcessingHistory("FFT'd");

		try { writeFFTNoiseProfile(); }
		catch(IOException e) { e.printStackTrace(); }
	}

	public void writeFFTNoiseProfile() throws IOException {
		int n = (int)Math.ceil(ExtraMath.hypot(sizeX/2.0, sizeY));

		double[] profile = new double[n];
		int[] pts = new int[n];

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			int d = (int)Math.round(ExtraMath.hypot(i - i0, j - j0));
			profile[d] += signal[i][j] * signal[i][j];
			pts[d]++;
		}

		for(int i=profile.length; --i >= 0; ) if(pts[i] > 0.0)
			profile[i] = Math.sqrt(profile[i] / pts[i]);

		String fileName = workPath + sourceName + ".fft-profile.dat";
		PrintWriter out = new PrintWriter(new FileOutputStream(fileName));

		double df = Unit.arcsec * delta;

		out.println("# l[\"]\tRMS");

		for(int i=0; i<profile.length; i++) {
			out.println(Util.e4.format(1.0 / (i*df)) + "\t" + Util.e4.format(profile[i]));
		}
		out.close();

		System.err.println(" Written " + fileName);
	}


	// Could speed up if FFT in second coordinate...
	protected void reorderFFTImage(final double[][] image) {
		final int N = sizeX/2;
		final double[] temp = new double[N+1];

		// rescale the constant an nyquist freqs.
		for(int i=sizeX; --i >= 0; ) {
			image[i][0] *= 2.0;
			image[i][sizeY-1] *= 2.0;
		}

		for(int j=sizeY; --j >= 0; ) {
			// rescale the constant an nyquist freqs.
			image[0][j] *= 2.0;
			image[sizeX-1][j] *= 2.0;

			for(int i=0; i<=N; i++) temp[i] = image[i][j];
			for(int i=N+1,k=0; i<sizeX; i++,k++) image[k][j] = image[i][j];
			for(int i=0,k=N-1; i<=N; i++,k++) image[k][j] = temp[i];
		}
	}

	public double[][] FFT(double[][] image) {
		Complex[][] spectrum = loadFFT(image);
		new MultiFFT().complexForward(spectrum);
		return toAmplitude(spectrum);
	}


	public static double[][] toAmplitude(Complex[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		double[][] a = new double[nx][ny];

		for(int i=nx; --i >= 0; ) for(int j=ny; --j >= 0; ) a[i][j] = data[i][j].length();

		// Change from integral to amplitide normalization
		for(int i=nx; --i >= 0; ) {
			a[i][0] /= 2.0;
			a[i][ny-1] /= 2.0;
		}

		for(int j=ny; --j >= 0; ) {
			a[0][j] /= 2.0;
			a[nx-1][j] /= 2.0;
		}

		return a;
	}

	public Vector2D[] getHistogram(double[][] image, double binSize) {
		double min = getMin(image);
		double max = getMax(image);

		int bins = 1 + (int)Math.round(max / binSize) - (int)Math.round(min / binSize);
		Vector2D[] bin = new Vector2D[bins];
		for(int i=bins; --i >= 0; ) bin[i] = new Vector2D(i*binSize, 0.0);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			bin[(int)Math.round(image[i][j] / binSize)].addY(1.0);

		return bin;
	}

	public void calcRMS() {
		rms = getRMS();
	}	      

	public void filterAbove(double extendedFWHM) { filterAbove(extendedFWHM, Double.NaN, true); }

	public void filterAbove(double extendedFWHM, double blankingValue) { filterAbove(extendedFWHM, blankingValue, true); }

	public void filterAbove(double extendedFWHM, double blankingValue, boolean excludeRegions) {
		if(verbose) {
			System.err.println("Filtering above " + (extendedFWHM/Unit.arcsec) + " arcsec FWHM.");
			//if(!Double.isNaN(blankingLevel))
			if(regions.size() > 0) System.err.println("... excluding  " + regions.size() + " masked regions.");
			System.err.println("... It may take a very long time. Have patience!");
		}

		// Undo prior filter corrections if necessary....
		final boolean wasVerbose = verbose;
		final double oldCorrectingFWHM = fluxCorrectingFWHM;

		verbose = false;
		if(!Double.isNaN(fluxCorrectingFWHM)) uncorrect();

		// Scale weights for filtering in the white noise assumption/
		double imageFWHM = getImageFWHM();
		double weightScale = 1.0 / (1.0 - imageFWHM * imageFWHM / (extendedFWHM * extendedFWHM));

		SkyMap extended = getSkyMap();

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			if(s2n[i][j] > blankingValue) extended.weight[i][j] = 0.0;
			else if(isInsideRegions(i,j)) extended.weight[i][j] = 0.0;
		}

		extended.convolveTo(extendedFWHM);
		weightScale(weightScale);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) signal[i][j] -= extended.signal[i][j];

		extFilterFWHM = Double.isNaN(extFilterFWHM) ? extendedFWHM : Math.min(extFilterFWHM, extendedFWHM);
		filterBlanking = blankingValue;

		if(!Double.isNaN(oldCorrectingFWHM)) correct(oldCorrectingFWHM);

		calcS2N();
		
		addProcessingHistory("LSS filtered above " + Util.S3.format(extendedFWHM/Unit.arcsec) + " arcsec FWHM"
			+ (Double.isNaN(blankingValue) ? " above S/N " + blankingValue : "") 
			+ (excludeRegions ? " excl. regions" : "") 
			+ "."
		);
		
		

		verbose = wasVerbose;
	}


	public static Complex[][] loadFFT(double[][] data) {
		Complex[][] cdata = null;
		return loadFFT(data, cdata);
	}

	public static Complex[][] loadFFT(double[][] data, Complex[][] cdata) {
		int nx = data.length;
		int ny = data[0].length;

		int nu = ExtraMath.pow2ceil(nx);
		int nv = ExtraMath.pow2ceil(ny);

		if(cdata == null) cdata = new Complex[nu][nv];
		else if(cdata.length != nu || cdata[0].length != nv) cdata = new Complex[nu][nv];

		for(int i=nu; --i >= 0; ) for(int j=nv; --j >= 0; ) {
			if(cdata[i][j] == null) cdata[i][j] = new Complex();
			cdata[i][j].set((i<nx && j<ny) ? data[i][j] : 0.0, 0.0);
		} 

		return cdata;
	}

	public static void unloadFFT(Complex[][] cdata, double[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		for(int i=nx; --i >= 0; ) for(int j=ny; --j >= 0; ) data[i][j] = cdata[i][j].x();
	}

	public static double[][] amplitude(Complex[][] data) {
		int nx = data.length;
		int ny = data[0].length;

		double[][] a = new double[nx][ny];

		for(int i=nx; --i >= 0; ) for(int j=ny; --j >= 0; ) a[i][j] = data[i][j].length();

		// Change from integral to amplitide normalization
		for(int i=nx; --i >= 0; ) {
			a[i][0] /= 2.0;
			a[i][ny-1] /= 2.0;
		}

		for(int j=ny; --j >= 0; ) {
			a[0][j] /= 2.0;
			a[nx-1][j] /= 2.0;
		}

		return a;
	}

	// Get the point RMS image for writing.
	public double[][] getRMS() {
		final double[][] rms = new double[sizeX][sizeY];
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) rms[i][j] = Math.sqrt(1.0 / weight[i][j]);
		return rms;
	}

	public double[][] getWeight(double[][] pointRMS) {
		final double[][] w = new double[sizeX][sizeY];
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) w[i][j] = 1.0 / (pointRMS[i][j] * pointRMS[i][j]);
		return w;
	}

	public int countPoints() {
		int points = 0;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) points++;
		return points;
	}

	public double countBeams() { return 1.0 + countPoints() / pointsPerBeam; }

	public double countInstrumentBeams() { return 1.0 + getArea() / (2.0 * Math.PI * instrumentFWHM * instrumentFWHM / 5.54); }	

	public double countIndependentPoints() {
		return countIndependentPoints(getArea());		
	}

	public double countIndependentPoints(double area) {
		double smoothArea = fwhm2size * fwhm2size * smoothFWHM*smoothFWHM;
		double filterArea = fwhm2size * fwhm2size * extFilterFWHM * extFilterFWHM;

		// Account for the filtering correction.
		double eta = 1.0;
		if(Double.isNaN(extFilterFWHM) && extFilterFWHM > 0.0) eta -= smoothArea / filterArea;
		double iPointsPerBeam = eta * Math.min(4.5, smoothArea / (delta*delta));

		return Math.ceil((1.0 + area/smoothArea) * iPointsPerBeam);
	}

	double assumedArea = Double.NaN;

	public double getArea() {
		return Double.isNaN(assumedArea) ? countPoints() * delta * delta : assumedArea; 		
	}

	public double getImageFWHM() { return Math.sqrt(instrumentFWHM * instrumentFWHM + smoothFWHM * smoothFWHM); }

	public double getImageBeamArea() { 
		return fwhm2size * fwhm2size * (instrumentFWHM * instrumentFWHM + smoothFWHM * smoothFWHM);
	}
	
	public double getInstrumentBeamArea() {
		return fwhm2size * fwhm2size * instrumentFWHM * instrumentFWHM;
	}
	
	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 / Constant.sigmasInFWHM;
		final double sigma2 = sigma * sigma;
		final double center = (size-1) / 2.0;

		for(int i=beam.length; --i >= 0; ) for(int j=beam[0].length; --j >= 0; ) {
			final double dx = delta * (i - center);
			final double dy = delta * (j - center);

			beam[i][j] = Math.exp(-(dx*dx+dy*dy)/(2.0*sigma2));
		}
		return beam;
	}

	public void clean() {
		double imageFWHM = getImageFWHM();
		clean(imageFWHM, 0.1, 0.5*imageFWHM);
	}

	public void clean(double FWHM, double gain, double replacementFWHM) {
		int dIndex = (int) Math.ceil(2.0*FWHM/delta);
		double[][] beam = getBeam(FWHM, 2*dIndex+1);
		clean(beam, gain, replacementFWHM); 
	}

	public void clean(double[][] beam, double gain, double replacementFWHM) {
		if(verbose) System.err.println("Deconvolving to " + Util.f1.format(replacementFWHM/Unit.arcsec) + " arcsec resolution.");

		final int ic = beam.length / 2;
		final int jc = beam[0].length / 2;

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

		// Normalize to beam center
		final double norm = beam[ic][jc];
		for(int ib = beam.length; --ib >= 0; ) for(int jb=beam[0].length; --jb >= 0; ) beam[ib][jb] /= norm;
		
		// Calculate the peak to replacement beam integral conversion...
		final double sigma = replacementFWHM * fwhm2size / delta / Constant.sigmasInFWHM;
		final double beamInt = Constant.twoPi * sigma * sigma;

		// Find the peak
		int[] index = indexOfMaxDev(s2n);

		int removed = 0;
		double peakS2N = s2n[index[0]][index[1]];
		double ave = Math.abs(peakS2N);

		// Remove until there is an 70% chance that the peak is real
		final double critical = CumulativeNormalDistribution.inverseAt(1.0 - 0.3 / Math.pow(getImageFWHM()/smoothFWHM, 2.0));

		// Or until the number of components can cover the map...
		final int maxComponents = (int) Math.ceil(countBeams() / gain);			

		do {			    
			// Get the peak value	 
			final int i = index[0];
			final int j = index[1];

			final double value = gain * signal[i][j];            
			peakS2N = s2n[i][j];            

			// Pole is the peak value times the beam integral to conserve flux	    
			deconvolved[i][j] += value * beamInt; // est. intergal flux in beam

			// 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) {
				// Remove the beam from the clean
				final double  dsignal = value * beam[ib][jb];
				signal[i1][j1] -= dsignal;
				// ..and from s2n
				s2n[i1][j1] -= dsignal / rms[i1][j1];
			}

			removed++;

			if(removed % 100 == 0) {
				System.out.print("\r\t" + removed + " components removed. (Last: " + Util.f2.format(peakS2N) + "-sigma, Ave: " + Util.f2.format(ave) + "-sigma)   ");
				System.out.flush();
			}


			index = indexOfMaxDev(s2n);

			ave *= Math.exp(-0.03);
			ave += 0.03 * Math.abs(peakS2N);

		} while(Math.abs(peakS2N) > critical && removed < maxComponents);


		System.out.println("\r\t" + removed + " components removed. (Last: " + Util.f2.format(peakS2N) + "-sigma, Ave: " + Util.f2.format(ave) + "-sigma)   ");

		deconvolved = getConvolved(deconvolved, replacementFWHM);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) signal[i][j] += deconvolved[i][j];
			

		// Reset the beam and resolution... 
		instrumentFWHM = replacementFWHM;
		smoothFWHM = delta / fwhm2size;	

		// recalculate beam related constants...
		calc();
		
		addProcessingHistory("deconvolved via CLEAN to " + Util.F3.format(replacementFWHM) + " arcsec FWHM.");

		System.out.println();
	}



	public int[] indexOfMax(double[][] image) {
		int[] index = new int[2];

		double peak = 0.0;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(image[i][j] > peak) {
				peak = image[i][j];
				index[0] = i;
				index[1] = j;
			}

		return index;
	}

	public int[] indexOfMin(double[][] image) {
		int[] index = new int[2];

		double peak = 0.0;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(image[i][j] < peak) {
				peak = image[i][j];
				index[0] = i;
				index[1] = j;
			}

		return index;
	}

	public int[] indexOfMaxDev(double[][] image) {
		int[] index = new int[2];

		double peak = 0.0;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0)
			if(Math.abs(image[i][j]) > peak) {
				peak = Math.abs(image[i][j]);
				index[0] = i;
				index[1] = j;
			}

		return index;
	}

	public Vector2D getOffset(int k) {
		Region region = regions.get(k);
		return new Vector2D(region.dX, region.dY);	    
	}


	public void removeGradient() {
		if(verbose) System.err.println("Removing gradient.");

		LinearRegression x = new LinearRegression();
		LinearRegression y = new LinearRegression();
		
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) { 
			boolean inside = isInsideRegions(i, j);

			if(!inside && flag[i][j] == 0) {
				final double w = weight[i][j];
				final double value = w * signal[i][j];
				
				x.add(i, value, w);
				y.add(j, value, w);
			}
		}
	
		final double mx = x.getSlope().value();
		final double my = y.getSlope().value();
		
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) 
			signal[i][j] -= mx * (i-sizeX/2) + my * (j-sizeY/2);
		
		addProcessingHistory("removed gradients (excl. regions).");
	}

	public double level() { 
		if(verbose) System.err.println("Leveling.");

		addProcessingHistory("leveling...");
		
		final double level = getLevel();
		offset(-level);
		calcS2N();

		return level;
	}

	public void offset(final double flux) {
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) signal[i][j] += flux;
		calcS2N();	
		addProcessingHistory("signal offset by " + Util.S3.format(flux / displayUnit.value()) + " " + displayUnit.name() + ".");
	}

	public void scale(final double scalar) {
		if(verbose) System.err.println("Scaling by " + scalar);

		final double ascalar = Math.abs(scalar);
		final double scalar2 = scalar*scalar;
		final double sign = Math.signum(scalar);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			signal[i][j] *= scalar;
			s2n[i][j] *= sign;
			rms[i][j] *= ascalar;
			weight[i][j] /= scalar2;
		}
		
		addProcessingHistory("scaled by " + scalar);
	}  

	public void rmsScale(final double scalar) {
		if(verbose) System.err.println("Scaling noise by " + scalar);

		final double scalar2 = scalar*scalar;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			rms[i][j] *= scalar;
			weight[i][j] /= scalar2;
		}
		
		addProcessingHistory("noise scaled by " + scalar);
	}

	public void setNoise(final double RMS) {
		final double w = 1.0 / (RMS * RMS);
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			rms[i][j] = RMS;
			weight[i][j] = w;			
		}
		addProcessingHistory("noise set to " + Util.S3.format(RMS / displayUnit.value()) + " " + displayUnit.name() + ".");
	}
	
	public void weightScale(final double scalar) {
		//if(verbose) System.err.println("Scaling weights by " + scalar);

		double rmsScalar = 1.0 / Math.sqrt(scalar);
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) {
			weight[i][j] *= scalar;
			rms[i][j] *= rmsScalar;
			s2n[i][j] /= rmsScalar;
		}
		
		addProcessingHistory("noise weights scaled by " + scalar);
	}

	public double median(double[][] image) {
		double[] point = new double[countPoints()];

		if(point.length == 0) return 0.0;

		int k=0;
		
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) 
			if(!isInsideRegions(i, j))  point[k++] = image[i][j];

		return Statistics.Inplace.median(point, 0, k);
	}

	public double robustLevel() {
		if(verbose) System.err.println("Median leveling (outside regions).");

		addProcessingHistory("median leveling...");
		
		final double level = median(signal);
		offset(-level);	
		calcS2N();

	
		
		return level;
	}

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

	public double robustWeight(boolean level) { return robustWeight(level, false); }

	public double robustWeight(boolean level, boolean oneSided) {		
		double weightCorrection = getRobustWeight(level, oneSided);

		if(verbose) System.err.println(
				(oneSided ? "Asymmetric" : "Symmetric") + 
				" robust weighting (rms " + Util.f2.format(Math.sqrt(1.0 / weightCorrection)) + "x).");

		weightScale(weightCorrection);
		weightFactor *= weightCorrection;

		addProcessingHistory("robust re-weighting...");
		
		return Math.sqrt(1.0 / weightCorrection);
	}



	// Adjust map weights to apparent map noise...
	public double rmsWeight() { return rmsWeight(true); }

	public double rmsWeight(boolean level) {		
		double weightCorrection = getRMSWeight(level);

		addProcessingHistory("RMS weighting...");
		
		if(verbose) System.err.println("Statistical weighting (rms " + Util.F3.format(Math.sqrt(1.0 / weightCorrection)) + "x).");

		weightScale(weightCorrection);
		weightFactor *= weightCorrection;

		return Math.sqrt(1.0 / weightCorrection);
	}

	// Return to calculated weights...
	public void dataWeight() {
		if(verbose) System.err.println("Switching to data weights.");

		weightScale(1.0/weightFactor);
		weightFactor = 1.0;
	}

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

	public double getRobustWeight(boolean level) {
		return getRobustWeight(level, false);
	}

	public double getRobustWeight(boolean level, boolean oneSided) {

		if(level) robustLevel(); 

		double[] chi2 = new double[countPoints()];
		if(chi2.length == 0) return 0.0;

		int k=0;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) if(!isInsideRegions(i, j)) {
			
			if(oneSided) {
				if(s2n[i][j] < 0.0) chi2[k++] = s2n[i][j] * s2n[i][j];
			}
			else chi2[k++] = s2n[i][j] * s2n[i][j];
		}

		// how does smoothing change the median noise statistics?
		// points round y: N ~ -ln(y)
		// int_{0, ymed)  y ln(y) = 0.5 * int_{0, 1) y ln(y)

		// median(x^2) = 0.454937 * sigma^2 
		return k > 0 ? Statistics.medianNormalizedVariance / Statistics.Inplace.median(chi2, 0, k) : 1.0;	
	}


	public double getGaussianity(double resolution) {
		final Hashtable<Integer, Integer> histogram = new Hashtable<Integer, Integer>();

		int max = (int)Math.ceil(6.0 / resolution);
		for(int bin = -max; bin <= max; bin++) histogram.put(bin, 0);

		int k=0;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			int bin = (int) Math.round(s2n[i][j] / resolution);
			int n = histogram.containsKey(bin) ? histogram.get(bin) + 1 : 1;
			histogram.put(bin, n);
			k++;
		}

		double chi2 = 0.0;
		for(int bin : histogram.keySet()) {
			int n = histogram.get(bin);
			double s2n = bin * resolution;
			double exp = k * Math.exp(-0.5*s2n*s2n) * resolution / Math.sqrt(2.0 * Math.PI);
			double dev = (n - exp) / Math.sqrt(exp);
			chi2 += dev * dev;
		}

		return Math.sqrt(histogram.size() / chi2);	
	}


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

	public double getRMSWeight(boolean level) {
		if(level) level(); 

		double chi2 = 0.0;
		int n=0;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			chi2 += s2n[i][j] * s2n[i][j];
			n++;
		}
		return n < 2 ? Double.NaN : chi2 / (n-1);	
	}


	public double continuousNoise() {
		if(verbose) System.err.print("Switching noise estimate discrete --> continuous.");
		double fc = Math.PI / Constant.sigmasInFWHM * instrumentFWHM / delta;
		double Q = CumulativeNormalDistribution.complementAt(fc);
		// ConfidenceCalculator.getOutsideProbability(fc);
		double x = 1.0 / (1.0 - 4.0 * Q);
		if(verbose) System.err.println(" (rms " + Util.f2.format(x) + "x)");
		weightScale(x);
		
		addProcessingHistory("switched from discrete to continuum noise...");
		
		return x;
	}



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

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

	public void getTotalFlux() {
		double flux = 0.0, var = 0.0;

		double A = (delta*delta) / getImageBeamArea() / janskyPerBeam.value();

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			flux += signal[i][j];
			var += 1.0 / weight[i][j];
		}

		DataPoint F = new DataPoint(flux, Math.sqrt(var));
		F.scale(A);

		System.out.print("  Total Flux = " + F.toString() + " Jy");
		System.out.println();

	}



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

		System.out.println("  ID            Type\t\tArea [sq\']\tFlux [Jy]");
		System.out.println("  ================================================================");

		for(int i=0; i<regions.size(); i++) {
			Region r = regions.get(i);
			DataPoint flux = r.getFlux();
			if(flux.value() == 0.0) continue;

			String radius = "r = " + Util.f1.format(r.radius / Unit.arcsec) + "\"   ";

			System.out.println("  " + getFluxString(r.id, radius, flux, r.getArea()));
		}


		for(int i=0; i<polygons.size(); i++) {
			Polygon polygon = polygons.get(i);
			DataPoint flux = polygon.getFlux(); 
			if(flux.value() == 0.0) continue;

			System.out.println("  " + getFluxString(polygon.name, "polygon(" + polygon.size() + ") ", flux, polygon.getArea()));
		}

		System.out.println("  ----------------------------------------------------------------");

		System.out.println();
	}

	private String getFluxString(String id, String type, DataPoint flux, double area) {
		String id12 = (id + "            ").substring(0, 12);
		return id12 + "  " + type + "\t" + Util.s[3].format(area / Unit.arcmin2) + "\t\t" + flux.toString();
	}


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

		DecimalFormat df = new DecimalFormat("0.00E0");
		for(Region region : regions) {
			double rms = region.getRMS();
			System.out.print("  " + region.id + "\t");
			System.out.print("RMS = " + df.format(rms) + " Jy/beam");
			System.out.println();
		}


		for(Polygon polygon : polygons) {
			double rms = polygon.getRMS();
			System.out.print("  " + polygon.name + "\t");
			System.out.print("RMS = " + df.format(rms) + " Jy/beam");
			System.out.println();
		}

		System.out.println();
	}

	public double getMin(double[][] image) { 
		double min=Double.POSITIVE_INFINITY;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j]==0) 	
			if(image[i][j] < min) min = image[i][j];

		return min;
	}



	public double getMax(double[][] image) {
		double max=Double.NEGATIVE_INFINITY;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j]==0) 
			if(image[i][j] > max) max = image[i][j];

		return max;
	}


	final static int SIGNAL_TO_NOISE = 0;
	final static int MAX_FALSE_DETECTIONS = 1;
	final static int FALSE_DETECTION_CHANCE = 2;
	final static int SINGLE_BEAM_CONFIDENCE = 3;
	final static int DETECTION_CONFIDENCE = 4;

	public void subtractSources() {
		double filterCorrection = getExtFilterCorrectionFactor(instrumentFWHM);		
		double filterBeamScale = 1.0 - 1.0 / filterCorrection;	
		double filterFWHM = Double.NaN;
		if(!Double.isNaN(extFilterFWHM)) filterFWHM = Math.sqrt(extFilterFWHM*extFilterFWHM + instrumentFWHM*instrumentFWHM);

		for(Region source : regions) {
			double S2N = source.peak / source.dpeak;

			//	 Correct the source flux for filtering...
			if(!Double.isNaN(extFilterFWHM) && S2N < filterBlanking) source.scale(filterCorrection);

			// Remove source only after the filtering corrections are applied to its flux...
			source.addGaussian(getImageFWHM(), -1.0);

			// Correct for filtering.
			if(!Double.isNaN(extFilterFWHM) && S2N < filterBlanking) source.addGaussian(filterFWHM, filterBeamScale);
		}

		addProcessingHistory("removed " + regions.size() + " Gaussian sources.");
	}


	public void unflag(final int pattern) {
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) flag[i][j] &= ~pattern;	
	}

	public Vector<Region> findPeaksAbove(double S2N) { return findPeaks(1, S2N); }

	public Vector<Region> findPeaksBelow(double S2N) { return findPeaks(-1, S2N); }

	// Return them in descending order
	public Vector<Region> findPeaks(final int dir, double S2N) {
		// Estimate how much of the peak may be missed due to regridding...
		final double sigma = getImageFWHM() / Constant.sigmasInFWHM;
		final double halfpixeldev = delta / (2.0 * sigma);
		final double searchS2N = S2N * Math.exp(-halfpixeldev * halfpixeldev / 2.0); // maximum peak position error is a half-diagonal pixel.

		Vector<Region> points = new Vector<Region>(100);

		// Peak cannot be an edge pixel (not reliable)...
		for(int i=sizeX-1; --i > 0; ) for(int j=sizeY-1; --j > 0; ) if(flag[i][j] == 0) if(dir * Double.compare(s2n[i][j], searchS2N) > 0) {

			// check that it is a peak in the neighbourhood...			
			boolean isPeak = true;	
			for(int i1=i-1; i1<=i+1 && isPeak; i1++) for(int j1=j-1; j1<=j+1 && isPeak; j1++) 
				if(flag[i1][j1] == 0) if(dir * Double.compare(s2n[i1][j1], s2n[i][j]) > 0) isPeak = false;

			if(isPeak) {	
				Region point = new Region(dXofIndex(i), dYofIndex(j), instrumentFWHM, false);
				point.finetunePeak();

				if(dir * Double.compare(point.peak / point.dpeak, S2N) > 0) points.add(point);
			}
		}

		// Return peaks sorted from far to near...
		Collections.sort(points, new Comparator<Region>() {
			@Override
			public int compare(Region a, Region b) {
				return dir * Double.compare(b.peak / b.dpeak, a.peak / a.dpeak);
			}
		});

		return points;
	}

	public double findMode(double searchRadius) {
		if(verbose) System.err.println("Finding Distribution Mode.");

		int[] bin = new int[3];
		double binSize = 2.0/3.0*searchRadius;

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			final double flux = signal[i][j];
			if(Math.abs(flux) < searchRadius) bin[(int)((flux + searchRadius) / binSize)]++;
		}

		double b = (bin[2] - bin[0]) / 2.0;
		double a = (bin[2] + bin[0]) / 2.0 - bin[1];

		//System.err.println("[" + bin[0] + ", " + bin[1] + ", " + bin[2] + "]");

		return -0.5 * b / a * binSize; // center offset
	}

	public double modalLevel() {
		double level = findMode(1.0 * getTypicalRMS());
		addProcessingHistory("mode levelling...");
		offset(-level);
		calcS2N();
		return level;
	}

	public double getTypicalRMS() {
		double sumw = 0.0;
		int n = 0;
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
			sumw += 1.0 / (rms[i][j] * rms[i][j]);
			n++;
		}
		return Math.sqrt(n/sumw);
	}


	public void updateStats(boolean modalLevel, boolean noiseRecalc) {
		// Re-level
		double level = 0.0;

		level = modalLevel ? modalLevel() : robustLevel();

		// Do an assymetric noise-weighting without levelling...
		double noiseScale = noiseRecalc ? robustWeight(false, true) : 1.0;
		//noiseScale *= continuousNoise();

		// Adjust prior detections by the changed level and noise...
		for(Region region : regions) {
			region.peak -= level;
			region.dpeak *= noiseScale;
		}
	}


	final static int FLAG_DETECTION = 1<<8;
	final static int FLAG_NEG = 1<<10;

	int detections = -1;
	int falseDetections = -1;
	boolean gaussianStats = false;
	boolean iterateNoise = false;

	public void extract(final int criterion, final double value, boolean isOneSided) {	
		System.err.println();

		double significance = 3.0;	
		int priorSources = regions.size();
		boolean reporting = verbose && priorSources == 0;

		// Make room for sources...
		if(priorSources < 100) regions.ensureCapacity(100);

		// Remove possible prior corrections to fluxes, to analyze raw maps.
		// This is important because corrections are calculated individually for each source extracted...
		if(!Double.isNaN(fluxCorrectingFWHM) || fluxCorrectingFWHM <= 0.0) uncorrect();

		switch(criterion) {
		case SIGNAL_TO_NOISE: significance = value; break;
		case MAX_FALSE_DETECTIONS: significance = CumulativeNormalDistribution.inverseAt(1.0 - value / countIndependentPoints());
		// ConfidenceCalculator.getSigma(1.0 - value / countIndependentPoints()); 
		break;
		case FALSE_DETECTION_CHANCE: {
			double Nfalse = -Math.log(1.0 - value);
			significance =  CumulativeNormalDistribution.inverseAt(1.0 - Nfalse / countIndependentPoints());
			// ConfidenceCalculator.getSigma(1.0 - Nfalse / countIndependentPoints());
			break;
		}
		case SINGLE_BEAM_CONFIDENCE: significance = CumulativeNormalDistribution.inverseAt(value);
		// ConfidenceCalculator.getSigma(value); break;
		case DETECTION_CONFIDENCE: significance = 0.0; break;
		}

		if(reporting && significance > 0.0) System.out.println("# [extract] Searching for peaks at " + Util.f2.format(significance) + " sigma...");

		final double points = countIndependentPoints();
		final double instrumentBeams = countInstrumentBeams();

		if(reporting) System.out.println("# [extract] Search area is " + Util.e2.format(getArea()/Unit.deg2) + " sqdeg...");
		if(reporting) System.out.println("# [extract] Search area contains " + Util.f1.format(instrumentBeams) + " instrument beams...");

		// The chance for each independent point to be above the detection significance level...
		double Q = CumulativeNormalDistribution.complementAt(significance);
		//ConfidenceCalculator.getOutsideProbability(significance);

		// the expected number of false detections...
		double falses = Q * points;

		if(criterion == MAX_FALSE_DETECTIONS || criterion == FALSE_DETECTION_CHANCE) falses = value;

		if(reporting) if(criterion != DETECTION_CONFIDENCE){
			System.out.println("# [extract] Chance of at least one false detection is " + Util.f1.format(100.0 * (1.0-Math.exp(-falses))) + "%.");
			System.out.println("# [extract] Expected number of false detections is " + Util.f1.format(falses) + ".");
		}	

		if(reporting) {
			if(gaussianStats) System.out.println("# [extract] Assuming Gaussian noise statistics.");
			else System.out.println("# [extract] Adapting statistics for non-Gaussian noise.");
		}


		System.err.println();

		// Here's the source extraction loop...
		int cleanup = 1;
		int maxk = cleanup + 1;

		for(int k=1; k<=maxk; k++) {
			System.err.println("[Extraction Round #" + k + "]");

			updateStats(isOneSided && priorSources > 0, iterateNoise || k == 1);

			// Remove anything that may have dropped below the search criterion...
			for(int i=0; i<regions.size(); ) {
				Region source = regions.get(i);
				final double S2N = source.peak / source.dpeak;

				if(S2N < significance) {
					source.unflag(FLAG_DETECTION);
					source.addPoint(true);
					regions.remove(i);
				}
				else i++;
			}

			System.err.println("Searching for sources...");

			for(Region source : findPeaksAbove(significance)) if(flag[indexOfdX(source.dX)][indexOfdY(source.dY)] == 0){			
				source.finetunePeak(); // Re-tune the peak. (may be necessary after the removal of a nearby source...)
				final double S2N = source.peak / source.dpeak;

				if(S2N > significance) {
					source.correct(instrumentFWHM);					
					source.subtractPoint(true);
					source.flag(FLAG_DETECTION);
					regions.add(source);
				}
			} 

			int detections = regions.size() - priorSources;

			if(detections != 0) {
				String lead = detections > 0 ? " +" : " ";
				System.err.println(lead + detections + " source candidate(s).");
			}

			if(detections > 0) maxk++;

			priorSources = regions.size();
		}

		/*
		 * OK, at this point the initial source list is ready. Now clean it up.
		 */



		if(regions.size() == 0) {
			System.err.println(" No detections.");
			// Report Gaussianity...
			//System.out.println("# [extract] Gaussianity of residuals: " + Util.f3.format(getGaussianity(0.5)) + ".");
			//return;			
		}


		System.err.println("Searching for matching negative peaks...");
		// Count the corresponding negatives...
		Vector<Region> negatives = new Vector<Region>(100);
		for(Region neg : findPeaksBelow(-significance)) {
			if(flag[indexOfdX(neg.dX)][indexOfdY(neg.dY)] == 0) { 
				neg.flag(FLAG_NEG);
				negatives.add(neg);
			}		
		}


		if(!regions.isEmpty()) System.err.println(" " + regions.size() + " source candidate(s) extracted.");
		System.err.println(" " + (negatives.size() > 0 ? negatives.size() + "" : "No") + " matching negative peaks.");

		// Sort sources in order of descending significance...
		if(!regions.isEmpty()) {
			System.err.println("Sorting detection w.r.t. detection significance.");
			Collections.sort(regions, new Comparator<Region>() {
				@Override
				public int compare(Region o1, Region o2) {
					return Double.compare(o2.peak/o2.dpeak, o1.peak/o1.dpeak);
				}				
			});
		}


		// Clear the flags around the negatives...
		for(Region neg : negatives) neg.unflag(FLAG_NEG);

		if(!regions.isEmpty())
			System.err.println("Revising list based on distribution of sources" + (gaussianStats ? "." : " and negatives."));


		System.out.println("#");
		System.out.println("# * confidence values: The percentages in the square brackets indicate");
		System.out.println("#                      a rough confidence estimate based on Gaussian");
		System.out.println("#                      statistics, modified by the actual counts of");
		System.out.println("#                      sources and negatives found above the same");
		System.out.println("#                      significance level. It is only a guesstimate!");
		System.out.println("#");
		System.out.println("# * false detections: first column is estimated false detection rate");
		System.out.println("#                     based on purely Gaussian statistics. The second");
		System.out.println("#                     value is a guesstimate of the effective rate based");
		System.out.println("#                     on actual negative counts.");


		double effectiveSources = 0.0;
		int kept = 0;
		double lastS2N = Double.NaN;

		for(int i=0; i<regions.size(); i++) {

			Region source = regions.elementAt(i);

			final double S2N = source.peak / source.dpeak;
			lastS2N = S2N;

			Q = CumulativeNormalDistribution.complementAt(S2N);		
			//ConfidenceCalculator.getOutsideProbability(S2N);

			double expectedNegs = Q * points;

			int actualNegs = 0;
			for(Region neg : negatives) if(neg.peak/neg.dpeak < -S2N) actualNegs++;

			double Nfalse = expectedNegs;
			if(!gaussianStats) Nfalse += (actualNegs - expectedNegs) * Math.abs(Math.tanh((actualNegs - expectedNegs) / (2.0 * Math.sqrt(expectedNegs))));

			// The chance of more than n false detections above some significance is
			// 1 - sum_{i,0,n}(P(i)) where P is Poission
			if(Double.isNaN(Nfalse)) Nfalse = 0.0;

			// The chance it is a false detection given an all noise map, and the expected number of
			// false detections (or empirically corrected false detection estimate)...
			double Pfalse = 1.0 - Math.exp(-Nfalse);

			// scale by Nfalse / (Nfalse + Nreal) = Nfalse / (i+1) = 1 - Nsources / (i+1)
			if(effectiveSources > 0.0) Pfalse *= 1.0 - effectiveSources / (i+1);

			double Preal = 1.0 - Pfalse;

			effectiveSources += Preal;

			if(criterion == MAX_FALSE_DETECTIONS || criterion == FALSE_DETECTION_CHANCE) 
				if((i+1) - effectiveSources > value) break;

			boolean pass = S2N > significance;
	
			if(criterion == DETECTION_CONFIDENCE) if(Preal < value) pass = false;

			source.id = "[" + (i+1) + ":" + ((int) Math.round(100.0 * Preal)) + "%]  ";
			source.comment = Util.f2.format(S2N) + " sigma";
			//if(-mostNegative > minDeviation) region.id += "!";
			//if(S2N < -mostNegative) region.id += "?";
			if(pass) {
				source.flag(FLAG_DETECTION);

				if(i==0) {
					System.out.println("#");
					System.out.println("#[ID:conf.]     RA          DEC                    beam S          dS               S/N         false  false' ");
					System.out.println("#-------------------------------------------------------------------------------------------------------------");		
				}

				kept++;
				System.out.println(source.toString() + "\t" + Util.f3.format(expectedNegs) + "\t" + Util.f3.format(Nfalse));
			}

			else source.addPoint(true);				

		}
		if(criterion != SIGNAL_TO_NOISE && !regions.isEmpty()) System.err.println(" " + kept + " source(s) kept.");

		int actualNegs = 0;
		if(!regions.isEmpty()) {
			for(Region neg : negatives) if(neg.peak/neg.dpeak < -lastS2N) actualNegs++;
			System.err.println(" " + actualNegs + " matching negative peaks remaining.");
		}

		detections = kept;
		falseDetections = actualNegs;

		System.err.println();

		if(verbose) {
			// Report Gaussianity...
			System.out.println("# [extract] Gaussianity of residuals: " + Util.f3.format(getGaussianity(0.5)) + ".");

			// Report significant negatives...
			if(actualNegs > 0) System.out.println("# [extract] WARNING! " + actualNegs + " matching negative peaks.");
			else System.out.println("# [extract] No negatives below search significance.");
		}



		System.out.println();
		System.err.println();

		// Reapply flux corrections to the residuals...
		if(Double.isNaN(fluxCorrectingFWHM) || fluxCorrectingFWHM <= 0.0) correct(instrumentFWHM);
	}



	public SkyMap stack() {
		if(verbose) System.err.print("Stacking sources... ");

		int size = (int) (10.0 * getImageFWHM() / delta);
		if(size %2 == 0) size++;
		SkyMap stacked = getSkyMap();
		stacked.setSize(size, size);
		stacked.i0 = (size - 1)/2;
		stacked.j0 = stacked.i0;
		int sources = 0;

		for(Region region : regions) {
			final double ic = Math.round(indexOfdX(region.dX));
			final double jc = Math.round(indexOfdY(region.dY));

			final int imin = Math.max(0, (int)Math.floor(ic - stacked.i0));
			final int jmin = Math.max(0, (int)Math.floor(jc - stacked.j0));
			final int imax = Math.min(sizeX, (int)Math.ceil(ic + stacked.i0));
			final int jmax = Math.min(sizeY, (int)Math.ceil(jc + stacked.j0));

			if(imax > ic && jmax > jc && imin < ic && jmin < jc) sources++;

			for(int i=imin; i<imax; i++) for(int j=jmin; j<jmax; j++) if(flag[i][j] == 0)
				stacked.addPointAtIndex(i-ic + stacked.i0, j-jc + stacked.j0, signal[i][j], weight[i][j], count[i][j]);
		}
		stacked.normalize();

		if(verbose) System.err.println(sources + " added.");

		addProcessingHistory("stacked " + regions.size() + "sources from mask(s).");
		
		return stacked;
	}


	// FWHM is underlying FWHM, not image FWHM (?)
	public void correct(double FWHM) {
		if(fluxCorrectingFWHM == FWHM) return;
		if(!Double.isNaN(fluxCorrectingFWHM)) uncorrect();

		if(verbose) System.err.println("Applying correction for " + Util.f1.format(FWHM/Unit.arcsec) + " arcsec FWHM structures.");

		calcS2N();

		final double filterC = getExtFilterCorrectionFactor(FWHM);
		final double filterC2 = filterC * filterC;

		//final double modelC2 = modelC * modelC;

		if(verbose) {
			if(filterC != 1.0) System.err.println("  " + Util.f2.format(filterC) + "x for explicit spatial filtering.");
			//if(modelC != 1.0) System.err.println("  " + Util.f2.format(modelC) + "x for filtering by models.");
		}

		if(Double.isNaN(filterBlanking)) filterBlanking = Double.POSITIVE_INFINITY;
		if(Double.isNaN(clippingS2N)) clippingS2N = Double.POSITIVE_INFINITY;


		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;				
			}
			/*
			if(s2n[i][j] < clippingS2N) {
				signal[i][j] *= modelC;
				weight[i][j] /= modelC2;
			}
			 */
		}

		fluxCorrectingFWHM = FWHM;

		if(Double.isInfinite(filterBlanking)) filterBlanking = Double.NaN;
		if(Double.isInfinite(clippingS2N)) clippingS2N = Double.NaN;

		addProcessingHistory("corrected fluxes for " 
				+ Util.F3.format(FWHM / displayUnit.value()) + " " + displayUnit.name() 
				+ (Double.isNaN(filterBlanking) ? "blanking above S/N " + Util.f1.format(filterBlanking) : "") 
				+ "."
		);
		
		calcRMS();
		calcS2N();
	}

	public void uncorrect() {
		if(Double.isNaN(fluxCorrectingFWHM)) return;
		if(verbose) System.err.println("Undoing correction for " + Util.f1.format(fluxCorrectingFWHM/Unit.arcsec) + " arcsec FWHM structures.");

		calcS2N();

		final double filterC = getExtFilterCorrectionFactor(fluxCorrectingFWHM);
		final double filterC2 = filterC * filterC;

		if(verbose) {
			if(filterC != 1.0) System.err.println("  " + Util.f2.format(filterC) + "x for explicit spatial filtering.");
			//if(modelC != 1.0) System.err.println("  " + Util.f2.format(modelC) + "x for filtering by models.");
		}

		if(Double.isNaN(filterBlanking)) filterBlanking = Double.POSITIVE_INFINITY;
		if(Double.isNaN(clippingS2N)) clippingS2N = Double.POSITIVE_INFINITY;

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
			if(s2n[i][j] < filterBlanking * filterC) {
				signal[i][j] /= filterC;
				weight[i][j] *= filterC2;				
			}
			/*
			if(s2n[i][j] < clippingS2N * modelC) {
				signal[i][j] /= modelC;
				weight[i][j] *= modelC2;
			}
			 */
		}

		fluxCorrectingFWHM = Double.NaN;

		if(Double.isInfinite(filterBlanking)) filterBlanking = Double.NaN;
		if(Double.isInfinite(clippingS2N)) clippingS2N = Double.NaN;

		addProcessingHistory("undid prior flux correction.");
		
		calcRMS();
		calcS2N();
	}

	public double getExtFilterCorrectionFactor(double FWHM) {
		// Smoothing to filterFWHM^2
		// to res = FWHM^2 + convFWHM^2 = FWHM^2 + filterFWHM^2 - imageFWHM^2
		if(Double.isNaN(extFilterFWHM)) return 1.0;
		FWHM = Math.min(instrumentFWHM, FWHM); // Nothing is ever smaller than the beam...
		double effectiveFilterFWHM2 = FWHM * FWHM + extFilterFWHM * extFilterFWHM;
		double effectiveFWHM2 = FWHM * FWHM + smoothFWHM * smoothFWHM;
		return 1.0 / (1.0 - effectiveFWHM2/effectiveFilterFWHM2);
	}



	protected void createModel(double[][] model) {
		for(int k=0; k<regions.size(); k++) regions.get(k).clear();		
		for(int k=0; k<regions.size(); k++) regions.get(k).add(false);    
	}



	public final double dXofIndex(final double i) { return (i-i0)*delta; }

	public final double dYofIndex(final double j) { return (j-j0)*delta; }

	public final int indexOfdX(final double dX) { return (int) Math.round(fracIndexOfdX(dX)); }

	public final int indexOfdY(final double dY) { return (int) Math.round(fracIndexOfdY(dY)); }

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

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



	public String getOffsetString(Vector2D offset) {
		double dx = offset.x() / Unit.arcsec;
		double dy = offset.y() / Unit.arcsec;

		SphericalCoordinates coords = projection.getReference();
		CoordinateSystem offsetSystem = coords.getLocalCoordinateSystem();

		return offsetSystem.get(0).getShortLabel() + " " + Util.f1.format(coords.isReverseLongitude() ? -dx : dx) 
				+ "  " + offsetSystem.get(1).getShortLabel() + " " + Util.f1.format(coords.isReverseLatitude()? -dy : dy) + "  ";

	}

	public String getPositionString(SphericalCoordinates coords) {
		int decimals = delta < 0.3 * Unit.arcsec ? 3 : 1;		
		String coordName = coords.getClass().getSimpleName();
		return coordName.substring(0, coordName.length() - 11) + " " + coords.toString(decimals);
	}


	public void addPolygon(String fileName) throws IOException {
		polygons.add(new Polygon(fileName));
		System.err.println("Reading polygon definition file.");
	}

	public void readMask(String maskName) throws IOException {
		readMask(maskName, true);
	}

	public void readMask(String maskName, boolean overwrite) throws IOException {
		BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(maskName)));
		if(overwrite) regions.clear();

		int prior = regions.size();

		System.err.print("Reading mask file... ");
		System.err.println();

		String line = null;
		while((line = in.readLine()) != null) if(line.length() > 0) if(line.charAt(0) != '#') {
			try { 
				Region r = new Region(line); 
				System.err.println(" " + r.toString());
				regions.add(r);
			}
			catch(Exception e) { System.err.println("WARNING! Skipping mask entry: " + e.getMessage()); }
		}
		in.close();

		System.err.println((regions.size() - prior) + " circular regions added.");

		addProcessingHistory("loaded mask file " + maskName + " (" + (regions.size() - prior) + " regions).");
	}


	public void writeMask() {
		String maskName = workPath + sourceName + ".mask";
		try {
			PrintWriter out = new PrintWriter(new BufferedOutputStream(new FileOutputStream(maskName)));

			// For ds9 style output
			/*
			out.println("# Region file format: DS9 version 4.1");
			out.println("# Filename: /home/pumukli/data/images/VESTA.8293.fits[Flux]");
			out.println("global color=white dashlist=8 3 width=2 font=\"helvetica 10 normal\" select=1 highlite=1 dash=0 fixed=0 edit=1 move=1 delete=1 include=1 source=1");
			out.println("fk5");
			 */

			for(int i=0; i<regions.size(); i++) out.println((regions.get(i)).toString());

			out.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 {
		if(id.equalsIgnoreCase("flux")) writeImage(signal, workPath + sourceName + ".flux.fits", nativeUnit);
		else if(id.equalsIgnoreCase("rms")) writeImage(getRMS(), workPath + sourceName + ".rms.fits", nativeUnit);
		else if(id.equalsIgnoreCase("time")) writeImage(weight, workPath + sourceName + ".time.fits", Unit.get("s"));	
		else if(id.equalsIgnoreCase("s2n")) {
			calcS2N();
			writeImage(s2n, workPath + sourceName + ".s2n.fits", Unit.get("1"));	
		}
		else System.out.println("WARNING! No image with id " + id + ".");
	}

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

		@SuppressWarnings("resource")
        BufferedDataOutputStream file = new BufferedDataOutputStream(new FileOutputStream(name));

		fits.write(file);
		System.out.println(" Written Image to " + name);
		
		file.close();
		fits.close();
	}

	public Fits getFits() throws HeaderCardException, FitsException, IOException {
		FitsFactory.setUseHierarch(true);
		Fits fits = new Fits();	

		calcS2N();

		fits.addHDU(makeHDU(signal, nativeUnit.value(), nativeUnit.name(), "Signal"));
		fits.addHDU(makeHDU(count, Unit.s, "s", "Integration Time"));
		fits.addHDU(makeHDU(getRMS(), nativeUnit.value(), nativeUnit.name(), "Noise"));
		fits.addHDU(makeHDU(s2n, 1.0, "1", "Signal-to-Noise"));

		for(int i=0; i<extraHDUs.size(); i++) fits.addHDU(extraHDUs.get(i));

		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 = workPath + sourceName + ".fits";  
		}

		@SuppressWarnings("resource")
        BufferedDataOutputStream file = new BufferedDataOutputStream(new FileOutputStream(fileName));

		fits.write(file);

		System.err.println(" Written " + fileName);
		
		file.close();
		fits.close();
	}


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

		double min = getMin(image);
		double max = getMax(image);
		
		if(!Double.isInfinite(min)) hdu.addValue("DATAMIN", min / unitValue, "");
		if(!Double.isInfinite(max)) hdu.addValue("DATAMAX", max / 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 boolean conforms(SkyMap map) {	
		if(map.delta != delta) return false;

		if(map.sizeX != sizeX) return false;
		if(map.sizeY != sizeY) return false;

		if(map.dXofIndex(0) != dXofIndex(0)) return false;
		if(map.dYofIndex(0) != dYofIndex(0)) return false;

		if(map.projection.getClass() != projection.getClass()) return false;

		return true;
	}

	public void conformTo(SkyMap map) {
		setSize(map.sizeX, map.sizeY);
		projection = map.projection;
		i0 = map.i0;
		j0 = map.j0;
		delta = map.delta;
	}

	public void addCoordinateInfo(BasicHDU<?> hdu) throws HeaderCardException {

		hdu.addValue("ALTAZ", projection.getReference() instanceof HorizontalCoordinates, "Is map in horizontal Alt/Az coordinates?");

		Header header = hdu.getHeader();
			
		projection.editHeader(header);

		SphericalCoordinates reference = projection.getReference();

		reference.editHeader(header, "CRVAL", "");

		hdu.addValue("CRPIX1", (i0+1), "Reference grid pixel (index 1).");
		hdu.addValue("CRPIX2", (j0+1), "Reference grid pixel (index 1).");

		hdu.addValue("CDELT1", (reference.isReverseLongitude() ? -1.0 : 1.0) * delta / Unit.deg, "Grid spacing (deg).");
		hdu.addValue("CDELT2", (reference.isReverseLatitude() ? -1.0 : 1.0) * delta / Unit.deg, "Grid spacing (deg).");

	}


	public void getCoordinateInfo(Header header) throws HeaderCardException {
		String type = header.getStringValue("CTYPE1");

		sizeX = header.getIntValue("NAXIS1");
		sizeY = header.getIntValue("NAXIS2");

		try { projection = SphericalProjection.forName(type.substring(5, 8)); }
		catch(Exception e) { System.err.println("ERROR! Unknown projection " + type.substring(5, 8)); }

		SphericalCoordinates reference = null;
		try {
			reference = SphericalCoordinates.getFITSClass(type.substring(0, 4).toUpperCase()).newInstance();
		} catch (Exception e) { e.printStackTrace(); }

		if(reference == null) System.err.println("ERROR! Unknown Coordinate Definition " + type);
		else reference.parseHeader(header, "CRVAL", "", new Coordinate2D());

		projection.setReference(reference);

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

		if(header.containsKey("CDELT2")) delta = header.getDoubleValue("CDELT2") * Unit.deg;
		else if(header.containsKey("CD2_2")) {
			double det = header.getDoubleValue("CD1_1") * header.getDoubleValue("CD2_2")
					- header.getDoubleValue("CD1_2") * header.getDoubleValue("CD2_1");
			delta = Math.sqrt(Math.abs(det)) * Unit.deg;
			if(header.getDoubleValue("CD1_2", 0.0) != 0.0 || header.getDoubleValue("CD2_1", 0.0) != 0.0) {
				System.err.println("*************************************************************************");
				System.err.println("WARNING! FITS image requires coordinate transformations not yet supported");
				System.err.println("         by CRUSH. Coordinate information will be wrong. Proceed at your");
				System.err.println("         own peril... (Continuing in 5 seconds)");
				System.err.println("*************************************************************************");
				try { Thread.sleep(5000); }
				catch(InterruptedException e) {}
			}
		}

		// Adjust the index to the determined pointing
		double di0 = (projection.getReference() instanceof HorizontalCoordinates ? -1 : 1) * header.getDoubleValue("RAP") * Unit.arcsec / delta;
		double dj0 = header.getDoubleValue("DECP") * Unit.arcsec / delta;

		i0 += di0;
		j0 += dj0;

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

	}

	public Fits getFits(String name) throws FitsException, IOException {
		FitsFactory.setUseHierarch(true);
		Fits fits;

		try { 
			fits = new Fits(new File(name)); 
			fileName = name;
		}
		catch(Exception e) { 
			fileName = workPath + name;
			fits = new Fits(new File(fileName)); 
		}

		// Change the workpath to the path of the last accessed file.
		int iPath = fileName.lastIndexOf(File.separator);
		workPath = fileName.substring(0, iPath+1); 
		if(verbose) System.err.println("Setting working directory to: " + workPath);

		return fits;
	}


	public void read(String name) throws HeaderCardException, FitsException, IOException {
		// Get the coordinate system information
	    // Turn off warnings about multiple occurences of header keys...
	    Logger.getLogger(Header.class.getName()).setLevel(Level.SEVERE);
	    
		BasicHDU<?>[] HDU = getFits(name).read();
		readHeader(HDU[0].getHeader(), HDU.length == 1);
		readData(HDU);
		
		/*
		if(nativeUnit.name().equalsIgnoreCase("Jy/pixel")) {
		    scale(getImageBeamArea() / (delta * delta));
		}
		*/
	}

	/* TODO
	private void readData(BasicHDU[] HDU) throws FitsException {
		super.readData(HDU[0]);
		
		BasicHDU timeHDU = null;
		BasicHDU noiseHDU = null;
		BasicHDU weightHDU = null;
		BasicHDU varHDU = null;
		
			
		for(int i=1; i<HDU.length; i++) {
			final String extName = HDU[i].getHeader().getStringValue("EXTNAME").toLowerCase();
			
			if(extName.contains("exposure")) timeHDU = HDU[i];
			else if(extName.contains("time")) { if(timeHDU == null) timeHDU = HDU[i]; }
			else if(extName.contains("weight")) weightHDU = HDU[i];	
			else if(extName.contains("-noise"));	// e.g. signal-to-noise
			else if(extName.contains("to noise"));  // e.g. signal to noise
			else if(extName.contains("/noise"));  // e.g. signal to noise
			else if(extName.contains("/ noise"));  // e.g. signal to noise
			else if(extName.contains("noise")) noiseHDU = HDU[i];							// noise weight -> weight
			else if(extName.contains("rms")) { if(noiseHDU == null) noiseHDU = HDU[i]; }
			else if(extName.contains("error")) { if(noiseHDU == null) noiseHDU = HDU[i]; }
			else if(extName.contains("uncertainty")) { if(noiseHDU == null) noiseHDU = HDU[i]; }
			else if(extName.contains("scatter")) { if(noiseHDU == null) noiseHDU = HDU[i]; }
			else if(extName.contains("depth")) { if(noiseHDU == null) noiseHDU = HDU[i]; }
			else if(extName.contains("coverage")) { if(timeHDU == null) timeHDU = HDU[i]; } // depth coverage -> noise
			else if(extName.contains("variance")) varHDU = HDU[i];
			else if(extName.equals("var")) { if(varHDU == null) varHDU = HDU[i]; }
			
		}
		
		if(timeHDU != null) getTimeImage().setImage(timeHDU);
		
		final Data2D image = getWeightImage();
		
		// Read weights, or caluclate from rms noise, variance, or use time, if available.
		if(weightHDU != null) image.setImage(weightHDU);
		else if(noiseHDU != null) {
			//image.setUnit(getUnit());
			image.setImage(noiseHDU);
			new Task<Void>() {
				@Override
				public void process(int i, int j) {
					final double rms = image.valueAtIndex(i, j);
					setWeight(i, j, 1.0 / (rms * rms)); 
				}
			}.process();
		}
		else if(varHDU != null) {
			image.setImage(varHDU);

			new Task<Void>() {
				@Override
				public void process(int i, int j) {
					setWeight(i, j, 1.0 / image.valueAtIndex(i, j)); 
				}
			}.process();
		}
		else if(timeHDU != null) getWeightImage().setImage(timeHDU);
		else setWeight(1.0);
	}
	*/
	
	private void readBasicHeaderData(Header header) throws HeaderCardException {
		getCoordinateInfo(header);

		creator = header.getStringValue("CREATOR");
		if(creator == null) creator = "unknown";
		creatorVersion = "unknown";
		sourceName = header.getStringValue("OBJECT");

		origin = header.getStringValue("ORIGIN");
		telescope = header.getStringValue("TELESCOP");
		instrument = header.getStringValue("INSTRUME");

		String unitName = header.getStringValue("BUNIT");	
		nativeUnit = new Unit(unitName == null ? "counts" : unitName, 1.0);
		
		// get the beam and calculate derived quantities
		if(header.containsKey("BMAJ")) instrumentFWHM = header.getDoubleValue("BMAJ", defaultBeamFWHM);
		else instrumentFWHM = 0.0;

		smoothFWHM = delta / fwhm2size;
		
		parseHistory(header);
	}


	public void readHeader(String name) throws FitsException, HeaderCardException, IOException {
		Fits fits = getFits(name);
		readHeader(fits.getHDU(0).getHeader(), false);
		fits.close();
	}

	public void readData() throws FitsException, IOException {
	    Fits fits = getFits(fileName);
		readData(fits.read());
		fits.close();
	}

	public void readData(BasicHDU<?>[] HDU) throws FitsException {
        extraHDUs.clear();
	    
		if(HDU.length == 1) readSingleData(HDU[0]);
		else if(creator.equalsIgnoreCase("BoA")) readBoAData(HDU);
		else if(creator.equalsIgnoreCase("SMURF")) readSMURFData(HDU);
		else readCrushData(HDU);
	}

	public double getAreaUnit(String unitName) {
	    String lUnitName = unitName.toLowerCase();
	
	    if(lUnitName.endsWith("pixel")) return delta * delta;
	    if(lUnitName.endsWith("pix")) return delta * delta;
	    if(lUnitName.endsWith("pxl")) return delta * delta;
	    if(lUnitName.endsWith("px")) return delta * delta;
	    if(lUnitName.endsWith("beam")) return getImageBeamArea();
	    if(lUnitName.endsWith("bm")) return getImageBeamArea();
	    if(lUnitName.endsWith("sr")) return Unit.sr;
	    if(lUnitName.endsWith("sqdeg")) return Unit.deg2;
	    if(lUnitName.endsWith("sqdegree")) return Unit.deg2;
	    if(lUnitName.endsWith("deg**2")) return Unit.deg2;
        if(lUnitName.endsWith("degree**2")) return Unit.deg2;
        if(lUnitName.endsWith("arcmin**2")) return Unit.arcmin2;
        if(lUnitName.endsWith("arcsec**2")) return Unit.arcsec2;
	    
	    return 1.0;
	}
	
	
	
	public void readHeader(Header header, boolean single) throws HeaderCardException {	
		this.header = header;

		units.clear();

		setJansky(1.0);
		weightFactor = 1.0;
		fluxCorrectingFWHM = Double.NaN;
		filterBlanking = Double.NaN;
		clippingS2N = Double.NaN;
		integrationTime = Double.NaN;
		options = new String[] {"unknown reduction options"};
		extFilterFWHM = Double.NaN;

		readBasicHeaderData(header);

		if(single) readSingleHeaderData(header);
		else if(creator.equalsIgnoreCase("BoA")) readBoAHeader(header);
		else if(header.containsKey("RECIPE")) {
			creator = "SMURF";
			readSMURFHeader(header);
		}
		else readCrushHeader(header);

		String unitName = nativeUnit.name().trim();
		
		double rescale = getAreaUnit(unitName) / getImageBeamArea();
		if(unitName.toLowerCase().substring(1).startsWith("jy")) {
		    rescale *= PrefixedUnit.getMultiplier(nativeUnit.name().charAt(0)).value();
		}
		
		setJansky(rescale);
		
		updateBeamQuantities();
		
		boolean hasNative = false;
		for(Unit u : units) if(unitName.equals(u.name())) {
		    hasNative = true;
		    break;
		}
		if(!hasNative) units.add(nativeUnit);

		/*
		try { setUnit(header.getStringValue("BUNIT")); }
		catch(IllegalArgumentException e) {
			System.err.println("WARNING! Cannot set data units from FITS. Using default 'Jy/beam'.");
			units.clear();
			nativeUnit = new Unit("counts", 1.0);
			units.add(nativeUnit);
			setUnit(nativeUnit.name());
		}
		 */

		if(verbose) info();
	}

	public double[][] readDoubleImage(BasicHDU<?> hdu) throws FitsException {
		return readDoubleImage(hdu, 1.0);
	}

	public double[][] readDoubleImage(BasicHDU<?> hdu, double scale) throws FitsException {
		int bits = hdu.getHeader().getIntValue("BITPIX");
		final double[][] image = new double[sizeX][sizeY];	

		if(bits == -32) {
			final float[][] data = (float[][]) hdu.getData().getData();
			for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) image[i][j] = data[j][i] * scale;	    
		}
		else if(bits == -64) {
			final double[][] data = (double[][]) hdu.getData().getData();
			for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; )  image[i][j] = data[j][i] * scale;	    
		}		
		else throw new IllegalArgumentException("Data is not floating point type.");

		return image;
	}

	public int[][] readIntImage(BasicHDU<?> hdu) throws FitsException {
		int bits = hdu.getHeader().getIntValue("BITPIX");
		int[][] image = new int[sizeX][sizeY];

		if(bits == 1) {
			boolean[][] data = (boolean[][]) hdu.getData().getData();
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) image[i][j] = data[j][i] ? 1 : 0;	    
		}
		else if(bits == 8) {
			byte[][] data = (byte[][]) hdu.getData().getData();
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) image[i][j] = data[j][i];	    
		}
		else if(bits == 16) {
			short[][] data = (short[][]) hdu.getData().getData();
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) image[i][j] = data[j][i];	    
		}
		else if(bits == 32) {
			int[][] data = (int[][]) hdu.getData().getData();
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) image[i][j] = data[j][i];	    
		}
		else if(bits == 64) {
			long[][] data = (long[][]) hdu.getData().getData();
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) image[i][j] = (int) data[j][i];	    
		}
		else throw new IllegalArgumentException("Cannot read integer image from data.");

		return image;
	}

	public void makeFlagFrom(double[][] image) {
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) flag[i][j] = Double.isNaN(image[i][j]) ? 1 : 0;
	}

	public void addFlagFrom(double[][] image) {
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) flag[i][j] |= Double.isNaN(image[i][j]) ? 1 : 0;
	}

	private double getFilterForPointCorrection(double C) {
		// C = 1 / (1 - (beam^2 / filter^2))
		// -> 1 - beam^2 / filter^2 = 1/C^2
		// -> beam/sqrt(1 - 1/C^2)
		return instrumentFWHM / Math.sqrt(1.0 - 1.0/(C*C));
	}

	private double getEquivalentWidthFor(Header header, String majorAxisKey, String minorAxisKey) {
		return getEquivalentWidthFor(header.getDoubleValue(majorAxisKey, 0.0) * Unit.deg, header.getDoubleValue(minorAxisKey, 0.0) * Unit.deg);
	}
	
	private double getEquivalentWidthFor(double majorAxis, double minorAxis) {
		if(minorAxis == 0.0) return majorAxis;
		return Math.sqrt(majorAxis * minorAxis);
	}
	
	private void readCrushHeader(Header header) throws HeaderCardException {	
		smoothedWeights = header.getBooleanValue("SMTHRMS", false);
		weightFactor =  header.getDoubleValue("XWEIGHT", 1.0);
		integrationTime = header.getDoubleValue("INTEGRTN", Double.NaN) * Unit.s;
		
		if(header.containsKey("CBMAJ")) fluxCorrectingFWHM = getEquivalentWidthFor(header, "CBMAJ", "CBMIN");
		fluxCorrectingFWHM = header.getDoubleValue("CORRECTN", Double.NaN) * Unit.arcsec;
		
		filterBlanking = header.getDoubleValue("FLTRBLNK", header.getDoubleValue("MAP_XBLK", Double.NaN));
		clippingS2N = header.getDoubleValue("CLIPS2N", header.getDoubleValue("MAP_CLIP", Double.NaN));

		creatorVersion = header.getStringValue("CRUSHVER");

		// Read in the option strings
		options = new String[header.getIntValue("ARGS")];
		for(int i=0; i<options.length; i++) options[i] = FitsToolkit.getLongKey(header, "ARG" + (i+1));

		String instrument  = header.getStringValue("INSTRUME");
		if(instrument != null) if(instrument.equals("SHARC2")) instrumentFWHM = defaultBeamFWHM;
		// get the map resolution
		if(header.containsKey("SBMAJ")) smoothFWHM = getEquivalentWidthFor(header, "SBMAJ", "SBMIN");
		else smoothFWHM = header.getDoubleValue("SMOOTH", Double.NaN) * Unit.arcsec;

		if(smoothFWHM < delta / fwhm2size) smoothFWHM = delta / fwhm2size;
		
		if(header.containsKey("XBMAJ")) extFilterFWHM = getEquivalentWidthFor(header, "XBMAJ", "XBMIN");
		else extFilterFWHM = header.getDoubleValue("EXTFLTR", Double.POSITIVE_INFINITY) * Unit.arcsec;	
		
		if(header.containsKey("IBMAJ")) instrumentFWHM = getEquivalentWidthFor(header, "IBMAJ", "IBMIN");
		else if(header.containsKey("BEAM")) instrumentFWHM = header.getDoubleValue("BEAM", defaultBeamFWHM / Unit.arcsec) * Unit.arcsec;
		else if(Double.isNaN(instrumentFWHM)) instrumentFWHM = smoothFWHM;
		else if(instrumentFWHM <= smoothFWHM) instrumentFWHM = smoothFWHM;
		else instrumentFWHM = Math.sqrt(instrumentFWHM * instrumentFWHM - smoothFWHM * smoothFWHM);
		
		if(header.containsKey("V2JY")) setJansky(header.getDoubleValue("V2JY", 1.0) * Unit.V);
	
		units.clear();
		units.add(janskyPerBeam);
		units.add(milliJanskyPerBeam);
		units.add(megaJanskyPerSr);

		Vector<Double> corrections = new Vector<Double>();
		// Check to see if needs correction for regional filtering
		corrections.add(header.getDoubleValue("ROW_PCOR", Double.NaN)); // Row drifts
		corrections.add(header.getDoubleValue("RBS_PCOR", Double.NaN));	// Row-bias drifts
		corrections.add(header.getDoubleValue("SKY_PCOR", Double.NaN));	// Sky-gradients
		corrections.add(header.getDoubleValue("REG_PCOR", Double.NaN));	// Regional correlations
		corrections.add(header.getDoubleValue("BLK_PCOR", Double.NaN));	// Block correlations

		for(Double C : corrections) if(!C.isNaN()) if(C != 1.0) 
			extFilterFWHM = Math.min(extFilterFWHM, getFilterForPointCorrection(C));

		if(Double.isInfinite(extFilterFWHM)) extFilterFWHM = Double.NaN;
	}


	private void readCrushData(BasicHDU<?>[] HDU) throws FitsException {	
	   	signal = readDoubleImage(HDU[0], nativeUnit.value());
		count = readDoubleImage(HDU[1], Unit.s);
		rms = readDoubleImage(HDU[2], nativeUnit.value());

		flag = new int[sizeX][sizeY];

		makeFlagFrom(signal);
		weight = getWeight(rms);		

		// Legacy support for old crush-1 maps (with unsmoothed weights)
		/*
		if(!smoothedWeights) if(fwhm2size * smoothFWHM > delta) {
			double ds = delta / fwhm2size;
			weight = getSmoothWeight(Math.sqrt(smoothFWHM * smoothFWHM - ds * ds));
			smoothedWeights = true;
		}
		*/

		s2n = new double[sizeX][sizeY];
		calcS2N();

		for(int i=4; i<HDU.length; i++) if(!(HDU[i] instanceof ImageHDU)) {
		    HDU[i].getData().getData(); // Work-around for deferred reading...
		    extraHDUs.add(HDU[i]);
		}

	}

	private void readBoAHeader(Header header) throws HeaderCardException {	
		units.add(janskyPerBeam);
		units.add(milliJanskyPerBeam);
		//smoothFWHM = 0.0;
	}


	private void readBoAData(BasicHDU<?>[] hdu) throws FitsException {	
	
		creator = "crush'd BoA";
		creatorVersion = version;

		signal = readDoubleImage(hdu[0], nativeUnit.value());
		count = readDoubleImage(hdu[1], 1.0);
		weight = readDoubleImage(hdu[2], 1.0/(nativeUnit.value()*nativeUnit.value()));

		flag = new int[sizeX][sizeY];
		makeFlagFrom(signal);

		rms = getRMS();

		// Calculate the signal-to-noise image
		s2n = new double[sizeX][sizeY];
		calcS2N();
	}


	private void readSMURFHeader(Header header) throws HeaderCardException {	
		units.add(janskyPerBeam);
		units.add(milliJanskyPerBeam);
		//smoothFWHM = 0.0;
	}


	private void readSMURFData(BasicHDU<?>[] hdu) throws FitsException {	
		
		creator = "crush'd SMURF";
		creatorVersion = version;

		signal = getSMURFImage(hdu[0], nativeUnit.value());
		weight = getSMURFImage(hdu[1], nativeUnit.value()*nativeUnit.value());	
		count = new double[sizeX][sizeY];
		flag = new int[sizeX][sizeY];
		makeFlagFrom(signal);

		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) weight[i][j] = 1.0 / weight[i][j];

		rms = getRMS();

		// Calculate the signal-to-noise image
		s2n = new double[sizeX][sizeY];
		calcS2N();
	}

	public double[][] getSMURFImage(BasicHDU<?> hdu, double scale) throws FitsException {
		double[][][] store = (double[][][]) hdu.getData().getData();
		double[][] image = new double[sizeX][sizeY];
		for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) image[i][j] = store[0][j][i] * scale;
		return image;
	}

	private void readSingleHeaderData(Header header) throws HeaderCardException {
		if(verbose) System.err.println("single> Beam is " + Util.f1.format(instrumentFWHM / Unit.arcsec) + " arcsec."); 

		// get the smoothing
		smoothFWHM = header.getDoubleValue("SMOOTH", delta / fwhm2size / Unit.arcsec) * Unit.arcsec;
	}


	private void readSingleData(BasicHDU<?> hdu) throws FitsException {
		units.add(nativeUnit);

		// Get the scan list
		extraHDUs.clear();

		creator += " (crush)";
		creatorVersion = version;


		// Get the map size
		int bits = Math.abs(hdu.getHeader().getIntValue("BITPIX"));
		int dims = hdu.getHeader().getIntValue("NAXIS");

		if(bits == 32) {		
			float[][] fdata = null;
			if(dims == 2) fdata = (float[][]) hdu.getData().getData();
			else if(dims == 3) fdata = ((float[][][]) hdu.getData().getData())[0];
			else if(dims == 4) fdata = ((float[][][][]) hdu.getData().getData())[0][0];
			else {
			    Util.warning(this, "Expected FITS data of 2-4 dimensions. Got: " + dims);
			    return;
			}
			
			setSize(fdata.length, fdata[0].length);
			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) signal[i][j] = fdata[i][j];
		}
		else if(bits == 64) {
			double[][] ddata = null;
			if(dims == 2) ddata = (double[][]) hdu.getData().getData();
			else if(dims == 3) ddata = ((double[][][]) hdu.getData().getData())[0];
			else if(dims == 4) ddata = ((double[][][][]) hdu.getData().getData())[0][0];
			else {
                Util.warning(this, "Expected FITS data of 2-4 dimensions. Got: " + dims);
                return;
            }
			
			setSize(ddata.length, ddata[0].length);
			signal = ddata;
		}

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) flag[i][j] = Double.isNaN(signal[i][j]) ? 1 : 0;	

		//estimateWeightImage(Double.NaN);
		for(int i=0; i<sizeX; i++) Arrays.fill(weight[i], 1.0);

		rms = getRMS();
		calcS2N();


	}



	public void estimateWeightImage(double blankingS2N) {
		//boolean blanking = !Double.isNaN(blankingS2N);

		if(verbose) System.err.println("Estimating noise... ");

		// Set uniform weights...
		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) weight[i][j] = 1.0;
		SkyMap nyquist = getSkyMap();

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

		if(FWHM > delta) nyquist.regrid(FWHM);
		else FWHM = delta;

		/*
		if(skip != null) for(int i=0; i<nyquist.sizeX; i++) for(int j=0; j<nyquist.sizeY; j++) if(nyquist.flag[i][j] == 0)
			if(skip[i][j]) nyquist.flag[i][j] |= 2;
		 */

		double[][] neighbours = new double[][] {{ 0, -1, 0 }, {-1, 0, -1}, {0, -1, 0}};
		double[][] difference = nyquist.getUniformConvolved(nyquist.signal, neighbours);

		//if(blanking) for(int i=0; i<nyquist.sizeX; i++) for(int j=0; j<nyquist.sizeY; j++) nyquist.flag[i][j] &= ~2;

		System.err.println("weight-estimate> Differencing.");
		for(int i=0; i<nyquist.sizeX; i++) for(int j=0; j<nyquist.sizeY; j++) if(nyquist.flag[i][j] == 0) {
			difference[i][j] += nyquist.signal[i][j];
			nyquist.weight[i][j] = difference[i][j] * difference[i][j];
		}

		System.err.println("weight-estimate> Smoothing squared differences to " + Util.f1.format(5.0 * FWHM/Unit.arcsec) + " arcsec.");
		nyquist.weight = nyquist.getUniformConvolved(nyquist.weight, 4.0 * FWHM);

		for(int i=0; i<nyquist.sizeX; i++) for(int j=0; j<nyquist.sizeY; j++) if(nyquist.flag[i][j] == 0) {
			nyquist.weight[i][j] = 1.0 / nyquist.weight[i][j];
			weight[indexOfdX(nyquist.dXofIndex(i))][indexOfdY(nyquist.dYofIndex(j))] = 1.5 * nyquist.weight[i][j];
		}

		weight = getUniformConvolved(weight, 2.0*FWHM);
		weightScale(getRobustWeight());

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
			count[i][j] = weight[i][j];
		}

		//calc();
		rms = getRMS();
		calcS2N();

		/*
		if(blanking) {
			skip = new boolean[nyquist.sizeX][nyquist.sizeY];
			for(int i=0; i<nyquist.sizeX; i++) for(int j=0; j<nyquist.sizeY; j++) {
				if(nyquist.flag[i][j] == 0) {
					double significance = Math.abs(difference[i][j] * Math.sqrt(nyquist.weight[i][j]));
					if(significance > blankingS2N) skip[i][j] = true;
				}
				else skip[i][j] = true;
			}
		}
		 */
	}


	public void editMasterHeader(Fits fits) throws HeaderCardException, FitsException, IOException {

	    Header master = fits.getHDU(0).getHeader();
		Cursor<String, HeaderCard> c = FitsToolkit.endOf(master);
	
		double FWHM = Math.sqrt(instrumentFWHM * instrumentFWHM + smoothFWHM * smoothFWHM);		

		c.add(new HeaderCard("CREATOR", "crushtools", "CRUSH image manipulation tools."));
		c.add(new HeaderCard("CRUSHVER", version, "Software version information."));
		c.add(new HeaderCard("DATE", FitsDate.getFitsDateString(), "The date of creation"));
		c.add(new HeaderCard("OBJECT", sourceName, "Source name as it appears in the UIP catalog"));

		if(origin != null) 
			c.add(new HeaderCard("ORIGIN", origin, "California Institute of Technology"));
		if(telescope != null) 
			c.add(new HeaderCard("TELESCOP", telescope, "Caltech Submillimeter Observatory"));
		if(instrument != null)
			c.add(new HeaderCard("INSTRUME", instrument, "Submillimeter High Angular Resolution Camera 2"));

		// The data descriptors
		c.add(new HeaderCard("BEAM", instrumentFWHM / Unit.arcsec, "The instrument FWHM (arcsec) of the beam."));
		c.add(new HeaderCard("BMAJ", FWHM / Unit.deg, "Image beam major axis (deg)"));
		c.add(new HeaderCard("BMIN", FWHM / Unit.deg, "Image beam minor axis (deg)."));
		c.add(new HeaderCard("BPA", 0.0, "Image beam position angle (deg)."));
		c.add(new HeaderCard("RESOLUTN", getImageFWHM() / Unit.arcsec, "{Deprecated} Effective image FWHM (arcsec)."));	

		
		c.add(new HeaderCard("IBMAJ", instrumentFWHM / Unit.deg, "Major axis of the instrument beam (deg)."));
		c.add(new HeaderCard("IBMIN", instrumentFWHM / Unit.deg, "Minor axis of the instrument beam (deg)."));
		c.add(new HeaderCard("IBPA", 0.0, "Instrument beam position angle (deg)."));
		
		c.add(new HeaderCard("SBMAJ", smoothFWHM / Unit.deg, "Major axis of the smoothing beam (deg)."));
		c.add(new HeaderCard("SBMIN", smoothFWHM / Unit.deg, "Minor axis of the smoothing beam (deg)."));
		c.add(new HeaderCard("SBPA", 0.0, "Smoothing beam position angle (deg)."));	
		c.add(new HeaderCard("SMOOTH", smoothFWHM / Unit.arcsec, "{Deprecated} FWHM (arcsec) of the smoothing applied."));	
			
	
		if(!Double.isNaN(extFilterFWHM)) {
			c.add(new HeaderCard("XBMAJ", extFilterFWHM / Unit.deg, "Major axis of the extended filter beam (deg)."));
			c.add(new HeaderCard("XBMIN", extFilterFWHM / Unit.deg, "Minor axis of the extended filter beam (deg)."));
			c.add(new HeaderCard("XBPA", 0.0, "Extended filter beam position angle (deg)."));
			c.add(new HeaderCard("EXTFLTR", extFilterFWHM / Unit.arcsec, "{Deprecated} Extended filter FWHM (arcsec)."));
		}
			
		if(!Double.isNaN(fluxCorrectingFWHM)) {
			c.add(new HeaderCard("CBMAJ", fluxCorrectingFWHM / Unit.deg, "Major axis for correct fluxes (deg)."));
			c.add(new HeaderCard("CBMIN", fluxCorrectingFWHM / Unit.deg, "Minor axis for correct fluxes (deg)."));
			c.add(new HeaderCard("CBPA", 0.0, "Position angle of correct fluxes (deg)."));
			c.add(new HeaderCard("CORRECTN", fluxCorrectingFWHM / Unit.arcsec, "{Deprecated} Fluxes correct for FWHM (arcsec)."));
		}
		

		c.add(new HeaderCard("SMTHRMS", true, "Is the Noise (RMS) image smoothed?"));
		
		if(!Double.isNaN(integrationTime))
		    c.add(new HeaderCard("INTEGRTN", integrationTime / Unit.s, "The total integration time in seconds."));

		if(!Double.isNaN(filterBlanking))
			c.add(new HeaderCard("FLTRBLNK", filterBlanking, "The S/N blanking of extended structure filters."));
		if(!Double.isNaN(clippingS2N))
			c.add(new HeaderCard("CLIPS2N", clippingS2N, "The S/N clipping level used in reduction."));

		c.add(new HeaderCard("RAP", 0.0, "The apparent RA pointing offset (arcsec). Edit manually."));
		c.add(new HeaderCard("DECP", 0.0, "The apparent DEC pointing offset (arcsec). Edit manually."));

		c.add(new HeaderCard("V2JY", Jy/Unit.V, "The conversion factor from V to Jy/Cassegrain-Pixel (4.77 x 4.93)."));
	
		if(weightFactor != 1.0) c.add(new HeaderCard("XWEIGHT", weightFactor, "Ratio of calculated to measured map noise."));

		c.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++) 
			FitsToolkit.addLongKey(c, "ARG" + (i+1), options[i], "CRUSH Option");


		// Add the system descriptors...
		c.add(new HeaderCard("JAVA", Util.getProperty("java.vendor"), "Java vendor name."));
		c.add(new HeaderCard("JAVAVER", Util.getProperty("java.version"), "The Java version."));
		c.add(new HeaderCard("JAVAHOME", Util.getProperty("java.home"), "Java location."));
		c.add(new HeaderCard("JRE", Util.getProperty("java.runtime.name"), "Java Runtime Environment."));
		c.add(new HeaderCard("JREVER", Util.getProperty("java.runtime.version"), "JRE version."));
		c.add(new HeaderCard("JVM", Util.getProperty("java.vm.name"), "Java Virtual Machine."));
		c.add(new HeaderCard("JVMVER", Util.getProperty("java.vm.version"), "JVM version."));

		c.add(new HeaderCard("OS", Util.getProperty("os.name"), "Operation System name."));
		c.add(new HeaderCard("OSVER", Util.getProperty("os.version"), "OS version."));
		c.add(new HeaderCard("OSARCH", Util.getProperty("os.arch"), "OS architecture."));

		c.add(new HeaderCard("CPUS", Runtime.getRuntime().availableProcessors(), "Number of CPU cores available."));
		c.add(new HeaderCard("DMODEL", Util.getProperty("sun.arch.data.model"), "Bits in data model."));
		c.add(new HeaderCard("CPUEND", Util.getProperty("sun.cpu.endian"), "CPU Endianness."));
		c.add(new HeaderCard("MAXMEM", Runtime.getRuntime().maxMemory() / (1024 * 1024), "MB of available memory."));

		c.add(new HeaderCard("COUNTRY", Util.getProperty("user.country"), "The user country."));
		c.add(new HeaderCard("LANGUAGE", Util.getProperty("user.language"), "The user language."));

		
		addHistory(master);
		
		
		if(header != null) {
            Cursor<String, HeaderCard> in = header.iterator();
            
            while(in.hasNext()) {
                HeaderCard card = in.next();
                String key = card.getKey();
                if(!master.containsKey(key)) c.add(card);
            }
        }
        
		
	} 

	
	
	public void changeKey(String key, String value) throws HeaderCardException {
		if(!header.containsKey(key)) return;
		
		String comment = header.findCard(key).getComment();
		
		HeaderCard card = null;
		try{ card = new HeaderCard(key, Util.parseBoolean(value), comment); }
        catch(NumberFormatException e1) { 
            try{ card = new HeaderCard(key, Integer.parseInt(value), comment); }
            catch(NumberFormatException e2) {
                try{ card = new HeaderCard(key, Double.parseDouble(value), comment); }
                catch(NumberFormatException e3) { card = new HeaderCard(key, value, comment); }
            }
        }
		
		header.updateLine(key, card);
	}

	public void clearRegions() { regions = new Vector<Region>(); }

	public double getRMS(int reg) { return regions.get(reg).getRMS(); }

	public double getMeanNoise(int reg) { return regions.get(reg).getMeanNoise(); }

	public double getMeanExposure(int reg) { return regions.get(reg).getMeanExposure(); }

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

	public void profile(boolean exitOnComplete) {
		if(verbose) System.err.print("Getting radial profiles...");
		for(Region source : regions) {
			try { source.radialProfile(); }
			catch(IOException e) { e.printStackTrace(); }
		}
		if(verbose) System.err.println();
		if(exitOnComplete) System.exit(0);
	}
	
	public void angularProfile(double rmin, double resolution, boolean exitOnComplete) {
        if(verbose) System.err.print("Getting angular profiles...");
        for(Region source : regions) {
            try { source.angularProfile(0.0, resolution); }
            catch(IOException e) { e.printStackTrace(); }
        }
        if(verbose) System.err.println();
        if(exitOnComplete) System.exit(0);
    }
	
	public void writeSources() throws Exception {

		fileName = workPath + sourceName + ".sources.fits";

		for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) signal[i][j] = 0.0;

		for(Region source : regions) {
			source.unflag(FLAG_DETECTION);
			source.add(true);
		}

		write();
	}		

	public void addSources() {
		if(verbose) System.err.print("Adding Gaussian regions...");
		for(Region source : regions) source.add(false);
		if(verbose) System.err.println();
	}

	public void addPointSources() {
		if(verbose) System.err.print("Adding regions as point sources...");
		for(Region source : regions) source.addPoint(false);
		if(verbose) System.err.println();
	}

	public void removeSources() {
		if(verbose) System.err.print("Removing Gaussian regions....");
		for(Region source : regions) source.subtract(false);	
		if(verbose) System.err.println();
	}

	public void removePointSources() {
		if(verbose) System.err.print("Removing regions as point sources....");
		for(Region source : regions) source.subtractPoint(false);	
		if(verbose) System.err.println();
	}

	public void flagRegions() {
		if(verbose) System.err.print("Flagging regions...");
		for(Region source : regions) source.flag(4);	
		for(Polygon p : polygons) p.flag(4);	
		if(verbose) System.err.println();		
	}

	public void flagOutsideRegions() {
        if(verbose) System.err.print("Flagging outside regions...");
        for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) 
            if(!isInsideRegions(i, j)) flag[i][j] = 4;
    }
	

	public void matchPeaks(double pointingRMS) {
		System.err.println("Matching source peaks...");

		calcS2N();
		uncorrect();

		for(Region source : regions) {
			source.comment = Util.f2.format(source.peak / source.dpeak);
			source.comment += "  " + Util.f2.format(valueAtOffset(s2n, source.dX, source.dY));
		}

		for(Region source : regions) {
			try {
				// TODO something bit more rigorous?
				final int i = indexOfdX(source.dX);
				final int j = indexOfdY(source.dY);
				if(i >= 0 && i < sizeX) if(j >= 0 && j<sizeY) {
					source.peak = signal[i][j];
					source.dpeak = rms[i][j];
				}
					
				source.setSearchRadius(pointingRMS);
				source.moveToPeak();
				source.correct(instrumentFWHM);
				source.subtractPoint(true);
				source.flag(4);
			}
			catch(IllegalStateException e) {}
		}

		updateStats(true, true);

		double cumulative = 0.0;

		for(Region source : regions) {
			double significance = source.peak / source.dpeak;
			double confidence = significance > 0.0 ?
					CumulativeNormalDistribution.at(significance) : CumulativeNormalDistribution.complementAt(-significance);	
					//ConfidenceCalculator.getConfidence(significance) : 1.0 - ConfidenceCalculator.getConfidence(-significance);	

					//System.err.println("### " + source.dpeak);
					
					double falses = 1.0 - Math.pow(confidence, countIndependentPoints(Math.PI * source.radius * source.radius));
					source.comment = "  " + Util.f2.format(source.peak / source.dpeak) + " " + Util.f1.format(100.0*falses) + "%";			
					source.id += "[" + (int)Math.round(100.0 * (1.0 - falses)) + "]";

					if(source.dpeak > 0.0) {
						System.out.println(source.toString());
						cumulative += falses;
					}
		}

		System.out.println("# Effective false detection rate is " + Util.f2.format(cumulative));
	}

	public void extractRegions(double pointingRMS) {
		calcS2N();
		uncorrect();

		for(Region source : regions) {
			try {
				source.counterPart(pointingRMS);
				source.correct(instrumentFWHM);
				if(!source.comment.equals("---")) {
					source.subtractPoint(true);
					source.addGaussian(instrumentFWHM, 1.0);
				}
			}
			catch(IllegalStateException e) {
				source.comment = "---";
			}
		}	
		for(Region source : regions) if(!source.comment.equals("---")) {
			System.out.println(source.toString());
		}
	}



	public void cleanPolygons() {
		int i=0;
		while(i<polygons.size()) {
			if(polygons.get(i).size() == 0) polygons.remove(i);
			else i++;
		}
	}

	// Polygon stores the indeces of the vertexes...
	class Polygon extends Vector<Vector2D> {	
		/**
		 * 
		 */
		private static final long serialVersionUID = 3592519416690624790L;

		String name = "polygon";
		boolean isClosed = false;

		public Polygon(String fileName) throws IOException {
			parse(fileName); 
		}

		public Polygon(int id) {
			name = "Aperture " + id;
		}

		public void close() { isClosed = true; }

		public boolean isInside(double x, double y) {
			int below = 0;

			for(int i=0; i<size(); i++) {
				double intersect = intersectForX(x, i);
				if(intersect <= y) below++;
			}

			return below%2 == 1;

		}

		public double intersectForX(double x, int sideIndex) {
			Vector2D from = get(sideIndex);
			Vector2D to = get((sideIndex+1)%size());

			double minx = Math.min(from.x(), to.x());
			double maxx = Math.max(from.x(), to.x());
			if(x < minx || x > maxx) return Double.NaN;

			return from.y() + (to.y()-from.y())*(x-from.x())/(to.x() - from.x());
		}

		public Vector2D[] getLimits() {
			Vector2D min = (Vector2D) get(0).clone();
			Vector2D max = (Vector2D) get(0).clone();

			for(Vector2D vertex : this) {
				if(vertex.x() < min.x()) min.setX(vertex.x());
				if(vertex.y() < min.y()) min.setY(vertex.y());
				if(vertex.x() > max.x()) max.setX(vertex.x());
				if(vertex.y() > max.y()) max.setY(vertex.y());				
			}	

			return new Vector2D[] { min, max };
		}

		public Rectangle getBounds() {
			Vector2D[] limits = getLimits();

			int fromx = Math.max(0, (int)Math.floor(limits[0].x()));
			int tox = Math.min(sizeX-1, (int)Math.ceil(limits[1].x()));
			int fromy = Math.max(0, (int)Math.floor(limits[0].y()));
			int toy = Math.min(sizeY-1, (int)Math.ceil(limits[1].y()));

			return new Rectangle(fromx, fromy, tox-fromx+1, toy-fromy+1);
		}

		public void flag(int pattern) {
			Rectangle bounds = getBounds(); 

			int tox = bounds.x + bounds.width;
			int toy = bounds.y + bounds.height;

			for(int x = bounds.x; x <= tox; x++) for(int y = bounds.y; y <= toy; y++) {
				if(flag[x][y] == 0) if(isInside(x, y)) flag[x][y] |= pattern;
			}
		}

		public DataPoint getFlux() {
			DataPoint flux = new DataPoint();
			Rectangle bounds = getBounds(); 

			int tox = bounds.x + bounds.width;
			int toy = bounds.y + bounds.height;

			for(int x = bounds.x; x <= tox; x++) for(int y = bounds.y; y <= toy; y++) {
				if(flag[x][y] == 0) if(isInside(x, y)) {
					flux.add(signal[x][y]);
					flux.addWeight(1.0 / weight[x][y]);
				}
			}


			flux.setWeight(1.0 / flux.weight() / pointsPerBeam);
			flux.scale(delta*delta / getImageBeamArea() / janskyPerBeam.value());

			return flux;
		}

		public double getInsideLevel() {
			double sum = 0.0, sumw = 0.0;
			Rectangle bounds = getBounds(); 

			int tox = bounds.x + bounds.width;
			int toy = bounds.y + bounds.height;

			for(int i = bounds.x; i <= tox; i++) for(int j = bounds.y; j <= toy; j++) {
				if(isInside(i, j)) if(flag[i][j] == 0) {
					sum += weight[i][j] * signal[i][j];
					sumw += weight[i][j];
				}
			}
			return sum / sumw;			
		}

		public double getArea() {
			Rectangle bounds = getBounds(); 

			int tox = bounds.x + bounds.width;
			int toy = bounds.y + bounds.height;

			int n = 0;

			for(int i = bounds.x; i <= tox; i++) for(int j = bounds.y; j <= toy; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) n++;

			return n * delta * delta;
		}


		public double getRMS() {
			double var = 0.0;
			int n = 0;
			double level = getInsideLevel();

			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++)
				if(flag[i][j] == 0) if(isInside(i, j)) {
					double value = signal[i][j] - level;
					var += value * value;
					n++;
				}
			var /= (n-1);

			return Math.sqrt(var) / janskyPerBeam.value();
		}

		public void parse(String fileName) throws IOException {
			name = fileName.substring(Math.max(0, 1 + fileName.lastIndexOf(File.separator)));
			clear();

			BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(fileName)));
			String line = null;

			Vector2D offset = new Vector2D();

			while((line = in.readLine()) != null) if(line.length() > 0) if(line.charAt(0) != '#') {
				SphericalCoordinates coords = (SphericalCoordinates) projection.getReference().clone();
				coords.parse(line);
				projection.project(coords, offset);
				add(new Vector2D(fracIndexOfdX(offset.x()), fracIndexOfdY(offset.y())));
			}
			in.close();

			close();
		}

	}




	// Regions
	class Region {
		String id = "unspecified";
		String comment = "";
		double dX, dY, radius, peak, max;
		double sigmaX, sigmaY, dradius, dpeak, dmax;
		protected int fromi, fromj, toi, toj;
		boolean isCorrected = false;

		public Region() { init(); }

		public Region(double offsetX, double offsetY, double r) {
			this(offsetX, offsetY, r, false);
		}

		public Region(double offsetX, double offsetY, double r, boolean guessPeak) {
			this(); dX = offsetX; dY=offsetY; radius = r; 
			if(guessPeak) moveToPeak();
		}

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

		protected void getLimits() { getLimits(1.0); }

		protected void getLimits(double beams) {
			getLimits(beams, radius);
		}

		protected void getLimits(double beams, double FWHM) {
			fromi = Math.max(0, indexOfdX(dX - beams * FWHM));
			toi = Math.min(sizeX-1, indexOfdX(dX + beams * FWHM));
			fromj = Math.max(0, indexOfdY(dY - beams * FWHM));
			toj = Math.min(sizeY-1, indexOfdY(dY + beams * FWHM));
		}

		public void clear() {
			getLimits();
			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) signal[i][j] = 0.0;
		}

		public double getAverage() {
			getLimits();
			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 += signal[i][j]; n++; }

			ave /= n;
			return ave;
		}

		public void init() { id = sourceName + "-" + (regions.size() + 1); }

		public double distanceFrom(int i, int j) {
			double temp = dXofIndex(i) - dX;
			double r = temp*temp;
			temp = dYofIndex(j) - dY;
			r += temp*temp;
			return Math.sqrt(r);
		}

		public boolean isInside(int i, int j) { return distanceFrom(i, j) <= radius; }

		public void scale(double factor) {
			peak *= factor;
			dpeak *= factor;			
		}

		public void moveToPeak() throws IllegalStateException {
			getLimits();

			int i0 = indexOfdX(dX);
			int j0 = indexOfdY(dY);

			if(i0 < 0 || i0 >= sizeX || j0 < 0 || j0 >= sizeY) throw new IllegalStateException("Region falls outside of map.");

			double significance = Double.NEGATIVE_INFINITY;

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(flag[i][j] == 0) 
				if(s2n[i][j] > significance) if(isInside(i, j)) {
					significance = s2n[i][j];
					i0 = i;
					j0 = j;				
				}

			if(flag[i0][j0] != 0) throw new IllegalStateException("No valid peak in search area. ");

			dX = dXofIndex(i0);
			dY = dYofIndex(j0);

			peak = signal[i0][j0];
			dpeak = rms[i0][j0];	

			finetunePeak();
		}

		public void counterPart(double pointingRMS) {	
			double dX0 = dX;
			double dY0 = dY;

			double maxr = ExtraMath.hypot(instrumentFWHM, 2.0*pointingRMS);
			radius = maxr;

			double area = 0.0;

			for(int i=0; i<5; i++) {
				moveToPeak();
				dX = dX0;
				dY = dY0;				
				setSearchRadius(pointingRMS);
				if(Double.isNaN(radius)) radius = maxr;
				else if(radius > maxr) radius = maxr;
				area = getArea();
			}

			if(area == 0.0) {
				comment = "---";
				return;
			}

			comment = Util.f2.format(peak / dpeak) + "\t";
			double points = countIndependentPoints(area);
			double Q = CumulativeNormalDistribution.complementAt(peak / dpeak); 
			//ConfidenceCalculator.getOutsideProbability(peak/dpeak);
			double conf = Q < 1e-4 ? 1.0 - points*Q :  Math.pow(1.0 - Q, points);
			id += "[" + (int)Math.round(100.0*conf) + "%]";
		}

		public DataPoint getFlux() {
			getLimits();
			double flux = 0.0, var = 0.0;

			double A = (delta*delta) / getImageBeamArea() / janskyPerBeam.value();
			//double level = getLevel();

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) {
					//flux += signal[i][j] - level;
					flux += signal[i][j];
					var += 1.0 / weight[i][j];
				}

			return new DataPoint(A * flux, A * Math.sqrt(var * pointsPerBeam));
		}

		public double getInsideLevel() {
			getLimits();
			double sum = 0.0, sumw = 0.0;
			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) {
					sum += weight[i][j] * signal[i][j];
					sumw += weight[i][j];
				}
			return sum / sumw;			
		}

		public double getRMS() {
			getLimits();
			double var = 0.0;
			int n = 0;
			double level = getInsideLevel();

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) {
					double value = signal[i][j] - level;
					var += value * value;
					n++;
				}
			var /= (n-1);

			return Math.sqrt(var) / janskyPerBeam.value();
		}


		public double getMeanNoise() {
			getLimits();
			double var = 0.0;
			int n = 0;

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) {
					double value = rms[i][j];
					var += value * value;
					n++;
				}
			var /= (n-1);

			return Math.sqrt(var) / janskyPerBeam.value();
		}

		public double getMeanExposure() {	
			getLimits();
			double sum = 0.0;
			int n = 0;

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
				if(isInside(i, j)) if(flag[i][j] == 0) {
					sum += count[i][j];
					n++;
				}

			return n > 0 ? sum / n : 0.0;
		}

		public void getChi2(WeightedPoint chi2, double level) {
			getLimits();
			double twoSigma2 = 2.0 * (radius * radius / 5.54 / (delta*delta));

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(flag[i][j] == 0) {
				double value = signal[i][j] - level;
				double di1 = i-i0;
				double dj1 = j-j0;
				double d2 = di1*di1 + dj1*dj1;
				double dev = (value - peak * Math.exp(-d2/twoSigma2)) / rms[i][j];
				chi2.add(dev * dev);
				chi2.addWeight(1.0);
			}
		}


		public void flag() { flag(1); }

		public void flag(int pattern) {
			getLimits();
			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(isInside(i, j)) flag[i][j] |= pattern;
		}

		public void unflag(int pattern) {
			getLimits();
			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(isInside(i, j)) flag[i][j] &= ~pattern;
		}

		public void radialProfile() throws IOException {
			System.err.println("Profiling " + id);

			int n = Math.max(sizeX, sizeY);
			WeightedPoint[] profile = new WeightedPoint[n];
			for(int i=profile.length; --i >= 0; ) profile[i] = new WeightedPoint();

			for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) if(flag[i][j] == 0) {
				int r = (int)Math.round(ExtraMath.hypot(dXofIndex(i)-dX, dYofIndex(j)-dY) / delta);
				double w = 1.0 / (rms[i][j] * rms[i][j]);
				profile[r].add(w * signal[i][j]);
				profile[r].addWeight(w);
			}

			for(WeightedPoint point : profile) point.scaleValue(1.0 / point.weight());

			String fileName = workPath + id + "-profile.dat";
			PrintWriter out = new PrintWriter(new FileOutputStream(fileName));
			out.println("# r[\"]\tS[Jy/beam]\tdS");

			for(int i=0; i<profile.length; i++) {
				WeightedPoint point = profile[i];

				out.println(
						Util.f1.format(i * delta / Unit.arcsec) + "\t" +
								Util.e3.format(point.value() / janskyPerBeam.value()) + "\t" +
								Util.e3.format(1.0/Math.sqrt(point.weight()) / janskyPerBeam.value())
						);
			}

			out.close();
			System.err.println("Written " + fileName);

		}

		public void angularProfile(double rmin, double resolution) throws IOException {
            System.err.println("Angular profiling " + id);

            int n = (int) Math.ceil(Constant.twoPi / resolution);
            
            WeightedPoint[] profile = new WeightedPoint[n];
            for(int i=profile.length; --i >= 0; ) profile[i] = new WeightedPoint();

            getLimits();
             
            for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++)
                if(isInside(i, j)) if(flag[i][j] == 0) {
                    double r = delta * Math.hypot(i-i0, j-j0);
                    if(r > rmin) {
                        int bin = (int) ((Math.PI + Math.atan2(j-j0, i-i0)) / resolution);
                        double w = 1.0 / (rms[i][j] * rms[i][j]);
                        profile[bin].add(w * signal[i][j]);
                        profile[bin].addWeight(w);   
                    }
                }
            
            for(WeightedPoint point : profile) point.scaleValue(1.0 / point.weight());

            String fileName = workPath + id + "-angular-profile.dat";
            PrintWriter out = new PrintWriter(new FileOutputStream(fileName));
            out.println("# phi[deg]\tS[Jy/beam]\tdS");

            for(int i=0; i<profile.length; i++) {
                WeightedPoint point = profile[i];

                out.println(
                        Util.f3.format(i * resolution / Unit.deg) + "\t" +
                                Util.e3.format(point.value() / janskyPerBeam.value()) + "\t" +
                                Util.e3.format(1.0/Math.sqrt(point.weight()) / janskyPerBeam.value())
                        );
            }

            out.close();
            System.err.println("Written " + fileName);

        }
		
		public double getArea() {
			int n = 0;
			getLimits();
			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) if(isInside(i, j)) if(flag[i][j] == 0) n++;
			return n * delta * delta;
		}

		public void addGaussian(double FWHM, double scaling) {	
			getLimits(3.0, FWHM);
			final double twoSigma2 = 2.0 * (FWHM * FWHM / 5.54 / (delta*delta));

			final double ic = fracIndexOfdX(dX);
			final double jc = fracIndexOfdY(dY);

			for(int i=fromi; i<=toi; i++) for(int j=fromj; j<=toj; j++) {
				final double di1 = i-ic;
				final double dj1 = j-jc;
				signal[i][j] += scaling * peak * Math.exp(-(di1*di1 + dj1*dj1) / twoSigma2);
				s2n[i][j] = signal[i][j] / rms[i][j];
			}

		}

		public void add(boolean adjustOthers) { add(radius, 1.0, adjustOthers); }

		public void addPoint(boolean adjustOthers) { add(instrumentFWHM, 1.0, adjustOthers); }	

		public void subtract(boolean adjustOthers) { add(radius, -1.0, adjustOthers); }

		public void subtractPoint(boolean adjustOthers) { add(instrumentFWHM, -1.0, adjustOthers); }

		public void add(final double FWHM, final double scaling, final boolean adjustOthers) {			
			// Remove source only after the filtering corrections are applied to its flux...
			addGaussian(ExtraMath.hypot(FWHM, smoothFWHM), scaling);

			if(Double.isNaN(extFilterFWHM)) return;

			final double filterFWHM = ExtraMath.hypot(FWHM, extFilterFWHM);
			final double filterSigma2 = filterFWHM * filterFWHM / 5.54;			
			final double S2N = peak / dpeak;			

			// Correct for filtering.
			double filterFraction = 1.0 - 1.0 / getExtFilterCorrectionFactor(FWHM);
			// Consider that only the tip of the source might escape the filter...
			filterFraction *= Double.isNaN(filterBlanking) ? 1.0 : Math.min(1.0, filterBlanking / S2N);
			addGaussian(filterFWHM, -scaling * filterFraction);
			//peak += scaling * filterBeamScale;

			double filterPeak = -filterFraction * scaling * peak;

			// Adjust prior detections.for the filtering around this one.
			if(adjustOthers) for(Region other : regions) if(other != this) {
				final double dx = other.dX - dX;
				final double dy = other.dY - dY;
				final double mHalfD2 = -0.5 * (dx*dx + dy*dy) / filterSigma2;

				if(!Double.isNaN(extFilterFWHM))
					other.peak += Math.exp(mHalfD2) * filterPeak;
			}

		}

		public double getCorrectionFactor(double FWHM) {	
			double correction = 1.0;
			final double S2N = peak / dpeak;			

			// Correct for filtering.
			// Consider that only the tip of the source might escape the filter...
			if(!Double.isNaN(extFilterFWHM)) {
				double filterFraction = Double.isNaN(filterBlanking) ? 1.0 : Math.min(1.0, filterBlanking / S2N);
				double filtering = 1.0 - 1.0 / getExtFilterCorrectionFactor(FWHM);
				correction *= 1.0 / (1.0 - filtering * filterFraction);
			}

			return correction;
		}

		public void correct(double FWHM) {	
			if(isCorrected) throw new IllegalStateException("Source is already corrected.");
			double correction = getCorrectionFactor(FWHM);
			peak *= correction;
			dpeak *= correction;
			isCorrected = true;
		}

		public void uncorrect(double FWHM) {
			if(!isCorrected) throw new IllegalStateException("Source is already uncorrected.");
			double correction = getCorrectionFactor(FWHM);
			peak /= correction;
			dpeak /= correction;
			isCorrected = false;
		}

		public void finetunePeak() {
			int i = indexOfdX(dX);
			int j = indexOfdY(dY);

			if(flag[i][j] != 0) return;

			double a=0.0,b=0.0,c=0.0,d=0.0;	
			double y0 = s2n[i][j];

			if(i>0 && i<sizeX-1) if((flag[i+1][j] | flag[i-1][j]) == 0) {
				a = 0.5 * (s2n[i+1][j] + s2n[i-1][j]) - y0;
				c = 0.5 * (s2n[i+1][j] - s2n[i-1][j]);
			}

			if(j>0 && j<sizeY-1) if((flag[i][j+1] | flag[i][j-1]) == 0) {
				b = 0.5 * (s2n[i][j+1] + s2n[i][j-1]) - y0;
				d = 0.5 * (s2n[i][j+1] - s2n[i][j-1]);
			}


			double di = (a == 0.0) ? 0.0 : -0.5*c/a;
			double dj = (b == 0.0) ? 0.0 : -0.5*d/b;		

			if(Math.abs(di) > 0.5) di = 0.0;
			if(Math.abs(dj) > 0.5) dj = 0.0;

			final double significance = y0 + (a*di + c)*di + (b*dj + d)*dj;			

			dX = dXofIndex(i + di);
			dY = dYofIndex(j + dj);

			peak = valueAtIndex(i + di, j + dj);
			dpeak = peak / significance;
		}


		public void setSearchRadius(double pointingRMS) {
			double beamSigma = instrumentFWHM / Constant.sigmasInFWHM;		
			radius = Math.sqrt(4.0 * pointingRMS * pointingRMS - 2.0 * beamSigma * beamSigma * Math.log(1.0 - 2.0 * Math.abs(dpeak/peak)));	
			double maxr = ExtraMath.hypot(instrumentFWHM, pointingRMS);
			if(radius > maxr) radius = maxr;
		}

		@Override
		public String toString() {
			SphericalCoordinates coords = (SphericalCoordinates) projection.getReference().clone();
			if(coords instanceof CelestialCoordinates) if(!(coords instanceof EquatorialCoordinates)) 
				coords = ((CelestialCoordinates) coords).toEquatorial();

			coords.addNativeOffset(new Vector2D(dX, dY));

			((AngleFormat) coords.getCoordinateSystem().get(0).format).colons();
			((AngleFormat) coords.getCoordinateSystem().get(1).format).colons();

			String line = id + "\t" + coords.toString() + "  " +
					Util.f1.format(radius/Unit.arcsec) + "\t" + Util.e3.format(peak/displayUnit.value()) + " +- " +
					Util.e2.format(dpeak/displayUnit.value()) + " " + displayUnit.name() + "  " + comment;

			// For GILDAS circles...
			/*
			String line = "ellipse " + Util.f1.format(radius/Unit.arcsec) + " /user " +
				Util.f1.format(-dX / Unit.arcsec) + " " + Util.f1.format(dY / Unit.arcsec);
			 */

			// For ds9 circles...
			/*
			String line = "circle(" 
				+ coords.coordinateSystem.get(0).format(coords.x) + ","
				+ coords.coordinateSystem.get(1).format(coords.y) + ","
				+ Util.f3.format(radius / Unit.arcsec) + "\") # false=";
			 */
			return line;
		}


		public void parse(String line) {
			if(line == null) return;
			if(line.length() == 0) return;
			if(line.charAt(0) == '#' || line.charAt(0) == '!') return;

			StringTokenizer tokens = new StringTokenizer(line);

			if(tokens.countTokens() < 5) return;

			id = tokens.nextToken();

			EquatorialCoordinates equatorial = new EquatorialCoordinates();
			equatorial.parse(tokens.nextToken() + " " + tokens.nextToken() + " " + tokens.nextToken());
			equatorial.precess(((EquatorialCoordinates) projection.getReference()).epoch);

			SphericalCoordinates reference = projection.getReference();
			CelestialCoordinates coords;

			if(!(reference instanceof EquatorialCoordinates)) { 	
				coords = (CelestialCoordinates) reference.clone();
				coords.fromEquatorial(equatorial);
			}
			else coords = equatorial;

			Vector2D offset = new Vector2D();
			projection.project(coords, offset);
			dX = offset.x();
			dY = offset.y();

			radius = Double.parseDouble(tokens.nextToken()) * Unit.arcsec;
			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) {}
			}

			Unit unit = nextArg == null ? janskyPerBeam : getUnit(nextArg);
			while(tokens.hasMoreTokens()) comment += tokens.nextToken() + " ";
			comment.trim();

			peak *= unit.value();
			dpeak *= unit.value();
		}


		public void print() {
			String id10 = id + "          ";

			System.out.print(" " + id10.substring(0, 10));

			SphericalCoordinates coords = projection.getReference();


			DataPoint S = new DataPoint(peak, dpeak);
			S.scale(1.0 / displayUnit.value());


			System.out.println(" " + Util.f1.format((coords.isReverseLongitude() ? -dX : dX)/Unit.arcsec)
					+ " +- " + Util.f1.format(sigmaX/Unit.arcsec) + "\t"
					+ Util.f1.format((coords.isReverseLatitude() ? -dY : dY)/Unit.arcsec)
					+ " +- " + Util.f1.format(sigmaY/Unit.arcsec) + "\t"
					+ Util.f1.format(radius/Unit.arcsec) 
					+ " +- " + Util.f1.format(dradius/Unit.arcsec) + "\t"
					+ S.toString()
					);

		}



	}

	public void usage() {
		System.out.println();
		System.out.println("    Display Operations:");
		System.out.println("\tmouse\tDragging with right button (Ctrl) moves image inside window.");
		System.out.println("\t      \tWheel motion (or two-finger scroll) changes zoom.");
		System.out.println("\tarrows\tMove image around in steps.");
		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,k\tToggle between color schemes.");
		System.out.println("\ti\tInvert colors.");
		System.out.println("\tx\tToggle between linear, logarithmic, and square-root scaling.");
		System.out.println("\t<,>\tToggle between Source, RMS, and S/N maps.");
		System.out.println("\ta\tToggle appearance between rendering speed & quality.");

		System.out.println();
		System.out.println("    Operations for Regions:");
		System.out.println("\tmouse\tCircles: Draw custom circle with left button, or 1x, 2x and 3x");
		System.out.println("\t     \t  beam radii apertures with left, middle (Alt), and right (Ctrl)");
		System.out.println("\t     \t  clicks."); 
		System.out.println("\t     \tPolygons: adds/remove a vertex with a left/right (Ctrl) click.");
		System.out.println("\tD\tDelete last circular region.");
		System.out.println("\tR\tRemove/Forget all regions (circles and polygons).");
		System.out.println("\tP\tStart/Finish drawing a polygonal region.");
		System.out.println("\tr\tDisplay regions (default).");
		System.out.println("\tn\tToggle displaying region names.");
		System.out.println("\tl\tLevel to zero as measured outside the regions.");
		System.out.println("\tg\tGradient removal as measured outside regions.");
		System.out.println("\tf\tFit Gaussians to the circular regions.");
		System.out.println("\tF\tPrint aperture fluxes inside the regions.");
		System.out.println("\tN\tPrint RMS values inside the regions.");

		System.out.println();
		System.out.println("    Miscellaneous Operations:");
		System.out.println("\t?\tDisplay image information summary.");
		System.out.println("\tA\tPrint map area summary.");
		System.out.println("\tm\tToggle distance measure mode on/off.");
		System.out.println("\tQ,q\tConvolve/deconvolve using the default Gaussian beam.");
		System.out.println("\tS,s\tResample onto a coarser/finer grid.");
		System.out.println("\tT\tFourier transform.");
		System.out.println("\tp\tPerform a pointing fit.");
		System.out.println("\tE\tExtended-structure filter (for point sources).");
		System.out.println("\tu\tChange display units.");
		//System.out.println("\tU\tUndo the last operation.");
		System.out.println("\tw\tWrite FITS image to disk.");
		System.out.println("\tW\tWrite regions (mask) file.");
	}


	class MapFrame extends JFrame {  

		/**
		 * 
		 */
		private static final long serialVersionUID = 362807547865045787L;

		JMenuBar menuBar;
		final JFileChooser fitsChooser = new JFileChooser();
		final JFileChooser maskChooser = new JFileChooser();
		boolean resizing = false;

		private FileMenu fileMenu;
		private EditMenu editMenu;
		private ViewMenu viewMenu;
		private RegionMenu regionMenu;
		private ToolsMenu toolsMenu;
		private HelpMenu helpMenu;


		public MapFrame() {
			super();

			setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

			display = new MapPanel();	    

			fitsChooser.addChoosableFileFilter(new FileNameExtensionFilter("FITS images (.fits)", "fits", "FITS"));
			fitsChooser.setFileFilter(fitsChooser.getChoosableFileFilters()[1]);
			maskChooser.addChoosableFileFilter(new FileNameExtensionFilter("CRUSH Masks (.mask, .cat)", "mask", "MASK", "cat", "CAT"));
			maskChooser.setFileFilter(maskChooser.getChoosableFileFilters()[1]);


			Container contentPane = getContentPane();
			contentPane.add(display, "Center");
			contentPane.add(display.scaleBox, "East");
			contentPane.add(display.infobar, "North");

			display.setEnabled(isLoaded());
			display.scaleBox.setVisible(isLoaded());
			display.infobar.setVisible(isLoaded());

			updateTitle();

			// Workaround for top window names in UNIX
			try {
				Toolkit xToolkit = Toolkit.getDefaultToolkit();
				java.lang.reflect.Field awtAppClassNameField = xToolkit.getClass().getDeclaredField("awtAppClassName");
				awtAppClassNameField.setAccessible(true);
				awtAppClassNameField.set(xToolkit, "CRUSH's show tool");
			}
			catch(Exception e) {}

			ArrayList<BufferedImage> icons = new ArrayList<BufferedImage>();		

			String[] iconNames = new String[] { "crush-256x256.png", "crush-64x64.png", "crush-48x48.png", "crush-32x32.png", "crush-24x24.png", "crush-16x16.png" };

			for(String iconName : iconNames) {
				try { icons.add(ImageIO.read(getClass().getResource("/resource/" + iconName))); }
				catch(Exception e) {}
			}

			if(!icons.isEmpty()) setIconImages(icons);

			menuBar = new JMenuBar();
			createMenu(menuBar);
			setJMenuBar(menuBar);

			display.infobar.setPreferredSize(getFontMetrics(display.infobar.font));

			pack();

			if(fileName != null) updateChooserPaths();


			addKeyListener(new KeyAdapter() {
				@Override
				public void keyPressed(KeyEvent evt) {
					int code = evt.getKeyCode();
					switch(code) {
					case KeyEvent.VK_LEFT: display.di -= 0.01 * sizeX; display.repaint(); break;
					case KeyEvent.VK_RIGHT: display.di += 0.01 * sizeX; display.repaint(); break;
					case KeyEvent.VK_UP: display.dj += 0.01 * sizeY; display.repaint(); break;
					case KeyEvent.VK_DOWN: display.dj -= 0.01 * sizeY; display.repaint(); break;
					}
				}

				@Override
				public void keyTyped(KeyEvent evt) {
					boolean repaint = true;

					switch(evt.getKeyChar()) {
					case '<' :
						display.prevMap(); repaint = false;
						break;
					case '>' :
						display.nextMap(); repaint = false;
						break;
					case '?' : info(); repaint = false; break;
					case 'T' : FFT(); setDefaults(); break;
					case '#' : robustWeight(); break;	

					}



					if(repaint) display.repaint();
				}
			});
		}	

		private void updateTitle() {
			setTitle(sourceName == null ? "CRUSH's show tool" : sourceName);
		}

		private void setPreferredSize() {
			display.setPreferredSize();
			//display.infobar.setPreferredSize(getFontMetrics(display.infobar.font));

			int w = display.getPreferredSize().width;
			if(display.scaleBox.isVisible()) w += display.scaleBox.getPreferredSize().width;
			int h = display.getPreferredSize().height;
			if(display.infobar.isVisible()) h += display.infobar.getPreferredSize().height;

			setSize(new Dimension(w, h));

		}

		private void updateChooserPaths() {
			File path = new File(fileName).getParentFile();	
			fitsChooser.setCurrentDirectory(path);
			maskChooser.setCurrentDirectory(path);
		}



		private void error(String text, String title) { 
			JOptionPane.showMessageDialog(frame, text, title, JOptionPane.ERROR_MESSAGE);
		}

		/*
		private void error(Exception e, String title) { 
			ByteArrayOutputStream os = new ByteArrayOutputStream();
			PrintStream ps = new PrintStream(os);
			e.printStackTrace(ps);
			try { JOptionPane.showMessageDialog(frame, e.getMessage() + "\n\n" + os.toString("UTF8"), title, JOptionPane.ERROR_MESSAGE); }
			catch(Exception e2) {
				JOptionPane.showMessageDialog(frame, e.getMessage(), title, JOptionPane.ERROR_MESSAGE);
			}
		}
		 */


		private void createMenu(JMenuBar menuBar) {
			menuBar.add(fileMenu = new FileMenu());
			menuBar.add(editMenu = new EditMenu());
			menuBar.add(viewMenu = new ViewMenu());
			menuBar.add(regionMenu = new RegionMenu());
			menuBar.add(toolsMenu = new ToolsMenu());
			menuBar.add(helpMenu = new HelpMenu());
		}

		private void updateMenus() {
			fileMenu.update();
			editMenu.update();
			viewMenu.update();
			regionMenu.update();
			toolsMenu.update();
			helpMenu.update();
		}

		private boolean isLoaded() {
			return fileName != null;
		}

		private boolean hasMask() {
			return !regions.isEmpty();
		}

		private class FileMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 3072409304375553888L;
			JMenuItem open, close, reload, save, saveAs, loadMask, writeMask, exit; 

			public FileMenu() {
				super("File");

				open = new JMenuItem("Open...");
				open.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						int returnVal = fitsChooser.showOpenDialog(MapFrame.this);
						boolean wasEmpty = !isLoaded();

						if (returnVal == JFileChooser.APPROVE_OPTION) {
							File file = fitsChooser.getSelectedFile();
							fileName = file.getPath();
							try { 
								regions.clear();
								polygons.clear();
								read(fileName); 
								updateChooserPaths();
								updateMenus();
								display.setMap(signal);
								updateTitle();
								setDefaults();

								if(wasEmpty) {
									viewMenu.infobar.setSelected(true);
									viewMenu.colorbar.setSelected(true);

									display.infobar.setVisible(true);
									display.scaleBox.setVisible(true);
								}

								display.setVisible(true);
								MapFrame.this.setPreferredSize();
								pack();

								display.repaint();
							}
							catch(Exception e) { error(e.getMessage(), "File open error"); }
						}
					}
				});


				close = new JMenuItem("Close");
				close.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						fileName = null;
						sourceName = null;
						setSize(1, 1);

						display.setMap(signal);
						updateTitle();
						updateMenus();
						setDefaults();

						display.setVisible(false);

						viewMenu.infobar.setSelected(false);
						viewMenu.colorbar.setSelected(false);

						display.infobar.setVisible(false);
						display.scaleBox.setVisible(false);

						display.repaint();

					}
				});

				reload = new JMenuItem("Reload");
				reload.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						try { 
							read(fileName); 
							display.setMap(signal);
							updateMenus();
							setDefaults();
							display.repaint();
						}
						catch(Exception e) { error(e.getMessage(), "File open error"); }
					}
				});

				save = new JMenuItem("Save");
				save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0));

				save.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent arg0) {
						try { write(); }
						catch(Exception e) { error(e.getMessage(), "File save error"); }
					}
				});

				saveAs = new JMenuItem("Save As...");
				saveAs.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						int returnVal = fitsChooser.showSaveDialog(MapFrame.this);

						if (returnVal == JFileChooser.APPROVE_OPTION) {
							File file = fitsChooser.getSelectedFile();
							fileName = file.getPath();
							try { write(); }
							catch(Exception e) { error(e.getMessage(), "File save error"); }
						}
					}
				});


				loadMask = new JMenuItem("Load Mask...");
				loadMask.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						int returnVal = maskChooser.showDialog(MapFrame.this, "Load Mask");

						if(returnVal == JFileChooser.APPROVE_OPTION) {
							File file = maskChooser.getSelectedFile();
							fileName = file.getPath();
							try { 
								readMask(fileName); 
								updateMenus();
								display.repaint();
							}
							catch(Exception e) { error(e.getMessage(), "Mask open error"); }
						}
					}
				});

				writeMask = new JMenuItem("Save Mask");
				writeMask.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.SHIFT_MASK));
				writeMask.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent arg0) {
						try { writeMask(); }
						catch(Exception e) { error(e.getMessage(), "Mask save error"); }
					}
				});


				exit = new JMenuItem("Exit");
				exit.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						System.err.println("Exiting.");
						System.exit(0);
					}
				});

				add(open);
				add(reload);
				add(close);
				addSeparator();

				add(save);
				add(saveAs);
				addSeparator();

				add(loadMask);
				add(writeMask);
				addSeparator();

				add(exit);

				update();
			}

			public void update() {
				boolean value = isLoaded();
				reload.setEnabled(value);
				save.setEnabled(value);
				saveAs.setEnabled(value);
				close.setEnabled(value);
				loadMask.setEnabled(value);

				value = hasMask();
				writeMask.setEnabled(value);
			}

		}

		private class EditMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 8186500679962447119L;
			JMenuItem nameChange, unitChange, bgcolor;

			public EditMenu() {
				super("Edit");


				nameChange = new JMenuItem("Source Name...");
				nameChange.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						sourceName = JOptionPane.showInputDialog(MapFrame.this, "Enter New Source Name:", sourceName);
						setTitle(sourceName);
					}
				});

				unitChange = new JMenuItem("Toggle Units");
				unitChange.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_U, 0));
				unitChange.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						if(display.map == s2n) return;

						int index = units.indexOf(displayUnit);
						index++;
						if(index >= units.size()) index = 0;
						Unit oldUnit = displayUnit;
						displayUnit = units.get(index);
						Scale scale = display.scale;
						scale.setScale(scale.min * oldUnit.value()/displayUnit.value(), scale.max * oldUnit.value()/displayUnit.value());

						display.fluxLabel.label = displayUnit.name();
						display.fluxLabel.repaint();

						display.repaint();
					}
				});

				bgcolor = new JMenuItem("Background Color...");
				bgcolor.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						final JColorChooser chooser = new JColorChooser();
						JDialog dialog = JColorChooser.createDialog(null, "Set Background", false, chooser, 
								new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent event) {
								display.setBackground(chooser.getColor());
							}
						},
						new ActionListener() {
							@Override
							public void actionPerformed(ActionEvent event) {
							}
						}
								);
						dialog.setVisible(true);
					}
				});

				// add(undo);
				add(nameChange);
				add(bgcolor);
				add(unitChange);


				update();
			}

			public void update() {
				boolean value = isLoaded();
				nameChange.setEnabled(value);
				unitChange.setEnabled(value);
				bgcolor.setEnabled(value);
			}
		}


		private class ViewMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 1219220382680359733L;
			JMenuItem defaults, contrastUp, contrastDown, brightnessUp, brightnessDown, nextPlane, prevPlane, nextColor, prevColor;
			JMenuItem scaling;
			JMenu zoom;
			JCheckBoxMenuItem rendering, colorbar, infobar, invert;

			public ViewMenu() {

				super("View");

				defaults = new JMenuItem("Default View");
				defaults.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, 0));
				defaults.addActionListener(new ActionListener() {

					@Override
					public void actionPerformed(ActionEvent evt) {
						setDefaults();
						display.repaint();
					}
				});

				rendering = new JCheckBoxMenuItem("No Frills Rendering");
				rendering.setSelected(!isHQRendering);
				rendering.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, 0));
				rendering.addItemListener(new ItemListener() {

					@Override
					public void itemStateChanged(ItemEvent evt) {
						isHQRendering = evt.getStateChange() == ItemEvent.SELECTED ? false : true;
						display.antialias = isHQRendering;
						display.repaint();
					}
				});


				colorbar = new JCheckBoxMenuItem("Show Color Scale");
				colorbar.setSelected(isLoaded());
				display.scaleBox.setVisible(isLoaded());
				colorbar.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						boolean value = evt.getStateChange() == ItemEvent.SELECTED ? true : false;
						display.scaleBox.setVisible(value);
						display.repaint();
					}
				});

				infobar = new JCheckBoxMenuItem("Show Information Panel");
				infobar.setSelected(isLoaded());
				display.infobar.setVisible(isLoaded());
				infobar.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						boolean value = evt.getStateChange() == ItemEvent.SELECTED ? true : false;
						display.infobar.setVisible(value);
						display.repaint();
					}
				});

				contrastUp = new JMenuItem("Contrast +");
				contrastUp.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.SHIFT_MASK));
				contrastUp.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.expandScale(Math.pow(10.0,-0.05));
						display.repaint();
					}
				});

				contrastDown = new JMenuItem("Contrast -");
				contrastDown.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, 0));
				contrastDown.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.expandScale(Math.pow(10.0,0.05));
						display.repaint();
					}
				});

				brightnessUp = new JMenuItem("Brightness +");
				brightnessUp.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, ActionEvent.SHIFT_MASK));
				brightnessUp.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.offsetScale(-0.02);
						display.repaint();
					}
				});

				brightnessDown = new JMenuItem("Brightness -");
				brightnessDown.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, 0));
				brightnessDown.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.offsetScale(0.02);
						display.repaint();
					}
				});


				nextPlane = new JMenuItem("Next Image Plane");
				//nextPlane.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_GREATER, 0));
				nextPlane.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.nextMap();
					}
				});

				prevPlane = new JMenuItem("Previous Image Plane");
				//prevPlane.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_LESS, 0));
				prevPlane.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.prevMap();
					}
				});


				nextColor = new JMenuItem("Next Color Scheme");
				nextColor.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, 0));
				nextColor.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						useScheme=(useScheme+1)%colorSchemes.size(); display.updateRGBImage();
						display.repaint();
					}
				});

				prevColor = new JMenuItem("Previous Color Scheme");
				prevColor.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_K, ActionEvent.SHIFT_MASK));
				prevColor.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						useScheme=(useScheme-1+colorSchemes.size())%colorSchemes.size(); display.updateRGBImage();
						display.repaint();
					}
				});

				invert = new JCheckBoxMenuItem("Invert Colors");
				invert.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_I, 0));
				invert.setSelected(display.inverted);
				invert.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						display.inverted = evt.getStateChange() == ItemEvent.SELECTED ? true : false;
						display.colorScale.invert(); 
						display.updateRGBImage();
						display.repaint();
					}
				});


				scaling = new JMenuItem("Toggle Color Scaling");
				scaling.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0));
				scaling.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						Scale scale = display.scale;
						if(scale.logarithmic) scale.sqrt();
						else if(scale.sqrt) scale.linear();
						else scale.log();
						display.updateRange();
						display.updateRGBImage();
						display.repaint();
					}
				});

				zoom = new JMenu("Zoom...");

				JMenuItem zoomIn = new JMenuItem("Zoom In");
				zoomIn.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.SHIFT_MASK));
				zoomIn.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.zoom *= Constant.sqrt2;
						display.repaint();
					}
				});

				JMenuItem zoomOut = new JMenuItem("Zoom Out");
				zoomOut.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, 0));
				zoomOut.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.zoom /= Constant.sqrt2;
						display.repaint();
					}
				});

				JMenuItem zoomFit = new JMenuItem("Fit into Window");
				zoomFit.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.zoom = Math.min((double) display.getWidth() / sizeX, (double) display.getHeight() / sizeY);
						display.repaint();
					}

				});

				JMenuItem zoomFill = new JMenuItem("Fill Window");
				zoomFill.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						display.zoom = Math.max((double) display.getWidth() / sizeX, (double) display.getHeight() / sizeY);
						display.repaint();
					}
				});

				zoom.add(zoomIn);
				zoom.add(zoomOut);
				zoom.add(zoomFit);
				zoom.add(zoomFill);
				zoom.addSeparator();

				double[] zooms = new double[] { 2.0, 1.5, 1.0, 0.75, 0.50, 1.0/3.0, 0.25 };

				for(final double z : zooms) {
					JMenuItem zoomX = new JMenuItem((int)Math.round(100.0 * z) + "%");
					zoomX.addActionListener(new ActionListener() {
						@Override
						public void actionPerformed(ActionEvent evt) {
							display.zoom = z;
							display.repaint();
						}
					});
					zoom.add(zoomX);
				}

				add(defaults);
				add(rendering);
				addSeparator();
				add(colorbar);
				add(infobar);
				addSeparator();
				add(nextPlane);
				add(prevPlane);
				addSeparator();
				add(nextColor);
				add(prevColor);
				add(invert);
				add(scaling);
				addSeparator();
				add(brightnessUp);
				add(brightnessDown);
				add(contrastUp);
				add(contrastDown);
				addSeparator();
				add(zoom);

				update();
			}

			public void update() {
				boolean value = isLoaded();
				defaults.setEnabled(value);
				rendering.setEnabled(value);
				colorbar.setEnabled(value);
				infobar.setEnabled(value);
				nextPlane.setEnabled(value);
				prevPlane.setEnabled(value);
				nextColor.setEnabled(value);
				prevColor.setEnabled(value);
				invert.setEnabled(value);
				scaling.setEnabled(value);
				contrastUp.setEnabled(value);
				contrastDown.setEnabled(value);
				brightnessUp.setEnabled(value);
				brightnessDown.setEnabled(value);
				zoom.setEnabled(value);
			}
		}


		private class ToolsMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 2937974408031126510L;
			JMenuItem level, reweight, coarser, finer, beamSmooth, smooth, smoothTo, deconvolve, efilter, filter;
			JCheckBoxMenuItem distance;

			public ToolsMenu() {

				super("Tools");

				distance = new JCheckBoxMenuItem("Distance Measure Mode");
				distance.setSelected(display.measureMode);
				distance.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_M, 0));
				distance.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						display.setMeasureMode(evt.getStateChange() == ItemEvent.SELECTED ? true : false);
					}
				});



				level = new JMenuItem("Robust Level");
				level.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_L, 0));
				level.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup(); robustLevel(); display.repaint();
					}
				});


				reweight = new JMenuItem("Robust Noise Rescale");
				//reweight.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_3, ActionEvent.SHIFT_MASK));
				reweight.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup(); robustWeight(); display.repaint();
					}
				});


				coarser = new JMenuItem("Resample onto Coarser Grid");
				coarser.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.SHIFT_MASK));
				coarser.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						resample(2.0 * delta);
						display.repaint();
					}
				});

				finer = new JMenuItem("Resample onto Finer Grid");
				finer.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, 0));
				finer.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						resample(0.5 * delta);
						display.repaint();
					}
				});


				beamSmooth = new JMenuItem("Beam Smooth");
				beamSmooth.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.SHIFT_MASK));
				beamSmooth.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup();
						convolveTo(instrumentFWHM); 
						display.setDefaults();
						display.repaint();
						beamSmooth.setEnabled(false);
					}
				});


				smooth = new JMenuItem("Smooth...");
				smooth.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup();

						String text = 
								"The smoothing will add in quadrature\n" +
										"to the instrument beam to define the\n" +
										"image resolution.\n\n" +
										"The new value must be larger than the\n" +
										"current smoothing of " + Util.F3.format(smoothFWHM / Unit.arcsec) + " arcsec,\n" +
										"which includes smoothing due to\n" +
										"pixelization.\n\n" +
										"The instrument beam is " + Util.F3.format(instrumentFWHM / Unit.arcsec) + " arcsec.\n\n" + 
										"Effective Smoothing FWHM (arcsec):";

						try {

							String sValue = (String) JOptionPane.showInputDialog(frame, text, "Set Smoothing", JOptionPane.QUESTION_MESSAGE, null, null, instrumentFWHM / Unit.arcsec);
							if(sValue == null) return;
							double value = Double.parseDouble(sValue);
							value *= Unit.arcsec;
							if(value < smoothFWHM) {
								error("New smoothing must be larger than current smoothing FWHM.", "Invalid smoothing value");
								return;
							}

							convolveTo(value); 
							display.setDefaults();
							display.repaint();
							updateMenus();
						}
						catch(NumberFormatException e) {
							error("Value must be a number.", "Invalid value");
						}


					}
				});

				smoothTo = new JMenuItem("Resolution...");
				smoothTo.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup();

						String text = 
								"The image resolution is the quadrature sum\n" +
										"of the instrument beam size and the applied\n" +
										"smoothing. Thus, the setting below will\n" +
										"apply the appropriate smoothing to\n" +
										"to produce the desired resolution.\n\n" +
										"The value must be larger than the current\n" +
										"resolution of " + Util.F3.format(getImageFWHM() / Unit.arcsec) + " arcsec.\n\n" +
										"Effective Image Resolution FWHM (arcsec):";

						try {
							String initValue = Util.f1.format(Math.ceil(getImageFWHM() / Unit.arcsec));
							String sValue = (String) JOptionPane.showInputDialog(frame, text, "Set Image Resolution", JOptionPane.QUESTION_MESSAGE, null, null, initValue);
							//String sValue = JOptionPane.showInputDialog(frame, text, imageFWHM / Unit.arcsec);
							if(sValue == null) return;
							double value = Double.parseDouble(sValue);
							value *= Unit.arcsec;
							if(value < getImageFWHM()) {
								error("Requested resolution is below current resolution.", "Invalid value");
								return;
							}

							value = Math.sqrt(value * value - instrumentFWHM * instrumentFWHM);
							convolveTo(value); 
							display.setDefaults();
							display.repaint();
							updateMenus();
						}
						catch(NumberFormatException e) {
							error("Value must be a number.", "Invalid value");
						}


					}
				});


				deconvolve = new JMenuItem("Deconvolve");
				deconvolve.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0));
				deconvolve.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup(); 
						clean(); 
						display.setDefaults();
						display.repaint();
						updateMenus();
					}
				});

				efilter = new JMenuItem("Default Extended Filter");
				efilter.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_E, ActionEvent.SHIFT_MASK));
				efilter.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup(); 
						filterAbove(5.0*instrumentFWHM); 
						display.setDefaults();
						display.repaint();
						updateMenus();
					}
				});

				filter = new JMenuItem("Filter Extended Structures...");
				filter.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						backup();

						String text = 
								"Apply a filter with a Gaussian profile.\n\n" +
										"The filter will skip over the masked regions.\n" +
										"Therefore, you may want to mask the bright\n" +
										"features first to avoid negative bowls around\n" +
										"them.\n\n" +
										(Double.isNaN(extFilterFWHM) ? "" : 
											"The new filter must have FWHM smaller than the\n" +
											"current filter FWHM of " + Util.F3.format(extFilterFWHM / Unit.arcsec) + " arcsec.\n\n"
												) +		
												"Effective Filter FWHM (arcsec):";

						try {

							String sValue = (String) JOptionPane.showInputDialog(frame, text, "Extended Structure Filter", JOptionPane.QUESTION_MESSAGE, null, null, 5.0 * instrumentFWHM / Unit.arcsec);
							//String sValue = JOptionPane.showInputDialog(frame, text, 5.0 * beamFWHM / Unit.arcsec);
							if(sValue == null) return;
							double value = Double.parseDouble(sValue);

							value *= Unit.arcsec;

							if(value > extFilterFWHM) {
								error("New filtering value must be smaller than current filtering FWHM.", "Invalid filter value.");
								return;
							}

							if(value <= smoothFWHM) {
								error("Filtering value must be larger than the current smoothing FWHM.", "Invalid filter value.");
								return;
							}

							filterAbove(value); 
							display.setDefaults();
							display.repaint();
							updateMenus();
						}
						catch(NumberFormatException e) {
							error("Value must be a number.", "Invalid value");
						}

					}
				});

				add(distance);
				addSeparator();
				add(level);
				add(reweight);
				//add(gradient);
				addSeparator();
				add(coarser);
				add(finer);
				addSeparator();
				add(beamSmooth);
				add(smooth);
				add(smoothTo);
				add(deconvolve);
				addSeparator();
				add(efilter);
				add(filter);
				//addSeparator();
				//add(fft);

				update();

			}

			public void update() {
				boolean value = isLoaded();
				level.setEnabled(value);
				reweight.setEnabled(value);
				beamSmooth.setEnabled(smoothFWHM < instrumentFWHM ? value : false);
				smooth.setEnabled(value);
				smoothTo.setEnabled(value);
				deconvolve.setEnabled(value);
				filter.setEnabled(value);
				efilter.setEnabled(extFilterFWHM <= 5.0 * instrumentFWHM || smoothFWHM >= 5.0 * instrumentFWHM ? false : value);
				coarser.setEnabled(value);
				finer.setEnabled(value);
			}


		}



		private void resample(double toDelta) {
			int index = 0;

			if(display.map == signal) index = 0;
			else if(display.map == rms) index = 1;
			else index = 2;

			backup();

			display.zoom *= toDelta / delta;

			regrid(toDelta);
			display.di *= 2.0;
			display.dj *= 2.0;

			if(index == 0) display.setMap(signal);
			else if(index == 1) display.setMap(rms);
			else display.setMap(s2n);

			display.setDefaults();
		}

		private class RegionMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 7325467960192975574L;
			JMenuItem remove, fluxes, rmsReport, flagInside, profile, deleteLast, fit;
			JCheckBoxMenuItem polyMode, draw, labeling;

			public RegionMenu() {

				super("Regions");

				polyMode = new JCheckBoxMenuItem("Polygon Draw Mode");
				polyMode.setSelected(display.polygonDrawMode);
				polyMode.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, ActionEvent.SHIFT_MASK));
				polyMode.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						display.setPolyMode(evt.getStateChange() == ItemEvent.SELECTED ? true : false);
					}
				});


				draw = new JCheckBoxMenuItem("Show Regions");
				draw.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0));
				draw.setSelected(circled);
				draw.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						circled = evt.getStateChange() == ItemEvent.SELECTED ? true : false;
						labeling.setEnabled(circled);
						display.repaint();
					}
				});

				labeling = new JCheckBoxMenuItem("Show Labels");
				labeling.setSelected(names);
				labeling.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, 0));
				labeling.setEnabled(circled);
				labeling.addItemListener(new ItemListener() {
					@Override
					public void itemStateChanged(ItemEvent evt) {
						names = evt.getStateChange() == ItemEvent.SELECTED ? true : false;
						display.repaint();
					}
				});

				remove = new JMenuItem("Remove All");
				remove.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_R, ActionEvent.SHIFT_MASK));
				remove.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						System.err.println("Flushing all regions (circular and polygonal).");
						polygons.clear();
						regions.clear();
						updateMenus();
						display.repaint();
					}
				});

				fluxes = new JMenuItem("Print Fluxes");
				fluxes.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, ActionEvent.SHIFT_MASK));
				fluxes.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						getFluxes();
					}
				});

				rmsReport = new JMenuItem("Print RMS Scatter");
				rmsReport.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.SHIFT_MASK));
				rmsReport.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						getRegionRMS();
					}
				});

				flagInside = new JMenuItem("Flag");
				flagInside.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						flagRegions();
						display.updateRGBImage();
						display.repaint();
					}
				});


				profile = new JMenuItem("Write Radial Profile(s)");
				profile.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						profile(false);
						JOptionPane.showMessageDialog(frame, "Radial source profiles have been written\nCheck console output for details.");
					}
				});


				deleteLast = new JMenuItem("Delete Last Circular Region");
				deleteLast.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_D, ActionEvent.SHIFT_MASK));
				deleteLast.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						if(regions.isEmpty()) return;
						System.err.println("Delete last circular region.");
						regions.remove(regions.size() - 1);
						if(regions.isEmpty()) updateMenus();
						display.repaint();
					}
				});

				fit = new JMenuItem("Fit Gaussians");
				fit.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, 0));
				fit.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						if(regions != null) { fit(); display.repaint(); }
					}
				});

				add(polyMode);
				addSeparator();
				add(draw);
				add(labeling);
				add(remove);
				addSeparator();
				add(fluxes);
				add(rmsReport);
				add(flagInside);
				addSeparator();
				add(deleteLast);
				add(fit);
				add(profile);

				update();
			}

			public void update() {
				boolean value = isLoaded();
				boolean circles = value && hasMask();
				boolean polys = value && !polygons.isEmpty();

				draw.setEnabled(circles || polys);
				labeling.setEnabled(circles || polys);
				remove.setEnabled(circles || polys);
				fluxes.setEnabled(circles || polys);
				rmsReport.setEnabled(circles || polys);
				flagInside.setEnabled(circles || polys);
				polyMode.setEnabled(value);
				deleteLast.setEnabled(!regions.isEmpty());
				fit.setEnabled(circles);
				profile.setEnabled(circles);
			}
		}



		private class HelpMenu extends JMenu {
			/**
			 * 
			 */
			private static final long serialVersionUID = 1119593471346453323L;
			JMenuItem hotkeys, info, stats, about;

			public HelpMenu() {
				super("Help");

				hotkeys = new JMenuItem("Hotkey Help");
				hotkeys.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_H, 0));
				hotkeys.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						usage();
					}
				});

				info = new JMenuItem("Image Summary");
				info.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						info();
					}
				});

				stats = new JMenuItem("Image Statistics");
				stats.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_T, 0));
				stats.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						getTotalFlux();

						System.err.println("  Map area is " + Util.S[3].format(getArea() / Unit.deg2) + " deg^2.");
						System.err.println("  Map contains " + Util.f1.format(countInstrumentBeams()) + " instrument beams and " +
								Util.f1.format(countBeams()) + " smoothed beams.");
						System.err.println();
					}
				});

				about = new JMenuItem("About...");
				about.addActionListener(new ActionListener() {
					@Override
					public void actionPerformed(ActionEvent evt) {
						Icon logo = new ImageIcon(getClass().getResource("/resource/crush-64x64.png"));	

						JLabel l1 = new JLabel("FITS Image Display and Manipulation Tool", JLabel.CENTER);
						JLabel l1a = new JLabel("for images created by CRUSH", JLabel.CENTER);
						JLabel l1b = new JLabel("(and some other FITS images...)", JLabel.CENTER);
						JLabel l1c = new JLabel("Version " + version, JLabel.CENTER);
						l1.setFont(l1.getFont().deriveFont(Font.BOLD));


						JLabel l2 = new JLabel("Created by Attila Kovács", JLabel.CENTER); 
						JLabel l2a = new JLabel("<attila[AT]sigmyne.com>", JLabel.CENTER);
						l2.setFont(l2.getFont().deriveFont(Font.BOLD));


						JLabel l3 = new JLabel("nom.tam.fits Libraries by", JLabel.CENTER);
						JLabel l3a = new JLabel("Tom A. McGlynn & Ritchie van Nieuwenhoven", JLabel.CENTER);
						l3.setFont(l3.getFont().deriveFont(Font.BOLD));
						l3a.setFont(l3.getFont().deriveFont(Font.BOLD));
						
						JLabel l4 = new JLabel("CRUSH Home:", JLabel.CENTER);
						JLabel l4a = new JLabel("www.submm.caltech.edu/~sharc/crush", JLabel.CENTER);

						l4.setFont(l4.getFont().deriveFont(Font.BOLD));
						l4a.setForeground(Color.BLUE);

						JLabel copyright = new JLabel(
								"Copyright " + Symbol.copyright + "2010-2017 Attila Kovács - All rights reserved."				
								, JLabel.CENTER); 						

						copyright.setFont(copyright.getFont().deriveFont(0.85F * copyright.getFont().getSize()));

						Object[] data = new Object[] {
								logo, " ",
								l1, l1a, l1b, l1c, " ", l2, l2a, " ", l3, l3a, " ",	l4, l4a, " ",						
								copyright
						};

						JOptionPane.showMessageDialog(frame, data, "About CRUSH's show tool", JOptionPane.PLAIN_MESSAGE);
					}
				});

				add(hotkeys);
				addSeparator();
				add(info);
				add(stats);
				addSeparator();
				add(about);

				update();
			}

			public void update() {
				boolean value = isLoaded();
				info.setEnabled(value);
				stats.setEnabled(value);
			}
		}
	}


	// Display with value/coordinate readout
	class MapPanel extends JPanel {
		/**
		 * 
		 */
		private static final long serialVersionUID = 8619921669585536452L;
		private double[][] map;
		private BufferedImage buffer;


		int height, width, imageHeight, imageWidth;
		double x0, y0; // The panel coordinates corresponding to the 0,0 data index (center of drawn pixel!) 
		double dx, dy; // The pixel sizes;
		int dw, dh;
		Graphics graphics;
		InfoPanel infobar;
		ColorScalePanel colorScale;
		Ruler ruler;
		Scale scale;
		AxisLabel fluxLabel;
		Box scaleBox;
		boolean antialias = true;
		boolean inverted = false;
		double zoom=1.0;
		boolean polygonDrawMode = false;
		boolean measureMode = false;
		double di = 0.0, dj = 0.0;
		Vector2D offset = new Vector2D();
		SphericalCoordinates coords;
		int coarseness = 1;
		private DataPoint point = new DataPoint();

		int interpolationType = AffineTransformOp.TYPE_NEAREST_NEIGHBOR;

		boolean interruptRepaint = false;

		int pressX, pressY;

		Vector2D measureFrom;
		Vector2D measureTo;
		SphericalCoordinates measureCoords;
		int button;


		Font font = new Font("sansserif", Font.PLAIN, 10);
		Stroke stroke = new BasicStroke(1.8F);


		// Create a new blank cursor.
		BufferedImage blankCursorImg = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
		java.awt.Cursor blankCursor = Toolkit.getDefaultToolkit().createCustomCursor(blankCursorImg, new Point(0, 0), "blank cursor");


		public MapPanel() {

			setBackground(Color.LIGHT_GRAY);

			colorScale = new ColorScalePanel(20);
			ruler = new Ruler();
			scale = ruler.scale;
			fluxLabel = new AxisLabel(displayUnit.name(), AxisLabel.RIGHT);

			scaleBox = Box.createHorizontalBox();

			scaleBox.add(colorScale);
			scaleBox.add(ruler);
			scaleBox.add(fluxLabel);

			infobar = new InfoPanel(3) {
				/**
				 * 
				 */
				private static final long serialVersionUID = 8553383989231802067L;

				@Override
				public void tabClicked() {
					nextMap();
					repaint();
				}
			};
			infobar.setTab((int) Math.round(scaleBox.getPreferredSize().getWidth() - colorScale.getPreferredSize().getWidth()));

			setMap(signal);

			coords = (SphericalCoordinates) projection.getReference().clone();

			setDefaults();
			setPreferredSize();
			setMinimumSize(new Dimension(100, 100));


			addMouseListener(new MouseAdapter() {

				@Override
				public void mouseEntered(MouseEvent evt) {
					//setCursor(new java.awt.Cursor(java.awt.Cursor.CROSSHAIR_CURSOR));
					setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
				}
				@Override
				public void mouseExited(MouseEvent evt) {
					setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
					infobar.clear();
				}
				@Override
				public void mousePressed(MouseEvent evt) {	
					//antialias = false;

					pressX = evt.getX();
					pressY = evt.getY();			
					button = evt.getButton();

					if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) {
						setCursor(new java.awt.Cursor(java.awt.Cursor.MOVE_CURSOR));					
					}
					else if(SwingUtilities.isLeftMouseButton(evt)) {
						if(measureMode) {
							measureFrom = new Vector2D(getXOffset(pressX), getYOffset(pressY));
							measureTo = new Vector2D(measureFrom);
							measureCoords = new SphericalCoordinates();
							projection.deproject(measureFrom, measureCoords);
							setCursor(blankCursor);	
							updateMeasureInfo(evt);
							repaint();
						}
						else if(!(polygonDrawMode || measureMode)) {
							regions.add(new Region(getXOffset(pressX), getYOffset(pressY), 0.0));
							frame.updateMenus();
						}
					}		

				}

				@Override
				public void mouseReleased(MouseEvent evt) {

					if(!(polygonDrawMode)) {
						antialias = true;
						coarseness = 1;
						if(measureMode) updateMeasureInfo(evt);
						else updateInfo(evt);
					}

					if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) {
						setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));		
						di += (evt.getX() - pressX) / dx;
						dj -= (evt.getY() - pressY) / dy;		
					}
					else if(SwingUtilities.isLeftMouseButton(evt)) {
						if(measureMode) {
							updateMeasureInfo(evt);
							setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));	
						}
						else if(!(polygonDrawMode || measureMode)) if(circled) {
							double radius = Math.pow(getXOffset(pressX) - getXOffset(evt.getX()), 2.0);
							radius += Math.pow(getYOffset(pressY) - getYOffset(evt.getY()), 2.0);
							radius = Math.sqrt(radius); 
							Region region = regions.get(regions.size()-1);
							region.radius = radius;
						}
					}

					repaint();
				}

				@Override
				public void mouseClicked(MouseEvent evt) {
					if(measureMode) {}
					else if(polygonDrawMode) {
						Polygon polygon = polygons.get(polygons.size() - 1);

						if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) { 
							if(polygon.size() > 1) polygon.remove(polygon.size() - 2);
						}
						else if(SwingUtilities.isLeftMouseButton(evt)) {
							double ip = fracIndexOfdX(getXOffset(evt.getX()));
							double jp = fracIndexOfdY(getYOffset(evt.getY()));

							Vector2D last = polygon.get(polygon.size() - 1);
							last.set(ip, jp);

							polygon.add(new Vector2D(ip, jp));
						}

					}
					else if(circled) {
						double imageFWHM = Math.sqrt(instrumentFWHM * instrumentFWHM + smoothFWHM * smoothFWHM);

						if(SwingUtilities.isMiddleMouseButton(evt) || evt.isAltDown()) {
							regions.add(new Region(getXOffset(pressX), getYOffset(pressY), 2.0*imageFWHM));
							frame.updateMenus();
						}
						else if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) {
							regions.add(new Region(getXOffset(pressX), getYOffset(pressY), 3.0*imageFWHM));
							frame.updateMenus();
						}
						else if(SwingUtilities.isLeftMouseButton(evt)) {
							regions.get(regions.size() - 1).radius = imageFWHM;
						}

					}
				}					    
			});


			addMouseMotionListener(new MouseMotionAdapter() {		
				@Override
				public void mouseDragged(MouseEvent evt) {

					if(!isHQRendering) return;

					if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) {	
						di += (evt.getX() - pressX) / dx;
						dj -= (evt.getY() - pressY) / dy;
						pressX = evt.getX();
						pressY = evt.getY();

						offset.set(di, dj);
						offset.scale(delta / Unit.arcsec);

						infobar.clear();
						infobar.lines[0] = "Move by " + Util.f1.format(offset.x()) + ", " + Util.f1.format(offset.y()) + " arcsecs";
						infobar.repaint();

						repaint();
					}
					else if(SwingUtilities.isLeftMouseButton(evt)) {
						if(measureMode) {
							updateMeasureInfo(evt);
							repaint();
						}
						else if(!polygonDrawMode) if(circled) {
							double radius = Math.pow(getXOffset(pressX) - getXOffset(evt.getX()), 2.0);
							radius += Math.pow(getYOffset(pressY) - getYOffset(evt.getY()), 2.0);
							radius = Math.sqrt(radius); 
							Region region = regions.get(regions.size()-1);
							region.radius = radius;

							offset.set(region.dX, region.dY);
							projection.deproject(offset, coords);

							infobar.clear();
							infobar.lines[0] = "[" + region.id + "]  @ " + getPositionString(coords); 
							infobar.lines[2] = "r = " + Util.f1.format(radius / Unit.arcsec) + " arcsec";
							infobar.repaint();

							repaint();
						}
					}
				}



				@Override
				public void mouseMoved(MouseEvent evt) {
					if(measureMode) { updateMeasureInfo(null); }
					else if(polygonDrawMode) {
						updatePolygonInfo(evt);
						Polygon polygon = polygons.get(polygons.size() - 1);
						if(polygon.size() < 2) return;
						Vector2D point = polygon.get(polygon.size() - 1);
						point.set(fracIndexOfdX(getXOffset(evt.getX())), fracIndexOfdY(getYOffset(evt.getY())));
						repaint();
					}
					else updateInfo(evt);
				}
			});


			addMouseWheelListener(new MouseWheelListener() {
				@Override
				public void mouseWheelMoved(MouseWheelEvent evt) {
					zoom *= Math.pow(0.95, evt.getWheelRotation());
					repaint();
				}
			});

			ruler.addMouseListener(new MouseAdapter() {

				@Override
				public void mouseEntered(MouseEvent evt) {
					setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
					infobar.lines[0] = "Scroll or drag the zero point to change level";
					infobar.lines[1] = "Drag ruler elsewhere to change dynamic range";
					infobar.lines[2] = "Click ruler to toggle linear/log/sqrt scaling";
				}

				@Override
				public void mouseExited(MouseEvent evt) {
					infobar.clear();
					setCursor(new java.awt.Cursor(java.awt.Cursor.DEFAULT_CURSOR));
				}

				@Override
				public void mouseClicked(MouseEvent evt) {
					if(SwingUtilities.isLeftMouseButton(evt)) {
						Scale scale = display.scale;
						if(scale.logarithmic) scale.sqrt();
						else if(scale.sqrt) scale.linear();
						else scale.log();
						display.setDefaults();
						display.repaint(); 
					}
				}

				@Override
				public void mousePressed(MouseEvent evt) {	
					fromScale = (Scale) scale.clone();
					fromScaled = ruler.getValue(evt.getY());
					double zeroScaled = scale.getScaled(0.0);

					if(Math.abs(fromScaled - zeroScaled) < 0.05) 
						setCursor(new java.awt.Cursor(java.awt.Cursor.MOVE_CURSOR));
					else 
						setCursor(new java.awt.Cursor(java.awt.Cursor.N_RESIZE_CURSOR));
				}
				@Override
				public void mouseReleased(MouseEvent evt) {

					setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR));
					repaint();
				}

			});

			ruler.addMouseMotionListener(new MouseMotionAdapter() {

				/* (non-Javadoc)
				 * @see java.awt.event.MouseMotionAdapter#mouseDragged(java.awt.event.MouseEvent)
				 */
				@Override
				public void mouseDragged(MouseEvent evt) {
					toScaled = ruler.getValue(evt.getY());

					double zeroScaled = fromScale.getScaled(scale.logarithmic ? 1.0 : 0.0);

					scale.min = fromScale.min;
					scale.max = fromScale.max;

					if(Math.abs(fromScaled - zeroScaled) < 0.05) {
						offsetScale(fromScaled - toScaled);
					}
					else {
						double stretch = (toScaled - zeroScaled) / (fromScaled - zeroScaled);
						if(stretch < 0.0) {
							fromScaled = 2.0 * zeroScaled - fromScaled;
							inverted = !inverted;
							colorScale.invert();
							stretch *= -1;
						}
						expandScale(1.0 / stretch);
					}

					if(!isHQRendering) ruler.repaint();	
					else repaint();
				}
			});

			ruler.addMouseWheelListener(new MouseWheelListener() {
				@Override
				public void mouseWheelMoved(MouseWheelEvent evt) {
					interruptRepaint = true;
					double dY = ruler.getValue(evt.getY()) - ruler.getValue(evt.getY() + (int)Math.round(0.03 * ruler.getHeight()));
					offsetScale(dY * evt.getWheelRotation());
					repaint();
				}
			});


			fluxLabel.addMouseListener(new MouseAdapter() {
				@Override
				public void mouseClicked(MouseEvent evt) {	
					//if(evt.getClickCount() == 1) return;
					if(map == s2n) return;

					int index = units.indexOf(displayUnit);
					index++;
					if(index >= units.size()) index = 0;
					Unit oldUnit = displayUnit;
					displayUnit = units.get(index);
					Scale scale = display.scale;
					scale.setScale(scale.min * oldUnit.value()/displayUnit.value(), scale.max * oldUnit.value()/displayUnit.value());

					display.fluxLabel.label = displayUnit.name();		
					fluxLabel.repaint();
					ruler.repaint();
				}	


				@Override
				public void mouseEntered(MouseEvent evt) {
					infobar.lines[0] = "Click on label to change unit.";
				}

				@Override
				public void mouseExited(MouseEvent evt) {
					infobar.clear();
				}

			});

			colorScale.addMouseListener(new MouseAdapter() {
				@Override
				public void mouseClicked(MouseEvent evt) {
					if(SwingUtilities.isRightMouseButton(evt) || evt.isControlDown()) {
						inverted = !inverted;
						frame.viewMenu.invert.setSelected(inverted);
						colorScale.invert();
						updateRGBImage();
						repaint();
					}
					else if(SwingUtilities.isLeftMouseButton(evt)) {
						useScheme=(useScheme+1)%colorSchemes.size();  
						updateRGBImage();
						repaint();
					}
				}	

				@Override
				public void mouseEntered(MouseEvent evt) {
					infobar.lines[0] = "Click mouse to change color scale.";
					infobar.lines[1] = "Right-click to invert colors.";
				}

				@Override
				public void mouseExited(MouseEvent evt) {
					infobar.clear();
				}

			});

			infobar.addMouseListener(new MouseAdapter() {

				@Override
				public void mouseEntered(MouseEvent evt) {
					infobar.lines[0] = "Press 'h' for help on action keys.";
					infobar.lines[1] = "Press '?' for image summary.";
					infobar.lines[2] = "";
				}

				@Override
				public void mouseExited(MouseEvent evt) {
					infobar.clear();
				}

			});

		}	


		public void setMeasureMode(boolean value) {
			measureMode = value;
			frame.toolsMenu.distance.setSelected(value);

			if(measureMode) {
				if(polygonDrawMode) setPolyMode(false);
				updateMeasureInfo(null);
			}
			else {
				measureCoords = null;
				measureFrom = null;
				measureTo = null;
				infobar.clear();
				infobar.repaint();
				repaint();
				return;
			}
		}

		public void setPolyMode(boolean value) {
			polygonDrawMode = value;
			frame.regionMenu.polyMode.setSelected(value);

			if(polygonDrawMode) {
				if(measureMode) setMeasureMode(false);

				System.err.println("Starting new polygon.");
				Polygon polygon = new Polygon(polygons.size() + 1);
				polygon.add(new Vector2D());
				polygons.add(polygon);
				updatePolygonInfo(null);
			}
			else if(!polygons.isEmpty()) {
				Polygon last = polygons.get(polygons.size() - 1);
				if(last.size() < 4) {
					polygons.remove(polygons.size() - 1);
					infobar.lines[2] = "Polygon incomplete. Disposed.";
					System.err.println("Polygon incomplete. Disposed.");
				}
				else {
					last.remove(last.size() - 1); // Remove last (unfinished) vertex.
					last.close(); // Close the polygon
					System.err.println("Finished drawing polygon with " + last.size() + " sides.");
					infobar.lines[2] = "Polygon complete!";
				}
				System.err.println("Reverting to circular region draw mode.");
				coarseness = 1;
				infobar.lines[1] = "";
				infobar.repaint();
				frame.updateMenus();
			}
		}

		public void setMap(double[][] data) {
			map = data;
			if(map == signal) infobar.tabText = "Signal";
			else if(map == rms) infobar.tabText = "Noise";
			else if(map == s2n) infobar.tabText = "S/N";
			else infobar.tabText = "";
			updateRange();
			updateRGBImage();
			infobar.repaint();
			repaint();
		}

		public void nextMap() {
			if(map == signal) setMap(rms);
			else if(map == rms) setMap(s2n);
			else if(map == s2n) setMap(signal);
			else setMap(signal);
		}

		public void prevMap() {
			if(map == signal) setMap(s2n);
			else if(map == rms) setMap(signal);
			else if(map == s2n) setMap(rms);
			else setMap(signal);
		}


		public void setPreferredSize() {
			zoom = 1.0;
			while(zoom*sizeX < 400 && zoom*sizeY < 400) zoom *= 1.2;		

			Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
			double maxw = screen.getWidth() - 100.0;
			double maxh = screen.getHeight() - 150.0;

			while(zoom*sizeX > maxw || zoom*sizeY > maxh) zoom /= 1.2;

			defaultZoom = zoom;

			int xsize = (int)Math.ceil(zoom * sizeX);
			int ysize = (int)Math.ceil(zoom * sizeY);

			setPreferredSize(new Dimension(xsize, ysize));
		}

		public void updatePolygonInfo(MouseEvent evt) {
			int vertex = polygons.size() > 0 ? polygons.get(polygons.size() - 1).size() : 1;

			infobar.lines[0] = "[Polygon Draw Mode]";

			if(evt != null) {
				offset.set(getXOffset(evt.getX()), getYOffset(evt.getY()));
				projection.deproject(offset, coords);
				infobar.lines[1] = getPositionString(coords) + "   (" + getOffsetString(offset) + ")";
			}
			else infobar.lines[1] = "";

			infobar.lines[2] = "Click to place vertex " + vertex + ", right button deletes last vertex, press 'P' to close/finish.";

			infobar.repaint();
		}

		public void updateInfo(MouseEvent evt) {
			int i = (int)Math.round(geti(evt.getX()));
			int j = (int)Math.round(getj(evt.getY()));

			offset.set(getXOffset(evt.getX()), getYOffset(evt.getY()));			
			projection.deproject(offset, coords);

			if(i>=0 && i<sizeX && j>=0 && j<sizeY) if(flag[i][j] == 0) {			
				if(map == signal) {
					point.setValue(map[i][j]);
					point.setRMS(rms[i][j]);

					infobar.lines[2] = "Flux " + point.toString(displayUnit);
				}
				else if(map == s2n) infobar.lines[2] = "S/N " + Util.getDecimalFormat(s2n[i][j]).format(s2n[i][j]); 
				else if(map == rms) infobar.lines[2] = "RMS " + Util.s3.format(rms[i][j]/displayUnit.value()) + " " + displayUnit.name(); 
			} 
			else infobar.lines[2] = "";		

			infobar.lines[1] = getOffsetString(offset);
			infobar.lines[0] = getPositionString(coords);

			infobar.repaint();

		}

		public void updateMeasureInfo(MouseEvent evt) {
		    if(measureTo == null) return;
		    
			if(evt != null) {
				measureTo.set(getXOffset(evt.getX()), getYOffset(evt.getY()));
				coords = new SphericalCoordinates();
				projection.deproject(measureTo, coords);
			}

			infobar.lines[0] = "[Distance Measure Mode]";
			infobar.lines[1] = "";
			infobar.lines[2] = measureCoords == null ?
					"Press mouse to start, then drag..."
					: "D = " + Util.f1.format(coords.distanceTo(measureCoords) / Unit.arcsec) + " arcsec";

			infobar.repaint();
		}

		public void updateRange() {
			if(map == s2n) {
				if(scale.logarithmic) scale.setScale(1.0, getMax(s2n));
				else scale.setScale(getMin(s2n), getMax(s2n));
			}
			else if(map == rms) scale.setScale(getMin(rms)/displayUnit.value(), getMax(rms)/displayUnit.value());
			else {
				if(scale.logarithmic) scale.setScale(getMin(rms)/displayUnit.value(), getMax(signal)/displayUnit.value());
				else scale.setScale(getMin(signal)/displayUnit.value(), getMax(signal)/displayUnit.value());				
			}
		}

		public void setDefaults() {	
			di = dj = 0.0;	
			updateRange();	
			updateRGBImage();
		}	

		private double fromScaled = 0.0, toScaled = 0.0;
		private Scale fromScale;



		public void expandScale(double factor) {
			if(scale.logarithmic) scale.setScale(Math.pow(scale.min, factor), Math.pow(scale.max, factor));
			else if(scale.sqrt) scale.setScale(factor*factor * scale.min, factor*factor * scale.max);
			else { scale.setScale(factor*scale.min, factor*scale.max); }
			updateRGBImage();
		}

		public void offsetScale(double relativeToRange) {
			double offset = scale.range * relativeToRange;

			if(scale.logarithmic) scale.setScale(scale.min * Math.pow(10.0, offset), scale.max * Math.pow(10.0, offset));
			else if(scale.sqrt) {
				// range ~ Smax * sqrt(|max|) - Smin * sqrt(|min|);
				// zero ~ -Smin * sqrt(|min|)
				// sqrt(|min|) ---> sqrt(|min|) + offset
				double min = Math.signum(scale.min) * Math.sqrt(Math.abs(scale.min));
				double max = Math.signum(scale.max) * Math.sqrt(Math.abs(scale.max));
				offset = (max - min) * relativeToRange;
				min += offset;
				max += offset;
				scale.setScale(Math.signum(min) * min * min, Math.signum(max) * max * max);
			}
			else scale.setScale(scale.min + offset, scale.max + offset);
			updateRGBImage();
		}


		@Override
		public void paintComponent(Graphics g) {
			interruptRepaint = false;

			colorScale.colorScheme = colorSchemes.get(useScheme);

			super.paintComponent(g);

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

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

			dx = (double) imageWidth / sizeX;
			dy = (double) imageHeight / sizeY;
			dw = (int)Math.ceil(coarseness * dx);
			dh = (int)Math.ceil(coarseness * dy);

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

			x0 = left + dx/2;
			y0 = height - top - dy/2;

			g.translate(left + (int) Math.round(dx * di), top - (int)Math.round(dy * dj));
			graphics = g;

			if(!isHQRendering) antialias = false;

			Graphics2D g2 = (Graphics2D) g;
			g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
			g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, antialias ? RenderingHints.VALUE_TEXT_ANTIALIAS_ON : RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
			g2.setRenderingHint(RenderingHints.KEY_RENDERING, antialias ? RenderingHints.VALUE_RENDER_QUALITY : RenderingHints.VALUE_RENDER_SPEED);
			g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, antialias ? RenderingHints.VALUE_STROKE_NORMALIZE : RenderingHints.VALUE_STROKE_PURE);
			//g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, antialias ? RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY : RenderingHints.VALUE_ALPHA_INTERPOLATION_SPEED);


			drawImage();

			setFont(font);
			g2.setStroke(stroke);
			graphics.setColor((colorSchemes.get(useScheme)).getHighlight());

			if(circled) for(Region region : regions) {
				if(interruptRepaint) return;
				circleRegion(region);
				if(names) idRegion(region);

			}

			Polygon last = polygons.isEmpty() ? null : polygons.get(polygons.size() - 1);

			if(circled) for(Polygon polygon : polygons) {
				if(interruptRepaint) return;
				drawPolygon(polygon, polygon.isClosed);
				if(names) if(polygon != last || !polygonDrawMode) idPolygon(polygon);
			}


			if(measureMode) {
				drawMeasure();
			}

			// Repaint the scales
			if(!interruptRepaint) scaleBox.repaint();
			if(!interruptRepaint) colorScale.repaint();
			if(!interruptRepaint) ruler.repaint();

			display.fluxLabel.label = map == s2n ? "S/N" : displayUnit.name();
			if(!interruptRepaint) display.fluxLabel.repaint();


		}



		/**
		 * Draw image.
		 *
		 * @param g the g
		 */
		protected void drawImage() {			
			AffineTransform toDisplay = new AffineTransform();
			toDisplay.translate(0, imageHeight);
			toDisplay.scale(dx, -dy);
			AffineTransformOp op = new AffineTransformOp(toDisplay, interpolationType);
			((Graphics2D) graphics).drawImage(buffer, op, 0, 0);				
		}	

		public void toggleInterpolated() {
			interpolationType = (interpolationType == AffineTransformOp.TYPE_NEAREST_NEIGHBOR) ? 
					AffineTransformOp.TYPE_BICUBIC : AffineTransformOp.TYPE_NEAREST_NEIGHBOR;
		}

		public void updateRGBImage() {
			interruptRepaint = false; 	

			if(buffer == null) buffer = new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_INT_ARGB_PRE);
			else if(buffer.getWidth() != sizeX || buffer.getHeight() != sizeY) {
				buffer = new BufferedImage(sizeX, sizeY, BufferedImage.TYPE_INT_ARGB_PRE);
			}

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



			try {
				new ParallelTask<Void>() {
					@Override
					protected void processChunk(int index, int threadCount) throws Exception {
						for(int i=index; i < sizeX; i += threadCount) {
							for(int j = sizeY; --j >= 0; ) {
								if(flag[i][j] == 0) {
									double value = map[i][j];
									value = scale.getScaled(map == s2n ? value : value/displayUnit.value());
									if(Double.isNaN(value)) if(scale.logarithmic)  value = 0.0;
									buffer.setRGB(i, j, colorScheme.getRGB(inverted ? 1.0 - value : value));
								}
								else buffer.setRGB(i, j, colorScheme.noData);		
							}	
							if(interruptRepaint) break;
							Thread.yield();
						}
					}

				}.process(Math.min(maxThreads, sizeX));
			}
			catch(Exception e) {}	
		}


		private void idRegion(Region region) {	
			final double a = Constant.isqrt2 * dx * region.radius / delta;
			final double b = Constant.isqrt2 * dy * region.radius / delta;				
			final double xc = fracXPixel(region.dX);
			final double yc = fracYPixel(region.dY);

			//setFont(font);

			graphics.drawString(region.id, (int)Math.ceil(xc + a + 3.0), (int)Math.floor(yc - b - 3.0));
		}


		private final int getXPixel(final double xOffset) {
			return (int) Math.round(fracXPixel(xOffset));
		}


		private final int getYPixel(final double yOffset) {
			return (int) Math.round(fracYPixel(yOffset));
		}

		private final double fracXPixel(final double xOffset) {
			return dx * (0.5 + fracIndexOfdX(xOffset));
		}

		private final double fracYPixel(final double yOffset) {
			return imageHeight - dy * (0.5 + fracIndexOfdY(yOffset));
		}


		private void circleRegion(Region region) {
			final double a = dx * 2.0 * region.radius / delta;
			final double b = dy * 2.0 * region.radius / delta;					
			final double x = fracXPixel(region.dX);
			final double y = fracYPixel(region.dY);

			graphics.drawOval((int)Math.round(x - 0.5 * a),  (int)Math.round(y - 0.5 * b), (int)Math.round(a), (int)Math.round(b));

			final int xc = (int)Math.round(x);
			final int yc = (int)Math.round(y);

			graphics.drawLine(xc - 5, yc, xc + 5, yc);
			graphics.drawLine(xc, yc - 5, xc, yc + 5);
		}

		private void drawPolygon(Polygon polygon, boolean closed) {
			int size = closed ? polygon.size() : polygon.size() - 1; 

			for(int i=0; i<size; i++) {
				Vector2D from = polygon.get(i);
				Vector2D to = polygon.get((i+1) % polygon.size());

				final int fromx = (int) Math.round(dx * from.x());
				final int fromy = imageHeight - (int)Math.round(dy * from.y());
				final int tox = (int) Math.round(dx * to.x());
				final int toy = imageHeight - (int) Math.round(dy * to.y());

				graphics.drawLine(fromx, fromy, tox, toy);
			}

		}

		private void idPolygon(Polygon polygon) {	
			Vector2D ne = new Vector2D();
			double maxd = Double.NEGATIVE_INFINITY;

			for(Vector2D vertex : polygon) {
				double d = vertex.x() + vertex.y(); 
				if(d > maxd) { maxd = d; ne = vertex; }
			}

			final double x = dx * ne.x();
			final double y = imageHeight - dy * ne.y();

			graphics.drawString(polygon.name, (int)Math.ceil(x + 3.0), (int)Math.floor(y - 3.0));
		}

		private void drawMeasure() {
			if(!measureMode || measureCoords == null) return;

			int x0 = getXPixel(measureFrom.x());
			int y0 = getYPixel(measureFrom.y());
			int x1 = getXPixel(measureTo.x());
			int y1 = getYPixel(measureTo.y());

			double d = ExtraMath.hypot(x1 - x0, y1 - y0);

			if(d == 0.0) {
				graphics.drawLine(x0 - 3, y0, x0 + 3, y0);
				graphics.drawLine(x0, y0 - 3, x0, y0 + 3);
			}
			else {
				graphics.drawLine(x0, y0, x1, y1);

				int dx1 = (int)Math.round(-5.0 * (y1 - y0) / d);
				int dy1 = (int)Math.round(5.0 * (x1 - x0) / d);

				graphics.drawLine(x0 - dx1, y0 - dy1, x0 + dx1, y0 + dy1);
				graphics.drawLine(x1 - dx1, y1 - dy1, x1 + dx1, y1 + dy1);
			}
		}


		public final double geti(final int x) { 
			return (x-x0)/dx - di;
		}

		public final double getj(final int y) { 
			return (y0-y)/dy - dj; 
		} 

		public final double getXOffset(final int x) { return dXofIndex(geti(x)); }

		public final double getYOffset(final int y) { return dYofIndex(getj(y)); }



	}

	class SourceFit extends AmoebaMinimizer {
		double[][] model = new double[sizeX][sizeY];
		double minValue;

		public SourceFit() {
			verbose = false;

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

			initparm[0] = getLevel();
			for(int k=0, m=1; k<regions.size(); k++) {
				Region source = regions.get(k);
				initparm[m++] = source.dX;
				initparm[m++] = source.dY;
				initparm[m++] = source.radius;
				initparm[m++] = signal[indexOfdX(source.dX)][indexOfdY(source.dY)];
			}	    

			init(initparm);

			Random random = new Random();
			double signalRange = getMax(signal) - getMin(signal);
			double imageFWHM = getImageFWHM();

			double[] range = new double[initparm.length];
			range[0] += 0.01 * signalRange * random.nextGaussian();

			for(int k=0, m=1; k<regions.size(); k++, m+=4) {
				range[m] += 0.3 * imageFWHM;
				range[m+1] += 0.3 * imageFWHM;
				range[m+2] += 0.3 * imageFWHM;
				range[m+3] += 0.1 * signalRange;	
			}

			setStartSize(range);		
		}

		@Override
		public double minimize() {
			calc();

			minValue = getMin(signal);

			double retval = super.minimize();		

			for(int k=0, m=1; k<regions.size(); k++, m+=4) {
				Region source = regions.get(k);

				source.sigmaX = getStandardError(m, delta);
				source.sigmaY = getStandardError(m+1, delta);
				source.dradius = getStandardError(m+2, delta);
				source.dpeak = getStandardError(m+3, 0.01*source.peak);

			}

			calcS2N();

			return retval;
		}		


		@Override
		public double evaluate(double[] tryparm) {
			double level = tryparm[0];

			for(int i=sizeX; --i >= 0; ) Arrays.fill(model[i], 0.0);

			double[][] backup = signal;
			signal = model;

			for(int k=0; k<regions.size(); k++) {
				Region source = regions.get(k);

				int m = 4*k + 1;
				source.dX = tryparm[m];
				source.dY = tryparm[m+1];
				source.radius = tryparm[m+2];
				source.peak = tryparm[m+3];

				source.add(false);
			}	

			signal = backup;

			double chi2 = 0.0;

			for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(flag[i][j] == 0) {
				double dev = (signal[i][j] - model[i][j] - level) / rms[i][j];
				chi2 += dev * dev;	
			}


			double imageFWHM = getImageFWHM();

			// 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, but at least FWHM/2;
			for(int k=0; k<regions.size(); k++) {
				Region source = regions.get(k);	       

				if(source.peak <= 0.0) chi2 *= Math.exp(-source.peak / Math.abs(minValue));

				if(source.radius < 0.5 * imageFWHM) chi2 *= Math.exp(0.5*imageFWHM / source.radius);	
				else {
					if(source.radius > sizeX*delta/2.0) chi2 *= Math.exp(source.radius / (sizeX * delta));
					if(source.radius > sizeY*delta/2.0) chi2 *= Math.exp(source.radius / (sizeY * delta));
				}

				double i = fracIndexOfdX(source.dX);
				double j = fracIndexOfdY(source.dY);

				if(i < 0.0) chi2 *= Math.exp(-i * delta/imageFWHM);
				else if(i > sizeX-1) chi2 *= Math.exp((i + 1 - sizeX) * delta/imageFWHM);

				if(j < 0.0) chi2 *= Math.exp(-j * delta/imageFWHM);
				else if(j > sizeY-1) chi2 *= Math.exp((j + 1 - sizeY) * delta/imageFWHM);
			}


			// Normalize chi2 s.t. it corresponds to the sum over independent points...
			// This is important for getting proper noise estimates...
			chi2 /= pointsPerBeam;


			return chi2;
		}		 

	}


	public static final int maxThreads = Runtime.getRuntime().availableProcessors();
	public static final double defaultBeamFWHM = 9.0 * Unit.arcsec;

}





