edu.fullerton.viewerplugin.SpectrumPlot.java Source code

Java tutorial

Introduction

Here is the source code for edu.fullerton.viewerplugin.SpectrumPlot.java

Source

/*
 * Copyright (C) 2012-2014 Joseph Areeda <joe@areeda.com>
 *
 * 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 edu.fullerton.viewerplugin;

import edu.emory.mathcs.jtransforms.fft.DoubleFFT_1D;
import edu.fullerton.jspWebUtils.PageFormCheckbox;
import edu.fullerton.jspWebUtils.PageFormSelect;
import edu.fullerton.jspWebUtils.PageItem;
import edu.fullerton.jspWebUtils.PageItemList;
import edu.fullerton.jspWebUtils.PageItemString;
import edu.fullerton.jspWebUtils.PageTable;
import edu.fullerton.jspWebUtils.PageTableRow;
import edu.fullerton.jspWebUtils.WebUtilException;
import edu.fullerton.ldvjutils.ChanInfo;
import edu.fullerton.ldvjutils.LdvTableException;
import edu.fullerton.ldvjutils.TimeAndDate;
import edu.fullerton.ldvjutils.TimeInterval;
import edu.fullerton.viewerplugin.SpectrumCalc.Scaling;
import edu.fullerton.viewerplugin.WindowGen.Window;
import java.awt.BasicStroke;
import java.awt.Color;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.sql.SQLException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.LogAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.xy.DefaultXYDataset;
import org.jfree.data.xy.XYSeries;
import org.jfree.data.xy.XYSeriesCollection;

/**
 * Generate a spectrum from a time series
 * 
 * @author Joseph Areeda <joe@areeda.com>
 */
public class SpectrumPlot extends PluginSupport implements PlotProduct {

    private boolean wantStacked = false;
    private float secperfft = 1.0f;
    private float overlap = 0.5f;
    private float fmin, fmax, fsMax;

    private Window window = Window.NONE;
    private Scaling pwrScale = Scaling.ASD;
    private boolean doDetrend = true;
    private boolean logXaxis = false;
    private boolean logYaxis = false;
    private double smallestX = 1e-3;
    private double smallestY = 1e-10;
    private int lineThickness;
    private int nfft;

    public SpectrumPlot() {
    }

    @Override
    public ArrayList<Integer> makePlot(ArrayList<ChanDataBuffer> dbufs, boolean compact) throws WebUtilException {
        ArrayList<Integer> ret = new ArrayList<>();
        if (parameterMap.containsKey("sp_newplt")) {
            try {
                ret = makeAddPlotFiles(dbufs, compact);
            } catch (LdvTableException ex) {
                throw new WebUtilException("Making spectrum plot: ", ex);
            }
        } else {
            int imageId;
            ChartPanel cpnl = getPanel(dbufs, compact);
            try {
                imageId = saveImageAsPNG(cpnl);
                ret.add(imageId);
            } catch (IOException | NoSuchAlgorithmException | SQLException ex) {
                throw new WebUtilException("Making spectrum plot: ", ex);
            }
        }

        return ret;

    }

    public void makePlotFile(ArrayList<ChanDataBuffer> dbufs, boolean compact, String fname)
            throws WebUtilException {
        ChartPanel cpnl = getPanel(dbufs, compact);
        try {
            saveImageAsPNGFile(cpnl, fname);
        } catch (WebUtilException ex) {
            throw new WebUtilException(
                    "Creating spectrum plot: " + ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage());
        }
    }

