/* * 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.windows; import com.bc.ceres.glayer.Layer; import com.bc.ceres.glayer.swing.LayerCanvas; import com.bc.ceres.glayer.swing.LayerCanvasModel; import com.bc.ceres.grender.AdjustableView; import com.bc.ceres.grender.Viewport; 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.util.math.MathUtils; import org.esa.snap.netbeans.docwin.WindowUtilities; import org.esa.snap.rcp.SnapApp; import org.esa.snap.rcp.actions.help.HelpAction; import org.esa.snap.rcp.actions.tools.SyncImageCursorsAction; import org.esa.snap.rcp.actions.tools.SyncImageViewsAction; import org.esa.snap.rcp.nav.NavigationCanvas; import org.esa.snap.ui.GridBagUtils; import org.esa.snap.ui.UIUtils; import org.esa.snap.ui.product.ProductSceneView; import org.esa.snap.ui.tool.ToolButtonFactory; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.Utilities; import org.openide.windows.TopComponent; import javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.JFormattedTextField; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSlider; import javax.swing.JSpinner; import javax.swing.JTextField; import javax.swing.SpinnerNumberModel; import javax.swing.text.NumberFormatter; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Dimension; import java.awt.GridBagConstraints; import java.awt.event.FocusAdapter; import java.awt.event.FocusEvent; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.Locale; import static java.lang.Math.*; @TopComponent.Description( preferredID = "NavigationTopComponent", iconBase = "org/esa/snap/rcp/icons/Navigation16.gif", persistenceType = TopComponent.PERSISTENCE_ALWAYS ) @TopComponent.Registration( mode = "navigator", openAtStartup = true, position = 10 ) @ActionID(category = "Window", id = "org.esa.snap.rcp.window.NavigationTopComponent") @ActionReference(path = "Menu/View/Tool Windows", position = 0) @TopComponent.OpenActionRegistration( displayName = "#CTL_NavigationTopComponentName", preferredID = "NavigationTopComponent" ) @NbBundle.Messages({ "CTL_NavigationTopComponentName=Navigation", "CTL_NavigationTopComponentDescription=Navigates through the currently selected image view", }) public class NavigationTopComponent extends TopComponent { public static final String ID = NavigationTopComponent.class.getName(); private static final int MIN_SLIDER_VALUE = -100; private static final int MAX_SLIDER_VALUE = +100; // TODO: Make sure help page is available for ID private static final String HELP_ID = "showNavigationWnd"; private LayerCanvasModelChangeHandler layerCanvasModelChangeChangeHandler; private ProductNodeListener productNodeChangeHandler; private ProductSceneView currentView; private NavigationCanvas canvas; private AbstractButton zoomInButton; private AbstractButton zoomDefaultButton; private AbstractButton zoomOutButton; private AbstractButton zoomAllButton; private AbstractButton syncViewsButton; private AbstractButton syncCursorButton; private JTextField zoomFactorField; private JFormattedTextField rotationAngleField; private JSlider zoomSlider; private boolean inUpdateMode; private DecimalFormat scaleFormat; private Color zeroRotationAngleBackground; private final Color positiveRotationAngleBackground = new Color(221, 255, 221); //#ddffdd private final Color negativeRotationAngleBackground = new Color(255, 221, 221); //#ffdddd private JSpinner rotationAngleSpinner; public NavigationTopComponent() { initComponent(); SnapApp.getDefault().getSelectionSupport(ProductSceneView.class).addHandler((oldValue, newValue) -> setCurrentView(newValue)); } public void initComponent() { layerCanvasModelChangeChangeHandler = new LayerCanvasModelChangeHandler(); productNodeChangeHandler = createProductNodeListener(); final DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(Locale.ENGLISH); scaleFormat = new DecimalFormat("#####.##", decimalFormatSymbols); scaleFormat.setGroupingUsed(false); scaleFormat.setDecimalSeparatorAlwaysShown(false); zoomInButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/ZoomIn24.gif"), false); zoomInButton.setToolTipText("Zoom in."); /*I18N*/ zoomInButton.setName("zoomInButton"); zoomInButton.addActionListener(e -> zoom(getCurrentView().getZoomFactor() * 1.2)); zoomOutButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/ZoomOut24.gif"), false); zoomOutButton.setName("zoomOutButton"); zoomOutButton.setToolTipText("Zoom out."); /*I18N*/ zoomOutButton.addActionListener(e -> zoom(getCurrentView().getZoomFactor() / 1.2)); zoomDefaultButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/ZoomPixel24.gif"), false); zoomDefaultButton.setToolTipText("Actual Pixels (image pixel = view pixel)."); /*I18N*/ zoomDefaultButton.setName("zoomDefaultButton"); zoomDefaultButton.addActionListener(e -> zoomToPixelResolution()); zoomAllButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/ZoomAll24.gif"), false); zoomAllButton.setName("zoomAllButton"); zoomAllButton.setToolTipText("Zoom all."); /*I18N*/ zoomAllButton.addActionListener(e -> zoomAll()); syncViewsButton = ToolButtonFactory.createButton(new SyncImageViewsAction(), true); syncViewsButton.setName("syncViewsButton"); syncViewsButton.setText(null); syncCursorButton = ToolButtonFactory.createButton(new SyncImageCursorsAction(), true); syncCursorButton.setName("syncCursorButton"); syncCursorButton.setText(null); AbstractButton helpButton = ToolButtonFactory.createButton(new HelpAction(this), false); helpButton.setName("helpButton"); final JPanel eastPane = GridBagUtils.createPanel(); final GridBagConstraints gbc = new GridBagConstraints(); gbc.anchor = GridBagConstraints.NORTHWEST; gbc.fill = GridBagConstraints.NONE; gbc.weightx = 0.0; gbc.weighty = 0.0; gbc.gridy = 0; eastPane.add(zoomInButton, gbc); gbc.gridy++; eastPane.add(zoomOutButton, gbc); gbc.gridy++; eastPane.add(zoomDefaultButton, gbc); gbc.gridy++; eastPane.add(zoomAllButton, gbc); gbc.gridy++; eastPane.add(syncViewsButton, gbc); gbc.gridy++; eastPane.add(syncCursorButton, gbc); gbc.gridy++; gbc.weighty = 1.0; gbc.fill = GridBagConstraints.VERTICAL; eastPane.add(new JLabel(" "), gbc); // filler gbc.fill = GridBagConstraints.NONE; gbc.weighty = 0.0; gbc.gridy++; eastPane.add(helpButton, gbc); zoomFactorField = new JTextField(); zoomFactorField.setColumns(8); zoomFactorField.setHorizontalAlignment(JTextField.CENTER); zoomFactorField.addActionListener(e -> handleZoomFactorFieldUserInput()); zoomFactorField.addFocusListener(new FocusAdapter() { @Override public void focusLost(final FocusEvent e) { handleZoomFactorFieldUserInput(); } }); rotationAngleSpinner = new JSpinner(new SpinnerNumberModel(0.0, -1800.0, 1800.0, 5.0)); final JSpinner.NumberEditor editor = (JSpinner.NumberEditor) rotationAngleSpinner.getEditor(); rotationAngleField = editor.getTextField(); final DecimalFormat rotationFormat; rotationFormat = new DecimalFormat("#####.##°", decimalFormatSymbols); rotationFormat.setGroupingUsed(false); rotationFormat.setDecimalSeparatorAlwaysShown(false); rotationAngleField.setFormatterFactory(new JFormattedTextField.AbstractFormatterFactory() { @Override public JFormattedTextField.AbstractFormatter getFormatter(JFormattedTextField tf) { return new NumberFormatter(rotationFormat); } }); rotationAngleField.setColumns(6); rotationAngleField.setEditable(true); rotationAngleField.setHorizontalAlignment(JTextField.CENTER); rotationAngleField.addActionListener(e -> handleRotationAngleFieldUserInput()); rotationAngleField.addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { handleRotationAngleFieldUserInput(); } }); rotationAngleField.addPropertyChangeListener("value", evt -> handleRotationAngleFieldUserInput()); zoomSlider = new JSlider(JSlider.HORIZONTAL); zoomSlider.setValue(0); zoomSlider.setMinimum(MIN_SLIDER_VALUE); zoomSlider.setMaximum(MAX_SLIDER_VALUE); zoomSlider.setPaintTicks(false); zoomSlider.setPaintLabels(false); zoomSlider.setSnapToTicks(false); zoomSlider.setPaintTrack(true); zoomSlider.addChangeListener(e -> { if (!inUpdateMode) { zoom(sliderValueToZoomFactor(zoomSlider.getValue())); } }); final JPanel zoomFactorPane = new JPanel(new BorderLayout()); zoomFactorPane.add(zoomFactorField, BorderLayout.WEST); final JPanel rotationAnglePane = new JPanel(new BorderLayout()); rotationAnglePane.add(rotationAngleSpinner, BorderLayout.EAST); rotationAnglePane.add(new JLabel(" "), BorderLayout.CENTER); final JPanel sliderPane = new JPanel(new BorderLayout(2, 2)); sliderPane.add(zoomFactorPane, BorderLayout.WEST); sliderPane.add(zoomSlider, BorderLayout.CENTER); sliderPane.add(rotationAnglePane, BorderLayout.EAST); canvas = createNavigationCanvas(); canvas.setBackground(new Color(138, 133, 128)); // image background canvas.setForeground(new Color(153, 153, 204)); // slider overlay final JPanel centerPane = new JPanel(new BorderLayout(4, 4)); centerPane.add(BorderLayout.CENTER, canvas); centerPane.add(BorderLayout.SOUTH, sliderPane); setLayout(new BorderLayout(4, 4)); setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); add(centerPane, BorderLayout.CENTER); add(eastPane, BorderLayout.EAST); setPreferredSize(new Dimension(320, 320)); updateCurrentView(); updateState(); } private void updateCurrentView() { setCurrentView(Utilities.actionsGlobalContext().lookup(ProductSceneView.class)); } public ProductSceneView getCurrentView() { return currentView; } public void setCurrentView(final ProductSceneView newView) { if (currentView != newView) { final ProductSceneView oldView = currentView; if (oldView != null) { Product product = oldView.getProduct(); if (product != null) { product.removeProductNodeListener(productNodeChangeHandler); } LayerCanvas layerCanvas = oldView.getLayerCanvas(); if (layerCanvas != null) { layerCanvas.getModel().removeChangeListener(layerCanvasModelChangeChangeHandler); } } currentView = newView; if (currentView != null) { Product product = currentView.getProduct(); if (product != null) { product.addProductNodeListener(productNodeChangeHandler); } LayerCanvas layerCanvas = currentView.getLayerCanvas(); if (layerCanvas != null) { layerCanvas.getModel().addChangeListener(layerCanvasModelChangeChangeHandler); } } canvas.handleViewChanged(oldView, newView); updateState(); } } @Override public HelpCtx getHelpCtx() { return new HelpCtx(HELP_ID); } NavigationCanvas createNavigationCanvas() { return new NavigationCanvas(this); } private void handleZoomFactorFieldUserInput() { Double zf = getZoomFactorFieldValue(); if (zf != null) { updateScaleField(zf); zoom(zf); } } private void handleRotationAngleFieldUserInput() { final double ra = Math.round(getRotationAngleFieldValue() / 5) * 5.0; updateRotationField(ra); rotate(ra); } private Double getZoomFactorFieldValue() { final String text = zoomFactorField.getText(); if (text.contains(":")) { return parseTextualValue(text); } else { try { double v = Double.parseDouble(text); return v > 0 ? v : null; } catch (NumberFormatException e) { return null; } } } private double getRotationAngleFieldValue() { String text = rotationAngleField.getText(); if (text != null) { while (text.endsWith("°")) { text = text.substring(0, text.length() - 1); } try { final double v = Double.parseDouble(text); final double max = 360 * 100; final double negMax = max * -1; if (v > max || v < negMax) { return 0.0; } return v; } catch (NumberFormatException e) { // ok } } return 0.0; } private static Double parseTextualValue(String text) { final String[] numbers = text.split(":"); if (numbers.length == 2) { double dividend; double divisor; try { dividend = Double.parseDouble(numbers[0]); divisor = Double.parseDouble(numbers[1]); } catch (NumberFormatException e) { return null; } if (divisor == 0) { return null; } double factor = dividend / divisor; return factor > 0 ? factor : null; } else { return null; } } public void setModelOffset(final double modelOffsetX, final double modelOffsetY) { final ProductSceneView view = getCurrentView(); if (view != null) { view.getLayerCanvas().getViewport().setOffset(modelOffsetX, modelOffsetY); maybeSynchronizeCompatibleProductViews(); } } private void zoomToPixelResolution() { final ProductSceneView view = getCurrentView(); if (view != null) { final LayerCanvas layerCanvas = view.getLayerCanvas(); layerCanvas.getViewport().setZoomFactor(layerCanvas.getDefaultZoomFactor()); maybeSynchronizeCompatibleProductViews(); } } public void zoom(final double zoomFactor) { final ProductSceneView view = getCurrentView(); if (view != null && zoomFactor > 0) { view.getLayerCanvas().getViewport().setZoomFactor(zoomFactor); maybeSynchronizeCompatibleProductViews(); } } private void rotate(Double rotationAngle) { final ProductSceneView view = getCurrentView(); if (view != null) { view.getLayerCanvas().getViewport().setOrientation(rotationAngle * MathUtils.DTOR); maybeSynchronizeCompatibleProductViews(); } } public void zoomAll() { final ProductSceneView view = getCurrentView(); if (view != null) { view.getLayerCanvas().zoomAll(); maybeSynchronizeCompatibleProductViews(); } } private void maybeSynchronizeCompatibleProductViews() { if (syncViewsButton.isSelected()) { synchronizeCompatibleProductViews(); } } private void synchronizeCompatibleProductViews() { final ProductSceneView currentView = getCurrentView(); if (currentView == null) { return; } WindowUtilities.getOpened(ProductSceneViewTopComponent.class).forEach(productSceneViewTopComponent -> { final ProductSceneView view = productSceneViewTopComponent.getView(); if (view != currentView) { currentView.synchronizeViewportIfPossible(view); } }); } /** * @param sv a value between MIN_SLIDER_VALUE and MAX_SLIDER_VALUE * @return a value between min and max zoom factor of the AdjustableView */ private double sliderValueToZoomFactor(final int sv) { AdjustableView adjustableView = getCurrentView().getLayerCanvas(); double f1 = scaleExp2Min(adjustableView); double f2 = scaleExp2Max(adjustableView); double s1 = zoomSlider.getMinimum(); double s2 = zoomSlider.getMaximum(); double v1 = (sv - s1) / (s2 - s1); double v2 = f1 + v1 * (f2 - f1); return exp2(v2); } /** * @param zf a value between min and max zoom factor of the AdjustableView * @return a value between MIN_SLIDER_VALUE and MAX_SLIDER_VALUE */ private int zoomFactorToSliderValue(final double zf) { AdjustableView adjustableView = getCurrentView().getLayerCanvas(); double f1 = scaleExp2Min(adjustableView); double f2 = scaleExp2Max(adjustableView); double s1 = zoomSlider.getMinimum(); double s2 = zoomSlider.getMaximum(); double v2 = log2(zf); double v1 = max(0.0, min(1.0, (v2 - f1) / (f2 - f1))); return (int) ((s1 + v1 * (s2 - s1)) + 0.5); } private void updateState() { final boolean canNavigate = getCurrentView() != null; zoomInButton.setEnabled(canNavigate); zoomDefaultButton.setEnabled(canNavigate); zoomOutButton.setEnabled(canNavigate); zoomAllButton.setEnabled(canNavigate); zoomSlider.setEnabled(canNavigate); syncViewsButton.setEnabled(canNavigate); syncCursorButton.setEnabled(canNavigate); zoomFactorField.setEnabled(canNavigate); rotationAngleSpinner.setEnabled(canNavigate); updateTitle(); updateValues(); } private void updateTitle() { if (getCurrentView() != null) { if (getCurrentView().isRGB()) { setDisplayName(Bundle.CTL_NavigationTopComponentName() + " - " + getCurrentView().getProduct().getProductRefString() + " RGB"); } else { setDisplayName(Bundle.CTL_NavigationTopComponentName() + " - " + getCurrentView().getRaster().getDisplayName()); } } else { setDisplayName(Bundle.CTL_NavigationTopComponentName()); } } private void updateValues() { final ProductSceneView view = getCurrentView(); if (view != null) { boolean oldState = inUpdateMode; inUpdateMode = true; double zf = view.getZoomFactor(); updateZoomSlider(zf); updateScaleField(zf); updateRotationField(view.getOrientation() * MathUtils.RTOD); inUpdateMode = oldState; } } private void updateZoomSlider(double zf) { int sv = zoomFactorToSliderValue(zf); zoomSlider.setValue(sv); } private void updateScaleField(double zf) { String text; if (zf > 1.0) { text = scaleFormat.format(roundScale(zf)) + " : 1"; } else if (zf < 1.0) { text = "1 : " + scaleFormat.format(roundScale(1.0 / zf)); } else { text = "1 : 1"; } zoomFactorField.setText(text); } private void updateRotationField(double ra) { while (ra > 180) { ra -= 360; } while (ra < -180) { ra += 360; } rotationAngleField.setValue(ra); if (zeroRotationAngleBackground == null) { zeroRotationAngleBackground = rotationAngleField.getBackground(); } if (ra > 0) { rotationAngleField.setBackground(positiveRotationAngleBackground); } else if (ra < 0) { rotationAngleField.setBackground(negativeRotationAngleBackground); } else { rotationAngleField.setBackground(zeroRotationAngleBackground); } } private static double roundScale(double x) { double e = floor((log10(x))); double f = 10.0 * pow(10.0, e); double fx = x * f; double rfx = round(fx); if (abs((rfx + 0.5) - fx) <= abs(rfx - fx)) { rfx += 0.5; } return rfx / f; } private static double scaleExp2Min(AdjustableView adjustableView) { return floor(log2(adjustableView.getMinZoomFactor())); } private static double scaleExp2Max(AdjustableView adjustableView) { return floor(log2(adjustableView.getMaxZoomFactor()) + 1); } private static double log2(double x) { return log(x) / log(2.0); } private static double exp2(double x) { return pow(2.0, x); } private ProductNodeListener createProductNodeListener() { return new ProductNodeListenerAdapter() { @Override public void nodeChanged(final ProductNodeEvent event) { if (event.getPropertyName().equalsIgnoreCase(Product.PROPERTY_NAME_NAME)) { final ProductNode sourceNode = event.getSourceNode(); if ((getCurrentView().isRGB() && sourceNode == getCurrentView().getProduct()) || sourceNode == getCurrentView().getRaster()) { updateTitle(); } } } }; } private class LayerCanvasModelChangeHandler implements LayerCanvasModel.ChangeListener { @Override public void handleLayerPropertyChanged(Layer layer, PropertyChangeEvent event) { } @Override public void handleLayerDataChanged(Layer layer, Rectangle2D modelRegion) { } @Override public void handleLayersAdded(Layer parentLayer, Layer[] childLayers) { } @Override public void handleLayersRemoved(Layer parentLayer, Layer[] childLayers) { } @Override public void handleViewportChanged(Viewport viewport, boolean orientationChanged) { updateValues(); } } }