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 3
086        JPanel p3 = new JPanel();
087        p3.setLayout(new BoxLayout(p3, BoxLayout.Y_AXIS));
088        JScrollPane pane3 = new JScrollPane(p3);
089        pane3.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("DestinationTrack")));
090        pane3.setMaximumSize(new Dimension(2000, 400));
091
092        JPanel pRadioButtons = new JPanel();
093        pRadioButtons.setLayout(new FlowLayout());
094
095        pRadioButtons.add(destinationsAll);
096        pRadioButtons.add(destinationsInclude);
097        pRadioButtons.add(destinationsExclude);
098
099        p3.add(pRadioButtons);
100        
101        // row 4 only for C/I and Staging
102        JPanel pFD = new JPanel();
103        pFD.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Options")));
104        pFD.add(onlyCarsWithFD);
105        pFD.setMaximumSize(new Dimension(2000, 200));
106
107        // row 5
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 12
117        JPanel panelButtons = new JPanel();
118        panelButtons.setLayout(new GridBagLayout());
119        panelButtons.setBorder(BorderFactory.createTitledBorder(""));
120        panelButtons.setMaximumSize(new Dimension(2000, 200));
121
122        // row 13
123        addItem(panelButtons, checkDestinationsButton, 0, 0);
124        addItem(panelButtons, saveButton, 1, 0);
125
126        getContentPane().add(p1);
127        getContentPane().add(pane3);
128        getContentPane().add(pFD);
129        getContentPane().add(paneDestinations);
130        getContentPane().add(panelButtons);
131
132        // setup buttons
133        addButtonAction(checkDestinationsButton);
134        addButtonAction(saveButton);
135
136        addRadioButtonAction(destinationsAll);
137        addRadioButtonAction(destinationsInclude);
138        addRadioButtonAction(destinationsExclude);
139
140        // load fields and enable buttons
141        if (_track != null) {
142            _track.addPropertyChangeListener(this);
143            trackName.setText(_track.getName());
144            onlyCarsWithFD.setSelected(_track.isOnlyCarsWithFinalDestinationEnabled());
145            pFD.setVisible(_track.isInterchange() || _track.isStaging());
146            enableButtons(true);
147        } else {
148            enableButtons(false);
149        }
150
151        updateDestinations();
152
153        locationManager.addPropertyChangeListener(this);
154
155        initMinimumSize(new Dimension(Control.panelWidth400, Control.panelHeight500));
156    }
157
158    // Save, Delete, Add
159    @Override
160    public void buttonActionPerformed(java.awt.event.ActionEvent ae) {
161        if (_track == null) {
162            return;
163        }
164        if (ae.getSource() == saveButton) {
165            log.debug("track save button activated");
166            _track.setOnlyCarsWithFinalDestinationEnabled(onlyCarsWithFD.isSelected());
167            OperationsXml.save();
168            if (Setup.isCloseWindowOnSaveEnabled()) {
169                dispose();
170            }
171        }
172        if (ae.getSource() == checkDestinationsButton) {
173            checkDestinationsButton.setEnabled(false); // testing can take awhile, so disable
174            checkDestinationsValid();
175        }
176    }
177
178    protected void enableButtons(boolean enabled) {
179        saveButton.setEnabled(enabled);
180        checkDestinationsButton.setEnabled(enabled);
181        destinationsAll.setEnabled(enabled);
182        destinationsInclude.setEnabled(enabled);
183        destinationsExclude.setEnabled(enabled);
184    }
185
186    @Override
187    public void radioButtonActionPerformed(java.awt.event.ActionEvent ae) {
188        log.debug("radio button activated");
189        if (ae.getSource() == destinationsAll) {
190            _track.setDestinationOption(Track.ALL_DESTINATIONS);
191        }
192        if (ae.getSource() == destinationsInclude) {
193            _track.setDestinationOption(Track.INCLUDE_DESTINATIONS);
194        }
195        if (ae.getSource() == destinationsExclude) {
196            _track.setDestinationOption(Track.EXCLUDE_DESTINATIONS);
197        }
198        updateDestinations();
199    }
200
201    private void updateDestinations() {
202        log.debug("Update destinations");
203        panelDestinations.removeAll();
204        if (_track != null) {
205            destinationsAll.setSelected(_track.getDestinationOption().equals(Track.ALL_DESTINATIONS));
206            destinationsInclude.setSelected(_track.getDestinationOption().equals(Track.INCLUDE_DESTINATIONS));
207            destinationsExclude.setSelected(_track.getDestinationOption().equals(Track.EXCLUDE_DESTINATIONS));
208        }
209        List<Location> locations = locationManager.getLocationsByNameList();
210        for (int i = 0; i < locations.size(); i++) {
211            Location loc = locations.get(i);
212            JCheckBox cb = new JCheckBox(loc.getName());
213            addItemLeft(panelDestinations, cb, 0, i);
214            cb.setEnabled(!destinationsAll.isSelected());
215            addCheckBoxAction(cb);
216            if (destinationsAll.isSelected()) {
217                cb.setSelected(true);
218            } else if (_track != null && _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 (checkLocationsLoop())
246                JmriJOptionPane.showMessageDialog(null, Bundle.getMessage("OkayMessage"));
247            checkDestinationsButton.setEnabled(true);
248        });
249    }
250
251    private boolean checkLocationsLoop() {
252        boolean noIssues = true;
253        // only report car type not serviced once
254        List<String> ignoreType = new ArrayList<String>();
255        for (Location destination : locationManager.getLocationsByNameList()) {
256            ignoreType.clear();
257            if (_track.isDestinationAccepted(destination)) {
258                log.debug("Track ({}) accepts destination ({})", _track.getName(), destination.getName());
259                if (_track.getLocation() == destination) {
260                    continue;
261                }
262                // now check to see if the track's rolling stock is accepted by the destination
263                checkTypes: for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
264                    if (!_track.isTypeNameAccepted(type)) {
265                        continue;
266                    }
267                    if (!destination.acceptsTypeName(type)) {
268                        noIssues = false;
269                        int response = JmriJOptionPane.showConfirmDialog(this,
270                                Bundle.getMessage("WarningDestinationCarType", 
271                                        destination.getName(), type), Bundle.getMessage("WarningCarMayNotMove"),
272                                JmriJOptionPane.OK_CANCEL_OPTION);
273                        if (response == JmriJOptionPane.OK_OPTION) {
274                            ignoreType.add(type);
275                            continue;
276                        }
277                        return false; // done
278                    }
279                    // now determine if there's a track willing to service car type
280                    for (Track track : destination.getTracksList()) {
281                        if (track.isTypeNameAccepted(type)) {
282                            continue checkTypes; // yes there's a track
283                        }
284                    }
285                    noIssues = false;
286                    int response = JmriJOptionPane.showConfirmDialog(this,
287                            Bundle.getMessage("WarningDestinationTrackCarType",
288                                    destination.getName(), type),
289                            Bundle.getMessage("WarningCarMayNotMove"),
290                            JmriJOptionPane.OK_CANCEL_OPTION);
291                    if (response == JmriJOptionPane.OK_OPTION) {
292                        ignoreType.add(type);
293                        continue;
294                    }
295                    return false; // done
296                }
297                // now check road names
298                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
299                    if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) {
300                        continue;
301                    }
302                    checkRoads: for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) {
303                        if (!_track.isRoadNameAccepted(road)) {
304                            continue;
305                        }
306                        // now determine if there's a track willing to service this road
307                        for (Track track : destination.getTracksList()) {
308                            if (!track.isTypeNameAccepted(type)) {
309                                continue;
310                            }
311                            if (track.isRoadNameAccepted(road)) {
312                                continue checkRoads; // yes there's a track
313                            }
314                        }
315                        noIssues = false;
316                        int response = JmriJOptionPane.showConfirmDialog(this,
317                                Bundle.getMessage("WarningDestinationTrackCarRoad",
318                                        destination.getName(), type, road),
319                                Bundle.getMessage("WarningCarMayNotMove"),
320                                JmriJOptionPane.OK_CANCEL_OPTION);
321                        if (response == JmriJOptionPane.OK_OPTION) {
322                            continue;
323                        }
324                        return false; // done
325                    }
326                }
327                // now check load names
328                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
329                    if (!_track.isTypeNameAccepted(type) || ignoreType.contains(type)) {
330                        continue;
331                    }
332                    List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type);
333                    checkLoads: for (String load : loads) {
334                        if (!_track.isLoadNameAccepted(load)) {
335                            continue;
336                        }
337                        // now determine if there's a track willing to service this load
338                        for (Track track : destination.getTracksList()) {
339                            if (!track.isTypeNameAccepted(type)) {
340                                continue;
341                            }
342                            if (track.isLoadNameAccepted(load)) {
343                                continue checkLoads;
344                            }
345                        }
346                        noIssues = false;
347                        int response = JmriJOptionPane.showConfirmDialog(this, Bundle
348                                .getMessage("WarningDestinationTrackCarLoad", destination.getName(),
349                                type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION);
350                        if (response == JmriJOptionPane.OK_OPTION) {
351                            continue;
352                        }
353                        return false; // done
354                    }
355                    // now check car type and load combinations
356                    checkLoads: for (String load : loads) {
357                        if (!_track.isLoadNameAndCarTypeAccepted(load, type)) {
358                            continue;
359                        }
360                        // now determine if there's a track willing to service this load
361                        for (Track track : destination.getTracksList()) {
362                            if (track.isLoadNameAndCarTypeAccepted(load, type)) {
363                                continue checkLoads;
364                            }
365                        }
366                        noIssues = false;
367                        int response = JmriJOptionPane.showConfirmDialog(this, Bundle
368                                .getMessage("WarningDestinationTrackCarLoad", destination.getName(),
369                                type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION);
370                        if (response == JmriJOptionPane.OK_OPTION) {
371                            continue;
372                        }
373                        return false; // done
374                    }
375                }
376                // now determine if there's a train or trains that can move a car from this track to the destinations
377                // need to check all car types, loads, and roads that this track services
378                Car car = new Car();
379                car.setLength(Integer.toString(-RollingStock.COUPLERS)); // set car length to net out to zero
380                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
381                    if (!_track.isTypeNameAccepted(type)) {
382                        continue;
383                    }
384                    List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type);
385                    for (String load : loads) {
386                        if (!_track.isLoadNameAndCarTypeAccepted(load, type)) {
387                            continue;
388                        }
389                        for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) {
390                            if (!_track.isRoadNameAccepted(road)) {
391                                continue;
392                            }
393                            // is there a car with this road?
394                            boolean foundCar = false;
395                            for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList()) {
396                                if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) {
397                                    foundCar = true;
398                                    break;
399                                }
400                            }
401                            if (!foundCar) {
402                                continue; // no car with this road name
403                            }
404
405                            car.setTypeName(type);
406                            car.setRoadName(road);
407                            car.setLoadName(load);
408                            car.setTrack(_track);
409                            car.setFinalDestination(destination);
410                            
411                            // does the destination accept this car?
412                            // this checks tracks that have schedules
413                            String testDest = "NO_TYPE";
414                            for (Track track : destination.getTracksList()) {
415                                if (!track.isTypeNameAccepted(type)) {
416                                    // already reported if type not accepted
417                                    continue; 
418                                }
419                                if (track.getScheduleMode() == Track.SEQUENTIAL) {
420                                    // must test in match mode
421                                    track.setScheduleMode(Track.MATCH);
422                                    String itemId = track.getScheduleItemId();
423                                    testDest = car.checkDestination(destination, track);
424                                    track.setScheduleMode(Track.SEQUENTIAL);
425                                    track.setScheduleItemId(itemId);
426                                } else {
427                                    testDest = car.checkDestination(destination, track);
428                                }
429                                if (testDest.equals(Track.OKAY)) {
430                                    break; // done
431                                }
432                            }
433                            
434                            if (testDest.equals("NO_TYPE")) {
435                                continue;
436                            }
437                            
438                            if (!testDest.equals(Track.OKAY)) {
439                                noIssues = false;
440                                int response = JmriJOptionPane.showConfirmDialog(this, Bundle
441                                        .getMessage("WarningNoTrack", destination.getName(), type, road, load,
442                                        destination.getName()), Bundle.getMessage("WarningCarMayNotMove"),
443                                        JmriJOptionPane.OK_CANCEL_OPTION);
444                                if (response == JmriJOptionPane.OK_OPTION) {
445                                    continue;
446                                }
447                                return false; // done
448                            }
449                            
450                            log.debug("Find train for car type ({}), road ({}), load ({})", type, road, load);
451
452                            boolean results = InstanceManager.getDefault(Router.class).setDestination(car, null, null);
453                            car.setDestination(null, null); // clear destination if set by router
454                            if (!results) {
455                                noIssues = false;
456                                int response = JmriJOptionPane.showConfirmDialog(this, Bundle
457                                        .getMessage("WarningNoTrain", type, road, load,
458                                        destination.getName()), Bundle.getMessage("WarningCarMayNotMove"),
459                                        JmriJOptionPane.OK_CANCEL_OPTION);
460                                if (response == JmriJOptionPane.OK_OPTION) {
461                                    continue;
462                                }
463                                return false; // done
464                            }
465                            // TODO need to check owners and car built dates
466                        }
467                    }
468                }
469            }
470        }
471        return noIssues;
472    }
473
474    @Override
475    public void dispose() {
476        if (_track != null) {
477            _track.removePropertyChangeListener(this);
478        }
479        locationManager.removePropertyChangeListener(this);
480        super.dispose();
481    }
482
483    @Override
484    public void propertyChange(java.beans.PropertyChangeEvent e) {
485        if (Control.SHOW_PROPERTY) {
486            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
487                    .getNewValue());
488        }
489        if (e.getPropertyName().equals(LocationManager.LISTLENGTH_CHANGED_PROPERTY) ||
490                e.getPropertyName().equals(Track.DESTINATIONS_CHANGED_PROPERTY)) {
491            updateDestinations();
492        }
493        if (e.getPropertyName().equals(Track.ROUTED_CHANGED_PROPERTY)) {
494            onlyCarsWithFD.setSelected((boolean) e.getNewValue());
495        }
496    }
497
498    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackDestinationEditFrame.class);
499}