/*
 * Copyright 2015 Andrew T. Flowers, K0SM
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package dopplerpsk;

import dopplerpsk.ui.MessagePanel;
import dopplerpsk.dsp.ITxListener;
import dopplerpsk.dsp.Psk31Transmitter;
import dopplerpsk.event.IMessageChangedListener;
import dopplerpsk.ui.StationInfoDialog;
import dopplerpsk.ui.SatelliteChooserDialog;
import dopplerpsk.event.SatelliteUpdateEvent;
import dopplerpsk.util.MacroParser;
import dopplerpsk.event.ISatelliteEventSource;
import dopplerpsk.event.ISatelliteEventSink;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.swing.JButton;
import org.orekit.errors.OrekitException;
import dopplerpsk.event.OrekitSatelliteEventSource;
import dopplerpsk.ui.MessageInfoPanel;
import dopplerpsk.util.AudioConfiguration;
import java.awt.Component;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JSeparator;

/**
 * Main UI for Doppler PSK
 *
 * @author Andrew T. Flowers
 */
public class DopplerPSK extends javax.swing.JFrame implements ISatelliteEventSink, ITxListener, IMessageChangedListener {

    private static final DecimalFormat mhzFormat = new DecimalFormat("##0.000");
    private static final DecimalFormat decimalFormat = new DecimalFormat("##0.00");
    private static final DecimalFormat integerFormat = new DecimalFormat("#####0");

    private static final String DEFAULT_AUDIO_DEVICE_NAME = "System default audio";
    //Directory for TLEs.  All TLEs in this directory will be loaded.
    private final File tleDir = new File("tle");

    //doppler.sqf (SatPC32 format) must be in the program directory.
    private final File sqfFile = new File("doppler.sqf");

    // Propagation engine that updates us with real-time satellite information
    // to control the LO.
    private ISatelliteEventSource propagator;

    // SatelliteDirectory is a utility class to reference TLEs and satellite
    // frequency information (wrapper for TLEs and SQF files).
    private SatelliteDirectory satelliteDirectory;

    // The satellite currently being tracked
    private String currentSatelliteName;

    // The PSK31 transmitter recieves characters to send (i.e., user input from
    // this class), generates a baseband 31.25Hz signal, mixes them with the 
    // local oscillator and writes them to the audio output device
    private Psk31Transmitter tx;

    // Station info read from configuration file
    double stationLon = 0;
    double stationLat = 0;
    double stationAlt = 0;

    double dopplerCenter = 2000.0;
    private JCheckBoxMenuItem systemDefaultCheckBoxMenuItem;
    private String lastUsedAudioDeviceName = null;
    private SatelliteUpdateEvent lastEvt;

    private String mycall = "k0sm";
    private String dxcall = "dx0dx";
    private String exchange = "fn13";
    private MessageInfoPanel messageInfoPanel;

    /**
     * Creates new form PskFrame
     */
    public DopplerPSK() {
        try {
            initComponents();
            loadSettings();
            loadSatelliteDirectory();
            addMacroButtons();
            AudioConfiguration.findAudioConfigurations();
            setupAudioCheckBoxes();
            loadSelectedSatellite(currentSatelliteName);
        } catch (IllegalArgumentException ex) {
            Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
            JOptionPane.showMessageDialog(null, ex.getMessage());
        }
    }

