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