/*
* 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.timeseries.ui.player;
import com.bc.ceres.core.ProgressMonitor;
import com.bc.ceres.glayer.Layer;
import com.bc.ceres.glayer.support.ImageLayer;
import com.bc.ceres.glayer.support.LayerUtils;
import com.bc.ceres.glevel.MultiLevelSource;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.ImageInfo;
import org.esa.snap.core.datamodel.Mask;
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.RasterDataNode;
import org.esa.snap.core.ui.product.ProductSceneImage;
import org.esa.snap.core.ui.product.ProductSceneView;
import org.esa.snap.glevel.BandImageMultiLevelSource;
import org.esa.snap.netbeans.docwin.WindowUtilities;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.rcp.util.SelectionSupport;
import org.esa.snap.rcp.windows.ProductSceneViewTopComponent;
import org.esa.snap.timeseries.core.TimeSeriesMapper;
import org.esa.snap.timeseries.core.TimeSeriesModule;
import org.esa.snap.timeseries.core.timeseries.datamodel.AbstractTimeSeries;
import org.esa.snap.timeseries.core.timeseries.datamodel.TimeSeriesChangeEvent;
import org.esa.snap.timeseries.core.timeseries.datamodel.TimeSeriesListener;
import org.esa.snap.util.math.MathUtils;
import org.openide.awt.ActionID;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
import javax.swing.BorderFactory;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.BorderLayout;
import java.lang.reflect.Field;
import java.util.List;
/**
* Main class for the player tool.
*
* @author Thomas Storm
* @author Marco Peters
*/
@TopComponent.Description(
preferredID = "TimeSeriesPlayerTopComponent",
iconBase = "org/esa/snap/timeseries/ui/icons/timeseries-player.png",
persistenceType = TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(
mode = "navigator",
openAtStartup = false,
position = 3
)
@TopComponent.OpenActionRegistration(
displayName = "#CTL_TimeSeriesPlayerTopComponentName",
preferredID = "TimeSeriesPlayerTopComponent"
)
@ActionID(category = "Window", id = "org.esa.snap.timeseries.ui.player.TimeSeriesPlayerTopComponent")
@ActionReferences({
@ActionReference(path = "Menu/View/Tool Windows/Time Series", position = 1230),
@ActionReference(path = "Toolbars/Time Series", position = 30)
})
@NbBundle.Messages({
"CTL_TimeSeriesPlayerTopComponentName=Time Series Player"
})
public class TimeSeriesPlayerTopComponent extends TopComponent {
private static final String HELP_ID = "timeSeriesPlayer";
private final TimeSeriesListener timeSeriesPlayerTSL;
private ProductSceneView currentView;
private TimeSeriesPlayerForm form;
public TimeSeriesPlayerTopComponent() {
initComponent();
timeSeriesPlayerTSL = new TimeSeriesPlayerTSL();
SnapApp.getDefault().getSelectionSupport(ProductSceneView.class).addHandler(new SceneViewSelectionChangeHandler());
}
private void initComponent() {
form = new TimeSeriesPlayerForm(HELP_ID);
form.getTimeSlider().addChangeListener(new SliderChangeListener());
ProductSceneView view = SnapApp.getDefault().getSelectedProductSceneView();
if (view != null) {
maybeUpdateCurrentView(view, view.getProduct().getProductType());
}
setLayout(new BorderLayout(4, 4));
setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
add(form, BorderLayout.CENTER);
setDisplayName(Bundle.CTL_TimeSeriesPlayerTopComponentName());
}
@Override
public HelpCtx getHelpCtx() {
return new HelpCtx(HELP_ID);
}
private void setCurrentView(ProductSceneView newView) {
if (currentView != newView) {
TimeSeriesMapper timeSeriesMapper = TimeSeriesMapper.getInstance();
if (currentView != null) {
final AbstractTimeSeries timeSeries = timeSeriesMapper.getTimeSeries(currentView.getProduct());
if (timeSeries != null) {
timeSeries.removeTimeSeriesListener(timeSeriesPlayerTSL);
}
}
currentView = newView;
form.setView(currentView);
if (currentView != null) {
final Product currentProduct = currentView.getProduct();
final AbstractTimeSeries timeSeries = timeSeriesMapper.getTimeSeries(currentProduct);
timeSeries.addTimeSeriesListener(timeSeriesPlayerTSL);
form.setTimeSeries(timeSeries);
exchangeRasterInProductSceneView(currentView.getRaster());
reconfigureBaseImageLayer(currentView);
form.configureTimeSlider(currentView.getRaster());
} else {
form.setTimeSeries(null);
form.configureTimeSlider(null);
form.getTimer().stop();
}
}
}
private void exchangeRasterInProductSceneView(RasterDataNode nextRaster) {
// todo use a real ProgressMonitor
final RasterDataNode currentRaster = currentView.getRaster();
final ImageInfo imageInfoClone = currentRaster.getImageInfo(ProgressMonitor.NULL).createDeepCopy();
nextRaster.setImageInfo(imageInfoClone);
currentView.setRasters(new RasterDataNode[]{nextRaster});
currentView.setImageInfo(imageInfoClone.createDeepCopy());
getTCForView(currentView).setDisplayName(nextRaster.getDisplayName());
}
private TopComponent getTCForView(ProductSceneView view) {
return WindowUtilities.getOpened(ProductSceneViewTopComponent.class)
.filter(topComponent -> view == topComponent.getView())
.findFirst()
.orElse(null);
}
private void reconfigureBaseImageLayer(ProductSceneView sceneView) {
final Layer rootLayer = currentView.getRootLayer();
final ImageLayer baseImageLayer = (ImageLayer) LayerUtils.getChildLayerById(rootLayer,
ProductSceneView.BASE_IMAGE_LAYER_ID);
final List<Band> bandList = form.getBandList(currentView.getRaster().getName());
final Band band = (Band) sceneView.getRaster();
int nextIndex = bandList.indexOf(band) + 1;
if (nextIndex >= bandList.size()) {
nextIndex = 0;
}
if (!(baseImageLayer instanceof BlendImageLayer)) {
final Band nextBand = bandList.get(nextIndex);
MultiLevelSource nextLevelSource = BandImageMultiLevelSource.create(nextBand, ProgressMonitor.NULL);
final BlendImageLayer blendLayer = new BlendImageLayer(baseImageLayer.getMultiLevelSource(),
nextLevelSource);
final List<Layer> children = rootLayer.getChildren();
final int baseIndex = children.indexOf(baseImageLayer);
children.remove(baseIndex);
blendLayer.setId(ProductSceneView.BASE_IMAGE_LAYER_ID);
blendLayer.setName(band.getDisplayName());
blendLayer.setTransparency(0);
children.add(baseIndex, blendLayer);
configureSceneView(sceneView, blendLayer.getBaseMultiLevelSource());
}
}
private void configureSceneView(ProductSceneView sceneView, MultiLevelSource multiLevelSource) {
// This is needed because sceneView must return correct ImageInfo
try {
final Field sceneImageField = ProductSceneView.class.getDeclaredField("sceneImage");
sceneImageField.setAccessible(true);
final Object sceneImage = sceneImageField.get(sceneView);
final Field multiLevelSourceField = ProductSceneImage.class.getDeclaredField("bandImageMultiLevelSource");
multiLevelSourceField.setAccessible(true);
multiLevelSourceField.set(sceneImage, multiLevelSource);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
private class SceneViewSelectionChangeHandler implements SelectionSupport.Handler<ProductSceneView> {
@Override
public void selectionChange(ProductSceneView oldValue, ProductSceneView newValue) {
if (currentView == oldValue) {
setCurrentView(null);
}
if (currentView != newValue) {
if (newValue != null) {
final RasterDataNode viewRaster = newValue.getRaster();
final String viewProductType = viewRaster.getProduct().getProductType();
maybeUpdateCurrentView(newValue, viewProductType);
} else {
setCurrentView(null);
}
}
}
}
private void maybeUpdateCurrentView(ProductSceneView view, String viewProductType) {
if (!view.isRGB() &&
viewProductType.equals(AbstractTimeSeries.TIME_SERIES_PRODUCT_TYPE) &&
TimeSeriesMapper.getInstance().getTimeSeries(view.getProduct()) != null) {
setCurrentView(view);
}
}
private class SliderChangeListener implements ChangeListener {
private int value;
@Override
public void stateChanged(ChangeEvent e) {
if (currentView == null) {
return;
}
final int currentValue = form.getTimeSlider().getValue();
if (currentValue == value || currentValue == -1) {
// nothing has changed -- do nothing
return;
}
if (currentView.getBaseImageLayer() instanceof BlendImageLayer) {
BlendImageLayer blendLayer = (BlendImageLayer) currentView.getBaseImageLayer();
int stepsPerTimespan = form.getStepsPerTimespan();
final float transparency = (currentValue % stepsPerTimespan) / (float) stepsPerTimespan;
blendLayer.setBlendFactor(transparency);
final List<Band> bandList = form.getBandList(currentView.getRaster().getName());
value = currentValue;
final int firstBandIndex = MathUtils.floorInt(currentValue / (float) stepsPerTimespan);
final int secondBandIndex = MathUtils.ceilInt(currentValue / (float) stepsPerTimespan);
BandImageMultiLevelSource newSource =
BandImageMultiLevelSource.create(bandList.get(secondBandIndex), ProgressMonitor.NULL);
if (secondBandIndex == firstBandIndex) {
exchangeRasterInProductSceneView(bandList.get(firstBandIndex));
blendLayer.setBaseLayer(newSource);
configureSceneView(currentView, blendLayer.getBaseMultiLevelSource());
blendLayer.setName(currentView.getRaster().getDisplayName());
// todo why use view to fire property changes and not time series itself?
currentView.firePropertyChange(TimeSeriesModule.TIME_PROPERTY, -1, firstBandIndex);
} else {
if (transparency == (float) 1 / stepsPerTimespan) {
blendLayer.setBlendLayer(newSource);
}
currentView.getLayerCanvas().repaint();
}
}
}
}
private class TimeSeriesPlayerTSL extends TimeSeriesListener {
@Override
public void timeSeriesChanged(TimeSeriesChangeEvent event) {
if (event.getType() == TimeSeriesChangeEvent.PROPERTY_PRODUCT_LOCATIONS ||
event.getType() == TimeSeriesChangeEvent.PROPERTY_EO_VARIABLE_SELECTION) {
form.configureTimeSlider(currentView.getRaster());
}
}
@Override
public void nodeAdded(ProductNodeEvent event) {
final ProductNode productNode = event.getSourceNode();
if (isValidProductNode(productNode) && currentView != null) {
form.configureTimeSlider((RasterDataNode) productNode);
}
}
@Override
public void nodeRemoved(ProductNodeEvent event) {
final ProductNode productNode = event.getSourceNode();
if (isValidProductNode(productNode) && currentView != null) {
if (currentView.getRaster() == productNode) {
form.configureTimeSlider((RasterDataNode) productNode);
}
}
}
@Override
public void nodeChanged(ProductNodeEvent event) {
String propertyName = event.getPropertyName();
if (propertyName.equals(RasterDataNode.PROPERTY_NAME_IMAGE_INFO)) {
adjustImageInfos(event);
}
}
private boolean isValidProductNode(ProductNode productNode) {
return productNode instanceof RasterDataNode && !(productNode instanceof Mask);
}
private void adjustImageInfos(ProductNodeEvent event) {
final ProductNode node = event.getSourceNode();
if (isValidProductNode(node)) {
final RasterDataNode raster = (RasterDataNode) node;
final ImageLayer baseImageLayer = currentView.getBaseImageLayer();
final ImageInfo imageInfo = raster.getImageInfo();
if (baseImageLayer instanceof BlendImageLayer) {
BlendImageLayer blendLayer = (BlendImageLayer) baseImageLayer;
blendLayer.getBlendMultiLevelSource().setImageInfo(imageInfo.createDeepCopy());
}
}
}
}
}