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