001package jmri.jmrit.logix;
002
003
004import java.awt.BorderLayout;
005import java.awt.Color;
006import java.awt.Dimension;
007import java.awt.FlowLayout;
008import java.awt.event.ActionEvent;
009import java.beans.PropertyChangeListener;
010import java.util.ArrayList;
011import java.util.HashMap;
012import java.util.List;
013import java.util.Set;
014
015import javax.annotation.Nonnull;
016import javax.swing.AbstractAction;
017import javax.swing.AbstractListModel;
018import javax.swing.Box;
019import javax.swing.BoxLayout;
020import javax.swing.ButtonGroup;
021import javax.swing.JButton;
022import javax.swing.JDialog;
023import javax.swing.JLabel;
024import javax.swing.JList;
025import javax.swing.JMenu;
026import javax.swing.JMenuBar;
027import javax.swing.JMenuItem;
028import javax.swing.JPanel;
029import javax.swing.JRadioButtonMenuItem;
030import javax.swing.JScrollPane;
031import javax.swing.JTable;
032import javax.swing.JTextField;
033import javax.swing.event.ListSelectionEvent;
034import javax.swing.event.ListSelectionListener;
035import javax.swing.table.AbstractTableModel;
036import javax.swing.table.TableRowSorter;
037
038import jmri.Block;
039import jmri.InstanceInitializer;
040import jmri.InstanceManager;
041import jmri.JmriException;
042import jmri.implementation.AbstractInstanceInitializer;
043import jmri.jmrit.display.LocoIcon;
044import jmri.jmrit.display.palette.ItemPalette;
045import jmri.jmrit.picker.PickListModel;
046import jmri.jmrit.picker.PickPanel;
047import jmri.util.JmriJFrame;
048import jmri.util.swing.JmriJOptionPane;
049import jmri.util.swing.JmriMouseEvent;
050import jmri.util.swing.JmriMouseListener;
051import jmri.util.table.ButtonEditor;
052import jmri.util.table.ButtonRenderer;
053
054import org.openide.util.lookup.ServiceProvider;
055
056/**
057 * This class displays a table of the occupancy detection trackers. It does
058 * the listening of block sensors for all the Trackers and chooses the tracker most
059 * likely to have entered a block becoming active or leaving a block when it
060 * becomes inactive.
061 *
062 * @author Peter Cressman
063 */
064public class TrackerTableAction extends AbstractAction implements PropertyChangeListener{
065
066    protected static final int STRUT_SIZE = 10;
067
068    private final ArrayList<Tracker> _trackerList = new ArrayList<>();
069    private final HashMap<OBlock, ArrayList<Tracker>> _trackerBlocks = new HashMap<>();
070    protected TableFrame _frame;
071    private ChooseTracker _trackerChooser;
072    private boolean _requirePaths;
073
074    private TrackerTableAction(String menuOption) {
075        super(menuOption);
076    }
077
078    @Override
079    public void actionPerformed(ActionEvent e) {
080        if (_frame != null) {
081            _frame.setVisible(true);
082        } else {
083            _frame = new TableFrame();
084        }
085    }
086
087    public synchronized boolean mouseClickedOnBlock(OBlock block) {
088        if (_frame != null) {
089            return _frame.mouseClickedOnBlock(block);
090        }
091        return false;
092    }
093
094    /**
095     * Create and register a new Tracker.
096     * @param block starting head block of the Tracker
097     * @param name name of the Tracker
098     * @param marker LocoIcon dropped on the block (optional)
099     * @return true if successfully created.
100     */
101    public boolean markNewTracker(OBlock block, String name, LocoIcon marker) {
102        if (_frame == null) {
103            _frame = new TableFrame();
104        }
105        if (name == null && marker != null) {
106            name = marker.getUnRotatedText();
107        }
108        return makeTracker(block, name, marker);
109    }
110
111    private boolean makeTracker(OBlock block, String name, LocoIcon marker) {
112        String msg = null;
113
114        if ((block.getState() & Block.OCCUPIED) == 0) {
115            msg = Bundle.getMessage("blockUnoccupied", block.getDisplayName());
116        } else if (name == null || name.length() == 0) {
117            msg = Bundle.getMessage("noTrainName");
118        } else if (nameInuse(name)) {
119            msg = Bundle.getMessage("duplicateName", name);
120        } else {
121            Tracker t = findTrackerIn(block);
122            if (t != null && !name.equals(block.getValue())) {
123                msg = Bundle.getMessage("blockInUse", t.getTrainName(), block.getDisplayName());
124            } else {
125                Warrant w = block.getWarrant();
126                if (w != null) {
127                    msg = Bundle.getMessage("AllocatedToWarrant",
128                            w.getDisplayName(), block.getDisplayName(), w.getTrainName());
129                }
130            }
131        }
132        if (msg != null) {
133            JmriJOptionPane.showMessageDialog(_frame, msg,
134                    Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
135            return false;
136        }
137        block.setValue(name);
138        new Tracker(block, name, marker, this);
139        return true;
140    }
141
142    protected void addTracker(Tracker t) {
143        synchronized(this) {
144            _trackerList.add(t);
145        }
146        addBlockListeners(t);
147        if (_frame == null) {
148            _frame = new TableFrame();
149        }
150        _frame._model.fireTableDataChanged();
151        _frame.setStatus(Bundle.getMessage("startTracker",
152               t.getTrainName(), t.getHeadBlock().getDisplayName()));
153    }
154
155    protected boolean checkBlock(OBlock b) {
156        if (findTrackerIn(b) == null && b.getWarrant() == null) {
157            b.setValue(null);
158            return true;
159        }
160        return false;
161    }
162
163    boolean nameInuse(String name) {
164        for (Tracker t : _trackerList) {
165            if (name.equals(t.getTrainName())) {
166                return true;
167            }
168        }
169        return false;
170    }
171
172    /**
173     * Stop a Tracker from tracking and remove from list
174     * @param t Tracker to be stopped
175     * @param b Block Tracker of its last move. Optional, for display purpose only.
176     */
177    public void stopTracker(Tracker t, OBlock b) {
178        if (_frame == null) {
179            _frame = new TableFrame();
180        }
181        stopTrain(t, b);
182    }
183
184    protected void setStatus(String msg) {
185        _frame.setStatus(msg);
186    }
187
188    /**
189     * See if any Trackers are occupying a given block.
190     * @param b Block being queried
191     * @return Tracker if found
192     */
193    public Tracker findTrackerIn(OBlock b) {
194        for (Tracker t : _trackerList) {
195            if (t.getBlocksOccupied().contains(b)) {
196                return t;
197            }
198        }
199        return null;
200    }
201
202    public void updateStatus() {
203        _frame._model.fireTableDataChanged();
204
205    }
206    /**
207     * Adds listeners to all blocks in the range of a Tracker. Called when a
208     * new tracker is created.
209     * @param tracker Tracker that is about to start
210     */
211    protected void addBlockListeners(Tracker tracker) {
212        List<OBlock> range = tracker.makeRange();
213        for (OBlock oBlock : range) {
214            addBlockListener(oBlock, tracker);
215        }
216    }
217
218    /**
219     * Adds listener to a block when a tracker enters.
220     */
221    private void addBlockListener(OBlock block, Tracker tracker) {
222        ArrayList<Tracker> trackers = _trackerBlocks.get(block);
223        if (trackers == null) {
224            trackers = new ArrayList<>();
225            trackers.add(tracker);
226            if ((block.getState() & Block.UNDETECTED) == 0) {
227                _trackerBlocks.put(block, trackers);
228                block.addPropertyChangeListener(this);
229            }
230        } else {
231            if (trackers.isEmpty()) {
232                if ((block.getState() & Block.UNDETECTED) == 0) {
233                    block.addPropertyChangeListener(this);
234                }
235            }
236            if (!trackers.contains(tracker)) {
237                trackers.add(tracker);
238            }
239        }
240    }
241
242    /**
243     * Do Venn Diagram between the two sets. Keep listeners held in common.
244     * Add new listeners. Remove old.
245     */
246    private void adjustBlockListeners(List<OBlock> oldRange, List<OBlock> newRange, Tracker tracker) {
247         for (OBlock b : newRange) {
248            if (oldRange.contains(b)) {
249                oldRange.remove(b);
250                continue; // held in common. keep listener
251            }
252            addBlockListener(b, tracker);       // new block.  Add Listener
253        }
254        // blocks left in oldRange that were not found in newRange.  Remove Listeners
255        for (OBlock b :oldRange) {
256            removeBlockListener(b, tracker);
257        }
258
259    }
260
261    protected void removeBlockListeners(Tracker tracker) {
262        for (OBlock block : _trackerBlocks.keySet()) {
263            removeBlockListener(block, tracker);
264        }
265    }
266
267    private void removeBlockListener(OBlock block, Tracker tracker) {
268        List<Tracker> trackers = _trackerBlocks.get(block);
269        if (trackers != null) {
270            trackers.remove(tracker);
271            if (trackers.isEmpty()) {
272                block.removePropertyChangeListener(this);
273            }
274        }
275    }
276
277    @Override
278    public synchronized void propertyChange(java.beans.PropertyChangeEvent evt) {
279        if (evt.getPropertyName().equals("state")) {
280            OBlock block = (OBlock) evt.getSource();
281            int state = ((Number) evt.getNewValue()).intValue();
282            int oldState = ((Number) evt.getOldValue()).intValue();
283            // The "jiggle" (see tracker.showBlockValue() causes some state changes to be duplicated.
284            // The following washes out the extra notifications
285            if ((state & Block.UNOCCUPIED) == (oldState & Block.UNOCCUPIED)
286                    && (state & Block.OCCUPIED) == (oldState & Block.OCCUPIED)) {
287                return;
288            }
289            ArrayList<Tracker> trackerListeners = _trackerBlocks.get(block);
290            if (trackerListeners == null || trackerListeners.isEmpty()) {
291                log.error("No Trackers found for block \"{}\" going to state= {}",
292                        block.getDisplayName(), state);
293                block.removePropertyChangeListener(this);
294                return;
295            }
296            if ((state & Block.OCCUPIED) != 0) {   // going occupied
297                List<Tracker> trackers = getAvailableTrackers(block);
298                if (trackers.isEmpty()) {
299                    return;
300                }
301                if (trackers.size() > 1) { // if several trackers listen for this block, user must identify which one.
302                    if (_trackerChooser != null) {
303                        _trackerChooser.dispose();
304                    }
305                    java.awt.Toolkit.getDefaultToolkit().beep();
306                    _trackerChooser = new ChooseTracker(block, trackers, state);
307                    return;
308                }
309
310                Tracker tracker = trackers.get(0);
311                if (block.getValue() != null &&  !block.getValue().equals(tracker.getTrainName())) {
312                    log.error("Block \"{} \" going active with value= {} for Tracker {}! Who/What is \"{}\"?",
313                            block.getDisplayName(), block.getValue(), tracker.getTrainName(), block.getValue());
314                    return;
315               } else {
316                   if (!_requirePaths) {
317                       try {
318                           tracker.hasPathInto(block);
319                       } catch (JmriException je) {
320                           log.error("Exception handling {} {}", tracker.getTrainName(), je.getMessage());
321                           return;
322                       }
323                   }
324                   processTrackerStateChange(tracker, block, state);
325               }
326            } else if ((state & Block.UNOCCUPIED) != 0) {
327                if (_trackerChooser != null) {
328                    _trackerChooser.checkClose(block);
329                }
330                for (Tracker t : trackerListeners) {
331                    if (t.getBlocksOccupied().contains(block)) {
332                        processTrackerStateChange(t, block, state);
333                        break;
334                    }
335                }
336            }
337        }
338        _frame._model.fireTableDataChanged();
339    }
340
341    private List<Tracker> getAvailableTrackers(OBlock block) {
342        List<Tracker> trackers = new ArrayList<>();
343        ArrayList<Tracker> trackerListeners = _trackerBlocks.get(block);
344        if (_requirePaths) {
345            ArrayList<Tracker> partials = new ArrayList<>();
346            // filter for trackers with paths set into block
347            for (Tracker t : trackerListeners) {
348                try {
349                    switch (t.hasPathInto(block)) {
350                        case SET:
351                            trackers.add(t);
352                            break;
353                        case PARTIAL:
354                            partials.add(t);
355                            break;
356                        default:
357                            break;
358                    }
359                } catch (JmriException je) {
360                    log.error("train: {} {}", t.getTrainName(), je.getMessage());
361                }
362            }
363            if (trackers.isEmpty()) {   // nobody has paths set.
364                // even so, likely to be possible for somebody to get there
365                if (!partials.isEmpty()) {
366                    trackers = partials; // OK, maybe not all switches are lined up
367                } else {
368                    trackers = trackerListeners; // maybe even this bad.
369                }
370            }
371        } else {
372            trackers = trackerListeners;
373        }
374        return trackers;
375    }
376    /**
377     * Called when a state change has occurred for one the blocks listened
378     * to for this tracker. Tracker.move makes the changes to OBlocks to
379     * indicate the new occupancy positions of the train. Upon return,
380     * update the listeners for the trains next move.
381     */
382    private synchronized void processTrackerStateChange(Tracker tracker, OBlock block, int state) {
383        List<OBlock> oldRange = tracker.makeRange();// total range in effect when state change was detected
384        if (tracker.move(block, state)) {   // new total range has been made after move was done.
385            if (tracker._statusMessage != null) {
386                _frame.setStatus(tracker._statusMessage);
387            } else {
388                block._entryTime = System.currentTimeMillis();
389                adjustBlockListeners(oldRange, tracker.makeRange(), tracker);
390                _frame.setStatus(Bundle.getMessage("TrackerBlockEnter",
391                        tracker.getTrainName(), block.getDisplayName()));
392            }
393        } else {
394            if (tracker._statusMessage != null) {
395                _frame.setStatus(tracker._statusMessage);
396            } else if (_trackerList.contains(tracker)) {
397                adjustBlockListeners(oldRange, tracker.makeRange(), tracker);
398                long et = (System.currentTimeMillis() - block._entryTime) / 1000;
399                _frame.setStatus(Bundle.getMessage("TrackerBlockLeave", tracker.getTrainName(),
400                        block.getDisplayName(), et / 60, et % 60));
401            }
402        }
403    }
404
405    private void stopTrain(Tracker t, OBlock b) {
406        t.stop();
407        removeBlockListeners(t);
408        synchronized(this) {
409            _trackerList.remove(t);
410        }
411        long et = (System.currentTimeMillis() - t._startTime) / 1000;
412        String location;
413        if (b!= null) {
414            location = b.getDisplayName();
415        } else {
416            location = Bundle.getMessage("BeanStateUnknown");
417        }
418        _frame.setStatus(Bundle.getMessage("TrackerStopped",
419                t.getTrainName(), location, et / 60, et % 60));
420        _frame._model.fireTableDataChanged();
421    }
422
423    class ChooseTracker extends JDialog implements ListSelectionListener {
424        OBlock block;
425        List<Tracker> trackers;
426        int state;
427        JList<Tracker> _jList;
428
429        ChooseTracker(OBlock b, List<Tracker> ts, int s) {
430            super(_frame);
431            setTitle(Bundle.getMessage("TrackerTitle"));
432            block = b;
433            trackers = ts;
434            state = s;
435            JPanel contentPanel = new JPanel();
436            contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
437
438            contentPanel.add(Box.createVerticalStrut(STRUT_SIZE));
439            JPanel panel = new JPanel();
440            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
441            panel.add(new JLabel(Bundle.getMessage("MultipleTrackers", block.getDisplayName())));
442            panel.add(new JLabel(Bundle.getMessage("ChooseTracker", block.getDisplayName())));
443            JPanel p = new JPanel();
444            p.add(panel);
445            contentPanel.add(p);
446            panel = new JPanel();
447            panel.setBorder(javax.swing.BorderFactory .createLineBorder(Color.black, 2));
448            _jList = new JList<>();
449            _jList.setModel(new TrackerListModel());
450            _jList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
451            _jList.addListSelectionListener(ChooseTracker.this);
452            panel.add(_jList);
453            p = new JPanel();
454            p.add(panel);
455            contentPanel.add(p);
456
457            contentPanel.add(Box.createVerticalStrut(STRUT_SIZE));
458            panel = new JPanel();
459            JButton cancelButton = new JButton(Bundle.getMessage("ButtonCancel"));
460            cancelButton.addActionListener((ActionEvent a) -> dispose());
461            panel.add(cancelButton);
462
463            contentPanel.add(panel);
464            setContentPane(contentPanel);
465            pack();
466            setLocation(_frame.getLocation());
467            setAlwaysOnTop(true);
468            setVisible(true);
469        }
470
471        @Override
472        public void valueChanged(ListSelectionEvent e) {
473            Tracker tr = _jList.getSelectedValue();
474            if (tr != null) {
475                processTrackerStateChange(tr, block, state);
476                dispose();
477            }
478        }
479
480        void checkClose(OBlock b) {
481            if (block.equals(b)) {
482                dispose();
483            }
484        }
485
486        class TrackerListModel extends AbstractListModel<Tracker> {
487            @Override
488            public int getSize() {
489                return trackers.size();
490            }
491
492            @Override
493            public Tracker getElementAt(int index) {
494                return trackers.get(index);
495            }
496        }
497    }
498
499    /**
500     * Holds a table of Trackers that follow adjacent occupancy. Needs to be a
501     * singleton to be opened and closed for trackers to report to it.
502     *
503     * @author Peter Cressman
504     */
505    class TableFrame extends JmriJFrame implements JmriMouseListener {
506
507        private final TrackerTableModel _model;
508        private JmriJFrame _pickFrame;
509        JDialog _dialog;
510        JTextField _trainNameBox = new JTextField(30);
511        JTextField _trainLocationBox = new JTextField(30);
512        JTextField _status = new JTextField(80);
513        ArrayList<String> _statusHistory = new ArrayList<>();
514        public int _maxHistorySize = 20;
515
516        TableFrame() {
517            super(true, true);
518            setTitle(Bundle.getMessage("TrackerTable"));
519            _model = new TrackerTableModel();
520            JTable table = new JTable(_model);
521            TableRowSorter<TrackerTableModel> sorter = new TableRowSorter<>(_model);
522            table.setRowSorter(sorter);
523            table.getColumnModel().getColumn(TrackerTableModel.STOP_COL).setCellEditor(new ButtonEditor(new JButton()));
524            table.getColumnModel().getColumn(TrackerTableModel.STOP_COL).setCellRenderer(new ButtonRenderer());
525            for (int i = 0; i < _model.getColumnCount(); i++) {
526                int width = _model.getPreferredWidth(i);
527                table.getColumnModel().getColumn(i).setPreferredWidth(width);
528            }
529            table.setDragEnabled(true);
530            table.setTransferHandler(new jmri.util.DnDTableExportHandler());
531            JScrollPane tablePane = new JScrollPane(table);
532            Dimension dim = table.getPreferredSize();
533            int height = new JButton("STOPIT").getPreferredSize().height;
534            dim.height = height * 2;
535            tablePane.getViewport().setPreferredSize(dim);
536
537            JPanel tablePanel = new JPanel();
538            tablePanel.setLayout(new BoxLayout(tablePanel, BoxLayout.Y_AXIS));
539            JLabel title = new JLabel(Bundle.getMessage("TrackerTable"));
540            tablePanel.add(title, BorderLayout.NORTH);
541            tablePanel.add(tablePane, BorderLayout.CENTER);
542
543            JPanel panel = new JPanel();
544            JPanel p = new JPanel();
545            p.add(new JLabel(Bundle.getMessage("lastEvent")));
546            p.add(_status);
547            _status.setEditable(false);
548            _status.setBackground(Color.white);
549            _status.addMouseListener(JmriMouseListener.adapt(TableFrame.this));
550            panel.add(p);
551
552            tablePanel.add(makeButtonPanel(), BorderLayout.CENTER);
553            tablePanel.add(panel, BorderLayout.CENTER);
554
555            setContentPane(tablePanel);
556
557            JMenuBar menuBar = new JMenuBar();
558            JMenu optionMenu = new JMenu(Bundle.getMessage("MenuMoreOptions"));
559            optionMenu.add(makePathRequirement());
560
561            JMenuItem pickerMenu = new JMenuItem(Bundle.getMessage("MenuBlockPicker"));
562            pickerMenu.addActionListener((ActionEvent a) -> openPickList());
563            optionMenu.add(pickerMenu);
564
565            optionMenu.add(WarrantTableAction.getDefault().makeLogMenu());
566            menuBar.add(optionMenu);
567            setJMenuBar(menuBar);
568            addHelpMenu("package.jmri.jmrit.logix.Tracker", true);
569
570            addWindowListener(new java.awt.event.WindowAdapter() {
571                @Override
572                public void windowClosing(java.awt.event.WindowEvent e) {
573                    setDefaultCloseOperation(javax.swing.WindowConstants.HIDE_ON_CLOSE);
574                    if (_pickFrame != null) {
575                        _pickFrame.dispose();
576                    }
577                    _model.fireTableDataChanged();
578                }
579            });
580            setLocation(0, 100);
581            setVisible(true);
582            pack();
583        }
584
585        private JPanel makeButtonPanel() {
586            JPanel panel = new JPanel();
587            JButton button = new JButton(Bundle.getMessage("MenuNewTracker", "..."));
588            button.addActionListener((ActionEvent a) -> newTrackerDialog());
589            panel.add(button);
590
591            button = new JButton(Bundle.getMessage("MenuRefresh"));
592            button.addActionListener((ActionEvent a) -> _model.fireTableDataChanged());
593            panel.add(button);
594
595            return panel;
596        }
597
598
599
600        private JMenuItem makePathRequirement() {
601            JMenu pathkMenu = new JMenu(Bundle.getMessage("MenuPathRanking"));
602            ButtonGroup pathButtonGroup = new ButtonGroup();
603            JRadioButtonMenuItem r;
604            r = new JRadioButtonMenuItem(Bundle.getMessage("showAllTrackers"));
605            r.addActionListener((ActionEvent e) -> _requirePaths = false);
606            pathButtonGroup.add(r);
607            r.setSelected(!_requirePaths);
608            pathkMenu.add(r);
609
610            r = new JRadioButtonMenuItem(Bundle.getMessage("showMostLikely"));
611            r.addActionListener((ActionEvent e) -> _requirePaths = true);
612            pathButtonGroup.add(r);
613            r.setSelected(_requirePaths);
614            pathkMenu.add(r);
615
616            return pathkMenu;
617        }
618
619        protected boolean mouseClickedOnBlock(OBlock block) {
620            if (_dialog != null) {
621                if ((block.getState() & Block.OCCUPIED) != 0 && block.getValue() != null) {
622                    markNewTracker(block, (String) block.getValue(), null);
623                    return true;
624                }
625                _trainLocationBox.setText(block.getDisplayName());
626                if (block.getValue() != null) {
627                    _trainNameBox.setText((String) block.getValue());
628                }
629                return true;
630            }
631            return false;
632        }
633
634        private void newTrackerDialog() {
635            _dialog = new JDialog(this, Bundle.getMessage("MenuNewTracker", ""), false);
636            JPanel panel = new JPanel();
637            panel.setLayout(new BorderLayout(10, 10));
638            JPanel mainPanel = new JPanel();
639            mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
640
641            mainPanel.add(Box.createVerticalStrut(STRUT_SIZE));
642            JPanel p = new JPanel();
643            p.add(new JLabel(Bundle.getMessage("createTracker")));
644            mainPanel.add(p);
645
646            mainPanel.add(Box.createVerticalStrut(STRUT_SIZE));
647            mainPanel.add(makeTrackerNamePanel());
648            mainPanel.add(Box.createVerticalStrut(STRUT_SIZE));
649            mainPanel.add(makeDoneButtonPanel());
650            panel.add(mainPanel);
651            _dialog.getContentPane().add(panel);
652            _dialog.setLocation(this.getLocation().x + 100, this.getLocation().y + 100);
653            _dialog.pack();
654            _dialog.setVisible(true);
655        }
656
657        private JPanel makeTrackerNamePanel() {
658            _trainNameBox.setText("");
659            _trainLocationBox.setText("");
660            JPanel namePanel = new JPanel();
661            namePanel.setLayout(new BoxLayout(namePanel, BoxLayout.Y_AXIS));
662            JPanel p = new JPanel();
663            p.setLayout(new java.awt.GridBagLayout());
664            java.awt.GridBagConstraints c = new java.awt.GridBagConstraints();
665            c.gridwidth = 1;
666            c.gridheight = 1;
667            c.gridx = 0;
668            c.gridy = 0;
669            c.anchor = java.awt.GridBagConstraints.EAST;
670            p.add(new JLabel(Bundle.getMessage("TrainName")), c);
671            c.gridy = 1;
672            p.add(new JLabel(Bundle.getMessage("TrainLocation")), c);
673            c.gridx = 1;
674            c.gridy = 0;
675            c.anchor = java.awt.GridBagConstraints.WEST;
676            c.weightx = 1.0;
677            c.fill = java.awt.GridBagConstraints.HORIZONTAL;  // text field will expand
678            p.add(_trainNameBox, c);
679            _trainNameBox.setToolTipText(Bundle.getMessage("TrackerNameTip"));
680            c.gridy = 1;
681            p.add(_trainLocationBox, c);
682            _trainLocationBox.setToolTipText(Bundle.getMessage("TrackerLocTip"));
683            namePanel.add(p);
684            return namePanel;
685        }
686
687        private JPanel makeDoneButtonPanel() {
688            JPanel buttonPanel = new JPanel();
689            JPanel panel0 = new JPanel();
690            panel0.setLayout(new FlowLayout());
691            JButton doneButton;
692            doneButton = new JButton(Bundle.getMessage("ButtonCreate"));
693            doneButton.addActionListener((ActionEvent a) -> {
694                    if (doDoneAction()) {
695                        _dialog.dispose();
696                        _dialog = null;
697                    }
698                });
699            panel0.add(doneButton);
700            buttonPanel.add(panel0);
701            return buttonPanel;
702        }
703
704        private boolean doDoneAction() {
705            boolean retOK = false;
706            String blockName = _trainLocationBox.getText();
707            if (blockName != null) {
708                OBlock block = InstanceManager.getDefault(OBlockManager.class).getOBlock(blockName);
709                if (block == null) {
710                    JmriJOptionPane.showMessageDialog(this, Bundle.getMessage("BlockNotFound", blockName),
711                            Bundle.getMessage("WarningTitle"), JmriJOptionPane.WARNING_MESSAGE);
712                } else {
713                    retOK = makeTracker(block, _trainNameBox.getText(), null);
714                }
715            }
716            return retOK;
717        }
718
719        void openPickList() {
720            _pickFrame = new JmriJFrame();
721            JPanel content = new JPanel();
722            content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
723
724            JPanel blurb = new JPanel();
725            blurb.setLayout(new BoxLayout(blurb, BoxLayout.Y_AXIS));
726            blurb.add(Box.createVerticalStrut(ItemPalette.STRUT_SIZE));
727            blurb.add(new JLabel(Bundle.getMessage("DragBlockName")));
728            blurb.add(Box.createVerticalStrut(ItemPalette.STRUT_SIZE));
729            JPanel panel = new JPanel();
730            panel.add(blurb);
731            content.add(panel);
732
733            PickListModel<?>[] models = {PickListModel.oBlockPickModelInstance()};
734            content.add(new PickPanel(models));
735
736            _pickFrame.setContentPane(content);
737            _pickFrame.setLocationRelativeTo(this);
738            _pickFrame.toFront();
739            _pickFrame.setVisible(true);
740            _pickFrame.pack();
741        }
742
743        @Override
744        public void mouseClicked(JmriMouseEvent event) {
745            javax.swing.JPopupMenu popup = new javax.swing.JPopupMenu();
746            for (int i = _statusHistory.size() - 1; i >= 0; i--) {
747                popup.add(_statusHistory.get(i));
748            }
749            popup.show(_status, 0, 0);
750        }
751
752        @Override
753        public void mousePressed(JmriMouseEvent event) {
754           // only handling mouseClicked
755        }
756
757        @Override
758        public void mouseEntered(JmriMouseEvent event) {
759            // only handling mouseClicked
760        }
761
762        @Override
763        public void mouseExited(JmriMouseEvent event) {
764            // only handling mouseClicked
765        }
766
767        @Override
768        public void mouseReleased(JmriMouseEvent event) {
769            // only handling mouseClicked
770        }
771
772        private void setStatus(String msg) {
773            _status.setText(msg);
774            if (msg != null && msg.length() > 0) {
775                WarrantTableAction.getDefault().writetoLog(msg);
776                _statusHistory.add(msg);
777                while (_statusHistory.size() > _maxHistorySize) {
778                    _statusHistory.remove(0);
779                }
780            }
781        }
782
783        // For test purposes. The Window close action is set to hide the Frame, not dispose.
784        @Override
785        public void dispose(){
786            if ( _pickFrame != null) {
787                _pickFrame.dispose();
788            }
789            if ( _dialog != null) {
790                _dialog.dispose();
791            }
792            super.dispose();
793        }
794
795    }
796
797    private class TrackerTableModel extends AbstractTableModel {
798
799        public static final int NAME_COL = 0;
800        public static final int STATUS_COL = 1;
801        public static final int STOP_COL = 2;
802        public static final int NUMCOLS = 3;
803
804        public TrackerTableModel() {
805            super();
806        }
807
808        @Override
809        public int getColumnCount() {
810            return NUMCOLS;
811        }
812
813        @Override
814        public synchronized int getRowCount() {
815            return _trackerList.size();
816        }
817
818        @Override
819        public String getColumnName(int col) {
820            switch (col) {
821                case NAME_COL:
822                    return Bundle.getMessage("TrainName");
823                case STATUS_COL:
824                    return Bundle.getMessage("status");
825                default:
826                    // fall out
827                    break;
828            }
829            return "";
830        }
831
832        @Override
833        public Object getValueAt(int rowIndex, int columnIndex) {
834            switch (columnIndex) {
835                case NAME_COL:
836                    return _trackerList.get(rowIndex).getTrainName();
837                case STATUS_COL:
838                    return _trackerList.get(rowIndex).getStatus();
839                case STOP_COL:
840                    return Bundle.getMessage("Stop");
841                default:
842                    // fall out
843                    break;
844            }
845            return "";
846        }
847
848        @Override
849        public void setValueAt(Object value, int row, int col) {
850            if (col == STOP_COL) {
851                Tracker t = _trackerList.get(row);
852                stopTrain(t, t.getHeadBlock());
853                fireTableDataChanged();
854            }
855        }
856
857        @Override
858        public boolean isCellEditable(int row, int col) {
859            return (col == STOP_COL);
860        }
861
862        @Override
863        public Class<?> getColumnClass(int col) {
864            if (col == STOP_COL) {
865                return JButton.class;
866            }
867            return String.class;
868        }
869
870        public int getPreferredWidth(int col) {
871            switch (col) {
872                case NAME_COL:
873                    return new JTextField(20).getPreferredSize().width;
874                case STATUS_COL:
875                    return new JTextField(60).getPreferredSize().width;
876                case STOP_COL:
877                    return new JButton("STOPIT").getPreferredSize().width;
878                default:
879                    // fall out
880                    break;
881            }
882            return 5;
883        }
884
885    }
886
887    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TrackerTableAction.class);
888
889    @ServiceProvider(service = InstanceInitializer.class)
890    public static class Initializer extends AbstractInstanceInitializer {
891
892        @Override
893        @Nonnull
894        public <T> Object getDefault(Class<T> type) {
895            if (type.equals(TrackerTableAction.class)) {
896                return new TrackerTableAction(Bundle.getMessage("MenuTrackers"));
897            }
898            return super.getDefault(type);
899        }
900
901        @Override
902        @Nonnull
903        public Set<Class<?>> getInitalizes() {
904            Set<Class<?>> set = super.getInitalizes();
905            set.add(TrackerTableAction.class);
906            return set;
907        }
908    }
909
910}