Updates pull request from Roman Tsypuk to convert the build system from

ant to gradle.  Removes linux and windows application launch scripts and
produces an executable jar with all resources included.
This commit is contained in:
Denny
2017-12-17 07:28:03 -05:00
parent a1535c4157
commit 9316599dec
981 changed files with 8368 additions and 9049 deletions

View File

@@ -0,0 +1,85 @@
package io.github.dsheirer.gui;
import io.github.dsheirer.controller.channel.Channel.ChannelType;
import io.github.dsheirer.controller.channel.ChannelEvent;
import io.github.dsheirer.controller.channel.ChannelEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.NumberFormat;
public class ChannelMemoryLogger implements Runnable, ChannelEventListener
{
private final static Logger mLog = LoggerFactory.getLogger( ChannelMemoryLogger.class );
public int mChannelCount;
public int mChannelProcessingCount;
public int mTrafficChannelCount;
@Override
public void run()
{
final Runtime runtime = Runtime.getRuntime();
final NumberFormat format = NumberFormat.getInstance();
StringBuilder sb = new StringBuilder();
sb.append( "CHAN TOT:" + mChannelCount );
sb.append( " TRFC:" + mTrafficChannelCount );
sb.append( " PROC:" + mChannelProcessingCount );
long maxMemory = runtime.maxMemory();
long allocatedMemory = runtime.totalMemory();
long freeMemory = runtime.freeMemory();
sb.append(" MEM FREE: " + format.format(freeMemory / 1024) );
sb.append(" ALLOC: " + format.format(allocatedMemory / 1024) );
sb.append(" MAX: " + format.format(maxMemory / 1024) );
sb.append(" TOTAL: " + format.format((freeMemory + (maxMemory - allocatedMemory)) / 1024) );
mLog.debug( sb.toString() );
}
@Override
public void channelChanged( ChannelEvent event )
{
switch( event.getEvent() )
{
case NOTIFICATION_ADD:
if( event.getChannel().getChannelType() == ChannelType.TRAFFIC )
{
mTrafficChannelCount++;
}
else
{
mChannelCount++;
}
break;
case NOTIFICATION_DELETE:
if( event.getChannel().getChannelType() == ChannelType.TRAFFIC )
{
mTrafficChannelCount--;
}
else
{
mChannelCount--;
}
break;
case REQUEST_DISABLE:
break;
case REQUEST_ENABLE:
break;
case NOTIFICATION_PROCESSING_START:
mChannelProcessingCount++;
break;
case NOTIFICATION_PROCESSING_STOP:
mChannelProcessingCount--;
break;
case NOTIFICATION_STATE_RESET:
break;
default:
break;
}
}
}

View File

