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    HashMap<String, RosterSpeedProfile> _sessionProfiles;  // candidate's speedprofile
042    Map<String, Map<Integer, Boolean>> _anomalyMap;
043    JPanel _viewPanel;
044    JmriJFrame _anomolyFrame;
045    static int STRUT = 20;
046
047    MergePrompt(String name, Map<String, Boolean> cand, Map<String, Map<Integer, Boolean>> anomalies) {
048        super();
049        _candidates = cand;
050        _anomalyMap = anomalies;
051        WarrantManager manager = InstanceManager.getDefault(WarrantManager.class);
052        _mergeProfiles = manager.getMergeProfiles();
053        _sessionProfiles = manager.getSessionProfiles();
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.size() > 0) {
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        setVisible(true);
135    }
136
137    private void noMerge() {
138        for (Map.Entry<String, Boolean> ent : _candidates.entrySet()) {
139            _candidates.put(ent.getKey(), false);
140        }
141    }
142
143    void showProfiles(String id) {
144        if (_viewPanel != null) {
145            getContentPane().remove(_viewPanel);
146        }
147        invalidate();
148        _viewPanel = makeViewPanel(id);
149        if (_viewPanel == null) {
150            return;
151        }
152        getContentPane().add(_viewPanel);
153        pack();
154        setVisible(true);
155    }
156
157    JPanel makeViewPanel(String id) {
158        if (Roster.getDefault().getEntryForId(id) == null) {
159            return null;
160        }
161        JPanel viewPanel = new JPanel();
162        viewPanel.setLayout(new BoxLayout(viewPanel, BoxLayout.PAGE_AXIS));
163        viewPanel.add(Box.createGlue());
164        JPanel panel = new JPanel();
165        panel.add(MergePrompt.makeEditInfoPanel(id));
166        viewPanel.add(panel);
167
168        JPanel spPanel = new JPanel();
169        spPanel.setLayout(new BoxLayout(spPanel, BoxLayout.LINE_AXIS));
170        spPanel.add(Box.createGlue());
171
172        RosterEntry re = Roster.getDefault().entryFromTitle(id);
173        RosterSpeedProfile speedProfile = null;
174        if (re != null) {
175            speedProfile = re.getSpeedProfile();
176            if (speedProfile != null ){
177                spPanel.add(makeSpeedProfilePanel("rosterSpeedProfile", speedProfile,  false, null));
178                spPanel.add(Box.createGlue());
179            }
180        }
181
182        WarrantManager manager = InstanceManager.getDefault(WarrantManager.class);
183        RosterSpeedProfile mergeProfile =  manager.getMergeProfile(id);
184        Map<Integer, Boolean> anomaly = MergePrompt.validateSpeedProfile(mergeProfile);
185        spPanel.add(makeSpeedProfilePanel("mergedSpeedProfile", mergeProfile, true, anomaly));
186        spPanel.add(Box.createGlue());
187
188        spPanel.add(makeSpeedProfilePanel("sessionSpeedProfile", manager.getSessionProfile(id), false, null));
189        spPanel.add(Box.createGlue());
190
191        viewPanel.add(spPanel);
192        return viewPanel;
193    }
194
195    static JPanel makeEditInfoPanel(String id) {
196        JPanel panel = new JPanel();
197        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
198        JLabel label = new JLabel(Bundle.getMessage("viewTitle", id));
199        label.setAlignmentX(Component.CENTER_ALIGNMENT);
200        panel.add(label);
201        label = new JLabel(Bundle.getMessage("deletePrompt1"));
202        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
203        label.setForeground(java.awt.Color.RED);
204        label.setAlignmentX(Component.CENTER_ALIGNMENT);
205        panel.add(label);
206        label = new JLabel(Bundle.getMessage("deletePrompt2"));
207        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
208        label.setAlignmentX(Component.CENTER_ALIGNMENT);
209        panel.add(label);
210        label = new JLabel(Bundle.getMessage("deletePrompt3"));
211        label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 12));
212        label.setAlignmentX(Component.CENTER_ALIGNMENT);
213        panel.add(label);
214        return panel;
215    }
216
217    static JPanel makeAnomalyPanel() {
218        JPanel panel = new JPanel();
219        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
220        JLabel l = new JLabel(Bundle.getMessage("anomalyPrompt"));
221        l.setForeground(java.awt.Color.RED);
222        l.setAlignmentX(Component.CENTER_ALIGNMENT);
223        panel.add(l);
224        return panel;
225    }
226
227    static JPanel makeSpeedProfilePanel(String title, RosterSpeedProfile profile, 
228                boolean edit, Map<Integer, Boolean> anomalies) {
229        JPanel panel = new JPanel();
230        panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
231        panel.add(new JLabel(Bundle.getMessage(title)));
232        SpeedProfilePanel speedPanel = new SpeedProfilePanel(profile, edit, anomalies);
233        panel.add(speedPanel);
234        return panel;
235    }
236    /**
237     * Check that non zero value are ascending for both forward and reverse
238     * speeds. Omit anomalies.
239     *
240     * @param speedProfile speedProfile
241     * @return Map of Key and direction of possible errors (anomalies)
242     */
243    public static Map<Integer, Boolean> validateSpeedProfile(RosterSpeedProfile speedProfile) {
244        // do forward speeds, then reverse
245        HashMap<Integer, Boolean> anomalies = new HashMap<>();
246        TreeMap<Integer, SpeedStep> rosterTree = speedProfile.getProfileSpeeds();
247        float lastForward = 0;
248        Integer lastKey = Integer.valueOf(0);
249        Iterator<Map.Entry<Integer, SpeedStep>> iter = rosterTree.entrySet().iterator();
250        while (iter.hasNext()) {
251            Map.Entry<Integer, SpeedStep> entry = iter.next();
252            float forward = entry.getValue().getForwardSpeed();
253            Integer key = entry.getKey();
254            if (forward > 0.0f) {
255                if (forward < lastForward) {  // anomaly found
256                    while (iter.hasNext()) {
257                        Map.Entry<Integer, SpeedStep> nextEntry = iter.next();
258                        float nextForward = nextEntry.getValue().getForwardSpeed();
259                        if (nextForward > 0.0f) {
260                            if (nextForward > lastForward) {    // remove forward
261                                anomalies.put(key, true);
262                                forward = nextForward;
263                                key = nextEntry.getKey();
264                            } else {    // remove lastForward
265                                anomalies.put(lastKey, true);
266                            }
267                            break;
268                        }
269                    }
270                }
271                lastForward = forward;
272                lastKey = key;
273            }
274        }
275
276        rosterTree = speedProfile.getProfileSpeeds();
277        float lastReverse = 0;
278        lastKey = Integer.valueOf(0);
279        iter = rosterTree.entrySet().iterator();
280        while (iter.hasNext()) {
281            Map.Entry<Integer, SpeedStep> entry = iter.next();
282            float reverse = entry.getValue().getReverseSpeed();
283            Integer key = entry.getKey();
284            if (reverse > 0.0f) {
285                if (reverse < lastReverse) {  // anomaly found
286                    while (iter.hasNext()) {
287                        Map.Entry<Integer, SpeedStep> nextEntry = iter.next();
288                        float nextreverse = nextEntry.getValue().getReverseSpeed();
289                        if (nextreverse > 0.0f) {
290                            if (nextreverse > lastReverse) {    // remove reverse
291                                anomalies.put(key, false);
292                                reverse = nextreverse;
293                                key = nextEntry.getKey();
294                            } else {    // remove lastReverse
295                                anomalies.put(lastKey, false);
296                            }
297                            break;
298                        }
299                    }
300                }
301                lastReverse = reverse;
302                lastKey = key;
303            }
304        }
305        return anomalies;
306    }
307
308    class MergeTableModel extends javax.swing.table.AbstractTableModel {
309
310        static final int MERGE_COL = 0;
311        static final int ID_COL = 1;
312        static final int VIEW_COL = 2;
313        static final int NUMCOLS = 3;
314
315        ArrayList<Map.Entry<String, Boolean>> candidateArray = new ArrayList<>();
316
317        MergeTableModel(Map<String, Boolean> map) {
318            Iterator<java.util.Map.Entry<String, Boolean>> iter = map.entrySet().iterator();
319            while (iter.hasNext()) {
320                candidateArray.add(iter.next());
321            }
322        }
323
324        boolean hasAnomaly(int row) {
325            Map.Entry<String, Boolean> entry = candidateArray.get(row);
326            Map<Integer, Boolean> anomaly = _anomalyMap.get(entry.getKey());
327            return(anomaly != null && anomaly.size() > 0);
328        }
329
330        @Override
331        public int getColumnCount() {
332            return NUMCOLS;
333        }
334
335        @Override
336        public int getRowCount() {
337            return candidateArray.size();
338        }
339
340        @Override
341        public String getColumnName(int col) {
342            switch (col) {
343                case MERGE_COL:
344                    return Bundle.getMessage("Merge");
345                case ID_COL:
346                    return Bundle.getMessage("TrainId");
347                case VIEW_COL:
348                    return Bundle.getMessage("SpeedProfiles");
349                default:
350                    // fall out
351                    break;
352            }
353            return "";
354        }
355
356        @Override
357        public Class<?> getColumnClass(int col) {
358            switch (col) {
359                case MERGE_COL:
360                    return Boolean.class;
361                case ID_COL:
362                    return String.class;
363                case VIEW_COL:
364                    return JButton.class;
365                default:
366                    break;
367            }
368            return String.class;
369        }
370
371        public int getPreferredWidth(int col) {
372            switch (col) {
373                case MERGE_COL:
374                    return new JTextField(3).getPreferredSize().width;
375                case ID_COL:
376                    return new JTextField(16).getPreferredSize().width;
377                case VIEW_COL:
378                    return new JTextField(7).getPreferredSize().width;
379                default:
380                    break;
381            }
382            return new JTextField(12).getPreferredSize().width;
383        }
384
385        @Override
386        public boolean isCellEditable(int row, int col) {
387            if (col == ID_COL) {
388                return false;
389            }
390            return true;
391        }
392
393        @Override
394        public Object getValueAt(int row, int col) {
395            Map.Entry<String, Boolean> entry = candidateArray.get(row);
396            switch (col) {
397                case MERGE_COL:
398                    return entry.getValue();
399                case ID_COL:
400                    String id = entry.getKey();
401                    if (id == null || id.isEmpty() ||
402                            (id.charAt(0) == '$' && id.charAt(id.length()-1) == '$')) {
403                        id = Bundle.getMessage("noSuchAddress");
404                    }
405                    return id;
406                case VIEW_COL:
407                    return Bundle.getMessage("View");
408                default:
409                    break;
410            }
411            return "";
412        }
413
414        @Override
415        public void setValueAt(Object value, int row, int col) {
416            Map.Entry<String, Boolean> entry = candidateArray.get(row);
417            switch (col) {
418                case MERGE_COL:
419                    String id = entry.getKey(); 
420                    if (Roster.getDefault().getEntryForId(id) == null) {
421                        _candidates.put(entry.getKey(), false);
422                    } else {
423                        _candidates.put(entry.getKey(), (Boolean) value);
424                    }
425                    break;
426                case ID_COL:
427                    break;
428                case VIEW_COL:
429                    showProfiles(entry.getKey());
430                    break;
431                default:
432                    break;
433            }
434        }
435    }
436
437    public static class ButtonCellRenderer extends DefaultTableCellRenderer {
438
439        @Override
440        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) {
441            Component b = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col);
442
443            JLabel l = (JLabel)b;
444            l.setHorizontalAlignment(SwingConstants.CENTER);
445            MergeTableModel tableModel = (MergeTableModel) table.getModel();
446            if (tableModel.hasAnomaly(row)) {
447                l.setBackground(java.awt.Color.RED);
448            } else {
449                l.setBackground(table.getBackground());
450            }
451            return b;
452        }
453    }
454
455//    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MergePrompt.class);
456}