package crushtools;
/*******************************************************************************
 * Copyright (c) 2019 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.09-1";
    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 janskyPerBeam = Double.NaN;
    public Unit nativeUnit = new Unit("counts", 1.0);
  

    public Unit displayUnit = new Unit("arb.u.", 1.0);
    private Vector<String> displayNames;
    private int displayIndex = 0;

    int useScheme = 0;

    Vector<Region> regions = new Vector<Region>();
    Vector<Polygon> polygons = new Vector<Polygon>();
    Vector<ColorScheme> colorSchemes = new Vector<ColorScheme>();
    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() { 
        try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } 
        catch (Exception e) {}
        
        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());

        displayNames = new Vector<String>();
        displayNames.add("Jy/beam");

        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.colorSchemes = (Vector<ColorScheme>) colorSchemes.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();	

        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;

            backup = newBackup;
        }
    }

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

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

        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) {
        janskyPerBeam = value;
    }

    public double getJansky() {
        return janskyPerBeam;
    }



    public double pointsPerBeam() {
        return getImageBeamArea() / (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 {		
        nativeUnit = getUnit(unitName);
        if(!displayNames.contains(nativeUnit.name())) displayNames.add(0, nativeUnit.name());
        if(verbose) System.err.println("Set map units to " + nativeUnit.name());
    }

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

        if(unitName.equalsIgnoreCase("Jy/beam")) return new Unit("Jy/beam", janskyPerBeam);
        else if(unitName.equalsIgnoreCase("mJy/beam")) return new Unit("mJy/beam", 1e-3 * janskyPerBeam);
        else if(unitName.equalsIgnoreCase("MJy/sr")) return new Unit("MJy/sr", 1e6 * janskyPerBeam * getImageBeamArea() / Unit.sr);
        else if(unitName.equalsIgnoreCase(nativeUnit.name())) return nativeUnit;
        else unit = Unit.get(unitName);

        return unit;
    }
    

    private Unit nextUnit() {
        if(++displayIndex >= displayNames.size()) displayIndex = 0;
        return getUnit(displayNames.get(displayIndex));
    }
    
    
    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;

        displayIndex = 0;
        displayUnit = getUnit(displayNames.get(displayIndex));

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

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

        // Adjust Jy/beam for the new beam unit...
        x /= getImageFWHM();
        janskyPerBeam *= x*x;
        
        // If the native unit is also beam-dependent, then adjust it too...
        String nativeName = nativeUnit.name().toLowerCase();
        if(nativeName.contains("beam") || nativeName.contains("bm")) nativeUnit = new Unit(nativeName, nativeUnit.value() * x * x);
        
        displayUnit = getUnit(displayUnit.name());
        
        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;
                
        reorderFFTImage(signal);
        reorderFFTImage(count);

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

        for(int i=sizeX; --i >= 0; ) {
            Arrays.fill(weight[i], 1.0);
            Arrays.fill(rms[i], 1.0);
        }
        
        i0 = sizeX>>1;
        j0 = sizeY>>1;

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

        for(int j=sizeY; --j >= 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>>>1;

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

        for(int j=ny; --j >= 0; ) a[0][ny+j] = a[0][ny-j] = data[0][j].length();
        
        for(int i=nx; --i > 0; ) for(int j=ny; --j >= 0; ) a[i][ny+j] = a[nx-i][ny-j] = data[i][j].length();
 
        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();
    }

 
    // 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 component = gain * signal[i][j];            
            peakS2N = s2n[i][j];            

            // Pole is the peak value times the beam integral to conserve flux	    
            deconvolved[i][j] += component * 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 = component * 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);

        double x = getImageFWHM();
        
        // Reset the beam and resolution... 
        instrumentFWHM = replacementFWHM;
        smoothFWHM = delta / fwhm2size; 
            
        x /= getImageFWHM();
        
        for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) signal[i][j] += deconvolved[i][j];
      
        // If the native unit is NOT beam-dependent, then adjust it...
        String nativeName = nativeUnit.name().toLowerCase();
        if(!(nativeName.contains("beam") || nativeName.contains("bm"))) nativeUnit = new Unit(nativeName, nativeUnit.value() * x * x);
  

        // 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 sqrt() {
        if(verbose) System.err.println("Square root...");

        for(int i=sizeX; --i >= 0; ) for(int j=sizeY; --j >= 0; ) if(signal[i][j] > 0) {
            signal[i][j] = Math.sqrt(signal[i][j]);
            s2n[i][j] = Math.sqrt(s2n[i][j]);
        }

        addProcessingHistory("sqrt()");
    }  

    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;

        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 / Unit.deg) * Unit.deg;
        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;

        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);
        
        displayNames.clear();
        displayNames.add("Jy/beam");
        displayNames.add("mJy/beam");
        displayNames.add("MJy/sr");
        
        if(!displayNames.contains(nativeUnit.name())) displayNames.add(0, nativeUnit.name());

        displayIndex = 0;
        displayUnit = getUnit(displayNames.get(0));
        
        
        /*
		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);


        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 {	

    }


    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()));
        rms = getRMS();
        s2n = new double[sizeX][sizeY];
        
        flag = new int[sizeX][sizeY];
        makeFlagFrom(signal);

        // Calculate the signal-to-noise image

        calcS2N();
    }


    private void readSMURFHeader(Header header) throws HeaderCardException {	
    }


    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 {
        // Get the scan list
        extraHDUs.clear();

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

        signal = readDoubleImage(hdu, nativeUnit.value());
        
        flag = new int[sizeX][sizeY];
        count = new double[sizeX][sizeY];
        weight = new double[sizeX][sizeY];
        for(int i=0; i<sizeX; i++) for(int j=0; j<sizeY; j++) {
            if(Double.isNaN(signal[i][j])) flag[i][j] = 1;
            else {
                count[i][j] = 1.0;
                weight[i][j] = 1.0;
            }
        }
        
        for(int i=0; i<sizeX; i++) Arrays.fill(weight[i], 1.0);

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



    public void guessWeightImage() {
        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(0.4 * FWHM);
        else FWHM = delta;

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

        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(4.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))] = nyquist.weight[i][j];
        }

        weight = getUniformConvolved(weight, 2.0*FWHM);

        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];
        }

        rms = getRMS();
        calcS2N();

        robustWeight();
    }


    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", janskyPerBeam/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);

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

        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;
            //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;
        }


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

        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) + "\t" +
                                Util.e3.format(1.0/Math.sqrt(point.weight()) / janskyPerBeam)
                        );
            }

            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) + "\t" +
                                Util.e3.format(1.0/Math.sqrt(point.weight()) / janskyPerBeam)
                        );
            }

            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 ? getUnit("Jy/beam") : 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();

            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 'V' : sqrt(); 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;

                        Unit oldUnit = displayUnit;
                        displayUnit = nextUnit();
                        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.sigmyne.com/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;

                    
                    Unit oldUnit = displayUnit;
                    displayUnit = nextUnit();
                    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;

}





