001package jmri.jmrit.operations.locations.tools;
002
003import java.awt.*;
004import java.util.List;
005
006import javax.swing.*;
007
008import jmri.InstanceManager;
009import jmri.jmrit.operations.OperationsFrame;
010import jmri.jmrit.operations.OperationsXml;
011import jmri.jmrit.operations.locations.*;
012import jmri.jmrit.operations.rollingstock.RollingStock;
013import jmri.jmrit.operations.rollingstock.cars.*;
014import jmri.jmrit.operations.router.Router;
015import jmri.jmrit.operations.setup.Control;
016import jmri.jmrit.operations.setup.Setup;
017import jmri.util.swing.JmriJOptionPane;
018
019/**
020 * Frame for user edit of track destinations
021 *
022 * @author Dan Boudreau Copyright (C) 2013, 2024
023 * 
024 */
025public class TrackDestinationEditFrame extends OperationsFrame implements java.beans.PropertyChangeListener {
026
027    Track _track = null;
028
029    LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
030
031    // panels
032    JPanel pControls = new JPanel();
033    JPanel panelDestinations = new JPanel();
034    JScrollPane paneDestinations = new JScrollPane(panelDestinations);
035
036    // major buttons
037    JButton saveButton = new JButton(Bundle.getMessage("ButtonSave"));
038    JButton checkDestinationsButton = new JButton(Bundle.getMessage("CheckDestinations"));
039
040    // radio buttons
041    JRadioButton destinationsAll = new JRadioButton(Bundle.getMessage("AcceptAll"));
042    JRadioButton destinationsInclude = new JRadioButton(Bundle.getMessage("AcceptOnly"));
043    JRadioButton destinationsExclude = new JRadioButton(Bundle.getMessage("Exclude"));
044    
045    // checkboxes
046    JCheckBox onlyCarsWithFD = new JCheckBox(Bundle.getMessage("OnlyCarsWithFD"));
047
048    // labels
049    JLabel trackName = new JLabel();
050
051    public static final String DISPOSE = "dispose"; // NOI18N
052
053    public TrackDestinationEditFrame() {
054        super(Bundle.getMessage("TitleEditTrackDestinations"));
055    }
056
057    public void initComponents(TrackEditFrame tef) {
058        _track = tef._track;
059
060        // the following code sets the frame's initial state
061        getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
062
063        // Layout the panel by rows
064        // row 1
065        JPanel p1 = new JPanel();
066        p1.setLayout(new BoxLayout(p1, BoxLayout.X_AXIS));
067        p1.setMaximumSize(new Dimension(2000, 250));
068
069        // row 1a
070        JPanel pTrackName = new JPanel();
071        pTrackName.setLayout(new GridBagLayout());
072        pTrackName.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Track")));
073        addItem(pTrackName, trackName, 0, 0);
074
075        // row 1b
076        JPanel pLocationName = new JPanel();
077        pLocationName.setLayout(new GridBagLayout());
078        pLocationName.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Location")));
079        addItem(pLocationName, new JLabel(_track.getLocation().getName()), 0, 0);
080
081        p1.add(pTrackName);
082        p1.add(pLocationName);
083
084        // row 3
085        JPanel p3 = new JPanel();
086        p3.setLayout(new BoxLayout(p3, BoxLayout.Y_AXIS));
087        JScrollPane pane3 = new JScrollPane(p3);
088        pane3.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("DestinationTrack")));
089        pane3.setMaximumSize(new Dimension(2000, 400));
090
091        JPanel pRadioButtons = new JPanel();
092        pRadioButtons.setLayout(new FlowLayout());
093
094        pRadioButtons.add(destinationsAll);
095        pRadioButtons.add(destinationsInclude);
096        pRadioButtons.add(destinationsExclude);
097
098        p3.add(pRadioButtons);
099        
100        // row 4 only for C/I and Staging
101        JPanel pFD = new JPanel();
102        pFD.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Options")));
103        pFD.add(onlyCarsWithFD);
104        pFD.setMaximumSize(new Dimension(2000, 200));
105
106        // row 5
107        panelDestinations.setLayout(new GridBagLayout());
108        paneDestinations.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Destinations")));
109
110        ButtonGroup bGroup = new ButtonGroup();
111        bGroup.add(destinationsAll);
112        bGroup.add(destinationsInclude);
113        bGroup.add(destinationsExclude);
114
115        // row 12
116        JPanel panelButtons = new JPanel();
117        panelButtons.setLayout(new GridBagLayout());
118        panelButtons.setBorder(BorderFactory.createTitledBorder(""));
119        panelButtons.setMaximumSize(new Dimension(2000, 200));
120
121        // row 13
122        addItem(panelButtons, checkDestinationsButton, 0, 0);
123        addItem(panelButtons, saveButton, 1, 0);
124
125        getContentPane().add(p1);
126        getContentPane().add(pane3);
127        getContentPane().add(pFD);
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        for (Location destination : locationManager.getLocationsByNameList()) {
253            if (_track.isDestinationAccepted(destination)) {
254                log.debug("Track ({}) accepts destination ({})", _track.getName(), destination.getName());
255                if (_track.getLocation() == destination) {
256                    continue;
257                }
258                // now check to see if the track's rolling stock is accepted by the destination
259                checkTypes: for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
260                    if (!_track.isTypeNameAccepted(type)) {
261                        continue;
262                    }
263                    if (!destination.acceptsTypeName(type)) {
264                        noIssues = false;
265                        int response = JmriJOptionPane.showConfirmDialog(this,
266                                Bundle.getMessage("WarningDestinationCarType", 
267                                        destination.getName(), type), Bundle.getMessage("WarningCarMayNotMove"),
268                                JmriJOptionPane.OK_CANCEL_OPTION);
269                        if (response == JmriJOptionPane.OK_OPTION)
270                            continue;
271                        return false; // done
272                    }
273                    // now determine if there's a track willing to service car type
274                    for (Track track : destination.getTracksList()) {
275                        if (track.isTypeNameAccepted(type)) {
276                            continue checkTypes; // yes there's a track
277                        }
278                    }
279                    noIssues = false;
280                    int response = JmriJOptionPane.showConfirmDialog(this,
281                            Bundle.getMessage("WarningDestinationTrackCarType",
282                                    destination.getName(), type),
283                            Bundle.getMessage("WarningCarMayNotMove"),
284                            JmriJOptionPane.OK_CANCEL_OPTION);
285                    if (response == JmriJOptionPane.OK_OPTION)
286                        continue;
287                    return false; // done
288                }
289                // now check road names
290                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
291                    if (!_track.isTypeNameAccepted(type)) {
292                        continue;
293                    }
294                    checkRoads: for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) {
295                        if (!_track.isRoadNameAccepted(road)) {
296                            continue;
297                        }
298                        // now determine if there's a track willing to service this road
299                        for (Track track : destination.getTracksList()) {
300                            if (!track.isTypeNameAccepted(type)) {
301                                continue;
302                            }
303                            if (track.isRoadNameAccepted(road)) {
304                                continue checkRoads; // yes there's a track
305                            }
306                        }
307                        noIssues = false;
308                        int response = JmriJOptionPane.showConfirmDialog(this,
309                                Bundle.getMessage("WarningDestinationTrackCarRoad",
310                                        destination.getName(), type, road),
311                                Bundle.getMessage("WarningCarMayNotMove"),
312                                JmriJOptionPane.OK_CANCEL_OPTION);
313                        if (response == JmriJOptionPane.OK_OPTION)
314                            continue;
315                        return false; // done
316                    }
317                }
318                // now check load names
319                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
320                    if (!_track.isTypeNameAccepted(type)) {
321                        continue;
322                    }
323                    List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type);
324                    checkLoads: for (String load : loads) {
325                        if (!_track.isLoadNameAccepted(load)) {
326                            continue;
327                        }
328                        // now determine if there's a track willing to service this load
329                        for (Track track : destination.getTracksList()) {
330                            if (track.isLoadNameAccepted(load)) {
331                                continue checkLoads;
332                            }
333                        }
334                        noIssues = false;
335                        int response = JmriJOptionPane.showConfirmDialog(this, Bundle
336                                .getMessage("WarningDestinationTrackCarLoad", destination.getName(),
337                                type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION);
338                        if (response == JmriJOptionPane.OK_OPTION)
339                            continue;
340                        return false; // done
341                    }
342                    // now check car type and load combinations
343                    checkLoads: for (String load : loads) {
344                        if (!_track.isLoadNameAndCarTypeAccepted(load, type)) {
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.isLoadNameAndCarTypeAccepted(load, type)) {
350                                continue checkLoads;
351                            }
352                        }
353                        noIssues = false;
354                        int response = JmriJOptionPane.showConfirmDialog(this, Bundle
355                                .getMessage("WarningDestinationTrackCarLoad", destination.getName(),
356                                type, load), Bundle.getMessage("WarningCarMayNotMove"), JmriJOptionPane.OK_CANCEL_OPTION);
357                        if (response == JmriJOptionPane.OK_OPTION)
358                            continue;
359                        return false; // done
360                    }
361                }
362                // now determine if there's a train or trains that can move a car from this track to the destinations
363                // need to check all car types, loads, and roads that this track services
364                Car car = new Car();
365                car.setLength(Integer.toString(-RollingStock.COUPLERS)); // set car length to net out to zero
366                for (String type : InstanceManager.getDefault(CarTypes.class).getNames()) {
367                    if (!_track.isTypeNameAccepted(type)) {
368                        continue;
369                    }
370                    List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(type);
371                    for (String load : loads) {
372                        if (!_track.isLoadNameAndCarTypeAccepted(load, type)) {
373                            continue;
374                        }
375                        for (String road : InstanceManager.getDefault(CarRoads.class).getNames(type)) {
376                            if (!_track.isRoadNameAccepted(road)) {
377                                continue;
378                            }
379                            // is there a car with this road?
380                            boolean foundCar = false;
381                            for (RollingStock rs : InstanceManager.getDefault(CarManager.class).getList()) {
382                                if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) {
383                                    foundCar = true;
384                                    break;
385                                }
386                            }
387                            if (!foundCar) {
388                                continue; // no car with this road name
389                            }
390
391                            car.setTypeName(type);
392                            car.setRoadName(road);
393                            car.setLoadName(load);
394                            car.setTrack(_track);
395                            car.setFinalDestination(destination);
396                            
397                            // does the destination accept this car?
398                            // this checks tracks that have schedules
399                            String testDest = "NO_TYPE";
400                            for (Track track : destination.getTracksList()) {
401                                if (!track.isTypeNameAccepted(type)) {
402                                    // already reported if type not accepted
403                                    continue; 
404                                }
405                                if (track.getScheduleMode() == Track.SEQUENTIAL) {
406                                    // must test in match mode
407                                    track.setScheduleMode(Track.MATCH);
408                                    String itemId = track.getScheduleItemId();
409                                    testDest = car.checkDestination(destination, track);
410                                    track.setScheduleMode(Track.SEQUENTIAL);
411                                    track.setScheduleItemId(itemId);
412                                } else {
413                                    testDest = car.checkDestination(destination, track);
414                                }
415                                if (testDest.equals(Track.OKAY)) {
416                                    break; // done
417                                }
418                            }
419                            
420                            if (testDest.equals("NO_TYPE")) {
421                                continue;
422                            }
423                            
424                            if (!testDest.equals(Track.OKAY)) {
425                                noIssues = false;
426                                int response = JmriJOptionPane.showConfirmDialog(this, Bundle
427                                        .getMessage("WarningNoTrack", destination.getName(), type, road, load,
428                                        destination.getName()), Bundle.getMessage("WarningCarMayNotMove"),
429                                        JmriJOptionPane.OK_CANCEL_OPTION);
430                                if (response == JmriJOptionPane.OK_OPTION)
431                                    continue;
432                                return false; // done
433                            }
434                            
435                            log.debug("Find train for car type ({}), road ({}), load ({})", type, road, load);
436
437                            boolean results = InstanceManager.getDefault(Router.class).setDestination(car, null, null);
438                            car.setDestination(null, null); // clear destination if set by router
439                            if (!results) {
440                                noIssues = false;
441                                int response = JmriJOptionPane.showConfirmDialog(this, Bundle
442                                        .getMessage("WarningNoTrain", type, road, load,
443                                        destination.getName()), Bundle.getMessage("WarningCarMayNotMove"),
444                                        JmriJOptionPane.OK_CANCEL_OPTION);
445                                if (response == JmriJOptionPane.OK_OPTION)
446                                    continue;
447                                return false; // done
448                            }
449                            // TODO need to check owners and car built dates
450                        }
451                    }
452                }
453            }
454        }
455        return noIssues;
456    }
457
458    @Override
459    public void dispose() {
460        if (_track != null) {
461            _track.removePropertyChangeListener(this);
462        }
463        locationManager.removePropertyChangeListener(this);
464        super.dispose();
465    }
466
467    @Override
468    public void propertyChange(java.beans.PropertyChangeEvent e) {
469        if (Control.SHOW_PROPERTY) {
470            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
471                    .getNewValue());
472        }
473        if (e.getPropertyName().equals(LocationManager.LISTLENGTH_CHANGED_PROPERTY) ||
474                e.getPropertyName().equals(Track.DESTINATIONS_CHANGED_PROPERTY)) {
475            updateDestinations();
476        }
477        if (e.getPropertyName().equals(Track.ROUTED_CHANGED_PROPERTY)) {
478            onlyCarsWithFD.setSelected((boolean) e.getNewValue());
479        }
480    }
481
482    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackDestinationEditFrame.class);
483}