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