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