    private void setupAudioCheckBoxes() {

        // if a new audio output device is selected, we handle it here
        final ITxListener txListener = this;
        ActionListener lis = new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent ae) {
                JCheckBoxMenuItem source = (JCheckBoxMenuItem) ae.getSource();
                System.out.println("Audio output changed to " + source.getText());
                try {
                    Psk31Transmitter newTx = getPsk31Tx(source.getText());
                    if (newTx != null) {
                        if (tx != null) {
                            tx.stopTx();
                            tx.stopAudioThread();
                        }
                        if (tx != null) {
                            tx.removeListener(txListener);
                        }
                        tx = newTx;
                        tx.addListener(txListener);
                        lastUsedAudioDeviceName = source.getText();
                        System.out.println(lastUsedAudioDeviceName);
                    }
                } catch (LineUnavailableException ex) {
                    Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
                }

            }
        };
        systemDefaultCheckBoxMenuItem = new JCheckBoxMenuItem(DEFAULT_AUDIO_DEVICE_NAME);
        systemDefaultCheckBoxMenuItem.setSelected(DEFAULT_AUDIO_DEVICE_NAME.equals(lastUsedAudioDeviceName));
        audioButtonGroup.add(systemDefaultCheckBoxMenuItem);
        AudioDeviceMenu.add(systemDefaultCheckBoxMenuItem);
        systemDefaultCheckBoxMenuItem.addActionListener(lis);
        AudioDeviceMenu.add(new JSeparator());
        for (String s : AudioConfiguration.getMixerNames()) {
            JCheckBoxMenuItem cb = new JCheckBoxMenuItem(s);
            cb.setSelected(s.equals(lastUsedAudioDeviceName));

            audioButtonGroup.add(cb);
            cb.addActionListener(lis);
            AudioDeviceMenu.add(cb);
        }
    }

    private void loadSatelliteDirectory() {
        if (!tleDir.canRead()) {
            satelliteDirectory = null;
            System.out.println("Program cannot read tle directory at "
                    + tleDir.getAbsolutePath() + ". Make sure directory"
                    + "exists and that you run this program with sufficient"
                    + "privilage to read this file.");
            System.exit(1);
        } else {
            satelliteDirectory = new SatelliteDirectory(tleDir, sqfFile);
        }
        try {
            satelliteDirectory.loadSatellites();
        } catch (IOException ex) {
            Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void loadSelectedSatellite(String name) {
        Satellite sat = satelliteDirectory.getSatellite(name);
        if (sat != null) {

            try {
                //remove the old sat propagator
                if (propagator != null) {
                    propagator.stop();
                    //propagator.removeEventListener(tx);
                    propagator.removeEventListener(this);
                }
                // create new one and hook it up to our LO
                propagator = new OrekitSatelliteEventSource(sat, stationLat, stationLon, stationAlt, sat.getUpLinkHz(), 1);
                //propagator.addEventListener(tx);
                propagator.addEventListener(this);
                propagator.start();
                currentSatelliteName = name;
            } catch (OrekitException ex) {
                Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

    }

    private Psk31Transmitter getPsk31Tx(String audioDeviceName) throws LineUnavailableException {

        AudioFormat format = new AudioFormat(48000, 16, 1, true, true);
        SourceDataLine line = AudioConfiguration.getSourceDataLine(audioDeviceName, format);
        line.open();
        Psk31Transmitter trans = new Psk31Transmitter(line, format, dopplerCenter);

        return trans;
    }

    /**
     * This method is called from within the constructor to initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is always
     * regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        audioButtonGroup = new javax.swing.ButtonGroup();
        macroButtonGroup = new javax.swing.ButtonGroup();
        topPanel = new javax.swing.JPanel();
        satInfoPanel = new javax.swing.JPanel();
        jLabel1 = new javax.swing.JLabel();
        satNameLabel = new javax.swing.JLabel();
        jLabel3 = new javax.swing.JLabel();
        azLabel = new javax.swing.JLabel();
        jLabel5 = new javax.swing.JLabel();
        elLabel = new javax.swing.JLabel();
        infoPanel = new javax.swing.JPanel();
        jLabel4 = new javax.swing.JLabel();
        uplinkFreqLabel = new javax.swing.JLabel();
        jLabel7 = new javax.swing.JLabel();
        dopplerLabel = new javax.swing.JLabel();
        jLabel9 = new javax.swing.JLabel();
        ddfLabel = new javax.swing.JLabel();
        jLabel2 = new javax.swing.JLabel();
        centerAfFreqLabel = new javax.swing.JLabel();
        jLabel6 = new javax.swing.JLabel();
        currentAfFreqLabel = new javax.swing.JLabel();
        jTabbedPane1 = new javax.swing.JTabbedPane();
        messagePanel = new javax.swing.JPanel();
        macroPanel = new javax.swing.JPanel();
        messageTxToggleButton = new javax.swing.JToggleButton();
        txPanel = new javax.swing.JPanel();
        jScrollPane1 = new javax.swing.JScrollPane();
        txTextArea = new javax.swing.JTextArea();
        keyboardTxToggleButton = new javax.swing.JToggleButton();
        clearBufferButton = new javax.swing.JButton();
        echoTextField = new javax.swing.JTextField();
        menuBar = new javax.swing.JMenuBar();
        fileMenu = new javax.swing.JMenu();
        exitMenuItem = new javax.swing.JMenuItem();
        stationMenu = new javax.swing.JMenu();
        stationMenuItem = new javax.swing.JMenuItem();
        satelliteMenu = new javax.swing.JMenu();
        chooseSatelliteMenuItem = new javax.swing.JMenuItem();
        AudioDeviceMenu = new javax.swing.JMenu();
        jSeparator1 = new javax.swing.JPopupMenu.Separator();
        jMenu1 = new javax.swing.JMenu();
        aboutMenuItem = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("DopplerPSK");
        addWindowListener(new java.awt.event.WindowAdapter() {
            public void windowClosing(java.awt.event.WindowEvent evt) {
                formWindowClosing(evt);
            }
        });

        satInfoPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Satellite Info"));
        satInfoPanel.setLayout(new java.awt.GridLayout(3, 2, 2, 0));

        jLabel1.setText("Sat:");
        jLabel1.setToolTipText("Satellite being tracked");
        satInfoPanel.add(jLabel1);

        satNameLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        satNameLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        satNameLabel.setText("--");
        satNameLabel.setToolTipText("Satellite being tracked");
        satInfoPanel.add(satNameLabel);

        jLabel3.setText("AZ:");
        jLabel3.setToolTipText("Satellite azimuth at station");
        satInfoPanel.add(jLabel3);

        azLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        azLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        azLabel.setText("--");
        azLabel.setToolTipText("Satellite azimuth at station");
        satInfoPanel.add(azLabel);

        jLabel5.setText("EL:");
        jLabel5.setToolTipText("Satellite elevation at station");
        satInfoPanel.add(jLabel5);

        elLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        elLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        elLabel.setText("--");
        elLabel.setToolTipText("Satellite elevation at station");
        satInfoPanel.add(elLabel);

        infoPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Uplink"));
        infoPanel.setToolTipText("");
        infoPanel.setLayout(new java.awt.GridLayout(5, 2));

        jLabel4.setText("Uplink Freq:");
        infoPanel.add(jLabel4);

        uplinkFreqLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        uplinkFreqLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        uplinkFreqLabel.setText("--");
        infoPanel.add(uplinkFreqLabel);

        jLabel7.setText("Doppler Adj.:");
        jLabel7.setToolTipText("Absolute Doppler shift adjustment in Hz");
        infoPanel.add(jLabel7);

        dopplerLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        dopplerLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        dopplerLabel.setText("--");
        dopplerLabel.setToolTipText("Absolute Doppler shift adjustment in Hz");
        infoPanel.add(dopplerLabel);

        jLabel9.setText("Doppler Adj. Rate:");
        jLabel9.setToolTipText("Change in Doppler shift of uplink(Hz/s)");
        infoPanel.add(jLabel9);

        ddfLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        ddfLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        ddfLabel.setText("--");
        ddfLabel.setToolTipText("Change in Doppler shift of uplink(Hz/s)");
        infoPanel.add(ddfLabel);

        jLabel2.setText("Center AF Freq.:");
        jLabel2.setToolTipText("Audio frequency of carrier without Doppler correction");
        infoPanel.add(jLabel2);

        centerAfFreqLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        centerAfFreqLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        centerAfFreqLabel.setText("--");
        centerAfFreqLabel.setToolTipText("Audio frequency of carrier without Doppler correction");
        infoPanel.add(centerAfFreqLabel);

        jLabel6.setText("Current AF Freq.:");
        jLabel6.setToolTipText("Current TX audio frequency, corrected for uplink Doppler (Hz)");
        infoPanel.add(jLabel6);

        currentAfFreqLabel.setFont(new java.awt.Font("Courier New", 0, 11)); // NOI18N
        currentAfFreqLabel.setHorizontalAlignment(javax.swing.SwingConstants.TRAILING);
        currentAfFreqLabel.setText("--");
        currentAfFreqLabel.setToolTipText("Current TX audio frequency, corrected for uplink Doppler (Hz)");
        infoPanel.add(currentAfFreqLabel);

        javax.swing.GroupLayout topPanelLayout = new javax.swing.GroupLayout(topPanel);
        topPanel.setLayout(topPanelLayout);
        topPanelLayout.setHorizontalGroup(
            topPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(topPanelLayout.createSequentialGroup()
                .addComponent(satInfoPanel, javax.swing.GroupLayout.PREFERRED_SIZE, 219, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(infoPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        );
        topPanelLayout.setVerticalGroup(
            topPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(satInfoPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, topPanelLayout.createSequentialGroup()
                .addComponent(infoPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(0, 0, Short.MAX_VALUE))
        );

        macroPanel.setLayout(new javax.swing.BoxLayout(macroPanel, javax.swing.BoxLayout.PAGE_AXIS));

        messageTxToggleButton.setText("TX");
        messageTxToggleButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                messageTxToggleButtonActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout messagePanelLayout = new javax.swing.GroupLayout(messagePanel);
        messagePanel.setLayout(messagePanelLayout);
        messagePanelLayout.setHorizontalGroup(
            messagePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(messagePanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(messagePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(macroPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, messagePanelLayout.createSequentialGroup()
                        .addGap(0, 521, Short.MAX_VALUE)
                        .addComponent(messageTxToggleButton, javax.swing.GroupLayout.PREFERRED_SIZE, 65, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addContainerGap())
        );
        messagePanelLayout.setVerticalGroup(
            messagePanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(messagePanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(macroPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(messageTxToggleButton)
                .addContainerGap())
        );

        jTabbedPane1.addTab("Messages", messagePanel);

        txTextArea.setColumns(20);
        txTextArea.setLineWrap(true);
        txTextArea.setRows(5);
        txTextArea.setWrapStyleWord(true);
        txTextArea.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyTyped(java.awt.event.KeyEvent evt) {
                txTextAreaKeyTyped(evt);
            }
        });
        jScrollPane1.setViewportView(txTextArea);

        keyboardTxToggleButton.setText("TX");
        keyboardTxToggleButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                keyboardTxToggleButtonActionPerformed(evt);
            }
        });

        clearBufferButton.setText("Clear Buffer");
        clearBufferButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                clearBufferButtonActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout txPanelLayout = new javax.swing.GroupLayout(txPanel);
        txPanel.setLayout(txPanelLayout);
        txPanelLayout.setHorizontalGroup(
            txPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(txPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(txPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 586, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, txPanelLayout.createSequentialGroup()
                        .addGap(0, 0, Short.MAX_VALUE)
                        .addComponent(clearBufferButton)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addComponent(keyboardTxToggleButton, javax.swing.GroupLayout.PREFERRED_SIZE, 65, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addContainerGap())
        );
        txPanelLayout.setVerticalGroup(
            txPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(txPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 240, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(txPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(keyboardTxToggleButton)
                    .addComponent(clearBufferButton))
                .addContainerGap())
        );

        jTabbedPane1.addTab("Keyboard Chat", txPanel);

        echoTextField.setEditable(false);
        echoTextField.setBackground(new java.awt.Color(204, 204, 204));
        echoTextField.setHorizontalAlignment(javax.swing.JTextField.TRAILING);

        fileMenu.setText("File");

        exitMenuItem.setText("Exit");
        exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                exitMenuItemActionPerformed(evt);
            }
        });
        fileMenu.add(exitMenuItem);

        menuBar.add(fileMenu);

        stationMenu.setText("Station");

        stationMenuItem.setText("Edit station information...");
        stationMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                stationMenuItemActionPerformed(evt);
            }
        });
        stationMenu.add(stationMenuItem);

        menuBar.add(stationMenu);

        satelliteMenu.setText("Satellite");

        chooseSatelliteMenuItem.setText("Choose Satellite...");
        chooseSatelliteMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                chooseSatelliteMenuItemActionPerformed(evt);
            }
        });
        satelliteMenu.add(chooseSatelliteMenuItem);

        menuBar.add(satelliteMenu);

        AudioDeviceMenu.setText("Audio Device");
        AudioDeviceMenu.add(jSeparator1);

        menuBar.add(AudioDeviceMenu);

        jMenu1.setText("Help");

        aboutMenuItem.setText("About");
        aboutMenuItem.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                aboutMenuItemActionPerformed(evt);
            }
        });
        jMenu1.add(aboutMenuItem);

        menuBar.add(jMenu1);

        setJMenuBar(menuBar);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTabbedPane1)
                    .addComponent(topPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(echoTextField))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addComponent(topPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(echoTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addGap(35, 35, 35)
                .addComponent(jTabbedPane1)
                .addContainerGap())
        );

        jTabbedPane1.getAccessibleContext().setAccessibleName("tab2");
        jTabbedPane1.getAccessibleContext().setAccessibleDescription("");

        pack();
    }// </editor-fold>//GEN-END:initComponents

    private void keyboardTxToggleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_keyboardTxToggleButtonActionPerformed
        
        if (messageTxToggleButton.isSelected()) {
            messageTxToggleButton.setSelected(false);
        }
        if (tx == null) {
            try {
                tx = getPsk31Tx(lastUsedAudioDeviceName);
                tx.addListener(this);

                // immediately set the the tx to the last known satellite params.
                // this will make sure the transmitter starts on the right freq.
                if (lastEvt != null) {
                    tx.SatelliteUpdateEventOccured(lastEvt);
                }

            } catch (LineUnavailableException ex) {
                Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        if (tx != null) {
            if (keyboardTxToggleButton.isSelected()) {
                if (tx.isIsLoop()) {
                    tx.setLoop(false);
                    tx.clearBuffer();
                }
                tx.startTx();
                
            } else {
                tx.stopTx();
            }
        }
    }//GEN-LAST:event_keyboardTxToggleButtonActionPerformed

    private void txTextAreaKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_txTextAreaKeyTyped
        if (tx != null) {
            int k = evt.getKeyChar();
            System.out.println(k);
            if (k == KeyEvent.VK_BACK_SPACE) {
                tx.removeChar();
            } else if (k == KeyEvent.VK_ENTER) {
                tx.putChar('\r');
            } else if (k == KeyEvent.VK_ESCAPE) {
                tx.stopTx();
            } else {
                tx.putChar(evt.getKeyChar());
            }
        }
    }//GEN-LAST:event_txTextAreaKeyTyped

    private void chooseSatelliteMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_chooseSatelliteMenuItemActionPerformed
        SatelliteChooserDialog d = new SatelliteChooserDialog(this, satelliteDirectory.getSatelliteNames(), currentSatelliteName, true);
        d.setVisible(true);  // blocks until dialog closes

        int option = d.getUserAction();
        if (option == SatelliteChooserDialog.OK_ACTION) {
            String s = d.getSelectedSatelliteName();
            System.out.println("User selected satellite: " + s);
            loadSelectedSatellite(s);
        } else {
            System.out.println("User cancelled satellite selection");
        }
    }//GEN-LAST:event_chooseSatelliteMenuItemActionPerformed

    private void clearBufferButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_clearBufferButtonActionPerformed
        if (tx != null) {
            tx.clearBuffer();
        }
        txTextArea.setText("");
    }//GEN-LAST:event_clearBufferButtonActionPerformed

    private void stationMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_stationMenuItemActionPerformed
        StationInfoDialog d = new StationInfoDialog(this, Math.toDegrees(stationLat), Math.toDegrees(stationLon), stationAlt, true);
        d.setVisible(true);  // blocks until dialog closes

        int option = d.getUserAction();
        if (option == StationInfoDialog.OK_ACTION) {
            stationAlt = d.getAlt();
            stationLat = Math.toRadians(d.getLat());
            stationLon = Math.toRadians(d.getLon());
            System.out.println("new groudstation location: " + stationLat + " " + stationLon + " " + stationAlt);
            // reload the satellite

            loadSelectedSatellite(currentSatelliteName);

        } else {
            System.out.println("User cancelled edit groudstation");
        }
    }//GEN-LAST:event_stationMenuItemActionPerformed

    private void exitMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exitMenuItemActionPerformed
        System.exit(0);
    }//GEN-LAST:event_exitMenuItemActionPerformed

    private void formWindowClosing(java.awt.event.WindowEvent evt) {//GEN-FIRST:event_formWindowClosing
        saveSettings();
    }//GEN-LAST:event_formWindowClosing

    private void aboutMenuItemActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_aboutMenuItemActionPerformed
        JOptionPane.showMessageDialog(this,
                "Version 0.20\nAndrew T. Flowers, K0SM",
                "About DopplerPSK",
                JOptionPane.INFORMATION_MESSAGE);
    }//GEN-LAST:event_aboutMenuItemActionPerformed

    private void messageTxToggleButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_messageTxToggleButtonActionPerformed
         
        if (keyboardTxToggleButton.isSelected()) {
            keyboardTxToggleButton.setSelected(false);
        }
        if (tx == null) {
            try {
                tx = getPsk31Tx(lastUsedAudioDeviceName);
                tx.addListener(this);

                // immediately set the the tx to the last known satellite params.
                // this will make sure the transmitter starts on the right freq.
                if (lastEvt != null) {
                    tx.SatelliteUpdateEventOccured(lastEvt);
                }

            } catch (LineUnavailableException ex) {
                Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
            }
        }

        if (tx != null) {
            if (messageTxToggleButton.isSelected()) {
                tx.clearBuffer();
                tx.startTx();
                tx.setLoop(true);
                
                //get selected message
                String message = null;
                Component[] comps = macroPanel.getComponents();
                for (Component c: comps) {
                    if (c instanceof MessagePanel) {
                        MessagePanel p = (MessagePanel) c;
                        if (p.isSelected()) {
                          message =  p.getFormattedMessage();
                          break;
                        }
                           
                    }
                }
                
                if (!message.endsWith(" ")) {
                    message = message + " ";
                }
                tx.putLoopMessage(message);
                
            } else {
                tx.stopTx();
            }
        }
        
    }//GEN-LAST:event_messageTxToggleButtonActionPerformed

    private void saveSettings() {
        Properties p = new Properties();
        p.put("stationLat", "" + Math.toDegrees(stationLat));
        p.put("stationLon", "" + Math.toDegrees(stationLon));
        p.put("stationAlt", "" + stationAlt);
        p.put("dopplerCenter", "" + dopplerCenter);
        p.put("currentSatelliteName", currentSatelliteName);
        p.put("lastUsedAudioDeviceName", "" + lastUsedAudioDeviceName);
        p.put("mycall", "" + messageInfoPanel.getMyCall());
        p.put("dxcall", "" + messageInfoPanel.getDxCall());
        p.put("exchange", "" + messageInfoPanel.getExchange());
        try {
            p.store(new FileOutputStream("settings.ini"), "");
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private void loadSettings() {
        try {
            Properties p = new Properties();
            File settingsFile = new File("settings.ini");
            if (settingsFile.exists()) {
                p.load(new FileInputStream("settings.ini"));
                stationLat = Math.toRadians(Double.valueOf((String) p.getOrDefault("stationLat", "0.0")));
                stationLon = Math.toRadians(Double.valueOf((String) p.getOrDefault("stationLon", "0.0")));
                stationAlt = Double.valueOf((String) p.getOrDefault("stationAlt", "0.0"));
                currentSatelliteName = (String) p.getOrDefault("currentSatelliteName", "NO-84");
                dopplerCenter = Double.valueOf((String) p.getOrDefault("dopplerCenter", "2000.0"));
                lastUsedAudioDeviceName = (String) p.getOrDefault("lastUsedAudioDeviceName", null);
                mycall = (String) p.getOrDefault("mycall", "k0sm");
                dxcall = (String) p.getOrDefault("dxcall", "dx0dx");
                exchange = (String) p.getOrDefault("exchange", "fn13");
            } else {
                stationLat = 0.0;
                stationLon = 0.0;
                stationAlt = 0.0;
                currentSatelliteName = "NO-84";
                dopplerCenter = 2000.0;

            }
        } catch (IOException ex) {
            Logger.getLogger(DopplerPSK.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private void addMacroButtons() {
        messageInfoPanel = new MessageInfoPanel(mycall, dxcall, exchange);
        macroPanel.add(messageInfoPanel);

        MacroParser mp = new MacroParser(new File("macros"));
        mp.loadMacros();
        Map<String, Macro> macroMap = mp.getMacroMap();
        for (String s : macroMap.keySet()) {
            final Macro m = macroMap.get(s);
            MessagePanel p = new MessagePanel(m, macroButtonGroup, mycall, dxcall, exchange, this);
            messageInfoPanel.addUpdateListener(p);
            macroPanel.add(p);
        }
        
        if (macroButtonGroup.getButtonCount() > 0) {
            macroButtonGroup.getElements().nextElement().setSelected(true);
        }
        

    }

    @Override
    public void SatelliteUpdateEventOccured(SatelliteUpdateEvent evt) {

        lastEvt = evt;
        satNameLabel.setText(evt.getName());
        elLabel.setText(decimalFormat.format(evt.getEl()));
        azLabel.setText(decimalFormat.format(evt.getAz()));
        dopplerLabel.setText(decimalFormat.format(evt.getDopplerShift()) + "   Hz");
        ddfLabel.setText(decimalFormat.format(evt.getDopplerRate()) + " Hz/s");

        uplinkFreqLabel.setText(mhzFormat.format((satelliteDirectory.getSatellite(currentSatelliteName)).getUpLinkHz() / 1000000.0) + "  MHz");

        if (tx != null) {
            tx.SatelliteUpdateEventOccured(evt);
            centerAfFreqLabel.setText(integerFormat.format(tx.getCenterFrequency()) + "   Hz");
            currentAfFreqLabel.setText(integerFormat.format(tx.getInstantaneousFrequency()) + "   Hz");
        } else {
            centerAfFreqLabel.setText("    --    ");
            currentAfFreqLabel.setText("    --    ");
        }

    }

    @Override
    public void TxCharacterSent(Character c) {
        String s = echoTextField.getText();
        if (s.length() > 200) {
            s = s.substring(s.length() - 200);
        }
        if (c == 10 || c == 13) {
            echoTextField.setText(s + " ");  //space for CR/LF
        } else {
            echoTextField.setText(s + c);
        }
        echoTextField.setCaretPosition(echoTextField.getText().length());
    }
    
    @Override
    public void messageChanged(String message) {
        if (messageTxToggleButton.isSelected()) {
             tx.clearBuffer();
                tx.startTx();
                tx.setLoop(true);

                if (!message.endsWith(" ")) {
                    message = message + " ";
                }
                tx.putLoopMessage(message);
        }
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String args[]) throws OrekitException {

        /* Set the Nimbus look and feel */
        //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
            /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
        try {
            for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
                if ("Nimbus".equals(info.getName())) {
                    javax.swing.UIManager.setLookAndFeel(info.getClassName());
                    break;
                }
            }
        } catch (ClassNotFoundException ex) {
            java.util.logging.Logger.getLogger(DopplerPSK.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (InstantiationException ex) {
            java.util.logging.Logger.getLogger(DopplerPSK.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (IllegalAccessException ex) {
            java.util.logging.Logger.getLogger(DopplerPSK.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        } catch (javax.swing.UnsupportedLookAndFeelException ex) {
            java.util.logging.Logger.getLogger(DopplerPSK.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
        }
        //</editor-fold>
        //</editor-fold>

        /* Create and display the form */
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                DopplerPSK pskFrame = new DopplerPSK();

                pskFrame.setVisible(true);
            }
        });

        //</editor-fold>
    }

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JMenu AudioDeviceMenu;
    private javax.swing.JMenuItem aboutMenuItem;
    private javax.swing.ButtonGroup audioButtonGroup;
    private javax.swing.JLabel azLabel;
    private javax.swing.JLabel centerAfFreqLabel;
    private javax.swing.JMenuItem chooseSatelliteMenuItem;
    private javax.swing.JButton clearBufferButton;
    private javax.swing.JLabel currentAfFreqLabel;
    private javax.swing.JLabel ddfLabel;
    private javax.swing.JLabel dopplerLabel;
    private javax.swing.JTextField echoTextField;
    private javax.swing.JLabel elLabel;
    private javax.swing.JMenuItem exitMenuItem;
    private javax.swing.JMenu fileMenu;
    private javax.swing.JPanel infoPanel;
    private javax.swing.JLabel jLabel1;
    private javax.swing.JLabel jLabel2;
    private javax.swing.JLabel jLabel3;
    private javax.swing.JLabel jLabel4;
    private javax.swing.JLabel jLabel5;
    private javax.swing.JLabel jLabel6;
    private javax.swing.JLabel jLabel7;
    private javax.swing.JLabel jLabel9;
    private javax.swing.JMenu jMenu1;
    private javax.swing.JScrollPane jScrollPane1;
    private javax.swing.JPopupMenu.Separator jSeparator1;
    private javax.swing.JTabbedPane jTabbedPane1;
    private javax.swing.JToggleButton keyboardTxToggleButton;
    private javax.swing.ButtonGroup macroButtonGroup;
    private javax.swing.JPanel macroPanel;
    private javax.swing.JMenuBar menuBar;
    private javax.swing.JPanel messagePanel;
    private javax.swing.JToggleButton messageTxToggleButton;
    private javax.swing.JPanel satInfoPanel;
    private javax.swing.JLabel satNameLabel;
    private javax.swing.JMenu satelliteMenu;
    private javax.swing.JMenu stationMenu;
    private javax.swing.JMenuItem stationMenuItem;
    private javax.swing.JPanel topPanel;
    private javax.swing.JPanel txPanel;
    private javax.swing.JTextArea txTextArea;
    private javax.swing.JLabel uplinkFreqLabel;
    // End of variables declaration//GEN-END:variables

    

}
