001package jmri.jmrit.roster.swing.speedprofile;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.awt.event.ActionEvent;
009import java.beans.PropertyChangeEvent;
010import java.beans.PropertyChangeListener;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Map;
014import java.util.TreeMap;
015
016import javax.swing.BorderFactory;
017import javax.swing.Box;
018import javax.swing.BoxLayout;
019import javax.swing.JButton;
020import javax.swing.JLabel;
021import javax.swing.JPanel;
022import javax.swing.JTextField;
023
024import jmri.Block;
025import jmri.DccThrottle;
026import jmri.InstanceManager;
027import jmri.Sensor;
028import jmri.SensorManager;
029import jmri.SpeedStepMode;
030import jmri.ThrottleListener;
031import jmri.jmrit.logix.WarrantPreferences;
032import jmri.jmrit.roster.Roster;
033import jmri.jmrit.roster.RosterEntry;
034import jmri.jmrit.roster.RosterSpeedProfile;
035import jmri.jmrit.roster.swing.RosterEntryComboBox;
036import jmri.profile.ProfileManager;
037import jmri.profile.ProfileUtils;
038import jmri.util.jdom.JDOMUtil;
039import jmri.util.swing.BeanSelectCreatePanel;
040import jmri.util.swing.JmriJOptionPane;
041
042import org.jdom2.Element;
043import org.jdom2.JDOMException;
044
045/**
046 * Set up and run automated speed table calibration.
047 * <p>
048 * Uses three sensors in a row (see diagram in window help):
049 * <ul>
050 * <li>Start sensor: Track where locomotive starts
051 * <li>Block sensor: Middle track. This time through this is used to measure the
052 * speed.
053 * <li>Finish sensor: Track where locomotive stops before repeating.
054 * </ul>
055 * The expected sequence is:
056 * <ul>
057 * <li>Start moving with Start sensor on, others off.
058 * <li>Block (middle) sensor goes active: startListener calls startTiming
059 * <li>Finish sensor goes active: finishListener calls stopCurrentSpeedStep
060 * <li>Block (middle) sensor goes inactive: startListener calls stopLoco, which
061 * stops loco after 2.5 seconds
062 * </ul>
063 * After a forward run, the Start and Finish sensors are swapped for a run in
064 * reverse.
065 */
066class SpeedProfilePanel extends jmri.util.swing.JmriPanel implements ThrottleListener {
067
068    public static final String XML_ROOT = "speedprofiler-config";
069    public static final String XML_NAMESPACE = "http://jmri.org/xml/schema/speedometer-3-9-3.xsd";
070    JButton profileButton = new JButton(Bundle.getMessage("ButtonStart"));
071    JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
072    JButton testButton = new JButton(Bundle.getMessage("ButtonTest"));
073    JButton testCancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
074    JButton clearNewDataButton = new JButton(Bundle.getMessage("ButtonClearNewData"));
075    JButton viewNewButton = new JButton(Bundle.getMessage("ButtonViewNew"));
076    JButton viewMergedButton = new JButton(Bundle.getMessage("ButtonViewMerged"));
077    JButton viewButton = new JButton(Bundle.getMessage("ButtonViewCurrent"));
078
079    JButton updateProfileButton = new JButton(Bundle.getMessage("ButtonUpdateProfile"));
080    JButton replaceProfileButton = new JButton(Bundle.getMessage("ButtonSaveProfile"));
081    JButton deleteProfileButton = new JButton(Bundle.getMessage("ButtonDeleteProfile"));
082    JButton saveDefaultsButton = new JButton(Bundle.getMessage("ButtonSaveDefaults"));
083    JTextField lengthField = new JTextField(10);
084    JTextField sensorDelay = new JTextField(5);
085    JTextField speedStepTest = new JTextField(5);
086    JTextField speedStepTestFwd = new JTextField(10);
087    JTextField speedStepTestRev = new JTextField(10);
088    JTextField speedStepFrom = new JTextField(5);
089    JTextField speedStepTo = new JTextField(5);
090    JTextField speedStepIncr = new JTextField(5);
091    JLabel warrentScaleLabel = new JLabel();
092
093    // Start or finish sensor
094    BeanSelectCreatePanel<Sensor> sensorAPanel = new BeanSelectCreatePanel<>(InstanceManager.sensorManagerInstance(), null);
095
096    // Finish or start sensor
097    BeanSelectCreatePanel<Sensor> sensorBPanel = new BeanSelectCreatePanel<>(InstanceManager.sensorManagerInstance(), null);
098
099    // Block sensor
100    BeanSelectCreatePanel<Block> blockCPanel = new BeanSelectCreatePanel<>(InstanceManager.getDefault(jmri.BlockManager.class), null);
101    BeanSelectCreatePanel<Sensor> sensorCPanel = new BeanSelectCreatePanel<>(InstanceManager.sensorManagerInstance(), null);
102
103    RosterEntryComboBox reBox = new RosterEntryComboBox();
104    SpeedProfileTable table = null;
105    boolean profile = false;
106    boolean test = false;
107    float testSpeedFwd = 0.0f;
108    float testSpeedRev = 0.0f;
109    boolean save = false;
110    boolean unmergedNewData = false;       // true if new data has been gathered but not merged to profile
111    boolean unsavedUpdatedProfile = false; // true if the roster profile has been updated but not saved
112
113    private JLabel sourceLabel;
114
115    public SpeedProfilePanel() {
116        JPanel main = new JPanel();
117
118        GridBagLayout gb = new GridBagLayout();
119        GridBagConstraints c = new GridBagConstraints();
120        main.setLayout(gb);
121
122        c.gridx = 0;
123        c.gridy = 0;
124        c.weightx = 1.0;
125        c.anchor = GridBagConstraints.CENTER;
126        JLabel label = new JLabel(Bundle.getMessage("LabelLengthOfBlock"));
127        addRow(main, gb, c, 0, label, lengthField);
128        label = new JLabel(Bundle.getMessage("LabelSensorDelay"));
129        addRow(main, gb, c, 1, label, sensorDelay);
130        label = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("LabelStartSensor")));
131        addRow(main, gb, c, 2, label, sensorAPanel);
132        label = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("LabelBlockSensor")));
133        addRow(main, gb, c, 3, label, sensorCPanel);
134        label = new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage("LabelFinishSensor")));
135        addRow(main, gb, c, 4, label, sensorBPanel);
136        label = new JLabel(Bundle.getMessage("LabelSelectRoster"));
137        JPanel left = makePadPanel(label);
138        JPanel right = makePadPanel(reBox);
139        addRow(main, gb, c, 5, left, right);
140        JPanel panelViews = new JPanel();
141        panelViews.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TitleView")));
142        panelViews.setLayout(new BoxLayout(panelViews, BoxLayout.LINE_AXIS));
143        panelViews.add(clearNewDataButton);
144        panelViews.add(viewNewButton);
145        panelViews.add(viewMergedButton);
146        panelViews.add(viewButton);
147        left = makePadPanel(panelViews);
148        JPanel panelProfileControl = new JPanel();
149        panelProfileControl.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("ButtonProfile")));
150        panelProfileControl.setLayout(new BoxLayout(panelProfileControl, BoxLayout.LINE_AXIS));
151        panelProfileControl.add(profileButton);
152        panelProfileControl.add(cancelButton);
153        right = makePadPanel(panelProfileControl);
154        addRow(main, gb, c, 6, left, right);
155
156        left = new JPanel();
157        left.add(Box.createRigidArea(new java.awt.Dimension(20, 10)));
158        left.setLayout(new BoxLayout(left, BoxLayout.PAGE_AXIS));
159        left.add(makeLabelPanel("LabelStartStep", speedStepFrom));
160        speedStepFrom.setToolTipText(Bundle.getMessage("StartStepToolTip"));
161        left.add(makeLabelPanel("LabelFinishStep", speedStepTo));
162        speedStepTo.setToolTipText(Bundle.getMessage("FinishStepToolTip"));
163        left.add(makeLabelPanel("LabelStepIncr", speedStepIncr));
164        speedStepIncr.setToolTipText(Bundle.getMessage("StepIncrToolTip"));
165        right = new JPanel();
166        addRow(main, gb, c, 7, left, right);
167
168        JPanel testDataPanel = new JPanel();
169        testDataPanel.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TestProfileData")));
170        testDataPanel.setLayout(new BoxLayout(testDataPanel, BoxLayout.LINE_AXIS));
171        testDataPanel.add(makeLabelPanel("LabelTestStep", speedStepTest));
172        speedStepTest.setToolTipText(Bundle.getMessage("StepTestToolTip"));
173        speedStepTestFwd.setEnabled(false);
174        testDataPanel.add(makeLabelPanel("LabelTestStepFwd", speedStepTestFwd));
175        speedStepTestFwd.setToolTipText(Bundle.getMessage("ForwardTestToolTip"));
176        speedStepTestRev.setEnabled(false);
177        testDataPanel.add(makeLabelPanel("LabelTestStepRev", speedStepTestRev));
178        speedStepTestRev.setToolTipText(Bundle.getMessage("ReverseTestToolTip"));
179        left = makePadPanel(testDataPanel);
180
181        JPanel testProfileControl = new JPanel();
182        testProfileControl.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TitleTestProfile")));
183        testProfileControl.setLayout(new BoxLayout(testProfileControl, BoxLayout.LINE_AXIS));
184        testProfileControl.add(testButton);
185        testProfileControl.add(testCancelButton);
186        right = makePadPanel(testProfileControl);
187
188        addRow(main, gb, c, 8, left, right);
189
190        c.fill = GridBagConstraints.HORIZONTAL;
191        c.gridx = 0;
192        c.gridy = 9;
193        c.gridwidth = 2;
194        sourceLabel = new JLabel("   ");
195        sourceLabel.setBackground(Color.white);
196        left = makePadPanel(sourceLabel);
197        gb.setConstraints(left, c);
198        main.add(left);
199
200        WarrantPreferences preferences = WarrantPreferences.getDefault();
201        warrentScaleLabel.setText(Bundle.getMessage("LabelLayoutScale") + " 1:" + Float.toString(preferences.getLayoutScale()));
202        warrentScaleLabel.setBackground(Color.white);
203        warrentScaleLabel.setToolTipText(Bundle.getMessage("LayoutScaleHint"));
204        left = makePadPanel(warrentScaleLabel);
205        c.gridy = 11;
206        gb.setConstraints(left, c);
207        main.add(left);
208
209        c.gridy = 12;
210        JPanel southBtnPanel = new JPanel();
211        southBtnPanel.add(clearNewDataButton);
212        southBtnPanel.add(updateProfileButton);
213        southBtnPanel.add(replaceProfileButton);
214        southBtnPanel.add(deleteProfileButton);
215        southBtnPanel.add(saveDefaultsButton);
216        main.add(southBtnPanel, c);
217
218        add(main, BorderLayout.CENTER);
219
220        profileButton.addActionListener((ActionEvent e) -> {
221            profile = true;
222            setupProfile();
223        });
224        cancelButton.addActionListener((ActionEvent e) -> {
225            cancelButton();
226        });
227        testButton.addActionListener((ActionEvent e) -> {
228            test = true;
229            testButton();
230        });
231        testCancelButton.addActionListener((ActionEvent e) -> {
232            cancelButton();
233        });
234        viewButton.addActionListener((ActionEvent e) -> {
235            viewRosterProfileData();
236        });
237
238        viewNewButton.addActionListener((ActionEvent e) -> {
239            viewNewProfileData();
240        });
241
242        saveDefaultsButton.addActionListener((ActionEvent e) -> {
243            doSaveSettings();
244        });
245        clearNewDataButton.addActionListener((ActionEvent e) -> {
246            clearNewData();
247        });
248        viewMergedButton.addActionListener((ActionEvent e) -> {
249            viewMergedData();
250        });
251        updateProfileButton.addActionListener((ActionEvent e) -> {
252            updateSpeedProfileWithResults();
253        });
254        replaceProfileButton.addActionListener((ActionEvent e) -> {
255            removeSpeedProfile();
256            updateSpeedProfileWithResults();
257        });
258        deleteProfileButton.addActionListener((ActionEvent e) -> {
259            removeSpeedProfile();
260        });
261
262        setButtonStates(true);
263        // Attempt to reload last values */
264        doLoad();
265
266    }
267
268    static void addRow(JPanel main, GridBagLayout gb, GridBagConstraints c, int row, Component left, Component right) {
269        c.gridx = 0;
270        c.gridy = row;
271        gb.setConstraints(left, c);
272        main.add(left);
273        c.gridx = 1;
274        gb.setConstraints(right, c);
275        main.add(right);
276    }
277
278    static JPanel makePadPanel(Component comp) {
279        JPanel panel = new JPanel();
280        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
281        panel.add(Box.createRigidArea(new java.awt.Dimension(20, 20)));
282        panel.add(comp);
283        return panel;
284    }
285
286    static JPanel makeLabelPanel(String text, Component comp) {
287        JPanel panel = new JPanel();
288        panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
289        panel.add(new JLabel(Bundle.getMessage("MakeLabel", Bundle.getMessage(text))));
290        panel.add(comp);
291        return panel;
292    }
293
294    SensorDetails sensorA;
295    SensorDetails sensorB;
296    RosterEntry re;
297    DccThrottle t;
298    int finishSpeedStep;
299    protected int stepIncr;
300    protected int profileStep;
301    protected float profileSpeed;
302    protected float profileIncrement;
303    protected int profileSpeedStepMode;
304    protected float profileSensorDelay;
305    protected float profileBlockLength;
306    RosterSpeedProfile rosterSpeedProfile;
307
308    protected float profileSpeedAtStart;
309    
310    void setupProfile() {
311        String text;
312        finishSpeedStep = 0;
313        stepIncr = 1;
314        profileStep = 1;
315        profileSensorDelay = 0.0f;
316        try {
317            profileBlockLength = Float.parseFloat(lengthField.getText());
318        } catch (Exception e) {
319            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorLengthInvalid"));
320            return;
321        }
322        text = sensorDelay.getText();
323        if (text != null && text.trim().length() > 0) {
324            try {
325                profileSensorDelay = Float.parseFloat(sensorDelay.getText());
326            } catch (Exception e) {
327                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorDelayInvalid"));
328                return;
329            }
330        }
331        setButtonStates(false);
332        if (sensorA == null) {
333            try {
334                sensorA = new SensorDetails(sensorAPanel.getNamedBean());
335            } catch (Exception e) {
336                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelStartSensor")));
337                setButtonStates(true);
338                return;
339            }
340        } else {
341            Sensor tmpSen = null;
342            try {
343                tmpSen = sensorAPanel.getNamedBean();
344            } catch (Exception e) {
345                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelStartSensor")));
346                setButtonStates(true);
347                return;
348            }
349            if (tmpSen != sensorA.getSensor()) {
350                sensorA.resetDetails();
351                sensorA = new SensorDetails(tmpSen);
352            }
353        }
354        if (sensorB == null) {
355            try {
356                sensorB = new SensorDetails(sensorBPanel.getNamedBean());
357            } catch (Exception e) {
358                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelFinishSensor")));
359                setButtonStates(true);
360                return;
361            }
362
363        } else {
364            Sensor tmpSen = null;
365            try {
366                tmpSen = sensorBPanel.getNamedBean();
367            } catch (Exception e) {
368                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelFinishSensor")));
369                setButtonStates(true);
370                return;
371            }
372            if (tmpSen != sensorB.getSensor()) {
373                sensorB.resetDetails();
374                sensorB = new SensorDetails(tmpSen);
375            }
376        }
377        if (middleBlockSensor == null) {
378            try {
379                middleBlockSensor = new SensorDetails(sensorCPanel.getNamedBean());
380            } catch (Exception e) {
381                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelBlockSensor")));
382                setButtonStates(true);
383                return;
384            }
385        } else {
386            Sensor tmpSen = null;
387            try {
388                tmpSen = sensorCPanel.getNamedBean();
389            } catch (Exception e) {
390                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSensorNotFound", Bundle.getMessage("LabelBlockSensor")));
391                setButtonStates(true);
392                return;
393            }
394            if (tmpSen != middleBlockSensor.getSensor()) {
395                middleBlockSensor.resetDetails();
396                middleBlockSensor = new SensorDetails(tmpSen);
397            }
398        }
399        if (reBox.getSelectedRosterEntries().length == 0) {
400            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoRosterSelected"));
401            log.warn("No Roster Entry selected.");
402            setButtonStates(true);
403            return;
404        }
405        text = speedStepFrom.getText();
406        if (text != null && text.trim().length() > 0) {
407            try {
408                profileStep = Integer.parseInt(text);
409                if (!speedStepNumOK(profileStep, "LabelStartStep")) {
410                    setButtonStates(true);
411                    return;
412                }
413            } catch (Exception e) {
414                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSpeedStep", Bundle.getMessage("LabelStartStep")));
415                setButtonStates(true);
416                return;
417            }
418        }
419        text = speedStepTo.getText();
420        if (text != null && text.trim().length() > 0) {
421            try {
422                finishSpeedStep = Integer.parseInt(text);
423                if (!speedStepNumOK(finishSpeedStep, "LabelFinishStep")) {
424                    setButtonStates(true);
425                    return;
426                }
427            } catch (Exception e) {
428                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSpeedStep", Bundle.getMessage("LabelFinishStep")));
429                setButtonStates(true);
430                return;
431            }
432        }
433        text = speedStepIncr.getText();
434        if (text != null && text.trim().length() > 0) {
435            try {
436                stepIncr = Integer.parseInt(text);
437                if (!speedStepNumOK(stepIncr, "LabelStepIncr")) {
438                    setButtonStates(true);
439                    return;
440                }
441            } catch (Exception e) {
442                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSpeedStep", Bundle.getMessage("LabelStepIncr")));
443                setButtonStates(true);
444                return;
445            }
446        }
447
448        throttleState = 0;
449        re = reBox.getSelectedRosterEntries()[0];
450        boolean ok = InstanceManager.throttleManagerInstance().requestThrottle(re, this, true); // we have a mechanism for steal / share
451        if (!ok) {
452            log.warn("Throttle for locomotive {} could not be set up.", re.getId());
453            setButtonStates(true);
454            return;
455        }
456        // Wait for throttle be correct and then run the profile
457        jmri.util.ThreadingUtil.newThread(new Runnable() {
458                @Override
459                public void run() {
460                    int count = 0;
461                    int trys = 10;
462                    while (throttleState == 0 && count < trys) {
463                        try {
464                            Thread.sleep(1000);
465                            log.debug("Wait");
466                        } catch (Exception ex) {
467                            log.warn("Throttle for locomotive {} could not be set up.", re.getId());
468                            setButtonStates(true);
469                            return;
470                        }
471                        count++;
472                    }
473                    log.debug("Run");
474                    if (throttleState != 1) {
475                        log.warn("No Throttle, Aborting");
476                        setButtonStates(true);
477                        return;
478                    }
479                    runProfile();
480                }
481            }).start();
482
483    }
484
485    boolean speedStepNumOK(int num, String step) {
486        if (num < 1 || num > 126) {
487            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorSpeedStep", Bundle.getMessage(step)));
488            setButtonStates(true);
489            return false;
490        }
491        return true;
492    }
493
494    javax.swing.Timer overRunTimer = null;
495
496    private volatile int throttleState = 0;   // zero waiting, -1 no throttle (message already shown), 1 
497            
498    @Override
499    public void notifyThrottleFound(DccThrottle _throttle) {
500        t = _throttle;
501        if (t == null) {
502            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorThrottleNotFound"));
503            log.warn("null throttle returned for train {} during automatic initialization.", re.getId());
504            setButtonStates(true);
505            throttleState = -1;
506            return;
507        }
508        if (log.isDebugEnabled()) {
509            log.debug("throttle address = {}", t.getLocoAddress().toString());
510        }
511        throttleState = 1;
512    }
513
514    private void runProfile() {
515        SpeedStepMode speedStepMode = t.getSpeedStepMode();
516        profileIncrement = t.getSpeedIncrement();
517        profileSpeedStepMode = speedStepMode.numSteps;
518        if (finishSpeedStep <= 0) {
519            finishSpeedStep = profileSpeedStepMode;
520        }
521
522        log.debug("Speed step mode {}", profileSpeedStepMode);
523        profileSpeed = profileIncrement * profileStep;
524
525        profileSpeedAtStart = profileSpeed;
526        
527        if (profile) {
528            startSensor = middleBlockSensor.getSensor();
529            finishSensor = sensorB.getSensor();
530            startListener = new PropertyChangeListener() {
531                @Override
532                public void propertyChange(PropertyChangeEvent e) {
533                    if (e.getPropertyName().equals("KnownState")) {
534                        if (((Integer) e.getNewValue()) == Sensor.ACTIVE) {
535                            startTiming();
536                        }
537                        if (((Integer) e.getNewValue()) == Sensor.INACTIVE) {
538                            stopLoco();
539                        }
540                    }
541                }
542            };
543            finishListener = new PropertyChangeListener() {
544                @Override
545                public void propertyChange(PropertyChangeEvent e) {
546                    if (e.getPropertyName().equals("KnownState")) {
547                        if (((Integer) e.getNewValue()) == Sensor.ACTIVE) {
548                            stopCurrentSpeedStep();
549                        }
550                    }
551                }
552            };
553
554            isForward = true;
555            startProfile();
556        } else {
557            // Speed test.
558            // Once back and forth
559            stepIncr = 1;
560            profileStep = Integer.parseInt(speedStepTest.getText());
561            finishSpeedStep = profileStep;
562            profileSpeed = profileIncrement * profileStep;
563            startSensor = middleBlockSensor.getSensor();
564            finishSensor = sensorB.getSensor();
565            startListener = new PropertyChangeListener() {
566                @Override
567                public void propertyChange(PropertyChangeEvent e) {
568                    if (e.getPropertyName().equals("KnownState")) {
569                        if (((Integer) e.getNewValue()) == Sensor.ACTIVE) {
570                            startTiming();
571                        }
572                        if (((Integer) e.getNewValue()) == Sensor.INACTIVE) {
573                            stopLoco();
574                        }
575                    }
576                }
577            };
578            finishListener = new PropertyChangeListener() {
579                @Override
580                public void propertyChange(PropertyChangeEvent e) {
581                    if (e.getPropertyName().equals("KnownState")) {
582                        if (((Integer) e.getNewValue()) == Sensor.ACTIVE) {
583                            stopCurrentSpeedStep();
584                        }
585                    }
586                }
587            };
588
589            isForward = true;
590            startProfile();
591        }
592    }
593
594    void setButtonStates(boolean state) {
595        cancelButton.setEnabled(!state);
596        profileButton.setEnabled(state);
597        testButton.setEnabled(state);
598        testCancelButton.setEnabled(!state);
599        viewButton.setEnabled(state);
600        deleteProfileButton.setEnabled(state);
601        if (state && speeds.size() > 0) {
602            viewNewButton.setEnabled(true);
603            viewMergedButton.setEnabled(true);
604            replaceProfileButton.setEnabled(true);
605            updateProfileButton.setEnabled(true);
606            clearNewDataButton.setEnabled(true);
607        } else {
608            viewNewButton.setEnabled(false);
609            viewMergedButton.setEnabled(false);
610            replaceProfileButton.setEnabled(false);
611            updateProfileButton.setEnabled(false);
612            clearNewDataButton.setEnabled(false);
613        }
614        if (state) {
615            sourceLabel.setText("   ");
616            profile = false;
617            test = false;
618        }
619        if (sensorA != null) {
620            sensorA.resetDetails();
621        }
622        if (sensorB != null) {
623            sensorB.resetDetails();
624        }
625        if (middleBlockSensor != null) {
626            middleBlockSensor.resetDetails();
627        }
628    }
629
630    @Override
631    public void notifyFailedThrottleRequest(jmri.LocoAddress address, String reason) {
632        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorFailThrottleRequest"));
633        log.error("Throttle request for {} failed because {}", address, reason);
634        setButtonStates(true);
635        throttleState = -1;
636    }
637    
638    /**
639    * Profiling on a stolen or shared throttle is invalid
640    * <p>
641    * {@inheritDoc}
642    */
643    @Override
644    public void notifyDecisionRequired(jmri.LocoAddress address, DecisionType question) {
645        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoStealing"));
646        InstanceManager.throttleManagerInstance().cancelThrottleRequest(address, this);
647        setButtonStates(true);
648        throttleState = -1;
649    }
650
651    PropertyChangeListener startListener = null;
652    PropertyChangeListener finishListener = null;
653    PropertyChangeListener middleListener = null;
654
655    Sensor startSensor;
656    Sensor finishSensor;
657    SensorDetails middleBlockSensor;
658
659    void startProfile() {
660        stepCalculated = false;
661        sourceLabel.setText(Bundle.getMessage("StatusLabelNextRun"));
662        if (isForward) {
663            finishSensor = sensorB.getSensor();
664        } else {
665            finishSensor = sensorA.getSensor();
666        }
667        startSensor = middleBlockSensor.getSensor();
668        startSensor.addPropertyChangeListener(startListener);
669        finishSensor.addPropertyChangeListener(finishListener);
670        t.setIsForward(!isForward);
671        // this switching back and forward helps if the throttle was stolen.
672        // the sleeps are needed as some systems dont like a speed setting right after a direction setting.
673        // If we had guarenteed access to the Dispatcher frame we could use
674        // Thread.sleep(InstanceManager.getDefault(DispatcherFrame.class).getMinThrottleInterval() * 2)
675        try {
676            Thread.sleep(250);
677        } catch (InterruptedException e) {
678            // Nothing I can do.
679        }
680
681        t.setIsForward(isForward);
682        try {
683            Thread.sleep(250);
684        } catch (InterruptedException e) {
685            // Nothing I can do.
686        }
687
688        log.debug("Set speed to [{}] isForward [{}] Increment [{}] Step [{}] SpeedStepMode [{}]",
689                profileSpeed, isForward, profileIncrement, profileStep, profileSpeedStepMode);
690        t.setSpeedSetting(profileSpeed);
691        sourceLabel.setText(Bundle.getMessage("StatusLabelBlockToGoActive"));
692    }
693
694    boolean isForward = true;
695
696    void startTiming() {
697        startTime = System.nanoTime();
698        sourceLabel.setText(Bundle.getMessage("StatusLabelCurrentRun",
699                (isForward ? Bundle.getMessage("LabelTestStepFwd") : Bundle.getMessage("LabelTestStepRev")),
700                profileStep, finishSpeedStep));
701    }
702
703    boolean stepCalculated = false;
704
705    void stopCurrentSpeedStep() {
706        finishTime = System.nanoTime();
707        stepCalculated = true;
708        finishSensor.removePropertyChangeListener(finishListener);
709        sourceLabel.setText(Bundle.getMessage("StatusLabelCalculating"));
710
711        if (profileSpeed/2 > profileSpeedAtStart) {
712            t.setSpeedSetting(profileSpeed / 2);
713        } else {
714            t.setSpeedSetting(profileSpeedAtStart);
715        }
716        
717        calculateSpeed();
718        sourceLabel.setText(Bundle.getMessage("StatusLabelWaitingToClear"));
719    }
720
721    void stopLoco() {
722
723        if (!stepCalculated) {
724            return;
725        }
726
727        startSensor.removePropertyChangeListener(startListener);
728        finishSensor.removePropertyChangeListener(finishListener);
729
730        isForward = !isForward;
731        if (isForward) {
732            profileSpeed = profileIncrement * stepIncr + profileSpeed;
733            profileStep += stepIncr;
734        }
735
736        if (profileStep > finishSpeedStep) {
737            t.setSpeedSetting(0.0f);
738            if (!profile) {
739                // there are only the 2 fields on screen to be updated after a test
740                speedStepTestFwd.setText(RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(testSpeedFwd));
741                speedStepTestRev.setText(RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(testSpeedRev));
742            }
743            releaseThrottle();
744            //updateSpeedProfileWithResults();
745            setButtonStates(true);
746            return;
747        }
748        // Loco may have been brought to half-speed in stopCurrentSpeedStep, so wait for that to take effect then stop & restart
749        javax.swing.Timer stopTimer = new javax.swing.Timer(2500, new java.awt.event.ActionListener() {
750            @Override
751            public void actionPerformed(java.awt.event.ActionEvent e) {
752
753                // finally command the stop
754                t.setSpeedSetting(0.0f);
755                // and a second later, restart going the other way
756                javax.swing.Timer restartTimer = new javax.swing.Timer(1000, new java.awt.event.ActionListener() {
757                    @Override
758                    public void actionPerformed(java.awt.event.ActionEvent e) {
759                        startProfile();
760                    }
761                });
762                restartTimer.setRepeats(false);
763                restartTimer.start();
764            }
765        });
766        stopTimer.setRepeats(false);
767        stopTimer.start();
768    }
769
770    void calculateSpeed() {
771        float duration = (((float) (finishTime - startTime)) / 1000000000); // convert to seconds
772        duration = duration - (profileSensorDelay / 1000); // allow for time differences between sensor delays
773        float speed = profileBlockLength / duration;
774        log.debug("Step: {} duration: {} length: {} speed: {}",
775                profileStep, duration, profileBlockLength, speed);
776
777
778        if (profile) {
779            // save results to table
780            int iSpeedStep = Math.round(profileSpeed * 1000);
781            if (!speeds.containsKey(iSpeedStep)) {
782                speeds.put(iSpeedStep, new SpeedStep());
783            }
784            SpeedStep ss = speeds.get(iSpeedStep);
785            if (isForward) {
786                ss.setForwardSpeed(speed);
787            } else {
788                ss.setReverseSpeed(speed);
789            }
790            save = true;
791        } else {
792            // testing, save results to the 2 fields.
793            if (isForward) {
794                testSpeedFwd = speed;
795            } else {
796                testSpeedRev = speed;
797            }
798        }
799    }
800
801    /**
802     * Merge the new data into the existing speedprofile, or create if not
803     * current, and save. Clear new data.
804     */
805    void updateSpeedProfileWithResults() {
806        cancelButton();
807        RosterSpeedProfile rosterSpeedProfile = re.getSpeedProfile();
808        if (rosterSpeedProfile == null) {
809            rosterSpeedProfile = new RosterSpeedProfile(re);
810            re.setSpeedProfile(rosterSpeedProfile);
811        }
812        for (Map.Entry<Integer, SpeedStep> entry : speeds.entrySet()) {
813            rosterSpeedProfile.setSpeed(entry.getKey(), entry.getValue().getForwardSpeed(), entry.getValue().getReverseSpeed());
814        }
815        re.updateFile();
816        Roster.getDefault().writeRoster();
817        clearNewData();
818        setButtonStates(true);
819        save = false;
820    }
821
822    /**
823     * Merge the current profile with the new data in a temp area and show.
824     */
825    void viewMergedData() {
826        // create a new temporay rosterspeedentry
827        RosterEntry tmpRe = new RosterEntry();
828        RosterSpeedProfile tmpRsp = new RosterSpeedProfile(tmpRe);
829        // reference the current one.
830        RosterSpeedProfile rosterSpeedProfile = re.getSpeedProfile();
831        //copy across the profile data
832        for (Integer i : rosterSpeedProfile.getProfileSpeeds().keySet()) {
833            tmpRsp.setSpeed(i, rosterSpeedProfile.getProfileSpeeds().get(i).getForwardSpeed(), rosterSpeedProfile.getProfileSpeeds().get(i).getReverseSpeed());
834        }
835        //copy, merge the newdata speed points
836        for (Map.Entry<Integer, SpeedStep> entry : speeds.entrySet()) {
837            tmpRsp.setSpeed(entry.getKey(), entry.getValue().getForwardSpeed(), entry.getValue().getReverseSpeed());
838        }
839        // show, its a bit convoluted, to get the speed table
840        // we have to set the new profile in the tmp rosterentry
841        // and ask for it back as a speedtable.
842        tmpRe.setSpeedProfile(tmpRsp);
843        RosterSpeedProfile tmpSp = tmpRe.getSpeedProfile();
844        if (tmpSp != null) {
845            if (table != null) {
846                table.dispose();
847            }
848            table = new SpeedProfileTable(tmpSp, tmpRe.getId());
849            table.setVisible(true);
850            return;
851        }
852        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoSpeedProfile"));
853        setButtonStates(true);
854    }
855
856    void clearNewData() {
857        speeds.clear();
858    }
859
860    void removeSpeedProfile() {
861        cancelButton();
862        RosterSpeedProfile rosterSpeedProfile = re.getSpeedProfile();
863        if (rosterSpeedProfile != null) {
864            rosterSpeedProfile.clearCurrentProfile();
865        }
866        re.updateFile();
867        Roster.getDefault().writeRoster();
868        save = false;
869    }
870
871    /**
872     * View the new data collected we create a dummy entry and file with
873     * collected data
874     */
875    void viewNewProfileData() {
876        RosterEntry tmpRe = new RosterEntry();
877        RosterSpeedProfile rosterSpeedProfile = tmpRe.getSpeedProfile();
878        if (rosterSpeedProfile == null) {
879            rosterSpeedProfile = new RosterSpeedProfile(tmpRe);
880            tmpRe.setSpeedProfile(rosterSpeedProfile);
881        }
882        for (Map.Entry<Integer, SpeedStep> entry : speeds.entrySet()) {
883            rosterSpeedProfile.setSpeed(entry.getKey(), entry.getValue().getForwardSpeed(), entry.getValue().getReverseSpeed());
884        }
885
886        RosterSpeedProfile speedProfile = tmpRe.getSpeedProfile();
887        if (speedProfile != null) {
888            if (table != null) {
889                table.dispose();
890            }
891            table = new SpeedProfileTable(speedProfile, tmpRe.getId());
892            table.setVisible(true);
893            return;
894        }
895
896        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoSpeedProfile"));
897        setButtonStates(true);
898    }
899
900    /**
901     * View the current speedprofile table entrys
902     */
903    void viewRosterProfileData() {
904        if (reBox.getSelectedRosterEntries().length == 0) {
905            JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoRosterSelected"));
906            setButtonStates(true);
907            return;
908        }
909        re = reBox.getSelectedRosterEntries()[0];
910        if (re != null) {
911            RosterSpeedProfile speedProfile = re.getSpeedProfile();
912            if (speedProfile != null) {
913                if (table != null) {
914                    table.dispose();
915                }
916                table = new SpeedProfileTable(re.getSpeedProfile(), re.getId());
917                table.setVisible(true);
918                return;
919            }
920        }
921        JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("ErrorNoSpeedProfile"));
922        setButtonStates(true);
923    }
924
925    /**
926     * If we have a throttle, set speed zero and release
927     */
928    private void releaseThrottle() {
929        if (t != null) {
930            log.debug("t not null");
931             t.setSpeedSetting(0.0f);
932             try {
933                 Thread.sleep(250);
934             } catch (InterruptedException e) {
935                 log.warn("Wait interupted, release throttle immediatlely");
936             }
937             log.debug("releaseing[{}]", t.getLocoAddress().getNumber());
938             InstanceManager.throttleManagerInstance().releaseThrottle(t, this);
939             t = null;
940         }
941    }
942
943    /**
944     * We are canceling, release throttle, reset sensors.
945     */
946
947    void cancelButton() {
948        releaseThrottle();
949        if (t != null) {
950            t.setSpeedSetting(0.0f);
951            try {
952                Thread.sleep(250);
953            } catch (InterruptedException e) {
954                // Nothing I can do.
955            }
956
957            InstanceManager.throttleManagerInstance().releaseThrottle(t, this);
958            t = null;
959        }
960        if (startSensor != null) {
961            startSensor.removePropertyChangeListener(startListener);
962        }
963        if (finishSensor != null) {
964            finishSensor.removePropertyChangeListener(finishListener);
965        }
966        if (middleListener != null) {
967            middleBlockSensor.getSensor().removePropertyChangeListener(middleListener);
968        }
969        setButtonStates(true);
970    }
971
972    void testButton() {
973        // TODO Should also test that the step is no greater than those available on the throttle.
974        try {
975            Integer.parseInt(speedStepTest.getText());
976        } catch (NumberFormatException e) {
977            JmriJOptionPane.showMessageDialog(this,
978                    Bundle.getMessage("ErrorSpeedStep", Bundle.getMessage("LabelTestStep")));
979            return;
980        }
981        setupProfile();
982
983    }
984
985    void stopTrainTest() {
986        int sectionlength = Integer.parseInt(lengthField.getText());
987        re.getSpeedProfile().changeLocoSpeed(t, sectionlength, 0.0f);
988        setButtonStates(true);
989        startSensor.removePropertyChangeListener(startListener);
990    }
991
992    long startTime;
993    long finishTime;
994
995    ArrayList<Double> forwardOverRuns = new ArrayList<>();
996    ArrayList<Double> reverseOverRuns = new ArrayList<>();
997
998    JPanel update;
999
1000    static class SensorDetails {
1001
1002        Sensor sensor = null;
1003        long inactiveDelay = 0;
1004        long activeDelay = 0;
1005        boolean usingGlobal = false;
1006
1007        SensorDetails(Sensor sen) {
1008            sensor = sen;
1009            usingGlobal = sen.getUseDefaultTimerSettings();
1010            activeDelay = sen.getSensorDebounceGoingActiveTimer();
1011            inactiveDelay = sen.getSensorDebounceGoingInActiveTimer();
1012        }
1013
1014        void setupSensor() {
1015            sensor.setUseDefaultTimerSettings(false);
1016            sensor.setSensorDebounceGoingActiveTimer(0);
1017            sensor.setSensorDebounceGoingInActiveTimer(0);
1018        }
1019
1020        void resetDetails() {
1021            sensor.setUseDefaultTimerSettings(usingGlobal);
1022            sensor.setSensorDebounceGoingActiveTimer(activeDelay);
1023            sensor.setSensorDebounceGoingInActiveTimer(inactiveDelay);
1024        }
1025
1026        Sensor getSensor() {
1027            return sensor;
1028        }
1029
1030    }
1031
1032    TreeMap<Integer, SpeedStep> speeds = new TreeMap<>();
1033    
1034    static class SpeedStep {
1035
1036        float forward = 0.0f;
1037        float reverse = 0.0f;
1038
1039        SpeedStep() {
1040        }
1041
1042        void setForwardSpeed(float speed) {
1043            forward = speed;
1044        }
1045
1046        void setReverseSpeed(float speed) {
1047            reverse = speed;
1048        }
1049
1050        float getForwardSpeed() {
1051            return forward;
1052        }
1053
1054        float getReverseSpeed() {
1055            return reverse;
1056        }
1057    }
1058
1059    /*
1060     *  Start of code for saving and restoring the settings
1061     */
1062
1063    /**
1064     * Save current sensor and block information to file
1065     */
1066    private void doSaveSettings() {
1067        log.debug("Start storing SpeedProfiler settings...");
1068
1069        // Create root element
1070        Element root = new Element(XML_ROOT, XML_NAMESPACE);
1071
1072        Element values;
1073
1074        // Store configuration
1075        root.addContent(values = new Element("configuration"));
1076        if (lengthField.getText().length() > 0) {
1077            values.addContent(new Element("length").addContent(lengthField.getText()));
1078        }
1079        if (sensorDelay.getText().length() > 0) {
1080            values.addContent(new Element("sensordelay").addContent(sensorDelay.getText()));
1081        }
1082        // Store values
1083        //if (sensorAPanel.getNamedBean(). > 0) {
1084        // Create sensors element
1085        root.addContent(values = new Element("sensors"));
1086
1087        // Store start sensor
1088        Element e = new Element("sensor");
1089        e.addContent(new Element("sensorname").addContent("sensorAPanel"));
1090        e.addContent(new Element("sensorvalue").addContent(sensorAPanel.getDisplayName()));
1091        values.addContent(e);
1092        e = new Element("sensor");
1093        e.addContent(new Element("sensorname").addContent("sensorBPanel"));
1094        e.addContent(new Element("sensorvalue").addContent(sensorBPanel.getDisplayName()));
1095        values.addContent(e);
1096        e = new Element("sensor");
1097        e.addContent(new Element("sensorname").addContent("sensorCPanel"));
1098        e.addContent(new Element("sensorvalue").addContent(sensorCPanel.getDisplayName()));
1099        values.addContent(e);
1100        root.addContent(values = new Element("steps"));
1101        if (speedStepFrom.getText().length() > 0) {
1102            values.addContent(new Element("speedStepFrom").addContent(speedStepFrom.getText()));
1103        }
1104        if (speedStepTo.getText().length() > 0) {
1105            values.addContent(new Element("speedStepTo").addContent(speedStepTo.getText()));
1106        }
1107        if (speedStepIncr.getText().length() > 0) {
1108            values.addContent(new Element("speedStepIncr").addContent(speedStepIncr.getText()));
1109        }
1110
1111        try {
1112            ProfileUtils.getAuxiliaryConfiguration(ProfileManager.getDefault().getActiveProfile())
1113                    .putConfigurationFragment(JDOMUtil.toW3CElement(root), true);
1114        } catch (JDOMException ex) {
1115            log.error("Unable to create create XML", ex);
1116        }
1117
1118        log.debug("...done");
1119    }
1120
1121    /**
1122     * Load the Block and sensor information previously saved.
1123     */
1124    private void doLoad() {
1125        Element root;
1126
1127        log.debug("Check if there's anything to load");
1128        try {
1129            root = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(ProfileManager.getDefault().getActiveProfile())
1130                    .getConfigurationFragment(XML_ROOT, XML_NAMESPACE, true));
1131        } catch (NullPointerException ex) {
1132            // expected if never saved before
1133            log.debug("Nothing to load");
1134            return;
1135        }
1136
1137        log.debug("Start loading SpeedProfiler settings...");
1138
1139        // First read configuration
1140        if (root.getChild("configuration") != null) {
1141            List<Element> l = root.getChild("configuration").getChildren();
1142            if (log.isDebugEnabled()) {
1143                log.debug("readFile sees {} configurations", l.size());
1144            }
1145            for (int i = 0; i < l.size(); i++) {
1146                Element e = l.get(i);
1147                switch (e.getName()) {
1148                    case "length":
1149                        lengthField.setText(e.getValue());
1150                        break;
1151                    case "sensordelay":
1152                        sensorDelay.setText(e.getValue());
1153                        break;
1154                    default:
1155                        log.warn("Invalid field in PanelProSpeedProfiler.xml");
1156                }
1157            }
1158        }
1159        // Now read sensor information
1160        if (root.getChild("sensors") != null) {
1161            List<Element> l = root.getChild("sensors").getChildren("sensor");
1162            if (log.isDebugEnabled()) {
1163                log.debug("readFile sees {} sensors", l.size());
1164            }
1165            SensorManager manager = InstanceManager.getDefault(SensorManager.class);
1166            for (int i = 0; i < l.size(); i++) {
1167                Element e = l.get(i);
1168                String sensorType = e.getChild("sensorname").getText();
1169                switch (sensorType) {
1170                    case "sensorAPanel":
1171                        sensorAPanel.setDefaultNamedBean(manager.getByUserName(e.getChild("sensorvalue").getText()));
1172                        break;
1173                    case "sensorBPanel":
1174                        sensorBPanel.setDefaultNamedBean(manager.getByUserName(e.getChild("sensorvalue").getText()));
1175                        break;
1176                    case "sensorCPanel":
1177                        sensorCPanel.setDefaultNamedBean(manager.getByUserName(e.getChild("sensorvalue").getText()));
1178                        break;
1179                    default:
1180                        log.warn("Invalid Sensor found in DecoderProSpeedProfile.xml");
1181                }
1182            }
1183        }
1184        if (root.getChild("steps") != null) {
1185            List<Element> l = root.getChild("steps").getChildren();
1186            for (int i = 0; i < l.size(); i++) {
1187                Element e = l.get(i);
1188                switch (e.getName()) {
1189                    case "speedStepFrom":
1190                        speedStepFrom.setText(e.getValue());
1191                        break;
1192                    case "speedStepTo":
1193                        speedStepTo.setText(e.getValue());
1194                        break;
1195                    case "speedStepIncr":
1196                        speedStepIncr.setText(e.getValue());
1197                        break;
1198                    default:
1199                        log.warn("Invalid field in steps of PanelProSpeedProfiler.xml");
1200                }
1201            }
1202        }
1203
1204        log.debug("...done");
1205    }
1206
1207    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SpeedProfilePanel.class);
1208
1209}