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