/*
* 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.graph;
import org.esa.snap.core.jexp.ParseException;
import org.esa.snap.core.datamodel.Band;
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.Placemark;
import org.esa.snap.core.datamodel.Product;
import org.esa.snap.core.datamodel.ProductData;
import org.esa.snap.core.datamodel.RasterDataNode;
import org.esa.snap.core.ui.product.ProductSceneView;
import org.esa.snap.rcp.SnapApp;
import org.esa.snap.timeseries.core.TimeSeriesMapper;
import org.esa.snap.timeseries.core.timeseries.datamodel.AbstractTimeSeries;
import org.esa.snap.timeseries.core.timeseries.datamodel.AxisMapping;
import org.esa.snap.timeseries.core.timeseries.datamodel.TimeCoding;
import org.esa.snap.util.StringUtils;
import org.esa.snap.util.SystemUtils;
import org.jfree.chart.annotations.XYAnnotation;
import org.jfree.chart.annotations.XYLineAnnotation;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.labels.StandardXYToolTipGenerator;
import org.jfree.chart.plot.XYPlot;
import org.jfree.chart.renderer.xy.XYErrorRenderer;
import org.jfree.chart.renderer.xy.XYItemRenderer;
import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer;
import org.jfree.data.Range;
import org.jfree.data.time.Millisecond;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.Stroke;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.stream.Collectors;
class TimeSeriesGraphModel implements TimeSeriesGraphUpdater.TimeSeriesDataHandler, TimeSeriesGraphDisplayController.PinSupport {
private static final String QUALIFIER_RASTER = "_r_";
private static final String QUALIFIER_INSITU = "_i_";
private static final Color DEFAULT_FOREGROUND_COLOR = Color.BLACK;
private static final Color DEFAULT_BACKGROUND_COLOR = new Color(225, 225, 225);
private static final String NO_DATA_MESSAGE = "No data to display";
private static final int CURSOR_COLLECTION_INDEX_OFFSET = 0;
private static final int PIN_COLLECTION_INDEX_OFFSET = 1;
private static final int INSITU_COLLECTION_INDEX_OFFSET = 2;
private static final Stroke PIN_STROKE = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f,
new float[]{10.0f}, 0.0f);
private static final Stroke CURSOR_STROKE = new BasicStroke();
private final Map<AbstractTimeSeries, TimeSeriesGraphDisplayController> displayControllerMap;
private final XYPlot timeSeriesPlot;
private final List<List<Band>> eoVariableBands;
private final AtomicInteger version = new AtomicInteger(0);
private final TimeSeriesGraphUpdater.WorkerChainSupport workerChainSupport;
private final Validation validation;
private final WorkerChain workerChain;
private final Map<String, Paint> paintMap = new HashMap<>();
private TimeSeriesGraphDisplayController displayController;
private boolean isShowingSelectedPins;
private boolean isShowingAllPins;
private AxisMapping displayAxisMapping;
private boolean showCursorTimeSeries = true;
TimeSeriesGraphModel(XYPlot plot, Validation validation) {
timeSeriesPlot = plot;
this.validation = validation;
validation.addValidationListener(() -> {
updateTimeSeries(null, TimeSeriesType.INSITU);
updateTimeSeries(null, TimeSeriesType.PIN);
});
eoVariableBands = new ArrayList<>();
displayControllerMap = new WeakHashMap<>();
workerChainSupport = createWorkerChainSupport();
workerChain = new WorkerChain();
initPlot();
}
void adaptToTimeSeries(AbstractTimeSeries timeSeries) {
version.incrementAndGet();
eoVariableBands.clear();
final boolean hasData = timeSeries != null;
if (hasData) {
displayController = displayControllerMap.get(timeSeries);
if (displayController == null) {
displayController = new TimeSeriesGraphDisplayController(this);
displayControllerMap.put(timeSeries, displayController);
}
displayController.adaptTo(timeSeries);
eoVariableBands.addAll(
displayController.getEoVariablesToDisplay().stream().map(timeSeries::getBandsForVariable).collect(Collectors.toList()));
displayAxisMapping = createDisplayAxisMapping(timeSeries);
} else {
displayAxisMapping = new AxisMapping();
}
validation.adaptTo(timeSeries, displayAxisMapping);
updatePlot(hasData);
}
AtomicInteger getVersion() {
return version;
}
void updateAnnotation(RasterDataNode raster) {
removeAnnotation();
final AbstractTimeSeries timeSeries = getTimeSeries();
TimeCoding timeCoding = timeSeries.getRasterTimeMap().get(raster);
if (timeCoding != null) {
final ProductData.UTC startTime = timeCoding.getStartTime();
final Millisecond timePeriod = new Millisecond(startTime.getAsDate(),
ProductData.UTC.UTC_TIME_ZONE,
Locale.getDefault());
double millisecond = timePeriod.getFirstMillisecond();
Range valueRange = null;
for (int i = 0; i < timeSeriesPlot.getRangeAxisCount(); i++) {
valueRange = Range.combine(valueRange, timeSeriesPlot.getRangeAxis(i).getRange());
}
if (valueRange != null) {
XYAnnotation annotation = new XYLineAnnotation(millisecond, valueRange.getLowerBound(), millisecond,
valueRange.getUpperBound());
timeSeriesPlot.addAnnotation(annotation, true);
}
}
}
void removeAnnotation() {
timeSeriesPlot.clearAnnotations();
}
void setIsShowingSelectedPins(boolean isShowingSelectedPins) {
if (isShowingSelectedPins && isShowingAllPins) {
throw new IllegalStateException("isShowingSelectedPins && isShowingAllPins");
}
this.isShowingSelectedPins = isShowingSelectedPins;
updateTimeSeries(null, TimeSeriesType.PIN);
updateTimeSeries(null, TimeSeriesType.INSITU);
}
void setIsShowingAllPins(boolean isShowingAllPins) {
if (isShowingAllPins && isShowingSelectedPins) {
throw new IllegalStateException("isShowingAllPins && isShowingSelectedPins");
}
this.isShowingAllPins = isShowingAllPins;
updateTimeSeries(null, TimeSeriesType.PIN);
updateTimeSeries(null, TimeSeriesType.INSITU);
}
void setIsShowingCursorTimeSeries(boolean showCursorTimeSeries) {
this.showCursorTimeSeries = showCursorTimeSeries;
}
synchronized void updateTimeSeries(TimeSeriesGraphUpdater.Position cursorPosition, TimeSeriesType type) {
if(getTimeSeries() == null) {
return;
}
final TimeSeriesGraphUpdater.PositionSupport positionSupport = createPositionSupport();
final TimeSeriesGraphUpdater w = new TimeSeriesGraphUpdater(getTimeSeries(), createVersionSafeDataSources(),
this, displayAxisMapping, workerChainSupport, cursorPosition, positionSupport, type,
showCursorTimeSeries, version.get());
final boolean chained = type != TimeSeriesType.CURSOR;
workerChain.setOrExecuteNextWorker(w, chained);
}
boolean isShowCursorTimeSeries() {
return showCursorTimeSeries;
}
@Override
public void addTimeSeries(List<TimeSeries> timeSeriesList, TimeSeriesType type) {
final int timeSeriesCount;
final int collectionOffset;
if (TimeSeriesType.INSITU.equals(type)) {
timeSeriesCount = displayAxisMapping.getInsituCount();
collectionOffset = INSITU_COLLECTION_INDEX_OFFSET;
} else {
timeSeriesCount = displayAxisMapping.getRasterCount();
if (TimeSeriesType.CURSOR.equals(type)) {
collectionOffset = CURSOR_COLLECTION_INDEX_OFFSET;
} else {
collectionOffset = PIN_COLLECTION_INDEX_OFFSET;
}
}
final String[] aliasNames = getAliasNames();
for (int aliasIdx = 0; aliasIdx < aliasNames.length; aliasIdx++) {
final int targetCollectionIndex = collectionOffset + aliasIdx * 3;
final TimeSeriesCollection targetTimeSeriesCollection = (TimeSeriesCollection) timeSeriesPlot.getDataset(targetCollectionIndex);
if(targetTimeSeriesCollection != null) {
targetTimeSeriesCollection.removeAllSeries();
}
}
if(timeSeriesCount == 0) {
return;
}
final int numPositions = timeSeriesList.size() / timeSeriesCount;
int timeSeriesIndexOffset = 0;
for (int posIdx = 0; posIdx < numPositions; posIdx++) {
final Shape posShape = getShapeForPosition(type, posIdx);
for (int aliasIdx = 0; aliasIdx < aliasNames.length; aliasIdx++) {
final int targetCollectionIndex = collectionOffset + aliasIdx * 3;
final TimeSeriesCollection targetTimeSeriesCollection = (TimeSeriesCollection) timeSeriesPlot.getDataset(targetCollectionIndex);
if (targetTimeSeriesCollection == null) {
continue;
}
final XYItemRenderer renderer = timeSeriesPlot.getRenderer(targetCollectionIndex);
final int dataSourceCount = getDataSourceCount(type, aliasNames[aliasIdx]);
for (int ignoredIndex = 0; ignoredIndex < dataSourceCount; ignoredIndex++) {
final TimeSeries currentTimeSeries = timeSeriesList.get(timeSeriesIndexOffset);
targetTimeSeriesCollection.addSeries(currentTimeSeries);
final int timeSeriesTargetIdx = targetTimeSeriesCollection.getSeriesCount() - 1;
renderer.setSeriesShape(timeSeriesTargetIdx, posShape);
renderer.setSeriesPaint(timeSeriesTargetIdx, renderer.getSeriesPaint(timeSeriesTargetIdx % dataSourceCount));
renderer.setSeriesVisibleInLegend(timeSeriesTargetIdx, !currentTimeSeries.isEmpty());
timeSeriesIndexOffset++;
}
final ValueAxis axisForDataset = timeSeriesPlot.getDomainAxisForDataset(targetCollectionIndex);
axisForDataset.configure();
}
}
updateAnnotation(getCurrentView().getRaster());
}
@Override
public TimeSeries getValidatedTimeSeries(TimeSeries timeSeries, String dataSourceName, TimeSeriesType type) {
try {
return validation.validate(timeSeries, dataSourceName, type);
} catch (ParseException e) {
SystemUtils.LOG.log(Level.SEVERE, e.getMessage(), e);
throw new IllegalStateException(e);
}
}
@Override
public boolean isShowingSelectedPins() {
return isShowingSelectedPins;
}
@Override
public Placemark[] getSelectedPins() {
return getCurrentView().getSelectedPins();
}
@Override
public boolean isShowingAllPins() {
return isShowingAllPins;
}
private TimeSeriesGraphUpdater.WorkerChainSupport createWorkerChainSupport() {
return new TimeSeriesGraphUpdater.WorkerChainSupport() {
@Override
public void removeWorkerAndStartNext(TimeSeriesGraphUpdater worker) {
workerChain.removeCurrentWorkerAndExecuteNext(worker);
}
};
}
private TimeSeriesGraphUpdater.PositionSupport createPositionSupport() {
return new TimeSeriesGraphUpdater.PositionSupport() {
private final GeoCoding geoCoding = getTimeSeries().getTsProduct().getGeoCoding();
private final PixelPos pixelPos = new PixelPos();
@Override
public TimeSeriesGraphUpdater.Position transformGeoPos(GeoPos geoPos) {
geoCoding.getPixelPos(geoPos, pixelPos);
return new TimeSeriesGraphUpdater.Position((int) pixelPos.getX(), (int) pixelPos.getY(), 0);
}
};
}
private void initPlot() {
final ValueAxis domainAxis = timeSeriesPlot.getDomainAxis();
domainAxis.setAutoRange(true);
XYLineAndShapeRenderer xyRenderer = new XYLineAndShapeRenderer(true, true);
xyRenderer.setBaseLegendTextPaint(DEFAULT_FOREGROUND_COLOR);
timeSeriesPlot.setRenderer(xyRenderer);
timeSeriesPlot.setBackgroundPaint(DEFAULT_BACKGROUND_COLOR);
timeSeriesPlot.setNoDataMessage(NO_DATA_MESSAGE);
timeSeriesPlot.setDrawingSupplier(null);
}
private void updatePlot(boolean hasData) {
for (int i = 0; i < timeSeriesPlot.getDatasetCount(); i++) {
timeSeriesPlot.setDataset(i, null);
}
timeSeriesPlot.clearRangeAxes();
if (!hasData) {
return;
}
paintMap.clear();
final Set<String> aliasNamesSet = displayAxisMapping.getAliasNames();
final String[] aliasNames = aliasNamesSet.toArray(new String[aliasNamesSet.size()]);
for (String aliasName : aliasNamesSet) {
consumeColors(aliasName, displayAxisMapping.getRasterNames(aliasName), QUALIFIER_RASTER);
consumeColors(aliasName, displayAxisMapping.getInsituNames(aliasName), QUALIFIER_INSITU);
}
for (int aliasIdx = 0; aliasIdx < aliasNames.length; aliasIdx++) {
String aliasName = aliasNames[aliasIdx];
timeSeriesPlot.setRangeAxis(aliasIdx, createValueAxis(aliasName));
final int aliasIndexOffset = aliasIdx * 3;
final int cursorCollectionIndex = aliasIndexOffset + CURSOR_COLLECTION_INDEX_OFFSET;
final int pinCollectionIndex = aliasIndexOffset + PIN_COLLECTION_INDEX_OFFSET;
final int insituCollectionIndex = aliasIndexOffset + INSITU_COLLECTION_INDEX_OFFSET;
TimeSeriesCollection cursorDataset = new TimeSeriesCollection();
timeSeriesPlot.setDataset(cursorCollectionIndex, cursorDataset);
TimeSeriesCollection pinDataset = new TimeSeriesCollection();
timeSeriesPlot.setDataset(pinCollectionIndex, pinDataset);
TimeSeriesCollection insituDataset = new TimeSeriesCollection();
timeSeriesPlot.setDataset(insituCollectionIndex, insituDataset);
timeSeriesPlot.mapDatasetToRangeAxis(cursorCollectionIndex, aliasIdx);
timeSeriesPlot.mapDatasetToRangeAxis(pinCollectionIndex, aliasIdx);
timeSeriesPlot.mapDatasetToRangeAxis(insituCollectionIndex, aliasIdx);
final XYErrorRenderer pinRenderer = createXYErrorRenderer();
final XYErrorRenderer cursorRenderer = createXYErrorRenderer();
final XYErrorRenderer insituRenderer = createXYErrorRenderer();
pinRenderer.setBaseStroke(PIN_STROKE);
cursorRenderer.setBaseStroke(CURSOR_STROKE);
insituRenderer.setBaseLinesVisible(false);
insituRenderer.setBaseShapesFilled(false);
final List<String> rasterNamesSet = displayAxisMapping.getRasterNames(aliasName);
final String[] rasterNames = rasterNamesSet.toArray(new String[rasterNamesSet.size()]);
for (int i = 0; i < rasterNames.length; i++) {
final String paintKey = aliasName + QUALIFIER_RASTER + rasterNames[i];
final Paint paint = paintMap.get(paintKey);
cursorRenderer.setSeriesPaint(i, paint);
pinRenderer.setSeriesPaint(i, paint);
}
final List<String> insituNamesSet = displayAxisMapping.getInsituNames(aliasName);
final String[] insituNames = insituNamesSet.toArray(new String[insituNamesSet.size()]);
for (int i = 0; i < insituNames.length; i++) {
final String paintKey = aliasName + QUALIFIER_INSITU + insituNames[i];
final Paint paint = paintMap.get(paintKey);
insituRenderer.setSeriesPaint(i, paint);
}
timeSeriesPlot.setRenderer(cursorCollectionIndex, cursorRenderer);
timeSeriesPlot.setRenderer(pinCollectionIndex, pinRenderer);
timeSeriesPlot.setRenderer(insituCollectionIndex, insituRenderer);
}
}
private void consumeColors(String aliasName, List<String> names, String identifier) {
final int registeredPaints = paintMap.size();
for (int i = 0; i < names.size(); i++) {
final Paint paint = displayController.getPaint(registeredPaints + i);
paintMap.put(aliasName + identifier + names.get(i), paint);
}
}
private XYErrorRenderer createXYErrorRenderer() {
final XYErrorRenderer renderer = new XYErrorRenderer();
renderer.setDrawXError(false);
renderer.setDrawYError(false);
renderer.setBaseLinesVisible(true);
renderer.setAutoPopulateSeriesStroke(false);
renderer.setAutoPopulateSeriesPaint(false);
renderer.setAutoPopulateSeriesFillPaint(false);
renderer.setAutoPopulateSeriesOutlinePaint(false);
renderer.setAutoPopulateSeriesOutlineStroke(false);
renderer.setAutoPopulateSeriesShape(false);
final StandardXYToolTipGenerator tipGenerator;
tipGenerator = new StandardXYToolTipGenerator("Value: {2} Date: {1}", new SimpleDateFormat(), new DecimalFormat());
renderer.setBaseToolTipGenerator(tipGenerator);
return renderer;
}
private NumberAxis createValueAxis(String aliasName) {
String unit = getUnit(displayAxisMapping, aliasName);
String axisLabel = getAxisLabel(aliasName, unit);
NumberAxis valueAxis = new NumberAxis(axisLabel);
valueAxis.setAutoRange(true);
return valueAxis;
}
private AxisMapping createDisplayAxisMapping(AbstractTimeSeries timeSeries) {
final List<String> eoVariables = displayController.getEoVariablesToDisplay();
if (eoVariables.size() == 0) {
final Product.AutoGrouping autoGrouping = this.getCurrentView().getProduct().getAutoGrouping();
eoVariables.addAll(autoGrouping.stream().map(strings -> strings[0]).collect(Collectors.toList()));
}
final List<String> insituVariables = displayController.getInsituVariablesToDisplay();
final AxisMapping axisMapping = timeSeries.getAxisMapping();
return createDisplayAxisMapping(eoVariables, insituVariables, axisMapping);
}
private AxisMapping createDisplayAxisMapping(List<String> eoVariables, List<String> insituVariables, AxisMapping axisMapping) {
final AxisMapping displayAxisMapping = new AxisMapping();
for (String eoVariable : eoVariables) {
final String aliasName = axisMapping.getRasterAlias(eoVariable);
if (aliasName == null) {
displayAxisMapping.addRasterName(eoVariable, eoVariable);
} else {
displayAxisMapping.addRasterName(aliasName, eoVariable);
}
}
for (String insituVariable : insituVariables) {
final String aliasName = axisMapping.getInsituAlias(insituVariable);
if (aliasName == null) {
displayAxisMapping.addInsituName(insituVariable, insituVariable);
} else {
displayAxisMapping.addInsituName(aliasName, insituVariable);
}
}
return displayAxisMapping;
}
private String getUnit(AxisMapping axisMapping, String aliasName) {
final List<String> rasterNames = axisMapping.getRasterNames(aliasName);
for (List<Band> eoVariableBandList : eoVariableBands) {
for (String rasterName : rasterNames) {
final Band raster = eoVariableBandList.get(0);
if (raster.getName().startsWith(rasterName)) {
return raster.getUnit();
}
}
}
return "";
}
private static String getAxisLabel(String variableName, String unit) {
if (StringUtils.isNotNullAndNotEmpty(unit)) {
return String.format("%s (%s)", variableName, unit);
} else {
return variableName;
}
}
private String[] getAliasNames() {
final Set<String> aliasNamesSet = displayAxisMapping.getAliasNames();
return aliasNamesSet.toArray(new String[aliasNamesSet.size()]);
}
private Shape getShapeForPosition(TimeSeriesType type, int posIdx) {
final Shape posShape;
if (TimeSeriesType.CURSOR.equals(type)) {
posShape = TimeSeriesGraphDisplayController.CURSOR_SHAPE;
} else {
posShape = displayController.getShape(posIdx);
}
return posShape;
}
private int getDataSourceCount(TimeSeriesType type, String aliasName) {
if (TimeSeriesType.INSITU.equals(type)) {
return displayAxisMapping.getInsituNames(aliasName).size();
} else {
return displayAxisMapping.getRasterNames(aliasName).size();
}
}
private TimeSeriesGraphUpdater.VersionSafeDataSources createVersionSafeDataSources() {
return new TimeSeriesGraphUpdater.VersionSafeDataSources(
displayController.getPinPositionsToDisplay(), getVersion().get()) {
@Override
public int getCurrentVersion() {
return version.get();
}
};
}
private AbstractTimeSeries getTimeSeries() {
final ProductSceneView sceneView = getCurrentView();
if(sceneView == null) {
return null;
}
final Product sceneViewProduct = sceneView.getProduct();
return TimeSeriesMapper.getInstance().getTimeSeries(sceneViewProduct);
}
private ProductSceneView getCurrentView() {
return SnapApp.getDefault().getSelectedProductSceneView();
}
static interface ValidationListener {
void expressionChanged();
}
static interface Validation {
TimeSeries validate(TimeSeries timeSeries, String sourceName, TimeSeriesType type) throws ParseException;
void adaptTo(Object timeSeriesKey, AxisMapping axisMapping);
void addValidationListener(ValidationListener listener);
}
}