@@ -0,0 +1,514 @@
/*******************************************************************************
* 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 io.github.dsheirer.gui;
import com.jidesoft.swing.JideSplitPane;
import io.github.dsheirer.audio.AudioManager;
import io.github.dsheirer.audio.broadcast.BroadcastModel;
import io.github.dsheirer.audio.broadcast.BroadcastStatusPanel;
import io.github.dsheirer.controller.ControllerPanel;
import io.github.dsheirer.controller.channel.ChannelModel;
import io.github.dsheirer.controller.channel.ChannelProcessingManager;
import io.github.dsheirer.controller.channel.ChannelSelectionManager;
import io.github.dsheirer.controller.channel.map.ChannelMapModel;
import io.github.dsheirer.icon.IconManager;
import io.github.dsheirer.map.MapService;
import io.github.dsheirer.playlist.PlaylistManager;
import io.github.dsheirer.properties.SystemProperties;
import io.github.dsheirer.record.RecorderManager;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.settings.SettingsManager;
import io.github.dsheirer.source.SourceManager;
import io.github.dsheirer.source.tuner.TunerEvent;
import io.github.dsheirer.source.tuner.TunerModel;
import io.github.dsheirer.source.tuner.TunerSpectralDisplayManager;
import io.github.dsheirer.source.tuner.configuration.TunerConfigurationModel;
import io.github.dsheirer.spectrum.SpectralDisplayPanel;
import io.github.dsheirer.util.ThreadPool;
import io.github.dsheirer.util.TimeStamp;
import net.miginfocom.swing.MigLayout;
import org.apache.commons.io.FileUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.github.dsheirer.alias.AliasModel;
import io.github.dsheirer.alias.action.AliasActionManager;
import io.github.dsheirer.module.log.EventLogManager;
import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
public class SDRTrunk implements Listener<TunerEvent>
{
private final static Logger mLog = LoggerFactory.getLogger(SDRTrunk.class);
private static final String PROPERTY_BROADCAST_STATUS_VISIBLE = "main.broadcast.status.visible";
private boolean mBroadcastStatusVisible;
private IconManager mIconManager;
private BroadcastStatusPanel mBroadcastStatusPanel;
private BroadcastModel mBroadcastModel;
private ControllerPanel mControllerPanel;
private SettingsManager mSettingsManager;
private SpectralDisplayPanel mSpectralPanel;
private JFrame mMainGui = new JFrame();
private JideSplitPane mSplitPane;
private String mTitle;
public SDRTrunk()
{
mLog.info("*******************************************************************");
mLog.info("**** sdrtrunk: a trunked radio and digital decoding application ***");
mLog.info("**** website: https://github.com/dsheirer/sdrtrunk ***");
mLog.info("*******************************************************************");
mLog.info("Memory Logging Format: [Used/Allocated PercentUsed%]");
mLog.info("Host CPU Cores: " + Runtime.getRuntime().availableProcessors());
mLog.info("Host OS Name: " + System.getProperty("os.name"));
mLog.info("Host OS Arch: " + System.getProperty("os.arch"));
mLog.info("Host OS Version: " + System.getProperty("os.version"));
mLog.info("Host Max Java Memory: " + FileUtils.byteCountToDisplaySize(Runtime.getRuntime().maxMemory()));
//Setup the application home directory
Path home = getHomePath();
ThreadPool.logSettings();
mLog.info("Home path: " + home.toString());
//Load properties file
if(home != null)
{
loadProperties(home);
}
//Log current properties setting
SystemProperties.getInstance().logCurrentSettings();
TunerConfigurationModel tunerConfigurationModel = new TunerConfigurationModel();
TunerModel tunerModel = new TunerModel(tunerConfigurationModel);
mIconManager = new IconManager();
mSettingsManager = new SettingsManager(tunerConfigurationModel);
AliasModel aliasModel = new AliasModel();
ChannelModel channelModel = new ChannelModel();
ChannelMapModel channelMapModel = new ChannelMapModel();
EventLogManager eventLogManager = new EventLogManager();
RecorderManager recorderManager = new RecorderManager();
SourceManager sourceManager = new SourceManager(tunerModel, mSettingsManager);
ChannelProcessingManager channelProcessingManager = new ChannelProcessingManager(
channelModel, channelMapModel, aliasModel, eventLogManager, recorderManager, sourceManager);
channelProcessingManager.addAudioPacketListener(recorderManager);
channelModel.addListener(channelProcessingManager);
ChannelSelectionManager channelSelectionManager =
new ChannelSelectionManager(channelModel);
channelModel.addListener(channelSelectionManager);
AliasActionManager aliasActionManager = new AliasActionManager();
channelProcessingManager.addMessageListener(aliasActionManager);
AudioManager audioManager = new AudioManager(sourceManager.getMixerManager());
channelProcessingManager.addAudioPacketListener(audioManager);
mBroadcastModel = new BroadcastModel(mIconManager);
channelProcessingManager.addAudioPacketListener(mBroadcastModel);
MapService mapService = new MapService(mIconManager);
channelProcessingManager.addMessageListener(mapService);
mControllerPanel = new ControllerPanel(audioManager, aliasModel, mBroadcastModel,
channelModel, channelMapModel, channelProcessingManager, mIconManager,
mapService, mSettingsManager, sourceManager, tunerModel);
mSpectralPanel = new SpectralDisplayPanel(channelModel,
channelProcessingManager, mSettingsManager);
TunerSpectralDisplayManager tunerSpectralDisplayManager =
new TunerSpectralDisplayManager(mSpectralPanel,
channelModel, channelProcessingManager, mSettingsManager);
tunerModel.addListener(tunerSpectralDisplayManager);
tunerModel.addListener(this);
PlaylistManager playlistManager = new PlaylistManager(aliasModel, mBroadcastModel, channelModel,
channelMapModel);
playlistManager.init();
mLog.info("starting main application gui");
//Initialize the GUI
initGUI();
tunerModel.requestFirstTunerDisplay();
//Start the gui
EventQueue.invokeLater(new Runnable()
{
public void run()
{
try
{
mMainGui.setVisible(true);
}
catch(Exception e)
{
e.printStackTrace();
}
}
});
}
/**
* Launch the application.
*/
public static void main(String[] args)
{
new SDRTrunk();
}
/**
* Initialize the contents of the frame.
*/
private void initGUI()
{
mMainGui.setLayout(new MigLayout("insets 0 0 0 0 ", "[grow,fill]", "[grow,fill]"));
/**
* Setup main JFrame window
*/
mTitle = SystemProperties.getInstance().getApplicationName();
mMainGui.setTitle(mTitle);
mMainGui.setBounds(100, 100, 1280, 800);
mMainGui.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//Set preferred sizes to influence the split
mSpectralPanel.setPreferredSize(new Dimension(1280, 300));
mControllerPanel.setPreferredSize(new Dimension(1280, 500));
mSplitPane = new JideSplitPane(JideSplitPane.VERTICAL_SPLIT);
mSplitPane.setDividerSize(5);
mSplitPane.add(mSpectralPanel);
mSplitPane.add(mControllerPanel);
mBroadcastStatusVisible = SystemProperties.getInstance().get(PROPERTY_BROADCAST_STATUS_VISIBLE, false);
//Show broadcast status panel when user requests - disabled by default
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
mMainGui.add(mSplitPane, "cell 0 0,span,grow");
/**
* Menu items
*/
JMenuBar menuBar = new JMenuBar();
mMainGui.setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenuItem logFilesMenu = new JMenuItem("Logs & Recordings");
logFilesMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
try
{
Desktop.getDesktop().open(getHomePath().toFile());
}
catch(Exception e)
{
mLog.error("Couldn't open file explorer");
JOptionPane.showMessageDialog(mMainGui,
"Can't launch file explorer - files are located at: " +
getHomePath().toString(),
"Can't launch file explorer",
JOptionPane.ERROR_MESSAGE);
}
}
});
fileMenu.add(logFilesMenu);
JMenuItem settingsMenu = new JMenuItem("Icon Manager");
settingsMenu.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
mIconManager.showEditor(mMainGui);
}
});
fileMenu.add(settingsMenu);
fileMenu.add(new JSeparator());
JMenuItem exitMenu = new JMenuItem("Exit");
exitMenu.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.exit(0);
}
}
);
fileMenu.add(exitMenu);
JMenu viewMenu = new JMenu("View");
viewMenu.add(new BroadcastStatusVisibleMenuItem(mControllerPanel));
menuBar.add(viewMenu);
JMenuItem screenCaptureItem = new JMenuItem("Screen Capture");
screenCaptureItem.setMnemonic(KeyEvent.VK_C);
screenCaptureItem.setAccelerator(
KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.ALT_MASK));
screenCaptureItem.addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent arg0)
{
try
{
Robot robot = new Robot();
final BufferedImage image =
robot.createScreenCapture(mMainGui.getBounds());
SystemProperties props = SystemProperties.getInstance();
Path capturePath = props.getApplicationFolder("screen_captures");
if(!Files.exists(capturePath))
{
try
{
Files.createDirectory(capturePath);
}
catch(IOException e)
{
mLog.error("Couldn't create 'screen_captures' "
+ "subdirectory in the " +
"SDRTrunk application directory", e);
}
}
String filename = TimeStamp.getTimeStamp("_") +
"_screen_capture.png";
final Path captureFile = capturePath.resolve(filename);
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
try
{
ImageIO.write(image, "png",
captureFile.toFile());
}
catch(IOException e)
{
mLog.error("Couldn't write screen capture to "
+ "file [" + captureFile.toString() + "]", e);
}
}
});
}
catch(AWTException e)
{
mLog.error("Exception while taking screen capture", e);
}
}
});
menuBar.add(screenCaptureItem);
}
/**
* Lazy constructor for broadcast status panel
*/
private BroadcastStatusPanel getBroadcastStatusPanel()
{
if(mBroadcastStatusPanel == null)
{
mBroadcastStatusPanel = new BroadcastStatusPanel(mBroadcastModel);
mBroadcastStatusPanel.setPreferredSize(new Dimension(880, 70));
mBroadcastStatusPanel.getTable().setEnabled(false);
}
return mBroadcastStatusPanel;
}
/**
* Toggles visibility of the broadcast channels status panel at the bottom of the controller panel
*/
private void toggleBroadcastStatusPanelVisibility()
{
mBroadcastStatusVisible = !mBroadcastStatusVisible;
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
if(mBroadcastStatusVisible)
{
mSplitPane.add(getBroadcastStatusPanel());
}
else
{
mSplitPane.remove(getBroadcastStatusPanel());
}
mMainGui.revalidate();
}
});
SystemProperties.getInstance().set(PROPERTY_BROADCAST_STATUS_VISIBLE, mBroadcastStatusVisible);
}
/**
* Loads the application properties file from the user's home directory,
* creating the properties file for the first-time, if necessary
*/
private void loadProperties(Path homePath)
{
Path propsPath = homePath.resolve("SDRTrunk.properties");
if(!Files.exists(propsPath))
{
try
{
mLog.info("SDRTrunk - creating application properties file [" +
propsPath.toAbsolutePath() + "]");
Files.createFile(propsPath);
}
catch(IOException e)
{
mLog.error("SDRTrunk - couldn't create application properties "
+ "file [" + propsPath.toAbsolutePath(), e);
}
}
if(Files.exists(propsPath))
{
SystemProperties.getInstance().load(propsPath);
}
else
{
mLog.error("SDRTrunk - couldn't find or recreate the SDRTrunk " +
"application properties file");
}
}
/**
* Gets (or creates) the SDRTRunk application home directory.
*
* Note: the user can change this setting to allow log files and other
* files to reside elsewhere on the file system.
*/
private Path getHomePath()
{
Path homePath = FileSystems.getDefault()
.getPath(System.getProperty("user.home"), "SDRTrunk");
if(!Files.exists(homePath))
{
try
{
Files.createDirectory(homePath);
mLog.info("SDRTrunk - created application home directory [" +
homePath.toString() + "]");
}
catch(Exception e)
{
homePath = null;
mLog.error("SDRTrunk: exception while creating SDRTrunk home " +
"directory in the user's home directory", e);
}
}
return homePath;
}
@Override
public void receive(TunerEvent event)
{
if(event.getEvent() == TunerEvent.Event.REQUEST_MAIN_SPECTRAL_DISPLAY)
{
mMainGui.setTitle(mTitle + " - " + event.getTuner().getName());
}
}
public class BroadcastStatusVisibleMenuItem extends JCheckBoxMenuItem
{
private ControllerPanel mControllerPanel;
public BroadcastStatusVisibleMenuItem(ControllerPanel controllerPanel)
{
super("Show Streaming Status");
mControllerPanel = controllerPanel;
setSelected(mBroadcastStatusPanel != null);
addActionListener(new ActionListener()
{
@Override
public void actionPerformed(ActionEvent e)
{
toggleBroadcastStatusPanelVisibility();
setSelected(mBroadcastStatusVisible);
}
});
}
}
}

