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