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            speedSlider.setValue(0);
933            speedSpinner.setValue(0);
934            speedSliderContinuous.setValue(0);           
935            throttle.setSpeedSetting(0);
936        });
937
938        addComponentListener(
939                new ComponentAdapter() {
940                    @Override
941                    public void componentResized(ComponentEvent e) {
942                        changeOrientation();
943                    }
944                });
945
946        speedPanel.addComponentListener(
947                new ComponentAdapter() {
948                    @Override
949                    public void componentResized(ComponentEvent e) {
950                        changeFontSizes();
951                    }
952                });
953
954        layoutButtonPanel();
955        layoutTopButtonPanel();
956
957        // Add a mouse listener all components to trigger the popup menu.
958        MouseInputAdapterInstaller.installMouseListenerOnAllComponents(new PopupListener(), this);
959
960        // set by default which speed selection method is on top
961        setSpeedController(_displaySlider);
962    }
963
964  /**
965   * Use the SAXSVGDocumentFactory to parse the given URI into a DOM.
966   *
967   * @param uri The path to the SVG file to read.
968   * @return A Document instance that represents the SVG file.
969   * @throws IOException The file could not be read.
970   */
971    private Document createSVGDocument( String uri ) throws IOException {
972      String parser = XMLResourceDescriptor.getXMLParserClassName();
973      SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
974      return factory.createDocument( uri );
975    }
976
977    /**
978     * Perform an emergency stop.
979     *
980     */
981    public void stop() {
982        if (this.throttle == null) {
983            return;
984        }
985        internalAdjust = true;
986        throttle.setSpeedSetting(-1);
987        speedSlider.setValue(0);
988        speedSpinnerModel.setValue(0);
989        speedSliderContinuous.setValue(0);        
990        internalAdjust = false;
991    }
992
993    /**
994     * The user has resized the Frame. Possibly change from Horizontal to
995     * Vertical layout.
996     */
997    private void changeOrientation() {
998        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
999        if (mainPanel.getWidth() > mainPanel.getHeight()) {
1000            speedSlider.setOrientation(JSlider.HORIZONTAL);                        
1001            speedSliderContinuous.setOrientation(JSlider.HORIZONTAL);
1002            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1003                int bpw = mainPanel.getHeight()*5/2;
1004                if (bpw > mainPanel.getWidth()/2) {
1005                    bpw = mainPanel.getWidth()/2;
1006                }
1007                buttonPanel.setSize(bpw, mainPanel.getHeight());
1008                resizeButtons();
1009            }
1010            mainPanel.remove(buttonPanel);
1011            mainPanel.add(buttonPanel, BorderLayout.EAST);
1012        } else {
1013            speedSlider.setOrientation(JSlider.VERTICAL);           
1014            speedSliderContinuous.setOrientation(JSlider.VERTICAL);                           
1015            if ( preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon() && preferences.isUsingLargeSpeedSlider() ) {
1016                int bph = mainPanel.getWidth()*2/5;
1017                if (bph > mainPanel.getHeight()/2) {
1018                    bph = mainPanel.getHeight()/2;
1019                }
1020                buttonPanel.setSize(mainPanel.getWidth(), bph);
1021                resizeButtons();
1022            }
1023            mainPanel.remove(buttonPanel);
1024            mainPanel.add(buttonPanel, BorderLayout.SOUTH);
1025        }
1026        updateSlidersLabelDisplay();        
1027    }
1028
1029    /**
1030     * A resizing has occurred, so determine the optimum font size for the speed spinner text font.
1031     */
1032    private void changeFontSizes() {
1033        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1034        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider() ) {
1035            int fontSize = speedSpinner.getFont().getSize();
1036            // fit vertically
1037            int fieldHeight = speedControlPanel.getSize().height;
1038            int stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1039            if (stringHeight > fieldHeight) { // component has shrunk vertically
1040                while ((stringHeight > fieldHeight) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) {
1041                    fontSize -= FONT_INCREMENT;
1042                    Font f = new Font("", Font.PLAIN, fontSize);
1043                    speedSpinner.setFont(f);
1044                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16;
1045                }
1046            } else { // component has grown vertically
1047                while (fieldHeight - stringHeight > 10) {
1048                    fontSize += FONT_INCREMENT;
1049                    Font f = new Font("", Font.PLAIN, fontSize);
1050                    speedSpinner.setFont(f);
1051                    stringHeight = speedSpinner.getFontMetrics(speedSpinner.getFont()).getHeight() + 16 ;
1052                }
1053            }
1054            // fit horizontally
1055            int fieldWidth = speedControlPanel.getSize().width;
1056            int stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1057            while ((stringWidth > fieldWidth) && (fontSize >= FONT_SIZE_MIN + FONT_INCREMENT)) { // component has shrunk horizontally
1058                fontSize -= FONT_INCREMENT;
1059                Font f = new Font("", Font.PLAIN, fontSize);
1060                speedSpinner.setFont(f);
1061                stringWidth = speedSpinner.getFontMetrics(speedSpinner.getFont()).stringWidth(LONGEST_SS_STRING) + 24 ;
1062            }
1063            speedSpinner.setMinimumSize(new Dimension(stringWidth,stringHeight)); //not sure why this helps here, required
1064        }
1065    }
1066
1067    /**
1068     * Intended for throttle scripting
1069     *
1070     * @param fwd direction: true for forward; false for reverse.
1071     */
1072    public void setForwardDirection(boolean fwd) {
1073        if (fwd) {
1074            if (forwardButton.isEnabled()) {
1075                forwardButton.doClick();
1076            } else {
1077                log.error("setForwardDirection(true) with forwardButton disabled, failed");
1078            }
1079        } else {
1080            if (reverseButton.isEnabled()) {
1081                reverseButton.doClick();
1082            } else {
1083                log.error("setForwardDirection(false) with reverseButton disabled, failed");
1084            }
1085        }
1086    }
1087
1088
1089    // update the state of this panel if any of the properties change
1090    @Override
1091    public void propertyChange(java.beans.PropertyChangeEvent e) {
1092        if (e.getPropertyName().equals(Throttle.SPEEDSETTING)) {
1093            float speed = ((Float) e.getNewValue());
1094            log.debug("Throttle panel speed updated to {} increment {}", speed,
1095                    throttle.getSpeedIncrement());
1096            setSpeedValues( throttle.getSpeedIncrement(), speed);
1097        } else if (e.getPropertyName().equals(Throttle.SPEEDSTEPS)) {
1098            SpeedStepMode steps = (SpeedStepMode)e.getNewValue();
1099            setSpeedStepsMode(steps);
1100        } else if (e.getPropertyName().equals(Throttle.ISFORWARD)) {
1101            boolean Forward = ((Boolean) e.getNewValue());
1102            setIsForward(Forward);
1103        } else if (e.getPropertyName().equals(switchSliderFunction)) {
1104            if ((Boolean) e.getNewValue()) { // switch only if displaying sliders
1105                updateSlidersLabelDisplay();
1106                if (_displaySlider == SLIDERDISPLAY) {
1107                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1108                }
1109            } else {
1110                updateSlidersLabelDisplay();
1111                if (_displaySlider == SLIDERDISPLAYCONTINUOUS) {
1112                    setSpeedController(SLIDERDISPLAY);
1113                }
1114            }
1115        }
1116        log.debug("Property change event received {} / {}", e.getPropertyName(), e.getNewValue());
1117    }
1118
1119    /**
1120     * Apply current throttles preferences to this panel
1121     */
1122    final void applyPreferences() {
1123        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1124
1125        if (preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1126             speedSlider.setUI(new ControlPanelCustomSliderUI(speedSlider));
1127             speedSliderContinuous.setUI(new ControlPanelCustomSliderUI(speedSliderContinuous));
1128             changeFontSizes();
1129        } else {
1130            speedSlider.setUI((new JSlider()).getUI());
1131            speedSliderContinuous.setUI((new JSlider()).getUI());
1132            speedSpinner.setFont(new JSpinner().getFont());
1133        }
1134        updateSlidersLabelDisplay();
1135
1136        setupButton(stopButton, preferences, "ButtonEStop");
1137        setupButton(idleButton, preferences, "ButtonIdle");
1138        setupButton(forwardButton, preferences, "ButtonForward");
1139        setupButton(reverseButton, preferences, "ButtonReverse");
1140        buttonPanel.removeAll();
1141        layoutButtonPanel();
1142        if (preferences.isUsingExThrottle() && preferences.isUsingFunctionIcon()) {
1143            changeOrientation(); // force buttons resizing
1144        }
1145    }
1146
1147    /**
1148     * A PopupListener to handle mouse clicks and releases. Handles the popup
1149     * menu.
1150     */
1151    private class PopupListener extends JmriMouseAdapter {
1152        /**
1153         * If the event is the popup trigger, which is dependent on the
1154         * platform, present the popup menu.
1155         * @param e The JmriMouseEvent causing the action.
1156         */
1157        @Override
1158        public void mouseClicked(JmriMouseEvent e) {
1159            checkTrigger(e);
1160        }
1161
1162        /**
1163         * If the event is the popup trigger, which is dependent on the
1164         * platform, present the popup menu.
1165         * @param e The JmriMouseEvent causing the action.
1166         */
1167        @Override
1168        public void mousePressed(JmriMouseEvent e) {
1169            checkTrigger( e);
1170        }
1171
1172        /**
1173         * If the event is the popup trigger, which is dependent on the
1174         * platform, present the popup menu.
1175         * @param e The JmriMouseEvent causing the action.
1176         */
1177        @Override
1178        public void mouseReleased(JmriMouseEvent e) {
1179            checkTrigger( e);
1180        }
1181
1182        private void checkTrigger( JmriMouseEvent e) {
1183            if (e.isPopupTrigger()) {
1184                initPopupMenu();
1185                popupMenu.show(e.getComponent(), e.getX(), e.getY());
1186            }
1187        }
1188    }
1189
1190    private void initPopupMenu() {
1191        if (popupMenu == null) {
1192            JMenuItem propertiesMenuItem = new JMenuItem(Bundle.getMessage("ControlPanelProperties"));
1193            propertiesMenuItem.addActionListener((ActionEvent e) -> {
1194                if (propertyEditor == null) {
1195                    propertyEditor = new ControlPanelPropertyEditor(this);
1196                }
1197                propertyEditor.setLocation(MouseInfo.getPointerInfo().getLocation());
1198                propertyEditor.resetProperties();
1199                propertyEditor.setVisible(true);
1200            });
1201            popupMenu = new JPopupMenu();
1202            popupMenu.add(propertiesMenuItem);
1203        }
1204    }
1205
1206    /**
1207     * Collect the prefs of this object into XML Element
1208     * <ul>
1209     * <li> Window prefs
1210     * </ul>
1211     *
1212     *
1213     * @return the XML of this object.
1214     */
1215    public Element getXml() {
1216        Element me = new Element("ControlPanel");
1217        me.setAttribute("displaySpeedSlider", String.valueOf(this._displaySlider));
1218        me.setAttribute("trackSlider", String.valueOf(this.trackSlider));
1219        me.setAttribute("trackSliderMinInterval", String.valueOf(this.trackSliderMinInterval));
1220        me.setAttribute("switchSliderOnFunction", switchSliderFunction != null ? switchSliderFunction : "Fxx");
1221        me.setAttribute("hideSpeedStep", String.valueOf(this.hideSpeedStep));
1222        //Element window = new Element("window");
1223        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
1224        children.add(WindowPreferences.getPreferences(this));
1225        me.setContent(children);
1226        return me;
1227    }
1228
1229    /**
1230     * Set the preferences based on the XML Element.
1231     * <ul>
1232     * <li> Window prefs
1233     * </ul>
1234     *
1235     *
1236     * @param e The Element for this object.
1237     */
1238    public void setXml(Element e) {
1239        internalAdjust = true;
1240        try {
1241            this.setSpeedController(e.getAttribute("displaySpeedSlider").getIntValue());
1242        } catch (org.jdom2.DataConversionException ex) {
1243            log.error("DataConverstionException in setXml", ex);
1244            // in this case, recover by displaying the speed slider.
1245            this.setSpeedController(SLIDERDISPLAY);
1246        }
1247        Attribute tsAtt = e.getAttribute("trackSlider");
1248        if (tsAtt != null) {
1249            try {
1250                trackSlider = tsAtt.getBooleanValue();
1251            } catch (org.jdom2.DataConversionException ex) {
1252                trackSlider = trackSliderDefault;
1253            }
1254        } else {
1255            trackSlider = trackSliderDefault;
1256        }
1257        Attribute tsmiAtt = e.getAttribute("trackSliderMinInterval");
1258        if (tsmiAtt != null) {
1259            try {
1260                trackSliderMinInterval = tsmiAtt.getLongValue();
1261            } catch (org.jdom2.DataConversionException ex) {
1262                trackSliderMinInterval = trackSliderMinIntervalDefault;
1263            }
1264            if (trackSliderMinInterval < trackSliderMinIntervalMin) {
1265                trackSliderMinInterval = trackSliderMinIntervalMin;
1266            } else if (trackSliderMinInterval > trackSliderMinIntervalMax) {
1267                trackSliderMinInterval = trackSliderMinIntervalMax;
1268            }
1269        } else {
1270            trackSliderMinInterval = trackSliderMinIntervalDefault;
1271        }
1272        Attribute hssAtt = e.getAttribute("hideSpeedStep");
1273        if (hssAtt != null) {
1274            try {
1275                setHideSpeedStep ( hssAtt.getBooleanValue() );
1276            } catch (org.jdom2.DataConversionException ex) {
1277                setHideSpeedStep ( false );
1278            }
1279        } else {
1280            setHideSpeedStep ( false );
1281        }
1282        if ((prevShuntingFn == null) && (e.getAttribute("switchSliderOnFunction") != null)) {
1283            setSwitchSliderFunction(e.getAttribute("switchSliderOnFunction").getValue());
1284        }
1285        internalAdjust = false;
1286        Element window = e.getChild("window");
1287        WindowPreferences.setPreferences(this, window);
1288    }
1289
1290    @Override
1291    public void notifyAddressChosen(LocoAddress l) {
1292    }
1293
1294    @Override
1295    public void notifyAddressReleased(LocoAddress la) {
1296        if (throttle == null) {
1297            log.debug("notifyAddressReleased() throttle already null, called for loc {}", la);
1298            return;
1299        }        
1300        this.setEnabled(false);
1301        if (throttle != null) {
1302            throttle.removePropertyChangeListener(this);
1303        }
1304        throttle = null;
1305        if (prevShuntingFn != null) {
1306            setSwitchSliderFunction(prevShuntingFn);
1307            prevShuntingFn = null;
1308        }
1309    }
1310
1311    private void addressThrottleFound() {
1312        setEnabled(true);
1313        setIsForward(throttle.getIsForward());
1314        setSpeedStepsMode(throttle.getSpeedStepMode());
1315        setSpeedValues(throttle.getSpeedIncrement(), throttle.getSpeedSetting());
1316        throttle.addPropertyChangeListener(this);
1317    }
1318
1319    @Override
1320    public void notifyAddressThrottleFound(DccThrottle t) {
1321        log.debug("control panel received new throttle {}", t);
1322        if (throttle != null) {
1323            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1324            return;
1325        }
1326        if (isConsist) {
1327            // ignore if is a consist
1328            return;
1329        }
1330        throttle = t;
1331        addressThrottleFound();
1332
1333        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getShuntingFunction() != null)) {
1334            prevShuntingFn = getSwitchSliderFunction();
1335            setSwitchSliderFunction(addressPanel.getRosterEntry().getShuntingFunction());                            
1336        } else {
1337            setSwitchSliderFunction(switchSliderFunction); // reset slider           
1338        }
1339        if (log.isDebugEnabled()) {
1340            jmri.DccLocoAddress Address = (jmri.DccLocoAddress) throttle.getLocoAddress();
1341            log.debug("new address is {}", Address.toString());
1342        }
1343    }
1344
1345    @Override
1346    public void notifyConsistAddressChosen(LocoAddress l) {
1347        notifyAddressChosen(l);
1348    }
1349
1350    @Override
1351    public void notifyConsistAddressReleased(LocoAddress la) {
1352        notifyAddressReleased(la);
1353        isConsist = false;
1354    }
1355
1356    @Override
1357    public void notifyConsistAddressThrottleFound(DccThrottle t) {
1358        log.debug("control panel received consist throttle {}", t);
1359        isConsist = true;
1360        throttle = t;
1361        addressThrottleFound();
1362    }
1363
1364    public void setSwitchSliderFunction(String fn) {
1365        switchSliderFunction = fn;
1366        if ((switchSliderFunction == null) || (switchSliderFunction.length() == 0)) {
1367            return;
1368        }
1369        if ((throttle != null) && (_displaySlider != STEPDISPLAY)) { // Update UI depending on function state
1370            try {
1371                // this uses reflection because the user is allowed to name a
1372                // throttle function that triggers this action.
1373                java.lang.reflect.Method getter = throttle.getClass().getMethod("get" + switchSliderFunction, (Class[]) null);
1374
1375                Boolean state = (Boolean) getter.invoke(throttle, (Object[]) null);
1376                if (state) {
1377                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1378                } else {
1379                    setSpeedController(SLIDERDISPLAY);
1380                }
1381
1382            } catch (IllegalAccessException|NoSuchMethodException|java.lang.reflect.InvocationTargetException ex) {
1383                log.debug("Exception in setSwitchSliderFunction: {} while looking for function {}", ex, switchSliderFunction);
1384            }
1385        }
1386    }
1387    
1388
1389    private void computeLabelsTable() {
1390        defaultLabelTable = new HashMap<>(5);
1391        defaultLabelTable.put(maxSpeed / 2, new JLabel("50%"));
1392        defaultLabelTable.put(maxSpeed, new JLabel("100%"));        
1393        defaultLabelTable.put(0, new JLabel(Bundle.getMessage("ButtonStop")));
1394        defaultLabelTable.put(-maxSpeed / 2, new JLabel("-50%"));
1395        defaultLabelTable.put(-maxSpeed, new JLabel("-100%"));
1396        
1397        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getAttribute("speedLabels") != null)) {
1398            ObjectMapper mapper = new ObjectMapper();
1399            try {
1400                SpeedLabel[] speedLabels = mapper.readValue(addressPanel.getRosterEntry().getAttribute("speedLabels"), SpeedLabel[].class );
1401                if (speedLabels != null && speedLabels.length>0) {
1402                    verticalLabelMap = new HashMap<>(speedLabels.length *2 );
1403                    horizontalLabelMap = new HashMap<>(speedLabels.length *2 );
1404                    JLabel label;
1405                    for (SpeedLabel sp : speedLabels) {
1406                        label = new JLabel( sp.label, speedLabelVerticalImageIcon, SwingConstants.LEFT );
1407                        label.setVerticalTextPosition(JLabel.CENTER);
1408                        verticalLabelMap.put( sp.value, label);
1409                        verticalLabelMap.put( -sp.value, label);
1410
1411                        label = new JLabel( sp.label, speedLabelHorizontalImageIcon, SwingConstants.LEFT );
1412                        label.setHorizontalTextPosition(JLabel.CENTER);
1413                        label.setVerticalTextPosition(JLabel.BOTTOM);
1414
1415                        horizontalLabelMap.put( sp.value, label);
1416                        horizontalLabelMap.put( -sp.value, label);
1417                    }
1418                    updateSlidersLabelDisplay();
1419                }
1420            } catch (JsonProcessingException ex) {
1421                log.error("Exception trying to parse speedLabels attribute from roster entry: {} ", ex.getMessage());                
1422            }                                             
1423        } else {
1424            verticalLabelMap = null;
1425            horizontalLabelMap = null;            
1426        }
1427    }
1428        
1429    // update slider label display depending on context (vertical|horizontal & normal|large)
1430    private void updateSlidersLabelDisplay() {
1431        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1432        Map<Integer, JLabel> labelTable = new HashMap<>(10);
1433        
1434        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1435            speedSlider.setPaintTicks(false);
1436            speedSliderContinuous.setPaintTicks(false);
1437        } else {
1438            speedSlider.setPaintTicks(true);
1439            speedSliderContinuous.setPaintTicks(true);            
1440            labelTable.putAll(defaultLabelTable);                                
1441        }
1442        if ((speedSlider.getOrientation() == JSlider.HORIZONTAL) && (horizontalLabelMap != null)) {
1443            labelTable.putAll(horizontalLabelMap);
1444        } 
1445        if ((speedSlider.getOrientation() == JSlider.VERTICAL) && (verticalLabelMap != null)) {
1446            labelTable.putAll(verticalLabelMap);                 
1447        }
1448        
1449        if (! labelTable.isEmpty()) {
1450            // setLabelTable() only likes Colection which is a HashTable
1451            speedSlider.setLabelTable(new Hashtable<>(labelTable));
1452            speedSliderContinuous.setLabelTable(new Hashtable<>(labelTable));
1453            speedSlider.setPaintLabels(true);
1454            speedSliderContinuous.setPaintLabels(true);
1455        } else {
1456            speedSlider.setPaintLabels(false);
1457            speedSliderContinuous.setPaintLabels(false);
1458        }
1459    }
1460
1461    public String getSwitchSliderFunction() {
1462        return switchSliderFunction;
1463    }
1464
1465    public void saveToRoster(RosterEntry re) {
1466        if (re == null) {
1467            return;
1468        }
1469        if ((re.getShuntingFunction() != null) && (re.getShuntingFunction().compareTo(getSwitchSliderFunction()) != 0)) {
1470            re.setShuntingFunction(getSwitchSliderFunction());
1471        } else if ((re.getShuntingFunction() == null) && (getSwitchSliderFunction() != null)) {
1472            re.setShuntingFunction(getSwitchSliderFunction());
1473        } else {
1474            return;
1475        }
1476        Roster.getDefault().writeRoster();
1477    }
1478
1479    // to handle svg transformation to displayable images
1480    private static class MyTranscoder extends ImageTranscoder {
1481        private BufferedImage image = null;
1482        @Override
1483        public BufferedImage createImage(int w, int h) {
1484            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1485            return image;
1486        }
1487        public BufferedImage getImage() {
1488            return image;
1489        }
1490        @Override
1491        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
1492            //not required here, do nothing
1493        }
1494    }
1495   
1496    // this mouse adapter makes sure to move the slider cursor to precisely where the user clicks
1497    // see https://jmri-developers.groups.io/g/jmri/message/7874
1498    private static class JSliderPreciseMouseAdapter extends JmriMouseAdapter {
1499
1500        @Override
1501        public void mousePressed(JmriMouseEvent e) {
1502            if (e.getButton() == JmriMouseEvent.BUTTON1) {
1503                JSlider sourceSlider = (JSlider) e.getSource();
1504                if (!sourceSlider.isEnabled()) {
1505                    return;
1506                }
1507                BasicSliderUI ui = (BasicSliderUI) sourceSlider.getUI();
1508                int value;
1509                if (sourceSlider.getOrientation() == JSlider.VERTICAL) {
1510                    value = ui.valueForYPosition(e.getY());
1511                } else {
1512                    value = ui.valueForXPosition(e.getX());
1513                }
1514                sourceSlider.setValue(value);
1515            }
1516        }
1517    }
1518    
1519    // For Jackson pasing of roster entry property holding speed labels (if any)
1520    private static class SpeedLabel {
1521        public int value = -1;
1522        public String label = "";      
1523    }
1524
1525    // initialize logging
1526    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ControlPanel.class);
1527}