View File

@@ -0,0 +1,178 @@
package io.github.dsheirer.gui.control;
import io.github.dsheirer.buffer.CircularBuffer;
import io.github.dsheirer.sample.Listener;
import io.github.dsheirer.sample.complex.Complex;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Ellipse2D;
import java.util.List;
public class ConstellationViewer extends JPanel implements Listener<Complex>
{
private static final long serialVersionUID = 1L;
private int mSampleRate;
private int mSymbolRate;
private float mSamplesPerSymbol;
private float mCounter = 0;
private float mOffset = 0;
private CircularBuffer<Complex> mBuffer =
new CircularBuffer<Complex>( 5000 );
private Complex mPrevious = new Complex( 1, 1 );
public ConstellationViewer( int sampleRate, int symbolRate )
{
mSampleRate = sampleRate;
mSymbolRate = symbolRate;
mSamplesPerSymbol = (float)mSampleRate / (float)mSymbolRate;
initGui();
}
private void initGui()
{
setPreferredSize( new Dimension( 200,200 ) );
addMouseListener( new MouseListener()
{
@Override
public void mouseClicked( MouseEvent e )
{
if( SwingUtilities.isRightMouseButton( e ) )
{
JPopupMenu menu = new JPopupMenu();
menu.add( new TimingOffsetItem( (int)( mSamplesPerSymbol * 10 ), (int)( mOffset * 10 ) ) );
menu.show( ConstellationViewer.this, e.getX(), e.getY() );
}
}
public void mouseReleased( MouseEvent e ) {}
public void mousePressed( MouseEvent e ) {}
public void mouseExited( MouseEvent e ) {}
public void mouseEntered( MouseEvent e ) {}
} );
}
@Override
public void receive( Complex sample )
{
mBuffer.receive( sample );
Complex angle = Complex.multiply( sample, mPrevious.conjugate() );
repaint();
}
/**
* Sets the timing offset ( 0 <> SamplesPerSymbol ) for selecting which
* sample to plot, within the symbol timeframe. Values greater than the
* samples per symbol value will simply wrap or delay into the next symbol
* period.
*
* @param offset
*/
public void setOffset( float offset )
{
mOffset = offset;
repaint();
}
@Override
public void paintComponent( Graphics g )
{
super.paintComponent( g );
Graphics2D graphics = (Graphics2D) g;
graphics.setColor( Color.BLUE );
List<Complex> samples = mBuffer.getElements();
double centerX = (double)getHeight() / 2.0d;
double centerY = (double)getWidth() / 2.0d;
double scale = 0.5d;
mCounter = 0;
for( Complex sample: samples )
{
if( mCounter > ( mOffset + mSamplesPerSymbol ) )
{
/**
* Multiply the current sample against the complex conjugate of the
* previous sample to derive the phase delta between the two samples
*
* Negating the previous sample quadrature produces the conjugate
*/
double i = ( sample.inphase() * mPrevious.inphase() ) -
( sample.quadrature() * -mPrevious.quadrature() );
double q = ( sample.quadrature() * mPrevious.inphase() ) +
( sample.inphase() * -mPrevious.quadrature() );
double angle;
//Check for divide by zero
if( i == 0 )
{
angle = 0.0;
}
else
{
/**
* Use the arcus tangent of imaginary (q) divided by real (i) to
* get the phase angle (+/-) which was directly manipulated by the
* original message waveform during the modulation. This value now
* serves as the instantaneous amplitude of the demodulated signal
*/
double denominator = 1.0d / i;
angle = Math.atan( (double)q * denominator );
}
Ellipse2D.Double ellipse =
new Ellipse2D.Double( centerX - ( i * scale ),
centerY - ( q * scale ), 4, 4 );
graphics.draw( ellipse );
mCounter -= mSamplesPerSymbol;
}
mCounter++;
}
}
public class TimingOffsetItem extends JSlider implements ChangeListener
{
private static final long serialVersionUID = 1L;
public TimingOffsetItem( int maxValue, int currentValue )
{
super( JSlider.HORIZONTAL, 0, maxValue, currentValue );
setMajorTickSpacing( 10 );
setMinorTickSpacing( 5 );
setPaintTicks( true );
setPaintLabels( true );
addChangeListener( this );
}
@Override
public void stateChanged( ChangeEvent event )
{
int value = ((JSlider)event.getSource()).getValue();
setOffset( (float)value / 10.0f );
}
}
}

