/******************************************************************************* * 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 plotter.xy; import java.util.HashSet; import java.util.Set; import plotter.DoubleData; import plotter.xy.Compressor.StreamingCompressor; /** * Implementation of an XY dataset that supports truncation and compression. * Note that the behavior is undefined if you manipulate the DoubleData buffers directly. * Points in the dataset must be stored in increasing X value. * @author Adam Crume */ public class CompressingXYDataset implements XYDataset { /** Plot line that displays the data. */ private LinearXYPlotLine line; /** Holds the X data. */ private DoubleData xData; /** Holds the Y data. */ private DoubleData yData; /** * Points under this value will be truncated. * @see #truncationOffset */ private double truncationPoint = Double.NEGATIVE_INFINITY; /** When truncating, this many points will be kept that are under {@link #truncationPoint}. */ private int truncationOffset = 1; /** Cached minimum X value of all points in the dataset. */ private double minX = Double.POSITIVE_INFINITY; /** Cached maximum X value of all points in the dataset. */ private double maxX = Double.NEGATIVE_INFINITY; /** Cached minimum Y value of all points in the dataset. */ private double minY = Double.POSITIVE_INFINITY; /** Cached maximum Y value of all points in the dataset. */ private double maxY = Double.NEGATIVE_INFINITY; /** Listeners that get notified if the X min or max changes. May be null. */ private Set<MinMaxChangeListener> xMinMaxListeners; /** Listeners that get notified if the Y min or max chagnes. May be null. */ private Set<MinMaxChangeListener> yMinMaxListeners; /** Value of {@link #minX} before a modification. */ private double oldMinX; /** Value of {@link #maxX} before a modification. */ private double oldMaxX; /** Value of {@link #minY} before a modification. */ private double oldMinY; /** Value of {@link #maxX} before a modification. */ private double oldMaxY; /** Performs the compression. */ private Compressor compressor; /** Performs the streaming compression. */ private StreamingCompressor streamingCompressor; /** Offset to use for compression. */ private double compressionOffset; /** Scale to use for compression. */ private double compressionScale; /** * Creates a dataset. * @param line line to plot the data * @param compressor performs the compression */ public CompressingXYDataset(LinearXYPlotLine line, Compressor compressor) { if(line.getIndependentDimension() == null) { throw new IllegalArgumentException("Can't use CompressingXYDataset with a scatter plot"); } this.line = line; this.compressor = compressor; xData = line.getXData(); yData = line.getYData(); } /** * Adds a point, truncating the buffer if necessary. * For the independent dimension, the coordinate must be must be greater than or equal to all other values in the dataset for that dimension. * @param x the X coordinate * @param y the Y coordinate */ @Override public void add(double x, double y) { preMod(); truncate(); if(streamingCompressor == null) { XYDataset out; if(line.getIndependentDimension() == XYDimension.X) { out = line; } else { out = new XYReversingDataset(line); } streamingCompressor = compressor.createStreamingCompressor(out, compressionOffset, compressionScale); } int updateSize; if(line.getIndependentDimension() == XYDimension.X) { updateSize = streamingCompressor.add(x, y); } else { updateSize = streamingCompressor.add(y, x); } updateMinMax(x, y); postMod(); } /** * Called after a modification. * Notifies any relevant listeners of changes. */ protected void postMod() { if(xMinMaxListeners != null) { if(minX != oldMinX || maxX != oldMaxX) { for(MinMaxChangeListener listener : xMinMaxListeners) { listener.minMaxChanged(this, XYDimension.X); } } } if(yMinMaxListeners != null) { if(minY != oldMinY || maxY != oldMaxY) { for(MinMaxChangeListener listener : yMinMaxListeners) { listener.minMaxChanged(this, XYDimension.Y); } } } } /** * Called before a modification. * Stores any state needed for {@link #postMod()}. */ protected void preMod() { oldMinX = minX; oldMaxX = maxX; oldMinY = minY; oldMaxY = maxY; } private void truncate() { DoubleData data = line.getIndependentDimension() == XYDimension.Y ? yData : xData; int length = data.getLength(); // We assume here that not many points will be truncated, so a simple linear search is best. // If a large number of points may be truncated, a binary search may be better. int i = 0; while(i < length && data.get(i) < truncationPoint) { i++; } i -= truncationOffset; if(i > 0) { _removeFirst(i); } } /** * Removes the first <code>removeCount</code> points from the dataset. * Does not call {@link #preMod()} or {@link #postMod()}. * @param removeCount number of points to remove */ private void _removeFirst(int removeCount) { // TODO: Use a more efficient method than rescanning the entire arrays. For example, cache min/max values for subranges. int length = xData.getLength(); boolean rescanX = false; boolean rescanY = false; for(int i = 0; i < removeCount; i++) { double xd = xData.get(i); double yd = yData.get(i); if(xd == minX || xd == maxX) { rescanX = true; } if(yd == minY || yd == maxY) { rescanY = true; } } XYDimension independentDimension = line.getIndependentDimension(); if(rescanX) { if(independentDimension == XYDimension.X) { if(removeCount < length) { minX = xData.get(removeCount); maxX = xData.get(length - 1); } else { minX = Double.POSITIVE_INFINITY; maxX = Double.NEGATIVE_INFINITY; } } else { minX = Double.POSITIVE_INFINITY; maxX = Double.NEGATIVE_INFINITY; for(int i = removeCount; i < length; i++) { double xd = xData.get(i); if(xd > maxX) { maxX = xd; } if(xd < minX) { minX = xd; } } } } if(rescanY) { if(independentDimension == XYDimension.Y) { if(removeCount < length) { minY = yData.get(removeCount); maxY = yData.get(length - 1); } else { minY = Double.POSITIVE_INFINITY; maxY = Double.NEGATIVE_INFINITY; } } else { minY = Double.POSITIVE_INFINITY; maxY = Double.NEGATIVE_INFINITY; for(int i = removeCount; i < length; i++) { double yd = yData.get(i); if(yd > maxY) { maxY = yd; } if(yd < minY) { minY = yd; } } } } line.removeFirst(removeCount); } @Override public void prepend(double[] x, int xoff, double[] y, int yoff, int len) { preMod(); PointData input = new PointData(); XYDimension independentDimension = line.getIndependentDimension(); if(independentDimension == XYDimension.X) { input.getX().add(x, xoff, len); input.getY().add(y, yoff, len); } else { input.getY().add(x, xoff, len); input.getX().add(y, yoff, len); } PointData output = new PointData(); compressor.compress(input, output, compressionOffset, compressionScale); DoubleData outx; DoubleData outy; if(independentDimension == XYDimension.X) { outx = output.getX(); outy = output.getY(); } else { outy = output.getX(); outx = output.getY(); } for(int i = 0; i < len; i++) { updateMinMax(x[xoff + i], y[yoff + i]); } // TODO: Only add data that wouldn't be truncated line.prepend(outx, outy); postMod(); } @Override public void prepend(DoubleData x, DoubleData y) { preMod(); PointData input = new PointData(x, y); PointData output = new PointData(); compressor.compress(input, output, compressionOffset, compressionScale); DoubleData outx; DoubleData outy; XYDimension independentDimension = line.getIndependentDimension(); if(independentDimension == XYDimension.X) { outx = output.getX(); outy = output.getY(); } else { outy = output.getX(); outx = output.getY(); } int length = outx.getLength(); for(int i = 0; i < length; i++) { updateMinMax(outx.get(i), outy.get(i)); } // TODO: Only add data that wouldn't be truncated line.prepend(outx, outy); postMod(); } /** * Updates the min/max cache to include this point. * @param x X coordinate * @param y Y coordinate */ private void updateMinMax(double x, double y) { if(x > maxX) { maxX = x; } if(x < minX) { minX = x; } if(y > maxY) { maxY = y; } if(y < minY) { minY = y; } } @Override public void removeAllPoints() { preMod(); line.removeAllPoints(); minX = Double.POSITIVE_INFINITY; maxX = Double.NEGATIVE_INFINITY; minY = Double.POSITIVE_INFINITY; maxY = Double.NEGATIVE_INFINITY; postMod(); } /** * Recompresses the existing data. * This is useful if the compression scale has increased since data was added. * Data may lose fidelity if compressed multiple times. */ public void recompress() { DoubleData newx =xData.clone(); DoubleData newy =yData.clone(); line.removeAllPoints(); prepend(newx, newy); } @Override public int getPointCount() { return xData.getLength(); } /** * Returns the X data. * @return the X data */ public DoubleData getXData() { return xData; } /** * Sets the X data. * @param xData the X data */ public void setXData(DoubleData xData) { this.xData = xData; } /** * Returns the Y data. * @return the Y data */ public DoubleData getYData() { return yData; } /** * Sets the Y data. * @param yData the Y data */ public void setYData(DoubleData yData) { this.yData = yData; } /** * Returns the truncation point. * @return the truncation point */ public double getTruncationPoint() { return truncationPoint; } /** * Sets the truncation point. * @param truncationPoint the truncation point */ public void setTruncationPoint(double truncationPoint) { this.truncationPoint = truncationPoint; } /** * Returns the truncation offset. * @return the truncation offset */ public int getTruncationOffset() { return truncationOffset; } /** * Sets the truncation offset. * @param truncationOffset the truncation offset */ public void setTruncationOffset(int truncationOffset) { this.truncationOffset = truncationOffset; } /** * Returns the compression offset. * @return the compression offset */ public double getCompressionOffset() { return compressionOffset; } /** * Sets the compression offset. * @param compressionOffset the compression offset */ public void setCompressionOffset(double compressionOffset) { assert !Double.isNaN(compressionOffset); assert !Double.isInfinite(compressionOffset); if(this.compressionOffset != compressionOffset) { this.compressionOffset = compressionOffset; streamingCompressor = null; } } /** * Returns the compression scale. * @return the compression scale */ public double getCompressionScale() { return compressionScale; } /** * Sets the compression scale. * @param compressionScale the compression scale */ public void setCompressionScale(double compressionScale) { assert !Double.isNaN(compressionScale) : "Scale cannot be NaN"; assert !Double.isInfinite(compressionScale) : "Scale cannot be infinite"; if(this.compressionScale != compressionScale) { this.compressionScale = compressionScale; streamingCompressor = null; } } /** * Returns the minimum X value. * @return the minimum X value */ public double getMinX() { return minX; } /** * Returns the maximum X value. * @return the maximum X value */ public double getMaxX() { return maxX; } /** * Returns the minimum Y value. * @return the minimum Y value */ public double getMinY() { return minY; } /** * Returns the maximum Y value. * @return the maximum Y value */ public double getMaxY() { return maxY; } /** * Adds a listener for min/max changes to the X data. * @param listener listener to add */ public void addXMinMaxChangeListener(MinMaxChangeListener listener) { if(xMinMaxListeners == null) { xMinMaxListeners = new HashSet<MinMaxChangeListener>(); } xMinMaxListeners.add(listener); } /** * Adds a listener for min/max changes to the Y data. * @param listener listener to add */ public void addYMinMaxChangeListener(MinMaxChangeListener listener) { if(yMinMaxListeners == null) { yMinMaxListeners = new HashSet<MinMaxChangeListener>(); } yMinMaxListeners.add(listener); } /** * Removes all {@link MinMaxChangeListener}s for the Y data. */ public void removeAllYMinMaxChangeListeners() { yMinMaxListeners = null; } /** * Removes all {@link MinMaxChangeListener}s for the X data. */ public void removeAllXMinMaxChangeListeners() { xMinMaxListeners = null; } /** * Note that this may not behave as expected. * In particular, removing as many points as you added will not work, * because the added points may have been compressed into a smaller number. */ @Override public void removeLast(int count) { line.removeLast(count); } /** * Listens to min/max changes. * @author Adam Crume */ public interface MinMaxChangeListener { /** * Notifies the listener that the min or max changed. * @param dataset dataset containing the data * @param dimension specifies whether this notification is for the X or Y data */ public void minMaxChanged(CompressingXYDataset dataset, XYDimension dimension); } }