    private ChartPanel getPanel(ArrayList<ChanDataBuffer> dbufs, boolean compact) throws WebUtilException {
        ChartPanel ret = null;
        try {
            float tfsMax = 0;
            for (ChanDataBuffer buf : dbufs) {
                ChanInfo ci = buf.getChanInfo();
                float fs = ci.getRate();
                tfsMax = Math.max(fs, tfsMax);
            }
            setFsMax(tfsMax);
            String gtitle = getTitle(dbufs, compact);
            int nbuf = dbufs.size();
            XYSeries[] xys = new XYSeries[nbuf];
            XYSeriesCollection mtds = new XYSeriesCollection();

            int cnum = 0;
            compact = dbufs.size() > 2 ? false : compact;
            float bw = 1.f;
            for (ChanDataBuffer dbuf : dbufs) {
                String legend = getLegend(dbuf, compact);

                xys[cnum] = new XYSeries(legend);

                bw = calcSpectrum(xys[cnum], dbuf);

                mtds.addSeries(xys[cnum]);
            }

            DefaultXYDataset ds = new DefaultXYDataset();
            String yLabel = pwrScale.toString();
            DecimalFormat dform = new DecimalFormat("0.0###");
            String xLabel;
            xLabel = String.format("Frequency Hz - (bw: %1$s, #fft: %2$,d, s/fft: %3$.2f, ov: %4$.2f)",
                    dform.format(bw), nfft, secperfft, overlap);

            Double minx, miny, maxx, maxy;
            Double[] rng = new Double[4];
            if (fmin <= 0) {
                fmin = bw;
            }
            float searchFmax = fmax;
            if (fmax <= 0 || fmax == Float.MAX_VALUE) {
                fmax = tfsMax / 2;
                searchFmax = tfsMax / 2 * 0.8f;
            }
            PluginSupport.getRangeLimits(mtds, rng, 2, fmin, searchFmax);
            minx = rng[0];
            miny = rng[1];
            maxx = rng[2];
            maxy = rng[3];

            findSmallest(mtds);
            int exp;
            if (maxy == 0. && miny == 0.) {
                miny = -1.;
                exp = 0;
                logYaxis = false;
            } else {
                miny = miny > 0 ? miny : smallestY;
                maxy = maxy > 0 ? maxy : miny * 10;
                exp = PluginSupport.scaleRange(mtds, miny, maxy);
                if (!logYaxis) {
                    yLabel += " x 1e-" + Integer.toString(exp);
                }
            }
            JFreeChart chart = ChartFactory.createXYLineChart(gtitle, xLabel, yLabel, ds, PlotOrientation.VERTICAL,
                    true, false, false);
            XYPlot plot = (XYPlot) chart.getPlot();
            if (logYaxis) {
                LogAxis rangeAxis = new LogAxis(yLabel);
                double smallest = miny * Math.pow(10, exp);
                rangeAxis.setSmallestValue(smallest);
                rangeAxis.setMinorTickCount(9);

                LogAxisNumberFormat lanf = new LogAxisNumberFormat();
                lanf.setExp(exp);

                rangeAxis.setNumberFormatOverride(lanf);
                rangeAxis.setRange(smallest, maxy * Math.pow(10, exp));
                rangeAxis.setStandardTickUnits(LogAxis.createLogTickUnits(Locale.US));
                plot.setRangeAxis(rangeAxis);
                plot.setRangeGridlinesVisible(true);
                plot.setRangeGridlinePaint(Color.BLACK);
            }
            if (logXaxis) {
                LogAxis domainAxis = new LogAxis(xLabel);
                domainAxis.setBase(2);
                domainAxis.setMinorTickCount(9);
                domainAxis.setMinorTickMarksVisible(true);
                domainAxis.setSmallestValue(smallestX);
                domainAxis.setNumberFormatOverride(new LogAxisNumberFormat());
                //domainAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
                plot.setDomainAxis(domainAxis);
                plot.setDomainGridlinesVisible(true);
                plot.setDomainGridlinePaint(Color.BLACK);
            }
            ValueAxis domainAxis = plot.getDomainAxis();
            if (fmin > Float.MIN_VALUE) {
                domainAxis.setLowerBound(fmin);
            }
            if (fmax != Float.MAX_VALUE) {
                domainAxis.setUpperBound(fmax);
            }
            plot.setDomainAxis(domainAxis);
            plot.setDataset(0, mtds);
            plot.setDomainGridlinePaint(Color.DARK_GRAY);
            plot.setRangeGridlinePaint(Color.DARK_GRAY);

            // Set the line thickness
            XYLineAndShapeRenderer r = (XYLineAndShapeRenderer) plot.getRenderer();
            BasicStroke str = new BasicStroke(lineThickness);
            int n = plot.getSeriesCount();
            for (int i = 0; i < n; i++) {
                r.setSeriesStroke(i, str);
            }
            plot.setBackgroundPaint(Color.WHITE);
            if (compact) {
                chart.removeLegend();
            }
            ret = new ChartPanel(chart);
        } catch (Exception ex) {
            throw new WebUtilException("Creating spectrum plot" + ex.getLocalizedMessage());
        }
        return ret;

    }

