001package jmri.jmrit.logix;
002
003import java.awt.Component;
004import java.awt.Dimension;
005import java.awt.Font;
006import java.awt.event.ActionEvent;
007
008import java.util.ArrayList;
009import java.util.HashMap;
010import java.util.Iterator;
011import java.util.Map;
012import java.util.TreeMap;
013
014import javax.annotation.CheckForNull;
015import javax.annotation.Nonnull;
016
017import javax.swing.Box;
018import javax.swing.BoxLayout;
019import javax.swing.JButton;
020import javax.swing.JDialog;
021import javax.swing.JLabel;
022import javax.swing.JPanel;
023import javax.swing.JScrollPane;
024import javax.swing.JTable;
025import javax.swing.JTextField;
026import javax.swing.SwingConstants;
027import javax.swing.table.DefaultTableCellRenderer;
028
029import jmri.InstanceManager;
030import jmri.jmrit.beantable.EnablingCheckboxRenderer;
031import jmri.jmrit.roster.Roster;
032import jmri.jmrit.roster.RosterEntry;
033import jmri.jmrit.roster.RosterSpeedProfile;
034import jmri.jmrit.roster.RosterSpeedProfile.SpeedStep;
035import jmri.util.table.ButtonEditor;
036
037/**
038 * Prompts user to select SpeedProfile to write to Roster
039 *
040 * @author Pete Cressman Copyright (C) 2017
041 */
042public class MergePrompt extends JDialog {
043
044    private final Map<String, Boolean> _candidates;   // merge candidate choices
045//    HashMap<String, RosterSpeedProfile> _mergeProfiles;  // candidate's speedprofile
046    private final Map<String, Map<Integer, Boolean>> _anomalyMap;
047    private JPanel _viewPanel;
048    private static final int STRUT = 20;
049
050    MergePrompt(String name, Map<String, Boolean> cand, Map<String, Map<Integer, Boolean>> anomalies) {
051        super();
052        _candidates = cand;
053        _anomalyMap = anomalies;
054        setTitle(name);
055        setModalityType(java.awt.Dialog.ModalityType.APPLICATION_MODAL);
056        addWindowListener(new java.awt.event.WindowAdapter() {
057            @Override
058            public void windowClosing(java.awt.event.WindowEvent e) {
059                noMerge();
060                dispose();
061            }
062        });
063
064        MergeTableModel model = new MergeTableModel(cand);
065        JTable table = new JTable(model);
066
067        table.setDefaultRenderer(Boolean.class, new EnablingCheckboxRenderer());
068        table.getColumnModel().getColumn(MergeTableModel.VIEW_COL).setCellEditor(new ButtonEditor(new JButton()));
069        table.getColumnModel().getColumn(MergeTableModel.VIEW_COL).setCellRenderer(new ButtonCellRenderer());
070
071        int tablewidth = 0;
072        for (int i = 0; i < model.getColumnCount(); i++) {
073            int width = model.getPreferredWidth(i);
074            table.getColumnModel().getColumn(i).setPreferredWidth(width);
075            tablewidth += width;
076        }
077        int rowHeight = new JButton("VIEW").getPreferredSize().height;
078        table.setRowHeight(rowHeight);
079        JPanel description = new JPanel();
080        JLabel label = new JLabel(Bundle.getMessage("MergePrompt"));
081        label.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
082        description.add(label);
083
084        JPanel panel = new JPanel();
085        panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
086        JButton button = new JButton(Bundle.getMessage("ButtonNoMerge"));
087        button.addActionListener((ActionEvent evt) -> {
088            noMerge();
089            dispose();
090        });
091        panel.add(button);
092        panel.add(Box.createHorizontalStrut(STRUT));
093        button = new JButton(Bundle.getMessage("ButtonMerge"));
094        button.addActionListener((ActionEvent evt) -> dispose());
095        panel.add(button);
096        panel.add(Box.createHorizontalStrut(STRUT));
097        button = new JButton(Bundle.getMessage("ButtonCloseView"));
098        button.addActionListener((ActionEvent evt) -> {
099            if (_viewPanel != null) {
100                getContentPane().remove(_viewPanel);
101            }
102            pack();
103        });
104        panel.add(button);
105
106        JScrollPane pane = new JScrollPane(table);
107        pane.setPreferredSize(new Dimension(tablewidth, tablewidth));
108
109        JPanel mainPanel = new JPanel();
110        mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
111        mainPanel.add(description);
112        mainPanel.add(pane);
113        if (_anomalyMap != null && !_anomalyMap.isEmpty()) {
114            mainPanel.add(makeAnomalyPanel());
115        }
116        mainPanel.add(panel);
117
118        JPanel p = new JPanel();
119        p.setLayout(new BoxLayout(p, BoxLayout.LINE_AXIS));
120        p.add(Box.createHorizontalStrut(STRUT));
121        p.add(Box.createHorizontalGlue());
122        p.add(mainPanel);
123        p.add(Box.createHorizontalGlue());
124        p.add(Box.createHorizontalStrut(STRUT));
125
126        JPanel contentPane = new JPanel();
127        contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS));
128        contentPane.add(p);
129        setContentPane(contentPane);
130        pack();
131        Dimension screen = getToolkit().getScreenSize();
132        setLocation(screen.width / 3, screen.height / 4);
133        setAlwaysOnTop(true);
134    }
135
136    private void noMerge() {
137        for (Map.Entry<String, Boolean> ent : _candidates.entrySet()) {
138            _candidates.put(ent.getKey(), false);
139        }
140    }
141
142    static JPanel makeEditInfoPanel(RosterEntry entry) {
143        JPanel panel = new JPanel();
144        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
145        JLabel label = new JLabel(Bundle.getMessage("viewTitle", entry.getId()));
146        label.setAlignmentX(Component.CENTER_ALIGNMENT);
147        panel.add(label);
148        label = new JLabel(Bundle.getMessage("deletePrompt1"));
149        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
150        label.setForeground(java.awt.Color.RED);
151        label.setAlignmentX(Component.CENTER_ALIGNMENT);
152        panel.add(label);
153        label = new JLabel(Bundle.getMessage("deletePrompt2"));
154        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
155        label.setAlignmentX(Component.CENTER_ALIGNMENT);
156        panel.add(label);
157        label = new JLabel(Bundle.getMessage("deletePrompt3"));
158        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
159        label.setAlignmentX(Component.CENTER_ALIGNMENT);
160        panel.add(label);
161        return panel;
162    }
163
164    static JPanel makeAnomalyPanel() {
165        JPanel panel = new JPanel();
166        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
167        JLabel l = new JLabel(Bundle.getMessage("anomalyPrompt"));
168        l.setForeground(java.awt.Color.RED);
169        l.setAlignmentX(Component.CENTER_ALIGNMENT);
170        panel.add(l);
171        return panel;
172    }
173
174    static JPanel makeSpeedProfilePanel(String title, RosterSpeedProfile profile, 
175                boolean edit, Map<Integer, Boolean> anomalies) {
176        JPanel panel = new JPanel();
177        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
178        panel.add(new JLabel(Bundle.getMessage(title)));
179        SpeedProfilePanel speedPanel = new SpeedProfilePanel(profile, edit, anomalies);
180        panel.add(speedPanel);
181        return panel;
182    }
183
184    /**
185     * Check that non zero value are ascending for both forward and reverse
186     * speeds. Omit anomalies.
187     *
188     * @param speedProfile speedProfile
189     * @return Map of Key and direction of possible errors (anomalies)
190     */
191    @Nonnull
192    public static Map<Integer, Boolean> validateSpeedProfile(@CheckForNull RosterSpeedProfile speedProfile) {
193        // do forward speeds, then reverse
194        HashMap<Integer, Boolean> anomalies = new HashMap<>();
195        if (speedProfile == null) {
196            return anomalies;
197        }
198        TreeMap<Integer, SpeedStep> rosterTree = speedProfile.getProfileSpeeds();
199        float lastForward = 0;
200        Integer lastKey = 0;
201        Iterator<Map.Entry<Integer, SpeedStep>> iter = rosterTree.entrySet().iterator();
202        while (iter.hasNext()) {
203            Map.Entry<Integer, SpeedStep> entry = iter.next();
204            float forward = entry.getValue().getForwardSpeed();
205            Integer key = entry.getKey();
206            if (forward > 0.0f) {
207                if (forward < lastForward) {  // anomaly found
208                    while (iter.hasNext()) {
209                        Map.Entry<Integer, SpeedStep> nextEntry = iter.next();
210                        float nextForward = nextEntry.getValue().getForwardSpeed();
211                        if (nextForward > 0.0f) {
212                            if (nextForward > lastForward) {    // remove forward
213                                anomalies.put(key, true);
214                                forward = nextForward;
215                                key = nextEntry.getKey();
216                            } else {    // remove lastForward
217                                anomalies.put(lastKey, true);
218                            }
219                            break;
220                        }
221                    }
222                }
223                lastForward = forward;
224                lastKey = key;
225            }
226        }
227
228        rosterTree = speedProfile.getProfileSpeeds();
229        float lastReverse = 0;
230        lastKey = 0;
231        iter = rosterTree.entrySet().iterator();
232        while (iter.hasNext()) {
233            Map.Entry<Integer, SpeedStep> entry = iter.next();
234            float reverse = entry.getValue().getReverseSpeed();
235            Integer key = entry.getKey();
236            if (reverse > 0.0f) {
237                if (reverse < lastReverse) {  // anomaly found
238                    while (iter.hasNext()) {
239                        Map.Entry<Integer, SpeedStep> nextEntry = iter.next();
240                        float nextreverse = nextEntry.getValue().getReverseSpeed();
241                        if (nextreverse > 0.0f) {
242                            if (nextreverse > lastReverse) {    // remove reverse
243                                anomalies.put(key, false);
244                                reverse = nextreverse;
245                                key = nextEntry.getKey();
246                            } else {    // remove lastReverse
247                                anomalies.put(lastKey, false);
248                            }
249                            break;
250                        }
251                    }
252                }
253                lastReverse = reverse;
254                lastKey = key;
255            }
256        }
257        return anomalies;
258    }
259
260    private class MergeTableModel extends javax.swing.table.AbstractTableModel {
261
262        static final int MERGE_COL = 0;
263        static final int ID_COL = 1;
264        static final int VIEW_COL = 2;
265        static final int NUMCOLS = 3;
266
267        final ArrayList<Map.Entry<String, Boolean>> candidateArray = new ArrayList<>();
268
269        MergeTableModel(Map<String, Boolean> map) {
270            Iterator<Map.Entry<String, Boolean>> iter = map.entrySet().iterator();
271            while (iter.hasNext()) {
272                candidateArray.add(iter.next());
273            }
274        }
275
276        boolean hasAnomaly(int row) {
277            Map.Entry<String, Boolean> entry = candidateArray.get(row);
278            Map<Integer, Boolean> anomaly = _anomalyMap.get(entry.getKey());
279            return(anomaly != null && !anomaly.isEmpty());
280        }
281
282        @Override
283        public int getColumnCount() {
284            return NUMCOLS;
285        }
286
287        @Override
288        public int getRowCount() {
289            return candidateArray.size();
290        }
291
292        @Override
293        public String getColumnName(int col) {
294            switch (col) {
295                case MERGE_COL:
296                    return Bundle.getMessage("Merge");
297                case ID_COL:
298                    return Bundle.getMessage("TrainId");
299                case VIEW_COL:
300                    return Bundle.getMessage("SpeedProfiles");
301                default:
302                    // fall out
303                    break;
304            }
305            return "";
306        }
307
308        @Override
309        public Class<?> getColumnClass(int col) {
310            switch (col) {
311                case MERGE_COL:
312                    return Boolean.class;
313                case ID_COL:
314                    return String.class;
315                case VIEW_COL:
316                    return JButton.class;
317                default:
318                    break;
319            }
320            return String.class;
321        }
322
323        public int getPreferredWidth(int col) {
324            switch (col) {
325                case MERGE_COL:
326                    return new JTextField(3).getPreferredSize().width;
327                case ID_COL:
328                    return new JTextField(16).getPreferredSize().width;
329                case VIEW_COL:
330                    return new JTextField(7).getPreferredSize().width;
331                default:
332                    break;
333            }
334            return new JTextField(12).getPreferredSize().width;
335        }
336
337        @Override
338        public boolean isCellEditable(int row, int col) {
339            return col != ID_COL;
340        }
341
342        @Override
343        public Object getValueAt(int row, int col) {
344            Map.Entry<String, Boolean> entry = candidateArray.get(row);
345            switch (col) {
346                case MERGE_COL:
347                    return entry.getValue();
348                case ID_COL:
349                    String id = entry.getKey();
350                    if (id == null || id.isEmpty() ||
351                            (id.charAt(0) == '$' && id.charAt(id.length()-1) == '$')) {
352                        id = Bundle.getMessage("noSuchAddress");
353                    }
354                    return id;
355                case VIEW_COL:
356                    return Bundle.getMessage("View");
357                default:
358                    break;
359            }
360            return "";
361        }
362
363        @Override
364        public void setValueAt(Object value, int row, int col) {
365            Map.Entry<String, Boolean> entry = candidateArray.get(row);
366            switch (col) {
367                case MERGE_COL:
368                    String id = entry.getKey(); 
369                    if (Roster.getDefault().getEntryForId(id) == null) {
370                        _candidates.put(entry.getKey(), false);
371                    } else {
372                        _candidates.put(entry.getKey(), (Boolean) value);
373                    }
374                    break;
375                case ID_COL:
376                    break;
377                case VIEW_COL:
378                    showProfiles(entry.getKey());
379                    break;
380                default:
381                    break;
382            }
383        }
384
385        private void showProfiles(String id) {
386            if (_viewPanel != null) {
387                getContentPane().remove(_viewPanel);
388            }
389            invalidate();
390            _viewPanel = makeViewPanel(id);
391            if (_viewPanel == null) {
392                return;
393            }
394            getContentPane().add(_viewPanel);
395            pack();
396            setVisible(true);
397        }
398
399        @CheckForNull
400        private JPanel makeViewPanel(String id) {
401            RosterEntry entry = Roster.getDefault().getEntryForId(id);
402            if (entry == null) {
403                return null;
404            }
405            JPanel viewPanel = new JPanel();
406            viewPanel.setLayout(new BoxLayout(viewPanel, BoxLayout.PAGE_AXIS));
407            viewPanel.add(Box.createGlue());
408            JPanel panel = new JPanel();
409            panel.add(MergePrompt.makeEditInfoPanel(entry));
410            viewPanel.add(panel);
411
412            JPanel spPanel = new JPanel();
413            spPanel.setLayout(new BoxLayout(spPanel, BoxLayout.LINE_AXIS));
414            spPanel.add(Box.createGlue());
415
416            RosterSpeedProfile speedProfile = entry.getSpeedProfile();
417            if (speedProfile != null ){
418                spPanel.add(makeSpeedProfilePanel("rosterSpeedProfile", speedProfile,  false, null));
419                spPanel.add(Box.createGlue());
420            }
421
422            WarrantManager manager = InstanceManager.getDefault(WarrantManager.class);
423            RosterSpeedProfile mergeProfile =  manager.getMergeProfile(id);
424            Map<Integer, Boolean> anomaly = MergePrompt.validateSpeedProfile(mergeProfile);
425            spPanel.add(makeSpeedProfilePanel("mergedSpeedProfile", mergeProfile, true, anomaly));
426            spPanel.add(Box.createGlue());
427
428            viewPanel.add(spPanel);
429            return viewPanel;
430        }
431
432    }
433
434    private static class ButtonCellRenderer extends DefaultTableCellRenderer {
435
436        @Override
437        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) {
438            Component b = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col);
439
440            JLabel l = (JLabel)b;
441            l.setHorizontalAlignment(SwingConstants.CENTER);
442            MergeTableModel tableModel = (MergeTableModel) table.getModel();
443            if (tableModel.hasAnomaly(row)) {
444                l.setBackground(java.awt.Color.RED);
445            } else {
446                l.setBackground(table.getBackground());
447            }
448            return b;
449        }
450    }
451
452//    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MergePrompt.class);
453
454}