spectrogram.Spectrogram.java Source code

Java tutorial

Introduction

Here is the source code for spectrogram.Spectrogram.java

Source

/*
 * Copyright (C) 2013 Joseph Areeda <joseph.areeda at ligo.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package spectrogram;

import au.com.bytecode.opencsv.CSVReader;
import com.areeda.jaDatabaseSupport.Database;
import edu.fullerton.jspWebUtils.WebUtilException;
import edu.fullerton.ldvjutils.ChanInfo;
import edu.fullerton.ldvjutils.LdvTableException;
import edu.fullerton.ldvjutils.Progress;
import edu.fullerton.ldvjutils.TimeAndDate;
import edu.fullerton.ldvtables.ChannelTable;
import edu.fullerton.ndsproxyclient.NDSBufferStatus;
import edu.fullerton.ndsproxyclient.NDSException;
import edu.fullerton.ndsproxyclient.NDSProxyClient;
import edu.fullerton.viewerplugin.GDSFilter;
import edu.fullerton.viewerplugin.SpectrumCalc;
import edu.fullerton.viewerplugin.WindowGen.Window;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.IndexColorModel;
import java.awt.image.WritableRaster;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Date;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import org.apache.commons.io.input.SwappedDataInputStream;
import viewerconfig.ViewConfigException;
import viewerconfig.ViewerConfig;

/**
 *
 * @author Joseph Areeda <joseph.areeda at ligo.org>
 */
public class Spectrogram {
    // database info
    private Database db;
    private ChannelTable chanTbl;
    private ChanInfo chanInfo;

    // program spec
    private final String version = "0.0.3";
    private final String programName = "Spectrogram.jar";
    private final int debugLevel = 2;

    // data spec
    private NDSProxyClient ndsClient;
    private String server;
    private String channelName;
    private String cType;

    // these are used if we're reading in data from a file
    private double sampleRate = 0.;
    private int startGPS;
    private int duration;
    private boolean useAltData;

    private String testDataFilename;
    private File testDataFile;
    private File rawDataFile;

    // output image specs
    private int outX = 1024, outY = 550; // final image size
    private int titleHeight;
    private int dimX, dimY; // size of plot area
    private int xTicks = 6;
    private float up, lo;
    private boolean smooth;

    private int em;
    private int lblHeight; // height of a character in axis label font

    // progress bar
    private boolean showProgressBar = false;
    private Progress progressBar;

    private final int minStride = 30;
    private final int targetFftCount = 200;
    private int bytesPerSample;

    private String color;

    // output
    String ofileName = "spectrogramTest.png";
    // timing
    private static long startMs;
    // return status
    private String status; // "Success" or an error message
    //---------------------------------------------------
    private int colPerSample;

    private boolean norm;
    private boolean logFreq;
    private boolean logIntensity;
    private boolean interp;

    private int yTicks;
    private Rectangle cmRect;
    private Rectangle pltRect;
    private IndexColorModel colorModel;

    // processing options
    private SpectraCache spectraCache;
    private double fmin = 0, fmax = 0; // frequency limits
    private int nfft;
    private int flen;
    private int overlapSamples;
    private SpectrumCalc spectrumCalculator;
    private boolean dodetrend;
    private Window window;
    private float secPerFFT;
    private float overlap;
    private SpectrumCalc.Scaling scaling;

    // prefilter specification
    private String filtType;
    private float cutoff;
    private int order;
    private String xferErrMsg;
    private GDSFilter gdsfilt;
    private double start;
    private double binWidth;
    private String rawDataFilename;
    private SwappedDataInputStream inStream;
    private SpectrogramCommandLine cmd;

    public Spectrogram() throws SQLException {

    }

    /**
     * This allows the jar file to be called from a command line but the Class can be used as part
     * of another program
     *
     * @param args the command line arguments
     */
    public static void main(String[] args) throws WebUtilException, NDSException {
        int stat;
        try {
            Spectrogram me = new Spectrogram();

            // decode command line and set parameters
            boolean doit = me.processArgs(args);

            // generate image
            if (doit) {
                stat = me.doPlot();
            } else {
                stat = 10;
            }
        } catch (SQLException | ViewConfigException ex) {
            Logger.getLogger(Spectrogram.class.getName()).log(Level.SEVERE, null, ex);
            stat = 11;
        }
        System.exit(stat);
    }

