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