001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.list;
032
033import java.awt.Color;
034import java.awt.event.ActionEvent;
035import java.awt.event.ActionListener;
036import java.awt.event.KeyAdapter;
037import java.awt.event.KeyEvent;
038import java.util.Objects;
039import java.util.regex.Matcher;
040import java.util.regex.Pattern;
041
042import javax.swing.JComponent;
043import javax.swing.JLabel;
044import javax.swing.JOptionPane;
045import javax.swing.JPanel;
046import javax.swing.JRadioButton;
047import javax.swing.JSlider;
048import javax.swing.event.ChangeListener;
049
050import edu.wisc.ssec.mcidasv.util.MakeToString;
051import edu.wisc.ssec.mcidasv.util.SystemState;
052
053import org.slf4j.Logger;
054import org.slf4j.LoggerFactory;
055
056import ucar.unidata.util.GuiUtils;
057import ucar.unidata.util.LayoutUtil;
058import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
059import edu.wisc.ssec.mcidasv.util.McVTextField;
060import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.OptionPlatform;
061import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Type;
062import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Visibility;
063
064public class MemoryOption extends AbstractOption implements ActionListener {
065    
066    /** Logger object. */
067    private static final Logger logger = LoggerFactory.getLogger(MemoryOption.class);
068    
069    private static final long MEGA_BYTES_TO_BYTES = 1024 * 1024;
070    
071    private static final String TOO_BIG_FMT = "Value exceeds your maximum available memory (%s MB)";
072    
073    private static final String BAD_MEM_FMT = "Badly formatted memory string: %s";
074    
075    private static final String LTE_ZERO_FMT = "Memory cannot be less than or equal to zero: %s";
076    
077    private static final String SLIDER_LABEL_FMT = "Using %s";
078    
079    private static final String SLIDER_LESS_THAN_MIN_LABEL_FMT = "Using < %s";
080    
081    private static final String SLIDER_GREATER_THAN_MAX_LABEL_FMT = "Using > %s";
082    
083    private static final String NO_MEM_PREFIX_FMT = "Could not find matching memory prefix for \"%s\" in string: %s";
084    
085    public enum Prefix {
086        PERCENT("P", "percent", 1),
087        MEGA("M", "megabytes", 1),
088        GIGA("G", "gigabytes", 1024),
089        TERA("T", "terabytes", 1024 * 1024);
090        
091        private final String javaChar;
092        private final String name;
093        private final long scale;
094        
095        Prefix(final String javaChar, final String name, final long scale) {
096            this.javaChar = javaChar;
097            this.name = name;
098            this.scale = scale;
099        }
100        
101        public long getScale() { 
102            return scale; 
103        }
104        
105        public String getJavaChar() {
106            return javaChar.toUpperCase();
107        }
108        
109        public String getName() {
110            return name;
111        }
112        
113        public String getJavaFormat(final String value) {
114            long longVal = Long.parseLong(value);
115            return longVal + javaChar;
116        }
117        
118        @Override public String toString() {
119            return name;
120        }
121    }
122    
123    private enum State { 
124        VALID(Color.BLACK, Color.WHITE),
125        WARN(Color.BLACK, new Color(255, 255, 204)),
126        ERROR(Color.WHITE, Color.PINK);
127        
128        private final Color foreground;
129        
130        private final Color background;
131        
132        State(final Color foreground, final Color background) {
133            this.foreground = foreground;
134            this.background = background;
135        }
136        
137        public Color getForeground() { 
138            return foreground; 
139        }
140        
141        public Color getBackground() { 
142            return background; 
143        }
144    }
145    
146    private static final Prefix[] PREFIXES = { Prefix.MEGA, Prefix.GIGA, Prefix.TERA };
147    
148    private Prefix currentPrefix = Prefix.MEGA;
149    
150    private boolean sliderActive = false;
151    
152    private static final Pattern MEMSTRING = 
153        Pattern.compile("^(\\d+)(M|G|T|P|MB|GB|TB)$", Pattern.CASE_INSENSITIVE);
154    
155    private final String defaultPrefValue;
156    
157    // default to 80% of system memory (in megabytes)
158    private String failsafeValue = 
159        String.valueOf((int) Math.ceil(0.8 * (getSystemMemory() / MemoryOption.MEGA_BYTES_TO_BYTES))) + 'M';
160    
161    private String value = failsafeValue; // bootstrap
162    
163    private JRadioButton jrbSlider = new JRadioButton();
164    
165    private JRadioButton jrbNumber = new JRadioButton();
166    
167    private JPanel sliderPanel = new JPanel();
168    
169    private JLabel sliderLabel = new JLabel();
170    
171    private JSlider slider = new JSlider();
172    
173    private JPanel textPanel = new JPanel();
174    private McVTextField text = new McVTextField();
175    private String initTextValue = value;
176    
177    private int minSliderValue = 10;
178    private int maxSliderValue = 80;
179    private int initSliderValue = minSliderValue;
180    
181    // max size of current JVM, in *megabytes*
182    private long maxmem = getSystemMemory() / (1024 * 1024);
183    
184    private State currentState = State.VALID;
185    
186    private boolean doneInit = false;
187    
188    public MemoryOption(final String id, final String label, 
189        final String defaultValue, final OptionPlatform optionPlatform,
190        final Visibility optionVisibility) 
191    {
192        super(id, label, Type.MEMORY, optionPlatform, optionVisibility);
193        
194        // Link the slider and numeric entry box as a button group
195        GuiUtils.buttonGroup(jrbSlider, jrbNumber);
196        if (maxmem == 0) {
197            defaultPrefValue = failsafeValue;
198        } else {
199            defaultPrefValue = defaultValue;
200        }
201        try {
202            setValue(defaultPrefValue);
203        } catch (IllegalArgumentException e) {
204            setValue(value);
205        }
206        text.setAllow('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'M', 'G', 'T', 'B');
207        text.setUppercase(true);
208        text.setToolTipText("A positive integer followed by unit, e.g. M, G, or T (no spaces).");
209        jrbSlider.setActionCommand("slider");
210        jrbSlider.addActionListener(this);
211        jrbNumber.setActionCommand("number");
212        jrbNumber.addActionListener(this);
213        sliderPanel.setEnabled(false);
214        textPanel.setEnabled(false);
215    }
216    
217    private void setState(final State newState) {
218        assert newState != null : newState;
219        currentState = newState;
220        text.setForeground(currentState.getForeground());
221        text.setBackground(currentState.getBackground());
222    }
223    
224    private boolean isValid() {
225        return currentState == State.VALID;
226    }
227    
228    private boolean isSlider() {
229        return sliderActive;
230    }
231    
232    public void actionPerformed(ActionEvent e) {
233        if ("slider".equals(e.getActionCommand())) {
234            sliderActive = true;
235            GuiUtils.enableTree(sliderPanel, true);
236            GuiUtils.enableTree(textPanel, false);
237            // Trigger the listener
238            int sliderValue = slider.getValue();
239            if (sliderValue == minSliderValue) {
240                slider.setValue(maxSliderValue);
241            } else {
242                slider.setValue(minSliderValue);
243            }
244            slider.setValue(sliderValue);
245        } else {
246            sliderActive = false;
247            GuiUtils.enableTree(sliderPanel, false);
248            GuiUtils.enableTree(textPanel, true);
249            // Trigger the listener
250            handleNewValue(text);
251        }
252    }
253    
254    private ChangeListener percentListener = evt -> {
255        if (sliderPanel.isEnabled()) {
256            int sliderValue = ((JSlider) evt.getSource()).getValue();
257            setValue(sliderValue + "P");
258            text.setText(String.valueOf(Math.round(sliderValue / 100.0 * maxmem)) + "MB");
259        }
260    };
261    
262    private void handleNewValue(final McVTextField field) {
263        
264        if (! textPanel.isEnabled()) {
265            return;
266        }
267        assert field != null;
268        
269        try {
270
271            String memWithSuffix = field.getText();
272            
273            if (memWithSuffix.isEmpty()) {
274                setState(State.ERROR);
275                return;
276            }
277            
278            if (!isValid()) {
279                setState(State.VALID);
280            }
281            
282            long newMemVal = -1;
283            // need to deal with both "G" and "GB" suffixes
284            int suffixLength = 1;
285            if (memWithSuffix.endsWith("MB") 
286                || memWithSuffix.endsWith("GB") 
287                || memWithSuffix.endsWith("TB")) 
288            {
289                suffixLength = 2;
290            }
291            String memWithoutSuffix = 
292                memWithSuffix.substring(0, memWithSuffix.length() - suffixLength);
293            
294            try {
295                newMemVal = Long.parseLong(memWithoutSuffix);
296            } catch (NumberFormatException nfe) {
297                // TJJ this should never happen, since validation is done live on keystrokes
298                // But if somebody ever changed the UI, better log an exception
299                logger.error("Memory value error:", nfe);
300            }
301            
302            if (memWithSuffix.endsWith("G") || memWithSuffix.endsWith("GB")) {
303                // megabytes per Gigabyte
304                newMemVal = newMemVal * Prefix.GIGA.getScale();
305            }
306            if (memWithSuffix.endsWith("T") || memWithSuffix.endsWith("TB")) {
307                // megabytes per Terabyte
308                newMemVal = newMemVal * Prefix.TERA.getScale();
309            }
310
311            if (newMemVal > maxmem) {
312                long memInGB = maxmem;
313                // Temporarily disable the text entry box, since Enter key in the modal
314                // dialog would just cycle back through the text field key handler and
315                // bring up a new dialog!
316                text.setEnabled(false);
317                JOptionPane.showMessageDialog(null, String.format(TOO_BIG_FMT, memInGB));
318                // Re-enable text field, user dismissed warning dialog
319                text.setEnabled(true);
320                setState(State.ERROR);
321            } else {
322                setValue(memWithSuffix);
323            }
324        } catch (IllegalArgumentException e) {
325            setState(State.ERROR);
326        }
327    }
328    
329    
330    public JPanel getComponent() {
331        JPanel topPanel = LayoutUtil.hbox(jrbSlider, getSliderComponent());
332        JPanel bottomPanel = LayoutUtil.hbox(jrbNumber, getTextComponent());
333        if (isSlider()) {
334            GuiUtils.enableTree(sliderPanel, true);
335            GuiUtils.enableTree(textPanel, false);
336        } else {
337            GuiUtils.enableTree(sliderPanel, false);
338            GuiUtils.enableTree(textPanel, true);
339        }
340        if (maxmem == 0) {
341            jrbSlider.setEnabled(false);
342        }
343        doneInit = true;
344        return McVGuiUtils.topBottom(topPanel, bottomPanel, null);
345    }
346    
347    public JComponent getSliderComponent() {
348        sliderLabel = new JLabel("Using " + initSliderValue + "% ");
349        String memoryString = maxmem + " MB";
350        if (maxmem == 0) {
351            memoryString = "Unknown";
352        }
353        JLabel postLabel = new JLabel(" of available memory (" + memoryString + ')');
354        JComponent[] sliderComps = GuiUtils.makeSliderPopup(minSliderValue, maxSliderValue+1, initSliderValue, percentListener);
355        slider = (JSlider) sliderComps[1];
356        slider.setMinorTickSpacing(5);
357        slider.setMajorTickSpacing(10);
358        slider.setSnapToTicks(true);
359        slider.setExtent(1);
360        slider.setPaintTicks(true);
361        slider.setPaintLabels(true);
362        sliderComps[0].setToolTipText("Set maximum memory by percent");
363        sliderPanel = LayoutUtil.hbox(sliderLabel, sliderComps[0], postLabel);
364        return sliderPanel;
365    }
366    
367    public JComponent getTextComponent() {
368
369        text.addKeyListener(new KeyAdapter() {
370            public void keyReleased(final KeyEvent e) {
371                handleNewValue(text);
372            }
373        });
374        
375        textPanel = LayoutUtil.hbox(new JPanel(), list(text), 0);
376        McVGuiUtils.setComponentWidth(text, McVGuiUtils.Width.ONEHALF);
377        return textPanel;
378    }
379    
380    public String toString() {
381        return MakeToString.fromInstance(this)
382                           .add("value", value)
383                           .add("currentPrefix", currentPrefix)
384                           .add("isSlider", isSlider()).toString();
385    }
386    
387    public String getValue() {
388        if (! isValid()) {
389            return defaultPrefValue;
390        }
391        return currentPrefix.getJavaFormat(value);
392    }
393    
394    // overridden so that any illegal vals coming *out of* a runMcV.prefs
395    // can be replaced with a legal val.
396    @Override public void fromPrefsFormat(final String prefText) {
397        try {
398            super.fromPrefsFormat(prefText);
399        } catch (IllegalArgumentException e) {
400            setValue(failsafeValue);
401        }
402    }
403
404    public void setValue(final String newValue) {
405        
406        Matcher m = MEMSTRING.matcher(newValue);
407        if (! m.matches()) {            
408            throw new IllegalArgumentException(String.format(BAD_MEM_FMT, newValue));
409        }
410        String quantity = m.group(1);
411        String prefix = m.group(2);
412        
413        // Fall back on failsafe value if user wants a percentage of an unknown maxmem
414        if ((maxmem == 0) && sliderActive) {
415            m = MEMSTRING.matcher(failsafeValue);
416            if (!m.matches()) {
417                throw new IllegalArgumentException(String.format(BAD_MEM_FMT, failsafeValue));
418            }
419            quantity = m.group(1);
420            prefix = m.group(2);
421        }
422        
423        int intVal = Integer.parseInt(quantity);
424        if (intVal <= 0) {
425            throw new IllegalArgumentException(String.format(LTE_ZERO_FMT, newValue));
426        }
427        if (prefix.isEmpty()) {
428            prefix = "M";
429        }
430        value = quantity;
431        
432        // TJJ Nov 2018 - if unit is P (Percentage), activate slider. 
433        // This also lets fresh install initialize correctly
434        if (prefix.equals("P")) {
435            sliderActive = true;
436        } else {
437            sliderActive = false;
438        }
439        
440        if (sliderActive) {
441            
442            // Work around all the default settings going on
443            initSliderValue = Integer.parseInt(value);
444            initTextValue = String.valueOf((int) Math.round(initSliderValue * maxmem / 100.0));
445            
446            sliderLabel.setText(String.format(SLIDER_LABEL_FMT, value) + "% ");
447            if (maxmem > 0) {
448                text.setText(initTextValue + "MB");
449            }
450            if (! doneInit) {
451                jrbSlider.setSelected(true);
452            }
453            currentPrefix = MemoryOption.Prefix.PERCENT;
454            return;
455        }
456        
457        for (Prefix tmp : MemoryOption.PREFIXES) {
458            String newPrefix = prefix;
459            if (prefix.length() > 1) newPrefix = prefix.substring(0, 1);
460            if (newPrefix.toUpperCase().equals(tmp.getJavaChar())) {
461                currentPrefix = tmp;
462                
463                // Work around all the default settings going on
464                initSliderValue = minSliderValue;
465                initTextValue = value;
466                
467                if (maxmem > 0) {
468                    initSliderValue = (int) Math.round(Integer.parseInt(value) * 100.0 * currentPrefix.getScale() / maxmem);
469                    boolean aboveMin = true;
470                    boolean aboveMax = false;
471                    if (initSliderValue < 10) aboveMin = false;
472                    if (initSliderValue > 80) aboveMax = true;
473                    initSliderValue = Math.max(Math.min(initSliderValue, maxSliderValue), minSliderValue);
474                    slider.setValue(initSliderValue);
475                    if (aboveMin) {
476                        if (aboveMax) {
477                            sliderLabel.setText(String.format(SLIDER_GREATER_THAN_MAX_LABEL_FMT, initSliderValue) + "% ");
478                        } else {
479                            sliderLabel.setText(String.format(SLIDER_LABEL_FMT, initSliderValue) + "% ");
480                        }
481                    } else {
482                        sliderLabel.setText(String.format(SLIDER_LESS_THAN_MIN_LABEL_FMT, initSliderValue) + "% ");
483                    }
484                }
485                if (! doneInit) {
486                    jrbNumber.setSelected(true);
487                }
488                text.setText(newValue);
489                return;
490            }
491        }
492        throw new IllegalArgumentException(String.format(NO_MEM_PREFIX_FMT, prefix, newValue));
493    }
494    
495    private static long getSystemMemory() {
496        String val = SystemState.queryOpSysProps().get("opsys.memory.physical.total");
497        if (Objects.equals(System.getProperty("os.name"), "Windows XP")) {
498            return 1536 * (1024 * 1024);
499        }
500        return Long.parseLong(val);
501    }
502}