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