    @Override
    public boolean isStackable() {
        return true;
    }

    @Override
    public String getProductName() {
        String name = "Spectrum";
        return name;
    }

    @Override
    public void setDispFormat(String dispFormat) {
        if (dispFormat.equalsIgnoreCase("Stacked")) {
            wantStacked = true;
        }
    }

    public double[][] calcSpectrum(ChanDataBuffer dbuf) throws LdvTableException {

        long dlen = dbuf.getDataLength();
        float rate = dbuf.getChanInfo().getRate();

        int flen = Math.round(rate * secperfft);
        int ov = Math.round(rate * overlap);
        //@todo verify all this will work

        float[] data = dbuf.getData();

        DoubleFFT_1D fft = new DoubleFFT_1D(flen);
        double[] fftd = new double[flen * 2];

        double[] result = new double[flen / 2 + 1];
        for (int i = 0; i < flen / 2 + 1; i++) {
            result[i] = 0;
        }

        double[] win = WindowGen.getWindow(window, flen);

        nfft = 0; // number of ffts summed into result
        float[] dtemp = new float[flen];
        for (int idx = 0; idx + flen <= dlen; idx += flen - ov) {
            // copy this segment into temp buffer
            for (int i2 = 0; i2 < flen; i2++) {
                dtemp[i2] = data[i2 + idx];
            }
            detrend(dtemp); // subtract a linear fit to the data

            // apply window function
            for (int i2 = 0; i2 < flen; i2++) {
                dtemp[i2] *= win[i2];
            }
            for (int i2 = 0; i2 < flen; i2++) {
                fftd[i2 * 2] = dtemp[i2];
                fftd[i2 * 2 + 1] = 0.f;
            }
            fft.complexForward(fftd);
            nfft++;
            double r;
            double im;
            for (int i3 = 0; i3 < flen / 2; i3++) {
                r = fftd[i3 * 2];
                im = fftd[i3 * 2 + 1];

                result[i3] += (r * r + im * im);
            }
        }
        // calculate scale factor
        double winsum = 0., winsumsq = 0;
        for (int idx = 0; idx < flen; idx++) {
            winsum += win[idx];
            winsumsq += win[idx] * win[idx];
        }
        double scale = 1.;
        if (pwrScale == Scaling.AS) {
            scale = Math.sqrt(2.) / winsum;
        } else if (pwrScale == Scaling.ASD) {
            scale = Math.sqrt(2. / (winsumsq * rate));
        } else if (pwrScale == Scaling.PS) {
            scale = 2 / (winsum * winsum);
        } else if (pwrScale == Scaling.PSD) {
            scale = 2 / (rate * winsumsq);
        }

        // create data series for plotting
        double df = 1 / secperfft; // frequency separation for each fft bin
        double[][] spectrum = new double[flen / 2 + 1][2];

        for (int idx = 0; idx < flen / 2 + 1; idx++) {
            double x = df * idx; // frequency of this bin
            double y = result[idx] / nfft;
            if (pwrScale == Scaling.AS || pwrScale == Scaling.ASD) {
                y = (Math.sqrt(y) * scale);
            } else {
                y *= scale;
            }
            spectrum[idx][0] = x;
            spectrum[idx][1] = y;
        }
        return spectrum;
    }

    private float calcSpectrum(XYSeries xySeries, ChanDataBuffer dbuf) throws LdvTableException {
        double[][] spectrum = calcSpectrum(dbuf);
        for (double[] spectrum1 : spectrum) {
            xySeries.add(spectrum1[0], spectrum1[1]);
        }
        float df = 1 / secperfft;
        return df;
    }

