001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
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 http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import java.awt.Color;
032import java.awt.event.ActionEvent;
033import java.awt.event.ActionListener;
034import java.awt.event.KeyAdapter;
035import java.awt.event.KeyEvent;
036import java.util.regex.Matcher;
037import java.util.regex.Pattern;
038
039import javax.swing.ButtonGroup;
040import javax.swing.JComboBox;
041import javax.swing.JComponent;
042import javax.swing.JLabel;
043import javax.swing.JPanel;
044import javax.swing.JRadioButton;
045import javax.swing.JSlider;
046import javax.swing.JTextField;
047import javax.swing.event.ChangeEvent;
048import javax.swing.event.ChangeListener;
049
050import ucar.unidata.util.GuiUtils;
051
052import edu.wisc.ssec.mcidasv.util.McVGuiUtils;
053import edu.wisc.ssec.mcidasv.util.McVTextField;
054import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
055import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.OptionPlatform;
056import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Type;
057import edu.wisc.ssec.mcidasv.startupmanager.options.OptionMaster.Visibility;
058
059public class MemoryOption extends AbstractOption implements ActionListener {
060    public enum Prefix {
061        MEGA("M", "megabytes"),
062        GIGA("G", "gigabytes"),
063        TERA("T", "terabytes"),
064        PERCENT("P", "percent");
065        
066        private final String javaChar;
067        private final String name;
068        
069        private Prefix(final String javaChar, final String name) {
070            this.javaChar = javaChar;
071            this.name = name;
072        }
073        
074        public String getJavaChar() {
075            return javaChar.toUpperCase();
076        }
077        
078        public String getName() {
079            return name;
080        }
081        
082        public String getJavaFormat(final String value) {
083            long longVal = Long.parseLong(value);
084            return longVal + javaChar;
085        }
086        
087        @Override public String toString() {
088            return name;
089        }
090    }
091    
092    private enum State { 
093        VALID(Color.BLACK, Color.WHITE),
094        WARN(Color.BLACK, new Color(255, 255, 204)),
095        ERROR(Color.WHITE, Color.PINK);
096        
097        private final Color foreground;
098        
099        private final Color background;
100        
101        private State(final Color foreground, final Color background) {
102            this.foreground = foreground;
103            this.background = background;
104        }
105        
106        public Color getForeground() { 
107            return foreground; 
108        }
109        
110        public Color getBackground() { 
111            return background; 
112        }
113    }
114    
115    private final static Prefix[] PREFIXES = { Prefix.MEGA, Prefix.GIGA, Prefix.TERA };
116    
117    private Prefix currentPrefix = Prefix.MEGA;
118    
119    private static final Pattern MEMSTRING = 
120        Pattern.compile("^(\\d+)([M|G|T|P]?)$", Pattern.CASE_INSENSITIVE);
121    
122    private final String defaultPrefValue;
123    
124    private String failsafeValue = "512M";
125    
126    private String value = failsafeValue; // bootstrap
127    
128    private JRadioButton jrbSlider = new JRadioButton();
129    
130    private JRadioButton jrbNumber = new JRadioButton();
131    
132    private ButtonGroup jtbBg = GuiUtils.buttonGroup(jrbSlider, jrbNumber);
133    
134    private JPanel sliderPanel = new JPanel();
135    
136    private JLabel sliderLabel = new JLabel();
137    
138    private JSlider slider = new JSlider();
139    
140    private JPanel textPanel = new JPanel();
141    private McVTextField text = new McVTextField();
142    private JComboBox memVals = new JComboBox(PREFIXES);
143    private String initTextValue = value;
144    private Prefix initPrefixValue = currentPrefix;
145    
146    private int minSliderValue = 10;
147    private int maxSliderValue = 80;
148    private int initSliderValue = minSliderValue;
149    
150    private int maxmem = StartupManager.getMaximumHeapSize();
151    
152    private State currentState = State.VALID;
153    
154    private boolean doneInit = false;
155    
156    public MemoryOption(final String id, final String label, 
157        final String defaultValue, final OptionPlatform optionPlatform,
158        final Visibility optionVisibility) 
159    {
160        super(id, label, Type.MEMORY, optionPlatform, optionVisibility);
161        if (maxmem == 0) {
162            defaultPrefValue = failsafeValue;
163        } else {
164            defaultPrefValue = defaultValue;
165        }
166        try {
167            setValue(defaultPrefValue);
168        } catch (IllegalArgumentException e) {
169            setValue(value);
170        }
171        text.setAllow('0', '1', '2', '3', '4', '5', '6', '7', '8', '9');
172        jrbSlider.setActionCommand("slider");
173        jrbSlider.addActionListener(this);
174        jrbNumber.setActionCommand("number");
175        jrbNumber.addActionListener(this);
176        sliderPanel.setEnabled(false);
177        textPanel.setEnabled(false);
178    }
179    
180    private String[] getNames(final Prefix[] arr) {
181        assert arr != null;
182        String[] newArr = new String[arr.length];
183        for (int i = 0; i < arr.length; i++) {
184            newArr[i] = arr[i].getName();
185        }
186        return newArr;
187    }
188    
189    private void setState(final State newState) {
190        assert newState != null : newState;
191        currentState = newState;
192        text.setForeground(currentState.getForeground());
193        text.setBackground(currentState.getBackground());
194    }
195    
196    private boolean isValid() {
197        return currentState == State.VALID;
198    }
199    
200    private boolean isSlider() {
201        return currentPrefix.equals(Prefix.PERCENT);
202    }
203    
204    public void actionPerformed(ActionEvent e) {
205        if ("slider".equals(e.getActionCommand())) {
206            GuiUtils.enableTree(sliderPanel, true);
207            GuiUtils.enableTree(textPanel, false);
208            // Trigger the listener
209            int sliderValue = slider.getValue();
210            if (sliderValue==minSliderValue) {
211                slider.setValue(maxSliderValue);
212            } else {
213                slider.setValue(minSliderValue);
214            }
215            slider.setValue(sliderValue);
216        } else {
217            GuiUtils.enableTree(sliderPanel, false);
218            GuiUtils.enableTree(textPanel, true);
219            // Trigger the listener
220            handleNewValue(text, memVals);
221        }
222    }
223    
224    public ChangeListener percentListener = evt -> {
225        if (!sliderPanel.isEnabled()) {
226            return;
227        }
228        int sliderValue = ((JSlider)evt.getSource()).getValue();
229        setValue(sliderValue + Prefix.PERCENT.getJavaChar());
230        text.setText("" + Math.round(sliderValue / 100.0 * maxmem));
231    };
232    
233    private void handleNewValue(final JTextField field, final JComboBox box) {
234        if (!textPanel.isEnabled()) return;
235        assert field != null;
236        assert box != null;
237        
238        try {
239            String newValue = field.getText();
240            String huh = ((Prefix)box.getSelectedItem()).getJavaFormat(newValue);
241            
242            if (!isValid()) {
243                setState(State.VALID);
244            }
245            setValue(huh);
246        } catch (IllegalArgumentException e) {
247            setState(State.ERROR);
248            text.setToolTipText("This value must be an integer greater than zero.");
249        }
250    }
251    
252    public JPanel getComponent() {
253        JPanel topPanel = GuiUtils.hbox(jrbSlider, getSliderComponent());
254        JPanel bottomPanel = GuiUtils.hbox(jrbNumber, getTextComponent());
255        if (isSlider()) {
256            GuiUtils.enableTree(sliderPanel, true);
257            GuiUtils.enableTree(textPanel, false);
258        } else {
259            GuiUtils.enableTree(sliderPanel, false);
260            GuiUtils.enableTree(textPanel, true);
261        }
262        if (maxmem == 0) {
263            jrbSlider.setEnabled(false);
264        }
265        doneInit = true;
266        return McVGuiUtils.topBottom(topPanel, bottomPanel, null);
267    }
268    
269    public JComponent getSliderComponent() {
270        sliderLabel = new JLabel("Use " + initSliderValue + "% ");
271        String memoryString = maxmem + "mb";
272        if (maxmem == 0) {
273            memoryString="Unknown";
274        }
275        JLabel postLabel = new JLabel(" of available memory (" + memoryString + ")");
276        JComponent[] sliderComps = GuiUtils.makeSliderPopup(minSliderValue, maxSliderValue+1, initSliderValue, percentListener);
277        slider = (JSlider) sliderComps[1];
278        slider.setMinorTickSpacing(5);
279        slider.setMajorTickSpacing(10);
280        slider.setSnapToTicks(true);
281        slider.setExtent(1);
282        slider.setPaintTicks(true);
283        slider.setPaintLabels(true);
284        sliderComps[0].setToolTipText("Set maximum memory by percent");
285        sliderPanel = GuiUtils.hbox(sliderLabel, sliderComps[0], postLabel);
286        return sliderPanel;
287    }
288    
289    public JComponent getTextComponent() {
290        text.setText(initTextValue);
291        text.addKeyListener(new KeyAdapter() {
292            public void keyReleased(final KeyEvent e) {
293                handleNewValue(text, memVals);
294            }
295        });
296        memVals.setSelectedItem(initPrefixValue);
297        memVals.addActionListener(e -> handleNewValue(text, memVals));
298        McVGuiUtils.setComponentWidth(text, McVGuiUtils.Width.ONEHALF);
299        McVGuiUtils.setComponentWidth(memVals, McVGuiUtils.Width.ONEHALF);
300        textPanel = GuiUtils.hbox(text, memVals);
301        return textPanel;
302    }
303    
304    public String toString() {
305        return String.format(
306            "[MemoryOption@%x: value=%s, currentPrefix=%s, isSlider=%s]", 
307            hashCode(), value, currentPrefix, isSlider());
308    }
309    
310    public String getValue() {
311        if (!isValid()) {
312            return defaultPrefValue;
313        }
314        return currentPrefix.getJavaFormat(value);
315    }
316    
317    // overridden so that any illegal vals coming *out of* a runMcV.prefs
318    // can be replaced with a legal val.
319    @Override public void fromPrefsFormat(final String prefText) {
320        try {
321            super.fromPrefsFormat(prefText);
322        } catch (IllegalArgumentException e) {
323            setValue(failsafeValue);
324        }
325    }
326    
327    public void setValue(final String newValue) {
328        Matcher m = MEMSTRING.matcher(newValue);
329        if (!m.matches()) {
330            throw new IllegalArgumentException("Badly formatted memory string: "+newValue);
331        }
332        String quantity = m.group(1);
333        String prefix = m.group(2);
334        
335        // Fall back on failsafe value if user wants a percentage of an unknown maxmem
336        if (maxmem==0 && prefix.toUpperCase().equals(Prefix.PERCENT.getJavaChar())) {
337            m = MEMSTRING.matcher(failsafeValue);
338            if (!m.matches()) {
339                throw new IllegalArgumentException("Badly formatted memory string: "+failsafeValue);
340            }
341            quantity = m.group(1);
342            prefix = m.group(2);
343        }
344        
345        int intVal = Integer.parseInt(quantity);
346        if (intVal <= 0) {
347            throw new IllegalArgumentException("Memory cannot be less than or equal to zero: "+newValue);
348        }
349        if (prefix.isEmpty()) {
350            prefix = "M";
351        }
352        value = quantity;
353        
354        if (prefix.toUpperCase().equals(Prefix.PERCENT.getJavaChar())) {
355            currentPrefix = Prefix.PERCENT;
356            
357            // Work around all the default settings going on
358            initSliderValue = Integer.parseInt(value);
359            initPrefixValue = Prefix.MEGA;
360            initTextValue = "" + (int)Math.round(initSliderValue * maxmem / 100.0);
361            
362            sliderLabel.setText("Use " + value + "% ");
363            if (maxmem > 0) {
364                memVals.setSelectedItem(initPrefixValue);
365                text.setText(initTextValue);
366            }
367            if (!doneInit) {
368                jrbSlider.setSelected(true);
369            }
370            return;
371        }
372        
373        for (Prefix tmp : PREFIXES) {
374            if (prefix.toUpperCase().equals(tmp.getJavaChar())) {
375                currentPrefix = tmp;
376                
377                // Work around all the default settings going on
378                initSliderValue = minSliderValue;
379                initPrefixValue = currentPrefix;
380                initTextValue = value;
381                
382                if (maxmem>0) {
383                    int multiplier = 1;
384                    if (currentPrefix.equals(Prefix.GIGA)) multiplier=1024;
385                    else if (currentPrefix.equals(Prefix.TERA)) multiplier=1024 * 1024;
386                    initSliderValue = (int)Math.round(Integer.parseInt(value) * 100.0 * multiplier / maxmem);
387                    initSliderValue = Math.max(Math.min(initSliderValue, maxSliderValue), minSliderValue);
388                    slider.setValue(initSliderValue);
389                    sliderLabel.setText("Use "+initSliderValue+"% ");
390                }
391                if (!doneInit) jrbNumber.setSelected(true);
392                return;
393            }
394        }
395        
396        throw new IllegalArgumentException("Could not find matching memory prefix for \""+prefix+"\" in string: "+newValue);
397    }
398}