/*
* 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.swing.TableLayout;
import org.esa.snap.core.datamodel.Band;
import org.esa.snap.core.datamodel.ProductData;
import org.esa.snap.core.datamodel.RasterDataNode;
import org.esa.snap.core.ui.UIUtils;
import org.esa.snap.core.ui.product.ProductSceneView;
import org.esa.snap.core.ui.tool.ToolButtonFactory;
import org.esa.snap.timeseries.core.timeseries.datamodel.AbstractTimeSeries;
import org.esa.snap.timeseries.core.timeseries.datamodel.TimeCoding;
import org.esa.snap.timeseries.export.animations.AnimatedGifExport;
import org.openide.util.HelpCtx;
import javax.swing.AbstractButton;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSeparator;
import javax.swing.JSlider;
import javax.swing.Timer;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionListener;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.Hashtable;
import java.util.List;
/**
* @author Thomas Storm
*/
class TimeSeriesPlayerForm extends JPanel {
private final ImageIcon playIcon = new ImageIcon(getClass().getResource("icons/timeseries-play24.png"));
private final ImageIcon stopIcon = new ImageIcon(getClass().getResource("icons/timeseries-stop24.png"));
private final ImageIcon pauseIcon = UIUtils.loadImageIcon("icons/Pause24.png");
private final ImageIcon blendIcon = new ImageIcon(getClass().getResource("icons/timeseries-blend24.png"));
private final ImageIcon repeatIcon = new ImageIcon(getClass().getResource("icons/timeseries-repeat24.png"));
private final ImageIcon minusIcon = UIUtils.loadImageIcon("icons/Remove16.png");
private final ImageIcon plusIcon = UIUtils.loadImageIcon("icons/Add16.png");
private final ImageIcon exportIcon = UIUtils.loadImageIcon("icons/Export24.gif");
private final JSlider timeSlider;
private final AbstractButton playButton;
private final AbstractButton stopButton;
private final JLabel dateLabel;
private final JSlider speedSlider;
private final JLabel speedLabel;
private final JLabel speedUnit;
private final AbstractButton blendButton;
private Timer timer;
private final AbstractButton repeatButton;
private final AbstractButton minusButton;
private final AbstractButton plusButton;
private final AbstractButton exportButton;
private int stepsPerTimespan = 1;
private int timerDelay = 1250;
private AbstractTimeSeries timeSeries;
private ProductSceneView currentView;
/**
* must be different from any character occuring in the date format
*/
private static final String DATE_SEPARATOR = " ";
TimeSeriesPlayerForm(String helpId) {
this.setLayout(new BorderLayout(4, 4));
this.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
this.setPreferredSize(new Dimension(350, 200));
JPanel firstPanel = new JPanel(createLayout());
firstPanel.setPreferredSize(new Dimension(300, 150));
final JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
final JPanel secondPanel = new JPanel(new BorderLayout());
// BoxLayout boxLayout = new BoxLayout(secondPanel, BoxLayout.Y_AXIS);
// secondPanel.setLayout(boxLayout);
dateLabel = new JLabel("Date: ");
timeSlider = createTimeSlider();
playButton = createPlayButton();
stopButton = createStopButton();
repeatButton = createRepeatButton();
blendButton = createBlendButton();
speedLabel = new JLabel("Speed:");
minusButton = createMinusButton();
speedSlider = createSpeedSlider();
speedUnit = new JLabel();
plusButton = createPlusButton();
exportButton = createExportButton();
AbstractButton helpButton = ToolButtonFactory.createButton(UIUtils.loadImageIcon("icons/Help22.png"), false);
helpButton.setToolTipText("Help");
helpButton.addActionListener(e -> new HelpCtx(helpId).display());
updateSpeedUnit();
setUIEnabled(false);
buttonsPanel.add(playButton);
buttonsPanel.add(stopButton);
buttonsPanel.add(repeatButton);
buttonsPanel.add(new JSeparator(JSeparator.VERTICAL));
buttonsPanel.add(blendButton);
buttonsPanel.add(new JSeparator(JSeparator.VERTICAL));
buttonsPanel.add(speedLabel);
buttonsPanel.add(minusButton);
buttonsPanel.add(speedSlider);
buttonsPanel.add(plusButton);
buttonsPanel.add(speedUnit);
buttonsPanel.add(new JLabel(" "));
secondPanel.add(exportButton, BorderLayout.NORTH);
secondPanel.add(helpButton, BorderLayout.SOUTH);
firstPanel.add(dateLabel);
firstPanel.add(timeSlider);
firstPanel.add(buttonsPanel);
this.add(BorderLayout.CENTER, firstPanel);
this.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
this.add(BorderLayout.EAST, secondPanel);
}
List<Band> getBandList(final String rasterName) {
final String variableName = AbstractTimeSeries.rasterToVariableName(rasterName);
return timeSeries.getBandsForVariable(variableName);
}
void setTimeSeries(AbstractTimeSeries timeSeries) {
this.timeSeries = timeSeries;
}
Timer getTimer() {
return timer;
}
void setView(ProductSceneView view) {
this.currentView = view;
}
int getStepsPerTimespan() {
return stepsPerTimespan;
}
JSlider getTimeSlider() {
return timeSlider;
}
void configureTimeSlider(RasterDataNode raster) {
if (timeSeries != null) {
List<Band> bandList = getBandList(raster.getName());
timeSlider.setMinimum(0);
final int nodeCount = bandList.size();
final int maximum = (nodeCount - 1) * stepsPerTimespan;
timeSlider.setMaximum(maximum);
final Hashtable<Integer, JLabel> labelTable = new Hashtable<>();
if (nodeCount > 1) {
setUIEnabled(true);
labelTable.put(0, new JLabel(createSliderLabelFormattedText(bandList, 0)));
labelTable.put(maximum,
new JLabel(createSliderLabelFormattedText(bandList, maximum / stepsPerTimespan)));
timeSlider.setLabelTable(labelTable);
} else {
timeSlider.setLabelTable(null);
setUIEnabled(false);
}
final int index = bandList.indexOf(raster);
if (index != -1) {
timeSlider.setValue(index * stepsPerTimespan);
}
} else {
timeSlider.setLabelTable(null);
setUIEnabled(false);
}
}
private String createSliderLabelText(List<Band> bandList, int index) {
Band band = bandList.get(index);
TimeCoding timeCoding = timeSeries.getRasterTimeMap().get(band);
if (timeCoding != null) {
final ProductData.UTC utcStartTime = timeCoding.getStartTime();
DateFormat dateFormat = ProductData.UTC.createDateFormat("dd-MMM-yyyy");
DateFormat timeFormat = ProductData.UTC.createDateFormat("HH:mm:ss");
final String dateText = dateFormat.format(utcStartTime.getAsCalendar().getTime());
final String timeText = timeFormat.format(utcStartTime.getAsCalendar().getTime());
return dateText + DATE_SEPARATOR + timeText;
} else {
return "";
}
}
private String createSliderLabelFormattedText(List<Band> bandList, int index) {
final String labelText = createSliderLabelText(bandList, index);
final String[] strings = labelText.split(DATE_SEPARATOR);
return String.format("<html><p align=\"center\"> <font size=\"2\">%s<br>%s</font></p>", strings[0], strings[1]);
}
private JSlider createTimeSlider() {
final JSlider timeSlider = new JSlider(JSlider.HORIZONTAL, 0, 0, 0);
timeSlider.setMajorTickSpacing(stepsPerTimespan);
timeSlider.setMinorTickSpacing(1);
timeSlider.setPaintTrack(true);
timeSlider.setSnapToTicks(true);
timeSlider.setPaintTicks(true);
timeSlider.addChangeListener(e -> {
final int index = timeSlider.getValue() / stepsPerTimespan;
final List<Band> bandList = getBandList(currentView.getRaster().getName());
final String labelText = createSliderLabelText(bandList, index);
dateLabel.setText("Date: " + labelText);
});
timeSlider.setPreferredSize(new Dimension(320, 60));
return timeSlider;
}
private AbstractButton createPlayButton() {
final ActionListener playAction = e -> {
int currentValue = timeSlider.getValue();
if (currentValue == timeSlider.getMaximum()) {
if (repeatButton.isSelected()) {
// if slider is on maximum value and repeat button is selected, start from beginning
currentValue = 0;
} else {
// if slider is on maximum value and repeat button is not selected, stop
playButton.setSelected(false);
timer.stop();
playButton.setIcon(playIcon);
playButton.setRolloverIcon(playIcon);
currentValue = 0;
}
} else {
// if slider is not on maximum value, go on
currentValue++;
}
timeSlider.setValue(currentValue);
};
timer = new Timer(timerDelay, playAction);
final AbstractButton playButton = ToolButtonFactory.createButton(playIcon, false);
playButton.setToolTipText("Play the time series");
playButton.setRolloverIcon(playIcon);
playButton.addActionListener(e -> {
if (playButton.getIcon() == playIcon) {
timer.start();
playButton.setIcon(pauseIcon);
playButton.setRolloverIcon(pauseIcon);
} else { // pause
timer.stop();
int newValue = timeSlider.getValue() / stepsPerTimespan * stepsPerTimespan;
timeSlider.setValue(newValue);
playButton.setIcon(playIcon);
playButton.setRolloverIcon(playIcon);
}
});
return playButton;
}
private AbstractButton createStopButton() {
final AbstractButton stopButton = ToolButtonFactory.createButton(stopIcon, false);
stopButton.setToolTipText("Stop playing the time series");
stopButton.addActionListener(e -> {
timer.stop();
timeSlider.setValue(0);
playButton.setIcon(playIcon);
playButton.setRolloverIcon(playIcon);
playButton.setSelected(false);
});
return stopButton;
}
private AbstractButton createRepeatButton() {
final AbstractButton repeatButton = ToolButtonFactory.createButton(repeatIcon, true);
repeatButton.setToolTipText("Toggle repeat");
return repeatButton;
}
private AbstractButton createBlendButton() {
final AbstractButton blendButton = ToolButtonFactory.createButton(blendIcon, true);
blendButton.setToolTipText("Toggle blending mode");
blendButton.addActionListener(e -> {
if (blendButton.isSelected()) {
stepsPerTimespan = 8;
timeSlider.setValue(0);
timer.setDelay(calculateTimerDelay());
configureTimeSlider(currentView.getRaster());
} else {
stepsPerTimespan = 1;
timeSlider.setValue(0);
timer.setDelay(calculateTimerDelay());
configureTimeSlider(currentView.getRaster());
}
});
return blendButton;
}
private AbstractButton createMinusButton() {
final AbstractButton minusButton = ToolButtonFactory.createButton(minusIcon, false);
minusButton.setToolTipText("Decrease playing speed");
minusButton.addActionListener(e -> {
if (speedSlider.getValue() > speedSlider.getMinimum()) {
speedSlider.setValue(speedSlider.getValue() - 1);
}
});
return minusButton;
}
private JSlider createSpeedSlider() {
final JSlider speedSlider = new JSlider(1, 10);
speedSlider.setToolTipText("Choose the playing speed");
speedSlider.setSnapToTicks(true);
speedSlider.setPaintTrack(true);
speedSlider.setPaintTicks(true);
speedSlider.setPaintLabels(true);
speedSlider.setValue(6);
speedSlider.setPreferredSize(new Dimension(80, speedSlider.getPreferredSize().height));
speedSlider.addChangeListener(e -> {
timerDelay = calculateTimerDelay();
timer.setDelay(timerDelay);
updateSpeedUnit();
});
return speedSlider;
}
private AbstractButton createPlusButton() {
final AbstractButton plusButton = ToolButtonFactory.createButton(plusIcon, false);
plusButton.setToolTipText("Increase playing speed");
plusButton.addActionListener(e -> {
if (speedSlider.getValue() < speedSlider.getMaximum()) {
speedSlider.setValue(speedSlider.getValue() + 1);
}
});
return plusButton;
}
private AbstractButton createExportButton() {
final AbstractButton exportButton = ToolButtonFactory.createButton(exportIcon, false);
exportButton.setToolTipText("Export as animated gif");
exportButton.addActionListener(e -> {
final AnimatedGifExport export = new AnimatedGifExport(TimeSeriesPlayerForm.this,
"Export time series as animated gif");
final String varName = AbstractTimeSeries.rasterToVariableName(currentView.getRaster().getName());
export.createFrames(timeSeries.getBandsForVariable(varName));
export.executeWithBlocking();
});
return exportButton;
}
private void setUIEnabled(boolean enable) {
dateLabel.setEnabled(enable);
timeSlider.setPaintLabels(enable);
timeSlider.setEnabled(enable);
playButton.setEnabled(enable);
stopButton.setEnabled(enable);
repeatButton.setEnabled(enable);
blendButton.setEnabled(enable);
speedLabel.setEnabled(enable);
minusButton.setEnabled(enable);
speedSlider.setEnabled(enable);
speedUnit.setEnabled(enable);
plusButton.setEnabled(enable);
exportButton.setEnabled(enable);
}
private int calculateTimerDelay() {
return 250 / stepsPerTimespan * (11 - speedSlider.getValue());
}
private void updateSpeedUnit() {
double fps = 1 / (timerDelay * stepsPerTimespan / 1000.0);
DecimalFormat formatter = new DecimalFormat("0.00");
speedUnit.setText(formatter.format(fps) + " FPS");
speedUnit.setToolTipText(formatter.format(fps) + " Frames per second");
}
private static TableLayout createLayout() {
final TableLayout tableLayout = new TableLayout(1);
tableLayout.setRowPadding(0, new Insets(4, 4, 4, 0));
tableLayout.setColumnWeightX(0, 1.0);
tableLayout.setRowWeightY(1, 1.0);
tableLayout.setTableFill(TableLayout.Fill.HORIZONTAL);
tableLayout.setRowFill(1, TableLayout.Fill.BOTH);
return tableLayout;
}
}