    @Override
    public void setParameters(Map<String, String[]> parameterMap) {
        this.parameterMap = parameterMap;
        String[] t;
        t = parameterMap.get("window");
        String it = t != null && t.length > 0 ? t[0] : "";

        if (it.isEmpty() || it.equalsIgnoreCase("none")) {
            window = Window.NONE;
        } else if (it.equalsIgnoreCase("hanning")) {
            window = Window.HANNING;
        } else if (it.equalsIgnoreCase("flattop")) {
            window = Window.FLATTOP;
        } else {
            window = Window.HANNING;
        }

        t = parameterMap.get("scaling");
        it = t != null && t.length > 0 ? t[0] : "";
        if (it.equalsIgnoreCase("Amplitude spectrum")) {
            pwrScale = Scaling.AS;
        } else if (it.equalsIgnoreCase("Amplitude spectral density")) {
            pwrScale = Scaling.ASD;
        } else if (it.equalsIgnoreCase("Power specturm")) {
            pwrScale = Scaling.PS;
        } else if (it.equalsIgnoreCase("Power spectral density")) {
            pwrScale = Scaling.PSD;
        }

        t = parameterMap.get("sp_linethickness");
        if (t == null || !t[0].trim().matches("^\\d+$")) {
            lineThickness = 2;
        } else {
            lineThickness = Integer.parseInt(t[0]);
        }

        t = parameterMap.get("secperfft");
        if (t == null) {
            secperfft = 1;
        } else if (t[0].matches("[\\d\\.]+")) {
            this.secperfft = (float) Double.parseDouble(t[0]);
        } else {
            vpage.add("Invalid entry for seconds per fft, using 1");
        }

        t = parameterMap.get("fftoverlap");
        if (t == null) {
            overlap = secperfft / 2;
        } else if (t[0].matches("[\\d\\.]+")) {
            double tmp = Double.parseDouble(t[0]);
            if (tmp >= 0 && tmp < 1) {
                this.overlap = (float) (tmp * secperfft);
            } else {
                vpage.add("Invalid entry for overlap, using 0.5");
                overlap = secperfft / 2;
            }
        } else {
            vpage.add("Invalid entry for overlap, using 0.5");
            overlap = secperfft / 2;
        }

        t = parameterMap.get("fmin");
        if (t == null) {
            fmin = Float.MIN_VALUE;
        } else if (t[0].matches("[\\d\\.]+")) {
            fmin = (float) Double.parseDouble(t[0]);
        } else {
            fmin = Float.MIN_VALUE;
        }

        t = parameterMap.get("fmax");
        if (t == null) {
            fmax = Float.MAX_VALUE;
        } else if (t[0].matches("[\\d\\.]+")) {
            fmax = (float) Double.parseDouble(t[0]);
        } else {
            fmax = Float.MAX_VALUE;
        }
        t = parameterMap.get("sp_logx");
        logXaxis = t != null;

        t = parameterMap.get("sp_logy");
        logYaxis = t != null;
    }

    private void findSmallest(XYSeriesCollection mtds) {
        double minx = Double.MAX_VALUE;
        double miny = Double.MAX_VALUE;
        for (Iterator it = mtds.getSeries().iterator(); it.hasNext();) {
            XYSeries ds = (XYSeries) it.next();
            for (int item = 1; item < ds.getItemCount() - 1; item++) {
                double x = ds.getX(item).doubleValue();
                double y = ds.getY(item).doubleValue();
                minx = Math.min(minx, x);
                miny = Math.min(miny, y);
            }
        }
        smallestX = minx;
        smallestY = 1e-40;
        double t;
        for (int e = 40; e > -40; e--) {
            t = Math.pow(10., e);
            if (miny >= t) {
                smallestY = t;
                break;
            }
        }

    }

    public void detrend(float[] data) {
        int dataLength = data.length;
        double[] x = new double[dataLength];
        double[] y = new double[dataLength];

        for (int i = 0; i < dataLength; i++) {

            x[i] = i;
            y[i] = data[i];
        }
        LinearRegression lr = new LinearRegression(x, y);
        double b = lr.getIntercept();
        double m = lr.getSlope();
        double fit, t;
        for (int i = 0; i < dataLength; i++) {
            data[i] = (float) (data[i] - (m * i + b));
        }
    }