    /**
     * The controller of the process loop, init -> get data -> process -> make image
     * @return 
     * @throws viewerconfig.ViewConfigException 
     * @throws edu.fullerton.jspWebUtils.WebUtilException 
     * @throws edu.fullerton.ndsproxyclient.NDSException 
     */
    public int doPlot() throws ViewConfigException, WebUtilException, NDSException {
        int ret = 0;
        long xfernt = 0;
        long procnt = 0;

        try {
            startMs = System.currentTimeMillis();
            initProgress();
            initChanInfo();
            initImage();
            initCache();
            if (debugLevel > 1) {
                System.out.format("\nChan: %1$s, sample rate: ", channelName);
                if (sampleRate >= 1) {
                    System.out.format("%1$.0f", sampleRate);
                } else {
                    System.out.format("%1$.3f", sampleRate);
                }
                System.out.format(", bytes/sample: %1$1d%n", bytesPerSample);
                System.out.format("Sec/fft: %1$.2f, overlap: %2$.2f%n", secPerFFT, overlap);
            }

            int stride = duration / 100;
            int maxStride = (int) (1000000 / sampleRate);
            stride = Math.max(stride, minStride);
            stride = Math.min(stride, duration);
            stride = Math.min(stride, maxStride);

            bufn = 0;
            boolean wantsToCancel = false;

            if (useAltData) {
                int stopSample = (int) (duration * sampleRate);

                if (!testDataFilename.isEmpty()) {
                    readAddTestData(testDataFile);
                } else if (!rawDataFilename.isEmpty()) {
                    readAddRawData();
                } else {
                    noData(0, stopSample);
                }
            } else {
                int stride2 = duration;

                for (int curT = 0; curT < duration && !wantsToCancel; curT += stride2) {
                    long xStrt = System.nanoTime();
                    setProgress(String.format("Processing %1$,4d of %2$,4d seconds of data", curT, duration));
                    setProgress(curT, duration);
                    int curgps = startGPS + curT;
                    int curDur = stride;
                    if (curDur + curgps > startGPS + duration) {
                        curDur = startGPS + duration - curgps;
                    }
                    try {
                        ndsClient = new NDSProxyClient(server);
                        ndsClient.connect();

                        boolean reqStat = ndsClient.requestData(channelName, chanInfo.getcType(), curgps,
                                curgps + duration, stride);
                        if (reqStat) {
                            // for the first buffer we get channel information from NDS2 and 
                            // adjust accordingly
                            int dt = (int) (ndsClient.getStartGPS() - startGPS);
                            setProgress(String.format("Processing %1$,4d of %2$,4d seconds of data", dt, duration));
                            setProgress(dt, duration);
                            NDSBufferStatus bufferStatus = ndsClient.getBufferStatus();
                            if (sampleRate != bufferStatus.getFs()) {
                                sampleRate = bufferStatus.getFs();
                                initImage();
                                initCache();
                            }
                            if (fmax == 0) {
                                fmax = sampleRate / 2;
                            }

                            int startSample = (int) (dt * sampleRate);
                            int nsample = (int) (duration * sampleRate);
                            long pStrt = System.nanoTime();
                            xfernt += pStrt - xStrt;

                            addBuf(startSample, nsample);
                            procnt += System.nanoTime() - pStrt;

                            bufn++;
                            wantsToCancel = checkCancel();
                        } else {
                            String msg = ndsClient.getLastError();
                            wantsToCancel = true;
                            System.err.format("Transfer error: %s\n", msg);
                            ret = 2;
                        }
                        ndsClient.bye();
                        ndsClient = null;
                    } catch (Exception ex) {
                        xferErrMsg = ex.getClass().getSimpleName() + ": " + ex.getMessage();
                        if (!checkNoData(curgps, curDur, xferErrMsg)) {
                            wantsToCancel = true;
                            System.err.format("Transfer error: %s\n", xferErrMsg);
                            ret = 2;
                        }
                        try {
                            //ndsClient.disconnect();
                            ndsClient.bye();
                            ndsClient = null;
                        } catch (Exception ex2) {
                            // Ignore error in error handler
                        }
                    }
                }
            }
            if (wantsToCancel) {
                if (spectraCache.size() > nfft / 4) {
                    status = "Shortened";
                } else {
                    status = "Canceled";
                }
                status += " by user or because of error";
                if (xferErrMsg == null || xferErrMsg.isEmpty()) {
                    xferErrMsg = status;
                } else {
                    xferErrMsg += " - " + status;
                }

            } else {
                status = "Success";
            }
            long mkimgnt = 0;
            if (spectraCache.size() > nfft / 4) {
                long mkimgStrt = System.nanoTime();
                makeImage();
                mkimgnt = System.nanoTime() - mkimgStrt;
            } else if (spectraCache.size() == 0) {
                System.err.println("Unable to transfer data.  Last error received: " + xferErrMsg);
                ret = 3;
            } else {
                System.err.println("Some data transfered but not enough spectra to make an image ("
                        + Integer.toString(spectraCache.size()) + ")");
                System.err.println("Last error received: " + xferErrMsg);
                ret = 4;
            }
            // timing info
            double elapsedSec = (System.currentTimeMillis() - startMs) / 1000.;
            long bytes = (long) (duration * sampleRate * Float.SIZE / 8);
            double rateKBps = bytes / elapsedSec / 1000;
            System.out.format("Run time: %1$.2fs, total bytes xfer: %2$,d, process rate %3$.1f KBps%n", elapsedSec,
                    bytes, rateKBps);
            double xfersec = xfernt / 1.0e9;
            double procsec = procnt / 1.0e9;
            double mkimgsec = mkimgnt / 1.0e9;
            double ovhdsec = elapsedSec - xfersec - procsec - mkimgsec;
            System.out.format("Transfer: %1$.2f, process: %2$.2f, image: %3$.2f, overhead: %4$.2f %n", xfersec,
                    procsec, mkimgsec, ovhdsec);
        } catch (LdvTableException | SQLException | IOException | IllegalArgumentException ex) {
            status = "Error: " + ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage();
            System.err.println(ex.toString());
            ret = 5;
        }
        closeProgress();
        return ret;
    }

    private void initChanInfo() throws LdvTableException, SQLException, ViewConfigException, WebUtilException {
        if (!useAltData) {
            setProgress("Getting Channel info.");

            getDbTables();

            setProgress("Getting Channel info.");
            if (!getChanInfo()) {
                System.exit(1);
            }
        } else {
            // test data can either be no data or input from a csv file
            testDataFile = null;
            rawDataFile = null;
            if (rawDataFilename.isEmpty() && testDataFilename.isEmpty()) {
                channelName = "No data (labels only)";
            } else if (!testDataFilename.isEmpty()) {
                testDataFile = new File(testDataFilename);
                channelName = testDataFile.getName();
                bytesPerSample = Float.SIZE / 8;
            } else if (!rawDataFilename.isEmpty()) {
                Pattern fnPat = Pattern.compile("(.*/)?(.+)-(\\d+)-(\\d+).dat");
                Matcher fnMat = fnPat.matcher(rawDataFilename);
                if (!fnMat.find()) {
                    throw new WebUtilException("Raw data file not named corrrectly.");
                }
                channelName = fnMat.group(2);
                startGPS = Integer.parseInt(fnMat.group(3));
                duration = Integer.parseInt(fnMat.group(4));
                bytesPerSample = Float.SIZE / 8;
            }

        }
        setProgress("Starting transfer.");
    }