View File

@@ -0,0 +1,207 @@
package io.github.dsheirer.gui.control;
import javafx.collections.ObservableList;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.chart.AreaChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.util.Pair;
public class CurveFittedAreaChart extends AreaChart<Number, Number>
{
public CurveFittedAreaChart( NumberAxis xAxis, NumberAxis yAxis )
{
super( xAxis, yAxis );
}
@Override
protected void layoutPlotChildren()
{
super.layoutPlotChildren();
for ( int seriesIndex = 0; seriesIndex < getDataSize(); seriesIndex++ )
{
final XYChart.Series<Number, Number> series = getData()
.get( seriesIndex );
final Path seriesLine = (Path) ( (Group) series.getNode() )
.getChildren().get( 1 );
final Path fillPath = (Path) ( (Group) series.getNode() )
.getChildren().get( 0 );
smooth( seriesLine.getElements(), fillPath.getElements() );
}
}
private int getDataSize()
{
final ObservableList<XYChart.Series<Number, Number>> data = getData();
return ( data != null ) ? data.size() : 0;
}
private static void smooth( ObservableList<PathElement> strokeElements,
ObservableList<PathElement> fillElements )
{
// as we do not have direct access to the data, first recreate the list
// of all the data points we have
final Point2D[] dataPoints = new Point2D[ strokeElements.size() ];
for ( int i = 0; i < strokeElements.size(); i++ )
{
final PathElement element = strokeElements.get( i );
if ( element instanceof MoveTo )
{
final MoveTo move = (MoveTo) element;
dataPoints[ i ] = new Point2D( move.getX(), move.getY() );
} else if ( element instanceof LineTo )
{
final LineTo line = (LineTo) element;
final double x = line.getX(), y = line.getY();
dataPoints[ i ] = new Point2D( x, y );
}
}
// next we need to know the zero Y value
final double zeroY = ( (MoveTo) fillElements.get( 0 ) ).getY();
// now clear and rebuild elements
strokeElements.clear();
fillElements.clear();
Pair<Point2D[], Point2D[]> result = calcCurveControlPoints(
dataPoints );
Point2D[] firstControlPoints = result.getKey();
Point2D[] secondControlPoints = result.getValue();
// start both paths
strokeElements.add(
new MoveTo( dataPoints[ 0 ].getX(), dataPoints[ 0 ].getY() ) );
fillElements.add( new MoveTo( dataPoints[ 0 ].getX(), zeroY ) );
fillElements.add(
new LineTo( dataPoints[ 0 ].getX(), dataPoints[ 0 ].getY() ) );
// add curves
for ( int i = 1; i < dataPoints.length; i++ )
{
final int ci = i - 1;
strokeElements
.add( new CubicCurveTo( firstControlPoints[ ci ].getX(),
firstControlPoints[ ci ].getY(),
secondControlPoints[ ci ].getX(),
secondControlPoints[ ci ].getY(),
dataPoints[ i ].getX(), dataPoints[ i ].getY() ) );
fillElements.add( new CubicCurveTo( firstControlPoints[ ci ].getX(),
firstControlPoints[ ci ].getY(),
secondControlPoints[ ci ].getX(),
secondControlPoints[ ci ].getY(), dataPoints[ i ].getX(),
dataPoints[ i ].getY() ) );
}
// end the paths
fillElements.add( new LineTo(
dataPoints[ dataPoints.length - 1 ].getX(), zeroY ) );
fillElements.add( new ClosePath() );
}
/**
* Calculate open-ended Bezier Spline Control Points.
*/
public static Pair<Point2D[], Point2D[]> calcCurveControlPoints(
Point2D[] dataPoints )
{
Point2D[] firstControlPoints;
Point2D[] secondControlPoints;
int n = dataPoints.length - 1;
if ( n == 1 )
{ // Special case: Bezier curve should be a straight line.
firstControlPoints = new Point2D[ 1 ];
// 3P1 = 2P0 + P3
firstControlPoints[ 0 ] = new Point2D(
( 2 * dataPoints[ 0 ].getX() + dataPoints[ 1 ].getX() ) / 3,
( 2 * dataPoints[ 0 ].getY() + dataPoints[ 1 ].getY() )
/ 3 );
secondControlPoints = new Point2D[ 1 ];
// P2 = 2P1 – P0
secondControlPoints[ 0 ] = new Point2D(
2 * firstControlPoints[ 0 ].getX() - dataPoints[ 0 ].getX(),
2 * firstControlPoints[ 0 ].getY()
- dataPoints[ 0 ].getY() );
return new Pair<Point2D[], Point2D[]>( firstControlPoints,
secondControlPoints );
}
// Calculate first Bezier control points
// Right hand side vector
double[] rhs = new double[ n ];
// Set right hand side X values
for ( int i = 1; i < n - 1; ++i )
{
rhs[ i ] = 4 * dataPoints[ i ].getX()
+ 2 * dataPoints[ i + 1 ].getX();
}
rhs[ 0 ] = dataPoints[ 0 ].getX() + 2 * dataPoints[ 1 ].getX();
rhs[ n - 1 ] = ( 8 * dataPoints[ n - 1 ].getX()
+ dataPoints[ n ].getX() ) / 2.0;
// Get first control points X-values
double[] x = GetFirstControlPoints( rhs );
// Set right hand side Y values
for ( int i = 1; i < n - 1; ++i )
{
rhs[ i ] = 4 * dataPoints[ i ].getY()
+ 2 * dataPoints[ i + 1 ].getY();
}
rhs[ 0 ] = dataPoints[ 0 ].getY() + 2 * dataPoints[ 1 ].getY();
rhs[ n - 1 ] = ( 8 * dataPoints[ n - 1 ].getY()
+ dataPoints[ n ].getY() ) / 2.0;
// Get first control points Y-values
double[] y = GetFirstControlPoints( rhs );
// Fill output arrays.
firstControlPoints = new Point2D[ n ];
secondControlPoints = new Point2D[ n ];
for ( int i = 0; i < n; ++i )
{
// First control point
firstControlPoints[ i ] = new Point2D( x[ i ], y[ i ] );
// Second control point
if ( i < n - 1 )
{
secondControlPoints[ i ] = new Point2D(
2 * dataPoints[ i + 1 ].getX() - x[ i + 1 ],
2 * dataPoints[ i + 1 ].getY() - y[ i + 1 ] );
} else
{
secondControlPoints[ i ] = new Point2D(
( dataPoints[ n ].getX() + x[ n - 1 ] ) / 2,
( dataPoints[ n ].getY() + y[ n - 1 ] ) / 2 );
}
}
return new Pair<Point2D[], Point2D[]>( firstControlPoints,
secondControlPoints );
}
/**
* Solves a tridiagonal system for one of coordinates (x or y) of first
* Bezier control points.
*/
private static double[] GetFirstControlPoints( double[] rhs )
{
int n = rhs.length;
double[] x = new double[ n ]; // Solution vector.
double[] tmp = new double[ n ]; // Temp workspace.
double b = 2.0;
x[ 0 ] = rhs[ 0 ] / b;
for ( int i = 1; i < n; i++ )
{// Decomposition and forward substitution.
tmp[ i ] = 1 / b;
b = ( i < n - 1 ? 4.0 : 3.5 ) - tmp[ i ];
x[ i ] = ( rhs[ i ] - x[ i - 1 ] ) / b;
}
for ( int i = 1; i < n; i++ )
{
x[ n - i - 1 ] -= tmp[ n - i ] * x[ n - i ]; // Backsubstitution.
}
return x;
}
}

