package org.esa.snap.opendap.ui;
import com.jidesoft.swing.FolderChooser;
import com.jidesoft.swing.JideScrollPane;
import com.jidesoft.swing.SimpleScrollPane;
import org.esa.snap.core.ui.AppContext;
import org.esa.snap.core.ui.GridBagUtils;
import org.esa.snap.core.ui.tool.ToolButtonFactory;
import org.esa.snap.opendap.datamodel.OpendapLeaf;
import org.esa.snap.opendap.utils.OpendapUtils;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.actions.file.OpenProductAction;
import org.esa.snap.tango.TangoIcons;
import org.esa.snap.util.StringUtils;
import org.openide.modules.Places;
import org.openide.util.HelpCtx;
import thredds.catalog.InvCatalog;
import thredds.catalog.InvCatalogFactory;
import thredds.catalog.InvDataset;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingWorker;
import javax.swing.border.EmptyBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
import java.awt.BorderLayout;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
public class OpendapAccessPanel extends JPanel implements CatalogTree.UIContext {
private static final Logger LOG = Logger.getLogger(OpendapAccessPanel.class.getName());
private static final String PROPERTY_KEY_SERVER_URLS = "opendap.server.urls";
private static final String PROPERTY_KEY_DOWNLOAD_DIR = "opendap.download.dir";
private final static int DDS_AREA_INDEX = 0;
private final static int DAS_AREA_INDEX = 1;
private JComboBox<String> urlField;
private AbstractButton refreshButton;
private CatalogTree catalogTree;
private JTabbedPane metaInfoArea;
private JCheckBox useDatasetNameFilter;
private FilterComponent datasetNameFilter;
private JCheckBox useTimeRangeFilter;
private FilterComponent timeRangeFilter;
private JCheckBox useRegionFilter;
private FilterComponent regionFilter;
private JCheckBox useVariableFilter;
private VariableFilter variableFilter;
private JCheckBox openInVisat;
private JPanel statusBar;
private double currentDataSize = 0.0;
private final Preferences preferences;
private final String helpId;
private JTextField folderTextField;
private JProgressBar progressBar;
private JLabel preMessageLabel;
private JLabel postMessageLabel;
private Map<Integer, JTextArea> textAreas;
private JButton downloadButton;
private AppContext appContext;
private JButton cancelButton;
private JLabel statusBarMessage;
public OpendapAccessPanel(AppContext appContext, String helpId) {
super();
this.preferences = SnapApp.getDefault().getPreferences();
this.helpId = helpId;
this.appContext = appContext;
initComponents();
initContentPane();
}
private void initComponents() {
urlField = new JComboBox<>();
urlField.setEditable(true);
urlField.getEditor().getEditorComponent().addKeyListener(new KeyAdapter() {
@Override
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER) {
refreshButton.doClick();
}
}
});
updateUrlField();
refreshButton = ToolButtonFactory.createButton(
TangoIcons.actions_view_refresh(TangoIcons.Res.R22),
false);
refreshButton.addActionListener(e -> {
final boolean usingUrl = refresh();
if (usingUrl) {
final String urls = preferences.get(PROPERTY_KEY_SERVER_URLS, "");
final String currentUrl = urlField.getSelectedItem().toString();
if (!urls.contains(currentUrl)) {
preferences.put(PROPERTY_KEY_SERVER_URLS, urls + "\n" + currentUrl);
updateUrlField();
}
}
});
metaInfoArea = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
JTextArea ddsArea = new JTextArea(10, 40);
JTextArea dasArea = new JTextArea(10, 40);
ddsArea.setEditable(false);
dasArea.setEditable(false);
textAreas = new HashMap<>();
textAreas.put(DAS_AREA_INDEX, dasArea);
textAreas.put(DDS_AREA_INDEX, ddsArea);
metaInfoArea.addTab("DDS", new JScrollPane(ddsArea));
metaInfoArea.addTab("DAS", new JScrollPane(dasArea));
metaInfoArea.setToolTipTextAt(DDS_AREA_INDEX, "Dataset Descriptor Structure: description of dataset variables");
metaInfoArea.setToolTipTextAt(DAS_AREA_INDEX, "Dataset Attribute Structure: description of dataset attributes");
metaInfoArea.addChangeListener(e -> {
if (catalogTree.getSelectedLeaf() != null) {
setMetadataText(metaInfoArea.getSelectedIndex(), catalogTree.getSelectedLeaf());
}
});
catalogTree = new CatalogTree(new DefaultLeafSelectionListener(), appContext, this);
useDatasetNameFilter = new JCheckBox("Use dataset name filter");
useTimeRangeFilter = new JCheckBox("Use time range filter");
useRegionFilter = new JCheckBox("Use region filter");
useVariableFilter = new JCheckBox("Use variable name filter");
DefaultFilterChangeListener filterChangeListener = new DefaultFilterChangeListener();
datasetNameFilter = new DatasetNameFilter(useDatasetNameFilter);
datasetNameFilter.addFilterChangeListener(filterChangeListener);
timeRangeFilter = new TimeRangeFilter(useTimeRangeFilter);
timeRangeFilter.addFilterChangeListener(filterChangeListener);
regionFilter = new RegionFilter(useRegionFilter);
regionFilter.addFilterChangeListener(filterChangeListener);
variableFilter = new VariableFilter(useVariableFilter, catalogTree);
variableFilter.addFilterChangeListener(filterChangeListener);
catalogTree.addCatalogTreeListener(new CatalogTree.CatalogTreeListener() {
@Override
public void leafAdded(OpendapLeaf leaf, boolean hasNestedDatasets) {
if (hasNestedDatasets) {
return;
}
if (leaf.getDataset().getGeospatialCoverage() != null) {
useRegionFilter.setEnabled(true);
}
filterLeaf(leaf);
}
@Override
public void catalogElementsInsertionFinished() {
}
});
openInVisat = new JCheckBox("Open in SNAP");
statusBarMessage = new JLabel("Ready.");
statusBarMessage.setText("Ready.");
preMessageLabel = new JLabel();
postMessageLabel = new JLabel();
progressBar = new JProgressBar(0, 100);
statusBar = new JPanel();
statusBar.setLayout(new BoxLayout(statusBar, BoxLayout.X_AXIS));
statusBar.add(statusBarMessage);
statusBar.add(Box.createHorizontalStrut(4));
statusBar.add(preMessageLabel);
statusBar.add(Box.createHorizontalGlue());
statusBar.add(progressBar);
statusBar.add(Box.createHorizontalGlue());
statusBar.add(postMessageLabel);
useRegionFilter.setEnabled(false);
}
private void setMetadataText(int componentIndex, OpendapLeaf leaf) {
String text = null;
try {
if (leaf.isDapAccess()) {
if (metaInfoArea.getSelectedIndex() == DDS_AREA_INDEX) {
text = OpendapUtils.getResponse(leaf.getDdsUri());
} else if (metaInfoArea.getSelectedIndex() == DAS_AREA_INDEX) {
text = OpendapUtils.getResponse(leaf.getDasUri());
}
} else if (leaf.isFileAccess()) {
if (metaInfoArea.getSelectedIndex() == DDS_AREA_INDEX) {
text = "No DDS information for file '" + leaf.getName() + "'.";
} else if (metaInfoArea.getSelectedIndex() == DAS_AREA_INDEX) {
text = "No DAS information for file '" + leaf.getName() + "'.";
}
}
} catch (IOException e) {
LOG.warning("Unable to retrieve meta information for file '" + leaf.getName() + "'.");
}
setResponseText(componentIndex, text);
}
private void setResponseText(int componentIndex, String response) {
JTextArea textArea = textAreas.get(componentIndex);
if (response.length() > 100000) {
response = response.substring(0, 10000) + "\nCut remaining file content";
}
textArea.setText(response);
textArea.setCaretPosition(0);
}
@Override
public void updateStatusBar(String message) {
statusBarMessage.setText(message);
}
private void filterLeaf(OpendapLeaf leaf) {
if ((!useDatasetNameFilter.isSelected() || datasetNameFilter.accept(leaf)) &&
(!useTimeRangeFilter.isSelected() || timeRangeFilter.accept(leaf)) &&
(!useRegionFilter.isSelected() || regionFilter.accept(leaf)) &&
(!useVariableFilter.isSelected() || variableFilter.accept(leaf))) {
catalogTree.setLeafVisible(leaf, true);
} else {
catalogTree.setLeafVisible(leaf, false);
}
}
private void updateUrlField() {
final String urlsProperty = preferences.get(PROPERTY_KEY_SERVER_URLS, "");
final String[] urls = urlsProperty.split("\n");
for (String url : urls) {
if (StringUtils.isNotNullAndNotEmpty(url) && !contains(urlField, url)) {
urlField.addItem(url);
}
}
}
private static boolean contains(JComboBox<String> urlField, String url) {
for (int i = 0; i < urlField.getItemCount(); i++) {
if (urlField.getItemAt(i).equals(url)) {
return true;
}
}
return false;
}
private void initContentPane() {
GridBagLayout layout = new GridBagLayout();
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets.right = 5;
final JPanel urlPanel = new JPanel(layout);
urlPanel.add(new JLabel("Root URL:"), gbc);
gbc.gridx = 1;
gbc.weightx = 1.0;
gbc.fill = GridBagConstraints.HORIZONTAL;
urlPanel.add(urlField, gbc);
gbc.gridx = 2;
gbc.weightx = 0.0;
gbc.fill = GridBagConstraints.NONE;
urlPanel.add(refreshButton, gbc);
gbc.gridx = 3;
gbc.insets.right = 0;
final AbstractButton helpButton = ToolButtonFactory.createButton(TangoIcons.apps_help_browser(TangoIcons.Res.R22), false);
helpButton.addActionListener(e -> new HelpCtx(helpId).display());
urlPanel.add(helpButton, gbc);
final JPanel variableInfo = new JPanel(new BorderLayout(5, 5));
variableInfo.setBorder(new EmptyBorder(10, 0, 0, 0));
variableInfo.add(metaInfoArea, BorderLayout.CENTER);
final JScrollPane openDapTree = new JScrollPane(catalogTree.getComponent());
final JSplitPane centerLeftPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, openDapTree, variableInfo);
centerLeftPane.setResizeWeight(1);
centerLeftPane.setContinuousLayout(true);
final JPanel filterPanel = new JPanel(new GridBagLayout());
final JComponent datasetNameFilterUI = datasetNameFilter.getUI();
final JComponent timeRangeFilterUI = timeRangeFilter.getUI();
final JComponent regionFilterUI = regionFilter.getUI();
final JComponent variableFilterUI = variableFilter.getUI();
GridBagUtils.addToPanel(filterPanel, new TitledPanel(useDatasetNameFilter, datasetNameFilterUI, true, true), gbc,
"gridx=0,gridy=0,anchor=NORTHWEST,weightx=1,weighty=0,,fill=BOTH");
GridBagUtils.addToPanel(filterPanel, new TitledPanel(useTimeRangeFilter, timeRangeFilterUI, true, true), gbc, "gridy=1");
GridBagUtils.addToPanel(filterPanel, new TitledPanel(useRegionFilter, regionFilterUI, true, true), gbc, "gridy=2");
GridBagUtils.addToPanel(filterPanel, new TitledPanel(useVariableFilter, variableFilterUI, true, true), gbc, "gridy=3");
GridBagUtils.addToPanel(filterPanel, new JLabel(), gbc, "gridy=4,weighty=1");
filterPanel.setPreferredSize(new Dimension(460, 800));
filterPanel.setMinimumSize(new Dimension(460, 120));
filterPanel.setMaximumSize(new Dimension(460, 800));
final JPanel downloadButtonPanel = new JPanel(new BorderLayout(8, 5));
downloadButtonPanel.setBorder(new EmptyBorder(8, 8, 8, 8));
cancelButton = new JButton("Cancel");
final DownloadProgressBarPM pm = new DownloadProgressBarPM(progressBar, preMessageLabel, postMessageLabel, cancelButton);
progressBar.setVisible(false);
File downloadDir = new File(preferences.get(PROPERTY_KEY_DOWNLOAD_DIR, Places.getUserDirectory().getAbsolutePath()));
if (!downloadDir.isDirectory()) {
downloadDir = new File(Places.getUserDirectory().getAbsolutePath());
}
folderTextField = new JTextField(downloadDir.getAbsolutePath());
JButton folderChooserButton = new JButton("...");
folderChooserButton.addActionListener(e -> fetchDownloadDirectory());
downloadButton = new JButton("Download");
downloadButton.setEnabled(false);
final DownloadAction downloadAction = createDownloadAction(pm);
downloadButton.addActionListener(downloadAction);
downloadButtonPanel.add(openInVisat, BorderLayout.NORTH);
downloadButtonPanel.add(folderTextField);
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
buttonPanel.add(folderChooserButton);
buttonPanel.add(downloadButton);
cancelButton.setEnabled(false);
downloadButton.addActionListener(e -> cancelButton.setEnabled(true));
cancelButton.addActionListener(e -> {
downloadAction.cancel();
cancelButton.setEnabled(false);
});
buttonPanel.add(cancelButton);
downloadButtonPanel.add(buttonPanel, BorderLayout.EAST);
JPanel centerRightPane = new JPanel(new BorderLayout());
final SimpleScrollPane simpleScrollPane = new SimpleScrollPane(filterPanel, JideScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
JideScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
simpleScrollPane.setBorder(BorderFactory.createEmptyBorder());
centerRightPane.add(simpleScrollPane, BorderLayout.CENTER);
centerRightPane.add(downloadButtonPanel, BorderLayout.SOUTH);
final JSplitPane centerPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, centerLeftPane, centerRightPane);
centerPanel.setResizeWeight(1);
centerPanel.setContinuousLayout(true);
this.setLayout(new BorderLayout(15, 15));
this.setBorder(new EmptyBorder(8, 8, 8, 8));
this.add(urlPanel, BorderLayout.NORTH);
this.add(centerPanel, BorderLayout.CENTER);
this.add(statusBar, BorderLayout.SOUTH);
}
private File fetchDownloadDirectory() {
FolderChooser folderChooser = new FolderChooser();
folderChooser.setRecentListVisible(false);
folderChooser.setCurrentDirectory(new File(folderTextField.getText()));
folderChooser.setNavigationFieldVisible(true);
folderChooser.setFileHidingEnabled(true);
int result = folderChooser.showOpenDialog(OpendapAccessPanel.this);
if (FolderChooser.APPROVE_OPTION == result) {
String downloadDirPath = folderChooser.getSelectedFile().getAbsolutePath();
preferences.put(PROPERTY_KEY_DOWNLOAD_DIR, downloadDirPath);
folderTextField.setText(downloadDirPath);
return folderChooser.getSelectedFile();
} else {
return null;
}
}
private DownloadAction createDownloadAction(DownloadProgressBarPM pm) {
return new DownloadAction(pm, new ParameterProviderImpl(), new DownloadAction.DownloadHandler() {
@Override
public void handleException(Exception e) {
SnapApp.getDefault().handleError("Unable to perform download. Reason: " + e.getMessage(), e);
}
@Override
public void handleDownloadFinished(File downloadedFile) {
if (openInVisat.isSelected()) {
OpenProductAction openProductAction = new OpenProductAction();
openProductAction.setFile(downloadedFile);
openProductAction.execute();
}
}
});
}
private boolean refresh() {
String url;
if (urlField.getSelectedItem() == null) {
url = urlField.getEditor().getItem().toString();
} else {
url = urlField.getSelectedItem().toString();
}
url = checkCatalogURLString(url);
final InvCatalogFactory factory = InvCatalogFactory.getDefaultFactory(true);
final InvCatalog catalog = factory.readXML(url);
final List<InvDataset> datasets = catalog.getDatasets();
if (datasets.size() == 0) {
JOptionPane.showMessageDialog(this, "Cannnot find THREDDS catalog service xml at '" + url + "'");
return false;
}
urlField.setSelectedItem(url);
catalogTree.setNewRootDatasets(datasets);
variableFilter.stopFiltering();
return true;
}
private String checkCatalogURLString(String url) {
if (url.endsWith("catalog.xml")) {
return url;
} else if (url.endsWith("catalog.html")) {
return url.substring(0, url.lastIndexOf("h")).concat("xml");
} else if (url.endsWith("/")) {
return url.concat("catalog.xml");
} else {
return url.concat("/catalog.xml");
}
}
private class DefaultFilterChangeListener implements FilterChangeListener {
@Override
public void filterChanged() {
final OpendapLeaf[] leaves = catalogTree.getLeaves();
for (OpendapLeaf leaf : leaves) {
filterLeaf(leaf);
}
}
}
private class ParameterProviderImpl implements DownloadAction.ParameterProvider {
Map<String, Boolean> dapURIs = new HashMap<>();
List<String> fileURIs = new ArrayList<>();
private boolean mayAlwaysOverwrite = false;
private boolean mayNeverOverwrite = false;
@Override
public Map<String, Boolean> getDapURIs() {
if (dapURIs.isEmpty() && fileURIs.isEmpty()) {
collectURIs();
}
return new HashMap<>(dapURIs);
}
@Override
public List<String> getFileURIs() {
if (dapURIs.isEmpty() && fileURIs.isEmpty()) {
collectURIs();
}
return new ArrayList<>(fileURIs);
}
private void collectURIs() {
final TreePath[] selectionPaths = ((JTree) catalogTree.getComponent()).getSelectionModel().getSelectionPaths();
if (selectionPaths == null || selectionPaths.length <= 0) {
return;
}
for (TreePath selectionPath : selectionPaths) {
final DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) selectionPath.getLastPathComponent();
if (CatalogTreeUtils.isDapNode(treeNode) || CatalogTreeUtils.isFileNode(treeNode)) {
final OpendapLeaf leaf = (OpendapLeaf) treeNode.getUserObject();
if (leaf.isDapAccess()) {
dapURIs.put(leaf.getDapUri(), leaf.getFileSize() >= 2 * 1024 * 1024);
} else if (leaf.isFileAccess()) {
fileURIs.add(leaf.getFileUri());
}
}
}
}
@Override
public void reset() {
dapURIs.clear();
fileURIs.clear();
mayAlwaysOverwrite = false;
mayNeverOverwrite = false;
}
@Override
public double getDatasizeInKb() {
return currentDataSize;
}
@Override
public File getTargetDirectory() {
final File targetDirectory;
if (folderTextField.getText() == null || folderTextField.getText().equals("")) {
targetDirectory = fetchDownloadDirectory();
} else {
targetDirectory = new File(folderTextField.getText());
}
return targetDirectory;
}
@Override
public boolean inquireOverwritePermission(String filename) {
if (mayAlwaysOverwrite) {
return true;
}
if (mayNeverOverwrite) {
return false;
}
boolean mayOverwrite = false;
String[] options = {
"Overwrite",
"Always overwrite",
"Skip this file",
"Never overwrite"
};
int result = JOptionPane.showOptionDialog(OpendapAccessPanel.this,
"Target file '" + filename + "' already exists.",
"Target file already exists",
JOptionPane.DEFAULT_OPTION,
JOptionPane.QUESTION_MESSAGE,
null, options, "Yes");
switch (result) {
case 0:
mayOverwrite = true;
break;
case 1:
mayOverwrite = true;
mayAlwaysOverwrite = true;
break;
case 2:
mayOverwrite = false;
break;
case 3:
mayOverwrite = false;
mayNeverOverwrite = true;
break;
}
return mayOverwrite;
}
}
private class DefaultLeafSelectionListener implements CatalogTree.LeafSelectionListener {
@Override
public void dapLeafSelected(final OpendapLeaf leaf) {
setText(leaf);
}
@Override
public void fileLeafSelected(OpendapLeaf leaf) {
setText(leaf);
}
private void setText(final OpendapLeaf leaf) {
new SwingWorker<Void, Void>() {
@Override
protected Void doInBackground() throws Exception {
updateStatusBar("Retrieving metadata...");
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
setMetadataText(metaInfoArea.getSelectedIndex(), leaf);
return null;
}
@Override
protected void done() {
updateStatusBar("Ready.");
setCursor(Cursor.getDefaultCursor());
}
}.execute();
}
@Override
public void leafSelectionChanged(boolean isSelected, OpendapLeaf dapObject) {
int dataSize = dapObject.getFileSize();
currentDataSize += isSelected ? dataSize : -dataSize;
if (currentDataSize <= 0) {
updateStatusBar("Ready.");
downloadButton.setEnabled(false);
} else {
downloadButton.setEnabled(true);
double dataSizeInMB = currentDataSize / 1024.0;
updateStatusBar("Total size of currently selected files: " + OpendapUtils.format(dataSizeInMB) + " MB");
}
}
}
}