/******************************************************************************* * sdrtrunk * Copyright (C) 2014-2017 Dennis Sheirer * * 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 spectrum; import controller.channel.Channel; import controller.channel.Channel.ChannelType; import controller.channel.ChannelEvent; import controller.channel.ChannelEventListener; import controller.channel.ChannelModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import settings.ColorSetting; import settings.ColorSetting.ColorSettingName; import settings.Setting; import settings.SettingChangeListener; import settings.SettingsManager; import source.tuner.TunerChannel; import source.tuner.frequency.FrequencyChangeEvent; import source.tuner.frequency.IFrequencyChangeProcessor; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.geom.Line2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.concurrent.CopyOnWriteArrayList; public class OverlayPanel extends JPanel implements ChannelEventListener, IFrequencyChangeProcessor, SettingChangeListener { private static final long serialVersionUID = 1L; private final static Logger mLog = LoggerFactory.getLogger(OverlayPanel.class); private final DecimalFormat PPM_FORMATTER = new DecimalFormat( "#.0" ); private final static RenderingHints RENDERING_HINTS = new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); static { RENDERING_HINTS.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); } private final static BasicStroke DASHED_STROKE = new BasicStroke(0.8f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 5.0f, new float[]{2.0f, 4.0f}, 0.0f); private static DecimalFormat CURSOR_FORMAT = new DecimalFormat("000.00000"); private long mFrequency = 0; private int mBandwidth = 0; private Point mCursorLocation = new Point(0, 0); private boolean mCursorVisible = false; private DFTSize mDFTSize = DFTSize.FFT04096; private int mZoom = 0; private int mDFTZoomWindowOffset = 0; /** * Colors used by this component */ private Color mColorChannelConfig; private Color mColorChannelConfigProcessing; private Color mColorChannelConfigSelected; private Color mColorSpectrumBackground; private Color mColorSpectrumCursor; private Color mColorSpectrumLine; //Currently visible/displayable channels private CopyOnWriteArrayList<Channel> mVisibleChannels = new CopyOnWriteArrayList<Channel>(); private ChannelDisplay mChannelDisplay = ChannelDisplay.ALL; //Defines the offset at the bottom of the spectral display to account for //the frequency labels private double mSpectrumInset = 20.0d; private LabelSizeManager mLabelSizeMonitor = new LabelSizeManager(); private SettingsManager mSettingsManager; private ChannelModel mChannelModel; /** * Translucent overlay panel for displaying channel configurations, * processing channels, selected channels, frequency labels and lines, and * a cursor with a frequency readout. */ public OverlayPanel(SettingsManager settingsManager, ChannelModel channelModel) { mSettingsManager = settingsManager; if(mSettingsManager != null) { mSettingsManager.addListener(this); } mChannelModel = channelModel; if(mChannelModel != null) { mChannelModel.addListener(this); } addComponentListener(mLabelSizeMonitor); //Set the background transparent, so the spectrum display can be seen setOpaque(false); //Fetch color settings from settings manager setColors(); } public void dispose() { if(mChannelModel != null) { mChannelModel.removeListener(this); } mChannelModel = null; mVisibleChannels.clear(); if(mSettingsManager != null) { mSettingsManager.removeListener(this); } mSettingsManager = null; } /** * Sets/changes the DFT bin size */ public void setDFTSize(DFTSize size) { mDFTSize = size; } public ChannelDisplay getChannelDisplay() { return mChannelDisplay; } public void setChannelDisplay(ChannelDisplay display) { mChannelDisplay = display; } public void setCursorLocation(Point point) { mCursorLocation = point; repaint(); } public void setCursorVisible(boolean visible) { mCursorVisible = visible; repaint(); } /** * Sets the current zoom level (2^zoom) * * 0 No Zoom * 1 2x Zoom * 2 4x Zoom * 3 8x Zoom * 4 16x Zoom * 5 32x Zoom * 6 64x Zoom * * @param zoom level, 0 - 6. */ public void setZoom(int zoom) { mZoom = zoom; mLabelSizeMonitor.update(); } public void setZoomWindowOffset(int offset) { mDFTZoomWindowOffset = offset; } /** * Fetches the color settings from the settings manager */ private void setColors() { mColorChannelConfig = getColor(ColorSettingName.CHANNEL_CONFIG); mColorChannelConfigProcessing = getColor(ColorSettingName.CHANNEL_CONFIG_PROCESSING); mColorChannelConfigSelected = getColor(ColorSettingName.CHANNEL_CONFIG_SELECTED); mColorSpectrumCursor = getColor(ColorSettingName.SPECTRUM_CURSOR); mColorSpectrumLine = getColor(ColorSettingName.SPECTRUM_LINE); mColorSpectrumBackground = getColor(ColorSettingName.SPECTRUM_BACKGROUND); } /** * Fetches a named color setting from the settings manager. If the setting * doesn't exist, creates the setting using the defaultColor */ private Color getColor(ColorSettingName name) { ColorSetting setting = mSettingsManager.getColorSetting(name); return setting.getColor(); } /** * Monitors for setting changes. Colors can be changed by external actions * and will automatically update in this class */ @Override public void settingChanged(Setting setting) { if(setting instanceof ColorSetting) { ColorSetting colorSetting = (ColorSetting)setting; switch(colorSetting.getColorSettingName()) { case CHANNEL_CONFIG: mColorChannelConfig = colorSetting.getColor(); break; case CHANNEL_CONFIG_PROCESSING: mColorChannelConfigProcessing = colorSetting.getColor(); break; case CHANNEL_CONFIG_SELECTED: mColorChannelConfigSelected = colorSetting.getColor(); break; case SPECTRUM_BACKGROUND: mColorSpectrumBackground = colorSetting.getColor(); break; case SPECTRUM_CURSOR: mColorSpectrumCursor = colorSetting.getColor(); break; case SPECTRUM_LINE: mColorSpectrumLine = colorSetting.getColor(); break; default: break; } } } /** * Renders the channel configs, lines, labels, and cursor */ @Override public void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D graphics = (Graphics2D)g; graphics.setBackground(mColorSpectrumBackground); graphics.setRenderingHints(RENDERING_HINTS); drawFrequencies(graphics); drawChannels(graphics); drawCursor(graphics); } /** * Draws a cursor on the panel, whenever the mouse is hovering over the * panel */ private void drawCursor(Graphics2D graphics) { if(mCursorVisible) { drawFrequencyLine(graphics, mCursorLocation.x, mColorSpectrumCursor); String frequency = CURSOR_FORMAT.format(getFrequencyFromAxis(mCursorLocation.getX()) / 1E6D); FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); Rectangle2D rect = fontMetrics.getStringBounds(frequency, graphics); if(mCursorLocation.y > rect.getHeight()) { graphics.drawString(frequency, mCursorLocation.x + 5, mCursorLocation.y); } if(mZoom != 0) { graphics.drawString("Zoom: " + (int)Math.pow(2.0, mZoom) + "x", mCursorLocation.x + 17, mCursorLocation.y + 11); } } } /** * Draws the frequency lines and labels every 10kHz */ private void drawFrequencies(Graphics2D graphics) { Stroke currentStroke = graphics.getStroke(); long minFrequency = getMinDisplayFrequency(); long maxFrequency = getMaxDisplayFrequency(); //Frequency increments for label and tick spacing int label = mLabelSizeMonitor.getLabelIncrement(graphics); int major = mLabelSizeMonitor.getMajorTickIncrement(graphics); int minor = mLabelSizeMonitor.getMinorTickIncrement(graphics); //Avoid divide by zero error if(minor == 0) { minor = 1; } //Adjust the start frequency to a multiple of the minor tick spacing long frequency = minFrequency - (minFrequency % minor); while(frequency < maxFrequency) { if(frequency % label == 0) { drawFrequencyLineAndLabel(graphics, frequency); } else if(frequency % major == 0) { drawTickLine(graphics, frequency, true); } else { drawTickLine(graphics, frequency, false); } frequency += minor; } } /** * Draws a vertical line and a corresponding frequency label at the bottom */ private void drawFrequencyLineAndLabel(Graphics2D graphics, long frequency) { double xAxis = getAxisFromFrequency(frequency); drawFrequencyLine(graphics, xAxis, mColorSpectrumLine); drawTickLine(graphics, frequency, false); graphics.setColor(mColorSpectrumLine); drawFrequencyLabel(graphics, xAxis, frequency); } /** * Draws a vertical line at the xaxis */ private void drawTickLine(Graphics2D graphics, long frequency, boolean major) { graphics.setColor(mColorSpectrumLine); double xAxis = getAxisFromFrequency(frequency); double start = getSize().getHeight() - mSpectrumInset; double end = start + (major ? 9.0d : 3.0d); graphics.draw(new Line2D.Double(xAxis, start, xAxis, end)); } /** * Draws a vertical line at the xaxis */ private void drawFrequencyLine(Graphics2D graphics, double xaxis, Color color) { graphics.setColor(color); graphics.draw(new Line2D.Double(xaxis, 0.0d, xaxis, getSize().getHeight() - mSpectrumInset)); } /** * Draws a vertical line at the xaxis */ private void drawChannelCenterLine(Graphics2D graphics, double xaxis) { double height = getSize().getHeight() - mSpectrumInset; graphics.setColor(Color.LIGHT_GRAY); graphics.draw(new Line2D.Double(xaxis, height * 0.65d, xaxis, height - 1.0d)); } /** * Draws the Automatic Frequency Control (AFC) channel center offset */ private void drawAFC(Graphics2D graphics, double frequencyAxis, double errorAxis, double bandwidth, int correction, long frequency) { double height = getSize().getHeight() - mSpectrumInset; double verticalAxisTop = height * 0.88d; double verticalAxisBottom = height * 0.98d; double halfBandwidth = bandwidth / 2.0; double errorEdgeStart = errorAxis - halfBandwidth; double errorEdgeStop = errorAxis + halfBandwidth; graphics.setColor(Color.YELLOW); //Horizontal line connecting frequency and error line graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisBottom, errorEdgeStop, verticalAxisBottom)); //Vertical band edge lines graphics.draw(new Line2D.Double(errorEdgeStart, verticalAxisTop, errorEdgeStart, verticalAxisBottom)); graphics.draw(new Line2D.Double(errorEdgeStop, verticalAxisTop, errorEdgeStop, verticalAxisBottom)); double ppm = (double)correction / ((double)frequency / 1E6d); String label = "PPM " + PPM_FORMATTER.format(ppm) ; FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); Rectangle2D rect = fontMetrics.getStringBounds(label, graphics); //Only render the correction value label if the spacing is large enough if(rect.getWidth() <= bandwidth && rect.getHeight() * 5 <= height) { graphics.drawString(label, (float)(errorEdgeStart + 1.0), (float)(verticalAxisBottom - 2.0)); } } /** * Returns the x-axis value corresponding to the frequency */ private double getAxisFromFrequency(long frequency) { double screenWidth = (double)getSize().getWidth(); double pixelsPerBin = screenWidth / (double)mDFTSize.getSize(); double pixelOffsetToMinDisplayFrequency = pixelsPerBin * 2.0d; //Calculate frequency offset from the min frequency double frequencyOffset = (double)(frequency - getMinDisplayFrequency()); //Determine ratio of frequency offset to overall bandwidth double ratio = frequencyOffset / (double)getDisplayBandwidth(); //Apply the ratio to the screen width minus 1 bin width double screenOffset = screenWidth * ratio; return pixelOffsetToMinDisplayFrequency + screenOffset; } /** * Returns the frequency corresponding to the x-axis value using the current * zoom level. */ public long getFrequencyFromAxis(double xAxis) { double width = getSize().getWidth(); double offset = xAxis / width; long frequency = getMinDisplayFrequency() + Math.round((double)getDisplayBandwidth() * offset); if(frequency > (getMaxFrequency())) { frequency = getMaxFrequency(); } return frequency; } /** * Draws a frequency label at the x-axis position, at the bottom of the panel */ private void drawFrequencyLabel(Graphics2D graphics, double xaxis, long frequency) { String label = mLabelSizeMonitor.format(frequency); FontMetrics fontMetrics = graphics.getFontMetrics(this.getFont()); Rectangle2D rect = fontMetrics.getStringBounds(label, graphics); float xOffset = (float)rect.getWidth() / 2; graphics.drawString(label, (float)(xaxis - xOffset), (float)(getSize().getHeight() - 2.0f)); } /** * Draws visible channel configs as translucent shaded frequency regions */ private void drawChannels(Graphics2D graphics) { for(Channel channel : mVisibleChannels) { if(mChannelDisplay == ChannelDisplay.ALL || (mChannelDisplay == ChannelDisplay.ENABLED && channel.getEnabled())) { //Choose the correct background color to use if(channel.isSelected()) { graphics.setColor(mColorChannelConfigSelected); } else if(channel.getEnabled()) { graphics.setColor(mColorChannelConfigProcessing); } else { graphics.setColor(mColorChannelConfig); } TunerChannel tunerChannel = channel.getTunerChannel(); if(tunerChannel != null) { double xAxis = getAxisFromFrequency(tunerChannel.getFrequency()); double width = (double)(tunerChannel.getBandwidth()) / (double)getDisplayBandwidth() * getSize().getWidth(); Rectangle2D.Double box = new Rectangle2D.Double(xAxis - (width / 2.0d), 0.0d, width, getSize().getHeight() - mSpectrumInset); //Fill the box with the correct color graphics.fill(box); graphics.draw(box); //Change to the line color to render the channel name, etc. graphics.setColor(mColorSpectrumLine); //Draw the labels starting at yAxis position 0 double yAxis = 0; //Draw the system label and adjust the y-axis position String system = channel.hasSystem() ? channel.getSystem() : " "; yAxis += drawLabel(graphics, system, this.getFont(), xAxis, yAxis, width); //Draw the site label and adjust the y-axis position String site = channel.hasSite() ? channel.getSite() : " "; yAxis += drawLabel(graphics, site, this.getFont(), xAxis, yAxis, width); //Draw the channel label and adjust the y-axis position yAxis += drawLabel(graphics, channel.getName(), this.getFont(), xAxis, yAxis, width); //Draw the decoder label drawLabel(graphics, channel.getDecodeConfiguration().getDecoderType().getShortDisplayString(), this.getFont(), xAxis, yAxis, width); long frequency = tunerChannel.getFrequency(); double frequencyAxis = getAxisFromFrequency(frequency); drawChannelCenterLine(graphics, frequencyAxis); /* Draw Automatic Frequency Control line */ int correction = channel.getChannelFrequencyCorrection(); if(correction != 0) { long error = frequency + correction; drawAFC(graphics, frequencyAxis, getAxisFromFrequency(error), width, correction, tunerChannel.getFrequency()); } } } } } /** * Draws a textual label at the x/y position, clipping the end of the text * to fit within the maxwidth value. * * @return height of the drawn label */ private double drawLabel(Graphics2D graphics, String text, Font font, double x, double baseY, double maxWidth) { FontMetrics fontMetrics = graphics.getFontMetrics(font); Rectangle2D label = fontMetrics.getStringBounds(text, graphics); double offset = label.getWidth() / 2.0d; double y = baseY + label.getHeight(); /** * If the label is wider than the max width, left justify the text and * clip the end of it */ if(offset > (maxWidth / 2.0d)) { label.setRect(x - (maxWidth / 2.0d), y - label.getHeight(), maxWidth, label.getHeight()); graphics.setClip(label); graphics.drawString(text, (float)(x - (maxWidth / 2.0d)), (float)y); graphics.setClip(null); } else { graphics.drawString(text, (float)(x - offset), (float)y); } return label.getHeight(); } /** * Frequency change event handler */ @Override public void frequencyChanged(FrequencyChangeEvent event) { switch(event.getEvent()) { case NOTIFICATION_SAMPLE_RATE_CHANGE: mBandwidth = event.getValue().intValue(); mLabelSizeMonitor.update(); break; case NOTIFICATION_FREQUENCY_CHANGE: mFrequency = event.getValue().longValue(); mLabelSizeMonitor.update(); break; default: break; } /** * Reset the visible channels list */ mVisibleChannels.clear(); mVisibleChannels.addAll(mChannelModel.getChannelsInFrequencyRange(getMinFrequency(), getMaxFrequency())); } /** * Channel change event handler */ @Override @SuppressWarnings("incomplete-switch") public void channelChanged(ChannelEvent event) { Channel channel = event.getChannel(); switch(event.getEvent()) { case NOTIFICATION_ADD: case NOTIFICATION_PROCESSING_START: if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency())) { mVisibleChannels.add(channel); } break; case NOTIFICATION_DELETE: mVisibleChannels.remove(channel); break; case NOTIFICATION_PROCESSING_STOP: if(channel.getChannelType() == ChannelType.TRAFFIC) { mVisibleChannels.remove(channel); } break; case NOTIFICATION_CONFIGURATION_CHANGE: if(mVisibleChannels.contains(channel) && !channel.isWithin(getMinFrequency(), getMaxFrequency())) { mVisibleChannels.remove(channel); } if(!mVisibleChannels.contains(channel) && channel.isWithin(getMinFrequency(), getMaxFrequency())) { mVisibleChannels.add(channel); } break; default: break; } repaint(); } public int getBandwidth() { return mBandwidth; } /** * Currently displayed minimum frequency */ public long getMinFrequency() { return mFrequency - (mBandwidth / 2); } /** * Currently displayed maximum frequency */ public long getMaxFrequency() { return mFrequency + (mBandwidth / 2); } public boolean containsFrequency(long frequency) { return Math.abs(mFrequency - frequency) <= (mBandwidth / 2); } private long getMinDisplayFrequency() { double bandwidthPerBin = (double)mBandwidth / (double)(mDFTSize.getSize()); return getMinFrequency() + (int)((mDFTZoomWindowOffset) * bandwidthPerBin); } private long getMaxDisplayFrequency() { return getMinDisplayFrequency() + getDisplayBandwidth(); } private int getDisplayBandwidth() { if(mZoom != 0) { return mBandwidth / (int)Math.pow(2.0, mZoom); } return mBandwidth; } /** * Returns a list of channel configs that contain the frequency within their * min/max frequency settings. */ public ArrayList<Channel> getChannelsAtFrequency(long frequency) { ArrayList<Channel> configs = new ArrayList<Channel>(); for(Channel config : mVisibleChannels) { TunerChannel channel = config.getTunerChannel(); if(channel != null && channel.getMinFrequency() <= frequency && channel.getMaxFrequency() >= frequency) { configs.add(config); } } return configs; } @Override public void settingDeleted(Setting setting) { /* not implemented */ } /** * Calculates correct spacing and format for frequency labels and major/minor * tick lines based on current frequency, bandwidth, zoom and screen size. */ public class LabelSizeManager implements ComponentListener { private static final double LABEL_FILL_THRESHOLD = 0.5d; private DecimalFormat mFrequencyFormat = new DecimalFormat("0.0"); private boolean mUpdateRequired = true; private int mLabelIncrement = 1; private int mMajorTickIncrement = 1; private int mMinorTickIncrement = 1; public String format(long frequency) { return mFrequencyFormat.format((double)frequency / 1E6D); } private void setPrecision(int precision) { if(precision < 1) { precision = 1; } if(precision > 5) { precision = 5; } mFrequencyFormat.setMinimumFractionDigits(precision); mFrequencyFormat.setMaximumFractionDigits(precision); } private void update(Graphics2D graphics) { if(mUpdateRequired) { //Set maximum precision as a starting point setPrecision(5); FontMetrics fontMetrics = graphics.getFontMetrics(OverlayPanel.this.getFont()); int maxLabelWidth = fontMetrics.stringWidth(format(getMaxDisplayFrequency())); double maxLabels = ((double)OverlayPanel.this.getWidth() * LABEL_FILL_THRESHOLD) / (double)maxLabelWidth; //Calculate the next smallest base 10 value for the major increment int power = (int)Math.log10((double)getDisplayBandwidth() / maxLabels); //Set the number of decimal places to display in frequency labels int precision = 5 - power; int start = (int)Math.pow(10.0, power + 1); int minimum = (int)Math.pow(10.0, power); int labelIncrement = start; while(((double)getDisplayBandwidth() / (double)labelIncrement) < maxLabels && labelIncrement >= minimum) { labelIncrement /= 2; precision++; } if(labelIncrement == minimum) { precision = 5 - power; } setPrecision(precision); mLabelIncrement = labelIncrement; mMajorTickIncrement = labelIncrement / 2; mMinorTickIncrement = labelIncrement / 10; mUpdateRequired = false; } } /** * Forces the display to update the label and frequency display * calculations */ public void update() { mUpdateRequired = true; } public int getMajorTickIncrement(Graphics2D graphics) { //Check to see if a calculation update is scheduled update(graphics); return mMajorTickIncrement; } public int getMinorTickIncrement(Graphics2D graphics) { return mMinorTickIncrement; } public int getLabelIncrement(Graphics2D graphics) { return mLabelIncrement; } @Override public void componentResized(ComponentEvent arg0) { update(); } public void componentHidden(ComponentEvent arg0) { } public void componentMoved(ComponentEvent arg0) { } public void componentShown(ComponentEvent arg0) { } } public enum ChannelDisplay { ALL, ENABLED, NONE; } }