/* * 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.ui; import com.bc.ceres.glayer.Layer; import com.bc.ceres.glayer.LayerContext; import com.bc.ceres.glayer.swing.LayerCanvas; import com.bc.ceres.glayer.swing.WakefulComponent; import com.bc.ceres.grender.Viewport; import org.esa.snap.core.datamodel.Product; import org.esa.snap.core.util.ProductUtils; import org.esa.snap.tango.TangoIcons; import org.geotools.referencing.crs.DefaultGeographicCRS; import org.openide.util.ImageUtilities; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.JPanel; import javax.swing.event.MouseInputAdapter; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.event.MouseWheelEvent; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class displays a world map specified by the {@link WorldMapPaneDataModel}. * * @author Marco Peters * @version $Revision$ $Date$ */ public class WorldMapPane extends JPanel { private LayerCanvas layerCanvas; private Layer worldMapLayer; private final WorldMapPaneDataModel dataModel; private boolean navControlShown; private WakefulComponent navControlWrapper; private PanSupport panSupport; private MouseHandler mouseHandler; private Set<ZoomListener> zoomListeners; public WorldMapPane(WorldMapPaneDataModel dataModel) { this(dataModel, null); } public WorldMapPane(WorldMapPaneDataModel dataModel, LayerCanvas.Overlay overlay) { this.dataModel = dataModel; layerCanvas = new LayerCanvas(); this.panSupport = new DefaultPanSupport(layerCanvas); this.zoomListeners = new HashSet<>(); getLayerCanvas().getModel().getViewport().setModelYAxisDown(false); if (overlay == null) { getLayerCanvas().addOverlay(new BoundaryOverlayImpl(dataModel)); } else { getLayerCanvas().addOverlay(overlay); } final Layer rootLayer = getLayerCanvas().getLayer(); final Dimension dimension = new Dimension(400, 200); final Viewport viewport = getLayerCanvas().getViewport(); viewport.setViewBounds(new Rectangle(dimension)); setPreferredSize(dimension); setSize(dimension); setLayout(new BorderLayout()); add(getLayerCanvas(), BorderLayout.CENTER); dataModel.addModelChangeListener(new ModelChangeListener()); worldMapLayer = dataModel.getWorldMapLayer(new WorldMapLayerContext(rootLayer)); installLayerCanvasNavigation(getLayerCanvas()); getLayerCanvas().getLayer().getChildren().add(worldMapLayer); zoomAll(); setNavControlVisible(true); addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { AffineTransform transform = getLayerCanvas().getViewport().getModelToViewTransform(); Rectangle2D maxVisibleModelBounds = getLayerCanvas().getMaxVisibleModelBounds(); double minX = maxVisibleModelBounds.getMinX(); double minY = maxVisibleModelBounds.getMinY(); double maxX = maxVisibleModelBounds.getMaxX(); double maxY = maxVisibleModelBounds.getMaxY(); final Point2D upperLeft = transform.transform(new Point2D.Double(minX, minY), null); final Point2D lowerRight = transform.transform(new Point2D.Double(maxX, maxY), null); /* * We need to give the borders a minimum width/height of 1 because otherwise the intersection * operation would not work */ Rectangle2D northBorder = new Rectangle2D.Double(upperLeft.getX(), upperLeft.getY(), lowerRight.getX() - upperLeft.getX(), 1); Rectangle2D southBorder = new Rectangle2D.Double(upperLeft.getX(), lowerRight.getY(), lowerRight.getX() - upperLeft.getX(), 1); Rectangle2D westBorder = new Rectangle2D.Double(upperLeft.getX(), lowerRight.getY(), 1, upperLeft.getY() - lowerRight.getY()); Rectangle2D eastBorder = new Rectangle2D.Double(lowerRight.getX(), lowerRight.getY(), 1, upperLeft.getY() - lowerRight.getY()); Rectangle layerCanvasBounds = getLayerCanvas().getBounds(); boolean isWorldMapFullyVisible = layerCanvasBounds.intersects(northBorder) || layerCanvasBounds.intersects(southBorder) || layerCanvasBounds.intersects(westBorder) || layerCanvasBounds.intersects(eastBorder); if (isWorldMapFullyVisible) { zoomAll(); } } }); } @Override public void doLayout() { if (navControlShown && navControlWrapper != null) { navControlWrapper.setLocation(getWidth() - navControlWrapper.getWidth() - 4, 4); } super.doLayout(); } public Product getSelectedProduct() { return dataModel.getSelectedProduct(); } public Product[] getProducts() { return dataModel.getProducts(); } public float getScale() { return (float) getLayerCanvas().getViewport().getZoomFactor(); } public void zoomToProduct(Product product) { if (product == null || product.getSceneGeoCoding() == null) { return; } final GeneralPath[] generalPaths = getGeoBoundaryPaths(product); Rectangle2D modelArea = new Rectangle2D.Double(); final Viewport viewport = getLayerCanvas().getViewport(); for (GeneralPath generalPath : generalPaths) { final Rectangle2D rectangle2D = generalPath.getBounds2D(); if (modelArea.isEmpty()) { if (!viewport.isModelYAxisDown()) { modelArea.setFrame(rectangle2D.getX(), rectangle2D.getMaxY(), rectangle2D.getWidth(), rectangle2D.getHeight()); } modelArea = rectangle2D; } else { modelArea.add(rectangle2D); } } Rectangle2D modelBounds = modelArea.getBounds2D(); modelBounds.setFrame(modelBounds.getX() - 2, modelBounds.getY() - 2, modelBounds.getWidth() + 4, modelBounds.getHeight() + 4); modelBounds = cropToMaxModelBounds(modelBounds); viewport.zoom(modelBounds); fireScrolled(); } public void zoomAll() { getLayerCanvas().getViewport().zoom(worldMapLayer.getModelBounds()); fireScrolled(); } /** * None API. Don't use this method! * * @param navControlShown true, if this canvas uses a navigation control. */ public void setNavControlVisible(boolean navControlShown) { boolean oldValue = this.navControlShown; if (oldValue != navControlShown) { if (navControlShown) { final Action[] overlayActions = getOverlayActions(); final ButtonOverlayControl navControl = new ButtonOverlayControl(overlayActions.length, overlayActions); navControlWrapper = new WakefulComponent(navControl); navControlWrapper.setMinAlpha(0.3f); getLayerCanvas().add(navControlWrapper); } else { getLayerCanvas().remove(navControlWrapper); navControlWrapper = null; } validate(); this.navControlShown = navControlShown; } } public void setPanSupport(PanSupport panSupport) { layerCanvas.removeMouseListener(mouseHandler); layerCanvas.removeMouseMotionListener(mouseHandler); this.panSupport = panSupport; mouseHandler = new MouseHandler(); layerCanvas.addMouseListener(mouseHandler); layerCanvas.addMouseMotionListener(mouseHandler); } public LayerCanvas getLayerCanvas() { return layerCanvas; } static GeneralPath[] getGeoBoundaryPaths(Product product) { final int step = Math.max(16, (product.getSceneRasterWidth() + product.getSceneRasterHeight()) / 250); return ProductUtils.createGeoBoundaryPaths(product, null, step); } public boolean addZoomListener(ZoomListener zoomListener) { return zoomListeners.add(zoomListener); } public boolean removeZoomListener(ZoomListener zoomListener) { return zoomListeners.remove(zoomListener); } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); navControlWrapper.setEnabled(enabled); } protected Action[] getOverlayActions() { return new Action[]{new ZoomAllAction(), new ZoomToSelectedAction()}; } protected void fireScrolled() { for (ZoomListener zoomListener : zoomListeners) { zoomListener.zoomed(); } } private void updateUiState(PropertyChangeEvent evt) { if (WorldMapPaneDataModel.PROPERTY_LAYER.equals(evt.getPropertyName())) { exchangeWorldMapLayer(); } if (WorldMapPaneDataModel.PROPERTY_PRODUCTS.equals(evt.getPropertyName())) { repaint(); } if (WorldMapPaneDataModel.PROPERTY_SELECTED_PRODUCT.equals(evt.getPropertyName()) || WorldMapPaneDataModel.PROPERTY_AUTO_ZOOM_ENABLED.equals(evt.getPropertyName())) { final Product selectedProduct = dataModel.getSelectedProduct(); if (selectedProduct != null && dataModel.isAutoZoomEnabled()) { zoomToProduct(selectedProduct); } else { repaint(); } } if (WorldMapPaneDataModel.PROPERTY_ADDITIONAL_GEO_BOUNDARIES.equals(evt.getPropertyName())) { repaint(); } } private void exchangeWorldMapLayer() { final List<Layer> children = getLayerCanvas().getLayer().getChildren(); for (Layer child : children) { child.dispose(); } children.clear(); final Layer rootLayer = getLayerCanvas().getLayer(); worldMapLayer = dataModel.getWorldMapLayer(new WorldMapLayerContext(rootLayer)); children.add(worldMapLayer); zoomAll(); } protected Rectangle2D cropToMaxModelBounds(Rectangle2D modelBounds) { final Rectangle2D maxModelBounds = worldMapLayer.getModelBounds(); if (modelBounds.getWidth() >= maxModelBounds.getWidth() - 1 || modelBounds.getHeight() >= maxModelBounds.getHeight() - 1) { modelBounds = maxModelBounds; } return modelBounds; } private void installLayerCanvasNavigation(LayerCanvas layerCanvas) { mouseHandler = new MouseHandler(); layerCanvas.addMouseListener(mouseHandler); layerCanvas.addMouseMotionListener(mouseHandler); layerCanvas.addMouseWheelListener(mouseHandler); } private static boolean viewportIsInWorldMapBounds(double dx, double dy, LayerCanvas layerCanvas) { AffineTransform transform = layerCanvas.getViewport().getModelToViewTransform(); double minX = layerCanvas.getMaxVisibleModelBounds().getMinX(); double minY = layerCanvas.getMaxVisibleModelBounds().getMinY(); double maxX = layerCanvas.getMaxVisibleModelBounds().getMaxX(); double maxY = layerCanvas.getMaxVisibleModelBounds().getMaxY(); final Point2D upperLeft = transform.transform(new Point2D.Double(minX, minY), null); final Point2D lowerRight = transform.transform(new Point2D.Double(maxX, maxY), null); /* * We need to give the borders a minimum width/height of 1 because otherwise the intersection * operation would not work */ Rectangle2D northBorder = new Rectangle2D.Double(upperLeft.getX() + dx, upperLeft.getY() + dy, lowerRight.getX() + dx - upperLeft.getX() + dx, 1); Rectangle2D southBorder = new Rectangle2D.Double(upperLeft.getX() + dx, lowerRight.getY() + dy, lowerRight.getX() + dx - upperLeft.getX() + dx, 1); Rectangle2D westBorder = new Rectangle2D.Double(upperLeft.getX() + dx, lowerRight.getY() + dy, 1, upperLeft.getY() + dy - lowerRight.getY() + dy); Rectangle2D eastBorder = new Rectangle2D.Double(lowerRight.getX() + dx, lowerRight.getY() + dy, 1, upperLeft.getY() + dy - lowerRight.getY() + dy); return (!layerCanvas.getBounds().intersects(northBorder) && !layerCanvas.getBounds().intersects(southBorder) && !layerCanvas.getBounds().intersects(westBorder) && !layerCanvas.getBounds().intersects(eastBorder)); } private class ModelChangeListener implements PropertyChangeListener { @Override public void propertyChange(PropertyChangeEvent evt) { updateUiState(evt); } } private class MouseHandler extends MouseInputAdapter { @Override public void mousePressed(MouseEvent e) { panSupport.panStarted(e); } @Override public void mouseDragged(MouseEvent e) { panSupport.performPan(e); } @Override public void mouseReleased(MouseEvent e) { panSupport.panStopped(e); } @Override public void mouseWheelMoved(MouseWheelEvent e) { if (!isEnabled()) { return; } double oldFactor = layerCanvas.getViewport().getZoomFactor(); final int wheelRotation = e.getWheelRotation(); final double newZoomFactor = layerCanvas.getViewport().getZoomFactor() * Math.pow(1.1, wheelRotation); final Rectangle viewBounds = layerCanvas.getViewport().getViewBounds(); final Rectangle2D modelBounds = worldMapLayer.getModelBounds(); final double minZoomFactor = Math.min(viewBounds.getWidth() / modelBounds.getWidth(), viewBounds.getHeight() / modelBounds.getHeight()); layerCanvas.getViewport().setZoomFactor(Math.max(newZoomFactor, minZoomFactor)); if (layerCanvas.getViewport().getZoomFactor() > oldFactor || viewportIsInWorldMapBounds(0, 0, layerCanvas)) { fireScrolled(); return; } layerCanvas.getViewport().setZoomFactor(oldFactor); } } private static class WorldMapLayerContext implements LayerContext { private final Layer rootLayer; private WorldMapLayerContext(Layer rootLayer) { this.rootLayer = rootLayer; } @Override public Object getCoordinateReferenceSystem() { return DefaultGeographicCRS.WGS84; } @Override public Layer getRootLayer() { return rootLayer; } } private class ZoomAllAction extends AbstractAction { private ZoomAllAction() { putValue(LARGE_ICON_KEY, TangoIcons.actions_view_fullscreen(TangoIcons.Res.R22)); } @Override public void actionPerformed(ActionEvent e) { if (isEnabled()) { zoomAll(); } } } private class ZoomToSelectedAction extends AbstractAction { private ZoomToSelectedAction() { putValue(LARGE_ICON_KEY, ImageUtilities.loadImageIcon("org/esa/snap/rcp/icons/ZoomTo24.gif", false)); } @Override public void actionPerformed(ActionEvent e) { if (isEnabled()) { final Product selectedProduct = getSelectedProduct(); zoomToProduct(selectedProduct); } } } public interface ZoomListener { void zoomed(); } public interface PanSupport { void panStarted(MouseEvent event); void performPan(MouseEvent event); void panStopped(MouseEvent event); } protected static class DefaultPanSupport implements PanSupport { private Point p0; private final LayerCanvas layerCanvas; protected DefaultPanSupport(LayerCanvas layerCanvas) { this.layerCanvas = layerCanvas; } @Override public void panStarted(MouseEvent event) { p0 = event.getPoint(); } @Override public void performPan(MouseEvent event) { final Point p = event.getPoint(); final double dx = p.x - p0.x; final double dy = p.y - p0.y; if (viewportIsInWorldMapBounds(dx, dy, layerCanvas)) { layerCanvas.getViewport().moveViewDelta(dx, dy); } p0 = p; } @Override public void panStopped(MouseEvent event) { } } /** * Set the worldmap's scale. * * @param scale the scale. * @deprecated since 4.10.1, use layer canvas for zooming instead */ @Deprecated public void setScale(final float scale) { if (getScale() != scale && scale > 0) { final float oldValue = getScale(); getLayerCanvas().getViewport().setZoomFactor(scale); firePropertyChange("scale", oldValue, scale); } } }