001package jmri.jmrit.logix;
002
003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
004import java.awt.BorderLayout;
005import java.awt.event.ActionEvent;
006import java.awt.event.ActionListener;
007import java.util.ArrayList;
008import java.util.Iterator;
009import java.util.List;
010
011import javax.annotation.Nonnull;
012import javax.swing.AbstractAction;
013import javax.swing.Box;
014import javax.swing.BoxLayout;
015import javax.swing.JButton;
016import javax.swing.JDialog;
017import javax.swing.JMenu;
018import javax.swing.JMenuItem;
019import javax.swing.JPanel;
020import javax.swing.JScrollPane;
021import javax.swing.JTextArea;
022import jmri.BeanSetting;
023import jmri.InstanceManager;
024import jmri.InvokeOnGuiThread;
025import jmri.NamedBean;
026import jmri.Path;
027
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031/**
032 * A WarrantAction contains the operating permissions and directives needed for
033 * a train to proceed from an Origin to a Destination. WarrantTableAction
034 * provides the menu for panels to List, Edit and Create Warrants. It launches
035 * the appropriate frame for each action.
036 * <br>
037 * <hr>
038 * This file is part of JMRI.
039 * <p>
040 * JMRI is free software; you can redistribute it and/or modify it under the
041 * terms of version 2 of the GNU General Public License as published by the Free
042 * Software Foundation. See the "COPYING" file for a copy of this license.
043 * <p>
044 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY
045 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
046 * A PARTICULAR PURPOSE. See the GNU General Public License for more details.
047 *
048 * @author Pete Cressman Copyright (C) 2009, 2010
049 **/
050public class WarrantTableAction extends AbstractAction {
051
052    static int STRUT_SIZE = 10;
053    private JMenu _warrantMenu;
054    
055    private boolean _hasErrors = false;
056    private JDialog _errorDialog;
057    private WarrantFrame _openFrame;
058    private NXFrame _nxFrame;
059    private boolean _logging = false;
060    private Runnable _shutDownTask = null;
061
062    private WarrantTableAction(String menuOption) {
063        super(Bundle.getMessage(menuOption));
064    }
065
066    public static WarrantTableAction getDefault() {
067        return InstanceManager.getOptionalDefault(WarrantTableAction.class).orElseGet(() -> {
068            WarrantTableAction wta = new WarrantTableAction("ShowWarrants"); // NOI18N
069            wta.errorCheck();
070            return InstanceManager.setDefault(WarrantTableAction.class, wta);
071        });
072    }
073
074    @Override
075    @InvokeOnGuiThread
076    public void actionPerformed(ActionEvent e) {
077        WarrantTableFrame.getDefault();
078    }
079
080    /**
081     * @param edit true if portal errors should be shown in window created from
082     *             menu item
083     * @return a menu containing warrant actions
084     */
085    public JMenu makeWarrantMenu(boolean edit) {
086        if (jmri.InstanceManager.getDefault(OBlockManager.class).getNamedBeanSet().size() > 1) {
087            synchronized (this) {
088                _warrantMenu = new JMenu(Bundle.getMessage("MenuWarrant"));
089                updateWarrantMenu();
090                return _warrantMenu;
091            }
092        }
093        return null;
094    }
095
096    @InvokeOnGuiThread
097    synchronized protected void updateWarrantMenu() {
098        _warrantMenu.removeAll();
099        _warrantMenu.add(getDefault());
100        JMenu editWarrantMenu = new JMenu(Bundle.getMessage("EditWarrantMenu"));
101        _warrantMenu.add(editWarrantMenu);
102        ActionListener editWarrantAction = (ActionEvent e) -> openWarrantFrame(e.getActionCommand());
103        WarrantManager manager = InstanceManager.getDefault(WarrantManager.class);
104        if (manager.getObjectCount() == 0) { // when there are no Warrants, enter the word "None" to the submenu
105            JMenuItem _noWarrants = new JMenuItem(Bundle.getMessage("None"));
106            editWarrantMenu.add(_noWarrants);
107            // disable it
108            _noWarrants.setEnabled(false);
109        } else { // when there are warrants, add them to the submenu
110            for (Warrant warrant : manager.getNamedBeanSet()) {
111                // Warrant warrent = (Warrant) object;
112                JMenuItem mi = new JMenuItem(warrant.getDisplayName());
113                mi.setActionCommand(warrant.getDisplayName());
114                mi.addActionListener(editWarrantAction);
115                editWarrantMenu.add(mi);
116            }
117        }
118         _warrantMenu.add(new AbstractAction(Bundle.getMessage("CreateWarrant")) {
119            @Override
120            public void actionPerformed(ActionEvent e) {
121                makeWarrantFrame(null, null);
122            }
123         });
124        _warrantMenu.add(InstanceManager.getDefault(TrackerTableAction.class));
125        _warrantMenu.add(new AbstractAction(Bundle.getMessage("CreateNXWarrant")) {
126            @Override
127            public void actionPerformed(ActionEvent e) {
128                makeNXFrame();
129            }
130        });
131        _warrantMenu.add(makeLogMenu());
132
133        log.debug("updateMenu to {} warrants.", manager.getObjectCount());
134    }
135
136    protected JMenuItem makeLogMenu() {
137        JMenuItem mi;
138        if (!_logging) {
139            mi = new JMenuItem(Bundle.getMessage("startLog"));
140            mi.addActionListener((ActionEvent e) -> {
141                if (!OpSessionLog.makeLogFile(WarrantTableFrame.getDefault())) {
142                    return;
143                }
144                _logging = true;
145                _shutDownTask = () -> {
146                    OpSessionLog.close();
147                    _logging = false;
148                };
149                jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).register(_shutDownTask);
150                updateWarrantMenu();
151            });
152        } else {
153            mi = new JMenuItem(Bundle.getMessage("flushLog"));
154            mi.addActionListener((ActionEvent e) -> OpSessionLog.flush());
155            _warrantMenu.add(mi);
156            mi = new JMenuItem(Bundle.getMessage("stopLog"));
157            mi.addActionListener((ActionEvent e) -> {
158                OpSessionLog.close();
159                jmri.InstanceManager.getDefault(jmri.ShutDownManager.class).deregister(_shutDownTask);
160                _shutDownTask = null;
161                _logging = false;
162                updateWarrantMenu();
163            });
164        }
165        return mi;
166    }
167
168    synchronized protected void writetoLog(String text) {
169        if (_logging) {
170            OpSessionLog.writeLn(text);
171        }
172    }
173
174    @InvokeOnGuiThread
175    protected void closeNXFrame() {
176        if (_nxFrame != null) {
177            _nxFrame.clearTempWarrant();
178            _nxFrame.dispose();
179            _nxFrame = null;
180        }
181    }
182
183    @InvokeOnGuiThread
184    protected void makeNXFrame() {
185        if (warrantFrameRunning()) {
186            return;
187        }
188        if (_nxFrame == null) {
189            _nxFrame = new NXFrame();
190        }
191        _nxFrame.setState(java.awt.Frame.NORMAL);
192        _nxFrame.setVisible(true);
193        _nxFrame.toFront();
194    }
195
196    protected void closeWarrantFrame() {
197        if (_openFrame != null) {
198            _openFrame.close();
199            _openFrame.dispose();
200            _openFrame = null;
201        }
202    }
203
204    // check if edited warrant is running test
205    private boolean warrantFrameRunning() {
206        if (_openFrame != null) {
207            if (_openFrame.isRunning()) {
208                _openFrame.toFront();
209                return true;
210            } else {
211                closeWarrantFrame();
212            }
213        }
214        return false;
215    }
216
217    protected void makeWarrantFrame(Warrant startW, Warrant endW) {
218        if (warrantFrameRunning()) {
219            return;
220        }
221        closeNXFrame();
222        _openFrame = new WarrantFrame(startW, endW);
223        _openFrame.setState(java.awt.Frame.NORMAL);
224        _openFrame.toFront();            
225    }
226
227    protected void editWarrantFrame(Warrant w) {
228        if (warrantFrameRunning()) {
229            return;
230        }
231        closeNXFrame();
232        _openFrame = new WarrantFrame(w);
233        _openFrame.setState(java.awt.Frame.NORMAL);
234        _openFrame.toFront();            
235    }
236
237    private void openWarrantFrame(String key) {
238        Warrant w = InstanceManager.getDefault(WarrantManager.class).getWarrant(key);
239        if (w != null) {
240            editWarrantFrame(w);
241        }
242    }
243
244    synchronized public void mouseClickedOnBlock(OBlock block) {
245        if (block == null) {
246            return;
247        }
248
249        if (_openFrame != null) {
250            _openFrame.mouseClickedOnBlock(block);
251            return;
252        }
253
254        if (_nxFrame != null && _nxFrame.isVisible() && _nxFrame.isRouteSeaching()) {
255            _nxFrame.mouseClickedOnBlock(block);
256            return;
257        }
258
259        InstanceManager.getDefault(TrackerTableAction.class).mouseClickedOnBlock(block);
260    }
261
262    /* ****************** Error checking ************************/
263    public boolean errorCheck() {
264        _hasErrors = false;
265        javax.swing.JTextArea textArea = new javax.swing.JTextArea(10, 50);
266        textArea.setEditable(false);
267        textArea.setTabSize(4);
268        textArea.append(Bundle.getMessage("ErrWarnAreaMsg"));
269        textArea.append("\n\n");
270        OBlockManager manager = InstanceManager.getDefault(OBlockManager.class);
271        for (OBlock block : manager.getNamedBeanSet()) {
272            textArea.append(checkPathPortals(block));
273        }
274        return showPathPortalErrors(textArea);
275    }
276
277    /**
278     * Validation of paths within a block. Gathers messages in a text area that
279     * can be displayed after all are written.
280     *
281     * @param b the block to validate
282     * @return error/warning message, if any
283     */
284    @Nonnull
285    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
286    public String checkPathPortals(OBlock b) {
287        if (log.isDebugEnabled()) {
288            log.debug("checkPathPortals for {}", b.getDisplayName());
289        }
290        StringBuilder sb = new StringBuilder();
291        List<Path> pathList = b.getPaths();
292        if (pathList.isEmpty()) {
293            if (b.getPortals().isEmpty()) {
294                sb.append(Bundle.getMessage("NoPortals"));
295                sb.append(" ");
296            }
297            sb.append(Bundle.getMessage("NoPaths", b.getDisplayName()));
298            sb.append("\n");
299            _hasErrors = true;
300            return sb.toString();
301        }
302        List<Portal> portalList = b.getPortals();
303        // make list of names of all portals.  Then remove those we check, leaving the orphans
304        ArrayList<String> portalNameList = new ArrayList<>();
305        for (Portal portal : portalList) {
306            if (portal.getFromPaths().isEmpty()) {
307                sb.append(Bundle.getMessage("BlockPortalNoPath", portal.getName(), portal.getFromBlockName()));
308                sb.append("\n");
309                _hasErrors = true;
310                return sb.toString();
311            }
312            if (portal.getToPaths().isEmpty()) {
313                sb.append(Bundle.getMessage("BlockPortalNoPath", portal.getName(), portal.getToBlockName()));
314                sb.append("\n");
315                _hasErrors = true;
316                return sb.toString();
317            }
318            portalNameList.add(portal.getName());
319        }
320        for (Path value : pathList) {
321            OPath path = (OPath) value;
322            OBlock block = (OBlock) path.getBlock();
323            if (block == null || !block.equals(b)) {
324                sb.append(Bundle.getMessage("PathWithBadBlock", path.getName(), b.getDisplayName()));
325                sb.append("\n");
326                _hasErrors = true;
327                return sb.toString();
328            }
329            String msg = null;
330            boolean hasPortal = false;
331            Portal fromPortal = path.getFromPortal();
332            if (fromPortal != null) {
333                if (!fromPortal.isValid()) {
334                    msg = fromPortal.getName();
335                }
336                hasPortal = true;
337                portalNameList.remove(fromPortal.getName());
338            }
339            Portal toPortal = path.getToPortal();
340            if (toPortal != null) {
341                if (!toPortal.isValid()) {
342                    msg = toPortal.getName();
343                }
344                hasPortal = true;
345                portalNameList.remove(toPortal.getName());
346                if (fromPortal != null && fromPortal.equals(toPortal)) {
347                    sb.append(Bundle.getMessage("PathWithDuplicatePortal", path.getName(), b.getDisplayName()));
348                    sb.append("\n");
349                }
350            }
351            if (msg != null) {
352                sb.append(Bundle.getMessage("PortalNeedsBlock", msg));
353                sb.append("\n");
354                _hasErrors = true;
355            } else if (!hasPortal) {
356                sb.append(Bundle.getMessage("PathNeedsPortal", path.getName(), b.getDisplayName()));
357                sb.append("\n");
358                _hasErrors = true;
359            }
360            // check that the path's portals have the path in their lists
361            boolean validPath;
362            if (toPortal != null) {
363                if (fromPortal != null) {
364                    validPath = toPortal.isValidPath(path) && fromPortal.isValidPath(path);
365                } else {
366                    validPath = toPortal.isValidPath(path);
367                }
368            } else {
369                if (fromPortal != null) {
370                    validPath = fromPortal.isValidPath(path);
371                } else {
372                    validPath = false;
373                }
374            }
375            if (!validPath) {
376                sb.append(Bundle.getMessage("PathNotConnectedToPortal", path.getName(), b.getDisplayName()));
377                sb.append("\n");
378                _hasErrors = true;
379            }
380        }
381        for (String s : portalNameList) {
382            sb.append(Bundle.getMessage("BlockPortalNoPath", s, b.getDisplayName()));
383            sb.append("\n");
384            _hasErrors = true;
385        }
386        // check whether any turnouts are shared between two blocks;
387        checkSharedTurnouts(b);
388        return sb.toString();
389    }
390
391    public boolean showPathPortalErrors(JTextArea textArea) {
392        if (_errorDialog != null) {
393            _errorDialog.dispose();
394        }
395        if (!_hasErrors) {
396            return false;
397        }
398        JScrollPane scrollPane = new JScrollPane(textArea);
399        _errorDialog = new JDialog();
400        _errorDialog.setTitle(Bundle.getMessage("ErrorDialogTitle"));
401        JButton ok = new JButton(Bundle.getMessage("ButtonOK"));
402        class myListener extends java.awt.event.WindowAdapter implements ActionListener {
403
404            /*  java.awt.Window _w;
405             myListener(java.awt.Window w) {
406                 _w = w;
407             }  */
408            @Override
409            public void actionPerformed(ActionEvent e) {
410                _errorDialog.dispose();
411            }
412
413            @Override
414            public void windowClosing(java.awt.event.WindowEvent e) {
415                _errorDialog.dispose();
416            }
417        }
418        ok.addActionListener(new myListener());
419        ok.setMaximumSize(ok.getPreferredSize());
420
421        java.awt.Container contentPane = _errorDialog.getContentPane();
422        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));
423        contentPane.add(scrollPane, BorderLayout.CENTER);
424        contentPane.add(Box.createVerticalStrut(5));
425        contentPane.add(Box.createVerticalGlue());
426        JPanel panel = new JPanel();
427        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
428        panel.add(ok);
429        contentPane.add(panel, BorderLayout.SOUTH);
430        _errorDialog.addWindowListener(new myListener());
431        _errorDialog.pack();
432        _errorDialog.setVisible(true);
433        return true;
434    }
435
436    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OPath extends Path")
437    public boolean checkSharedTurnouts(OBlock block) {
438        boolean hasShared = false;
439        OBlockManager manager = InstanceManager.getDefault(OBlockManager.class);
440        List<Path> pathList = block.getPaths();
441        for (Path value : pathList) {
442            OPath path = (OPath) value;
443            for (OBlock b : manager.getNamedBeanSet()) {
444                if (block.getSystemName().equals(b.getSystemName())) {
445                    continue;
446                }
447                for (Path item : b.getPaths()) {
448                    boolean shared = sharedTO(path, (OPath) item);
449                    if (shared) {
450                        hasShared = true;
451                        break;
452                    }
453                }
454            }
455        }
456        return hasShared;
457    }
458
459    @SuppressFBWarnings(value = "BC_UNCONFIRMED_CAST_OF_RETURN_VALUE", justification = "OBlock extends Block")
460    private boolean sharedTO(OPath myPath, OPath path) {
461        List<BeanSetting> myTOs = myPath.getSettings();
462        Iterator<BeanSetting> iter = myTOs.iterator();
463        List<BeanSetting> tos = path.getSettings();
464        boolean ret = false;
465        while (iter.hasNext()) {
466            BeanSetting mySet = iter.next();
467            NamedBean myTO = mySet.getBean();
468            int myState = mySet.getSetting();
469            for (BeanSetting set : tos) {
470                NamedBean to = set.getBean();
471                if (myTO.equals(to)) {
472                    // turnouts are equal.  check if settings are compatible.
473                    OBlock myBlock = (OBlock) myPath.getBlock();
474                    int state = set.getSetting();
475                    OBlock block = (OBlock) path.getBlock();
476                    if (myState != state) {
477                        ret = myBlock.addSharedTurnout(myPath, block, path);
478                    }
479                }
480            }
481        }
482        return ret;
483    }
484
485    private final static Logger log = LoggerFactory.getLogger(WarrantTableAction.class);
486
487}