    /**
     * Using previously set up object members verify the channel and get needed info, complication is
     * when the channel is only partially specified
     *
     * @return true if we got what we needed, else return after we've printed errors.
     */
    private boolean getChanInfo() throws SQLException {
        boolean ret = false;
        long strt = System.currentTimeMillis();
        {
            int n;
            if (channelName == null || channelName.isEmpty()) {
                throw new IllegalArgumentException("No Channel specified");
            }
            if ((server == null || server.isEmpty()) && (cType == null || cType.isEmpty())) {
                n = chanTbl.getBestMatch(channelName);
                chanInfo = chanTbl.getChanInfo(n);
                server = chanInfo.getServer();
            } else {
                if (cType == null || cType.isEmpty()) {
                    cType = "raw";
                }
                TreeSet<ChanInfo> chSet = chanTbl.getAsSet(server, channelName, cType, 10);
                if (chSet.size() > 1) {
                    chanInfo = null;
                    if (sampleRate > 0) {
                        for (ChanInfo ci : chSet) {
                            if (Math.abs(ci.getRate() - sampleRate) < .0001) {
                                chanInfo = ci;
                                break;
                            }
                        }
                    }
                    if (chanInfo == null) {
                        System.err.print("Warning: more than one channel matches: " + channelName);
                        System.err.println(" and no applicable rate specified.");
                    }
                } else if (chSet.size() == 1) {
                    chanInfo = chSet.first();
                }
            }
            if (chanInfo == null) {
                System.err.println("Channel requested was not found: " + channelName);
            } else {
                sampleRate = chanInfo.getRate();
                String dtyp = chanInfo.getdType();

                if (dtyp.equalsIgnoreCase("INT-16")) {
                    bytesPerSample = 2;
                } else if (dtyp.equalsIgnoreCase("INT-32")) {
                    bytesPerSample = 4;
                } else if (dtyp.equalsIgnoreCase("INT-64")) {
                    bytesPerSample = 8;
                } else if (dtyp.equalsIgnoreCase("FLT-32")) {
                    bytesPerSample = 4;
                } else if (dtyp.equalsIgnoreCase("FLT-64")) {
                    bytesPerSample = 8;
                } else if (dtyp.equalsIgnoreCase("CPX-64")) {
                    bytesPerSample = 8;
                }
                ret = true;

                float dur = (System.currentTimeMillis() - strt) / 1000.f;
                System.out.format("Get channel info took %1$.1f sec.\n", dur);
            }
        }
        return ret;
    }

    private int bufn = 0;
    private double[] rawData;

    /**
     * Given a NDS connection ready to transfer time series data add it to our freq domain cache
     * 
     * @param strtSampleNum - starting position in samples
     * @param len - number of samples in buffer
     */
    private void addBuf(int strtSampleNum, int len) throws NDSException, WebUtilException {
        int cnt = 0;
        start = strtSampleNum / sampleRate;
        binWidth = 1. / secPerFFT;
        gdsfilt = new GDSFilter();

        while (cnt < len) {
            bufn++;
            boolean gotBuf = false;
            int idx;
            if (rawData == null) {
                // first buffer fill the whole thing
                rawData = new double[flen];
                for (idx = 0; idx < flen && idx < len; idx++) {
                    rawData[idx] = ndsClient.getNextDouble();
                }
                cnt += flen;
                gotBuf = idx == (len - 1);
            } else {
                // subsequent buffers we first deal with overlap
                int offset = flen - overlapSamples;
                for (idx = 0; idx < overlapSamples; idx++) {
                    rawData[idx] = rawData[idx + offset];
                }

                for (idx = overlapSamples; idx < flen && cnt < len; idx++, cnt++) {
                    rawData[idx] = ndsClient.getNextDouble();
                }

                gotBuf = idx == (len - 1);
            }
            filtAndSave();
        }

    }

    /**
     * Given a buffer of real data add it to our freq domain cache
     * almost a duplicate of NDS2 version for efficiency
     *
     * @param rawBuf - array of time domain samples
     * @param strtSampleNum - starting position in samples
     * @param len - number of samples in buffer (it doesn't need to be full)
     */
    private void addBuf(double[] rawBuf, int strtSampleNum, int len) throws NDSException, WebUtilException {
        int cnt = 0;
        start = strtSampleNum / sampleRate;
        binWidth = 1. / secPerFFT;
        rawData = new double[flen];

        while (cnt < len) {
            bufn++;
            // copy data for next fft and add it to cache
            if (cnt + flen < len) {
                System.arraycopy(rawBuf, cnt, rawData, 0, flen);
                filtAndSave();
            }
            cnt += flen - overlapSamples;
        }

    }

    private void filtAndSave() throws WebUtilException {
        writeRawBuffer(bufn, rawData);
        double[] temp = new double[rawData.length];
        System.arraycopy(rawData, 0, temp, 0, rawData.length);
        if (!filtType.isEmpty()) {
            gdsfilt.apply(temp, (float) sampleRate, filtType, cutoff, order);
        }
        double[] result = spectrumCalculator.doCalc(temp, sampleRate);
        spectraCache.add(start, result, sampleRate, fmax, fmin, binWidth);
        start += flen - overlapSamples;

    }

    private void noData(int strtSampleNum, int stopSampleNum) {
        long len = stopSampleNum - strtSampleNum;
        bufn++;

    }

    // Image generation from data array
    private WritableRaster rast; // image pixels of the colored plot section
    private BufferedImage img; // output image
    private Graphics2D grph; // Graphics context of image
    private Font lblFont; // the font we're using to make labels
    private Font titleFont; // guess what we're going to use that one for
    private int imgX0, imgY0; // origin of the plot section
    FontMetrics labelMetrics; // used for positioning and sizing labels

