/* * 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); } }