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}