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