package org.esa.snap.rcp; import com.bc.ceres.core.ExtensionFactory; import com.bc.ceres.core.ExtensionManager; import org.esa.snap.core.dataio.ProductReader; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.datamodel.ProductManager; import org.esa.snap.core.datamodel.ProductNode; import org.esa.snap.core.datamodel.ProductNodeEvent; import org.esa.snap.core.datamodel.ProductNodeListenerAdapter; import org.esa.snap.core.gpf.GPF; import org.esa.snap.core.gpf.OperatorSpi; import org.esa.snap.core.gpf.OperatorSpiRegistry; import org.esa.snap.core.util.PreferencesPropertyMap; import org.esa.snap.core.util.PropertyMap; import org.esa.snap.core.util.SystemUtils; import org.esa.snap.netbeans.docwin.DocumentWindowManager; import org.esa.snap.rcp.actions.file.OpenProductAction; import org.esa.snap.rcp.actions.file.SaveProductAction; import org.esa.snap.rcp.cli.SnapArgs; import org.esa.snap.rcp.session.OpenSessionAction; import org.esa.snap.rcp.util.ContextGlobalExtenderImpl; import org.esa.snap.rcp.util.Dialogs; import org.esa.snap.rcp.util.SelectionSupport; import org.esa.snap.rcp.util.internal.DefaultSelectionSupport; import org.esa.snap.runtime.Config; import org.esa.snap.runtime.Engine; import org.esa.snap.tango.TangoIcons; import org.esa.snap.ui.AppContext; import org.esa.snap.ui.product.ProductSceneView; import org.openide.awt.NotificationDisplayer; import org.openide.awt.StatusDisplayer; import org.openide.awt.ToolbarPool; import org.openide.awt.UndoRedo; import org.openide.modules.ModuleInfo; import org.openide.modules.OnStart; import org.openide.modules.OnStop; import org.openide.util.ContextGlobalProvider; import org.openide.util.Lookup; import org.openide.util.NbBundle; import org.openide.util.NbPreferences; import org.openide.util.Utilities; import org.openide.util.lookup.ServiceProvider; import org.openide.windows.OnShowing; import org.openide.windows.WindowManager; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import java.awt.Desktop; import java.awt.Frame; import java.awt.Window; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; /** * The class {@code SnapApp} is a facade for SNAP Desktop applications. There is only a single instance of * a SNAP application which is retrieved by * <pre> * SnapApp app = SnapApp.getDefault(); * </pre> * {@code SnapApp} is the main entry point for most SNAP Desktop extensions. An extension might want to be informed * about selection changes in the application. Here are some examples: * <pre> * app.getSelectionSupport(Product.class).addHandler(myProductSelectionHandler); * app.getSelectionSupport(ProductNode.class).addHandler(myProductNodeSelectionHandler); * app.getSelectionSupport(RasterDataNode.class).addHandler(myRasterDataNodeSelectionHandler); * app.getSelectionSupport(ProductSceneView.class).addHandler(myViewSelectionHandler); * </pre> * Or might want to retrieve the currently selected objects: * <pre> * Product product = app.getSelectedProduct(); * ProductNode productNode = getSelectedProductNode(); * ProductSceneView view = app.getSelectedProductSceneView(); * // For any other type of selected object, use: * Figure figure = Utilities.actionsGlobalContext().lookup(Figure.class); * </pre> * <p> * If you want to alter the behaviour of the default implementation of the SNAP Desktop application, * then register your derived class as a service using * <pre> * @ServiceProvider(service = MoonApp.class, supersedes = "SnapApp") * public class MoonApp extends SnapApp { * ... * } * </pre> * * @author Norman Fomferra * @see SelectionSupport * @see org.esa.snap.rcp.util.SelectionSupport.Handler * @since 2.0 */ @ServiceProvider(service = SnapApp.class) @SuppressWarnings("UnusedDeclaration") public class SnapApp { private final static Logger LOG = Logger.getLogger(SnapApp.class.getName()); private final ProductManager productManager; private Map<Class<?>, SelectionSupport<?>> selectionChangeSupports; private Engine engine; /** * Gets the SNAP application singleton which provides access to various SNAP APIs and resources. * <p> * The the method basically returns * <pre> * Lookup.getDefault().lookup(SnapApp.class) * </pre> * * @return The SNAP applications global singleton instance. */ public static SnapApp getDefault() { SnapApp instance = Lookup.getDefault().lookup(SnapApp.class); if (instance == null) { instance = new SnapApp(); } return instance; } /** * Constructor. * <p> * As this class is a registered service, the constructor is not supposed to be called directly. */ public SnapApp() { productManager = new ProductManager(); // Register a provider that delivers an UndoManager for a Product instance. UndoManagerProvider undoManagerProvider = new UndoManagerProvider(); ExtensionManager.getInstance().register(Product.class, undoManagerProvider); productManager.addListener(undoManagerProvider); productManager.addListener(new MultiSizeWarningListener()); selectionChangeSupports = new HashMap<>(); } /** * Gets SNAP's global document window manager. Use it to open your own document windows, or register a listener to * be notified on window events such as opening, closing, selection, deselection. * * @return SNAP's global document window manager. */ public DocumentWindowManager getDocumentWindowManager() { return DocumentWindowManager.getDefault(); } /** * Gets SNAP's global data product manager. Use it to add your own data product instances, or register a listener to * be notified on product addition and removal events. * * @return SNAP's global product manager. */ public ProductManager getProductManager() { return productManager; } /** * @return SNAP's global undo / redo manager. */ public UndoRedo.Manager getUndoManager(Product product) { return product.getExtension(UndoRedo.Manager.class); } /** * @return SNAP's main frame window. */ public Frame getMainFrame() { return WindowManager.getDefault().getMainWindow(); } /** * Sets the current status bar message. * * @param message The new status bar message. */ public void setStatusBarMessage(String message) { StatusDisplayer.getDefault().setStatusText(message); } /** * @return The (display) name of this application. * * @deprecated use {@link #getInstanceName()} */ @Deprecated public String getAppName() { return getInstanceName(); } /** * @return The SNAP application's name. The default is {@code "SNAP"}. */ public String getInstanceName() { try { return NbBundle.getBundle("org.netbeans.core.ui.Bundle").getString("LBL_ProductInformation"); } catch (Exception e) { return "SNAP"; } } /** * @return The user's application preferences. */ public Preferences getPreferences() { return NbPreferences.forModule(getClass()); } /** * Gets the {@link #getPreferences() preferences} wrapped by a {@link PropertyMap}. * <p> * Its main use is to provide compatibility for SNAP heritage GUI code (from BEAM & NEST) which used * the {@link PropertyMap} interface. * * @return The user's application preferences as {@link PropertyMap} instance. * * @deprecated Use {@link #getPreferences()} or {@link Config#preferences()} instead. */ @Deprecated public PropertyMap getPreferencesPropertyMap() { return new PreferencesPropertyMap(getPreferences()); } /** * @return The SNAP logger. */ public Logger getLogger() { return LOG; } /** * Handles an error. * * @param message An error message. * @param t An exception or {@code null}. */ public void handleError(String message, Throwable t) { if (t != null) { t.printStackTrace(); } Dialogs.showError("Error", message); getLogger().log(Level.SEVERE, message, t); ImageIcon icon = TangoIcons.status_dialog_error(TangoIcons.Res.R16); JLabel balloonDetails = new JLabel(message); JButton popupDetails = new JButton("Report"); popupDetails.addActionListener(e -> { try { Desktop.getDesktop().browse(new URI("http://forum.step.esa.int/")); } catch (URISyntaxException | IOException e1) { getLogger().log(Level.SEVERE, message, e1); } }); NotificationDisplayer.getDefault().notify("Error", icon, balloonDetails, popupDetails, NotificationDisplayer.Priority.HIGH, NotificationDisplayer.Category.ERROR); } /** * Provides a {@link SelectionSupport} instance for object selections. * * @param type The type of selected objects whose selection state to observe. * @return A selection support instance for the given object type, or {@code null}. */ public <T> SelectionSupport<T> getSelectionSupport(Class<T> type) { @SuppressWarnings("unchecked") DefaultSelectionSupport<T> selectionChangeSupport = (DefaultSelectionSupport<T>) selectionChangeSupports.get(type); if (selectionChangeSupport == null) { selectionChangeSupport = new DefaultSelectionSupport<>(type); selectionChangeSupports.put(type, selectionChangeSupport); } return selectionChangeSupport; } /** * @return The currently selected product scene view, or {@code null}. */ public ProductSceneView getSelectedProductSceneView() { return Utilities.actionsGlobalContext().lookup(ProductSceneView.class); } /** * Return the currently selected product node. * <p> * The {@link SelectionSourceHint hint} defines what is the primary and secondary selection source. Source is either the * {@link SelectionSourceHint#VIEW scene view} or the {@link SelectionSourceHint#EXPLORER product explorer}. If it is set to * {@link SelectionSourceHint#AUTO} the algorithm tries to make a good guess, checking which component has the focus. * * @return The currently selected product node, or {@code null}. */ public ProductNode getSelectedProductNode(SelectionSourceHint hint) { ProductNode viewNode = null; ProductSceneView productSceneView = getSelectedProductSceneView(); if (productSceneView != null) { viewNode = productSceneView.getProduct(); } ProductNode explorerNode = Utilities.actionsGlobalContext().lookup(ProductNode.class); return getProductNode(explorerNode, viewNode, productSceneView, hint); } /** * Return the currently selected product. * <p> * The {@link SelectionSourceHint hint} defines what is the primary and secondary selection source. Source is either the * {@link SelectionSourceHint#VIEW scene view} or the {@link SelectionSourceHint#EXPLORER product explorer}. If it is set to * {@link SelectionSourceHint#AUTO} the algorithm tries to make a good guess, checking which component has the focus. * * @param hint gives a hint to the implementation which selection source should be preferred. * @return The currently selected product or {@code null}. */ public Product getSelectedProduct(SelectionSourceHint hint) { Product viewProduct = null; ProductSceneView productSceneView = getSelectedProductSceneView(); if (productSceneView != null) { viewProduct = productSceneView.getProduct(); } Product explorerProduct = null; ProductNode productNode = Utilities.actionsGlobalContext().lookup(ProductNode.class); if (productNode != null) { explorerProduct = productNode.getProduct(); } return getProductNode(explorerProduct, viewProduct, productSceneView, hint); } /** * Gets an {@link AppContext} representation of the SNAP application. * <p> * Its main use is to provide compatibility for SNAP heritage GUI code (from BEAM & NEST) which used * the {@link AppContext} interface. * * @return An {@link AppContext} representation of this {@code SnapApp}. */ public AppContext getAppContext() { return new SnapContext(); } /** * Called if SNAP starts up. The method is not supposed to be called by clients directly. * <p> * Overrides should call {@code super.onStart()} as a first step unless they know what they are doing. */ public void onStart() { engine = Engine.start(false); String toolbarConfig = "Standard"; if (Config.instance().debug()) { WindowManager.getDefault().setRole("developer"); toolbarConfig = "Developer"; } // See src/main/resources/org/esa/snap/rcp/layer.xml // See src/main/resources/org/esa/snap/rcp/toolbars/Standard.xml // See src/main/resources/org/esa/snap/rcp/toolbars/Developer.xml ToolbarPool.getDefault().setConfiguration(toolbarConfig); } /** * Called if SNAP shuts down. The method is not supposed to be called by clients directly. * <p> * Overrides should call {@code super.onStop()} in a final step unless they know what they are doing. */ public void onStop() { engine.stop(); try { getPreferences().flush(); } catch (BackingStoreException e) { getLogger().log(Level.SEVERE, e.getMessage(), e); } } /** * Called if SNAP is showing on the user's desktop. The method is not supposed to be called by clients directly. * <p> * Overrides should call {@code super.onShowing()} as a first step unless they know whet they are doing. */ public void onShowing() { getMainFrame().setTitle(getEmptyTitle()); getSelectionSupport(ProductSceneView.class).addHandler(new SceneViewListener()); getSelectionSupport(ProductNode.class).addHandler(new ProductNodeListener()); NodeNameListener nodeNameListener = new NodeNameListener(); getProductManager().addListener(new ProductManager.Listener() { @Override public void productAdded(ProductManager.Event event) { event.getProduct().addProductNodeListener(nodeNameListener); } @Override public void productRemoved(ProductManager.Event event) { event.getProduct().removeProductNodeListener(nodeNameListener); } }); if (SnapArgs.getDefault().getSessionFile() != null) { File sessionFile = SnapArgs.getDefault().getSessionFile().toFile(); if (sessionFile != null) { new OpenSessionAction().openSession(sessionFile); } } List<Path> fileList = SnapArgs.getDefault().getFileList(); if (!fileList.isEmpty()) { OpenProductAction productAction = new OpenProductAction(); File[] files = fileList.stream().map(Path::toFile).filter(file -> file != null).toArray(File[]::new); productAction.setFiles(files); productAction.execute(); } } /** * Called if SNAP is about to shut down. The method is not supposed to be called by clients directly. * <p> * Overrides should call {@code super.onTryStop()()} unless they know whet they are doing. The method should return * immediately {@code false} if the super call returns {@code false}. * * @return {@code false} if the shutdown process shall be cancelled immediately. {@code true}, if it is ok * to continue shut down. */ public boolean onTryStop() { final ArrayList<Product> modifiedProducts = new ArrayList<>(5); final Product[] products = getProductManager().getProducts(); for (final Product product : products) { final ProductReader reader = product.getProductReader(); if (reader != null) { final Object input = reader.getInput(); if (input instanceof Product) { modifiedProducts.add(product); } } if (!modifiedProducts.contains(product) && product.isModified()) { modifiedProducts.add(product); } } if (!modifiedProducts.isEmpty()) { final StringBuilder message = new StringBuilder(); if (modifiedProducts.size() == 1) { message.append("The following product has been modified:"); message.append("\n ").append(modifiedProducts.get(0).getDisplayName()); message.append("\n\nDo you wish to save it?"); } else { message.append("The following products have been modified:"); for (Product modifiedProduct : modifiedProducts) { message.append("\n ").append(modifiedProduct.getDisplayName()); } message.append("\n\nDo you want to save them?"); } Dialogs.Answer answer = Dialogs.requestDecision("Exit", message.toString(), true, null); if (answer == Dialogs.Answer.YES) { //Save Products in reverse order is necessary because derived products must be saved first Collections.reverse(modifiedProducts); for (Product modifiedProduct : modifiedProducts) { Boolean saveStatus = new SaveProductAction(modifiedProduct).execute(); if (saveStatus == null) { // save cancelled --> cancel SNAP shutdown return false; } } } else if (answer == Dialogs.Answer.CANCELLED) { // decision request cancelled --> cancel SNAP shutdown return false; } } return true; } /** * This non-API class is public as an implementation detail. Don't use it, it may be removed anytime. * <p> * NetBeans {@code @OnStart}: {@code Runnable}s defined by various modules are invoked in parallel and as soon * as possible. It is guaranteed that execution of all {@code runnable}s is finished * before the startup sequence is claimed over. */ @OnStart public static class StartOp implements Runnable { @Override public void run() { LOG.info("Starting SNAP Desktop"); try { SnapApp.getDefault().onStart(); } finally { initImageIO(); SystemUtils.initGeoTools(); SystemUtils.initJAI(Lookup.getDefault().lookup(ClassLoader.class)); // uncomment if we encounter problems with the stmt above //SystemUtils.init3rdPartyLibs(null); SnapApp.getDefault().initGPF(); } } } /** * This non-API class is public as an implementation detail. Don't use it, it may be removed anytime. * <p> * NetBeans {@code @OnShowing}: Annotation to place on a {@code Runnable} with default constructor which should be invoked as soon as the window * system is shown. The {@code Runnable}s are invoked in AWT event dispatch thread one by one */ @OnShowing public static class ShowingOp implements Runnable { @Override public void run() { LOG.info("Showing SNAP Desktop"); SnapApp.getDefault().onShowing(); } } /** * This non-API class is public as an implementation detail. Don't use it, it may be removed anytime. * <p> * NetBeans {@code @OnStop}: Annotation that can be applied to {@code Runnable} or {@code Callable<Boolean>} * subclasses with default constructor which will be invoked during shutdown sequence or when the * module is being shutdown. * <p> * First of all call {@code Callable}s are consulted to allow or deny proceeding with the shutdown. * <p> * If the shutdown is approved, all {@code Runnable}s registered are acknowledged and can perform the shutdown * cleanup. The {@code Runnable}s are invoked in parallel. It is guaranteed their execution is finished before * the shutdown sequence is over. */ @OnStop public static class MaybeStopOp implements Callable { @Override public Boolean call() { LOG.info("Request to stop SNAP Desktop"); return SnapApp.getDefault().onTryStop(); } } /** * This non-API class is public as an implementation detail. Don't use it, it may be removed anytime. */ @OnStop public static class StopOp implements Runnable { @Override public void run() { LOG.info("Stopping SNAP Desktop"); SnapApp.getDefault().onStop(); } } private static void initImageIO() { // todo - actually this should be done in the activator of ceres-jai which does not exist yet Lookup.Result<ModuleInfo> moduleInfos = Lookup.getDefault().lookupResult(ModuleInfo.class); String ceresJaiCodeName = "org.esa.snap.ceres.jai"; Optional<? extends ModuleInfo> info = moduleInfos.allInstances().stream().filter( moduleInfo -> ceresJaiCodeName.equals(moduleInfo.getCodeName())).findFirst(); if (info.isPresent()) { ClassLoader classLoader = info.get().getClassLoader(); IIORegistry iioRegistry = IIORegistry.getDefaultInstance(); iioRegistry.registerServiceProviders(IIORegistry.lookupProviders(ImageReaderSpi.class, classLoader)); iioRegistry.registerServiceProviders(IIORegistry.lookupProviders(ImageWriterSpi.class, classLoader)); } else { LOG.warning(String.format("Module '%s' not found. Not able to load image-IO services.", ceresJaiCodeName)); } } private void initGPF() { OperatorSpiRegistry operatorSpiRegistry = GPF.getDefaultInstance().getOperatorSpiRegistry(); Set<OperatorSpi> services = operatorSpiRegistry.getServiceRegistry().getServices(); for (OperatorSpi service : services) { LOG.info(String.format("GPF operator SPI: %s (alias '%s')", service.getClass(), service.getOperatorAlias())); } GPF.getDefaultInstance().setProductManager(getProductManager()); } private void updateMainFrameTitle(ProductSceneView sceneView) { String title; if (sceneView != null) { Product product = sceneView.getProduct(); if (product != null) { title = String.format("%s - %s - %s", sceneView.getSceneName(), product.getName(), getProductPath(product)); } else { title = sceneView.getSceneName(); } title = appendTitleSuffix(title); } else { title = getEmptyTitle(); } getMainFrame().setTitle(title); } private void updateMainFrameTitle(ProductNode node) { String title; if (node != null) { Product product = node.getProduct(); if (product != null) { if (node == product) { title = String.format("%s - [%s]", product.getDisplayName(), getProductPath(product)); } else { title = String.format("%s - [%s] - [%s]", node.getDisplayName(), product.getName(), getProductPath(product)); } } else { title = node.getDisplayName(); } title = appendTitleSuffix(title); } else { title = getEmptyTitle(); } getMainFrame().setTitle(title); } private String getProductPath(Product product) { File fileLocation = product.getFileLocation(); if (fileLocation != null) { try { return fileLocation.getCanonicalPath(); } catch (IOException e) { return fileLocation.getAbsolutePath(); } } else { return "not saved"; } } private String appendTitleSuffix(String title) { String appendix = !Utilities.isMac() ? String.format(" - %s", getInstanceName()) : ""; return title + appendix; } private String getEmptyTitle() { String title; if (Utilities.isMac()) { title = String.format("[%s]", "Empty"); } else { title = String.format("%s", getInstanceName()); } return title; } private static <T extends ProductNode> T getProductNode(T explorerNode, T viewNode, ProductSceneView sceneView, SelectionSourceHint hint) { switch (hint) { case VIEW: if (viewNode != null) { return viewNode; } else { return explorerNode; } case EXPLORER: if (explorerNode != null) { return explorerNode; } else { return viewNode; } case AUTO: default: if (sceneView != null && sceneView.hasFocus()) { return viewNode; } return explorerNode; } } /** * This non-API class is public as an implementation detail. Don't use it, it may be removed anytime. * <p> * This class proxies the original ContextGlobalProvider and ensures that a set * of additional objects remain in the GlobalContext regardless of the TopComponent * selection. * * @see org.esa.snap.rcp.util.ContextGlobalExtenderImpl */ @ServiceProvider( service = ContextGlobalProvider.class, supersedes = "org.netbeans.modules.openide.windows.GlobalActionContextImpl" ) public static class ActionContextExtender extends ContextGlobalExtenderImpl { } /** * Associates objects with an undo manager. */ private static class UndoManagerProvider implements ExtensionFactory, ProductManager.Listener { private Map<Object, UndoRedo.Manager> undoManagers = new HashMap<>(); @Override public Class<?>[] getExtensionTypes() { return new Class<?>[]{UndoRedo.Manager.class}; } @Override public Object getExtension(Object object, Class<?> extensionType) { return undoManagers.get(object); } @Override public void productAdded(ProductManager.Event event) { undoManagers.put(event.getProduct(), new UndoRedo.Manager()); } @Override public void productRemoved(ProductManager.Event event) { UndoRedo.Manager manager = undoManagers.remove(event.getProduct()); if (manager != null) { manager.die(); } } } private static class SnapContext implements AppContext { @Override public ProductManager getProductManager() { return getDefault().getProductManager(); } @Override public Product getSelectedProduct() { return getDefault().getSelectedProduct(SelectionSourceHint.AUTO); } @Override public Window getApplicationWindow() { return getDefault().getMainFrame(); } @Override public String getApplicationName() { return getDefault().getInstanceName(); } @Override public void handleError(String message, Throwable t) { getDefault().handleError(message, t); } @Override @Deprecated public PropertyMap getPreferences() { return getDefault().getPreferencesPropertyMap(); } @Override public ProductSceneView getSelectedProductSceneView() { return getDefault().getSelectedProductSceneView(); } } private static class MultiSizeWarningListener implements ProductManager.Listener { @Override public void productAdded(ProductManager.Event event) { final Product product = event.getProduct(); } @Override public void productRemoved(ProductManager.Event event) { } } private class SceneViewListener implements SelectionSupport.Handler<ProductSceneView> { @Override public void selectionChange(ProductSceneView oldValue, ProductSceneView newValue) { updateMainFrameTitle(newValue); } } private class ProductNodeListener implements SelectionSupport.Handler<ProductNode> { @Override public void selectionChange(ProductNode oldValue, ProductNode newValue) { updateMainFrameTitle(newValue); } } private class NodeNameListener extends ProductNodeListenerAdapter { @Override public void nodeChanged(ProductNodeEvent event) { if (ProductNode.PROPERTY_NAME_NAME.equals(event.getPropertyName())) { updateMainFrameTitle(event.getSourceNode()); } } } /** * Provides a hint to {@link SnapApp#getSelectedProduct(SelectionSourceHint)} } which selection provider should be used as primary selection source */ public enum SelectionSourceHint { /** * The scene view shall be preferred as selection source. */ VIEW, /** * The product explorer shall be preferred as selection source. */ EXPLORER, /** * The primary selection source is automatically detected. */ AUTO, } }