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}