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