001package jmri.jmrix.bachrus;
002
003//<editor-fold defaultstate="collapsed" desc="Imports">
004import jmri.jmrix.bachrus.speedmatcher.basic.BasicSpeedMatcherFactory;
005
006import java.awt.BorderLayout;
007import java.awt.CardLayout;
008import java.awt.Color;
009import java.awt.Dimension;
010import java.awt.FlowLayout;
011import java.awt.Font;
012import java.awt.GridBagConstraints;
013import java.awt.GridBagLayout;
014import java.awt.Insets;
015import java.beans.PropertyChangeEvent;
016import java.beans.PropertyChangeListener;
017import java.text.MessageFormat;
018import java.text.SimpleDateFormat;
019import java.util.*;
020
021import javax.swing.*;
022import javax.swing.border.*;
023
024import jmri.CommandStation;
025import jmri.DccLocoAddress;
026import jmri.DccThrottle;
027import jmri.GlobalProgrammerManager;
028import jmri.InstanceManager;
029import jmri.JmriException;
030import jmri.PowerManager;
031import jmri.ProgListener;
032import jmri.Programmer;
033import jmri.ProgrammerException;
034import jmri.SpeedStepMode;
035import jmri.ThrottleListener;
036import jmri.jmrit.DccLocoAddressSelector;
037import jmri.jmrit.roster.RosterEntry;
038import jmri.jmrit.roster.RosterEntrySelector;
039import jmri.jmrit.roster.swing.GlobalRosterEntryComboBox;
040import jmri.jmrix.bachrus.speedmatcher.*;
041import jmri.jmrix.bachrus.speedmatcher.SpeedMatcher.SpeedTableStep;
042import jmri.jmrix.bachrus.speedmatcher.basic.*;
043import jmri.jmrix.bachrus.speedmatcher.speedStepScale.*;
044import jmri.util.JmriJFrame;
045import jmri.util.swing.JmriJOptionPane;
046
047//</editor-fold>
048/**
049 * Frame for Speedo Console for Bachrus running stand reader interface
050 *
051 * @author Andrew Crosland Copyright (C) 2010
052 * @author Dennis Miller Copyright (C) 2015
053 * @author Todd Wegter Copyright (C) 2019-2024
054 */
055public class SpeedoConsoleFrame extends JmriJFrame implements SpeedoListener,
056        ThrottleListener,
057        ProgListener,
058        PropertyChangeListener {
059
060    /**
061     * TODO: Complete the help file
062     */
063    //<editor-fold defaultstate="collapsed" desc="Enums">
064    protected enum DisplayType {
065        NUMERIC, DIAL
066    }
067
068    protected enum ProfileState {
069        IDLE, WAIT_FOR_THROTTLE, RUNNING
070    }
071
072    protected enum ProfileDirection {
073        FORWARD, REVERSE
074    }
075
076    protected enum ProgState {
077        IDLE,
078        READ1,
079        READ3,
080        READ4,
081        READ17,
082        READ18,
083        READ29,
084        WRITE3,
085        WRITE4,
086    }
087    //</editor-fold>
088
089    //<editor-fold defaultstate="collapsed" desc="Member Variables">
090    //<editor-fold defaultstate="collapsed" desc="General GUI Elements">
091    protected JLabel scaleLabel = new JLabel();
092    protected JLabel customScaleLabel = new JLabel();
093    protected JTextField customScaleField = new JTextField(3);
094    protected int customScale = 148;
095    protected JTextField speedTextField = new JTextField(12);
096    protected JPanel displayCards = new JPanel();
097
098    protected ButtonGroup speedGroup = new ButtonGroup();
099    protected JRadioButton mphButton = new JRadioButton(Bundle.getMessage("MPH"));
100    protected JRadioButton kphButton = new JRadioButton(Bundle.getMessage("KPH"));
101    protected ButtonGroup displayGroup = new ButtonGroup();
102    protected JRadioButton numButton = new JRadioButton(Bundle.getMessage("Numeric"));
103    protected JRadioButton dialButton = new JRadioButton(Bundle.getMessage("Dial"));
104    protected SpeedoDial speedoDialDisplay = new SpeedoDial();
105    protected JCheckBox dirFwdButton = new JCheckBox(Bundle.getMessage("ScanForward"));
106    protected JCheckBox dirRevButton = new JCheckBox(Bundle.getMessage("ScanReverse"));
107    protected JCheckBox toggleGridButton = new JCheckBox(Bundle.getMessage("ToggleGrid"));
108
109    protected JLabel statusLabel = new JLabel(" ");
110    protected javax.swing.JLabel readerLabel = new javax.swing.JLabel();
111    //</editor-fold>
112
113    //<editor-fold defaultstate="collapsed" desc="General Member Variables">
114    protected static final int DEFAULT_SCALE = 8;
115
116    protected float selectedScale = 0;
117    protected int series = 0;
118    protected float sampleSpeed = 0;
119    protected float targetSpeed = 0;
120    protected float currentSpeed = 0;
121    protected float incSpeed = 0;
122    protected float oldSpeed = 0;
123    protected float acc = 0;
124    protected float avSpeed = 0;
125    protected int range = 1;
126    protected float circ = 0;
127    protected float count = 1;
128    protected float freq;
129    protected static final int DISPLAY_UPDATE = 500;
130    protected static final int FAST_DISPLAY_RATIO = 5;
131
132    /*
133     * At low speed, readings arrive less often and less filtering
134     * is applied to minimize the delay in updating the display
135     *
136     * Speed measurement is split into 4 ranges with an overlap, to
137     * prevent "hunting" between the ranges.
138     */
139    protected static final int RANGE1LO = 0;
140    protected static final int RANGE1HI = 9;
141    protected static final int RANGE2LO = 7;
142    protected static final int RANGE2HI = 31;
143    protected static final int RANGE3LO = 29;
144    protected static final int RANGE3HI = 62;
145    protected static final int RANGE4LO = 58;
146    protected static final int RANGE4HI = 9999;
147    static final int[] FILTER_LENGTH = {0, 3, 6, 10, 20};
148
149    String selectedScalePref = this.getClass().getName() + ".SelectedScale"; // NOI18N
150    String customScalePref = this.getClass().getName() + ".CustomScale"; // NOI18N
151    String speedUnitsKphPref = this.getClass().getName() + ".SpeedUnitsKph"; // NOI18N
152    String dialTypePref = this.getClass().getName() + ".DialType"; // NOI18N
153    jmri.UserPreferencesManager prefs;
154
155    // members for handling the Speedo interface
156    SpeedoTrafficController tc = null;
157
158    protected String[] scaleStrings = new String[]{
159        Bundle.getMessage("ScaleZ"),
160        Bundle.getMessage("ScaleEuroN"),
161        Bundle.getMessage("ScaleNFine"),
162        Bundle.getMessage("ScaleJapaneseN"),
163        Bundle.getMessage("ScaleBritishN"),
164        Bundle.getMessage("Scale3mm"),
165        Bundle.getMessage("ScaleTT"),
166        Bundle.getMessage("Scale00"),
167        Bundle.getMessage("ScaleH0"),
168        Bundle.getMessage("ScaleS"),
169        Bundle.getMessage("Scale048"),
170        Bundle.getMessage("Scale045"),
171        Bundle.getMessage("Scale043"),
172        Bundle.getMessage("ScaleOther")
173    };
174
175    protected float[] scales = new float[]{
176        220,
177        160,
178        152,
179        150,
180        148,
181        120,
182        101.6F,
183        76,
184        87,
185        64,
186        48,
187        45,
188        43,
189        -1
190    };
191
192    //Create the combo box, and assign the scales to it
193    JComboBox<String> scaleList = new JComboBox<>(scaleStrings);
194
195    private SpeedoSystemConnectionMemo _memo = null;
196
197    protected DisplayType display = DisplayType.NUMERIC;
198    //</editor-fold>
199
200    //<editor-fold defaultstate="collapsed" desc="DCC Services">
201    /*
202     * Keep track of the DCC services available
203     */
204    protected int dccServices;
205    protected static final int BASIC = 0;
206    protected static final int PROG = 1;
207    protected static final int COMMAND = 2;
208    protected static final int THROTTLE = 4;
209
210    protected boolean timerRunning = false;
211
212    protected ProgState progState = ProgState.IDLE;
213
214    protected float throttleIncrement;
215    protected Programmer prog = null;
216    protected CommandStation commandStation = null;
217
218    private PowerManager pm = null;
219    //</editor-fold>
220
221    //<editor-fold defaultstate="collapsed" desc="Address Selector GUI Elements">
222    //protected JLabel profileAddressLabel = new JLabel(Bundle.getMessage("LocoAddress"));
223    //protected JTextField profileAddressField = new JTextField(6);
224    protected JButton readAddressButton = new JButton(Bundle.getMessage("Read"));
225
226    private final DccLocoAddressSelector addrSelector = new DccLocoAddressSelector();
227    private JButton setButton;
228    private GlobalRosterEntryComboBox rosterBox;
229    protected RosterEntry rosterEntry;
230    //</editor-fold>
231
232    //<editor-fold defaultstate="collapsed" desc="Address Selector Member Variables">
233    private final boolean disableRosterBoxActions = false;
234    private DccLocoAddress locomotiveAddress = new DccLocoAddress(0, false);
235
236    //protected int profileAddress = 0;
237    protected int readAddress = 0;
238    //</editor-fold>
239
240    //<editor-fold defaultstate="collapsed" desc="Momentum GUI Elements">
241    protected SpinnerNumberModel accelerationSM = new SpinnerNumberModel(0, 0, 255, 1);
242    protected SpinnerNumberModel decelerationSM = new SpinnerNumberModel(0, 0, 255, 1);
243
244    protected JLabel accelerationLabel = new JLabel(Bundle.getMessage("MomentumAccelLabel"));
245    protected JSpinner accelerationField = new JSpinner(accelerationSM);
246
247    protected JLabel decelerationLabel = new JLabel(Bundle.getMessage("MomentumDecelLabel"));
248    protected JSpinner decelerationField = new JSpinner(decelerationSM);
249
250    protected JButton readMomentumButton = new JButton(Bundle.getMessage("MomentumReadBtn"));
251    protected JButton setMomentumButton = new JButton(Bundle.getMessage("MomentumSetBtn"));
252    //</editor-fold>
253
254    //<editor-fold defaultstate="collapsed" desc="Speed Profile GUI Elements">
255    protected JButton trackPowerButton = new JButton(Bundle.getMessage("PowerUp"));
256    protected JButton startProfileButton = new JButton(Bundle.getMessage("Start"));
257    protected JButton stopProfileButton = new JButton(Bundle.getMessage("Stop"));
258    protected JButton exportProfileButton = new JButton(Bundle.getMessage("Export"));
259    protected JButton printProfileButton = new JButton(Bundle.getMessage("Print"));
260    protected JButton resetGraphButton = new JButton(Bundle.getMessage("ResetGraph"));
261    protected JButton loadProfileButton = new JButton(Bundle.getMessage("LoadRef"));
262    protected JTextField printTitleText = new JTextField();
263
264    GraphPane profileGraphPane;
265    //</editor-fold>
266
267    //<editor-fold defaultstate="collapsed" desc="Speed Profile Member Variables">
268    protected DccSpeedProfile spFwd;
269    protected DccSpeedProfile spRev;
270    protected DccSpeedProfile spRef;
271
272    protected ProfileDirection profileDir = ProfileDirection.FORWARD;
273    protected DccThrottle throttle = null;
274    protected int profileStep = 0;
275    protected float profileSpeed;
276
277    protected ProfileState profileState = ProfileState.IDLE;
278    //</editor-fold>
279
280    //<editor-fold defaultstate="collapsed" desc="Speed Matching GUI Elements">
281    //<editor-fold defaultstate="collapsed" desc="Basic">
282    protected JLabel basicSpeedMatchInfo = new JLabel("<html><p>"
283            + Bundle.getMessage("BasicSpeedMatchDescLine1")
284            + "<br/><br/>" + Bundle.getMessage("BasicSpeedMatchDescLine2")
285            + "<br/><br/>" + Bundle.getMessage("BasicSpeedMatchDescSettings")
286            + "<br/><ul>"
287            + "<li>" + Bundle.getMessage("BasicSpeedMatchDescDigitrax") + "</li>"
288            + "<li>" + Bundle.getMessage("BasicSpeedMatchDescESU") + "</li>"
289            + "<li>" + Bundle.getMessage("BasicSpeedMatchDescNCE") + "</li>"
290            + "<li>" + Bundle.getMessage("BasicSpeedMatchDescSoundtraxx") + "</li>"
291            + "</ul>"
292            + Bundle.getMessage("BasicSpeedMatchDescLine3")
293            + "<br/><br/>" + Bundle.getMessage("BasicSpeedMatchDescLine4")
294            + "<br/><br/>" + Bundle.getMessage("BasicSpeedMatchDescLine5")
295            + "<br/><br/></p></html>");
296
297    protected ButtonGroup basicSpeedMatcherTypeGroup = new ButtonGroup();
298    protected JRadioButton basicSimpleCVSpeedMatchButton = new JRadioButton(Bundle.getMessage("SpeedMatchSimpleCVRadio"));
299    protected JRadioButton basicSpeedTableSpeedMatchButton = new JRadioButton(Bundle.getMessage("SpeedMatchSpeedTableRadio"));
300    protected JRadioButton basicESUSpeedMatchButton = new JRadioButton(Bundle.getMessage("SpeedMatchESUSpeedTableRadio"));
301
302    protected SpinnerNumberModel basicSpeedMatchWarmUpForwardSecondsSM = new SpinnerNumberModel(240, 0, 480, 1);
303    protected SpinnerNumberModel basicSpeedMatchWarmUpReverseSecondsSM = new SpinnerNumberModel(120, 0, 480, 1);
304    protected JCheckBox basicSpeedMatchReverseCheckbox = new JCheckBox(Bundle.getMessage("SpeedMatchTrimReverseChk"));
305    protected JCheckBox basicSpeedMatchWarmUpCheckBox = new JCheckBox(Bundle.getMessage("SpeedMatchWarmUpChk"));
306    protected JLabel basicSpeedMatchWarmUpForwardLabel = new JLabel(Bundle.getMessage("SpeedMatchForwardWarmUpLabel"));
307    protected JSpinner basicSpeedMatchWarmUpForwardSeconds = new JSpinner(basicSpeedMatchWarmUpForwardSecondsSM);
308    protected JLabel basicSpeedMatchWarmUpForwardUnit = new JLabel(Bundle.getMessage("SpeedMatchSecondsLabel"));
309    protected JLabel basicSpeedMatchWarmUpReverseLabel = new JLabel(Bundle.getMessage("SpeedMatchReverseWarmUpLabel"));
310    protected JSpinner basicSpeedMatchWarmUpReverseSeconds = new JSpinner(basicSpeedMatchWarmUpReverseSecondsSM);
311    protected JLabel basicSpeedMatchWarmUpReverseUnit = new JLabel(Bundle.getMessage("SpeedMatchSecondsLabel"));
312
313    protected JLabel basicSpeedMatchTargetStartSpeedLabel = new JLabel(Bundle.getMessage("BasioSpeedMatchStartSpeedLabel"));
314    protected SpinnerNumberModel startSpeedSM = new SpinnerNumberModel(3, 1, 255, 1);
315    protected JSpinner basicSpeedMatchTargetStartSpeedField = new JSpinner(startSpeedSM);
316    protected JLabel basicSpeedMatchTargetStartSpeedUnit = new JLabel(Bundle.getMessage("SpeedMatchMPHLabel"));
317
318    protected JLabel basicSpeedMatchTargetHighSpeedLabel = new JLabel(Bundle.getMessage("BasicSpeedMatchTopSpeedLabel"));
319    protected SpinnerNumberModel highSpeedSM = new SpinnerNumberModel(55, 1, 255, 1);
320    protected JSpinner basicSpeedMatchTargetHighSpeedField = new JSpinner(highSpeedSM);
321    protected JLabel basicSpeedMatchTargetHighSpeedUnit = new JLabel(Bundle.getMessage("SpeedMatchMPHLabel"));
322    protected JButton basicSpeedMatchStartStopButton = new JButton(Bundle.getMessage(("SpeedMatchStartBtn")));
323    //</editor-fold>
324
325    //<editor-fold defaultstate="collapsed" desc="Advanced">
326    protected JLabel speedStepScaleSpeedMatchInfo = new JLabel("<html><p>"
327            + Bundle.getMessage("AdvancedSpeedMatchDescLine1")
328            + "<br/><br/>" + Bundle.getMessage("AdvancedSpeedMatchDescLine2")
329            + "<br/><br/>" + Bundle.getMessage("AdvancedSpeedMatchDescSettings")
330            + "<br/><ul>"
331            + "<li>" + Bundle.getMessage("AdvancedSpeedMatchDescDigitrax") + "</li>"
332            + "<li>" + Bundle.getMessage("AdvancedSpeedMatchDescESU") + "</li>"
333            + "<li>" + Bundle.getMessage("AdvancedSpeedMatchDescNCE") + "</li>"
334            + "<li>" + Bundle.getMessage("AdvancedSpeedMatchDescSoundtraxx") + "</li>"
335            + "</ul>"
336            + Bundle.getMessage("AdvancedSpeedMatchDescLine3")
337            + "<br/><br/>" + Bundle.getMessage("AdvancedSpeedMatchDescLine4")
338            + "<br/><br/>" + Bundle.getMessage("AdvancedSpeedMatchDescLine5")
339            + "<br/><br/>" + Bundle.getMessage("AdvancedSpeedMatchDescLine6")
340            + "<br/><br/></p></html>");
341
342    protected ButtonGroup speedStepScaleSpeedMatcherTypeGroup = new ButtonGroup();
343    protected JRadioButton speedStepScaleSpeedTableSpeedMatchButton = new JRadioButton(Bundle.getMessage("SpeedMatchSpeedTableRadio"));
344    protected JRadioButton speedStepScaleESUSpeedMatchButton = new JRadioButton(Bundle.getMessage("SpeedMatchESUSpeedTableRadio"));
345
346    protected SpinnerNumberModel speedStepScaleSpeedMatchWarmUpForwardSecondsSM = new SpinnerNumberModel(240, 0, 480, 1);
347    protected SpinnerNumberModel speedStepScaleSpeedMatchWarmUpReverseSecondsSM = new SpinnerNumberModel(120, 0, 480, 1);
348    protected JCheckBox speedStepScaleSpeedMatchReverseCheckbox = new JCheckBox(Bundle.getMessage("SpeedMatchTrimReverseChk"));
349    protected JCheckBox speedStepScaleSpeedMatchWarmUpCheckBox = new JCheckBox(Bundle.getMessage("SpeedMatchWarmUpChk"));
350    protected JLabel speedStepScaleSpeedMatchWarmUpForwardLabel = new JLabel(Bundle.getMessage("SpeedMatchForwardWarmUpLabel"));
351    protected JSpinner speedStepScaleSpeedMatchWarmUpForwardSeconds = new JSpinner(speedStepScaleSpeedMatchWarmUpForwardSecondsSM);
352    protected JLabel speedStepScaleSpeedMatchWarmUpForwardUnit = new JLabel(Bundle.getMessage("SpeedMatchSecondsLabel"));
353    protected JLabel speedStepScaleSpeedMatchWarmUpReverseLabel = new JLabel(Bundle.getMessage("SpeedMatchReverseWarmUpLabel"));
354    protected JSpinner speedStepScaleSpeedMatchWarmUpReverseSeconds = new JSpinner(speedStepScaleSpeedMatchWarmUpReverseSecondsSM);
355    protected JLabel speedStepScaleSpeedMatchWarmUpReverseUnit = new JLabel(Bundle.getMessage("SpeedMatchSecondsLabel"));
356
357    protected JLabel speedStepScaleMaxSpeedTargetLabel = new JLabel(Bundle.getMessage("AdvancedSpeedMatchMaxSpeed"));
358    protected JComboBox<SpeedTableStepSpeed> speedStepScaleSpeedMatchMaxSpeedField = new JComboBox<>();
359    protected JLabel speedStepScaleSpeedMatchMaxSpeedUnit = new JLabel(Bundle.getMessage("SpeedMatchMPHLabel"));
360    protected JButton speedStepScaleSpeedMatchStartStopButton = new JButton(Bundle.getMessage(("SpeedMatchStartBtn")));
361    protected JLabel speedStepScaleMaxSpeedActualLabel = new JLabel(Bundle.getMessage("AdvancedSpeedMatchActualMaxSpeed"), SwingConstants.RIGHT);
362    protected JLabel speedStepScaleMaxSpeedActualField = new JLabel("___");
363    protected JLabel speedStepScaleMaxSpeedActualUnit = new JLabel(Bundle.getMessage("SpeedMatchMPHLabel"));
364    //</editor-fold>
365    //</editor-fold>
366
367    //<editor-fold defaultstate="collapsed" desc="Speed Matching Member Variables">
368    protected SpeedMatcher speedMatcher;
369    //</editor-fold>
370    //</editor-fold>
371
372    // For testing only, must be 1 for normal use
373    protected static final int SPEED_TEST_SCALE_FACTOR = 1;
374
375    /**
376     * Constructor for the SpeedoConsoleFrame
377     *
378     * @param memo the memo for the connection the Speedo is using
379     */
380    public SpeedoConsoleFrame(SpeedoSystemConnectionMemo memo) {
381        super();
382        _memo = memo;
383    }
384
385    /**
386     * Grabs the title for the SpeedoConsoleFrame
387     *
388     * @return the frame's title
389     */
390    protected String title() {
391        return Bundle.getMessage("SpeedoConsole");
392    }
393
394    /**
395     * Sets the description for the speed profile
396     */
397    private void setTitle() {
398        Date today;
399        String result;
400        SimpleDateFormat formatter;
401        formatter = new SimpleDateFormat("EEE d MMM yyyy", Locale.getDefault());
402        today = new Date();
403        result = formatter.format(today);
404        String annotate = Bundle.getMessage("ProfileFor") + " "
405                + locomotiveAddress.getNumber() + " " + Bundle.getMessage("CreatedOn")
406                + " " + result;
407        printTitleText.setText(annotate);
408    }
409
410    /**
411     * Override for the JmriJFrame's dispose function
412     */
413    @Override
414    public void dispose() {
415        if (prefs != null) {
416            prefs.setComboBoxLastSelection(selectedScalePref, (String) scaleList.getSelectedItem());
417            prefs.setProperty(customScalePref, "customScale", customScale);
418            prefs.setSimplePreferenceState(speedUnitsKphPref, kphButton.isSelected());
419            prefs.setSimplePreferenceState(dialTypePref, dialButton.isSelected());
420        }
421        _memo.getTrafficController().removeSpeedoListener(this);
422        super.dispose();
423    }
424
425    // FIXME: Why does the if statement in this method include a direct false?
426    /**
427     * Override for the JmriJFrame's initComponents function
428     */
429    @Override
430    public void initComponents() {
431        prefs = jmri.InstanceManager.getDefault(jmri.UserPreferencesManager.class);
432
433        setTitle(title());
434        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
435
436        // What services do we have?
437        dccServices = BASIC;
438        if (InstanceManager.getNullableDefault(GlobalProgrammerManager.class) != null) {
439            if (InstanceManager.getDefault(GlobalProgrammerManager.class).isGlobalProgrammerAvailable()) {
440                prog = InstanceManager.getDefault(GlobalProgrammerManager.class).getGlobalProgrammer();
441                dccServices |= PROG;
442            }
443        }
444        if (InstanceManager.getNullableDefault(jmri.ThrottleManager.class) != null) {
445            // otherwise we'll send speed commands
446            log.info("Using Throttle interface for profiling");
447            dccServices |= THROTTLE;
448        }
449
450        if (InstanceManager.getNullableDefault(jmri.PowerManager.class) != null) {
451            pm = InstanceManager.getDefault(jmri.PowerManager.class);
452            pm.addPropertyChangeListener(this);
453        }
454
455        //<editor-fold defaultstate="collapsed" desc="GUI Layout and Button Handlers">
456        //<editor-fold defaultstate="collapsed" desc="Basic Setup Panel">
457        /*
458         * Setup pane for basic operations
459         */
460        JPanel basicPane = new JPanel();
461        basicPane.setLayout(new BoxLayout(basicPane, BoxLayout.Y_AXIS));
462
463        // Scale panel to hold the scale selector
464        JPanel scalePanel = new JPanel();
465        scalePanel.setBorder(BorderFactory.createTitledBorder(
466                BorderFactory.createEtchedBorder(), Bundle.getMessage("SelectScale")));
467        scalePanel.setLayout(new FlowLayout());
468
469        scaleList.setToolTipText(Bundle.getMessage("SelectScaleToolTip"));
470        String lastSelectedScale = prefs.getComboBoxLastSelection(selectedScalePref);
471        if (lastSelectedScale != null && !lastSelectedScale.equals("")) {
472            try {
473                scaleList.setSelectedItem(lastSelectedScale);
474            } catch (ArrayIndexOutOfBoundsException e) {
475                scaleList.setSelectedIndex(DEFAULT_SCALE);
476            }
477        } else {
478            scaleList.setSelectedIndex(DEFAULT_SCALE);
479        }
480
481        if (scaleList.getSelectedIndex() > -1) {
482            selectedScale = scales[scaleList.getSelectedIndex()];
483        }
484
485        // Listen to selection of scale
486        scaleList.addActionListener(e -> {
487            selectedScale = scales[scaleList.getSelectedIndex()];
488            checkCustomScale();
489        });
490
491        scaleLabel.setText(Bundle.getMessage("Scale"));
492        scaleLabel.setVisible(true);
493
494        readerLabel.setText(Bundle.getMessage("UnknownReader"));
495        readerLabel.setVisible(true);
496
497        scalePanel.add(scaleLabel);
498        scalePanel.add(scaleList);
499        scalePanel.add(readerLabel);
500
501        // Custom Scale panel to hold the custome scale selection
502        JPanel customScalePanel = new JPanel();
503        customScalePanel.setBorder(BorderFactory.createTitledBorder(
504                BorderFactory.createEtchedBorder(), Bundle.getMessage("CustomScale")));
505        customScalePanel.setLayout(new FlowLayout());
506
507        customScaleLabel.setText("1: ");
508        customScaleLabel.setVisible(true);
509        customScaleField.setVisible(true);
510        try {
511            customScaleField.setText(prefs.getProperty(customScalePref, "customScale").toString());
512        } catch (java.lang.NullPointerException npe) {
513            customScaleField.setText("1");
514        }
515        checkCustomScale();
516        getCustomScale();
517
518        // Let user press return to enter custom scale
519        customScaleField.addActionListener(e -> getCustomScale());
520
521        customScalePanel.add(customScaleLabel);
522        customScalePanel.add(customScaleField);
523
524        basicPane.add(scalePanel);
525        basicPane.add(customScalePanel);
526        //</editor-fold>
527
528        //<editor-fold defaultstate="collapsed" desc="Speedometer Panel">
529        // Speed panel for the dial or digital speed display
530        JPanel speedPanel = new JPanel();
531        speedPanel.setBorder(BorderFactory.createTitledBorder(
532                BorderFactory.createEtchedBorder(), Bundle.getMessage("MeasuredSpeed")));
533        speedPanel.setLayout(new BoxLayout(speedPanel, BoxLayout.X_AXIS));
534
535        // Display Panel which is a card layout with cards to show
536        // numeric or dial type speed display
537        displayCards.setLayout(new CardLayout());
538
539        // Numeric speed card
540        JPanel numericSpeedPanel = new JPanel();
541        numericSpeedPanel.setLayout(new BoxLayout(numericSpeedPanel, BoxLayout.X_AXIS));
542        Font f = new Font("", Font.PLAIN, 96);
543        speedTextField.setFont(f);
544        speedTextField.setHorizontalAlignment(JTextField.RIGHT);
545        speedTextField.setColumns(3);
546        speedTextField.setText("0.0");
547        speedTextField.setVisible(true);
548        speedTextField.setToolTipText(Bundle.getMessage("SpeedHere"));
549        numericSpeedPanel.add(speedTextField);
550
551        // Dial speed card
552        JPanel dialSpeedPanel = new JPanel();
553        dialSpeedPanel.setLayout(new BoxLayout(dialSpeedPanel, BoxLayout.X_AXIS));
554        dialSpeedPanel.add(speedoDialDisplay);
555        speedoDialDisplay.update(0.0F);
556
557        // Add cards to panel
558        displayCards.add(dialSpeedPanel, "DIAL");
559        displayCards.add(numericSpeedPanel, "NUMERIC");
560        CardLayout cl = (CardLayout) displayCards.getLayout();
561        cl.show(displayCards, "DIAL");
562
563        // button panel
564        JPanel buttonPanel = new JPanel();
565        buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.Y_AXIS));
566        speedGroup.add(mphButton);
567        speedGroup.add(kphButton);
568        mphButton.setToolTipText(Bundle.getMessage("TTDisplayMPH"));
569        kphButton.setToolTipText(Bundle.getMessage("TTDisplayKPH"));
570        mphButton.setSelected(!prefs.getSimplePreferenceState(speedUnitsKphPref));
571        kphButton.setSelected(prefs.getSimplePreferenceState(speedUnitsKphPref));
572        displayGroup.add(numButton);
573        displayGroup.add(dialButton);
574        numButton.setToolTipText(Bundle.getMessage("TTDisplayNumeric"));
575        dialButton.setToolTipText(Bundle.getMessage("TTDisplayDial"));
576        numButton.setSelected(!prefs.getSimplePreferenceState(dialTypePref));
577        dialButton.setSelected(prefs.getSimplePreferenceState(dialTypePref));
578        buttonPanel.add(mphButton);
579        buttonPanel.add(kphButton);
580        buttonPanel.add(numButton);
581        buttonPanel.add(dialButton);
582
583        speedPanel.add(displayCards);
584        speedPanel.add(buttonPanel);
585
586        // Listen to change of units, convert current average and update display
587        mphButton.addActionListener(e -> setUnits());
588        kphButton.addActionListener(e -> setUnits());
589
590        // Listen to change of display
591        numButton.addActionListener(e -> setDial());
592        dialButton.addActionListener(e -> setDial());
593
594        basicPane.add(speedPanel);
595        //</editor-fold>
596
597        //<editor-fold defaultstate="collapsed" desc="Address, Speed Profiling, Speed Matching, and Title Panel">
598        JPanel profileAndSpeedMatchingPane = new JPanel();
599        profileAndSpeedMatchingPane.setLayout(new BorderLayout());
600
601        //<editor-fold defaultstate="collapsed" desc="Address and Momentum Panel">      
602        //<editor-fold defaultstate="collapsed" desc="Address Pane">
603        JPanel addrPane = new JPanel();
604        GridBagLayout gLayout = new GridBagLayout();
605        GridBagConstraints gConstraints = new GridBagConstraints();
606        gConstraints.insets = new Insets(3, 3, 3, 3);
607        Border addrPaneBorder = javax.swing.BorderFactory.createEtchedBorder();
608        TitledBorder addrPaneTitle = javax.swing.BorderFactory.createTitledBorder(addrPaneBorder, Bundle.getMessage("LocoSelection"));
609        addrPane.setLayout(gLayout);
610        addrPane.setBorder(addrPaneTitle);
611
612        setButton = new JButton(Bundle.getMessage("ButtonSet"));
613        setButton.addActionListener(e -> changeOfAddress());
614        addrSelector.setAddress(null);
615
616        rosterBox = new GlobalRosterEntryComboBox();
617        rosterBox.setNonSelectedItem(Bundle.getMessage("NoLocoSelected"));
618        rosterBox.setToolTipText(Bundle.getMessage("TTSelectLocoFromRoster"));
619
620        /*
621         Using an ActionListener didn't select a loco from the ComboBox properly
622         so changed it to a PropertyChangeListener approach modeled on the code
623         in CombinedLocoSelPane class, layoutRosterSelection method, which is known to work.
624         Not sure why the ActionListener didn't work properly, but this fixes the bug
625         */
626        rosterBox.addPropertyChangeListener(RosterEntrySelector.SELECTED_ROSTER_ENTRIES, pce -> {
627            if (!disableRosterBoxActions) { //Have roster box actions been disabled?
628                rosterItemSelected();
629            }
630        });
631
632        readAddressButton.setToolTipText(Bundle.getMessage("ReadLoco"));
633
634        addrPane.add(addrSelector.getCombinedJPanel(), gConstraints);
635        addrPane.add(new JLabel(" "), gConstraints);
636        addrPane.add(setButton, gConstraints);
637        addrPane.add(new JLabel(" "), gConstraints);
638        addrPane.add(rosterBox, gConstraints);
639        addrPane.add(new JLabel(" "), gConstraints);
640        addrPane.add(readAddressButton, gConstraints);
641
642        if ((dccServices & PROG) != PROG) {
643            // No programming facility so user must enter address
644            readAddressButton.setEnabled(false);
645            readMomentumButton.setEnabled(false);
646        } else {
647            readAddressButton.setEnabled(true);
648            readMomentumButton.setEnabled(true);
649        }
650
651        // Listen to read button
652        readAddressButton.addActionListener(e -> readAddress());
653        //</editor-fold>
654
655        //<editor-fold defaultstate="collapsed" desc="Momentum Panel">
656        JPanel momentumPane = new JPanel();
657        momentumPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("MomentumTitle")));
658        momentumPane.setLayout(new FlowLayout());
659        momentumPane.add(accelerationLabel);
660        momentumPane.add(accelerationField);
661        momentumPane.add(decelerationLabel);
662        momentumPane.add(decelerationField);
663        momentumPane.add(readMomentumButton);
664        momentumPane.add(setMomentumButton);
665
666        // Listen to read momentum button
667        readMomentumButton.addActionListener(e -> readMomentum());
668
669        //Listen to set momentum button
670        setMomentumButton.addActionListener(e -> setMomentum());
671        //</editor-fold>
672
673        JPanel profileAndSpeedMatchingNorthPane = new JPanel();
674        profileAndSpeedMatchingNorthPane.setLayout(new BoxLayout(profileAndSpeedMatchingNorthPane, BoxLayout.Y_AXIS));
675        profileAndSpeedMatchingNorthPane.add(addrPane);
676        profileAndSpeedMatchingNorthPane.add(momentumPane);
677
678        profileAndSpeedMatchingPane.add(profileAndSpeedMatchingNorthPane, BorderLayout.NORTH);
679        //</editor-fold>
680
681        //<editor-fold defaultstate="collapsed" desc="Speed Matching and Profiling Panel">
682        JTabbedPane profileAndSpeedMatchingTabs = new JTabbedPane();
683
684        GridBagConstraints row1 = new GridBagConstraints();
685        row1.anchor = GridBagConstraints.WEST;
686        row1.fill = GridBagConstraints.HORIZONTAL;
687        GridBagConstraints row2 = new GridBagConstraints();
688        row2.gridy = 1;
689        row2.anchor = GridBagConstraints.EAST;
690        GridBagConstraints row3 = new GridBagConstraints();
691        row3.gridy = 2;
692        row3.anchor = GridBagConstraints.WEST;
693
694        GridBagConstraints gbc = new GridBagConstraints();
695
696        //<editor-fold defaultstate="collapsed" desc="Speed Profiling Tab">
697        // Pane for profiling loco speed curve
698        JPanel profilePane = new JPanel();
699        profilePane.setLayout(new BorderLayout());
700
701        // pane to hold the graph
702        spFwd = new DccSpeedProfile(29);       // 28 step plus step 0
703        spRev = new DccSpeedProfile(29);       // 28 step plus step 0
704        spRef = new DccSpeedProfile(29);       // 28 step plus step 0
705        profileGraphPane = new GraphPane(spFwd, spRev, spRef);
706        profileGraphPane.setPreferredSize(new Dimension(600, 300));
707        profileGraphPane.setXLabel(Bundle.getMessage("SpeedStep"));
708        profileGraphPane.setUnitsMph();
709
710        profilePane.add(profileGraphPane, BorderLayout.CENTER);
711
712        // pane to hold the buttons
713        JPanel profileButtonPane = new JPanel();
714        profileButtonPane.setLayout(new FlowLayout());
715        profileButtonPane.add(trackPowerButton);
716        trackPowerButton.setToolTipText(Bundle.getMessage("TTPower"));
717        profileButtonPane.add(startProfileButton);
718        startProfileButton.setToolTipText(Bundle.getMessage("TTStartProfile"));
719        profileButtonPane.add(stopProfileButton);
720        stopProfileButton.setToolTipText(Bundle.getMessage("TTStopProfile"));
721        profileButtonPane.add(exportProfileButton);
722        exportProfileButton.setToolTipText(Bundle.getMessage("TTSaveProfile"));
723        profileButtonPane.add(printProfileButton);
724        printProfileButton.setToolTipText(Bundle.getMessage("TTPrintProfile"));
725        profileButtonPane.add(resetGraphButton);
726        resetGraphButton.setToolTipText(Bundle.getMessage("TTResetGraph"));
727        profileButtonPane.add(loadProfileButton);
728        loadProfileButton.setToolTipText(Bundle.getMessage("TTLoadProfile"));
729
730        // pane to hold the title
731        JPanel titlePane = new JPanel();
732        titlePane.setLayout(new BoxLayout(titlePane, BoxLayout.X_AXIS));
733        titlePane.setBorder(new EmptyBorder(3, 0, 3, 0));
734        //JTextArea profileTitle = new JTextArea("Title: ");
735        //profileTitlePane.add(profileTitle);
736        printTitleText.setToolTipText(Bundle.getMessage("TTPrintTitle"));
737        printTitleText.setText(Bundle.getMessage("TTText1"));
738        titlePane.add(printTitleText);
739
740        // pane to wrap buttons and title
741        JPanel profileSouthPane = new JPanel();
742        profileSouthPane.setLayout(new BoxLayout(profileSouthPane, BoxLayout.Y_AXIS));
743        profileSouthPane.add(profileButtonPane);
744        profileSouthPane.add(titlePane);
745
746        profilePane.add(profileSouthPane, BorderLayout.SOUTH);
747
748        // Pane to hold controls
749        JPanel profileControlPane = new JPanel();
750        profileControlPane.setLayout(new BoxLayout(profileControlPane, BoxLayout.Y_AXIS));
751        dirFwdButton.setSelected(true);
752        dirFwdButton.setToolTipText(Bundle.getMessage("TTMeasFwd"));
753        dirRevButton.setToolTipText(Bundle.getMessage("TTMeasRev"));
754        dirFwdButton.setForeground(Color.RED);
755        dirRevButton.setForeground(Color.BLUE);
756        profileControlPane.add(dirFwdButton);
757        profileControlPane.add(dirRevButton);
758        toggleGridButton.setSelected(true);
759        profileControlPane.add(toggleGridButton);
760        profileGraphPane.showGrid(toggleGridButton.isSelected());
761
762        profilePane.add(profileControlPane, BorderLayout.EAST);
763
764        profileAndSpeedMatchingTabs.addTab("Speed Profile", profilePane);
765
766        //<editor-fold defaultstate="collapsed" desc="Speed Profiling Button Handlers">
767        // Listen to track Power button
768        trackPowerButton.addActionListener(e -> trackPower());
769
770        // Listen to start profile button
771        startProfileButton.addActionListener(e -> {
772            getCustomScale();
773            startProfile();
774        });
775
776        // Listen to stop profile button
777        stopProfileButton.addActionListener(e -> stopProfileAndSpeedMatch());
778
779        // Listen to grid button
780        toggleGridButton.addActionListener(e -> {
781            profileGraphPane.showGrid(toggleGridButton.isSelected());
782            profileGraphPane.repaint();
783        });
784
785        // Listen to export button
786        exportProfileButton.addActionListener(e -> {
787            if (dirFwdButton.isSelected() && dirRevButton.isSelected()) {
788                DccSpeedProfile[] sp = {spFwd, spRev};
789                DccSpeedProfile.export(sp, locomotiveAddress.getNumber(), profileGraphPane.getUnits());
790            } else if (dirFwdButton.isSelected()) {
791                DccSpeedProfile.export(spFwd, locomotiveAddress.getNumber(), "fwd", profileGraphPane.getUnits());
792            } else if (dirRevButton.isSelected()) {
793                DccSpeedProfile.export(spRev, locomotiveAddress.getNumber(), "rev", profileGraphPane.getUnits());
794            }
795        });
796
797        // Listen to print button
798        printProfileButton.addActionListener(e -> profileGraphPane.printProfile(printTitleText.getText()));
799
800        // Listen to reset graph button
801        resetGraphButton.addActionListener(e -> {
802            spFwd.clear();
803            spRev.clear();
804            spRef.clear();
805            speedoDialDisplay.reset();
806            profileGraphPane.repaint();
807        });
808
809        // Listen to Load Reference button
810        loadProfileButton.addActionListener(e -> {
811            spRef.clear();
812            int response = spRef.importDccProfile(profileGraphPane.getUnits());
813            if (response == -1) {
814                statusLabel.setText(Bundle.getMessage("StatFileError"));
815            } else {
816                statusLabel.setText(Bundle.getMessage("StatFileSuccess"));
817            }
818            profileGraphPane.repaint();
819        });
820        //</editor-fold>
821        //</editor-fold> 
822
823        //<editor-fold defaultstate="collapsed" desc="Basic Speed Matching Tab">
824        basicSpeedMatcherTypeGroup.add(basicSimpleCVSpeedMatchButton);
825        basicSpeedMatcherTypeGroup.add(basicSpeedTableSpeedMatchButton);
826        basicSpeedMatcherTypeGroup.add(basicESUSpeedMatchButton);
827        basicSimpleCVSpeedMatchButton.setSelected(true);
828
829        basicSpeedMatchReverseCheckbox.setSelected(true);
830        basicSpeedMatchWarmUpCheckBox.setSelected(true);
831
832        JPanel basicSpeedMatcherPane = new JPanel();
833        basicSpeedMatcherPane.setLayout(new BorderLayout());
834        JPanel basicSpeedMatchSettingsPane = new JPanel();
835        basicSpeedMatchSettingsPane.setLayout(new BoxLayout(basicSpeedMatchSettingsPane, BoxLayout.PAGE_AXIS));
836
837        //Important Information
838        JPanel basicSpeedMatchImportantInfoPane = new JPanel();
839        basicSpeedMatchImportantInfoPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchDescTitle")));
840        basicSpeedMatchImportantInfoPane.setLayout(new BoxLayout(basicSpeedMatchImportantInfoPane, BoxLayout.LINE_AXIS));
841        basicSpeedMatchImportantInfoPane.add(basicSpeedMatchInfo);
842        basicSpeedMatchSettingsPane.add(basicSpeedMatchImportantInfoPane);
843
844        //Speed Matcher Mode
845        JPanel basicSpeedMatchModePane = new JPanel();
846        basicSpeedMatchModePane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchModeTitle")));
847        basicSpeedMatchModePane.setLayout(new FlowLayout());
848        basicSpeedMatchModePane.add(basicSimpleCVSpeedMatchButton);
849        basicSpeedMatchModePane.add(basicSpeedTableSpeedMatchButton);
850        basicSpeedMatchModePane.add(basicESUSpeedMatchButton);
851        basicSpeedMatchSettingsPane.add(basicSpeedMatchModePane);
852
853        //Other Settings
854        JPanel basicSpeedMatchOtherSettingsPane = new JPanel();
855        basicSpeedMatchOtherSettingsPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchOtherSettingTitle")));
856        basicSpeedMatchOtherSettingsPane.setLayout(new GridBagLayout());
857        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpCheckBox, row1);
858        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpForwardLabel, row2);
859        basicSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
860        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpForwardSeconds, row2);
861        basicSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
862        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpForwardUnit, row2);
863        basicSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(30, 0)), row2);
864        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpReverseLabel, row2);
865        basicSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
866        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpReverseSeconds, row2);
867        basicSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
868        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchWarmUpReverseUnit, row2);
869        basicSpeedMatchOtherSettingsPane.add(basicSpeedMatchReverseCheckbox, row3);
870        basicSpeedMatchSettingsPane.add(basicSpeedMatchOtherSettingsPane);
871
872        //Speed Settings
873        JPanel basicSpeedMatchSpeedPane = new JPanel();
874        basicSpeedMatchSpeedPane.setLayout(new GridBagLayout());
875        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetStartSpeedLabel, gbc);
876        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
877        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetStartSpeedField, gbc);
878        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
879        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetStartSpeedUnit, gbc);
880        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(15, 0)), gbc);
881        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetHighSpeedLabel, gbc);
882        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
883        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetHighSpeedField, gbc);
884        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
885        basicSpeedMatchSpeedPane.add(basicSpeedMatchTargetHighSpeedUnit, gbc);
886        basicSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(15, 0)), gbc);
887        basicSpeedMatchSpeedPane.add(basicSpeedMatchStartStopButton, gbc);
888
889        basicSpeedMatcherPane.add(basicSpeedMatchSettingsPane, BorderLayout.NORTH);
890        basicSpeedMatcherPane.add(basicSpeedMatchSpeedPane, BorderLayout.CENTER);
891
892        profileAndSpeedMatchingTabs.add(Bundle.getMessage("BasicSpeedMatchTab"), basicSpeedMatcherPane);
893
894        //<editor-fold defaultstate="collapsed" desc="Basic Speed Matcher Button Handlers">
895        // Listen to speed match button
896        basicSpeedMatchStartStopButton.addActionListener(e -> {
897            int targetStartSpeed;
898            int targetHighSpeed;
899            boolean speedMatchReverse;
900            boolean warmUpLoco;
901            int warmUpForwardSeconds;
902            int warmUpReverseSeconds;
903
904            BasicSpeedMatcherConfig.SpeedTable speedTableType;
905
906            if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
907                targetStartSpeed = startSpeedSM.getNumber().intValue();
908                targetHighSpeed = highSpeedSM.getNumber().intValue();
909
910                if (basicSpeedTableSpeedMatchButton.isSelected()) {
911                    speedTableType = BasicSpeedMatcherConfig.SpeedTable.ADVANCED;
912                } else if (basicESUSpeedMatchButton.isSelected()) {
913                    speedTableType = BasicSpeedMatcherConfig.SpeedTable.ESU;
914                } else {
915                    speedTableType = BasicSpeedMatcherConfig.SpeedTable.SIMPLE;
916                }
917
918                speedMatchReverse = basicSpeedMatchReverseCheckbox.isSelected();
919                warmUpLoco = basicSpeedMatchWarmUpCheckBox.isSelected();
920                warmUpForwardSeconds = basicSpeedMatchWarmUpForwardSecondsSM.getNumber().intValue();
921                warmUpReverseSeconds = basicSpeedMatchWarmUpReverseSecondsSM.getNumber().intValue();
922
923                speedMatcher = BasicSpeedMatcherFactory.getSpeedMatcher(
924                        speedTableType,
925                        new BasicSpeedMatcherConfig(
926                                locomotiveAddress,
927                                targetStartSpeed,
928                                targetHighSpeed,
929                                mphButton.isSelected() ? Speed.Unit.MPH : Speed.Unit.KPH,
930                                speedMatchReverse,
931                                warmUpLoco ? warmUpForwardSeconds : 0,
932                                warmUpLoco ? warmUpReverseSeconds : 0,
933                                pm,
934                                statusLabel,
935                                basicSpeedMatchStartStopButton
936                        )
937                );
938
939                if (!speedMatcher.startSpeedMatcher()) {
940                    speedMatcher = null;
941                }
942            } else {
943                stopProfileAndSpeedMatch();
944            }
945        });
946
947        basicSpeedMatchWarmUpCheckBox.addActionListener(e -> {
948            boolean enableWarmUp = basicSpeedMatchWarmUpCheckBox.isSelected();
949
950            basicSpeedMatchWarmUpForwardLabel.setEnabled(enableWarmUp);
951            basicSpeedMatchWarmUpForwardSeconds.setEnabled(enableWarmUp);
952            basicSpeedMatchWarmUpForwardUnit.setEnabled(enableWarmUp);
953            basicSpeedMatchWarmUpReverseLabel.setEnabled(enableWarmUp);
954            basicSpeedMatchWarmUpReverseSeconds.setEnabled(enableWarmUp);
955            basicSpeedMatchWarmUpReverseUnit.setEnabled(enableWarmUp);
956        });
957        //</editor-fold>
958        //</editor-fold>
959
960        //<editor-fold defaultstate="collapsed" desc="Advanced Speed Matcher Tab">
961        speedStepScaleSpeedMatcherTypeGroup.add(speedStepScaleSpeedTableSpeedMatchButton);
962        speedStepScaleSpeedMatcherTypeGroup.add(speedStepScaleESUSpeedMatchButton);
963        speedStepScaleSpeedTableSpeedMatchButton.setSelected(true);
964
965        speedStepScaleSpeedMatchReverseCheckbox.setSelected(true);
966        speedStepScaleSpeedMatchWarmUpCheckBox.setSelected(true);
967
968        JPanel speedStepScaleSpeedMatcherPane = new JPanel();
969        speedStepScaleSpeedMatcherPane.setLayout(new BorderLayout());
970        JPanel speedStepScaleSpeedMatchSettingsPane = new JPanel();
971        speedStepScaleSpeedMatchSettingsPane.setLayout(new BoxLayout(speedStepScaleSpeedMatchSettingsPane, BoxLayout.PAGE_AXIS));
972
973        //Important Information
974        JPanel speedStepScaleSpeedMatchImportantInfoPane = new JPanel();
975        speedStepScaleSpeedMatchImportantInfoPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchDescTitle")));
976        speedStepScaleSpeedMatchImportantInfoPane.setLayout(new BoxLayout(speedStepScaleSpeedMatchImportantInfoPane, BoxLayout.LINE_AXIS));
977        speedStepScaleSpeedMatchImportantInfoPane.add(speedStepScaleSpeedMatchInfo);
978        speedStepScaleSpeedMatchSettingsPane.add(speedStepScaleSpeedMatchImportantInfoPane);
979
980        //Speed Matcher Mode
981        JPanel speedStepScaleSpeedMatchModePane = new JPanel();
982        speedStepScaleSpeedMatchModePane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchModeTitle")));
983        speedStepScaleSpeedMatchModePane.setLayout(new FlowLayout());
984        speedStepScaleSpeedMatchModePane.add(speedStepScaleSpeedTableSpeedMatchButton);
985        speedStepScaleSpeedMatchModePane.add(speedStepScaleESUSpeedMatchButton);
986        speedStepScaleSpeedMatchSettingsPane.add(speedStepScaleSpeedMatchModePane);
987
988        //Other Settings
989        JPanel speedStepScaleSpeedMatchOtherSettingsPane = new JPanel();
990        speedStepScaleSpeedMatchOtherSettingsPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), Bundle.getMessage("SpeedMatchOtherSettingTitle")));
991        speedStepScaleSpeedMatchOtherSettingsPane.setLayout(new GridBagLayout());
992        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpCheckBox, row1);
993        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpForwardLabel, row2);
994        speedStepScaleSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
995        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpForwardSeconds, row2);
996        speedStepScaleSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
997        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpForwardUnit, row2);
998        speedStepScaleSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(30, 0)), row2);
999        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpReverseLabel, row2);
1000        speedStepScaleSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
1001        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpReverseSeconds, row2);
1002        speedStepScaleSpeedMatchOtherSettingsPane.add(Box.createRigidArea(new Dimension(5, 0)), row2);
1003        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchWarmUpReverseUnit, row2);
1004        speedStepScaleSpeedMatchOtherSettingsPane.add(speedStepScaleSpeedMatchReverseCheckbox, row3);
1005        speedStepScaleSpeedMatchSettingsPane.add(speedStepScaleSpeedMatchOtherSettingsPane);
1006
1007        //Speed Settings        
1008        SpeedTableStep tempStep = SpeedTableStep.STEP1;
1009        while (tempStep != null) {
1010            speedStepScaleSpeedMatchMaxSpeedField.addItem(new SpeedTableStepSpeed(tempStep));
1011            tempStep = tempStep.getNext();
1012        }
1013        speedStepScaleSpeedMatchMaxSpeedField.setSelectedIndex(12);
1014        
1015        JPanel speedStepScaleSpeedMatchSpeedPane = new JPanel();
1016        speedStepScaleSpeedMatchSpeedPane.setLayout(new GridBagLayout());
1017        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleMaxSpeedTargetLabel, gbc);
1018        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
1019        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleSpeedMatchMaxSpeedField, gbc);
1020        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
1021        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleSpeedMatchMaxSpeedUnit, gbc);
1022        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(15, 0)), gbc);
1023        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleSpeedMatchStartStopButton, gbc);
1024        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(15, 0)), gbc);
1025        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleMaxSpeedActualLabel, gbc);
1026        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
1027        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleMaxSpeedActualField, gbc);
1028        speedStepScaleSpeedMatchSpeedPane.add(Box.createRigidArea(new Dimension(5, 0)), gbc);
1029        speedStepScaleSpeedMatchSpeedPane.add(speedStepScaleMaxSpeedActualUnit, gbc);
1030
1031        speedStepScaleSpeedMatcherPane.add(speedStepScaleSpeedMatchSettingsPane, BorderLayout.NORTH);
1032        speedStepScaleSpeedMatcherPane.add(speedStepScaleSpeedMatchSpeedPane, BorderLayout.CENTER);
1033
1034        profileAndSpeedMatchingTabs.add(Bundle.getMessage("AdvancedSpeedMatchTab"), speedStepScaleSpeedMatcherPane);
1035
1036        //<editor-fold defaultstate="collapsed" desc="Speed Step Scale Speed Matcher Button Handlers">
1037        // Listen to speed match button
1038        speedStepScaleSpeedMatchStartStopButton.addActionListener(e -> {
1039            SpeedTableStepSpeed targetMaxSpeedStep;
1040            boolean speedMatchReverse;
1041            boolean warmUpLoco;
1042            int warmUpForwardSeconds;
1043            int warmUpReverseSeconds;
1044
1045            SpeedStepScaleSpeedMatcherConfig.SpeedTable speedTableType;
1046
1047            if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
1048                targetMaxSpeedStep = (SpeedTableStepSpeed)speedStepScaleSpeedMatchMaxSpeedField.getSelectedItem();
1049
1050                if (speedStepScaleESUSpeedMatchButton.isSelected()) {
1051                    speedTableType = SpeedStepScaleSpeedMatcherConfig.SpeedTable.ESU;
1052                } else {
1053                    speedTableType = SpeedStepScaleSpeedMatcherConfig.SpeedTable.ADVANCED;
1054                }
1055
1056                speedMatchReverse = speedStepScaleSpeedMatchReverseCheckbox.isSelected();
1057                warmUpLoco = speedStepScaleSpeedMatchWarmUpCheckBox.isSelected();
1058                warmUpForwardSeconds = speedStepScaleSpeedMatchWarmUpForwardSecondsSM.getNumber().intValue();
1059                warmUpReverseSeconds = speedStepScaleSpeedMatchWarmUpReverseSecondsSM.getNumber().intValue();
1060
1061                speedMatcher = SpeedStepScaleSpeedMatcherFactory.getSpeedMatcher(
1062                        speedTableType,
1063                        new SpeedStepScaleSpeedMatcherConfig(
1064                                locomotiveAddress,
1065                                targetMaxSpeedStep,
1066                                mphButton.isSelected() ? Speed.Unit.MPH : Speed.Unit.KPH,
1067                                speedMatchReverse,
1068                                warmUpLoco ? warmUpForwardSeconds : 0,
1069                                warmUpLoco ? warmUpReverseSeconds : 0,
1070                                pm,
1071                                statusLabel,
1072                                speedStepScaleMaxSpeedActualField,
1073                                speedStepScaleSpeedMatchStartStopButton
1074                        )
1075                );
1076
1077                if (!speedMatcher.startSpeedMatcher()) {
1078                    speedMatcher = null;
1079                }
1080            } else {
1081                stopProfileAndSpeedMatch();
1082            }
1083        });
1084
1085        speedStepScaleSpeedMatchWarmUpCheckBox.addActionListener(e -> {
1086            boolean enableWarmUp = speedStepScaleSpeedMatchWarmUpCheckBox.isSelected();
1087
1088            speedStepScaleSpeedMatchWarmUpForwardLabel.setEnabled(enableWarmUp);
1089            speedStepScaleSpeedMatchWarmUpForwardSeconds.setEnabled(enableWarmUp);
1090            speedStepScaleSpeedMatchWarmUpForwardUnit.setEnabled(enableWarmUp);
1091            speedStepScaleSpeedMatchWarmUpReverseLabel.setEnabled(enableWarmUp);
1092            speedStepScaleSpeedMatchWarmUpReverseSeconds.setEnabled(enableWarmUp);
1093            speedStepScaleSpeedMatchWarmUpReverseUnit.setEnabled(enableWarmUp);
1094        });
1095        //</editor-fold>
1096        //</editor-fold>
1097
1098        profileAndSpeedMatchingPane.add(profileAndSpeedMatchingTabs, BorderLayout.CENTER);
1099        //</editor-fold>
1100        //</editor-fold>
1101        //</editor-fold>
1102
1103        // Create the main pane and add the sub-panes
1104        JPanel mainPane = new JPanel();
1105        mainPane.setLayout(new BoxLayout(mainPane, BoxLayout.X_AXIS));
1106        // make basic panel
1107        mainPane.add(basicPane);
1108
1109        if (((dccServices & THROTTLE) == THROTTLE) || ((dccServices & COMMAND) == COMMAND)) {
1110            mainPane.add(profileAndSpeedMatchingPane);
1111        } else {
1112            log.info("{} Connection:{}", Bundle.getMessage("StatNoDCC"), _memo.getUserName());
1113            statusLabel.setText(Bundle.getMessage("StatNoDCC"));
1114        }
1115
1116        // add help menu to window
1117        addHelpMenu("package.jmri.jmrix.bachrus.SpeedoConsoleFrame", true);
1118
1119        // Create a wrapper with a status line and add the main content
1120        JPanel statusWrapper = new JPanel();
1121        statusWrapper.setLayout(new BorderLayout());
1122        JPanel statusPanel = new JPanel();
1123        statusPanel.setLayout(new BorderLayout());
1124        statusPanel.add(statusLabel, BorderLayout.WEST);
1125
1126        statusPanel.setBorder(BorderFactory.createEtchedBorder(EtchedBorder.RAISED));
1127        statusWrapper.add(mainPane, BorderLayout.CENTER);
1128        statusWrapper.add(statusPanel, BorderLayout.SOUTH);
1129
1130        getContentPane().add(statusWrapper);
1131        //</editor-fold>
1132
1133        // connect to TrafficController
1134        tc = _memo.getTrafficController();
1135        tc.addSpeedoListener(this);
1136
1137        setUnits();
1138        setDial();
1139
1140        // pack for display
1141        pack();
1142
1143        speedoDialDisplay.scaleFace();
1144    }
1145
1146    //<editor-fold defaultstate="collapsed" desc="Speed Reader and Calculations">
1147    /**
1148     * Handle "replies" from the hardware. In fact, all the hardware does is
1149     * send a constant stream of unsolicited speed updates.
1150     *
1151     * @param l the reply to handle
1152     */
1153    @Override
1154    public synchronized void reply(SpeedoReply l) {  // receive a reply message and log it
1155        //log.debug("Speedo reply " + l.toString());
1156        count = l.getCount();
1157        series = l.getSeries();
1158        if (count > 0) {
1159            switch (series) {
1160                case 4:
1161                    circ = 12.5664F;
1162                    readerLabel.setText(Bundle.getMessage("Reader40"));
1163                    break;
1164                case 5:
1165                    circ = 18.8496F;
1166                    readerLabel.setText(Bundle.getMessage("Reader50"));
1167                    break;
1168                case 6:
1169                    circ = 50.2655F;
1170                    readerLabel.setText(Bundle.getMessage("Reader60"));
1171                    break;
1172                case 103:
1173                    circ = (float) ((5.95 + 0.9) * Math.PI);
1174                    readerLabel.setText(Bundle.getMessage("Reader103"));
1175                    break;
1176                default:
1177                    speedTextField.setText(Bundle.getMessage("ReaderErr"));
1178                    log.error("Invalid reader type");
1179                    break;
1180            }
1181
1182            // Update speed
1183            calcSpeed();
1184        }
1185        if (timerRunning == false) {
1186            // first reply starts the timer
1187            startReplyTimer();
1188            startDisplayTimer();
1189            startFastDisplayTimer();
1190            timerRunning = true;
1191        } else {
1192            // subsequent replies restart it
1193            replyTimer.restart();
1194        }
1195    }
1196
1197    /**
1198     * Calculates the scale speed in KPH
1199     */
1200    protected void calcSpeed() {
1201        float thisScale = (selectedScale == -1) ? customScale : selectedScale;
1202        if (series == 103) {
1203            // KPF-Zeller
1204            // calculate kph: r/sec * circumference converted to hours and kph in scaleFace()
1205            sampleSpeed = (float) ((count / 8.) * circ * 3600 / 1.0E6 * thisScale * SPEED_TEST_SCALE_FACTOR);
1206            // data arrives at constant rate, so we don't average nor switch range
1207            avSpeed = sampleSpeed;
1208            log.debug("New KPF-Zeller sample: {} Average: {}", sampleSpeed, avSpeed);
1209
1210        } else if (series > 0 && series <= 6) {
1211            // Bachrus
1212            // Scale the data and calculate kph
1213            try {
1214                freq = 1500000 / count;
1215                sampleSpeed = (freq / 24) * circ * thisScale * 3600 / 1000000 * SPEED_TEST_SCALE_FACTOR;
1216            } catch (ArithmeticException ae) {
1217                log.error("Exception calculating sampleSpeed", ae);
1218            }
1219            avFn(sampleSpeed);
1220            log.debug("New Bachrus sample: {} Average: {}", sampleSpeed, avSpeed);
1221            log.debug("Acc: {} range: {}", acc, range);
1222            switchRange();
1223        }
1224    }
1225
1226    /**
1227     * Calculates the average speed using a filter
1228     *
1229     * @param speed the speed of the latest interation
1230     */
1231    protected void avFn(float speed) {
1232        // Averaging function used for speed is
1233        // S(t) = S(t-1) - [S(t-1)/N] + speed
1234        // A(t) = S(t)/N
1235        //
1236        // where S is an accumulator, N is the length of the filter (i.e.,
1237        // the number of samples included in the rolling average), and A is
1238        // the result of the averaging function.
1239        //
1240        // Re-arranged
1241        // S(t) = S(t-1) - A(t-1) + speed
1242        // A(t) = S(t)/N
1243        acc = acc - avSpeed + speed;
1244        avSpeed = acc / FILTER_LENGTH[range];
1245    }
1246
1247    /**
1248     * Clears the average speed calculation
1249     */
1250    protected void avClr() {
1251        acc = 0;
1252        avSpeed = 0;
1253    }
1254
1255    /**
1256     * Switches the filter used for averaging speed based on the measured speed
1257     */
1258    protected void switchRange() {
1259        // When we switch range we must compensate the current accumulator
1260        // value for the longer filter.
1261        switch (range) {
1262            case 1:
1263                if (sampleSpeed > RANGE1HI) {
1264                    range++;
1265                    acc = acc * FILTER_LENGTH[2] / FILTER_LENGTH[1];
1266                }
1267                break;
1268            case 2:
1269                if (sampleSpeed < RANGE2LO) {
1270                    range--;
1271                    acc = acc * FILTER_LENGTH[1] / FILTER_LENGTH[2];
1272                } else if (sampleSpeed > RANGE2HI) {
1273                    range++;
1274                    acc = acc * FILTER_LENGTH[3] / FILTER_LENGTH[2];
1275                }
1276                break;
1277            case 3:
1278                if (sampleSpeed < RANGE3LO) {
1279                    range--;
1280                    acc = acc * FILTER_LENGTH[2] / FILTER_LENGTH[3];
1281                } else if (sampleSpeed > RANGE3HI) {
1282                    range++;
1283                    acc = acc * FILTER_LENGTH[4] / FILTER_LENGTH[3];
1284                }
1285                break;
1286            case 4:
1287                if (sampleSpeed < RANGE4LO) {
1288                    range--;
1289                    acc = acc * FILTER_LENGTH[3] / FILTER_LENGTH[4];
1290                }
1291                break;
1292            default:
1293                log.debug("range {} unsupported, range unchanged.", range);
1294        }
1295    }
1296
1297    /**
1298     * Displays the speed in the SpeedoConsoleFrame's digital/analog speedometer
1299     */
1300    protected void showSpeed() {
1301        float speedForText = currentSpeed;
1302        if (mphButton.isSelected()) {
1303            speedForText = Speed.kphToMph(speedForText);
1304        }
1305        if (series > 0) {
1306            if ((currentSpeed < 0) || (currentSpeed > 999)) {
1307                log.error("Calculated speed out of range: {}", currentSpeed);
1308                speedTextField.setText("999");
1309            } else {
1310                // Final smoothing as applied by Bachrus Console. Don't update display
1311                // unless speed has changed more than 2%
1312                if ((currentSpeed > oldSpeed * 1.02) || (currentSpeed < oldSpeed * 0.98)) {
1313                    speedTextField.setText(MessageFormat.format("{0,number,##0.0}", speedForText));
1314                    speedTextField.setHorizontalAlignment(JTextField.RIGHT);
1315                    oldSpeed = currentSpeed;
1316                    speedoDialDisplay.update(currentSpeed);
1317                }
1318            }
1319        }
1320    }
1321    //</editor-fold>
1322
1323    //<editor-fold defaultstate="collapsed" desc="Speedometer Helper Functions">
1324    /**
1325     * Check if custom scale selected and enable the custom scale entry field.
1326     */
1327    protected void checkCustomScale() {
1328        if (selectedScale == -1) {
1329            customScaleField.setEnabled(true);
1330        } else {
1331            customScaleField.setEnabled(false);
1332        }
1333    }
1334
1335    /**
1336     * Set the speed to be displayed as a dial or numeric
1337     */
1338    protected void setDial() {
1339        CardLayout cl = (CardLayout) displayCards.getLayout();
1340        if (numButton.isSelected()) {
1341            display = DisplayType.NUMERIC;
1342            cl.show(displayCards, "NUMERIC");
1343        } else {
1344            display = DisplayType.DIAL;
1345            cl.show(displayCards, "DIAL");
1346        }
1347    }
1348
1349    /**
1350     * Set the displays to mile per hour or kilometers per hour
1351     */
1352    protected void setUnits() {
1353        if (mphButton.isSelected()) {
1354            profileGraphPane.setUnitsMph();
1355            basicSpeedMatchTargetStartSpeedUnit.setText(Bundle.getMessage("SpeedMatchMPHLabel"));
1356            basicSpeedMatchTargetHighSpeedUnit.setText(Bundle.getMessage("SpeedMatchMPHLabel"));
1357            speedStepScaleSpeedMatchMaxSpeedUnit.setText(Bundle.getMessage("SpeedMatchMPHLabel"));
1358            speedStepScaleMaxSpeedActualUnit.setText(Bundle.getMessage("SpeedMatchMPHLabel"));
1359        } else {
1360            profileGraphPane.setUnitsKph();
1361            basicSpeedMatchTargetStartSpeedUnit.setText(Bundle.getMessage("SpeedMatchKPHLabel"));
1362            basicSpeedMatchTargetHighSpeedUnit.setText(Bundle.getMessage("SpeedMatchKPHLabel"));
1363            speedStepScaleSpeedMatchMaxSpeedUnit.setText(Bundle.getMessage("SpeedMatchKPHLabel"));
1364            speedStepScaleMaxSpeedActualUnit.setText(Bundle.getMessage("SpeedMatchKPHLabel"));
1365        }
1366        profileGraphPane.repaint();
1367        if (mphButton.isSelected()) {
1368            speedoDialDisplay.setUnitsMph();
1369        } else {
1370            speedoDialDisplay.setUnitsKph();
1371        }
1372        speedoDialDisplay.update(currentSpeed);
1373        speedoDialDisplay.repaint();
1374    }
1375
1376    /**
1377     * Validate the users custom scale entry.
1378     */
1379    protected void getCustomScale() {
1380        if (selectedScale == -1) {
1381            try {
1382                customScale = Integer.parseUnsignedInt(customScaleField.getText());
1383            } catch (NumberFormatException ex) {
1384                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("CustomScaleDialog"),
1385                        Bundle.getMessage("CustomScaleTitle"), JmriJOptionPane.ERROR_MESSAGE);
1386            }
1387        }
1388    }
1389    //</editor-fold>
1390
1391    //<editor-fold defaultstate="collapsed" desc="Address Helper Functions">
1392    /**
1393     * Handle changing/setting the address.
1394     */
1395    private synchronized void changeOfAddress() {
1396        if (addrSelector.getAddress() != null) {
1397            locomotiveAddress = addrSelector.getAddress();
1398            setTitle();
1399        } else {
1400            locomotiveAddress = new DccLocoAddress(0, true);
1401            setTitle();
1402        }
1403    }
1404
1405    /**
1406     * Set the RosterEntry for this throttle.
1407     *
1408     * @param entry roster entry selected for throttle
1409     */
1410    public void setRosterEntry(RosterEntry entry) {
1411        rosterBox.setSelectedItem(entry);
1412        addrSelector.setAddress(entry.getDccLocoAddress());
1413        rosterEntry = entry;
1414        changeOfAddress();
1415    }
1416
1417    /**
1418     * Called when a RosterEntry is selected
1419     */
1420    private void rosterItemSelected() {
1421        if (rosterBox.getSelectedRosterEntries().length != 0) {
1422            setRosterEntry(rosterBox.getSelectedRosterEntries()[0]);
1423        }
1424    }
1425    //</editor-fold>
1426
1427    //<editor-fold defaultstate="collapsed" desc="Power Manager Helper Functions">
1428    /**
1429     * {@inheritDoc}
1430     * <p>
1431     * Handles property changes from the power manager.
1432     */
1433    @Override
1434    public void propertyChange(PropertyChangeEvent evt) {
1435        setPowerStatus();
1436    }
1437
1438    /**
1439     * Switches the track power on or off
1440     */
1441    private void setPowerStatus() {
1442        if (pm == null) {
1443            return;
1444        }
1445        if (pm.getPower() == PowerManager.ON) {
1446            trackPowerButton.setText(Bundle.getMessage("PowerDown"));
1447            //statusLabel.setText(Bundle.getMessage("StatTOn"));
1448        } else if (pm.getPower() == PowerManager.OFF) {
1449            trackPowerButton.setText(Bundle.getMessage("PowerUp"));
1450            //statusLabel.setText(Bundle.getMessage("StatTOff"));
1451        }
1452    }
1453
1454    /**
1455     * Called when the track power button is clicked to turn on or off track
1456     * power Allows user to power up and give time for sound decoder startup
1457     * sequence before running a profile
1458     */
1459    protected void trackPower() {
1460        try {
1461            if (pm.getPower() != PowerManager.ON) {
1462                pm.setPower(PowerManager.ON);
1463            } else {
1464                stopProfileAndSpeedMatch();
1465                pm.setPower(PowerManager.OFF);
1466            }
1467        } catch (JmriException e) {
1468            log.error("Exception during power on: {}", e.toString());
1469        }
1470    }
1471    //</editor-fold>
1472
1473    //<editor-fold defaultstate="collapsed" desc="Speed Profiling">
1474    javax.swing.Timer profileTimer = null;
1475
1476    /**
1477     * Start the speed profiling process
1478     */
1479    protected synchronized void startProfile() {
1480        if (locomotiveAddress.getNumber() > 0) {
1481            if (dirFwdButton.isSelected() || dirRevButton.isSelected()) {
1482                if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
1483                    profileTimer = new javax.swing.Timer(4000, e -> profileTimeout());
1484                    profileTimer.setRepeats(false);
1485                    profileState = ProfileState.WAIT_FOR_THROTTLE;
1486                    // Request a throttle
1487                    statusLabel.setText(Bundle.getMessage("StatReqThrottle"));
1488                    spFwd.clear();
1489                    spRev.clear();
1490                    if (dirFwdButton.isSelected()) {
1491                        profileDir = ProfileDirection.FORWARD;
1492                    } else {
1493                        profileDir = ProfileDirection.REVERSE;
1494                    }
1495                    resetGraphButton.setEnabled(false);
1496                    profileGraphPane.repaint();
1497                    profileTimer.start();
1498                    log.info("Requesting throttle");
1499                    boolean requestOK = jmri.InstanceManager.throttleManagerInstance().requestThrottle(locomotiveAddress, this, true);
1500                    if (!requestOK) {
1501                        log.error("Loco Address in use, throttle request failed.");
1502                    }
1503                }
1504            }
1505        } else {
1506            // Must have a non-zero address
1507            //profileAddressField.setBackground(Color.RED);
1508            log.error("Attempt to profile loco address 0");
1509        }
1510    }
1511
1512    /**
1513     * Profile timer timeout handler
1514     */
1515    protected synchronized void profileTimeout() {
1516        switch (profileState) {
1517            case WAIT_FOR_THROTTLE:
1518                tidyUp();
1519                log.error("Timeout waiting for throttle");
1520                statusLabel.setText(Bundle.getMessage("StatusTimeout"));
1521                break;
1522            case RUNNING:
1523                if (profileDir == ProfileDirection.FORWARD) {
1524                    spFwd.setPoint(profileStep, avSpeed);
1525                    statusLabel.setText(Bundle.getMessage("Fwd", profileStep));
1526                } else {
1527                    spRev.setPoint(profileStep, avSpeed);
1528                    statusLabel.setText(Bundle.getMessage("Rev", profileStep));
1529                }
1530                profileGraphPane.repaint();
1531                if (profileStep == 29) {
1532                    if ((profileDir == ProfileDirection.FORWARD)
1533                            && dirRevButton.isSelected()) {
1534                        // Start reverse profile
1535                        profileDir = ProfileDirection.REVERSE;
1536                        throttle.setIsForward(false);
1537                        profileStep = 0;
1538                        avClr();
1539                        statusLabel.setText(Bundle.getMessage("StatCreateRev"));
1540                    } else {
1541                        tidyUp();
1542                        statusLabel.setText(Bundle.getMessage("StatDone"));
1543                    }
1544                } else {
1545                    if (profileStep == 28) {
1546                        profileSpeed = 0.0F;
1547                    } else {
1548                        profileSpeed += throttleIncrement;
1549                    }
1550                    throttle.setSpeedSetting(profileSpeed);
1551                    profileStep += 1;
1552                    // adjust delay as we get faster and averaging is quicker
1553                    profileTimer.setDelay(7000 - range * 1000);
1554                }
1555                break;
1556            default:
1557                log.error("Unexpected profile timeout");
1558                profileTimer.stop();
1559                break;
1560        }
1561    }
1562    //</editor-fold>
1563
1564    //<editor-fold defaultstate="collapsed" desc="Speed Profiling and Speed Matching Cleanup">
1565    /**
1566     * Resets profiling and speed matching timers and other pertinent values and
1567     * releases the throttle and ops mode programmer
1568     * <p>
1569     * Called both when profiling or speed matching finish successfully or error
1570     * out
1571     */
1572    protected void tidyUp() {
1573        stopTimers();
1574
1575        //turn off power
1576        //Turning power off is bad for some systems, e.g. Digitrax
1577//      try {
1578//          pm.setPower(PowerManager.OFF);
1579//      } catch (JmriException e) {
1580//          log.error("Exception during power off: "+e.toString());
1581//      }
1582        //release throttle
1583        if (throttle != null) {
1584            throttle.setSpeedSetting(0.0F);
1585            InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this);
1586            throttle = null;
1587        }
1588
1589        //clean up speed matcher
1590        if (speedMatcher != null) {
1591            speedMatcher.stopSpeedMatcher();
1592            speedMatcher = null;
1593        }
1594
1595        resetGraphButton.setEnabled(true);
1596        progState = ProgState.IDLE;
1597        profileState = ProfileState.IDLE;
1598    }
1599
1600    /**
1601     * Stops the profiling and speed matching processes. Called by pressing
1602     * either the stop profile or stop speed matching buttons.
1603     */
1604    protected synchronized void stopProfileAndSpeedMatch() {
1605        if (profileState != ProfileState.IDLE || !speedMatcher.isSpeedMatcherIdle()) {
1606            if (profileState != ProfileState.IDLE) {
1607                log.info("Profiling/Speed Matching stopped by user");
1608            }
1609
1610            tidyUp();
1611        }
1612    }
1613
1614    /**
1615     * Stops profile and speed match timers
1616     */
1617    protected void stopTimers() {
1618        if (profileTimer != null) {
1619            profileTimer.stop();
1620        }
1621    }
1622    //</editor-fold>
1623
1624    //<editor-fold defaultstate="collapsed" desc="Notifiers">
1625    /**
1626     * Called when a throttle is found
1627     *
1628     * @param t the requested DccThrottle
1629     */
1630    @Override
1631    public synchronized void notifyThrottleFound(DccThrottle t) {
1632        stopTimers();
1633
1634        throttle = t;
1635        log.info("Throttle acquired");
1636        throttle.setSpeedStepMode(SpeedStepMode.NMRA_DCC_28);
1637        if (throttle.getSpeedStepMode() != SpeedStepMode.NMRA_DCC_28) {
1638            log.error("Failed to set 28 step mode");
1639            statusLabel.setText(Bundle.getMessage("ThrottleError28"));
1640            InstanceManager.throttleManagerInstance().releaseThrottle(throttle, this);
1641            return;
1642        }
1643
1644        // turn on power
1645        try {
1646            pm.setPower(PowerManager.ON);
1647        } catch (JmriException e) {
1648            log.error("Exception during power on: {}", e.toString());
1649            return;
1650        }
1651
1652        throttleIncrement = throttle.getSpeedIncrement();
1653
1654        if (profileState == ProfileState.WAIT_FOR_THROTTLE) {
1655            log.info("Starting profiling");
1656            profileState = ProfileState.RUNNING;
1657            // Start at step 0 with 28 step packets
1658            profileSpeed = 0.0F;
1659            profileStep = 0;
1660            throttle.setSpeedSetting(profileSpeed);
1661            if (profileDir == ProfileDirection.FORWARD) {
1662                throttle.setIsForward(true);
1663                statusLabel.setText(Bundle.getMessage("StatCreateFwd"));
1664            } else {
1665                throttle.setIsForward(false);
1666                statusLabel.setText(Bundle.getMessage("StatCreateRev"));
1667            }
1668            // using profile timer to trigger each next step
1669            profileTimer.setRepeats(true);
1670            profileTimer.start();
1671        } else {
1672            tidyUp();
1673        }
1674    }
1675
1676    /**
1677     * Called when a throttle could not be obtained
1678     *
1679     * @param address the requested address
1680     * @param reason  the reason the throttle could not be obtained
1681     */
1682    @Override
1683    public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
1684    }
1685
1686    /**
1687     * Called when we must decide to steal the throttle for the requested
1688     * address. Since this is a an automatically stealing implementation, the
1689     * throttle will be automatically stolen.
1690     */
1691    @Override
1692    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
1693        InstanceManager.throttleManagerInstance().responseThrottleDecision(address, this, DecisionType.STEAL);
1694    }
1695    //</editor-fold>
1696
1697    //<editor-fold defaultstate="collapsed" desc="Other Timers">
1698    javax.swing.Timer replyTimer = null;
1699    javax.swing.Timer displayTimer = null;
1700    javax.swing.Timer fastDisplayTimer = null;
1701
1702    /**
1703     * Starts the speedo hardware reply timer. Once we receive a speedoReply we
1704     * expect them regularly, at least once every 4 seconds.
1705     */
1706    protected void startReplyTimer() {
1707        replyTimer = new javax.swing.Timer(4000, e -> replyTimeout());
1708        replyTimer.setRepeats(true);     // refresh until stopped by dispose
1709        replyTimer.start();
1710    }
1711
1712    /**
1713     * Starts the timer used to update the speedometer display speed.
1714     */
1715    protected void startDisplayTimer() {
1716        displayTimer = new javax.swing.Timer(DISPLAY_UPDATE, e -> displayTimeout());
1717        displayTimer.setRepeats(true);     // refresh until stopped by dispose
1718        displayTimer.start();
1719    }
1720
1721    /**
1722     * Starts the timer used to update the speedometer display speed at a faster
1723     * rate.
1724     */
1725    protected void startFastDisplayTimer() {
1726        fastDisplayTimer = new javax.swing.Timer(DISPLAY_UPDATE / FAST_DISPLAY_RATIO, e -> fastDisplayTimeout());
1727        fastDisplayTimer.setRepeats(true);     // refresh until stopped by dispose
1728        fastDisplayTimer.start();
1729    }
1730
1731    //<editor-fold defaultstate="collapsed" desc="Timer Timeout Handlers">
1732    /**
1733     * Internal routine to reset the speed on a timeout.
1734     */
1735    protected synchronized void replyTimeout() {
1736        //log.debug("Timed out - display speed zero");
1737        targetSpeed = 0;
1738        avClr();
1739        oldSpeed = 0;
1740        showSpeed();
1741    }
1742
1743    /**
1744     * Internal routine to update the target speed for display
1745     */
1746    protected synchronized void displayTimeout() {
1747        //log.info("Display timeout");
1748        targetSpeed = avSpeed;
1749        incSpeed = (targetSpeed - currentSpeed) / FAST_DISPLAY_RATIO;
1750    }
1751
1752    /**
1753     * Internal routine to update the displayed speed
1754     */
1755    protected synchronized void fastDisplayTimeout() {
1756        //log.info("Display timeout");
1757        if (Math.abs(targetSpeed - currentSpeed) < Math.abs(incSpeed)) {
1758            currentSpeed = targetSpeed;
1759        } else {
1760
1761            currentSpeed += incSpeed;
1762        }
1763        if (currentSpeed < 0.01F) {
1764            currentSpeed = 0.0F;
1765        }
1766
1767        showSpeed();
1768
1769        if (speedMatcher != null) {
1770            speedMatcher.updateCurrentSpeed(currentSpeed);
1771        }
1772    }
1773
1774    /**
1775     * Timeout requesting a throttle.
1776     */
1777    protected synchronized void throttleTimeout() {
1778        jmri.InstanceManager.throttleManagerInstance().cancelThrottleRequest(locomotiveAddress, this);
1779        profileState = ProfileState.IDLE;
1780        log.error("Timeout waiting for throttle");
1781    }
1782    //</editor-fold>
1783    //</editor-fold>
1784
1785    //<editor-fold defaultstate="collapsed" desc="Programming Functions">
1786    /**
1787     * Starts reading the address (CVs 29 then 1 (short) or 17 and 18 (long))
1788     * using the service mode programmer
1789     */
1790    protected void readAddress() {
1791        if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
1792            progState = ProgState.READ29;
1793            statusLabel.setText(Bundle.getMessage("ProgRd29"));
1794            startRead("29");
1795        }
1796    }
1797
1798    /**
1799     * Starts reading the momentum CVs (CV 3 and 4) using the global programmer
1800     */
1801    protected void readMomentum() {
1802        if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
1803            progState = ProgState.READ3;
1804            statusLabel.setText(Bundle.getMessage("ProgReadAccel"));
1805            startRead("3");
1806        }
1807    }
1808
1809    /**
1810     * Starts writing the momentum CVs (CV 3 and 4) using the global programmer
1811     */
1812    protected void setMomentum() {
1813        if ((speedMatcher == null || speedMatcher.isSpeedMatcherIdle()) && (profileState == ProfileState.IDLE)) {
1814            progState = ProgState.WRITE3;
1815            int acceleration = accelerationSM.getNumber().intValue();
1816            statusLabel.setText(Bundle.getMessage("ProgSetAccel", acceleration));
1817            startWrite("3", acceleration);
1818        }
1819    }
1820
1821    /**
1822     * Starts reading a CV using the service mode programmer
1823     *
1824     * @param cv the CV
1825     */
1826    protected void startRead(String cv) {
1827        try {
1828            prog.readCV(cv, this);
1829        } catch (ProgrammerException e) {
1830            log.error("Exception reading CV {}", cv, e);
1831        }
1832    }
1833
1834    /**
1835     * STarts writing a CV using the global programmer
1836     *
1837     * @param cv    the CV
1838     * @param value the value to write to the CV
1839     */
1840    protected void startWrite(String cv, int value) {
1841        try {
1842            prog.writeCV(cv, value, this);
1843        } catch (ProgrammerException e) {
1844            log.error("Exception setting CV {} to {}", cv, value, e);
1845        }
1846    }
1847
1848    /**
1849     * Called when the programmer (ops mode or service mode) has completed its
1850     * operation
1851     *
1852     * @param value  Value from a read operation, or value written on a write
1853     * @param status Denotes the completion code. Note that this is a bitwise
1854     *               combination of the various states codes defined in this
1855     *               interface. (see ProgListener.java for possible values)
1856     */
1857    @Override
1858    public void programmingOpReply(int value, int status) {
1859        if (status == 0) {
1860            switch (progState) {
1861                case IDLE:
1862                    log.debug("unexpected reply in IDLE state");
1863                    break;
1864
1865                case READ29:
1866                    // Check extended address bit
1867                    if ((value & 0x20) == 0) {
1868                        progState = ProgState.READ1;
1869                        statusLabel.setText(Bundle.getMessage("ProgRdShort"));
1870                        startRead("1");
1871                    } else {
1872                        progState = ProgState.READ17;
1873                        statusLabel.setText(Bundle.getMessage("ProgRdExtended"));
1874                        startRead("17");
1875                    }
1876                    break;
1877
1878                case READ1:
1879                    readAddress = value;
1880                    //profileAddressField.setText(Integer.toString(profileAddress));
1881                    //profileAddressField.setBackground(Color.WHITE);
1882                    addrSelector.setAddress(new DccLocoAddress(readAddress, false));
1883                    changeOfAddress();
1884                    progState = ProgState.IDLE;
1885                    break;
1886
1887                case READ3:
1888                    accelerationSM.setValue(value);
1889                    progState = ProgState.READ4;
1890                    statusLabel.setText(Bundle.getMessage("ProgReadDecel"));
1891                    startRead("4");
1892                    break;
1893
1894                case READ4:
1895                    decelerationSM.setValue(value);
1896                    progState = ProgState.IDLE;
1897                    statusLabel.setText(Bundle.getMessage("ProgRdComplete"));
1898                    break;
1899
1900                case READ17:
1901                    readAddress = value;
1902                    progState = ProgState.READ18;
1903                    startRead("18");
1904                    break;
1905
1906                case READ18:
1907                    readAddress = (readAddress & 0x3f) * 256 + value;
1908                    //profileAddressField.setText(Integer.toString(profileAddress));
1909                    //profileAddressField.setBackground(Color.WHITE);
1910                    addrSelector.setAddress(new DccLocoAddress(readAddress, true));
1911                    changeOfAddress();
1912                    statusLabel.setText(Bundle.getMessage("ProgRdComplete"));
1913                    progState = ProgState.IDLE;
1914                    break;
1915
1916                case WRITE3:
1917                    progState = ProgState.WRITE4;
1918                    int deceleration = decelerationSM.getNumber().intValue();
1919                    statusLabel.setText(Bundle.getMessage("ProgSetDecel", deceleration));
1920                    startWrite("4", deceleration);
1921                    break;
1922
1923                case WRITE4:
1924                    statusLabel.setText(Bundle.getMessage("ProgRdComplete"));
1925                    progState = ProgState.IDLE;
1926                    break;
1927
1928                default:
1929                    progState = ProgState.IDLE;
1930                    log.warn("Unhandled read state: {}", progState);
1931                    break;
1932            }
1933        } else {
1934            // Error during programming
1935            log.error("Status not OK during {}: {}", progState.toString(), status);
1936            //profileAddressField.setText("Error");
1937            statusLabel.setText(Bundle.getMessage("ProgError"));
1938            progState = ProgState.IDLE;
1939            tidyUp();
1940        }
1941    }
1942    //</editor-fold>
1943
1944    //debugging logger
1945    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SpeedoConsoleFrame.class);
1946}