001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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 = new ChangeListener() {
225        public void stateChanged(ChangeEvent evt) {
226            if (!sliderPanel.isEnabled()) {
227                return;
228            }
229            int sliderValue = ((JSlider)evt.getSource()).getValue();
230            setValue(sliderValue + Prefix.PERCENT.getJavaChar());
231            text.setText("" + Math.round(sliderValue / 100.0 * maxmem));
232        }
233    };
234    
235    private void handleNewValue(final JTextField field, final JComboBox box) {
236        if (!textPanel.isEnabled()) return;
237        assert field != null;
238        assert box != null;
239        
240        try {
241            String newValue = field.getText();
242            String huh = ((Prefix)box.getSelectedItem()).getJavaFormat(newValue);
243            
244            if (!isValid()) {
245                setState(State.VALID);
246            }
247            setValue(huh);
248        } catch (IllegalArgumentException e) {
249            setState(State.ERROR);
250            text.setToolTipText("This value must be an integer greater than zero.");
251        }
252    }
253    
254    public JPanel getComponent() {
255        JPanel topPanel = GuiUtils.hbox(jrbSlider, getSliderComponent());
256        JPanel bottomPanel = GuiUtils.hbox(jrbNumber, getTextComponent());
257        if (isSlider()) {
258            GuiUtils.enableTree(sliderPanel, true);
259            GuiUtils.enableTree(textPanel, false);
260        } else {
261            GuiUtils.enableTree(sliderPanel, false);
262            GuiUtils.enableTree(textPanel, true);
263        }
264        if (maxmem == 0) {
265            jrbSlider.setEnabled(false);
266        }
267        doneInit = true;
268        return McVGuiUtils.topBottom(topPanel, bottomPanel, null);
269    }
270    
271    public JComponent getSliderComponent() {
272        sliderLabel = new JLabel("Use " + initSliderValue + "% ");
273        String memoryString = maxmem + "mb";
274        if (maxmem == 0) {
275            memoryString="Unknown";
276        }
277        JLabel postLabel = new JLabel(" of available memory (" + memoryString + ")");
278        JComponent[] sliderComps = GuiUtils.makeSliderPopup(minSliderValue, maxSliderValue+1, initSliderValue, percentListener);
279        slider = (JSlider) sliderComps[1];
280        slider.setMinorTickSpacing(5);
281        slider.setMajorTickSpacing(10);
282        slider.setSnapToTicks(true);
283        slider.setExtent(1);
284        slider.setPaintTicks(true);
285        slider.setPaintLabels(true);
286        sliderComps[0].setToolTipText("Set maximum memory by percent");
287        sliderPanel = GuiUtils.hbox(sliderLabel, sliderComps[0], postLabel);
288        return sliderPanel;
289    }
290    
291    public JComponent getTextComponent() {
292        text.setText(initTextValue);
293        text.addKeyListener(new KeyAdapter() {
294            public void keyReleased(final KeyEvent e) {
295                handleNewValue(text, memVals);
296            }
297        });
298        memVals.setSelectedItem(initPrefixValue);
299        memVals.addActionListener(new ActionListener() {
300            public void actionPerformed(final ActionEvent e) {
301                handleNewValue(text, memVals);
302            }
303        });
304        McVGuiUtils.setComponentWidth(text, McVGuiUtils.Width.ONEHALF);
305        McVGuiUtils.setComponentWidth(memVals, McVGuiUtils.Width.ONEHALF);
306        textPanel = GuiUtils.hbox(text, memVals);
307        return textPanel;
308    }
309    
310    public String toString() {
311        return String.format(
312            "[MemoryOption@%x: value=%s, currentPrefix=%s, isSlider=%s]", 
313            hashCode(), value, currentPrefix, isSlider());
314    }
315    
316    public String getValue() {
317        if (!isValid()) {
318            return defaultPrefValue;
319        }
320        return currentPrefix.getJavaFormat(value);
321    }
322    
323    // overridden so that any illegal vals coming *out of* a runMcV.prefs
324    // can be replaced with a legal val.
325    @Override public void fromPrefsFormat(final String prefText) {
326        try {
327            super.fromPrefsFormat(prefText);
328        } catch (IllegalArgumentException e) {
329            setValue(failsafeValue);
330        }
331    }
332    
333    public void setValue(final String newValue) {
334        Matcher m = MEMSTRING.matcher(newValue);
335        if (!m.matches()) {
336            throw new IllegalArgumentException("Badly formatted memory string: "+newValue);
337        }
338        String quantity = m.group(1);
339        String prefix = m.group(2);
340        
341        // Fall back on failsafe value if user wants a percentage of an unknown maxmem
342        if (maxmem==0 && prefix.toUpperCase().equals(Prefix.PERCENT.getJavaChar())) {
343            m = MEMSTRING.matcher(failsafeValue);
344            if (!m.matches()) {
345                throw new IllegalArgumentException("Badly formatted memory string: "+failsafeValue);
346            }
347            quantity = m.group(1);
348            prefix = m.group(2);
349        }
350        
351        int intVal = Integer.parseInt(quantity);
352        if (intVal <= 0) {
353            throw new IllegalArgumentException("Memory cannot be less than or equal to zero: "+newValue);
354        }
355        if (prefix.isEmpty()) {
356            prefix = "M";
357        }
358        value = quantity;
359        
360        if (prefix.toUpperCase().equals(Prefix.PERCENT.getJavaChar())) {
361            currentPrefix = Prefix.PERCENT;
362            
363            // Work around all the default settings going on
364            initSliderValue = Integer.parseInt(value);
365            initPrefixValue = Prefix.MEGA;
366            initTextValue = "" + (int)Math.round(initSliderValue * maxmem / 100.0);
367            
368            sliderLabel.setText("Use " + value + "% ");
369            if (maxmem > 0) {
370                memVals.setSelectedItem(initPrefixValue);
371                text.setText(initTextValue);
372            }
373            if (!doneInit) {
374                jrbSlider.setSelected(true);
375            }
376            return;
377        }
378        
379        for (Prefix tmp : PREFIXES) {
380            if (prefix.toUpperCase().equals(tmp.getJavaChar())) {
381                currentPrefix = tmp;
382                
383                // Work around all the default settings going on
384                initSliderValue = minSliderValue;
385                initPrefixValue = currentPrefix;
386                initTextValue = value;
387                
388                if (maxmem>0) {
389                    int multiplier = 1;
390                    if (currentPrefix.equals(Prefix.GIGA)) multiplier=1024;
391                    else if (currentPrefix.equals(Prefix.TERA)) multiplier=1024 * 1024;
392                    initSliderValue = (int)Math.round(Integer.parseInt(value) * 100.0 * multiplier / maxmem);
393                    initSliderValue = Math.max(Math.min(initSliderValue, maxSliderValue), minSliderValue);
394                    slider.setValue(initSliderValue);
395                    sliderLabel.setText("Use "+initSliderValue+"% ");
396                }
397                if (!doneInit) jrbNumber.setSelected(true);
398                return;
399            }
400        }
401        
402        throw new IllegalArgumentException("Could not find matching memory prefix for \""+prefix+"\" in string: "+newValue);
403    }
404}