package gui.control;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.geom.Ellipse2D;
import java.util.List;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import sample.Listener;
import sample.complex.Complex;
import buffer.CircularBuffer;
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 );
}
}
}