001package jmri.jmrit.dispatcher;
002
003import java.awt.BorderLayout;
004import java.awt.Component;
005import java.awt.Container;
006import java.awt.Dimension;
007import java.awt.FlowLayout;
008import java.awt.event.ActionEvent;
009import java.awt.event.ActionListener;
010import java.awt.event.ItemEvent;
011import java.util.ArrayList;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Locale;
015import java.util.Set;
016
017import javax.swing.BorderFactory;
018import javax.swing.BoxLayout;
019import javax.swing.ButtonGroup;
020import javax.swing.JButton;
021import javax.swing.JCheckBox;
022import javax.swing.JComboBox;
023import javax.swing.JLabel;
024import javax.swing.JPanel;
025import javax.swing.JRadioButton;
026import javax.swing.JScrollPane;
027import javax.swing.JSeparator;
028import javax.swing.JSpinner;
029import javax.swing.JTable;
030import javax.swing.JTextField;
031import javax.swing.ScrollPaneConstants;
032import javax.swing.SpinnerNumberModel;
033import javax.swing.table.DefaultTableModel;
034import javax.swing.table.TableCellRenderer;
035import javax.swing.table.TableColumnModel;
036import javax.swing.DefaultListCellRenderer;
037import javax.swing.JList;
038import java.awt.Color;
039
040import jmri.Block;
041import jmri.jmrit.display.layoutEditor.LayoutBlock;
042import jmri.jmrit.display.layoutEditor.LayoutBlockManager;
043import jmri.InstanceManager;
044import jmri.Sensor;
045import jmri.Transit;
046import jmri.TransitManager;
047import jmri.UserPreferencesManager;
048import jmri.jmrit.dispatcher.ActiveTrain.TrainDetection;
049import jmri.jmrit.dispatcher.ActiveTrain.TrainLengthUnits;
050import jmri.jmrit.dispatcher.DispatcherFrame.TrainsFrom;
051import jmri.jmrit.operations.trains.Train;
052import jmri.jmrit.operations.trains.TrainManager;
053import jmri.jmrit.roster.RosterEntry;
054import jmri.jmrit.roster.RosterSpeedProfile;
055import jmri.jmrit.roster.swing.RosterEntryComboBox;
056import jmri.jmrit.roster.swing.RosterEntrySelectorPanel;
057import jmri.jmrit.roster.swing.RosterGroupComboBox;
058import jmri.swing.NamedBeanComboBox;
059import jmri.util.JmriJFrame;
060import jmri.util.swing.JComboBoxUtil;
061import jmri.util.swing.JmriJOptionPane;
062
063/**
064 * Displays the Activate New Train Frame and processes information entered
065 * there.
066 * <p>
067 * This module works with Dispatcher, which initiates the display of this Frame.
068 * Dispatcher also creates the ActiveTrain.
069 * <p>
070 * This file is part of JMRI.
071 * <p>
072 * JMRI is open source software; you can redistribute it and/or modify it under
073 * the terms of version 2 of the GNU General Public License as published by the
074 * Free Software Foundation. See the "COPYING" file for a copy of this license.
075 * <p>
076 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
077 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
078 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
079 *
080 * @author Dave Duchamp Copyright (C) 2009
081 */
082public class ActivateTrainFrame extends JmriJFrame {
083
084    public ActivateTrainFrame(DispatcherFrame d) {
085        super(true,true);
086        _dispatcher = d;
087        _tiFile = new TrainInfoFile();
088    }
089
090    // operational instance variables
091    private DispatcherFrame _dispatcher = null;
092    private TrainInfoFile _tiFile = null;
093    private final TransitManager _TransitManager = InstanceManager.getDefault(jmri.TransitManager.class);
094    private String _trainInfoName = "";
095    UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class);
096    String upmGroupName = this.getClass().getName() + ".rosterGroupSelector";
097
098    // initiate train window variables
099    private Transit selectedTransit = null;
100    //private String selectedTrain = "";
101    private JmriJFrame initiateFrame = null;
102    private Container initiatePane = null;
103    private final jmri.swing.NamedBeanComboBox<Transit> transitSelectBox = new jmri.swing.NamedBeanComboBox<>(_TransitManager);
104    private final JComboBox<Object> trainSelectBox = new JComboBox<>();
105    // private final List<RosterEntry> trainBoxList = new ArrayList<>();
106    private RosterEntrySelectorPanel rosterComboBox = null;
107    private final JLabel trainFieldLabel = new JLabel(Bundle.getMessage("TrainBoxLabel") + ":");
108    private final JTextField trainNameField = new JTextField(10);
109    private final JLabel dccAddressFieldLabel = new JLabel("     " + Bundle.getMessage("DccAddressFieldLabel") + ":");
110    private final JSpinner dccAddressSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 9999, 1));
111    private final JCheckBox inTransitBox = new JCheckBox(Bundle.getMessage("TrainInTransit"));
112    private final JComboBox<String> startingBlockBox = new JComboBox<>();
113    private final JComboBox<String> viaBlockBox = new JComboBox<>();
114    private final JLabel viaBlockBoxLabel = new JLabel(Bundle.getMessage("ViaBlockBoxLabel"));
115    private List<Block> startingBlockBoxList = new ArrayList<>();
116    private final List<Block> viaBlockBoxList = new ArrayList<>();
117    private List<Integer> startingBlockSeqList = new ArrayList<>();
118    private final JComboBox<String> destinationBlockBox = new JComboBox<>();
119
120    private List<Block> destinationBlockBoxList = new ArrayList<>();
121    private List<Integer> destinationBlockSeqList = new ArrayList<>();
122    private JButton addNewTrainButton = null;
123    private JButton loadButton = null;
124    private JButton saveButton = null;
125    private JButton saveAsTemplateButton  = null;
126    private JButton deleteButton = null;
127    private final JCheckBox autoRunBox = new JCheckBox(Bundle.getMessage("AutoRun"));
128    private final JCheckBox loadAtStartupBox = new JCheckBox(Bundle.getMessage("LoadAtStartup"));
129
130    private final JRadioButton radioTrainsFromRoster = new JRadioButton(Bundle.getMessage("TrainsFromRoster"));
131    private final JRadioButton radioTrainsFromOps = new JRadioButton(Bundle.getMessage("TrainsFromTrains"));
132    private final JRadioButton radioTrainsFromUser = new JRadioButton(Bundle.getMessage("TrainsFromUser"));
133    private final JRadioButton radioTrainsFromSetLater = new JRadioButton(Bundle.getMessage("TrainsFromSetLater"));
134    private final ButtonGroup trainsFromButtonGroup = new ButtonGroup();
135
136    private final JRadioButton radioTransitsPredefined = new JRadioButton(Bundle.getMessage("TransitsPredefined"));
137    private final JRadioButton radioTransitsAdHoc = new JRadioButton(Bundle.getMessage("TransitsAdHoc"));
138    private final ButtonGroup transitsFromButtonGroup = new ButtonGroup();
139    //private final JCheckBox adHocCloseLoop = new JCheckBox(Bundle.getMessage("TransitCloseLoop"));
140
141    private final JRadioButton allocateBySafeRadioButton = new JRadioButton(Bundle.getMessage("ToSafeSections"));
142    private final JRadioButton allocateAllTheWayRadioButton = new JRadioButton(Bundle.getMessage("AsFarAsPos"));
143    private final JRadioButton allocateNumberOfBlocks = new JRadioButton(Bundle.getMessage("NumberOfBlocks") + ":");
144    private final ButtonGroup allocateMethodButtonGroup = new ButtonGroup();
145    private final JSpinner allocateCustomSpinner = new JSpinner(new SpinnerNumberModel(3, 1, 100, 1));
146    private final JCheckBox terminateWhenDoneBox = new JCheckBox(Bundle.getMessage("TerminateWhenDone"));
147    private final JPanel terminateWhenDoneDetails = new JPanel();
148    private final JComboBox<String> nextTrain = new JComboBox<>();
149    private final JLabel nextTrainLabel = new JLabel(Bundle.getMessage("TerminateWhenDoneNextTrain"));
150    private final JSpinner prioritySpinner = new JSpinner(new SpinnerNumberModel(5, 0, 100, 1));
151    private final JCheckBox resetWhenDoneBox = new JCheckBox(Bundle.getMessage("ResetWhenDone"));
152    private final JCheckBox reverseAtEndBox = new JCheckBox(Bundle.getMessage("ReverseAtEnd"));
153
154    int[] delayedStartInt = new int[]{ActiveTrain.NODELAY, ActiveTrain.TIMEDDELAY, ActiveTrain.SENSORDELAY};
155    String[] delayedStartString = new String[]{Bundle.getMessage("DelayedStartNone"), Bundle.getMessage("DelayedStartTimed"), Bundle.getMessage("DelayedStartSensor")};
156
157    private final JComboBox<String> reverseDelayedRestartType = new JComboBox<>(delayedStartString);
158    private final JLabel delayReverseReStartLabel = new JLabel(Bundle.getMessage("DelayRestart"));
159    private final JLabel delayReverseReStartSensorLabel = new JLabel(Bundle.getMessage("RestartSensor"));
160    private final JCheckBox delayReverseResetSensorBox = new JCheckBox(Bundle.getMessage("ResetRestartSensor"));
161    private final NamedBeanComboBox<Sensor> delayReverseReStartSensor = new NamedBeanComboBox<>(InstanceManager.sensorManagerInstance());
162    private final JSpinner delayReverseMinSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
163    private final JLabel delayReverseMinLabel = new JLabel(Bundle.getMessage("RestartTimed"));
164
165    private final JCheckBox resetStartSensorBox = new JCheckBox(Bundle.getMessage("ResetStartSensor"));
166    private final JComboBox<String> delayedStartBox = new JComboBox<>(delayedStartString);
167    private final JLabel delayedReStartLabel = new JLabel(Bundle.getMessage("DelayRestart"));
168    private final JLabel delayReStartSensorLabel = new JLabel(Bundle.getMessage("RestartSensor"));
169    private final JCheckBox resetRestartSensorBox = new JCheckBox(Bundle.getMessage("ResetRestartSensor"));
170    private final JComboBox<String> delayedReStartBox = new JComboBox<>(delayedStartString);
171    private final NamedBeanComboBox<Sensor> delaySensor = new NamedBeanComboBox<>(InstanceManager.sensorManagerInstance());
172    private final NamedBeanComboBox<Sensor> delayReStartSensor = new NamedBeanComboBox<>(InstanceManager.sensorManagerInstance());
173
174    private final JSpinner departureHrSpinner = new JSpinner(new SpinnerNumberModel(8, 0, 23, 1));
175    private final JSpinner departureMinSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 59, 1));
176    private final JLabel departureTimeLabel = new JLabel(Bundle.getMessage("DepartureTime"));
177    private final JLabel departureSepLabel = new JLabel(":");
178
179    private final JSpinner delayMinSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 1000, 1));
180    private final JLabel delayMinLabel = new JLabel(Bundle.getMessage("RestartTimed"));
181
182    private final JComboBox<String> trainTypeBox = new JComboBox<>();
183    // Note: See also items related to automatically running trains near the end of this module
184
185    boolean transitsFromSpecificBlock = false;
186
187    private TrainInfo trainInfo;
188
189    private final String nameOfTemplateFile="TrainInfoDefaultTemplate.xml";
190    // to be added and removed.
191    private final ActionListener viaBlockBoxListener = e -> handleViaBlockSelectionChanged();
192    // roster entries excluded due to already in use.
193    private ArrayList<RosterEntry> excludedRosterEntries;
194
195    /**
196     * Open up a new train window for a given roster entry located in a specific
197     * block.
198     *
199     * @param e  the action event triggering the new window
200     * @param re the roster entry to open the new window for
201     * @param b  the block where the train is located
202     */
203    public void initiateTrain(ActionEvent e, RosterEntry re, Block b) {
204        initiateTrain(e);
205        if (trainInfo.getTrainsFrom() == TrainsFrom.TRAINSFROMROSTER && re != null) {
206            setRosterEntryBox(rosterComboBox, re.getId());
207            //Add in some bits of code as some point to filter down the transits that can be used.
208        }
209        if (b != null && selectedTransit != null) {
210            List<Transit> transitList = _TransitManager.getListUsingBlock(b);
211            List<Transit> transitEntryList = _TransitManager.getListEntryBlock(b);
212            for (Transit t : transitEntryList) {
213                if (!transitList.contains(t)) {
214                    transitList.add(t);
215                }
216            }
217            transitsFromSpecificBlock = true;
218            initializeFreeTransitsCombo(transitList);
219            List<Block> tmpBlkList = new ArrayList<>();
220            if (selectedTransit.getEntryBlocksList().contains(b)) {
221                tmpBlkList = selectedTransit.getEntryBlocksList();
222                inTransitBox.setSelected(false);
223            } else if (selectedTransit.containsBlock(b)) {
224                tmpBlkList = selectedTransit.getInternalBlocksList();
225                inTransitBox.setSelected(true);
226            }
227            List<Integer> tmpSeqList = selectedTransit.getBlockSeqList();
228            for (int i = 0; i < tmpBlkList.size(); i++) {
229                if (tmpBlkList.get(i) == b) {
230                    setComboBox(startingBlockBox, getBlockName(b) + "-" + tmpSeqList.get(i));
231                    break;
232                }
233            }
234        }
235    }
236
237    /**
238     * Displays a window that allows a new ActiveTrain to be activated.
239     * <p>
240     * Called by Dispatcher in response to the dispatcher clicking the New Train
241     * button.
242     *
243     * @param e the action event triggering the window display
244     */
245    protected void initiateTrain(ActionEvent e) {
246        // set Dispatcher defaults
247        // create window if needed
248        // if template exists open it
249        try {
250            trainInfo = _tiFile.readTrainInfo(nameOfTemplateFile);
251            if (trainInfo == null) {
252                trainInfo = new TrainInfo();
253            }
254        } catch (java.io.IOException ioe) {
255            log.error("IO Exception when reading train info file", ioe);
256            return;
257        } catch (org.jdom2.JDOMException jde) {
258            log.error("JDOM Exception when reading train info file", jde);
259            return;
260        }
261
262        if (initiateFrame == null) {
263            initiateFrame = this;
264            initiateFrame.setTitle(Bundle.getMessage("AddTrainTitle"));
265            initiateFrame.addHelpMenu("package.jmri.jmrit.dispatcher.NewTrain", true);
266            initiatePane = initiateFrame.getContentPane();
267            initiatePane.setLayout(new BoxLayout(initiatePane, BoxLayout.Y_AXIS));
268
269            // add buttons to load and save train information
270            JPanel hdr = new JPanel();
271            hdr.add(loadButton = new JButton(Bundle.getMessage("LoadButton")));
272            loadButton.addActionListener(this::loadTrainInfo);
273            loadButton.setToolTipText(Bundle.getMessage("LoadButtonHint"));
274            hdr.add(saveButton = new JButton(Bundle.getMessage("SaveButton")));
275            saveButton.addActionListener( ev -> saveTrainInfo());
276            saveButton.setToolTipText(Bundle.getMessage("SaveButtonHint"));
277            hdr.add(saveAsTemplateButton = new JButton(Bundle.getMessage("SaveAsTemplateButton")));
278            saveAsTemplateButton.addActionListener( ev -> saveTrainInfoAsTemplate());
279            saveAsTemplateButton.setToolTipText(Bundle.getMessage("SaveAsTemplateButtonHint"));
280            hdr.add(deleteButton = new JButton(Bundle.getMessage("DeleteButton")));
281            deleteButton.addActionListener( ev -> deleteTrainInfo());
282            deleteButton.setToolTipText(Bundle.getMessage("DeleteButtonHint"));
283
284            // add items relating to both manually run and automatic trains.
285
286            // Trains From choices.
287            JPanel p1 = new JPanel();
288            p1.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TrainsFrom")));
289            radioTrainsFromRoster.setActionCommand("TRAINSFROMROSTER");
290            trainsFromButtonGroup.add(radioTrainsFromRoster);
291            radioTrainsFromOps.setActionCommand("TRAINSFROMOPS");
292            trainsFromButtonGroup.add(radioTrainsFromOps);
293            radioTrainsFromUser.setActionCommand("TRAINSFROMUSER");
294            trainsFromButtonGroup.add(radioTrainsFromUser);
295            radioTrainsFromSetLater.setActionCommand("TRAINSFROMSETLATER");
296            trainsFromButtonGroup.add(radioTrainsFromSetLater);
297            p1.add(radioTrainsFromRoster);
298            radioTrainsFromRoster.setToolTipText(Bundle.getMessage("TrainsFromRosterHint"));
299            p1.add(radioTrainsFromOps);
300            radioTrainsFromOps.setToolTipText(Bundle.getMessage("TrainsFromTrainsHint"));
301            p1.add(radioTrainsFromUser);
302            radioTrainsFromUser.setToolTipText(Bundle.getMessage("TrainsFromUserHint"));
303            p1.add(radioTrainsFromSetLater);
304            radioTrainsFromSetLater.setToolTipText(Bundle.getMessage("TrainsFromSetLaterHint"));
305
306            radioTrainsFromOps.addItemListener( e1 -> {
307                if (e1.getStateChange() == ItemEvent.SELECTED) {
308                    setTrainsFromOptions(TrainsFrom.TRAINSFROMOPS);
309                }
310            });
311            radioTrainsFromRoster.addItemListener( e1 -> {
312                if (e1.getStateChange() == ItemEvent.SELECTED) {
313                    setTrainsFromOptions(TrainsFrom.TRAINSFROMROSTER);
314                }
315            });
316            radioTrainsFromUser.addItemListener( e1 -> {
317                if (e1.getStateChange() == ItemEvent.SELECTED) {
318                    setTrainsFromOptions(TrainsFrom.TRAINSFROMUSER);
319                }
320            });
321            radioTrainsFromSetLater.addItemListener( e1 -> {
322                if (e1.getStateChange() == ItemEvent.SELECTED) {
323                    setTrainsFromOptions(TrainsFrom.TRAINSFROMSETLATER);
324                }
325            });
326            initiatePane.add(p1);
327
328            // Select train
329            JPanel p2 = new JPanel();
330
331            // Dispatcher train name
332            p2.add(trainFieldLabel);
333            p2.add(trainNameField);
334            trainNameField.setToolTipText(Bundle.getMessage("TrainFieldHint"));
335
336            // Roster combo box
337            rosterComboBox = new RosterEntrySelectorPanel(null,upm.getComboBoxLastSelection(upmGroupName));
338            rosterComboBox.getRosterGroupComboBox().addActionListener( e3 -> {
339                    String s =((RosterGroupComboBox) e3.getSource()).getSelectedItem();
340                    upm.setComboBoxLastSelection(upmGroupName, s);
341            });
342            initializeFreeRosterEntriesCombo();
343            rosterComboBox.getRosterEntryComboBox().addActionListener(this::handleRosterSelectionChanged);
344            p2.add(rosterComboBox);
345
346            // Operations combo box
347            p2.add(trainSelectBox);
348            trainSelectBox.addActionListener( e1 -> handleTrainSelectionChanged());
349            trainSelectBox.setToolTipText(Bundle.getMessage("TrainBoxHint"));
350
351            // DCC address selector
352            p2.add(dccAddressFieldLabel);
353            p2.add(dccAddressSpinner);
354            dccAddressSpinner.setToolTipText(Bundle.getMessage("DccAddressFieldHint"));
355
356            initiatePane.add(p2);
357
358            // Select transit type
359            JPanel p3 = new JPanel();
360            p3.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TransitsFrom")));
361            radioTransitsPredefined.setActionCommand("USETRANSITS");
362            transitsFromButtonGroup.add(radioTransitsPredefined);
363            radioTransitsAdHoc.setActionCommand("USEADHOC");
364            transitsFromButtonGroup.add(radioTransitsAdHoc);
365            p3.add(radioTransitsPredefined);
366            radioTransitsPredefined.setToolTipText(Bundle.getMessage("TransitsPredefinedHint"));
367            p3.add(radioTransitsAdHoc);
368            radioTransitsAdHoc.setToolTipText(Bundle.getMessage("TransitsAdHocHint"));
369            radioTransitsPredefined.addItemListener( e1 -> {
370                if (e1.getStateChange() == ItemEvent.SELECTED) {
371                    transitSelectBox.setEnabled(true);
372                    //adHocCloseLoop.setEnabled(false);
373                    inTransitBox.setEnabled(true);
374                    handleInTransitClick();
375                    viaBlockBox.setVisible(false);
376                    viaBlockBoxLabel.setVisible(false);
377                }
378            });
379            radioTransitsAdHoc.addItemListener( e1 -> {
380                if (e1.getStateChange() == ItemEvent.SELECTED) {
381                    checkAdvancedRouting();
382                    transitSelectBox.setEnabled(false);
383                    //adHocCloseLoop.setEnabled(true);
384                    inTransitBox.setEnabled(false);
385                    inTransitBox.setSelected(true);
386                    initializeStartingBlockComboDynamic();
387                    viaBlockBox.setVisible(true);
388                    viaBlockBoxLabel.setVisible(true);
389                }
390            });
391
392            //p3.add(adHocCloseLoop);
393            //adHocCloseLoop.setToolTipText(Bundle.getMessage("TransitCloseLoopHint"));
394
395            p3.add(new JLabel(Bundle.getMessage("TransitBoxLabel") + " :"));
396            p3.add(transitSelectBox);
397            transitSelectBox.addActionListener(this::handleTransitSelectionChanged);
398            transitSelectBox.setToolTipText(Bundle.getMessage("TransitBoxHint"));
399            initiatePane.add(p3);
400
401            // Train in transit
402            JPanel p4 = new JPanel();
403            p4.add(inTransitBox);
404            inTransitBox.addActionListener( ev -> handleInTransitClick());
405            inTransitBox.setToolTipText(Bundle.getMessage("InTransitBoxHint"));
406            initiatePane.add(p4);
407
408            // Starting block, add Via for adhoc transits
409            JPanel p5 = new JPanel();
410            p5.add(new JLabel(Bundle.getMessage("StartingBlockBoxLabel") + " :"));
411            p5.add(startingBlockBox);
412            startingBlockBox.setToolTipText(Bundle.getMessage("StartingBlockBoxHint"));
413            startingBlockBox.addActionListener( ev -> handleStartingBlockSelectionChanged());
414            p5.add(viaBlockBoxLabel);
415            p5.add(viaBlockBox);
416            viaBlockBox.setToolTipText(Bundle.getMessage("ViaBlockBoxHint"));
417            viaBlockBox.addActionListener(viaBlockBoxListener);
418            initiatePane.add(p5);
419
420            // Destination block
421            JPanel p6 = new JPanel();
422            p6.add(new JLabel(Bundle.getMessage("DestinationBlockBoxLabel") + ":"));
423            p6.add(destinationBlockBox);
424            destinationBlockBox.setToolTipText(Bundle.getMessage("DestinationBlockBoxHint"));
425            initiatePane.add(p6);
426
427            // Train detection scope
428            JPanel p7 = new JPanel();
429            p7.add(trainDetectionLabel);
430            initializeTrainDetectionBox();
431            p7.add(trainDetectionComboBox);
432            trainDetectionComboBox.setToolTipText(Bundle.getMessage("TrainDetectionBoxHint"));
433            initiatePane.add(p7);
434
435            // Allocation method
436            JPanel p8 = new JPanel();
437            p8.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("AllocateMethodLabel")));
438            allocateMethodButtonGroup.add(allocateAllTheWayRadioButton);
439            allocateMethodButtonGroup.add(allocateBySafeRadioButton);
440            allocateMethodButtonGroup.add(allocateNumberOfBlocks);
441            p8.add(allocateAllTheWayRadioButton);
442            allocateAllTheWayRadioButton.setToolTipText(Bundle.getMessage("AllocateAllTheWayHint"));
443            p8.add(allocateBySafeRadioButton);
444            allocateBySafeRadioButton.setToolTipText(Bundle.getMessage("AllocateSafeHint"));
445            p8.add(allocateNumberOfBlocks);
446            allocateNumberOfBlocks.setToolTipText(Bundle.getMessage("AllocateMethodHint"));
447            allocateAllTheWayRadioButton.addActionListener( ev -> handleAllocateAllTheWayButtonChanged());
448            allocateBySafeRadioButton.addActionListener( ev -> handleAllocateBySafeButtonChanged());
449            allocateNumberOfBlocks.addActionListener( ev -> handleAllocateNumberOfBlocksButtonChanged());
450            p8.add(allocateCustomSpinner);
451            allocateCustomSpinner.setToolTipText(Bundle.getMessage("AllocateMethodHint"));
452            initiatePane.add(p8);
453
454            // Restart at end
455            JPanel p9 = new JPanel();
456            p9.add(resetWhenDoneBox);
457            resetWhenDoneBox.addActionListener( ev -> handleResetWhenDoneClick());
458            resetWhenDoneBox.setToolTipText(Bundle.getMessage("ResetWhenDoneBoxHint"));
459            initiatePane.add(p9);
460
461            // Restart using sensor
462            JPanel p9a = new JPanel();
463            ((FlowLayout) p9a.getLayout()).setVgap(1);
464            p9a.add(delayedReStartLabel);
465            p9a.add(delayedReStartBox);
466            p9a.add(resetRestartSensorBox);
467            resetRestartSensorBox.setToolTipText(Bundle.getMessage("ResetRestartSensorHint"));
468            resetRestartSensorBox.setSelected(true);
469            delayedReStartBox.addActionListener( ev -> handleResetWhenDoneClick());
470            delayedReStartBox.setToolTipText(Bundle.getMessage("DelayedReStartHint"));
471            initiatePane.add(p9a);
472
473            // Restart using timer
474            JPanel p9b = new JPanel();
475            ((FlowLayout) p9b.getLayout()).setVgap(1);
476            p9b.add(delayMinLabel);
477            p9b.add(delayMinSpinner); // already set to 0
478            delayMinSpinner.setToolTipText(Bundle.getMessage("RestartTimedHint"));
479            p9b.add(delayReStartSensorLabel);
480            p9b.add(delayReStartSensor);
481            delayReStartSensor.setAllowNull(true);
482            handleResetWhenDoneClick();
483            initiatePane.add(p9b);
484
485            initiatePane.add(new JSeparator());
486
487            // Reverse at end
488            JPanel p10 = new JPanel();
489            p10.add(reverseAtEndBox);
490            reverseAtEndBox.setToolTipText(Bundle.getMessage("ReverseAtEndBoxHint"));
491            initiatePane.add(p10);
492            reverseAtEndBox.addActionListener( ev -> handleReverseAtEndBoxClick());
493
494            // Reverse using sensor
495            JPanel pDelayReverseRestartDetails = new JPanel();
496            ((FlowLayout) pDelayReverseRestartDetails.getLayout()).setVgap(1);
497            pDelayReverseRestartDetails.add(delayReverseReStartLabel);
498            pDelayReverseRestartDetails.add(reverseDelayedRestartType);
499            pDelayReverseRestartDetails.add(delayReverseResetSensorBox);
500            delayReverseResetSensorBox.setToolTipText(Bundle.getMessage("ReverseResetRestartSensorHint"));
501            delayReverseResetSensorBox.setSelected(true);
502            reverseDelayedRestartType.addActionListener( ev -> handleReverseAtEndBoxClick());
503            reverseDelayedRestartType.setToolTipText(Bundle.getMessage("ReverseDelayedReStartHint"));
504            initiatePane.add(pDelayReverseRestartDetails);
505
506            // Reverse using timer
507            JPanel pDelayReverseRestartDetails2 = new JPanel();
508            ((FlowLayout) pDelayReverseRestartDetails2.getLayout()).setVgap(1);
509            pDelayReverseRestartDetails2.add(delayReverseMinLabel);
510            pDelayReverseRestartDetails2.add(delayReverseMinSpinner); // already set to 0
511            delayReverseMinSpinner.setToolTipText(Bundle.getMessage("ReverseRestartTimedHint"));
512            pDelayReverseRestartDetails2.add(delayReverseReStartSensorLabel);
513            pDelayReverseRestartDetails2.add(delayReverseReStartSensor);
514            delayReverseReStartSensor.setAllowNull(true);
515            handleReverseAtEndBoxClick();
516            initiatePane.add(pDelayReverseRestartDetails2);
517
518            initiatePane.add(new JSeparator());
519
520            // Terminate when done option
521            JPanel p11 = new JPanel();
522            p11.setLayout(new FlowLayout());
523            p11.add(terminateWhenDoneBox);
524            terminateWhenDoneBox.addActionListener( ev -> handleTerminateWhenDoneBoxClick());
525            initiatePane.add(p11);
526
527            // Optional next train, tied to terminate when done.
528            terminateWhenDoneDetails.setLayout(new FlowLayout());
529            terminateWhenDoneDetails.add(nextTrainLabel);
530            terminateWhenDoneDetails.add(nextTrain);
531            nextTrain.setToolTipText(Bundle.getMessage("TerminateWhenDoneNextTrainHint"));
532            initiatePane.add(terminateWhenDoneDetails);
533            handleTerminateWhenDoneBoxClick();
534
535            initiatePane.add(new JSeparator());
536
537            // Priority and train type.
538            JPanel p12 = new JPanel();
539            p12.setLayout(new FlowLayout());
540            p12.add(new JLabel(Bundle.getMessage("PriorityLabel") + ":"));
541            p12.add(prioritySpinner); // already set to 5
542            prioritySpinner.setToolTipText(Bundle.getMessage("PriorityHint"));
543            p12.add(new JLabel("     "));
544            p12.add(new JLabel(Bundle.getMessage("TrainTypeBoxLabel")));
545            initializeTrainTypeBox();
546            p12.add(trainTypeBox);
547            trainTypeBox.setSelectedIndex(1);
548            trainTypeBox.setToolTipText(Bundle.getMessage("TrainTypeBoxHint"));
549            initiatePane.add(p12);
550
551            // Delayed start option
552            JPanel p13 = new JPanel();
553            p13.add(new JLabel(Bundle.getMessage("DelayedStart")));
554            p13.add(delayedStartBox);
555            delayedStartBox.setToolTipText(Bundle.getMessage("DelayedStartHint"));
556            delayedStartBox.addActionListener(this::handleDelayStartClick);
557            p13.add(departureTimeLabel);
558            departureHrSpinner.setEditor(new JSpinner.NumberEditor(departureHrSpinner, "00"));
559            p13.add(departureHrSpinner);
560            departureHrSpinner.setValue(8);
561            departureHrSpinner.setToolTipText(Bundle.getMessage("DepartureTimeHrHint"));
562            p13.add(departureSepLabel);
563            departureMinSpinner.setEditor(new JSpinner.NumberEditor(departureMinSpinner, "00"));
564            p13.add(departureMinSpinner);
565            departureMinSpinner.setValue(0);
566            departureMinSpinner.setToolTipText(Bundle.getMessage("DepartureTimeMinHint"));
567            p13.add(delaySensor);
568            delaySensor.setAllowNull(true);
569            p13.add(resetStartSensorBox);
570            resetStartSensorBox.setToolTipText(Bundle.getMessage("ResetStartSensorHint"));
571            resetStartSensorBox.setSelected(true);
572            handleDelayStartClick(null);
573            initiatePane.add(p13);
574
575            // Load at startup option
576            JPanel p14 = new JPanel();
577            p14.setLayout(new FlowLayout());
578            p14.add(loadAtStartupBox);
579            loadAtStartupBox.setToolTipText(Bundle.getMessage("LoadAtStartupBoxHint"));
580            loadAtStartupBox.setSelected(false);
581            initiatePane.add(p14);
582
583            // Auto run option
584            initiatePane.add(new JSeparator());
585            JPanel p15 = new JPanel();
586            p15.add(autoRunBox);
587            autoRunBox.addActionListener( ev -> handleAutoRunClick());
588            autoRunBox.setToolTipText(Bundle.getMessage("AutoRunBoxHint"));
589            autoRunBox.setSelected(false);
590            initiatePane.add(p15);
591            initializeAutoRunItems();
592
593            // Footer buttons
594            JPanel ftr = new JPanel();
595            JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
596            ftr.add(cancelButton);
597            cancelButton.addActionListener( ev -> cancelInitiateTrain());
598            cancelButton.setToolTipText(Bundle.getMessage("CancelButtonHint"));
599            ftr.add(addNewTrainButton = new JButton(Bundle.getMessage("ButtonCreate")));
600            addNewTrainButton.addActionListener( e1 -> addNewTrain());
601            addNewTrainButton.setToolTipText(Bundle.getMessage("AddNewTrainButtonHint"));
602
603            JPanel mainPane = new JPanel(new BorderLayout());
604            JScrollPane scrPane = new JScrollPane(initiatePane);
605            mainPane.add(hdr, BorderLayout.NORTH);
606            mainPane.add(scrPane, BorderLayout.CENTER);
607            mainPane.add(ftr, BorderLayout.SOUTH);
608            initiateFrame.setContentPane(mainPane);
609            switch (trainInfo.getTrainsFrom()) {
610                case TRAINSFROMROSTER:
611                    radioTrainsFromRoster.setSelected(true);
612                    break;
613                case TRAINSFROMOPS:
614                    radioTrainsFromOps.setSelected(true);
615                    break;
616                case TRAINSFROMUSER:
617                    radioTrainsFromUser.setSelected(true);
618                    break;
619                case TRAINSFROMSETLATER:
620                default:
621                    radioTrainsFromSetLater.setSelected(true);
622            }
623
624        }
625        autoRunBox.setSelected(false);
626        loadAtStartupBox.setSelected(false);
627        initializeFreeTransitsCombo(new ArrayList<>());
628        refreshNextTrainCombo();
629        setTrainsFromOptions(trainInfo.getTrainsFrom());
630        initiateFrame.pack();
631        initiateFrame.setVisible(true);
632
633        trainInfoToDialog(trainInfo);
634    }
635
636    private void refreshNextTrainCombo() {
637        Object saveEntry = null;
638        if (nextTrain.getSelectedIndex() > 0) {
639            saveEntry=nextTrain.getSelectedItem();
640        }
641        nextTrain.removeAllItems();
642        nextTrain.addItem(" ");
643        for (String file: _tiFile.getTrainInfoFileNames()) {
644            nextTrain.addItem(file);
645        }
646        if (saveEntry != null) {
647            nextTrain.setSelectedItem(saveEntry);
648        }
649    }
650
651    private void setTrainsFromOptions(TrainsFrom transFrom) {
652        switch (transFrom) {
653            case TRAINSFROMROSTER:
654                initializeFreeRosterEntriesCombo();
655                rosterComboBox.setVisible(true);
656                trainSelectBox.setVisible(false);
657                trainFieldLabel.setVisible(true);
658                trainNameField.setVisible(true);
659                dccAddressFieldLabel.setVisible(false);
660                dccAddressSpinner.setVisible(false);
661                break;
662            case TRAINSFROMOPS:
663                initializeFreeTrainsCombo();
664                trainSelectBox.setVisible(true);
665                rosterComboBox.setVisible(false);
666                trainFieldLabel.setVisible(true);
667                trainNameField.setVisible(true);
668                dccAddressFieldLabel.setVisible(true);
669                dccAddressSpinner.setVisible(true);
670                setSpeedProfileOptions(trainInfo,false);
671                break;
672            case TRAINSFROMUSER:
673                trainNameField.setText("");
674                trainSelectBox.setVisible(false);
675                rosterComboBox.setVisible(false);
676                trainFieldLabel.setVisible(true);
677                trainNameField.setVisible(true);
678                dccAddressFieldLabel.setVisible(true);
679                dccAddressSpinner.setVisible(true);
680                dccAddressSpinner.setEnabled(true);
681                setSpeedProfileOptions(trainInfo,false);
682                break;
683            case TRAINSFROMSETLATER:
684            default:
685                rosterComboBox.setVisible(false);
686                trainSelectBox.setVisible(false);
687                trainFieldLabel.setVisible(true);
688                trainNameField.setVisible(true);
689                dccAddressFieldLabel.setVisible(false);
690                dccAddressSpinner.setVisible(false);
691                setSpeedProfileOptions(trainInfo, true);
692        }
693    }
694    
695
696    // MPH↔KMH conversion helpers
697    private static float mphToKmh(float mph) { return mph * 1.60934f; }
698    private static float kmhToMph(float kmh) { return kmh / 1.60934f; }
699
700    // Safe Bundle lookup with fallback; avoids MissingResourceException breaking the UI.
701    private static String bundleOrDefault(String key, String fallback) {
702        try {
703            return Bundle.getMessage(key);
704        } catch (Exception ex) {
705            return fallback;
706        }
707    }
708    
709
710     // Safe access to current layout scale ratio (prototype/model length ratio)
711     private float getScaleRatioSafe() {
712         return (_dispatcher.getScale() != null)
713                 ? (float) _dispatcher.getScale().getScaleRatio()
714                 : 1.0f; // CI-safe default
715     }
716    
717     // Do we have a concrete roster entry with a non-empty speed profile?
718     private boolean isConcreteSpeedProfileAvailable() {
719         Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
720         if (!(sel instanceof jmri.jmrit.roster.RosterEntry)) return false;
721         jmri.jmrit.roster.RosterEntry re = (jmri.jmrit.roster.RosterEntry) sel;
722         return re.getSpeedProfile() != null && re.getSpeedProfile().getProfileSize() > 0;
723     }
724
725     // Direction for profile lookups: the "Run in reverse" checkbox selects reverse running.
726     private boolean isMaxSpeedProfileForward() {
727         return !runInReverseBox.isSelected();
728     }
729    
730     // Convert throttle % -> scale mph (via mm/s from profile)
731     private float percentToScaleMph(float pct) {
732         Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
733         if (!(sel instanceof jmri.jmrit.roster.RosterEntry)) return cachedScaleMph;
734         jmri.jmrit.roster.RosterEntry re = (jmri.jmrit.roster.RosterEntry) sel;
735         jmri.jmrit.roster.RosterSpeedProfile sp = re.getSpeedProfile();
736         if (sp == null || sp.getProfileSize() < 1) return cachedScaleMph;
737    
738         float mms = sp.getSpeed(pct, isMaxSpeedProfileForward()); // mm/s for this % in the selected direction
739         float scaleRatio = getScaleRatioSafe();
740         // mm/s -> m/s -> mph, then × scale ratio (scale speed)
741         return (mms / 1000.0f) * 2.236936f * scaleRatio;
742     }
743    
744     // Convert throttle % -> scale km/h
745     private float percentToScaleKmh(float pct) {
746         Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
747         if (!(sel instanceof jmri.jmrit.roster.RosterEntry)) return cachedScaleMph * 1.60934f;
748         jmri.jmrit.roster.RosterEntry re = (jmri.jmrit.roster.RosterEntry) sel;
749         jmri.jmrit.roster.RosterSpeedProfile sp = re.getSpeedProfile();
750         if (sp == null || sp.getProfileSize() < 1) return cachedScaleMph * 1.60934f;
751    
752         float mms = sp.getSpeed(pct, isMaxSpeedProfileForward());
753         float scaleRatio = getScaleRatioSafe();
754         // mm/s -> m/s -> km/h, then × scale ratio
755         return (mms / 1000.0f) * 3.6f * scaleRatio;
756     }
757    
758     // Convert target scale speed (mph or km/h) -> throttle % by inverting the profile via bisection
759     private float scaleSpeedToPercentFromProfile(float speedValue, boolean isKmh) {
760         Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
761         if (!(sel instanceof jmri.jmrit.roster.RosterEntry)) return cachedThrottlePercent;
762         jmri.jmrit.roster.RosterEntry re = (jmri.jmrit.roster.RosterEntry) sel;
763         jmri.jmrit.roster.RosterSpeedProfile sp = re.getSpeedProfile();
764         if (sp == null || sp.getProfileSize() < 1) return cachedThrottlePercent;
765    
766         float scaleRatio = getScaleRatioSafe();
767         // scale mph/kmh -> m/s -> mm/s (model), divide by scale ratio to remove scale
768         float mps = isKmh ? (speedValue / 3.6f) : (speedValue / 2.236936f);
769         float targetMms = (mps * 1000.0f) / scaleRatio;
770    
771         // Bisection in [0.0 .. 1.0] on sp.getSpeed(%) in the selected direction
772         float lo = 0.0f, hi = 1.0f;
773         boolean fwd = isMaxSpeedProfileForward();
774         for (int i = 0; i < 24; i++) {
775             float mid = 0.5f * (lo + hi);
776             float midMms = sp.getSpeed(mid, fwd);
777             if (midMms < targetMms) lo = mid; else hi = mid;
778         }
779         float pct = 0.5f * (lo + hi);
780         // Clamp to spinner's [%] domain 0.10 .. 1.00 (the UI model)
781         if (pct < 0.10f) pct = 0.10f;
782         if (pct > 1.00f) pct = 1.00f;
783         return pct;
784     }
785    
786     // Keep the sticky caches aligned with user's edits on the numeric spinner
787     private void updateMaxSpeedCachesFromSpinner() {
788         if (suppressMaxSpeedSpinnerEvents) {
789             return;
790         }
791         float v = ((Number) maxSpeedSpinner.getValue()).floatValue();
792         switch (lastMaxSpeedCapMode) {
793            case THROTTLE:
794                // Clamp percent [0.10 .. 1.00] before caching; editor "# %" multiplies by 100 for display
795                if (v < 0.10f) v = 0.10f;
796                if (v > 1.00f) v = 1.00f;
797                cachedThrottlePercent = v;
798                break;
799             case SCALE_MPH:  cachedScaleMph       = v;             break;
800             case SCALE_KMH:  cachedScaleMph       = kmhToMph(v);   break;
801             default: break;
802             }
803         }
804
805
806     // Format the min-reliable operating speed label in the user's preferred units.
807     // When the Max Speed dropdown is in SCALE_MPH or SCALE_KMH, show "scale mph" or "scale km/h" respectively.
808     // Otherwise, fall back to the existing localized profile conversion with units.
809     private String formatScaleSpeedWithPreferredUnits(float mms) {
810         Object sel = maxSpeedCapModeBox.getSelectedItem();
811         MaxSpeedCapMode mode = (sel instanceof MaxSpeedCapModeItem)
812                 ? ((MaxSpeedCapModeItem) sel).getValue()
813                 : MaxSpeedCapMode.THROTTLE;
814    
815         // Scale speed = actual speed × scale ratio (time same in model/prototype)
816        float scaleRatio = (_dispatcher.getScale() != null)
817            ? (float) _dispatcher.getScale().getScaleRatio()
818            : 1.0f; // CI-safe default
819    
820         if (mode == MaxSpeedCapMode.SCALE_MPH) {
821             // mm/s → m/s → mph, then × scaleRatio
822             float mph = (mms / 1000.0f) * 2.236936f * scaleRatio;
823             return String.format(
824                     Locale.getDefault(),
825                     "%.1f %s",
826                     mph,
827                     Bundle.getMessage("ScaleMilesPerHourShort")  // e.g., "scale mph"
828             );
829         } else if (mode == MaxSpeedCapMode.SCALE_KMH) {
830             // mm/s → m/s → km/h, then × scaleRatio
831             float kmh = (mms / 1000.0f) * 3.6f * scaleRatio;
832             return String.format(
833                     Locale.getDefault(),
834                     "%.1f %s",
835                     kmh,
836                     Bundle.getMessage("ScaleKilometresPerHourShort")  // e.g., "scale km/h"
837             );
838         }
839    
840         // Default: use JMRI's existing localised conversion (includes units)
841         return RosterSpeedProfile.convertMMSToScaleSpeedWithUnits(mms);
842     }
843
844
845    // Switch the spinner model & editor format to match the selected cap mode
846    private void updateMaxSpeedSpinnerModelForMode(MaxSpeedCapMode mode) {
847        switch (mode) {
848            default:
849            case THROTTLE:
850                // 0.10 .. 1.00 (% throttle), step 0.01
851                maxSpeedSpinner.setModel(new SpinnerNumberModel(Float.valueOf(1.0f), Float.valueOf(0.1f), Float.valueOf(1.0f), Float.valueOf(0.01f)));
852                maxSpeedSpinner.setEditor(new JSpinner.NumberEditor(maxSpeedSpinner, "# %"));
853                maxSpeedUnitLabel.setText("%");
854                maxSpeedSpinner.setToolTipText(Bundle.getMessage("MaxSpeedHint"));
855                break;
856            case SCALE_MPH:
857                // Typical scale speeds: 1 .. 200 mph, step 0.1
858                maxSpeedSpinner.setModel(new SpinnerNumberModel(Float.valueOf(60.0f), Float.valueOf(1.0f), Float.valueOf(200.0f), Float.valueOf(0.1f)));
859                maxSpeedSpinner.setEditor(new JSpinner.NumberEditor(maxSpeedSpinner, "0.0"));
860                maxSpeedUnitLabel.setText(Bundle.getMessage("ScaleMilesPerHourShort"));
861                maxSpeedSpinner.setToolTipText(Bundle.getMessage("MaxSpeedHint")); // reuse hint
862                break;
863            case SCALE_KMH:
864                // Typical scale speeds: 1 .. 320 km/h, step 0.1
865                maxSpeedSpinner.setModel(new SpinnerNumberModel(Float.valueOf(100.0f), Float.valueOf(1.0f), Float.valueOf(320.0f), Float.valueOf(0.1f)));
866                maxSpeedSpinner.setEditor(new JSpinner.NumberEditor(maxSpeedSpinner, "0.0"));
867                maxSpeedUnitLabel.setText(Bundle.getMessage("ScaleKilometresPerHourShort"));
868                maxSpeedSpinner.setToolTipText(Bundle.getMessage("MaxSpeedHint")); // reuse hint
869                break;
870        }   
871    }
872
873     // Enable/disable speed entries depending on speed-profile availability
874     private void updateMaxSpeedCapModeAvailability(boolean speedProfileAvailable) {
875         suppressMaxSpeedCapModeEvents = true;
876         try {
877             // Remember previous selection (if any)
878             MaxSpeedCapMode prevMode = null;
879             Object previous = maxSpeedCapModeBox.getSelectedItem();
880             if (previous instanceof MaxSpeedCapModeItem) {
881                 prevMode = ((MaxSpeedCapModeItem) previous).getValue();
882             }
883    
884             // Rebuild the dropdown model to include/exclude the speed options
885             maxSpeedCapModeBox.removeAllItems();
886             maxSpeedCapModeBox.addItem(
887                 new MaxSpeedCapModeItem(Bundle.getMessage("MaxSpeedLabel"), MaxSpeedCapMode.THROTTLE)
888             );
889            if (speedProfileAvailable) {
890                maxSpeedCapModeBox.addItem(new MaxSpeedCapModeItem(
891                    Bundle.getMessage("MaxSpeedScaleMph"), MaxSpeedCapMode.SCALE_MPH));
892                maxSpeedCapModeBox.addItem(new MaxSpeedCapModeItem(
893                    Bundle.getMessage("MaxSpeedScaleKmh"), MaxSpeedCapMode.SCALE_KMH));
894            }
895    
896             // Restore the previous mode if still valid; otherwise default to THROTTLE
897             int toSelect = 0; // THROTTLE
898             if (speedProfileAvailable && (prevMode == MaxSpeedCapMode.SCALE_MPH || prevMode == MaxSpeedCapMode.SCALE_KMH)) {
899                 toSelect = (prevMode == MaxSpeedCapMode.SCALE_MPH) ? 1 : 2;
900             }
901             maxSpeedCapModeBox.setSelectedIndex(toSelect);
902    
903             // Ensure spinner model matches the (programmatically) selected mode
904             Object cur = maxSpeedCapModeBox.getSelectedItem();
905             MaxSpeedCapMode mode = (cur instanceof MaxSpeedCapModeItem)
906                     ? ((MaxSpeedCapModeItem) cur).getValue()
907                     : MaxSpeedCapMode.THROTTLE;
908             updateMaxSpeedSpinnerModelForMode(mode);
909             lastMaxSpeedCapMode = mode;  // <— keep the tracker aligned with the programmatic selection
910         } finally {
911             suppressMaxSpeedCapModeEvents = false;
912         }
913     }
914
915    private void initializeTrainTypeBox() {
916        trainTypeBox.removeAllItems();
917        trainTypeBox.addItem("<" + Bundle.getMessage("None").toLowerCase() + ">"); // <none>
918        trainTypeBox.addItem(Bundle.getMessage("LOCAL_PASSENGER"));
919        trainTypeBox.addItem(Bundle.getMessage("LOCAL_FREIGHT"));
920        trainTypeBox.addItem(Bundle.getMessage("THROUGH_PASSENGER"));
921        trainTypeBox.addItem(Bundle.getMessage("THROUGH_FREIGHT"));
922        trainTypeBox.addItem(Bundle.getMessage("EXPRESS_PASSENGER"));
923        trainTypeBox.addItem(Bundle.getMessage("EXPRESS_FREIGHT"));
924        trainTypeBox.addItem(Bundle.getMessage("MOW"));
925        // NOTE: The above must correspond in order and name to definitions in ActiveTrain.java.
926    }
927
928    private void initializeTrainDetectionBox() {
929        trainDetectionComboBox.addItem(new TrainDetectionItem(Bundle.getMessage("TrainDetectionWholeTrain"),TrainDetection.TRAINDETECTION_WHOLETRAIN));
930        trainDetectionComboBox.addItem(new TrainDetectionItem(Bundle.getMessage("TrainDetectionHeadAndTail"),TrainDetection.TRAINDETECTION_HEADANDTAIL));
931        trainDetectionComboBox.addItem(new TrainDetectionItem(Bundle.getMessage("TrainDetectionHeadOnly"),TrainDetection.TRAINDETECTION_HEADONLY));
932        trainDetectionComboBox.addActionListener(e -> updateTrainLengthVisibility());
933    }
934
935    /*
936     * A train whose tail is not always detected must have a length, whether
937     * or not it runs automatically, so show the length for it.
938     */
939    private boolean detectionNeedsLength() {
940        Object selected = trainDetectionComboBox.getSelectedItem();
941        return selected instanceof TrainDetectionItem
942                && ((TrainDetectionItem) selected).value != TrainDetection.TRAINDETECTION_WHOLETRAIN;
943    }
944
945    private void updateTrainLengthVisibility() {
946        pa4.setVisible(autoRunBox.isSelected() || detectionNeedsLength());
947    }
948
949    private void initializeScaleLengthBox() {
950        trainLengthUnitsComboBox.addItem(new TrainLengthUnitsItem(Bundle.getMessage("TrainLengthInScaleFeet"), TrainLengthUnits.TRAINLENGTH_SCALEFEET));
951        trainLengthUnitsComboBox.addItem(new TrainLengthUnitsItem(Bundle.getMessage("TrainLengthInScaleMeters"), TrainLengthUnits.TRAINLENGTH_SCALEMETERS));
952        trainLengthUnitsComboBox.addItem(new TrainLengthUnitsItem(Bundle.getMessage("TrainLengthInActualInchs"), TrainLengthUnits.TRAINLENGTH_ACTUALINCHS));
953        trainLengthUnitsComboBox.addItem(new TrainLengthUnitsItem(Bundle.getMessage("TrainLengthInActualcm"), TrainLengthUnits.TRAINLENGTH_ACTUALCM));
954    }
955
956    private void handleTransitSelectionChanged(ActionEvent e) {
957        int index = transitSelectBox.getSelectedIndex();
958        if (index < 0) {
959            return;
960        }
961        Transit t = transitSelectBox.getSelectedItem();
962        if ((t != null) && (t != selectedTransit)) {
963            selectedTransit = t;
964            initializeStartingBlockCombo();
965            initializeDestinationBlockCombo();
966            initiateFrame.pack();
967        }
968    }
969
970    private void handleInTransitClick() {
971        if (selectedTransit == null) {
972            // No transit yet; avoid NPE and present empty combos.
973            startingBlockBox.removeAllItems();
974            destinationBlockBox.removeAllItems();
975            return;
976        }
977        if (!inTransitBox.isSelected() && selectedTransit.getEntryBlocksList().isEmpty()) {
978            JmriJOptionPane.showMessageDialog(
979                initiateFrame,
980                Bundle.getMessage("NoEntryBlocks"),
981                Bundle.getMessage("MessageTitle"),
982                JmriJOptionPane.INFORMATION_MESSAGE
983            );
984            inTransitBox.setSelected(true);
985        }
986        initializeStartingBlockCombo();
987        initializeDestinationBlockCombo();
988        initiateFrame.pack();
989    }
990
991    private void handleTrainSelectionChanged() {
992        int ix = trainSelectBox.getSelectedIndex();
993        if (ix < 1) { // no train selected
994            dccAddressSpinner.setEnabled(false);
995            return;
996        }
997        dccAddressSpinner.setEnabled(true);
998        int dccAddress;
999        try {
1000            dccAddress = Integer.parseInt((((Train) trainSelectBox.getSelectedItem()).getLeadEngineDccAddress()));
1001        } catch (NumberFormatException ex) {
1002            JmriJOptionPane.showMessageDialog(initiateFrame, Bundle.getMessage("Error43"),
1003                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1004            return;
1005        }
1006        dccAddressSpinner.setValue (dccAddress);
1007        trainNameField.setText(((Train) trainSelectBox.getSelectedItem()).getName());
1008    }
1009
1010    private void handleRosterSelectionChanged(ActionEvent e) {
1011        RosterEntry r ;
1012        int ix = rosterComboBox.getRosterEntryComboBox().getSelectedIndex();
1013        if (ix > 0) { // first item is "Select Loco" string
1014             r = (RosterEntry) rosterComboBox.getRosterEntryComboBox().getSelectedItem();
1015            // check to see if speed profile exists and is not empty
1016            if (r.getSpeedProfile() == null || r.getSpeedProfile().getProfileSize() < 1) {
1017                // disable profile boxes etc.
1018                setSpeedProfileOptions(trainInfo,false);
1019            } else {
1020                // enable profile boxes
1021                setSpeedProfileOptions(trainInfo,true);
1022            }
1023            maxSpeedSpinner.setValue(r.getMaxSpeedPCT()/100.0f);
1024            trainNameField.setText(r.titleString());
1025            if (r.getAttribute("DispatcherTrainType") != null && !r.getAttribute("DispatcherTrainType").equals("")) {
1026                trainTypeBox.setSelectedItem(r.getAttribute("DispatcherTrainType"));
1027            }
1028        } else {
1029            setSpeedProfileOptions(trainInfo,false);
1030        }
1031        ensureRampRateRendererInstalled();
1032        enforcePhysicsRampSelectionAllowed();
1033        rampRateBox.repaint();
1034        handleMinReliableOperatingSpeedUpdate(); // update the min-speed label to reflect current units
1035    }
1036
1037    private void handleDelayStartClick(ActionEvent e) {
1038        departureHrSpinner.setVisible(false);
1039        departureMinSpinner.setVisible(false);
1040        departureTimeLabel.setVisible(false);
1041        departureSepLabel.setVisible(false);
1042        delaySensor.setVisible(false);
1043        resetStartSensorBox.setVisible(false);
1044        if (delayedStartBox.getSelectedItem().equals(Bundle.getMessage("DelayedStartTimed"))) {
1045            departureHrSpinner.setVisible(true);
1046            departureMinSpinner.setVisible(true);
1047            departureTimeLabel.setVisible(true);
1048            departureSepLabel.setVisible(true);
1049        } else if (delayedStartBox.getSelectedItem().equals(Bundle.getMessage("DelayedStartSensor"))) {
1050            delaySensor.setVisible(true);
1051            resetStartSensorBox.setVisible(true);
1052        }
1053        initiateFrame.pack(); // to fit extra hh:mm in window
1054    }
1055
1056    private void handleResetWhenDoneClick() {
1057        delayMinSpinner.setVisible(false);
1058        delayMinLabel.setVisible(false);
1059        delayedReStartLabel.setVisible(false);
1060        delayedReStartBox.setVisible(false);
1061        delayReStartSensorLabel.setVisible(false);
1062        delayReStartSensor.setVisible(false);
1063        resetRestartSensorBox.setVisible(false);
1064        if (resetWhenDoneBox.isSelected()) {
1065            delayedReStartLabel.setVisible(true);
1066            delayedReStartBox.setVisible(true);
1067            terminateWhenDoneBox.setSelected(false);
1068            if (delayedReStartBox.getSelectedItem().equals(Bundle.getMessage("DelayedStartTimed"))) {
1069                delayMinSpinner.setVisible(true);
1070                delayMinLabel.setVisible(true);
1071            } else if (delayedReStartBox.getSelectedItem().equals(Bundle.getMessage("DelayedStartSensor"))) {
1072                delayReStartSensor.setVisible(true);
1073                delayReStartSensorLabel.setVisible(true);
1074                resetRestartSensorBox.setVisible(true);
1075            }
1076        } else {
1077            terminateWhenDoneBox.setEnabled(true);
1078        }
1079        initiateFrame.pack();
1080    }
1081
1082    private void handleTerminateWhenDoneBoxClick() {
1083        if (terminateWhenDoneBox.isSelected()) {
1084            refreshNextTrainCombo();
1085            resetWhenDoneBox.setSelected(false);
1086            terminateWhenDoneDetails.setVisible(true);
1087        } else {
1088            terminateWhenDoneDetails.setVisible(false);
1089        }
1090    }
1091
1092    private void handleReverseAtEndBoxClick() {
1093        delayReverseMinSpinner.setVisible(false);
1094        delayReverseMinLabel.setVisible(false);
1095        delayReverseReStartLabel.setVisible(false);
1096        reverseDelayedRestartType.setVisible(false);
1097        delayReverseReStartSensorLabel.setVisible(false);
1098        delayReverseReStartSensor.setVisible(false);
1099        delayReverseResetSensorBox.setVisible(false);
1100        if (reverseAtEndBox.isSelected()) {
1101            delayReverseReStartLabel.setVisible(true);
1102            reverseDelayedRestartType.setVisible(true);
1103            if (reverseDelayedRestartType.getSelectedItem().equals(Bundle.getMessage("DelayedStartTimed"))) {
1104                delayReverseMinSpinner.setVisible(true);
1105                delayReverseMinLabel.setVisible(true);
1106            } else if (reverseDelayedRestartType.getSelectedItem().equals(Bundle.getMessage("DelayedStartSensor"))) {
1107                delayReverseReStartSensor.setVisible(true);
1108                delayReStartSensorLabel.setVisible(true);
1109                delayReverseResetSensorBox.setVisible(true);
1110            }
1111        }
1112        initiateFrame.pack();
1113
1114        if (resetWhenDoneBox.isSelected()) {
1115            terminateWhenDoneBox.setSelected(false);
1116            terminateWhenDoneBox.setEnabled(false);
1117        } else {
1118            terminateWhenDoneBox.setEnabled(true);
1119        }
1120    }
1121
1122    private void handleAutoRunClick() {
1123        showHideAutoRunItems(autoRunBox.isSelected());
1124        initiateFrame.pack();
1125    }
1126
1127    private void handleStartingBlockSelectionChanged() {
1128        if (radioTransitsAdHoc.isSelected() ) {
1129            initializeViaBlockDynamicCombo();
1130            initializeDestinationBlockDynamicCombo();
1131        } else {
1132            initializeDestinationBlockCombo();
1133        }
1134        initiateFrame.pack();
1135    }
1136
1137    private void handleViaBlockSelectionChanged() {
1138        if (radioTransitsAdHoc.isSelected() ) {
1139            initializeDestinationBlockDynamicCombo();
1140        } else {
1141            initializeDestinationBlockCombo();
1142        }
1143        initiateFrame.pack();
1144    }
1145
1146    private void handleAllocateAllTheWayButtonChanged() {
1147        allocateCustomSpinner.setVisible(false);
1148    }
1149
1150    private void handleAllocateBySafeButtonChanged() {
1151        allocateCustomSpinner.setVisible(false);
1152    }
1153
1154    private void handleAllocateNumberOfBlocksButtonChanged() {
1155        allocateCustomSpinner.setVisible(true);
1156    }
1157
1158    private void cancelInitiateTrain() {
1159        _dispatcher.newTrainDone(null);
1160    }
1161
1162    /*
1163     * Handles press of "Add New Train" button.
1164     * Move data to TrainInfo validating basic information
1165     * Call dispatcher to start the train from traininfo which
1166     * completes validation.
1167     */
1168    private void addNewTrain() {
1169        try {
1170            validateDialog();
1171            trainInfo = new TrainInfo();
1172            dialogToTrainInfo(trainInfo);
1173            if (radioTransitsAdHoc.isSelected()) {
1174                int ixStart, ixEnd, ixVia;
1175                ixStart = startingBlockBox.getSelectedIndex();
1176                ixEnd = destinationBlockBox.getSelectedIndex();
1177                ixVia = viaBlockBox.getSelectedIndex();
1178                // search for a transit if ones available.
1179                Transit tmpTransit = null;
1180                int routeCount = 9999;
1181                int startBlockSeq = 0;
1182                int endBlockSeq = 0;
1183                log.debug("Start[{}]Via[{}]Dest[{}}]",
1184                        startingBlockBoxList.get(ixStart).getDisplayName(),
1185                        viaBlockBoxList.get(ixVia).getDisplayName(),
1186                        destinationBlockBoxList.get(ixEnd).getDisplayName());
1187                for (Transit tr : InstanceManager.getDefault(jmri.TransitManager.class)
1188                        .getListUsingBlock(startingBlockBoxList.get(ixStart))) {
1189                    if (tr.getState() == Transit.IDLE
1190                            && tr.containsBlock(startingBlockBoxList.get(ixStart))
1191                            && tr.containsBlock(viaBlockBoxList.get(ixVia)) &&
1192                            tr.containsBlock(destinationBlockBoxList.get(ixEnd))) {
1193                        log.debug("[{}]  contains all blocks", tr.getDisplayName());
1194                        int ixCountStart = -1, ixCountVia = -1, ixCountDest = -1, ixCount = 0;
1195                        List<Block> transitBlocks = tr.getInternalBlocksList();
1196                        List<Integer> transitBlockSeq = tr.getBlockSeqList();
1197                        for (Block blk : transitBlocks) {
1198                            log.debug("Checking Block[{}] t[{}] BlockSequ[{}]",
1199                                    blk.getDisplayName(),
1200                                    ixCount,
1201                                    transitBlockSeq.get(ixCount));
1202                            if (ixCountStart == -1 && blk == startingBlockBoxList.get(ixStart)) {
1203                                log.trace("ixOne[{}]block[{}]",ixCount,blk.getDisplayName());
1204                                ixCountStart = ixCount;
1205                            } else if (ixCountStart != -1 && ixCountVia == -1 && blk == viaBlockBoxList.get(ixVia)) {
1206                                log.trace("ixTwo[{}]block[{}]",ixCount,blk.getDisplayName());
1207                                if (ixCount != ixCountStart + 1) {
1208                                    log.debug("AdHoc {}:via and start not ajacent",tr.getDisplayName());
1209                                    break;
1210                                }
1211                                ixCountVia = ixCount;
1212                            } else if (ixCountStart != -1 && ixCountVia != -1 && ixCountDest == -1 && blk == destinationBlockBoxList.get(ixEnd)) {
1213                                ixCountDest = ixCount;
1214                                log.trace("ixThree[{}]block[{}]",ixCountDest,blk.getDisplayName());
1215                                break;
1216                            }
1217                            ixCount++;
1218                        }
1219                        if (ixCountVia == (ixCountStart + 1) && ixCountDest > ixCountStart) {
1220                            log.debug("Canuse [{}", tr.getDisplayName());
1221                            Integer routeBlockLength =
1222                                    transitBlockSeq.get(ixCountDest) - transitBlockSeq.get(ixCountStart);
1223                            if (routeBlockLength < routeCount) {
1224                                routeCount = ixCountDest - ixCountStart;
1225                                tmpTransit = tr;
1226                                startBlockSeq = transitBlockSeq.get(ixCountStart).intValue();
1227                                endBlockSeq = transitBlockSeq.get(ixCountDest).intValue();
1228                            }
1229                        }
1230                    }
1231                }
1232                if (tmpTransit != null &&
1233                        (JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("Question6",tmpTransit.getDisplayName()),
1234                                "Question",
1235                                JmriJOptionPane.YES_NO_OPTION,
1236                                JmriJOptionPane.QUESTION_MESSAGE) == JmriJOptionPane.YES_OPTION)) {
1237                    // use transit found
1238                    trainInfo.setDynamicTransit(false);
1239                    trainInfo.setTransitName(tmpTransit.getDisplayName());
1240                    trainInfo.setTransitId(tmpTransit.getDisplayName());
1241                    trainInfo.setStartBlockSeq(startBlockSeq);
1242                    trainInfo.setStartBlockName(getBlockName(startingBlockBoxList.get(ixStart)) + "-" + startBlockSeq);
1243                    trainInfo.setDestinationBlockSeq(endBlockSeq);
1244                    trainInfo.setDestinationBlockName(getBlockName(destinationBlockBoxList.get(ixEnd)) + "-" + endBlockSeq);
1245                    trainInfoToDialog(trainInfo);
1246                } else {
1247                    // use a true ad-hoc
1248                    List<LayoutBlock> blockList = _dispatcher.getAdHocRoute(startingBlockBoxList.get(ixStart),
1249                            destinationBlockBoxList.get(ixEnd),
1250                            viaBlockBoxList.get(ixVia));
1251                    if (blockList == null) {
1252                        JmriJOptionPane.showMessageDialog(initiateFrame, Bundle.getMessage("Error51"),
1253                                Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1254                        return;
1255                    }
1256                }
1257            }
1258            _dispatcher.loadTrainFromTrainInfoThrowsException(trainInfo,"NONE","");
1259        } catch (IllegalArgumentException ex) {
1260            JmriJOptionPane.showMessageDialog(initiateFrame, ex.getMessage(),
1261                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1262        }
1263    }
1264
1265    private void initializeFreeTransitsCombo(List<Transit> transitList) {
1266        Set<Transit> excludeTransits = new HashSet<>();
1267        for (Transit t : _TransitManager.getNamedBeanSet()) {
1268            if (t.getState() != Transit.IDLE) {
1269                excludeTransits.add(t);
1270            }
1271        }
1272        transitSelectBox.setExcludedItems(excludeTransits);
1273        JComboBoxUtil.setupComboBoxMaxRows(transitSelectBox);
1274
1275        if (transitSelectBox.getItemCount() > 0) {
1276            transitSelectBox.setSelectedIndex(0);
1277            selectedTransit = transitSelectBox.getItemAt(0);
1278        } else {
1279            selectedTransit = null;
1280        }
1281    }
1282
1283    private void initializeFreeRosterEntriesCombo() {
1284        excludedRosterEntries = new ArrayList<RosterEntry>();
1285        // remove used entries
1286        for (int ix = rosterComboBox.getRosterEntryComboBox().getItemCount() - 1; ix > 1; ix--) {  // remove from back first item is the "select loco" message
1287            if ( !_dispatcher.isAddressFree( ((RosterEntry)rosterComboBox.getRosterEntryComboBox().getItemAt(ix)).getDccLocoAddress().getNumber() ) ) {
1288                excludedRosterEntries.add((RosterEntry)rosterComboBox.getRosterEntryComboBox().getItemAt(ix));
1289            }
1290        }
1291        rosterComboBox.getRosterEntryComboBox().setExcludeItems(excludedRosterEntries);
1292        rosterComboBox.getRosterEntryComboBox().update();
1293    }
1294
1295    private void initializeFreeTrainsCombo() {
1296        Train prevValue = null;
1297        if (trainSelectBox.getSelectedIndex() > 0) {
1298            // item zero is a string
1299            prevValue = (Train)trainSelectBox.getSelectedItem();
1300        }
1301        ActionListener[] als = trainSelectBox.getActionListeners();
1302        for ( ActionListener al: als) {
1303            trainSelectBox.removeActionListener(al);
1304        }
1305        trainSelectBox.removeAllItems();
1306        trainSelectBox.addItem(Bundle.getMessage("SelectTrain"));
1307        // initialize free trains from operations
1308        List<Train> trains = InstanceManager.getDefault(TrainManager.class).getTrainsByNameList();
1309        if (trains.size() > 0) {
1310            for (int i = 0; i < trains.size(); i++) {
1311                Train t = trains.get(i);
1312                if (t != null) {
1313                    String tName = t.getName();
1314                    if (_dispatcher.isTrainFree(tName)) {
1315                        trainSelectBox.addItem(t);
1316                    }
1317                }
1318            }
1319        }
1320        if (prevValue != null) {
1321            trainSelectBox.setSelectedItem(prevValue);
1322        }
1323        for ( ActionListener al: als) {
1324            trainSelectBox.addActionListener(al);
1325        }
1326    }
1327
1328    /**
1329     * Sets the labels and inputs for speed profile running
1330     * @param b True if the roster entry has valid speed profile else false
1331     */
1332    private void setSpeedProfileOptions(TrainInfo info,boolean b) {
1333        useSpeedProfileLabel.setEnabled(b);
1334        useSpeedProfileCheckBox.setEnabled(b);
1335        stopBySpeedProfileLabel.setEnabled(b);
1336        stopBySpeedProfileCheckBox.setEnabled(b);
1337        stopBySpeedProfileAdjustLabel.setEnabled(b);
1338        stopBySpeedProfileAdjustSpinner.setEnabled(b);
1339        minReliableOperatingScaleSpeedLabel.setVisible(b);
1340        if (!b) {
1341            useSpeedProfileCheckBox.setSelected(false);
1342            stopBySpeedProfileCheckBox.setSelected(false);
1343
1344        }
1345        updateStopByDistanceEnable();
1346        // Physics option is available iff speed profile UI is enabled (availability only)
1347        updateRampPhysicsAvailability(b);
1348        updateMaxSpeedCapModeAvailability(b);
1349    }
1350
1351    // Map between Stop-by-distance units and Max Train Length units.
1352    // Note: Max Train Length has no millimetres; ACTUAL_MM maps to ACTUALCM.
1353    private TrainLengthUnits mapStopDistanceUnitsToTrainLengthUnits(StopDistanceUnits units) {
1354        switch (units) {
1355            case SCALE_FEET:
1356                return TrainLengthUnits.TRAINLENGTH_SCALEFEET;
1357            case SCALE_METERS:
1358                return TrainLengthUnits.TRAINLENGTH_SCALEMETERS;
1359            case ACTUAL_INCHES:
1360                return TrainLengthUnits.TRAINLENGTH_ACTUALINCHS;
1361            case ACTUAL_CM:
1362            case ACTUAL_MM:
1363            default:
1364                return TrainLengthUnits.TRAINLENGTH_ACTUALCM;
1365        }
1366    }
1367
1368    private StopDistanceUnits mapTrainLengthUnitsToStopDistanceUnits(TrainLengthUnits units) {
1369        switch (units) {
1370            case TRAINLENGTH_SCALEFEET:
1371                return StopDistanceUnits.SCALE_FEET;
1372            case TRAINLENGTH_SCALEMETERS:
1373                return StopDistanceUnits.SCALE_METERS;
1374            case TRAINLENGTH_ACTUALINCHS:
1375                return StopDistanceUnits.ACTUAL_INCHES;
1376            case TRAINLENGTH_ACTUALCM:
1377            default:
1378                return StopDistanceUnits.ACTUAL_CM;
1379        }
1380    }
1381
1382    // Default Stop-by-distance units follow the current Max Train Length unit selection.
1383    private StopDistanceUnits getPreferredStopDistanceUnitsFromMaxTrainLengthUnits() {
1384        Object sel = trainLengthUnitsComboBox.getSelectedItem();
1385        TrainLengthUnits units = (sel instanceof TrainLengthUnitsItem) ? ((TrainLengthUnitsItem) sel).getValue()
1386                : TrainLengthUnits.TRAINLENGTH_SCALEMETERS;
1387        return mapTrainLengthUnitsToStopDistanceUnits(units);
1388    }
1389
1390    private void setStopByDistanceUnitsSelection(StopDistanceUnits units) {
1391        for (int i = 0; i < stopByDistanceUnitsComboBox.getItemCount(); i++) {
1392            Object o = stopByDistanceUnitsComboBox.getItemAt(i);
1393            if (o instanceof StopDistanceUnitsItem && ((StopDistanceUnitsItem) o).getValue() == units) {
1394                stopByDistanceUnitsComboBox.setSelectedIndex(i);
1395                return;
1396            }
1397        }
1398        if (stopByDistanceUnitsComboBox.getItemCount() > 0) {
1399            stopByDistanceUnitsComboBox.setSelectedIndex(0);
1400        }
1401    }
1402
1403    private TrainLengthUnits getSelectedMaxTrainLengthUnitsSafe() {
1404        Object sel = trainLengthUnitsComboBox.getSelectedItem();
1405        return (sel instanceof TrainLengthUnitsItem) ? ((TrainLengthUnitsItem) sel).getValue()
1406                : TrainLengthUnits.TRAINLENGTH_SCALEMETERS;
1407    }
1408
1409    private StopDistanceUnits getSelectedStopByDistanceUnitsSafe() {
1410        Object sel = stopByDistanceUnitsComboBox.getSelectedItem();
1411        return (sel instanceof StopDistanceUnitsItem) ? ((StopDistanceUnitsItem) sel).getValue()
1412                : StopDistanceUnits.ACTUAL_CM;
1413    }
1414
1415    private void handleStopByDistanceUnitsComboSelectionChanged() {
1416        handleStopByDistanceUnitsChanged();
1417        if (suppressDistanceAndTrainLengthUnitSync) {
1418            return;
1419        }
1420        TrainLengthUnits target = mapStopDistanceUnitsToTrainLengthUnits(getSelectedStopByDistanceUnitsSafe());
1421        TrainLengthUnits current = getSelectedMaxTrainLengthUnitsSafe();
1422        if (target == current) {
1423            return;
1424        }
1425        suppressDistanceAndTrainLengthUnitSync = true;
1426        try {
1427            trainLengthUnitsComboBox.setSelectedItemByValue(target);
1428        } finally {
1429            suppressDistanceAndTrainLengthUnitSync = false;
1430        }
1431    }
1432
1433    private void handleTrainLengthUnitsComboSelectionChanged() {
1434        handleTrainLengthUnitsChanged();
1435        if (suppressDistanceAndTrainLengthUnitSync) {
1436            return;
1437        }
1438        StopDistanceUnits target = mapTrainLengthUnitsToStopDistanceUnits(getSelectedMaxTrainLengthUnitsSafe());
1439        StopDistanceUnits current = getSelectedStopByDistanceUnitsSafe();
1440        if (target == current) {
1441            return;
1442        }
1443        suppressDistanceAndTrainLengthUnitSync = true;
1444        try {
1445            setStopByDistanceUnitsSelection(target);
1446        } finally {
1447            suppressDistanceAndTrainLengthUnitSync = false;
1448        }
1449    }
1450
1451    private void updateStopByDistanceEnable() {
1452        // Row is relevant only if Stop-by-speed-profile is available & selected
1453        boolean baseOn = stopBySpeedProfileCheckBox.isEnabled() && stopBySpeedProfileCheckBox.isSelected();
1454        stopByDistanceLabel.setEnabled(baseOn);
1455        stopByDistanceEnableCheckBox.setEnabled(baseOn);
1456
1457        boolean distanceMode = baseOn && stopByDistanceEnableCheckBox.isSelected();
1458
1459        // Distance controls active only in distanceMode
1460        stopByDistanceMmSpinner.setEnabled(distanceMode);
1461        stopByDistanceUnitsComboBox.setEnabled(distanceMode);
1462        stopByDistanceHead.setEnabled(distanceMode);
1463        stopByDistanceTail.setEnabled(distanceMode);
1464
1465        // Stop-by-% into block is still meaningful even when distance stopping is enabled,
1466        // because it applies to non-destination (intermediate) stops.
1467        stopBySpeedProfileAdjustLabel.setEnabled(baseOn);
1468        stopBySpeedProfileAdjustSpinner.setEnabled(baseOn);
1469
1470        // Tooltip: when distance stopping is enabled, clarify that the % adjustment still applies to non-destination stops.
1471        String baseTip = Bundle.getMessage("StopBySpeedProfileAdjustHint");
1472        if (distanceMode) {
1473            stopBySpeedProfileAdjustSpinner.setToolTipText(
1474                    bundleOrDefault("StopBySpeedProfileAdjustHintWithDistance", baseTip));
1475        } else {
1476            stopBySpeedProfileAdjustSpinner.setToolTipText(baseTip);
1477        }
1478    }
1479
1480   
1481      // Dynamically adjust spinner precision & format to match selected units.
1482      // NOTE: This does not convert units; that’s handled by handleStopByDistanceUnitsChanged().
1483      private void updateStopByDistanceSpinnerModelForUnits(StopDistanceUnits units) {
1484          // Preserve current display value while we swap models/editors
1485          float displayValue = ((Number) stopByDistanceMmSpinner.getValue()).floatValue();
1486    
1487          float step;
1488          String pattern;
1489          switch (units) {
1490              case ACTUAL_MM:
1491                  step = 1.0f;          // whole millimetres
1492                  pattern = "0";
1493                  break;
1494              case ACTUAL_CM:
1495              default:
1496                  step = 0.1f; // tenths of a centimetre (1 mm)
1497                  pattern = "0.0";
1498                  break;
1499              case ACTUAL_INCHES:
1500              case SCALE_METERS:
1501              case SCALE_FEET:
1502                  step = 0.01f;         // hundredths
1503                  pattern = "0.00";
1504                  break;
1505          }
1506    
1507          // Keep wide range; only granularity changes
1508          stopByDistanceMmSpinner.setModel(
1509              new SpinnerNumberModel(Float.valueOf(displayValue),
1510                                     Float.valueOf(0.0f),
1511                                     Float.valueOf(1000000.0f),
1512                                     Float.valueOf(step))
1513          );
1514          stopByDistanceMmSpinner.setEditor(new JSpinner.NumberEditor(stopByDistanceMmSpinner, pattern));
1515      }     
1516
1517        private void handleStopByDistanceUnitsChanged() {
1518            // Convert current display -> mm
1519            float currentDisplay = ((Number) stopByDistanceMmSpinner.getValue()).floatValue();
1520            float mm = convertStopDisplayToMm(currentStopDistanceUnits, currentDisplay);
1521        
1522            // Update selected units
1523            currentStopDistanceUnits = stopByDistanceUnitsComboBox.getSelectedUnits();
1524        
1525            // Convert mm -> new display units
1526            float newDisplay = convertMmToStopDisplay(currentStopDistanceUnits, mm);
1527        
1528            // Update precision & format for the new units, then show the new value
1529            updateStopByDistanceSpinnerModelForUnits(currentStopDistanceUnits);
1530            stopByDistanceMmSpinner.setValue(Float.valueOf(newDisplay));
1531        }
1532    
1533      /*
1534       * Convert underlying actual millimetres (mm) to the chosen display units.
1535       * Uses the same scale concepts as the Max Train Length UI:
1536       *  - "_dispatcher.getScale().getScaleRatio()" converts actual length to scale length
1537       *  - inches <-> mm conversions use constants 25.4 and 3.28084 as in the train-length panel
1538       */
1539      private float convertMmToStopDisplay(StopDistanceUnits units, float mm) {
1540          final float scaleRatio = (_dispatcher.getScale() != null) ? (float) _dispatcher.getScale().getScaleRatio() : 1.0f;
1541          switch (units) {
1542              case ACTUAL_MM:
1543                  return mm;
1544              case ACTUAL_CM:
1545                  return mm / 10.0f;
1546              case ACTUAL_INCHES:
1547                  return mm / 25.4f;
1548              case SCALE_METERS: {
1549                  // actual metres to scale metres -> (mm / 1000) * scaleRatio
1550                  float scaleMeters = (mm / 1000.0f) * scaleRatio;
1551                  return scaleMeters;
1552              }
1553              case SCALE_FEET: {
1554                  // scale feet = scale metres * 3.28084
1555                  float scaleFeet = ((mm / 1000.0f) * scaleRatio) * 3.28084f;
1556                  return scaleFeet;
1557              }
1558              default:
1559                  return mm;
1560          }
1561      }
1562    
1563      /*
1564       * Convert a displayed value in the chosen units back to underlying mm (actual).
1565       */
1566      private float convertStopDisplayToMm(StopDistanceUnits units, float value) {
1567          final float scaleRatio = (_dispatcher.getScale() != null) ? (float) _dispatcher.getScale().getScaleRatio() : 1.0f;
1568          switch (units) {
1569              case ACTUAL_MM:
1570                  return value;
1571              case ACTUAL_CM:
1572                  return value * 10.0f;
1573              case ACTUAL_INCHES:
1574                  return value * 25.4f;
1575              case SCALE_METERS: {
1576                  float mm = (value / scaleRatio) * 1000.0f;
1577                  return mm;
1578              }
1579              case SCALE_FEET: {
1580                  float mm = (value / 3.28084f / scaleRatio) * 1000.0f;
1581                  return mm;
1582              }
1583              default:
1584                  return value;
1585          }
1586      }
1587     
1588     private void initializeStartingBlockCombo() {
1589        String prevValue = (String)startingBlockBox.getSelectedItem();
1590        startingBlockBox.removeAllItems();
1591        startingBlockBoxList.clear();
1592        if (selectedTransit == null) {
1593            return;
1594        }
1595        if (!inTransitBox.isSelected() && selectedTransit.getEntryBlocksList().isEmpty()) {
1596            inTransitBox.setSelected(true);
1597        }
1598        if (inTransitBox.isSelected()) {
1599            startingBlockBoxList = selectedTransit.getInternalBlocksList();
1600        } else {
1601            startingBlockBoxList = selectedTransit.getEntryBlocksList();
1602        }
1603        startingBlockSeqList = selectedTransit.getBlockSeqList();
1604        boolean found = false;
1605        for (int i = 0; i < startingBlockBoxList.size(); i++) {
1606            Block b = startingBlockBoxList.get(i);
1607            int seq = startingBlockSeqList.get(i).intValue();
1608            startingBlockBox.addItem(getBlockName(b) + "-" + seq);
1609            if (!found && b.getState() == Block.OCCUPIED) {
1610                startingBlockBox.setSelectedItem(getBlockName(b) + "-" + seq);
1611                found = true;
1612            }
1613        }
1614        if (prevValue != null) {
1615            startingBlockBox.setSelectedItem(prevValue);
1616        }
1617        JComboBoxUtil.setupComboBoxMaxRows(startingBlockBox);
1618    }
1619
1620    private void initializeDestinationBlockCombo() {
1621        String prevValue = (String)destinationBlockBox.getSelectedItem();
1622        destinationBlockBox.removeAllItems();
1623        destinationBlockBoxList.clear();
1624        int index = startingBlockBox.getSelectedIndex();
1625        if (index < 0) {
1626            return;
1627        }
1628        Block startBlock = startingBlockBoxList.get(index);
1629        destinationBlockBoxList = selectedTransit.getDestinationBlocksList(
1630                startBlock, inTransitBox.isSelected());
1631        destinationBlockSeqList = selectedTransit.getDestBlocksSeqList();
1632        for (int i = 0; i < destinationBlockBoxList.size(); i++) {
1633            Block b = destinationBlockBoxList.get(i);
1634            String bName = getBlockName(b);
1635            if (selectedTransit.getBlockCount(b) > 1) {
1636                int seq = destinationBlockSeqList.get(i).intValue();
1637                bName = bName + "-" + seq;
1638            }
1639            destinationBlockBox.addItem(bName);
1640        }
1641        if (prevValue != null) {
1642            destinationBlockBox.setSelectedItem(prevValue);
1643        }
1644        JComboBoxUtil.setupComboBoxMaxRows(destinationBlockBox);
1645    }
1646
1647    private String getBlockName(Block b) {
1648        if (b != null) {
1649            return b.getDisplayName();
1650        }
1651        return " ";
1652    }
1653
1654    protected void showActivateFrame() {
1655        if (initiateFrame != null) {
1656            initializeFreeTransitsCombo(new ArrayList<>());
1657            initiateFrame.setVisible(true);
1658        } else {
1659            _dispatcher.newTrainDone(null);
1660        }
1661    }
1662
1663    /**
1664     * Show the Frame.
1665     * @param re currently unused.
1666     */
1667    public void showActivateFrame(RosterEntry re) {
1668        showActivateFrame();
1669    }
1670
1671    protected void loadTrainInfo(ActionEvent e) {
1672        List<TrainInfoFileSummary> names = _tiFile.getTrainInfoFileSummaries();
1673        if (!names.isEmpty()) {
1674            JTable table = new JTable(){
1675                @Override
1676                public Dimension getPreferredScrollableViewportSize() {
1677                  return new Dimension(super.getPreferredSize().width,
1678                      super.getPreferredScrollableViewportSize().height);
1679                }
1680            };
1681            DefaultTableModel tm = new DefaultTableModel(
1682                    new Object[]{
1683                            Bundle.getMessage("FileNameColumnTitle"),
1684                            Bundle.getMessage("TrainColumnTitle"),
1685                            Bundle.getMessage("TransitColumnTitle"),
1686                            Bundle.getMessage("StartBlockColumnTitle"),
1687                            Bundle.getMessage("EndBlockColumnTitle"),
1688                            Bundle.getMessage("DccColumnTitleColumnTitle")
1689                    }, 0) {
1690                @Override
1691                public boolean isCellEditable(int row, int column) {
1692                    //all cells false
1693                    return false;
1694                }
1695            };
1696
1697            table.setModel(tm);
1698            for (TrainInfoFileSummary fs: names) {
1699                tm.addRow(new Object[] {fs.getFileName(),fs.getTrainName(),
1700                        fs.getTransitName(),fs.getStartBlockName()
1701                        ,fs.getEndBlockName(),fs.getDccAddress()});
1702            }
1703            JPanel jp = new JPanel(new BorderLayout());
1704            TableColumnModel columnModel = table.getColumnModel();
1705            table.setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
1706            for (int column = 0; column < table.getColumnCount(); column++) {
1707                int width = 30; // Min width
1708                for (int row = 0; row < table.getRowCount(); row++) {
1709                    TableCellRenderer renderer = table.getCellRenderer(row, column);
1710                    Component comp = table.prepareRenderer(renderer, row, column);
1711                    width = Math.max(comp.getPreferredSize().width +1 , width);
1712                }
1713                if(width > 300)
1714                    width=300;
1715                columnModel.getColumn(column).setPreferredWidth(width);
1716            }
1717            //jp.setPreferredSize(table.getPreferredSize());
1718            jp.add(table);
1719            table.setAutoCreateRowSorter(true);
1720            JScrollPane sp = new JScrollPane(table,
1721                            ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
1722                            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
1723            int optionSelected = JmriJOptionPane.showOptionDialog(initiateFrame,
1724                    sp, Bundle.getMessage("LoadTrainTitle"), JmriJOptionPane.OK_CANCEL_OPTION, JmriJOptionPane.PLAIN_MESSAGE,
1725                    null,null,null);
1726            if (optionSelected != JmriJOptionPane.OK_OPTION) {
1727                //Canceled
1728                return;
1729            }
1730            if (table.getSelectedRow() < 0) {
1731                return;
1732            }
1733            String selName = (String)table.getModel().getValueAt( table.getRowSorter().convertRowIndexToModel(table.getSelectedRow()),0);
1734            if ((selName == null) || (selName.isEmpty())) {
1735                return;
1736            }
1737            //read xml data from selected filename and move it into the new train dialog box
1738            _trainInfoName = selName;
1739            try {
1740                trainInfo = _tiFile.readTrainInfo( selName);
1741                if (trainInfo != null) {
1742                    // process the information just read
1743                    trainInfoToDialog(trainInfo);
1744                }
1745            } catch (java.io.IOException ioe) {
1746                log.error("IO Exception when reading train info file", ioe);
1747            } catch (org.jdom2.JDOMException jde) {
1748                log.error("JDOM Exception when reading train info file", jde);
1749            }
1750            handleDelayStartClick(null);
1751            handleReverseAtEndBoxClick();
1752        }
1753    }
1754
1755    private void saveTrainInfo() {
1756        saveTrainInfo(false);
1757        refreshNextTrainCombo();
1758    }
1759
1760    private void saveTrainInfoAsTemplate() {
1761        saveTrainInfo(true);
1762    }
1763
1764    private void saveTrainInfo(boolean asTemplate) {
1765        try {
1766            dialogToTrainInfo(trainInfo);
1767        } catch (IllegalArgumentException ide) {
1768            JmriJOptionPane.showMessageDialog(initiateFrame, ide.getMessage(),
1769                    Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1770            return;
1771        }
1772        // get file name
1773        String fileName;
1774        if (asTemplate) {
1775            fileName = normalizeXmlFileName(nameOfTemplateFile);
1776        } else {
1777            String eName = JmriJOptionPane.showInputDialog(initiateFrame,
1778                    Bundle.getMessage("EnterFileName") + " :", _trainInfoName);
1779            if (eName == null) {  //Cancel pressed
1780                return;
1781            }
1782            if (eName.length() < 1) {
1783                JmriJOptionPane.showMessageDialog(initiateFrame, Bundle.getMessage("Error25"),
1784                        Bundle.getMessage("ErrorTitle"), JmriJOptionPane.ERROR_MESSAGE);
1785                return;
1786            }
1787            fileName = normalizeXmlFileName(eName);
1788            _trainInfoName = fileName;
1789        }
1790        // check if train info file name is in use
1791        String[] names = _tiFile.getTrainInfoFileNames();
1792        if (names.length > 0) {
1793            boolean found = false;
1794            for (int i = 0; i < names.length; i++) {
1795                if (fileName.equals(names[i])) {
1796                    found = true;
1797                }
1798            }
1799            if (found) {
1800                // file by that name is already present
1801                int selectedValue = JmriJOptionPane.showOptionDialog(initiateFrame,
1802                        Bundle.getMessage("Question3", fileName),
1803                        Bundle.getMessage("WarningTitle"), JmriJOptionPane.DEFAULT_OPTION,
1804                        JmriJOptionPane.QUESTION_MESSAGE, null,
1805                        new Object[]{Bundle.getMessage("ButtonReplace"),Bundle.getMessage("ButtonNo")},
1806                        Bundle.getMessage("ButtonNo"));
1807                if (selectedValue != 0 ) { // array position 0 , replace not selected
1808                    return;   // return without writing if "No" response
1809                }
1810            }
1811        }
1812        // write the Train Info file
1813        try {
1814            _tiFile.writeTrainInfo(trainInfo, fileName);
1815        } //catch (org.jdom2.JDOMException jde) {
1816        // log.error("JDOM exception writing Train Info: "+jde);
1817        //}
1818        catch (java.io.IOException ioe) {
1819            log.error("IO exception writing Train Info", ioe);
1820        }
1821    }
1822
1823    private void deleteTrainInfo() {
1824        String[] names = _tiFile.getTrainInfoFileNames();
1825        if (names.length > 0) {
1826            Object selName = JmriJOptionPane.showInputDialog(initiateFrame,
1827                    Bundle.getMessage("DeleteTrainChoice"), Bundle.getMessage("DeleteTrainTitle"),
1828                    JmriJOptionPane.QUESTION_MESSAGE, null, names, names[0]);
1829            if ((selName == null) || (((String) selName).isEmpty())) {
1830                return;
1831            }
1832            _tiFile.deleteTrainInfoFile((String) selName);
1833        }
1834    }
1835
1836    private void trainInfoToDialog(TrainInfo info) {
1837        if (!info.getDynamicTransit()) {
1838            radioTransitsPredefined.setSelected(true);
1839            if (!info.getTransitName().isEmpty()) {
1840                try {
1841                    transitSelectBox.setSelectedItemByName(info.getTransitName());
1842                } catch (Exception ex) {
1843                    log.warn("Transit {} from file not in Transit menu", info.getTransitName());
1844                    JmriJOptionPane.showMessageDialog(initiateFrame,
1845                            Bundle.getMessage("TransitWarn", info.getTransitName()),
1846                            null, JmriJOptionPane.WARNING_MESSAGE);
1847                }
1848            }
1849        } else {
1850            radioTransitsAdHoc.setSelected(true);
1851        }
1852        switch (info.getTrainsFrom()) {
1853            case TRAINSFROMROSTER:
1854                radioTrainsFromRoster.setSelected(true);
1855                if (!info.getRosterId().isEmpty()) {
1856                    if (!setRosterEntryBox(rosterComboBox, info.getRosterId())) {
1857                        log.warn("Roster {} from file not in Roster Combo", info.getRosterId());
1858                        JmriJOptionPane.showMessageDialog(initiateFrame,
1859                                Bundle.getMessage("TrainWarn", info.getRosterId()),
1860                                null, JmriJOptionPane.WARNING_MESSAGE);
1861                    }
1862                }
1863                break;
1864            case TRAINSFROMOPS:
1865                radioTrainsFromOps.setSelected(true);
1866                if (!info.getTrainName().isEmpty()) {
1867                    if (!setTrainComboBox(trainSelectBox, info.getTrainName())) {
1868                        log.warn("Train {} from file not in Train Combo", info.getTrainName());
1869                        JmriJOptionPane.showMessageDialog(initiateFrame,
1870                                Bundle.getMessage("TrainWarn", info.getTrainName()),
1871                                null, JmriJOptionPane.WARNING_MESSAGE);
1872                    }
1873                }
1874                break;
1875            case TRAINSFROMUSER:
1876                radioTrainsFromUser.setSelected(true);
1877                dccAddressSpinner.setValue(Integer.valueOf(info.getDccAddress()));
1878                break;
1879            case TRAINSFROMSETLATER:
1880            default:
1881                radioTrainsFromSetLater.setSelected(true);
1882        }
1883        setTrainsFromOptions(info.getTrainsFrom());
1884        trainNameField.setText(info.getTrainUserName());
1885        trainDetectionComboBox.setSelectedItemByValue(info.getTrainDetection());
1886        inTransitBox.setSelected(info.getTrainInTransit());
1887        if (radioTransitsAdHoc.isSelected()) {
1888            initializeStartingBlockComboDynamic();
1889        } else {
1890            initializeStartingBlockCombo();
1891        }
1892        setComboBox(startingBlockBox, info.getStartBlockName());
1893        if (radioTransitsAdHoc.isSelected()) {
1894            initializeViaBlockDynamicCombo();
1895            setComboBox(viaBlockBox, info.getViaBlockName());
1896        }
1897        if (radioTransitsAdHoc.isSelected()) {
1898            initializeDestinationBlockDynamicCombo();
1899        } else {
1900            initializeDestinationBlockCombo();
1901        }
1902        setComboBox(destinationBlockBox, info.getDestinationBlockName());
1903
1904        setAllocateMethodButtons(info.getAllocationMethod());
1905        prioritySpinner.setValue(info.getPriority());
1906        resetWhenDoneBox.setSelected(info.getResetWhenDone());
1907        reverseAtEndBox.setSelected(info.getReverseAtEnd());
1908        setDelayModeBox(info.getDelayedStart(), delayedStartBox);
1909        //delayedStartBox.setSelected(info.getDelayedStart());
1910        departureHrSpinner.setValue(info.getDepartureTimeHr());
1911        departureMinSpinner.setValue(info.getDepartureTimeMin());
1912        delaySensor.setSelectedItem(info.getDelaySensor());
1913        resetStartSensorBox.setSelected(info.getResetStartSensor());
1914        setDelayModeBox(info.getDelayedRestart(), delayedReStartBox);
1915        delayMinSpinner.setValue(info.getRestartDelayMin());
1916        delayReStartSensor.setSelectedItem(info.getRestartSensor());
1917        resetRestartSensorBox.setSelected(info.getResetRestartSensor());
1918
1919        resetStartSensorBox.setSelected(info.getResetStartSensor());
1920        setDelayModeBox(info.getReverseDelayedRestart(), reverseDelayedRestartType);
1921        delayReverseMinSpinner.setValue(info.getReverseRestartDelayMin());
1922        delayReverseReStartSensor.setSelectedItem(info.getReverseRestartSensor());
1923        delayReverseResetSensorBox.setSelected(info.getReverseResetRestartSensor());
1924
1925        terminateWhenDoneBox.setSelected(info.getTerminateWhenDone());
1926        nextTrain.setSelectedIndex(-1);
1927        try {
1928            nextTrain.setSelectedItem(info.getNextTrain());
1929        } catch (Exception ex){
1930            nextTrain.setSelectedIndex(-1);
1931        }
1932        handleTerminateWhenDoneBoxClick();
1933        setComboBox(trainTypeBox, info.getTrainType());
1934        autoRunBox.setSelected(info.getAutoRun());
1935        loadAtStartupBox.setSelected(info.getLoadAtStartup());
1936        setAllocateMethodButtons(info.getAllocationMethod());
1937        autoTrainInfoToDialog(info);
1938    }
1939
1940    private boolean validateDialog() throws IllegalArgumentException {
1941        int index = transitSelectBox.getSelectedIndex();
1942        if (index < 0) {
1943            throw new IllegalArgumentException(Bundle.getMessage("Error44"));
1944        }
1945        switch (trainsFromButtonGroup.getSelection().getActionCommand()) {
1946            case "TRAINSFROMROSTER":
1947                if (rosterComboBox.getRosterEntryComboBox().getSelectedIndex() < 1 ) {
1948                    throw new IllegalArgumentException(Bundle.getMessage("Error41"));
1949                }
1950                break;
1951            case "TRAINSFROMOPS":
1952                if (trainSelectBox.getSelectedIndex() < 1) {
1953                    throw new IllegalArgumentException(Bundle.getMessage("Error42"));
1954                }
1955                break;
1956            case "TRAINSFROMUSER":
1957                if (trainNameField.getText().isEmpty()) {
1958                    throw new IllegalArgumentException(Bundle.getMessage("Error22"));
1959                }
1960                break;
1961            case "TRAINSFROMSETLATER":
1962            default:
1963        }
1964        index = startingBlockBox.getSelectedIndex();
1965        if (index < 0) {
1966            throw new IllegalArgumentException(Bundle.getMessage("Error13"));
1967        }
1968        index = destinationBlockBox.getSelectedIndex();
1969        if (index < 0) {
1970            throw new IllegalArgumentException(Bundle.getMessage("Error8"));
1971        }
1972        if (radioTransitsAdHoc.isSelected()) {
1973            index = viaBlockBox.getSelectedIndex();
1974            if (index < 0) {
1975                throw new IllegalArgumentException(Bundle.getMessage("Error8"));
1976            }
1977        }
1978        if ((!reverseAtEndBox.isSelected()) && resetWhenDoneBox.isSelected()
1979                && (!selectedTransit.canBeResetWhenDone())) {
1980            resetWhenDoneBox.setSelected(false);
1981            throw new IllegalArgumentException(Bundle.getMessage("NoResetMessage"));
1982        }
1983        MaxSpeedCapMode mode = ((MaxSpeedCapModeItem) maxSpeedCapModeBox.getSelectedItem()).getValue();
1984        if (mode == MaxSpeedCapMode.THROTTLE) {
1985            int max = Math.round(((Number) maxSpeedSpinner.getValue()).floatValue()*100.0f);
1986            int min = Math.round(((Number) minReliableOperatingSpeedSpinner.getValue()).floatValue()*100.0f);
1987            if ((max - min) < 10) {
1988                throw new IllegalArgumentException(Bundle.getMessage("Error49", maxSpeedSpinner.getValue(), minReliableOperatingSpeedSpinner.getValue()));
1989            }
1990        }
1991        // In speed mode, we skip this percent-gap check; runtime will cap via the profile+scale conversion.
1992        return true;
1993    }
1994
1995    private boolean dialogToTrainInfo(TrainInfo info) {
1996        int index = transitSelectBox.getSelectedIndex();
1997        info.setDynamicTransit(radioTransitsAdHoc.isSelected());
1998        if (!info.getDynamicTransit() && index >= 0 ) {
1999            info.setTransitName(transitSelectBox.getSelectedItem().getDisplayName());
2000            info.setTransitId(transitSelectBox.getSelectedItem().getDisplayName());
2001        }
2002        switch (trainsFromButtonGroup.getSelection().getActionCommand()) {
2003            case "TRAINSFROMROSTER":
2004                if (rosterComboBox.getRosterEntryComboBox().getSelectedItem() instanceof RosterEntry) {
2005                    info.setRosterId(((RosterEntry) rosterComboBox.getRosterEntryComboBox().getSelectedItem()).getId());
2006                    info.setDccAddress(((RosterEntry) rosterComboBox.getRosterEntryComboBox().getSelectedItem()).getDccAddress());
2007                }
2008                trainInfo.setTrainsFrom(TrainsFrom.TRAINSFROMROSTER);
2009                setTrainsFromOptions(trainInfo.getTrainsFrom());
2010                break;
2011            case "TRAINSFROMOPS":
2012                if (trainSelectBox.getSelectedIndex() > 0) {
2013                    info.setTrainName(((Train) trainSelectBox.getSelectedItem()).toString());
2014                    info.setDccAddress(String.valueOf(dccAddressSpinner.getValue()));
2015                }
2016                trainInfo.setTrainsFrom(TrainsFrom.TRAINSFROMOPS);
2017                setTrainsFromOptions(trainInfo.getTrainsFrom());
2018                break;
2019            case "TRAINSFROMUSER":
2020                trainInfo.setTrainsFrom(TrainsFrom.TRAINSFROMUSER);
2021                info.setDccAddress(String.valueOf(dccAddressSpinner.getValue()));
2022                break;
2023            case "TRAINSFROMSETLATER":
2024            default:
2025                trainInfo.setTrainsFrom(TrainsFrom.TRAINSFROMSETLATER);
2026                info.setTrainName("");
2027                info.setDccAddress("");
2028        }
2029        info.setTrainUserName(trainNameField.getText());
2030        info.setTrainInTransit(inTransitBox.isSelected());
2031        info.setStartBlockName((String) startingBlockBox.getSelectedItem());
2032        index = startingBlockBox.getSelectedIndex();
2033        info.setStartBlockId(startingBlockBoxList.get(index).getDisplayName());
2034        if (info.getDynamicTransit()) {
2035            info.setStartBlockSeq(1);
2036        } else {
2037            info.setStartBlockSeq(startingBlockSeqList.get(index).intValue());
2038        }
2039        index = destinationBlockBox.getSelectedIndex();
2040        info.setDestinationBlockId(destinationBlockBoxList.get(index).getDisplayName());
2041        info.setDestinationBlockName(destinationBlockBoxList.get(index).getDisplayName());
2042        if (info.getDynamicTransit()) {
2043            info.setViaBlockName(viaBlockBoxList.get(viaBlockBox.getSelectedIndex()).getDisplayName());
2044        } else {
2045            info.setDestinationBlockSeq(destinationBlockSeqList.get(index).intValue());
2046        }
2047        info.setPriority((Integer) prioritySpinner.getValue());
2048        info.setTrainDetection(((TrainDetectionItem)trainDetectionComboBox.getSelectedItem()).value);
2049        info.setResetWhenDone(resetWhenDoneBox.isSelected());
2050        info.setReverseAtEnd(reverseAtEndBox.isSelected());
2051        info.setDelayedStart(delayModeFromBox(delayedStartBox));
2052        info.setDelaySensorName(delaySensor.getSelectedItemDisplayName());
2053        info.setResetStartSensor(resetStartSensorBox.isSelected());
2054        info.setDepartureTimeHr((Integer) departureHrSpinner.getValue());
2055        info.setDepartureTimeMin((Integer) departureMinSpinner.getValue());
2056        info.setTrainType((String) trainTypeBox.getSelectedItem());
2057        info.setAutoRun(autoRunBox.isSelected());
2058        info.setLoadAtStartup(loadAtStartupBox.isSelected());
2059        info.setAllocateAllTheWay(false); // force to false next field is now used.
2060        if (allocateAllTheWayRadioButton.isSelected()) {
2061            info.setAllocationMethod(ActiveTrain.ALLOCATE_AS_FAR_AS_IT_CAN);
2062        } else if (allocateBySafeRadioButton.isSelected()) {
2063            info.setAllocationMethod(ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS);
2064        } else {
2065            info.setAllocationMethod((Integer) allocateCustomSpinner.getValue());
2066        }
2067        info.setDelayedRestart(delayModeFromBox(delayedReStartBox));
2068        info.setRestartSensorName(delayReStartSensor.getSelectedItemDisplayName());
2069        info.setResetRestartSensor(resetRestartSensorBox.isSelected());
2070        info.setRestartDelayMin((Integer) delayMinSpinner.getValue());
2071
2072        info.setReverseDelayedRestart(delayModeFromBox(reverseDelayedRestartType));
2073        info.setReverseRestartSensorName(delayReverseReStartSensor.getSelectedItemDisplayName());
2074        info.setReverseResetRestartSensor(delayReverseResetSensorBox.isSelected());
2075        info.setReverseRestartDelayMin((Integer) delayReverseMinSpinner.getValue());
2076
2077        info.setTerminateWhenDone(terminateWhenDoneBox.isSelected());
2078        if (nextTrain.getSelectedIndex() > 0 ) {
2079            info.setNextTrain((String)nextTrain.getSelectedItem());
2080        } else {
2081            info.setNextTrain("None");
2082        }
2083        autoRunItemsToTrainInfo(info);
2084        return true;
2085    }
2086
2087    private boolean setRosterEntryBox(RosterEntrySelectorPanel box, String txt) {
2088        /*
2089         * Due to the different behaviour of GUI comboboxs
2090         * we cannot just set the item and catch an exception.
2091         * We first inspect the combo items with the current filter,
2092         * if found well and good else we remove the filter and try again.
2093         */
2094        boolean found = false;
2095        setRosterComboBox(box.getRosterEntryComboBox(),txt);
2096        if (found) {
2097            return found;
2098        }
2099        box.setSelectedRosterGroup(null);
2100       return setRosterComboBox(box.getRosterEntryComboBox(),txt);
2101    }
2102
2103    private boolean setRosterComboBox(RosterEntryComboBox box, String txt) {
2104        boolean found = false;
2105        for (int i = 1; i < box.getItemCount(); i++) {
2106            if (txt.equals(((RosterEntry) box.getItemAt(i)).getId())) {
2107                box.setSelectedIndex(i);
2108                found = true;
2109                break;
2110            }
2111        }
2112        if (!found && box.getItemCount() > 0) {
2113            box.setSelectedIndex(0);
2114        }
2115        return found;
2116    }
2117
2118    // Normalizes a suggested xml file name.  Returns null string if a valid name cannot be assembled
2119    private String normalizeXmlFileName(String name) {
2120        if (name.length() < 1) {
2121            return "";
2122        }
2123        String newName = name;
2124        // strip off .xml or .XML if present
2125        if ((name.endsWith(".xml")) || (name.endsWith(".XML"))) {
2126            newName = name.substring(0, name.length() - 4);
2127            if (newName.length() < 1) {
2128                return "";
2129            }
2130        }
2131        // replace all non-alphanumeric characters with underscore
2132        newName = newName.replaceAll("[\\W]", "_");
2133        return (newName + ".xml");
2134    }
2135
2136    private boolean setTrainComboBox(JComboBox<Object> box, String txt) {
2137        boolean found = false;
2138        for (int i = 1; i < box.getItemCount(); i++) { //skip the select train item
2139            if (txt.equals(box.getItemAt(i).toString())) {
2140                box.setSelectedIndex(i);
2141                found = true;
2142                break;
2143            }
2144        }
2145        if (!found && box.getItemCount() > 0) {
2146            box.setSelectedIndex(0);
2147        }
2148        return found;
2149    }
2150
2151    private boolean setComboBox(JComboBox<String> box, String txt) {
2152        boolean found = false;
2153        for (int i = 0; i < box.getItemCount(); i++) {
2154            if (txt.equals(box.getItemAt(i))) {
2155                box.setSelectedIndex(i);
2156                found = true;
2157                break;
2158            }
2159        }
2160        if (!found && box.getItemCount() > 0) {
2161            box.setSelectedIndex(0);
2162        }
2163        return found;
2164    }
2165
2166    int delayModeFromBox(JComboBox<String> box) {
2167        String mode = (String) box.getSelectedItem();
2168        int result = jmri.util.StringUtil.getStateFromName(mode, delayedStartInt, delayedStartString);
2169
2170        if (result < 0) {
2171            log.warn("unexpected mode string in turnoutMode: {}", mode);
2172            throw new IllegalArgumentException();
2173        }
2174        return result;
2175    }
2176
2177    void setDelayModeBox(int mode, JComboBox<String> box) {
2178        String result = jmri.util.StringUtil.getNameFromState(mode, delayedStartInt, delayedStartString);
2179        box.setSelectedItem(result);
2180    }
2181
2182    /**
2183     * The following are for items that are only for automatic running of
2184     * ActiveTrains They are isolated here to simplify changing them in the
2185     * future.
2186     * <ul>
2187     * <li>initializeAutoRunItems - initializes the display of auto run items in
2188     * this window
2189     * <li>initializeAutoRunValues - initializes the values of auto run items
2190     * from values in a saved train info file hideAutoRunItems - hides all auto
2191     * run items in this window showAutoRunItems - shows all auto run items in
2192     * this window
2193     * <li>autoTrainInfoToDialog - gets auto run items from a train info, puts
2194     * values in items, and initializes auto run dialog items
2195     * <li>autoTrainItemsToTrainInfo - copies values of auto run items to train
2196     * info for saving to a file
2197     * <li>readAutoRunItems - reads and checks values of all auto run items.
2198     * returns true if OK, sends appropriate messages and returns false if not
2199     * OK
2200     * <li>setAutoRunItems - sets the user entered auto run items in the new
2201     * AutoActiveTrain
2202     * </ul>
2203     */
2204    // auto run items in ActivateTrainFrame
2205    private final JPanel pa1 = new JPanel();
2206    private final JLabel speedFactorLabel = new JLabel(Bundle.getMessage("SpeedFactorLabel"));
2207    private final JSpinner speedFactorSpinner = new JSpinner();
2208    private final JLabel minReliableOperatingSpeedLabel = new JLabel(Bundle.getMessage("MinReliableOperatingSpeedLabel"));
2209    private final JSpinner minReliableOperatingSpeedSpinner = new JSpinner();
2210    private final JLabel minReliableOperatingScaleSpeedLabel = new JLabel();
2211    private final JSpinner maxSpeedSpinner = new JSpinner();
2212    private final JComboBox<MaxSpeedCapModeItem> maxSpeedCapModeBox = new JComboBox<>();
2213    private final JLabel maxSpeedUnitLabel = new JLabel("%"); // changes to "mph" or "km/h" when speed mode selected
2214    // Suppress mode-change events while programmatically rebuilding the dropdown
2215    private boolean suppressMaxSpeedCapModeEvents = false;
2216    private boolean suppressMaxSpeedSpinnerEvents = false;
2217    // Remember last user-visible mode so we can convert values on mode switches
2218    private MaxSpeedCapMode lastMaxSpeedCapMode = MaxSpeedCapMode.THROTTLE;
2219    private float cachedThrottlePercent = 1.0f; // spinner shows 0.10..1.00; we cache user's last % (0.0..1.0)
2220    private float cachedScaleMph       = 100.0f; // default "sensible" scale speed (mph)
2221    private final JPanel pa2 = new JPanel();
2222    private final JLabel rampRateLabel = new JLabel(Bundle.getMessage("RampRateBoxLabel"));
2223    private final JComboBox<String> rampRateBox = new JComboBox<>();
2224    private boolean suppressRampRateEvents = false;
2225    // Remember last non-Physics selection so we can revert if Physics is disallowed
2226    private String lastNonPhysicsRampSelection = null;
2227    private boolean rampRateRendererInstalled = false;
2228
2229    private final JPanel pa2a = new JPanel();
2230    private final JLabel useSpeedProfileLabel = new JLabel(Bundle.getMessage("UseSpeedProfileLabel"));
2231    private final JCheckBox useSpeedProfileCheckBox = new JCheckBox( );
2232    private final JLabel stopBySpeedProfileLabel = new JLabel(Bundle.getMessage("StopBySpeedProfileLabel"));
2233    private final JCheckBox stopBySpeedProfileCheckBox = new JCheckBox( );
2234    private final JLabel stopBySpeedProfileAdjustLabel = new JLabel(Bundle.getMessage("StopBySpeedProfileAdjustLabel"));
2235    // Explicit override: ignore hardware Stop Sensors in Sections (default OFF = use sensors)
2236    private final JCheckBox overrideStopSensorCheckBox = new JCheckBox(Bundle.getMessage("OverrideStopSensorLabel"));
2237    private final JSpinner stopBySpeedProfileAdjustSpinner = new JSpinner();
2238    private final JPanel pa2b = new JPanel();
2239    private final JLabel stopByDistanceLabel = new JLabel(Bundle.getMessage("StopByDistanceLabel"));
2240    private final JSpinner stopByDistanceMmSpinner = new JSpinner();
2241    private final JRadioButton stopByDistanceHead = new JRadioButton(Bundle.getMessage("StopByDistanceHead"));
2242    private final JRadioButton stopByDistanceTail = new JRadioButton(Bundle.getMessage("StopByDistanceTail"));
2243    private final ButtonGroup stopByDistanceRefGroup = new ButtonGroup();
2244    private final JCheckBox stopByDistanceEnableCheckBox = new JCheckBox();
2245
2246    private enum StopDistanceUnits {
2247        ACTUAL_CM,
2248        ACTUAL_MM,
2249        ACTUAL_INCHES,
2250        SCALE_METERS,
2251        SCALE_FEET
2252    }
2253    
2254    protected static class StopDistanceUnitsItem {
2255        private final String key;
2256        private final StopDistanceUnits value;
2257        public StopDistanceUnitsItem(String text, StopDistanceUnits units) { this.key = text; this.value = units; }
2258        @Override public String toString() { return key; }
2259        public StopDistanceUnits getValue() { return value; }
2260    }
2261    
2262    protected static class StopDistanceUnitsJCombo extends JComboBox<StopDistanceUnitsItem> {
2263        public StopDistanceUnits getSelectedUnits() {
2264            // getSelectedItem() is Object in Swing; use a narrow cast or index->getItemAt(i)
2265            StopDistanceUnitsItem it = (StopDistanceUnitsItem) getSelectedItem();
2266            return it != null ? it.getValue() : StopDistanceUnits.ACTUAL_CM;
2267
2268        }
2269    }    
2270    
2271     // Helper: is "Physics" ramp selected?
2272    private boolean isPhysicsRampSelected() {
2273        // rampRateBox contains display strings from Bundle.getMessage(...)
2274        String sel = (String) rampRateBox.getSelectedItem();
2275        return sel != null && sel.equals(Bundle.getMessage("RAMP_PHYSICS"));
2276    }
2277
2278    // Determine if the Physics ramp option should be allowed for the current context.
2279    // Requirement: If a concrete roster entry has been selected (Trains From Roster, and an actual entry chosen),
2280    // then Physics must only be selectable when that roster entry has physics metadata configured.
2281    // Otherwise (e.g. Set Later, user-defined, ops trains, or no roster entry chosen yet), Physics remains selectable.
2282    private boolean isPhysicsRampAllowedForCurrentContext() {
2283        if (!radioTrainsFromRoster.isSelected()) {
2284            return true;
2285        }
2286        Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
2287        if (!(sel instanceof RosterEntry)) {
2288            // Either "Select Loco" placeholder or nothing selected yet: allow Physics.
2289            return true;
2290        }
2291        return rosterEntryHasPhysicsParameters((RosterEntry) sel);
2292    }
2293
2294    // Return true if the roster entry contains any non-default physics metadata.
2295    // Defaults are all numeric fields == 0, traction type == DIESEL_ELECTRIC, mechanical transmission == false.
2296    private boolean rosterEntryHasPhysicsParameters(RosterEntry re) {
2297        if (re == null) {
2298            return false;
2299        }
2300        boolean anyNumeric = (re.getPhysicsWeightKg() > 0.0f) ||
2301                (re.getPhysicsPowerKw() > 0.0f) ||
2302                (re.getPhysicsTractiveEffortKn() > 0.0f) ||
2303                (re.getPhysicsMaxSpeedKmh() > 0.0f);
2304        boolean anyNonDefaultTraction = (re.getPhysicsTractionType() != null &&
2305                re.getPhysicsTractionType() != RosterEntry.TractionType.DIESEL_ELECTRIC);
2306        boolean mech = re.isPhysicsMechanicalTransmission();
2307        return anyNumeric || anyNonDefaultTraction || mech;
2308    }
2309
2310    // Renderer that greys out the Physics option when present but not allowed.
2311    private class RampRateCellRenderer extends DefaultListCellRenderer {
2312        @Override
2313        public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected,
2314                boolean cellHasFocus) {
2315            Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
2316            if (value != null &&
2317                    value.equals(Bundle.getMessage("RAMP_PHYSICS")) &&
2318                    !isPhysicsRampAllowedForCurrentContext()) {
2319                c.setForeground(Color.GRAY);
2320            }
2321            return c;
2322        }
2323    }
2324
2325    // Ensure the ramp-rate combobox has our custom renderer installed once.
2326    private void ensureRampRateRendererInstalled() {
2327        if (rampRateRendererInstalled) {
2328            return;
2329        }
2330        rampRateBox.setRenderer(new RampRateCellRenderer());
2331        rampRateRendererInstalled = true;
2332    }
2333
2334    // If Physics is selected but disallowed, immediately revert selection to a safe non-Physics value.
2335    private void enforcePhysicsRampSelectionAllowed() {
2336        if (!isPhysicsRampSelected()) {
2337            return;
2338        }
2339        if (isPhysicsRampAllowedForCurrentContext()) {
2340            return;
2341        }
2342        suppressRampRateEvents = true;
2343        try {
2344            String fallback = lastNonPhysicsRampSelection;
2345            if (fallback == null ||
2346                    !comboContainsItem(rampRateBox, fallback) ||
2347                    Bundle.getMessage("RAMP_PHYSICS").equals(fallback)) {
2348                // Prefer Speed Profile if present, else first item.
2349                String sp = Bundle.getMessage("RAMP_SPEEDPROFILE");
2350                if (comboContainsItem(rampRateBox, sp)) {
2351                    fallback = sp;
2352                } else if (rampRateBox.getItemCount() > 0) {
2353                    fallback = rampRateBox.getItemAt(0);
2354                }
2355            }
2356            if (fallback != null) {
2357                rampRateBox.setSelectedItem(fallback);
2358            }
2359        } finally {
2360            suppressRampRateEvents = false;
2361        }
2362        pa2Physics.setVisible(false);
2363    }
2364
2365    private boolean comboContainsItem(JComboBox<String> box, String item) {
2366        if (item == null) {
2367            return false;
2368        }
2369        for (int i = 0; i < box.getItemCount(); i++) {
2370            String o = box.getItemAt(i);
2371            if (item.equals(o)) {
2372                return true;
2373            }
2374        }
2375        return false;
2376    }
2377
2378  
2379     // Add/remove "Physics" in the ramp rate box depending on speed-profile being enabled & selected
2380     // Preserve the previous selection if it still exists after the rebuild.
2381     private void updateRampPhysicsAvailability(boolean speedProfileOn) {
2382         // Snapshot current selection text (display label)
2383         String prev = (String) rampRateBox.getSelectedItem();
2384    
2385         // Collect current items, excluding any existing "Physics" to avoid duplicates
2386         java.util.List<String> toKeep = new java.util.ArrayList<>();
2387         for (int i = 0; i < rampRateBox.getItemCount(); i++) {
2388             String it = rampRateBox.getItemAt(i);
2389             if (!Bundle.getMessage("RAMP_PHYSICS").equals(it)) {
2390                 toKeep.add(it);
2391             }
2392         }
2393    
2394         // Rebuild the list
2395         rampRateBox.removeAllItems();
2396         for (String it : toKeep) {
2397             rampRateBox.addItem(it);
2398         }
2399         if (speedProfileOn) {
2400             rampRateBox.addItem(Bundle.getMessage("RAMP_PHYSICS"));
2401         }
2402    
2403         // Try to restore previous selection if it still exists
2404         boolean restored = false;
2405         if (prev != null) {
2406             for (int i = 0; i < rampRateBox.getItemCount(); i++) {
2407                 if (prev.equals(rampRateBox.getItemAt(i))) {
2408                     rampRateBox.setSelectedIndex(i);
2409                     restored = true;
2410                     break;
2411                 }
2412             }
2413         }
2414    
2415         // If Physics was selected but is no longer available, fall back to SPEEDPROFILE (if present) or first item
2416         if (!restored && prev != null && Bundle.getMessage("RAMP_PHYSICS").equals(prev) && !speedProfileOn) {
2417             boolean set = false;
2418             for (int i = 0; i < rampRateBox.getItemCount(); i++) {
2419                 String candidate = rampRateBox.getItemAt(i);
2420                 if (Bundle.getMessage("RAMP_SPEEDPROFILE").equals(candidate)) {
2421                     rampRateBox.setSelectedIndex(i);
2422                     set = true;
2423                     break;
2424                 }
2425             }
2426             if (!set && rampRateBox.getItemCount() > 0) {
2427                 rampRateBox.setSelectedIndex(0);
2428             }
2429         }
2430    
2431         // Physics panel visibility follows current selection
2432         pa2Physics.setVisible(isPhysicsRampSelected());
2433     }
2434    
2435     // Selection changed -> toggle physics panel
2436     // Selection changed -> toggle physics panel
2437     private void handleRampRateSelectionChanged() {
2438         if (suppressRampRateEvents) {
2439             return;
2440         }
2441         ensureRampRateRendererInstalled();
2442         String sel = (String) rampRateBox.getSelectedItem();
2443         if (sel == null) {
2444             pa2Physics.setVisible(false);
2445             return;
2446         }
2447         // Track last non-Physics choice for fallback
2448         if (!sel.equals(Bundle.getMessage("RAMP_PHYSICS"))) {
2449             lastNonPhysicsRampSelection = sel;
2450         }
2451         if (sel.equals(Bundle.getMessage("RAMP_PHYSICS")) && !isPhysicsRampAllowedForCurrentContext()) {
2452             // Disallowed: revert immediately
2453             enforcePhysicsRampSelectionAllowed();
2454             rampRateBox.repaint();
2455             return;
2456         }
2457         pa2Physics.setVisible(isPhysicsRampSelected());
2458         rampRateBox.repaint();
2459     }
2460  
2461    private final StopDistanceUnitsJCombo stopByDistanceUnitsComboBox = new StopDistanceUnitsJCombo();
2462
2463     // --- Physics "Additional train weight" UI + units ---
2464     // Units: Metric tonnes (t), Long tons (imperial), Short tons (US)
2465    
2466     // Top-level enum: referenced across several methods
2467     private enum AdditionalWeightUnits { METRIC_TONNES, LONG_TONS, SHORT_TONS }
2468    
2469     protected static class AdditionalWeightUnitsItem {
2470         private final String key;
2471         private final AdditionalWeightUnits value;
2472         public AdditionalWeightUnitsItem(String text, AdditionalWeightUnits units) {
2473             this.key = text;
2474             this.value = units;
2475         }
2476         @Override public String toString() { return key; }
2477         public AdditionalWeightUnits getValue() { return value; }
2478     }
2479    
2480     protected static class AdditionalWeightUnitsJCombo extends JComboBox<AdditionalWeightUnitsItem> {
2481         public AdditionalWeightUnits getSelectedUnits() {
2482             AdditionalWeightUnitsItem it = (AdditionalWeightUnitsItem) getSelectedItem();
2483             return it != null ? it.getValue() : AdditionalWeightUnits.METRIC_TONNES;
2484         }
2485     }
2486
2487     // Panel + controls
2488     private final JPanel pa2Physics = new JPanel();
2489     private final JLabel additionalWeightLabel = new JLabel(Bundle.getMessage("AdditionalTrainWeightLabel"));
2490     private final JSpinner additionalWeightSpinner = new JSpinner();
2491     private final AdditionalWeightUnitsJCombo additionalWeightUnitsComboBox = new AdditionalWeightUnitsJCombo();
2492     
2493     // Rolling resistance coefficient (dimensionless)
2494     private final JLabel rollingResistanceCoeffLabel = new JLabel(Bundle.getMessage("RollingResistanceCoeffLabel"));
2495     private final JSpinner rollingResistanceCoeffSpinner = new JSpinner();
2496     
2497    // Driver power (% of full power/regulator) used during acceleration when Physics ramp is selected
2498    private final JLabel driverPowerPercentLabel = new JLabel(Bundle.getMessage("DriverPowerPercentLabel"));
2499    private final JSpinner driverPowerPercentSpinner = new JSpinner();
2500        
2501     // Track current display units for conversion
2502     private AdditionalWeightUnits currentAdditionalWeightUnits = AdditionalWeightUnits.METRIC_TONNES;
2503    
2504     // Unit conversions: all values stored to TrainInfo as metric tonnes
2505     private static float convertTonnesToDisplay(AdditionalWeightUnits units, float tonnes) {
2506         switch (units) {
2507             case METRIC_TONNES: return tonnes;            // t
2508             case LONG_TONS:     return tonnes / 1.0160469f; // 1 long ton ≈ 1.0160469 t
2509             case SHORT_TONS:    return tonnes / 0.9071847f; // 1 short ton ≈ 0.9071847 t
2510             default:            return tonnes;
2511         }
2512     }
2513     private static float convertDisplayToTonnes(AdditionalWeightUnits units, float value) {
2514         switch (units) {
2515             case METRIC_TONNES: return value;            // t
2516             case LONG_TONS:     return value * 1.0160469f;
2517             case SHORT_TONS:    return value * 0.9071847f;
2518             default:            return value;
2519         }
2520     }
2521
2522    // Track the “current UI units” so we can convert correctly when user changes the dropdown
2523    private StopDistanceUnits currentStopDistanceUnits = StopDistanceUnits.ACTUAL_CM;
2524    // Prevent recursion when synchronising Stop-by-distance units with Max Train Length units.
2525    private boolean suppressDistanceAndTrainLengthUnitSync = false;
2526
2527    private final JPanel pa3 = new JPanel();
2528    private final JCheckBox soundDecoderBox = new JCheckBox(Bundle.getMessage("SoundDecoder"));
2529    private final JCheckBox runInReverseBox = new JCheckBox(Bundle.getMessage("RunInReverse"));
2530    private final JPanel pa4 = new JPanel();
2531    private final JLabel fNumberBellLabel = new JLabel(Bundle.getMessage("fnumberbelllabel"));
2532    private final JSpinner fNumberBellSpinner = new JSpinner();
2533    private final JLabel fNumberHornLabel = new JLabel(Bundle.getMessage("fnumberhornlabel"));
2534    private final JSpinner fNumberHornSpinner = new JSpinner();
2535    private final JLabel fNumberLightLabel = new JLabel(Bundle.getMessage("fnumberlightlabel"));
2536    private final JSpinner fNumberLightSpinner = new JSpinner();
2537    private final JPanel pa5_FNumbers = new JPanel();
2538    protected static class TrainDetectionJCombo extends JComboBox<TrainDetectionItem> {
2539        public void setSelectedItemByValue(TrainDetection trainDetVar) {
2540            for ( int ix = 0; ix < getItemCount() ; ix ++ ) {
2541                if (getItemAt(ix).value == trainDetVar) {
2542                    this.setSelectedIndex(ix);
2543                    break;
2544                }
2545            }
2546        }
2547    }
2548
2549    private final JLabel trainDetectionLabel = new JLabel(Bundle.getMessage("TrainDetection"));
2550    public final TrainDetectionJCombo trainDetectionComboBox = new TrainDetectionJCombo();
2551
2552    protected static class TrainLengthUnitsJCombo extends JComboBox<TrainLengthUnitsItem> {
2553        public void setSelectedItemByValue(TrainLengthUnits var) {
2554            for ( int ix = 0; ix < getItemCount() ; ix ++ ) {
2555                if (getItemAt(ix).value == var) {
2556                    this.setSelectedIndex(ix);
2557                    break;
2558                }
2559            }
2560        }
2561    }
2562
2563    /* ComboBox item for speed-cap mode. */
2564    protected enum MaxSpeedCapMode { THROTTLE, SCALE_MPH, SCALE_KMH }
2565
2566    protected static class MaxSpeedCapModeItem {
2567        private final String key;
2568        private final MaxSpeedCapMode value;
2569        public MaxSpeedCapModeItem(String text, MaxSpeedCapMode mode) { this.key = text; this.value = mode; }
2570        @Override public String toString() { return key; }
2571        public MaxSpeedCapMode getValue() { return value; }
2572    }
2573
2574    public final TrainLengthUnitsJCombo trainLengthUnitsComboBox = new TrainLengthUnitsJCombo();
2575    private final JLabel trainLengthLabel = new JLabel(Bundle.getMessage("MaxTrainLengthLabel"));
2576    private JLabel trainLengthAltLengthLabel; // I18N Label
2577    private final JSpinner maxTrainLengthSpinner = new JSpinner(); // initialized later
2578    // Track current units displayed in the spinner and suppress conversions during programmatic updates
2579    private TrainLengthUnits currentTrainLengthUnits = TrainLengthUnits.TRAINLENGTH_SCALEMETERS;
2580    private boolean suppressTrainLengthUnitsEvents = false;
2581
2582    private void initializeAutoRunItems() {
2583        initializeRampCombo();
2584        initializeScaleLengthBox();
2585        pa1.setLayout(new FlowLayout());
2586        pa1.add(speedFactorLabel);
2587        speedFactorSpinner.setModel(new SpinnerNumberModel(Float.valueOf(1.0f), Float.valueOf(0.1f), Float.valueOf(2.0f), Float.valueOf(0.01f)));
2588        speedFactorSpinner.setEditor(new JSpinner.NumberEditor(speedFactorSpinner, "# %"));
2589        pa1.add(speedFactorSpinner);
2590        speedFactorSpinner.setToolTipText(Bundle.getMessage("SpeedFactorHint"));
2591        pa1.add(new JLabel("   "));
2592     // Mode dropdown
2593     maxSpeedCapModeBox.addItem(
2594         new MaxSpeedCapModeItem(Bundle.getMessage("MaxSpeedLabel"), MaxSpeedCapMode.THROTTLE)
2595     ); // default; speed entries added later when roster profile is available
2596     pa1.add(maxSpeedCapModeBox);
2597
2598     // Initial spinner/editor state: throttle % mode
2599     updateMaxSpeedSpinnerModelForMode(MaxSpeedCapMode.THROTTLE);
2600
2601     // Spinner + unit label (unit changes with dropdown)
2602     pa1.add(maxSpeedSpinner);
2603     pa1.add(maxSpeedUnitLabel);
2604     maxSpeedSpinner.addChangeListener(e -> updateMaxSpeedCachesFromSpinner());
2605
2606      // --- Max Speed mode change: % <-> mph/km/h (with profile-aware conversions and sticky fallbacks) ---
2607      maxSpeedCapModeBox.addActionListener(new java.awt.event.ActionListener() {
2608          @Override
2609          public void actionPerformed(java.awt.event.ActionEvent ev) {
2610    
2611              // Ignore programmatic changes while rebuilding the combo model.
2612              if (suppressMaxSpeedCapModeEvents) {
2613                  return;
2614              }
2615    
2616              Object sel = maxSpeedCapModeBox.getSelectedItem();
2617              if (!(sel instanceof MaxSpeedCapModeItem)) {
2618                  // Transient state while the model is being rebuilt.
2619                  return;
2620              }
2621    
2622              // 1) Capture current spinner value and the previous/new modes.
2623              float prevDisplay = ((Number) maxSpeedSpinner.getValue()).floatValue();
2624              MaxSpeedCapMode prevMode = lastMaxSpeedCapMode;
2625              MaxSpeedCapMode mode     = ((MaxSpeedCapModeItem) sel).getValue();
2626    
2627              // Keep our sticky caches aligned with what the user just had visible.
2628              switch (prevMode) {
2629                case THROTTLE:
2630                    // Clamp percent [0.10 .. 1.00] before caching
2631                    if (prevDisplay < 0.10f) prevDisplay = 0.10f;
2632                    if (prevDisplay > 1.00f) prevDisplay = 1.00f;
2633                    cachedThrottlePercent = prevDisplay;
2634                    break;
2635                  case SCALE_MPH:
2636                      cachedScaleMph = prevDisplay;
2637                      break;
2638                  case SCALE_KMH:
2639                      cachedScaleMph = kmhToMph(prevDisplay);
2640                      break;
2641                  default:
2642                      break;
2643              }
2644    
2645              // 2) Compute the new display value for the target mode.
2646              float newDisplay = prevDisplay;
2647    
2648              // mph <-> km/h always converts the number, then clamps to the new spinner model.
2649              if (prevMode == MaxSpeedCapMode.SCALE_KMH && mode == MaxSpeedCapMode.SCALE_MPH) {
2650                  newDisplay = kmhToMph(prevDisplay);
2651                  newDisplay = Math.max(1.0f, Math.min(200.0f, newDisplay)); // clamp to MPH model 1..200
2652    
2653              } else if (prevMode == MaxSpeedCapMode.SCALE_MPH && mode == MaxSpeedCapMode.SCALE_KMH) {
2654                  newDisplay = mphToKmh(prevDisplay);
2655                  newDisplay = Math.max(1.0f, Math.min(320.0f, newDisplay)); // clamp to KMH model 1..320
2656    
2657              // % -> mph/km/h : only convert if a concrete roster speed profile is available
2658              } else if (prevMode == MaxSpeedCapMode.THROTTLE
2659                      && (mode == MaxSpeedCapMode.SCALE_MPH || mode == MaxSpeedCapMode.SCALE_KMH)) {
2660    
2661                  if (isConcreteSpeedProfileAvailable()) {
2662                      if (mode == MaxSpeedCapMode.SCALE_MPH) {
2663                          newDisplay = percentToScaleMph(prevDisplay);
2664                          newDisplay = Math.max(1.0f, Math.min(200.0f, newDisplay));
2665                      } else {
2666                          newDisplay = percentToScaleKmh(prevDisplay);
2667                          newDisplay = Math.max(1.0f, Math.min(320.0f, newDisplay));
2668                      }
2669                  } else {
2670                      // No profile: do NOT convert. Show the last sticky scale speed.
2671                      newDisplay = (mode == MaxSpeedCapMode.SCALE_MPH)
2672                              ? cachedScaleMph
2673                              : mphToKmh(cachedScaleMph);
2674                  }
2675    
2676              // mph/km/h -> % : only convert if a concrete roster speed profile is available
2677              } else if ((prevMode == MaxSpeedCapMode.SCALE_MPH || prevMode == MaxSpeedCapMode.SCALE_KMH)
2678                      && mode == MaxSpeedCapMode.THROTTLE) {
2679    
2680                  if (isConcreteSpeedProfileAvailable()) {
2681                      if (prevMode == MaxSpeedCapMode.SCALE_MPH) {
2682                          newDisplay = scaleSpeedToPercentFromProfile(prevDisplay, false); // mph -> %
2683                      } else {
2684                          newDisplay = scaleSpeedToPercentFromProfile(prevDisplay, true);  // km/h -> %
2685                      }
2686                      // Clamp to spinner's % model 0.10..1.00
2687                      newDisplay = Math.max(0.10f, Math.min(1.00f, newDisplay));
2688                  } else {
2689                           // No profile: do NOT convert. Show the last sticky % value (clamped)
2690                           newDisplay = cachedThrottlePercent;
2691                           newDisplay = Math.max(0.10f, Math.min(1.00f, newDisplay));
2692                         }  
2693              } else {
2694                  // Same-mode selection or THROTTLE->THROTTLE: keep numeric as-is
2695              }
2696    
2697              // 3) Update spinner model/editor/unit to the new mode, then set the display value.
2698                suppressMaxSpeedSpinnerEvents = true;
2699                try {
2700                    // Set the visible mode first so any incidental listeners see the correct mode
2701                    lastMaxSpeedCapMode = mode;
2702                
2703                    // Now change the spinner model and the numeric value
2704                    updateMaxSpeedSpinnerModelForMode(mode);
2705                    maxSpeedSpinner.setValue(Float.valueOf(newDisplay));
2706                } finally {
2707                    suppressMaxSpeedSpinnerEvents = false;
2708                }
2709                handleMinReliableOperatingSpeedUpdate();
2710          }
2711        });
2712        maxSpeedSpinner.setToolTipText(Bundle.getMessage("MaxSpeedHint"));
2713        pa1.add(minReliableOperatingSpeedLabel);
2714        minReliableOperatingSpeedSpinner.setModel(new SpinnerNumberModel(Float.valueOf(0.0f), Float.valueOf(0.0f), Float.valueOf(1.0f), Float.valueOf(0.01f)));
2715        minReliableOperatingSpeedSpinner.setEditor(new JSpinner.NumberEditor(minReliableOperatingSpeedSpinner, "# %"));
2716        pa1.add(minReliableOperatingSpeedSpinner);
2717        minReliableOperatingSpeedSpinner.setToolTipText(Bundle.getMessage("MinReliableOperatingSpeedHint"));
2718        minReliableOperatingSpeedSpinner.addChangeListener( e -> handleMinReliableOperatingSpeedUpdate());
2719        pa1.add(minReliableOperatingScaleSpeedLabel);
2720        initiatePane.add(pa1);
2721        pa2.setLayout(new FlowLayout());
2722        pa2.add(rampRateLabel);
2723        pa2.add(rampRateBox);
2724        rampRateBox.setToolTipText(Bundle.getMessage("RampRateBoxHint"));
2725        pa2.add(useSpeedProfileLabel);
2726        pa2.add(useSpeedProfileCheckBox);
2727        useSpeedProfileCheckBox.setToolTipText(Bundle.getMessage("UseSpeedProfileHint"));
2728        initiatePane.add(pa2);
2729        pa2a.setLayout(new FlowLayout());
2730        pa2a.add(stopBySpeedProfileLabel);
2731        pa2a.add(stopBySpeedProfileCheckBox);
2732        stopBySpeedProfileCheckBox.setToolTipText(Bundle.getMessage("UseSpeedProfileHint")); // reuse identical hint for Stop
2733        pa2a.add(stopBySpeedProfileAdjustLabel);
2734        stopBySpeedProfileAdjustSpinner.setModel(new SpinnerNumberModel( Float.valueOf(1.0f), Float.valueOf(0.1f), Float.valueOf(5.0f), Float.valueOf(0.01f)));
2735        stopBySpeedProfileAdjustSpinner.setEditor(new JSpinner.NumberEditor(stopBySpeedProfileAdjustSpinner, "# %"));
2736        pa2a.add(stopBySpeedProfileAdjustSpinner);
2737        stopBySpeedProfileAdjustSpinner.setToolTipText(Bundle.getMessage("StopBySpeedProfileAdjustHint"));
2738        initiatePane.add(pa2a);       
2739
2740         // “Override stop sensor” (default OFF = use sensors when present).
2741         // NOTE: No mutual exclusion with Stop-by-speed-profile or distance mode.
2742         // When checked, ignore stop sensors and rely on distance/profile stopping.
2743         // When unchecked, use sensors if present; runtime will slow within distance and finally stop at the sensor.
2744         pa2a.add(overrideStopSensorCheckBox);
2745         overrideStopSensorCheckBox.setToolTipText(Bundle.getMessage("OverrideStopSensorHint"));
2746         overrideStopSensorCheckBox.addActionListener(ev -> {
2747             // Keep UI coherent, but do NOT disable profile/distance options here.
2748             updateStopByDistanceEnable();
2749         });
2750            
2751        pa2b.add(stopByDistanceLabel);
2752        pa2b.add(stopByDistanceEnableCheckBox);
2753        
2754        // Distance value first
2755        stopByDistanceMmSpinner.setModel(
2756            new SpinnerNumberModel(Float.valueOf(0.0f), Float.valueOf(0.0f), Float.valueOf(1000000.0f), Float.valueOf(0.1f))
2757        );
2758        stopByDistanceMmSpinner.setEditor(new JSpinner.NumberEditor(stopByDistanceMmSpinner, "0.0"));
2759        stopByDistanceMmSpinner.setToolTipText(Bundle.getMessage("StopByDistanceHint"));
2760        pa2b.add(stopByDistanceMmSpinner);
2761        
2762        // Units dropdown next 
2763        stopByDistanceUnitsComboBox.addItem(
2764                new StopDistanceUnitsItem(bundleOrDefault("StopByDistanceUnitsCm", "cm"), StopDistanceUnits.ACTUAL_CM));
2765        stopByDistanceUnitsComboBox.addItem(
2766            new StopDistanceUnitsItem(Bundle.getMessage("StopByDistanceUnitsMm"), StopDistanceUnits.ACTUAL_MM)
2767        );
2768        stopByDistanceUnitsComboBox.addItem(
2769            new StopDistanceUnitsItem(Bundle.getMessage("StopByDistanceUnitsInch"), StopDistanceUnits.ACTUAL_INCHES)
2770        );
2771        stopByDistanceUnitsComboBox.addItem(
2772            new StopDistanceUnitsItem(Bundle.getMessage("StopByDistanceUnitsScaleMeters"), StopDistanceUnits.SCALE_METERS)
2773        );
2774        stopByDistanceUnitsComboBox.addItem(
2775            new StopDistanceUnitsItem(Bundle.getMessage("StopByDistanceUnitsScaleFeet"), StopDistanceUnits.SCALE_FEET)
2776        );
2777        pa2b.add(stopByDistanceUnitsComboBox);
2778
2779         // Initialize Physics visibility based on current availability (enabled state)
2780         updateRampPhysicsAvailability(useSpeedProfileCheckBox.isEnabled());
2781        
2782        // Head/Tail radios last (to the right of the units dropdown)
2783        stopByDistanceRefGroup.add(stopByDistanceHead);
2784        stopByDistanceRefGroup.add(stopByDistanceTail);
2785        
2786        // Localised tooltips for Head/Tail reference selection
2787        stopByDistanceHead.setToolTipText(Bundle.getMessage("StopByDistanceHeadHint"));
2788        stopByDistanceTail.setToolTipText(Bundle.getMessage("StopByDistanceTailHint"));
2789        
2790        stopByDistanceHead.setSelected(true);
2791        pa2b.add(stopByDistanceHead);
2792        pa2b.add(stopByDistanceTail);
2793        
2794        initiatePane.add(pa2b);
2795
2796        // Event wiring:
2797        // - Toggle mutually-exclusive mode (adjust% vs. distance) and availability
2798        stopByDistanceEnableCheckBox.addActionListener(ev -> updateStopByDistanceEnable());
2799        stopBySpeedProfileCheckBox.addActionListener(ev -> updateStopByDistanceEnable());
2800    
2801        // - Units change: convert current displayed value from old units to new, preserving the underlying mm value
2802        stopByDistanceUnitsComboBox.addActionListener(ev -> handleStopByDistanceUnitsComboSelectionChanged());
2803
2804        updateStopByDistanceEnable();
2805        StopDistanceUnits preferredStopUnits = getPreferredStopDistanceUnitsFromMaxTrainLengthUnits();
2806        currentStopDistanceUnits = preferredStopUnits;
2807        setStopByDistanceUnitsSelection(preferredStopUnits);
2808        updateStopByDistanceSpinnerModelForUnits(preferredStopUnits);
2809        pa2b.add(stopByDistanceUnitsComboBox);      
2810
2811         // --- Physics: Additional train weight panel ---
2812         pa2Physics.setLayout(new FlowLayout());
2813         pa2Physics.add(additionalWeightLabel);
2814    
2815         // Numeric spinner: wide range, fine step; display value depends on chosen units
2816         additionalWeightSpinner.setModel(
2817             new SpinnerNumberModel(Float.valueOf(0.0f), Float.valueOf(0.0f), Float.valueOf(10000.0f), Float.valueOf(0.1f))
2818         );
2819         additionalWeightSpinner.setEditor(new JSpinner.NumberEditor(additionalWeightSpinner, "0.0"));
2820         additionalWeightSpinner.setToolTipText(Bundle.getMessage("AdditionalTrainWeightHint"));
2821         pa2Physics.add(additionalWeightSpinner);
2822    
2823         // Units dropdown (replaces a static label); conversions are UI-only; TrainInfo stores metric tonnes
2824        additionalWeightUnitsComboBox.addItem(new AdditionalWeightUnitsItem(Bundle.getMessage("AdditionalWeightUnitsMetricTonnes"), AdditionalWeightUnits.METRIC_TONNES));
2825        additionalWeightUnitsComboBox.addItem(new AdditionalWeightUnitsItem(Bundle.getMessage("AdditionalWeightUnitsLongTons"),   AdditionalWeightUnits.LONG_TONS));
2826        additionalWeightUnitsComboBox.addItem(new AdditionalWeightUnitsItem(Bundle.getMessage("AdditionalWeightUnitsShortTons"),  AdditionalWeightUnits.SHORT_TONS));
2827        pa2Physics.add(additionalWeightUnitsComboBox);     
2828         
2829         // Rolling resistance coefficient (dimensionless, default 0.002)
2830         rollingResistanceCoeffSpinner.setModel(
2831             new SpinnerNumberModel(Float.valueOf(0.002f), Float.valueOf(0.000f), Float.valueOf(0.020f), Float.valueOf(0.0001f))
2832         );
2833         rollingResistanceCoeffSpinner.setEditor(new JSpinner.NumberEditor(rollingResistanceCoeffSpinner, "0.0000"));
2834         rollingResistanceCoeffSpinner.setToolTipText(Bundle.getMessage("RollingResistanceCoeffHint"));
2835         pa2Physics.add(rollingResistanceCoeffLabel);
2836         pa2Physics.add(rollingResistanceCoeffSpinner);      
2837
2838          // Driver power during acceleration (% of full power); stored 0..1 in TrainInfo
2839          driverPowerPercentSpinner.setModel(
2840              new SpinnerNumberModel(Float.valueOf(100.0f), Float.valueOf(10.0f), Float.valueOf(100.0f), Float.valueOf(1.0f))
2841          );
2842          driverPowerPercentSpinner.setEditor(new JSpinner.NumberEditor(driverPowerPercentSpinner, "##0'%'"));
2843          driverPowerPercentSpinner.setToolTipText(Bundle.getMessage("DriverPowerPercentHint"));
2844          pa2Physics.add(driverPowerPercentLabel);
2845          pa2Physics.add(driverPowerPercentSpinner);
2846
2847         // Rolling resistance coefficient (dimensionless, default 0.002)
2848         rollingResistanceCoeffSpinner.setModel(
2849             new SpinnerNumberModel(Float.valueOf(0.002f), Float.valueOf(0.000f), Float.valueOf(0.020f), Float.valueOf(0.0001f))
2850         );
2851         rollingResistanceCoeffSpinner.setEditor(new JSpinner.NumberEditor(rollingResistanceCoeffSpinner, "0.0000"));
2852         rollingResistanceCoeffSpinner.setToolTipText(Bundle.getMessage("RollingResistanceCoeffHint"));
2853         pa2Physics.add(rollingResistanceCoeffLabel);
2854         pa2Physics.add(rollingResistanceCoeffSpinner);
2855            
2856         // Units change => convert displayed value to new units, preserving underlying tonnes
2857         additionalWeightUnitsComboBox.addActionListener(ev -> {
2858             float display = ((Number) additionalWeightSpinner.getValue()).floatValue();
2859             float tonnes = convertDisplayToTonnes(currentAdditionalWeightUnits, display);
2860             currentAdditionalWeightUnits = additionalWeightUnitsComboBox.getSelectedUnits();
2861             float newDisplay = convertTonnesToDisplay(currentAdditionalWeightUnits, tonnes);
2862             additionalWeightSpinner.setValue(Float.valueOf(newDisplay));
2863         });
2864    
2865         // Initially hidden; becomes visible only when speed profile is ON and ramp == Physics
2866         pa2Physics.setVisible(false);
2867         initiatePane.add(pa2Physics);
2868    
2869        // Events to keep Physics option availability in sync with "Use speed profile"
2870        useSpeedProfileCheckBox.addActionListener(ev -> {
2871            // Show/hide Physics purely based on availability (enabled), not checkbox selection.
2872            updateRampPhysicsAvailability(useSpeedProfileCheckBox.isEnabled());
2873        });
2874        rampRateBox.addActionListener(ev -> handleRampRateSelectionChanged());        
2875        pa3.setLayout(new FlowLayout());
2876        pa3.add(soundDecoderBox);
2877        soundDecoderBox.setToolTipText(Bundle.getMessage("SoundDecoderBoxHint"));
2878        pa3.add(new JLabel("   "));
2879        pa3.add(runInReverseBox);
2880        runInReverseBox.setToolTipText(Bundle.getMessage("RunInReverseBoxHint"));
2881        initiatePane.add(pa3);
2882        maxTrainLengthSpinner.setModel(new SpinnerNumberModel(Float.valueOf(18.0f), Float.valueOf(0.0f), Float.valueOf(10000.0f), Float.valueOf(0.5f)));
2883        maxTrainLengthSpinner.setEditor(new JSpinner.NumberEditor(maxTrainLengthSpinner, "###0.0"));
2884        maxTrainLengthSpinner.setToolTipText(Bundle.getMessage("MaxTrainLengthHint")); // won't be updated while Dispatcher is open
2885        maxTrainLengthSpinner.addChangeListener( e -> handlemaxTrainLengthChangeUnitsLength());
2886        trainLengthUnitsComboBox.addActionListener(e -> handleTrainLengthUnitsComboSelectionChanged());
2887        trainLengthAltLengthLabel=new JLabel();
2888        pa4.setLayout(new FlowLayout());
2889        pa4.add(trainLengthLabel);
2890        pa4.add(maxTrainLengthSpinner);
2891        pa4.add(trainLengthUnitsComboBox);
2892        pa4.add(trainLengthAltLengthLabel);
2893        initiatePane.add(pa4);
2894        pa5_FNumbers.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("fnumbers")));
2895        pa5_FNumbers.setLayout(new FlowLayout());
2896        fNumberLightSpinner.setModel(new SpinnerNumberModel(0,0,100,1));
2897        fNumberLightSpinner.setToolTipText(Bundle.getMessage("fnumberlighthint"));
2898        pa5_FNumbers.add(fNumberLightLabel);
2899        pa5_FNumbers.add(fNumberLightSpinner);
2900        fNumberBellSpinner.setModel(new SpinnerNumberModel(0,0,100,1));
2901        fNumberBellSpinner.setToolTipText(Bundle.getMessage("fnumberbellhint"));
2902        pa5_FNumbers.add(fNumberBellLabel);
2903        pa5_FNumbers.add(fNumberBellSpinner);
2904        fNumberHornSpinner.setModel(new SpinnerNumberModel(0,0,100,1));
2905        fNumberHornSpinner.setToolTipText(Bundle.getMessage("fnumberhornhint"));
2906        pa5_FNumbers.add(fNumberHornLabel);
2907        pa5_FNumbers.add(fNumberHornSpinner);
2908        initiatePane.add(pa5_FNumbers);
2909        showHideAutoRunItems(autoRunBox.isSelected());   // initialize with auto run items hidden
2910    }
2911
2912    private void handleMinReliableOperatingSpeedUpdate() {
2913        // Read % as float
2914        float mROS = ((Number) minReliableOperatingSpeedSpinner.getValue()).floatValue();
2915        // Clear label by default
2916        minReliableOperatingScaleSpeedLabel.setText("");
2917    
2918        // Only attempt conversion when speed-profile UI is enabled
2919        if (!useSpeedProfileCheckBox.isEnabled()) {
2920            return;
2921        }
2922    
2923        // RosterEntryComboBox is JComboBox<Object>; first item is a String ("no selection").
2924        Object sel = rosterComboBox.getRosterEntryComboBox().getSelectedItem();
2925        if (!(sel instanceof RosterEntry)) {
2926            // No roster entry selected yet; nothing to display
2927            return;
2928        }
2929    
2930        RosterEntry re = (RosterEntry) sel;
2931        RosterSpeedProfile sp = re.getSpeedProfile();
2932        if (sp == null || sp.getProfileSize() < 1) {
2933            // No profile data; nothing to display
2934            return;
2935        }
2936    
2937        // Convert % -> mm/s in the selected running direction, then format in the currently selected preferred units
2938        float mms = sp.getSpeed(mROS, isMaxSpeedProfileForward());
2939        minReliableOperatingScaleSpeedLabel.setText(formatScaleSpeedWithPreferredUnits(mms));
2940    }
2941
2942    private void handlemaxTrainLengthChangeUnitsLength() {
2943        trainLengthAltLengthLabel.setText(maxTrainLengthCalculateAltFormatted(
2944                ((TrainLengthUnitsItem) trainLengthUnitsComboBox.getSelectedItem()).getValue(),
2945                (float) maxTrainLengthSpinner.getValue()));
2946    }
2947
2948     // Convert the displayed length when the user changes the units combo.
2949     // We preserve the actual length by converting display -> scale meters -> new display units.
2950     private void handleTrainLengthUnitsChanged() {
2951         if (suppressTrainLengthUnitsEvents) {
2952             // Programmatic change (e.g., file load): just refresh the alternate label.
2953             handlemaxTrainLengthChangeUnitsLength();
2954             return;
2955         }
2956         TrainLengthUnits newUnits =
2957             ((TrainLengthUnitsItem) trainLengthUnitsComboBox.getSelectedItem()).getValue();
2958    
2959         // 1) Capture the current display value and convert it to scale meters.
2960         float currentDisplay = ((Number) maxTrainLengthSpinner.getValue()).floatValue();
2961         float scaleMeters = maxTrainLengthToScaleMeters(currentTrainLengthUnits, currentDisplay);
2962    
2963         // 2) Convert the common baseline (scale meters) to the newly selected display units.
2964         float newDisplay = scaleMetersToDisplay(newUnits, scaleMeters);
2965    
2966         // 3) Update spinner without re‑entering this handler; update current unit tracker.
2967         suppressTrainLengthUnitsEvents = true;
2968         try {
2969             maxTrainLengthSpinner.setValue(Float.valueOf(newDisplay));
2970             currentTrainLengthUnits = newUnits;
2971         } finally {
2972             suppressTrainLengthUnitsEvents = false;
2973         }
2974    
2975         // 4) Keep the alternate-length label in sync.
2976         handlemaxTrainLengthChangeUnitsLength();
2977     }
2978
2979
2980    /**
2981     * Get an I18N String of the max TrainLength.
2982     * @param fromUnits the Length Unit.
2983     * @param fromValue the length.
2984     * @return String format of the length.
2985     */
2986    private String maxTrainLengthCalculateAltFormatted(TrainLengthUnits fromUnits, float fromValue) {
2987        float value = maxTrainLengthCalculateAlt(fromUnits, fromValue);
2988        switch (fromUnits) {
2989            case TRAINLENGTH_ACTUALINCHS:
2990                return String.format(Locale.getDefault(), "%.2f %s",
2991                    value, Bundle.getMessage("TrainLengthInScaleFeet"));
2992            case TRAINLENGTH_ACTUALCM:
2993                return String.format(Locale.getDefault(), "%.1f %s",
2994                    value, Bundle.getMessage("TrainLengthInScaleMeters"));
2995            case TRAINLENGTH_SCALEFEET:
2996                return String.format(Locale.getDefault(), "%.1f %s",
2997                    value, Bundle.getMessage("TrainLengthInActualInchs"));
2998            case TRAINLENGTH_SCALEMETERS:
2999                return String.format(Locale.getDefault(), "%.0f %s",
3000                    value, Bundle.getMessage("TrainLengthInActualcm"));
3001            default:
3002                log.error("Invalid TrainLengthUnits must have been updated, fix maxTrainLengthCalculateAltFormatted");
3003        }
3004        return "";
3005    }
3006
3007    private float maxTrainLengthToScaleMeters(TrainLengthUnits fromUnits, float fromValue) {
3008        float value;
3009        final float scaleRatio = (_dispatcher.getScale() != null)
3010                ? (float) _dispatcher.getScale().getScaleRatio()
3011                : 1.0f;
3012        // convert to meters.
3013        switch (fromUnits) {
3014            case TRAINLENGTH_ACTUALINCHS:
3015                value = fromValue / 12.0f * scaleRatio;
3016                value = value / 3.28084f;
3017                break;
3018            case TRAINLENGTH_ACTUALCM:
3019                value = fromValue / 100.0f * scaleRatio;
3020                break;
3021           case TRAINLENGTH_SCALEFEET:
3022               value = fromValue / 3.28084f;
3023               break;
3024           case TRAINLENGTH_SCALEMETERS:
3025               value = fromValue;
3026               break;
3027           default:
3028               value = 0;
3029               log.error("Invalid TrainLengthUnits has been updated, fix me");
3030        }
3031        return value;
3032    }
3033    
3034    /**
3035     * Convert from scale meters to the requested display units.
3036     */
3037    private float scaleMetersToDisplay(TrainLengthUnits toUnits, float scaleMeters) {
3038        final float scaleFactor = (_dispatcher.getScale() != null)
3039                ? (float) _dispatcher.getScale().getScaleFactor()
3040                : 1.0f; // CI-safe default
3041    
3042        switch (toUnits) {
3043            case TRAINLENGTH_SCALEMETERS:
3044                return scaleMeters;
3045            case TRAINLENGTH_SCALEFEET:
3046                return scaleMeters * 3.28084f;
3047            case TRAINLENGTH_ACTUALINCHS:
3048                // actual inches = scale meters × scaleFactor (scale→actual) × feet/m × 12 in/ft
3049                return scaleMeters * scaleFactor * 3.28084f * 12.0f;
3050            case TRAINLENGTH_ACTUALCM:
3051                // actual cm = scale meters × scaleFactor (scale→actual) × 100 cm/m
3052                return scaleMeters * scaleFactor * 100.0f;
3053            default:
3054                return scaleMeters;
3055        }
3056    }
3057
3058    /**
3059     * Calculates the reciprocal unit. Actual to Scale and vice versa
3060     */
3061    private float maxTrainLengthCalculateAlt(TrainLengthUnits fromUnits, float fromValue) {
3062        final float scaleRatio = (_dispatcher.getScale() != null)
3063                ? (float) _dispatcher.getScale().getScaleRatio()
3064                : 1.0f;
3065        switch (fromUnits) {
3066            case TRAINLENGTH_ACTUALINCHS:
3067                // calc scale feet
3068                return (float) jmri.util.MathUtil.granulize(fromValue / 12 * scaleRatio, 0.1f);
3069            case TRAINLENGTH_ACTUALCM:
3070                // calc scale meter
3071                return fromValue / 100 * scaleRatio;
3072            case TRAINLENGTH_SCALEFEET: { // calc actual inches
3073                final float scaleFactor = (_dispatcher.getScale() != null)
3074                        ? (float) _dispatcher.getScale().getScaleFactor()
3075                        : 1.0f;
3076                return fromValue * 12.0f * scaleFactor;
3077            }
3078            case TRAINLENGTH_SCALEMETERS: { // calc actual cm.
3079                final float scaleFactor = (_dispatcher.getScale() != null)
3080                        ? (float) _dispatcher.getScale().getScaleFactor()
3081                        : 1.0f;
3082                return fromValue * 100.0f * scaleFactor;
3083            }
3084           default:
3085               log.error("Invalid TrainLengthUnits has been updated, fix me");
3086        }
3087        return 0;
3088    }
3089
3090    private void showHideAutoRunItems(boolean value) {
3091        pa1.setVisible(value);
3092        pa2.setVisible(value);
3093        pa2a.setVisible(value);
3094        pa3.setVisible(value);
3095        pa4.setVisible(value || detectionNeedsLength());
3096        pa5_FNumbers.setVisible(value);
3097    }
3098
3099    private void autoTrainInfoToDialog(TrainInfo info) {
3100        speedFactorSpinner.setValue(info.getSpeedFactor());
3101        // Choose mode by presence of scale km/h
3102        boolean hasScaleKmh = info.getMaxSpeedScaleKmh() > 0.0f;
3103        if (hasScaleKmh && useSpeedProfileCheckBox.isEnabled()) {
3104            // Default to km/h display when loading from file
3105            maxSpeedCapModeBox.setSelectedIndex(Math.min(2, maxSpeedCapModeBox.getItemCount()-1)); // item 2 is KMH when enabled
3106            updateMaxSpeedSpinnerModelForMode(MaxSpeedCapMode.SCALE_KMH);
3107            maxSpeedSpinner.setValue(info.getMaxSpeedScaleKmh());
3108        } else {
3109            maxSpeedCapModeBox.setSelectedIndex(0); // THROTTLE
3110            updateMaxSpeedSpinnerModelForMode(MaxSpeedCapMode.THROTTLE);
3111            maxSpeedSpinner.setValue(info.getMaxSpeed());
3112        }
3113        minReliableOperatingSpeedSpinner.setValue(info.getMinReliableOperatingSpeed());
3114        String rampLabel = normalizeRampLabel(info.getRampRate()); 
3115
3116         // Physics: set additional weight spinner (convert stored metric tonnes to current UI units)
3117         currentAdditionalWeightUnits = AdditionalWeightUnits.METRIC_TONNES;
3118         additionalWeightUnitsComboBox.setSelectedIndex(0);
3119         additionalWeightSpinner.setValue(Float.valueOf(convertTonnesToDisplay(currentAdditionalWeightUnits, info.getAdditionalTrainWeightMetricTonnes())));       
3120         additionalWeightUnitsComboBox.setSelectedIndex(0);
3121         rollingResistanceCoeffSpinner.setValue(Float.valueOf(info.getRollingResistanceCoeff()));
3122         
3123         // Driver power % -> spinner uses 0..100; file stores 0..1
3124         float dp = info.getDriverPowerPercent();
3125         if (dp <= 0.0f) dp = 0.0f;
3126         if (dp > 1.0f) dp = 1.0f;
3127         driverPowerPercentSpinner.setValue(Float.valueOf(dp * 100.0f));
3128    
3129         // Physics availability & panel visibility based on current Speed-profile + ramp
3130        useSpeedProfileCheckBox.setSelected(info.getUseSpeedProfile());
3131        // Physics availability depends only on whether speed profile UI is enabled (availability), not on selection
3132        updateRampPhysicsAvailability(useSpeedProfileCheckBox.isEnabled());
3133        
3134        // Now that the items are rebuilt, set the ramp selection using the normalized label
3135        setComboBox(rampRateBox, rampLabel);
3136        
3137        // Physics weight row visibility follows the current ramp selection
3138        pa2Physics.setVisible(isPhysicsRampSelected());
3139
3140      
3141        trainDetectionComboBox.setSelectedItemByValue(info.getTrainDetection());
3142        runInReverseBox.setSelected(info.getRunInReverse());
3143        soundDecoderBox.setSelected(info.getSoundDecoder());
3144        try {
3145            trainLengthUnitsComboBox.setSelectedItemByValue(info.getTrainLengthUnits());
3146            switch (info.getTrainLengthUnits()) {
3147                case TRAINLENGTH_SCALEFEET:
3148                    maxTrainLengthSpinner.setValue(info.getMaxTrainLengthScaleFeet());
3149                    break;
3150                case TRAINLENGTH_SCALEMETERS:
3151                    maxTrainLengthSpinner.setValue(info.getMaxTrainLengthScaleMeters());
3152                    break;
3153                case TRAINLENGTH_ACTUALINCHS: {
3154                    float sf = (_dispatcher.getScale() != null)
3155                        ? (float)_dispatcher.getScale().getScaleFactor()
3156                        : 1.0f; // CI-safe default
3157                    maxTrainLengthSpinner.setValue(info.getMaxTrainLengthScaleFeet() * 12.0f * sf);
3158                    break;
3159                }
3160                case TRAINLENGTH_ACTUALCM: {
3161                    float sf = (_dispatcher.getScale() != null)
3162                        ? (float)_dispatcher.getScale().getScaleFactor()
3163                        : 1.0f; // CI-safe default
3164                    maxTrainLengthSpinner.setValue(info.getMaxTrainLengthScaleMeters() * 100.0f * sf);
3165                    break;
3166                }
3167    
3168                default:
3169                    maxTrainLengthSpinner.setValue(0.0f);
3170            }
3171        } finally {
3172            suppressTrainLengthUnitsEvents = false;
3173        }
3174        
3175        useSpeedProfileCheckBox.setSelected(info.getUseSpeedProfile());
3176        stopBySpeedProfileCheckBox.setSelected(info.getStopBySpeedProfile());
3177        stopBySpeedProfileAdjustSpinner.setValue(info.getStopBySpeedProfileAdjust());
3178        overrideStopSensorCheckBox.setSelected(!info.getUseStopSensor());
3179        updateStopByDistanceEnable();
3180        stopByDistanceEnableCheckBox.setSelected(info.getStopByDistanceMm() > 0.0f);
3181    
3182        // Default Stop-by-distance units follow current Max Train Length units. Convert the stored mm to current display units.
3183        StopDistanceUnits preferredStopUnits = getPreferredStopDistanceUnitsFromMaxTrainLengthUnits();
3184        currentStopDistanceUnits = preferredStopUnits;
3185        setStopByDistanceUnitsSelection(preferredStopUnits);
3186        float displayValue = convertMmToStopDisplay(currentStopDistanceUnits, info.getStopByDistanceMm());
3187        stopByDistanceMmSpinner.setValue(Float.valueOf(displayValue));
3188        updateStopByDistanceSpinnerModelForUnits(currentStopDistanceUnits);
3189    
3190         if (info.getStopByDistanceRef() == TrainInfo.StopReference.TAIL) {
3191             stopByDistanceTail.setSelected(true);
3192         } else {
3193             stopByDistanceHead.setSelected(true);
3194         }
3195         updateStopByDistanceEnable();
3196        fNumberLightSpinner.setValue(info.getFNumberLight());
3197        fNumberBellSpinner.setValue(info.getFNumberBell());
3198        fNumberHornSpinner.setValue(info.getFNumberHorn());
3199         showHideAutoRunItems(autoRunBox.isSelected());
3200        initiateFrame.pack();
3201    }
3202
3203    private void autoRunItemsToTrainInfo(TrainInfo info) {
3204        info.setSpeedFactor((float) speedFactorSpinner.getValue());
3205        MaxSpeedCapMode mode = ((MaxSpeedCapModeItem) maxSpeedCapModeBox.getSelectedItem()).getValue();
3206        if (mode == MaxSpeedCapMode.THROTTLE) {
3207            // Throttle mode: write % (0.0..1.0) and clear scale-speed
3208            info.setMaxSpeed((float) maxSpeedSpinner.getValue());
3209            info.setMaxSpeedScaleKmh(0.0f);
3210        } else if (mode == MaxSpeedCapMode.SCALE_MPH) {
3211            // Convert mph → km/h for storage
3212            float mph = ((Number) maxSpeedSpinner.getValue()).floatValue();
3213            info.setMaxSpeedScaleKmh(mphToKmh(mph));
3214            // Preserve existing throttle % (fallback) untouched
3215        } else { // SCALE_KMH
3216            float kmh = ((Number) maxSpeedSpinner.getValue()).floatValue();
3217            info.setMaxSpeedScaleKmh(kmh);
3218            // Preserve existing throttle % (fallback) untouched
3219        }
3220        info.setMinReliableOperatingSpeed((float) minReliableOperatingSpeedSpinner.getValue());
3221        info.setRampRate((String) rampRateBox.getSelectedItem());
3222         // Physics: when ramp == Physics, store additional weight (metric tonnes); else store 0.0f
3223         if (isPhysicsRampSelected()) {
3224             float display = ((Number) additionalWeightSpinner.getValue()).floatValue();
3225             float tonnes = convertDisplayToTonnes(currentAdditionalWeightUnits, display);
3226             info.setAdditionalTrainWeightMetricTonnes(tonnes);
3227         } else {
3228             info.setAdditionalTrainWeightMetricTonnes(0.0f);
3229         }
3230
3231
3232         // Driver power percent: only meaningful for Physics ramp, but we always persist (default 1.0 when not physics)
3233         float dpct = ((Number) driverPowerPercentSpinner.getValue()).floatValue() / 100.0f;
3234         if (dpct < 0.0f) dpct = 0.0f; else if (dpct > 1.0f) dpct = 1.0f;
3235         info.setDriverPowerPercent(isPhysicsRampSelected() ? dpct : 1.0f);
3236             
3237         // Always store c_rr (independent of ramp selection)
3238        info.setRollingResistanceCoeff(((Number) rollingResistanceCoeffSpinner.getValue()).floatValue());
3239        info.setRunInReverse(runInReverseBox.isSelected());
3240        info.setSoundDecoder(soundDecoderBox.isSelected());
3241        info.setTrainLengthUnits(((TrainLengthUnitsItem) trainLengthUnitsComboBox.getSelectedItem()).getValue());
3242        info.setMaxTrainLengthScaleMeters(maxTrainLengthToScaleMeters( info.getTrainLengthUnits(), (float) maxTrainLengthSpinner.getValue()));
3243
3244        // Only use speed profile values if enabled
3245        if (useSpeedProfileCheckBox.isEnabled()) {
3246            info.setUseSpeedProfile(useSpeedProfileCheckBox.isSelected());
3247            info.setStopBySpeedProfile(stopBySpeedProfileCheckBox.isSelected());
3248            info.setStopBySpeedProfileAdjust((float) stopBySpeedProfileAdjustSpinner.getValue());
3249        } else {
3250            info.setUseSpeedProfile(false);
3251            info.setStopBySpeedProfile(false);
3252            info.setStopBySpeedProfileAdjust(1.0f);
3253        }       
3254
3255         // Persist inverse of “Override stop sensor” (unchecked = use sensors)
3256         info.setUseStopSensor(!overrideStopSensorCheckBox.isSelected());
3257
3258         // Only meaningful if Stop-by-speed-profile is enabled & selected
3259         boolean baseOn = stopBySpeedProfileCheckBox.isEnabled() && stopBySpeedProfileCheckBox.isSelected();
3260         if (baseOn && stopByDistanceEnableCheckBox.isSelected()) {
3261             float displayValue = ((Number) stopByDistanceMmSpinner.getValue()).floatValue();
3262             float mm = convertStopDisplayToMm(currentStopDistanceUnits, displayValue);
3263    
3264             info.setStopByDistanceMm(mm);
3265             info.setStopByDistanceRef(stopByDistanceTail.isSelected()
3266                 ? TrainInfo.StopReference.TAIL
3267                 : TrainInfo.StopReference.HEAD);
3268         } else {
3269             info.setStopByDistanceMm(0.0f);
3270             info.setStopByDistanceRef(TrainInfo.StopReference.HEAD);
3271         }
3272    
3273            info.setFNumberLight((int)fNumberLightSpinner.getValue());
3274            info.setFNumberBell((int)fNumberBellSpinner.getValue());
3275            info.setFNumberHorn((int)fNumberHornSpinner.getValue());
3276        }
3277
3278    // Map legacy ramp values (numeric codes or Bundle keys) to the current display label used in rampRateBox.
3279     private String normalizeRampLabel(String raw) {
3280         if (raw == null || raw.trim().isEmpty()) {
3281             return Bundle.getMessage("RAMP_NONE");
3282         }
3283         String s = raw.trim();
3284    
3285         // Numeric legacy codes -> display labels; order must match AutoActiveTrain constants
3286         if (s.matches("\\d+")) {
3287             switch (Integer.parseInt(s)) {
3288                 case 0: return Bundle.getMessage("RAMP_NONE");
3289                 case 1: return Bundle.getMessage("RAMP_FAST");
3290                 case 2: return Bundle.getMessage("RAMP_MEDIUM");
3291                 case 3: return Bundle.getMessage("RAMP_MED_SLOW");
3292                 case 4: return Bundle.getMessage("RAMP_SLOW");
3293                 case 5: return Bundle.getMessage("RAMP_SPEEDPROFILE");
3294                 case 6: return Bundle.getMessage("RAMP_PHYSICS");
3295                 default: return Bundle.getMessage("RAMP_NONE");
3296             }
3297         }
3298    
3299         // Bundle key -> display label (e.g., "RAMP_MEDIUM")
3300         if ("RAMP_NONE".equals(s))         return Bundle.getMessage("RAMP_NONE");
3301         if ("RAMP_FAST".equals(s))         return Bundle.getMessage("RAMP_FAST");
3302         if ("RAMP_MEDIUM".equals(s))       return Bundle.getMessage("RAMP_MEDIUM");
3303         if ("RAMP_MED_SLOW".equals(s))     return Bundle.getMessage("RAMP_MED_SLOW");
3304         if ("RAMP_SLOW".equals(s))         return Bundle.getMessage("RAMP_SLOW");
3305         if ("RAMP_SPEEDPROFILE".equals(s)) return Bundle.getMessage("RAMP_SPEEDPROFILE");
3306         if ("RAMP_PHYSICS".equals(s))      return Bundle.getMessage("RAMP_PHYSICS");
3307    
3308         // Otherwise assume it's already a localized display label
3309         return s;
3310     }
3311  
3312    private void initializeRampCombo() {
3313        rampRateBox.removeAllItems();
3314        rampRateBox.addItem(Bundle.getMessage("RAMP_NONE"));
3315        rampRateBox.addItem(Bundle.getMessage("RAMP_FAST"));
3316        rampRateBox.addItem(Bundle.getMessage("RAMP_MEDIUM"));
3317        rampRateBox.addItem(Bundle.getMessage("RAMP_MED_SLOW"));
3318        rampRateBox.addItem(Bundle.getMessage("RAMP_SLOW"));
3319        rampRateBox.addItem(Bundle.getMessage("RAMP_SPEEDPROFILE"));
3320        // Default fallback if Physics cannot be selected
3321        lastNonPhysicsRampSelection = Bundle.getMessage("RAMP_SPEEDPROFILE");
3322        rampRateBox.addItem(Bundle.getMessage("RAMP_PHYSICS")); // Visible only when speed-profile is enabled & selected
3323        // Note: the order above must correspond to the numbers in AutoActiveTrain.java
3324    }
3325
3326    /**
3327     * Sets up the RadioButtons and visability of spinner for the allocation method
3328     *
3329     * @param value 0, Allocate by Safe spots, -1, allocate as far as possible Any
3330     *            other value the number of sections to allocate
3331     */
3332    private void setAllocateMethodButtons(int value) {
3333        switch (value) {
3334            case ActiveTrain.ALLOCATE_BY_SAFE_SECTIONS:
3335                allocateBySafeRadioButton.setSelected(true);
3336                allocateCustomSpinner.setVisible(false);
3337                break;
3338            case ActiveTrain.ALLOCATE_AS_FAR_AS_IT_CAN:
3339                allocateAllTheWayRadioButton.setSelected(true);
3340                allocateCustomSpinner.setVisible(false);
3341                break;
3342            default:
3343                allocateNumberOfBlocks.setSelected(true);
3344                allocateCustomSpinner.setVisible(true);
3345                allocateCustomSpinner.setValue(value);
3346        }
3347    }
3348
3349    /*
3350     * Layout block stuff
3351     */
3352    private ArrayList<LayoutBlock> getOccupiedBlockList() {
3353        LayoutBlockManager lBM = InstanceManager.getDefault(LayoutBlockManager.class);
3354        ArrayList<LayoutBlock> lBlocks = new ArrayList<>();
3355        for (LayoutBlock lB : lBM.getNamedBeanSet()) {
3356            if (lB.getBlock().getState() == Block.OCCUPIED) {
3357                lBlocks.add(lB);
3358            }
3359        }
3360        return lBlocks;
3361    }
3362
3363    private void initializeStartingBlockComboDynamic() {
3364        startingBlockBox.removeAllItems();
3365        startingBlockBoxList.clear();
3366        for (LayoutBlock lB: getOccupiedBlockList()) {
3367            if (!startingBlockBoxList.contains(lB.getBlock())) {
3368                startingBlockBoxList.add(lB.getBlock());
3369                startingBlockBox.addItem(getBlockName(lB.getBlock()));
3370            }
3371        }
3372        JComboBoxUtil.setupComboBoxMaxRows(startingBlockBox);
3373    }
3374
3375    private void initializeViaBlockDynamicCombo() {
3376        String prevValue = (String) viaBlockBox.getSelectedItem();
3377        viaBlockBox.removeActionListener(viaBlockBoxListener);
3378        viaBlockBox.removeAllItems();
3379        viaBlockBoxList.clear();
3380        LayoutBlockManager lBM = InstanceManager.getDefault(LayoutBlockManager.class);
3381        if (startingBlockBox.getSelectedItem() != null) {
3382            LayoutBlock lBSrc;
3383            if (startingBlockBox.getSelectedIndex() >= 0) {
3384                lBSrc = lBM.getByUserName((String) startingBlockBox.getSelectedItem());
3385                if (lBSrc != null) {
3386                    int rX = lBSrc.getNumberOfNeighbours() - 1;
3387                    for (; rX > -1; rX--) {
3388                        viaBlockBox.addItem(lBSrc.getNeighbourAtIndex(rX).getDisplayName());
3389                        viaBlockBoxList.add(lBSrc.getNeighbourAtIndex(rX));
3390                    }
3391                }
3392            }
3393        }
3394        if (prevValue != null) {
3395            viaBlockBox.setSelectedItem(prevValue);
3396        }
3397        viaBlockBox.addActionListener(viaBlockBoxListener);
3398    }
3399
3400    private void initializeDestinationBlockDynamicCombo() {
3401        destinationBlockBox.removeAllItems();
3402        destinationBlockBoxList.clear();
3403        LayoutBlockManager lBM = InstanceManager.getDefault(LayoutBlockManager.class);
3404        if (startingBlockBox.getSelectedItem() != null) {
3405            LayoutBlock lBSrc;
3406            if (startingBlockBox.getSelectedIndex() >= 0
3407                    && viaBlockBox.getSelectedIndex() >= 0) {
3408                lBSrc = lBM.getByUserName((String) startingBlockBox.getSelectedItem());
3409                Block b = viaBlockBoxList.get(viaBlockBox.getSelectedIndex());
3410                if (lBSrc != null) {
3411                    int rX = lBSrc.getNumberOfRoutes() - 1;
3412                    for (; rX > -1; rX--) {
3413                        if (lBSrc.getRouteNextBlockAtIndex(rX) == b) {
3414                            destinationBlockBox.addItem(lBSrc.getRouteDestBlockAtIndex(rX).getDisplayName());
3415                            destinationBlockBoxList.add(lBSrc.getRouteDestBlockAtIndex(rX));
3416                        }
3417                    }
3418                }
3419            }
3420        }
3421    }
3422
3423    /*
3424     * Check Advanced routing
3425    */
3426    private boolean checkAdvancedRouting() {
3427        if (!InstanceManager.getDefault(LayoutBlockManager.class).isAdvancedRoutingEnabled()) {
3428            int response = JmriJOptionPane.showConfirmDialog(this, Bundle.getMessage("AdHocNeedsEnableBlockRouting"),
3429                    Bundle.getMessage("AdHocNeedsBlockRouting"), JmriJOptionPane.YES_NO_OPTION);
3430            if (response == 0) {
3431                InstanceManager.getDefault(LayoutBlockManager.class).enableAdvancedRouting(true);
3432                JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("AdhocNeedsBlockRoutingEnabled"));
3433            } else {
3434                return false;
3435            }
3436        }
3437        return true;
3438    }
3439    
3440    
3441
3442    /*
3443     * ComboBox item.
3444     */
3445    protected static class TrainDetectionItem {
3446
3447        private final String key;
3448        private TrainDetection value;
3449
3450        public TrainDetectionItem(String text, TrainDetection trainDetection ) {
3451            this.key = text;
3452            this.value = trainDetection;
3453        }
3454
3455        @Override
3456        public String toString() {
3457            return key;
3458        }
3459
3460        public String getKey() {
3461            return key;
3462        }
3463
3464        public TrainDetection getValue() {
3465            return value;
3466        }
3467    }
3468
3469    /*
3470     * ComboBox item.
3471     */
3472    protected static class TrainLengthUnitsItem {
3473
3474        private final String key;
3475        private TrainLengthUnits value;
3476
3477        public TrainLengthUnitsItem(String text, TrainLengthUnits trainLength ) {
3478            this.key = text;
3479            this.value = trainLength;
3480        }
3481
3482        @Override
3483        public String toString() {
3484            return key;
3485        }
3486
3487        public String getKey() {
3488            return key;
3489        }
3490
3491        public TrainLengthUnits getValue() {
3492            return value;
3493        }
3494    }
3495
3496    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ActivateTrainFrame.class);
3497
3498}