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