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