001package jmri.jmrit.swing.meter;
002
003import java.awt.event.*;
004import java.beans.PropertyChangeListener;
005import java.text.DecimalFormat;
006import java.text.DecimalFormatSymbols;
007import java.util.ArrayList;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Map;
011import java.util.UUID;
012
013import javax.swing.*;
014
015import jmri.*;
016import jmri.jmrit.catalog.NamedIcon;
017import jmri.util.JmriJFrame;
018
019/**
020 * Frame providing a simple LCD-based display of track voltage.
021 * <p>
022 * Adapted from ammeter to display voltage and current.
023 *
024 * @author Ken Cameron        Copyright (C) 2007
025 * @author Mark Underwood     Copyright (C) 2007
026 * @author Andrew Crosland    Copyright (C) 2020
027 * @author Daniel Bergqvist   Copyright (C) 2020
028 * @author B. Milhaupt        Copyright (C) 2020
029 */
030public class MeterFrame extends JmriJFrame {
031
032    public enum Unit {
033        Percent(1.0),    // Not a unit, but here anyway as display option
034        MicroVolt(1000*1000),
035        MilliVolt(1000),
036        Volt(1.0),
037        KiloVolt(1/1000.0),
038        MicroAmpere(1000*1000),
039        MilliAmpere(1000),
040        Ampere(1.0),
041        KiloAmpere(1/1000.0);
042
043        private final double multiply;
044
045        Unit(double m) { multiply = m; }
046    }
047
048    private static final int MAX_INTEGER_DIGITS = 7;
049    private static final int MAX_DECIMAL_DIGITS = 3;
050
051    private final UUID uuid;
052
053    private final List<Meter> voltageMeters = new ArrayList<>();
054    private final List<Meter> currentMeters = new ArrayList<>();
055
056    // GUI member declarations
057    private JMenuBar menuBar;
058    ArrayList<JLabel> integerDigitIcons;
059    ArrayList<JLabel> decimalDigitIcons;
060    JLabel decimal;
061    boolean decimalDot = true;
062    Map<Unit, JLabel> unitLabels = new HashMap<>();
063
064    Map<Meter, JCheckBoxMenuItem> meter_MenuItemMap = new HashMap<>();
065    Map<Unit, JCheckBoxMenuItem> units_MenuItemMap = new HashMap<>();
066    Map<Integer, JCheckBoxMenuItem> integerDigits_MenuItemMap = new HashMap<>();
067    Map<Integer, JCheckBoxMenuItem> decimalDigits_MenuItemMap = new HashMap<>();
068    JMenuItem lastSelectedMeterMenuItem;
069    JMenuItem lastSelectedIntegerDigitsMenuItem;
070    JMenuItem lastSelectedDecimalDigitsMenuItem;
071    int numIntegerDigits = 3;
072    int numDecimalDigits = 0;
073    int lastNumDecimalDigits = -1;
074    int widthOfAllIconsToDisplay = 0;
075    int oldWidthOfAllIconsToDisplay = -1;
076    boolean frameIsInitialized = false;
077    Unit selectedUnit = Unit.Volt;
078
079    int digitIconWidth;
080    int decimalIconWidth;
081    int unitIconWidth;
082    int iconHeight;
083
084    private PropertyChangeListener propertyChangeListener;
085
086    private Meter meter;
087    
088    private String initialMeterName=""; // remember the initially selected meter since it may not exist at frame creation
089
090    NamedIcon[] integerDigits = new NamedIcon[10];
091    NamedIcon[] decimalDigits = new NamedIcon[10];
092    NamedIcon decimalIcon;
093    NamedIcon microVoltIcon;
094    NamedIcon milliVoltIcon;
095    NamedIcon voltIcon;
096    NamedIcon kiloVoltIcon;
097    NamedIcon microAmpIcon;
098    NamedIcon milliAmpIcon;
099    NamedIcon ampIcon;
100    NamedIcon kiloAmpIcon;
101    NamedIcon percentIcon;
102    NamedIcon errorIcon;
103
104    JPanel pane1;
105    JPanel meterPane;
106
107    public MeterFrame() {
108        this(UUID.randomUUID());
109    }
110
111    public MeterFrame(UUID uuid) {
112        super(Bundle.getMessage("VoltageMeterTitle"));
113
114        this.uuid = uuid;
115
116        MeterManager mm = InstanceManager.getNullableDefault(MeterManager.class);
117        if (mm == null) throw new RuntimeException("No meter manager exists");
118
119        addAllMeters();
120
121        if (!voltageMeters.isEmpty()) {
122            setMeter(voltageMeters.get(0));
123        } else if (!currentMeters.isEmpty()) {
124            setMeter(currentMeters.get(0));
125        } else {
126            setTitle(Bundle.getMessage("VoltageMeterTitle"));
127        }
128
129        MeterFrameManager.getInstance().register(this);
130    }
131
132    /**
133     * Get the UUID of this frame.
134     * <p>
135     * The UUID is used if two different panel files are loaded with the same
136     * meter frame.
137     *
138     * @return the UUID of this frame
139     */
140    public UUID getUUID() {
141        return uuid;
142    }
143
144    /**
145     * Get the meter that is displayed.
146     * @return the meter
147     */
148    public Meter getMeter() {
149        return meter;
150    }
151
152    /**
153     * Set the meter that is displayed.
154     * @param m the meter or null if no meter is to be shown
155     */
156    public void setMeter(Meter m) {
157        if (lastSelectedMeterMenuItem != null) lastSelectedMeterMenuItem.setSelected(false);
158
159        if (meter != null) {
160            meter.disable();
161            meter.removePropertyChangeListener(NamedBean.PROPERTY_STATE, propertyChangeListener);
162        }
163
164        meter = m;
165
166        if (meter == null) return;
167
168        meter.addPropertyChangeListener(NamedBean.PROPERTY_STATE, propertyChangeListener);
169        meter.enable();
170
171        if (frameIsInitialized) {
172            // Initially we want to scale the icons to fit the previously saved window size
173            scaleImage();
174
175            JCheckBoxMenuItem menuItem = meter_MenuItemMap.get(meter);
176            menuItem.setSelected(true);
177            lastSelectedMeterMenuItem = menuItem;
178
179            updateMenuUnits();
180
181            //set Units and Digits, except for restored settings
182            if (!getInitialMeterName().equals(m.getSystemName())) {
183                initSettingsMenu();
184                setInitialMeterName(""); //clear out saved name after first use
185            }
186        }
187
188        if (meter instanceof VoltageMeter) {
189            setTitle(Bundle.getMessage("VoltageMeterTitle2", m.getDisplayName()));
190        } else {
191            setTitle(Bundle.getMessage("CurrentMeterTitle2", m.getDisplayName()));
192        }
193    }
194    
195    JMenu voltageMetersMenu = null;
196    JMenu currentMetersMenu = null;
197    
198    @Override
199    public void initComponents() {
200        MeterManager mm = InstanceManager.getNullableDefault(MeterManager.class);
201        if (mm == null) {
202            return;
203        }
204        mm.addDataListener(new MeterFrame.BeanListListener(this));
205        // Create menu bar
206
207        menuBar = new JMenuBar();
208        voltageMetersMenu = new JMenu(Bundle.getMessage("MenuVoltageMeters"));
209        menuBar.add(voltageMetersMenu);
210        if (voltageMeters.size() == 0) {
211            voltageMetersMenu.add(new JMenuItem(Bundle.getMessage("NoMeters")));
212        } else {
213            for (Meter m : voltageMeters) {
214                JCheckBoxMenuItem item = new JCheckBoxMenuItem(new SelectMeterAction(m.getDisplayName(), m));
215                voltageMetersMenu.add(item);
216                meter_MenuItemMap.put(m, item);
217            }
218        }
219
220        currentMetersMenu = new JMenu(Bundle.getMessage("MenuCurrentMeters"));
221        menuBar.add(currentMetersMenu);
222        if (currentMeters.size() == 0) {
223            currentMetersMenu.add(new JMenuItem(Bundle.getMessage("NoMeters")));
224        } else {
225            for (Meter m : currentMeters) {
226                JCheckBoxMenuItem item = new JCheckBoxMenuItem(new SelectMeterAction(m.getDisplayName(), m));
227                currentMetersMenu.add(item);
228                meter_MenuItemMap.put(m, item);
229            }
230        }
231
232        JMenu settingsMenu = new JMenu(Bundle.getMessage("MenuMeterSettings"));
233        menuBar.add(settingsMenu);
234
235        JMenu unitsMenu = new JMenu(Bundle.getMessage("MenuMeterUnitMenu"));
236        settingsMenu.add(unitsMenu);
237        for (Unit unit : Unit.values()) {
238            final Unit u = unit;
239            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(Bundle.getMessage("MenuMeter_" + unit.name())) {
240                @Override
241                public void actionPerformed(ActionEvent e) {
242                    units_MenuItemMap.get(selectedUnit).setSelected(false);
243                    unitLabels.get(selectedUnit).setVisible(false);
244                    units_MenuItemMap.get(u).setSelected(true);
245                    unitLabels.get(u).setVisible(true);
246                    selectedUnit = u;
247                    update();
248                }
249            });
250            units_MenuItemMap.put(unit, item);
251            unitsMenu.add(item);
252        }
253
254        settingsMenu.addSeparator();
255        JMenu digitsMenu = new JMenu(Bundle.getMessage("MenuMeterIntegerDigits"));
256        settingsMenu.add(digitsMenu);
257        for (int i = 1; i <= MAX_INTEGER_DIGITS; i++) {
258            final int ii = i;
259            final String label = i + "";
260            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(label) {
261                @Override
262                public void actionPerformed(ActionEvent e) {
263                    integerDigits_MenuItemMap.get(numIntegerDigits).setSelected(false);
264                    numIntegerDigits = ii;
265                    update();
266                }
267            });
268            integerDigits_MenuItemMap.put(ii, item);
269            digitsMenu.add(item);
270            if (ii == numIntegerDigits)
271                item.setSelected(true);
272        }
273
274        JMenu decimalMenu = new JMenu(Bundle.getMessage("MenuMeterDecimalDigits"));
275        settingsMenu.add(decimalMenu);
276        for (int i = 0; i <= MAX_DECIMAL_DIGITS; i++) {
277            final int ii = i;
278            final String label2 = i + "";
279            JCheckBoxMenuItem item = new JCheckBoxMenuItem(new AbstractAction(label2) {
280                @Override
281                public void actionPerformed(ActionEvent e) {
282                    decimalDigits_MenuItemMap.get(numDecimalDigits).setSelected(false);
283                    decimalDigits_MenuItemMap.get(ii).setSelected(true);
284                    numDecimalDigits = ii;
285                    update();
286                }
287            });
288            decimalDigits_MenuItemMap.put(ii, item);
289            decimalMenu.add(item);
290            if (ii == numDecimalDigits)
291                item.setSelected(true);
292        }
293
294        setJMenuBar(menuBar);
295
296        // clear the contents
297        getContentPane().removeAll();
298
299        pane1 = new JPanel();
300        pane1.setLayout(new BoxLayout(pane1, BoxLayout.Y_AXIS));
301
302        meterPane = new JPanel();
303        meterPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder()));
304
305        // build the actual multimeter display.
306        meterPane.setLayout(new BoxLayout(meterPane, BoxLayout.X_AXIS));
307
308        // decimal separator is ',' instead of '.' in some locales
309        DecimalFormat format = (DecimalFormat) DecimalFormat.getInstance();
310        DecimalFormatSymbols symbols = format.getDecimalFormatSymbols();
311        if (symbols.getDecimalSeparator() == ',') {
312            decimalDot = false;
313        }
314        //Load the images (these are now the larger version of the original gifs
315        for (int i = 0; i < 10; i++) {
316            integerDigits[i] = new NamedIcon("resources/icons/misc/LCD/Lcd_" + i + "b.GIF", "resources/icons/misc/LCD/Lcd_" + i + "b.GIF");
317        }
318        for (int i = 0; i < 10; i++) {
319            decimalDigits[i] = new NamedIcon("resources/icons/misc/LCD/Lcd_" + i + "b.GIF", "resources/icons/misc/LCD/Lcd_" + i + "b.GIF");
320        }
321        if (decimalDot) {
322            decimalIcon = new NamedIcon("resources/icons/misc/LCD/decimalb.gif", "resources/icons/misc/LCD/decimalb.gif");
323        } else {
324            decimalIcon = new NamedIcon("resources/icons/misc/LCD/decimalc.gif", "resources/icons/misc/LCD/decimalc.gif");
325        }
326        microVoltIcon = new NamedIcon("resources/icons/misc/LCD/uvoltb.gif", "resources/icons/misc/LCD/uvoltb.gif");
327        milliVoltIcon = new NamedIcon("resources/icons/misc/LCD/mvoltb.gif", "resources/icons/misc/LCD/mvoltb.gif");
328        voltIcon = new NamedIcon("resources/icons/misc/LCD/voltb.gif", "resources/icons/misc/LCD/voltb.gif");
329        kiloVoltIcon = new NamedIcon("resources/icons/misc/LCD/kvoltb.gif", "resources/icons/misc/LCD/kvoltb.gif");
330        microAmpIcon = new NamedIcon("resources/icons/misc/LCD/uampb.gif", "resources/icons/misc/LCD/uampb.gif");
331        milliAmpIcon = new NamedIcon("resources/icons/misc/LCD/mampb.gif", "resources/icons/misc/LCD/mampb.gif");
332        ampIcon = new NamedIcon("resources/icons/misc/LCD/ampb.gif", "resources/icons/misc/LCD/ampb.gif");
333        kiloAmpIcon = new NamedIcon("resources/icons/misc/LCD/kampb.gif", "resources/icons/misc/LCD/kampb.gif");
334        percentIcon = new NamedIcon("resources/icons/misc/LCD/percentb.gif", "resources/icons/misc/LCD/percentb.gif");
335        errorIcon = new NamedIcon("resources/icons/misc/LCD/Lcd_Error.GIF", "resources/icons/misc/LCD/Lcd_Error.GIF");
336
337        decimal = new JLabel(decimalIcon);
338        unitLabels.put(Unit.Percent, new JLabel(percentIcon));
339        unitLabels.put(Unit.MicroVolt, new JLabel(microVoltIcon));
340        unitLabels.put(Unit.MilliVolt, new JLabel(milliVoltIcon));
341        unitLabels.put(Unit.Volt, new JLabel(voltIcon));
342        unitLabels.put(Unit.KiloVolt, new JLabel(kiloVoltIcon));
343        unitLabels.put(Unit.MicroAmpere, new JLabel(microAmpIcon));
344        unitLabels.put(Unit.MilliAmpere, new JLabel(milliAmpIcon));
345        unitLabels.put(Unit.Ampere, new JLabel(ampIcon));
346        unitLabels.put(Unit.KiloAmpere, new JLabel(kiloAmpIcon));
347
348        for (Unit unit : Unit.values()) unitLabels.get(unit).setVisible(false);
349
350        integerDigitIcons = new ArrayList<>(MAX_INTEGER_DIGITS);
351        for (int i = 0; i < MAX_INTEGER_DIGITS; i++) {
352            integerDigitIcons.add(i, new JLabel(integerDigits[i]));
353            meterPane.add(integerDigitIcons.get(i));
354        }
355
356        meterPane.add(decimal);
357
358        decimalDigitIcons = new ArrayList<>(MAX_DECIMAL_DIGITS);
359        for (int i = 0; i < MAX_DECIMAL_DIGITS; i++) {
360            decimalDigitIcons.add(i, new JLabel(decimalDigits[i]));
361            meterPane.add(decimalDigitIcons.get(i));
362        }
363
364        for (JLabel label : unitLabels.values()) meterPane.add(label);
365
366        iconHeight = integerDigits[0].getIconHeight();
367        digitIconWidth = integerDigits[0].getIconWidth();
368        decimalIconWidth = decimalIcon.getIconWidth();
369        unitIconWidth = milliVoltIcon.getIconWidth();
370
371        // Initially we want to scale the icons to fit the previously saved window size
372        scaleImage();
373
374        if (meter != null) {
375            meter.enable();
376        }
377
378        updateMenuUnits();
379        initSettingsMenu();
380
381        // Request callback to update time
382        propertyChangeListener = (java.beans.PropertyChangeEvent e) -> update();
383        if (meter != null) {
384            meter.addPropertyChangeListener(NamedBean.PROPERTY_STATE, propertyChangeListener);
385        }
386
387        // Add component listener to handle frame resizing event
388        this.addComponentListener(
389                new ComponentAdapter() {
390            @Override
391            public void componentResized(ComponentEvent e) {
392                scaleImage();
393            }
394        });
395
396        pane1.add(meterPane);
397        getContentPane().add(pane1);
398
399        getContentPane().setPreferredSize(meterPane.getPreferredSize());
400
401        pack();
402
403        frameIsInitialized = true;
404    }
405
406    /* Set default Units, Digits and Decimals for Settings menu
407     *   based on initial/selected meter configuration.                */
408    private void initSettingsMenu() {
409        
410        boolean isPercent = (meter != null) && (meter.getUnit() == Meter.Unit.Percent);
411        boolean isVoltage = (meter != null) && (meter instanceof VoltageMeter) && !isPercent;
412        boolean isCurrent = (meter != null) && (meter instanceof CurrentMeter) && !isPercent;
413
414        units_MenuItemMap.get(selectedUnit).setSelected(false);
415        unitLabels.get(selectedUnit).setVisible(false);
416
417        if (isPercent) selectedUnit = Unit.Percent;
418        else if (isVoltage && (meter.getUnit() == Meter.Unit.Milli)) selectedUnit = Unit.MilliVolt;
419        else if (isVoltage) selectedUnit = Unit.Volt;
420        else if (isCurrent && (meter.getUnit() == Meter.Unit.Milli)) selectedUnit = Unit.MilliAmpere;
421        else selectedUnit = Unit.Ampere;
422
423        units_MenuItemMap.get(selectedUnit).setSelected(true);
424        unitLabels.get(selectedUnit).setVisible(true);
425        log.debug("selectedUnit set to '{}' for '{}'", selectedUnit, uuid);               
426        update();
427
428        if (meter == null) return; // skip if meter not set
429        
430        double max = meter.getMax();
431        int iDigits = (int) (Math.log10(max) + 1);
432        log.debug("integer digits set to {} for max={} for '{}'", iDigits, max, uuid);               
433        setNumIntegerDigits(iDigits);
434        
435        double res = meter.getResolution();
436        int dDigits = 0; //assume no decimals
437        if (res % 1 != 0) { //not a whole number
438          dDigits = new java.math.BigDecimal(String.valueOf(res)).scale(); // get decimal places used by resolution
439        }
440        log.debug("decimal digits set to {} for resolution={} for '{}'", dDigits, res, uuid);               
441        setNumDecimalDigits(dDigits);
442    }
443
444    // Scale the clock digit images to fit the size of the display window.
445    synchronized public void scaleImage() {
446
447        int frameHeight = this.getContentPane().getHeight()
448                - meterPane.getInsets().top - meterPane.getInsets().bottom;
449        int frameWidth = this.getContentPane().getWidth()
450                - meterPane.getInsets().left - meterPane.getInsets().right;
451
452        double hscale = ((double)frameHeight)/((double)iconHeight);
453        double wscale = ((double)frameWidth)/((double)widthOfAllIconsToDisplay);
454        double scale = Math.min(hscale, wscale);
455
456        for (int i = 0; i < 10; i++) {
457            integerDigits[i].scale(scale,this);
458        }
459        for (int i = 0; i < 10; i++) {
460            decimalDigits[i].scale(scale,this);
461        }
462        decimalIcon.scale(scale,this);
463        microVoltIcon.scale(scale,this);
464        milliVoltIcon.scale(scale,this);
465        voltIcon.scale(scale,this);
466        kiloVoltIcon.scale(scale,this);
467        microAmpIcon.scale(scale,this);
468        milliAmpIcon.scale(scale,this);
469        ampIcon.scale(scale, this);
470        kiloAmpIcon.scale(scale,this);
471        percentIcon.scale(scale, this);
472        errorIcon.scale(scale, this);
473
474        meterPane.revalidate();
475        this.getContentPane().revalidate();
476    }
477
478    private void updateMenuUnits() {
479        boolean isPercent = (meter != null) && (meter.getUnit() == Meter.Unit.Percent);
480        boolean isVoltage = (meter != null) && (meter instanceof VoltageMeter) && !isPercent;
481        boolean isCurrent = (meter != null) && (meter instanceof CurrentMeter) && !isPercent;
482
483        units_MenuItemMap.get(Unit.Percent).setVisible(isPercent);
484
485        units_MenuItemMap.get(Unit.MicroVolt).setVisible(isVoltage);
486        units_MenuItemMap.get(Unit.MilliVolt).setVisible(isVoltage);
487        units_MenuItemMap.get(Unit.Volt).setVisible(isVoltage);
488        units_MenuItemMap.get(Unit.KiloVolt).setVisible(isVoltage);
489
490        units_MenuItemMap.get(Unit.MicroAmpere).setVisible(isCurrent);
491        units_MenuItemMap.get(Unit.MilliAmpere).setVisible(isCurrent);
492        units_MenuItemMap.get(Unit.Ampere).setVisible(isCurrent);
493        units_MenuItemMap.get(Unit.KiloAmpere).setVisible(isCurrent);
494    }
495
496    private void showError() {
497        for (int i=0; i < MAX_INTEGER_DIGITS; i++) {
498            JLabel label = integerDigitIcons.get(i);
499            if (i < numIntegerDigits) {
500                label.setIcon(errorIcon);
501                label.setVisible(true);
502            } else {
503                label.setVisible(false);
504            }
505        }
506
507        decimal.setVisible(numDecimalDigits > 0);
508
509        for (int i=0; i < MAX_DECIMAL_DIGITS; i++) {
510            JLabel label = decimalDigitIcons.get(i);
511            if (i < numDecimalDigits) {
512                label.setIcon(errorIcon);
513                label.setVisible(true);
514            } else {
515                label.setVisible(false);
516            }
517        }
518
519        // Add width of integer digits
520        widthOfAllIconsToDisplay = digitIconWidth * numIntegerDigits;
521
522        // Add decimal point
523        if (numDecimalDigits > 0) widthOfAllIconsToDisplay += decimalIconWidth;
524
525        // Add width of decimal digits
526        widthOfAllIconsToDisplay += digitIconWidth * numDecimalDigits;
527
528        // Add one for the unit icon
529        widthOfAllIconsToDisplay += unitIconWidth;
530
531        if (widthOfAllIconsToDisplay != oldWidthOfAllIconsToDisplay){
532            // clear the content pane and rebuild it.
533            scaleImage();
534            oldWidthOfAllIconsToDisplay = widthOfAllIconsToDisplay;
535        }
536    }
537
538    /**
539     * Update the displayed value.
540     *
541     * Assumes an integer value has an extra, non-displayed decimal digit.
542     */
543    synchronized void update() {
544        if (meter == null) {
545            showError();
546            return;
547        }
548
549        double meterValue = meter.getKnownAnalogValue() * selectedUnit.multiply;
550
551        switch (meter.getUnit()) {
552            case Kilo:
553                meterValue *= 1000.0;
554                break;
555
556            case Milli:
557                meterValue /= 1000.0;
558                break;
559
560            case Micro:
561                meterValue /= 1000_000.0;
562                break;
563
564            case NoPrefix:
565            case Percent:
566            default:
567                // Do nothing
568        }
569
570        // We want at least one decimal digit so we cut the last digit later.
571        // The problem is that the format string %05.0f will not add the dot
572        // and we always want the dot to be able to split the string at the dot.
573        int numChars = numIntegerDigits + numDecimalDigits + 2;
574        String formatStr = String.format("%%0%d.%df", numChars, numDecimalDigits+1);
575        String valueStr = String.format(formatStr, meterValue);
576        String rgx = "\\.";
577        if (!decimalDot) {
578            rgx = ","; // decimal separator is "," in some locales
579        }
580        String[] valueParts = valueStr.split(rgx);
581        if (valueParts.length < 2) {
582            log.warn("cannot parse meter value using locale decimal separator");
583            return;
584        }
585        // Show error if we don't have enough integer digits to show the result
586        if (valueParts[0].length() > MAX_INTEGER_DIGITS) {
587            showError();
588            return;
589        }
590
591        for (int i = 0; i < MAX_INTEGER_DIGITS; i++) {
592            JLabel label = integerDigitIcons.get(i);
593            if (i < valueParts[0].length()) {
594                label.setIcon(integerDigits[valueParts[0].charAt(i)-'0']);
595                label.setVisible(true);
596            } else {
597                label.setVisible(false);
598            }
599        }
600
601        decimal.setVisible(numDecimalDigits > 0);
602
603        for (int i = 0; i < MAX_DECIMAL_DIGITS; i++) {
604            JLabel label = decimalDigitIcons.get(i);
605            if (i < valueParts[1].length()-1) {     // the decimal part has one digit too much
606                label.setIcon(integerDigits[valueParts[1].charAt(i)-'0']);
607                label.setVisible(true);
608            } else {
609                label.setVisible(false);
610            }
611        }
612
613
614        // Add width of integer digits
615        widthOfAllIconsToDisplay = digitIconWidth * valueParts[0].length();
616
617        // Add width of decimal point (or other delimiter as set in system locale?)
618        if (numDecimalDigits > 0) widthOfAllIconsToDisplay += decimalIconWidth;
619
620        // Add width of decimal digits
621        widthOfAllIconsToDisplay += digitIconWidth * (valueParts[1].length()-1);
622
623        // Add one for the unit icon
624        widthOfAllIconsToDisplay += unitIconWidth;
625
626        if (widthOfAllIconsToDisplay != oldWidthOfAllIconsToDisplay){
627            // clear the content pane and rebuild it.
628            scaleImage();
629            oldWidthOfAllIconsToDisplay = widthOfAllIconsToDisplay;
630        }
631    }
632
633    @Override
634    public void dispose() {
635        if (meter != null) {
636            meter.disable();
637            meter.removePropertyChangeListener(propertyChangeListener);
638        }
639        MeterFrameManager.getInstance().deregister(this);
640        super.dispose();
641    }
642
643    /**
644     * Get the number of integer digits.
645     *
646     * @return the number of integer digits
647     */
648    public int getNumIntegerDigits() {
649        return numIntegerDigits;
650    }
651
652    /**
653     * Set the number of integer digits.
654     *
655     * @param digits the number of integer digits
656     */
657    public void setNumIntegerDigits(int digits) {
658        integerDigits_MenuItemMap.get(numIntegerDigits).setSelected(false);
659        integerDigits_MenuItemMap.get(digits).setSelected(true);
660        numIntegerDigits = digits;
661        update();
662    }
663
664    /**
665     * Get the number of decimal digits.
666     *
667     * @return the number of decimal digits
668     */
669    public int getNumDecimalDigits() {
670        return numDecimalDigits;
671    }
672
673    /**
674     * Set the number of decimal digits.
675     *
676     * @param digits the number of decimal digits
677     */
678    public void setNumDecimalDigits(int digits) {
679        decimalDigits_MenuItemMap.get(numDecimalDigits).setSelected(false);
680        decimalDigits_MenuItemMap.get(digits).setSelected(true);
681        numDecimalDigits = digits;
682        update();
683    }
684
685    /**
686     * Get the unit.
687     *
688     * @return the unit
689     */
690    public Unit getUnit() {
691        return selectedUnit;
692    }
693
694    /**
695     * Set the unit.
696     *
697     * @param unit the unit
698     */
699    public void setUnit(Unit unit) {
700        units_MenuItemMap.get(selectedUnit).setSelected(false);
701        unitLabels.get(selectedUnit).setVisible(false);
702        units_MenuItemMap.get(unit).setSelected(true);
703        unitLabels.get(unit).setVisible(true);
704        selectedUnit = unit;
705        update();
706    }
707
708    public String getInitialMeterName() {
709        return initialMeterName;
710    }
711
712    public void setInitialMeterName(String initialMeterName) {
713        this.initialMeterName = initialMeterName;
714    }
715
716    /**
717     * Update the list of available meters.
718     */
719    private void addAllMeters() {
720        MeterManager mm = InstanceManager.getNullableDefault(MeterManager.class);
721        if (mm == null) {
722            return;
723        }
724        if (log.isTraceEnabled()) { 
725            log.trace("attempting to add all meters.  There are {} meters to add.",
726                    mm.getNamedBeanSet().size());
727        }
728        mm.getNamedBeanSet().forEach((m) -> {
729            String n = m.getDisplayName();
730            if (m instanceof VoltageMeter) {
731                if (voltageMeters.contains(m)) {
732                    log.trace("voltage meter '{}' is already present", n);
733                } else {
734                    voltageMeters.add(m);
735                    log.trace("Added voltage meter '{}'", n);
736                }
737            } else if (m instanceof CurrentMeter) {
738                if (currentMeters.contains(m)) {
739                    log.trace("current meter '{}' is already present", n);
740                } else {
741                    currentMeters.add(m);
742                    log.trace("Added current meter '{}'", n);
743                }
744            }
745        });
746
747        if ((menuBar != null) && (voltageMetersMenu != null)) {
748            updateMetersMenu(voltageMetersMenu, voltageMeters);
749        }
750
751        if ((menuBar != null) && (currentMetersMenu != null)) {
752            updateMetersMenu(currentMetersMenu, currentMeters);
753        }
754    }
755
756    /**
757     * Update specified menu with specified list of meters
758     *
759     * @param menu - Menu to be updated
760     * @param meters - list of Meters
761     * 
762     */
763    private void updateMetersMenu(JMenu menu, List<Meter> meters) {
764        for (Meter meter : meters) {            
765            String n = meter.getDisplayName();
766            log.trace("need to add a new checkbox for meter '{}'?", n);
767            boolean found = false;
768
769            if (menu.getItemCount() > 0) {
770                for (int i =0; (i < menu.getItemCount()) && (!found);++i) {
771                    JMenuItem jim = menu.getItem(i);
772                    if (jim instanceof JCheckBoxMenuItem) {
773                        if (jim.getText().compareTo(meter.getDisplayName()) == 0 ) {
774                            log.trace("item '{}' is already in menu", n);
775                            found = true;
776                        } else {
777                            log.trace("item '{}' is not already in menu", n);
778                        }
779                    }
780                }
781            }
782            if (!found) {
783                log.trace("Adding item '{}' to menu for frame {}", n, uuid);
784                JCheckBoxMenuItem item = new JCheckBoxMenuItem(new SelectMeterAction(n, meter));
785                menu.add(item);
786                meter_MenuItemMap.put(meter, item);
787                //if this new meter was selected when panel stored, activate it
788                if (getInitialMeterName().equals(n)) {
789                    setMeter(meter);
790                }
791            }
792        }
793        // if more menu options than meters, remove any "NoMeters" menu item.
794        if (menu.getItemCount() > meters.size()) {
795            for (int i =0; (i < menu.getItemCount());++i) {
796                JMenuItem jim = menu.getItem(i);
797                if (jim.getText().compareTo(Bundle.getMessage("NoMeters")) == 0 ) {
798                    menu.remove(jim);
799                    log.trace("item '{}' removed from this menu for frame {}", jim.getText(), uuid);
800                    break;
801                }                
802            }
803        }    
804    }
805
806    /**
807     * Update the menu items in the voltage meters and current meters menus.
808     */
809    private void updateCheckboxList() {
810        log.trace("Updating the checkbox lists of meters.");
811        addAllMeters();
812        currentMetersMenu.repaint();
813        voltageMetersMenu.repaint();
814    }
815
816    /**
817     * Provide hooks so that menus of available meters may be updated "on-the-fly"
818     * as new meters are created and/or old meters are disposed of.
819     */
820    private static class BeanListListener implements jmri.Manager.ManagerDataListener<Meter> {
821
822        private BeanListListener(MeterFrame mf) {
823            this.mf = mf;
824        }
825        MeterFrame mf;
826
827        @Override
828        public void contentsChanged(Manager.ManagerDataEvent<Meter> e) {
829            log.warn("contents of the bean list changed.");
830            mf.updateCheckboxList();
831        }
832
833        @Override
834        public void intervalRemoved(Manager.ManagerDataEvent<Meter> e) {
835            mf.updateCheckboxList();
836        }
837
838        @Override
839        public void intervalAdded(Manager.ManagerDataEvent<Meter> e) {
840            mf.updateCheckboxList();
841        }
842    }
843
844    /**
845     * Mechanism for acting upon selection of a meter from one of the menu items.
846     */
847    public class SelectMeterAction extends AbstractAction {
848
849        private final Meter m;
850
851        public SelectMeterAction(String actionName, Meter meter) {
852            super(actionName);
853            this.m = meter;
854        }
855
856        @Override
857        public void actionPerformed(ActionEvent e) {
858            setMeter(m);
859
860            JMenuItem selectedItem = (JMenuItem) e.getSource();
861            selectedItem.setSelected(true);
862            lastSelectedMeterMenuItem = selectedItem;
863        }
864    }
865
866    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MeterFrame.class);
867
868}