001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2025 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}