    /**
     * Generate and write the output image
     *
     * @throws IOException some problem with the file writing
     */
    private void makeImage() throws IOException {
        Rectangle imgR = new Rectangle(imgX0, imgY0, dimX, dimY);
        SpectraCache.Normalization normalization = norm ? SpectraCache.Normalization.DIVBYMEAN
                : SpectraCache.Normalization.ALL;
        spectraCache.makeImage(rast, imgR, normalization, logIntensity, logFreq);
        finalizeImage();
    }

    /**
     * Define the size and position of different parts of image and add global labels
     */
    private void initImage() {

        colorModel = IndexColorTables.getColorTable(color);

        // use anti-aliasing font representation
        System.setProperty("awt.useSystemAAFontSettings", "on");
        System.setProperty("swing.aatext", "true");

        // img contains the output image with all the labels
        img = new BufferedImage(outX, outY, BufferedImage.TYPE_BYTE_INDEXED, colorModel);
        grph = img.createGraphics();
        grph.setBackground(Color.white);
        grph.clearRect(0, 0, outX, outY);

        String fontName = getAGoodFont();
        lblFont = new Font(fontName, Font.PLAIN, 16);
        titleFont = new Font(fontName, Font.BOLD, 18);
        FontMetrics titleMetrics = grph.getFontMetrics(titleFont);
        labelMetrics = grph.getFontMetrics(lblFont);
        em = labelMetrics.stringWidth("M");

        String title;
        String utcDate;

        String hrTime = TimeAndDate.hrTime(duration);
        String cname;
        if (useAltData) {
            cname = "Test Data";
            if (testDataFilename != null && !testDataFilename.isEmpty()) {
                File f = new File(testDataFilename);
                cname = f.getName();
            }
            if (rawDataFilename != null && !rawDataFilename.isEmpty()) {
                File f = new File(rawDataFilename);
                cname = f.getName();
            }
            if (channelName != null && !channelName.isEmpty()) {
                cname = channelName;
            }
            if (startGPS == 0) {
                Date now = new Date();
                startGPS = (int) TimeAndDate.utc2gps(now.getTime() / 1000);
            }
        } else {
            cname = channelName;
        }
        utcDate = TimeAndDate.gpsAsUtcString(startGPS);
        title = String.format("%1$s %2$s - %3$,d (%4$s)", cname, utcDate, startGPS, hrTime);

        titleHeight = titleMetrics.getHeight();
        int titleWidth = titleMetrics.stringWidth(title);

        lblHeight = labelMetrics.getHeight();
        int lblWidth = labelMetrics.stringWidth("99,999");

        int tbMargin = 10;
        int lrMargin = 10;

        // plot title
        int tx = outX / 2 - titleWidth / 2;
        int ty = (int) Math.round(lblHeight * 1.5);
        grph.setPaint(Color.BLACK);
        grph.setFont(titleFont);
        grph.drawString(title, tx, ty);

        // calc and draw color map
        int cmWidth = 48;
        int cmLblWidth = labelMetrics.stringWidth("0.000000") + em;
        int cmLeft = outX - lrMargin * 2 - titleHeight - cmLblWidth - cmWidth;
        imgY0 = titleHeight + tbMargin * 2;
        dimY = outY - imgY0 - lblHeight * 5 - tbMargin;
        cmRect = new Rectangle(cmLeft, imgY0, cmWidth, dimY);
        grph.drawRect(cmLeft, imgY0, cmWidth, dimY);

        // calc and draw image rectangle
        imgX0 = lrMargin * 2 + titleHeight + lblWidth;
        dimX = cmLeft - imgX0 - lrMargin;
        pltRect = new Rectangle(imgX0, imgY0, dimX, dimY);
        grph.drawRect(imgX0, imgY0, dimX, dimY);

        // y-axis label
        String leftAxisLabel = "Frequency (Hz)";

        grph.setColor(Color.BLACK);
        tx = lblHeight + lrMargin;
        titleWidth = titleMetrics.stringWidth(leftAxisLabel);
        ty = outY / 2 + titleWidth / 2;
        drawRotatedText(tx, ty, -90, leftAxisLabel);

        // color map label
        String rightAxisLabel = scaling.toString();
        if (norm) {
            rightAxisLabel += " Normalized.";
        }
        tx = outX - lblHeight - lrMargin;
        titleWidth = titleMetrics.stringWidth(rightAxisLabel);
        ty = outY / 2 + titleWidth / 2;
        drawRotatedText(tx, ty, -90, rightAxisLabel);

        int nsamples = (int) (duration * sampleRate);
        colPerSample = 1;

        if (nsamples < dimX) {
            // we have more pixels available than we have samples so making it pretty takes some work
            colPerSample = dimX / nsamples;
            dimX = colPerSample * nsamples;
        }

        rast = img.getRaster();
        fillColorMap();
    }

    /**
     * The spectrum cache holds all spectra until we're ready to clip, normalize and interpolate them into an image
     */
    private void initCache() {

        flen = (int) (secPerFFT * sampleRate);

        if (overlap < 0) {
            // set overlap to make prettier pictures
            float t = (duration / secPerFFT);
            if (t < targetFftCount) {
                overlap = 1 - t / targetFftCount;
            } else {
                overlap = 0.5f;
            }
        }
        nfft = (int) Math.round(duration / secPerFFT / (1 - overlap));
        overlapSamples = (int) (flen * overlap);
        if (overlapSamples >= flen) {
            overlapSamples = flen - 1;
        }
        if (overlapSamples < 0) {
            overlapSamples = 0;
        }
        if (overlapSamples > 0) {
            nfft -= 1;
        }
        overlap = (float) overlapSamples / flen;

        spectraCache = new SpectraCache(nfft);
        spectraCache.setDebugLevel(debugLevel);
        spectraCache.setSmooth(smooth);
        spectraCache.setInterp(interp);
        spectraCache.setUp(up);
        spectraCache.setLo(lo);

    }

