001package jmri.jmrit.roster; 002 003import java.awt.FlowLayout; 004import java.awt.GridBagConstraints; 005import java.awt.GridBagLayout; 006import java.awt.Insets; 007import java.util.MissingResourceException; 008 009import javax.swing.ButtonGroup; 010import javax.swing.JButton; 011import javax.swing.JCheckBox; 012import javax.swing.JComboBox; 013import javax.swing.JLabel; 014import javax.swing.JPanel; 015import javax.swing.JRadioButton; 016import javax.swing.JSpinner; 017import javax.swing.SpinnerNumberModel; 018import javax.swing.JTextArea; 019import javax.swing.UIManager; 020 021/** 022 * Display and enable editing of Physics parameters for a {@link RosterEntry}. 023 * This is intended to be hosted as its own tab in the programmer window. 024 */ 025public class RosterPhysicsPane extends JPanel { 026 027 private static final long serialVersionUID = 1L; 028 029 private RosterEntry re; 030 031 // Persist unit selections per roster entry (stored as non-localised codes in RosterEntry attributes) 032 private static final String PHYSICS_WEIGHT_UNIT_ATTR = "physicsWeightUnit"; 033 private static final String PHYSICS_POWER_UNIT_ATTR = "physicsPowerUnit"; 034 private static final String PHYSICS_TE_UNIT_ATTR = "physicsTractiveEffortUnit"; 035 private static final String PHYSICS_SPEED_UNIT_ATTR = "physicsMaxSpeedUnit"; 036 037 038 // --- Physics (locomotive-level) controls --- 039 private final JRadioButton physicsSteamRadio = new JRadioButton(Bundle.getMessage("PhysicsSteam")); 040 private final JRadioButton physicsDieselElectricRadio = 041 new JRadioButton(Bundle.getMessage("PhysicsDieselElectric")); 042 private final JCheckBox physicsMechanicalTransmissionCheck = 043 new JCheckBox(Bundle.getMessage("PhysicsMechanicalTransmission")); 044 private final ButtonGroup physicsTractionGroup = new ButtonGroup(); 045 046 // Unit combos per field (display units; storage stays metric in RosterEntry) 047 private final JComboBox<String> physicsWeightUnitCombo = new JComboBox<>(); 048 private final JComboBox<String> physicsPowerUnitCombo = new JComboBox<>(); 049 private final JComboBox<String> physicsTeUnitCombo = new JComboBox<>(); 050 private final JComboBox<String> physicsSpeedUnitCombo = new JComboBox<>(); 051 052 // Editors (spinners) 053 private final JSpinner physicsWeightSpinner = new JSpinner(); 054 private final JSpinner physicsPowerSpinner = new JSpinner(); 055 private final JSpinner physicsTractiveEffortSpinner = new JSpinner(); 056 private final JSpinner physicsMaxSpeedSpinner = new JSpinner(); 057 058 private boolean refreshing = false; 059 060 /** 061 * Return the localized title to use when this pane is added as a tab. This 062 * method exists so callers in other packages don't need direct access to 063 * this package's Bundle helper. 064 * 065 * @return localized tab title 066 */ 067 public static String getTabTitle() { 068 return Bundle.getMessage("PhysicsTabTitle"); 069 } 070 071 private static String getPhysicsPaneExplanation() { 072 try { 073 return Bundle.getMessage("PhysicsPaneExplanation"); 074 } catch (MissingResourceException ex) { 075 // Fallback to avoid hard failure if the resource key is not present. 076 return "These controls are primarily used for physics-based acceleration. They affect how JMRI calculates throttle changes over time when accelerating."; 077 } 078 } 079 080 public RosterPhysicsPane(RosterEntry r) { 081 super(); 082 re = r; 083 084 initGui(); 085 wireListeners(); 086 087 // Apply any per-roster saved unit selections before showing values 088 applyUnitSelectionsFromRosterEntry(re); 089 // Initialize display from current roster entry (metric storage -> chosen units) 090 refreshFromRosterEntry(); 091 } 092 093 private void initGui() { 094 GridBagLayout gbLayout = new GridBagLayout(); 095 GridBagConstraints cL = new GridBagConstraints(); 096 GridBagConstraints cR = new GridBagConstraints(); 097 098 setLayout(gbLayout); 099 100 // Explanation text for the Physics pane 101 JTextArea explanation = new JTextArea(getPhysicsPaneExplanation()); 102 explanation.setEditable(false); 103 explanation.setLineWrap(true); 104 explanation.setWrapStyleWord(true); 105 explanation.setOpaque(false); 106 explanation.setFocusable(false); 107 explanation.setFont(UIManager.getFont("Label.font")); 108 explanation.setForeground(UIManager.getColor("Label.foreground")); 109 GridBagConstraints cE = new GridBagConstraints(); 110 cE.gridx = 0; 111 cE.gridy = 0; 112 cE.gridwidth = 2; 113 cE.anchor = GridBagConstraints.WEST; 114 cE.fill = GridBagConstraints.HORIZONTAL; 115 cE.weightx = 1.0; 116 cE.insets = new Insets(0, 10, 20, 10); 117 gbLayout.setConstraints(explanation, cE); 118 add(explanation); 119 120 121 cL.gridx = 0; 122 cL.gridy = 1; 123 cL.ipadx = 3; 124 cL.anchor = GridBagConstraints.NORTHWEST; 125 cL.insets = new Insets(0, 10, 0, 15); 126 127 cR.gridx = 1; 128 cR.gridy = 1; 129 cR.anchor = GridBagConstraints.WEST; 130 cR.insets = new Insets(0, 0, 0, 10); 131 132 // Traction type (Steam / Diesel/Electric) 133 JLabel physicsTractionLabel = new JLabel(Bundle.getMessage("PhysicsTractionType") + ":"); 134 gbLayout.setConstraints(physicsTractionLabel, cL); 135 add(physicsTractionLabel); 136 137 JPanel tractionRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); 138 physicsTractionGroup.add(physicsSteamRadio); 139 physicsTractionGroup.add(physicsDieselElectricRadio); 140 tractionRow.add(physicsSteamRadio); 141 tractionRow.add(physicsDieselElectricRadio); 142 143 physicsSteamRadio.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsSteam")); 144 physicsDieselElectricRadio.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsDieselElectric")); 145 146 gbLayout.setConstraints(tractionRow, cR); 147 add(tractionRow); 148 149 // Transmission (mechanical DMU option) 150 cL.gridy++; 151 cR.gridy = cL.gridy; 152 153 JLabel physicsTransLabel = new JLabel(Bundle.getMessage("PhysicsTransmission") + ":"); 154 gbLayout.setConstraints(physicsTransLabel, cL); 155 add(physicsTransLabel); 156 157 gbLayout.setConstraints(physicsMechanicalTransmissionCheck, cR); 158 add(physicsMechanicalTransmissionCheck); 159 160 physicsMechanicalTransmissionCheck.getAccessibleContext() 161 .setAccessibleName(Bundle.getMessage("PhysicsMechanicalTransmission")); 162 physicsMechanicalTransmissionCheck.setToolTipText(Bundle.getMessage("ToolTipPhysicsMechanicalTransmission")); 163 164 // Locomotive weight 165 cL.gridy++; 166 cR.gridy = cL.gridy; 167 168 JLabel physicsWeightLabel = new JLabel(Bundle.getMessage("PhysicsWeight") + ":"); 169 gbLayout.setConstraints(physicsWeightLabel, cL); 170 add(physicsWeightLabel); 171 172 JPanel weightRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); 173 physicsWeightSpinner.setModel(new SpinnerNumberModel(0.0d, 0.0d, 1_000_000.0d, 0.1d)); 174 physicsWeightSpinner.setEditor(new JSpinner.NumberEditor(physicsWeightSpinner, "0.0")); 175 physicsWeightUnitCombo.addItem(Bundle.getMessage("PhysicsWeightUnitTonne")); 176 physicsWeightUnitCombo.addItem(Bundle.getMessage("PhysicsWeightUnitLongTon")); 177 physicsWeightUnitCombo.addItem(Bundle.getMessage("PhysicsWeightUnitShortTon")); 178 weightRow.add(physicsWeightSpinner); 179 weightRow.add(physicsWeightUnitCombo); 180 181 physicsWeightSpinner.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsWeightValue")); 182 physicsWeightUnitCombo.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsWeightUnits")); 183 184 gbLayout.setConstraints(weightRow, cR); 185 add(weightRow); 186 187 // Continuous power 188 cL.gridy++; 189 cR.gridy = cL.gridy; 190 191 JLabel physicsPowerLabel = new JLabel(Bundle.getMessage("PhysicsPower") + ":"); 192 gbLayout.setConstraints(physicsPowerLabel, cL); 193 add(physicsPowerLabel); 194 195 JPanel powerRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); 196 physicsPowerSpinner.setModel(new SpinnerNumberModel(0.0d, 0.0d, 1_000_000.0d, 0.1d)); 197 physicsPowerSpinner.setEditor(new JSpinner.NumberEditor(physicsPowerSpinner, "0.0")); 198 physicsPowerUnitCombo.addItem(Bundle.getMessage("PhysicsPowerUnitKW")); 199 physicsPowerUnitCombo.addItem(Bundle.getMessage("PhysicsPowerUnitHP")); 200 powerRow.add(physicsPowerSpinner); 201 powerRow.add(physicsPowerUnitCombo); 202 203 physicsPowerSpinner.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsPowerValue")); 204 physicsPowerUnitCombo.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsPowerUnits")); 205 206 gbLayout.setConstraints(powerRow, cR); 207 add(powerRow); 208 209 // Tractive effort 210 cL.gridy++; 211 cR.gridy = cL.gridy; 212 213 JLabel physicsTeLabel = new JLabel(Bundle.getMessage("PhysicsTractiveEffort") + ":"); 214 gbLayout.setConstraints(physicsTeLabel, cL); 215 add(physicsTeLabel); 216 217 JPanel teRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); 218 physicsTractiveEffortSpinner.setModel(new SpinnerNumberModel(0.0d, 0.0d, 1_000_000.0d, 0.1d)); 219 physicsTractiveEffortSpinner.setEditor(new JSpinner.NumberEditor(physicsTractiveEffortSpinner, "0.0")); 220 physicsTeUnitCombo.addItem(Bundle.getMessage("PhysicsTeUnitKN")); 221 physicsTeUnitCombo.addItem(Bundle.getMessage("PhysicsTeUnitLbf")); 222 teRow.add(physicsTractiveEffortSpinner); 223 teRow.add(physicsTeUnitCombo); 224 225 physicsTractiveEffortSpinner.getAccessibleContext() 226 .setAccessibleName(Bundle.getMessage("PhysicsTractiveEffortValue")); 227 physicsTeUnitCombo.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsTractiveEffortUnits")); 228 229 gbLayout.setConstraints(teRow, cR); 230 add(teRow); 231 232 // Maximum speed 233 cL.gridy++; 234 cR.gridy = cL.gridy; 235 236 JLabel physicsSpeedLabel = new JLabel(Bundle.getMessage("PhysicsMaxSpeed") + ":"); 237 gbLayout.setConstraints(physicsSpeedLabel, cL); 238 add(physicsSpeedLabel); 239 240 JPanel speedRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 0)); 241 physicsMaxSpeedSpinner.setModel(new SpinnerNumberModel(0.0d, 0.0d, 1_000.0d, 0.1d)); 242 physicsMaxSpeedSpinner.setEditor(new JSpinner.NumberEditor(physicsMaxSpeedSpinner, "0.0")); 243 physicsSpeedUnitCombo.addItem(Bundle.getMessage("PhysicsSpeedUnitKmh")); 244 physicsSpeedUnitCombo.addItem(Bundle.getMessage("PhysicsSpeedUnitMph")); 245 speedRow.add(physicsMaxSpeedSpinner); 246 speedRow.add(physicsSpeedUnitCombo); 247 248 physicsMaxSpeedSpinner.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsMaxSpeedValue")); 249 physicsSpeedUnitCombo.getAccessibleContext().setAccessibleName(Bundle.getMessage("PhysicsSpeedUnits")); 250 251 gbLayout.setConstraints(speedRow, cR); 252 add(speedRow); 253 254 // Profile tools row: single button to open JMRI Speed Profiling UI 255 cL.gridy++; 256 cR.gridy = cL.gridy; 257 258 JLabel profileToolsLabel = new JLabel(Bundle.getMessage("PhysicsProfileTools") + ":"); 259 gbLayout.setConstraints(profileToolsLabel, cL); 260 add(profileToolsLabel); 261 262 JButton physicsSpeedProfileButton = new JButton(Bundle.getMessage("PhysicsSpeedProfileButton")); 263 gbLayout.setConstraints(physicsSpeedProfileButton, cR); 264 add(physicsSpeedProfileButton); 265 266 physicsSpeedProfileButton.addActionListener(ev -> { 267 jmri.util.swing.WindowInterface wi = getWindowInterfaceOrNull(); 268 jmri.jmrit.roster.swing.speedprofile.SpeedProfileAction act = 269 (wi != null) 270 ? new jmri.jmrit.roster.swing.speedprofile.SpeedProfileAction( 271 Bundle.getMessage("PhysicsSpeedProfilingTitle"), wi) 272 : new jmri.jmrit.roster.swing.speedprofile.SpeedProfileAction( 273 Bundle.getMessage("PhysicsSpeedProfilingTitle")); 274 act.actionPerformed(new java.awt.event.ActionEvent(this, 275 java.awt.event.ActionEvent.ACTION_PERFORMED, "open")); 276 }); 277 } 278 279 private void wireListeners() { 280 physicsSteamRadio.addActionListener(ev -> { 281 if (refreshing) 282 return; 283 if (re != null) 284 re.setPhysicsTractionType(RosterEntry.TractionType.STEAM); 285 }); 286 physicsDieselElectricRadio.addActionListener(ev -> { 287 if (refreshing) 288 return; 289 if (re != null) 290 re.setPhysicsTractionType(RosterEntry.TractionType.DIESEL_ELECTRIC); 291 }); 292 physicsMechanicalTransmissionCheck.addActionListener(ev -> { 293 if (refreshing) 294 return; 295 if (re != null) 296 re.setPhysicsMechanicalTransmission(physicsMechanicalTransmissionCheck.isSelected()); 297 }); 298 299 // Unit combo changes -> refresh display from stored metric 300 physicsWeightUnitCombo.addActionListener(ev -> { 301 if (refreshing) 302 return; 303 persistUnitSelectionOnRosterEntry(re, PHYSICS_WEIGHT_UNIT_ATTR, 304 weightUnitIndexToCode(physicsWeightUnitCombo.getSelectedIndex())); 305 refreshFromRosterEntry(); 306 }); 307 physicsPowerUnitCombo.addActionListener(ev -> { 308 if (refreshing) 309 return; 310 persistUnitSelectionOnRosterEntry(re, PHYSICS_POWER_UNIT_ATTR, 311 powerUnitIndexToCode(physicsPowerUnitCombo.getSelectedIndex())); 312 refreshFromRosterEntry(); 313 }); 314 physicsTeUnitCombo.addActionListener(ev -> { 315 if (refreshing) 316 return; 317 persistUnitSelectionOnRosterEntry(re, PHYSICS_TE_UNIT_ATTR, 318 teUnitIndexToCode(physicsTeUnitCombo.getSelectedIndex())); 319 refreshFromRosterEntry(); 320 }); 321 physicsSpeedUnitCombo.addActionListener(ev -> { 322 if (refreshing) 323 return; 324 persistUnitSelectionOnRosterEntry(re, PHYSICS_SPEED_UNIT_ATTR, 325 speedUnitIndexToCode(physicsSpeedUnitCombo.getSelectedIndex())); 326 refreshFromRosterEntry(); 327 }); 328 329 // Spinner changes -> update stored metric in RosterEntry 330 physicsWeightSpinner.addChangeListener(ev -> { 331 if (refreshing) 332 return; 333 if (re != null) 334 re.setPhysicsWeightKg(displayWeightToKg(((Number) physicsWeightSpinner.getValue()).floatValue())); 335 }); 336 physicsPowerSpinner.addChangeListener(ev -> { 337 if (refreshing) 338 return; 339 if (re != null) 340 re.setPhysicsPowerKw(displayPowerToKw(((Number) physicsPowerSpinner.getValue()).floatValue())); 341 }); 342 physicsTractiveEffortSpinner.addChangeListener(ev -> { 343 if (refreshing) 344 return; 345 if (re != null) 346 re.setPhysicsTractiveEffortKn( 347 displayTeToKn(((Number) physicsTractiveEffortSpinner.getValue()).floatValue())); 348 }); 349 physicsMaxSpeedSpinner.addChangeListener(ev -> { 350 if (refreshing) 351 return; 352 if (re != null) 353 re.setPhysicsMaxSpeedKmh(displaySpeedToKmh(((Number) physicsMaxSpeedSpinner.getValue()).floatValue())); 354 }); 355 } 356 357 /** 358 * Update the roster entry from the GUI contents. 359 * 360 * @param r roster entry to update 361 */ 362 public void update(RosterEntry r) { 363 if (r == null) 364 return; 365 // Persist current unit selections onto the roster entry being saved 366 persistUnitSelectionOnRosterEntry(r, PHYSICS_WEIGHT_UNIT_ATTR, 367 weightUnitIndexToCode(physicsWeightUnitCombo.getSelectedIndex())); 368 persistUnitSelectionOnRosterEntry(r, PHYSICS_POWER_UNIT_ATTR, 369 powerUnitIndexToCode(physicsPowerUnitCombo.getSelectedIndex())); 370 persistUnitSelectionOnRosterEntry(r, PHYSICS_TE_UNIT_ATTR, 371 teUnitIndexToCode(physicsTeUnitCombo.getSelectedIndex())); 372 persistUnitSelectionOnRosterEntry(r, PHYSICS_SPEED_UNIT_ATTR, 373 speedUnitIndexToCode(physicsSpeedUnitCombo.getSelectedIndex())); 374 375 RosterEntry.TractionType t = physicsSteamRadio.isSelected() 376 ? RosterEntry.TractionType.STEAM 377 : RosterEntry.TractionType.DIESEL_ELECTRIC; 378 r.setPhysicsTractionType(t); 379 r.setPhysicsWeightKg(displayWeightToKg(((Number) physicsWeightSpinner.getValue()).floatValue())); 380 r.setPhysicsPowerKw(displayPowerToKw(((Number) physicsPowerSpinner.getValue()).floatValue())); 381 r.setPhysicsTractiveEffortKn(displayTeToKn(((Number) physicsTractiveEffortSpinner.getValue()).floatValue())); 382 r.setPhysicsMaxSpeedKmh(displaySpeedToKmh(((Number) physicsMaxSpeedSpinner.getValue()).floatValue())); 383 r.setPhysicsMechanicalTransmission(physicsMechanicalTransmissionCheck.isSelected()); 384 } 385 386 /** 387 * Fill GUI from roster contents. 388 * 389 * @param r roster entry to display 390 */ 391 public void updateGUI(RosterEntry r) { 392 re = r; 393 applyUnitSelectionsFromRosterEntry(re); 394 refreshFromRosterEntry(); 395 } 396 397 /** 398 * Compare GUI with roster contents. 399 * 400 * @param r roster entry to compare 401 * @return true if GUI differs from r 402 */ 403 public boolean guiChanged(RosterEntry r) { 404 if (r == null) 405 return false; 406 407 RosterEntry.TractionType t = physicsSteamRadio.isSelected() 408 ? RosterEntry.TractionType.STEAM 409 : RosterEntry.TractionType.DIESEL_ELECTRIC; 410 if (r.getPhysicsTractionType() != t) 411 return true; 412 413 if (r.isPhysicsMechanicalTransmission() != physicsMechanicalTransmissionCheck.isSelected()) 414 return true; 415 416 // use a small epsilon to avoid noise from float/double conversions 417 final float eps = 0.0005f; 418 419 float wKg = displayWeightToKg(((Number) physicsWeightSpinner.getValue()).floatValue()); 420 if (Math.abs(r.getPhysicsWeightKg() - wKg) > eps) 421 return true; 422 423 float pKw = displayPowerToKw(((Number) physicsPowerSpinner.getValue()).floatValue()); 424 if (Math.abs(r.getPhysicsPowerKw() - pKw) > eps) 425 return true; 426 427 float teKn = displayTeToKn(((Number) physicsTractiveEffortSpinner.getValue()).floatValue()); 428 if (Math.abs(r.getPhysicsTractiveEffortKn() - teKn) > eps) 429 return true; 430 431 float vKmh = displaySpeedToKmh(((Number) physicsMaxSpeedSpinner.getValue()).floatValue()); 432 if (Math.abs(r.getPhysicsMaxSpeedKmh() - vKmh) > eps) 433 return true; 434 435 return false; 436 } 437 438 // --- Helpers: persist unit selections per roster entry (non-localised codes) --- 439 private String weightUnitIndexToCode(int ix) { 440 switch (ix) { 441 case 1: 442 return "LT"; // long ton 443 case 2: 444 return "ST"; // short ton 445 case 0: 446 default: 447 return "T"; // metric tonne 448 } 449 } 450 451 private int weightUnitCodeToIndex(String code) { 452 if (code == null) { 453 return 0; 454 } 455 if ("LT".equalsIgnoreCase(code)) { 456 return 1; 457 } 458 if ("ST".equalsIgnoreCase(code)) { 459 return 2; 460 } 461 return 0; 462 } 463 464 private String powerUnitIndexToCode(int ix) { 465 switch (ix) { 466 case 1: 467 return "HP"; 468 case 0: 469 default: 470 return "KW"; 471 } 472 } 473 474 private int powerUnitCodeToIndex(String code) { 475 if (code == null) { 476 return 0; 477 } 478 if ("HP".equalsIgnoreCase(code)) { 479 return 1; 480 } 481 return 0; 482 } 483 484 private String teUnitIndexToCode(int ix) { 485 switch (ix) { 486 case 1: 487 return "LBF"; 488 case 0: 489 default: 490 return "KN"; 491 } 492 } 493 494 private int teUnitCodeToIndex(String code) { 495 if (code == null) { 496 return 0; 497 } 498 if ("LBF".equalsIgnoreCase(code)) { 499 return 1; 500 } 501 return 0; 502 } 503 504 private String speedUnitIndexToCode(int ix) { 505 switch (ix) { 506 case 1: 507 return "MPH"; 508 case 0: 509 default: 510 return "KMH"; 511 } 512 } 513 514 private int speedUnitCodeToIndex(String code) { 515 if (code == null) { 516 return 0; 517 } 518 if ("MPH".equalsIgnoreCase(code)) { 519 return 1; 520 } 521 return 0; 522 } 523 524 private void persistUnitSelectionOnRosterEntry(RosterEntry target, String key, String code) { 525 if (target == null) { 526 return; 527 } 528 if (refreshing) { 529 return; 530 } 531 target.putAttribute(key, (code != null) ? code : ""); 532 } 533 534 private String readUnitSelectionFromRosterEntry(RosterEntry target, String key) { 535 if (target == null) { 536 return null; 537 } 538 return target.getAttribute(key); 539 } 540 541 private void applyUnitSelectionsFromRosterEntry(RosterEntry target) { 542 if (target == null) { 543 return; 544 } 545 int wIx = weightUnitCodeToIndex(readUnitSelectionFromRosterEntry(target, PHYSICS_WEIGHT_UNIT_ATTR)); 546 int pIx = powerUnitCodeToIndex(readUnitSelectionFromRosterEntry(target, PHYSICS_POWER_UNIT_ATTR)); 547 int teIx = teUnitCodeToIndex(readUnitSelectionFromRosterEntry(target, PHYSICS_TE_UNIT_ATTR)); 548 int sIx = speedUnitCodeToIndex(readUnitSelectionFromRosterEntry(target, PHYSICS_SPEED_UNIT_ATTR)); 549 550 if (wIx >= 0 && wIx < physicsWeightUnitCombo.getItemCount()) { 551 physicsWeightUnitCombo.setSelectedIndex(wIx); 552 } 553 if (pIx >= 0 && pIx < physicsPowerUnitCombo.getItemCount()) { 554 physicsPowerUnitCombo.setSelectedIndex(pIx); 555 } 556 if (teIx >= 0 && teIx < physicsTeUnitCombo.getItemCount()) { 557 physicsTeUnitCombo.setSelectedIndex(teIx); 558 } 559 if (sIx >= 0 && sIx < physicsSpeedUnitCombo.getItemCount()) { 560 physicsSpeedUnitCombo.setSelectedIndex(sIx); 561 } 562 } 563 564 // --- Helpers: convert between metric storage (RosterEntry) and display units --- 565 566 private void refreshFromRosterEntry() { 567 if (re == null) 568 return; 569 570 refreshing = true; 571 try { 572 // Traction type 573 if (re.getPhysicsTractionType() == RosterEntry.TractionType.STEAM) { 574 physicsSteamRadio.setSelected(true); 575 } else { 576 physicsDieselElectricRadio.setSelected(true); 577 } 578 physicsMechanicalTransmissionCheck.setSelected(re.isPhysicsMechanicalTransmission()); 579 580 // Weight kg -> display 581 physicsWeightSpinner.setValue(Double.valueOf(kgToDisplayWeight(re.getPhysicsWeightKg()))); 582 583 // Power kW -> display 584 physicsPowerSpinner.setValue(Double.valueOf(kwToDisplayPower(re.getPhysicsPowerKw()))); 585 586 // Tractive effort kN -> display 587 physicsTractiveEffortSpinner.setValue(Double.valueOf(knToDisplayTe(re.getPhysicsTractiveEffortKn()))); 588 589 // Max speed km/h -> display 590 physicsMaxSpeedSpinner.setValue(Double.valueOf(kmhToDisplaySpeed(re.getPhysicsMaxSpeedKmh()))); 591 } finally { 592 refreshing = false; 593 } 594 } 595 596 private float displayWeightToKg(float displayVal) { 597 switch (physicsWeightUnitCombo.getSelectedIndex()) { 598 case 0: 599 return displayVal * 1000.0f; // t -> kg 600 case 1: 601 return displayVal * 1016.0469f; // long ton -> kg 602 case 2: 603 return displayVal * 907.18474f; // short ton -> kg 604 default: 605 return displayVal; 606 } 607 } 608 609 private float kgToDisplayWeight(float kg) { 610 switch (physicsWeightUnitCombo.getSelectedIndex()) { 611 case 0: 612 return kg / 1000.0f; // t 613 case 1: 614 return kg / 1016.0469f; // long ton 615 case 2: 616 return kg / 907.18474f; // short ton 617 default: 618 return kg; 619 } 620 } 621 622 private float displayPowerToKw(float displayVal) { 623 switch (physicsPowerUnitCombo.getSelectedIndex()) { 624 case 0: 625 return displayVal; // kW 626 case 1: 627 return displayVal * 0.7456999f; // hp -> kW 628 default: 629 return displayVal; 630 } 631 } 632 633 private float displayTeToKn(float displayVal) { 634 switch (physicsTeUnitCombo.getSelectedIndex()) { 635 case 0: 636 return displayVal; // kN 637 case 1: 638 return displayVal / 224.80894f; // lbf -> kN 639 default: 640 return displayVal; 641 } 642 } 643 644 private float knToDisplayTe(float kn) { 645 switch (physicsTeUnitCombo.getSelectedIndex()) { 646 case 0: 647 return kn; // kN 648 case 1: 649 return kn * 224.80894f; // kN -> lbf 650 default: 651 return kn; 652 } 653 } 654 655 private float displaySpeedToKmh(float displayVal) { 656 switch (physicsSpeedUnitCombo.getSelectedIndex()) { 657 case 0: 658 return displayVal; // km/h 659 case 1: 660 return displayVal * 1.609344f; // mph -> km/h 661 default: 662 return displayVal; 663 } 664 } 665 666 private float kmhToDisplaySpeed(float kmh) { 667 switch (physicsSpeedUnitCombo.getSelectedIndex()) { 668 case 0: 669 return kmh; // km/h 670 case 1: 671 return kmh / 1.609344f; // km/h -> mph 672 default: 673 return kmh; 674 } 675 } 676 677 private float kwToDisplayPower(float kw) { 678 switch (physicsPowerUnitCombo.getSelectedIndex()) { 679 case 0: 680 return kw; // kW 681 case 1: 682 return kw / 0.7456999f; // kW -> hp 683 default: 684 return kw; 685 } 686 } 687 688 private jmri.util.swing.WindowInterface getWindowInterfaceOrNull() { 689 java.awt.Window w = javax.swing.SwingUtilities.getWindowAncestor(this); 690 return (w instanceof jmri.util.swing.WindowInterface) 691 ? (jmri.util.swing.WindowInterface) w 692 : null; 693 } 694}