001package jmri.jmrit.symbolicprog;
002
003import java.awt.BorderLayout;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.GridBagConstraints;
007import java.awt.GridBagLayout;
008import java.util.ArrayList;
009import java.util.HashMap;
010import java.util.List;
011import java.util.ResourceBundle;
012
013import javax.swing.BoundedRangeModel;
014import javax.swing.DefaultBoundedRangeModel;
015import javax.swing.JButton;
016import javax.swing.JCheckBox;
017import javax.swing.JComponent;
018import javax.swing.JLabel;
019import javax.swing.JPanel;
020import javax.swing.JSlider;
021import javax.swing.JTextField;
022import javax.swing.event.ChangeEvent;
023import javax.swing.event.ChangeListener;
024
025import jmri.InstanceManager;
026import jmri.UserPreferencesManager;
027import jmri.util.CvUtil;
028
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032/**
033 * Represent an entire speed table as a single Variable.
034 * <p>
035 * This presents as a set of vertically oriented sliders, with numeric values
036 * above them. That it turn is done using VarSlider and DecVariableValue objects
037 * respectively. VarSlider is an interior class to color a JSlider by state. The
038 * respective VarSlider and DecVariableValue communicate through their
039 * underlying CV objects. Changes to CV Values are listened to by this class,
040 * which updates the model objects for the VarSliders; the DecVariableValues
041 * listen directly.
042 * <p>
043 * Color (hence state) of individual sliders (hence CVs) are directly coupled to
044 * the state of those CVs.
045 * <p>
046 * The state of the entire variable has to be a composite of all the sliders,
047 * hence CVs. The mapping is (in order):
048 * <ul>
049 * <li>If any CVs are UNKNOWN, its UNKNOWN..
050 * <li>If not, and any are EDITED, its EDITED.
051 * <li>If not, and any are FROMFILE, its FROMFILE.
052 * <li>If not, and any are READ, its READ.
053 * <li>If not, and any are STORED, its STORED.
054 * <li>And if we get to here, something awful has happened.
055 * </ul>
056 * <p>
057 * A similar pattern is used for a read or write request. Write writes them all;
058 * Read reads any that aren't READ or WRITTEN.
059 * <p>
060 * Speed tables can have different numbers of entries; 28 is the default, and
061 * also the maximum.
062 * <p>
063 * The NMRA specification says that speed table entries cannot be non-monotonic
064 * (e.g. cannot decrease when moving from lower to higher CV numbers). In
065 * earlier versions of the code, this was enforced any time a value was changed
066 * (for any reason). This caused a problem when CVs were read that were
067 * non-monotonic: That value was read, causing lower CVs to be made consistent,
068 * a change in their value which changed their state, so they were read again.
069 * To avoid this, the class now only enforces non-monotonicity when the slider
070 * is adjusted.
071 * <p>
072 * _value is a holdover from the LongAddrVariableValue, which this was copied
073 * from; it should be removed.
074 *
075 * @author Bob Jacobsen, Alex Shepherd Copyright (C) 2001, 2004, 2013
076 * @author Dave Heap Copyright (C) 2012 Added support for Marklin mfx style speed table
077 * @author Dave Heap Copyright (C) 2013 Changes to fix mfx speed table issue (Vstart and Vhigh not written)
078 * @author Dave Heap - generate cvList array to incorporate Vstart and Vhigh
079 *
080 */
081public class SpeedTableVarValue extends VariableValue implements ChangeListener {
082
083    int nValues;
084    int numCvs;
085    String[] cvList;
086    BoundedRangeModel[] models;
087    int _min;
088    int _max;
089    int _range;
090    boolean mfx;
091
092    List<JCheckBox> stepCheckBoxes;
093
094    /**
095     * Create the object with a "standard format ctor".
096     * @param name name.
097     * @param comment comment.
098     * @param cvName cv name.
099     * @param readOnly true if read only, else false.
100     * @param infoOnly true if info only, else false.
101     * @param writeOnly true if write only, else false.
102     * @param opsOnly true if ops only, else false.
103     * @param cvNum cv number.
104     * @param mask cv mask.
105     * @param minVal minimum value.
106     * @param maxVal maximum value.
107     * @param v hashmap of string and cv value.
108     * @param status status label.
109     * @param stdname std name.
110     * @param entries number entries.
111     * @param mfxFlag set mx flag true or false.
112     */
113    public SpeedTableVarValue(String name, String comment, String cvName,
114            boolean readOnly, boolean infoOnly, boolean writeOnly, boolean opsOnly,
115            String cvNum, String mask, int minVal, int maxVal,
116            HashMap<String, CvValue> v, JLabel status, String stdname, int entries, boolean mfxFlag) {
117        super(name, comment, cvName, readOnly, infoOnly, writeOnly, opsOnly, cvNum, mask, v, status, stdname);
118
119        nValues = entries;
120        _min = minVal;
121        _max = maxVal;
122        _range = maxVal - minVal;
123        mfx = mfxFlag;
124
125        numCvs = nValues;
126        cvList = new String[numCvs];
127
128        models = new BoundedRangeModel[nValues];
129
130        // create the set of models
131        for (int i = 0; i < nValues; i++) {
132            // populate cvList
133            cvList[i] = Integer.toString(Integer.parseInt(getCvNum()) + i);
134            // create each model
135            DefaultBoundedRangeModel j = new DefaultBoundedRangeModel(_range * i / (nValues - 1) + _min, 0, _min, _max);
136            models[i] = j;
137            // connect each model to CV for notification
138            // the connection is to cvNum through cvNum+nValues (28 values total typically)
139            // The invoking code (e.g. VariableTableModel) must ensure the CVs exist
140            // Note that the default values in the CVs are zero, but are the ramp
141            // values here.  We leave that as work item 177, and move on to set the
142            // CV states to "FromFile"
143            CvValue c = _cvMap.get(cvList[i]);
144            c.setValue(_range * i / (nValues - 1) + _min);
145            c.addPropertyChangeListener(this);
146            c.setState(ValueState.FROMFILE);
147        }
148
149        _defaultColor = (new JSlider()).getBackground();
150        // simplifyMask(); // not required as mask is ignored
151    }
152
153    /**
154     * Create a null object. Normally only used for tests and to pre-load
155     * classes.
156     */
157    public SpeedTableVarValue() {
158    }
159
160    @Override
161    public Object rangeVal() {
162        log.warn("rangeVal doesn't make sense for a speed table");
163        return "Speed table";
164    }
165
166    @Override
167    public CvValue[] usesCVs() {
168        CvValue[] retval = new CvValue[numCvs];
169        int i;
170        for (i = 0; i < numCvs; i++) {
171            retval[i] = _cvMap.get(cvList[i]);
172        }
173        return retval;
174    }
175
176    /**
177     * Called for new values of a slider.
178     * <p>
179     * Sets the CV(s) as needed.
180     *
181     */
182    @Override
183    public void stateChanged(ChangeEvent e) {
184        // e.getSource() points to the JSlider object - find it in the list
185        JSlider j = (JSlider) e.getSource();
186        BoundedRangeModel r = j.getModel();
187
188        for (int i = 0; i < nValues; i++) {
189            if (r == models[i]) {
190                // found it, and i is useful!
191                setModel(i, r.getValue());
192                break; // no need to continue loop
193            }
194        }
195        // notify that Value property changed
196        prop.firePropertyChange("Value", null, j);
197    }
198
199    void setModel(int i, int value) {  // value is _min to _max
200        if (value < _min || (mfx && (i == 0))) {
201            value = _min;
202        }
203        if (value > _max || (mfx && (i == nValues - 1))) {
204            value = _max;
205        }
206        if (i < nValues && models[i].getValue() != value) {
207            models[i].setValue(value);
208        }
209        // update the CV
210        _cvMap.get(cvList[i]).setValue(value);
211        // if programming, that's it
212        if (isReading || isWriting) {
213            return;
214        } else if (i < nValues && !(mfx && (i == 0 || i == (nValues - 1)))) {
215            forceMonotonic(i, value);
216            matchPoints(i);
217        }
218    }
219
220    /**
221     * Check entries on either side to see if they are set monotonically. If
222     * not, adjust.
223     *
224     * @param modifiedStepIndex number (index) of the entry
225     * @param value             new value
226     */
227    void forceMonotonic(int modifiedStepIndex, int value) {
228        // check the neighbors, and force them if needed
229        if (modifiedStepIndex > 0) {
230            // left neighbour
231            if (models[modifiedStepIndex - 1].getValue() > value) {
232                setModel(modifiedStepIndex - 1, value);
233            }
234        }
235        if (modifiedStepIndex < nValues - 1) {
236            // right neighbour
237            if (value > models[modifiedStepIndex + 1].getValue()) {
238                setModel(modifiedStepIndex + 1, value);
239            }
240        }
241    }
242
243    /**
244     * If there are fixed points specified, set linear step settings to them.
245     * @param modifiedStepIndex Index of requested break point
246     *
247     */
248    void matchPoints(int modifiedStepIndex) {
249        if (stepCheckBoxes == null) {
250            // if no stepCheckBoxes, then GUI not present, and
251            // no need to use the matchPoints algorithm
252            return;
253        }
254        if (modifiedStepIndex < 0) {
255            log.error("matchPoints called with index too small: {}", modifiedStepIndex);
256        }
257        if (modifiedStepIndex >= stepCheckBoxes.size()) {
258            log.error("matchPoints called with index too large: {} >= {}", modifiedStepIndex, stepCheckBoxes.size());
259        }
260        if (stepCheckBoxes.get(modifiedStepIndex) == null) {
261            log.error("matchPoints found null checkbox {}", modifiedStepIndex);
262        }
263
264        // don't do the match if this step isn't checked,
265        // which is necessary to keep from an infinite
266        // recursion
267        if (!stepCheckBoxes.get(modifiedStepIndex).isSelected()) {
268            return;
269        }
270        matchPointsLeft(modifiedStepIndex);
271        matchPointsRight(modifiedStepIndex);
272    }
273
274    void matchPointsLeft(int modifiedStepIndex) {
275        // search for checkbox if any
276        for (int i = modifiedStepIndex - 1; i >= 0; i--) {
277            if (stepCheckBoxes.get(i).isSelected()) {
278                // now have two ends to adjust
279                int leftval = _cvMap.get(cvList[i]).getValue();
280                int rightval = _cvMap.get(cvList[modifiedStepIndex]).getValue();
281                int steps = modifiedStepIndex - i;
282                log.debug("left found {} {} {}", leftval, rightval, steps);
283                // loop to set values
284                for (int j = i + 1; j < modifiedStepIndex; j++) {
285                    int newValue = leftval + (rightval - leftval) * (j - i) / steps;
286                    log.debug("left set {} to {}", j, newValue);
287                    if (_cvMap.get(cvList[j]).getValue() != newValue) {
288                        _cvMap.get(cvList[j]).setValue(newValue);
289                    }
290                }
291                return;
292            }
293        }
294        // no match, so don't adjust
295        return;
296    }
297
298    void matchPointsRight(int modifiedStepIndex) {
299        // search for checkbox if any
300        for (int i = modifiedStepIndex + 1; i < nValues; i++) { // need at least one intervening point
301            if (stepCheckBoxes.get(i).isSelected()) {
302                // now have two ends to adjust
303                int rightval = _cvMap.get(cvList[i]).getValue();
304                int leftval = _cvMap.get(cvList[modifiedStepIndex]).getValue();
305                int steps = i - modifiedStepIndex;
306                log.debug("right found {} {} {}", leftval, rightval, steps);
307                // loop to set values
308                for (int j = modifiedStepIndex + 1; j < i; j++) {
309                    int newValue = leftval + (rightval - leftval) * (j - modifiedStepIndex) / steps;
310                    log.debug("right set {} to {}", j, newValue);
311                    if (_cvMap.get(cvList[j]).getValue() != newValue) {
312                        _cvMap.get(cvList[j]).setValue(newValue);
313                    }
314                }
315                return;
316            }
317        }
318        // no match, so don't adjust
319        return;
320    }
321
322    @Override
323    public ValueState getState() {
324        int i;
325        for (i = 0; i < numCvs; i++) {
326            if (_cvMap.get(cvList[i]).getState() == ValueState.UNKNOWN) {
327                return ValueState.UNKNOWN;
328            }
329        }
330        for (i = 0; i < numCvs; i++) {
331            if (_cvMap.get(cvList[i]).getState() == ValueState.EDITED) {
332                return ValueState.EDITED;
333            }
334        }
335        for (i = 0; i < numCvs; i++) {
336            if (_cvMap.get(cvList[i]).getState() == ValueState.FROMFILE) {
337                return ValueState.FROMFILE;
338            }
339        }
340        for (i = 0; i < numCvs; i++) {
341            if (_cvMap.get(cvList[i]).getState() == ValueState.READ) {
342                return ValueState.READ;
343            }
344        }
345        for (i = 0; i < numCvs; i++) {
346            if (_cvMap.get(cvList[i]).getState() == ValueState.STORED) {
347                return ValueState.STORED;
348            }
349        }
350        log.error("getState did not decode a possible state");
351        return ValueState.UNKNOWN;
352    }
353
354    // to complete this class, fill in the routines to handle "Value" parameter
355    // and to read/write/hear parameter changes.
356    @Override
357    public String getValueString() {
358        StringBuffer buf = new StringBuffer();
359        for (int i = 0; i < models.length; i++) {
360            if (i != 0) {
361                buf.append(",");
362            }
363            buf.append(Integer.toString(models[i].getValue()));
364        }
365        return buf.toString();
366    }
367
368    /**
369     * Set value from a String value.
370     * <p>
371     * Requires the format written by getValueString, not implemented yet
372     */
373    @Override
374    public void setValue(String value) {
375        log.debug("skipping setValue in SpeedTableVarValue");
376    }
377
378    @Override
379    public void setIntValue(int i) {
380        log.warn("setIntValue doesn't make sense for a speed table: {}", i);
381    }
382
383    @Override
384    public int getIntValue() {
385        log.warn("getValue doesn't make sense for a speed table");
386        return 0;
387    }
388
389    @Override
390    public Object getValueObject() {
391        return null;
392    }
393
394    @Override
395    public Component getCommonRep() {
396        log.warn("getValue not implemented yet");
397        return new JLabel("speed table");
398    }
399
400    public void setValue(int value) {
401        log.warn("setValue doesn't make sense for a speed table: {}", value);
402    }
403
404    Color _defaultColor;
405
406    // implement an abstract member to set colors
407    @Override
408    void setColor(Color c) {
409        // prop.firePropertyChange("Value", null, null);
410    }
411
412    @Override
413    public Component getNewRep(String format) {
414        final int GRID_Y_BUTTONS = 3;
415        // put together a new panel in scroll pane
416        JPanel j = new JPanel();
417
418        GridBagLayout g = new GridBagLayout();
419        GridBagConstraints cs = new GridBagConstraints();
420        j.setLayout(g);
421
422        initStepCheckBoxes();
423
424        for (int i = 0; i < nValues; i++) {
425            cs.gridy = 0;
426            cs.gridx = i;
427
428            CvValue cv = _cvMap.get(cvList[i]);
429            JSlider s = new VarSlider(models[i], cv, i + 1);
430            s.setOrientation(JSlider.VERTICAL);
431            s.addChangeListener(this);
432
433            ValueState currentState = cv.getState();
434            int currentValue = cv.getValue();
435
436            DecVariableValue decVal = new DecVariableValue("val" + i, "", "", false, false, false, false,
437                    cvList[i], "VVVVVVVV", _min, _max,
438                    _cvMap, _status, "");
439            decVal.setValue(currentValue);
440            decVal.setState(currentState);
441
442            Component v = decVal.getCommonRep();
443            String start = ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TextStep")
444                    + " " + (i + 1);
445            ((JTextField) v).setToolTipText(CvUtil.addCvDescription(start, "CV " + cvList[i], null));
446            ((JComponent) v).setBorder(null);  // pack tighter
447
448            if (mfx && (i == 0 || i == (nValues - 1))) {
449                ((JTextField) v).setEditable(false); // disable field editing
450                s.setEnabled(false);    // disable slider adjustment
451            }
452
453            g.setConstraints(v, cs);
454
455            if (i == 0 && log.isDebugEnabled()) {
456                log.debug("Font size {}", v.getFont().getSize());
457            }
458            float newSize = v.getFont().getSize() * 0.8f;
459            v.setFont(v.getFont().deriveFont(newSize));
460            j.add(v);
461
462            cs.gridy++;
463            g.setConstraints(s, cs);
464
465            j.add(s);
466
467            cs.gridy++;
468            JCheckBox b = stepCheckBoxes.get(i);
469
470            g.setConstraints(b, cs);
471            j.add(b, cs);
472
473            UserPreferencesManager upm = InstanceManager.getDefault(UserPreferencesManager.class);
474            Object speedTableNumbersSelectionObj = upm.getProperty(SpeedTableNumbers.class.getName(), "selection");
475
476            if (speedTableNumbersSelectionObj != null) {
477                SpeedTableNumbers speedTableNumbersSelection =
478                        SpeedTableNumbers.valueOf(speedTableNumbersSelectionObj.toString());
479
480                if ((speedTableNumbersSelection != SpeedTableNumbers.None)
481                        && (speedTableNumbersSelection.filter(i) || (i==0) || (i+1 == nValues))) {
482
483                    cs.gridy++;
484                    JLabel num = new JLabel(""+(i+1));
485                    num.setToolTipText("Step Number");
486
487                    g.setConstraints(num, cs);
488                    j.add(num, cs);
489                }
490            }
491        }
492
493        // add control buttons
494        JPanel k = new JPanel();
495        JButton b;
496        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonForceStraight")));
497        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipForceStraight"));
498        b.addActionListener(new java.awt.event.ActionListener() {
499            @Override
500            public void actionPerformed(java.awt.event.ActionEvent e) {
501                doForceStraight(e);
502            }
503        });
504        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonMatchEnds")));
505        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipMatchEnds"));
506        b.addActionListener(new java.awt.event.ActionListener() {
507            @Override
508            public void actionPerformed(java.awt.event.ActionEvent e) {
509                doMatchEnds(e);
510            }
511        });
512        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonConstantRatio")));
513        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipConstantRatio"));
514        b.addActionListener(new java.awt.event.ActionListener() {
515            @Override
516            public void actionPerformed(java.awt.event.ActionEvent e) {
517                doRatioCurve(e);
518            }
519        });
520        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonLogCurve")));
521        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipLogCurve"));
522        b.addActionListener(new java.awt.event.ActionListener() {
523            @Override
524            public void actionPerformed(java.awt.event.ActionEvent e) {
525                doLogCurve(e);
526            }
527        });
528        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonShiftLeft")));
529        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipShiftLeft"));
530        b.addActionListener(new java.awt.event.ActionListener() {
531            @Override
532            public void actionPerformed(java.awt.event.ActionEvent e) {
533                doShiftLeft(e);
534            }
535        });
536        k.add(b = new JButton(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("ButtonShiftRight")));
537        b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipShiftRight"));
538        b.addActionListener(new java.awt.event.ActionListener() {
539            @Override
540            public void actionPerformed(java.awt.event.ActionEvent e) {
541                doShiftRight(e);
542            }
543        });
544
545        cs.gridy = GRID_Y_BUTTONS;
546        cs.gridx = 0;
547        cs.gridwidth = GridBagConstraints.RELATIVE;
548        g.setConstraints(k, cs);
549
550        // add Vstart & Vhigh if applicable
551        JPanel l = new JPanel();
552
553        JPanel val = new JPanel();
554        val.setLayout(new BorderLayout());
555        val.add(j, BorderLayout.NORTH);
556        val.add(k, BorderLayout.CENTER);
557        if (mfx) {
558            val.add(l, BorderLayout.SOUTH);
559        }
560
561        updateRepresentation(val);
562        return val;
563
564    }
565
566    void initStepCheckBoxes() {
567        stepCheckBoxes = new ArrayList<JCheckBox>();
568        for (int i = 0; i < nValues; i++) {
569            JCheckBox b = new JCheckBox();
570            b.setToolTipText(ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TooltipCheckToFix"));
571            stepCheckBoxes.add(b);
572        }
573    }
574
575    /**
576     * Set the values to a straight line from _min to _max
577     * @param e Event triggering this operation
578     */
579    void doForceStraight(java.awt.event.ActionEvent e) {
580        _cvMap.get(cvList[0]).setValue(_min);
581        _cvMap.get(cvList[nValues - 1]).setValue(_max);
582        doMatchEnds(e);
583    }
584
585    /**
586     * Set the values to a straight line from existing ends
587     * @param e Event triggering this operation
588     */
589    void doMatchEnds(java.awt.event.ActionEvent e) {
590        int first = _cvMap.get(cvList[0]).getValue();
591        int last = _cvMap.get(cvList[nValues - 1]).getValue();
592        log.debug(" first={} last={}", first, last);
593        // to avoid repeatedly bumping up later values, push the first one
594        // all the way up now
595        _cvMap.get(cvList[0]).setValue(last);
596        // and push each one down
597        for (int i = 0; i < nValues; i++) {
598            int value = first + i * (last - first) / (nValues - 1);
599            _cvMap.get(cvList[i]).setValue(value);
600        }
601//         enforceEndPointsMfx();
602    }
603
604    /**
605     * Set a constant ratio curve
606     * @param e Event triggering this operation
607     */
608    void doRatioCurve(java.awt.event.ActionEvent e) {
609        double first = _cvMap.get(cvList[0]).getValue();
610        if (first < 1.) {
611            first = 1.;
612        }
613        double last = _cvMap.get(cvList[nValues - 1]).getValue();
614        if (last < first + 1) {
615            last = first + 1.;
616        }
617        double step = Math.log(last / first) / (nValues - 1);
618        log.debug("log ratio step is {}", step);
619        // to avoid repeatedly bumping up later values, push the first one
620        // all the way up now
621        _cvMap.get(cvList[0]).setValue((int) Math.round(last));
622        // and push each one down
623        for (int i = 0; i < nValues; i++) {
624            int value = (int) (Math.floor(first * Math.exp(step * i)));
625            _cvMap.get(cvList[i]).setValue(value);
626        }
627//         enforceEndPointsMfx();
628    }
629
630    /**
631     * Set a log curve
632     * @param e Event triggering this operation
633     */
634    void doLogCurve(java.awt.event.ActionEvent e) {
635        double first = _cvMap.get(cvList[0]).getValue();
636        double last = _cvMap.get(cvList[nValues - 1]).getValue();
637        if (last < first + 1.) {
638            last = first + 1.;
639        }
640        double factor = 1. / 10.;
641        // to avoid repeatedly bumping up later values, push the second one
642        // all the way up now
643        _cvMap.get(cvList[1]).setValue((int) Math.round(last));
644        // and push each one down (except the first, left as it was)
645        double ratio = Math.pow(1. - factor, nValues - 1.);
646        double limit = last + (last - first) * ratio;
647        for (int i = 1; i < nValues; i++) {
648            double previous = limit - (limit - first) * ratio / Math.pow(1. - factor, nValues - 1. - i);
649            int value = (int) (Math.floor(previous));
650            _cvMap.get(cvList[i]).setValue(value);
651        }
652//         enforceEndPointsMfx();
653    }
654
655    /**
656     * Shift the curve one CV to left. The last entry is left unchanged.
657     * @param e Event triggering this operation
658     */
659    void doShiftLeft(java.awt.event.ActionEvent e) {
660        for (int i = 0; i < nValues - 1; i++) {
661            int value = _cvMap.get(cvList[i + 1]).getValue();
662            _cvMap.get(cvList[i]).setValue(value);
663        }
664//         enforceEndPointsMfx();
665    }
666
667    /**
668     * Shift the curve one CV to right. The first entry is left unchanged.
669     * @param e Event triggering this operation
670     */
671    void doShiftRight(java.awt.event.ActionEvent e) {
672        for (int i = nValues - 1; i > 0; i--) {
673            int value = _cvMap.get(cvList[i - 1]).getValue();
674            _cvMap.get(cvList[i]).setValue(value);
675        }
676//         enforceEndPointsMfx();
677    }
678
679    /**
680     * IDLE if a read/write operation is not in progress. During an operation,
681     * it indicates the index of the CV to handle when the current programming
682     * operation finishes.
683     */
684    private int _progState = IDLE;
685
686    private static final int IDLE = -1;
687    boolean isReading;
688    boolean isWriting;
689
690    /**
691     * Count number of retries done
692     */
693    private int retries = 0;
694
695    /**
696     * Define maximum number of retries of read/write operations before moving
697     * on
698     */
699    private static final int RETRY_MAX = 2;
700
701    boolean onlyChanges = false;
702
703    /**
704     * Notify the connected CVs of a state change from above
705     *
706     */
707    @Override
708    public void setCvState(ValueState state) {
709        _cvMap.get(cvList[0]).setState(state);
710    }
711
712    @Override
713    public boolean isChanged() {
714        for (int i = 0; i < numCvs; i++) {
715            if (considerChanged(_cvMap.get(cvList[i]))) {
716                // this one is changed, return true
717                return true;
718            }
719        }
720        return false;
721    }
722
723    @Override
724    public void readChanges() {
725        if (log.isDebugEnabled()) {
726            log.debug("readChanges() invoked");
727        }
728        if (!isChanged()) {
729            return;
730        }
731        onlyChanges = true;
732        setBusy(true);  // will be reset when value changes
733        if (_progState != IDLE) {
734            log.warn("Programming state {}, not IDLE, in read()", _progState);
735        }
736        isReading = true;
737        isWriting = false;
738        _progState = -1;
739        retries = 0;
740        if (log.isDebugEnabled()) {
741            log.debug("start series of read operations");
742        }
743        readNext();
744    }
745
746    @Override
747    public void writeChanges() {
748        if (log.isDebugEnabled()) {
749            log.debug("writeChanges() invoked");
750        }
751        if (!isChanged()) {
752            return;
753        }
754        onlyChanges = true;
755        if (getReadOnly()) {
756            log.error("unexpected write operation when readOnly is set");
757        }
758        setBusy(true);  // will be reset when value changes
759        super.setState(ValueState.STORED);
760        if (_progState != IDLE) {
761            log.warn("Programming state {}, not IDLE, in write()", _progState);
762        }
763        isReading = false;
764        isWriting = true;
765        _progState = -1;
766        retries = 0;
767        if (log.isDebugEnabled()) {
768            log.debug("start series of write operations");
769        }
770        writeNext();
771    }
772
773    @Override
774    public void readAll() {
775        if (log.isDebugEnabled()) {
776            log.debug("readAll() invoked");
777        }
778        onlyChanges = false;
779        setToRead(false);
780        setBusy(true);  // will be reset when value changes
781        if (_progState != IDLE) {
782            log.warn("Programming state {}, not IDLE, in read()", _progState);
783        }
784        isReading = true;
785        isWriting = false;
786        _progState = -1;
787        retries = 0;
788        if (log.isDebugEnabled()) {
789            log.debug("start series of read operations");
790        }
791        readNext();
792    }
793
794    @Override
795    public void writeAll() {
796        if (log.isDebugEnabled()) {
797            log.debug("writeAll() invoked");
798        }
799        onlyChanges = false;
800        if (getReadOnly()) {
801            log.error("unexpected write operation when readOnly is set");
802        }
803        setToWrite(false);
804        setBusy(true);  // will be reset when value changes
805        super.setState(ValueState.STORED);
806        if (_progState != IDLE) {
807            log.warn("Programming state {}, not IDLE, in write()", _progState);
808        }
809        isReading = false;
810        isWriting = true;
811        _progState = -1;
812        retries = 0;
813        if (log.isDebugEnabled()) {
814            log.debug("start series of write operations");
815        }
816        writeNext();
817    }
818
819    void readNext() {
820        // read operation start/continue
821        // check for retry if needed
822        if ((_progState >= 0) && (retries < RETRY_MAX)
823                && (_cvMap.get(cvList[_progState]).getState() != ValueState.READ)) {
824            // need to retry an error; leave progState (CV number) as it was
825            retries++;
826        } else {
827            // normal read operation of next CV
828            retries = 0;
829            _progState++;  // progState is the index of the CV to handle now
830        }
831
832        if (_progState >= numCvs) {
833            // done, clean up and return to invoker
834            _progState = IDLE;
835            isReading = false;
836            isWriting = false;
837            setBusy(false);
838            return;
839        }
840        // not done, proceed to do the next
841        CvValue cv = _cvMap.get(cvList[_progState]);
842        ValueState state = cv.getState();
843        if (log.isDebugEnabled()) {
844            log.debug("invoke CV read index {} cv state {}", _progState, state);
845        }
846        if (!onlyChanges || considerChanged(cv)) {
847            cv.read(_status);
848        } else {
849            readNext(); // repeat until end
850        }
851    }
852
853    void writeNext() {
854        // write operation start/continue
855        // check for retry if needed
856        if ((_progState >= 0) && (retries < RETRY_MAX)
857                && (_cvMap.get(cvList[_progState]).getState() != ValueState.STORED)) {
858            // need to retry an error; leave progState (CV number) as it was
859            retries++;
860        } else {
861            // normal read operation of next CV
862            retries = 0;
863            _progState++;  // progState is the index of the CV to handle now
864        }
865
866        if (_progState >= numCvs) {
867            _progState = IDLE;
868            isReading = false;
869            isWriting = false;
870            setBusy(false);
871            return;
872        }
873        CvValue cv = _cvMap.get(cvList[_progState]);
874        ValueState state = cv.getState();
875        if (log.isDebugEnabled()) {
876            log.debug("invoke CV write index {} cv state {}", _progState, state);
877        }
878        if (!onlyChanges || considerChanged(cv)) {
879            cv.write(_status);
880        } else {
881            writeNext();
882        }
883    }
884
885    // handle incoming parameter notification
886    @Override
887    public void propertyChange(java.beans.PropertyChangeEvent e) {
888        if (log.isDebugEnabled()) {
889            log.debug("property changed event - name: {}", e.getPropertyName());
890        }
891        // notification from CV; check for Value being changed
892        if (e.getPropertyName().equals("Busy") && ((Boolean) e.getNewValue()).equals(Boolean.FALSE)) {
893            // busy transitions drive an ongoing programming operation
894            // see if actually done
895
896            if (isReading) {
897                readNext();
898            } else if (isWriting) {
899                writeNext();
900            } else {
901                return;
902            }
903        } else if (e.getPropertyName().equals("State")) {
904            CvValue cv = _cvMap.get(cvList[0]);
905            if (log.isDebugEnabled()) {
906                log.debug("CV State changed to {}", cv.getState());
907            }
908            setState(cv.getState());
909        } else if (e.getPropertyName().equals("Value")) {
910            // find the CV that sent this
911            CvValue cv = (CvValue) e.getSource();
912            int value = cv.getValue();
913            // find the index of that CV
914            for (int i = 0; i < numCvs; i++) {
915                if (_cvMap.get(cvList[i]) == cv) {
916                    // this is the one, so use this i
917                    setModel(i, value);
918                    break;
919                }
920            }
921//         enforceEndPointsMfx();
922        }
923    }
924
925    /* Internal class extends a JSlider so that its color is consistent with
926     * an underlying CV; we return one of these in getNewRep.
927     * <p>
928     * Unlike similar cases elsewhere, this doesn't have to listen to
929     * value changes.  Those are handled automagically since we're sharing the same
930     * model between this object and others.  And this is listening to
931     * a CV state, not a variable.
932     *
933     * @author   Bob Jacobsen   Copyright (C) 2001
934     */
935    public class VarSlider extends JSlider {
936
937        VarSlider(BoundedRangeModel m, CvValue var, int step) {
938            super(m);
939            _var = var;
940            // get the original color right
941            setBackground(_var.getColor());
942            if (_var.getColor() == _var.getDefaultColor()) {
943                setOpaque(false);
944            } else {
945                setOpaque(true);
946            }
947            // tooltip label
948            String start = ResourceBundle.getBundle("jmri.jmrit.symbolicprog.SymbolicProgBundle").getString("TextStep")
949                    + " " + step;
950            setToolTipText(CvUtil.addCvDescription(start, "CV " + var.number(), null));
951            // listen for changes to original state
952            _var.addPropertyChangeListener(new java.beans.PropertyChangeListener() {
953                @Override
954                public void propertyChange(java.beans.PropertyChangeEvent e) {
955                    originalPropertyChanged(e);
956                }
957            });
958        }
959
960        CvValue _var;
961
962        void originalPropertyChanged(java.beans.PropertyChangeEvent e) {
963            if (log.isDebugEnabled()) {
964                log.debug("VarSlider saw property change: {}", e);
965            }
966            // update this color from original state
967            if (e.getPropertyName().equals("State")) {
968                setBackground(_var.getColor());
969                if (_var.getColor() == _var.getDefaultColor()) {
970                    setOpaque(false);
971                } else {
972                    setOpaque(true);
973                }
974            }
975        }
976
977    }  // end class definition
978
979    // clean up connections when done
980    @Override
981    public void dispose() {
982        if (log.isDebugEnabled()) {
983            log.debug("dispose");
984        }
985        // the connection is to cvNum through cvNum+numCvs (28 values typical)
986        for (int i = 0; i < numCvs; i++) {
987            _cvMap.get(cvList[i]).removePropertyChangeListener(this);
988        }
989
990        // do something about the VarSlider objects
991    }
992
993    // initialize logging
994    private final static Logger log = LoggerFactory.getLogger(SpeedTableVarValue.class);
995
996}