/* SignalFFTPlot.java created 2007-12-16 * */ package org.signalml.plugin.fftsignaltool; import static org.signalml.plugin.fftsignaltool.FFTSignalTool._; import static org.signalml.plugin.fftsignaltool.FFTSignalTool._R; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Insets; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.util.Calendar; import javax.swing.JComponent; import javax.swing.border.LineBorder; import org.apache.log4j.Logger; import org.jfree.chart.JFreeChart; import org.jfree.chart.axis.LogAxis; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.NumberTickUnit; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; import org.jfree.chart.renderer.xy.XYSplineRenderer; import org.jfree.chart.title.TextTitle; import org.jfree.data.xy.DefaultXYDataset; import org.signalml.exception.SanityCheckException; import org.signalml.math.fft.FourierTransform; import org.signalml.math.fft.WindowType; import org.signalml.plugin.export.NoActiveObjectException; import org.signalml.plugin.export.SvarogAccess; import org.signalml.plugin.export.signal.ChannelSamples; import org.signalml.plugin.export.signal.SvarogAccessSignal; import org.signalml.plugin.export.view.ExportedSignalPlot; import org.signalml.util.FormatUtils; import org.signalml.util.Util; /** * Plot on which the power spectrum of the signal (fragment near cursor) is * displayed: * <ul><li> * The parameters of the display are stored in {@link SignalFFTSettings}.</li> * <li>The power spectrum is calculated using the {@link FourierTransform FFT}. * </li></ul> * * @author Michal Dobaczewski © 2007-2008 CC Otwarte Systemy Komputerowe * Sp. z o.o., Marcin Szumski */ public class SignalFFTPlot extends JComponent { /** * serialization constant */ private static final long serialVersionUID = 1L; /** * the logger */ protected static final Logger logger = Logger .getLogger(SignalFFTPlot.class); /** * how many FFT result points to cutoff at the left - discards the mean to * improve plot clarity */ private static final int LEFT_CUTOFF = 1; /** * the {@link ExportedSignalPlot signal plot} for which this FFT plot is * calculated */ private ExportedSignalPlot plot; /** * the {@link SvarogAccessSignal access} to Svarog logic */ private SvarogAccessSignal signalAccess; /** * the currently clicked point on the signal plot */ private Point focusPoint; /** * the number of the channel for which the FFT is drawn */ private int channel; /** * the size of this plot */ private Dimension plotSize; /** * the number of samples in the window */ private int windowWidth; /** * the {@link WindowType type} of the window function */ private WindowType windowType; /** * the parameter of the window function */ private double windowParameter; /** * boolean which tells if the axis with logarithmic scale should be used * ({@code true}) or the normal scale should be used */ private boolean logarithmic; /** * boolean which tells if the points should be connected using splines * ({@code true}) or lines ({@code false}) */ private boolean spline; /** * boolean which tells if the chart should be antialiased */ private boolean antialias; /** * boolean which tells if the title of this plot should be displayed */ private boolean titleVisible; /** * boolean which tells if the labels with frequencies should be displayed */ private boolean frequencyAxisLabelsVisible; /** * boolean which tells if the labels with power values should be displayed */ private boolean powerAxisLabelsVisible; /** * {@link SignalFFTSettings settings} how the power spectrum should be * displayed */ private SignalFFTSettings fftSettings = new SignalFFTSettings(); /** * boolean which tells if the power spectrum has already been calculated * for the current parameters (if {@code true} - the calculated spectrum * will be stored in {@link #powerSpectrum}) */ private boolean calculated = false; /** * the stored samples of the signal that are used to calculate power * spectrum */ private double[] samples = null; /** * the calculated power spectrum - estimates of the * power spectrum for these frequencies. */ private double[] powerSpectrum; /** * the frequencies for the powerSpectrum values */ private double[] frequencies; /** * the axis with frequencies */ private NumberAxis xAxis; /** * the axis with powers with normal scale */ private NumberAxis normalYAxis; /** * the axis with powers with logarithmic scale */ private LogAxis logYAxis; /** * the renderer that connects points with lines */ private XYLineAndShapeRenderer normalRenderer; /** * the renderer that connects points using splines */ private XYSplineRenderer splineRenderer; /** * the actual plot with power spectrum */ private XYPlot powerSpectrumPlot; /** * the chart with {@link #powerSpectrumPlot} */ private JFreeChart powerSpectrumChart; /** * the font used in title */ private Font titleFont; /** * the message of the error that has occured */ private String error; /** * The last time FFT was recalculated. (Useful for online signals). */ private Calendar lastFFTRecalculationTime; /** * Constructor. Sets the source of messages, title font and border. */ public SignalFFTPlot() { super(); setBorder(new LineBorder(Color.LIGHT_GRAY, 3, false)); titleFont = new Font(Font.DIALOG, Font.PLAIN, 12); } /** * Calculates the powers spectrum and displays it: * <ul> * <li>obtains the samples near the selected point,</li> * <li>{@link FourierTransform#powerSpectrumReal(double[], double) * calculates} the power spectrum,</li> * <li>displays the result on the chart.</li> * </ul> */ private void calculate() { if (calculated) { return; } calculated = true; lastFFTRecalculationTime = Calendar.getInstance(); error = null; double timeZoomFactor = plot.getTimeZoomFactor(); int firstSample = (int) Math.floor((focusPoint.x / timeZoomFactor) - windowWidth / 2); if (firstSample < 0) { error = _("Not enough signal points"); } int lastSample = firstSample + windowWidth; if (lastSample >= plot.getMaxSampleCount()) { error = _("Not enough signal points"); } if (error != null) { return; } int sampleCnt = lastSample - firstSample; if (sampleCnt != windowWidth) { throw new SanityCheckException( "Sanity failed - sample count different than window size"); } if (samples == null || samples.length != sampleCnt) { samples = new double[sampleCnt]; } ChannelSamples channelSamples = null; try { channelSamples = signalAccess.getActiveProcessedSignalSamples( channel, firstSample, sampleCnt); samples = channelSamples.getSamples(); } catch (RuntimeException ex) { setVisible(false); throw ex; } catch (NoActiveObjectException e) { setVisible(false); throw new RuntimeException(e); } try { logger.debug("Samples requested [" + sampleCnt + "] array size [" + samples.length + "]"); FourierTransform fourierTransform = new FourierTransform(windowType, windowParameter); powerSpectrum = fourierTransform.calculatePowerSpectrum(samples); frequencies = fourierTransform.getFrequencies(samples, channelSamples.getSamplingFrequency()); if (powerSpectrum == null) { throw new NullPointerException("Null spectrum returned"); } logger.debug("powerSpectrum length = " + powerSpectrum.length); } catch (RuntimeException ex) { setVisible(false); throw ex; } if (powerSpectrumPlot == null) { xAxis = new NumberAxis(); xAxis.setAutoRange(false); /* xAxis.setTickUnit(new NumberTickUnit(4)); */ normalRenderer = new XYLineAndShapeRenderer(true, false); splineRenderer = new XYSplineRenderer(); splineRenderer.setSeriesShapesVisible(0, false); splineRenderer.setPrecision(10); powerSpectrumPlot = new XYPlot(null, xAxis, null, null); normalYAxis = new NumberAxis(); normalYAxis.setAutoRange(false); logYAxis = new LogAxis(); logYAxis.setAutoRange(false); } if (powerSpectrumChart == null) { powerSpectrumChart = new JFreeChart(null, titleFont, powerSpectrumPlot, false); powerSpectrumChart.setBorderVisible(false); powerSpectrumChart.setBackgroundPaint(Color.WHITE); } double pixelPerSecond = plot.getPixelPerSecond(); float minTime = (float)((firstSample * timeZoomFactor) / pixelPerSecond); float maxTime = (float)((lastSample * timeZoomFactor) / pixelPerSecond); StringBuilder minTimeSb = new StringBuilder(20); FormatUtils.addTime(minTime, minTimeSb); StringBuilder maxTimeSb = new StringBuilder(20); FormatUtils.addTime(maxTime, maxTimeSb); String title = _R("FFT over {0} points {1} - {2} ({3})", windowWidth, minTimeSb.toString(), maxTimeSb.toString(), channelSamples.getName()); powerSpectrumChart.setTitle(titleVisible ? new TextTitle(title, titleFont) : null); powerSpectrumChart.setAntiAlias(antialias); int startIndex = LEFT_CUTOFF; int endIndex = powerSpectrum.length; { // FIXME: check double rangeStart = frequencies[LEFT_CUTOFF]; double rangeEnd = channelSamples.getSamplingFrequency() / 2.0D; double rangeSize = rangeEnd - rangeStart; double oldRangeStart = rangeStart; if (fftSettings.getVisibleRangeStart() > rangeStart) { rangeStart = fftSettings.getVisibleRangeStart(); } if (fftSettings.getVisibleRangeEnd() < rangeEnd) { rangeEnd = fftSettings.getVisibleRangeEnd(); } if (fftSettings.getXAxisLabelCount() > 0) { double dist = rangeEnd - rangeStart; double step = dist / fftSettings.getXAxisLabelCount(); if (step < 1) step = 1; xAxis.setTickUnit(new NumberTickUnit(Math.round(step))); } if (rangeEnd < rangeStart + 1) { rangeEnd = rangeStart + 1; } xAxis.setRange(rangeStart, rangeEnd); } double max = 0; double min = Double.MAX_VALUE; if (fftSettings.isAutoScaleYAxis()){ for (int i = startIndex; i < endIndex; i++) { max = Math.max(max, powerSpectrum[i]); min = Math.min(min, powerSpectrum[i]); } max *= 1.15; // scale up by 15% as per ZFB request (related to spline // overshooting points). } else { max = fftSettings.getMaxPowerAxis(); min = fftSettings.getMinPowerAxis(); } if (logarithmic) { logYAxis.setTickLabelsVisible(powerAxisLabelsVisible); logYAxis.setRange(min, max); powerSpectrumPlot.setRangeAxis(logYAxis); } else { normalYAxis.setTickLabelsVisible(powerAxisLabelsVisible); if (fftSettings.isAutoScaleYAxis()) min = 0; normalYAxis.setRange(min, max); powerSpectrumPlot.setRangeAxis(normalYAxis); } xAxis.setTickLabelsVisible(frequencyAxisLabelsVisible); if (spline) { splineRenderer.setSeriesPaint(0, Color.RED); powerSpectrumPlot.setRenderer(splineRenderer); } else { normalRenderer.setSeriesPaint(0, Color.RED); powerSpectrumPlot.setRenderer(normalRenderer); } DefaultXYDataset dataset = new DefaultXYDataset(); dataset.addSeries("data", new double[][] {frequencies, powerSpectrum}); powerSpectrumPlot.setDataset(dataset); } /** * Paints this plot: * <ul> * <li>if the error occurred while calculating the power spectrum - draws * the message with the error,</li> * <li>otherwise draw the chart with the power spectrum.</li></ul> */ @Override protected void paintComponent(Graphics gOrig) { if (plot == null || focusPoint == null) { return; } calculate(); Graphics2D g = (Graphics2D) gOrig; Dimension size = getSize(); Insets insets = getInsets(); g.setColor(Color.WHITE); g.fillRect(0, 0, size.width, size.height); size.width -= (insets.left + insets.right); size.height -= (insets.top + insets.bottom); if (error != null) { g.setColor(Color.RED); FontMetrics fontMetrics = g.getFontMetrics(); Rectangle2D stringBounds = fontMetrics.getStringBounds(error, g); int width = (int) stringBounds.getWidth(); int height = (int) stringBounds.getHeight(); int x; if (width > size.width) { x = 0; } else { x = (size.width - width) / 2; } g.drawString(error, insets.left + x, insets.top + (size.height - height) / 2 + fontMetrics.getAscent()); return; } powerSpectrumChart.draw(g, new Rectangle(insets.left, insets.top, size.width, size.height)); } /** * @return true */ @Override public boolean isDoubleBuffered() { return true; } /** * @return true */ @Override public boolean isOpaque() { return true; } /** * @return the size of this plot */ @Override public Dimension getPreferredSize() { return plotSize; } /** * @return 200x100 pixel */ @Override public Dimension getMinimumSize() { return new Dimension(200, 100); } /** * @return the size of this plot */ @Override public Dimension getMaximumSize() { return getPreferredSize(); } /** * Returns the selected point. * @return the selected point */ public Point getFocusPoint() { return focusPoint; } /** * Sets the selected point and, if it has changed, invalidates the * calculated power spectrum. * @param focusPoint the selected point */ public void setFocusPoint(Point focusPoint) { if (!Util.equalsWithNulls(this.focusPoint, focusPoint)) { this.focusPoint = focusPoint; calculated = false; repaint(); } } /** * Returns the number of the channel for which the FFT is drawn. * @return the number of the channel for which the FFT is drawn */ public int getChannel() { return channel; } /** * Sets the number of the channel for which the FFT is drawn and if it has * changed invalidates the calculated power spectrum. * @param channel the number of the channel for which the FFT is drawn */ public void setChannel(int channel) { if (this.channel != channel) { this.channel = channel; calculated = false; repaint(); } } /** * Returns the signal plot for which this FFT plot is calculated. * @return the signal plot for which this FFT plot is calculated */ public ExportedSignalPlot getPlot() { return plot; } /** * Sets the signal plot for which this FFT plot is calculated and if it * has changed invalidates the calculated power spectrum. * @param plot the signal plot for which this FFT plot is calculated */ public void setPlot(ExportedSignalPlot plot) { if (this.plot != plot) { this.plot = plot; calculated = false; repaint(); } } /** * Sets the following parameters and invalidated the calculated power * spectrum. * @param plot the signal plot for which this FFT plot is calculated * @param focusPoint the selected point * @param channel the number of the channel for which the FFT is calculated */ public void setParameters(ExportedSignalPlot plot, Point focusPoint, int channel) { this.plot = plot; this.focusPoint = focusPoint; this.channel = channel; recalculateAndRepaint(); } /** * Sets the following parameters and invalidated the calculated power * spectrum. * @param focusPoint the selected point * @param channel the number of the channel for which the FFT is calculated */ public void setParameters(Point focusPoint, int channel) { this.focusPoint = focusPoint; this.channel = channel; recalculateAndRepaint(); } public void recalculateAndRepaint() { calculated = false; repaint(); } /** * Returns the size of this plot. * @return the size of this plot */ public Dimension getPlotSize() { return plotSize; } /** * Sets the size of this plot and invalidates the calculated power * spectrum. * @param plotSize the size of this plot */ public void setPlotSize(Dimension plotSize) { if (plotSize == null) { throw new NullPointerException("No size"); } if (!plotSize.equals(this.plotSize)) { this.plotSize = plotSize; calculated = false; revalidate(); repaint(); } } /** * Returns the number of samples in the window. * @return the number of samples in the window */ public int getWindowWidth() { return windowWidth; } /** * Sets the number of samples in the window and invalidates the calculated * power spectrum. * @param windowWidth the number of samples in the window */ public void setWindowWidth(int windowWidth) { if (this.windowWidth != windowWidth) { this.windowWidth = windowWidth; calculated = false; repaint(); } } /** * Returns the {@link WindowType type} of the window function. * @return the type of the window function */ public WindowType getWindowType() { return windowType; } /** * Sets the {@link WindowType type} of the window function and invalidates * the calculated power spectrum. * @param windowType the type of the window function */ public void setWindowType(WindowType windowType) { if (this.windowType != windowType) { this.windowType = windowType; calculated = false; repaint(); } } /** * Returns the parameter of the window function. * @return the parameter of the window function */ public double getWindowParameter() { return windowParameter; } /** * Sets the parameter of the window function and invalidates * the calculated power spectrum. * @param windowParameter the parameter of the window function */ public void setWindowParameter(double windowParameter) { if (this.windowParameter != windowParameter) { this.windowParameter = windowParameter; calculated = false; repaint(); } } /** * Returns if the axis with logarithmic scale should be used * ({@code true}) or the normal scale should be used. * @return if the axis with logarithmic scale should be used */ public boolean isLogarithmic() { return logarithmic; } /** * Sets if the axis with logarithmic scale should be used * ({@code true}) or the normal scale should be used and if the value * has changed invalidates the calculated power spectrum. * @param logarithmic if the axis with logarithmic scale should be used */ public void setLogarithmic(boolean logarithmic) { if (this.logarithmic != logarithmic) { this.logarithmic = logarithmic; calculated = false; repaint(); } } /** * Returns if the points should be connected using splines * ({@code true}) or lines ({@code false}) * @return if the points should be connected using splines * ({@code true}) or lines ({@code false}) */ public boolean isSpline() { return spline; } /** * Returns if the points should be connected using splines * ({@code true}) or lines ({@code false}). * If the value has changed invalidates the calculated power spectrum. * @param spline if the points should be connected using splines * ({@code true}) or lines ({@code false}) */ public void setSpline(boolean spline) { if (this.spline != spline) { this.spline = spline; calculated = false; repaint(); } } /** * Returns if the chart should be antialiased. * @return if the chart should be antialiased */ public boolean isAntialias() { return antialias; } /** * Sets if the chart should be antialiased. * If the value has changed invalidates the calculated power spectrum. * @param antialias if the chart should be antialiased */ public void setAntialias(boolean antialias) { if (this.antialias != antialias) { this.antialias = antialias; calculated = false; repaint(); } } /** * Returns if the title of this plot should be displayed. * @return if the title of this plot should be displayed */ public boolean isTitleVisible() { return titleVisible; } /** * Sets if the title of this plot should be displayed * If the value has changed invalidates the calculated power spectrum. * @param titleVisible if the title of this plot should be displayed */ public void setTitleVisible(boolean titleVisible) { if (this.titleVisible != titleVisible) { this.titleVisible = titleVisible; calculated = false; repaint(); } } /** * Returns if the labels with frequencies should be displayed. * @return if the labels with frequencies should be displayed */ public boolean isFrequencyAxisLabelsVisible() { return frequencyAxisLabelsVisible; } /** * Sets if the labels with frequencies should be displayed * If the value has changed invalidates the calculated power spectrum. * @param frequencyAxisLabelsVisible if the labels with frequencies should * be displayed */ public void setFrequencyAxisLabelsVisible(boolean frequencyAxisLabelsVisible) { if (this.frequencyAxisLabelsVisible != frequencyAxisLabelsVisible) { this.frequencyAxisLabelsVisible = frequencyAxisLabelsVisible; calculated = false; repaint(); } } /** * Returns if the labels with power values should be displayed. * @return if the labels with power values should be displayed */ public boolean isPowerAxisLabelsVisible() { return powerAxisLabelsVisible; } /** * Sets if the labels with power values should be displayed * If the value has changed invalidates the calculated power spectrum. * @param powerAxisLabelsVisible if the labels with power values should * be displayed */ public void setPowerAxisLabelsVisible(boolean powerAxisLabelsVisible) { if (this.powerAxisLabelsVisible != powerAxisLabelsVisible) { this.powerAxisLabelsVisible = powerAxisLabelsVisible; calculated = false; repaint(); } } /** * Sets the {@link SignalFFTSettings settings} how the power spectrum should be * displayed. * Invalidates the calculated power spectrum. * @param settings the settings how the power spectrum should be * displayed */ public void setSettings(SignalFFTSettings settings) { fftSettings = settings; plotSize = settings.getPlotSize(); windowWidth = settings.getWindowWidth(); windowType = settings.getWindowType(); windowParameter = settings.getWindowParameter(); logarithmic = settings.isLogarithmic(); antialias = settings.isAntialias(); spline = settings.isSpline(); titleVisible = settings.isTitleVisible(); frequencyAxisLabelsVisible = settings.isFrequencyAxisLabelsVisible(); powerAxisLabelsVisible = settings.isPowerAxisLabelsVisible(); calculated = false; revalidate(); repaint(); } /** * Sets the {@link SvarogAccessSignal access} to Svarog logic. * @param access the access to Svarog logic */ public void setSvarogAccess(SvarogAccess access) { signalAccess = access.getSignalAccess(); } /** * Returns the last time FFT was recalculated. * @return the last time FFT was recalculated */ public Calendar getLastFFTRecalculationTime() { return lastFFTRecalculationTime; } public double[] getPowerSpectrum() { return powerSpectrum; } public double[] getFrequencies() { return frequencies; } }