001package jmri.jmrit.throttle;
002
003import com.fasterxml.jackson.core.JsonProcessingException;
004import com.fasterxml.jackson.databind.ObjectMapper;
005
006import java.awt.*;
007import java.awt.event.*;
008import java.awt.image.BufferedImage;
009import java.io.IOException;
010import java.util.*;
011
012import javax.swing.*;
013import javax.swing.event.ChangeEvent;
014import javax.swing.plaf.basic.BasicSliderUI;
015
016import jmri.*;
017import jmri.jmrit.roster.Roster;
018import jmri.jmrit.roster.RosterEntry;
019import jmri.util.FileUtil;
020import jmri.util.MouseInputAdapterInstaller;
021import jmri.util.swing.JmriMouseAdapter;
022import jmri.util.swing.JmriMouseEvent;
023import jmri.util.swing.JmriMouseListener;
024
025import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
026import org.apache.batik.transcoder.*;
027import org.apache.batik.transcoder.image.ImageTranscoder;
028import org.apache.batik.util.XMLResourceDescriptor;
029import org.jdom2.Element;
030import org.jdom2.Attribute;
031import org.w3c.dom.Document;
032
033/**
034 * A JInternalFrame that contains a JSlider to control loco speed, and buttons
035 * for forward, reverse and STOP.
036 *
037 * @author glen Copyright (C) 2002
038 * @author Bob Jacobsen Copyright (C) 2007, 2021
039 * @author Ken Cameron Copyright (C) 2008
040 * @author Lionel Jeanson 2009-2021
041 */
042public class ControlPanel extends JInternalFrame implements java.beans.PropertyChangeListener, AddressListener {
043
044    private final ThrottleManager throttleManager;
045
046    private DccThrottle throttle;
047    private boolean isConsist = false;
048
049    private JSlider speedSlider;
050    private JSlider speedSliderContinuous;
051    private JSpinner speedSpinner;
052    private SpinnerNumberModel speedSpinnerModel;
053    private JComboBox<SpeedStepMode> speedStepBox;
054    private JRadioButton forwardButton, reverseButton;
055    private JButton stopButton;
056    private JButton idleButton;
057    private JPanel buttonPanel;
058    private JPanel topButtonPanel;
059
060    private Document forwardButtonSvgIcon;
061    private Document forwardSelectedButtonSvgIcon;
062    private Document forwardRollButtonSvgIcon;
063    private ImageIcon forwardButtonImageIcon;
064    private ImageIcon forwardSelectedButtonImageIcon;
065    private ImageIcon forwardRollButtonImageIcon;
066
067    private Document reverseButtonSvgIcon;
068    private Document reverseSelectedButtonSvgIcon;
069    private Document reverseRollButtonSvgIcon;
070    private ImageIcon reverseButtonImageIcon;
071    private ImageIcon reverseSelectedButtonImageIcon;
072    private ImageIcon reverseRollButtonImageIcon;
073
074    private Document idleButtonSvgIcon;
075    private Document idleSelectedButtonSvgIcon;
076    private Document idleRollButtonSvgIcon;
077    private ImageIcon idleButtonImageIcon;
078    private ImageIcon idleSelectedButtonImageIcon;
079    private ImageIcon idleRollButtonImageIcon;
080
081    private Document stopButtonSvgIcon;
082    private Document stopSelectedButtonSvgIcon;
083    private Document stopRollButtonSvgIcon;
084    private ImageIcon stopButtonImageIcon;
085    private ImageIcon stopSelectedButtonImageIcon;
086    private ImageIcon stopRollButtonImageIcon;
087    
088    private ImageIcon speedLabelVerticalImageIcon;
089    private ImageIcon speedLabelHorizontalImageIcon;
090    
091    private Map<Integer, JLabel> defaultLabelTable;    
092    private Map<Integer, JLabel> verticalLabelMap;
093    private Map<Integer, JLabel> horizontalLabelMap;
094
095    private boolean internalAdjust = false; // protecting the speed slider, continuous slider and spinner when doing internal adjust
096
097    private JPopupMenu popupMenu;
098    private ControlPanelPropertyEditor propertyEditor;
099    private JPanel speedControlPanel;
100    private JPanel spinnerPanel;
101    private JPanel sliderPanel;
102    private JPanel speedSliderContinuousPanel;
103
104    private AddressPanel addressPanel; //for access to roster entry
105    /* Constants for speed selection method */
106    final public static int SLIDERDISPLAY = 0;
107    final public static int STEPDISPLAY = 1;
108    final public static int SLIDERDISPLAYCONTINUOUS = 2;
109
110    final public static int DEFAULT_BUTTON_SIZE = 24;
111    private static final String LONGEST_SS_STRING="999";
112    private static final int FONT_SIZE_MIN=12;
113    private static final int FONT_INCREMENT = 2;
114
115    private int _displaySlider = SLIDERDISPLAY;
116
117    /* real time tracking of speed slider - on iff trackSlider==true
118     * Min interval for sending commands to the actual throttle can be configured
119     * as part of the throttle config but is bounded
120     */
121    private JPanel mainPanel;
122
123    private boolean trackSlider = false;
124    private boolean hideSpeedStep = false;
125    private final boolean trackSliderDefault = false;
126    private long trackSliderMinInterval = 200;         // milliseconds
127    private final long trackSliderMinIntervalDefault = 200;  // milliseconds
128    private final long trackSliderMinIntervalMin = 50;       // milliseconds
129    private final long trackSliderMinIntervalMax = 1000;     // milliseconds
130    private long lastTrackedSliderMovementTime = 0;
131
132    // LocoNet really only has 126 speed steps i.e. 0..127 - 1 for em stop
133    private int intSpeedSteps = 126;
134
135    private int maxSpeed = 126; //The maximum permissible speed
136
137    private boolean speedControllerEnable = false;
138
139    // Switch to continuous slider on function...
140    private String switchSliderFunction = "Fxx";
141    private String prevShuntingFn = null;
142
143    /**
144     * Constructor.
145     */
146    public ControlPanel() {
147        this(InstanceManager.getDefault(ThrottleManager.class));
148    }
149
150    /**
151     * Constructor.
152     * @param tm the throttle manager
153     */
154    public ControlPanel(ThrottleManager tm) {
155        throttleManager = tm;
156        initGUI();
157        applyPreferences();
158    }
159
160    /*
161     * Set the AddressPanel this throttle control is listenning for new throttle event
162     */
163    public void setAddressPanel(AddressPanel addressPanel) {
164        this.addressPanel = addressPanel;
165    }
166
167    /*
168     * "Destructor"
169     */
170    public void destroy() {
171        if (addressPanel != null) {
172            addressPanel.removeAddressListener(this);
173            addressPanel = null;
174        }
175        if (throttle != null) {
176            throttle.removePropertyChangeListener(this);
177            throttle = null;
178        }
179    }
180
181    /**
182     * Enable/Disable all buttons and slider.
183     *
184     * @param isEnabled True if the buttons/slider should be enabled, false
185     *                  otherwise.
186     */
187    @Override
188    public void setEnabled(boolean isEnabled) {
189        forwardButton.setEnabled(isEnabled);
190        reverseButton.setEnabled(isEnabled);
191        speedStepBox.setEnabled(isEnabled);
192        stopButton.setEnabled(isEnabled);
193        idleButton.setEnabled(isEnabled);
194        speedControllerEnable = isEnabled;
195        switch (_displaySlider) {
196            case STEPDISPLAY: {
197                speedSpinner.setEnabled(isEnabled);
198                speedSliderContinuous.setEnabled(false);                
199                speedSlider.setEnabled(false);
200                break;
201            }
202            case SLIDERDISPLAYCONTINUOUS: {
203                speedSliderContinuous.setEnabled(isEnabled);            
204                speedSpinner.setEnabled(false);                
205                speedSlider.setEnabled(false);
206                break;
207            }
208            default: {
209                speedSpinner.setEnabled(false);
210                speedSliderContinuous.setEnabled(false);
211                speedSlider.setEnabled(isEnabled);
212            }
213        }
214    }
215
216    /**
217     * is this enabled?
218     * @return true if enabled
219     */
220    @Override
221    public boolean isEnabled() {
222        return speedControllerEnable;
223    }
224
225    /**
226     * Set the GUI to match that the loco is set to forward.
227     *
228     * @param isForward True if the loco is set to forward, false otherwise.
229     */
230    private void setIsForward(boolean isForward) {
231        forwardButton.setSelected(isForward);
232        reverseButton.setSelected(!isForward);
233        internalAdjust = true;
234        if (isForward) {
235            speedSliderContinuous.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));
236        } else {
237            speedSliderContinuous.setValue(-java.lang.Math.abs(speedSliderContinuous.getValue()));
238        }
239        internalAdjust = false;        
240    }
241
242    /**
243     * Set the GUI to match the speed steps of the current address. Initialises
244     * the speed slider and spinner - including setting their maximums based on
245     * the speed step setting and the max speed for the particular loco
246     *
247     * @param speedStepMode Desired speed step mode. One of:
248     *                      SpeedStepMode.NMRA_DCC_128,
249     *                      SpeedStepMode.NMRA_DCC_28,
250     *                      SpeedStepMode.NMRA_DCC_27,
251     *                      SpeedStepMode.NMRA_DCC_14 step mode
252     */
253    public void setSpeedStepsMode(SpeedStepMode speedStepMode) {
254        internalAdjust = true;
255        int maxSpeedPCT = 100;
256        if (addressPanel != null && addressPanel.getRosterEntry() != null) {
257            maxSpeedPCT = addressPanel.getRosterEntry().getMaxSpeedPCT();
258        }
259
260        // Save the old speed as a float
261        float oldSpeed = (speedSlider.getValue() / (maxSpeed * 1.0f));
262
263        if (speedStepMode == SpeedStepMode.UNKNOWN) {
264            speedStepMode = (SpeedStepMode) speedStepBox.getSelectedItem();
265        } else {
266            speedStepBox.setSelectedItem(speedStepMode);
267        }
268        intSpeedSteps = speedStepMode.numSteps;
269
270        /* Set maximum speed based on the max speed stored in the roster as a percentage of the maximum */
271        maxSpeed = (int) ((float) intSpeedSteps * ((float) maxSpeedPCT) / 100);
272
273        // rescale the speed slider to match the new speed step mode
274        speedSlider.setMaximum(maxSpeed);
275        speedSlider.setValue((int) (oldSpeed * maxSpeed));
276        speedSlider.setMajorTickSpacing(maxSpeed / 2);
277
278        speedSliderContinuous.setMaximum(maxSpeed);
279        speedSliderContinuous.setMinimum(-maxSpeed);
280        if (forwardButton.isSelected()) {
281            speedSliderContinuous.setValue((int) (oldSpeed * maxSpeed));
282        } else {
283            speedSliderContinuous.setValue(-(int) (oldSpeed * maxSpeed));
284        }
285        speedSliderContinuous.setMajorTickSpacing(maxSpeed / 2);
286
287        computeLabelsTable();
288        updateSlidersLabelDisplay();
289                
290        speedSpinnerModel.setMaximum(maxSpeed);
291        speedSpinnerModel.setMinimum(0);
292        // rescale the speed value to match the new speed step mode
293        speedSpinnerModel.setValue(speedSlider.getValue());
294        internalAdjust = false;
295    }
296
297    /**
298     * Is this Speed Control selection method possible?
299     *
300     * @param displaySlider integer value. possible values: SLIDERDISPLAY = use
301     *                      speed slider display STEPDISPLAY = use speed step
302     *                      display
303     * @return true if speed controller of the selected type is available.
304     */
305    public boolean isSpeedControllerAvailable(int displaySlider) {
306        switch (displaySlider) {
307            case STEPDISPLAY:
308            case SLIDERDISPLAY:
309            case SLIDERDISPLAYCONTINUOUS:
310                return true;
311            default:
312                return false;
313        }
314    }
315
316    /**
317     * Set the Speed Control selection method
318     *
319     * @param displaySlider integer value. possible values: SLIDERDISPLAY = use
320     *                      speed slider display STEPDISPLAY = use speed step
321     *                      display
322     */
323    public void setSpeedController(int displaySlider) {
324        _displaySlider = displaySlider;
325        switch (displaySlider) {
326            case STEPDISPLAY:
327                sliderPanel.setVisible(false);
328                speedSlider.setEnabled(false);
329                speedSliderContinuousPanel.setVisible(false);
330                speedSliderContinuous.setEnabled(false);                
331                spinnerPanel.setVisible(true);
332                speedSpinner.setEnabled(speedControllerEnable);
333                return;
334                
335            case SLIDERDISPLAYCONTINUOUS:
336                sliderPanel.setVisible(false);
337                speedSlider.setEnabled(false);
338                speedSliderContinuousPanel.setVisible(true);
339                speedSliderContinuous.setEnabled(speedControllerEnable);
340                spinnerPanel.setVisible(false);
341                speedSpinner.setEnabled(false);
342                return;
343                
344            case SLIDERDISPLAY:
345                // normal, drop through
346                break;
347            default:
348                jmri.util.LoggingUtil.warnOnce(log, "Unexpected displaySlider = {}", displaySlider);
349                break;
350        }
351        sliderPanel.setVisible(true);
352        speedSlider.setEnabled(speedControllerEnable);
353        spinnerPanel.setVisible(false);
354        speedSpinner.setEnabled(false);
355        speedSliderContinuousPanel.setVisible(false);
356        speedSliderContinuous.setEnabled(false);        
357    }
358
359    /**
360     * Get the value indicating what speed input we're displaying
361     *
362     * @return SLIDERDISPLAY, STEPDISPLAY or SLIDERDISPLAYCONTINUOUS
363     */
364    public int getDisplaySlider() {
365        return _displaySlider;
366    }
367
368    /**
369     * Provide direct access to speed slider for
370     * scripting.
371     * @return the speed slider
372     */
373    public JSlider getSpeedSlider() {
374        return speedSlider;
375    }
376
377    /**
378     * Set real-time tracking of speed slider, or not
379     *
380     * @param track boolean value, true to track, false to set speed on unclick
381     */
382    public void setTrackSlider(boolean track) {
383        trackSlider = track;
384    }
385
386    /**
387     * Get status of real-time speed slider tracking
388     *
389     * @return true if slider is tracking.
390     */
391    public boolean getTrackSlider() {
392        return trackSlider;
393    }
394
395    /**
396     * Set hiding speed step selector (or not)
397     *
398     * @param hide boolean value, true to hide, false to show
399     */
400    public void setHideSpeedStep(boolean hide) {
401        hideSpeedStep = hide;
402        this.speedStepBox.setVisible(! hideSpeedStep);
403    }
404
405    /**
406     * Get status of hiding  speed step selector
407     *
408     * @return true if speed step selector is hiden.
409     */
410    public boolean getHideSpeedStep() {
411        return hideSpeedStep;
412    }
413
414    /**
415     * Set the GUI to match that the loco speed.
416     *
417     *
418     * @param speedIncrement The throttle back end's speed increment value - %
419     *                       increase for each speed step.
420     * @param speed          The speed value of the loco.
421     */
422    private void setSpeedValues(float speedIncrement, float speed) {
423        //This is an internal speed adjustment
424        internalAdjust = true;
425        //Translate the speed sent in to the max allowed by any set speed limit
426        speedSlider.setValue(java.lang.Math.round(speed / speedIncrement));
427        log.debug("SpeedSlider value: {}", speedSlider.getValue());
428        // Spinner Speed should be the raw integer speed value
429        speedSpinnerModel.setValue(speedSlider.getValue());        
430        if (forwardButton.isSelected()) {
431            speedSliderContinuous.setValue(( speedSlider.getValue()));
432        } else {
433            speedSliderContinuous.setValue(-( speedSlider.getValue()));
434        }
435        
436        stopButton.setSelected((speed == -1 ));
437        idleButton.setSelected((speed == 0 ));
438        internalAdjust = false;
439    }
440
441    private GridBagConstraints makeDefaultGridBagConstraints() {
442        GridBagConstraints constraints = new GridBagConstraints();
443        constraints.anchor = GridBagConstraints.CENTER;
444        constraints.fill = GridBagConstraints.BOTH;
445        constraints.gridheight = 1;
446        constraints.gridwidth = 1;
447        constraints.ipadx = 0;
448        constraints.ipady = 0;
449        constraints.insets = new Insets(2, 2, 2, 2);
450        constraints.weightx = 1;
451        constraints.weighty = 1;
452        constraints.gridx = 0;
453        constraints.gridy = 0;
454
455        return constraints;
456    }
457
458    private void layoutTopButtonPanel() {
459        GridBagConstraints constraints = makeDefaultGridBagConstraints();
460
461        constraints.gridx = 0;
462        constraints.gridy = 0;
463        constraints.fill = GridBagConstraints.HORIZONTAL;
464        topButtonPanel.add(speedStepBox, constraints);
465    }
466
467    private void layoutButtonPanel() {
468        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
469        GridBagConstraints constraints = makeDefaultGridBagConstraints();
470        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
471            resizeButtons();
472            constraints.insets =  new Insets(0, 0, 0, 0);
473            constraints.gridheight = 2;
474            constraints.gridwidth = 2;
475            constraints.gridy = 0;
476            constraints.gridx = 0;
477            buttonPanel.add(reverseButton, constraints);
478            constraints.gridx = 3;
479            buttonPanel.add(forwardButton, constraints);
480
481            constraints.gridheight = 1;
482            constraints.gridwidth = 1;
483            constraints.gridx = 2;
484            constraints.gridy = 0;
485            buttonPanel.add(idleButton, constraints);
486            constraints.gridy = 1;
487            buttonPanel.add(stopButton, constraints);
488        } else {
489            constraints.fill = GridBagConstraints.NONE;
490            constraints.gridy = 1;
491            buttonPanel.add(forwardButton, constraints);
492            constraints.gridy = 2;
493            buttonPanel.add(reverseButton, constraints);
494            constraints.gridy = 3;
495            buttonPanel.add(idleButton, constraints);
496            constraints.gridy = 4;
497            buttonPanel.add(stopButton, constraints);
498        }
499    }
500
501    private void resizeButtons() {
502        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
503        int w = buttonPanel.getWidth();
504        int h = buttonPanel.getHeight();
505        if ((buttonPanel.getWidth() == 0 || buttonPanel.getHeight() == 0)
506                || !(preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) ){
507            w = DEFAULT_BUTTON_SIZE * 5;
508            h = DEFAULT_BUTTON_SIZE * 2;
509        }
510        float f = Math.min( Math.floorDiv(w*2,5), h );
511        if (forwardButtonSvgIcon != null ) {
512            forwardButton.setIcon(scaleTo(forwardButtonSvgIcon, f));
513        } else {
514            forwardButton.setIcon(scaleTo(forwardButtonImageIcon, (int)f));
515        }
516        if (forwardSelectedButtonSvgIcon != null) {
517            forwardButton.setSelectedIcon(scaleTo(forwardSelectedButtonSvgIcon, f));
518        } else {
519            forwardButton.setSelectedIcon(scaleTo(forwardSelectedButtonImageIcon, (int)f));
520        }
521        if (forwardRollButtonSvgIcon != null) {
522            forwardButton.setRolloverIcon(scaleTo(forwardRollButtonSvgIcon, f));
523        } else {
524            forwardButton.setRolloverIcon(scaleTo(forwardRollButtonImageIcon, (int)f));
525        }
526        if (reverseButtonSvgIcon != null) {
527            reverseButton.setIcon(scaleTo(reverseButtonSvgIcon, f));
528        } else {
529            reverseButton.setIcon(scaleTo(reverseButtonImageIcon, (int)f));
530        }
531        if (reverseSelectedButtonSvgIcon != null) {
532            reverseButton.setSelectedIcon(scaleTo(reverseSelectedButtonSvgIcon, f));
533        } else {
534            reverseButton.setSelectedIcon(scaleTo(reverseSelectedButtonImageIcon, (int)f));
535        }
536        if (reverseRollButtonSvgIcon != null) {
537            reverseButton.setRolloverIcon(scaleTo(reverseRollButtonSvgIcon, f));
538        } else {
539            reverseButton.setRolloverIcon(scaleTo(reverseRollButtonImageIcon, (int)f));
540        }
541
542        f = Math.min( Math.floorDiv(w,5), h/2 );
543        if (idleButtonSvgIcon != null) {
544            idleButton.setIcon(scaleTo(idleButtonSvgIcon, f));
545        } else {
546            idleButton.setIcon(scaleTo(idleButtonImageIcon, (int)f));
547        }
548        if (idleSelectedButtonSvgIcon != null) {
549            idleButton.setSelectedIcon(scaleTo(idleSelectedButtonSvgIcon, f));
550        } else {
551            idleButton.setSelectedIcon(scaleTo(idleSelectedButtonImageIcon, (int)f));
552        }
553        if (idleRollButtonSvgIcon != null) {
554            idleButton.setRolloverIcon(scaleTo(idleRollButtonSvgIcon, f));
555        } else {
556            idleButton.setRolloverIcon(scaleTo(idleRollButtonImageIcon, (int)f));
557        }
558        if (stopButtonSvgIcon != null) {
559            stopButton.setIcon(scaleTo(stopButtonSvgIcon, f));
560        } else {
561            stopButton.setIcon(scaleTo(stopButtonImageIcon, (int)f));
562        }
563        if (stopSelectedButtonSvgIcon != null) {
564            stopButton.setSelectedIcon(scaleTo(stopSelectedButtonSvgIcon, f));
565        } else {
566            stopButton.setSelectedIcon(scaleTo(stopSelectedButtonImageIcon, (int)f));
567        }
568        if (stopRollButtonSvgIcon != null) {
569            stopButton.setRolloverIcon(scaleTo(stopRollButtonSvgIcon, f));
570        } else {
571            stopButton.setRolloverIcon(scaleTo(stopRollButtonImageIcon, (int)f));
572        }
573    }
574
575    private ImageIcon scaleTo(ImageIcon imic, int s ) {
576        return new ImageIcon(imic.getImage().getScaledInstance(s, s, Image.SCALE_SMOOTH));
577    }
578    
579    MyTranscoder transcoder = new MyTranscoder();
580
581    private ImageIcon scaleTo(Document svgImage, Float f ) {        
582        TranscodingHints hints = new TranscodingHints();
583        hints.put(ImageTranscoder.KEY_WIDTH, f );
584        hints.put(ImageTranscoder.KEY_HEIGHT, f );
585        transcoder.setTranscodingHints(hints);
586        try {
587            transcoder.transcode(new TranscoderInput(svgImage), null);
588        } catch (TranscoderException ex) {
589            // log it, but continue
590            log.debug("Exception while transposing : {}", ex.getMessage());
591        }
592        return new ImageIcon(transcoder.getImage());
593    }
594
595    private void layoutSliderPanel() {
596        sliderPanel.setLayout(new GridBagLayout());
597        sliderPanel.add(speedSlider, makeDefaultGridBagConstraints());
598    }
599
600    private void layoutSpeedSliderContinuous() {
601        speedSliderContinuousPanel.setLayout(new GridBagLayout());
602        speedSliderContinuousPanel.add(speedSliderContinuous, makeDefaultGridBagConstraints());
603    }
604
605    private void layoutSpinnerPanel() {
606        spinnerPanel.setLayout(new GridBagLayout());
607        GridBagConstraints constraints = makeDefaultGridBagConstraints();
608        constraints.fill = GridBagConstraints.HORIZONTAL;
609        spinnerPanel.add(speedSpinner, constraints);
610    }
611
612    private void setupButton(AbstractButton button, final ThrottlesPreferences preferences, final String message) {
613        button.setHorizontalAlignment(SwingConstants.CENTER);
614        button.setVerticalAlignment(SwingConstants.CENTER);
615        button.setToolTipText(Bundle.getMessage(message));
616        if (preferences != null && preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
617            button.setBorder(null);
618            button.setBorderPainted(false);
619            button.setContentAreaFilled(false);
620            button.setText(null);
621            button.setRolloverEnabled(true);
622        } else {
623            button.setBorder((new JButton()).getBorder());
624            button.setBorderPainted(true);
625            button.setContentAreaFilled(true);
626            button.setText(Bundle.getMessage(message));
627            button.setIcon(null);
628            button.setSelectedIcon(null);
629            button.setRolloverIcon(null);
630            button.setRolloverEnabled(false);
631        }
632    }
633
634    /**
635     * Create, initialize and place GUI components.
636     */
637    private void initGUI() {
638        mainPanel = new JPanel(new BorderLayout());
639        this.setContentPane(mainPanel);
640        this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
641
642        JPanel speedPanel = new JPanel();
643        speedPanel.setLayout(new BorderLayout());
644        speedPanel.setOpaque(false);
645        mainPanel.add(speedPanel, BorderLayout.CENTER);
646
647        topButtonPanel = new JPanel();
648        topButtonPanel.setLayout(new GridBagLayout());
649        speedPanel.add(topButtonPanel, BorderLayout.NORTH);
650
651        speedControlPanel = new JPanel();
652        speedControlPanel.setLayout(new BoxLayout(speedControlPanel, BoxLayout.X_AXIS));
653        speedControlPanel.setOpaque(false);
654        speedPanel.add(speedControlPanel, BorderLayout.CENTER);
655        sliderPanel = new JPanel();
656        sliderPanel.setOpaque(false);
657
658        speedSlider = new JSlider(0, intSpeedSteps);
659        speedSlider.setOpaque(false);
660        speedSlider.setValue(0);
661        speedSlider.setFocusable(false);
662        speedSlider.addMouseListener(JmriMouseListener.adapt(new JSliderPreciseMouseAdapter()));
663
664        speedSliderContinuous = new JSlider(-intSpeedSteps, intSpeedSteps);
665        speedSliderContinuous.setValue(0);
666        speedSliderContinuous.setOpaque(false);
667        speedSliderContinuous.setFocusable(false);
668        speedSliderContinuous.addMouseListener(JmriMouseListener.adapt(new JSliderPreciseMouseAdapter()));
669
670        speedSpinner = new JSpinner();
671        speedSpinnerModel = new SpinnerNumberModel(0, 0, intSpeedSteps, 1);
672        speedSpinner.setModel(speedSpinnerModel);
673
674        // customize speed spinner keyboard and focus interactions to not conflict with throttle keyboard shortcuts
675        speedSpinner.getActionMap().put("doNothing", new AbstractAction() {
676            @Override
677            public void actionPerformed(ActionEvent e) {
678                //do nothing
679            }
680        });
681        speedSpinner.getActionMap().put("giveUpFocus", new AbstractAction() {
682            @Override
683            public void actionPerformed(ActionEvent e) {
684               InstanceManager.getDefault(ThrottleFrameManager.class).getCurrentThrottleFrame().getRootPane().requestFocusInWindow();
685            }
686        });
687
688        for ( int i : new ArrayList<>(Arrays.asList(
689                KeyEvent.VK_0, KeyEvent.VK_1, KeyEvent.VK_2, KeyEvent.VK_3, KeyEvent.VK_4, KeyEvent.VK_5, KeyEvent.VK_6, KeyEvent.VK_7, KeyEvent.VK_8, KeyEvent.VK_9,
690                KeyEvent.VK_NUMPAD0, KeyEvent.VK_NUMPAD1, KeyEvent.VK_NUMPAD2, KeyEvent.VK_NUMPAD3, KeyEvent.VK_NUMPAD4, KeyEvent.VK_NUMPAD5, KeyEvent.VK_NUMPAD6, KeyEvent.VK_NUMPAD7, KeyEvent.VK_NUMPAD8, KeyEvent.VK_NUMPAD9,
691                KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, KeyEvent.VK_DOWN,
692                KeyEvent.VK_DELETE, KeyEvent.VK_BACK_SPACE
693        ))) {
694            speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(i, 0, true), "doNothing");
695            speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(i, 0, false), "doNothing");
696        }
697        speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "giveUpFocus");
698        speedSpinner.getInputMap(WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "giveUpFocus");
699
700        EnumSet<SpeedStepMode> speedStepModes = throttleManager.supportedSpeedModes();
701        speedStepBox = new JComboBox<>(speedStepModes.toArray(SpeedStepMode[]::new));
702
703        forwardButton = new JRadioButton();
704        reverseButton = new JRadioButton();
705        try {
706            forwardButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdOff.svg").toString());
707        } catch (Exception ex) {
708            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
709            forwardButtonSvgIcon = null;
710            forwardButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdOff64.png"));
711        }
712        try {
713            forwardSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdOn.svg").toString());
714        } catch (Exception ex) {
715            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
716            forwardSelectedButtonSvgIcon = null;
717            forwardSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdOn64.png"));
718        }
719        try {
720            forwardRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirFwdRoll.svg").toString());
721        } catch (Exception ex) {
722            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
723            forwardRollButtonSvgIcon = null;
724            forwardRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirFwdRoll64.png"));
725        }
726        try {
727            reverseButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckOff.svg").toString());
728        } catch (Exception ex) {
729            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
730            reverseButtonSvgIcon = null;
731            reverseButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckOff64.png"));
732        }
733        try {
734            reverseSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckOn.svg").toString());
735        } catch (Exception ex) {
736            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
737            reverseSelectedButtonSvgIcon = null;
738            reverseSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckOn64.png"));
739        }
740        try {
741            reverseRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/dirBckRoll.svg").toString());
742        } catch (Exception ex) {
743            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
744            reverseRollButtonSvgIcon = null;
745            reverseRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/dirBckRoll64.png"));
746        }
747        
748        speedLabelVerticalImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/labelArrowVertical.png"));
749        speedLabelHorizontalImageIcon  = new ImageIcon(FileUtil.findURL("resources/icons/throttles/labelArrowHorizontal.png"));
750
751        layoutSliderPanel();
752        speedControlPanel.add(sliderPanel);
753        speedSlider.setOrientation(JSlider.VERTICAL);
754        speedSlider.setMajorTickSpacing(maxSpeed / 2);
755
756        // remove old actions
757        speedSlider.addChangeListener((ChangeEvent e) -> {
758            if (!internalAdjust) {
759                boolean doIt = false;
760                if (!speedSlider.getValueIsAdjusting()) {
761                    doIt = true;
762                    lastTrackedSliderMovementTime = System.currentTimeMillis() - trackSliderMinInterval;
763                } else if (trackSlider
764                        && System.currentTimeMillis() - lastTrackedSliderMovementTime >= trackSliderMinInterval) {
765                    doIt = true;
766                    lastTrackedSliderMovementTime = System.currentTimeMillis();
767                }
768                if (doIt) {
769                    float newSpeed = (speedSlider.getValue() / (intSpeedSteps * 1.0f));
770                    if (log.isDebugEnabled()) {
771                        log.debug("stateChanged: slider pos: {} speed: {}", speedSlider.getValue(), newSpeed);
772                    }
773                    if (sliderPanel.isVisible() && throttle != null) {
774                        throttle.setSpeedSetting(newSpeed);
775                    }
776                    speedSpinnerModel.setValue(speedSlider.getValue());
777                    if (forwardButton.isSelected()) {
778                        speedSliderContinuous.setValue(( speedSlider.getValue()));
779                    } else {
780                        speedSliderContinuous.setValue(-( speedSlider.getValue()));
781                    }                    
782                }
783            }
784        });
785
786        speedSliderContinuousPanel = new JPanel();
787        layoutSpeedSliderContinuous();
788
789        speedControlPanel.add(speedSliderContinuousPanel);
790        speedSliderContinuous.setOrientation(JSlider.VERTICAL);
791        speedSliderContinuous.setMajorTickSpacing(maxSpeed / 2);
792        // remove old actions
793        speedSliderContinuous.addChangeListener((ChangeEvent e) -> {
794            if (!internalAdjust) {
795                boolean doIt = false;
796                if (!speedSliderContinuous.getValueIsAdjusting()) {
797                    doIt = true;
798                    lastTrackedSliderMovementTime = System.currentTimeMillis() - trackSliderMinInterval;
799                } else if (trackSlider
800                        && System.currentTimeMillis() - lastTrackedSliderMovementTime >= trackSliderMinInterval) {
801                    doIt = true;
802                    lastTrackedSliderMovementTime = System.currentTimeMillis();
803                }
804                if (doIt) {
805                    float newSpeed = (java.lang.Math.abs(speedSliderContinuous.getValue()) / (intSpeedSteps * 1.0f));
806                    boolean newDir = (speedSliderContinuous.getValue() >= 0);
807                    if (log.isDebugEnabled()) {
808                        log.debug("stateChanged: slider pos: {} speed: {} dir: {}", speedSliderContinuous.getValue(), newSpeed, newDir);
809                    }
810                    if (speedSliderContinuousPanel.isVisible() && throttle != null) {
811                        throttle.setSpeedSetting(newSpeed);
812                        if ((newSpeed > 0) && (newDir != forwardButton.isSelected())) {
813                            throttle.setIsForward(newDir);
814                        }
815                    }
816                    speedSpinnerModel.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));
817                    speedSlider.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));                    
818                }
819            }
820        });
821        computeLabelsTable();
822        updateSlidersLabelDisplay();
823
824        spinnerPanel = new JPanel();
825        layoutSpinnerPanel();
826
827        speedControlPanel.add(spinnerPanel);
828
829        // remove old actions
830        speedSpinner.addChangeListener((ChangeEvent e) -> {
831            if (!internalAdjust) {
832                float newSpeed = ((Integer) speedSpinner.getValue()).floatValue() / (intSpeedSteps * 1.0f);
833                if (log.isDebugEnabled()) {
834                    log.debug("stateChanged: spinner pos: {} speed: {}", speedSpinner.getValue(), newSpeed);
835                }
836                if (throttle != null) {
837                    if (spinnerPanel.isVisible()) {
838                        throttle.setSpeedSetting(newSpeed);
839                    }
840                    speedSlider.setValue(((Integer) speedSpinner.getValue()));
841                    if (forwardButton.isSelected()) {
842                        speedSliderContinuous.setValue(((Integer) speedSpinner.getValue()));
843                    } else {
844                        speedSliderContinuous.setValue(-((Integer) speedSpinner.getValue()));
845                    }                    
846                } else {
847                    log.warn("no throttle object in stateChanged, ignoring change of speed to {}", newSpeed);
848                }
849            }
850        });
851
852        speedStepBox.addActionListener((ActionEvent e) -> {
853            SpeedStepMode s = (SpeedStepMode)speedStepBox.getSelectedItem();
854            setSpeedStepsMode(s);
855            if (throttle != null) {
856              throttle.setSpeedStepMode(s);
857            }
858        });
859
860        buttonPanel = new JPanel();
861        buttonPanel.setLayout(new GridBagLayout());
862        mainPanel.add(buttonPanel, BorderLayout.SOUTH);
863
864        ButtonGroup directionButtons = new ButtonGroup();
865        directionButtons.add(forwardButton);
866        directionButtons.add(reverseButton);
867
868        forwardButton.addActionListener((ActionEvent e) -> {
869            if (throttle != null) {
870              throttle.setIsForward(true);
871            }
872            speedSliderContinuous.setValue(java.lang.Math.abs(speedSliderContinuous.getValue()));            
873        });
874
875        reverseButton.addActionListener((ActionEvent e) -> {
876            if (throttle != null) {
877              throttle.setIsForward(false);
878            }
879            speedSliderContinuous.setValue(-java.lang.Math.abs(speedSliderContinuous.getValue()));            
880        });
881
882        stopButton = new JButton();
883        idleButton = new JButton();
884        try {
885            stopButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estop.svg").toString());
886        } catch (Exception ex) {
887            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
888            stopButtonSvgIcon = null;
889            stopButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estop64.png"));
890        }
891        try {
892            stopSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estopOn.svg").toString());
893        } catch (Exception ex) {
894            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
895            stopSelectedButtonSvgIcon = null;
896            stopSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estopOn64.png"));
897        }
898        try {
899            stopRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/estopRoll.svg").toString());
900        } catch (Exception ex) {
901            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
902            stopRollButtonSvgIcon = null;
903            stopRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/estopRoll64.png"));
904        }
905        try {
906            idleButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stop.svg").toString());
907        } catch (Exception ex) {
908            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
909            idleButtonSvgIcon = null;
910            idleButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stop64.png"));
911        }
912        try {
913            idleSelectedButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stopOn.svg").toString());
914        } catch (Exception ex) {
915            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
916            idleSelectedButtonSvgIcon = null;
917            idleSelectedButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stopOn64.png"));
918        }
919        try {
920            idleRollButtonSvgIcon = createSVGDocument(FileUtil.findURI("resources/icons/throttles/stopRoll.svg").toString());
921        } catch (Exception ex) {
922            log.debug("Issue loading svg icon, reverting to png : {}", ex.getMessage());
923            idleRollButtonSvgIcon = null;
924            idleRollButtonImageIcon = new ImageIcon(FileUtil.findURL("resources/icons/throttles/stopRoll64.png"));
925        }
926
927        stopButton.addActionListener((ActionEvent e) -> {
928            stop();
929        });
930
931        idleButton.addActionListener((ActionEvent e) -> {
932            idle();
933        });
934
935        addComponentListener(
936                new ComponentAdapter() {
937                    @Override
938                    public void componentResized(ComponentEvent e) {
939                        changeOrientation();
940                    }
941                });
942
943        speedPanel.addComponentListener(
944                new ComponentAdapter() {
945                    @Override
946                    public void componentResized(ComponentEvent e) {
947                        changeFontSizes();
948                    }
949                });
950
951        layoutButtonPanel();
952        layoutTopButtonPanel();
953
954        // Add a mouse listener all components to trigger the popup menu.
955        MouseInputAdapterInstaller.installMouseListenerOnAllComponents(new PopupListener(), this);
956
957        // set by default which speed selection method is on top
958        setSpeedController(_displaySlider);
959    }
960
961  /**
962   * Use the SAXSVGDocumentFactory to parse the given URI into a DOM.
963   *
964   * @param uri The path to the SVG file to read.
965   * @return A Document instance that represents the SVG file.
966   * @throws IOException The file could not be read.
967   */
968    private Document createSVGDocument( String uri ) throws IOException {
969      String parser = XMLResourceDescriptor.getXMLParserClassName();
970      SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
971      return factory.createDocument( uri );
972    }
973
974    /**
975     * Perform an emergency stop.
976     *
977     */
978    public void stop() {
979        if (this.throttle == null) {
980            return;
981        }
982        internalAdjust = true;
983        throttle.setSpeedSetting(-1);
984        speedSlider.setValue(0);
985        speedSpinnerModel.setValue(0);
986        speedSliderContinuous.setValue(0);        
987        internalAdjust = false;
988    }
989
990    private void idle() {
991        if (this.throttle == null) {
992            return;
993        }
994        internalAdjust = true;
995        throttle.setSpeedSetting(0);
996        speedSlider.setValue(0);
997        speedSpinner.setValue(0);
998        speedSliderContinuous.setValue(0);
999        internalAdjust = false;
1000    }
1001
1002    /**
1003     * The user has resized the Frame. Possibly change from Horizontal to
1004     * Vertical layout.
1005     */
1006    private void changeOrientation() {
1007        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1008        if (mainPanel.getWidth() > mainPanel.getHeight()) {
1009            speedSlider.setOrientation(JSlider.HORIZONTAL);                        
1010            speedSliderContinuous.setOrientation(JSlider.HORIZONTAL);
1011            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1012                int bpw = mainPanel.getHeight()*5/2;
1013                if (bpw > mainPanel.getWidth()/2) {
1014                    bpw = mainPanel.getWidth()/2;
1015                }
1016                buttonPanel.setSize(bpw, mainPanel.getHeight());
1017                resizeButtons();
1018            }
1019            mainPanel.remove(buttonPanel);
1020            mainPanel.add(buttonPanel, BorderLayout.EAST);
1021        } else {
1022            speedSlider.setOrientation(JSlider.VERTICAL);           
1023            speedSliderContinuous.setOrientation(JSlider.VERTICAL);                           
1024            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1025                int bph = mainPanel.getWidth()*2/5;
1026                if (bph > mainPanel.getHeight()/2) {
1027                    bph = mainPanel.getHeight()/2;
1028                }
1029                buttonPanel.setSize(mainPanel.getWidth(), bph);
1030                resizeButtons();
1031            }
1032            mainPanel.remove(buttonPanel);
1033            mainPanel.add(buttonPanel, BorderLayout.SOUTH);
1034        }
1035        updateSlidersLabelDisplay();        
1036    }
1037
1038    /**
1039     * A resizing has occurred, so determine the optimum font size for the speed spinner text font.
1040     */
1041    private void changeFontSizes() {
1042        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1043        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider() ) {
1044            int fontSize = speedSpinner.getFont().getSize();
1045            // fit vertically
1046            int fieldHeight = speedControlPanel.getSize().height;
1047            int stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1048            if (stringHeight > fieldHeight) { // component has shrunk vertically
1049                while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
1050                    fontSize -= FONT_INCREMENT;
1051                    Font f = new Font("", Font.PLAIN, fontSize);
1052                    speedSpinner.setFont(f);
1053                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1054                }
1055            } else { // component has grown vertically
1056                while (fieldHeight - stringHeight > 10) {
1057                    fontSize += FONT_INCREMENT;
1058                    Font f = new Font("", Font.PLAIN, fontSize);
1059                    speedSpinner.setFont(f);
1060                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16 ;
1061                }
1062            }
1063            // fit horizontally
1064            int fieldWidth = speedControlPanel.getSize().width;
1065            int stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1066            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) { // component has shrunk horizontally
1067                fontSize -= FONT_INCREMENT;
1068                Font f = new Font("", Font.PLAIN, fontSize);
1069                speedSpinner.setFont(f);
1070                stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1071            }
1072            speedSpinner.setMinimumSize(new Dimension(stringWidth,stringHeight)); //not sure why this helps here, required
1073        }
1074    }
1075
1076    /**
1077     * Intended for throttle scripting
1078     *
1079     * @param fwd direction: true for forward; false for reverse.
1080     */
1081    public void setForwardDirection(boolean fwd) {
1082        if (fwd) {
1083            if (forwardButton.isEnabled()) {
1084                forwardButton.doClick();
1085            } else {
1086                log.error("setForwardDirection(true) with forwardButton disabled, failed");
1087            }
1088        } else {
1089            if (reverseButton.isEnabled()) {
1090                reverseButton.doClick();
1091            } else {
1092                log.error("setForwardDirection(false) with reverseButton disabled, failed");
1093            }
1094        }
1095    }
1096
1097
1098    // update the state of this panel if any of the properties change
1099    @Override
1100    public void propertyChange(java.beans.PropertyChangeEvent e) {
1101        if (e.getPropertyName().equals(Throttle.SPEEDSETTING)) {
1102            float speed = ((Float) e.getNewValue());
1103            log.debug("Throttle panel speed updated to {} increment {}", speed,
1104                    throttle.getSpeedIncrement());
1105            setSpeedValues( throttle.getSpeedIncrement(), speed);
1106        } else if (e.getPropertyName().equals(Throttle.SPEEDSTEPS)) {
1107            SpeedStepMode steps = (SpeedStepMode)e.getNewValue();
1108            setSpeedStepsMode(steps);
1109        } else if (e.getPropertyName().equals(Throttle.ISFORWARD)) {
1110            boolean Forward = ((Boolean) e.getNewValue());
1111            setIsForward(Forward);
1112        } else if (e.getPropertyName().equals(switchSliderFunction)) {
1113            if ((Boolean) e.getNewValue()) { // switch only if displaying sliders
1114                updateSlidersLabelDisplay();
1115                if (_displaySlider == SLIDERDISPLAY) {
1116                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1117                }
1118            } else {
1119                updateSlidersLabelDisplay();
1120                if (_displaySlider == SLIDERDISPLAYCONTINUOUS) {
1121                    setSpeedController(SLIDERDISPLAY);
1122                }
1123            }
1124        }
1125        log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue());
1126    }
1127
1128    /**
1129     * Apply current throttles preferences to this panel
1130     */
1131    final void applyPreferences() {
1132        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1133
1134        if (preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1135             speedSlider.setUI(new ControlPanelCustomSliderUI(speedSlider));
1136             speedSliderContinuous.setUI(new ControlPanelCustomSliderUI(speedSliderContinuous));
1137             changeFontSizes();
1138        } else {
1139            speedSlider.setUI((new JSlider()).getUI());
1140            speedSliderContinuous.setUI((new JSlider()).getUI());
1141            speedSpinner.setFont(new JSpinner().getFont());
1142        }
1143        updateSlidersLabelDisplay();
1144
1145        setupButton(stopButton, preferences, "ButtonEStop");
1146        setupButton(idleButton, preferences, "ButtonIdle");
1147        setupButton(forwardButton, preferences, "ButtonForward");
1148        setupButton(reverseButton, preferences, "ButtonReverse");
1149        buttonPanel.removeAll();
1150        layoutButtonPanel();
1151        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
1152            changeOrientation(); // force buttons resizing
1153        }
1154        
1155        setHideSpeedStep(preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector());        
1156    }
1157
1158    /**
1159     * A PopupListener to handle mouse clicks and releases. Handles the popup
1160     * menu.
1161     */
1162    private class PopupListener extends JmriMouseAdapter {
1163        /**
1164         * If the event is the popup trigger, which is dependent on the
1165         * platform, present the popup menu.
1166         * @param e The JmriMouseEvent causing the action.
1167         */
1168        @Override
1169        public void mouseClicked(JmriMouseEvent e) {
1170            checkTrigger(e);
1171        }
1172
1173        /**
1174         * If the event is the popup trigger, which is dependent on the
1175         * platform, present the popup menu.
1176         * @param e The JmriMouseEvent causing the action.
1177         */
1178        @Override
1179        public void mousePressed(JmriMouseEvent e) {
1180            checkTrigger( e);
1181        }
1182
1183        /**
1184         * If the event is the popup trigger, which is dependent on the
1185         * platform, present the popup menu.
1186         * @param e The JmriMouseEvent causing the action.
1187         */
1188        @Override
1189        public void mouseReleased(JmriMouseEvent e) {
1190            checkTrigger( e);
1191        }
1192
1193        private void checkTrigger( JmriMouseEvent e) {
1194            if (e.isPopupTrigger()) {
1195                initPopupMenu();
1196                popupMenu.show(e.getComponent(), e.getX(), e.getY());
1197            }
1198        }
1199    }
1200
1201    private void initPopupMenu() {
1202        if (popupMenu == null) {
1203            JMenuItem propertiesMenuItem = new JMenuItem(Bundle.getMessage("ControlPanelProperties"));
1204            propertiesMenuItem.addActionListener((ActionEvent e) -> {
1205                if (propertyEditor == null) {
1206                    propertyEditor = new ControlPanelPropertyEditor(this);
1207                }
1208                propertyEditor.setLocation(MouseInfo.getPointerInfo().getLocation());
1209                propertyEditor.resetProperties();
1210                propertyEditor.setVisible(true);
1211            });
1212            popupMenu = new JPopupMenu();
1213            popupMenu.add(propertiesMenuItem);
1214        }
1215    }
1216
1217    /**
1218     * Collect the prefs of this object into XML Element
1219     * <ul>
1220     * <li> Window prefs
1221     * </ul>
1222     *
1223     *
1224     * @return the XML of this object.
1225     */
1226    public Element getXml() {
1227        Element me = new Element("ControlPanel");
1228        me.setAttribute("displaySpeedSlider", String.valueOf(this._displaySlider));
1229        me.setAttribute("trackSlider", String.valueOf(this.trackSlider));
1230        me.setAttribute("trackSliderMinInterval", String.valueOf(this.trackSliderMinInterval));
1231        me.setAttribute("switchSliderOnFunction", switchSliderFunction != null ? switchSliderFunction : "Fxx");
1232        me.setAttribute("hideSpeedStep", String.valueOf(this.hideSpeedStep));
1233        //Element window = new Element("window");
1234        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
1235        children.add(WindowPreferences.getPreferences(this));
1236        me.setContent(children);
1237        return me;
1238    }
1239
1240    /**
1241     * Set the preferences based on the XML Element.
1242     * <ul>
1243     * <li> Window prefs
1244     * </ul>
1245     *
1246     *
1247     * @param e The Element for this object.
1248     */
1249    public void setXml(Element e) {
1250        internalAdjust = true;
1251        try {
1252            this.setSpeedController(e.getAttribute("displaySpeedSlider").getIntValue());
1253        } catch (org.jdom2.DataConversionException ex) {
1254            log.error("DataConverstionException in setXml", ex);
1255            // in this case, recover by displaying the speed slider.
1256            this.setSpeedController(SLIDERDISPLAY);
1257        }
1258        Attribute tsAtt = e.getAttribute("trackSlider");
1259        if (tsAtt != null) {
1260            try {
1261                trackSlider = tsAtt.getBooleanValue();
1262            } catch (org.jdom2.DataConversionException ex) {
1263                trackSlider = trackSliderDefault;
1264            }
1265        } else {
1266            trackSlider = trackSliderDefault;
1267        }
1268        Attribute tsmiAtt = e.getAttribute("trackSliderMinInterval");
1269        if (tsmiAtt != null) {
1270            try {
1271                trackSliderMinInterval = tsmiAtt.getLongValue();
1272            } catch (org.jdom2.DataConversionException ex) {
1273                trackSliderMinInterval = trackSliderMinIntervalDefault;
1274            }
1275            if (trackSliderMinInterval < trackSliderMinIntervalMin) {
1276                trackSliderMinInterval = trackSliderMinIntervalMin;
1277            } else if (trackSliderMinInterval > trackSliderMinIntervalMax) {
1278                trackSliderMinInterval = trackSliderMinIntervalMax;
1279            }
1280        } else {
1281            trackSliderMinInterval = trackSliderMinIntervalDefault;
1282        }
1283        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);        
1284        Attribute hssAtt = e.getAttribute("hideSpeedStep");
1285        if (hssAtt != null) {
1286            try {
1287                setHideSpeedStep ( hssAtt.getBooleanValue() );
1288            } catch (org.jdom2.DataConversionException ex) {
1289                setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1290            }
1291        } else {
1292            setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1293        }
1294        if ((prevShuntingFn == null) && (e.getAttribute("switchSliderOnFunction") != null)) {
1295            setSwitchSliderFunction(e.getAttribute("switchSliderOnFunction").getValue());
1296        }
1297        internalAdjust = false;
1298        Element window = e.getChild("window");
1299        WindowPreferences.setPreferences(this, window);
1300    }
1301
1302    @Override
1303    public void notifyAddressChosen(LocoAddress l) {
1304    }
1305
1306    @Override
1307    public void notifyAddressReleased(LocoAddress la) {
1308        if (throttle == null) {
1309            log.debug("notifyAddressReleased() throttle already null, called for loc {}", la);
1310            return;
1311        }        
1312        this.setEnabled(false);
1313        if (throttle != null) {
1314            throttle.removePropertyChangeListener(this);
1315        }
1316        throttle = null;
1317        if (prevShuntingFn != null) {
1318            setSwitchSliderFunction(prevShuntingFn);
1319            prevShuntingFn = null;
1320        }
1321    }
1322
1323    private void addressThrottleFound() {
1324        setEnabled(true);
1325        setIsForward(throttle.getIsForward());
1326        setSpeedStepsMode(throttle.getSpeedStepMode());
1327        setSpeedValues(throttle.getSpeedIncrement(), throttle.getSpeedSetting());
1328        throttle.addPropertyChangeListener(this);
1329    }
1330
1331    @Override
1332    public void notifyAddressThrottleFound(DccThrottle t) {
1333        log.debug("control panel received new throttle {}", t);
1334        if (throttle != null) {
1335            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1336            return;
1337        }
1338        if (isConsist) {
1339            // ignore if is a consist
1340            return;
1341        }
1342        throttle = t;
1343        addressThrottleFound();
1344
1345        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getShuntingFunction() != null)) {
1346            prevShuntingFn = getSwitchSliderFunction();
1347            setSwitchSliderFunction(addressPanel.getRosterEntry().getShuntingFunction());                            
1348        } else {
1349            setSwitchSliderFunction(switchSliderFunction); // reset slider           
1350        }
1351        if (log.isDebugEnabled()) {
1352            jmri.DccLocoAddress Address = (jmri.DccLocoAddress) throttle.getLocoAddress();
1353            log.debug("new address is {}", Address.toString());
1354        }
1355    }
1356
1357    @Override
1358    public void notifyConsistAddressChosen(LocoAddress l) {
1359        notifyAddressChosen(l);
1360    }
1361
1362    @Override
1363    public void notifyConsistAddressReleased(LocoAddress la) {
1364        notifyAddressReleased(la);
1365        isConsist = false;
1366    }
1367
1368    @Override
1369    public void notifyConsistAddressThrottleFound(DccThrottle t) {
1370        log.debug("control panel received consist throttle {}", t);
1371        isConsist = true;
1372        throttle = t;
1373        addressThrottleFound();
1374    }
1375
1376    public void setSwitchSliderFunction(String fn) {
1377        switchSliderFunction = fn;
1378        if ((switchSliderFunction == null) || (switchSliderFunction.length() == 0)) {
1379            return;
1380        }
1381        if ((throttle != null) && (_displaySlider != STEPDISPLAY)) { // Update UI depending on function state
1382            try {
1383                // this uses reflection because the user is allowed to name a
1384                // throttle function that triggers this action.
1385                java.lang.reflect.Method getter = throttle.getClass().getMethod("get" + switchSliderFunction, (Class[]) null);
1386
1387                Boolean state = (Boolean) getter.invoke(throttle, (Object[]) null);
1388                if (state) {
1389                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1390                } else {
1391                    setSpeedController(SLIDERDISPLAY);
1392                }
1393
1394            } catch (IllegalAccessException|NoSuchMethodException|java.lang.reflect.InvocationTargetException ex) {
1395                log.debug("Exception in setSwitchSliderFunction: {} while looking for function {}", ex, switchSliderFunction);
1396            }
1397        }
1398    }
1399    
1400
1401    private void computeLabelsTable() {
1402        defaultLabelTable = new HashMap<>(5);
1403        defaultLabelTable.put(maxSpeed / 2, new JLabel("50%"));
1404        defaultLabelTable.put(maxSpeed, new JLabel("100%"));        
1405        defaultLabelTable.put(0, new JLabel(Bundle.getMessage("ButtonStop")));
1406        defaultLabelTable.put(-maxSpeed / 2, new JLabel("-50%"));
1407        defaultLabelTable.put(-maxSpeed, new JLabel("-100%"));
1408        
1409        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getAttribute("speedLabels") != null)) {
1410            ObjectMapper mapper = new ObjectMapper();
1411            try {
1412                SpeedLabel[] speedLabels = mapper.readValue(addressPanel.getRosterEntry().getAttribute("speedLabels"), SpeedLabel[].class );
1413                if (speedLabels != null && speedLabels.length>0) {
1414                    verticalLabelMap = new HashMap<>(speedLabels.length *2 );
1415                    horizontalLabelMap = new HashMap<>(speedLabels.length *2 );
1416                    JLabel label;
1417                    for (SpeedLabel sp : speedLabels) {
1418                        label = new JLabel( sp.label, speedLabelVerticalImageIcon, SwingConstants.LEFT );
1419                        label.setVerticalTextPosition(JLabel.CENTER);
1420                        verticalLabelMap.put( sp.value, label);
1421                        verticalLabelMap.put( -sp.value, label);
1422
1423                        label = new JLabel( sp.label, speedLabelHorizontalImageIcon, SwingConstants.LEFT );
1424                        label.setHorizontalTextPosition(JLabel.CENTER);
1425                        label.setVerticalTextPosition(JLabel.BOTTOM);
1426
1427                        horizontalLabelMap.put( sp.value, label);
1428                        horizontalLabelMap.put( -sp.value, label);
1429                    }
1430                    updateSlidersLabelDisplay();
1431                }
1432            } catch (JsonProcessingException ex) {
1433                log.error("Exception trying to parse speedLabels attribute from roster entry: {} ", ex.getMessage());                
1434            }                                             
1435        } else {
1436            verticalLabelMap = null;
1437            horizontalLabelMap = null;            
1438        }
1439    }
1440        
1441    // update slider label display depending on context (vertical|horizontal & normal|large)
1442    private void updateSlidersLabelDisplay() {
1443        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1444        Map<Integer, JLabel> labelTable = new HashMap<>(10);
1445        
1446        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1447            speedSlider.setPaintTicks(false);
1448            speedSliderContinuous.setPaintTicks(false);
1449        } else {
1450            speedSlider.setPaintTicks(true);
1451            speedSliderContinuous.setPaintTicks(true);            
1452            labelTable.putAll(defaultLabelTable);                                
1453        }
1454        if ((speedSlider.getOrientation() == JSlider.HORIZONTAL) && (horizontalLabelMap != null)) {
1455            labelTable.putAll(horizontalLabelMap);
1456        } 
1457        if ((speedSlider.getOrientation() == JSlider.VERTICAL) && (verticalLabelMap != null)) {
1458            labelTable.putAll(verticalLabelMap);                 
1459        }
1460        
1461        if (! labelTable.isEmpty()) {
1462            // setLabelTable() only likes Colection which is a HashTable
1463            speedSlider.setLabelTable(new Hashtable<>(labelTable));
1464            speedSliderContinuous.setLabelTable(new Hashtable<>(labelTable));
1465            speedSlider.setPaintLabels(true);
1466            speedSliderContinuous.setPaintLabels(true);
1467        } else {
1468            speedSlider.setPaintLabels(false);
1469            speedSliderContinuous.setPaintLabels(false);
1470        }
1471    }
1472
1473    public String getSwitchSliderFunction() {
1474        return switchSliderFunction;
1475    }
1476
1477    public void saveToRoster(RosterEntry re) {
1478        if (re == null) {
1479            return;
1480        }
1481        if ((re.getShuntingFunction() != null) && (re.getShuntingFunction().compareTo(getSwitchSliderFunction()) != 0)) {
1482            re.setShuntingFunction(getSwitchSliderFunction());
1483        } else if ((re.getShuntingFunction() == null) && (getSwitchSliderFunction() != null)) {
1484            re.setShuntingFunction(getSwitchSliderFunction());
1485        } else {
1486            return;
1487        }
1488        Roster.getDefault().writeRoster();
1489    }
1490
1491    // to handle svg transformation to displayable images
1492    private static class MyTranscoder extends ImageTranscoder {
1493        private BufferedImage image = null;
1494        @Override
1495        public BufferedImage createImage(int w, int h) {
1496            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1497            return image;
1498        }
1499        public BufferedImage getImage() {
1500            return image;
1501        }
1502        @Override
1503        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
1504            //not required here, do nothing
1505        }
1506    }
1507   
1508    // this mouse adapter makes sure to move the slider cursor to precisely where the user clicks
1509    // see https://jmri-developers.groups.io/g/jmri/message/7874
1510    private static class JSliderPreciseMouseAdapter extends JmriMouseAdapter {
1511
1512        @Override
1513        public void mousePressed(JmriMouseEvent e) {
1514            if (e.getButton() == JmriMouseEvent.BUTTON1) {
1515                JSlider sourceSlider = (JSlider) e.getSource();
1516                if (!sourceSlider.isEnabled()) {
1517                    return;
1518                }
1519                BasicSliderUI ui = (BasicSliderUI) sourceSlider.getUI();
1520                int value;
1521                if (sourceSlider.getOrientation() == JSlider.VERTICAL) {
1522                    value = ui.valueForYPosition(e.getY());
1523                } else {
1524                    value = ui.valueForXPosition(e.getX());
1525                }
1526                sourceSlider.setValue(value);
1527            }
1528        }
1529    }
1530    
1531    // For Jackson pasing of roster entry property holding speed labels (if any)
1532    private static class SpeedLabel {
1533        public int value = -1;
1534        public String label = "";      
1535    }
1536
1537    // initialize logging
1538    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ControlPanel.class);
1539}