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}