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}