001package jmri.jmrit.roster.swing.speedprofile;
002
003import java.awt.BorderLayout;
004import java.awt.Component;
005import java.awt.event.ActionEvent;
006import java.awt.event.KeyEvent;
007import java.awt.event.KeyListener;
008import java.util.ArrayList;
009import java.util.Map;
010import java.util.TreeMap;
011
012import javax.swing.Box;
013import javax.swing.BoxLayout;
014import javax.swing.JLabel;
015import javax.swing.JPanel;
016import javax.swing.JRadioButton;
017import javax.swing.JScrollPane;
018import javax.swing.JTable;
019import javax.swing.JTextField;
020import javax.swing.border.EmptyBorder;
021import javax.swing.table.DefaultTableCellRenderer;
022
023import jmri.implementation.SignalSpeedMap;
024import jmri.jmrit.roster.RosterEntry;
025import jmri.jmrit.roster.RosterSpeedProfile;
026import jmri.jmrit.roster.RosterSpeedProfile.SpeedStep;
027import jmri.util.swing.JmriJOptionPane;
028
029/**
030 * Display Speed Profile.
031 *
032 * @author  Pete Cressman Copyright (C) 2015
033 */
034public class SpeedProfileTable extends jmri.util.JmriJFrame {
035
036    java.text.DecimalFormat threeDigit = new java.text.DecimalFormat("0.000");
037    int interp;
038    float loScale;
039    JLabel description;
040    String rosterId;
041    RosterSpeedProfile speedProfile;
042    Map<Integer, Boolean> anomalies;
043    boolean hasAnomaly;
044    // divided by layout scale, gives a rough conversion for throttle setting to track speed
045    static float SCALE = jmri.jmrit.logix.SpeedUtil.SCALE_FACTOR;
046    static java.awt.Color myRed = new java.awt.Color(255, 120, 120);
047
048    public SpeedProfileTable(RosterEntry re) {
049        this(re.getSpeedProfile(), re.getId());
050    }
051
052    public SpeedProfileTable(RosterSpeedProfile sp, String id) {
053        super(false, true);
054        speedProfile = sp;
055        rosterId = id;
056        anomalies = jmri.jmrit.logix.MergePrompt.validateSpeedProfile(speedProfile);
057        hasAnomaly = (anomalies !=null && anomalies.size() > 0);
058        setTitle(Bundle.getMessage("SpeedTable", rosterId));
059        getContentPane().setLayout(new BorderLayout(15,15));
060        
061        interp = jmri.InstanceManager.getDefault(SignalSpeedMap.class).getInterpretation();
062        loScale = jmri.InstanceManager.getDefault(SignalSpeedMap.class).getLayoutScale();
063        SpeedTableModel model = new SpeedTableModel(speedProfile);
064        JTable table = new JTable(model);
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(table);
071                }
072            }
073            @Override
074            public void keyPressed(KeyEvent e) {}
075            @Override
076            public void keyReleased(KeyEvent e) {}
077        });
078        
079        table.getColumnModel().getColumn(SpeedTableModel.FORWARD_SPEED_COL).setCellRenderer(new ColorCellRenderer());
080        table.getColumnModel().getColumn(SpeedTableModel.REVERSE_SPEED_COL).setCellRenderer(new ColorCellRenderer());
081        
082        for (int i = 0; i < model.getColumnCount(); i++) {
083            int width = model.getPreferredWidth(i);
084            javax.swing.table.TableColumn column = table.getColumnModel().getColumn(i);
085            column.setPreferredWidth(width);
086        }
087        
088        JPanel contentPane = new JPanel();
089        contentPane.setLayout(new BorderLayout(5, 5));
090        java.awt.Font font = table.getFont();
091        
092        JPanel panel = new JPanel();
093        panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS));
094        JLabel label = new JLabel(Bundle.getMessage("units"));
095        label.setFont(font);
096        javax.swing.ButtonGroup bp = new javax.swing.ButtonGroup();
097        JRadioButton mm = new JRadioButton(Bundle.getMessage("mmps"));
098        mm.setFont(font);
099        mm.addActionListener((ActionEvent e) -> {
100            update(model, SignalSpeedMap.PERCENT_NORMAL);
101        });
102        JRadioButton mph = new JRadioButton(Bundle.getMessage("mph"));
103        mph.setFont(font);
104        mph.addActionListener((ActionEvent e) -> {
105            update(model, SignalSpeedMap.SPEED_MPH);
106        });
107        JRadioButton kph = new JRadioButton(Bundle.getMessage("kph"));
108        kph.setFont(font);
109        kph.addActionListener((ActionEvent e) -> {
110            update(model, SignalSpeedMap.SPEED_KMPH);
111        });
112        bp.add(mm);
113        bp.add(mph);
114        bp.add(kph);
115        panel.add(Box.createHorizontalGlue());
116        panel.add(label);
117        panel.add(mm);
118        panel.add(mph);
119        panel.add(kph);
120        panel.add(Box.createHorizontalGlue());
121        String str;
122        switch(interp) {
123            case SignalSpeedMap.SPEED_MPH:
124                mph.setSelected(true);
125                str = "scale"; // NOI18N
126                break;
127            case SignalSpeedMap.SPEED_KMPH:
128                kph.setSelected(true);
129                str = "scale"; // NOI18N
130                break;
131            default:
132                mm.setSelected(true);
133                str = "track"; // NOI18N
134        }
135        JPanel topPanel = new JPanel();
136        topPanel.setBorder( new EmptyBorder( 0, 8, 8, 8 ) ); // keep text away from edges of pane
137        topPanel.setLayout(new BoxLayout(topPanel, BoxLayout.PAGE_AXIS));
138        description = new JLabel(Bundle.getMessage("rosterId", Bundle.getMessage(str), rosterId));
139        description.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
140        topPanel.add(description);
141        JLabel info = new JLabel(Bundle.getMessage("cellInfo"));
142        info.setFont(font);
143        topPanel.add(info);
144        if (hasAnomaly) {
145            JLabel redInfo = new JLabel(Bundle.getMessage("redInfo_1"));
146            redInfo.setFont(font);
147            redInfo.setForeground(java.awt.Color.RED);
148            redInfo.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
149            topPanel.add(redInfo);
150            redInfo = new JLabel(Bundle.getMessage("redInfo_2"));
151            redInfo.setForeground(java.awt.Color.RED);
152            redInfo.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
153            redInfo.setFont(font);
154            topPanel.add(redInfo);
155            redInfo = new JLabel(Bundle.getMessage("redInfo_3"));
156            redInfo.setForeground(java.awt.Color.RED);
157            redInfo.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
158            redInfo.setFont(font);
159            topPanel.add(redInfo);
160        }
161
162        contentPane.add(topPanel, BorderLayout.NORTH);
163        contentPane.add(panel, BorderLayout.CENTER);
164
165        JScrollPane pane = new JScrollPane(table);
166        contentPane.add(pane, BorderLayout.SOUTH);
167        getContentPane().add(contentPane);
168        pack();
169    }
170
171    private void deleteRow(JTable table) {
172        int row = table.getSelectedRow();
173        if (row >= 0) {
174            SpeedTableModel model = (SpeedTableModel)table.getModel();
175            Map.Entry<Integer, SpeedStep> entry = model.speedArray.get(row);
176            int step = Math.round((float)(entry.getKey()*126)/1000);
177            if ( JmriJOptionPane.YES_OPTION  == JmriJOptionPane.showConfirmDialog(table, 
178                    Bundle.getMessage("DeleteRow", step), Bundle.getMessage("SpeedTable", rosterId),
179                    JmriJOptionPane.YES_NO_OPTION)) {
180                model.speedArray.remove(entry);
181                speedProfile.deleteStep(entry.getKey());
182                model.fireTableDataChanged();
183                // re.updateFile();
184                // Roster.getDefault().writeRoster();
185            }
186        }
187    }
188    
189    private void update(SpeedTableModel model, int which) {
190        interp = which;
191        // can't figure out a way to update column names
192        //model.getColumnName(SpeedTableModel.FORWARD_SPEED_COL);
193        //model.getColumnName(SpeedTableModel.REVERSE_SPEED_COL);
194        String str;
195        switch(interp) {
196            case SignalSpeedMap.SPEED_MPH:
197            case SignalSpeedMap.SPEED_KMPH:
198                str = "scale"; // NOI18N
199                break;
200            default:
201                str = "track"; // NOI18N
202        }
203        description.setText(Bundle.getMessage("rosterId", Bundle.getMessage(str), rosterId));
204        model.fireTableDataChanged();
205    }
206
207    public class ColorCellRenderer extends DefaultTableCellRenderer {
208        @Override
209        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) {
210            Component c = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col);
211
212            SpeedTableModel model = (SpeedTableModel) table.getModel();
213  
214            if (anomalies == null || anomalies.size() == 0) {
215                c.setBackground(table.getBackground());                                
216                return c;
217            }           
218            Map.Entry<Integer, SpeedStep> entry = model.getEntry(row);
219            Boolean direction = anomalies.get(entry.getKey());
220            if (direction == null) {
221                c.setBackground(table.getBackground());
222                return c;
223            }
224            boolean dir =  direction.booleanValue();
225            if ( dir && col == SpeedTableModel.FORWARD_SPEED_COL) {
226                c.setBackground(myRed);                
227            } else if (!dir && col == SpeedTableModel.REVERSE_SPEED_COL){
228                c.setBackground(myRed);
229            }
230            return c;
231        }
232    }
233    
234    class SpeedTableModel extends javax.swing.table.AbstractTableModel {
235        static final int STEP_COL = 0;
236        static final int THROTTLE_COL = 1;
237        static final int FORWARD_SPEED_COL = 2;
238        static final int FORWARD_FACTOR_COL = 3;
239        static final int REVERSE_SPEED_COL = 4;
240        static final int REVERSE_FACTOR_COL = 5;
241        static final int NUMCOLS = 6;
242        
243        ArrayList<Map.Entry<Integer, SpeedStep>> speedArray = new  ArrayList<>();
244        
245        SpeedTableModel(RosterSpeedProfile sp) {
246            TreeMap<Integer, SpeedStep> speeds = sp.getProfileSpeeds();
247            Map.Entry<Integer, SpeedStep> entry = speeds.firstEntry();
248            while (entry!=null) {
249                speedArray.add(entry);
250                entry = speeds.higherEntry(entry.getKey());
251            }
252        }
253        
254        @Override
255        public int getColumnCount() {
256            return NUMCOLS;
257        }
258
259        @Override
260        public int getRowCount() {
261            return speedArray.size();
262        }
263        @Override
264        public String getColumnName(int col) {
265            String rate = Bundle.getMessage("speed");
266            /* can't figure out a way to update column names
267            switch(interp) {
268                case SignalSpeedMap.SPEED_MPH:
269                    rate = "Mph";
270                    break;
271                case SignalSpeedMap.SPEED_KMPH:
272                    rate = "KMph";
273                    break;
274                default:
275                    rate = "mm/s";
276            }*/
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("forwardSpeed", rate);
284                case REVERSE_SPEED_COL:
285                    return Bundle.getMessage("reverseSpeed", rate);
286                case FORWARD_FACTOR_COL:
287                case REVERSE_FACTOR_COL:
288                    return Bundle.getMessage("factor");
289                default:
290                    // fall out
291                    break;
292            }
293            return "";
294        }
295        @Override
296        public Class<?> getColumnClass(int col) {
297            return String.class;
298        }
299
300        public int getPreferredWidth(int col) {
301            switch (col) {
302                case STEP_COL:
303                    return new JTextField(3).getPreferredSize().width;
304                case THROTTLE_COL:
305                case FORWARD_FACTOR_COL:
306                case REVERSE_FACTOR_COL:
307                    return new JTextField(5).getPreferredSize().width;
308                case FORWARD_SPEED_COL:
309                case REVERSE_SPEED_COL:
310                    return new JTextField(8).getPreferredSize().width;
311                default:
312                    // fall out
313                    break;
314            }
315            return new JTextField(8).getPreferredSize().width;
316        }
317        
318        @Override
319        public boolean isCellEditable(int row, int col) {
320            if (hasAnomaly && (col == FORWARD_SPEED_COL || col == REVERSE_SPEED_COL)) {
321                return true;
322            }
323            return false;
324        }
325
326
327        @Override
328        public Object getValueAt(int row, int col) {
329            Map.Entry<Integer, SpeedStep> entry = speedArray.get(row);
330            switch (col) {
331                case STEP_COL:
332                    return Math.round((float)(entry.getKey()*126)/1000);
333                case THROTTLE_COL:
334                    return threeDigit.format((float)(entry.getKey())/1000);
335                case FORWARD_SPEED_COL:
336                    float speed = entry.getValue().getForwardSpeed();
337                    switch(interp) {
338                        case SignalSpeedMap.SPEED_MPH:
339                            speed = speed*loScale*3.6f*0.621371f/1000;
340                            break;
341                        case SignalSpeedMap.SPEED_KMPH:
342                            speed = speed*loScale*3.6f/1000;
343                            break;
344                        default:
345                    }
346                    return threeDigit.format(speed);
347                case FORWARD_FACTOR_COL:
348                    return threeDigit.format(
349                            entry.getValue().getForwardSpeed() * SCALE / (loScale * entry.getKey()));
350                case REVERSE_SPEED_COL:
351                    speed = entry.getValue().getReverseSpeed();
352                    switch(interp) {
353                        case SignalSpeedMap.SPEED_MPH:
354                            speed = speed*loScale*3.6f*0.621371f/1000;
355                            break;
356                        case SignalSpeedMap.SPEED_KMPH:
357                            speed = speed*loScale*3.6f/1000;
358                            break;
359                        default:
360                    }
361                    return threeDigit.format(speed);
362                case REVERSE_FACTOR_COL:
363                    return threeDigit.format(
364                            entry.getValue().getReverseSpeed() * SCALE / (loScale * entry.getKey()));
365                default:
366                    // fall out
367                    break;
368            }
369            return "";
370        }
371
372        @Override
373        public void setValueAt(Object value, int row, int col) {
374            if (!hasAnomaly) {
375                return;
376            }
377            Map.Entry<Integer, SpeedStep> entry = speedArray.get(row);
378            try {
379            switch (col) {
380                case FORWARD_SPEED_COL:
381                    entry.getValue().setForwardSpeed(Float.parseFloat(((String)value).replace(',', '.')));
382                    return;
383                case REVERSE_SPEED_COL:
384                    entry.getValue().setReverseSpeed(Float.parseFloat(((String)value).replace(',', '.')));
385                    return;
386                default:
387                    // fall out
388                    break;
389            }
390            } catch (NumberFormatException nfe) {   // ignore
391            }
392        }
393 
394        Map.Entry<Integer, SpeedStep> getEntry(int row) {
395            return speedArray.get(row); 
396        }
397        
398    }
399}