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.isLoadNameAndCarTypeAccepted(load, type)) {
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}