/*******************************************************************************
* Mission Control Technologies, Copyright (c) 2009-2012, United States Government
* as represented by the Administrator of the National Aeronautics and Space
* Administration. All rights reserved.
*
* The MCT platform is licensed under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* MCT includes source code licensed under additional open source licenses. See
* the MCT Open Source Licenses file included with this distribution or the About
* MCT Licenses dialog available at runtime from the MCT Help menu for additional
* information.
*******************************************************************************/
package gov.nasa.arc.mct.fastplot.bridge;
import gov.nasa.arc.mct.components.FeedProvider;
import gov.nasa.arc.mct.fastplot.bridge.PlotConstants.AxisOrientationSetting;
import gov.nasa.arc.mct.fastplot.bridge.PlotConstants.TimeAxisSubsequentBoundsSetting;
import java.awt.Color;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.swing.Timer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import plotter.xy.CompressingXYDataset;
import plotter.xy.XYPlotContents;
/**
* Manages the data associated with a plot. This class supports<br>
* <ul>
* <li>adding data and data sets
* <li>compressing data (during adds)
* <li>sizing plot data buffers based on the number of pixels available for the plot on the screen.
* </ul>
*/
public class PlotDataManager implements AbstractPlotDataManager {
private final static Logger logger = LoggerFactory.getLogger(PlotDataManager.class);
/** Do we allow new values in data sets - should always be true. */
final static boolean DATA_SET_ENABLE_UPDATE_STATE = true;
/** Do we force the data buffer to be truncated when it becomes full */
final static boolean DATA_SET_BUFFER_TRUNCATE_STATE = true;
/** QC provide a different mechanism for triggering rescales on the non-time axis to us.
This constant is the number and it must be set to ZERO. We use % padding to control
growth on this axis. */
final static int MIN_SAMPLES_FOR_AUTOSCALE = 0;
/** The Set of data items to be displayed on this plot. */
private Map<String, PlotDataSeries> dataSeries;
/** The QuinnCurtis plot on which we're displaying our data. */
private PlotterPlot plot;
/** Cache for maintaining min/max non time values displayed on the plot */
private PlotNonTimeMinMaxValueManager minMaxValueManager;
/** Timer to wait for user window resize actions to complete before
requesting new data at the window's updated compression ratio. */
private Timer resizeTimmer;
/** We only need to resize when the time axis dimension of the window is resized.
This variable caches the previous size so upon a resize event we can test to
see if the new size differs from the old. */
private int previousTimeAxisDimensionSize = -1;
/** Flag to record if a request needs to be made for a plot buffer update but that
request could not happen because an updateFromFeed event was in process. */
private boolean bufferRequestWaiting = false;
/** Flag to record if a buffer truncation event occurred on a scrunch plot.*/
private boolean scrunchBufferTruncationOccured = false;
/** Span of the plot data buffer. */
private GregorianCalendar plotDataBufferStartTime;
private GregorianCalendar plotDataBufferEndTime;
/**
* Create a datamanager for the plot passed in
* @param thePlot to manage data for
*/
public PlotDataManager(PlotterPlot thePlot) {
plot = thePlot;
dataSeries = new HashMap<String, PlotDataSeries>(PlotConstants.MAX_NUMBER_OF_DATA_ITEMS_ON_A_PLOT,
PlotConstants.MAX_NUMBER_OF_DATA_ITEMS_ON_A_PLOT);
minMaxValueManager = new PlotNonTimeMinMaxValueManager(this);
setupResizeTimmer();
}
/**
* Setup a timer to cause a delay before data update requests are made when the
* plot window is resized.
*/
private void setupResizeTimmer() {
resizeTimmer = new Timer(PlotConstants.RESIZE_TIMER, new ActionListener() {
public void actionPerformed(ActionEvent e) {
resizeAndReloadPlotBuffer();
}
});
resizeTimmer.setRepeats(false);
}
/* (non-Javadoc)
* @see gov.nasa.arc.mct.fastplot.bridge.AbstractPlotDataManager#addDataSet(java.lang.String, java.awt.Color)
*/
@Override
public void addDataSet(String dataSetName, Color plottingColor) {
if (dataSetName != null) {
// This is the first data item, setup up the plot buffer size etc.
if (dataSeries.size() == 0) {
setupBufferSizeAndCompressionRatio();
}
LegendEntry legendEntry = new LegendEntry(PlotConstants.LEGEND_BACKGROUND_COLOR, plottingColor, Font.decode(Font.SANS_SERIF).deriveFont(9f), plot.getPlotLabelingAlgorithm());
legendEntry.setDataSetName(dataSetName);
dataSeries.put(dataSetName, new PlotDataSeries(plot, dataSetName, plottingColor));
// create the legend.
legendEntry.setPlot(dataSeries.get(dataSetName).getPlot());
legendEntry.setRegressionLine(dataSeries.get(dataSetName).getRegressionLine());
dataSeries.get(dataSetName).setLegend(legendEntry);
}
}
void addDataSet(String dataSetName, Color plottingColor, String displayName) {
addDataSet(dataSetName, plottingColor);
assert dataSeries.get(dataSetName).getLegendEntry() != null : "Legend entry null!";
dataSeries.get(dataSetName).getLegendEntry().setBaseDisplayName(displayName);
}
boolean isKnownDataSet(String setName) {
assert dataSeries!=null;
return dataSeries.containsKey(setName);
}
int getDataSetSize() {
return dataSeries.size();
}
/* (non-Javadoc)
* @see gov.nasa.arc.mct.fastplot.bridge.AbstractPlotDataManager#addData(java.lang.String, java.util.SortedMap)
*/
@Override
public void addData(String feed, SortedMap<Long, Double> points) {
assert plot.getPlotView() !=null : "Plot Object not initalized";
assert isKnownDataSet(feed) : "Data set " + feed + " not defined.";
if(points.isEmpty()) {
return;
}
setupCompressionRatio();
// prevent plotting of data if it is not compatible with scrunch settings.
if(plot.getTimeAxisSubsequentSetting() == TimeAxisSubsequentBoundsSetting.SCRUNCH) {
boolean needsFixing = false;
for(Long time : points.keySet()) {
if(time <= plot.getMinTime()) {
needsFixing = true;
break;
}
}
if(needsFixing) {
SortedMap<Long, Double> points2 = new TreeMap<Long, Double>();
for(Entry<Long, Double> point : points.entrySet()) {
if(point.getKey() > plot.getMinTime()) {
points2.put(point.getKey(), point.getValue());
}
}
points = points2;
}
}
// Don't plot points off the end if the time axis is pinned
if (plot.getPlotAbstraction().getTimeAxis().isPinned()) {
long max = plot.getMaxTime();
boolean needsFixing = false;
for(Long time : points.keySet()) {
if(time > max) {
needsFixing = true;
break;
}
}
if(needsFixing) {
SortedMap<Long, Double> points2 = new TreeMap<Long, Double>();
for(Entry<Long, Double> point : points.entrySet()) {
if(point.getKey() <= max) {
points2.put(point.getKey(), point.getValue());
}
}
points = points2;
dataSeries.get(feed).setUpdateRegressionLine(false);
}
}
if(points.isEmpty()) {
return;
}
CompressingXYDataset dataset = dataSeries.get(feed).getData();
double min;
double max;
if(plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME) {
min = dataset.getMinX();
max = dataset.getMaxX();
} else {
min = dataset.getMinY();
max = dataset.getMaxY();
}
double datasetMinTime = Math.min(min, max);
double datasetMaxTime = Math.max(min, max);
if(dataset.getPointCount() == 0 || points.firstKey() >= datasetMaxTime) {
// TODO: Change this to use an aggregate add method
if(plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME) {
for(Entry<Long, Double> point : points.entrySet()) {
dataset.add(point.getKey(), point.getValue());
}
} else {
for(Entry<Long, Double> point : points.entrySet()) {
dataset.add(point.getValue(), point.getKey());
}
}
if (plot.getMaxTime() >= datasetMaxTime) {
dataSeries.get(feed).setUpdateRegressionLine(true);
}
} else if(points.lastKey() <= datasetMinTime) {
// TODO: Make this efficient
double[] x = new double[points.size()];
double[] y = new double[x.length];
int i = 0;
for(Entry<Long, Double> p : points.entrySet()) {
x[i] = p.getKey();
y[i] = p.getValue();
i++;
}
if(plot.getAxisOrientationSetting() == AxisOrientationSetting.Y_AXIS_AS_TIME) {
double[] tmp = x;
x = y;
y = tmp;
}
dataset.prepend(x, 0, y, 0, x.length);
} else {
// Data appearing in the middle of the dataset.
// Assume that it's caused by the last second of data arriving twice,
// once from the initial historical request and once from the once-per-second update.
// It may also be values for predictive data we already loaded.
// In either case, the overlapping data should be identical to what we already have, so ignore it.
// Append the data that isn't redundant.
SortedMap<Long, Double> before = points.subMap(0L, (long) datasetMinTime);
SortedMap<Long, Double> after = points.subMap((long) datasetMaxTime, Long.MAX_VALUE);
SortedMap<Long, Double> overlap = points.subMap((long) datasetMinTime, (long) datasetMaxTime);
if(!overlap.isEmpty()) {
if(overlap.lastKey() - overlap.firstKey() > 10000) {
logger.warn("Cannot currently insert into the middle of a dataset: minX = " + datasetMinTime + ", maxX = " + datasetMaxTime
+ ", firstKey = " + points.firstKey() + ", lastKey = " + points.lastKey());
}
}
// TODO: Change this to use an aggregate add method
if(plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME) {
if(!before.isEmpty()) {
double[] x = new double[before.size()];
double[] y = new double[x.length];
int i = 0;
for(Entry<Long, Double> point : before.entrySet()) {
x[i] = point.getKey();
y[i] = point.getValue();
i++;
}
dataset.prepend(x, 0, y, 0, x.length);
}
for(Entry<Long, Double> point : after.entrySet()) {
dataset.add(point.getKey(), point.getValue());
}
} else {
if(!before.isEmpty()) {
double[] x = new double[before.size()];
double[] y = new double[x.length];
int i = 0;
for(Entry<Long, Double> point : before.entrySet()) {
y[i] = point.getKey();
x[i] = point.getValue();
i++;
}
dataset.prepend(x, 0, y, 0, x.length);
}
for(Entry<Long, Double> point : after.entrySet()) {
dataset.add(point.getValue(), point.getKey());
}
}
}
dataSeries.get(feed).updateRegressionLine();
for(Entry<Long, Double> point : points.entrySet()) {
Long timestamp = point.getKey();
Double value = point.getValue();
boolean isValidForPlot = !Double.isNaN(value);
if (isValidForPlot) {
minMaxValueManager.updateMinMaxCache(timestamp, value);
}
}
for(Entry<Long, Double> e : points.entrySet()) {
plot.getLimitManager().informPointPlottedAtTime(e.getKey(), e.getValue());
}
if (plot instanceof PlotterPlot) plot.setInitialized();
}
void updateLegend(String dataSetName, FeedProvider.RenderingInfo info) {
dataSeries.get(dataSetName).getLegendEntry().setData(info);
}
double getNonTimeMaxDataValueCurrentlyDisplayed() {
return minMaxValueManager.getNonTimeMaxDataValueCurrentlyDisplayed();
}
double getNonTimeMinDataValueCurrentlyDisplayed() {
return minMaxValueManager.getNonTimeMinDataValueCurrentlyDisplayed();
}
/**
* Returns true if a time is not valid with the Quinn Curtis scrunch mode panel on a plot. False, otherwise.
* @param time the time to evaluate
* @return true if the time is not valid for a plot's scrunch mode. True otherwise.
*/
boolean scrunchProtect(long time) {
// Protection only applies when we are in scrunch mode
if (plot.getTimeAxisSubsequentSetting() == TimeAxisSubsequentBoundsSetting.SCRUNCH) {
// protection required if the time is before or equal to the plot's starts time.
return time <= plot.getMinTime();
} else {
// not in scrunch mode.
return false;
}
}
/**
* An event has occurred that means the plots buffer needs to be resized
* and data requested at the new resolution demanded by that buffer.
*
* If an update from feed event is in process when a processDataBufferResizeEvent is
* requested, a flag will be set. When the updateFromFeed event is completed, it will check
* for waiting buffer requests and initiate one.
*/
public void resizeAndReloadPlotBuffer() {
if (!plot.isUpdateFromCacheDataStreamInProcess()) {
bufferRequestWaiting = false;
resetPlotDataVariablesAndRequestDataRefreshAtNewResolution();
} else {
// update is locked out as we're in the middle of an updateFromFeedEvent.
// Note that a bufferRequest is waiting.
bufferRequestWaiting = true;
}
}
void setupCompressionRatio() {
AbstractAxis axis = plot.getTimeAxis();
double start = axis.getStart();
double end = axis.getEnd();
assert start != end;
XYPlotContents contents = plot.getPlotView().getContents();
// the height or width could be zero if the plot is showing in an area which is closed. One scenario is the inspector area where the slider is
// closed
double width = Math.max(0,plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME ? contents.getWidth() : contents.getHeight());
double compressionScale = width == 0 ? Double.MAX_VALUE : Math.abs(end - start) / width;
if(plot.getTimeAxisSubsequentSetting() == TimeAxisSubsequentBoundsSetting.SCRUNCH) {
for(PlotDataSeries s : dataSeries.values()) {
CompressingXYDataset d = s.getData();
double scale = d.getCompressionScale();
// Compress by integral factors to minimize artifacts from multiple compression runs
if(scale == 0) {
scale = compressionScale;
} else {
while(scale < compressionScale / 2) {
scale *= 2;
}
}
if(scale > d.getCompressionScale()) {
d.setCompressionOffset(start);
d.setCompressionScale(scale);
d.recompress();
}
}
} else {
for(PlotDataSeries s : dataSeries.values()) {
CompressingXYDataset d = s.getData();
d.setCompressionOffset(start);
d.setCompressionScale(compressionScale);
}
}
}
/**
* Setup the data compression ratio and plot buffer sizes based on the number of pixels on the plot's span
*/
void setupBufferSizeAndCompressionRatio() {
setupPlotBufferMinAndMaxTimes();
setupCompressionRatio();
}
/**
* Reset every data series on the plot.
* remove all process variables from the scroll frame.
* It would be preferable to have each PlotDataSeries remove itself. However,
* QC only provides a single method to remove all.
*/
void resetPlotDataSeries() {
for(String datasetName : dataSeries.keySet()) {
dataSeries.get(datasetName).resetData();
}
}
/**
* Reset the plot's buffer and compression ratio to the current plot window's size and request new data and the
* updated compression ratio.
*/
private void resetPlotDataVariablesAndRequestDataRefreshAtNewResolution() {
// Setup size of the buffer and compression ration of the plot.
setupBufferSizeAndCompressionRatio();
/* IMPORTANT
* setup the buffer size and compression ratio before resstPlotDataSeris is called as the data series
* is scaled to the buffer size calculated in the above call.
*/
if (!scrunchBufferTruncationOccured) {
// prevent further resize events from occurring until this event is completed.
plot.setUpdateFromCacheDataStreamInProcess(true);
// Window size has changed so recalculated compression ratio;
assert plotDataBufferEndTime.after(plotDataBufferStartTime) : "Attempting a request to the data buffer with negative span";
// limit request window to PLOT_DATA_BUFFER_SLIZE_REQUEST_SIZE
if (plotDataBufferEndTime.getTimeInMillis() - plotDataBufferStartTime.getTimeInMillis() > PlotConstants.MAXIMUM_PLOT_DATA_BUFFER_SLIZE_REQUEST_SIZE) {
plotDataBufferStartTime.setTimeInMillis(plotDataBufferEndTime.getTimeInMillis() - PlotConstants.MAXIMUM_PLOT_DATA_BUFFER_SLIZE_REQUEST_SIZE);
}
requestDataFromMCTBuffer(plotDataBufferStartTime, plotDataBufferEndTime);
} else {
logger.debug("Refreshing a scrunch plots data buffer from its own buffer.");
// for scrunch plots, we compress the existing data in the plots local buffer when a truncation event occurs.
// We compress that data further rather than accept the overhead of going to the MCT data buffer and compressing
// data at full fidelity.
assert plot.getTimeAxisSubsequentSetting() == TimeAxisSubsequentBoundsSetting.SCRUNCH: "A scrunch event has occured on a non scrunch plot!";
minMaxValueManager.setMinMaxCacheState(false);
// // We will reset all the process vars from this plot, so remove all from scroll frame.
// plot.removeAllProcessVarFromScrollFrame();
for (String key: dataSeries.keySet()) {
PlotDataSeries data = dataSeries.get(key);
data.compressByFiftyPercent();
}
minMaxValueManager.setMinMaxCacheState(true);
}
}
/**
* Determine the span of the plot data buffer based upon the current PlotDisplayState.
*/
void setupPlotBufferMinAndMaxTimes() {
// we only need to buffer from the start to end time of the plot.
plotDataBufferStartTime = plot.getCurrentTimeAxisMin();
plotDataBufferEndTime = plot.getCurrentTimeAxisMax();
assert plotDataBufferStartTime != null : "buffer start time not intialized when it should have been";
assert plotDataBufferEndTime != null : "buffer end time not intialized when it should have been";
}
/**
* Request data from the MCT buffer spanning startTime to endTime at the specified compression ratio. The method
* will also
* @param startTime the start time of the data requested
* @param endTime the end time of the data requested
*/
private void requestDataFromMCTBuffer(GregorianCalendar startTime, GregorianCalendar endTime) {
if (startTime == null || endTime == null) {
throw new IllegalArgumentException("Start and/or end time was null.");
}
// Don't request if local controls are not enabled. This only occurs when we are a secondary plot in a stacked plot and we
// rely on the master plot in the stack to make requests.
if (plot.getPlotAbstraction() != null && plot.isTimeLabelEnabled) {
// Request new data.
plot.getPlotAbstraction().requestPlotData(startTime, endTime);
}
}
void informUpdateFromLiveDataStreamStarted() {
// nothing to do.
}
void informUpdateFromLiveDataStreamCompleted() {
if (scrunchBufferTruncationOccured || bufferRequestWaiting) {
resizeAndReloadPlotBuffer();
// flag must be reset after resizeAndReloadPlotBuffer call
// as method uses this flag to determine if it is process a scrunch truncation event.
// clear the waiting flags
scrunchBufferTruncationOccured = false;
bufferRequestWaiting = false;
}
}
/* (non-Javadoc)
* @see gov.nasa.arc.mct.fastplot.bridge.AbstractPlotDataManager#informUpdateCacheDataStreamStarted()
*/
@Override
public void informUpdateCacheDataStreamStarted() {
minMaxValueManager.setMinMaxCacheState(false);
resetPlotDataSeries();
// There should be no data on the plot at this point.
}
void informUpdateCacheDataStreamCompleted() {
logger.debug("Update from cache completed" );
minMaxValueManager.setMinMaxCacheState(true);
if (scrunchBufferTruncationOccured) {
resizeAndReloadPlotBuffer();
// flag must be reset after resizeAndReloadPlotBuffer call
// as method uses this flag to determine if it is process a scrunch truncation event.
scrunchBufferTruncationOccured = false;
}
}
void informBufferTrunctionEventOccured() {
if (plot.getTimeAxisSubsequentSetting() == TimeAxisSubsequentBoundsSetting.SCRUNCH &&
plot.isCompressionEnabled()) {
logger.debug("Scrunch truncation event occured");
// record that a buffer truncation event occurred.
scrunchBufferTruncationOccured = true;
}
}
/**
* Inform the data manager that a resize event has occurred. The method
* determines if the event changed the size of the time axis and if it did starts
* the resize timmer which will cause the plots buffer to be resized and refreshed.
*/
public void informResizeEvent() {
// only initiate a resize if the time axis has change size.
Rectangle bounds = plot.getPlotView().getContents().getBounds();
int currentSize = (int) (
(plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME) ?
bounds.getWidth() : bounds.getHeight() ) ;
if (currentSize != previousTimeAxisDimensionSize) {
resizeTimmer.restart();
}
// cache the now current size to compare with when the next resize event occurs.
previousTimeAxisDimensionSize = currentSize;
}
double getTimeAxisWidthInPixes() {
Rectangle bounds = plot.getPlotView().getContents().getBounds();
if (plot.getAxisOrientationSetting() == AxisOrientationSetting.X_AXIS_AS_TIME) {
return bounds.getWidth();
} else {
return bounds.getHeight();
}
}
boolean isBufferRequestWaiting() {
return bufferRequestWaiting;
}
boolean hasScrunchTruncationOccured() {
return scrunchBufferTruncationOccured;
}
@Override
public PlotDataSeries getNamedDataSeries(String name) {
return dataSeries.get(name);
}
/**
* @param dataSeries the dataSeries to set
*/
public void setDataSeries(Map<String, PlotDataSeries> dataSeries) {
this.dataSeries = dataSeries;
}
/**
* @return the dataSeries
*/
public Map<String, PlotDataSeries> getDataSeries() {
return dataSeries;
}
/**
* @param plot the plot to set
*/
public void setPlot(PlotterPlot plot) {
this.plot = plot;
}
/**
* @return the plot
*/
public AbstractPlottingPackage getPlot() {
return plot;
}
}