    /**
     * Put the created raster into the image, label the axis and generally make it look pretty and write it out.
     * 
     * @throws IOException probably image write failed
     */
    private void finalizeImage() throws IOException {
        img.setData(rast);
        grph.setFont(lblFont);
        // add the axis labels
        int lasc = labelMetrics.getAscent();
        int imgXmax = imgX0 + dimX;

        // label the X-axis of plot
        if (xTicks == 0) {
            // @todo make an intelligent choice
            xTicks = 11;
        }
        double tw = (double) dimX / (xTicks - 1);
        double dt = secPerFFT * (1 - overlap) * spectraCache.size() / ((float) xTicks - 1);
        String fmt = "%1$.0f";
        if (duration < xTicks) {
            fmt = "%1$.2f";
        } else if (duration < xTicks * 2) {
            fmt = "%1$.1f";
        }
        int y = (int) (pltRect.getY() + pltRect.getHeight() + 5);
        int yp = y + 5 + lasc;
        int gpsyp = (int) (yp + lblHeight * 1.2);
        int lastGpsPos = 0; // too see if it fits nicely
        for (int t = 0; t < xTicks; t++) {
            int x = (int) (t * tw + imgX0);
            long tsec = Math.round(t * dt);
            String xLbl;
            if (duration > 1000) {
                xLbl = TimeAndDate.hrTime(tsec, true);
            } else {
                xLbl = String.format(fmt, t * dt);
            }
            int xw = labelMetrics.stringWidth(xLbl);
            int xp = x - xw / 2;
            if (t == xTicks) { // make sure the last tick is at the edge (round off errors)
                               // and the label ends there since we can't center it
                x = imgXmax;
                xp = x - xw;
            }

            grph.drawLine(x, imgY0, x, y);
            grph.drawString(xLbl, xp, yp);

            // add the gps time for this tick if it fits
            String gpsStr = String.format("%1$,d", (int) (startGPS + t * dt));
            int gpsWdt = labelMetrics.stringWidth(gpsStr);
            int gpsPos = x - gpsWdt / 2;

            if (gpsPos > lastGpsPos + em * 2) {
                grph.drawString(gpsStr, gpsPos, gpsyp);
                lastGpsPos = gpsPos + gpsWdt;
            }
        }
        // label the Y-axis
        if (yTicks == 0) {
            // @todo make a more intelligent choice
            yTicks = 11;
        }
        int xps = imgX0 - 3;
        int xpe = imgX0 + dimX;
        float bw = 1.f / secPerFFT;

        // get the actual frequency range
        fmin = spectraCache.getSmin();
        fmax = spectraCache.getSmax();

        if (logFreq) {
            double mxexp = Math.log10(fmax - fmin);
            double mnexp;
            if (fmin > 0) {
                mnexp = Math.log10(fmin);
            } else {
                fmin = bw;
                mnexp = Math.log10(bw);
            }

            double val = 0;
            double lastLabel = 0;
            for (int ex = (int) Math.floor(mnexp); val < fmax; ex++) {
                double exp;
                if (mnexp < mxexp) {
                    exp = Math.pow(10, ex);
                } else {
                    exp = Math.pow(10, ex - 1);
                }
                for (int s = 1; s < 10; s++) {
                    val = s * exp;
                    if (mnexp == mxexp) {
                        val += Math.pow(10, mnexp);
                    }
                    if (val >= fmin && val <= fmax) {
                        drawGrid(mxexp, val);
                        if (s == 1 || s == 2 || s == 5) {
                            labelYTick(mxexp, val);
                            lastLabel = val;
                        }
                    }
                }
            }
            // mark the max value if it's not too close to the last label
            if ((fmax - lastLabel) / fmax > .15) {
                labelYTick(mxexp, (int) fmax);
            }
        } else {
            double yfact = (fmax - fmin) / yTicks;
            double ypf = ((double) dimY) / yTicks;

            for (int yt = 0; yt <= yTicks; yt++) {
                double yv = yt * yfact + fmin;
                int ytp = (int) (dimY - Math.round(yt * ypf) + imgY0);
                grph.drawLine(xps, ytp, xpe, ytp);
                String fstr = String.format("%1$,.0f", yv);
                int fstrLen = labelMetrics.stringWidth(fstr);
                int fsp = xps - fstrLen - 2;
                grph.drawString(fstr, fsp, ytp + lblHeight / 2);
            }
        }

        // add the aux info line at the bottom
        int imgCenter = (imgXmax - imgX0) / 2;
        String auxInfo = getAuxInfo();

        int xw = labelMetrics.stringWidth(auxInfo);
        int xp = imgCenter - xw / 2 + imgX0;
        int auxyp = (int) (outY - lblHeight * 1.5);

        grph.drawString(auxInfo, xp, auxyp + lblHeight + 3);

        // label the color scale
        labelColorMapTics();

        // write the file
        File outputfile = new File(ofileName);
        ImageIO.write(img, "png", outputfile);
        System.out.println("Wrote: " + ofileName);
    }

    /**
     * draw the grid lines for a log axis
     * @param mxexp - log10 of max value
     * @param val - this value
     */
    private void drawGrid(double mxexp, double val) {
        Color oldcolor = grph.getColor();
        Color transparentWhite = new Color(1, 1, 1, 0.5f);
        grph.setColor(transparentWhite);
        int ypos = getYpos(mxexp, val);
        int xpe = imgX0 + dimX;

        grph.drawLine(imgX0 - 3, ypos, xpe, ypos);
        grph.setColor(oldcolor);
    }

    private int getYpos(double mxexp, double val) {
        int ypos;

        double logMin = fmin == 0 ? 1 : Math.log10(fmin);

        double yfrac = (Math.log10(val) - logMin) / (Math.log10(fmax) - logMin);
        ypos = dimY - (int) (yfrac * dimY) + imgY0;
        return ypos;
    }

