001package jmri.jmrit.logix; 002 003import java.awt.Component; 004import java.awt.Dimension; 005import java.awt.datatransfer.DataFlavor; 006import java.awt.datatransfer.Transferable; 007import java.awt.datatransfer.UnsupportedFlavorException; 008import java.awt.event.KeyEvent; 009import java.awt.event.KeyListener; 010 011import java.io.IOException; 012 013import java.util.AbstractMap.SimpleEntry; 014import java.util.ArrayList; 015import java.util.Map; 016import java.util.TreeMap; 017 018import javax.swing.JComponent; 019import javax.swing.JPanel; 020import javax.swing.JScrollBar; 021import javax.swing.JScrollPane; 022import javax.swing.JTable; 023import javax.swing.JTextField; 024import javax.swing.TransferHandler; 025import javax.swing.table.DefaultTableCellRenderer; 026import javax.swing.table.TableColumn; 027 028import jmri.jmrit.roster.RosterSpeedProfile; 029import jmri.jmrit.roster.RosterSpeedProfile.SpeedStep; 030 031/** 032 * 033 * Allows user to decide if (and which) SpeedProfiles to write to the Roster at 034 * the end of a session. Locos running warrants have had their speeds measured 035 * and this new data may or may not be merged into any existing SpeedProfiles 036 * in the Roster. 037 * 038 * @author Pete cressman Copyright (C) 2017 039 */ 040public class SpeedProfilePanel extends JPanel { 041 042 private JTable _table; 043 private JScrollPane _scrollPane; 044 private static final java.awt.Color MY_RED = new java.awt.Color(255, 120, 120); 045 private static final String ENTRY_FLAVOR_TYPE = 046 DataFlavor.javaJVMLocalObjectMimeType + ";class=java.util.AbstractMap"; 047 private DataFlavor _entryFlavor; 048 049 /** 050 * @param speedProfile a RosterSpeedProfile 051 * @param editable allow editing. 052 * @param anomalies map of entries where speed decreases from previous speed 053 */ 054 public SpeedProfilePanel(RosterSpeedProfile speedProfile, boolean editable, Map<Integer, Boolean> anomalies) { 055 SpeedTableModel model = new SpeedTableModel(speedProfile, editable, anomalies); 056 _table = new JTable(model); 057 int tablewidth = 0; 058 for (int i = 0; i < model.getColumnCount(); i++) { 059 TableColumn column = _table.getColumnModel().getColumn(i); 060 int width = model.getPreferredWidth(i); 061 column.setPreferredWidth(width); 062 tablewidth += width; 063 } 064 if (editable) { 065 _table.addKeyListener(new KeyListener() { 066 @Override 067 public void keyTyped(KeyEvent ke) { 068 char ch = ke.getKeyChar(); 069 if (ch == KeyEvent.VK_DELETE || ch == KeyEvent.VK_X) { 070 deleteRow(); 071 } else if (ch == KeyEvent.VK_ENTER) { 072 int row = _table.getEditingRow(); 073 if (row < 0) { 074 row = _table.getSelectedRow(); 075 } 076 if (row >= 0) { 077 rePack(row); 078 } 079 } 080 } 081 @Override 082 public void keyPressed(KeyEvent e) { 083 // only handling keyTyped events 084 } 085 @Override 086 public void keyReleased(KeyEvent e) { 087 // only handling keyTyped events 088 } 089 }); 090 _table.getColumnModel().getColumn(SpeedTableModel.FORWARD_SPEED_COL) 091 .setCellRenderer(new ColorCellRenderer()); 092 _table.getColumnModel().getColumn(SpeedTableModel.REVERSE_SPEED_COL) 093 .setCellRenderer(new ColorCellRenderer()); 094 } 095 _scrollPane = new JScrollPane(_table); 096 int barWidth = 5+_scrollPane.getVerticalScrollBar().getPreferredSize().width; 097 tablewidth += barWidth; 098 _scrollPane.setPreferredSize(new Dimension(tablewidth, tablewidth)); 099 try { 100 _entryFlavor = new DataFlavor(ENTRY_FLAVOR_TYPE); 101 if (editable) { 102 _table.setTransferHandler(new ImportEntryTranferHandler()); 103 _table.setDragEnabled(true); 104 _scrollPane.setTransferHandler(new ImportEntryTranferHandler()); 105 } else { 106 _table.setTransferHandler(new ExportEntryTranferHandler()); 107 _table.setDragEnabled(true); 108 } 109 } catch (ClassNotFoundException cnfe) { 110 log.error("SpeedProfilePanel unable to Drag and Drop",cnfe); 111 } 112 add(_scrollPane); 113 if (anomalies != null) { 114 setAnomalies(anomalies); 115 } 116 } 117 118 private void setAnomalies(Map<Integer, Boolean> anomalies) { 119 SpeedTableModel model = (SpeedTableModel)_table.getModel(); 120 model.setAnomaly(anomalies); 121 if (anomalies != null && !anomalies.isEmpty()) { 122 JScrollBar bar = _scrollPane.getVerticalScrollBar(); 123 bar.setValue(50); // important to "prime" the setting for bar.getMaximum() 124 int numRows = model.getRowCount(); 125 Integer key = 1000; 126 for (int k : anomalies.keySet()) { 127 if (k < key) { 128 key = k; 129 } 130 } 131 TreeMap<Integer, SpeedStep> speeds = model.getProfileSpeeds(); 132 Map.Entry<Integer, SpeedStep> entry = speeds.higherEntry(key); 133 if (entry == null) { 134 entry = speeds.lowerEntry(key); 135 } 136 int row = model.getRow(entry); 137 int pos = (int)(((float)row)*bar.getMaximum() / numRows + .5); 138 bar.setValue(pos); 139 } 140 } 141 142 private void deleteRow() { 143 int row = _table.getSelectedRow(); 144 if (row >= 0) { 145 SpeedTableModel model = (SpeedTableModel)_table.getModel(); 146 Map.Entry<Integer, SpeedStep> entry = model.speedArray.get(row); 147 model.speedArray.remove(entry); 148 model._profile.deleteStep(entry.getKey()); 149 model.fireTableDataChanged(); 150 } 151 } 152 153 private static class ColorCellRenderer extends DefaultTableCellRenderer { 154 @Override 155 public Component getTableCellRendererComponent(JTable table, Object value, 156 boolean isSelected, boolean hasFocus, int row, int col) { 157 Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); 158 159 SpeedTableModel model = (SpeedTableModel) table.getModel(); 160 Map<Integer, Boolean> anomalies = model.getAnomalies(); 161 162 if (anomalies == null || anomalies.isEmpty()) { 163 c.setBackground(table.getBackground()); 164 return c; 165 } 166 Map.Entry<Integer, SpeedStep> entry = model.getRowEntry(row); 167 Boolean direction = anomalies.get(entry.getKey()); 168 if (direction == null) { 169 c.setBackground(table.getBackground()); 170 return c; 171 } 172 if (( direction && col == SpeedTableModel.FORWARD_SPEED_COL) 173 || (!direction && col == SpeedTableModel.REVERSE_SPEED_COL)){ 174 c.setBackground(MY_RED); 175 } 176 return c; 177 } 178 } 179 180 private void rePack(int row) { 181 SpeedTableModel model = (SpeedTableModel)_table.getModel(); 182 Map.Entry<Integer, SpeedStep> entry = model.getRowEntry(row); 183 setAnomalies(model.updateAnomaly(entry)); 184 model.fireTableDataChanged(); 185 } 186 187 private static class SpeedTableModel extends javax.swing.table.AbstractTableModel { 188 static final int STEP_COL = 0; 189 static final int THROTTLE_COL = 1; 190 static final int FORWARD_SPEED_COL = 2; 191 static final int REVERSE_SPEED_COL = 3; 192 static final int NUMCOLS = 4; 193 194 java.text.DecimalFormat threeDigit = new java.text.DecimalFormat("0.000"); 195 ArrayList<Map.Entry<Integer, SpeedStep>> speedArray = new ArrayList<>(); 196 RosterSpeedProfile _profile; 197 boolean _editable; 198 transient Map<Integer, Boolean> _anomaly; 199 200 SpeedTableModel(RosterSpeedProfile sp, boolean editable, Map<Integer, Boolean> anomalies) { 201 _profile = sp; 202 _editable = editable; // allow mergeProfile editing 203 _anomaly = anomalies; 204 TreeMap<Integer, SpeedStep> speeds = sp.getProfileSpeeds(); 205 Map.Entry<Integer, SpeedStep> entry = speeds.firstEntry(); 206 while (entry!=null) { 207 speedArray.add(entry); 208 entry = speeds.higherEntry(entry.getKey()); 209 } 210 } 211 212 Map<Integer, Boolean> getAnomalies() { 213 return _anomaly; 214 } 215 216 void setAnomaly(Map<Integer, Boolean> an) { 217 _anomaly = an; 218 } 219 220 private Map<Integer, Boolean> updateAnomaly(Map.Entry<Integer, SpeedStep> entry) { 221 SpeedStep ss = entry.getValue(); 222 _profile.setSpeed(entry.getKey(), ss.getForwardSpeed(), ss.getReverseSpeed()); 223 _anomaly = MergePrompt.validateSpeedProfile(_profile); 224 log.debug("updateAnomaly size={}", _anomaly.size()); 225 return _anomaly; 226 } 227 228 Map.Entry<Integer, SpeedStep> getRowEntry(int row) { 229 return speedArray.get(row); 230 } 231 232 Map.Entry<Integer, SpeedStep> getKeyEntry(Integer key) { 233 for (Map.Entry<Integer, SpeedStep> entry : speedArray) { 234 if (entry.getKey().equals(key)) { 235 return entry; 236 } 237 } 238 return null; 239 } 240 241 TreeMap<Integer, SpeedStep> getProfileSpeeds() { 242 return _profile.getProfileSpeeds(); 243 } 244 245 void addEntry( Map.Entry<Integer, SpeedStep> entry) { 246 SpeedStep ss = entry.getValue(); 247 Integer key = entry.getKey(); 248 _profile.setSpeed(key, ss.getForwardSpeed(), ss.getReverseSpeed()); 249 for (int row = 0; row<speedArray.size(); row++) { 250 int k = speedArray.get(row).getKey(); 251 if (key < k) { 252 speedArray.add(row, entry); 253 log.debug("addEntry _profile size={}, speedArray size={}", 254 _profile.getProfileSize(), speedArray.size()); 255 return; 256 } 257 } 258 speedArray.add(entry); 259 } 260 261 int getRow(Map.Entry<Integer, SpeedStep> entry) { 262 return speedArray.indexOf(entry); 263 } 264 265 @Override 266 public int getColumnCount() { 267 return NUMCOLS; 268 } 269 270 @Override 271 public int getRowCount() { 272 return speedArray.size(); 273 } 274 275 @Override 276 public String getColumnName(int col) { 277 switch (col) { 278 case STEP_COL: 279 return Bundle.getMessage("step"); 280 case THROTTLE_COL: 281 return Bundle.getMessage("throttle"); 282 case FORWARD_SPEED_COL: 283 return Bundle.getMessage("forward"); 284 case REVERSE_SPEED_COL: 285 return Bundle.getMessage("reverse"); 286 default: 287 // fall out 288 break; 289 } 290 return ""; 291 } 292 @Override 293 public Class<?> getColumnClass(int col) { 294 return String.class; 295 } 296 297 public int getPreferredWidth(int col) { 298 switch (col) { 299 case STEP_COL: 300 return new JTextField(3).getPreferredSize().width; 301 case THROTTLE_COL: 302 return new JTextField(6).getPreferredSize().width; 303 case FORWARD_SPEED_COL: 304 case REVERSE_SPEED_COL: 305 return new JTextField(8).getPreferredSize().width; 306 default: 307 break; 308 } 309 return new JTextField(8).getPreferredSize().width; 310 } 311 312 @Override 313 public boolean isCellEditable(int row, int col) { 314 return (_editable && (col == FORWARD_SPEED_COL || col == REVERSE_SPEED_COL)); 315 } 316 317 @Override 318 public Object getValueAt(int row, int col) { 319 Map.Entry<Integer, SpeedStep> entry = speedArray.get(row); 320 switch (col) { 321 case STEP_COL: 322 return Math.round((float)(entry.getKey()*126)/1000); 323 case THROTTLE_COL: 324 return threeDigit.format((float)(entry.getKey())/1000); 325 case FORWARD_SPEED_COL: 326 float speed = entry.getValue().getForwardSpeed(); 327 return threeDigit.format(speed); 328 case REVERSE_SPEED_COL: 329 speed = entry.getValue().getReverseSpeed(); 330 return threeDigit.format(speed); 331 default: 332 // fall out 333 break; 334 } 335 return ""; 336 } 337 338 @Override 339 public void setValueAt(Object value, int row, int col) { 340 if (!_editable) { 341 return; 342 } 343 Map.Entry<Integer, SpeedStep> entry = speedArray.get(row); 344 try { 345 switch (col) { 346 case FORWARD_SPEED_COL: 347 entry.getValue().setForwardSpeed(Float.parseFloat(((String)value).replace(',', '.'))); 348 return; 349 case REVERSE_SPEED_COL: 350 entry.getValue().setReverseSpeed(Float.parseFloat(((String)value).replace(',', '.'))); 351 return; 352 default: 353 // fall out 354 break; 355 } 356 } catch (NumberFormatException nfe) { 357 log.error("SpeedTableModel ({}, {}) value={}", row, col, value); 358 } 359 } 360 } 361 362 private class ExportEntryTranferHandler extends TransferHandler { 363 364 @Override 365 public int getSourceActions(JComponent c) { 366 return COPY; 367 } 368 369 @Override 370 public Transferable createTransferable(JComponent c) { 371 if (!(c instanceof JTable )){ 372 return null; 373 } 374 JTable table = (JTable) c; 375 int row = table.getSelectedRow(); 376 if (row < 0) { 377 return null; 378 } 379 row = table.convertRowIndexToModel(row); 380 SpeedTableModel model = (SpeedTableModel)table.getModel(); 381 return new EntrySelection(model.getRowEntry(row)); 382 } 383 } 384 385 private class ImportEntryTranferHandler extends ExportEntryTranferHandler { 386 387 @Override 388 public boolean canImport(TransferHandler.TransferSupport support) { 389 DataFlavor[] flavors = support.getDataFlavors(); 390 if (flavors == null) { 391 return false; 392 } 393 for (DataFlavor flavor : flavors) { 394 if (_entryFlavor.equals(flavor)) { 395 return true; 396 } 397 } 398 return false; 399 } 400 401 @Override 402 public boolean importData(TransferHandler.TransferSupport support) { 403 if (!canImport(support)) { 404 return false; 405 } 406 if (!support.isDrop()) { 407 return false; 408 } 409 410 JTable table = _table; 411 try { 412 Transferable trans = support.getTransferable(); 413 Object obj = trans.getTransferData(_entryFlavor); 414 if (!(obj instanceof Map.Entry)) { 415 return false; 416 } 417 @SuppressWarnings("unchecked") 418 Map.Entry<Integer, SpeedStep> sourceEntry = (Map.Entry<Integer, SpeedStep>)obj; 419 SpeedStep sss = sourceEntry.getValue(); 420 SpeedTableModel model = (SpeedTableModel)table.getModel(); 421 Integer key = sourceEntry.getKey(); 422 Map.Entry<Integer, SpeedStep> entry = model.getKeyEntry(key); 423 if (entry != null ) { 424 SpeedStep ss = entry.getValue(); 425 if (sss.getForwardSpeed() > 0f) { 426 if (ss.getForwardSpeed() <= 0f) { 427 ss.setForwardSpeed(sss.getForwardSpeed()); 428 } else { 429 ss.setForwardSpeed((sss.getForwardSpeed() + ss.getForwardSpeed()) / 2); 430 } 431 } 432 if (sss.getReverseSpeed() > 0f) { 433 if (ss.getReverseSpeed() <= 0f) { 434 ss.setReverseSpeed(sss.getReverseSpeed()); 435 } else { 436 ss.setReverseSpeed((sss.getReverseSpeed() + ss.getReverseSpeed()) / 2); 437 } 438 } 439 } else { 440 model.addEntry(sourceEntry); 441 } 442 rePack(key); 443 444 return true; 445 } catch (UnsupportedFlavorException | IOException ufe) { 446 log.warn("MergeTranferHandler.importData",ufe); 447 } 448 return false; 449 } 450 451 private void rePack(Integer key) { 452 SpeedTableModel model = (SpeedTableModel)_table.getModel(); 453 setAnomalies(model.updateAnomaly(model.getKeyEntry(key))); 454 model.fireTableDataChanged(); 455 } 456 } 457 458 private class EntrySelection implements Transferable { 459 Integer _key; 460 SpeedStep _step; 461 public EntrySelection(Map.Entry<Integer, SpeedStep> entry) { 462 _key = entry.getKey(); 463 _step = new SpeedStep(); 464 SpeedStep step = entry.getValue(); 465 _step.setForwardSpeed(step.getForwardSpeed()); 466 _step.setReverseSpeed(step.getReverseSpeed()); 467 } 468 469 @Override 470 public DataFlavor[] getTransferDataFlavors() { 471 return new DataFlavor[] {_entryFlavor, DataFlavor.stringFlavor}; 472 } 473 474 @Override 475 public boolean isDataFlavorSupported(DataFlavor flavor) { 476 return _entryFlavor.equals(flavor) || DataFlavor.stringFlavor.equals(flavor); 477 } 478 479 @Override 480 public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException { 481 if (_entryFlavor.equals(flavor)) { 482 return new SimpleEntry<>(_key, _step); 483 } else if (DataFlavor.stringFlavor.equals(flavor)) { 484 StringBuilder msg = new StringBuilder (); 485 msg.append(_key.toString()); 486 msg.append(','); 487 msg.append(_step.getForwardSpeed()); 488 msg.append(','); 489 msg.append(_step.getReverseSpeed()); 490 return msg.toString(); 491 } 492 log.warn("EntrySelection.getTransferData: {}",flavor); 493 throw(new UnsupportedFlavorException(flavor)); 494 } 495 } 496 497 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(SpeedProfilePanel.class); 498 499}