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}