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