    /**
     * Add a tick to the y axis
     * @param mxexp
     * @param val 
     */
    private void labelYTick(double mxexp, double val) {
        int ypos = getYpos(mxexp, val);
        int xps = imgX0 - 5;
        int xpe = imgX0 + dimX;

        grph.drawLine(xps, ypos, xpe, ypos);
        String fmt = "%1$,.0f";
        if (val == 0) {
            fmt = "%1$.1f";
        } else if (val < 1) {
            int exp = -(int) Math.floor(Math.log10(val));
            fmt = "%1$." + Integer.toString(exp) + "f";
        }
        String fstr = String.format(fmt, val);
        int fstrLen = labelMetrics.stringWidth(fstr);
        int fsp = xps - fstrLen - 2;
        grph.drawString(fstr, fsp, ypos + lblHeight / 2);
    }

    /**
     * Process the command line arguments and set fields
     *
     * @param args the main passed in arguments
     * @return true if we continue false means exit (didn't want to do it here)
     */
    private boolean processArgs(String[] args) {
        boolean ret;
        cmd = new SpectrogramCommandLine();
        if (cmd.parseCommand(args, programName, version)) {
            channelName = cmd.getChannelName();
            server = cmd.getServer();
            cType = cmd.getcType();
            sampleRate = cmd.getSampleRate();

            duration = cmd.getDuration() < 1 ? 20 : cmd.getDuration();

            startGPS = cmd.getStartGPS();

            useAltData = cmd.isUseTestData();
            testDataFilename = cmd.getTestDataFile();
            rawDataFilename = cmd.getRawDataFile();
            showProgressBar = cmd.isShowProgressBar();
            smooth = cmd.isSmooth();
            interp = cmd.isInterp();

            secPerFFT = cmd.getSecPerFFT() <= 0 ? 1 : cmd.getSecPerFFT();

            Float ft = cmd.getFmax();
            fmax = ft == null || ft < 0 ? 0. : ft;

            ft = cmd.getFmin();
            fmin = ft == null || ft < 0 ? 0. : ft;

            logFreq = cmd.isLogFreq();
            logIntensity = cmd.isLogIntensity();
            norm = cmd.isNorm();
            scaling = cmd.getScaling();
            dodetrend = cmd.isDetrend();
            window = cmd.getWindow();
            filtType = cmd.getFiltType();
            cutoff = cmd.getCutoff();
            order = cmd.getOrder();

            ofileName = cmd.getOfileName().isEmpty() ? "/tmp/test.png" : cmd.getOfileName();
            outX = Math.max(1024, cmd.getOutX());
            outY = Math.max(768, cmd.getOutY());

            ft = cmd.getOverlap();
            if (ft == null || ft < 0) {
                overlap = -1.f;
            } else {
                overlap = ft;
            }

            xTicks = cmd.getxTicks() < 3 ? 7 : cmd.getxTicks();
            yTicks = cmd.getyTicks() < 3 ? 3 : cmd.getyTicks();
            up = cmd.getUp();
            if (up <= 0 || up > 1) {
                up = 1;
            }
            lo = cmd.getLo();
            if (lo < 0 || lo >= 1) {
                lo = 0;
            }
            color = cmd.getColor();
            if (color.isEmpty()) {
                color = "jet";
            }
            spectrumCalculator = new SpectrumCalc();
            spectrumCalculator.setDoDetrend(dodetrend);
            spectrumCalculator.setScaling(scaling);
            spectrumCalculator.setWindow(window);

            ret = true;
        } else {
            ret = false;
        }

        return ret;
    }
    //===================================|
    // progress bar control              |
    //===================================|

    public void setProgressDialog(Progress pb) {
        progressBar = pb;
    }

    /**
     * set up the progress bar if so requested
     */
    private void initProgress() {

        if (showProgressBar && progressBar == null) { // there's an issue if called from matlab of getting the progress frame
                                                      // in matlab's event loop so we can't just create a new one here
            progressBar = new Progress();
        }
        if (progressBar != null) {
            progressBar.setChanName(channelName);
            progressBar.setWorkingOn("Initializing");
            progressBar.setEstTime("Time remaining: unknown");
            progressBar.setProgress(-1);
            progressBar.setPosition();
        }
    }

    private void setProgress(int dt, int duration) {
        if (progressBar != null) {
            String cur = String.format("%1$d of %2$d seconds of data", dt, duration);
            int pct = Math.round(dt * 100.f / duration);
            double elapsed = (System.currentTimeMillis() - startMs) / 1000.;
            double remaining;
            String etl;
            if (dt > 0) {
                remaining = elapsed * duration / dt - elapsed;
                etl = String.format("Elapsed: %4.0f, remaining: %2$4.0f seconds", elapsed, remaining);
            } else {
                etl = String.format("Elapsed: %4.0f, remaining: unknown", elapsed);
            }
            progressBar.setEstTime(etl);
            progressBar.setProgress(pct);
        }
    }

    /**
     * Set the progress "working on" string and update times
     * @param what new content of working on
     */
    private void setProgress(String what) {
        if (progressBar != null) {
            double elapsed = (System.currentTimeMillis() - startMs) / 1000.;
            String etl = String.format("Elapsed: %4.0f", elapsed);
            progressBar.setEstTime(etl);
            progressBar.setWorkingOn(what);
            progressBar.setProgress(-1);
        }
    }

    private void closeProgress() {
        if (progressBar != null) {
            progressBar.done();
        }
    }

    private boolean checkCancel() {
        boolean ret = false;
        if (progressBar != null) {
            ret = progressBar.wantsCancel();
        }
        return ret;
    }

    //---------------------------------------------|
    //  Access functions so others can control us  |
    //---------------------------------------------|
    public void setStartGPS(int startGPS) {
        this.startGPS = startGPS;
    }

    public void setDuration(int duration) {
        this.duration = duration;
    }

    public void setOfileName(String ofileName) {
        this.ofileName = ofileName;
    }

    public void setShowProgressBar(boolean showProgressBar) {
        this.showProgressBar = showProgressBar;
    }

    //-------------------------------
    public String getStatus() {
        return status;
    }

