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