001package jmri.jmrit.logix;
002
003import java.awt.event.ActionEvent;
004import java.text.NumberFormat;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.ListIterator;
008
009import javax.swing.Box;
010import javax.swing.BoxLayout;
011import javax.swing.ButtonGroup;
012import javax.swing.JButton;
013import javax.swing.JCheckBox;
014import javax.swing.JLabel;
015import javax.swing.JMenuBar;
016import javax.swing.JOptionPane;
017import javax.swing.JPanel;
018import javax.swing.JRadioButton;
019import javax.swing.JSpinner;
020import javax.swing.JTextField;
021import javax.swing.SpinnerNumberModel;
022
023import jmri.jmrit.logix.ThrottleSetting.Command;
024import jmri.jmrit.logix.ThrottleSetting.ValueType;
025import jmri.JmriException;
026import jmri.SpeedStepMode;
027
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * Frame for defining and launching an entry/exit warrant. An NX warrant is a
033 * warrant that can be defined on the run without a pre-recorded learn mode
034 * session using a set script for ramping startup and stop throttle settings.
035 * <p>
036 * The route can be defined in a form or by mouse clicking on the OBlock
037 * IndicatorTrack icons.
038 * <br>
039 * <hr>
040 * This file is part of JMRI.
041 * <p>
042 * JMRI is free software; you can redistribute it and/or modify it under the
043 * terms of version 2 of the GNU General Public License as published by the Free
044 * Software Foundation. See the "COPYING" file for a copy of this license.
045 * <p>
046 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
047 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
048 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
049 *
050 * @author Pete Cressman Copyright (C) 2009, 2010, 2015
051 */
052public class NXFrame extends WarrantRoute {
053
054    private float _maxThrottle = 0.75f;
055    private float _startDist;   // mm start distance to portal
056    private float _stopDist;    // mm stop distance from portal
057
058    private final JTextField _maxThrottleBox = new JTextField(6);
059    private final JTextField _maxSpeedBox = new JTextField(6);
060    private final JLabel _maxSpeedBoxLabel = new JLabel(Bundle.getMessage("scaleSpeed"));
061    private DisplayButton _speedUnits;
062    private final JTextField _originDist = new JTextField(6);
063    private DisplayButton _originUnits;
064    private final JTextField _destDist = new JTextField(6);
065    private DisplayButton _destUnits;
066    private final JSpinner _timeIncre = new JSpinner(new SpinnerNumberModel(750, 200, 9000, 1));
067    private final JTextField _rampIncre = new JTextField(6);
068    private final JRadioButton _forward = new JRadioButton();
069    private final JRadioButton _reverse = new JRadioButton();
070    private final JCheckBox _noRamp = new JCheckBox();
071    private final JCheckBox _noSound = new JCheckBox();
072    private final JCheckBox _stageEStop = new JCheckBox();
073    private final JCheckBox _shareRouteBox = new JCheckBox();
074    private final JCheckBox _haltStartBox = new JCheckBox();
075    private final JCheckBox _addTracker = new JCheckBox();
076    private final JRadioButton _runAuto = new JRadioButton(Bundle.getMessage("RunAuto"));
077    private final JRadioButton _runManual = new JRadioButton(Bundle.getMessage("RunManual"));
078    
079    private JPanel _routePanel = new JPanel();
080    private JPanel _autoRunPanel;
081    private final JPanel __trainHolder = new JPanel();
082    private JPanel _switchPanel;
083    private JPanel _trainPanel;
084    
085    protected NXFrame() {
086        super();
087        init();
088    }
089    
090    private void init() {
091        if (log.isDebugEnabled()) log.debug("newInstance");
092        makeMenus();
093
094        _routePanel = new JPanel();
095        _routePanel.setLayout(new BoxLayout(_routePanel, BoxLayout.PAGE_AXIS));
096        _routePanel.add(Box.createVerticalGlue());
097        _routePanel.add(makeBlockPanels(true));
098 
099        _forward.setSelected(true);
100        _speedUtil.setIsForward(true);
101        _stageEStop.setSelected(false);
102        _haltStartBox.setSelected(false);
103        _runAuto.setSelected(true);
104
105        _autoRunPanel = makeAutoRunPanel();
106        _switchPanel = makeSwitchPanel();
107        
108        JPanel mainPanel = new JPanel();
109        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
110        mainPanel.add(_routePanel);
111        getContentPane().add(mainPanel);
112        
113        float prefMaxThrottle = WarrantPreferences.getDefault().getThrottleScale()*100;
114        _maxThrottleBox.setText(NumberFormat.getNumberInstance().format(prefMaxThrottle));
115        maxThrottleEventAction();
116
117        addWindowListener(new java.awt.event.WindowAdapter() {
118            @Override
119            public void windowClosing(java.awt.event.WindowEvent e) {
120                WarrantTableAction.getDefault().closeNXFrame();
121            }
122        });
123        setAlwaysOnTop(true);
124        setVisible(true);
125        pack();
126    }
127
128    protected boolean isRouteSeaching() {
129        return _routePanel.isVisible();
130    }
131
132    private void setPanel() {
133        __trainHolder.add(_trainPanel);
134    }
135    private void setPanel(JPanel p) {
136        JPanel con = (JPanel)getContentPane().getComponent(0);
137        con.removeAll();
138        con.add(p);
139        con.add(_switchPanel);
140        pack();
141    }
142    
143    private JPanel makeSwitchPanel() {
144        ButtonGroup bg = new ButtonGroup();
145        bg.add(_runAuto);
146        bg.add(_runManual);
147        _runAuto.addActionListener((ActionEvent event) -> {
148            setPanel();
149            setPanel(_autoRunPanel);
150        });
151        _runManual.addActionListener((ActionEvent event) -> {
152            setPanel(_trainPanel);
153            _stageEStop.setSelected(false);
154            _shareRouteBox.setSelected(false);
155            _haltStartBox.setSelected(false);
156            _addTracker.setSelected(false);
157        });
158        JPanel pp = new JPanel();
159        pp.setLayout(new BoxLayout(pp, BoxLayout.LINE_AXIS));
160        pp.add(Box.createHorizontalGlue());
161        pp.add(_runAuto);
162        pp.add(Box.createHorizontalStrut(STRUT_SIZE));
163        pp.add(_runManual);
164        pp.add(Box.createHorizontalGlue());
165        
166        JPanel p = new JPanel();
167        p.add(Box.createGlue());
168        JButton button = new JButton(Bundle.getMessage("ButtonRoute"));
169        button.addActionListener((ActionEvent e) -> {
170            clearTempWarrant();
171            JPanel con = (JPanel)getContentPane().getComponent(0);
172            con.removeAll();
173            con.add(_routePanel);
174            pack();
175        });
176        p.add(button);
177        p.add(Box.createHorizontalStrut(2 * STRUT_SIZE));
178        button = new JButton(Bundle.getMessage("ButtonRunNX"));
179        button.addActionListener((ActionEvent e) -> {
180            clearTempWarrant();
181            makeAndRunWarrant();
182        });
183        p.add(button);
184        p.add(Box.createHorizontalStrut(2 * STRUT_SIZE));
185        button = new JButton(Bundle.getMessage("ButtonCancel"));
186        button.addActionListener((ActionEvent e) -> {
187            WarrantTableAction.getDefault().closeNXFrame();
188        });
189        p.add(button);
190        p.add(Box.createGlue());
191        
192        JPanel panel = new JPanel();
193        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
194        panel.add(pp);
195        panel.add(p);
196        return panel;
197    }
198
199    @Override
200    protected void maxThrottleEventAction() {
201        NumberFormat formatter = NumberFormat.getNumberInstance(); 
202        float num = 0;
203        try {
204            num =  formatter.parse(_maxThrottleBox.getText()).floatValue();
205            num = Math.min(100.0f, Math.max(num,  0.f));
206            _maxThrottleBox.setText(formatter.format(num));
207        } catch (java.text.ParseException pe) {
208            _maxThrottleBox.setText(null);
209            _maxSpeedBox.setText(null);
210            return;
211        }
212        float speed = _speedUtil.getTrackSpeed(num/100);    // returns mm/ms (meters/sec)
213        switch(_displayPref) {
214            case MPH:
215                // Convert meters/sec to scale miles/hr
216                _maxSpeedBox.setText(formatter.format(speed * _scale * 2.2369363f));
217                break;
218            case KPH:
219                // Convert meters/sec to scale kilometers/hr
220                _maxSpeedBox.setText(formatter.format(speed * _scale * 3.6f));
221                break;
222            case MMPS:
223                // Convert meters/sec to millimeters/sec
224                _maxSpeedBox.setText(formatter.format(speed * 1000));  // mm/sec
225                break;
226            case INPS:
227            default:
228                // Convert meters/sec to inchec/sec
229                _maxSpeedBox.setText(formatter.format(speed * 39.37f));  // in/sec
230        }
231    }
232
233    private void unitsEventAction(JButton button, JTextField field) {
234        if (button.getText().equals(Display.IN.toString())) {
235            _units = Display.CM;
236        } else {
237            _units = Display.IN;
238        }
239        setFieldText(button, field);
240    }
241    // convert to units change
242    private void setFieldText(JButton button, JTextField field) {
243        NumberFormat formatter = NumberFormat.getNumberInstance(); 
244        float num = 0;
245        try {
246            num =  formatter.parse(field.getText()).floatValue();
247        } catch (java.text.ParseException pe) {
248            // errors reported later
249        }
250        if (_units.equals(Display.IN)) {
251            num = Math.round(num * 0.393701f);  // convert centimeters to inches
252        } else {
253            num = Math.round(num * 2.54f);  // convert inches to centimeters
254        }
255        button.setText(_units.toString());
256        field.setText(formatter.format(num));
257    }
258
259    private JPanel makeAutoRunPanel() {
260        JPanel p1 = new JPanel();
261        p1.setLayout(new BoxLayout(p1, BoxLayout.PAGE_AXIS));
262
263        _speedUnits = new DisplayButton(_displayPref);
264        _originUnits = new DisplayButton(_units);
265        _destUnits = new DisplayButton(_units);
266        
267        _maxThrottleBox.addActionListener((ActionEvent evt)-> maxThrottleEventAction());
268
269        _maxSpeedBox.addActionListener((ActionEvent evt)-> {
270            NumberFormat formatter = NumberFormat.getNumberInstance(); 
271            float num = 0;
272            try {
273                num =  formatter.parse(_maxSpeedBox.getText()).floatValue();
274            } catch (java.text.ParseException pe) {
275                _maxSpeedBox.setText("");
276                return;
277            }
278            if (num < 0) {
279                _maxSpeedBox.setText(formatter.format(0));
280                _maxThrottleBox.setText(formatter.format(0));
281                return;
282            }
283            // maxSpeed is speed at full throttle in mm/sec
284            float maxSpeed = _speedUtil.getTrackSpeed(1);   // mm/ms, i.e. m/s
285            // maximum number is maxSpeed when converted to selected units
286            float maxNum;
287            // convert to display units. Note real world speed is converted to scaled world speed
288            // display label changes "Scale speed" to "Track Speed" accordingly
289            switch (_displayPref) {
290                case MPH:
291                    maxNum = maxSpeed * 2.2369363f *_scale; // convert meters/sec to miles/hr
292                    break;
293                case KPH:
294                    maxNum = maxSpeed * 3.6f * _scale;  // convert meters/sec to to kilometers/hr 
295                    break;
296                case MMPS:
297                    maxNum = maxSpeed * 1000;   // convert meters/sec to milimeters/sec
298                    break;
299                default:
300                    maxNum = maxSpeed * 39.37f; // convert meters/sec to inches/sec
301                    break;
302            }
303            if (num > maxNum) {
304                String name = _speedUtil.getRosterId();
305                if (name == null || name.charAt(0) == '$') {
306                    name = getTrainName();
307                    if (name == null || name.isEmpty()) {
308                        name = Bundle.getMessage("unknownTrain");
309                    }
310                }
311                JOptionPane.showMessageDialog(null, Bundle.getMessage("maxSpeedLimit", 
312                        name, formatter.format(maxNum), _speedUnits.getText()),
313                        Bundle.getMessage("MessageTitle"), JOptionPane.INFORMATION_MESSAGE);
314                _maxSpeedBox.setText(formatter.format(maxNum));
315                _maxThrottleBox.setText(formatter.format(100));
316                return;
317            }
318            // convert to display num in selected units to track speed in meters/sec (mm/ms)
319            // reciprocal of above
320            switch (_displayPref) {
321                case MPH:
322                    num = num * 0.44704f / _scale;  // convert scale miles/hr to mm/msec
323                    break;
324                case KPH:
325                    num = num * 0.277778f / _scale;  // convert scale kilometers/hr to mm/msec
326                    break;
327                case MMPS:
328                    num = num / 1000;  // convert mm/sec to mm/msec
329                    break;
330                default:
331                    num = num / 39.37f;  // convert inches/sec to mm/msec
332                    break;
333            }
334            // get throttla setting and display as percent full throttle.
335            float throttle = _speedUtil.getThrottleSettingForSpeed(num)*100;
336            _maxThrottleBox.setText(formatter.format(throttle));
337        });
338
339        // User makes a choice for their desired units (_displayPref) to show max speed
340        _speedUnits.addActionListener((ActionEvent evt)-> {
341            NumberFormat formatter = NumberFormat.getNumberInstance(); 
342            float num = 0;
343            try {
344                num =  formatter.parse(_maxSpeedBox.getText()).floatValue();
345            } catch (java.text.ParseException pe) {
346                _maxSpeedBox.setText(null);
347                return;
348            }
349            // display preference for units cycles through 4 choices
350            // convert old choice to new 
351            switch (_displayPref) {
352                case MPH:
353                    _displayPref = Display.KPH;
354                    _maxSpeedBox.setText(formatter.format(num * 1.60934f)); // miles/hr to km/hr
355                    break;
356                case KPH:
357                    _displayPref = Display.MMPS;
358                    _maxSpeedBox.setText(formatter.format(num * 0277.778f / _scale));   // scale km/hr to mm/sec
359                    _maxSpeedBoxLabel.setText(Bundle.getMessage("trackSpeed"));
360                    break;
361                case MMPS:
362                    _displayPref = Display.INPS;
363                    _maxSpeedBox.setText(formatter.format(num * 0.03937f)); // mm/sec to in/sec
364                    break;
365                default:
366                    _displayPref = Display.MPH;
367                    _maxSpeedBox.setText(formatter.format(num * 0.056818f * _scale)); // inches/sec to scale miles/hr
368                    _maxSpeedBoxLabel.setText(Bundle.getMessage("scaleSpeed"));
369                    break;
370                }
371                // display label changes "Scale speed" to "Track Speed" accordingly
372                _speedUnits.setDisplayPref(_displayPref);
373            });
374
375        p1.add(makeTextAndButtonPanel(_maxThrottleBox, new JLabel(Bundle.getMessage("percent")), 
376                new JLabel(Bundle.getMessage("MaxSpeed")), "ToolTipPercentThrottle"));
377        p1.add(makeTextAndButtonPanel(_maxSpeedBox, _speedUnits, 
378                _maxSpeedBoxLabel, "ToolTipScaleSpeed"));
379
380        _originUnits.addActionListener((ActionEvent evt)-> {
381            unitsEventAction(_originUnits, _originDist);
382            setFieldText(_destUnits, _destDist);
383        });
384        _destUnits.addActionListener((ActionEvent evt)-> {
385            unitsEventAction(_destUnits, _destDist);
386            setFieldText(_originUnits, _originDist);
387        });
388
389        p1.add(makeTextAndButtonPanel(_originDist, _originUnits, 
390                new JLabel(Bundle.getMessage("startDistance")), "ToolTipStartDistance"));
391        p1.add(makeTextAndButtonPanel(_destDist, _destUnits, 
392                new JLabel(Bundle.getMessage("stopDistance")), "ToolTipStopDistance"));
393        p1.add(WarrantPreferencesPanel.timeIncrementPanel(false, _timeIncre));
394        p1.add(WarrantPreferencesPanel.throttleIncrementPanel(false, _rampIncre));
395        _rampIncre.addActionListener((ActionEvent e)->{
396                String text = _rampIncre.getText();
397                boolean showdialog = false;
398                try {
399                    float incr = NumberFormat.getNumberInstance().parse(text).floatValue();
400                    showdialog = (incr < 0.5f || incr > 25f);
401                } catch (java.text.ParseException pe) {
402                    showdialog = true;
403                }
404                if (showdialog) {
405                    JOptionPane.showMessageDialog(null, Bundle.getMessage("rampIncrWarning", text),
406                            Bundle.getMessage("WarningTitle"), JOptionPane.WARNING_MESSAGE);
407                }
408            });
409        ButtonGroup bg = new ButtonGroup();
410        bg.add(_forward);
411        bg.add(_reverse);
412        JPanel pp = new JPanel();
413        pp.setLayout(new BoxLayout(pp, BoxLayout.LINE_AXIS));
414        pp.add(Box.createHorizontalGlue());
415        pp.add(makeTextBoxPanel(false, _forward, "forward", null));
416        pp.add(makeTextBoxPanel(false, _reverse, "reverse", null));
417        pp.add(Box.createHorizontalGlue());
418        p1.add(pp);
419
420        __trainHolder.setLayout(new BoxLayout(__trainHolder, BoxLayout.PAGE_AXIS));
421        _trainPanel = makeTrainIdPanel(null);
422        __trainHolder.add(_trainPanel);
423
424        JPanel p2 = new JPanel();
425        p2.setLayout(new BoxLayout(p2, BoxLayout.PAGE_AXIS));      
426        p2.add(__trainHolder);
427        p2.add(makeTextBoxPanel(_noRamp, "NoRamping", "ToolTipNoRamping"));
428        p2.add(makeTextBoxPanel(_noSound, "NoSound", "ToolTipNoSound"));
429        p2.add(makeTextBoxPanel(_stageEStop, "StageEStop", null));
430        p2.add(makeTextBoxPanel(_haltStartBox, "HaltAtStart", null));
431        p2.add(makeTextBoxPanel(_shareRouteBox, "ShareRoute", "ToolTipShareRoute"));
432        p2.add(makeTextBoxPanel(_addTracker, "AddTracker", "ToolTipAddTracker"));
433        
434
435        JPanel autoRunPanel = new JPanel();
436        autoRunPanel.setLayout(new BoxLayout(autoRunPanel, BoxLayout.PAGE_AXIS));
437        JPanel ppp = new JPanel();
438        ppp.setLayout(new BoxLayout(ppp, BoxLayout.LINE_AXIS));
439        ppp.add(Box.createHorizontalStrut(STRUT_SIZE));
440        ppp.add(p1);
441        ppp.add(Box.createHorizontalGlue());
442        ppp.add(p2);
443        ppp.add(Box.createHorizontalStrut(STRUT_SIZE));
444        autoRunPanel.add(ppp);
445
446        _forward.addActionListener((ActionEvent evt)-> maxThrottleEventAction());
447        _reverse.addActionListener((ActionEvent evt)-> maxThrottleEventAction());
448
449        return autoRunPanel;
450    }
451    
452    private void updateAutoRunPanel() {
453        _startDist = getPathLength(_orders.get(0)) / 2;
454        _stopDist = getPathLength(_orders.get(_orders.size()-1)) / 2;
455        NumberFormat formatter = NumberFormat.getNumberInstance(); 
456        if (_units.equals(Display.IN)) {
457            // convert millimeters to inches
458            _originDist.setText(formatter.format(_startDist * 0.0393701));
459            _destDist.setText(formatter.format(_stopDist * 0.0393701));
460        } else {
461         // convert millimeters to centimeters
462            _originDist.setText(formatter.format(_startDist / 10));
463            _destDist.setText(formatter.format(_stopDist / 10));
464        }
465         _autoRunPanel.repaint();
466    }
467
468    private void makeMenus() {
469        setTitle(Bundle.getMessage("AutoWarrant"));
470        JMenuBar menuBar = new JMenuBar();
471        setJMenuBar(menuBar);
472        addHelpMenu("package.jmri.jmrit.logix.NXWarrant", true);
473    }
474
475    @Override
476    public void propertyChange(java.beans.PropertyChangeEvent e) {
477        String property = e.getPropertyName();
478        log.trace("propertyChange \"{}\" old= {} new= {} source= {}",property,
479                                            e.getOldValue(),e.getNewValue(),
480                                            e.getSource().getClass().getName());
481        if (property.equals("DnDrop")) {
482            doAction(e.getSource());
483        }
484    }
485
486    /**
487     * Called by {@link jmri.jmrit.logix.RouteFinder#run()}. If all goes well,
488     * WarrantTableFrame.runTrain(warrant) will run the warrant
489     *
490     * @param orders list of block orders
491     */
492    @Override
493    protected void selectedRoute(ArrayList<BlockOrder> orders) {
494        JPanel con = (JPanel)getContentPane().getComponent(0);
495        con.removeAll();
496        if (_runAuto.isSelected()) {
497            con.add(_autoRunPanel);            
498        } else {
499            con.add(_trainPanel);
500        }
501        con.add(_switchPanel);
502        updateAutoRunPanel();
503        pack();
504    }
505
506    private void makeAndRunWarrant() {
507        if (log.isDebugEnabled()) {
508            log.debug("NXFrame selectedRoute()");
509        }
510        String msg = getBoxData();
511        if (msg == null) {
512            msg = checkLocoAddress();
513        }
514        if (msg != null) {
515            JOptionPane.showMessageDialog(this, msg,
516                    Bundle.getMessage("WarningTitle"), JOptionPane.WARNING_MESSAGE);
517            return;
518        }
519        // There is a dccAddress so a throttle can be acquired
520        String s = ("" + Math.random()).substring(2);
521        Warrant warrant = new Warrant("IW" + s, "NX(" + getAddress() + ")");
522        warrant.setBlockOrders(_orders);
523        warrant.setTrainName(getTrainName());
524        warrant.setNoRamp(_noRamp.isSelected());
525        _speedUtil.setIsForward(_forward.isSelected());
526        // position distance from start of path
527        _speedUtil.setDistanceTravelled(getPathLength(_orders.get(0)) - _startDist);
528        warrant.setSpeedUtil(_speedUtil);   // transfer SpeedUtil to warrant
529        log.debug("Warrant {}. Route and loco set.", warrant.getDisplayName());
530        int mode;
531        if (!_runManual.isSelected()) {
532            mode = Warrant.MODE_RUN;
533            warrant.setShareRoute(_shareRouteBox.isSelected());
534            warrant.setAddTracker(_addTracker.isSelected());
535            msg = makeCommands(warrant);
536        } else {
537            mode = Warrant.MODE_MANUAL;
538        }
539        WarrantTableFrame tableFrame = WarrantTableFrame.getDefault();
540        if (msg == null) {
541            warrant.setNXWarrant(true);
542            tableFrame.getModel().addNXWarrant(warrant);   //need to catch propertyChange at start
543            if (log.isDebugEnabled()) {
544                log.debug("NXWarrant added to table");
545            }
546            msg = tableFrame.runTrain(warrant, mode);
547            tableFrame.scrollTable();
548        }
549        if (msg != null) {
550            log.debug("WarrantTableFrame run warrant. msg= {} Remove warrant {}",msg,warrant.getDisplayName());
551            tableFrame.getModel().removeWarrant(warrant, false);
552        }
553
554        if (msg == null && mode == Warrant.MODE_RUN) {
555            if (_haltStartBox.isSelected()) {
556                class Halter implements Runnable {
557
558                    Warrant war;
559
560                    Halter(Warrant w) {
561                        war = w;
562                    }
563
564                    @Override
565                    public void run() {
566                        int limit = 0;
567                        try {
568                            // wait until _engineer is assigned so HALT can take effect
569                            while (!war.controlRunTrain(Warrant.HALT) && limit < 3000) {
570                                Thread.sleep(200);
571                                limit += 200;
572                            }
573                        } catch (InterruptedException e) {
574                            war.controlRunTrain(Warrant.HALT);
575                        }
576                    }
577                }
578                Halter h = new Halter(warrant);
579                jmri.util.ThreadingUtil.newThread(h).start();
580            }
581        }
582        if (msg != null) {
583            JOptionPane.showMessageDialog(this, msg,
584                    Bundle.getMessage("WarningTitle"), JOptionPane.WARNING_MESSAGE);
585        } else {
586            WarrantTableAction.getDefault().closeNXFrame();
587        }
588    }
589
590    // for testing
591    protected void setMaxSpeed(float s) {
592        _maxThrottle = s;
593        _maxThrottleBox.setText(NumberFormat.getNumberInstance().format(s));
594    }
595    
596    private String getBoxData() {
597        String text = null;
598        float maxSpeed;
599        NumberFormat formatter = NumberFormat.getNumberInstance(); 
600        try {
601            text = _maxThrottleBox.getText();
602            maxSpeed = formatter.parse(text).floatValue();
603        } catch (java.text.ParseException pe) {
604            if (text==null) {
605                text = "\"\"";
606            }
607            return Bundle.getMessage("badSpeed100", text);
608        }
609
610        try {
611            _startDist = getDistance(_originDist, _orders.get(0));
612        } catch (JmriException je) {
613            return je.getMessage();
614        }
615
616        try {
617            _stopDist = getDistance(_destDist, _orders.get(_orders.size()-1));
618        } catch (JmriException je) {
619            return je.getMessage();
620        }
621
622        if (maxSpeed > 100f || maxSpeed < 0.001f) {
623            return Bundle.getMessage("badSpeed100", maxSpeed);
624        }
625        _maxThrottle = maxSpeed / 100;
626
627        String msg = setAddress();
628        if (msg != null) {
629            return msg;
630        }
631
632        int time = (Integer)_timeIncre.getValue();
633        _speedUtil.setRampTimeIncrement(time);
634
635        try {
636            text = _rampIncre.getText();
637            float incre = NumberFormat.getNumberInstance().parse(text).floatValue();
638            if (incre < 0.5f || incre > 25f) {
639                return Bundle.getMessage("rampIncrWarning", text);                
640            } else {
641                _speedUtil.setRampThrottleIncrement(incre/100);
642            }
643        } catch (java.text.ParseException pe) {
644            return Bundle.getMessage("MustBeFloat", text);
645        }
646        
647        _speedUtil.resetSpeedProfile();
648        return null;
649    }
650
651    private float getDistance(JTextField field, BlockOrder bo) throws JmriException {
652        NumberFormat formatter = NumberFormat.getNumberInstance();
653        float distance;
654        String text = field.getText();
655        try {
656            distance = formatter.parse(text).floatValue();
657        } catch (java.text.ParseException pe) {
658            throw new JmriException(Bundle.getMessage("MustBeFloat", text));
659        }
660        float pathLen = getPathLength(bo);
661        if (pathLen <= 0) {
662            throw new JmriException(Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName()));
663        }
664        if (_units.equals(Display.IN)){
665            distance *= 25.4f;  // convert inches to millimeters
666            if (distance < 0 || distance > pathLen) {
667                field.setText(formatter.format(pathLen * 12.07));
668                throw new JmriException(Bundle.getMessage(
669                        "BadLengthIn", bo.getPathName(), bo.getBlock().getDisplayName(), pathLen*0.039701f, text));                                        
670            }
671        } else {
672            distance *= 10f;  // convert centimeters to millimeters
673            if (distance < 0 || distance > pathLen) {
674                field.setText(formatter.format(pathLen * 5));
675                throw new JmriException(Bundle.getMessage(
676                        "BadLengthCm", bo.getPathName(), bo.getBlock().getDisplayName(), pathLen*0.5f, text));                                        
677            }
678        }
679        return distance;
680    }
681
682    private float getPathLength(BlockOrder bo) {
683        float len = bo.getPath().getLengthMm();
684        if (len <= 0) {
685            len = bo.getTempPathLen();
686            if ( len <= 0) {
687                String sLen = JOptionPane.showInputDialog(this, 
688                        Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName())
689                        + Bundle.getMessage("getPathLength", bo.getPathName(), bo.getBlock().getDisplayName()),
690                        Bundle.getMessage("WarningTitle"), JOptionPane.WARNING_MESSAGE);
691                try {
692                    len = NumberFormat.getNumberInstance().parse(sLen).floatValue();                    
693                } catch (java.text.ParseException | java.lang.NullPointerException pe) {
694                    len = 0.0f;
695                }
696                bo.setTempPathLen(len);
697            }
698        }
699       return len;
700    }
701    private float adjustdistance(float fromSpeed, float toSpeed, float distance, BlockOrder bo) throws JmriException {
702        float pathLen = getPathLength(bo);
703        if (pathLen <= 0) {
704            throw new JmriException(Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName()));
705        }
706        int timeIncrement = _speedUtil.getRampTimeIncrement();
707        float minDist = _speedUtil.getDistanceOfSpeedChange(fromSpeed, toSpeed, timeIncrement) +.1f;
708        if (distance < minDist) {
709            distance = minDist;
710        } else if (distance > pathLen - minDist) {
711            distance = pathLen - minDist;
712        }
713        return distance;
714    }
715    /*
716     * Return length of warrant route in mm.  Assume start and end is in the middle of first
717     * and last blocks.  Use a default length for blocks with unspecified length.
718     */
719    private float getTotalLength() throws JmriException {
720        float totalLen = 0.0f;
721        List<BlockOrder> orders = getOrders();
722        float throttleIncrement = _speedUtil.getRampThrottleIncrement();
723        _startDist = adjustdistance(0f, throttleIncrement, _startDist, orders.get(0));
724        totalLen = _startDist;
725        for (int i = 1; i < orders.size() - 1; i++) {
726            BlockOrder bo = orders.get(i);
727            float pathLen = getPathLength(bo);
728            if (pathLen <= 0) {
729                throw new JmriException(Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName()));
730            }
731            totalLen += pathLen;
732        }
733        _stopDist = adjustdistance(throttleIncrement, 0f, _stopDist, orders.get(0));
734        totalLen += _stopDist;
735        return totalLen;
736    }
737
738    private String makeCommands(Warrant w) {
739
740        int nextIdx = 0;        // block index - increment after getting a block order
741        List<BlockOrder> orders = getOrders();
742        BlockOrder bo = orders.get(nextIdx++);
743        String blockName = bo.getBlock().getDisplayName();
744
745        int cmdNum;
746        w.addThrottleCommand(new ThrottleSetting(0, Command.FKEY, 0, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0, blockName));
747        if (_forward.isSelected()) {
748            w.addThrottleCommand(new ThrottleSetting(100, Command.FORWARD, -1, ValueType.VAL_TRUE, SpeedStepMode.UNKNOWN, 0,  blockName));
749            if (!_noSound.isSelected()) {
750                w.addThrottleCommand(new ThrottleSetting(1000, Command.FKEY, 2, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0, blockName));
751                w.addThrottleCommand(new ThrottleSetting(2500, Command.FKEY, 2, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
752                w.addThrottleCommand(new ThrottleSetting(1000, Command.FKEY, 2, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0, blockName));
753                w.addThrottleCommand(new ThrottleSetting(2500, Command.FKEY, 2, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
754                cmdNum = 7;
755            } else {
756                cmdNum = 3;
757            }
758        } else {
759            w.addThrottleCommand(new ThrottleSetting(100, Command.FORWARD, -1, ValueType.VAL_FALSE, SpeedStepMode.UNKNOWN, 0, blockName));
760            if (!_noSound.isSelected()) {
761                w.addThrottleCommand(new ThrottleSetting(1000, Command.FKEY, 3, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0,  blockName));
762                w.addThrottleCommand(new ThrottleSetting(500, Command.FKEY, 3, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
763                w.addThrottleCommand(new ThrottleSetting(500, Command.FKEY, 3, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0, blockName));
764                w.addThrottleCommand(new ThrottleSetting(500, Command.FKEY, 3, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
765                cmdNum = 6;
766            } else {
767                cmdNum = 2;
768            }
769        }
770
771        float totalLen;
772        try {
773            totalLen = getTotalLength();
774        } catch (JmriException je) {
775            return je.getMessage();
776        }
777
778        RampData upRamp;
779        RampData downRamp;
780        ListIterator<Float> downIter;
781        float intervalDist;
782        do {
783            upRamp = _speedUtil.getRampForSpeedChange(0f, _maxThrottle);
784            downRamp = _speedUtil.getRampForSpeedChange(_maxThrottle, 0f);
785            downIter = downRamp.speedIterator(false);
786            float prevSetting = downIter.previous().floatValue();   // top value is _maxThrottle 
787            _maxThrottle -= prevSetting  - downIter.previous().floatValue();    // last throttle increment
788            // distance attaining final speed
789            intervalDist = _speedUtil.getDistanceOfSpeedChange(_maxThrottle, prevSetting, downRamp.getRampTimeIncrement());
790            log.debug("Route length= {}, upRampLength= {}, dnRampLength= {}",
791                    totalLen, upRamp.getRampLength(), downRamp.getRampLength());
792        } while ((upRamp.getRampLength() + intervalDist + downRamp.getRampLength()) > totalLen);
793        _maxThrottle = downRamp.getMaxSpeed();
794
795        float blockLen = _startDist;    // length of path in current block
796
797        // start train
798        float speedTime = 0;    // ms time to complete speed step from last block
799        float noopTime = 0;     // ms time for entry into next block
800        ListIterator<Float> iter = upRamp.speedIterator(true);
801        float curThrottle = iter.next();  // throttle setting
802        float nextThrottle = 0f;
803        float curDistance = 0;  // current distance traveled mm
804        float blkDistance = 0;  // distance traveled in current block mm
805        float upRampLength = upRamp.getRampLength();
806        float remRamp = upRampLength;
807        float remTotal = totalLen;
808        float dnRampLength = downRamp.getRampLength();
809        int timeInterval = downRamp.getRampTimeIncrement();
810        boolean rampsShareBlock = false;
811
812        if (log.isDebugEnabled()) {
813            log.debug("Start in block \"{}\" startDist= {} stopDist= {}", blockName, _startDist, _stopDist);
814        }
815        while (iter.hasNext()) {       // ramp up loop
816
817            while (blkDistance < blockLen && iter.hasNext()) {
818                nextThrottle = iter.next().floatValue();
819                float dist = _speedUtil.getDistanceOfSpeedChange(curThrottle, nextThrottle, timeInterval);
820                if (blkDistance + dist <= blockLen) {
821                    blkDistance += dist;
822                    remRamp -= dist;
823                    curThrottle = nextThrottle;
824                    w.addThrottleCommand(new ThrottleSetting((int) speedTime, Command.SPEED, -1, ValueType.VAL_FLOAT, 
825                            SpeedStepMode.UNKNOWN, curThrottle, blockName, _speedUtil.getTrackSpeed(curThrottle)));
826                    if (log.isDebugEnabled()) {
827                        log.debug("{}. Ramp Up in block \"{}\" to speed {} in {}ms to distance= {}mm, remRamp= {}",
828                                ++cmdNum, blockName, curThrottle, (int) speedTime, blkDistance, remRamp);
829                    }
830                    speedTime = timeInterval;
831                } else {
832                    iter.previous();
833                    break;
834                }
835            }
836            curDistance += blkDistance;
837
838            if (blkDistance >= blockLen) {
839                // Possible case where blkDistance can exceed the length of a block that was just entered.
840                // Skip over block and move to next block and adjust the distance times into that block
841                noopTime = _speedUtil.getTimeForDistance(curThrottle, blockLen);   // noop distance to run through block 
842                speedTime = _speedUtil.getTimeForDistance(curThrottle, blkDistance - blockLen);
843                curDistance += blockLen;
844            } else {
845                // typical case where next speed change broke out of above loop
846                noopTime = _speedUtil.getTimeForDistance(curThrottle, (blockLen - blkDistance));   // time to next block
847                if (noopTime > timeInterval) {  // after last speed change
848                    speedTime = 0;  // irrelevant, loop will end
849                    if (!iter.hasNext()) {
850                        noopTime += timeInterval;   // add time to complete last speed change
851                    }
852                } else {
853                    speedTime = timeInterval - noopTime;   // time to next speed change
854                }
855                curDistance += blockLen - blkDistance;  // noop distance
856            }
857
858            // break out here if done or deceleration is to be started in this block
859            if (!iter.hasNext() || remTotal - blockLen <= dnRampLength) {
860                break;
861            }
862
863            remTotal -= blockLen;
864            if (log.isDebugEnabled()) {
865                log.debug("Leave RampUp block \"{}\"  blkDistance= {}, blockLen= {} remRamp= {} curDistance= {} remTotal={}",
866                        blockName, blkDistance, blockLen, remRamp, curDistance, remTotal);
867            }
868            if (nextIdx < orders.size()) {
869                bo = orders.get(nextIdx++);
870                blockLen = getPathLength(bo);
871                if (blockLen <= 0) {
872                    return Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName());
873                 }
874                blockName = bo.getBlock().getDisplayName();
875                w.addThrottleCommand(new ThrottleSetting((int) noopTime, Command.NOOP, -1, ValueType.VAL_NOOP, 
876                        SpeedStepMode.UNKNOWN, 0, blockName, _speedUtil.getTrackSpeed(curThrottle)));
877                if (log.isDebugEnabled()) {
878                    log.debug("{}. Enter RampUp block \"{}\" noopTime= {}, speedTime= {} blockLen= {}, remTotal= {}",
879                        cmdNum++, blockName, noopTime, speedTime, blockLen, remTotal);
880                }
881            }
882            blkDistance = _speedUtil.getDistanceTraveled(curThrottle, Warrant.Normal, speedTime);
883        }
884        if (log.isDebugEnabled()) {
885            log.debug("Ramp Up done at block \"{}\" curThrottle={} blkDistance={} curDistance={} remTotal= {} remRamp={}", 
886                    blockName, curThrottle, blkDistance, curDistance, remTotal, remRamp);
887        }
888
889        if (remTotal - blockLen > dnRampLength) {    // At maxThrottle, remainder of block at max speed
890            if (nextIdx < orders.size()) {    // not the last block
891                remTotal -= blockLen;
892                bo = orders.get(nextIdx++);
893                blockLen = getPathLength(bo);
894                if (blockLen <= 0) {
895                    return Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName());
896                 }
897                blockName = bo.getBlock().getDisplayName();
898                w.addThrottleCommand(new ThrottleSetting((int) noopTime, Command.NOOP, -1, ValueType.VAL_NOOP, 
899                        SpeedStepMode.UNKNOWN, 0, blockName, _speedUtil.getTrackSpeed(curThrottle)));
900                if (log.isDebugEnabled()) {
901                    log.debug("{}. Enter block \"{}\" noopTime= {}, blockLen= {}, curDistance={}",
902                            cmdNum++, blockName, noopTime, blockLen, curDistance);
903                }
904            }
905
906            // run through mid route at max speed
907            while (nextIdx < orders.size() && remTotal - blockLen > dnRampLength) {
908                remTotal -= blockLen;
909                // constant speed, get time to next block
910                noopTime = _speedUtil.getTimeForDistance(curThrottle, blockLen);   // time to next block
911                curDistance += blockLen;
912                if (log.isDebugEnabled()) {
913                    log.debug("Leave MidRoute block \"{}\" noopTime= {} blockLen= {} curDistance={} remTotal= {}",
914                            blockName, noopTime, blockLen, curDistance, remTotal);
915                }
916                bo = orders.get(nextIdx++);
917                blockLen = getPathLength(bo);
918                if (blockLen <= 0) {
919                    return Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName());
920                 }
921                blockName = bo.getBlock().getDisplayName();
922                if (nextIdx == orders.size()) {
923                    blockLen = _stopDist;
924                }
925                w.addThrottleCommand(new ThrottleSetting((int) noopTime, Command.NOOP, -1, ValueType.VAL_NOOP, 
926                        SpeedStepMode.UNKNOWN, 0, blockName, _speedUtil.getTrackSpeed(curThrottle)));
927                if (log.isDebugEnabled()) {
928                    log.debug("{}. Enter MidRoute block \"{}\" noopTime= {}, blockLen= {}, curDistance={}",
929                            cmdNum++, blockName, noopTime, blockLen, curDistance);
930                }
931             }
932            blkDistance = 0;
933       } else {
934            // else Start ramp down in current block
935            rampsShareBlock = true;
936        }
937
938        // Ramp down.
939        remRamp = dnRampLength;
940        iter = downRamp.speedIterator(false);
941        iter.previous();   // discard, equals curThrottle
942        float remMaxSpeedDist;
943        if (!rampsShareBlock) {
944            remMaxSpeedDist = remTotal - dnRampLength;
945        } else {
946            remMaxSpeedDist = totalLen - upRampLength - dnRampLength;
947        }
948        // distance in block where down ramp is started
949        blkDistance += remMaxSpeedDist;
950        // time to start down ramp
951        speedTime = _speedUtil.getTimeForDistance(curThrottle, remMaxSpeedDist);
952
953        if (log.isDebugEnabled()) {
954            log.debug("Begin Ramp Down at block \"{}\" blockLen={}, at distance= {} curDistance = {} remTotal= {} curThrottle= {} ({})",
955                    blockName, blockLen, blkDistance, curDistance, remTotal, curThrottle, remMaxSpeedDist);
956        }
957
958        while (iter.hasPrevious()) {
959            boolean atLastBlock = false;
960            if (nextIdx == orders.size()) { // at last block
961                atLastBlock = true;
962                if (_stageEStop.isSelected()) {
963                    w.addThrottleCommand(new ThrottleSetting(50, Command.SPEED, -1, ValueType.VAL_FLOAT, 
964                            SpeedStepMode.UNKNOWN, -0.5f, blockName, _speedUtil.getTrackSpeed(curThrottle)));
965                    if (log.isDebugEnabled()) {
966                        log.debug("{}. At block \"{}\" EStop set speed= {}", cmdNum++, blockName, -0.5);
967                    }
968                    break;
969                }
970            }
971
972            do /*while (blkDistance < blockLen && iter.hasPrevious())*/ {
973                boolean hasPrevious = false;
974                if (iter.hasPrevious()) {
975                    nextThrottle = iter.previous();
976                    hasPrevious = true;
977                }
978                float dist = _speedUtil.getDistanceOfSpeedChange(curThrottle, nextThrottle, timeInterval);
979                blkDistance += dist;
980                remRamp -= dist;                
981                curThrottle = nextThrottle;
982                if (curThrottle <= 0f && !atLastBlock) {
983                    log.warn("Set curThrottle = {} in block \"{}\" (NOT the last block)!", curThrottle, blockName);
984                    break;
985                }
986                if (hasPrevious) {
987                    w.addThrottleCommand(new ThrottleSetting((int) speedTime, Command.SPEED, -1, ValueType.VAL_FLOAT, 
988                            SpeedStepMode.UNKNOWN, curThrottle, blockName, _speedUtil.getTrackSpeed(curThrottle)));
989                    if (log.isDebugEnabled()) {
990                        log.debug("{}. Ramp Down in block \"{}\" to curThrottle {} in {}ms to distance= {}mm, remRamp= {}",
991                                ++cmdNum, blockName, curThrottle, (int) speedTime, blkDistance, remRamp);
992                    }
993                } else {
994                    if (curThrottle > 0f) {
995                        log.warn("No speed setting after command {} in block \"{}\". curThrottle= {} blkDistance= {}mm",
996                                cmdNum, blockName, curThrottle, blkDistance);
997                    }
998                    break;
999                }
1000                speedTime = timeInterval;
1001            } while (blkDistance < blockLen);
1002            curDistance += blkDistance;
1003
1004            if (log.isDebugEnabled()) {
1005                log.debug("Leave RampDown block \"{}\"  blkDistance= {}, blockLen= {} remRamp= {} curDistance= {} remTotal= {}",
1006                        blockName, blkDistance, blockLen, remRamp, curDistance, remTotal);
1007            }
1008            if (blkDistance >= blockLen) {
1009                // typical case where next speed change broke out of above loop
1010                speedTime = _speedUtil.getTimeForDistance(curThrottle, blkDistance - blockLen); // time to run in next block
1011                if (speedTime > timeInterval) {
1012                    noopTime = 0;
1013                } else {
1014                    noopTime = timeInterval - speedTime;
1015                }
1016            } else {
1017                speedTime = timeInterval - noopTime;
1018            }
1019
1020            remTotal -= blockLen;
1021            if (!atLastBlock) {
1022                curDistance += blockLen - blkDistance;  // noop distance
1023                bo = orders.get(nextIdx++);
1024                if (nextIdx == orders.size()) {
1025                    blockLen = _stopDist;
1026                } else {
1027                    blockLen = getPathLength(bo);
1028                    if (blockLen <= 0) {
1029                        return Bundle.getMessage("zeroPathLength", bo.getPathName(), bo.getBlock().getDisplayName());
1030                     }
1031                }
1032                blockName = bo.getBlock().getDisplayName();
1033                w.addThrottleCommand(new ThrottleSetting((int) noopTime, Command.NOOP, -1, ValueType.VAL_NOOP, 
1034                        SpeedStepMode.UNKNOWN, 0, blockName, _speedUtil.getTrackSpeed(curThrottle)));
1035                if (log.isDebugEnabled()) {
1036                    log.debug("{}. Enter block \"{}\" noopTime= {}ms, blockLen= {}, curDistance={}",
1037                            cmdNum++, blockName, noopTime, blockLen, curDistance);
1038                }
1039                blkDistance = _speedUtil.getDistanceTraveled(curThrottle, Warrant.Normal, speedTime);
1040            } else {
1041                blkDistance = 0f;
1042            }
1043        }
1044
1045        // Ramp down finished
1046        log.debug("Ramp down done at block \"{}\",  remRamp= {}, curDistance= {} remRamp= {}",
1047                blockName, remRamp, curDistance, remTotal);
1048        if (!_noSound.isSelected()) {
1049            w.addThrottleCommand(new ThrottleSetting(500, Command.FKEY, 1, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
1050            w.addThrottleCommand(new ThrottleSetting(1000, Command.FKEY, 2, ValueType.VAL_ON, SpeedStepMode.UNKNOWN, 0, blockName));
1051            w.addThrottleCommand(new ThrottleSetting(2000, Command.FKEY, 2, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
1052        }
1053        w.addThrottleCommand(new ThrottleSetting(500, Command.FKEY, 0, ValueType.VAL_OFF, SpeedStepMode.UNKNOWN, 0, blockName));
1054        return null;
1055    }
1056
1057    private static final Logger log = LoggerFactory.getLogger(NXFrame.class);
1058}