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 java.awt.*; 032import java.awt.geom.AffineTransform; 033import java.io.BufferedReader; 034import java.io.BufferedWriter; 035import java.io.File; 036import java.io.FileNotFoundException; 037import java.io.FileReader; 038import java.io.FileWriter; 039import java.io.IOException; 040import java.util.ArrayList; 041import java.util.Collection; 042import java.util.Collections; 043import java.util.HashMap; 044import java.util.List; 045import java.util.Map; 046import java.util.stream.Collectors; 047 048import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 049import edu.wisc.ssec.mcidasv.startupmanager.Platform; 050import org.slf4j.Logger; 051import org.slf4j.LoggerFactory; 052 053public class OptionMaster { 054 055 private static final Logger logger = 056 LoggerFactory.getLogger(OptionMaster.class); 057 058 public final static String SET_PREFIX = "SET "; 059 public final static String EMPTY_STRING = ""; 060 public final static String QUOTE_STRING = "\""; 061 public final static char QUOTE_CHAR = '"'; 062 public final static String DEF_SCALING = "1"; 063 064 // TODO(jon): write CollectionHelpers.zip() and CollectionHelpers.zipWith() 065 public final Object[][] blahblah = { 066 // Default memory initial setting is 80% of system memory 067 { "HEAP_SIZE", "Memory", "80P", Type.MEMORY, OptionPlatform.ALL, Visibility.VISIBLE }, 068 { "JOGL_TOGL", "Enable JOGL", "1", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE }, 069 { "USE_3DSTUFF", "Enable 3D controls", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 070 { "DEFAULT_LAYOUT", "Load default layout", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 071 { "STARTUP_BUNDLE", "Defaults", "0;", Type.FILE, OptionPlatform.ALL, Visibility.VISIBLE }, 072 // mcidasv enables this (the actual property is "visad.java3d.geometryByRef") 073 // by default in mcidasv.properties. 074 { "USE_GEOBYREF", "Enable access to geometry by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 075 { "USE_IMAGEBYREF", "Enable access to image data by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 076 { "USE_NPOT", "Enable Non-Power of Two (NPOT) textures", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 077 // USE_CMSGC is no longer in use, so the visibility is "HIDDEN". 078 // If we remove the option entirely, existing users with USE_CMSGC will may see 079 // a warning message. 080 { "USE_CMSGC", "Enable concurrent mark-sweep garbage collector", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.HIDDEN }, 081 { "LOG_LEVEL", "Log Level", "INFO", Type.LOGLEVEL, OptionPlatform.ALL, Visibility.VISIBLE }, 082 { "JVM_OPTIONS", "Java Virtual Machine Options", "", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE }, 083 { "TEXTURE_WIDTH", "Texture Size", "4096", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE }, 084 { "MCV_SCALING", "GUI Scaling", DEF_SCALING, Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE }, 085 { "USE_DARK_MODE", "Enable Dark Mode", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE }, 086 }; 087 088 /** 089 * {@link Option}s can be either platform-specific or applicable to all 090 * platforms. Options that are platform-specific still appear in the 091 * UI, but their component is not enabled. 092 */ 093 public enum OptionPlatform { ALL, UNIXLIKE, WINDOWS, MAC }; 094 095 /** 096 * The different types of {@link Option}s. 097 * 098 * @see TextOption 099 * @see BooleanOption 100 * @see MemoryOption 101 * @see DirectoryOption 102 * @see SliderOption 103 * @see LoggerLevelOption 104 * @see FileOption 105 */ 106 public enum Type { TEXT, BOOLEAN, MEMORY, DIRTREE, SLIDER, LOGLEVEL, FILE }; 107 108 /** 109 * Different ways that an {@link Option} might be displayed. 110 */ 111 public enum Visibility { VISIBLE, HIDDEN }; 112 113 /** Maps an option ID to the corresponding object. */ 114 private Map<String, ? extends Option> optionMap; 115 116 private static OptionMaster instance; 117 118 public OptionMaster() { 119 normalizeUserDirectory(); 120 optionMap = buildOptions(blahblah); 121// readStartup(); 122 } 123 124 public static OptionMaster getInstance() { 125 if (instance == null) { 126 instance = new OptionMaster(); 127 } 128 return instance; 129 } 130 131 // McIDAS Inquiry #3124-3141 and #3125-3141 -> Unused but it might be useful some time in the future 132 // HiDPI, Retina Display, MacOS 133 public static boolean hasRetinaDisplay() { 134 logger.info("Testing for HiDPI"); 135 final GraphicsConfiguration config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); 136 final AffineTransform transform = config.getDefaultTransform(); 137 return !transform.isIdentity(); 138 } 139 140 /** 141 * Creates the specified options and returns a mapping of the option ID 142 * to the actual {@link Option} object. 143 * 144 * @param options An array specifying the {@code Option}s to be built. 145 * 146 * @return Mapping of ID to {@code Option}. 147 * 148 * @throws AssertionError if the option array contained an entry that 149 * this method cannot build. 150 */ 151 private Map<String, Option> buildOptions(final Object[][] options) { 152 // TODO(jon): seriously, get that zip stuff working! this array 153 // stuff is BAD. 154 Map<String, Option> optMap = new HashMap<>(options.length); 155 156 for (Object[] arrayOption : options) { 157 String id = (String)arrayOption[0]; 158 String label = (String)arrayOption[1]; 159 String defaultValue = (String)arrayOption[2]; 160 Type type = (Type)arrayOption[3]; 161 OptionPlatform platform = (OptionPlatform)arrayOption[4]; 162 Visibility visibility = (Visibility)arrayOption[5]; 163 164 switch (type) { 165 case TEXT: 166 optMap.put(id, new TextOption(id, label, defaultValue, platform, visibility)); 167 break; 168 case BOOLEAN: 169 optMap.put(id, new BooleanOption(id, label, defaultValue, platform, visibility)); 170 break; 171 case MEMORY: 172 optMap.put(id, new MemoryOption(id, label, defaultValue, platform, visibility)); 173 break; 174 case DIRTREE: 175 optMap.put(id, new DirectoryOption(id, label, defaultValue, platform, visibility)); 176 break; 177 case SLIDER: 178 optMap.put(id, new SliderOption(id, label, defaultValue, platform, visibility)); 179 break; 180 case LOGLEVEL: 181 optMap.put(id, new LoggerLevelOption(id, label, defaultValue, platform, visibility)); 182 break; 183 case FILE: 184 optMap.put(id, new FileOption(id, label, defaultValue, platform, visibility)); 185 break; 186 default: 187 throw new AssertionError(type + 188 " is not known to OptionMaster.buildOptions()"); 189 } 190 } 191 return optMap; 192 } 193 194 /** 195 * Converts a {@link Platform} to its corresponding 196 * {@link OptionPlatform} type. 197 * 198 * @return The current platform as a {@code OptionPlatform} type. 199 * 200 * @throws AssertionError if {@link StartupManager#getPlatform()} 201 * returned something that this method cannot convert. 202 */ 203 // a lame-o hack :( 204 protected OptionPlatform convertToOptionPlatform() { 205 Platform platform = StartupManager.getInstance().getPlatform(); 206 switch (platform) { 207 case WINDOWS: 208 return OptionPlatform.WINDOWS; 209 case MAC: 210 return OptionPlatform.MAC; 211 case UNIXLIKE: 212 return OptionPlatform.UNIXLIKE; 213 default: 214 throw new AssertionError("Unknown platform: " + platform); 215 } 216 } 217 218 /** 219 * Returns the {@link Option} mapped to {@code id}. 220 * 221 * @param id The ID whose associated {@code Option} is to be returned. 222 * 223 * @return Either the {@code Option} associated with {@code id}, or 224 * {@code null} if there was no association. 225 * 226 * 227 * @see #getMemoryOption 228 * @see #getBooleanOption 229 * @see #getDirectoryOption 230 * @see #getSliderOption 231 * @see #getTextOption 232 * @see #getLoggerLevelOption 233 * @see #getFileOption 234 */ 235 private Option getOption(final String id) { 236 return optionMap.get(id); 237 } 238 239 /** 240 * Searches {@link #optionMap} for the {@link MemoryOption} that 241 * corresponds with the given {@code id}. 242 * 243 * @param id Identifier for the desired {@code MemoryOption}. 244 * Should not be {@code null}. 245 * 246 * @return Either the {@code MemoryOption} that corresponds to {@code id} 247 * or {@code null}. 248 */ 249 public MemoryOption getMemoryOption(final String id) { 250 return (MemoryOption)optionMap.get(id); 251 } 252 253 /** 254 * Searches {@link #optionMap} for the {@link BooleanOption} that 255 * corresponds with the given {@code id}. 256 * 257 * @param id Identifier for the desired {@code BooleanOption}. 258 * Should not be {@code null}. 259 * 260 * @return Either the {@code BooleanOption} that corresponds to {@code id} 261 * or {@code null}. 262 */ 263 public BooleanOption getBooleanOption(final String id) { 264 return (BooleanOption)optionMap.get(id); 265 } 266 267 /** 268 * Searches {@link #optionMap} for the {@link DirectoryOption} that 269 * corresponds with the given {@code id}. 270 * 271 * @param id Identifier for the desired {@code DirectoryOption}. 272 * Should not be {@code null}. 273 * 274 * @return Either the {@code DirectoryOption} that corresponds to 275 * {@code id} or {@code null}. 276 */ 277 public DirectoryOption getDirectoryOption(final String id) { 278 return (DirectoryOption)optionMap.get(id); 279 } 280 281 /** 282 * Searches {@link #optionMap} for the {@link SliderOption} that 283 * corresponds with the given {@code id}. 284 * 285 * @param id Identifier for the desired {@code SliderOption}. 286 * Should not be {@code null}. 287 * 288 * @return Either the {@code SliderOption} that corresponds to {@code id} 289 * or {@code null}. 290 */ 291 public SliderOption getSliderOption(final String id) { 292 return (SliderOption)optionMap.get(id); 293 } 294 295 /** 296 * Searches {@link #optionMap} for the {@link TextOption} that 297 * corresponds with the given {@code id}. 298 * 299 * @param id Identifier for the desired {@code TextOption}. 300 * Should not be {@code null}. 301 * 302 * @return Either the {@code TextOption} that corresponds to {@code id} 303 * or {@code null}. 304 */ 305 public TextOption getTextOption(final String id) { 306 return (TextOption)optionMap.get(id); 307 } 308 309 /** 310 * Searches {@link #optionMap} for the {@link LoggerLevelOption} that 311 * corresponds with the given {@code id}. 312 * 313 * @param id Identifier for the desired {@code LoggerLevelOption}. 314 * Should not be {@code null}. 315 * 316 * @return Either the {@code LoggerLevelOption} that corresponds to {@code id} 317 * or {@code null}. 318 */ 319 public LoggerLevelOption getLoggerLevelOption(final String id) { 320 return (LoggerLevelOption)optionMap.get(id); 321 } 322 323 public FileOption getFileOption(final String id) { 324 return (FileOption)optionMap.get(id); 325 } 326 327 // TODO(jon): getAllOptions and optionsBy* really need some work. 328 // I want to eventually do something like: 329 // Collection<Option> = getOpts().byPlatform(WINDOWS, ALL).byType(BOOLEAN).byVis(HIDDEN) 330 /** 331 * Returns all the available startup manager options. 332 * 333 * @return Either all available startup manager options or an empty 334 * {@link Collection}. 335 */ 336 public Collection<Option> getAllOptions() { 337 return Collections.unmodifiableCollection(optionMap.values()); 338 } 339 340 /** 341 * Returns the {@link Option Options} applicable to the given 342 * {@link OptionPlatform OptionPlatforms}. 343 * 344 * @param platforms Desired platforms. Cannot be {@code null}. 345 * 346 * @return Either a {@link List} of {code Option}-s applicable to 347 * {@code platforms} or an empty {@code List}. 348 */ 349 public List<Option> optionsByPlatform( 350 final Collection<OptionPlatform> platforms) 351 { 352 if (platforms == null) { 353 throw new NullPointerException("must specify platforms"); 354 } 355 Collection<Option> allOptions = getAllOptions(); 356 List<Option> filteredOptions = 357 new ArrayList<>(allOptions.size()); 358 359 filteredOptions.addAll( 360 allOptions.stream() 361 .filter(option -> 362 platforms.contains(option.getOptionPlatform())) 363 .collect(Collectors.toList())); 364 365 return filteredOptions; 366 } 367 368 /** 369 * Returns the {@link Option Options} that match the given 370 * {@link Type Types}. 371 * 372 * @param types Desired {@code Option} types. Cannot be {@code null}. 373 * 374 * @return Either the {@code List} of {@code Option}-s that match the given 375 * types or an empty {@code List}. 376 */ 377 public List<Option> optionsByType(final Collection<Type> types) { 378 if (types == null) { 379 throw new NullPointerException("must specify types"); 380 } 381 Collection<Option> allOptions = getAllOptions(); 382 List<Option> filteredOptions = 383 new ArrayList<>(allOptions.size()); 384 filteredOptions.addAll( 385 allOptions.stream() 386 .filter(option -> types.contains(option.getOptionType())) 387 .collect(Collectors.toList())); 388 return filteredOptions; 389 } 390 391 /** 392 * Returns the {@link Option Options} that match the given levels of 393 * {@link Visibility visibility}. 394 * 395 * @param visibilities Desired visibility levels. Cannot be {@code null}. 396 * 397 * @return Either the {@code List} of {@code Option}-s that match the given 398 * visibility levels or an empty {@code List}. 399 */ 400 public List<Option> optionsByVisibility( 401 final Collection<Visibility> visibilities) 402 { 403 if (visibilities == null) { 404 throw new NullPointerException("must specify visibilities"); 405 } 406 Collection<Option> allOptions = getAllOptions(); 407 List<Option> filteredOptions = 408 new ArrayList<>(allOptions.size()); 409 filteredOptions.addAll( 410 allOptions.stream() 411 .filter(option -> 412 visibilities.contains(option.getOptionVisibility())) 413 .collect(Collectors.toList())); 414 return filteredOptions; 415 } 416 417 public void normalizeUserDirectory() { 418 StartupManager startup = StartupManager.getInstance(); 419 Platform platform = startup.getPlatform(); 420 File dir = new File(platform.getUserDirectory()); 421 File prefs = new File(platform.getUserPrefs()); 422 423 if (!dir.exists()) { 424 dir.mkdir(); 425 } 426 if (!prefs.exists()) { 427 try { 428 File defaultPrefs = new File(platform.getDefaultPrefs()); 429 startup.copy(defaultPrefs, prefs); 430 } catch (IOException e) { 431 System.err.println("Non-fatal error copying user preference template: "+e.getMessage()); 432 } 433 } 434 } 435 436 public void readStartup() { 437 File script = 438 new File(StartupManager.getInstance().getPlatform().getUserPrefs()); 439 try (BufferedReader br = new BufferedReader(new FileReader(script))) { 440 String line; 441 while ((line = br.readLine()) != null) { 442 if (line.startsWith("#")) { 443 continue; 444 } 445 int splitAt = line.indexOf('='); 446 if (splitAt >= 0) { 447 String id = line.substring(0, splitAt).replace(SET_PREFIX, EMPTY_STRING); 448 Option option = getOption(id); 449 if (option != null) { 450 System.err.println("setting '"+id+"' with '"+line+'\''); 451 option.fromPrefsFormat(line); 452 } else { 453 System.err.println("Warning: Unknown ID '"+id+'\''); 454 } 455 } else { 456 System.err.println("Warning: Bad line format '"+line+'\''); 457 } 458 } 459 } catch (IOException e) { 460 System.err.println("Non-fatal error reading the user preferences: "+e.getMessage()); 461 } 462 } 463 464 public void writeStartup() { 465 File script = 466 new File(StartupManager.getInstance().getPlatform().getUserPrefs()); 467 if (script.getPath().isEmpty()) { 468 return; 469 } 470 // TODO(jon): use filters when you've made 'em less stupid 471 String newLine = 472 StartupManager.getInstance().getPlatform().getNewLine(); 473 OptionPlatform currentPlatform = convertToOptionPlatform(); 474 StringBuilder contents = new StringBuilder(2048); 475 for (Object[] arrayOption : blahblah) { 476 Option option = getOption((String)arrayOption[0]); 477 OptionPlatform platform = option.getOptionPlatform(); 478 if ((platform == OptionPlatform.ALL) || (platform == currentPlatform)) { 479 contents.append(option.toPrefsFormat()).append(newLine); 480 } 481 } 482 483 try (BufferedWriter out = new BufferedWriter(new FileWriter(script))) { 484 out.write(contents.toString()); 485 } catch (IOException e) { 486 logger.error("Could not write to McIDAS-V startup prefs file", e); 487 } 488 } 489}