/*
* Copyright (C) 2014 by Array Systems Computing Inc. http://www.array.ca
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 3 of the License, or (at your option)
* any later version.
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, see http://www.gnu.org/licenses/
*/
package org.esa.snap.graphbuilder.rcp.dialogs;
import com.bc.ceres.core.ProgressMonitor;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.gpf.GPF;
import org.esa.snap.core.gpf.common.ReadOp;
import org.esa.snap.core.gpf.graph.GraphException;
import org.esa.snap.core.util.io.SnapFileFilter;
import org.esa.snap.engine_utilities.db.CommonReaders;
import org.esa.snap.engine_utilities.util.MemUtils;
import org.esa.snap.engine_utilities.util.ProductFunctions;
import org.esa.snap.engine_utilities.util.ResourceUtils;
import org.esa.snap.graphbuilder.gpf.ui.ProductSetReaderOpUI;
import org.esa.snap.graphbuilder.gpf.ui.SourceUI;
import org.esa.snap.graphbuilder.gpf.ui.UIValidation;
import org.esa.snap.graphbuilder.rcp.dialogs.support.GraphDialog;
import org.esa.snap.graphbuilder.rcp.dialogs.support.GraphExecuter;
import org.esa.snap.graphbuilder.rcp.dialogs.support.GraphNode;
import org.esa.snap.graphbuilder.rcp.dialogs.support.GraphPanel;
import org.esa.snap.graphbuilder.rcp.dialogs.support.GraphsMenu;
import org.esa.snap.graphbuilder.rcp.progress.LabelBarProgressMonitor;
import org.esa.snap.graphbuilder.rcp.utils.DialogUtils;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.Dialogs;
import org.esa.snap.tango.TangoIcons;
import org.esa.snap.ui.AppContext;
import org.esa.snap.ui.ModelessDialog;
import org.openide.util.HelpCtx;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
/**
* Provides the User Interface for creating, loading and saving Graphs
*/
public class GraphBuilderDialog extends ModelessDialog implements Observer, GraphDialog, LabelBarProgressMonitor.ProgressBarListener {
private static final ImageIcon processIcon = TangoIcons.actions_media_playback_start(TangoIcons.Res.R22);
private static final ImageIcon saveIcon = TangoIcons.actions_document_save_as(TangoIcons.Res.R22);
private static final ImageIcon loadIcon = TangoIcons.actions_document_open(TangoIcons.Res.R22);
private static final ImageIcon clearIcon = TangoIcons.actions_edit_clear(TangoIcons.Res.R22);
private static final ImageIcon helpIcon = TangoIcons.apps_help_browser(TangoIcons.Res.R22);
private static final ImageIcon infoIcon = TangoIcons.apps_accessories_text_editor(TangoIcons.Res.R22);
private final AppContext appContext;
private GraphPanel graphPanel = null;
private JLabel statusLabel = null;
private String lastWarningMsg = "";
private JPanel progressPanel = null;
private JProgressBar progressBar = null;
private LabelBarProgressMonitor progBarMonitor = null;
private JLabel progressMsgLabel = null;
private boolean initGraphEnabled = true;
private final GraphExecuter graphEx;
private boolean isProcessing = false;
private boolean allowGraphBuilding = true;
private final List<ProcessingListener> listenerList = new ArrayList<>(1);
public final static String LAST_GRAPH_PATH = "graphbuilder.last_graph_path";
private JTabbedPane tabbedPanel = null;
public GraphBuilderDialog(final AppContext theAppContext, final String title, final String helpID) {
this(theAppContext, title, helpID, true);
}
public GraphBuilderDialog(final AppContext theAppContext, final String title, final String helpID, final boolean allowGraphBuilding) {
super(theAppContext.getApplicationWindow(), title, 0, helpID);
this.allowGraphBuilding = allowGraphBuilding;
appContext = theAppContext;
graphEx = new GraphExecuter();
graphEx.addObserver(this);
String lastDir = SnapApp.getDefault().getPreferences().get(LAST_GRAPH_PATH,
ResourceUtils.getGraphFolder("").toFile().getAbsolutePath());
if (new File(lastDir).exists()) {
SnapApp.getDefault().getPreferences().put(LAST_GRAPH_PATH, lastDir);
}
initUI();
}
/**
* Initializes the dialog components
*/
private void initUI() {
if (this.allowGraphBuilding) {
super.getJDialog().setMinimumSize(new Dimension(650, 750));
} else {
super.getJDialog().setMinimumSize(new Dimension(650, 500));
}
final JPanel mainPanel = new JPanel(new BorderLayout(4, 4));
// north panel
final JPanel northPanel = new JPanel(new BorderLayout(4, 4));
if (allowGraphBuilding) {
graphPanel = new GraphPanel(graphEx);
graphPanel.setBackground(Color.WHITE);
graphPanel.setPreferredSize(new Dimension(1500, 1000));
final JScrollPane scrollPane = new JScrollPane(graphPanel);
scrollPane.setPreferredSize(new Dimension(300, 300));
northPanel.add(scrollPane, BorderLayout.CENTER);
mainPanel.add(northPanel, BorderLayout.NORTH);
}
// mid panel
final JPanel midPanel = new JPanel(new BorderLayout(4, 4));
tabbedPanel = new JTabbedPane();
//tabbedPanel.setTabPlacement(JTabbedPane.LEFT);
tabbedPanel.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
tabbedPanel.addChangeListener(new ChangeListener() {
public void stateChanged(final ChangeEvent e) {
ValidateAllNodes();
}
});
statusLabel = new JLabel("");
statusLabel.setForeground(new Color(255, 0, 0));
midPanel.add(tabbedPanel, BorderLayout.CENTER);
midPanel.add(statusLabel, BorderLayout.SOUTH);
mainPanel.add(midPanel, BorderLayout.CENTER);
// south panel
final JPanel southPanel = new JPanel(new BorderLayout(4, 4));
final JPanel buttonPanel = new JPanel();
initButtonPanel(buttonPanel);
southPanel.add(buttonPanel, BorderLayout.CENTER);
// progress Bar
progressBar = new JProgressBar();
progressBar.setName(getClass().getName() + "progressBar");
progressBar.setStringPainted(true);
progressPanel = new JPanel();
progressPanel.setLayout(new BorderLayout(2, 2));
progressMsgLabel = new JLabel();
progressPanel.add(progressMsgLabel, BorderLayout.NORTH);
progressPanel.add(progressBar, BorderLayout.CENTER);
progBarMonitor = new LabelBarProgressMonitor(progressBar, progressMsgLabel);
progBarMonitor.addListener(this);
final JButton progressCancelBtn = new JButton("Cancel");
progressCancelBtn.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
CancelProcessing();
}
});
progressPanel.add(progressCancelBtn, BorderLayout.EAST);
progressPanel.setVisible(false);
southPanel.add(progressPanel, BorderLayout.SOUTH);
mainPanel.add(southPanel, BorderLayout.SOUTH);
mainPanel.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
if (getJDialog().getJMenuBar() == null && allowGraphBuilding) {
final GraphsMenu operatorMenu = new GraphsMenu(getJDialog(), this);
getJDialog().setJMenuBar(operatorMenu.createDefaultMenu());
}
setContent(mainPanel);
}
private void initButtonPanel(final JPanel panel) {
panel.setLayout(new GridBagLayout());
final GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.anchor = GridBagConstraints.NORTHWEST;
final JButton processButton = DialogUtils.createButton("processButton", "Run", processIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
processButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
DoProcessing();
}
});
final JButton saveButton = DialogUtils.createButton("saveButton", "Save", saveIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
saveButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
SaveGraph();
}
});
final JButton loadButton = DialogUtils.createButton("loadButton", "Load", loadIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
loadButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
LoadGraph();
}
});
final JButton clearButton = DialogUtils.createButton("clearButton", "Clear", clearIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
clearButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
ClearGraph();
}
});
final JButton infoButton = DialogUtils.createButton("infoButton", "Note", infoIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
infoButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
OnInfo();
}
});
//getClass().getName() + name
final JButton helpButton = DialogUtils.createButton("helpButton", "Help", helpIcon, panel, DialogUtils.ButtonStyle.TextAndIcon);
helpButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
OnHelp();
}
});
gbc.weightx = 0;
if (allowGraphBuilding) {
panel.add(loadButton, gbc);
panel.add(saveButton, gbc);
panel.add(clearButton, gbc);
panel.add(infoButton, gbc);
}
panel.add(helpButton, gbc);
panel.add(processButton, gbc);
}
/**
* Validates the input and then call the GPF to execute the graph
*/
public void DoProcessing() {
if (ValidateAllNodes()) {
if (!checkIfOutputExists()) {
return;
}
MemUtils.freeAllMemory();
progressBar.setValue(0);
final ProcessThread processThread = new ProcessThread(progBarMonitor);
processThread.execute();
} else {
showErrorDialog(statusLabel.getText());
}
}
public void notifyProgressStart() {
progressPanel.setVisible(true);
}
public void notifyProgressDone() {
progressPanel.setVisible(false);
}
private boolean checkIfOutputExists() {
final File[] files = graphEx.getPotentialOutputFiles();
for (File file : files) {
if (file.exists()) {
final int answer = JOptionPane.showOptionDialog(getJDialog(),
"File " + file.getPath() + " already exists.\nWould you like to overwrite?", "Overwrite?",
JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, null, null, null);
if (answer == JOptionPane.NO_OPTION) {
return false;
}
}
}
return true;
}
private void CancelProcessing() {
if (progBarMonitor != null)
progBarMonitor.setCanceled(true);
}
private boolean InitGraph() {
boolean result = true;
try {
if (initGraphEnabled) {
result = graphEx.InitGraph();
}
if (!result && allowGraphBuilding) {
statusLabel.setText("Graph is incomplete");
}
} catch (Exception e) {
if (e.getMessage() != null) {
statusLabel.setText("Error: " + e.getMessage());
} else {
statusLabel.setText("Error: " + e.toString());
}
result = false;
}
return result;
}
public boolean canSaveGraphs() {
return true;
}
/**
* Validates the input and then saves the current graph to a file
*/
public void SaveGraph() {
//if(ValidateAllNodes()) {
try {
final File file = graphEx.saveGraph();
if (file != null) {
setTitle(file.getName());
}
} catch (GraphException e) {
showErrorDialog(e.getMessage());
}
//} else {
// showErrorDialog(statusLabel.getText());
//}
}
@Override
public void setTitle(final String title) {
super.setTitle("Graph Builder : " + title);
}
/**
* Loads a new graph from a file
*/
public void LoadGraph() {
final SnapFileFilter fileFilter = new SnapFileFilter("XML", "xml", "Graph");
final File graphFile = Dialogs.requestFileForOpen("Load Graph", false, fileFilter, LAST_GRAPH_PATH);
if (graphFile == null) return;
LoadGraph(graphFile);
}
/**
* Loads a new graph from a file
*
* @param file the graph file to load
*/
public void LoadGraph(final File file) {
try {
LoadGraph(new FileInputStream(file), file);
if (allowGraphBuilding) {
setTitle(file.getName());
}
} catch (IOException e) {
SnapApp.getDefault().handleError("Unable to load graph " + file.toString(), e);
}
}
/**
* Loads a new graph from a file
*
* @param fileStream the graph file to load
*/
public void LoadGraph(final InputStream fileStream, final File file) {
try {
initGraphEnabled = false;
tabbedPanel.removeAll();
graphEx.loadGraph(fileStream, file, true, false);
if (allowGraphBuilding) {
graphPanel.showRightClickHelp(false);
refreshGraph();
}
initGraphEnabled = true;
} catch (GraphException e) {
showErrorDialog(e.getMessage());
}
}
private void refreshGraph() {
if(graphPanel != null) {
graphPanel.repaint();
}
}
public String getGraphAsString() throws GraphException, IOException {
return graphEx.getGraphAsString();
}
public void EnableInitialInstructions(final boolean flag) {
if (this.allowGraphBuilding) {
graphPanel.showRightClickHelp(flag);
}
}
/**
* Removes all tabs and clears the graph
*/
private void ClearGraph() {
initGraphEnabled = false;
tabbedPanel.removeAll();
graphEx.ClearGraph();
refreshGraph();
initGraphEnabled = true;
statusLabel.setText("");
}
/**
* pass in a file list for a ProductSetReader
*
* @param productFileList the product files
*/
public void setInputFiles(final File[] productFileList) {
final GraphNode productSetNode = graphEx.getGraphNodeList().findGraphNodeByOperator("ProductSet-Reader");
if (productSetNode != null) {
ProductSetReaderOpUI ui = (ProductSetReaderOpUI) productSetNode.GetOperatorUI();
ui.setProductFileList(productFileList);
}
}
/**
* pass in a file list for a ProductSetReader
*
* @param product the product files
*/
public void setInputFile(final Product product) {
final GraphNode readerNode = graphEx.getGraphNodeList().findGraphNodeByOperator(
ReadOp.Spi.getOperatorAlias(ReadOp.class));
if (readerNode != null) {
SourceUI ui = (SourceUI) readerNode.GetOperatorUI();
ui.setSourceProduct(product);
ValidateAllNodes();
}
}
/**
* Call Help
*/
private void OnHelp() {
new HelpCtx(getHelpID()).display();
}
/**
* Call description dialog
*/
private void OnInfo() {
final PromptDialog dlg = new PromptDialog("Graph Description", "Description", graphEx.getGraphDescription(), true);
dlg.show();
if (dlg.IsOK()) {
graphEx.setGraphDescription(dlg.getValue());
}
}
public boolean isProcessing() {
return isProcessing;
}
/**
* lets all operatorUIs validate their parameters
* If parameter validation fails then a list of the failures is presented to the user
*
* @return true if validation passes
*/
private boolean ValidateAllNodes() {
if (isProcessing) return false;
boolean isValid = true;
final StringBuilder errorMsg = new StringBuilder(100);
final StringBuilder warningMsg = new StringBuilder(100);
for (GraphNode n : graphEx.GetGraphNodes()) {
try {
final UIValidation validation = n.validateParameterMap();
if (validation.getState() == UIValidation.State.ERROR) {
isValid = false;
errorMsg.append(validation.getMsg()).append('\n');
} else if (validation.getState() == UIValidation.State.WARNING) {
warningMsg.append(validation.getMsg()).append('\n');
}
} catch (Exception e) {
isValid = false;
errorMsg.append(e.getMessage()).append('\n');
}
}
statusLabel.setForeground(new Color(255, 0, 0));
statusLabel.setText("");
final String warningStr = warningMsg.toString();
if (!isValid) {
statusLabel.setText(errorMsg.toString());
return false;
} else if (!warningStr.isEmpty()) {
if (warningStr.length() > 100 && !warningStr.equals(lastWarningMsg)) {
Dialogs.showWarning(warningStr);
lastWarningMsg = warningStr;
} else {
statusLabel.setForeground(new Color(0, 100, 255));
statusLabel.setText("Warning: " + warningStr);
}
}
return InitGraph();
}
public void addListener(final ProcessingListener listener) {
if (!listenerList.contains(listener)) {
listenerList.add(listener);
}
}
public void removeListener(final ProcessingListener listener) {
listenerList.remove(listener);
}
private void notifyMSG(final ProcessingListener.MSG msg, final String text) {
for (final ProcessingListener listener : listenerList) {
listener.notifyMSG(msg, text);
}
}
private void notifyMSG(final ProcessingListener.MSG msg, final File[] fileList) {
for (final ProcessingListener listener : listenerList) {
listener.notifyMSG(msg, fileList);
}
}
/**
* Implements the functionality of Observer participant of Observer Design Pattern to define a one-to-many
* dependency between a Subject object and any number of Observer objects so that when the
* Subject object changes state, all its Observer objects are notified and updated automatically.
* <p>
* Defines an updating interface for objects that should be notified of changes in a subject.
*
* @param subject The Observerable subject
* @param data optional data
*/
public void update(Observable subject, Object data) {
try {
final GraphExecuter.GraphEvent event = (GraphExecuter.GraphEvent) data;
final GraphNode node = (GraphNode) event.getData();
final GraphExecuter.events eventType = event.getEventType();
switch(eventType) {
case ADD_EVENT:
tabbedPanel.addTab(node.getID(), null, CreateOperatorTab(node), node.getID() + " Operator");
refreshGraph();
break;
case REMOVE_EVENT:
tabbedPanel.remove(tabbedPanel.indexOfTab(node.getID()));
refreshGraph();
break;
case SELECT_EVENT:
int newTabIndex = tabbedPanel.indexOfTab(node.getID());
if(tabbedPanel.getSelectedIndex() != newTabIndex) {
tabbedPanel.setSelectedIndex(newTabIndex);
}
break;
case CONNECT_EVENT:
ValidateAllNodes();
break;
case REFRESH_EVENT:
refreshGraph();
break;
default:
throw new Exception("Unhandled GraphExecuter event " + eventType.name());
}
} catch (Exception e) {
String msg = e.getMessage();
if (msg == null || msg.isEmpty()) {
msg = e.toString();
}
statusLabel.setText(msg);
}
}
private JComponent CreateOperatorTab(final GraphNode node) {
return node.GetOperatorUI().CreateOpTab(node.getOperatorName(), node.getParameterMap(), appContext);
}
private class ProcessThread extends SwingWorker<GraphExecuter, Object> {
private final ProgressMonitor pm;
private Date executeStartTime = null;
private boolean errorOccured = false;
public ProcessThread(final ProgressMonitor pm) {
this.pm = pm;
}
@Override
protected GraphExecuter doInBackground() throws Exception {
pm.beginTask("Processing Graph...", 10);
try {
executeStartTime = Calendar.getInstance().getTime();
isProcessing = true;
graphEx.executeGraph(pm);
} catch (Throwable e) {
System.out.print(e.getMessage());
if (e.getMessage() != null && !e.getMessage().isEmpty())
statusLabel.setText(e.getMessage());
else
statusLabel.setText(e.getCause().toString());
errorOccured = true;
} finally {
isProcessing = false;
graphEx.disposeGraphContext();
// free cache
MemUtils.freeAllMemory();
pm.done();
}
return graphEx;
}
@Override
public void done() {
if (!errorOccured) {
final Date now = Calendar.getInstance().getTime();
final long totalSeconds = (now.getTime() - executeStartTime.getTime()) / 1000;
statusLabel.setText(ProductFunctions.getProcessingStatistics(totalSeconds));
final List<File> fileList = graphEx.getProductsToOpenInDAT();
final File[] files = fileList.toArray(new File[fileList.size()]);
notifyMSG(ProcessingListener.MSG.DONE, files);
ProcessingStats stats = openTargetProducts(files);
statusLabel.setText(ProductFunctions.getProcessingStatistics(totalSeconds, stats.totalBytes, stats.totalPixels));
if (SnapApp.getDefault().getPreferences().getBoolean(GPF.BEEP_AFTER_PROCESSING_PROPERTY, false)) {
Toolkit.getDefaultToolkit().beep();
}
}
}
}
private ProcessingStats openTargetProducts(final File[] fileList) {
ProcessingStats stats = new ProcessingStats();
if (fileList.length != 0) {
for (File file : fileList) {
try {
final Product product = CommonReaders.readProduct(file);
if (product != null) {
appContext.getProductManager().addProduct(product);
stats.totalBytes += ProductFunctions.getRawStorageSize(product);
stats.totalPixels = ProductFunctions.getTotalPixels(product);
}
} catch (IOException e) {
showErrorDialog(e.getMessage());
}
}
}
return stats;
}
private static class ProcessingStats {
long totalBytes = 0;
long totalPixels = 0;
}
public static File getInternalGraphFolder() {
return ResourceUtils.getGraphFolder("internal").toFile();
}
public static File getStandardGraphFolder() {
return ResourceUtils.getGraphFolder("Standard Graphs").toFile();
}
public interface ProcessingListener {
enum MSG {DONE, UPDATE}
void notifyMSG(final MSG msg, final File[] fileList);
void notifyMSG(final MSG msg, final String text);
}
}