View File

@@ -0,0 +1,507 @@
/*******************************************************************************
* SDR Trunk
* Copyright (C) 2014 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 io.github.dsheirer.gui.control;
import io.github.dsheirer.source.InvalidFrequencyException;
import io.github.dsheirer.source.SourceException;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent;
import io.github.dsheirer.source.tuner.frequency.FrequencyChangeEvent.Event;
import io.github.dsheirer.source.tuner.frequency.IFrequencyChangeProcessor;
import net.miginfocom.swing.MigLayout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class JFrequencyControl extends JPanel implements IFrequencyChangeProcessor
{
private static final long serialVersionUID = 1L;
private final static Logger mLog =
LoggerFactory.getLogger(JFrequencyControl.class);
private List<IFrequencyChangeProcessor> mProcessors = new ArrayList<>();
private Color mHighlightColor = Color.YELLOW;
private long mFrequency;
private Cursor mBlankCursor;
private HashMap<Integer, Digit> mDigits = new HashMap<Integer, Digit>();
public JFrequencyControl()
{
init();
}
public JFrequencyControl(long value)
{
this();
mFrequency = value;
}
private void init()
{
setLayout(new MigLayout("insets 0 0 0 0", "[]0[]", ""));
Font font = new Font(Font.MONOSPACED, Font.BOLD, 30);
for (int x = 9; x >= 0; x--)
{
Digit digit = null;
try
{
digit = new Digit(x);
}
catch (ParseException e)
{
mLog.error("JFrequencyControl - parse exception "
+ "constructing a digit - " + e);
}
if (digit != null)
{
mDigits.put(x, digit);
add(digit);
digit.setFont(font);
if (x == 6)
{
JLabel period = new JLabel(".");
add(period);
}
}
}
add(new JLabel(" MHz"));
/**
* Create a blank cursor to use when the mouse is over the digits
*/
//Create an empty byte array
byte[] imageByte = new byte[0];
//Create image for cursor using empty array
Image cursorImage = Toolkit.getDefaultToolkit().createImage(imageByte);
mBlankCursor = Toolkit.getDefaultToolkit()
.createCustomCursor(cursorImage, new Point(0, 0), "cursor");
revalidate();
}
@Override
public void setEnabled(boolean enabled)
{
super.setEnabled(enabled);
for (Digit digit : mDigits.values())
{
digit.setEnabled(enabled);
}
}
/**
* Receives a frequency change event invoked by another control. We don't
* rebroadcast this event, just set the control to indicate the new frequency.
*/
@Override
public void frequencyChanged(FrequencyChangeEvent event)
{
if (event.getEvent() == Event.NOTIFICATION_FREQUENCY_CHANGE)
{
setFrequency(event.getValue().longValue(), false);
}
}
/**
* Loads the frequency into the display and optionally fires a change event
* to all registered listeners
*/
public void setFrequency(long frequency, boolean fireChangeEvent)
{
mFrequency = frequency;
for (Digit digit : mDigits.values())
{
digit.setFrequency(frequency, fireChangeEvent);
}
}
public long getFrequency()
{
return mFrequency;
}
private void updateFrequency()
{
long frequency = 0;
for (Digit digit : mDigits.values())
{
frequency += digit.getFrequency();
}
mFrequency = frequency;
}
private void fireFrequencyChanged() throws SourceException
{
updateFrequency();
Iterator<IFrequencyChangeProcessor> it = mProcessors.iterator();
FrequencyChangeEvent event = new FrequencyChangeEvent(
Event.REQUEST_FREQUENCY_CHANGE, mFrequency);
while (it.hasNext())
{
it.next().frequencyChanged(event);
}
}
public void addListener(IFrequencyChangeProcessor processor)
{
mProcessors.add(processor);
}
public void removeListener(IFrequencyChangeProcessor processor)
{
mProcessors.remove(processor);
}
public class Digit extends JTextField
{
private static final long serialVersionUID = 1L;
private int mPower = 0;
private long mValue = 0;
private Digit(int position) throws ParseException
{
super("0");
mPower = position;
Listener listener = new Listener();
this.addKeyListener(listener);
this.addMouseListener(listener);
this.addMouseWheelListener(listener);
}
/**
* Sets this digit to the value of the column in frequency that corresponds
* to the power (ie column) set for this digit. Optionally, fires a
* value change event to all listeners.
*/
public void setFrequency(long frequency, boolean fireChangeEvent)
{
//Strip the digits higher than this one
long lower = frequency % (long) (Math.pow(10, mPower + 1));
//Set the value to int value of dividing by 10 to this power
long value = (long) (lower / (long) (Math.pow(10, mPower)));
set(value, fireChangeEvent);
}
public long getFrequency()
{
return mValue * (long) Math.pow(10, mPower);
}
public void increment()
{
increment(true);
}
public void increment(boolean fireChangeEvent)
{
if (isEnabled())
{
set(mValue + 1, fireChangeEvent);
}
}
public void decrement()
{
decrement(true);
}
public void decrement(boolean fireChangeEvent)
{
if (isEnabled())
{
set(mValue - 1, fireChangeEvent);
}
}
/**
* Convenience wrapper to change amount and fire change event
*/
private void set(long amount)
{
set(amount, true);
}
/**
* Changes the value and optionally fires change event to listeners
*/
private void set(long amount, boolean fireChangeEvent)
{
long previous = mValue;
boolean higherDigitIncremented = false;
boolean higherDigitDecremented = false;
mValue = amount;
while (mValue < 0)
{
mValue += 10;
Digit nextHigherDigit = mDigits.get(mPower + 1);
if (nextHigherDigit != null)
{
nextHigherDigit.decrement(false);
higherDigitDecremented = true;
}
}
while (mValue > 9)
{
mValue -= 10;
Digit nextHigherDigit = mDigits.get(mPower + 1);
if (nextHigherDigit != null)
{
nextHigherDigit.increment(false);
higherDigitIncremented = true;
}
}
if (fireChangeEvent)
{
try
{
fireFrequencyChanged();
}
catch (SourceException se)
{
mValue = previous;
if (higherDigitIncremented)
{
Digit nextHigherDigit = mDigits.get(mPower + 1);
if (nextHigherDigit != null)
{
nextHigherDigit.decrement(false);
}
}
if (higherDigitDecremented)
{
Digit nextHigherDigit = mDigits.get(mPower + 1);
if (nextHigherDigit != null)
{
nextHigherDigit.increment(false);
}
}
if (se instanceof InvalidFrequencyException)
{
InvalidFrequencyException ife = (InvalidFrequencyException) se;
JOptionPane.showMessageDialog(this, "Frequency [" + ife.getInvalidFrequency() +
"] exceeds the frequency limit [" + ife.getValidFrequency() + "] for this tuner.");
}
}
}
setText(String.valueOf(mValue));
repaint();
}
private class Listener implements KeyListener,
MouseListener,
MouseWheelListener
{
@Override
public void keyReleased(KeyEvent e)
{
int key = e.getKeyCode();
switch (key)
{
case KeyEvent.VK_0:
case KeyEvent.VK_NUMPAD0:
set(0);
Digit.this.transferFocus();
break;
case KeyEvent.VK_1:
case KeyEvent.VK_NUMPAD1:
set(1);
Digit.this.transferFocus();
break;
case KeyEvent.VK_2:
case KeyEvent.VK_NUMPAD2:
set(2);
Digit.this.transferFocus();
break;
case KeyEvent.VK_3:
case KeyEvent.VK_NUMPAD3:
set(3);
Digit.this.transferFocus();
break;
case KeyEvent.VK_4:
case KeyEvent.VK_NUMPAD4:
set(4);
Digit.this.transferFocus();
break;
case KeyEvent.VK_5:
case KeyEvent.VK_NUMPAD5:
set(5);
Digit.this.transferFocus();
break;
case KeyEvent.VK_6:
case KeyEvent.VK_NUMPAD6:
set(6);
Digit.this.transferFocus();
break;
case KeyEvent.VK_7:
case KeyEvent.VK_NUMPAD7:
set(7);
Digit.this.transferFocus();
break;
case KeyEvent.VK_8:
case KeyEvent.VK_NUMPAD8:
set(8);
Digit.this.transferFocus();
break;
case KeyEvent.VK_9:
case KeyEvent.VK_NUMPAD9:
set(9);
Digit.this.transferFocus();
break;
case KeyEvent.VK_LEFT:
Digit.this.transferFocusBackward();
break;
case KeyEvent.VK_RIGHT:
Digit.this.transferFocus();
break;
case KeyEvent.VK_UP:
increment();
break;
case KeyEvent.VK_DOWN:
decrement();
break;
case KeyEvent.VK_TAB:
break;
default:
set(mValue);
break;
}
repaint();
}
@Override
public void keyPressed(KeyEvent e)
{
}
@Override
public void keyTyped(KeyEvent e)
{
}
@Override
public void mouseClicked(MouseEvent e)
{
switch (e.getButton())
{
case MouseEvent.BUTTON1:
increment();
break;
case MouseEvent.BUTTON2:
break;
case MouseEvent.BUTTON3:
decrement();
break;
}
}
public void mousePressed(MouseEvent e)
{
}
public void mouseReleased(MouseEvent e)
{
}
@Override
public void mouseEntered(MouseEvent e)
{
Digit.this.setBackground(mHighlightColor);
setCursor(mBlankCursor);
repaint();
}
@Override
public void mouseExited(MouseEvent e)
{
Digit.this.setBackground(Color.WHITE);
setCursor(Cursor.getDefaultCursor());
repaint();
}
@Override
public void mouseWheelMoved(MouseWheelEvent e)
{
set(mValue - e.getWheelRotation());
}
}
}
}