    /**
     * Connect to the database and create table objects we need
     */
    private void getDbTables() throws LdvTableException, SQLException, ViewConfigException {
        if (db == null) {
            ViewerConfig vc = new ViewerConfig();
            db = vc.getDb();
            if (db == null) {
                throw new LdvTableException("Can't connect to LigoDV-web database");
            }
        }
        if (chanTbl == null) {
            chanTbl = new ChannelTable(db);
        }
    }

    private boolean checkNoData(int curgps, int curDur, String msg) {
        boolean ret = false;
        if (msg.toLowerCase().contains("requested data were not") || msg.toLowerCase().contains("no such channel")
                || msg.toLowerCase().contains("read timed out") || msg.toLowerCase().contains("unknown error")) {
            int strt = (curgps - startGPS);
            int strtSample = (int) (strt * sampleRate);
            int stopSample = (int) ((strt + curDur) * sampleRate - 1);
            noData(strtSample, stopSample);
            ret = true;
        }
        return ret;
    }

    private void drawRotatedText(int tx, int ty, double theta, String text) {

        AffineTransform fontAT = new AffineTransform();
        fontAT.setToIdentity();

        fontAT.rotate(Math.toRadians(theta));

        Font curFont = grph.getFont();
        Font rotFont = curFont.deriveFont(fontAT);

        grph.setFont(rotFont);
        grph.drawString(text, tx, ty);

        grph.setFont(curFont);
    }

    /**
     * draw the color map in predefined rectangle
     */
    private void fillColorMap() {
        int modelSize = colorModel.getMapSize();
        int mapWidth = (int) cmRect.getWidth();
        int mapHeight = (int) cmRect.getHeight();

        // draw the color bar
        double fact = (modelSize - 6) / ((double) mapHeight);
        byte[] colorval = new byte[mapWidth];

        for (int y = 0; y < mapHeight - 1; y++) {
            int my = (int) (cmRect.getY() + mapHeight - y - 1);
            int colorIdx = (int) (y * fact);
            colorIdx = colorIdx >= modelSize ? modelSize - 1 : colorIdx;
            for (int b = 0; b < mapWidth; b++) {
                colorval[b] = (byte) (colorIdx & 255);
            }
            rast.setDataElements((int) (cmRect.getX() + 1), my, mapWidth - 1, 1, colorval);
        }

    }

    /**
     * label the color map appropriately
     */
    private void labelColorMapTics() {
        int mapWidth = (int) cmRect.getWidth();
        int mapHeight = (int) cmRect.getHeight();

        double imin = spectraCache.getiMin();
        double imax = spectraCache.getiMax();

        // add tick marks and labels to the color bar
        int y0 = (int) cmRect.getY();
        int x0 = (int) (cmRect.getX() + mapWidth - 4);
        grph.setFont(lblFont);
        grph.setColor(Color.BLACK);
        for (int p = 0; p <= 100; p++) {
            if ((p % 10 == 0 && p != 90) || (logIntensity && p < 10 && p % 2 == 0)) {
                int p1 = p;
                double p2; // actual value in the image
                p2 = p / 100. * (imax - imin) + imin;
                if (logIntensity) {
                    p1 = p == 0 ? 0 : (int) (Math.log10((double) p) * 50);
                    p2 = Math.pow(10, p2);
                }
                int y = mapHeight - Math.round(p1 * mapHeight / 100.f) + y0;
                grph.drawLine(x0, y, x0 + 6, y);
                String lbl;
                int exp = (int) Math.log10(p2);
                if (Math.abs(exp) < 4) {
                    lbl = String.format("%1$4f", p2);
                } else {
                    double p3 = p2 / Math.pow(10, exp) * 10;
                    lbl = String.format("%1$.2fe%2$d", p3, exp);
                }

                grph.drawString(lbl, x0 + 8, y + lblHeight / 3);
            }
        }
    }

    /**
     * Generate a line of text that describes what we did
     * @return description string
     */
    private String getAuxInfo() {
        String auxInfo;
        int nsamples = (int) (duration * sampleRate);

        float bw = 1.f / secPerFFT;

        if (sampleRate >= 1) {
            auxInfo = String.format("Fs=%1$,.0fHz", sampleRate);
        } else {
            auxInfo = String.format("Fs=%1$,.3fHz", sampleRate);
        }

        auxInfo += String.format(", sec/fft = %1$.2f, overlap = %2$.2f, fft length=%3$,d", secPerFFT, overlap,
                (int) (secPerFFT * sampleRate));

        auxInfo += String.format(", #-FFT = %1$d", spectraCache.size());

        if (bw >= 1) {
            auxInfo += String.format(", bw = %1$.0f", bw);
        } else {
            auxInfo += String.format(", bw = %1$.2f", bw);
        }
        auxInfo += String.format(", in samples = %1$,.0fK", nsamples / 1000.);
        if (up < .999) {
            auxInfo += String.format(", up = %1$.2f", up);
        }
        if (lo > .001) {
            auxInfo += String.format(", low = %1$.2f", lo);
        }
        return auxInfo;
    }

