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