    @Override
    public PageItem getSelector(String enableKey, int nSel, String[] multDisp) throws WebUtilException {
        String[] windows = { "Hanning", "Flattop", "None", };
        String[] scalingNames = { "Amplitude spectral density", "Amplitude spectrum", "Power specturm",
                "Power spectral density" };
        String[] lineThicknessOptions = { "1", "2", "3", "4" };

        this.enableKey = enableKey; // save for fancy page formatting

        PageItemList ret = new PageItemList();
        String enableText = "Generate spectrum plot";
        enableText += nSel > 1 ? "s" : "";
        boolean enabled = getPrevValue(enableKey);
        PageFormCheckbox cb = new PageFormCheckbox(enableKey, enableText, enabled);
        cb.setId(enableKey + "_cb");
        String fun = String.format("boldTextOnCheckbox('%1$s_cb','%1$s_accLbl')", enableKey);
        cb.addEvent("onclick", fun);
        ret.add(cb);

        ret.addBlankLines(1);
        ret.add("Set appropriate parameters.");
        ret.addBlankLines(1);
        ret.addBlankLines(1);

        PageTable product = new PageTable();
        product.setClassName("SelectorTable");
        PageTableRow ptr;

        // window and scaling
        PageFormSelect win = new PageFormSelect("window", windows);
        String prevVal = getPrevValue("window", 0, windows[0]);
        win.setSelected(prevVal);
        ptr = GUISupport.getObjRow(win, "Window:", "");
        product.addRow(ptr);

        PageFormSelect scale = new PageFormSelect("scaling", scalingNames);
        prevVal = getPrevValue("scaling", 0, scalingNames[0]);
        ptr = GUISupport.getObjRow(scale, "Scaling:", "");
        product.addRow(ptr);

        PageFormSelect lineThicknessSelector = new PageFormSelect("sp_linethickness", lineThicknessOptions);
        prevVal = getPrevValue("sp_linethickness", 0, "2");
        lineThicknessSelector.setSelected(prevVal);
        ptr = GUISupport.getObjRow(lineThicknessSelector, "Line thickness: ", "");
        product.addRow(ptr);

        // length and overlap off fft
        prevVal = getPrevValue("secperfft", 0, "1.0");
        ptr = GUISupport.getTxtRow("secperfft", "Sec/fft:", "", 16, prevVal);
        product.addRow(ptr);

        prevVal = getPrevValue("fftoverlap", 0, "0.5");
        ptr = GUISupport.getTxtRow("fftoverlap", "Overlap fraction [0-1):", "", 16, prevVal);
        product.addRow(ptr);

        // frequncy axis limits
        prevVal = getPrevValue("fmin", 0, "");
        ptr = GUISupport.getTxtRow("fmin", "Min freq:", "Leave blank for auto", 16, prevVal);
        product.addRow(ptr);

        prevVal = getPrevValue("fmax", 0, "");
        ptr = GUISupport.getTxtRow("fmax", "Max freq:", "Leave blank for auto", 16, prevVal);
        product.addRow(ptr);

        // do we want axis to be logarithmic
        PageFormCheckbox logx = new PageFormCheckbox("sp_logx", "Freq axis logarithmic", true);
        ptr = GUISupport.getObjRow(logx, "", "");
        product.addRow(ptr);

        PageFormCheckbox logy = new PageFormCheckbox("sp_logy", "Range axis logarithmic", true);
        ptr = GUISupport.getObjRow(logy, "", "");
        product.addRow(ptr);

        enabled = getPrevValue("sp_dnld");
        PageFormCheckbox dnld = new PageFormCheckbox("sp_dnld", "Download spectrum as CSV", enabled);
        ptr = GUISupport.getObjRow(dnld, "", "No plots will be produced and 1 series only can be selected");
        product.addRow(ptr);

        PageFormCheckbox newPlt = new PageFormCheckbox("sp_newplt", "Use new plot functions", true);
        ptr = GUISupport.getObjRow(newPlt, "", "Uncheck to use classic plots, leave for new routines");
        product.addRow(ptr);

        ret.add(product);

        return ret;
    }

