001package jmri.jmrit.operations.locations.tools; 002 003import java.awt.*; 004import java.util.ArrayList; 005import java.util.List; 006 007import javax.swing.*; 008 009import jmri.InstanceManager; 010import jmri.jmrit.operations.OperationsFrame; 011import jmri.jmrit.operations.OperationsXml; 012import jmri.jmrit.operations.locations.*; 013import jmri.jmrit.operations.locations.gui.TrackEditFrame; 014import jmri.jmrit.operations.rollingstock.RollingStock; 015import jmri.jmrit.operations.rollingstock.cars.*; 016import jmri.jmrit.operations.router.Router; 017import jmri.jmrit.operations.setup.Control; 018import jmri.jmrit.operations.setup.Setup; 019import jmri.util.swing.JmriJOptionPane; 020 021/** 022 * Frame for user edit of track destinations 023 * 024 * @author Dan Boudreau Copyright (C) 2013, 2024 025 */ 026public class TrackDestinationEditFrame extends OperationsFrame implements java.beans.PropertyChangeListener { 027 028 Track _track = null; 029 030 LocationManager locationManager = InstanceManager.getDefault(LocationManager.class); 031 032 // panels 033 JPanel pControls = new JPanel(); 034 JPanel panelDestinations = new JPanel(); 035 JScrollPane paneDestinations = new JScrollPane(panelDestinations); 036 037 // major buttons 038 JButton saveButton = new JButton(Bundle.getMessage("ButtonSave")); 039 JButton checkDestinationsButton = new JButton(Bundle.getMessage("CheckDestinations")); 040 041 // radio buttons 042 JRadioButton destinationsAll = new JRadioButton(Bundle.getMessage("AcceptAll")); 043 JRadioButton destinationsInclude = new JRadioButton(Bundle.getMessage("AcceptOnly")); 044 JRadioButton destinationsExclude = new JRadioButton(Bundle.getMessage("Exclude")); 045 046 // checkboxes 047 JCheckBox onlyCarsWithFD = new JCheckBox(Bundle.getMessage("OnlyCarsWithFD")); 048 049 // labels 050 JLabel trackName = new JLabel(); 051 052 public static final String DISPOSE = "dispose"; // NOI18N 053 054 public TrackDestinationEditFrame() { 055 super(Bundle.getMessage("TitleEditTrackDestinations")); 056 } 057 058 public void initComponents(TrackEditFrame tef) { 059 _track = tef._track; 060 061 // the following code sets the frame's initial state 062 getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); 063 064 // Layout the panel by rows 065 // row 1 066 JPanel p1 = new JPanel(); 067 p1.setLayout(new BoxLayout(p1, BoxLayout.X_AXIS)); 068 p1.setMaximumSize(new Dimension(2000, 250)); 069 070 // row 1a 071 JPanel pTrackName = new JPanel(); 072 pTrackName.setLayout(new GridBagLayout()); 073 pTrackName.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Track"))); 074 addItem(pTrackName, trackName, 0, 0); 075 076 // row 1b 077 JPanel pLocationName = new JPanel(); 078 pLocationName.setLayout(new GridBagLayout()); 079 pLocationName.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Location"))); 080 addItem(pLocationName, new JLabel(_track.getLocation().getName()), 0, 0); 081 082 p1.add(pTrackName); 083 p1.add(pLocationName); 084 085 // row 2 only for C/I and Staging 086 JPanel pFD = new JPanel(); 087 pFD.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Options"))); 088 pFD.add(onlyCarsWithFD); 089 pFD.setMaximumSize(new Dimension(2000, 200)); 090 091 // row 3 092 JPanel p3 = new JPanel(); 093 p3.setLayout(new BoxLayout(p3, BoxLayout.Y_AXIS)); 094 JScrollPane pane3 = new JScrollPane(p3); 095 pane3.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("DestinationTrack"))); 096 pane3.setMaximumSize(new Dimension(2000, 400)); 097 098 JPanel pRadioButtons = new JPanel(); 099 pRadioButtons.setLayout(new FlowLayout()); 100 101 pRadioButtons.add(destinationsAll); 102 pRadioButtons.add(destinationsInclude); 103 pRadioButtons.add(destinationsExclude); 104 105 p3.add(pRadioButtons); 106 107 // row 4 108 panelDestinations.setLayout(new GridBagLayout()); 109 paneDestinations.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Destinations"))); 110 111 ButtonGroup bGroup = new ButtonGroup(); 112 bGroup.add(destinationsAll); 113 bGroup.add(destinationsInclude); 114 bGroup.add(destinationsExclude); 115 116 // row last 117 JPanel panelButtons = new JPanel(); 118 panelButtons.setLayout(new GridBagLayout()); 119 panelButtons.setBorder(BorderFactory.createTitledBorder("")); 120 panelButtons.setMaximumSize(new Dimension(2000, 200)); 121 122 addItem(panelButtons, checkDestinationsButton, 0, 0); 123 addItem(panelButtons, saveButton, 1, 0); 124 125 getContentPane().add(p1); 126 getContentPane().add(pFD); 127 getContentPane().add(pane3); 128 getContentPane().add(paneDestinations); 129 getContentPane().add(panelButtons); 130 131 // setup buttons 132 addButtonAction(checkDestinationsButton); 133 addButtonAction(saveButton); 134 135 addRadioButtonAction(destinationsAll); 136 addRadioButtonAction(destinationsInclude); 137 addRadioButtonAction(destinationsExclude); 138 139 // load fields and enable buttons 140 if (_track != null) { 141 _track.addPropertyChangeListener(this); 142 trackName.setText(_track.getName()); 143 onlyCarsWithFD.setSelected(_track.isOnlyCarsWithFinalDestinationEnabled()); 144 pFD.setVisible(_track.isInterchange() || _track.isStaging()); 145 enableButtons(true); 146 } else { 147 enableButtons(false); 148 } 149 150 updateDestinations(); 151 152 locationManager.addPropertyChangeListener(this); 153 154 initMinimumSize(new Dimension(Control.panelWidth400, Control.panelHeight500)); 155 } 156 157 // Save, Delete, Add 158 @Override 159 public void buttonActionPerformed(java.awt.event.ActionEvent ae) { 160 if (_track == null) { 161 return; 162 } 163 if (ae.getSource() == saveButton) { 164 log.debug("track save button activated"); 165 _track.setOnlyCarsWithFinalDestinationEnabled(onlyCarsWithFD.isSelected()); 166 OperationsXml.save(); 167 if (Setup.isCloseWindowOnSaveEnabled()) { 168 dispose(); 169 } 170 } 171 if (ae.getSource() == checkDestinationsButton) { 172 checkDestinationsButton.setEnabled(false); // testing can take awhile, so disable 173 checkDestinationsValid(); 174 } 175 } 176 177 protected void enableButtons(boolean enabled) { 178 saveButton.setEnabled(enabled); 179 checkDestinationsButton.setEnabled(enabled); 180 destinationsAll.setEnabled(enabled); 181 destinationsInclude.setEnabled(enabled); 182 destinationsExclude.setEnabled(enabled); 183 } 184 185 @Override 186 public void radioButtonActionPerformed(java.awt.event.ActionEvent ae) { 187 log.debug("radio button activated"); 188 if (ae.getSource() == destinationsAll) { 189 _track.setDestinationOption(Track.ALL_DESTINATIONS); 190 } 191 if (ae.getSource() == destinationsInclude) { 192 _track.setDestinationOption(Track.INCLUDE_DESTINATIONS); 193 } 194 if (ae.getSource() == destinationsExclude) { 195 _track.setDestinationOption(Track.EXCLUDE_DESTINATIONS); 196 } 197 updateDestinations(); 198 } 199 200 private void updateDestinations() { 201 log.debug("Update destinations"); 202 panelDestinations.removeAll(); 203 if (_track != null) { 204 destinationsAll.setSelected(_track.getDestinationOption().equals(Track.ALL_DESTINATIONS)); 205 destinationsInclude.setSelected(_track.getDestinationOption().equals(Track.INCLUDE_DESTINATIONS)); 206 destinationsExclude.setSelected(_track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS)); 207 } 208 List<Location> locations = locationManager.getLocationsByNameList(); 209 for (int i = 0; i < locations.size(); i++) { 210 Location loc = locations.get(i); 211 JCheckBox cb = new JCheckBox(loc.getName()); 212 addItemLeft(panelDestinations, cb, 0, i); 213 cb.setEnabled(!destinationsAll.isSelected()); 214 addCheckBoxAction(cb); 215 if (destinationsAll.isSelected()) { 216 cb.setSelected(true); 217 } else if (_track != null && 218 _track.isDestinationAccepted(loc) ^ 219 _track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS)) { 220 cb.setSelected(true); 221 } 222 } 223 panelDestinations.revalidate(); 224 } 225 226 @Override 227 public void checkBoxActionPerformed(java.awt.event.ActionEvent ae) { 228 JCheckBox b = (JCheckBox) ae.getSource(); 229 log.debug("checkbox change {}", b.getText()); 230 if (_track == null) { 231 return; 232 } 233 Location loc = locationManager.getLocationByName(b.getText()); 234 if (loc != null) { 235 if (b.isSelected() ^ _track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS)) { 236 _track.addDestination(loc); 237 } else { 238 _track.deleteDestination(loc); 239 } 240 } 241 } 242 243 private void checkDestinationsValid() { 244 SwingUtilities.invokeLater(() -> { 245 if (checkDestinations()) 246 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("OkayMessage")); 247 checkDestinationsButton.setEnabled(true); 248 }); 249 } 250 251 private boolean checkDestinations() { 252 // 1st check to see if all car types have a destination 253 if (!checkDestinationCarTypes()) { 254 return false; 255 } 256 // now check each destination 257 boolean noIssues = true; 258 // only report car type not serviced once 259 List<String> ignoreType = new ArrayList<String>(); 260 // only report car type and load not serviced once 261 List<String> ignoreTypeAndLoad = new ArrayList<String>(); 262 for (Location destination : locationManager.getLocationsByNameList()) { 263 ignoreType.clear(); 264 if (_track.isDestinationAccepted(destination)) { 265 log.debug("Track ({}) accepts destination ({})", _track.getName(), destination.getName()); 266 if (_track.getLocation() == destination) { 267 continue; 268 } 269 // now check to see if the track's rolling stock is accepted by the destination 270 checkTypes: for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 271 if (!_track.isTypeNameAccepted(type)) { 272 continue; 273 } 274 if (!destination.acceptsTypeName(type)) { 275 noIssues = false; 276 int response = JmriJOptionPane.showConfirmDialog(this, 277 Bundle.getMessage("WarningDestinationCarType", 278 destination.getName(), type), 279 Bundle.getMessage("WarningCarMayNotMove"), 280 JmriJOptionPane.OK_CANCEL_OPTION); 281 if (response == JmriJOptionPane.OK_OPTION) { 282 ignoreType.add(type); 283 continue; 284 } 285 return false; // done 286 } 287 // now determine if there's a track willing to service car type 288 for (Track track : destination.getTracksList()) { 289 if (track.isTypeNameAccepted(type)) { 290 continue checkTypes; // yes there's a track 291 } 292 } 293 noIssues = false; 294 int response = JmriJOptionPane.showConfirmDialog(this, 295 Bundle.getMessage("WarningDestinationTrackCarType", 296 destination.getName(), type), 297 Bundle.getMessage("WarningCarMayNotMove"), 298 JmriJOptionPane.OK_CANCEL_OPTION); 299 if (response == JmriJOptionPane.OK_OPTION) { 300 ignoreType.add(type); 301 continue; 302 } 303 return false; // done 304 } 305 // now check road names 306 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 307 if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) { 308 continue; 309 } 310 checkRoads: for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) { 311 if (!_track.isRoadNameAccepted(road)) { 312 continue; 313 } 314 // now determine if there's a track willing to service this road 315 for (Track track : destination.getTracksList()) { 316 if (!track.isTypeNameAccepted(type)) { 317 continue; 318 } 319 if (track.isRoadNameAccepted(road)) { 320 continue checkRoads; // yes there's a track 321 } 322 } 323 noIssues = false; 324 int response = JmriJOptionPane.showConfirmDialog(this, 325 Bundle.getMessage("WarningDestinationTrackCarRoad", 326 destination.getName(), type, road), 327 Bundle.getMessage("WarningCarMayNotMove"), 328 JmriJOptionPane.OK_CANCEL_OPTION); 329 if (response == JmriJOptionPane.OK_OPTION) { 330 continue; 331 } 332 return false; // done 333 } 334 } 335 // now check load names 336 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 337 if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) { 338 continue; 339 } 340 // only report car load not serviced once 341 List<String> ignoreLoad = new ArrayList<String>(); 342 List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type); 343 checkLoads: for (String load : loads) { 344 if (!_track.isLoadNameAccepted(load)) { 345 continue; 346 } 347 // now determine if there's a track willing to service this load 348 for (Track track : destination.getTracksList()) { 349 if (!track.isTypeNameAccepted(type)) { 350 continue; 351 } 352 if (track.isLoadNameAndCarTypeAccepted(load, type)) { 353 continue checkLoads; 354 } 355 } 356 noIssues = false; 357 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 358 .getMessage("WarningDestinationTrackCarLoad", destination.getName(), 359 type, load), 360 Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION); 361 if (response == JmriJOptionPane.OK_OPTION) { 362 ignoreLoad.add(load); 363 ignoreTypeAndLoad.add(type + load); 364 continue; 365 } 366 return false; // done 367 } 368 // now check car type and load combinations 369 checkLoads: for (String load : loads) { 370 if (!_track.isLoadNameAndCarTypeAccepted(load, type) || ignoreLoad.contains(load)) { 371 continue; 372 } 373 // now determine if there's a track willing to service this load 374 for (Track track : destination.getTracksList()) { 375 if (track.isLoadNameAndCarTypeAccepted(load, type)) { 376 continue checkLoads; 377 } 378 } 379 noIssues = false; 380 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 381 .getMessage("WarningDestinationTrackCarLoad", destination.getName(), 382 type, load), 383 Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION); 384 if (response == JmriJOptionPane.OK_OPTION) { 385 ignoreTypeAndLoad.add(type + load); 386 continue; 387 } 388 return false; // done 389 } 390 } 391 // now determine if there's a train or trains that can move a car from this track to the destinations 392 // need to check all car types, loads, and roads that this track services 393 Car car = new Car(); 394 car.setLength(Integer.toString(-RollingStock.COUPLERS)); // set car length to net out to zero 395 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 396 if (!_track.isTypeNameAccepted(type)) { 397 continue; 398 } 399 List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type); 400 for (String load : loads) { 401 if (!_track.isLoadNameAndCarTypeAccepted(load, type) || 402 ignoreTypeAndLoad.contains(type + load)) { 403 continue; 404 } 405 for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) { 406 if (!_track.isRoadNameAccepted(road)) { 407 continue; 408 } 409 // is there a car in the roster with this road? 410 boolean foundCar = false; 411 for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList()) { 412 if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) { 413 foundCar = true; 414 break; 415 } 416 } 417 if (!foundCar) { 418 continue; // no car with this road name 419 } 420 421 car.setTypeName(type); 422 car.setRoadName(road); 423 car.setLoadName(load); 424 car.setTrack(_track); 425 // TODO There's an issue when the only track available is an interchange that has destination restrictions 426 car.setFinalDestination(destination); 427 428 // does the destination accept this car? 429 // this checks tracks that have schedules 430 String testDest = "NO_TYPE"; 431 for (Track track : destination.getTracksList()) { 432 if (!track.isTypeNameAccepted(type)) { 433 // already reported if type not accepted 434 continue; 435 } 436 if (track.getScheduleMode() == Track.SEQUENTIAL) { 437 // must test in match mode 438 track.setScheduleMode(Track.MATCH); 439 String itemId = track.getScheduleItemId(); 440 testDest = car.checkDestination(destination, track); 441 track.setScheduleMode(Track.SEQUENTIAL); 442 track.setScheduleItemId(itemId); 443 } else { 444 testDest = car.checkDestination(destination, track); 445 } 446 if (testDest.equals(Track.OKAY)) { 447 break; // done 448 } 449 } 450 451 if (testDest.equals("NO_TYPE")) { 452 continue; 453 } 454 455 if (!testDest.equals(Track.OKAY)) { 456 noIssues = false; 457 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 458 .getMessage("WarningNoTrack", destination.getName(), type, road, load, 459 destination.getName()), 460 Bundle.getMessage("WarningCarMayNotMove"), 461 JmriJOptionPane.OK_CANCEL_OPTION); 462 if (response == JmriJOptionPane.OK_OPTION) { 463 continue; 464 } 465 return false; // done 466 } 467 468 log.debug("Find train for car type ({}), road ({}), load ({})", type, road, load); 469 470 boolean results = InstanceManager.getDefault(Router.class).setDestination(car, null, null); 471 car.setDestination(null, null); // clear destination if set by router 472 if (!results) { 473 noIssues = false; 474 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 475 .getMessage("WarningNoTrain", type, road, load, 476 destination.getName()), 477 Bundle.getMessage("WarningCarMayNotMove"), 478 JmriJOptionPane.OK_CANCEL_OPTION); 479 if (response == JmriJOptionPane.OK_OPTION) { 480 continue; 481 } 482 return false; // done 483 } 484 // TODO need to check owners and car built dates 485 } 486 } 487 } 488 } 489 } 490 return noIssues; 491 } 492 493 /* 494 * Used to determine if every car type accepted by the interchange can be 495 * sent to another destination. 496 */ 497 private boolean checkDestinationCarTypes() { 498 checkTypes: for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 499 if (!_track.isTypeNameAccepted(type)) { 500 continue; 501 } 502 for (Location destination : locationManager.getLocationsByNameList()) { 503 if (_track.getLocation() == destination || !_track.isDestinationAccepted(destination)) { 504 continue; 505 } 506 // check destination and tracks at destination 507 if (destination.acceptsTypeName(type)) { 508 for (Track track : destination.getTracksByNameList(null)) { 509 if (track.isTypeNameAccepted(type)) { 510 continue checkTypes; 511 } 512 } 513 } 514 } 515 int response = JmriJOptionPane.showConfirmDialog(this, 516 Bundle.getMessage("ErrorNoDestinatonType", type, _track.getTrackTypeName(), 517 _track.getLocation().getName(), _track.getName()), 518 Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION); 519 if (response == JmriJOptionPane.OK_OPTION) { 520 response = JmriJOptionPane.showConfirmDialog(this, 521 Bundle.getMessage("RemoveCarType", type, _track.getTrackTypeName(), 522 _track.getLocation().getName(), _track.getName()), 523 Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.YES_NO_OPTION); 524 if (response == JmriJOptionPane.YES_OPTION) { 525 _track.deleteTypeName(type); 526 } 527 continue; 528 } 529 return false; // done 530 } 531 return true; 532 } 533 534 @Override 535 public void dispose() { 536 if (_track != null) { 537 _track.removePropertyChangeListener(this); 538 } 539 locationManager.removePropertyChangeListener(this); 540 super.dispose(); 541 } 542 543 @Override 544 public void propertyChange(java.beans.PropertyChangeEvent e) { 545 if (Control.SHOW_PROPERTY) { 546 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e 547 .getNewValue()); 548 } 549 if (e.getPropertyName().equals(LocationManager.LISTLENGTH_CHANGED_PROPERTY) || 550 e.getPropertyName().equals(Track.DESTINATIONS_CHANGED_PROPERTY)) { 551 updateDestinations(); 552 } 553 if (e.getPropertyName().equals(Track.ROUTED_CHANGED_PROPERTY)) { 554 onlyCarsWithFD.setSelected((boolean) e.getNewValue()); 555 } 556 } 557 558 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackDestinationEditFrame.class); 559}