    /**
     * Use a local file as test data
     * written during the LVC meeting in the Bethesda Hyatt where Internet service sucked big time
     * @param testData the csv file to read
     */
    private void readAddTestData(File testData) {
        try {
            setProgress("Read and add test data.");
            ArrayList<Double> data = new ArrayList<>();
            // read the file
            CSVReader csvReader = new CSVReader(new InputStreamReader(new FileInputStream(testData)));

            String[] rowAsTokens;
            String val;
            Double v;
            int nvals = 0;
            String fltPat = "[+\\-]?(([1-9][0-9]*\\.?[0-9]*)|(\\.[0-9]+))([Ee][+-]?[0-9]+)?";
            while ((rowAsTokens = csvReader.readNext()) != null) {
                val = rowAsTokens.length == 2 ? rowAsTokens[1] : rowAsTokens[0];
                val = val.trim();
                if (val.matches(fltPat)) {
                    v = Double.parseDouble(val);
                    data.add(v);
                    nvals++;
                }
            }

            if (data.size() < 16) {
                noData(0, (int) (16 * sampleRate));
            } else {
                if (debugLevel > 4) {
                    double[] dbgRaw = new double[data.size()];
                    for (int idx = 0; idx < data.size(); idx++) {
                        dbgRaw[idx] = data.get(idx);
                    }
                    writeRawBuffer(-1, dbgRaw);
                }
                int startPos = 0;
                duration = (int) (data.size() / sampleRate);
                double[] fftdata = new double[flen];
                binWidth = 1.0 / secPerFFT;
                while (startPos + flen <= data.size()) {
                    for (int idx = 0; idx < flen; idx++) {
                        fftdata[idx] = data.get(idx + startPos);
                    }

                    double[] temp = new double[fftdata.length];
                    System.arraycopy(fftdata, 0, temp, 0, fftdata.length);
                    writeRawBuffer(startPos, temp);
                    double[] result = spectrumCalculator.doCalc(temp, sampleRate);
                    spectraCache.add(startPos, result, sampleRate, fmax, fmin, binWidth);
                    startPos += flen - overlapSamples;
                }
            }
            setProgress("All spectra calculated");
        } catch (IOException | NumberFormatException ex) {
            Logger.getLogger(Spectrogram.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void readAddRawData() throws WebUtilException, NDSException {
        if (rawDataFilename != null && !rawDataFilename.isEmpty()) {
            rawDataFile = new File(rawDataFilename);
        }
        if (rawDataFile == null || !rawDataFile.canRead()) {
            throw new WebUtilException("Request for raw data but file cannot be read, or not set.");
        }
        long nSamples = rawDataFile.length() / (Float.SIZE / 8) / 2;
        try {

            inStream = new SwappedDataInputStream(new FileInputStream(rawDataFile));
            int startPos = 0;
            int blen = (int) Math.min(nSamples, 1024 * 1024);
            double[] rawDataBuffer = new double[blen];

            for (long n = 0; n < nSamples; n += blen) {
                int dlen = blen;
                if (n + blen > nSamples) {
                    dlen = (int) (nSamples - n);
                }
                for (int i = 0; i < dlen; i++) {
                    Float t = inStream.readFloat();
                    Float d = inStream.readFloat();
                    rawDataBuffer[i] = d;
                }
                addBuf(rawDataBuffer, (int) n, dlen);
            }
        } catch (IOException ex) {
            throw new WebUtilException("Error reading raw data file", ex);
        }

    }

    /**
     * Select a font that a) looks good and b) is available on this system (Macs were getting me)
     * @return name of the font
     */
    private String getAGoodFont() {
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();

        String[] fontNames = ge.getAvailableFontFamilyNames();
        String ret = "";
        for (int idx = 0; idx < fontNames.length && ret.isEmpty(); idx++) {
            String font = fontNames[idx];
            if (font.equalsIgnoreCase("DejaVu Sans")) {
                ret = fontNames[idx];
            }
        }
        for (int idx = 0; idx < fontNames.length && ret.isEmpty(); idx++) {
            String font = fontNames[idx];
            if (font.toLowerCase().contains("century schoolbook l")) {
                ret = fontNames[idx];
            }
        }
        if (ret.isEmpty()) {
            ret = "Serif";
        }
        return ret;
    }

    /**
     * If the appropriate debug level is set write the raw data used for ffts
     * @param bufn - identifying buffer number -1 means all
     * @param rawData array to be written
     */
    private void writeRawBuffer(int bufn, double[] rawData) {
        if (debugLevel > 2) {
            BufferedWriter bw = null;
            try {
                String fname;
                if (bufn == -1) {
                    fname = "/tmp/raw-all.csv";
                } else {
                    fname = String.format("/tmp/raw-%1$03d.csv", bufn);
                }
                bw = new BufferedWriter(new FileWriter(fname));
                for (int idx = 0; idx < rawData.length; idx++) {
                    String l = String.format("%1$.10f\n", rawData[idx]);
                    bw.write(l);
                }
            } catch (IOException ex) {
                Logger.getLogger(Spectrogram.class.getName()).log(Level.SEVERE, null, ex);
            } finally {
                try {
                    bw.close();
                } catch (IOException ex) {
                    Logger.getLogger(Spectrogram.class.getName()).log(Level.SEVERE, null, ex);
                }
            }
        }

    }

    /**
     * For binary input files from our frame reader set up the input stream
     *
     * @return number of entries to read
     * @throws WebUtilException
     */
    private long setupFileReads(String infilename) throws WebUtilException {
        setProgress("Scan input file for min/max GPS times.");
        File inFile = new File(infilename);
        long siz = inFile.length() / (Float.SIZE / 8) / 2; // convert bytes to # entries (time, val)
        if (!inFile.canRead()) {
            throw new WebUtilException("Can't open " + infilename + " for reading");
        }
        try {
            inStream = new SwappedDataInputStream(new FileInputStream(inFile));
            float minTime = Float.MAX_VALUE;
            float maxTime = -Float.MAX_VALUE;

            setProgress("Searhing for min/max time in input file.");
            int opct = 0;
            for (int i = 0; i < siz; i++) {
                int pct = (int) (100 * i / siz);
                if (pct > opct) {
                    setProgress(pct, 100);
                    opct = pct;
                }
                Float t = inStream.readFloat();
                Float d = inStream.readFloat();
                minTime = Math.min(minTime, t);
                maxTime = Math.max(maxTime, t);
            }
            startGPS = (int) (minTime);
            duration = (int) (maxTime - minTime);
            inStream.close();
            inStream = new SwappedDataInputStream(new FileInputStream(inFile));
        } catch (IOException ex) {
            throw new WebUtilException("Can't open " + infilename + " for reading");
        }

        return siz;
    }

}