    public void setWantStacked(boolean wantStacked) {
        this.wantStacked = wantStacked;
    }

    public void setSecperfft(float secperfft) {
        this.secperfft = secperfft;
    }

    public void setOverlap(float overlap) {
        this.overlap = overlap;
    }

    public void setFmin(float fmin) {
        this.fmin = fmin;
    }

    public void setFmax(float fmax) {
        this.fmax = fmax;
    }

    public void setFsMax(float fsMax) {
        this.fsMax = fsMax;
    }

    public void setWindow(Window window) {
        this.window = window;
    }

    public void setScaling(SpectrumCalc.Scaling scaling) {
        this.pwrScale = scaling;
    }

    public void setDoDetrend(boolean doDetrend) {
        this.doDetrend = doDetrend;
    }

    public void setLogXaxis(boolean logXaxis) {
        this.logXaxis = logXaxis;
    }

    public void setLogYaxis(boolean logYaxis) {
        this.logYaxis = logYaxis;
    }

    @Override
    public boolean needsImageDescriptor() {
        return true;
    }

    @Override
    public boolean hasImages() {
        return true;
    }

    private class genPlotInfo {
        public File spFile;
        public String title;
        public String legend;

        genPlotInfo(File spFile, String title, String legend) {
            this.spFile = spFile;
            this.title = title;
            this.legend = legend;
        }
    }

