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("&", "&amp;").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}