/*
* Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
*
* 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.rcp.pixelinfo;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductNode;
import org.esa.snap.core.datamodel.ProductNodeEvent;
import org.esa.snap.core.datamodel.ProductNodeListener;
import org.esa.snap.core.datamodel.ProductNodeListenerAdapter;
import org.esa.snap.core.datamodel.RasterDataNode;
import org.esa.snap.netbeans.docwin.WindowUtilities;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.CollapsibleItemsPanel;
import org.esa.snap.rcp.windows.ProductSceneViewTopComponent;
import org.esa.snap.ui.UIUtils;
import org.esa.snap.ui.product.ProductSceneView;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ScrollPaneConstants;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableModel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Vector;
import java.util.prefs.PreferenceChangeEvent;
import java.util.prefs.PreferenceChangeListener;
import java.util.prefs.Preferences;
/**
* The pixel info view component is used to display the geophysical values for the pixel at a given pixel position
* (x,y). The pixel info view can simultaneously display band, tie point grid and flag values.
*
* @author Norman Fomferra
* @author Sabine Embacher
* @version 1.2
*/
public class PixelInfoView extends JPanel {
public static final String HELP_ID = "pixelInfoView";
/**
* Preferences key for showing all band pixel values in pixel info view
*/
public static final String PREFERENCE_KEY_SHOW_ONLY_DISPLAYED_BAND_PIXEL_VALUES = "pixelview.showOnlyDisplayedBands";
/**
* Preferences key for display style of geo-locations
*/
public static final String PREFERENCE_KEY_SHOW_GEO_POS_DECIMALS = "pixelview.showGeoPosDecimals";
/**
* Preferences key for showing floating-point image coordinates
*/
public static final String PREFERENCE_KEY_SHOW_PIXEL_POS_DECIMALS = "pixelview.showPixelPosDecimals";
/**
* Preferences key for coordinate system starting at (1,1)
*/
public static final String PREFERENCE_KEY_SHOW_PIXEL_POS_OFFSET_ONE = "pixelview.showPixelPosOffsetOne";
public static final boolean PREFERENCE_DEFAULT_SHOW_DISPLAYED_BAND_PIXEL_VALUES = true;
public static final boolean PREFERENCE_DEFAULT_SHOW_GEO_POS_DECIMALS = false;
public static final boolean PREFERENCE_DEFAULT_SHOW_PIXEL_POS_DECIMALS = false;
public static final boolean PREFERENCE_DEFAULT_SHOW_PIXEL_POS_OFFSET_1 = false;
private static final int NAME_COLUMN = 0;
private static final int VALUE_COLUMN = 1;
private static final int UNIT_COLUMN = 2;
private boolean showGeoPosDecimals;
public static final int POSITION_INDEX = 0;
public static final int TIME_INDEX = 1;
public static final int BANDS_INDEX = 2;
public static final int TIE_POINT_GRIDS_INDEX = 3;
public static final int FLAGS_INDEX = 4;
private final PropertyChangeListener displayFilterListener;
private final ProductNodeListener productNodeListener;
private boolean showPixelPosDecimals;
private boolean showPixelPosOffset1;
private DisplayFilter displayFilter;
private final PixelInfoViewTableModel positionTableModel;
private final PixelInfoViewTableModel timeTableModel;
private final PixelInfoViewTableModel bandsTableModel;
private final PixelInfoViewTableModel tiePointGridsTableModel;
private final PixelInfoViewTableModel flagsTableModel;
private final PixelInfoViewModelUpdater modelUpdater;
private final PixelInfoUpdateService updateService;
private CollapsibleItemsPanel collapsibleItemsPanel;
/**
* Constructs a new pixel info view.
*/
public PixelInfoView() {
super(new BorderLayout());
displayFilterListener = createDisplayFilterListener();
productNodeListener = createProductNodeListener();
positionTableModel = new PixelInfoViewTableModel(new String[]{"Position", "Value", "Unit"});
timeTableModel = new PixelInfoViewTableModel(new String[]{"Time", "Value", "Unit"});
bandsTableModel = new PixelInfoViewTableModel(new String[]{"Band", "Value", "Unit"});
tiePointGridsTableModel = new PixelInfoViewTableModel(new String[]{"Tie-Point Grid", "Value", "Unit"});
flagsTableModel = new PixelInfoViewTableModel(new String[]{"Flag", "Value",});
modelUpdater = new PixelInfoViewModelUpdater(this, positionTableModel,
timeTableModel,
bandsTableModel,
tiePointGridsTableModel,
flagsTableModel
);
updateService = new PixelInfoUpdateService(modelUpdater);
setDisplayFilter(new DisplayFilter());
final Preferences preferences = SnapApp.getDefault().getPreferences();
preferences.addPreferenceChangeListener(new PreferenceChangeListener() {
@Override
public void preferenceChange(PreferenceChangeEvent evt) {
final String propertyName = evt.getKey();
if (PREFERENCE_KEY_SHOW_ONLY_DISPLAYED_BAND_PIXEL_VALUES.equals(propertyName)) {
setShowOnlyLoadedBands(preferences);
} else if (PREFERENCE_KEY_SHOW_PIXEL_POS_DECIMALS.equals(propertyName)) {
setShowPixelPosDecimals(preferences);
} else if (PREFERENCE_KEY_SHOW_GEO_POS_DECIMALS.equals(propertyName)) {
setShowGeoPosDecimals(preferences);
} else if (PREFERENCE_KEY_SHOW_PIXEL_POS_OFFSET_ONE.equals(propertyName)) {
setShowPixelPosOffset1(preferences);
}
}
});
setShowOnlyLoadedBands(preferences);
setShowPixelPosDecimals(preferences);
setShowGeoPosDecimals(preferences);
setShowPixelPosOffset1(preferences);
createUI();
}
ProductNodeListener getProductNodeListener() {
return productNodeListener;
}
private ProductNodeListener createProductNodeListener() {
return new ProductNodeListenerAdapter() {
@Override
public void nodeChanged(ProductNodeEvent event) {
updateService.requestUpdate();
}
@Override
public void nodeAdded(ProductNodeEvent event) {
updateService.requestUpdate();
}
@Override
public void nodeRemoved(ProductNodeEvent event) {
updateService.requestUpdate();
}
};
}
private PropertyChangeListener createDisplayFilterListener() {
return evt -> {
if (getCurrentProduct() != null) {
updateService.requestUpdate();
clearSelectionInRasterTables();
}
};
}
/**
* Returns the current product
*
* @return the current Product
*/
public Product getCurrentProduct() {
return modelUpdater.getCurrentProduct();
}
/**
* Sets the filter to be used to filter the displayed bands. <p>
*
* @param displayFilter the filter, can be null
*/
public void setDisplayFilter(DisplayFilter displayFilter) {
if (this.displayFilter != displayFilter) {
if (this.displayFilter != null) {
this.displayFilter.removePropertyChangeListener(displayFilterListener);
}
this.displayFilter = displayFilter;
this.displayFilter.addPropertyChangeListener(displayFilterListener);
}
}
/**
* Returns the display filter
*
* @return the display filter, can be null
*/
public DisplayFilter getDisplayFilter() {
return displayFilter;
}
private void setShowPixelPosDecimals(boolean showPixelPosDecimals) {
if (this.showPixelPosDecimals != showPixelPosDecimals) {
this.showPixelPosDecimals = showPixelPosDecimals;
updateService.requestUpdate();
}
}
boolean getShowPixelPosDecimal() {
return showPixelPosDecimals;
}
private void setShowGeoPosDecimals(boolean showGeoPosDecimals) {
if (this.showGeoPosDecimals != showGeoPosDecimals) {
this.showGeoPosDecimals = showGeoPosDecimals;
updateService.requestUpdate();
}
}
boolean getShowGeoPosDecimals() {
return showGeoPosDecimals;
}
private void setShowPixelPosOffset1(boolean showPixelPosOffset1) {
if (this.showPixelPosOffset1 != showPixelPosOffset1) {
this.showPixelPosOffset1 = showPixelPosOffset1;
updateService.requestUpdate();
}
}
public boolean getShowPixelPosOffset1() {
return showPixelPosOffset1;
}
public void updatePixelValues(ProductSceneView view, int pixelX, int pixelY, int level, boolean pixelPosValid) {
updateService.updateState(view, pixelX, pixelY, level, pixelPosValid);
}
private void createUI() {
DefaultTableCellRenderer pixelValueRenderer = new ValueCellRenderer();
FlagCellRenderer flagCellRenderer = new FlagCellRenderer();
setLayout(new BorderLayout());
CollapsibleItemsPanel.Item<JTable> positionItem = CollapsibleItemsPanel.createTableItem("Position", 6, 3);
positionItem.getComponent().setModel(positionTableModel);
positionItem.getComponent().getColumnModel().getColumn(1).setCellRenderer(pixelValueRenderer);
CollapsibleItemsPanel.Item<JTable> timeItem = CollapsibleItemsPanel.createTableItem("Time", 2, 3);
timeItem.getComponent().setModel(timeTableModel);
timeItem.getComponent().getColumnModel().getColumn(1).setCellRenderer(pixelValueRenderer);
CollapsibleItemsPanel.Item<JTable> tiePointGridsItem = CollapsibleItemsPanel.createTableItem("Tie-Point Grids", 0, 3);
tiePointGridsItem.getComponent().setModel(tiePointGridsTableModel);
tiePointGridsItem.getComponent().getColumnModel().getColumn(1).setCellRenderer(pixelValueRenderer);
CollapsibleItemsPanel.Item<JTable> bandsItem = CollapsibleItemsPanel.createTableItem("Bands", 18, 3);
bandsItem.getComponent().setModel(bandsTableModel);
bandsItem.getComponent().getColumnModel().getColumn(1).setCellRenderer(pixelValueRenderer);
CollapsibleItemsPanel.Item<JTable> flagsItem = CollapsibleItemsPanel.createTableItem("Flags", 0, 2);
flagsItem.getComponent().setModel(flagsTableModel);
flagsItem.getComponent().getColumnModel().getColumn(1).setCellRenderer(flagCellRenderer);
collapsibleItemsPanel = new CollapsibleItemsPanel(
positionItem, // POSITION_INDEX = 0
timeItem, // TIME_INDEX = 1
bandsItem, // BANDS_INDEX = 2
tiePointGridsItem, // TIE_POINT_GRIDS_INDEX = 3
flagsItem // FLAGS_INDEX = 4
);
collapsibleItemsPanel.setCollapsed(POSITION_INDEX, false);
collapsibleItemsPanel.setCollapsed(TIME_INDEX, true);
collapsibleItemsPanel.setCollapsed(BANDS_INDEX, false);
collapsibleItemsPanel.setCollapsed(TIE_POINT_GRIDS_INDEX, true);
collapsibleItemsPanel.setCollapsed(FLAGS_INDEX, true);
collapsibleItemsPanel.addCollapseListener(new CollapsibleItemsPanel.CollapseListener() {
@Override
public void collapse(int index) {
//do nothing
}
@Override
public void expand(int index) {
updateService.requestUpdate();
}
});
JScrollPane scrollPane = new JScrollPane(collapsibleItemsPanel,
ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar().setUnitIncrement(20);
add(scrollPane, BorderLayout.CENTER);
}
void clearSelectionInRasterTables() {
final JTable bandsTable = (JTable) collapsibleItemsPanel.getItem(BANDS_INDEX).getComponent();
final JTable tiePointGridsTable = (JTable) collapsibleItemsPanel.getItem(TIE_POINT_GRIDS_INDEX).getComponent();
bandsTable.clearSelection();
tiePointGridsTable.clearSelection();
final RasterDataNode raster = modelUpdater.getCurrentRaster();
if (raster != null) {
final String rasterName = raster.getName();
if (!selectCurrentRaster(rasterName, bandsTable)) {
selectCurrentRaster(rasterName, tiePointGridsTable);
}
}
}
public void clearProductNodeRefs() {
modelUpdater.clearProductNodeRefs();
updateService.clearState();
}
boolean isCollapsiblePaneVisible(int index) {
return !collapsibleItemsPanel.isCollapsed(index);
}
private boolean selectCurrentRaster(String rasterName, JTable table) {
final TableModel model = table.getModel();
for (int i = 0; i < model.getRowCount(); i++) {
final String s = model.getValueAt(i, NAME_COLUMN).toString();
if (rasterName.equals(s)) {
table.changeSelection(i, NAME_COLUMN, false, false);
return true;
}
}
return false;
}
private void setShowOnlyLoadedBands(final Preferences preferences) {
final boolean showOnlyLoadedOrDisplayedBands = preferences.getBoolean(
PixelInfoView.PREFERENCE_KEY_SHOW_ONLY_DISPLAYED_BAND_PIXEL_VALUES,
PixelInfoView.PREFERENCE_DEFAULT_SHOW_DISPLAYED_BAND_PIXEL_VALUES);
displayFilter.setShowOnlyLoadedOrDisplayedBands(showOnlyLoadedOrDisplayedBands);
}
private void setShowPixelPosOffset1(final Preferences preferences) {
setShowPixelPosOffset1(preferences.getBoolean(
PREFERENCE_KEY_SHOW_PIXEL_POS_OFFSET_ONE,
PREFERENCE_DEFAULT_SHOW_PIXEL_POS_OFFSET_1));
}
private void setShowPixelPosDecimals(final Preferences preferences) {
setShowPixelPosDecimals(preferences.getBoolean(
PREFERENCE_KEY_SHOW_PIXEL_POS_DECIMALS,
PREFERENCE_DEFAULT_SHOW_PIXEL_POS_DECIMALS));
}
private void setShowGeoPosDecimals(final Preferences preferences) {
setShowGeoPosDecimals(preferences.getBoolean(
PREFERENCE_KEY_SHOW_GEO_POS_DECIMALS,
PREFERENCE_DEFAULT_SHOW_GEO_POS_DECIMALS));
}
private static class ValueCellRenderer extends DefaultTableCellRenderer {
Font valueFont;
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
if (valueFont == null) {
Font font = getFont();
valueFont = new Font(Font.MONOSPACED, Font.PLAIN, font != null ? font.getSize() : 12);
}
setFont(valueFont);
setHorizontalAlignment(RIGHT);
return this;
}
}
private static class FlagCellRenderer extends ValueCellRenderer {
static final Color VERY_LIGHT_BLUE = new Color(230, 230, 255);
static final Color VERY_LIGHT_RED = new Color(255, 230, 230);
@Override
public Component getTableCellRendererComponent(JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row,
int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setForeground(Color.black);
setBackground(Color.white);
if (column == VALUE_COLUMN && value != null) {
if (value.equals("true")) {
setForeground(UIUtils.COLOR_DARK_RED);
setBackground(VERY_LIGHT_BLUE);
} else if (value.equals("false")) {
setForeground(UIUtils.COLOR_DARK_BLUE);
setBackground(VERY_LIGHT_RED);
}
}
return this;
}
}
public static class DisplayFilter {
private final Vector<PropertyChangeListener> propertyChangeListeners = new Vector<>();
private boolean showOnlyLoadedOrDisplayedBands;
public void addPropertyChangeListener(PropertyChangeListener displayFilterListener) {
if (displayFilterListener != null && !propertyChangeListeners.contains(displayFilterListener)) {
propertyChangeListeners.add(displayFilterListener);
}
}
public void removePropertyChangeListener(PropertyChangeListener displayFilterListener) {
if (displayFilterListener != null && propertyChangeListeners.contains(displayFilterListener)) {
propertyChangeListeners.remove(displayFilterListener);
}
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
final PropertyChangeEvent event = new PropertyChangeEvent(this, propertyName, oldValue, newValue);
for (int i = 0; i < propertyChangeListeners.size(); i++) {
(propertyChangeListeners.elementAt(i)).propertyChange(event);
}
}
public void setShowOnlyLoadedOrDisplayedBands(boolean v) {
if (showOnlyLoadedOrDisplayedBands != v) {
final boolean oldValue = showOnlyLoadedOrDisplayedBands;
showOnlyLoadedOrDisplayedBands = v;
firePropertyChange("showOnlyLoadedOrDisplayedBands", oldValue, v);
}
}
public boolean accept(ProductNode node) {
if (node instanceof RasterDataNode) {
final RasterDataNode rasterDataNode = (RasterDataNode) node;
if (showOnlyLoadedOrDisplayedBands) {
return rasterDataNode.hasRasterData() ||
WindowUtilities.getOpened(ProductSceneViewTopComponent.class).anyMatch(
topComponent -> rasterDataNode == topComponent.getView().getRaster());
}
}
return true;
}
}
}