001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2017 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 http://www.gnu.org/licenses. 027 */ 028 029package edu.wisc.ssec.mcidasv; 030 031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 032import static ucar.unidata.xml.XmlUtil.getAttribute; 033 034import java.awt.Insets; 035import java.awt.event.ActionEvent; 036import java.awt.event.ActionListener; 037import java.awt.geom.Rectangle2D; 038 039import java.io.BufferedReader; 040import java.io.File; 041import java.io.FileOutputStream; 042import java.io.FileReader; 043import java.io.IOException; 044import java.io.PrintStream; 045 046import java.lang.reflect.Method; 047import java.net.URL; 048import java.net.URLConnection; 049import java.net.URLStreamHandler; 050import java.rmi.RemoteException; 051 052import java.security.Security; 053import java.util.Collections; 054import java.util.Date; 055import java.util.EnumSet; 056import java.util.HashMap; 057import java.util.Hashtable; 058import java.util.LinkedList; 059import java.util.List; 060import java.util.Map; 061import java.util.Objects; 062import java.util.Properties; 063import java.util.Set; 064 065import javax.swing.Icon; 066import javax.swing.JButton; 067import javax.swing.JCheckBox; 068import javax.swing.JComponent; 069import javax.swing.JDialog; 070import javax.swing.JLabel; 071import javax.swing.JOptionPane; 072import javax.swing.SwingUtilities; 073import javax.swing.ToolTipManager; 074 075import edu.wisc.ssec.mcidas.adde.AddeURL; 076import edu.wisc.ssec.mcidas.adde.AddeURLStreamHandler; 077import edu.wisc.ssec.mcidasv.util.McVGuiUtils; 078import edu.wisc.ssec.mcidasv.util.OptionPaneClicker; 079import edu.wisc.ssec.mcidasv.util.SystemState; 080import edu.wisc.ssec.mcidasv.util.WebBrowser; 081import edu.wisc.ssec.mcidasv.util.WelcomeWindow; 082 083import javafx.application.Platform; 084import javafx.embed.swing.JFXPanel; 085import org.joda.time.DateTime; 086import org.python.util.PythonInterpreter; 087import org.w3c.dom.Element; 088 089import ucar.nc2.NetcdfFile; 090import visad.VisADException; 091 092import ucar.unidata.data.DataManager; 093import ucar.unidata.idv.ArgsManager; 094import ucar.unidata.idv.ControlDescriptor; 095import ucar.unidata.idv.IdvObjectStore; 096import ucar.unidata.idv.IdvPersistenceManager; 097import ucar.unidata.idv.IdvPreferenceManager; 098import ucar.unidata.idv.IdvResourceManager; 099import ucar.unidata.idv.IntegratedDataViewer; 100import ucar.unidata.idv.PluginManager; 101import ucar.unidata.idv.VMManager; 102import ucar.unidata.idv.ViewDescriptor; 103import ucar.unidata.idv.ViewManager; 104import ucar.unidata.idv.chooser.IdvChooserManager; 105import ucar.unidata.idv.ui.IdvUIManager; 106import ucar.unidata.ui.colortable.ColorTableManager; 107import ucar.unidata.ui.InteractiveShell.ShellHistoryEntry; 108import ucar.unidata.util.FileManager; 109import ucar.unidata.util.GuiUtils; 110import ucar.unidata.util.IOUtil; 111import ucar.unidata.util.LogUtil; 112import ucar.unidata.util.Misc; 113import ucar.unidata.xml.XmlDelegateImpl; 114import ucar.unidata.xml.XmlEncoder; 115import ucar.unidata.xml.XmlUtil; 116 117import org.bushe.swing.event.EventBus; 118import org.bushe.swing.event.annotation.AnnotationProcessor; 119import org.bushe.swing.event.annotation.EventSubscriber; 120 121import org.slf4j.bridge.SLF4JBridgeHandler; 122import org.slf4j.Logger; 123import org.slf4j.LoggerFactory; 124 125import uk.org.lidalia.sysoutslf4j.context.LogLevel; 126import uk.org.lidalia.sysoutslf4j.context.SysOutOverSLF4J; 127 128import edu.wisc.ssec.mcidasv.data.GpmIosp; 129import edu.wisc.ssec.mcidasv.chooser.McIdasChooserManager; 130import edu.wisc.ssec.mcidasv.control.LambertAEA; 131import edu.wisc.ssec.mcidasv.data.McvDataManager; 132import edu.wisc.ssec.mcidasv.monitors.MonitorManager; 133import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource; 134import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus; 135import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType; 136import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity; 137import edu.wisc.ssec.mcidasv.servermanager.AddePreferences; 138import edu.wisc.ssec.mcidasv.servermanager.EntryStore; 139import edu.wisc.ssec.mcidasv.servermanager.EntryTransforms; 140import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry; 141import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat; 142import edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry; 143import edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager; 144import edu.wisc.ssec.mcidasv.startupmanager.StartupManager; 145import edu.wisc.ssec.mcidasv.ui.LayerAnimationWindow; 146import edu.wisc.ssec.mcidasv.ui.McIdasColorTableManager; 147import edu.wisc.ssec.mcidasv.ui.UIManager; 148import edu.wisc.ssec.mcidasv.util.gui.EventDispatchThreadHangMonitor; 149import edu.wisc.ssec.mcidasv.util.pathwatcher.DirectoryWatchService; 150import edu.wisc.ssec.mcidasv.util.pathwatcher.OnFileChangeListener; 151import edu.wisc.ssec.mcidasv.util.pathwatcher.SimpleDirectoryWatchService; 152 153/** 154 * Class used as the {@literal "gateway"} to a running McIDAS-V session. 155 * 156 * <p>This is where the startup and shutdown processes are handled, as well as 157 * the initialization of the application's various {@literal "managers"}.</p> 158 */ 159@SuppressWarnings("unchecked") 160public class McIDASV extends IntegratedDataViewer { 161 162 /** Logging object. */ 163 private static final Logger logger = 164 LoggerFactory.getLogger(McIDASV.class); 165 166 /** 167 * Initialization start time. This value is set at the beginning of 168 * {@link #main(String[])}, and is used to estimate the duration of the 169 * session's initialization phase. 170 */ 171 private static long startTime; 172 173 /** 174 * Initialization duration. Set at the end of {@link #initDone()}. 175 */ 176 private static long estimate; 177 178 /** 179 * Path to a {@literal "session"} file--it's created upon McIDAS-V 180 * starting and removed when McIDAS-V exits cleanly. This allows us to 181 * perform a primitive check to see if the current session has happened 182 * after a crash. 183 */ 184 private static String SESSION_FILE = getSessionFilePath(); 185 186 /** 187 * Whether or not the previous session was able to exit as it should. 188 * If {@code false}, the previous session likely crashed. 189 */ 190 private static boolean cleanExit = true; 191 192 /** Date the previous session was started. May be {@code null}. */ 193 private static Date previousStart = null; 194 195 /** Set to true only if "-forceaqua" was found in the command line. */ 196 public static boolean useAquaLookAndFeel = false; 197 198 /** Points to the adde image defaults. */ 199 public static final IdvResourceManager.XmlIdvResource RSC_FRAMEDEFAULTS = 200 new IdvResourceManager.XmlIdvResource("idv.resource.framedefaults", 201 "McIDAS-X Frame Defaults"); 202 203 /** Points to the server definitions. */ 204 public static final IdvResourceManager.XmlIdvResource RSC_SERVERS = 205 new IdvResourceManager.XmlIdvResource("idv.resource.servers", 206 "Servers", "servers\\.xml$"); 207 208 /** Used to access McIDAS-V state in a static context. */ 209 private static McIDASV staticMcv; 210 211 /** Accessory in file save dialog */ 212 private JCheckBox overwriteDataCbx = 213 new JCheckBox("Change data paths", false); 214 215 /** Chooser manager */ 216 protected McIdasChooserManager chooserManager; 217 218 /** HTTP based monitor to dump stack traces and shutdown McIDAS-V. */ 219 private McIDASVMonitor mcvMonitor; 220 221 /** 222 * {@link MonitorManager} allows for relatively easy and efficient 223 * monitoring of various resources. 224 */ 225 private final MonitorManager monitorManager = new MonitorManager(); 226 227 /** 228 * Actions passed into {@link #handleAction(String, Hashtable, boolean)}. 229 */ 230 private final List<String> actions = new LinkedList<>(); 231 232 private enum WarningResult { OK, CANCEL, SHOW, HIDE }; 233 234 /** Reference to the ADDE server manager. */ 235 private EntryStore addeEntries; 236 237 /** 238 * GUI wrapper for ADDE server management. Reference is kept due to some 239 * of the trickery used by {@link AddePreferences}. 240 * 241 * <p>Value may be {@code null}.</p> 242 */ 243 private TabbedAddeManager tabbedAddeManager = null; 244 245 /** Directory monitoring service. */ 246 private final DirectoryWatchService watchService; 247 248 /** 249 * Create the McIDASV with the given command line arguments. 250 * This constructor calls {@link IntegratedDataViewer#init()} 251 * 252 * @param args Command line arguments 253 * @exception VisADException from construction of VisAd objects 254 * @exception RemoteException from construction of VisAD objects 255 */ 256 public McIDASV(String[] args) throws IOException, VisADException { 257 super(args); 258 259 AnnotationProcessor.process(this); 260 261 staticMcv = this; 262 263 // Set up our application to respond to the Mac OS X application menu 264 registerForMacOSXEvents(); 265 266 // we're tired of the IDV's default missing image, so reset it 267 GuiUtils.MISSING_IMAGE = 268 "/edu/wisc/ssec/mcidasv/resources/icons/toolbar/mcidasv-round22.png"; 269 270 watchService = new SimpleDirectoryWatchService(); 271 272 this.init(); 273 274 // ensure jython init only happens once per application session 275 String cacheDir = getStore().getJythonCacheDir(); 276 Properties pyProps = new Properties(); 277 if (cacheDir != null) { 278 pyProps.put("python.home", cacheDir); 279 } 280 String[] blank = new String[] { "" }; 281 PythonInterpreter.initialize(System.getProperties(), pyProps, blank); 282 } 283 284 /** 285 * Generic registration with the macOS application menu. 286 * 287 * <p>Checks the platform, then attempts to register with Apple's 288 * {@literal "EAWT"} stuff.</p> 289 * 290 * <p>See {@code OSXAdapter.java} to learn how this is done without 291 * directly referencing any Apple APIs.</p> 292 */ 293 public void registerForMacOSXEvents() { 294 // TODO(jon): remove? 295 if (isMac()) { 296 try { 297 // Generate and register the OSXAdapter, passing it a hash of all the methods we wish to 298 // use as delegates for various com.apple.eawt.ApplicationListener methods 299 Class<?> thisClass = getClass(); 300 Class<?>[] args = (Class[])null; 301 OSXAdapter.setQuitHandler(this, thisClass.getDeclaredMethod("MacOSXQuit", args)); 302 OSXAdapter.setAboutHandler(this, thisClass.getDeclaredMethod("MacOSXAbout", args)); 303 OSXAdapter.setPreferencesHandler(this, thisClass.getDeclaredMethod("MacOSXPreferences", args)); 304 } catch (Exception e) { 305 logger.error("Error while loading the OSXAdapter", e); 306 } 307 } 308 } 309 310 public boolean MacOSXQuit() { 311 // TODO(jon): remove? 312 return quit(); 313 } 314 315 public void MacOSXAbout() { 316 // TODO(jon): remove? 317 getIdvUIManager().about(); 318 } 319 320 public void MacOSXPreferences() { 321 // TODO(jon): remove? 322 showPreferenceManager(); 323 } 324 325 /** 326 * Get the maximum number of threads to be used when rendering in VisAD. 327 * 328 * @return Number of threads for rendering. Default value is the same as 329 * {@link Runtime#availableProcessors()}. 330 */ 331 @Override public int getMaxRenderThreadCount() { 332 StateManager stateManager = (StateManager)getStateManager(); 333 return stateManager.getPropertyOrPreference(PREF_THREADS_RENDER, 334 Runtime.getRuntime().availableProcessors()); 335 } 336 337 /** 338 * Get the maximum number of threads to be used when reading data. 339 * 340 * @return Number of threads for reading data. Default value is {@code 4}. 341 */ 342 @Override public int getMaxDataThreadCount() { 343 StateManager stateManager = (StateManager)getStateManager(); 344 return stateManager.getPropertyOrPreference(PREF_THREADS_DATA, 4); 345 } 346 347 /** 348 * Start up the McIDAS-V monitor server. 349 * 350 * <p>This is an HTTP server on the port defined by the property 351 * {@code idv.monitorport}. Default value is 8788.</p> 352 * 353 * <p>It is only accessible to 127.0.0.1 (localhost).</p> 354 */ 355 @Override protected void startMonitor() { 356 if (mcvMonitor != null) { 357 return; 358 } 359 final String monitorPort = getProperty(PROP_MONITORPORT, ""); 360 if (monitorPort!=null && monitorPort.trim().length()>0 && !"none".equals(monitorPort.trim())) { 361 Misc.run(() -> { 362 try { 363 mcvMonitor = 364 new McIDASVMonitor(McIDASV.this, 365 Integer.parseInt(monitorPort)); 366 mcvMonitor.init(); 367 } catch (Exception exc) { 368 LogUtil.consoleMessage("Unable to start McIDAS-V monitor on port:" + monitorPort); 369 LogUtil.consoleMessage("Error:" + exc); 370 } 371 }); 372 } 373 } 374 375 /** 376 * Initializes a XML encoder with McIDAS-V specific XML delegates. 377 * 378 * @param encoder XML encoder that'll be dealing with persistence. 379 * @param forRead Not used as of yet. 380 */ 381 // TODO: if we ever get up past three or so XML delegates, I vote that we 382 // make our own version of VisADPersistence. 383 @Override protected void initEncoder(XmlEncoder encoder, boolean forRead) { 384 385 encoder.addDelegateForClass(LambertAEA.class, new XmlDelegateImpl() { 386 @Override public Element createElement(XmlEncoder e, Object o) { 387 LambertAEA projection = (LambertAEA)o; 388 Rectangle2D rect = projection.getDefaultMapArea(); 389 List args = Misc.newList(rect); 390 List types = Misc.newList(rect.getClass()); 391 return e.createObjectConstructorElement(o, args, types); 392 } 393 }); 394 395 encoder.addDelegateForClass(ShellHistoryEntry.class, new XmlDelegateImpl() { 396 @Override public Element createElement(XmlEncoder e, Object o) { 397 ShellHistoryEntry entry = (ShellHistoryEntry)o; 398 List args = Misc.newList(entry.getEntryBytes(), entry.getInputMode().toString()); 399 return e.createObjectConstructorElement(o, args); 400 } 401 }); 402 403 // TODO(jon): ultra fashion makeover!! 404 encoder.addDelegateForClass(RemoteAddeEntry.class, new XmlDelegateImpl() { 405 @Override public Element createElement(XmlEncoder e, Object o) { 406 RemoteAddeEntry entry = (RemoteAddeEntry)o; 407 Element element = e.createObjectElement(o.getClass()); 408 element.setAttribute("address", entry.getAddress()); 409 element.setAttribute("group", entry.getGroup()); 410 element.setAttribute("username", entry.getAccount().getUsername()); 411 element.setAttribute("project", entry.getAccount().getProject()); 412 element.setAttribute("source", entry.getEntrySource().toString()); 413 element.setAttribute("type", entry.getEntryType().toString()); 414 element.setAttribute("validity", entry.getEntryValidity().toString()); 415 element.setAttribute("status", entry.getEntryStatus().toString()); 416 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 417 element.setAttribute("alias", entry.getEntryAlias()); 418 return element; 419 } 420 421 @Override public Object createObject(XmlEncoder e, Element element) { 422 String address = getAttribute(element, "address"); 423 String group = getAttribute(element, "group"); 424 String username = getAttribute(element, "username"); 425 String project = getAttribute(element, "project"); 426 String source = getAttribute(element, "source"); 427 String type = getAttribute(element, "type"); 428 String validity = getAttribute(element, "validity"); 429 String status = getAttribute(element, "status"); 430 boolean temporary = getAttribute(element, "temporary", false); 431 String alias = getAttribute(element, "alias", ""); 432 433 EntrySource entrySource = EntryTransforms.strToEntrySource(source); 434 EntryType entryType = EntryTransforms.strToEntryType(type); 435 EntryValidity entryValidity = EntryTransforms.strToEntryValidity(validity); 436 EntryStatus entryStatus = EntryTransforms.strToEntryStatus(status); 437 438 return new RemoteAddeEntry.Builder(address, group) 439 .account(username, project) 440 .source(entrySource) 441 .type(entryType) 442 .validity(entryValidity) 443 .temporary(temporary) 444 .alias(alias) 445 .status(entryStatus).build(); 446 } 447 }); 448 449 encoder.addDelegateForClass(LocalAddeEntry.class, new XmlDelegateImpl() { 450 @Override public Element createElement(XmlEncoder e, Object o) { 451 LocalAddeEntry entry = (LocalAddeEntry)o; 452 Element element = e.createObjectElement(o.getClass()); 453 element.setAttribute("group", entry.getGroup()); 454 element.setAttribute("descriptor", entry.getDescriptor()); 455 element.setAttribute("realtime", entry.getRealtimeAsString()); 456 element.setAttribute("format", entry.getFormat().name()); 457 element.setAttribute("start", entry.getStart()); 458 element.setAttribute("end", entry.getEnd()); 459 element.setAttribute("fileMask", entry.getMask()); 460 element.setAttribute("name", entry.getName()); 461 element.setAttribute("status", entry.getEntryStatus().name()); 462 element.setAttribute("temporary", Boolean.toString(entry.isEntryTemporary())); 463 element.setAttribute("alias", entry.getEntryAlias()); 464 return element; 465 } 466 467 @Override public Object createObject(XmlEncoder e, Element element) { 468 String group = getAttribute(element, "group"); 469 String descriptor = getAttribute(element, "descriptor"); 470 String realtime = getAttribute(element, "realtime", ""); 471 AddeFormat format = EntryTransforms.strToAddeFormat(XmlUtil.getAttribute(element, "format")); 472 String start = getAttribute(element, "start", "1"); 473 String end = getAttribute(element, "end", "999999"); 474 String fileMask = getAttribute(element, "fileMask"); 475 String name = getAttribute(element, "name"); 476 String status = getAttribute(element, "status", "ENABLED"); 477 boolean temporary = getAttribute(element, "temporary", false); 478 String alias = getAttribute(element, "alias", ""); 479 480 return new LocalAddeEntry.Builder(name, group, fileMask, format) 481 .range(start, end) 482 .descriptor(descriptor) 483 .realtime(realtime) 484 .status(status) 485 .temporary(temporary) 486 .alias(alias).build(); 487 } 488 }); 489 490 // Move legacy classes to a new location 491 encoder.registerNewClassName( 492 "edu.wisc.ssec.mcidasv.data.Test2ImageDataSource", 493 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 494 encoder.registerNewClassName( 495 "edu.wisc.ssec.mcidasv.data.Test2AddeImageDataSource", 496 "edu.wisc.ssec.mcidasv.data.adde.AddeImageParameterDataSource"); 497 encoder.registerNewClassName( 498 "edu.wisc.ssec.mcidasv.data.AddePointDataSource", 499 "edu.wisc.ssec.mcidasv.data.adde.AddePointDataSource"); 500 encoder.registerNewClassName( 501 "edu.wisc.ssec.mcidasv.data.AddeSoundingAdapter", 502 "edu.wisc.ssec.mcidasv.data.adde.AddeSoundingAdapter"); 503 } 504 505 /** 506 * Returns <i>all</i> of the actions used in this McIDAS-V session. This is 507 * possibly TMI and might be removed... 508 * 509 * @return Actions executed thus far. 510 */ 511 public List<String> getActionHistory() { 512 return actions; 513 } 514 515 /** 516 * Converts {@link ArgsManager#getOriginalArgs()} to a {@link List} and 517 * returns. 518 * 519 * @return The command-line arguments used to start McIDAS-V, as an 520 * {@code ArrayList}. 521 */ 522 public List<String> getCommandLineArgs() { 523 String[] originalArgs = getArgsManager().getOriginalArgs(); 524 List<String> args = arrList(originalArgs.length); 525 Collections.addAll(args, originalArgs); 526 return args; 527 } 528 529 /** 530 * Captures the action passed to {@code handleAction}. The action is logged 531 * and additionally, if the action is a HTML link, we attempt to visit the 532 * link in the user's preferred browser. 533 */ 534 @Override public boolean handleAction(String action, Hashtable properties, 535 boolean checkForAlias) 536 { 537 actions.add(action); 538 539 boolean result = false; 540 DateTime start = DateTime.now(); 541 logger.trace("started: action='{}', checkForAlias={}, properties='{}'", action, checkForAlias, properties); 542 if (IOUtil.isHtmlFile(action)) { 543 WebBrowser.browse(action); 544 result = true; 545 } else { 546 if (action.toLowerCase().contains("showsupportform")) { 547 logger.trace("showing support form 'manually'..."); 548 getIdvUIManager().showSupportForm(); 549 result = true; 550 } else { 551 result = super.handleAction(action, properties, checkForAlias); 552 } 553 } 554 long duration = new DateTime().minus(start.getMillis()).getMillis(); 555 logger.trace("finished: action='{}', duration: {} (ms), checkForAlias={}, properties='{}'", action, duration, checkForAlias, properties); 556 557 return result; 558 } 559 560 /** 561 * This method checks if the given action is one of the following. 562 * <ul> 563 * <li>Jython code: starts with {@literal "jython:"}.</li> 564 * <li>Help link: starts with {@literal "help:"}.</li> 565 * <li>Resource bundle file: ends with {@literal ".rbi"}.</li> 566 * <li>Bundle file: ends with {@literal ".xidv"}.</li> 567 * <li>JNLP file: ends with {@literal ".jnlp"}.</li> 568 * </ul> 569 * 570 * <p>It returns {@code true} if the action is one of these. {@code false} 571 * otherwise.</p> 572 * 573 * @param action The string action 574 * @param properties any properties 575 * 576 * @return {@code true} if the action was {@literal "handled"}; 577 * {@code false} otherwise. 578 */ 579 @Override protected boolean handleFileOrUrlAction(String action, 580 Hashtable properties) { 581 boolean result = false; 582 boolean idvAction = action.startsWith("idv:"); 583 boolean jythonAction = action.startsWith("jython:"); 584 585 if (!idvAction && !jythonAction) { 586 return super.handleFileOrUrlAction(action, properties); 587 } 588 589 Map<String, Object> hashProps; 590 if (properties != null) { 591 hashProps = new HashMap<>(properties); 592 } else { 593 //noinspection CollectionWithoutInitialCapacity 594 hashProps = new HashMap<>(); 595 } 596 597 ucar.unidata.idv.JythonManager jyManager = getJythonManager(); 598 if (idvAction) { 599 action = action.replace("&", "&").substring(4); 600 jyManager.evaluateUntrusted(action, hashProps); 601 result = true; 602 } else if (jythonAction) { 603 action = action.substring(7); 604 jyManager.evaluateAction(action, hashProps); 605 result = true; 606 } else { 607 result = super.handleFileOrUrlAction(action, properties); 608 } 609 return result; 610 } 611 612 /** 613 * Add a new {@link ControlDescriptor} into the {@code controlDescriptor} 614 * list and {@code controlDescriptorMap}. 615 * 616 * <p>This method differs from the IDV's in that McIDAS-V <b>overwrites</b> 617 * existing {@code ControlDescriptor ControlDescriptors} if 618 * {@link ControlDescriptor#getControlId()} matches. 619 * 620 * @param cd The ControlDescriptor to add. 621 * 622 * @throws NullPointerException if {@code cd} is {@code null}. 623 */ 624 @Override protected void addControlDescriptor(ControlDescriptor cd) { 625 cd = Objects.requireNonNull(cd, "Cannot add a null control descriptor to the list of control descriptors."); 626 String id = cd.getControlId(); 627 if (controlDescriptorMap.get(id) == null) { 628 controlDescriptors.add(cd); 629 controlDescriptorMap.put(id, cd); 630 } else { 631 for (int i = 0; i < controlDescriptors.size(); i++) { 632 ControlDescriptor tmp = (ControlDescriptor)controlDescriptors.get(i); 633 if (tmp.getControlId().equals(id)) { 634 controlDescriptors.set(i, cd); 635 controlDescriptorMap.put(id, cd); 636 break; 637 } 638 } 639 } 640 } 641 642 // pop up an incredibly rudimentary window that controls layer viz animation. 643 public void showLayerVisibilityAnimator() { 644 logger.trace("probably should try to do something here."); 645 SwingUtilities.invokeLater(() -> { 646 try { 647 LayerAnimationWindow window = new LayerAnimationWindow(); 648 window.setVisible(true); 649 } catch (Exception e) { 650 logger.error("oh no! something happened!", e); 651 } 652 }); 653 } 654 655 /** 656 * Handles removing all loaded data sources. 657 * 658 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 659 * will ignore the user's preferences and remove all data sources. 660 * 661 * @param showWarning Whether or not to display a warning message before 662 * removing <i>all</i> data sources. See the return details for more. 663 * 664 * @return Either {@code true} if the user wants to continue showing the 665 * warning dialog, or {@code false} if they've elected to stop showing the 666 * warning. If {@code showWarning} is {@code false}, this method will 667 * always return {@code false}, as the user isn't interested in seeing the 668 * warning. 669 */ 670 public boolean removeAllData(final boolean showWarning) { 671 boolean reallyRemove = false; 672 boolean continueWarning = true; 673 674 if (getArgsManager().getIsOffScreen()) { 675 super.removeAllDataSources(); 676 return continueWarning; 677 } 678 679 if (showWarning) { 680 Set<WarningResult> result = showWarningDialog( 681 "Confirm Data Removal", 682 "This action will remove all of the data currently loaded in McIDAS-V.<br>Is this what you want to do?", 683 Constants.PREF_CONFIRM_REMOVE_DATA, 684 "Always ask?", 685 "Remove all data", 686 "Do not remove any data"); 687 reallyRemove = result.contains(WarningResult.OK); 688 continueWarning = result.contains(WarningResult.SHOW); 689 } else { 690 // user doesn't want to see warning messages. 691 reallyRemove = true; 692 continueWarning = false; 693 } 694 695 if (reallyRemove) { 696 super.removeAllDataSources(); 697 } 698 699 return continueWarning; 700 } 701 702 /** 703 * Handles removing all loaded layers ({@literal "displays"} in IDV-land). 704 * 705 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 706 * will ignore the user's preferences and remove all layers. 707 * 708 * @param showWarning Whether or not to display a warning message before 709 * removing <i>all</i> layers. See the return details for more. 710 * 711 * @return Either {@code true} if the user wants to continue showing the 712 * warning dialog, or {@code false} if they've elected to stop showing the 713 * warning. If {@code showWarning} is {@code false}, this method will 714 * always return {@code false}, as the user isn't interested in seeing the 715 * warning. 716 */ 717 public boolean removeAllLayers(final boolean showWarning) { 718 boolean reallyRemove = false; 719 boolean continueWarning = true; 720 721 if (getArgsManager().getIsOffScreen()) { 722 super.removeAllDisplays(); 723 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 724 return continueWarning; 725 } 726 727 if (showWarning) { 728 Set<WarningResult> result = showWarningDialog( 729 "Confirm Layer Removal", 730 "This action will remove every layer currently loaded in McIDAS-V.<br>Is this what you want to do?", 731 Constants.PREF_CONFIRM_REMOVE_LAYERS, 732 "Always ask?", 733 "Remove all layers", 734 "Do not remove any layers"); 735 reallyRemove = result.contains(WarningResult.OK); 736 continueWarning = result.contains(WarningResult.SHOW); 737 } else { 738 // user doesn't want to see warning messages. 739 reallyRemove = true; 740 continueWarning = false; 741 } 742 743 if (reallyRemove) { 744 super.removeAllDisplays(); 745 ((ViewManagerManager)getVMManager()).disableAllLayerVizAnimations(); 746 } 747 748 return continueWarning; 749 } 750 751 /** 752 * Overridden so that McIDAS-V can prompt the user before removing, if 753 * necessary. 754 */ 755 @Override public void removeAllDataSources() { 756 IdvObjectStore store = getStore(); 757 boolean showWarning = 758 store.get(Constants.PREF_CONFIRM_REMOVE_DATA, true); 759 showWarning = removeAllData(showWarning); 760 store.put(Constants.PREF_CONFIRM_REMOVE_DATA, showWarning); 761 } 762 763 /** 764 * Overridden so that McIDAS-V can prompt the user before removing, if 765 * necessary. 766 */ 767 @Override public void removeAllDisplays() { 768 IdvObjectStore store = getStore(); 769 boolean showWarning = 770 store.get(Constants.PREF_CONFIRM_REMOVE_LAYERS, true); 771 showWarning = removeAllLayers(showWarning); 772 store.put(Constants.PREF_CONFIRM_REMOVE_LAYERS, showWarning); 773 } 774 775 /** 776 * Handles removing all loaded layers ({@literal "displays"} in IDV-land) 777 * and data sources. 778 * 779 * <p>If {@link ArgsManager#getIsOffScreen()} is {@code true}, this method 780 * will ignore the user's preferences and remove all layers and data. 781 * 782 * @see #removeAllData(boolean) 783 * @see #removeAllLayers(boolean) 784 */ 785 public void removeAllLayersAndData() { 786 boolean reallyRemove = false; 787 boolean continueWarning = true; 788 789 if (getArgsManager().getIsOffScreen()) { 790 removeAllData(false); 791 removeAllLayers(false); 792 } 793 794 IdvObjectStore store = getStore(); 795 boolean showWarning = store.get(Constants.PREF_CONFIRM_REMOVE_BOTH, true); 796 if (showWarning) { 797 Set<WarningResult> result = showWarningDialog( 798 "Confirm Removal", 799 "This action will remove all of your currently loaded layers and data.<br>Is this what you want to do?", 800 Constants.PREF_CONFIRM_REMOVE_BOTH, 801 "Always ask?", 802 "Remove all layers and data", 803 "Do not remove anything"); 804 reallyRemove = result.contains(WarningResult.OK); 805 continueWarning = result.contains(WarningResult.SHOW); 806 } else { 807 // user doesn't want to see warning messages. 808 reallyRemove = true; 809 continueWarning = false; 810 } 811 812 // don't show the individual warning messages as the user has attempted 813 // to remove *both* 814 if (reallyRemove) { 815 removeAllData(false); 816 removeAllLayers(false); 817 } 818 819 store.put(Constants.PREF_CONFIRM_REMOVE_BOTH, continueWarning); 820 } 821 822 /** 823 * Helper method for showing the removal warning dialog. Note that none of 824 * these parameters should be {@code null} or empty. 825 * 826 * @param title Title of the warning dialog. 827 * @param message Contents of the warning. May contain HTML, but you do 828 * not need to provide opening and closing {@literal "html"} tags. 829 * @param prefId ID of the preference that controls whether or not the 830 * dialog should be displayed. 831 * @param prefLabel Brief description of the preference. 832 * @param okLabel Text of button that signals removal. 833 * @param cancelLabel Text of button that signals cancelling removal. 834 * 835 * @return A {@code Set} of {@link WarningResult WarningResults} that 836 * describes what the user opted to do. Should always contain only 837 * <b>two</b> elements. One for whether or not {@literal "ok"} or 838 * {@literal "cancel"} was clicked, and one for whether or not the warning 839 * should continue to be displayed. 840 */ 841 private Set<WarningResult> showWarningDialog(final String title, 842 final String message, final String prefId, final String prefLabel, 843 final String okLabel, final String cancelLabel) 844 { 845 JCheckBox box = new JCheckBox(prefLabel, true); 846 JComponent comp = GuiUtils.vbox( 847 new JLabel("<html>"+message+"</html>"), 848 GuiUtils.inset(box, new Insets(4, 15, 0, 10))); 849 850 Object[] options = { okLabel, cancelLabel }; 851 int result = JOptionPane.showOptionDialog( 852 LogUtil.getCurrentWindow(), // parent 853 comp, // msg 854 title, // title 855 JOptionPane.YES_NO_OPTION, // option type 856 JOptionPane.WARNING_MESSAGE, // message type 857 (Icon)null, // icon? 858 options, // selection values 859 options[1]); // initial? 860 861 WarningResult button = WarningResult.CANCEL; 862 if (result == JOptionPane.YES_OPTION) { 863 button = WarningResult.OK; 864 } 865 866 WarningResult show = WarningResult.HIDE; 867 if (box.isSelected()) { 868 show = WarningResult.SHOW; 869 } 870 871 return EnumSet.of(button, show); 872 } 873 874 public void removeTabData() { 875 } 876 877 public void removeTabLayers() { 878 879 } 880 881 public void removeTabLayersAndData() { 882 } 883 884 /** 885 * Overridden so that McIDAS-V doesn't have to create an entire new 886 * {@link ucar.unidata.idv.ui.IdvWindow} if 887 * {@link VMManager#findViewManager(ViewDescriptor)} can't find an 888 * appropriate ViewManager for {@code viewDescriptor}. 889 * 890 * <p>Not doing the above causes McIDAS-V to get stuck in a window creation 891 * loop.</p> 892 */ 893 @Override public ViewManager getViewManager(ViewDescriptor viewDescriptor, 894 boolean newWindow, String properties) 895 { 896 ViewManager vm = 897 getVMManager().findOrCreateViewManager(viewDescriptor, properties); 898 if (vm == null) { 899 vm = super.getViewManager(viewDescriptor, newWindow, properties); 900 } 901 return vm; 902 } 903 904 /** 905 * Returns a reference to the current McIDAS-V object. Useful for working 906 * inside static methods. <b>Always check for null when using this 907 * method</b>. 908 * 909 * @return Either the current McIDAS-V "god object" or {@code null}. 910 */ 911 public static McIDASV getStaticMcv() { 912 return staticMcv; 913 } 914 915 /** 916 * @see ucar.unidata.idv.IdvBase#setIdv(ucar.unidata.idv.IntegratedDataViewer) 917 */ 918 @Override public void setIdv(IntegratedDataViewer idv) { 919 this.idv = idv; 920 } 921 922 /** 923 * Load the McV properties. All other property files are disregarded. 924 * 925 * @see ucar.unidata.idv.IntegratedDataViewer#initPropertyFiles(java.util.List) 926 */ 927 @Override public void initPropertyFiles(List files) { 928 files.clear(); 929 files.add(Constants.PROPERTIES_FILE); 930 } 931 932 /** 933 * Makes {@link PersistenceManager} save off a default {@literal "layout"} 934 * bundle. 935 */ 936 public void doSaveAsDefaultLayout() { 937 Misc.run(() -> ((PersistenceManager)getPersistenceManager()).doSaveAsDefaultLayout()); 938 } 939 940 /** 941 * Determines whether or not a default layout exists. 942 * 943 * @return {@code true} if there is a default layout, {@code false} 944 * otherwise. 945 */ 946 public boolean hasDefaultLayout() { 947 String path = 948 getResourceManager().getResources(IdvResourceManager.RSC_BUNDLES) 949 .getWritable(); 950 return new File(path).exists(); 951 } 952 953 /** 954 * Called from the menu command to clear the default bundle. Overridden 955 * in McIDAS-V so that we reference the <i>layout</i> rather than the 956 * bundle. 957 */ 958 @Override public void doClearDefaults() { 959 if (GuiUtils.showYesNoDialog(null, 960 "Are you sure you want to delete your default layout?", 961 "Delete confirmation")) { 962 resourceManager.clearDefaultBundles(); 963 } 964 } 965 966 /** 967 * Returns the time it took for McIDAS-V to start up. 968 * 969 * @return These results are created from subtracting the results of two 970 * {@link System#nanoTime()} calls against one another. 971 */ 972 public long getStartupDuration() { 973 return estimate; 974 } 975 976 /** 977 * <p> 978 * Overridden so that the support form becomes non-modal if launched from 979 * an exception dialog. 980 * </p> 981 * 982 * @see IntegratedDataViewer#addErrorButtons(JDialog, List, String, Throwable) 983 */ 984 @Override public void addErrorButtons(final JDialog dialog, 985 List buttonList, final String msg, final Throwable exc) 986 { 987 JButton supportBtn = new JButton("Support Form"); 988 supportBtn.addActionListener(ae -> 989 getIdvUIManager().showSupportForm(msg, 990 LogUtil.getStackTrace(exc), 991 null)); 992 buttonList.add(supportBtn); 993 } 994 995 /** 996 * This method is useful for storing commandline {@literal "properties"} 997 * with the user's preferences. 998 */ 999 private void overridePreferences() { 1000 StateManager stateManager = (StateManager)getStateManager(); 1001 int renderThreads = getMaxRenderThreadCount(); 1002 stateManager.putPreference(PREF_THREADS_RENDER, renderThreads); 1003 stateManager.putPreference(PREF_THREADS_DATA, getMaxDataThreadCount()); 1004 visad.util.ThreadManager.setGlobalMaxThreads(renderThreads); 1005 } 1006 1007 /** 1008 * Determine if the last {@literal "exit"} was clean--whether or not 1009 * {@code SESSION_FILE} was removed before the McIDAS-V process terminated. 1010 * 1011 * <p>If the exit was not clean, the user is prompted to submit a support 1012 * request.</p> 1013 */ 1014 private void detectAndHandleCrash() { 1015 GuiUtils.setApplicationTitle(""); 1016 if (cleanExit || getArgsManager().getIsOffScreen()) { 1017 return; 1018 } 1019 1020 String msg = "The previous McIDAS-V session did not exit cleanly.<br>"+ 1021 "Do you want to send the log file to the McIDAS Help Desk?"; 1022 if (previousStart != null) { 1023 msg = "The previous McIDAS-V session (start time: %s) did not exit cleanly.<br>"+ 1024 "Do you want to send the log file to the McIDAS Help Desk?"; 1025 msg = String.format(msg, previousStart); 1026 } 1027 1028 boolean continueAsking = getStore().get("mcv.crash.boom.send.report", true); 1029 if (!continueAsking) { 1030 return; 1031 } 1032 1033 Set<WarningResult> result = showWarningDialog( 1034 "Report Crash", 1035 msg, 1036 "mcv.crash.boom.send.report", 1037 "Always ask?", 1038 "Open support form", 1039 "Do not report"); 1040 1041 getStore().put("mcv.crash.boom.send.report", result.contains(WarningResult.SHOW)); 1042 if (!result.contains(WarningResult.OK)) { 1043 return; 1044 } 1045 1046 getIdvUIManager().showSupportForm(); 1047 } 1048 1049 /** 1050 * Called after the IDV has finished setting everything up after starting. 1051 * McIDAS-V is currently only using this method to determine if the last 1052 * {@literal "exit"} was clean--whether or not {@code SESSION_FILE} was 1053 * removed before the McIDAS-V process terminated. 1054 * 1055 * Called after the IDV has finished setting everything up. McIDAS-V uses 1056 * this method to handle: 1057 * 1058 * <ul> 1059 * <li>Clearing out the automatic display creation arguments.</li> 1060 * <li>Presence of certain properties on the commandline.</li> 1061 * <li>Detection and handling of a crashed McIDAS-V session.</li> 1062 * <li>Run action specified by {@code -doaction} flag (if any).</li> 1063 * <li>Allowing tooltips to remain visible for more than 4 seconds.</li> 1064 * </ul> 1065 * 1066 * @see ArgumentManager#clearAutomaticDisplayArgs() 1067 * @see #overridePreferences() 1068 * @see #detectAndHandleCrash() 1069 */ 1070 @Override public void initDone() { 1071 ((ArgumentManager)argsManager).clearAutomaticDisplayArgs(); 1072 1073 overridePreferences(); 1074 1075 detectAndHandleCrash(); 1076 1077 if (addeEntries == null) { 1078 getServerManager(); 1079 } 1080 addeEntries.startLocalServer(); 1081 1082 estimate = System.nanoTime() - startTime; 1083 logger.info("estimated startup duration: {} ms", estimate / 1.0e6); 1084 System.setProperty("mcv.start.duration", Long.toString(estimate)); 1085 1086 // handle the -doAction <action id> startup option. 1087 ((ArgumentManager)getArgsManager()).runStartupAction(); 1088 1089 // disable idiotic tooltip dismissal (seriously, 4 seconds!?) 1090 ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE); 1091 1092 // turn on directory monitoring in the file choosers. 1093 startWatchService(); 1094 EventBus.publish(Constants.EVENT_FILECHOOSER_START, "init finished"); 1095 1096 EventDispatchThreadHangMonitor.initMonitoring(); 1097 } 1098 1099 /** 1100 * @see IntegratedDataViewer#doOpen(String, boolean, boolean) 1101 */ 1102 @Override public void doOpen(final String filename, 1103 final boolean checkUserPreference, final boolean andRemove) 1104 { 1105 doOpenInThread(filename, checkUserPreference, andRemove); 1106 } 1107 1108 /** 1109 * Have the user select a bundle. If andRemove is true then we remove all 1110 * data sources and displays. 1111 * 1112 * Then we open the bundle and start doing unpersistence things. 1113 * 1114 * @param filename The filename to open 1115 * @param checkUserPreference Should we show, if needed, the 1116 * {@literal "open"} dialog 1117 * @param andRemove If true then first remove all data sources and displays 1118 */ 1119 private void doOpenInThread(String filename, boolean checkUserPreference, 1120 boolean andRemove) 1121 { 1122 boolean overwriteData = false; 1123 if (filename == null) { 1124 if (overwriteDataCbx.getToolTipText() == null) { 1125 overwriteDataCbx.setToolTipText("Change the file paths that the data sources use"); 1126 } 1127 1128 filename = FileManager.getReadFile("Open File", 1129 ((ArgumentManager)getArgsManager()).getBundleFilters(true), 1130 GuiUtils.top(overwriteDataCbx)); 1131 1132 if (filename == null) { 1133 return; 1134 } 1135 1136 overwriteData = overwriteDataCbx.isSelected(); 1137 } 1138 1139 if (ArgumentManager.isXmlBundle(filename)) { 1140 getPersistenceManager().decodeXmlFile(filename, 1141 checkUserPreference, overwriteData); 1142 return; 1143 } 1144 handleAction(filename, null); 1145 } 1146 1147 /** 1148 * Factory method to create the McIDAS-V @link JythonManager}. 1149 * 1150 * @return New {@code JythonManager}. 1151 */ 1152 @Override protected JythonManager doMakeJythonManager() { 1153 logger.debug("returning a new JythonManager"); 1154 return new JythonManager(this); 1155 } 1156 1157 /** 1158 * Factory method to create a McIDAS-V {@link McIdasChooserManager}. 1159 * Here we create our own manager so it can do things specific to McIDAS-V. 1160 * 1161 * @return {@code McIdasChooserManager} indicated by the startup properties. 1162 * 1163 * @see ucar.unidata.idv.IdvBase#doMakeIdvChooserManager() 1164 */ 1165 @Override 1166 protected IdvChooserManager doMakeIdvChooserManager() { 1167 chooserManager = (McIdasChooserManager)makeManager( 1168 McIdasChooserManager.class, new Object[] { this }); 1169 chooserManager.init(); 1170 return chooserManager; 1171 } 1172 1173 /** 1174 * Factory method to create the {@link IdvUIManager}. Here we create our 1175 * own UI manager so it can do things specific to McIDAS-V. 1176 * 1177 * @return {@link UIManager} indicated by the startup properties. 1178 * 1179 * @see ucar.unidata.idv.IdvBase#doMakeIdvUIManager() 1180 */ 1181 @Override 1182 protected IdvUIManager doMakeIdvUIManager() { 1183 return new UIManager(this); 1184 } 1185 1186 /** 1187 * Create our own VMManager so that we can make the tabs play nice. 1188 * @see ucar.unidata.idv.IdvBase#doMakeVMManager() 1189 */ 1190 @Override 1191 protected VMManager doMakeVMManager() { 1192 // what an ugly class name :( 1193 return new ViewManagerManager(this); 1194 } 1195 1196 /** 1197 * Make the {@link McIdasPreferenceManager}. 1198 * @see ucar.unidata.idv.IdvBase#doMakePreferenceManager() 1199 */ 1200 @Override 1201 protected IdvPreferenceManager doMakePreferenceManager() { 1202 return new McIdasPreferenceManager(this); 1203 } 1204 1205 /** 1206 * <p>McIDAS-V (alpha 10+) needs to handle both IDV bundles without 1207 * component groups and all bundles from prior McV alphas. You better 1208 * believe we need to extend the persistence manager functionality!</p> 1209 * 1210 * @see ucar.unidata.idv.IdvBase#doMakePersistenceManager() 1211 */ 1212 @Override protected IdvPersistenceManager doMakePersistenceManager() { 1213 return new PersistenceManager(this); 1214 } 1215 1216 /** 1217 * Create, if needed, and return the {@link McIdasChooserManager}. 1218 * 1219 * @return The Chooser manager 1220 */ 1221 public McIdasChooserManager getMcIdasChooserManager() { 1222 return (McIdasChooserManager)getIdvChooserManager(); 1223 } 1224 1225 /** 1226 * Returns the {@link MonitorManager}. 1227 * 1228 * @return McIDAS-V {@literal "monitor manager"}. 1229 */ 1230 public MonitorManager getMonitorManager() { 1231 return monitorManager; 1232 } 1233 1234 /** 1235 * Responds to events generated by the server manager's GUI. Currently 1236 * limited to {@link edu.wisc.ssec.mcidasv.servermanager.TabbedAddeManager.Event#CLOSED TabbedAddeManager.Event#CLOSED}. 1237 * 1238 * @param evt {@code TabbedAddeManager} event to respond to. 1239 */ 1240 @EventSubscriber(eventClass=TabbedAddeManager.Event.class) 1241 public void onServerManagerWindowEvent(TabbedAddeManager.Event evt) { 1242 if (evt == TabbedAddeManager.Event.CLOSED) { 1243 tabbedAddeManager = null; 1244 } 1245 } 1246 1247 /** 1248 * Creates (if needed) the server manager GUI and displays it. 1249 */ 1250 public void showServerManager() { 1251 if (tabbedAddeManager == null) { 1252 tabbedAddeManager = new TabbedAddeManager(getServerManager()); 1253 } 1254 tabbedAddeManager.showManager(); 1255 } 1256 1257 /** 1258 * Creates a new server manager (if needed) and returns it. 1259 * 1260 * @return The McIDAS-V ADDE server manager. 1261 */ 1262 public EntryStore getServerManager() { 1263 if (addeEntries == null) { 1264 addeEntries = new EntryStore(getStore(), getResourceManager()); 1265 } 1266 return addeEntries; 1267 } 1268 1269 public McvDataManager getMcvDataManager() { 1270 return (McvDataManager)getDataManager(); 1271 } 1272 1273 /** 1274 * Get McIDASV. 1275 * @see ucar.unidata.idv.IdvBase#getIdv() 1276 */ 1277 @Override public IntegratedDataViewer getIdv() { 1278 return this; 1279 } 1280 1281 /** 1282 * Creates a McIDAS-V argument manager so that McV can handle some non-IDV 1283 * command line things. 1284 * 1285 * @param args The arguments from the command line. 1286 * 1287 * @see ucar.unidata.idv.IdvBase#doMakeArgsManager(java.lang.String[]) 1288 */ 1289 @Override protected ArgsManager doMakeArgsManager(String[] args) { 1290 return new ArgumentManager(this, args); 1291 } 1292 1293 /** 1294 * Factory method to create the {@link McvDataManager}. 1295 * 1296 * @return The data manager 1297 * 1298 * @see ucar.unidata.idv.IdvBase#doMakeDataManager() 1299 */ 1300 @Override protected DataManager doMakeDataManager() { 1301 return new McvDataManager(this); 1302 } 1303 1304 /** 1305 * Make the McIDAS-V {@link StateManager}. 1306 * @see ucar.unidata.idv.IdvBase#doMakeStateManager() 1307 */ 1308 @Override protected StateManager doMakeStateManager() { 1309 return new StateManager(this); 1310 } 1311 1312 /** 1313 * Make the McIDAS-V {@link ResourceManager}. 1314 * @see ucar.unidata.idv.IdvBase#doMakeResourceManager() 1315 */ 1316 @Override protected IdvResourceManager doMakeResourceManager() { 1317 return new ResourceManager(this); 1318 } 1319 1320 /** 1321 * Make the {@link McIdasColorTableManager}. 1322 * @see ucar.unidata.idv.IdvBase#doMakeColorTableManager() 1323 */ 1324 @Override protected ColorTableManager doMakeColorTableManager() { 1325 return new McIdasColorTableManager(); 1326 } 1327 1328 /** 1329 * Factory method to create the {@link McvPluginManager}. 1330 * 1331 * @return The McV plugin manager. 1332 * 1333 * @see ucar.unidata.idv.IdvBase#doMakePluginManager() 1334 */ 1335 @Override protected PluginManager doMakePluginManager() { 1336 return new McvPluginManager(this); 1337 } 1338 1339// /** 1340// * Make the {@link edu.wisc.ssec.mcidasv.data.McIDASVProjectionManager}. 1341// * @see ucar.unidata.idv.IdvBase#doMakeIdvProjectionManager() 1342// */ 1343// @Override 1344// protected IdvProjectionManager doMakeIdvProjectionManager() { 1345// return new McIDASVProjectionManager(this); 1346// } 1347 1348 /** 1349 * Make a help button for a particular help topic 1350 * 1351 * @param helpId the topic id 1352 * @param toolTip the tooltip 1353 * 1354 * @return the button 1355 */ 1356 @Override public JComponent makeHelpButton(String helpId, String toolTip) { 1357 JButton btn = McVGuiUtils.makeImageButton(Constants.ICON_HELP, 1358 getIdvUIManager(), "showHelp", helpId, "Show help"); 1359 1360 if (toolTip != null) { 1361 btn.setToolTipText(toolTip); 1362 } 1363 return btn; 1364 } 1365 1366 /** 1367 * Return the current {@literal "userpath"}. 1368 * 1369 * @return Path to the user's {@literal "McIDAS-V directory"}. 1370 */ 1371 public String getUserDirectory() { 1372 return StartupManager.getInstance().getPlatform().getUserDirectory(); 1373 } 1374 1375 /** 1376 * Return the path to a file within {@literal "userpath"}. 1377 * 1378 * @param filename File within the userpath. 1379 * 1380 * @return Path to a file within the user's {@literal "McIDAS-V directory"}. 1381 * No path validation is performed, so please be aware that the returned 1382 * path may not exist. 1383 */ 1384 public String getUserFile(String filename) { 1385 return StartupManager.getInstance().getPlatform().getUserFile(filename); 1386 } 1387 1388 /** 1389 * Invokes the main method for a given class. 1390 * 1391 * <p>Note: this is rather limited so far as it doesn't pass in any 1392 * arguments.</p> 1393 * 1394 * @param className Class whose main method is to be invoked. Cannot be 1395 * {@code null}. 1396 */ 1397 public void runPluginMainMethod(final String className) { 1398 final String[] dummyArgs = { "" }; 1399 try { 1400 Class<?> clazz = Misc.findClass(className); 1401 Class[] args = new Class[] { dummyArgs.getClass() }; 1402 Method mainMethod = Misc.findMethod(clazz, "main", args); 1403 if (mainMethod != null) { 1404 mainMethod.invoke(null, new Object[] { dummyArgs }); 1405 } 1406 } catch (Exception e) { 1407 logger.error("problem with plugin class", e); 1408 LogUtil.logException("problem running main method for class: "+className, e); 1409 } 1410 } 1411 1412 /** 1413 * Attempts to determine if a given string is a 1414 * {@literal "loopback address"} (aka localhost). 1415 * 1416 * <p>Strings are <b>trimmed and converted to lowercase</b>, and currently 1417 * checked against: 1418 * <ul> 1419 * <li>{@code 127.0.0.1}</li> 1420 * <li>{@code ::1} (for IPv6)</li> 1421 * <li>Strings starting with {@code localhost}.</li> 1422 * </ul> 1423 * 1424 * @param host {@code String} to check. Should not be {@code null}. 1425 * 1426 * @return {@code true} if {@code host} is a recognized loopback address. 1427 * {@code false} otherwise. 1428 * 1429 * @throws NullPointerException if {@code host} is {@code null}. 1430 */ 1431 public static boolean isLoopback(final String host) { 1432 String cleaned = Objects.requireNonNull(host.trim().toLowerCase()); 1433 return "127.0.0.1".startsWith(cleaned) 1434 || "::1".startsWith(cleaned) 1435 || cleaned.startsWith("localhost"); 1436 } 1437 1438 /** 1439 * Are we on a Mac? Used to build the MRJ handlers, taken from TN2110. 1440 * 1441 * @return {@code true} if this session is running on top of OS X, 1442 * {@code false} otherwise. 1443 * 1444 * @see <a href="http://developer.apple.com/technotes/tn2002/tn2110.html">TN2110</a> 1445 */ 1446 public static boolean isMac() { 1447 String osName = System.getProperty("os.name"); 1448 return osName.contains("OS X"); 1449 } 1450 1451 /** 1452 * Queries the {@code os.name} system property and if the result does not 1453 * start with {@literal "Windows"}, the platform is assumed to be 1454 * {@literal "unix-like"}. 1455 * 1456 * <p>Given the McIDAS-V supported platforms (Windows, {@literal "Unix"}, 1457 * and OS X), the above logic is safe. 1458 * 1459 * @return {@code true} if we're not running on Windows, {@code false} 1460 * otherwise. 1461 * 1462 * @throws RuntimeException if there is no property associated with 1463 * {@code os.name}. 1464 */ 1465 public static boolean isUnixLike() { 1466 String osName = System.getProperty("os.name"); 1467 if (osName == null) { 1468 throw new RuntimeException("no os.name system property!"); 1469 } 1470 1471 if (System.getProperty("os.name").startsWith("Windows")) { 1472 return false; 1473 } 1474 return true; 1475 } 1476 1477 /** 1478 * Queries the {@code os.name} system property and if the result starts 1479 * with {@literal "Windows"}, the platform is assumed to be Windows. Duh. 1480 * 1481 * @return {@code true} if we're running on Windows, {@code false} 1482 * otherwise. 1483 * 1484 * @throws RuntimeException if there is no property associated with 1485 * {@code os.name}. 1486 */ 1487 public static boolean isWindows() { 1488 String osName = System.getProperty("os.name"); 1489 if (osName == null) { 1490 throw new RuntimeException("no os.name system property!"); 1491 } 1492 1493 return osName.startsWith("Windows"); 1494 } 1495 1496 /** 1497 * If McIDAS-V is running on Windows, this method will return a 1498 * {@code String} that looks like {@literal "C:"} or {@literal "D:"}, etc. 1499 * 1500 * <p>If McIDAS-V is not running on Windows, this method will return an 1501 * empty {@code String}. 1502 * 1503 * @return Either the {@literal "drive letter"} of the {@code java.home} 1504 * property or an empty {@code String} if McIDAS-V isn't running on Windows. 1505 * 1506 * @throws RuntimeException if there is no property associated with 1507 * {@code java.home}. 1508 */ 1509 public static String getJavaDriveLetter() { 1510 if (!isWindows()) { 1511 return ""; 1512 } 1513 1514 String home = System.getProperty("java.home"); 1515 if (home == null) { 1516 throw new RuntimeException("no java.home system property!"); 1517 } 1518 1519 return home.substring(0, 2); 1520 } 1521 1522 /** 1523 * Attempts to create a {@literal "session"} file. This method will create 1524 * a {@literal "userpath"} if it does not already exist. 1525 * 1526 * @param path Path of the session file that should get created. 1527 * {@code null} values are not allowed, and sufficient priviledges are 1528 * assumed. 1529 * 1530 * @throws AssertionError if McIDAS-V couldn't write to {@code path}. 1531 * 1532 * @see #SESSION_FILE 1533 * @see #hadCleanExit(String) 1534 * @see #removeSessionFile(String) 1535 */ 1536 private static void createSessionFile(final String path) { 1537 assert path != null : "Cannot create a null path"; 1538 FileOutputStream out = null; 1539 PrintStream p = null; 1540 1541 File dir = new File(StartupManager.getInstance().getPlatform().getUserDirectory()); 1542 if (!dir.exists()) { 1543 dir.mkdir(); 1544 } 1545 1546 try { 1547 out = new FileOutputStream(path); 1548 p = new PrintStream(out); 1549 p.println(new Date().getTime()); 1550 } catch (Exception e) { 1551 throw new AssertionError("Could not write to "+path+". Error message: "+e.getMessage(), e); 1552 } finally { 1553 if (p != null) { 1554 p.close(); 1555 } 1556 if (out != null) { 1557 try { 1558 out.close(); 1559 } catch (IOException e) { 1560 throw new AssertionError("Could not close "+path+". Error message: "+e.getMessage(), e); 1561 } 1562 } 1563 } 1564 } 1565 1566 /** 1567 * Attempts to extract a timestamp from {@code path}. {@code path} is 1568 * expected to <b>only</b> contain a single line consisting of a 1569 * {@link Long} integer. 1570 * 1571 * @param path Path to the file of interest. 1572 * 1573 * @return Either a {@link Date} of the timestamp contained in 1574 * {@code path} or {@code null} if the extraction failed. 1575 */ 1576 private static Date extractDate(final String path) { 1577 assert path != null; 1578 Date savedDate = null; 1579 BufferedReader reader = null; 1580 try { 1581 reader = new BufferedReader(new FileReader(path)); 1582 String line = reader.readLine(); 1583 if (line != null) { 1584 savedDate = new Date(Long.parseLong(line.trim())); 1585 } 1586 } catch (Exception e) { 1587 logger.trace("problem extracting the date!", e); 1588 } finally { 1589 if (reader != null) { 1590 try { 1591 reader.close(); 1592 } catch (IOException e) { 1593 logger.trace("problem closing session file!", e); 1594 } 1595 } 1596 } 1597 return savedDate; 1598 } 1599 1600 /** 1601 * Attempts to remove the file accessible via {@code path}. 1602 * 1603 * @param path Path of the file that'll get removed. This should be 1604 * non-null and point to an existing and writable filename (not a 1605 * directory). 1606 * 1607 * @throws AssertionError if the file at {@code path} could not be 1608 * removed. 1609 * 1610 * @see #SESSION_FILE 1611 * @see #createSessionFile(String) 1612 * @see #hadCleanExit(String) 1613 */ 1614 private static void removeSessionFile(final String path) { 1615 if (path == null) { 1616 return; 1617 } 1618 1619 File f = new File(path); 1620 1621 if (!f.exists() || !f.canWrite() || f.isDirectory()) { 1622 return; 1623 } 1624 1625 if (!f.delete()) { 1626 throw new AssertionError("Could not delete session file"); 1627 } 1628 } 1629 1630 /** 1631 * Tries to determine whether or not the last McIDAS-V session ended 1632 * {@literal "cleanly"}. Currently a simple check for a 1633 * {@literal "session"} file that is created upon starting and removed upon 1634 * ending. 1635 * 1636 * @param path Path to the session file to check. Can't be {@code null}. 1637 * 1638 * @return Either {@code true} if the file pointed at by {@code path} does 1639 * <b><i>NOT</i></b> exist, {@code false} if it does exist. 1640 * 1641 * @see #SESSION_FILE 1642 * @see #createSessionFile(String) 1643 * @see #removeSessionFile(String) 1644 */ 1645 private static boolean hadCleanExit(final String path) { 1646 assert path != null : "Cannot test for a null path"; 1647 return !(new File(path).exists()); 1648 } 1649 1650 /** 1651 * Returns the (<i>current</i>) path to the session file. Note that the 1652 * location of the file may change arbitrarily. 1653 * 1654 * @return {@code String} pointing to the session file. 1655 * 1656 * @see #SESSION_FILE 1657 */ 1658 public static String getSessionFilePath() { 1659 return StartupManager.getInstance().getPlatform().getUserFile("session.tmp"); 1660 } 1661 1662 /** 1663 * Useful for providing the startup manager with values other than the 1664 * defaults... Note that this method attempts to update the value of 1665 * {@link #SESSION_FILE}. 1666 * 1667 * @param args Likely the argument array coming from the main method. 1668 */ 1669 private static void applyArgs(final String[] args) { 1670 assert args != null : "Cannot use a null argument array"; 1671 StartupManager.applyArgs(true, false, args); 1672 SESSION_FILE = getSessionFilePath(); 1673 } 1674 1675 /** 1676 * This returns the set of {@link ControlDescriptor ControlDescriptors} 1677 * that can be shown. The ordering of this list determines the 1678 * "default" controls shown in the Field Selector, so we override 1679 * here for control over the ordering. 1680 * 1681 * @param includeTemplates If true then include the display templates 1682 * @return re-ordered List of shown control descriptors 1683 */ 1684 @Override public List getControlDescriptors(boolean includeTemplates) { 1685 List l = super.getControlDescriptors(includeTemplates); 1686 for (int i = 0; i < l.size(); i++) { 1687 ControlDescriptor cd = (ControlDescriptor) l.get(i); 1688 if (cd.getControlId().equals("omni")) { 1689 // move the omni control to the end of the list 1690 // so it will never be "default" in Field Selector 1691 l.remove(i); 1692 l.add(cd); 1693 } 1694 } 1695 return l; 1696 } 1697 1698 /** 1699 * Show the McIDAS-V {@literal "Welcome Window"} for the first start up. 1700 * 1701 * @param args Commandline arguments, used to handle autoquit stress testing. 1702 */ 1703 private static void handleWelcomeWindow(String... args) { 1704 boolean showWelcome = false; 1705 boolean welcomeAutoQuit = false; 1706 long welcomeAutoQuitDelay = WelcomeWindow.DEFAULT_QUIT_DELAY; 1707 for (int i = 0; i < args.length; i++) { 1708 if ("-welcomewindow".equals(args[i])) { 1709 showWelcome = true; 1710 } else if ("-autoquit".equals(args[i])) { 1711 welcomeAutoQuit = true; 1712 int delayIdx = i + 1; 1713 if (delayIdx < args.length) { 1714 welcomeAutoQuitDelay = Long.parseLong(args[delayIdx]); 1715 } 1716 } 1717 } 1718 1719 // if user elects to quit, System.exit(1) will be called. 1720 // if the user decides to start, the welcome window will be simply be 1721 // closed, allowing McV to continue starting up. 1722 if (showWelcome) { 1723 WelcomeWindow ww; 1724 if (welcomeAutoQuit) { 1725 ww = new WelcomeWindow(true, welcomeAutoQuitDelay); 1726 } else { 1727 ww = new WelcomeWindow(); 1728 } 1729 ww.setVisible(true); 1730 } 1731 } 1732 1733 /** 1734 * Register {@literal "adde"} and {@literal "idvresource"} URL protocols. 1735 * 1736 * <p>This needs to be called pretty early in the McIDAS-V initialization 1737 * process. They're currently being registered immediately after the 1738 * session file is created.</p> 1739 */ 1740 private static void registerProtocolHandlers() { 1741 try { 1742 URL.setURLStreamHandlerFactory(protocol -> { 1743 switch (protocol.toLowerCase()) { 1744 case AddeURL.ADDE_PROTOCOL: 1745 return new AddeURLStreamHandler(); 1746 case PluginManager.PLUGIN_PROTOCOL: 1747 return new IdvResourceStreamHandler(); 1748 default: 1749 return null; 1750 } 1751 }); 1752 } catch (Throwable e) { 1753 logger.error("Could not register protocol handlers!", e); 1754 } 1755 } 1756 1757 /** 1758 * Responsible for handling {@literal "idvresource"} URLs. 1759 * 1760 * <p>Really just a redirect to {@link IOUtil#getURL(String, Class)}.</p> 1761 */ 1762 private static class IdvResourceStreamHandler extends URLStreamHandler { 1763 @Override protected URLConnection openConnection(URL u) 1764 throws IOException 1765 { 1766 return IOUtil.getURL(u.getPath(), McIDASV.class).openConnection(); 1767 } 1768 } 1769 1770 /** 1771 * Configure the logging and create the McIDAS-V object responsible for 1772 * initializing the application session. 1773 * 1774 * @param args Command line arguments. 1775 * 1776 * @throws Exception When something untoward happens. 1777 */ 1778 public static void main(String... args) throws Exception { 1779 // show the welcome window if needed. 1780 // since the welcome window is intended to be a one time thing, 1781 // it probably shouldn't count for the startTime stuff. 1782 handleWelcomeWindow(args); 1783 1784 startTime = System.nanoTime(); 1785 1786 // allow use of the "unlimited strength" crypto policy. 1787 // this becomes the default in 1.8.0_162, but we need to ship with 1788 // 1.8.0_152. 1789 Security.setProperty("crypto.policy", "unlimited"); 1790 1791 // the following two lines are required if we want to embed JavaFX 1792 // widgets into McV (which is Swing). The first line initializes the 1793 // JavaFX runtime, and the second line allows the JavaFX runtime to 1794 // hang around even if there are no JavaFX windows. 1795 JFXPanel dummy = new JFXPanel(); 1796 Platform.setImplicitExit(false); 1797 1798 try { 1799 applyArgs(args); 1800 1801 SysOutOverSLF4J.sendSystemOutAndErrToSLF4J(LogLevel.INFO, LogLevel.WARN); 1802 1803 // Optionally remove existing handlers attached to j.u.l root logger 1804 SLF4JBridgeHandler.removeHandlersForRootLogger(); // (since SLF4J 1.6.5) 1805 1806 // add SLF4JBridgeHandler to j.u.l's root logger, should be done once during 1807 // the initialization phase of your application 1808 SLF4JBridgeHandler.install(); 1809 1810// Properties pythonProps = new Properties(); 1811// logger.trace("calling PythonInterpreter.initialize..."); 1812// PythonInterpreter.initialize(System.getProperties(), pythonProps, new String[] {""}); 1813 1814 LogUtil.configure(); 1815 1816 NetcdfFile.registerIOProvider(GpmIosp.class); 1817 1818 long sysMem = Long.valueOf(SystemState.queryOpSysProps().get("opsys.memory.physical.total")); 1819 logger.info("============================================================================="); 1820 logger.info("Starting McIDAS-V @ {}", new Date()); 1821 logger.info("Versions:"); 1822 logger.info("{}", SystemState.getMcvVersionString()); 1823 logger.info("{}", SystemState.getIdvVersionString()); 1824 logger.info("{}", SystemState.getVisadVersionString()); 1825 logger.info("{}", SystemState.getNcidvVersionString()); 1826 logger.info("{} MB system memory", Math.round(sysMem/1024/1024)); 1827 1828 if (!hadCleanExit(SESSION_FILE)) { 1829 previousStart = extractDate(SESSION_FILE); 1830 } 1831 1832 createSessionFile(SESSION_FILE); 1833 registerProtocolHandlers(); 1834 1835 McIDASV myself = new McIDASV(args); 1836 } catch (IllegalArgumentException e) { 1837 String msg = "McIDAS-V could not initialize itself. "; 1838 String osName = System.getProperty("os.name"); 1839 if (osName.contains("Windows")) { 1840 LogUtil.userErrorMessage(msg+'\n'+e.getMessage()); 1841 } else { 1842 System.err.println(msg+e.getMessage()); 1843 } 1844 } 1845 } 1846 1847 /** 1848 * Attempts a clean shutdown of McIDAS-V. Currently this entails 1849 * suppressing any error dialogs, explicitly killing the 1850 * {@link #addeEntries}, removing {@link #SESSION_FILE}, and disabling 1851 * the directory monitors found in the file choosers. 1852 * 1853 * @param exitCode System exit code to use. 1854 * 1855 * @see IntegratedDataViewer#quit() 1856 */ 1857 @Override protected void exit(int exitCode) { 1858 LogUtil.setShowErrorsInGui(false); 1859 1860 // turn off the directory monitors in the file choosers. 1861 EventBus.publish(Constants.EVENT_FILECHOOSER_STOP, "shutting down"); 1862 stopWatchService(); 1863 1864 if (addeEntries != null) { 1865 addeEntries.saveForShutdown(); 1866 addeEntries.stopLocalServer(); 1867 } 1868 1869 removeSessionFile(SESSION_FILE); 1870 1871 // shut down javafx runtime 1872 Platform.exit(); 1873 1874 logger.info("Exiting McIDAS-V @ {}", new Date()); 1875 1876 System.exit(exitCode); 1877 } 1878 1879 /** 1880 * This method is largely a copy of {@link IntegratedDataViewer#quit()}, 1881 * but allows for some GUI testing. 1882 */ 1883 public boolean autoQuit() { 1884 IdvObjectStore store = getStore(); 1885 1886 boolean showQuitConfirm = store.get(PREF_SHOWQUITCONFIRM, true); 1887 long quitDelay = store.get("mcidasv.autoexit.delay", 3000); 1888 1889 if (showQuitConfirm) { 1890 JCheckBox cbx = new JCheckBox("Always ask", true); 1891 JComponent comp = 1892 GuiUtils.vbox( 1893 new JLabel("<html><b>Do you really want to exit?</b></html>"), 1894 GuiUtils.inset(cbx, new Insets(4, 15, 0, 10))); 1895 1896 JOptionPane pane = new JOptionPane(comp, 1897 JOptionPane.QUESTION_MESSAGE, 1898 JOptionPane.YES_NO_OPTION); 1899 1900 new OptionPaneClicker(pane, "Exit Confirmation", quitDelay, "Yes"); 1901 getStore().put(PREF_SHOWQUITCONFIRM, cbx.isSelected()); 1902 } 1903 1904 if (!getStationModelManager().checkCloseWindow()) { 1905 return false; 1906 } 1907 1908 if (!getJythonManager().saveOnExit()) { 1909 return false; 1910 } 1911 1912 store.saveIfNeeded(); 1913 store.cleanupTmpFiles(); 1914 getPluginManager().handleApplicationExit(); 1915 getJythonManager().handleApplicationExit(); 1916 1917 if (getInteractiveMode()) { 1918 exit(0); 1919 } 1920 return true; 1921 } 1922 1923 /** 1924 * Register the given {@code listener} so that changes to files matching 1925 * {@code glob} in {@code path} can be handled. 1926 * 1927 * @param path Directory to watch. 1928 * @param glob Only respond to files matching this file mask. 1929 * @param listener Listener that can handle file changes. 1930 * 1931 * @throws IOException if there was a problem registering {@code listener}. 1932 */ 1933 public void watchDirectory(final String path, 1934 final String glob, 1935 final OnFileChangeListener listener) 1936 throws IOException 1937 { 1938 watchService.register(listener, path, glob); 1939 } 1940 1941 /** 1942 * Returns McIDAS-V's {@link DirectoryWatchService}. 1943 * 1944 * @return {@code DirectoryWatchService} responsible for handling all of 1945 * McIDAS-V's directory monitoring. 1946 */ 1947 public DirectoryWatchService getWatchService() { 1948 return watchService; 1949 } 1950 1951 /** 1952 * Enable directory monitoring. 1953 */ 1954 public void startWatchService() { 1955 watchService.start(); 1956 } 1957 1958 /** 1959 * Disable directory monitoring. 1960 */ 1961 public void stopWatchService() { 1962 watchService.stop(); 1963 } 1964 1965 /** 1966 * Exposes {@link #exit(int)} to other classes. 1967 * 1968 * @param exitCode System exit code to use. 1969 * 1970 * @see #exit(int) 1971 */ 1972 public void exitMcIDASV(int exitCode) { 1973 exit(exitCode); 1974 } 1975}