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}