View File

@@ -0,0 +1,29 @@
package io.github.dsheirer.gui.editor;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
public abstract class DocumentListenerEditor<T> extends Editor<T>
implements DocumentListener
{
private static final long serialVersionUID = 1L;
public DocumentListenerEditor()
{
}
@Override
public void insertUpdate( DocumentEvent e )
{
setModified( true );
}
@Override
public void removeUpdate( DocumentEvent e )
{
setModified( true );
}
@Override
public void changedUpdate( DocumentEvent e ) { }
}

View File

@@ -0,0 +1,148 @@
/*******************************************************************************
* 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 io.github.dsheirer.gui.editor;
import javax.swing.*;
/**
* Generic editor with change detection, save and reset functions.
*
* This editor supports hierarchical nesting of sub-component editors where changes are detected
* at the lowest editor in the tree and percolated up to the top-level editor for managing the
* configuration set as a whole.
*
* To configure this editor as a collective, register each parent editor as a saveRequestListener
* on each child editor. If no save request listener is registered, the editor will simply invoke
* save when a change is detected and the user opts to save the contents.
*
* The top-level editor can override the saveRequest() method to perform custom save operations
* and validation.
*/
public abstract class Editor<T> extends JPanel
{
private static final long serialVersionUID = 1L;
protected T mItem;
protected boolean mModified = false;
protected Editor<?> mSaveRequestListener;
public Editor()
{
}
/**
* Requests to save the contents of this editor.
*
* If a save request listener is registered, the listener is notified that a save is requested,
* otherwise the save is request is processed by the instance.
*
* This allows for a save event to be percolated up the hierarchical set of editors so that
* a top-level editor can invoke custom operations such as configuration validation.
*/
protected void saveRequest()
{
if(mSaveRequestListener != null)
{
mSaveRequestListener.saveRequest();
}
else
{
save();
}
}
/**
* Registers the editor to be notified when this editor contents have been saved.
*/
public void setSaveRequestListener(Editor<?> listener)
{
mSaveRequestListener = listener;
}
/**
* Sets the object to be edited. If the contents from the previous object have changed, this
* prompts the user to save these changes. A saveRequest() is issued and either this editor
* or the parent editor will respond to the save request and invoke the save() method.
*/
public void setItem(T item)
{
if(isModified())
{
int option = JOptionPane.showConfirmDialog(
Editor.this,
"This item has changed. Do you want to save these changes?",
"Save Changes?",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if(option == JOptionPane.YES_OPTION)
{
saveRequest();
}
setModified(false);
}
mItem = item;
}
/**
* Current editing item
*/
public T getItem()
{
return mItem;
}
public boolean hasItem()
{
return mItem != null;
}
/**
* Sets the contents modified flag
*/
public void setModified(boolean modified)
{
mModified = modified;
}
/**
* Indicates if this editor contents have been modified
*/
public boolean isModified()
{
return mModified;
}
/**
* Save the contents of the editor
*/
public abstract void save();
/**
* Reset the contents of the editor without saving any changes
*/
public void reset()
{
mModified = false;
setItem(mItem);
}
}

View File

@@ -0,0 +1,25 @@
package io.github.dsheirer.gui.editor;
public class EditorValidationException extends Exception
{
private static final long serialVersionUID = 1L;
private Editor<?> mEditor;
private String mReason;
public EditorValidationException( Editor<?> editor, String reason )
{
mEditor = editor;
mReason = reason;
}
public Editor<?> getEditor()
{
return mEditor;
}
public String getReason()
{
return mReason;
}
}

View File

@@ -0,0 +1,31 @@
package io.github.dsheirer.gui.editor;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
public class EmptyEditor<T> extends Editor<T>
{
private static final long serialVersionUID = 1L;
public EmptyEditor( String descriptor )
{
setLayout( new MigLayout( "insets 0 0 0 0", "[center]", "[grow]" ) );
add( new JLabel( "Please select " + descriptor ), "wrap" );
}
public EmptyEditor()
{
this( "an item" );
}
@Override
public void save()
{
}
@Override
public void reset()
{
}
}

View File

@@ -0,0 +1,36 @@
package io.github.dsheirer.gui.editor;
import net.miginfocom.swing.MigLayout;
import javax.swing.*;
public class EmptyValidatingEditor<T> extends ValidatingEditor<T>
{
private static final long serialVersionUID = 1L;
public EmptyValidatingEditor( String descriptor )
{
setLayout( new MigLayout( "insets 0 0 0 0", "", "" ) );
add( new JLabel( "Please select " + descriptor ), "wrap" );
}
public EmptyValidatingEditor()
{
this( "an item" );
}
@Override
public void save()
{
}
@Override
public void reset()
{
}
@Override
public void validate( Editor<T> editor ) throws EditorValidationException
{
}
}

View File

@@ -0,0 +1,15 @@
package io.github.dsheirer.gui.editor;
/**
* Editor class that allows other editors to be validated for configuration with this editor
*/
public abstract class ValidatingEditor<T> extends Editor<T>
{
private static final long serialVersionUID = 1L;
/**
* Indicates if the editor argument is valid according to the settings of this editor
*/
public abstract void validate( Editor<T> editor ) throws EditorValidationException;
}