/*
* 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.gpf.ui.worldmap;
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.Rendering;
import com.bc.ceres.grender.Viewport;
import org.apache.commons.math3.util.FastMath;
import org.esa.snap.core.datamodel.GeoCoding;
import org.esa.snap.core.datamodel.GeoPos;
import org.esa.snap.core.datamodel.PixelPos;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.util.ProductUtils;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.ui.ButtonOverlayControl;
import org.esa.snap.ui.UIUtils;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import javax.swing.AbstractAction;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
/**
* This class displays a world map specified by the {@link NestWorldMapPaneDataModel}.
*
* @author Marco Peters
*/
public class NestWorldMapPane extends JPanel {
private LayerCanvas layerCanvas;
private Layer worldMapLayer;
private final NestWorldMapPaneDataModel dataModel;
private boolean navControlShown;
private WakefulComponent navControlWrapper;
private final static Color transWhiteColor = new Color(255, 255, 255, 5);
private final static Color borderWhiteColor = new Color(255, 255, 255, 100);
private final static Color transRedColor = new Color(255, 0, 0, 30);
private final static Color borderRedColor = new Color(255, 0, 0, 100);
private final static Color selectionFillColor = new Color(255, 255, 0, 70);
private final static Color selectionBorderColor = new Color(255, 255, 0, 255);
public NestWorldMapPane(NestWorldMapPaneDataModel dataModel) {
this.dataModel = dataModel;
try {
layerCanvas = new LayerCanvas();
layerCanvas.getModel().getViewport().setModelYAxisDown(false);
installLayerCanvasNavigation(layerCanvas, dataModel);
layerCanvas.addOverlay(new BoundaryOverlay());
final Layer rootLayer = layerCanvas.getLayer();
final Dimension dimension = new Dimension(400, 150);
final Viewport viewport = layerCanvas.getViewport();
viewport.setViewBounds(new Rectangle(dimension));
setPreferredSize(dimension);
setSize(dimension);
setLayout(new BorderLayout());
add(layerCanvas, BorderLayout.CENTER);
dataModel.addModelChangeListener(new ModelChangeListener());
worldMapLayer = dataModel.getWorldMapLayer(new WorldMapLayerContext(rootLayer));
layerCanvas.getLayer().getChildren().add(worldMapLayer);
layerCanvas.getViewport().zoom(worldMapLayer.getModelBounds());
setNavControlVisible(true);
} catch (Exception e) {
SnapApp.getDefault().handleError("Error in worldmap initialization", e);
}
}
public LayerCanvas getLayerCanvas() {
return layerCanvas;
}
@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) layerCanvas.getViewport().getZoomFactor();
}
public void setScale(final float scale) {
if (getScale() != scale) {
final float oldValue = getScale();
layerCanvas.getViewport().setZoomFactor(scale);
firePropertyChange("scale", oldValue, scale);
}
}
public void zoomToProduct(Product product) {
final GeoPos[][] selGeoBoundaries = dataModel.getSelectedGeoBoundaries();
if ((product == null || product.getSceneGeoCoding() == null) && selGeoBoundaries.length == 0) {
return;
}
//NESTMOD
final GeneralPath[] generalPaths;
if (product != null && product.getSceneGeoCoding() != null) {
generalPaths = getGeoBoundaryPaths(product);
} else {
final ArrayList<GeneralPath> pathList = assemblePathList(selGeoBoundaries[0]);
generalPaths = pathList.toArray(new GeneralPath[pathList.size()]);
}
Rectangle2D modelArea = new Rectangle2D.Double();
final Viewport viewport = layerCanvas.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);
}
/**
* 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 ButtonOverlayControl navControl = new ButtonOverlayControl(new ZoomAllAction(),
new ZoomToSelectedAction());//, new ZoomToLocationAction());
navControlWrapper = new WakefulComponent(navControl);
navControlWrapper.setMinAlpha(0.5f);
layerCanvas.add(navControlWrapper);
} else {
layerCanvas.remove(navControlWrapper);
navControlWrapper = null;
}
validate();
this.navControlShown = navControlShown;
}
}
private void updateUiState(PropertyChangeEvent evt) {
if (NestWorldMapPaneDataModel.PROPERTY_LAYER.equals(evt.getPropertyName())) {
exchangeWorldMapLayer();
}
if (NestWorldMapPaneDataModel.PROPERTY_PRODUCTS.equals(evt.getPropertyName())) {
repaint();
}
if (NestWorldMapPaneDataModel.PROPERTY_SELECTED_PRODUCT.equals(evt.getPropertyName()) ||
NestWorldMapPaneDataModel.PROPERTY_AUTO_ZOOM_ENABLED.equals(evt.getPropertyName())) {
final Product selectedProduct = dataModel.getSelectedProduct();
if (selectedProduct != null && dataModel.isAutoZommEnabled()) {
zoomToProduct(selectedProduct);
} else {
repaint();
}
}
if (NestWorldMapPaneDataModel.PROPERTY_ADDITIONAL_GEO_BOUNDARIES.equals(evt.getPropertyName()) ||
NestWorldMapPaneDataModel.PROPERTY_SELECTED_GEO_BOUNDARIES.equals(evt.getPropertyName())) {
repaint();
}
}
private void exchangeWorldMapLayer() {
final List<Layer> children = layerCanvas.getLayer().getChildren();
for (Layer child : children) {
child.dispose();
}
children.clear();
final Layer rootLayer = layerCanvas.getLayer();
worldMapLayer = dataModel.getWorldMapLayer(new WorldMapLayerContext(rootLayer));
children.add(worldMapLayer);
layerCanvas.getViewport().zoom(worldMapLayer.getModelBounds());
}
private Rectangle2D cropToMaxModelBounds(Rectangle2D modelBounds) {
final Rectangle2D maxModelBounds = worldMapLayer.getModelBounds();
if (modelBounds.getWidth() >= maxModelBounds.getWidth() - 1 ||
modelBounds.getHeight() >= maxModelBounds.getHeight() - 1) {
modelBounds = maxModelBounds;
}
return modelBounds;
}
public static GeneralPath[] getGeoBoundaryPaths(Product product) {
final int step = Math.max(16, (product.getSceneRasterWidth() + product.getSceneRasterHeight()) / 250);
return ProductUtils.createGeoBoundaryPaths(product, null, step);
}
private PixelPos getProductCenter(final Product product) {
final GeoCoding geoCoding = product.getSceneGeoCoding();
PixelPos centerPos = null;
if (geoCoding != null) {
final float pixelX = (float) Math.floor(0.5f * product.getSceneRasterWidth()) + 0.5f;
final float pixelY = (float) Math.floor(0.5f * product.getSceneRasterHeight()) + 0.5f;
final GeoPos geoPos = geoCoding.getGeoPos(new PixelPos(pixelX, pixelY), null);
final AffineTransform transform = layerCanvas.getViewport().getModelToViewTransform();
final Point2D point2D = transform.transform(new Point2D.Double(geoPos.getLon(), geoPos.getLat()), null);
centerPos = new PixelPos((float) point2D.getX(), (float) point2D.getY());
}
return centerPos;
}
private static void installLayerCanvasNavigation(final LayerCanvas layerCanvas, final NestWorldMapPaneDataModel dataModel) {
MouseHandler mouseHandler = new MouseHandler(layerCanvas, dataModel);
layerCanvas.addMouseListener(mouseHandler);
layerCanvas.addMouseMotionListener(mouseHandler);
layerCanvas.addMouseWheelListener(mouseHandler);
}
/**
* @param bufferedImage is ignored
* @deprecated since BEAM 4.7, no replacement
*/
@Deprecated
@SuppressWarnings({"UnusedDeclaration"})
public void setWorldMapImage(java.awt.image.BufferedImage bufferedImage) {
}
private class ModelChangeListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateUiState(evt);
}
}
public static class MouseHandler extends MouseInputAdapter {
private final LayerCanvas layerCanvas;
private final NestWorldMapPaneDataModel dataModel;
private Point p0;
private Point.Float selectionStart = new Point.Float();
private Point.Float selectionEnd = new Point.Float();
private boolean leftButtonDown = false;
private MouseHandler(final LayerCanvas layerCanvas, final NestWorldMapPaneDataModel dataModel) {
this.layerCanvas = layerCanvas;
this.dataModel = dataModel;
}
@Override
public void mousePressed(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
leftButtonDown = true;
final AffineTransform viewToModelTransform = layerCanvas.getViewport().getViewToModelTransform();
viewToModelTransform.transform(e.getPoint(), selectionStart);
dataModel.setSelectionBoxStart(selectionStart.y, selectionStart.x);
dataModel.setSelectionBoxEnd(selectionStart.y, selectionStart.x);
layerCanvas.updateUI();
} else {
p0 = e.getPoint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON1) {
leftButtonDown = false;
}
}
@Override
public void mouseDragged(MouseEvent e) {
final Point p = e.getPoint();
if (leftButtonDown) {
final AffineTransform viewToModelTransform = layerCanvas.getViewport().getViewToModelTransform();
viewToModelTransform.transform(e.getPoint(), selectionEnd);
dataModel.setSelectionBoxEnd(selectionEnd.y, selectionEnd.x);
layerCanvas.updateUI();
} else if (p0 != null) {
final double dx = p.x - p0.x;
final double dy = p.y - p0.y;
layerCanvas.getViewport().moveViewDelta(dx, dy);
p0 = p;
}
}
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
final int wheelRotation = e.getWheelRotation();
final double newZoomFactor = layerCanvas.getViewport().getZoomFactor() * FastMath.pow(1.1, wheelRotation);
layerCanvas.getViewport().setZoomFactor(newZoomFactor);
}
}
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 BoundaryOverlay implements LayerCanvas.Overlay {
@Override
public void paintOverlay(LayerCanvas canvas, Rendering rendering) {
for (final GeoPos[] extraGeoBoundary : dataModel.getAdditionalGeoBoundaries()) {
drawGeoBoundary(rendering.getGraphics(), extraGeoBoundary, null, null,
transWhiteColor, borderWhiteColor);
}
for (final GeoPos[] selectGeoBoundary : dataModel.getSelectedGeoBoundaries()) {
drawGeoBoundary(rendering.getGraphics(), selectGeoBoundary, null, null,
transRedColor, borderRedColor);
}
final Product selectedProduct = dataModel.getSelectedProduct();
for (final Product product : dataModel.getProducts()) {
if (product != null && selectedProduct != product) {
drawProduct(rendering.getGraphics(), product,
transWhiteColor, Color.WHITE);
}
}
if (selectedProduct != null) {
drawProduct(rendering.getGraphics(), selectedProduct,
transWhiteColor, Color.RED);
}
drawGeoBoundary(rendering.getGraphics(), dataModel.getSelectionBox(), null, null,
selectionFillColor, selectionBorderColor);
}
private void drawProduct(final Graphics2D g2d, final Product product,
final Color fillColor, final Color borderColor) {
final GeoCoding geoCoding = product.getSceneGeoCoding();
if (geoCoding == null) {
return;
}
GeneralPath[] boundaryPaths = getGeoBoundaryPaths(product);
final String text = String.valueOf(product.getRefNo());
final PixelPos textCenter = getProductCenter(product);
drawGeoBoundary(g2d, boundaryPaths, text, textCenter, fillColor, borderColor);
}
private void drawGeoBoundary(final Graphics2D g2d, final GeneralPath[] boundaryPaths,
final String text, final PixelPos textCenter,
final Color fillColor, final Color borderColor) {
final AffineTransform transform = layerCanvas.getViewport().getModelToViewTransform();
for (GeneralPath boundaryPath : boundaryPaths) {
boundaryPath.transform(transform);
drawPath(g2d, boundaryPath, fillColor, borderColor);
}
drawText(g2d, text, textCenter, 0.0f);
}
private void drawGeoBoundary(final Graphics2D g2d, final GeoPos[] geoBoundary,
final String text, final PixelPos textCenter,
final Color fillColor, final Color borderColor) {
ProductUtils.normalizeGeoPolygon(geoBoundary);
final List<GeneralPath> boundaryPaths = assemblePathList(geoBoundary);
final AffineTransform transform = layerCanvas.getViewport().getModelToViewTransform();
for (GeneralPath boundaryPath : boundaryPaths) {
boundaryPath.transform(transform);
//drawPath(g2d, boundaryPath, fillColor, borderColor);
g2d.setColor(fillColor);
g2d.fill(boundaryPath);
g2d.setColor(borderColor);
g2d.draw(boundaryPath);
}
drawText(g2d, text, textCenter, 0.0f);
}
private void drawPath(Graphics2D g2d, final GeneralPath gp,
final Color fillColor, final Color borderColor) {
g2d.setColor(fillColor);
g2d.fill(gp);
g2d.setColor(borderColor);
g2d.draw(gp);
}
private GeneralPath convertToPixelPath(final GeoPos[] geoBoundary) {
final GeneralPath gp = new GeneralPath();
for (int i = 0; i < geoBoundary.length; i++) {
final GeoPos geoPos = geoBoundary[i];
final AffineTransform m2vTransform = layerCanvas.getViewport().getModelToViewTransform();
final Point2D viewPos = m2vTransform.transform(new PixelPos.Double(geoPos.lon, geoPos.lat), null);
if (i == 0) {
gp.moveTo(viewPos.getX(), viewPos.getY());
} else {
gp.lineTo(viewPos.getX(), viewPos.getY());
}
}
gp.closePath();
return gp;
}
private void drawText(Graphics2D g2d, final String text, final PixelPos textCenter, final float offsetX) {
if (text == null || textCenter == null) {
return;
}
g2d = prepareGraphics2D(offsetX, g2d);
final FontMetrics fontMetrics = g2d.getFontMetrics();
final Color color = g2d.getColor();
g2d.setColor(Color.black);
g2d.drawString(text,
(int)textCenter.x - fontMetrics.stringWidth(text) / 2.0f,
(int)textCenter.y + fontMetrics.getAscent() / 2.0f);
g2d.setColor(color);
}
private Graphics2D prepareGraphics2D(final float offsetX, Graphics2D g2d) {
if (offsetX != 0.0f) {
g2d = (Graphics2D) g2d.create();
final AffineTransform transform = g2d.getTransform();
final AffineTransform offsetTrans = new AffineTransform();
offsetTrans.setToTranslation(+offsetX, 0);
transform.concatenate(offsetTrans);
g2d.setTransform(transform);
}
return g2d;
}
}
private class ZoomAllAction extends AbstractAction {
private ZoomAllAction() {
putValue(LARGE_ICON_KEY, UIUtils.loadImageIcon("icons/ZoomAll24.gif"));
}
@Override
public void actionPerformed(ActionEvent e) {
layerCanvas.getViewport().zoom(worldMapLayer.getModelBounds());
}
}
private class ZoomToSelectedAction extends AbstractAction {
private ZoomToSelectedAction() {
putValue(LARGE_ICON_KEY, UIUtils.loadImageIcon("icons/ZoomTo24.gif"));
}
@Override
public void actionPerformed(ActionEvent e) {
zoomToProduct(getSelectedProduct());
}
}
private class ZoomToLocationAction extends AbstractAction {
private ZoomToLocationAction() {
putValue(LARGE_ICON_KEY, UIUtils.loadImageIcon("/org/esa/nest/icons/define-location-24.png", this.getClass()));
}
@Override
public void actionPerformed(ActionEvent e) {
zoomToProduct(getSelectedProduct());
}
}
public static ArrayList<GeneralPath> assemblePathList(GeoPos[] geoPoints) {
final GeneralPath path = new GeneralPath(GeneralPath.WIND_NON_ZERO, geoPoints.length + 8);
final ArrayList<GeneralPath> pathList = new ArrayList<>(16);
if (geoPoints.length > 1) {
double lon, lat;
double minLon=0, maxLon=0;
boolean first = true;
for(GeoPos gp : geoPoints) {
lon = gp.getLon();
lat = gp.getLat();
if(first) {
minLon = lon;
maxLon = lon;
path.moveTo(lon, lat);
first = false;
}
if (lon < minLon) {
minLon = lon;
}
if (lon > maxLon) {
maxLon = lon;
}
path.lineTo(lon, lat);
}
path.closePath();
int runIndexMin = (int) Math.floor((minLon + 180) / 360);
int runIndexMax = (int) Math.floor((maxLon + 180) / 360);
if (runIndexMin == 0 && runIndexMax == 0) {
// the path is completely within [-180, 180] longitude
pathList.add(path);
return pathList;
}
final Area pathArea = new Area(path);
final GeneralPath pixelPath = new GeneralPath(GeneralPath.WIND_NON_ZERO);
for (int k = runIndexMin; k <= runIndexMax; k++) {
final Area currentArea = new Area(new Rectangle2D.Float(k * 360.0f - 180.0f, -90.0f, 360.0f, 180.0f));
currentArea.intersect(pathArea);
if (!currentArea.isEmpty()) {
pathList.add(areaToPath(currentArea, -k * 360.0, pixelPath));
}
}
}
return pathList;
}
public static GeneralPath areaToPath(final Area negativeArea, final double deltaX, final GeneralPath pixelPath) {
final float[] floats = new float[6];
// move to correct rectangle
final AffineTransform transform = AffineTransform.getTranslateInstance(deltaX, 0.0);
final PathIterator iterator = negativeArea.getPathIterator(transform);
while (!iterator.isDone()) {
final int segmentType = iterator.currentSegment(floats);
if (segmentType == PathIterator.SEG_LINETO) {
pixelPath.lineTo(floats[0], floats[1]);
} else if (segmentType == PathIterator.SEG_MOVETO) {
pixelPath.moveTo(floats[0], floats[1]);
} else if (segmentType == PathIterator.SEG_CLOSE) {
pixelPath.closePath();
}
iterator.next();
}
return pixelPath;
}
}