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.rollingstock.RollingStock; 014import jmri.jmrit.operations.rollingstock.cars.*; 015import jmri.jmrit.operations.router.Router; 016import jmri.jmrit.operations.setup.Control; 017import jmri.jmrit.operations.setup.Setup; 018import jmri.util.swing.JmriJOptionPane; 019 020/** 021 * Frame for user edit of track destinations 022 * 023 * @author Dan Boudreau Copyright (C) 2013, 2024 024 * 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 && _track.isDestinationAccepted(loc) 218 ^ _track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS)) { 219 cb.setSelected(true); 220 } 221 } 222 panelDestinations.revalidate(); 223 } 224 225 @Override 226 public void checkBoxActionPerformed(java.awt.event.ActionEvent ae) { 227 JCheckBox b = (JCheckBox) ae.getSource(); 228 log.debug("checkbox change {}", b.getText()); 229 if (_track == null) { 230 return; 231 } 232 Location loc = locationManager.getLocationByName(b.getText()); 233 if (loc != null) { 234 if (b.isSelected() ^ _track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS)) { 235 _track.addDestination(loc); 236 } else { 237 _track.deleteDestination(loc); 238 } 239 } 240 } 241 242 private void checkDestinationsValid() { 243 SwingUtilities.invokeLater(() -> { 244 if (checkLocationsLoop()) 245 JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("OkayMessage")); 246 checkDestinationsButton.setEnabled(true); 247 }); 248 } 249 250 private boolean checkLocationsLoop() { 251 boolean noIssues = true; 252 // only report car type not serviced once 253 List<String> ignoreType = new ArrayList<String>(); 254 for (Location destination : locationManager.getLocationsByNameList()) { 255 ignoreType.clear(); 256 if (_track.isDestinationAccepted(destination)) { 257 log.debug("Track ({}) accepts destination ({})", _track.getName(), destination.getName()); 258 if (_track.getLocation() == destination) { 259 continue; 260 } 261 // now check to see if the track's rolling stock is accepted by the destination 262 checkTypes: for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 263 if (!_track.isTypeNameAccepted(type)) { 264 continue; 265 } 266 if (!destination.acceptsTypeName(type)) { 267 noIssues = false; 268 int response = JmriJOptionPane.showConfirmDialog(this, 269 Bundle.getMessage("WarningDestinationCarType", 270 destination.getName(), type), Bundle.getMessage("WarningCarMayNotMove"), 271 JmriJOptionPane.OK_CANCEL_OPTION); 272 if (response == JmriJOptionPane.OK_OPTION) { 273 ignoreType.add(type); 274 continue; 275 } 276 return false; // done 277 } 278 // now determine if there's a track willing to service car type 279 for (Track track : destination.getTracksList()) { 280 if (track.isTypeNameAccepted(type)) { 281 continue checkTypes; // yes there's a track 282 } 283 } 284 noIssues = false; 285 int response = JmriJOptionPane.showConfirmDialog(this, 286 Bundle.getMessage("WarningDestinationTrackCarType", 287 destination.getName(), type), 288 Bundle.getMessage("WarningCarMayNotMove"), 289 JmriJOptionPane.OK_CANCEL_OPTION); 290 if (response == JmriJOptionPane.OK_OPTION) { 291 ignoreType.add(type); 292 continue; 293 } 294 return false; // done 295 } 296 // now check road names 297 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 298 if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) { 299 continue; 300 } 301 checkRoads: for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) { 302 if (!_track.isRoadNameAccepted(road)) { 303 continue; 304 } 305 // now determine if there's a track willing to service this road 306 for (Track track : destination.getTracksList()) { 307 if (!track.isTypeNameAccepted(type)) { 308 continue; 309 } 310 if (track.isRoadNameAccepted(road)) { 311 continue checkRoads; // yes there's a track 312 } 313 } 314 noIssues = false; 315 int response = JmriJOptionPane.showConfirmDialog(this, 316 Bundle.getMessage("WarningDestinationTrackCarRoad", 317 destination.getName(), type, road), 318 Bundle.getMessage("WarningCarMayNotMove"), 319 JmriJOptionPane.OK_CANCEL_OPTION); 320 if (response == JmriJOptionPane.OK_OPTION) { 321 continue; 322 } 323 return false; // done 324 } 325 } 326 // now check load names 327 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 328 if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) { 329 continue; 330 } 331 List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type); 332 checkLoads: for (String load : loads) { 333 if (!_track.isLoadNameAccepted(load)) { 334 continue; 335 } 336 // now determine if there's a track willing to service this load 337 for (Track track : destination.getTracksList()) { 338 if (!track.isTypeNameAccepted(type)) { 339 continue; 340 } 341 if (track.isLoadNameAccepted(load)) { 342 continue checkLoads; 343 } 344 } 345 noIssues = false; 346 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 347 .getMessage("WarningDestinationTrackCarLoad", destination.getName(), 348 type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION); 349 if (response == JmriJOptionPane.OK_OPTION) { 350 continue; 351 } 352 return false; // done 353 } 354 // now check car type and load combinations 355 checkLoads: for (String load : loads) { 356 if (!_track.isLoadNameAndCarTypeAccepted(load, type)) { 357 continue; 358 } 359 // now determine if there's a track willing to service this load 360 for (Track track : destination.getTracksList()) { 361 if (track.isLoadNameAndCarTypeAccepted(load, type)) { 362 continue checkLoads; 363 } 364 } 365 noIssues = false; 366 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 367 .getMessage("WarningDestinationTrackCarLoad", destination.getName(), 368 type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION); 369 if (response == JmriJOptionPane.OK_OPTION) { 370 continue; 371 } 372 return false; // done 373 } 374 } 375 // now determine if there's a train or trains that can move a car from this track to the destinations 376 // need to check all car types, loads, and roads that this track services 377 Car car = new Car(); 378 car.setLength(Integer.toString(-RollingStock.COUPLERS)); // set car length to net out to zero 379 for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) { 380 if (!_track.isTypeNameAccepted(type)) { 381 continue; 382 } 383 List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type); 384 for (String load : loads) { 385 if (!_track.isLoadNameAndCarTypeAccepted(load, type)) { 386 continue; 387 } 388 for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) { 389 if (!_track.isRoadNameAccepted(road)) { 390 continue; 391 } 392 // is there a car with this road? 393 boolean foundCar = false; 394 for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList()) { 395 if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) { 396 foundCar = true; 397 break; 398 } 399 } 400 if (!foundCar) { 401 continue; // no car with this road name 402 } 403 404 car.setTypeName(type); 405 car.setRoadName(road); 406 car.setLoadName(load); 407 car.setTrack(_track); 408 car.setFinalDestination(destination); 409 410 // does the destination accept this car? 411 // this checks tracks that have schedules 412 String testDest = "NO_TYPE"; 413 for (Track track : destination.getTracksList()) { 414 if (!track.isTypeNameAccepted(type)) { 415 // already reported if type not accepted 416 continue; 417 } 418 if (track.getScheduleMode() == Track.SEQUENTIAL) { 419 // must test in match mode 420 track.setScheduleMode(Track.MATCH); 421 String itemId = track.getScheduleItemId(); 422 testDest = car.checkDestination(destination, track); 423 track.setScheduleMode(Track.SEQUENTIAL); 424 track.setScheduleItemId(itemId); 425 } else { 426 testDest = car.checkDestination(destination, track); 427 } 428 if (testDest.equals(Track.OKAY)) { 429 break; // done 430 } 431 } 432 433 if (testDest.equals("NO_TYPE")) { 434 continue; 435 } 436 437 if (!testDest.equals(Track.OKAY)) { 438 noIssues = false; 439 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 440 .getMessage("WarningNoTrack", destination.getName(), type, road, load, 441 destination.getName()), Bundle.getMessage("WarningCarMayNotMove"), 442 JmriJOptionPane.OK_CANCEL_OPTION); 443 if (response == JmriJOptionPane.OK_OPTION) { 444 continue; 445 } 446 return false; // done 447 } 448 449 log.debug("Find train for car type ({}), road ({}), load ({})", type, road, load); 450 451 boolean results = InstanceManager.getDefault(Router.class).setDestination(car, null, null); 452 car.setDestination(null, null); // clear destination if set by router 453 if (!results) { 454 noIssues = false; 455 int response = JmriJOptionPane.showConfirmDialog(this, Bundle 456 .getMessage("WarningNoTrain", type, road, load, 457 destination.getName()), Bundle.getMessage("WarningCarMayNotMove"), 458 JmriJOptionPane.OK_CANCEL_OPTION); 459 if (response == JmriJOptionPane.OK_OPTION) { 460 continue; 461 } 462 return false; // done 463 } 464 // TODO need to check owners and car built dates 465 } 466 } 467 } 468 } 469 } 470 return noIssues; 471 } 472 473 @Override 474 public void dispose() { 475 if (_track != null) { 476 _track.removePropertyChangeListener(this); 477 } 478 locationManager.removePropertyChangeListener(this); 479 super.dispose(); 480 } 481 482 @Override 483 public void propertyChange(java.beans.PropertyChangeEvent e) { 484 if (Control.SHOW_PROPERTY) { 485 log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e 486 .getNewValue()); 487 } 488 if (e.getPropertyName().equals(LocationManager.LISTLENGTH_CHANGED_PROPERTY) || 489 e.getPropertyName().equals(Track.DESTINATIONS_CHANGED_PROPERTY)) { 490 updateDestinations(); 491 } 492 if (e.getPropertyName().equals(Track.ROUTED_CHANGED_PROPERTY)) { 493 onlyCarsWithFD.setSelected((boolean) e.getNewValue()); 494 } 495 } 496 497 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackDestinationEditFrame.class); 498}