    /**
     * use the external program genPlot.py to generate the graph
     * @param dbufs input spec
     * @param compact minimize label because output image will be small
     * @return 
     */
    private ArrayList<Integer> makeAddPlotFiles(ArrayList<ChanDataBuffer> dbufs, boolean compact)
            throws WebUtilException, LdvTableException {
        ExternalProgramManager epm = new ExternalProgramManager();
        ArrayList<Integer> ret = new ArrayList<>();
        float fsMax = 0;
        try {
            ArrayList<String> cmd = new ArrayList<>();
            ArrayList<genPlotInfo> spectra = new ArrayList<>();

            File tempDir = epm.getTempDir("sp_");
            File outFile = epm.getTempFile("sp_plot_", ".png");

            // we need to know for labeling
            boolean sameChannel = true;
            boolean sameTime = true;
            boolean multiPlot = dbufs.size() > 1;

            Set<ChanInfo> cis = new TreeSet<>();
            Set<TimeInterval> tis = new TreeSet<>();

            if (multiPlot) {
                for (ChanDataBuffer buf : dbufs) {
                    cis.add(buf.getChanInfo());
                    tis.add(buf.getTimeInterval());
                }
                sameChannel = cis.size() == 1;
                sameTime = tis.size() == 1;
            }

            for (ChanDataBuffer buf : dbufs) {
                double[][] spectrum = calcSpectrum(buf);
                File spFile = epm.writeTempCSV("Spectrum_", spectrum);
                String fTitle = "";
                String fLegend = "";
                float fs = buf.getChanInfo().getRate();
                fsMax = Math.max(fsMax, fs);
                if (!sameChannel) {
                    String chanName = buf.getChanInfo().getChanName();
                    String chanFs;
                    if (fs >= 1) {
                        chanFs = String.format("%1$.0f Hz", fs);
                    } else {
                        chanFs = String.format("%1$.3f Hz", fs);
                    }
                    if (fTitle.length() > 0) {
                        fTitle += ", ";
                    }
                    fLegend += String.format("%1$s at %2$s ", chanName, chanFs);
                }
                if (!sameTime) {
                    long gps = buf.getTimeInterval().getStartGps();
                    String utc = TimeAndDate.gpsAsUtcString(gps);
                    fLegend += String.format("%1$s (%2$d)", utc, gps);
                }
                spectra.add(new genPlotInfo(spFile, fTitle, fLegend));
            }
            File outImg = epm.getTempFile("spPlot_", ".png");

            cmd.add("/usr/local/ldvw/bin/genPlot.py");
            // add input files
            for (genPlotInfo gpi : spectra) {
                File f = gpi.spFile;
                cmd.add("--infile");
                cmd.add(f.getCanonicalPath());
                if (!sameChannel || !sameTime) {
                    if (gpi.title.length() > 0) {
                        cmd.add("--title");
                        cmd.add(gpi.title);
                    }
                    if (gpi.legend.length() > 0) {
                        cmd.add("--legend");
                        cmd.add(gpi.legend);
                    }
                }
            }
            // add and output file
            cmd.add("--out");
            cmd.add(outFile.getCanonicalPath());
            // add options
            if (parameterMap.containsKey("sp_logy")) {
                cmd.add("--logy");
            }
            if (parameterMap.containsKey("sp_logx")) {
                cmd.add("--logx");
            }
            if (height > 100 && width > 100) {
                cmd.add("--geometry");
                cmd.add(String.format("%1$dx%2$d", width, height));
            }
            if (fmin > 0) {
                cmd.add("--xmin");
                cmd.add(String.format("%1$.2f", fmin));
            }
            if (fmax < fsMax && fmax > 0) {
                cmd.add("--xmax");
                cmd.add(String.format("%1$.2f", fmax));
            }
            // add the super title
            String supTitle = "Spectrum plot";
            long dur = dbufs.get(0).getTimeInterval().getDuration();
            String durStr = String.format("%1$,d s", dur);
            if (dur >= 3600) {
                durStr = TimeAndDate.hrTime(dur);
            }
            if (!multiPlot) {
                supTitle = getTitle(dbufs, false);
            } else if (!sameChannel && sameTime) {
                long gps = dbufs.get(0).getTimeInterval().getStartGps();

                supTitle = String.format("%1$s (%2$d) t = %3$s", TimeAndDate.gpsAsUtcString(gps), gps, durStr);
            } else if (sameChannel && !sameTime) {
                String chanFs;
                float fs = dbufs.get(0).getChanInfo().getRate();
                if (fs > 1) {
                    chanFs = String.format("%1$.0f", fs);
                } else {
                    chanFs = String.format("%1$.3f", fs);
                }
                supTitle = String.format("%1$s at %2$s Hz, t=%3$s", dbufs.get(0).getChanInfo().getChanName(),
                        chanFs, durStr);
            } else if (!sameChannel && !sameTime) {
                supTitle = "Spectrum plot";
            }
            cmd.add("--suptitle");
            cmd.add(supTitle);
            // axis labels
            DecimalFormat dform = new DecimalFormat("0.0###");
            float bw = 1 / secperfft;

            cmd.add("--xlabel");
            cmd.add(String.format("Frequency Hz,  bw: %1$s, " + "fft: %2$,d, s/fft: %3$.2f, ov: %4$.2f",
                    dform.format(bw), nfft, secperfft, overlap));
            pwrScale.setTex(true);
            cmd.add("--ylabel");
            cmd.add(pwrScale.toString());
            //cmd.add("--test");
            if (epm.runExternalProgram(cmd)) {
                int imgId = epm.addImg2Db(outFile, db, vuser.getCn());
                ret.add(imgId);
            } else {
                vpage.add("Problem generating plot of spectrum.");
                vpage.addBlankLines(2);
                vpage.add("Command line: ");
                vpage.addBlankLines(1);
                vpage.add(new PageItemString(cmd.toString(), false));
                vpage.addBlankLines(2);
                vpage.add("Stderr:");
                vpage.addBlankLines(1);
                vpage.add(new PageItemString(epm.getStderr(), false));
                vpage.addBlankLines(2);
                vpage.add("Stdout:");
                vpage.addBlankLines(1);
                vpage.add(new PageItemString(epm.getStdout(), false));
                vpage.addBlankLines(2);
            }

        } catch (IOException ex) {
            throw new WebUtilException("Spectrum plot (genPlot.py):", ex);
        } finally {
            epm.removeTemps();
        }
        return ret;
    }

}