001package jmri.jmrit.operations.locations.gui;
002
003import java.awt.Color;
004import java.awt.Dimension;
005import java.awt.event.ActionEvent;
006import java.text.MessageFormat;
007import java.util.ArrayList;
008import java.util.List;
009
010import javax.swing.*;
011
012import jmri.InstanceManager;
013import jmri.jmrit.operations.CommonConductorYardmasterPanel;
014import jmri.jmrit.operations.locations.*;
015import jmri.jmrit.operations.rollingstock.RollingStock;
016import jmri.jmrit.operations.rollingstock.cars.*;
017import jmri.jmrit.operations.rollingstock.engines.Engine;
018import jmri.jmrit.operations.routes.RouteLocation;
019import jmri.jmrit.operations.setup.Control;
020import jmri.jmrit.operations.setup.Setup;
021import jmri.jmrit.operations.trains.Train;
022import jmri.jmrit.operations.trains.TrainSwitchListText;
023import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
024
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * Yardmaster frame by track. Shows work at one location listed by track.
030 *
031 * @author Dan Boudreau Copyright (C) 2015, 2026
032 */
033public class YardmasterByTrackPanel extends CommonConductorYardmasterPanel {
034
035    protected Track _track = null;
036
037    // text panes
038    JTextPane textTrackCommentPane = new JTextPane();
039
040    // combo boxes
041    JComboBox<Track> trackComboBox = new JComboBox<>();
042
043    // buttons
044    JButton nextButton = new JButton(Bundle.getMessage("Next"));
045
046    // panel
047    JPanel pTrack = new JPanel();
048    JScrollPane pTrackPane;
049
050    public YardmasterByTrackPanel() {
051        this(null);
052    }
053
054    public YardmasterByTrackPanel(Location location) {
055        super();
056
057        // this window doesn't use the set button
058        modifyButton.setVisible(false);
059
060        _location = location;
061
062        textTrackCommentPane.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("TrackComment")));
063        textTrackCommentPane.setBackground(null);
064        textTrackCommentPane.setEditable(false);
065        textTrackCommentPane.setMaximumSize(new Dimension(1000, 200));
066
067        JPanel pTrackSelect = new JPanel();
068        pTrackSelect.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Track")));
069        pTrackSelect.add(trackComboBox);
070        // add next button for web server
071        pTrackSelect.add(nextButton);
072
073        // work at this location by track
074        pTrack.setLayout(new BoxLayout(pTrack, BoxLayout.Y_AXIS));
075        pTrackPane = new JScrollPane(pTrack);
076
077        pLocationName.setMaximumSize(new Dimension(2000, 200));
078        pTrackSelect.setMaximumSize(new Dimension(2000, 200));
079        pButtons.setMaximumSize(new Dimension(2000, 200));
080
081        add(pLocationName);
082        add(textLocationCommentPane);
083        add(textSwitchListCommentPane);
084        add(pTrackSelect);
085        add(textTrackCommentPane);
086        add(pTrackComments);
087        add(pTrackPane);
088        add(pButtons);
089
090        if (_location != null) {
091            textLocationName.setText(_location.getName());
092            loadLocationComment(_location);
093            loadLocationSwitchListComment(_location);
094            updateTrackComboBox();
095            _location.addPropertyChangeListener(this);
096        }
097
098        update();
099
100        addComboBoxAction(trackComboBox);
101        addButtonAction(nextButton);
102    }
103
104    // Select, Clear, and Next Buttons
105    @Override
106    public void buttonActionPerformed(ActionEvent ae) {
107        if (ae.getSource() == nextButton) {
108            nextButtonAction();
109        }
110        super.buttonActionPerformed(ae);
111    }
112
113    private void nextButtonAction() {
114        log.debug("next button activated");
115        if (trackComboBox.getItemCount() > 1) {
116            int index = trackComboBox.getSelectedIndex();
117            // index = -1 if first item (null) in trainComboBox
118            if (index == -1) {
119                index = 1;
120            } else {
121                index++;
122            }
123            if (index >= trackComboBox.getItemCount()) {
124                index = 0;
125            }
126            trackComboBox.setSelectedIndex(index);
127        }
128    }
129
130    @Override
131    protected void comboBoxActionPerformed(ActionEvent ae) {
132        // made the combo box not visible during updates, so ignore if not visible
133        if (ae.getSource() == trackComboBox && trackComboBox.isVisible()) {
134            _track = null;
135            if (trackComboBox.getSelectedItem() != null) {
136                _track = (Track) trackComboBox.getSelectedItem();
137            }
138            update();
139        }
140    }
141
142    @Override
143    protected void update() {
144        // use invokeLater to prevent deadlock
145        SwingUtilities.invokeLater(() -> {
146            runUpdate();
147        });
148    }
149
150    private void runUpdate() {
151        log.debug("run update");
152        removePropertyChangeListerners();
153        trainCommon.clearUtilityCarTypes(); // reset the utility car counts
154        checkBoxes.clear();
155        pTrack.removeAll();
156        if (_track != null) {
157            pTrackPane.setBorder(BorderFactory.createTitledBorder(_track.getSplitName()));
158            textTrackCommentPane.setText(TrainCommon.getOnlyText(_track.getComment()));
159            textTrackCommentPane.setForeground(TrainCommon.getTextColor(_track.getComment()));
160            textTrackCommentPane.setVisible(!_track.getComment().equals(Track.NONE));
161            for (Train train : trainManager.getTrainsArrivingThisLocationList(_track.getLocation())) {
162                JPanel pTrain = new JPanel();
163                pTrain.setLayout(new BoxLayout(pTrain, BoxLayout.Y_AXIS));
164                pTrain.setBorder(BorderFactory
165                        .createTitledBorder(MessageFormat.format(TrainSwitchListText.getStringScheduledWork(),
166                                new Object[]{train.getName(), train.getDescription()})));
167                // Track work comments
168                boolean pickupCar = false;
169                boolean setoutCar = false;
170                JTextPane textTrackCommentWorkPane = getTrackWorkCommentPane();
171                pTrain.add(textTrackCommentWorkPane);
172
173                boolean localCar = false;
174
175                // Engine
176                boolean pickupEngine = false;
177                boolean setoutEngine = false;
178
179                // pick ups
180                JPanel pPickups = new JPanel();
181                pPickups.setLayout(new BoxLayout(pPickups, BoxLayout.Y_AXIS));
182                pPickups.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Pickup")));
183                pPickups.setMaximumSize(new Dimension(2000, 2000));
184
185                // set outs
186                JPanel pSetouts = new JPanel();
187                pSetouts.setLayout(new BoxLayout(pSetouts, BoxLayout.Y_AXIS));
188                pSetouts.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("SetOut")));
189                pSetouts.setMaximumSize(new Dimension(2000, 2000));
190
191                // local moves
192                JPanel pLocal = new JPanel();
193                pLocal.setLayout(new BoxLayout(pLocal, BoxLayout.Y_AXIS));
194                pLocal.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("LocalMoves")));
195                pLocal.setMaximumSize(new Dimension(2000, 2000));
196
197                // List locos first
198                List<Engine> engList = engManager.getByTrainBlockingList(train);
199                if (Setup.isPrintHeadersEnabled()) {
200                    for (Engine engine : engList) {
201                        if (engine.getSplitTrackName().equals(_track.getSplitName())) {
202                            JLabel header = new JLabel(
203                                    Tab + trainCommon.getPickupEngineHeader(!TrainCommon.IS_TWO_COLUMN_TRACK));
204                            setLabelFont(header);
205                            pPickups.add(header);
206                            break;
207                        }
208                    }
209                }
210                for (Engine engine : engList) {
211                    if (engine.getSplitTrackName().equals(_track.getSplitName())) {
212                        engine.addPropertyChangeListener(this);
213                        rollingStock.add(engine);
214                        JCheckBox checkBox =
215                                new JCheckBox(trainCommon.pickupEngine(engine, !TrainCommon.IS_TWO_COLUMN_TRACK));
216                        setCheckBoxFont(checkBox, Setup.getPickupEngineColor());
217                        pPickups.add(checkBox);
218                        pickupEngine = true;
219                        checkBoxes.put(engine.getId() + "p", checkBox);
220                        pTrack.add(pTrain);
221                    }
222                }
223                // now do locomotive set outs
224                if (Setup.isPrintHeadersEnabled()) {
225                    for (Engine engine : engList) {
226                        if (engine.getSplitDestinationTrackName().equals(_track.getSplitName())) {
227                            JLabel header =
228                                    new JLabel(Tab + trainCommon.getDropEngineHeader(!TrainCommon.IS_TWO_COLUMN_TRACK));
229                            setLabelFont(header);
230                            pSetouts.add(header);
231                            break;
232                        }
233                    }
234                }
235                for (Engine engine : engList) {
236                    if (engine.getSplitDestinationTrackName().equals(_track.getSplitName())) {
237                        engine.addPropertyChangeListener(this);
238                        rollingStock.add(engine);
239                        JCheckBox checkBox =
240                                new JCheckBox(trainCommon.dropEngine(engine, !TrainCommon.IS_TWO_COLUMN_TRACK));
241                        setCheckBoxFont(checkBox, Setup.getDropEngineColor());
242                        pSetouts.add(checkBox);
243                        setoutEngine = true;
244                        checkBoxes.put(engine.getId(), checkBox);
245                        pTrack.add(pTrain);
246                    }
247                }
248                // now cars
249                List<Car> carList = carManager.getByTrainDestinationList(train);
250                if (Setup.isPrintHeadersEnabled()) {
251                    for (Car car : carList) {
252                        if (car.getSplitTrackName().equals(_track.getSplitName()) &&
253                                car.getRouteDestination() != car.getRouteLocation()) {
254                            JLabel header = new JLabel(Tab +
255                                    trainCommon.getPickupCarHeader(!IS_MANIFEST, !TrainCommon.IS_TWO_COLUMN_TRACK));
256                            setLabelFont(header);
257                            pPickups.add(header);
258                            break;
259                        }
260                    }
261                }
262                // sort car pick ups by their destination
263                List<RouteLocation> routeList = train.getRoute().getLocationsBySequenceList();
264                for (RouteLocation rl : routeList) {
265                    for (Car car : carList) {
266                        if (car.getSplitTrackName().equals(_track.getSplitName()) &&
267                                car.getRouteDestination() != car.getRouteLocation() &&
268                                car.getRouteDestination() == rl) {
269                            car.addPropertyChangeListener(this);
270                            rollingStock.add(car);
271                            String text;
272                            if (car.isUtility()) {
273                                text = trainCommon.pickupUtilityCars(carList, car, !IS_MANIFEST,
274                                        !TrainCommon.IS_TWO_COLUMN_TRACK);
275                                if (text == null) {
276                                    continue; // this car type has already been processed
277                                }
278                            } else {
279                                text = trainCommon.pickupCar(car, !IS_MANIFEST, !TrainCommon.IS_TWO_COLUMN_TRACK);
280                            }
281                            pickupCar = true;
282                            JCheckBox checkBox = new JCheckBox(text);
283                            setCheckBoxFont(checkBox, Setup.getPickupColor());
284                            pPickups.add(checkBox);
285                            checkBoxes.put(car.getId() + "p", checkBox);
286                            pTrack.add(pTrain);
287                        }
288                    }
289                }
290                // now do car set outs
291                if (Setup.isPrintHeadersEnabled()) {
292                    for (Car car : carList) {
293                        if (car.getSplitDestinationTrackName().equals(_track.getSplitName()) &&
294                                (car.getTrack() == null ||
295                                        car.getRouteDestination() != car.getRouteLocation())) {
296                            JLabel header = new JLabel(
297                                    Tab + trainCommon.getDropCarHeader(!IS_MANIFEST, !TrainCommon.IS_TWO_COLUMN_TRACK));
298                            setLabelFont(header);
299                            pSetouts.add(header);
300                            break;
301                        }
302                    }
303                }
304                for (Car car : carList) {
305                    if (car.getSplitDestinationTrackName().equals(_track.getSplitName()) &&
306                            (car.getTrack() == null || car.getRouteLocation() != car.getRouteDestination())) {
307                        car.addPropertyChangeListener(this);
308                        rollingStock.add(car);
309                        String text;
310                        if (car.isUtility()) {
311                            text = trainCommon.setoutUtilityCars(carList, car, !TrainCommon.LOCAL, !IS_MANIFEST);
312                            if (text == null) {
313                                continue; // this car type has already been processed
314                            }
315                        } else {
316                            text = trainCommon.dropCar(car, !IS_MANIFEST, !TrainCommon.IS_TWO_COLUMN_TRACK);
317                        }
318                        setoutCar = true;
319                        JCheckBox checkBox = new JCheckBox(text);
320                        setCheckBoxFont(checkBox, Setup.getDropColor());
321                        pSetouts.add(checkBox);
322                        checkBoxes.put(car.getId(), checkBox);
323                        pTrack.add(pTrain);
324                    }
325                }
326                // now do local car moves
327                if (Setup.isPrintHeadersEnabled()) {
328                    for (Car car : carList) {
329                        if ((car.getSplitTrackName().equals(_track.getSplitName()) ||
330                                car.getTrack() != null &&
331                                        car.getSplitDestinationTrackName().equals(_track.getSplitName())) &&
332                                car.getRouteDestination() == car.getRouteLocation()) {
333                            JLabel header = new JLabel(Tab + trainCommon.getLocalMoveHeader(!IS_MANIFEST));
334                            setLabelFont(header);
335                            pLocal.add(header);
336                            break;
337                        }
338                    }
339                }
340                for (Car car : carList) {
341                    if ((car.getSplitTrackName().equals(_track.getSplitName()) ||
342                            car.getTrack() != null &&
343                                    car.getSplitDestinationTrackName().equals(_track.getSplitName())) &&
344                            car.getRouteLocation() != null &&
345                            car.getRouteLocation() == car.getRouteDestination()) {
346                        car.addPropertyChangeListener(this);
347                        rollingStock.add(car);
348                        String text;
349                        if (car.isUtility()) {
350                            text = trainCommon.setoutUtilityCars(carList, car, TrainCommon.LOCAL, !IS_MANIFEST);
351                            if (text == null) {
352                                continue; // this car type has already been processed
353                            }
354                        } else {
355                            text = trainCommon.localMoveCar(car, !IS_MANIFEST);
356                        }
357                        if (car.getSplitTrackName().equals(_track.getSplitName())) {
358                            pickupCar = true;
359                        }
360                        if (car.getSplitDestinationTrackName().equals(_track.getSplitName())) {
361                            setoutCar = true;
362                        }
363                        JCheckBox checkBox = new JCheckBox(text);
364                        setCheckBoxFont(checkBox, Setup.getLocalColor());
365                        pLocal.add(checkBox);
366                        localCar = true;
367                        checkBoxes.put(car.getId(), checkBox);
368                        pTrack.add(pTrain);
369                    }
370                }
371                if (pickupCar && !setoutCar) {
372                    textTrackCommentWorkPane.setText(_track.getCommentPickup());
373                    textTrackCommentWorkPane
374                            .setForeground(TrainCommon.getTextColor(_track.getCommentPickupWithColor()));
375                } else if (!pickupCar && setoutCar) {
376                    textTrackCommentWorkPane.setText(_track.getCommentSetout());
377                    textTrackCommentWorkPane
378                            .setForeground(TrainCommon.getTextColor(_track.getCommentSetoutWithColor()));
379                } else if (pickupCar && setoutCar) {
380                    textTrackCommentWorkPane.setText(_track.getCommentBoth());
381                    textTrackCommentWorkPane.setForeground(TrainCommon.getTextColor(_track.getCommentBothWithColor()));
382                }
383                textTrackCommentWorkPane.setVisible(!textTrackCommentWorkPane.getText().isEmpty());
384
385                // only show panels that have work
386                if (pickupCar || pickupEngine) {
387                    pTrain.add(pPickups);
388                }
389                if (setoutCar || setoutEngine) {
390                    pTrain.add(pSetouts);
391                }
392                if (localCar) {
393                    pTrain.add(pLocal);
394                }
395
396                pTrackPane.validate();
397                pTrain.setMaximumSize(new Dimension(2000, pTrain.getHeight()));
398                pTrain.revalidate();
399            }
400            // now do car holds
401            // we only need the cars on this track
402            List<Car> rsList = carManager.getByTrainList();
403            List<Car> carList = new ArrayList<Car>();
404            for (Car rs : rsList) {
405                if (!rs.getSplitTrackName().equals(_track.getSplitName()) || rs.getRouteLocation() != null)
406                    continue;
407                carList.add(rs);
408            }
409            JPanel pHoldCars = new JPanel();
410            pHoldCars.setLayout(new BoxLayout(pHoldCars, BoxLayout.Y_AXIS));
411            pHoldCars.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("HoldCars")));
412            for (Car car : carList) {
413                String text;
414                if (car.isUtility()) {
415                    String s = trainCommon.pickupUtilityCars(carList, car, !IS_MANIFEST,
416                            !TrainCommon.IS_TWO_COLUMN_TRACK);
417                    if (s == null)
418                        continue;
419                    text = TrainSwitchListText.getStringHoldCar().split("\\{")[0] + s.trim();
420                } else {
421                    text = MessageFormat.format(TrainSwitchListText.getStringHoldCar(),
422                            new Object[]{
423                                    TrainCommon.padAndTruncateIfNeeded(car.getRoadName(),
424                                            InstanceManager.getDefault(CarRoads.class).getMaxNameLength()),
425                                    TrainCommon.padAndTruncateIfNeeded(TrainCommon.splitString(car.getNumber()),
426                                            Control.max_len_string_print_road_number),
427                                    TrainCommon.padAndTruncateIfNeeded(car.getTypeName().split(TrainCommon.HYPHEN)[0],
428                                            InstanceManager.getDefault(CarTypes.class).getMaxNameLength()),
429                                    TrainCommon.padAndTruncateIfNeeded(car.getLength() + Setup.getLengthUnitAbv(),
430                                            Control.max_len_string_length_name),
431                                    TrainCommon.padAndTruncateIfNeeded(car.getLoadName(),
432                                            InstanceManager.getDefault(CarLoads.class).getMaxNameLength()),
433                                    TrainCommon.padAndTruncateIfNeeded(_track.getName(),
434                                            InstanceManager.getDefault(LocationManager.class).getMaxTrackNameLength()),
435                                    TrainCommon.padAndTruncateIfNeeded(car.getColorName(),
436                                            InstanceManager.getDefault(CarColors.class).getMaxNameLength())});
437
438                }
439                JCheckBox checkBox = new JCheckBox(text);
440                setCheckBoxFont(checkBox, Color.black);
441                pHoldCars.add(checkBox);
442                checkBoxes.put(car.getId(), checkBox);
443                pTrack.add(pHoldCars);
444            }
445            pTrackPane.validate();
446            pHoldCars.setMaximumSize(new Dimension(2000, pHoldCars.getHeight()));
447            pHoldCars.revalidate();
448        } else {
449            pTrackPane.setBorder(BorderFactory.createTitledBorder(""));
450            textTrackCommentPane.setVisible(false);
451        }
452    }
453
454    private JTextPane getTrackWorkCommentPane() {
455        JTextPane textTrackCommentWorkPane = new JTextPane();
456        textTrackCommentWorkPane.setBorder(BorderFactory.createTitledBorder(Bundle.getMessage("Comment")));
457        textTrackCommentWorkPane.setBackground(null);
458        textTrackCommentWorkPane.setEditable(false);
459        return textTrackCommentWorkPane;
460    }
461
462    private void updateTrackComboBox() {
463        Object selectedItem = trackComboBox.getSelectedItem();
464        trackComboBox.setVisible(false); // used as a flag to ignore updates
465        if (_location != null) {
466            _location.updateComboBox(trackComboBox);
467        }
468        filterTrackComboBox(trackComboBox);
469        if (selectedItem != null) {
470            trackComboBox.setSelectedItem(selectedItem);
471        }
472        trackComboBox.setVisible(true);
473    }
474
475    // remove tracks with "similar" names
476    private void filterTrackComboBox(JComboBox<Track> box) {
477        String trackName = NONE;
478        for (int i = 0; i < box.getItemCount(); i++) {
479            Track track = box.getItemAt(i);
480            if (track != null) {
481                if (track.getSplitName().equals(trackName)) {
482                    box.removeItemAt(i--);
483                }
484                trackName = track.getSplitName();
485            }
486        }
487    }
488
489    @Override
490    public void dispose() {
491        if (_location != null)
492            _location.removePropertyChangeListener(this);
493        removePropertyChangeListerners();
494    }
495
496    @Override
497    public void propertyChange(java.beans.PropertyChangeEvent e) {
498        if (Control.SHOW_PROPERTY) {
499            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(),
500                    e.getNewValue());
501        }
502        if (e.getPropertyName().equals(RollingStock.ROUTE_LOCATION_CHANGED_PROPERTY)) {
503            update();
504        }
505        if (e.getPropertyName().equals(Location.TRACK_LISTLENGTH_CHANGED_PROPERTY)) {
506            updateTrackComboBox();
507        }
508    }
509
510    private static final Logger log = LoggerFactory.getLogger(YardmasterByTrackPanel.class);
511}