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        setHideSpeedStep(preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector());        
1147    }
1148
1149    /**
1150     * A PopupListener to handle mouse clicks and releases. Handles the popup
1151     * menu.
1152     */
1153    private class PopupListener extends JmriMouseAdapter {
1154        /**
1155         * If the event is the popup trigger, which is dependent on the
1156         * platform, present the popup menu.
1157         * @param e The JmriMouseEvent causing the action.
1158         */
1159        @Override
1160        public void mouseClicked(JmriMouseEvent e) {
1161            checkTrigger(e);
1162        }
1163
1164        /**
1165         * If the event is the popup trigger, which is dependent on the
1166         * platform, present the popup menu.
1167         * @param e The JmriMouseEvent causing the action.
1168         */
1169        @Override
1170        public void mousePressed(JmriMouseEvent e) {
1171            checkTrigger( e);
1172        }
1173
1174        /**
1175         * If the event is the popup trigger, which is dependent on the
1176         * platform, present the popup menu.
1177         * @param e The JmriMouseEvent causing the action.
1178         */
1179        @Override
1180        public void mouseReleased(JmriMouseEvent e) {
1181            checkTrigger( e);
1182        }
1183
1184        private void checkTrigger( JmriMouseEvent e) {
1185            if (e.isPopupTrigger()) {
1186                initPopupMenu();
1187                popupMenu.show(e.getComponent(), e.getX(), e.getY());
1188            }
1189        }
1190    }
1191
1192    private void initPopupMenu() {
1193        if (popupMenu == null) {
1194            JMenuItem propertiesMenuItem = new JMenuItem(Bundle.getMessage("ControlPanelProperties"));
1195            propertiesMenuItem.addActionListener((ActionEvent e) -> {
1196                if (propertyEditor == null) {
1197                    propertyEditor = new ControlPanelPropertyEditor(this);
1198                }
1199                propertyEditor.setLocation(MouseInfo.getPointerInfo().getLocation());
1200                propertyEditor.resetProperties();
1201                propertyEditor.setVisible(true);
1202            });
1203            popupMenu = new JPopupMenu();
1204            popupMenu.add(propertiesMenuItem);
1205        }
1206    }
1207
1208    /**
1209     * Collect the prefs of this object into XML Element
1210     * <ul>
1211     * <li> Window prefs
1212     * </ul>
1213     *
1214     *
1215     * @return the XML of this object.
1216     */
1217    public Element getXml() {
1218        Element me = new Element("ControlPanel");
1219        me.setAttribute("displaySpeedSlider", String.valueOf(this._displaySlider));
1220        me.setAttribute("trackSlider", String.valueOf(this.trackSlider));
1221        me.setAttribute("trackSliderMinInterval", String.valueOf(this.trackSliderMinInterval));
1222        me.setAttribute("switchSliderOnFunction", switchSliderFunction != null ? switchSliderFunction : "Fxx");
1223        me.setAttribute("hideSpeedStep", String.valueOf(this.hideSpeedStep));
1224        //Element window = new Element("window");
1225        java.util.ArrayList<Element> children = new java.util.ArrayList<>(1);
1226        children.add(WindowPreferences.getPreferences(this));
1227        me.setContent(children);
1228        return me;
1229    }
1230
1231    /**
1232     * Set the preferences based on the XML Element.
1233     * <ul>
1234     * <li> Window prefs
1235     * </ul>
1236     *
1237     *
1238     * @param e The Element for this object.
1239     */
1240    public void setXml(Element e) {
1241        internalAdjust = true;
1242        try {
1243            this.setSpeedController(e.getAttribute("displaySpeedSlider").getIntValue());
1244        } catch (org.jdom2.DataConversionException ex) {
1245            log.error("DataConverstionException in setXml", ex);
1246            // in this case, recover by displaying the speed slider.
1247            this.setSpeedController(SLIDERDISPLAY);
1248        }
1249        Attribute tsAtt = e.getAttribute("trackSlider");
1250        if (tsAtt != null) {
1251            try {
1252                trackSlider = tsAtt.getBooleanValue();
1253            } catch (org.jdom2.DataConversionException ex) {
1254                trackSlider = trackSliderDefault;
1255            }
1256        } else {
1257            trackSlider = trackSliderDefault;
1258        }
1259        Attribute tsmiAtt = e.getAttribute("trackSliderMinInterval");
1260        if (tsmiAtt != null) {
1261            try {
1262                trackSliderMinInterval = tsmiAtt.getLongValue();
1263            } catch (org.jdom2.DataConversionException ex) {
1264                trackSliderMinInterval = trackSliderMinIntervalDefault;
1265            }
1266            if (trackSliderMinInterval < trackSliderMinIntervalMin) {
1267                trackSliderMinInterval = trackSliderMinIntervalMin;
1268            } else if (trackSliderMinInterval > trackSliderMinIntervalMax) {
1269                trackSliderMinInterval = trackSliderMinIntervalMax;
1270            }
1271        } else {
1272            trackSliderMinInterval = trackSliderMinIntervalDefault;
1273        }
1274        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);        
1275        Attribute hssAtt = e.getAttribute("hideSpeedStep");
1276        if (hssAtt != null) {
1277            try {
1278                setHideSpeedStep ( hssAtt.getBooleanValue() );
1279            } catch (org.jdom2.DataConversionException ex) {
1280                setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1281            }
1282        } else {
1283            setHideSpeedStep ( preferences.isUsingExThrottle() && preferences.isHidingSpeedStepSelector() );
1284        }
1285        if ((prevShuntingFn == null) && (e.getAttribute("switchSliderOnFunction") != null)) {
1286            setSwitchSliderFunction(e.getAttribute("switchSliderOnFunction").getValue());
1287        }
1288        internalAdjust = false;
1289        Element window = e.getChild("window");
1290        WindowPreferences.setPreferences(this, window);
1291    }
1292
1293    @Override
1294    public void notifyAddressChosen(LocoAddress l) {
1295    }
1296
1297    @Override
1298    public void notifyAddressReleased(LocoAddress la) {
1299        if (throttle == null) {
1300            log.debug("notifyAddressReleased() throttle already null, called for loc {}", la);
1301            return;
1302        }        
1303        this.setEnabled(false);
1304        if (throttle != null) {
1305            throttle.removePropertyChangeListener(this);
1306        }
1307        throttle = null;
1308        if (prevShuntingFn != null) {
1309            setSwitchSliderFunction(prevShuntingFn);
1310            prevShuntingFn = null;
1311        }
1312    }
1313
1314    private void addressThrottleFound() {
1315        setEnabled(true);
1316        setIsForward(throttle.getIsForward());
1317        setSpeedStepsMode(throttle.getSpeedStepMode());
1318        setSpeedValues(throttle.getSpeedIncrement(), throttle.getSpeedSetting());
1319        throttle.addPropertyChangeListener(this);
1320    }
1321
1322    @Override
1323    public void notifyAddressThrottleFound(DccThrottle t) {
1324        log.debug("control panel received new throttle {}", t);
1325        if (throttle != null) {
1326            log.debug("notifyAddressThrottleFound() throttle non null, called for loc {}",t.getLocoAddress());
1327            return;
1328        }
1329        if (isConsist) {
1330            // ignore if is a consist
1331            return;
1332        }
1333        throttle = t;
1334        addressThrottleFound();
1335
1336        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getShuntingFunction() != null)) {
1337            prevShuntingFn = getSwitchSliderFunction();
1338            setSwitchSliderFunction(addressPanel.getRosterEntry().getShuntingFunction());                            
1339        } else {
1340            setSwitchSliderFunction(switchSliderFunction); // reset slider           
1341        }
1342        if (log.isDebugEnabled()) {
1343            jmri.DccLocoAddress Address = (jmri.DccLocoAddress) throttle.getLocoAddress();
1344            log.debug("new address is {}", Address.toString());
1345        }
1346    }
1347
1348    @Override
1349    public void notifyConsistAddressChosen(LocoAddress l) {
1350        notifyAddressChosen(l);
1351    }
1352
1353    @Override
1354    public void notifyConsistAddressReleased(LocoAddress la) {
1355        notifyAddressReleased(la);
1356        isConsist = false;
1357    }
1358
1359    @Override
1360    public void notifyConsistAddressThrottleFound(DccThrottle t) {
1361        log.debug("control panel received consist throttle {}", t);
1362        isConsist = true;
1363        throttle = t;
1364        addressThrottleFound();
1365    }
1366
1367    public void setSwitchSliderFunction(String fn) {
1368        switchSliderFunction = fn;
1369        if ((switchSliderFunction == null) || (switchSliderFunction.length() == 0)) {
1370            return;
1371        }
1372        if ((throttle != null) && (_displaySlider != STEPDISPLAY)) { // Update UI depending on function state
1373            try {
1374                // this uses reflection because the user is allowed to name a
1375                // throttle function that triggers this action.
1376                java.lang.reflect.Method getter = throttle.getClass().getMethod("get" + switchSliderFunction, (Class[]) null);
1377
1378                Boolean state = (Boolean) getter.invoke(throttle, (Object[]) null);
1379                if (state) {
1380                    setSpeedController(SLIDERDISPLAYCONTINUOUS);
1381                } else {
1382                    setSpeedController(SLIDERDISPLAY);
1383                }
1384
1385            } catch (IllegalAccessException|NoSuchMethodException|java.lang.reflect.InvocationTargetException ex) {
1386                log.debug("Exception in setSwitchSliderFunction: {} while looking for function {}", ex, switchSliderFunction);
1387            }
1388        }
1389    }
1390    
1391
1392    private void computeLabelsTable() {
1393        defaultLabelTable = new HashMap<>(5);
1394        defaultLabelTable.put(maxSpeed / 2, new JLabel("50%"));
1395        defaultLabelTable.put(maxSpeed, new JLabel("100%"));        
1396        defaultLabelTable.put(0, new JLabel(Bundle.getMessage("ButtonStop")));
1397        defaultLabelTable.put(-maxSpeed / 2, new JLabel("-50%"));
1398        defaultLabelTable.put(-maxSpeed, new JLabel("-100%"));
1399        
1400        if ((addressPanel != null) && (addressPanel.getRosterEntry() != null) && (addressPanel.getRosterEntry().getAttribute("speedLabels") != null)) {
1401            ObjectMapper mapper = new ObjectMapper();
1402            try {
1403                SpeedLabel[] speedLabels = mapper.readValue(addressPanel.getRosterEntry().getAttribute("speedLabels"), SpeedLabel[].class );
1404                if (speedLabels != null && speedLabels.length>0) {
1405                    verticalLabelMap = new HashMap<>(speedLabels.length *2 );
1406                    horizontalLabelMap = new HashMap<>(speedLabels.length *2 );
1407                    JLabel label;
1408                    for (SpeedLabel sp : speedLabels) {
1409                        label = new JLabel( sp.label, speedLabelVerticalImageIcon, SwingConstants.LEFT );
1410                        label.setVerticalTextPosition(JLabel.CENTER);
1411                        verticalLabelMap.put( sp.value, label);
1412                        verticalLabelMap.put( -sp.value, label);
1413
1414                        label = new JLabel( sp.label, speedLabelHorizontalImageIcon, SwingConstants.LEFT );
1415                        label.setHorizontalTextPosition(JLabel.CENTER);
1416                        label.setVerticalTextPosition(JLabel.BOTTOM);
1417
1418                        horizontalLabelMap.put( sp.value, label);
1419                        horizontalLabelMap.put( -sp.value, label);
1420                    }
1421                    updateSlidersLabelDisplay();
1422                }
1423            } catch (JsonProcessingException ex) {
1424                log.error("Exception trying to parse speedLabels attribute from roster entry: {} ", ex.getMessage());                
1425            }                                             
1426        } else {
1427            verticalLabelMap = null;
1428            horizontalLabelMap = null;            
1429        }
1430    }
1431        
1432    // update slider label display depending on context (vertical|horizontal & normal|large)
1433    private void updateSlidersLabelDisplay() {
1434        final ThrottlesPreferences preferences = InstanceManager.getDefault(ThrottlesPreferences.class);
1435        Map<Integer, JLabel> labelTable = new HashMap<>(10);
1436        
1437        if ( preferences.isUsingExThrottle() && preferences.isUsingLargeSpeedSlider()) {
1438            speedSlider.setPaintTicks(false);
1439            speedSliderContinuous.setPaintTicks(false);
1440        } else {
1441            speedSlider.setPaintTicks(true);
1442            speedSliderContinuous.setPaintTicks(true);            
1443            labelTable.putAll(defaultLabelTable);                                
1444        }
1445        if ((speedSlider.getOrientation() == JSlider.HORIZONTAL) && (horizontalLabelMap != null)) {
1446            labelTable.putAll(horizontalLabelMap);
1447        } 
1448        if ((speedSlider.getOrientation() == JSlider.VERTICAL) && (verticalLabelMap != null)) {
1449            labelTable.putAll(verticalLabelMap);                 
1450        }
1451        
1452        if (! labelTable.isEmpty()) {
1453            // setLabelTable() only likes Colection which is a HashTable
1454            speedSlider.setLabelTable(new Hashtable<>(labelTable));
1455            speedSliderContinuous.setLabelTable(new Hashtable<>(labelTable));
1456            speedSlider.setPaintLabels(true);
1457            speedSliderContinuous.setPaintLabels(true);
1458        } else {
1459            speedSlider.setPaintLabels(false);
1460            speedSliderContinuous.setPaintLabels(false);
1461        }
1462    }
1463
1464    public String getSwitchSliderFunction() {
1465        return switchSliderFunction;
1466    }
1467
1468    public void saveToRoster(RosterEntry re) {
1469        if (re == null) {
1470            return;
1471        }
1472        if ((re.getShuntingFunction() != null) && (re.getShuntingFunction().compareTo(getSwitchSliderFunction()) != 0)) {
1473            re.setShuntingFunction(getSwitchSliderFunction());
1474        } else if ((re.getShuntingFunction() == null) && (getSwitchSliderFunction() != null)) {
1475            re.setShuntingFunction(getSwitchSliderFunction());
1476        } else {
1477            return;
1478        }
1479        Roster.getDefault().writeRoster();
1480    }
1481
1482    // to handle svg transformation to displayable images
1483    private static class MyTranscoder extends ImageTranscoder {
1484        private BufferedImage image = null;
1485        @Override
1486        public BufferedImage createImage(int w, int h) {
1487            image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
1488            return image;
1489        }
1490        public BufferedImage getImage() {
1491            return image;
1492        }
1493        @Override
1494        public void writeImage(BufferedImage bi, TranscoderOutput to) throws TranscoderException {
1495            //not required here, do nothing
1496        }
1497    }
1498   
1499    // this mouse adapter makes sure to move the slider cursor to precisely where the user clicks
1500    // see https://jmri-developers.groups.io/g/jmri/message/7874
1501    private static class JSliderPreciseMouseAdapter extends JmriMouseAdapter {
1502
1503        @Override
1504        public void mousePressed(JmriMouseEvent e) {
1505            if (e.getButton() == JmriMouseEvent.BUTTON1) {
1506                JSlider sourceSlider = (JSlider) e.getSource();
1507                if (!sourceSlider.isEnabled()) {
1508                    return;
1509                }
1510                BasicSliderUI ui = (BasicSliderUI) sourceSlider.getUI();
1511                int value;
1512                if (sourceSlider.getOrientation() == JSlider.VERTICAL) {
1513                    value = ui.valueForYPosition(e.getY());
1514                } else {
1515                    value = ui.valueForXPosition(e.getX());
1516                }
1517                sourceSlider.setValue(value);
1518            }
1519        }
1520    }
1521    
1522    // For Jackson pasing of roster entry property holding speed labels (if any)
1523    private static class SpeedLabel {
1524        public int value = -1;
1525        public String label = "";      
1526    }
1527
1528    // initialize logging
1529    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ControlPanel.class);
1530}