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