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