001package jmri.jmrit.timetable.swing;
002
003import java.awt.*;
004import java.awt.event.*;
005import java.io.File;
006import java.io.IOException;
007import java.text.NumberFormat;
008import java.text.ParseException;
009import java.time.LocalTime;
010import java.time.format.DateTimeFormatter;
011import java.util.ArrayList;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Locale;
015
016import javax.swing.*;
017import javax.swing.colorchooser.AbstractColorChooserPanel;
018import javax.swing.event.ChangeEvent;
019import javax.swing.event.ChangeListener;
020import javax.swing.event.TreeSelectionEvent;
021import javax.swing.event.TreeSelectionListener;
022import javax.swing.filechooser.FileNameExtensionFilter;
023import javax.swing.tree.*;
024
025import jmri.InstanceManager;
026import jmri.Scale;
027import jmri.ScaleManager;
028import jmri.jmrit.operations.trains.tools.ExportTimetable;
029import jmri.jmrit.timetable.*;
030import jmri.jmrit.timetable.configurexml.TimeTableXml;
031import jmri.util.JmriJFrame;
032import jmri.util.swing.SplitButtonColorChooserPanel;
033import jmri.util.swing.JmriJOptionPane;
034
035/**
036 * Create and maintain timetables.
037 * <p>
038 * A timetable describes the layout and trains along with the times that each train should be at specified locations.
039 *
040 *   Logical Schema
041 * Layout
042 *    Train Types
043 *    Segments
044 *        Stations
045 *    Schedules
046 *        Trains
047 *           Stops
048 *
049 * @author Dave Sand Copyright (c) 2018
050 */
051public class TimeTableFrame extends jmri.util.JmriJFrame {
052
053    public static final String EMPTY_GRID = "EmptyGrid";
054
055    public TimeTableFrame() {
056    }
057
058    public TimeTableFrame(String tt) {
059        super(true, true);
060        setTitle(Bundle.getMessage("TitleTimeTable"));  // NOI18N
061        InstanceManager.setDefault(TimeTableFrame.class, this);
062        _dataMgr = TimeTableDataManager.getDataManager();
063        buildComponents();
064        createFrame();
065        createMenu();
066        setEditMode(false);
067        setShowReminder(false);
068    }
069
070    TimeTableDataManager _dataMgr;
071    boolean _isDirty = false;
072    boolean _showTrainTimes = false;
073    boolean _twoPage = false;
074
075    // ------------ Tree variables ------------
076    JTree _timetableTree;
077    DefaultTreeModel _timetableModel;
078    DefaultMutableTreeNode _timetableRoot;
079    TreeSelectionListener _timetableListener;
080    TreePath _curTreePath = null;
081
082    // ------------ Tree components ------------
083    TimeTableTreeNode _layoutNode = null;
084    TimeTableTreeNode _typeHead = null;
085    TimeTableTreeNode _typeNode = null;
086    TimeTableTreeNode _segmentHead = null;
087    TimeTableTreeNode _segmentNode = null;
088    TimeTableTreeNode _stationNode = null;
089    TimeTableTreeNode _scheduleHead = null;
090    TimeTableTreeNode _scheduleNode = null;
091    TimeTableTreeNode _trainNode = null;
092    TimeTableTreeNode _stopNode = null;
093    TimeTableTreeNode _leafNode = null;
094
095    // ------------ Current tree node variables ------------
096    TimeTableTreeNode _curNode = null;
097    int _curNodeId = 0;
098    String _curNodeType = null;
099    String _curNodeText = null;
100    int _curNodeRow = -1;
101
102    // ------------ Edit detail components ------------
103    JPanel _detailGrid = new JPanel();
104    JPanel _detailFooter = new JPanel();
105    JPanel _gridPanel;  // Child of _detailGrid, contains the current grid labels and fields
106    boolean _editActive = false;
107    JButton _cancelAction;
108    JButton _updateAction;
109
110    // Layout
111    JTextField _editLayoutName;
112    JComboBox<Scale> _editScale;
113    JTextField _editFastClock;
114    JTextField _editThrottles;
115    JCheckBox _editMetric;
116    private JLabel _showScaleMK;
117
118    // TrainType
119    JTextField _editTrainTypeName;
120    JColorChooser _editTrainTypeColor;
121
122    // Segment
123    JTextField _editSegmentName;
124
125    // Station
126    JTextField _editStationName;
127    JTextField _editDistance;
128    JCheckBox _editDoubleTrack;
129    JSpinner _editSidings;
130    JSpinner _editStaging;
131
132    // Schedule
133    JTextField _editScheduleName;
134    JTextField _editEffDate;
135    JSpinner _editStartHour;
136    JSpinner _editDuration;
137
138    // Train
139    JTextField _editTrainName;
140    JTextField _editTrainDesc;
141    JComboBox<TrainType> _editTrainType;
142    JTextField _editDefaultSpeed;
143    JTextField _editTrainStartTime;
144    JSpinner _editThrottle;
145    JTextArea _editTrainNotes;
146    JLabel _showRouteDuration;
147
148    // Stop
149    JLabel _showStopSeq;
150    JComboBox<TimeTableDataManager.SegmentStation> _editStopStation;
151    JTextField _editStopDuration;
152    JTextField _editNextSpeed;
153    JSpinner _editStagingTrack;
154    JTextArea _editStopNotes;
155    JLabel _showArriveTime;
156    JLabel _showDepartTime;
157
158    // ------------ Button bar components ------------
159    JPanel _leftButtonBar;
160    JPanel _addButtonPanel;
161    JPanel _duplicateButtonPanel;
162    JPanel _copyButtonPanel;
163    JPanel _deleteButtonPanel;
164    JPanel _moveButtonPanel;
165    JPanel _graphButtonPanel;
166    JButton _addButton = new JButton();
167    JButton _duplicateButton = new JButton();
168    JButton _copyButton = new JButton();
169    JButton _deleteButton = new JButton();
170    JButton _displayButton = new JButton();
171    JButton _printButton = new JButton();
172    JButton _saveButton = new JButton();
173
174    // ------------ Create Panel and components ------------
175
176    /**
177     * Create the main Timetable Window
178     * The left side contains the timetable tree.
179     * The right side contains the current edit grid.
180     */
181    private void createFrame() {
182        Container contentPane = getContentPane();
183        contentPane.setLayout(new BorderLayout());
184
185        // ------------ Body - tree (left side) ------------
186        JTree treeContent = buildTree();
187        JScrollPane treeScroll = new JScrollPane(treeContent);
188
189        // ------------ Body - detail (right side) ------------
190        JPanel detailPane = new JPanel();
191        detailPane.setBorder(BorderFactory.createMatteBorder(0, 2, 0, 0, Color.DARK_GRAY));
192        detailPane.setLayout(new BoxLayout(detailPane, BoxLayout.Y_AXIS));
193
194        // ------------ Edit Detail Panel ------------
195        makeDetailGrid(EMPTY_GRID);  // NOI18N
196
197        JPanel panel = new JPanel();
198        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
199
200        _cancelAction = new JButton(Bundle.getMessage("ButtonCancel"));  // NOI18N
201        _cancelAction.setToolTipText(Bundle.getMessage("HintCancelButton"));  // NOI18N
202        panel.add(_cancelAction);
203        _cancelAction.addActionListener((ActionEvent e) -> cancelPressed());
204        panel.add(Box.createHorizontalStrut(10));
205
206        _updateAction = new JButton(Bundle.getMessage("ButtonUpdate"));  // NOI18N
207        _updateAction.setToolTipText(Bundle.getMessage("HintUpdateButton"));  // NOI18N
208        panel.add(_updateAction);
209        _updateAction.addActionListener((ActionEvent e) -> updatePressed());
210        _detailFooter.add(panel);
211
212        JPanel detailEdit = new JPanel(new BorderLayout());
213        detailEdit.add(_detailGrid, BorderLayout.NORTH);
214        detailEdit.add(_detailFooter, BorderLayout.SOUTH);
215        detailPane.add(detailEdit);
216
217        JSplitPane bodyPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, treeScroll, detailPane);
218        bodyPane.setDividerSize(10);
219        bodyPane.setResizeWeight(.35);
220        bodyPane.setOneTouchExpandable(true);
221        contentPane.add(bodyPane);
222
223        // ------------ Footer ------------
224        JPanel footer = new JPanel(new BorderLayout());
225        _leftButtonBar = new JPanel();
226
227        // ------------ Add Button ------------
228        _addButton = new JButton(Bundle.getMessage("AddLayoutButtonText"));    // NOI18N
229        _addButton.setToolTipText(Bundle.getMessage("HintAddButton"));       // NOI18N
230        _addButton.addActionListener(new ActionListener() {
231            @Override
232            public void actionPerformed(ActionEvent e) {
233                addPressed();
234            }
235        });
236        _addButtonPanel = new JPanel();
237        _addButtonPanel.add(_addButton);
238        _leftButtonBar.add(_addButtonPanel);
239
240        // ------------ Duplicate Button ------------
241        _duplicateButton = new JButton(Bundle.getMessage("DuplicateLayoutButtonText"));    // NOI18N
242        _duplicateButton.setToolTipText(Bundle.getMessage("HintDuplicateButton"));       // NOI18N
243        _duplicateButton.addActionListener(new ActionListener() {
244            @Override
245            public void actionPerformed(ActionEvent e) {
246                duplicatePressed();
247            }
248        });
249        _duplicateButtonPanel = new JPanel();
250        _duplicateButtonPanel.add(_duplicateButton);
251        _leftButtonBar.add(_duplicateButtonPanel);
252
253        // ------------ Copy Button ------------
254        _copyButton = new JButton(Bundle.getMessage("CopyStopsButton"));    // NOI18N
255        _copyButton.setToolTipText(Bundle.getMessage("HintCopyButton"));       // NOI18N
256        _copyButton.addActionListener(new ActionListener() {
257            @Override
258            public void actionPerformed(ActionEvent e) {
259                copyPressed();
260            }
261        });
262        _copyButtonPanel = new JPanel();
263        _copyButtonPanel.add(_copyButton);
264        _leftButtonBar.add(_copyButtonPanel);
265
266        // ------------ Delete Button ------------
267        _deleteButton = new JButton(Bundle.getMessage("DeleteLayoutButtonText")); // NOI18N
268        _deleteButton.setToolTipText(Bundle.getMessage("HintDeleteButton"));    // NOI18N
269        _deleteButton.addActionListener(new ActionListener() {
270            @Override
271            public void actionPerformed(ActionEvent e) {
272                deletePressed();
273            }
274        });
275        _deleteButtonPanel = new JPanel();
276        _deleteButtonPanel.add(_deleteButton);
277        _deleteButtonPanel.setVisible(false);
278        _leftButtonBar.add(_deleteButtonPanel);
279
280        // ------------ Move Buttons ------------
281        JLabel moveLabel = new JLabel(Bundle.getMessage("LabelMove"));      // NOI18N
282
283        JButton upButton = new JButton(Bundle.getMessage("ButtonUp"));      // NOI18N
284        upButton.setToolTipText(Bundle.getMessage("HintUpButton"));         // NOI18N
285        JButton downButton = new JButton(Bundle.getMessage("ButtonDown"));  // NOI18N
286        downButton.setToolTipText(Bundle.getMessage("HintDownButton"));     // NOI18N
287
288        upButton.addActionListener(new ActionListener() {
289            @Override
290            public void actionPerformed(ActionEvent e) {
291                downButton.setEnabled(false);
292                upButton.setEnabled(false);
293                upPressed();
294            }
295        });
296
297        downButton.addActionListener(new ActionListener() {
298            @Override
299            public void actionPerformed(ActionEvent e) {
300                upButton.setEnabled(false);
301                downButton.setEnabled(false);
302                downPressed();
303            }
304        });
305
306        _moveButtonPanel = new JPanel();
307        _moveButtonPanel.add(moveLabel);
308        _moveButtonPanel.add(upButton);
309        _moveButtonPanel.add(new JLabel("|"));
310        _moveButtonPanel.add(downButton);
311        _moveButtonPanel.setVisible(false);
312        _leftButtonBar.add(_moveButtonPanel);
313
314        // ------------ Graph Buttons ------------
315        JLabel graphLabel = new JLabel(Bundle.getMessage("LabelGraph"));      // NOI18N
316
317        _displayButton = new JButton(Bundle.getMessage("ButtonDisplay"));  // NOI18N
318        _displayButton.setToolTipText(Bundle.getMessage("HintDisplayButton"));     // NOI18N
319        _displayButton.addActionListener(new ActionListener() {
320            @Override
321            public void actionPerformed(ActionEvent e) {
322                graphPressed("Display");  // NOI18N
323            }
324        });
325
326        _printButton = new JButton(Bundle.getMessage("ButtonPrint"));  // NOI18N
327        _printButton.setToolTipText(Bundle.getMessage("HintPrintButton"));     // NOI18N
328        _printButton.addActionListener(new ActionListener() {
329            @Override
330            public void actionPerformed(ActionEvent e) {
331                graphPressed("Print");  // NOI18N
332            }
333        });
334
335        _graphButtonPanel = new JPanel();
336        _graphButtonPanel.add(graphLabel);
337        _graphButtonPanel.add(_displayButton);
338        _graphButtonPanel.add(new JLabel("|"));
339        _graphButtonPanel.add(_printButton);
340        _leftButtonBar.add(_graphButtonPanel);
341
342        footer.add(_leftButtonBar, BorderLayout.WEST);
343        JPanel rightButtonBar = new JPanel();
344
345        // ------------ Save Button ------------
346        _saveButton = new JButton(Bundle.getMessage("ButtonSave"));  // NOI18N
347        _saveButton.setToolTipText(Bundle.getMessage("HintSaveButton"));     // NOI18N
348        _saveButton.addActionListener(new ActionListener() {
349            @Override
350            public void actionPerformed(ActionEvent e) {
351                savePressed();
352            }
353        });
354        JPanel saveButtonPanel = new JPanel();
355        saveButtonPanel.add(_saveButton);
356        rightButtonBar.add(saveButtonPanel);
357
358        // ------------ Done Button ------------
359        JButton doneButton = new JButton(Bundle.getMessage("ButtonDone"));  // NOI18N
360        doneButton.setToolTipText(Bundle.getMessage("HintDoneButton"));     // NOI18N
361        doneButton.addActionListener(new ActionListener() {
362            @Override
363            public void actionPerformed(ActionEvent e) {
364                donePressed();
365            }
366        });
367        JPanel doneButtonPanel = new JPanel();
368        doneButtonPanel.add(doneButton);
369        rightButtonBar.add(doneButtonPanel);
370
371        footer.add(rightButtonBar, BorderLayout.EAST);
372        contentPane.add(footer, BorderLayout.SOUTH);
373
374        addWindowListener(new java.awt.event.WindowAdapter() {
375            @Override
376            public void windowClosing(java.awt.event.WindowEvent e) {
377                donePressed();
378            }
379        });
380        setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
381
382        pack();
383        _addButtonPanel.setVisible(false);
384        _duplicateButtonPanel.setVisible(false);
385        _copyButtonPanel.setVisible(false);
386        _deleteButtonPanel.setVisible(false);
387        _graphButtonPanel.setVisible(false);
388    }
389
390    /**
391     * Create a Options/Tools menu.
392     * - Option: Show train times on the graph.
393     * - Option: Enable two page graph printing.
394     * - Tool: Import a SchedGen data file.
395     * - Tool: Import a CSV data file.
396     * - Tool: Export a CSV data file.
397     * Include the standard Windows and Help menu bar items.
398     */
399    void createMenu() {
400        _showTrainTimes = InstanceManager.getDefault(jmri.UserPreferencesManager.class).
401                getSimplePreferenceState("jmri.jmrit.timetable:TrainTimes");      // NOI18N
402
403        JCheckBoxMenuItem trainTime = new JCheckBoxMenuItem(Bundle.getMessage("MenuTrainTimes"));  // NOI18N
404        trainTime.setSelected(_showTrainTimes);
405        trainTime.addActionListener((ActionEvent event) -> {
406            _showTrainTimes = trainTime.isSelected();
407            InstanceManager.getDefault(jmri.UserPreferencesManager.class).
408                    setSimplePreferenceState("jmri.jmrit.timetable:TrainTimes", _showTrainTimes);  // NOI18N
409        });
410
411        _twoPage = InstanceManager.getDefault(jmri.UserPreferencesManager.class).
412                getSimplePreferenceState("jmri.jmrit.timetable:TwoPage");      // NOI18N
413
414        JCheckBoxMenuItem twoPage = new JCheckBoxMenuItem(Bundle.getMessage("MenuTwoPage"));  // NOI18N
415        twoPage.setSelected(_twoPage);
416        twoPage.addActionListener((ActionEvent event) -> {
417            _twoPage = twoPage.isSelected();
418            InstanceManager.getDefault(jmri.UserPreferencesManager.class).
419                    setSimplePreferenceState("jmri.jmrit.timetable:TwoPage", _twoPage);  // NOI18N
420        });
421
422        JMenuItem impsgn = new JMenuItem(Bundle.getMessage("MenuImportSgn"));  // NOI18N
423        impsgn.addActionListener((ActionEvent event) -> importPressed());
424
425        JMenuItem impcsv = new JMenuItem(Bundle.getMessage("MenuImportCsv"));  // NOI18N
426        impcsv.addActionListener((ActionEvent event) -> importCsvPressed());
427
428        JMenuItem impopr = new JMenuItem(Bundle.getMessage("MenuImportOperations"));  // NOI18N
429        impopr.addActionListener((ActionEvent event) -> importFromOperationsPressed());
430
431        JMenuItem expcsv = new JMenuItem(Bundle.getMessage("MenuExportCsv"));  // NOI18N
432        expcsv.addActionListener((ActionEvent event) -> exportCsvPressed());
433
434        JMenu ttMenu = new JMenu(Bundle.getMessage("MenuTimetable"));  // NOI18N
435        ttMenu.add(trainTime);
436        ttMenu.addSeparator();
437        ttMenu.add(twoPage);
438        ttMenu.addSeparator();
439        ttMenu.add(impsgn);
440        ttMenu.add(impcsv);
441        ttMenu.add(impopr);
442        ttMenu.add(expcsv);
443
444        JMenuBar menuBar = new JMenuBar();
445        menuBar.add(ttMenu);
446        setJMenuBar(menuBar);
447
448        //setup Help menu
449        addHelpMenu("html.tools.TimeTable", true);  // NOI18N
450    }
451
452    /**
453     * Initialize components.
454     * Add Focus and Change listeners to activate edit mode.
455     * Create the color selector for train types.
456     */
457    void buildComponents() {
458        // Layout
459        _editLayoutName = new JTextField(20);
460        _editScale = new JComboBox<>();
461        _editScale.addItemListener(layoutScaleItemEvent);
462        _editFastClock = new JTextField(5);
463        _editThrottles = new JTextField(5);
464        _editMetric = new JCheckBox();
465        _showScaleMK = new JLabel();
466
467        _editLayoutName.addFocusListener(detailFocusEvent);
468        _editScale.addFocusListener(detailFocusEvent);
469        _editFastClock.addFocusListener(detailFocusEvent);
470        _editThrottles.addFocusListener(detailFocusEvent);
471        _editMetric.addChangeListener(detailChangeEvent);
472
473        // TrainType
474        _editTrainTypeName = new JTextField(20);
475        _editTrainTypeColor = new JColorChooser(Color.BLACK);
476        _editTrainTypeColor.setPreviewPanel(new JPanel()); // remove the preview panel
477        AbstractColorChooserPanel[] editTypeColorPanels = {new SplitButtonColorChooserPanel()};
478        _editTrainTypeColor.setChooserPanels(editTypeColorPanels);
479
480        _editTrainTypeName.addFocusListener(detailFocusEvent);
481        _editTrainTypeColor.getSelectionModel().addChangeListener(detailChangeEvent);
482
483        // Segment
484        _editSegmentName = new JTextField(20);
485
486        _editSegmentName.addFocusListener(detailFocusEvent);
487
488        // Station
489        _editStationName = new JTextField(20);
490        _editDistance = new JTextField(5);
491        _editDoubleTrack = new JCheckBox();
492        _editSidings = new JSpinner(new SpinnerNumberModel(0, 0, null, 1));
493        _editStaging = new JSpinner(new SpinnerNumberModel(0, 0, null, 1));
494
495        _editStationName.addFocusListener(detailFocusEvent);
496        _editDistance.addFocusListener(detailFocusEvent);
497        _editDoubleTrack.addChangeListener(detailChangeEvent);
498        _editSidings.addChangeListener(detailChangeEvent);
499        _editStaging.addChangeListener(detailChangeEvent);
500
501        // Schedule
502        _editScheduleName = new JTextField(20);
503        _editEffDate = new JTextField(10);
504        _editStartHour = new JSpinner(new SpinnerNumberModel(0, 0, 23, 1));
505        _editDuration = new JSpinner(new SpinnerNumberModel(24, 1, 24, 1));
506
507        _editScheduleName.addFocusListener(detailFocusEvent);
508        _editEffDate.addFocusListener(detailFocusEvent);
509        _editStartHour.addChangeListener(detailChangeEvent);
510        _editDuration.addChangeListener(detailChangeEvent);
511
512        // Train
513        _editTrainName = new JTextField(10);
514        _editTrainDesc = new JTextField(20);
515        _editTrainType = new JComboBox<>();
516        _editDefaultSpeed = new JTextField(5);
517        _editTrainStartTime = new JTextField(5);
518        _editThrottle = new JSpinner(new SpinnerNumberModel(0, 0, null, 1));
519        _editTrainNotes = new JTextArea(4, 30);
520        _showRouteDuration = new JLabel();
521
522        _editTrainName.addFocusListener(detailFocusEvent);
523        _editTrainDesc.addFocusListener(detailFocusEvent);
524        _editTrainType.addFocusListener(detailFocusEvent);
525        _editDefaultSpeed.addFocusListener(detailFocusEvent);
526        _editTrainStartTime.addFocusListener(detailFocusEvent);
527        _editThrottle.addChangeListener(detailChangeEvent);
528        _editTrainNotes.addFocusListener(detailFocusEvent);
529
530        // Stop
531        _showStopSeq = new JLabel();
532        _editStopStation = new JComboBox<>();
533        _editStopDuration = new JTextField(5);
534        _editNextSpeed = new JTextField(5);
535        _editStagingTrack = new JSpinner(new SpinnerNumberModel(0, 0, null, 1));
536        _editStopNotes = new JTextArea(4, 30);
537        _showArriveTime = new JLabel();
538        _showDepartTime = new JLabel();
539
540        _editStopStation.addFocusListener(detailFocusEvent);
541        _editStopStation.addItemListener(stopStationItemEvent);
542        _editStopDuration.addFocusListener(detailFocusEvent);
543        _editNextSpeed.addFocusListener(detailFocusEvent);
544        _editStagingTrack.addChangeListener(detailChangeEvent);
545        _editStopNotes.addFocusListener(detailFocusEvent);
546    }
547
548    /**
549     * Enable edit mode.  Used for JTextFields and JComboBoxs.
550     */
551    transient FocusListener detailFocusEvent = new FocusListener() {
552        @Override
553        public void focusGained(FocusEvent e) {
554            if (!_editActive) {
555                setEditMode(true);
556            }
557        }
558
559        @Override
560        public void focusLost(FocusEvent e) {
561        }
562    };
563
564    /**
565     * Enable edit mode.  Used for JCheckBoxs, JSpinners and JColorChoosers.
566     */
567    transient ChangeListener detailChangeEvent = new ChangeListener() {
568        @Override
569        public void stateChanged(ChangeEvent e) {
570            if (!_editActive) {
571                setEditMode(true);
572            }
573        }
574    };
575
576    /**
577     * Change the max spinner value based on the station data.
578     * The number of staging tracks varies depending on the selected station.
579     */
580    transient ItemListener stopStationItemEvent = new ItemListener() {
581        @Override
582        public void itemStateChanged(ItemEvent e) {
583            if (e.getStateChange() == ItemEvent.SELECTED) {
584                TimeTableDataManager.SegmentStation segmentStation = (TimeTableDataManager.SegmentStation) e.getItem();
585                int stagingTracks = _dataMgr.getStation(segmentStation.getStationId()).getStaging();
586                Stop stop = _dataMgr.getStop(_curNodeId);
587                if (stop.getStagingTrack() <= stagingTracks) {
588                    _editStagingTrack.setModel(new SpinnerNumberModel(stop.getStagingTrack(), 0, stagingTracks, 1));
589                }
590            }
591        }
592    };
593
594    /**
595     * If the custom scale item is selected provide a dialog to set the scale ratio
596     */
597    transient ItemListener layoutScaleItemEvent = new ItemListener() {
598        @Override
599        public void itemStateChanged(ItemEvent e) {
600            if (e.getStateChange() == ItemEvent.SELECTED) {
601                if (_editScale.hasFocus()) {
602                    Scale scale = (Scale) _editScale.getSelectedItem();
603                    if (scale.getScaleName().equals("CUSTOM")) {  // NOI18N
604                        String ans = JmriJOptionPane.showInputDialog( _editScale,
605                                Bundle.getMessage("ScaleRatioChange"),  // NOI18N
606                                String.valueOf(scale.getScaleRatio())
607                                );
608                        if (ans != null) {
609                            try {
610                                double newRatio = Double.parseDouble(ans);
611                                scale.setScaleRatio(newRatio);
612                            } catch (java.lang.IllegalArgumentException
613                                    | java.beans.PropertyVetoException ex) {
614                                log.warn("Unable to change custom ratio: {}", ex.getMessage());  // NOI18N
615                                JmriJOptionPane.showMessageDialog( _editScale,
616                                        Bundle.getMessage("NumberFormatError", ans, "Custom ratio"),  // NOI18N
617                                        Bundle.getMessage("WarningTitle"),  // NOI18N
618                                        JmriJOptionPane.WARNING_MESSAGE);
619                                Layout layout = _dataMgr.getLayout(_curNodeId);
620                                _editScale.setSelectedItem(layout.getScale());
621                            }
622                        }
623                    }
624                }
625            }
626        }
627    };
628
629    // ------------ Create GridBag panels ------------
630
631    /**
632     * Build new GridBag content. The grid panel is hidden, emptied, re-built and
633     * made visible.
634     *
635     * @param gridType The type of grid to create
636     */
637    void makeDetailGrid(String gridType) {
638        _detailGrid.setVisible(false);
639        _detailGrid.removeAll();
640        _detailFooter.setVisible(true);
641
642        _gridPanel = new JPanel(new GridBagLayout());
643        GridBagConstraints c = new GridBagConstraints();
644        c.gridwidth = 1;
645        c.gridheight = 1;
646        c.ipadx = 5;
647
648        switch (gridType) {
649            case EMPTY_GRID:  // NOI18N
650                makeEmptyGrid(c);
651                _detailFooter.setVisible(false);
652                break;
653
654            case "Layout":  // NOI18N
655                makeLayoutGrid(c);
656                break;
657
658            case "TrainType":  // NOI18N
659                makeTrainTypeGrid(c);
660                break;
661
662            case "Segment":  // NOI18N
663                makeSegmentGrid(c);
664                break;
665
666            case "Station":  // NOI18N
667                makeStationGrid(c);
668                break;
669
670            case "Schedule":  // NOI18N
671                makeScheduleGrid(c);
672                break;
673
674            case "Train":  // NOI18N
675                makeTrainGrid(c);
676                break;
677
678            case "Stop":  // NOI18N
679                makeStopGrid(c);
680                break;
681
682            default:
683                log.warn("Invalid grid type: '{}'", gridType);  // NOI18N
684                makeEmptyGrid(c);
685        }
686
687        _detailGrid.add(_gridPanel);
688        _detailGrid.setVisible(true);
689    }
690
691    /**
692     * This grid is used when there are no edit grids required.
693     *
694     * @param c The constraints object used for the grid construction
695     */
696    void makeEmptyGrid(GridBagConstraints c) {
697        // Variable type box
698        c.gridy = 0;
699        c.gridx = 0;
700        c.anchor = java.awt.GridBagConstraints.CENTER;
701        JLabel rowLabel = new JLabel(Bundle.getMessage("LabelBlank"));  // NOI18N
702        _gridPanel.add(rowLabel, c);
703    }
704
705    /**
706     * This grid is used to edit Layout data.
707     *
708     * @param c The constraints object used for the grid construction
709     */
710    void makeLayoutGrid(GridBagConstraints c) {
711        makeGridLabel(0, "LabelLayoutName", "HintLayoutName", c);  // NOI18N
712        _gridPanel.add(_editLayoutName, c);
713
714        makeGridLabel(1, "LabelScale", "HintScale", c);  // NOI18N
715        _gridPanel.add(_editScale, c);
716
717        makeGridLabel(2, "LabelFastClock", "HintFastClock", c);  // NOI18N
718        _gridPanel.add(_editFastClock, c);
719
720        makeGridLabel(3, "LabelThrottles", "HintThrottles", c);  // NOI18N
721        _gridPanel.add(_editThrottles, c);
722
723        makeGridLabel(4, "LabelMetric", "HintMetric", c);  // NOI18N
724        _gridPanel.add(_editMetric, c);
725
726        makeGridLabel(5, "LabelScaleMK", "HintScaleMK", c);  // NOI18N
727        _gridPanel.add(_showScaleMK, c);
728    }
729
730    /**
731     * This grid is used to edit the Train Type data.
732     *
733     * @param c The constraints object used for the grid construction
734     */
735    void makeTrainTypeGrid(GridBagConstraints c) {
736        makeGridLabel(0, "LabelTrainTypeName", "HintTrainTypeName", c);  // NOI18N
737        _gridPanel.add(_editTrainTypeName, c);
738
739        makeGridLabel(1, "LabelTrainTypeColor", "HintTrainTypeColor", c);  // NOI18N
740        _gridPanel.add(_editTrainTypeColor, c);
741    }
742
743    /**
744     * This grid is used to edit the Segment data.
745     *
746     * @param c The constraints object used for the grid construction
747     */
748    void makeSegmentGrid(GridBagConstraints c) {
749        makeGridLabel(0, "LabelSegmentName", "HintSegmentName", c);  // NOI18N
750        _gridPanel.add(_editSegmentName, c);
751    }
752
753    /**
754     * This grid is used to edit the Station data.
755     *
756     * @param c The constraints object used for the grid construction
757     */
758    void makeStationGrid(GridBagConstraints c) {
759        makeGridLabel(0, "LabelStationName", "HintStationName", c);  // NOI18N
760        _gridPanel.add(_editStationName, c);
761
762        makeGridLabel(1, "LabelDistance", "HintDistance", c);  // NOI18N
763        _gridPanel.add(_editDistance, c);
764
765        makeGridLabel(2, "LabelDoubleTrack", "HintDoubleTrack", c);  // NOI18N
766        _gridPanel.add(_editDoubleTrack, c);
767
768        makeGridLabel(3, "LabelSidings", "HintSidings", c);  // NOI18N
769        _gridPanel.add(_editSidings, c);
770
771        makeGridLabel(4, "LabelStaging", "HintStaging", c);  // NOI18N
772        _gridPanel.add(_editStaging, c);
773    }
774
775    /**
776     * This grid is used to edit the Schedule data.
777     *
778     * @param c The constraints object used for the grid construction
779     */
780    void makeScheduleGrid(GridBagConstraints c) {
781        makeGridLabel(0, "LabelScheduleName", "HintScheduleName", c);  // NOI18N
782        _gridPanel.add(_editScheduleName, c);
783
784        makeGridLabel(1, "LabelEffDate", "HintEffDate", c);  // NOI18N
785        _gridPanel.add(_editEffDate, c);
786
787        makeGridLabel(2, "LabelStartHour", "HintStartHour", c);  // NOI18N
788        _gridPanel.add(_editStartHour, c);
789
790        makeGridLabel(3, "LabelDuration", "HintDuration", c);  // NOI18N
791        _gridPanel.add(_editDuration, c);
792    }
793
794    /**
795     * This grid is used to edit the Train data.
796     *
797     * @param c The constraints object used for the grid construction
798     */
799    void makeTrainGrid(GridBagConstraints c) {
800        makeGridLabel(0, "LabelTrainName", "HintTrainName", c);  // NOI18N
801        _gridPanel.add(_editTrainName, c);
802
803        makeGridLabel(1, "LabelTrainDesc", "HintTrainDesc", c);  // NOI18N
804        _gridPanel.add(_editTrainDesc, c);
805
806        makeGridLabel(2, "LabelTrainType", "HintTrainType", c);  // NOI18N
807        _gridPanel.add(_editTrainType, c);
808
809        makeGridLabel(3, "LabelDefaultSpeed", "HintDefaultSpeed", c);  // NOI18N
810        _gridPanel.add(_editDefaultSpeed, c);
811
812        makeGridLabel(4, "LabelTrainStartTime", "HintTrainStartTime", c);  // NOI18N
813        _gridPanel.add(_editTrainStartTime, c);
814
815        makeGridLabel(5, "LabelThrottle", "HintThrottle", c);  // NOI18N
816        _gridPanel.add(_editThrottle, c);
817
818        makeGridLabel(6, "LabelRouteDuration", "HintRouteDuration", c);  // NOI18N
819        _gridPanel.add(_showRouteDuration, c);
820
821        makeGridLabel(7, "LabelTrainNotes", "HintTrainNotes", c);  // NOI18N
822        _gridPanel.add(_editTrainNotes, c);
823    }
824
825    /**
826     * This grid is used to edit the Stop data.
827     *
828     * @param c The constraints object used for the grid construction
829     */
830    void makeStopGrid(GridBagConstraints c) {
831        makeGridLabel(0, "LabelStopSeq", "HintStopSeq", c);  // NOI18N
832        _gridPanel.add(_showStopSeq, c);
833
834        makeGridLabel(1, "LabelStopStation", "HintStopStation", c);  // NOI18N
835        _gridPanel.add(_editStopStation, c);
836
837        makeGridLabel(2, "LabelStopDuration", "HintStopDuration", c);  // NOI18N
838        _gridPanel.add(_editStopDuration, c);
839
840        makeGridLabel(3, "LabelNextSpeed", "HintNextSpeed", c);  // NOI18N
841        _gridPanel.add(_editNextSpeed, c);
842
843        makeGridLabel(4, "LabelStagingTrack", "HintStagingTrack", c);  // NOI18N
844        _gridPanel.add(_editStagingTrack, c);
845
846        makeGridLabel(5, "LabelArriveTime", "HintArriveTime", c);  // NOI18N
847        _gridPanel.add(_showArriveTime, c);
848
849        makeGridLabel(6, "LabelDepartTime", "HintDepartTime", c);  // NOI18N
850        _gridPanel.add(_showDepartTime, c);
851
852        makeGridLabel(7, "LabelStopNotes", "HintStopNotes", c);  // NOI18N
853        _gridPanel.add(_editStopNotes, c);
854    }
855
856    /**
857     * Create the label portion of a grid row.
858     * @param row The grid row number.
859     * @param label The bundle key for the label text.
860     * @param hint The bundle key for the label tool tip.
861     * @param c The grid bag contraints object.
862     */
863    void makeGridLabel(int row, String label, String hint, GridBagConstraints c) {
864        c.gridy = row;
865        c.gridx = 0;
866        c.anchor = java.awt.GridBagConstraints.EAST;
867        JLabel rowLabel = new JLabel(Bundle.getMessage(label));
868        rowLabel.setToolTipText(Bundle.getMessage(hint));
869        _gridPanel.add(rowLabel, c);
870        c.gridx = 1;
871        c.anchor = java.awt.GridBagConstraints.WEST;
872    }
873
874    // ------------ Process button bar and tree events ------------
875
876    /**
877     * Add new items.
878     */
879    void addPressed() {
880        switch (_curNodeType) {
881            case "Layout":     // NOI18N
882                addLayout();
883                break;
884
885            case "TrainTypes": // NOI18N
886                addTrainType();
887                break;
888
889            case "Segments":   // NOI18N
890                addSegment();
891                break;
892
893            case "Segment":    // NOI18N
894                addStation();
895                break;
896
897            case "Schedules":  // NOI18N
898                addSchedule();
899                break;
900
901            case "Schedule":   // NOI18N
902                addTrain();
903                break;
904
905            case "Train":      // NOI18N
906                addStop();
907                break;
908
909            default:
910                log.error("Add called for unsupported node type: '{}'", _curNodeType);  // NOI18N
911        }
912    }
913
914    /**
915     * Create a new Layout object with default values.
916     * Add the layout node and the TrainTypes, Segments and Schedules collection nodes.
917     */
918    void addLayout() {
919        Layout newLayout = new Layout();
920        setShowReminder(true);
921
922        // Build tree components
923        _curNode = new TimeTableTreeNode(newLayout.getLayoutName(), "Layout", newLayout.getLayoutId(), 0);    // NOI18N
924        _timetableRoot.add(_curNode);
925        _leafNode = new TimeTableTreeNode(buildNodeText("TrainTypes", null, 0), "TrainTypes", 0, 0);    // NOI18N
926        _curNode.add(_leafNode);
927        _leafNode = new TimeTableTreeNode(buildNodeText("Segments", null, 0), "Segments", 0, 0);    // NOI18N
928        _curNode.add(_leafNode);
929        _leafNode = new TimeTableTreeNode(buildNodeText("Schedules", null, 0), "Schedules", 0, 0);    // NOI18N
930        _curNode.add(_leafNode);
931        _timetableModel.nodeStructureChanged(_timetableRoot);
932
933        // Switch to new node
934        _timetableTree.setSelectionPath(new TreePath(_curNode.getPath()));
935    }
936
937    /**
938     * Create a new Train Type object.
939     * The default color is black.
940     */
941    void addTrainType() {
942        TimeTableTreeNode layoutNode = (TimeTableTreeNode) _curNode.getParent();
943        int layoutId = layoutNode.getId();
944        TrainType newType = new TrainType(layoutId);
945        setShowReminder(true);
946
947        // Build tree components
948        _leafNode = new TimeTableTreeNode(newType.getTypeName(), "TrainType", newType.getTypeId(), 0);    // NOI18N
949        _curNode.add(_leafNode);
950        _timetableModel.nodeStructureChanged(_curNode);
951
952        // Switch to new node
953        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
954    }
955
956    /**
957     * Create a new Segment object with default values.
958     */
959    void addSegment() {
960        TimeTableTreeNode layoutNode = (TimeTableTreeNode) _curNode.getParent();
961        int layoutId = layoutNode.getId();
962        Segment newSegment = new Segment(layoutId);
963        setShowReminder(true);
964
965        // Build tree components
966        _leafNode = new TimeTableTreeNode(newSegment.getSegmentName(), "Segment", newSegment.getSegmentId(), 0);    // NOI18N
967        _curNode.add(_leafNode);
968        _timetableModel.nodeStructureChanged(_curNode);
969
970        // Switch to new node
971        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
972    }
973
974    /**
975     * Create a new Station object with default values.
976     */
977    void addStation() {
978        Station newStation = new Station(_curNodeId);
979        setShowReminder(true);
980
981        // Build tree components
982        _leafNode = new TimeTableTreeNode(newStation.getStationName(), "Station", newStation.getStationId(), 0);    // NOI18N
983        _curNode.add(_leafNode);
984        _timetableModel.nodeStructureChanged(_curNode);
985
986        // Switch to new node
987        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
988    }
989
990    /**
991     * Create a new Schedule object with default values.
992     */
993    void addSchedule() {
994        TimeTableTreeNode layoutNode = (TimeTableTreeNode) _curNode.getParent();
995        int layoutId = layoutNode.getId();
996        Schedule newSchedule = new Schedule(layoutId);
997        setShowReminder(true);
998
999        // Build tree components
1000        _leafNode = new TimeTableTreeNode(newSchedule.getScheduleName(), "Schedule", newSchedule.getScheduleId(), 0);    // NOI18N
1001        _curNode.add(_leafNode);
1002        _timetableModel.nodeStructureChanged(_curNode);
1003
1004        // Switch to new node
1005        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1006    }
1007
1008    void addTrain() {
1009        Train newTrain = new Train(_curNodeId);
1010        newTrain.setStartTime(_dataMgr.getSchedule(_curNodeId).getStartHour() * 60);
1011        setShowReminder(true);
1012
1013        // Build tree components
1014        _leafNode = new TimeTableTreeNode(newTrain.getTrainName(), "Train", newTrain.getTrainId(), 0);    // NOI18N
1015        _curNode.add(_leafNode);
1016        _timetableModel.nodeStructureChanged(_curNode);
1017
1018        // Switch to new node
1019        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1020    }
1021
1022    void addStop() {
1023        int newSeq = _dataMgr.getStops(_curNodeId, 0, false).size();
1024        Stop newStop = new Stop(_curNodeId, newSeq + 1);
1025        setShowReminder(true);
1026
1027        // Build tree components
1028        _leafNode = new TimeTableTreeNode(String.valueOf(newSeq + 1), "Stop", newStop.getStopId(), newSeq + 1);    // NOI18N
1029        _curNode.add(_leafNode);
1030        _timetableModel.nodeStructureChanged(_curNode);
1031
1032        // Switch to new node
1033        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1034    }
1035
1036    /**
1037     * Duplicate selected item.
1038     */
1039    void duplicatePressed() {
1040        _dataMgr.setLockCalculate(true);
1041        switch (_curNodeType) {
1042            case "Layout":     // NOI18N
1043                duplicateLayout(_curNodeId);
1044                break;
1045
1046            case "TrainType": // NOI18N
1047                duplicateTrainType(0, _curNodeId, (TimeTableTreeNode) _curNode.getParent());
1048                break;
1049
1050            case "Segment":    // NOI18N
1051                duplicateSegment(0, _curNodeId,  (TimeTableTreeNode) _curNode.getParent());
1052                break;
1053
1054            case "Station":    // NOI18N
1055                duplicateStation(0, _curNodeId, (TimeTableTreeNode) _curNode.getParent());
1056                break;
1057
1058            case "Schedule":  // NOI18N
1059                duplicateSchedule(0, _curNodeId, (TimeTableTreeNode) _curNode.getParent());
1060                break;
1061
1062            case "Train":   // NOI18N
1063                duplicateTrain(0, _curNodeId, 0, (TimeTableTreeNode) _curNode.getParent());
1064                break;
1065
1066            case "Stop":      // NOI18N
1067                duplicateStop(0, _curNodeId, 0, 0, (TimeTableTreeNode) _curNode.getParent());
1068                break;
1069
1070            default:
1071                log.error("Duplicate called for unsupported node type: '{}'", _curNodeType);  // NOI18N
1072        }
1073        _dataMgr.setLockCalculate(false);
1074    }
1075
1076    // Trains have references to train types and stops have references to stations.
1077    // When a layout is copied, the references have to be changed to the copied element.
1078    private HashMap<Integer, Integer> typeMap = new HashMap<>();      // THe key is the source train type, the value is the destination train type.
1079    private HashMap<Integer, Integer> stationMap = new HashMap<>();   // THe key is the source layout stations, the value is the destination stations.
1080
1081    private boolean dupLayout = false;
1082
1083    /**
1084     * Create a copy of a layout.
1085     * @param layoutId The id of the layout to be duplicated.
1086     */
1087    void duplicateLayout(int layoutId) {
1088        dupLayout = true;
1089        Layout layout = _dataMgr.getLayout(layoutId);
1090        Layout newLayout = layout.getCopy();
1091        setShowReminder(true);
1092
1093        // Build tree components
1094        _curNode = new TimeTableTreeNode(newLayout.getLayoutName(), "Layout", newLayout.getLayoutId(), 0);    // NOI18N
1095        _timetableRoot.add(_curNode);
1096
1097        _leafNode = new TimeTableTreeNode(buildNodeText("TrainTypes", null, 0), "TrainTypes", 0, 0);    // NOI18N
1098        _curNode.add(_leafNode);
1099        var typesNode = _leafNode;
1100
1101        _leafNode = new TimeTableTreeNode(buildNodeText("Segments", null, 0), "Segments", 0, 0);    // NOI18N
1102        _curNode.add(_leafNode);
1103        var segmentsNode = _leafNode;
1104
1105        _leafNode = new TimeTableTreeNode(buildNodeText("Schedules", null, 0), "Schedules", 0, 0);    // NOI18N
1106        _curNode.add(_leafNode);
1107        var schedlulesNode = _leafNode;
1108
1109        _timetableModel.nodeStructureChanged(_timetableRoot);
1110
1111
1112        // Copy train types
1113        typeMap.clear();
1114        for (var type : _dataMgr.getTrainTypes(layoutId, true)) {
1115            duplicateTrainType(newLayout.getLayoutId(), type.getTypeId(), typesNode);
1116        }
1117
1118        // Copy segments
1119        stationMap.clear();
1120        for (var segment : _dataMgr.getSegments(layoutId, true)) {
1121            duplicateSegment(newLayout.getLayoutId(), segment.getSegmentId(), segmentsNode);
1122        }
1123
1124        // schedules
1125        for (var schedule : _dataMgr.getSchedules(layoutId, true)) {
1126            duplicateSchedule(newLayout.getLayoutId(), schedule.getScheduleId(), schedlulesNode);
1127        }
1128
1129        // Switch to new node
1130        _timetableTree.setSelectionPath(new TreePath(_curNode.getPath()));
1131
1132        dupLayout = false;
1133    }
1134
1135    /**
1136     * Create a copy of a train type.
1137     * @param layoutId The id for the parent layout.  Zero if within the same layout.
1138     * @param typeId The id of the train type to be duplicated.
1139     * @param typesNode The types node which will be parent for the new train type.
1140     */
1141    void duplicateTrainType(int layoutId, int typeId, TimeTableTreeNode typesNode) {
1142        TrainType type = _dataMgr.getTrainType(typeId);
1143        TrainType newType = type.getCopy(layoutId);
1144        setShowReminder(true);
1145
1146        // If part of duplicating a layout, create a type map entry.
1147        if (dupLayout) {
1148            typeMap.put(type.getTypeId(), newType.getTypeId());
1149        }
1150
1151        // Build tree components
1152        _leafNode = new TimeTableTreeNode(newType.getTypeName(), "TrainType", newType.getTypeId(), 0);    // NOI18N
1153        typesNode.add(_leafNode);
1154        _timetableModel.nodeStructureChanged(typesNode);
1155
1156        // Switch to new node
1157        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1158    }
1159
1160    /**
1161     * Create a copy of a segment.
1162     * @param layoutId The id for the parent layout.  Zero if within the same layout.
1163     * @param segmentId The id of the segment to be duplicated.
1164     * @param segmentsNode The segments node which will be parent for the new segment.
1165     */
1166    void duplicateSegment(int layoutId, int segmentId, TimeTableTreeNode segmentsNode) {
1167        Segment segment = _dataMgr.getSegment(segmentId);
1168        Segment newSegment = segment.getCopy(layoutId);
1169        setShowReminder(true);
1170
1171        // Build tree components
1172        _leafNode = new TimeTableTreeNode(newSegment.getSegmentName(), "Segment", newSegment.getSegmentId(), 0);    // NOI18N
1173        segmentsNode.add(_leafNode);
1174        _timetableModel.nodeStructureChanged(segmentsNode);
1175
1176        // Duplicate the stations using the stations from the orignal segment
1177        var segmentNode = _leafNode;
1178        for (var station : _dataMgr.getStations(segmentId, true)) {
1179            duplicateStation(newSegment.getSegmentId(), station.getStationId(), segmentNode);
1180        }
1181
1182        // Switch to new node
1183        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1184    }
1185
1186    /**
1187     * Create a copy of a station.
1188     * @param segmentId The id for the parent segment.  Zero if within the same segment.
1189     * @param stationId The id of the station to be duplicated.
1190     * @param segmentNode The segment node which will be parent for the new station.
1191     */
1192    void duplicateStation(int segmentId, int stationId, TimeTableTreeNode segmentNode) {
1193        Station station = _dataMgr.getStation(stationId);
1194        Station newStation = station.getCopy(segmentId);
1195        setShowReminder(true);
1196
1197        // If part of duplicating a layout, create a station map entry.
1198        if (dupLayout) {
1199            stationMap.put(station.getStationId(), newStation.getStationId());
1200        }
1201
1202        // Build tree components
1203        _leafNode = new TimeTableTreeNode(newStation.getStationName(), "Station", newStation.getStationId(), 0);    // NOI18N
1204        segmentNode.add(_leafNode);
1205        _timetableModel.nodeStructureChanged(segmentNode);
1206
1207        // Switch to new node
1208        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1209    }
1210
1211    /**
1212     * Create a copy of a schedule.
1213     * @param layoutId The id for the parent layout.  Zero if within the same layout.
1214     * @param scheduleId The id of the schedule to be duplicated.
1215     * @param schedulesNode The schedules node which will be parent for the new schedule.
1216     */
1217    void duplicateSchedule(int layoutId, int scheduleId, TimeTableTreeNode schedulesNode) {
1218        Schedule schedule = _dataMgr.getSchedule(scheduleId);
1219        Schedule newSchedule = schedule.getCopy(layoutId);
1220        setShowReminder(true);
1221
1222        // Build tree components
1223        _leafNode = new TimeTableTreeNode(buildNodeText("Schedule", newSchedule, 0), "Schedule", newSchedule.getScheduleId(), 0);    // NOI18N
1224        schedulesNode.add(_leafNode);
1225        _timetableModel.nodeStructureChanged(schedulesNode);
1226
1227        // Duplicate the trains using the trains from the orignal schedule
1228        TimeTableTreeNode scheduleNode = _leafNode;
1229        for (Train train : _dataMgr.getTrains(scheduleId, 0, true)) {
1230            duplicateTrain(newSchedule.getScheduleId(), train.getTrainId(), 0, scheduleNode);
1231        }
1232
1233        // Switch to new node
1234        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1235    }
1236
1237    /**
1238     * Create a copy of a train.
1239     * @param schedId The id for the parent schedule.  Zero if within the same schedule.
1240     * @param trainId The id of the train to be duplicated.
1241     * @param typeId The id of the train type.  If zero use the source train type.
1242     * @param schedNode The schedule node which will be parent for the new train.
1243     */
1244    void duplicateTrain(int schedId, int trainId, int typeId, TimeTableTreeNode schedNode ) {
1245        Train train = _dataMgr.getTrain(trainId);
1246        if (typeMap != null && typeMap.containsKey(train.getTypeId())) typeId = typeMap.get(train.getTypeId());
1247        Train newTrain = train.getCopy(schedId, typeId);
1248        setShowReminder(true);
1249
1250        // If part of duplicating a layout, update the type reference.
1251        if (dupLayout && typeMap.containsKey(train.getTypeId())) {
1252            newTrain.setTypeId(typeMap.get(train.getTypeId()));
1253        }
1254
1255        // Build tree components
1256        _leafNode = new TimeTableTreeNode(newTrain.toString(), "Train", newTrain.getTrainId(), 0);    // NOI18N
1257        schedNode.add(_leafNode);
1258        _timetableModel.nodeStructureChanged(schedNode);
1259
1260        // Duplicate the stops using the stops from the orignal train
1261        TimeTableTreeNode trainNode = _leafNode;
1262        for (Stop stop : _dataMgr.getStops(trainId, 0, true)) {
1263            duplicateStop(newTrain.getTrainId(), stop.getStopId(), 0, stop.getSeq(), trainNode);
1264        }
1265
1266        // Switch to new node
1267        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1268    }
1269
1270    /**
1271     * Create a copy of a stop.
1272     * @param trainId The id for the parent train.  Zero if within the same train.
1273     * @param stopId The id of the stop to be duplicated.
1274     * @param stationId The id of the station.  If zero use the source station.
1275     * @param seq The sequence for the new stop.  If zero calculate the next sequence number.
1276     * @param trainNode The train node which will be parent for the new stop.
1277     */
1278    void duplicateStop(int trainId, int stopId, int stationId, int seq, TimeTableTreeNode trainNode) {
1279        Stop stop = _dataMgr.getStop(stopId);
1280        if (seq == 0) seq = _dataMgr.getStops(stop.getTrainId(), 0, false).size() + 1;
1281        Stop newStop = stop.getCopy(trainId, stationId, seq);
1282        setShowReminder(true);
1283
1284        // If part of duplicating a layout, update the station reference.
1285        if (dupLayout && stationMap.containsKey(stop.getStationId())) {
1286            newStop.setStationId(stationMap.get(stop.getStationId()));
1287        }
1288
1289        // Build tree components
1290        _leafNode = new TimeTableTreeNode(buildNodeText("Stop", newStop, 0), "Stop", newStop.getStopId(), seq);    // NOI18N
1291        trainNode.add(_leafNode);
1292        _timetableModel.nodeStructureChanged(trainNode);
1293
1294        // Switch to new node
1295        _timetableTree.setSelectionPath(new TreePath(_leafNode.getPath()));
1296    }
1297
1298    /**
1299     * Copy the stops from an existing train.
1300     */
1301    void copyPressed() {
1302        var selectedTrain = copyTrainSelection();
1303        if (selectedTrain != null) {
1304            for (var stop : _dataMgr.getStops(selectedTrain.getTrainId(), 0, true)) {
1305                // Create stop
1306                var newSeq = _dataMgr.getStops(_curNodeId, 0, false).size();
1307                var newStop = new Stop(_curNodeId, newSeq + 1);
1308
1309                // Clone stop
1310                newStop.setStationId(stop.getStationId());
1311                newStop.setDuration(stop.getDuration());
1312                newStop.setNextSpeed(stop.getNextSpeed());
1313                newStop.setStagingTrack(stop.getStagingTrack());
1314                newStop.setStopNotes(stop.getStopNotes());
1315
1316                // Build tree content
1317                _leafNode = new TimeTableTreeNode(buildNodeText("Stop", newStop, 0),  // NOI18N
1318                         "Stop", newStop.getStopId(), newSeq + 1);    // NOI18N
1319                _curNode.add(_leafNode);
1320                _timetableModel.nodeStructureChanged(_curNode);
1321            }
1322        }
1323    }
1324
1325    /**
1326     * Select the train whose stops will be added to the new train.
1327     * @return the selected train or null if there is no selection made.
1328     */
1329    Train copyTrainSelection() {
1330        var newTrain = _dataMgr.getTrain(_curNodeId);
1331        var trainList = _dataMgr.getTrains(newTrain.getScheduleId(), 0, true);
1332        trainList.remove(newTrain);
1333
1334        var trainArray = new Train[trainList.size()];
1335        trainList.toArray(trainArray);
1336
1337        try {
1338            var icon = new ImageIcon(jmri.util.FileUtil.getProgramPath() + jmri.Application.getLogo());
1339            var choice = JmriJOptionPane.showInputDialog(
1340                    null,
1341                    Bundle.getMessage("LabelCopyStops"),  // NOI18N
1342                    Bundle.getMessage("TitleCopyStops"),  // NOI18N
1343                    JmriJOptionPane.QUESTION_MESSAGE,
1344                    icon,
1345                    trainArray,
1346                    null);
1347            return (Train) choice;
1348        } catch (HeadlessException ex) {
1349            return null;
1350        }
1351    }
1352
1353    /**
1354     * Set up the edit environment for the selected node Called from
1355     * {@link #treeRowSelected}. This takes the place of an actual button.
1356     */
1357    void editPressed() {
1358        switch (_curNodeType) {
1359            case "Layout":     // NOI18N
1360                editLayout();
1361                makeDetailGrid("Layout");  // NOI18N
1362                break;
1363
1364            case "TrainType":     // NOI18N
1365                editTrainType();
1366                makeDetailGrid("TrainType");  // NOI18N
1367                break;
1368
1369            case "Segment":     // NOI18N
1370                editSegment();
1371                makeDetailGrid("Segment");  // NOI18N
1372                break;
1373
1374            case "Station":     // NOI18N
1375                editStation();
1376                makeDetailGrid("Station");  // NOI18N
1377                break;
1378
1379            case "Schedule":     // NOI18N
1380                editSchedule();
1381                makeDetailGrid("Schedule");  // NOI18N
1382                break;
1383
1384            case "Train":     // NOI18N
1385                editTrain();
1386                makeDetailGrid("Train");  // NOI18N
1387                break;
1388
1389            case "Stop":     // NOI18N
1390                editStop();
1391                makeDetailGrid("Stop");  // NOI18N
1392                break;
1393
1394            default:
1395                log.error("Edit called for unsupported node type: '{}'", _curNodeType);  // NOI18N
1396        }
1397        setEditMode(false);
1398    }
1399
1400    /*
1401     * Set Layout edit variables and labels
1402     */
1403    void editLayout() {
1404        Layout layout = _dataMgr.getLayout(_curNodeId);
1405        _editLayoutName.setText(layout.getLayoutName());
1406        _editFastClock.setText(Integer.toString(layout.getFastClock()));
1407        _editThrottles.setText(Integer.toString(layout.getThrottles()));
1408        _editMetric.setSelected(layout.getMetric());
1409        String unitMeasure = (layout.getMetric())
1410                ? Bundle.getMessage("LabelRealMeters") // NOI18N
1411                : Bundle.getMessage("LabelRealFeet"); // NOI18N
1412        _showScaleMK.setText(String.format(Locale.getDefault(), "%.2f %s",
1413            layout.getScaleMK(), unitMeasure));
1414
1415        _editScale.removeAllItems();
1416        for (Scale scale : ScaleManager.getScales()) {
1417            _editScale.addItem(scale);
1418        }
1419        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(_editScale);
1420        _editScale.setSelectedItem(layout.getScale());
1421    }
1422
1423    /*
1424     * Set TrainType edit variables and labels
1425     */
1426    void editTrainType() {
1427        TrainType type = _dataMgr.getTrainType(_curNodeId);
1428        _editTrainTypeName.setText(type.getTypeName());
1429        _editTrainTypeColor.setColor(Color.decode(type.getTypeColor()));
1430    }
1431
1432    /*
1433     * Set Segment edit variables and labels
1434     */
1435    void editSegment() {
1436        Segment segment = _dataMgr.getSegment(_curNodeId);
1437        _editSegmentName.setText(segment.getSegmentName());
1438    }
1439
1440    /*
1441     * Set Station edit variables and labels
1442     */
1443    void editStation() {
1444        Station station = _dataMgr.getStation(_curNodeId);
1445        _editStationName.setText(station.getStationName());
1446        _editDistance.setText(NumberFormat.getNumberInstance().format(station.getDistance()));
1447        _editDoubleTrack.setSelected(station.getDoubleTrack());
1448        _editSidings.setValue(station.getSidings());
1449        _editStaging.setValue(station.getStaging());
1450    }
1451
1452    /*
1453     * Set Schedule edit variables and labels
1454     */
1455    void editSchedule() {
1456        Schedule schedule = _dataMgr.getSchedule(_curNodeId);
1457        _editScheduleName.setText(schedule.getScheduleName());
1458        _editEffDate.setText(schedule.getEffDate());
1459        _editStartHour.setValue(schedule.getStartHour());
1460        _editDuration.setValue(schedule.getDuration());
1461    }
1462
1463    /*
1464     * Set Train edit variables and labels
1465     */
1466    void editTrain() {
1467        Train train = _dataMgr.getTrain(_curNodeId);
1468        int layoutId = _dataMgr.getSchedule(train.getScheduleId()).getLayoutId();
1469
1470        _editTrainName.setText(train.getTrainName());
1471        _editTrainDesc.setText(train.getTrainDesc());
1472        _editDefaultSpeed.setText(Integer.toString(train.getDefaultSpeed()));
1473        _editTrainStartTime.setText(String.format("%02d:%02d",  // NOI18N
1474                train.getStartTime() / 60,
1475                train.getStartTime() % 60));
1476        _editThrottle.setModel(new SpinnerNumberModel(train.getThrottle(), 0, _dataMgr.getLayout(layoutId).getThrottles(), 1));
1477        _editTrainNotes.setText(train.getTrainNotes());
1478        _showRouteDuration.setText(String.format("%02d:%02d",  // NOI18N
1479                train.getRouteDuration() / 60,
1480                train.getRouteDuration() % 60));
1481
1482        _editTrainType.removeAllItems();
1483        for (TrainType type : _dataMgr.getTrainTypes(layoutId, true)) {
1484            _editTrainType.addItem(type);
1485        }
1486        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(_editTrainType);
1487        if (train.getTypeId() > 0) {
1488            _editTrainType.setSelectedItem(_dataMgr.getTrainType(train.getTypeId()));
1489        }
1490    }
1491
1492    /*
1493     * Set Stop edit variables and labels
1494     * The station combo box uses a data manager internal class to present
1495     * both the segment name and the station name.  This is needed since a station
1496     * can be in multiple segments.
1497     */
1498    void editStop() {
1499        Stop stop = _dataMgr.getStop(_curNodeId);
1500        Layout layout = _dataMgr.getLayoutForStop(_curNodeId);
1501
1502        _showStopSeq.setText(Integer.toString(stop.getSeq()));
1503        _editStopDuration.setText(Integer.toString(stop.getDuration()));
1504        _editNextSpeed.setText(Integer.toString(stop.getNextSpeed()));
1505        _editStopNotes.setText(stop.getStopNotes());
1506        _showArriveTime.setText(String.format("%02d:%02d",  // NOI18N
1507                stop.getArriveTime() / 60,
1508                stop.getArriveTime() % 60));
1509        _showDepartTime.setText(String.format("%02d:%02d",  // NOI18N
1510                stop.getDepartTime() / 60,
1511                stop.getDepartTime() % 60));
1512
1513        _editStopStation.removeAllItems();
1514        for (TimeTableDataManager.SegmentStation segmentStation : _dataMgr.getSegmentStations(layout.getLayoutId())) {
1515            _editStopStation.addItem(segmentStation);
1516            if (stop.getStationId() == segmentStation.getStationId()) {
1517                // This also triggers stopStationItemEvent which will set _editStagingTrack
1518                _editStopStation.setSelectedItem(segmentStation);
1519            }
1520        }
1521        jmri.util.swing.JComboBoxUtil.setupComboBoxMaxRows(_editStopStation);
1522        setMoveButtons();
1523    }
1524
1525    /**
1526     * Apply the updates to the current node.
1527     */
1528    void updatePressed() {
1529        switch (_curNodeType) {
1530            case "Layout":     // NOI18N
1531                updateLayout();
1532                break;
1533
1534            case "TrainType":     // NOI18N
1535                updateTrainType();
1536                break;
1537
1538            case "Segment":     // NOI18N
1539                updateSegment();
1540                break;
1541
1542            case "Station":     // NOI18N
1543                updateStation();
1544                break;
1545
1546            case "Schedule":     // NOI18N
1547                updateSchedule();
1548                break;
1549
1550            case "Train":     // NOI18N
1551                updateTrain();
1552                break;
1553
1554            case "Stop":     // NOI18N
1555                updateStop();
1556                break;
1557
1558            default:
1559                log.warn("Invalid update button press");  // NOI18N
1560        }
1561        setEditMode(false);
1562        _timetableTree.setSelectionPath(_curTreePath);
1563        _timetableTree.grabFocus();
1564        editPressed();
1565    }
1566
1567    /**
1568     * Update the layout information.
1569     * If the fast clock or metric values change, a recalc will be required.
1570     * The throttles value cannot be less than the highest throttle assigned to a train.
1571     */
1572    void updateLayout() {
1573        Layout layout = _dataMgr.getLayout(_curNodeId);
1574
1575        // Pre-validate and convert inputs
1576        String newName = _editLayoutName.getText().trim();
1577        Scale newScale = (Scale) _editScale.getSelectedItem();
1578        int newFastClock = parseNumber(_editFastClock, "fast clock");  // NOI18N
1579        if (newFastClock < 1) {
1580            newFastClock = layout.getFastClock();
1581        }
1582        int newThrottles = parseNumber(_editThrottles, "throttles");  // NOI18N
1583        if (newThrottles < 0) {
1584            newThrottles = layout.getThrottles();
1585        }
1586        boolean newMetric =_editMetric.isSelected();
1587
1588        boolean update = false;
1589        List<String> exceptionList = new ArrayList<>();
1590
1591        // Perform updates
1592        if (!layout.getLayoutName().equals(newName)) {
1593            layout.setLayoutName(newName);
1594            _curNode.setText(newName);
1595            _timetableModel.nodeChanged(_curNode);
1596            update = true;
1597        }
1598
1599        if (!layout.getScale().equals(newScale)) {
1600            try {
1601                layout.setScale(newScale);
1602                update = true;
1603            } catch (IllegalArgumentException ex) {
1604                exceptionList.add(ex.getMessage());
1605            }
1606        }
1607
1608        if (layout.getFastClock() != newFastClock) {
1609            try {
1610                layout.setFastClock(newFastClock);
1611                update = true;
1612            } catch (IllegalArgumentException ex) {
1613                exceptionList.add(ex.getMessage());
1614            }
1615        }
1616
1617        if (layout.getMetric() != newMetric) {
1618            try {
1619                layout.setMetric(newMetric);
1620                update = true;
1621            } catch (IllegalArgumentException ex) {
1622                exceptionList.add(ex.getMessage());
1623            }
1624        }
1625
1626        if (layout.getThrottles() != newThrottles) {
1627            try {
1628                layout.setThrottles(newThrottles);
1629                update = true;
1630            } catch (IllegalArgumentException ex) {
1631                exceptionList.add(ex.getMessage());
1632            }
1633        }
1634
1635        if (update) {
1636            setShowReminder(true);
1637        }
1638
1639        // Display exceptions if necessary
1640        if (!exceptionList.isEmpty()) {
1641            StringBuilder msg = new StringBuilder(Bundle.getMessage("LayoutUpdateErrors"));  // NOI18N
1642            for (String keyWord : exceptionList) {
1643                if (keyWord.startsWith(TimeTableDataManager.TIME_OUT_OF_RANGE)) {
1644                    String[] comps = keyWord.split("~");
1645                    msg.append(Bundle.getMessage(comps[0], comps[1], comps[2]));
1646                } else if (keyWord.startsWith(TimeTableDataManager.SCALE_NF)) {
1647                    String[] scaleMsg = keyWord.split("~");
1648                    msg.append(Bundle.getMessage(scaleMsg[0], scaleMsg[1]));
1649                } else {
1650                    msg.append(String.format("%n%s", Bundle.getMessage(keyWord)));
1651                    if (keyWord.equals(TimeTableDataManager.THROTTLES_IN_USE)) {
1652                        // Add the affected trains
1653                        for (Schedule schedule : _dataMgr.getSchedules(_curNodeId, true)) {
1654                            for (Train train : _dataMgr.getTrains(schedule.getScheduleId(), 0, true)) {
1655                                if (train.getThrottle() > newThrottles) {
1656                                    msg.append(String.format("%n      %s [ %d ]", train.getTrainName(), train.getThrottle()));
1657                                }
1658                            }
1659                        }
1660                    }
1661                }
1662            }
1663            JmriJOptionPane.showMessageDialog(this,
1664                    msg.toString(),
1665                    Bundle.getMessage("WarningTitle"),  // NOI18N
1666                    JmriJOptionPane.WARNING_MESSAGE);
1667        }
1668    }
1669
1670    /**
1671     * Update the train type information.
1672     */
1673    void updateTrainType() {
1674        TrainType type = _dataMgr.getTrainType(_curNodeId);
1675
1676        String newName = _editTrainTypeName.getText().trim();
1677        Color newColor = _editTrainTypeColor.getColor();
1678        String newColorHex = jmri.util.ColorUtil.colorToHexString(newColor);
1679
1680        boolean update = false;
1681
1682        if (!type.getTypeName().equals(newName)) {
1683            type.setTypeName(newName);
1684            _curNode.setText(newName);
1685            update = true;
1686        }
1687        if (!type.getTypeColor().equals(newColorHex)) {
1688            type.setTypeColor(newColorHex);
1689            update = true;
1690        }
1691        _timetableModel.nodeChanged(_curNode);
1692
1693        if (update) {
1694            setShowReminder(true);
1695        }
1696    }
1697
1698    /**
1699     * Update the segment information.
1700     */
1701    void updateSegment() {
1702        String newName = _editSegmentName.getText().trim();
1703
1704        Segment segment = _dataMgr.getSegment(_curNodeId);
1705        if (!segment.getSegmentName().equals(newName)) {
1706            segment.setSegmentName(newName);
1707            _curNode.setText(newName);
1708            setShowReminder(true);
1709        }
1710        _timetableModel.nodeChanged(_curNode);
1711    }
1712
1713    /**
1714     * Update the station information.
1715     * The staging track value cannot be less than any train references.
1716     */
1717    void updateStation() {
1718        Station station = _dataMgr.getStation(_curNodeId);
1719
1720        // Pre-validate and convert inputs
1721        String newName = _editStationName.getText().trim();
1722        double newDistance;
1723        try {
1724            newDistance = NumberFormat.getNumberInstance().parse(_editDistance.getText()).floatValue();
1725        } catch (NumberFormatException | ParseException ex) {
1726            log.warn("'{}' is not a valid number for {}", _editDistance.getText(), "station distance");  // NOI18N
1727            JmriJOptionPane.showMessageDialog(this,
1728                    Bundle.getMessage("NumberFormatError", _editDistance.getText(), "station distance"),  // NOI18N
1729                    Bundle.getMessage("WarningTitle"),  // NOI18N
1730                    JmriJOptionPane.WARNING_MESSAGE);
1731            newDistance = station.getDistance();
1732        }
1733        boolean newDoubleTrack =_editDoubleTrack.isSelected();
1734        int newSidings = (int) _editSidings.getValue();
1735        int newStaging = (int) _editStaging.getValue();
1736
1737        boolean update = false;
1738        List<String> exceptionList = new ArrayList<>();
1739
1740        // Perform updates
1741        if (!station.getStationName().equals(newName)) {
1742            station.setStationName(newName);
1743            _curNode.setText(newName);
1744            _timetableModel.nodeChanged(_curNode);
1745            update = true;
1746        }
1747
1748        if (newDistance < 0.0) {
1749            newDistance = station.getDistance();
1750        }
1751        if (Math.abs(station.getDistance() - newDistance) > .01 ) {
1752            try {
1753                station.setDistance(newDistance);
1754                update = true;
1755            } catch (IllegalArgumentException ex) {
1756                exceptionList.add(ex.getMessage());
1757            }
1758        }
1759
1760        if (station.getDoubleTrack() != newDoubleTrack) {
1761            station.setDoubleTrack(newDoubleTrack);
1762            update = true;
1763        }
1764
1765        if (station.getSidings() != newSidings) {
1766            station.setSidings(newSidings);
1767            update = true;
1768        }
1769
1770        if (station.getStaging() != newStaging) {
1771            try {
1772                station.setStaging(newStaging);
1773                update = true;
1774            } catch (IllegalArgumentException ex) {
1775                exceptionList.add(ex.getMessage());
1776            }
1777        }
1778
1779        if (update) {
1780            setShowReminder(true);
1781        }
1782
1783        // Display exceptions if necessary
1784        if (!exceptionList.isEmpty()) {
1785            StringBuilder msg = new StringBuilder(Bundle.getMessage("StationUpdateErrors"));  // NOI18N
1786            for (String keyWord : exceptionList) {
1787                if (keyWord.startsWith(TimeTableDataManager.TIME_OUT_OF_RANGE)) {
1788                    String[] comps = keyWord.split("~");
1789                    msg.append(Bundle.getMessage(comps[0], comps[1], comps[2]));
1790                } else {
1791                    msg.append(String.format("%n%s", Bundle.getMessage(keyWord)));
1792                    if (keyWord.equals(TimeTableDataManager.STAGING_IN_USE)) {
1793                        // Add the affected stops
1794                        for (Stop stop : _dataMgr.getStops(0, _curNodeId, false)) {
1795                            if (stop.getStagingTrack() > newStaging) {
1796                                Train train = _dataMgr.getTrain(stop.getTrainId());
1797                                msg.append(String.format("%n      %s, %d", train.getTrainName(), stop.getSeq()));
1798                            }
1799                        }
1800                    }
1801                }
1802            }
1803            JmriJOptionPane.showMessageDialog(this,
1804                    msg.toString(),
1805                    Bundle.getMessage("WarningTitle"),  // NOI18N
1806                    JmriJOptionPane.WARNING_MESSAGE);
1807        }
1808    }
1809
1810    /**
1811     * Update the schedule information.
1812     * Changes to the schedule times cannot make a train start time or
1813     * a stop's arrival or departure times invalid.
1814     */
1815    void updateSchedule() {
1816        Schedule schedule = _dataMgr.getSchedule(_curNodeId);
1817
1818        // Pre-validate and convert inputs
1819        String newName = _editScheduleName.getText().trim();
1820        String newEffDate = _editEffDate.getText().trim();
1821        int newStartHour = (int) _editStartHour.getValue();
1822        if (newStartHour < 0 || newStartHour > 23) {
1823            newStartHour = schedule.getStartHour();
1824        }
1825        int newDuration = (int) _editDuration.getValue();
1826        if (newDuration < 1 || newDuration > 24) {
1827            newDuration = schedule.getDuration();
1828        }
1829
1830        boolean update = false;
1831        List<String> exceptionList = new ArrayList<>();
1832
1833        // Perform updates
1834        if (!schedule.getScheduleName().equals(newName)) {
1835            schedule.setScheduleName(newName);
1836            update = true;
1837        }
1838
1839        if (!schedule.getEffDate().equals(newEffDate)) {
1840            schedule.setEffDate(newEffDate);
1841            update = true;
1842        }
1843
1844        if (update) {
1845            _curNode.setText(buildNodeText("Schedule", schedule, 0));  // NOI18N
1846            _timetableModel.nodeChanged(_curNode);
1847        }
1848
1849        if (schedule.getStartHour() != newStartHour) {
1850            try {
1851                schedule.setStartHour(newStartHour);
1852                update = true;
1853            } catch (IllegalArgumentException ex) {
1854                exceptionList.add(ex.getMessage());
1855            }
1856        }
1857
1858        if (schedule.getDuration() != newDuration) {
1859            try {
1860                schedule.setDuration(newDuration);
1861                update = true;
1862            } catch (IllegalArgumentException ex) {
1863                exceptionList.add(ex.getMessage());
1864            }
1865        }
1866
1867        if (update) {
1868            setShowReminder(true);
1869        }
1870
1871        // Display exceptions if necessary
1872        if (!exceptionList.isEmpty()) {
1873            StringBuilder msg = new StringBuilder(Bundle.getMessage("ScheduleUpdateErrors"));  // NOI18N
1874            for (String keyWord : exceptionList) {
1875                if (keyWord.startsWith(TimeTableDataManager.TIME_OUT_OF_RANGE)) {
1876                    String[] comps = keyWord.split("~");
1877                    msg.append(Bundle.getMessage(comps[0], comps[1], comps[2]));
1878                } else {
1879                    msg.append(String.format("%n%s", Bundle.getMessage(keyWord)));
1880                }
1881            }
1882            JmriJOptionPane.showMessageDialog(this,
1883                    msg.toString(),
1884                    Bundle.getMessage("WarningTitle"),  // NOI18N
1885                    JmriJOptionPane.WARNING_MESSAGE);
1886        }
1887    }
1888
1889    /**
1890     * Update the train information.
1891     * The train start time has to have a h:mm format and cannot fall outside
1892     * of the schedules times.
1893     */
1894    void updateTrain() {
1895        Train train = _dataMgr.getTrain(_curNodeId);
1896        List<String> exceptionList = new ArrayList<>();
1897
1898        // Pre-validate and convert inputs
1899        String newName = _editTrainName.getText().trim();
1900        String newDesc = _editTrainDesc.getText().trim();
1901        int newType = ((TrainType) _editTrainType.getSelectedItem()).getTypeId();
1902        int newSpeed = parseNumber(_editDefaultSpeed, "default train speed");  // NOI18N
1903        if (newSpeed < 0) {
1904            newSpeed = train.getDefaultSpeed();
1905        }
1906
1907        LocalTime newTime;
1908        int newStart;
1909        try {
1910            newTime = LocalTime.parse(_editTrainStartTime.getText().trim(), DateTimeFormatter.ofPattern("H:mm"));  // NOI18N
1911            newStart = newTime.getHour() * 60 + newTime.getMinute();
1912        } catch (java.time.format.DateTimeParseException ex) {
1913            exceptionList.add(TimeTableDataManager.START_TIME_FORMAT + "~" + ex.getParsedString());
1914            newStart = train.getStartTime();
1915        }
1916
1917        int newThrottle = (int) _editThrottle.getValue();
1918        String newNotes = _editTrainNotes.getText();
1919
1920        boolean update = false;
1921
1922        // Perform updates
1923        if (!train.getTrainName().equals(newName)) {
1924            train.setTrainName(newName);
1925            update = true;
1926        }
1927
1928        if (!train.getTrainDesc().equals(newDesc)) {
1929            train.setTrainDesc(newDesc);
1930            update = true;
1931        }
1932
1933        if (update) {
1934            _curNode.setText(buildNodeText("Train", train, 0));  // NOI18N
1935            _timetableModel.nodeChanged(_curNode);
1936        }
1937
1938        if (train.getTypeId() != newType) {
1939            train.setTypeId(newType);
1940            update = true;
1941        }
1942
1943        if (train.getDefaultSpeed() != newSpeed) {
1944            try {
1945                train.setDefaultSpeed(newSpeed);
1946                update = true;
1947            } catch (IllegalArgumentException ex) {
1948                exceptionList.add(ex.getMessage());
1949            }
1950        }
1951
1952        if (train.getStartTime() != newStart) {
1953            try {
1954                train.setStartTime(newStart);
1955                update = true;
1956            } catch (IllegalArgumentException ex) {
1957                exceptionList.add(ex.getMessage());
1958            }
1959        }
1960
1961        if (train.getThrottle() != newThrottle) {
1962            try {
1963                train.setThrottle(newThrottle);
1964                update = true;
1965            } catch (IllegalArgumentException ex) {
1966                exceptionList.add(ex.getMessage());
1967            }
1968        }
1969
1970        if (!train.getTrainNotes().equals(newNotes)) {
1971            train.setTrainNotes(newNotes);
1972            update = true;
1973        }
1974
1975        if (update) {
1976            setShowReminder(true);
1977        }
1978
1979        // Display exceptions if necessary
1980        if (!exceptionList.isEmpty()) {
1981            StringBuilder msg = new StringBuilder(Bundle.getMessage("TrainUpdateErrors"));  // NOI18N
1982            for (String keyWord : exceptionList) {
1983                log.info("kw = {}", keyWord);
1984                if (keyWord.startsWith(TimeTableDataManager.TIME_OUT_OF_RANGE)) {
1985                    String[] comps = keyWord.split("~");
1986                    msg.append(Bundle.getMessage(comps[0], comps[1], comps[2]));
1987                } else if (keyWord.startsWith(TimeTableDataManager.START_TIME_FORMAT)) {
1988                    String[] timeMsg = keyWord.split("~");
1989                    msg.append(Bundle.getMessage(timeMsg[0], timeMsg[1]));
1990                } else if (keyWord.startsWith(TimeTableDataManager.START_TIME_RANGE)) {
1991                    String[] schedMsg = keyWord.split("~");
1992                    msg.append(Bundle.getMessage(schedMsg[0], schedMsg[1], schedMsg[2]));
1993                } else {
1994                    msg.append(String.format("%n%s", Bundle.getMessage(keyWord)));
1995                }
1996            }
1997            JmriJOptionPane.showMessageDialog(this,
1998                    msg.toString(),
1999                    Bundle.getMessage("WarningTitle"),  // NOI18N
2000                    JmriJOptionPane.WARNING_MESSAGE);
2001        }
2002    }
2003
2004    /**
2005     * Update the stop information.
2006     */
2007    void updateStop() {
2008        Stop stop = _dataMgr.getStop(_curNodeId);
2009
2010        // Pre-validate and convert inputs
2011        TimeTableDataManager.SegmentStation stopSegmentStation =
2012                (TimeTableDataManager.SegmentStation) _editStopStation.getSelectedItem();
2013        int newStation = stopSegmentStation.getStationId();
2014        int newDuration = parseNumber(_editStopDuration, "stop duration");  // NOI18N
2015        if (newDuration < 0) {
2016            newDuration = stop.getDuration();
2017        }
2018        int newSpeed = parseNumber(_editNextSpeed, "next speed");  // NOI18N
2019        if (newSpeed < 0) {
2020            newSpeed = stop.getNextSpeed();
2021        }
2022        int newStagingTrack = (int) _editStagingTrack.getValue();
2023        String newNotes = _editStopNotes.getText();
2024
2025        boolean update = false;
2026        List<String> exceptionList = new ArrayList<>();
2027
2028        // Perform updates
2029        if (stop.getStationId() != newStation) {
2030            stop.setStationId(newStation);
2031            _curNode.setText(buildNodeText("Stop", stop, 0));  // NOI18N
2032            _timetableModel.nodeChanged(_curNode);
2033            update = true;
2034        }
2035
2036        if (stop.getDuration() != newDuration) {
2037            try {
2038                stop.setDuration(newDuration);
2039                update = true;
2040            } catch (IllegalArgumentException ex) {
2041                exceptionList.add(ex.getMessage());
2042            }
2043        }
2044
2045        if (stop.getNextSpeed() != newSpeed) {
2046            try {
2047                stop.setNextSpeed(newSpeed);
2048                update = true;
2049            } catch (IllegalArgumentException ex) {
2050                exceptionList.add(ex.getMessage());
2051            }
2052        }
2053
2054        if (stop.getStagingTrack() != newStagingTrack) {
2055            try {
2056                stop.setStagingTrack(newStagingTrack);
2057                update = true;
2058            } catch (IllegalArgumentException ex) {
2059                exceptionList.add(ex.getMessage());
2060            }
2061        }
2062
2063        if (!stop.getStopNotes().equals(newNotes)) {
2064            stop.setStopNotes(newNotes);
2065            update = true;
2066        }
2067
2068        if (update) {
2069            setShowReminder(true);
2070        }
2071
2072        // Display exceptions if necessary
2073        if (!exceptionList.isEmpty()) {
2074            StringBuilder msg = new StringBuilder(Bundle.getMessage("StopUpdateErrors"));  // NOI18N
2075            for (String keyWord : exceptionList) {
2076                if (keyWord.startsWith(TimeTableDataManager.TIME_OUT_OF_RANGE)) {
2077                    String[] comps = keyWord.split("~");
2078                    msg.append(Bundle.getMessage(comps[0], comps[1], comps[2]));
2079                } else {
2080                    msg.append(String.format("%n%s", Bundle.getMessage(keyWord)));
2081                }
2082            }
2083            JmriJOptionPane.showMessageDialog(this,
2084                    msg.toString(),
2085                    Bundle.getMessage("WarningTitle"),  // NOI18N
2086                    JmriJOptionPane.WARNING_MESSAGE);
2087        }
2088    }
2089
2090    /**
2091     * Convert text input to an integer.
2092     * @param textField JTextField containing the probable integer.
2093     * @param fieldName The name of the field for the dialog.
2094     * @return the valid number or -1 for an invalid input.
2095     */
2096    int parseNumber(JTextField textField, String fieldName) {
2097        String text = textField.getText().trim();
2098        try {
2099            return Integer.parseInt(text);
2100        } catch (NumberFormatException ex) {
2101            log.warn("'{}' is not a valid number for {}", text, fieldName);  // NOI18N
2102            JmriJOptionPane.showMessageDialog(textField,
2103                    Bundle.getMessage("NumberFormatError", text, fieldName),  // NOI18N
2104                    Bundle.getMessage("WarningTitle"),  // NOI18N
2105                    JmriJOptionPane.WARNING_MESSAGE);
2106            return -1;
2107        }
2108    }
2109
2110    /**
2111     * Process the node delete request.
2112     */
2113    void deletePressed() {
2114        switch (_curNodeType) {
2115            case "Layout":  // NOI18N
2116                deleteLayout();
2117                break;
2118
2119            case "TrainType":  // NOI18N
2120                deleteTrainType();
2121                break;
2122
2123            case "Segment":  // NOI18N
2124                deleteSegment();
2125                break;
2126
2127            case "Station":  // NOI18N
2128                deleteStation();
2129                break;
2130
2131            case "Schedule":  // NOI18N
2132                deleteSchedule();
2133                break;
2134
2135            case "Train":  // NOI18N
2136                deleteTrain();
2137                break;
2138
2139            case "Stop":
2140                deleteStop();  // NOI18N
2141                break;
2142
2143            default:
2144                log.error("Delete called for unsupported node type: '{}'", _curNodeType);  // NOI18N
2145        }
2146    }
2147
2148    /**
2149     * After confirmation, perform a cascade delete of the layout and its components.
2150     */
2151    void deleteLayout() {
2152        Object[] options = {Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")};  // NOI18N
2153        int selectedOption = JmriJOptionPane.showOptionDialog(this,
2154                Bundle.getMessage("LayoutCascade"), // NOI18N
2155                Bundle.getMessage("QuestionTitle"),   // NOI18N
2156                JmriJOptionPane.DEFAULT_OPTION,
2157                JmriJOptionPane.QUESTION_MESSAGE,
2158                null, options, options[0]);
2159        if (selectedOption != 1) { // return if option is not array position 1, YES
2160            return;
2161        }
2162
2163        _dataMgr.setLockCalculate(true);
2164
2165        // Delete the components
2166        for (Schedule schedule : _dataMgr.getSchedules(_curNodeId, false)) {
2167            for (Train train : _dataMgr.getTrains(schedule.getScheduleId(), 0, false)) {
2168                for (Stop stop : _dataMgr.getStops(train.getTrainId(), 0, false)) {
2169                    _dataMgr.deleteStop(stop.getStopId());
2170                }
2171                _dataMgr.deleteTrain(train.getTrainId());
2172            }
2173            _dataMgr.deleteSchedule(schedule.getScheduleId());
2174        }
2175
2176        for (Segment segment : _dataMgr.getSegments(_curNodeId, false)) {
2177            for (Station station : _dataMgr.getStations(segment.getSegmentId(), false)) {
2178                _dataMgr.deleteStation(station.getStationId());
2179            }
2180            _dataMgr.deleteSegment(segment.getSegmentId());
2181        }
2182
2183        for (TrainType type : _dataMgr.getTrainTypes(_curNodeId, false)) {
2184            _dataMgr.deleteTrainType(type.getTypeId());
2185        }
2186
2187        // delete the Layout
2188        _dataMgr.deleteLayout(_curNodeId);
2189        setShowReminder(true);
2190
2191        // Update the tree
2192//         TreePath parentPath = _curTreePath.getParentPath();
2193        TreeNode parentNode = _curNode.getParent();
2194        _curNode.removeFromParent();
2195        _curNode = null;
2196        _timetableModel.nodeStructureChanged(parentNode);
2197//         _timetableTree.setSelectionPath(parentPath);
2198        _dataMgr.setLockCalculate(false);
2199    }
2200
2201    /**
2202     * Delete a train type after checking for usage.
2203     */
2204    void deleteTrainType() {
2205        // Check train references
2206        ArrayList<String> typeReference = new ArrayList<>();
2207        for (Train train : _dataMgr.getTrains(0, _curNodeId, true)) {
2208            typeReference.add(train.getTrainName());
2209        }
2210        if (!typeReference.isEmpty()) {
2211            StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteWarning", _curNodeType));  // NOI18N
2212            for (String trainName : typeReference) {
2213                msg.append("\n    " + trainName);  // NOI18N
2214            }
2215            JmriJOptionPane.showMessageDialog(this,
2216                    msg.toString(),
2217                    Bundle.getMessage("WarningTitle"),  // NOI18N
2218                    JmriJOptionPane.WARNING_MESSAGE);
2219            return;
2220        }
2221        _dataMgr.deleteTrainType(_curNodeId);
2222        setShowReminder(true);
2223
2224        // Update the tree
2225        TreePath parentPath = _curTreePath.getParentPath();
2226        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2227        parentNode.remove(_curNode);
2228        _timetableModel.nodeStructureChanged(parentNode);
2229        _curNode = null;
2230        _timetableTree.setSelectionPath(parentPath);
2231    }
2232
2233    /**
2234     * Delete a Segment.
2235     * If the segment contains inactive stations, provide the option to perform
2236     * a cascade delete.
2237     */
2238    void deleteSegment() {
2239        List<Station> stationList = new ArrayList<>(_dataMgr.getStations(_curNodeId, true));
2240        if (!stationList.isEmpty()) {
2241            // The segment still has stations.  See if any are still used by Stops
2242            List<Station> activeList = new ArrayList<>();
2243            for (Station checkActive : stationList) {
2244                List<Stop> stopList = new ArrayList<>(_dataMgr.getStops(0, checkActive.getStationId(), true));
2245                if (!stopList.isEmpty()) {
2246                    activeList.add(checkActive);
2247                }
2248            }
2249            if (!activeList.isEmpty()) {
2250                // Cannot delete the Segment
2251                StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteWarning", _curNodeType));  // NOI18N
2252                for (Station activeStation : activeList) {
2253                    msg.append("\n    " + activeStation.getStationName());  // NOI18N
2254                }
2255                JmriJOptionPane.showMessageDialog(this,
2256                        msg.toString(),
2257                        Bundle.getMessage("WarningTitle"),  // NOI18N
2258                        JmriJOptionPane.WARNING_MESSAGE);
2259                return;
2260            }
2261            // Present the option to delete the stations and the segment
2262            Object[] options = {Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")};  // NOI18N
2263            int selectedOption = JmriJOptionPane.showOptionDialog(this,
2264                    Bundle.getMessage("SegmentCascade"), // NOI18N
2265                    Bundle.getMessage("QuestionTitle"),   // NOI18N
2266                    JmriJOptionPane.DEFAULT_OPTION,
2267                    JmriJOptionPane.QUESTION_MESSAGE,
2268                    null, options, options[0]);
2269            if (selectedOption != 1) {  // return if option is not array position 1, YES
2270                return;
2271            }
2272            for (Station delStation : stationList) {
2273                _dataMgr.deleteStation(delStation.getStationId());
2274            }
2275        }
2276        // delete the segment
2277        _dataMgr.deleteSegment(_curNodeId);
2278        setShowReminder(true);
2279
2280        // Update the tree
2281        TreePath parentPath = _curTreePath.getParentPath();
2282        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2283        _curNode.removeFromParent();
2284        _curNode = null;
2285        _timetableModel.nodeStructureChanged(parentNode);
2286        _timetableTree.setSelectionPath(parentPath);
2287    }
2288
2289    /**
2290     * Delete a Station after checking for usage.
2291     */
2292    void deleteStation() {
2293        // Check stop references
2294        List<String> stopReference = new ArrayList<>();
2295        for (Stop stop : _dataMgr.getStops(0, _curNodeId, true)) {
2296            Train train = _dataMgr.getTrain(stop.getTrainId());
2297            String trainSeq = String.format("%s : %d", train.getTrainName(), stop.getSeq());  // NOI18N
2298            stopReference.add(trainSeq);
2299        }
2300        if (!stopReference.isEmpty()) {
2301            StringBuilder msg = new StringBuilder(Bundle.getMessage("DeleteWarning", _curNodeType));  // NOI18N
2302            for (String stopTrainSeq : stopReference) {
2303                msg.append("\n    " + stopTrainSeq);  // NOI18N
2304            }
2305            JmriJOptionPane.showMessageDialog(this,
2306                    msg.toString(),
2307                    Bundle.getMessage("WarningTitle"),  // NOI18N
2308                    JmriJOptionPane.WARNING_MESSAGE);
2309            return;
2310        }
2311        _dataMgr.deleteStation(_curNodeId);
2312        setShowReminder(true);
2313
2314        // Update the tree
2315        TreePath parentPath = _curTreePath.getParentPath();
2316        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2317        parentNode.remove(_curNode);
2318        _timetableModel.nodeStructureChanged(parentNode);
2319        _curNode = null;
2320        _timetableTree.setSelectionPath(parentPath);
2321    }
2322
2323    /**
2324     * Delete a Schedule.
2325     * If the schedule contains trains, provide the option to perform
2326     * a cascade delete of trains and their stops.
2327     */
2328    void deleteSchedule() {
2329        List<Train> trainList = new ArrayList<>(_dataMgr.getTrains(_curNodeId, 0, true));
2330        if (!trainList.isEmpty()) {
2331            // The schedule still has trains.
2332            // Present the option to delete the stops, trains and the schedule
2333            Object[] options = {Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")};  // NOI18N
2334            int selectedOption = JmriJOptionPane.showOptionDialog(this,
2335                    Bundle.getMessage("ScheduleCascade"), // NOI18N
2336                    Bundle.getMessage("QuestionTitle"),   // NOI18N
2337                    JmriJOptionPane.DEFAULT_OPTION,
2338                    JmriJOptionPane.QUESTION_MESSAGE,
2339                    null, options, options[0]);
2340            if (selectedOption != 1) { // return if option is not array position 1, YES
2341                return;
2342            }
2343            for (Train train : trainList) {
2344                for (Stop stop : _dataMgr.getStops(train.getTrainId(), 0, false)) {
2345                    _dataMgr.deleteStop(stop.getStopId());
2346                }
2347                _dataMgr.deleteTrain(train.getTrainId());
2348            }
2349        }
2350        // delete the schedule
2351        _dataMgr.deleteSchedule(_curNodeId);
2352        setShowReminder(true);
2353
2354        // Update the tree
2355        TreePath parentPath = _curTreePath.getParentPath();
2356        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2357        _curNode.removeFromParent();
2358        _curNode = null;
2359        _timetableModel.nodeStructureChanged(parentNode);
2360        _timetableTree.setSelectionPath(parentPath);
2361    }
2362
2363    /**
2364     * Delete a Train.
2365     * If the train contains stops, provide the option to perform
2366     * a cascade delete of the stops.
2367     */
2368    void deleteTrain() {
2369        List<Stop> stopList = new ArrayList<>(_dataMgr.getStops(_curNodeId, 0, true));
2370        if (!stopList.isEmpty()) {
2371            // The trains still has stops.
2372            // Present the option to delete the stops and the train
2373            Object[] options = {Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")};  // NOI18N
2374            int selectedOption = JmriJOptionPane.showOptionDialog(this,
2375                    Bundle.getMessage("TrainCascade"), // NOI18N
2376                    Bundle.getMessage("QuestionTitle"),   // NOI18N
2377                    JmriJOptionPane.DEFAULT_OPTION,
2378                    JmriJOptionPane.QUESTION_MESSAGE,
2379                    null, options, options[0]);
2380            if (selectedOption != 1) { // return if option is not array position 1, YES
2381                return;
2382            }
2383            for (Stop stop : stopList) {
2384                _dataMgr.deleteStop(stop.getStopId());
2385            }
2386        }
2387        // delete the train
2388        _dataMgr.deleteTrain(_curNodeId);
2389        setShowReminder(true);
2390
2391        // Update the tree
2392        TreePath parentPath = _curTreePath.getParentPath();
2393        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2394        _curNode.removeFromParent();
2395        _curNode = null;
2396        _timetableModel.nodeStructureChanged(parentNode);
2397        _timetableTree.setSelectionPath(parentPath);
2398    }
2399
2400    /**
2401     * Delete a Stop.
2402     */
2403    void deleteStop() {
2404        // delete the stop
2405        _dataMgr.deleteStop(_curNodeId);
2406        setShowReminder(true);
2407
2408        // Update the tree
2409        TreePath parentPath = _curTreePath.getParentPath();
2410        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2411        _curNode.removeFromParent();
2412        _curNode = null;
2413        _timetableModel.nodeStructureChanged(parentNode);
2414        _timetableTree.setSelectionPath(parentPath);
2415    }
2416
2417    /**
2418     * Cancel the current node edit.
2419     */
2420    void cancelPressed() {
2421        setEditMode(false);
2422        _timetableTree.setSelectionPath(_curTreePath);
2423        _timetableTree.grabFocus();
2424    }
2425
2426    /**
2427     * Move a Stop row up 1 row.
2428     */
2429    void upPressed() {
2430        setShowReminder(true);
2431
2432        DefaultMutableTreeNode prevNode = _curNode.getPreviousSibling();
2433        if (!(prevNode instanceof TimeTableTreeNode)) {
2434            log.warn("At first node, cannot move up");  // NOI18N
2435            return;
2436        }
2437        int prevStopId = ((TimeTableTreeNode) prevNode).getId();
2438        Stop prevStop = _dataMgr.getStop(prevStopId);
2439        prevStop.setSeq(prevStop.getSeq() + 1);
2440        Stop currStop = _dataMgr.getStop(_curNodeId);
2441        currStop.setSeq(currStop.getSeq() - 1);
2442        moveTreeNode("Up");     // NOI18N
2443    }
2444
2445    /**
2446     * Move a Stop row down 1 row.
2447     */
2448    void downPressed() {
2449        setShowReminder(true);
2450
2451        DefaultMutableTreeNode nextNode = _curNode.getNextSibling();
2452        if (!(nextNode instanceof TimeTableTreeNode)) {
2453            log.warn("At last node, cannot move down");  // NOI18N
2454            return;
2455        }
2456        int nextStopId = ((TimeTableTreeNode) nextNode).getId();
2457        Stop nextStop = _dataMgr.getStop(nextStopId);
2458        nextStop.setSeq(nextStop.getSeq() - 1);
2459        Stop currStop = _dataMgr.getStop(_curNodeId);
2460        currStop.setSeq(currStop.getSeq() + 1);
2461        moveTreeNode("Down");     // NOI18N
2462    }
2463
2464    /**
2465     * Move a tree node in response to a up or down request.
2466     *
2467     * @param direction The direction of movement, Up or Down
2468     */
2469    void moveTreeNode(String direction) {
2470        // Update the node
2471        if (direction.equals("Up")) {    // NOI18N
2472            _curNodeRow -= 1;
2473        } else {
2474            _curNodeRow += 1;
2475        }
2476        _curNode.setRow(_curNodeRow);
2477        _timetableModel.nodeChanged(_curNode);
2478
2479        // Update the sibling
2480        DefaultMutableTreeNode siblingNode;
2481        TimeTableTreeNode tempNode;
2482        if (direction.equals("Up")) {    // NOI18N
2483            siblingNode = _curNode.getPreviousSibling();
2484            if (siblingNode instanceof TimeTableTreeNode) {
2485                tempNode = (TimeTableTreeNode) siblingNode;
2486                tempNode.setRow(tempNode.getRow() + 1);
2487            }
2488        } else {
2489            siblingNode = _curNode.getNextSibling();
2490            if (siblingNode instanceof TimeTableTreeNode) {
2491                tempNode = (TimeTableTreeNode) siblingNode;
2492                tempNode.setRow(tempNode.getRow() - 1);
2493            }
2494        }
2495        _timetableModel.nodeChanged(siblingNode);
2496
2497        // Update the tree
2498        TimeTableTreeNode parentNode = (TimeTableTreeNode) _curNode.getParent();
2499        parentNode.insert(_curNode, _curNodeRow - 1);
2500        _timetableModel.nodeStructureChanged(parentNode);
2501        _timetableTree.setSelectionPath(new TreePath(_curNode.getPath()));
2502        setMoveButtons();
2503
2504        // Update times
2505        _dataMgr.calculateTrain(_dataMgr.getStop(_curNodeId).getTrainId(), true);
2506    }
2507
2508    /**
2509     * Enable/Disable the Up and Down buttons based on the postion in the list.
2510     */
2511    void setMoveButtons() {
2512        if (_curNode == null) {
2513            return;
2514        }
2515
2516        Component[] compList = _moveButtonPanel.getComponents();
2517        JButton up = (JButton) compList[1];
2518        JButton down = (JButton) compList[3];
2519
2520        up.setEnabled(true);
2521        down.setEnabled(true);
2522
2523        int rows = _curNode.getSiblingCount();
2524        if (_curNodeRow < 2) {
2525            up.setEnabled(false);
2526        }
2527        if (_curNodeRow > rows - 1) {
2528            down.setEnabled(false);
2529        }
2530
2531        // Disable move buttons during Variable or Action add or edit processing, or nothing selected
2532        if (_editActive) {
2533            up.setEnabled(false);
2534            down.setEnabled(false);
2535        }
2536
2537        _moveButtonPanel.setVisible(true);
2538    }
2539
2540    void graphPressed(String graphType) {
2541
2542        // select a schedule if necessary
2543        Segment segment = _dataMgr.getSegment(_curNodeId);
2544        Layout layout = _dataMgr.getLayout(segment.getLayoutId());
2545        int scheduleId;
2546        List<Schedule> schedules = _dataMgr.getSchedules(layout.getLayoutId(), true);
2547
2548        if (schedules.size() == 0) {
2549            log.warn("no schedule");  // NOI18N
2550            return;
2551        } else {
2552            scheduleId = schedules.get(0).getScheduleId();
2553            if (schedules.size() > 1) {
2554                // do selection dialog
2555                Schedule[] schedArr = new Schedule[schedules.size()];
2556                schedArr = schedules.toArray(schedArr);
2557                Schedule schedSelected = (Schedule) JmriJOptionPane.showInputDialog(
2558                        null,
2559                        Bundle.getMessage("GraphScheduleMessage"),  // NOI18N
2560                        Bundle.getMessage("QuestionTitle"),  // NOI18N
2561                        JmriJOptionPane.QUESTION_MESSAGE,
2562                        null,
2563                        schedArr,
2564                        schedArr[0]
2565                );
2566                if (schedSelected == null) {
2567                    log.warn("Schedule not selected, graph request cancelled");  // NOI18N
2568                    return;
2569                }
2570                scheduleId = schedSelected.getScheduleId();
2571            }
2572        }
2573
2574        if (graphType.equals("Display")) {
2575            TimeTableDisplayGraph graph = new TimeTableDisplayGraph(_curNodeId, scheduleId, _showTrainTimes);
2576
2577            JmriJFrame f = new JmriJFrame(Bundle.getMessage("TitleTimeTableGraph"), true, true);  // NOI18N
2578            f.setMinimumSize(new Dimension(600, 300));
2579            f.getContentPane().add(graph);
2580            f.pack();
2581            f.addHelpMenu("html.tools.TimeTable", true);  // NOI18N
2582            f.setVisible(true);
2583        }
2584
2585        if (graphType.equals("Print")) {
2586            TimeTablePrintGraph print = new TimeTablePrintGraph(_curNodeId, scheduleId, _showTrainTimes, _twoPage);
2587            print.printGraph();
2588        }
2589    }
2590
2591    JFileChooser fileChooser;
2592    void importPressed() {
2593        fileChooser = jmri.jmrit.XmlFile.userFileChooser("SchedGen File", "sgn");  // NOI18N
2594        int retVal = fileChooser.showOpenDialog(null);
2595        if (retVal == JFileChooser.APPROVE_OPTION) {
2596            File file = fileChooser.getSelectedFile();
2597            try {
2598                new TimeTableImport().importSgn(_dataMgr, file);
2599            } catch (IOException ex) {
2600                log.error("Import exception", ex);  // NOI18N
2601                JmriJOptionPane.showMessageDialog(this,
2602                        Bundle.getMessage("ImportFailed", "SGN"),  // NOI18N
2603                        Bundle.getMessage("ErrorTitle"),  // NOI18N
2604                        JmriJOptionPane.ERROR_MESSAGE);
2605                return;
2606            }
2607            savePressed();
2608            JmriJOptionPane.showMessageDialog(this,
2609                    Bundle.getMessage("ImportCompleted", "SGN"),  // NOI18N
2610                    Bundle.getMessage("MessageTitle"),  // NOI18N
2611                    JmriJOptionPane.INFORMATION_MESSAGE);
2612        }
2613    }
2614
2615    List<String> feedbackList;
2616    void importCsvPressed() {
2617        fileChooser = new jmri.util.swing.JmriJFileChooser(jmri.util.FileUtil.getUserFilesPath());
2618        fileChooser.setFileFilter(new FileNameExtensionFilter("Import File", "csv"));
2619        int retVal = fileChooser.showOpenDialog(null);
2620        if (retVal == JFileChooser.APPROVE_OPTION) {
2621            File file = fileChooser.getSelectedFile();
2622            completeImport(file);
2623        }
2624    }
2625
2626    void completeImport(File file) {
2627        try {
2628            feedbackList = new TimeTableCsvImport().importCsv(file);
2629        } catch (IOException ex) {
2630            log.error("Import exception", ex); // NOI18N
2631            JmriJOptionPane.showMessageDialog(this,
2632                    Bundle.getMessage("ImportCsvFailed", "CVS"), // NOI18N
2633                    Bundle.getMessage("ErrorTitle"), // NOI18N
2634                    JmriJOptionPane.ERROR_MESSAGE);
2635            return;
2636        }
2637        if (feedbackList.size() > 0) {
2638            StringBuilder msg = new StringBuilder(Bundle.getMessage("ImportCsvErrors")); // NOI18N
2639            for (String feedback : feedbackList) {
2640                msg.append(feedback + "\n");
2641            }
2642            JmriJOptionPane.showMessageDialog(this,
2643                    msg.toString(),
2644                    Bundle.getMessage("ErrorTitle"), // NOI18N
2645                    JmriJOptionPane.ERROR_MESSAGE);
2646            return;
2647        }
2648        savePressed();
2649        JmriJOptionPane.showMessageDialog(this,
2650                Bundle.getMessage("ImportCompleted", "CSV"), // NOI18N
2651                Bundle.getMessage("MessageTitle"), // NOI18N
2652                JmriJOptionPane.INFORMATION_MESSAGE);
2653    }
2654
2655    void importFromOperationsPressed() {
2656        ExportTimetable ex = new ExportTimetable();
2657        new ExportTimetable().writeOperationsTimetableFile();
2658        completeImport(ex.getExportFile());
2659    }
2660
2661    void exportCsvPressed() {
2662        // Select layout
2663        List<Layout> layouts = _dataMgr.getLayouts(true);
2664        if (layouts.size() == 0) {
2665            JmriJOptionPane.showMessageDialog(this,
2666                    Bundle.getMessage("ExportLayoutError"),  // NOI18N
2667                    Bundle.getMessage("ErrorTitle"),  // NOI18N
2668                    JmriJOptionPane.ERROR_MESSAGE);
2669            return;
2670        }
2671        int layoutId = layouts.get(0).getLayoutId();
2672        if (layouts.size() > 1) {
2673            Layout layout = (Layout) JmriJOptionPane.showInputDialog(
2674                    this,
2675                    Bundle.getMessage("ExportSelectLayout"),  // NOI18N
2676                    Bundle.getMessage("QuestionTitle"),  // NOI18N
2677                    JmriJOptionPane.PLAIN_MESSAGE,
2678                    null,
2679                    layouts.toArray(),
2680                    null);
2681            if (layout == null) return;
2682            layoutId = layout.getLayoutId();
2683        }
2684
2685        // Select segment
2686        List<Segment> segments = _dataMgr.getSegments(layoutId, true);
2687        if (segments.size() == 0) {
2688            JmriJOptionPane.showMessageDialog(this,
2689                    Bundle.getMessage("ExportSegmentError"),  // NOI18N
2690                    Bundle.getMessage("ErrorTitle"),  // NOI18N
2691                    JmriJOptionPane.ERROR_MESSAGE);
2692            return;
2693        }
2694        int segmentId = segments.get(0).getSegmentId();
2695        if (segments.size() > 1) {
2696            Segment segment = (Segment) JmriJOptionPane.showInputDialog(
2697                    this,
2698                    Bundle.getMessage("ExportSelectSegment"),  // NOI18N
2699                    Bundle.getMessage("QuestionTitle"),  // NOI18N
2700                    JmriJOptionPane.PLAIN_MESSAGE,
2701                    null,
2702                    segments.toArray(),
2703                    null);
2704            if (segment == null) return;
2705            segmentId = segment.getSegmentId();
2706        }
2707
2708        // Select schedule
2709        List<Schedule> schedules = _dataMgr.getSchedules(layoutId, true);
2710        if (schedules.size() == 0) {
2711            JmriJOptionPane.showMessageDialog(this,
2712                    Bundle.getMessage("ExportScheduleError"),  // NOI18N
2713                    Bundle.getMessage("ErrorTitle"),  // NOI18N
2714                    JmriJOptionPane.ERROR_MESSAGE);
2715            return;
2716        }
2717        int scheduleId = schedules.get(0).getScheduleId();
2718        if (schedules.size() > 1) {
2719            Schedule schedule = (Schedule) JmriJOptionPane.showInputDialog(
2720                    this,
2721                    Bundle.getMessage("ExportSelectSchedule"),  // NOI18N
2722                    Bundle.getMessage("QuestionTitle"),  // NOI18N
2723                    JmriJOptionPane.PLAIN_MESSAGE,
2724                    null,
2725                    schedules.toArray(),
2726                    null);
2727            if (schedule == null) return;
2728            scheduleId = schedule.getScheduleId();
2729        }
2730
2731        fileChooser = new jmri.util.swing.JmriJFileChooser(jmri.util.FileUtil.getUserFilesPath());
2732        fileChooser.setFileFilter(new FileNameExtensionFilter("Export as CSV File", "csv"));  // NOI18N
2733        int retVal = fileChooser.showSaveDialog(null);
2734        if (retVal == JFileChooser.APPROVE_OPTION) {
2735            File file = fileChooser.getSelectedFile();
2736            String fileName = file.getAbsolutePath();
2737            String fileNameLC = fileName.toLowerCase();
2738            if (!fileNameLC.endsWith(".csv")) {  // NOI18N
2739                fileName = fileName + ".csv";  // NOI18N
2740                file = new File(fileName);
2741            }
2742            if (file.exists()) {
2743                if (JmriJOptionPane.showConfirmDialog(this,
2744                        Bundle.getMessage("FileOverwriteWarning", file.getName()),  // NOI18N
2745                        Bundle.getMessage("QuestionTitle"),  // NOI18N
2746                        JmriJOptionPane.OK_CANCEL_OPTION,
2747                        JmriJOptionPane.QUESTION_MESSAGE) != JmriJOptionPane.OK_OPTION) {
2748                    return;
2749                }
2750            }
2751
2752
2753            boolean hasErrors;
2754            try {
2755                hasErrors = new TimeTableCsvExport().exportCsv(file, layoutId, segmentId, scheduleId);
2756            } catch (IOException ex) {
2757                log.error("Export exception", ex);  // NOI18N
2758                JmriJOptionPane.showMessageDialog(this,
2759                        Bundle.getMessage("ExportFailed"),  // NOI18N
2760                        Bundle.getMessage("ErrorTitle"),  // NOI18N
2761                        JmriJOptionPane.ERROR_MESSAGE);
2762                return;
2763            }
2764
2765            if (hasErrors) {
2766                JmriJOptionPane.showMessageDialog(this,
2767                        Bundle.getMessage("ExportFailed"),  // NOI18N
2768                        Bundle.getMessage("ErrorTitle"),  // NOI18N
2769                        JmriJOptionPane.ERROR_MESSAGE);
2770            } else {
2771                JmriJOptionPane.showMessageDialog(this,
2772                        Bundle.getMessage("ExportCompleted", file),  // NOI18N
2773                        Bundle.getMessage("MessageTitle"),  // NOI18N
2774                        JmriJOptionPane.INFORMATION_MESSAGE);
2775            }
2776        }
2777    }
2778
2779    /**
2780     * Save the current set of timetable data.
2781     */
2782    void savePressed() {
2783        TimeTableXml.doStore();
2784        setShowReminder(false);
2785    }
2786
2787    /**
2788     * Check for pending updates and close if none or approved.
2789     */
2790    void donePressed() {
2791        if (_isDirty) {
2792            Object[] options = {Bundle.getMessage("ButtonNo"), Bundle.getMessage("ButtonYes")};  // NOI18N
2793            int selectedOption = JmriJOptionPane.showOptionDialog(this,
2794                    Bundle.getMessage("DirtyDataWarning"), // NOI18N
2795                    Bundle.getMessage("WarningTitle"),   // NOI18N
2796                    JmriJOptionPane.DEFAULT_OPTION,
2797                    JmriJOptionPane.WARNING_MESSAGE,
2798                    null, options, options[0]);
2799            if (selectedOption == 0) {
2800                return;
2801            }
2802        }
2803        InstanceManager.reset(TimeTableFrame.class);
2804        dispose();
2805    }
2806
2807    // ------------  Tree Content and Navigation ------------
2808
2809    /**
2810     * Create the TimeTable tree structure.
2811     *
2812     * @return _timetableTree The tree ddefinition with its content
2813     */
2814    JTree buildTree() {
2815        _timetableRoot = new DefaultMutableTreeNode("Root Node");      // NOI18N
2816        _timetableModel = new DefaultTreeModel(_timetableRoot);
2817        _timetableTree = new JTree(_timetableModel);
2818
2819        createTimeTableContent();
2820
2821        // build the tree GUI
2822        _timetableTree.expandPath(new TreePath(_timetableRoot));
2823        _timetableTree.setRootVisible(false);
2824        _timetableTree.setShowsRootHandles(true);
2825        _timetableTree.setScrollsOnExpand(true);
2826        _timetableTree.setExpandsSelectedPaths(true);
2827        _timetableTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
2828
2829        // tree listeners
2830        _timetableTree.addTreeSelectionListener(_timetableListener = new TreeSelectionListener() {
2831            @Override
2832            public void valueChanged(TreeSelectionEvent e) {
2833                if (_editActive) {
2834                    if (e.getNewLeadSelectionPath() != _curTreePath) {
2835                        _timetableTree.setSelectionPath(e.getOldLeadSelectionPath());
2836                        showNodeEditMessage();
2837                    }
2838                    return;
2839                }
2840
2841                _curTreePath = _timetableTree.getSelectionPath();
2842                if (_curTreePath != null) {
2843                    Object chkLast = _curTreePath.getLastPathComponent();
2844                    if (chkLast instanceof TimeTableTreeNode) {
2845                        treeRowSelected((TimeTableTreeNode) chkLast);
2846                    }
2847                }
2848            }
2849        });
2850
2851        return _timetableTree;
2852    }
2853
2854    /**
2855     * Create the tree content.
2856     * Level 1 -- Layouts
2857     * Level 2 -- Train Type, Segment and Schedule Containers
2858     * Level 3 -- Train Types, Segments, Schedules
2859     * Level 4 -- Stations, Trains
2860     * Level 5 -- Stops
2861     */
2862    void createTimeTableContent() {
2863        for (Layout l : _dataMgr.getLayouts(true)) {
2864            _layoutNode = new TimeTableTreeNode(l.getLayoutName(), "Layout", l.getLayoutId(), 0);    // NOI18N
2865            _timetableRoot.add(_layoutNode);
2866
2867            _typeHead = new TimeTableTreeNode(buildNodeText("TrainTypes", null, 0), "TrainTypes", 0, 0);    // NOI18N
2868            _layoutNode.add(_typeHead);
2869            for (TrainType y : _dataMgr.getTrainTypes(l.getLayoutId(), true)) {
2870                _typeNode = new TimeTableTreeNode(y.getTypeName(), "TrainType", y.getTypeId(), 0);    // NOI18N
2871                _typeHead.add(_typeNode);
2872            }
2873
2874            _segmentHead = new TimeTableTreeNode(buildNodeText("Segments", null, 0), "Segments", 0, 0);    // NOI18N
2875            _layoutNode.add(_segmentHead);
2876            for (Segment sg : _dataMgr.getSegments(l.getLayoutId(), true)) {
2877                _segmentNode = new TimeTableTreeNode(sg.getSegmentName(), "Segment", sg.getSegmentId(), 0);    // NOI18N
2878                _segmentHead.add(_segmentNode);
2879                for (Station st : _dataMgr.getStations(sg.getSegmentId(), true)) {
2880                    _leafNode = new TimeTableTreeNode(st.getStationName(), "Station", st.getStationId(), 0);    // NOI18N
2881                    _segmentNode.add(_leafNode);
2882                }
2883            }
2884
2885            _scheduleHead = new TimeTableTreeNode(buildNodeText("Schedules", null, 0), "Schedules", 0, 0);    // NOI18N
2886            _layoutNode.add(_scheduleHead);
2887            for (Schedule c : _dataMgr.getSchedules(l.getLayoutId(), true)) {
2888                _scheduleNode = new TimeTableTreeNode(buildNodeText("Schedule", c, 0), "Schedule", c.getScheduleId(), 0);    // NOI18N
2889                _scheduleHead.add(_scheduleNode);
2890                for (Train tr : _dataMgr.getTrains(c.getScheduleId(), 0, true)) {
2891                    _trainNode = new TimeTableTreeNode(buildNodeText("Train", tr, 0), "Train", tr.getTrainId(), 0);    // NOI18N
2892                    _scheduleNode.add(_trainNode);
2893                    for (Stop sp : _dataMgr.getStops(tr.getTrainId(), 0, true)) {
2894                        _leafNode = new TimeTableTreeNode(buildNodeText("Stop", sp, 0), "Stop", sp.getStopId(), sp.getSeq());    // NOI18N
2895                        _trainNode.add(_leafNode);
2896                    }
2897                }
2898            }
2899        }
2900    }
2901
2902    /**
2903     * Create the localized node text display strings based on node type.
2904     *
2905     * @param nodeType  The type of the node
2906     * @param component The object or child object
2907     * @param idx       Optional index value
2908     * @return nodeText containing the text to display on the node
2909     */
2910    String buildNodeText(String nodeType, Object component, int idx) {
2911        switch (nodeType) {
2912            case "TrainTypes":
2913                return Bundle.getMessage("LabelTrainTypes");  // NOI18N
2914            case "Segments":
2915                return Bundle.getMessage("LabelSegments");  // NOI18N
2916            case "Schedules":
2917                return Bundle.getMessage("LabelSchedules");  // NOI18N
2918            case "Schedule":
2919                Schedule schedule = (Schedule) component;
2920                return Bundle.getMessage("LabelSchedule", schedule.getScheduleName(), schedule.getEffDate());  // NOI18N
2921            case "Train":
2922                Train train = (Train) component;
2923                return Bundle.getMessage("LabelTrain", train.getTrainName(), train.getTrainDesc());  // NOI18N
2924            case "Stop":
2925                Stop stop = (Stop) component;
2926                int stationId = stop.getStationId();
2927                return Bundle.getMessage("LabelStop", stop.getSeq(), _dataMgr.getStation(stationId).getStationName());  // NOI18N
2928            default:
2929                return "None";  // NOI18N
2930        }
2931    }
2932
2933    /**
2934     * Change the button row based on the currently selected node type. Invoke
2935     * edit where appropriate.
2936     *
2937     * @param selectedNode The node object
2938     */
2939    void treeRowSelected(TimeTableTreeNode selectedNode) {
2940        // Set the current node variables
2941        _curNode = selectedNode;
2942        _curNodeId = selectedNode.getId();
2943        _curNodeType = selectedNode.getType();
2944        _curNodeText = selectedNode.getText();
2945        _curNodeRow = selectedNode.getRow();
2946
2947        // Reset button bar
2948        _addButtonPanel.setVisible(false);
2949        _duplicateButtonPanel.setVisible(false);
2950        _copyButtonPanel.setVisible(false);
2951        _deleteButtonPanel.setVisible(false);
2952        _moveButtonPanel.setVisible(false);
2953        _graphButtonPanel.setVisible(false);
2954
2955        switch (_curNodeType) {
2956            case "Layout":     // NOI18N
2957                _addButton.setText(Bundle.getMessage("AddLayoutButtonText"));  // NOI18N
2958                _addButtonPanel.setVisible(true);
2959                _duplicateButton.setText(Bundle.getMessage("DuplicateLayoutButtonText"));  // NOI18N
2960                _duplicateButtonPanel.setVisible(true);
2961                _deleteButton.setText(Bundle.getMessage("DeleteLayoutButtonText"));  // NOI18N
2962                _deleteButtonPanel.setVisible(true);
2963                editPressed();
2964                break;
2965
2966            case "TrainTypes":     // NOI18N
2967                _addButton.setText(Bundle.getMessage("AddTrainTypeButtonText"));  // NOI18N
2968                _addButtonPanel.setVisible(true);
2969                makeDetailGrid(EMPTY_GRID);  // NOI18N
2970                break;
2971
2972            case "TrainType":     // NOI18N
2973                _duplicateButton.setText(Bundle.getMessage("DuplicateTrainTypeButtonText"));  // NOI18N
2974                _duplicateButtonPanel.setVisible(true);
2975                _deleteButton.setText(Bundle.getMessage("DeleteTrainTypeButtonText"));  // NOI18N
2976                _deleteButtonPanel.setVisible(true);
2977                editPressed();
2978                break;
2979
2980            case "Segments":     // NOI18N
2981                _addButton.setText(Bundle.getMessage("AddSegmentButtonText"));  // NOI18N
2982                _addButtonPanel.setVisible(true);
2983                makeDetailGrid(EMPTY_GRID);  // NOI18N
2984                break;
2985
2986            case "Segment":     // NOI18N
2987                _addButton.setText(Bundle.getMessage("AddStationButtonText"));  // NOI18N
2988                _addButtonPanel.setVisible(true);
2989                _duplicateButton.setText(Bundle.getMessage("DuplicateSegmentButtonText"));  // NOI18N
2990                _duplicateButtonPanel.setVisible(true);
2991                _deleteButton.setText(Bundle.getMessage("DeleteSegmentButtonText"));  // NOI18N
2992                _deleteButtonPanel.setVisible(true);
2993                _graphButtonPanel.setVisible(true);
2994                editPressed();
2995                break;
2996
2997            case "Station":     // NOI18N
2998                _duplicateButton.setText(Bundle.getMessage("DuplicateStationButtonText"));  // NOI18N
2999                _duplicateButtonPanel.setVisible(true);
3000                _deleteButton.setText(Bundle.getMessage("DeleteStationButtonText"));  // NOI18N
3001                _deleteButtonPanel.setVisible(true);
3002                editPressed();
3003                break;
3004
3005            case "Schedules":     // NOI18N
3006                _addButton.setText(Bundle.getMessage("AddScheduleButtonText"));  // NOI18N
3007                _addButtonPanel.setVisible(true);
3008                makeDetailGrid(EMPTY_GRID);  // NOI18N
3009                break;
3010
3011            case "Schedule":     // NOI18N
3012                _addButton.setText(Bundle.getMessage("AddTrainButtonText"));  // NOI18N
3013                _addButtonPanel.setVisible(true);
3014                _duplicateButton.setText(Bundle.getMessage("DuplicateScheduleButtonText"));  // NOI18N
3015                _duplicateButtonPanel.setVisible(true);
3016                _deleteButton.setText(Bundle.getMessage("DeleteScheduleButtonText"));  // NOI18N
3017                _deleteButtonPanel.setVisible(true);
3018                editPressed();
3019                break;
3020
3021            case "Train":     // NOI18N
3022                _addButton.setText(Bundle.getMessage("AddStopButtonText"));  // NOI18N
3023                _addButtonPanel.setVisible(true);
3024
3025                var stops = _dataMgr.getStops(_curNodeId, 0, false);
3026                if (stops.size() == 0) {
3027                    _copyButtonPanel.setVisible(true);
3028                }
3029
3030                _duplicateButton.setText(Bundle.getMessage("DuplicateTrainButtonText"));  // NOI18N
3031                _duplicateButtonPanel.setVisible(true);
3032                _deleteButton.setText(Bundle.getMessage("DeleteTrainButtonText"));  // NOI18N
3033                _deleteButtonPanel.setVisible(true);
3034                editPressed();
3035                break;
3036
3037            case "Stop":     // NOI18N
3038                _duplicateButton.setText(Bundle.getMessage("DuplicateStopButtonText"));  // NOI18N
3039                _duplicateButtonPanel.setVisible(true);
3040                _deleteButton.setText(Bundle.getMessage("DeleteStopButtonText"));  // NOI18N
3041                _deleteButtonPanel.setVisible(true);
3042                editPressed();
3043                break;
3044
3045            default:
3046                log.warn("Should not be here");  // NOI18N
3047        }
3048    }
3049
3050    /**
3051     * Display reminder to save.
3052     */
3053    void showNodeEditMessage() {
3054        if (InstanceManager.getNullableDefault(jmri.UserPreferencesManager.class) != null) {
3055            InstanceManager.getDefault(jmri.UserPreferencesManager.class).
3056                    showInfoMessage( this, Bundle.getMessage("NodeEditTitle"), // NOI18N
3057                            Bundle.getMessage("NodeEditText"), // NOI18N
3058                            getClassName(),
3059                            "SkipTimeTableEditMessage", true, false); // NOI18N
3060        }
3061    }
3062
3063    /**
3064     * Set/clear dirty flag and save button
3065     * @param dirty True if changes have been made that are not saved.
3066     */
3067    public void setShowReminder(boolean dirty) {
3068        _isDirty = dirty;
3069        _saveButton.setEnabled(dirty);
3070    }
3071
3072    /**
3073     * Enable/disable buttons based on edit state.
3074     * The edit state controls the ability to select tree nodes.
3075     *
3076     * @param active True to make edit active, false to make edit inactive
3077     */
3078    void setEditMode(boolean active) {
3079        _editActive = active;
3080        _cancelAction.setEnabled(active);
3081        _updateAction.setEnabled(active);
3082        _addButton.setEnabled(!active);
3083        _deleteButton.setEnabled(!active);
3084        if (_curNodeType != null && _curNodeType.equals("Stop")) {  // NOI18N
3085            setMoveButtons();
3086        }
3087    }
3088
3089    /**
3090     * Timetable Tree Node Definition.
3091     */
3092    static class TimeTableTreeNode extends DefaultMutableTreeNode {
3093
3094        private String ttText;
3095        private String ttType;
3096        private int ttId;
3097        private int ttRow;
3098
3099        public TimeTableTreeNode(String nameText, String type, int sysId, int row) {
3100            this.ttText = nameText;
3101            this.ttType = type;
3102            this.ttId = sysId;
3103            this.ttRow = row;
3104        }
3105
3106        public String getType() {
3107            return ttType;
3108        }
3109
3110        public int getId() {
3111            return ttId;
3112        }
3113
3114        public void setId(int newId) {
3115            ttId = newId;
3116        }
3117
3118        public int getRow() {
3119            return ttRow;
3120        }
3121
3122        public void setRow(int newRow) {
3123            ttRow = newRow;
3124        }
3125
3126        public String getText() {
3127            return ttText;
3128        }
3129
3130        public void setText(String newText) {
3131            ttText = newText;
3132        }
3133
3134        @Override
3135        public String toString() {
3136            return ttText;
3137        }
3138    }
3139
3140    protected String getClassName() {
3141        return TimeTableFrame.class.getName();
3142    }
3143
3144    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TimeTableFrame.class);
3145}