blob: e2a3e527fa47337a7f62721fadd66ae6ae61c438 [file] [log] [blame]
package com.pivotal.javafx.scene.chart;
import java.nio.ByteBuffer;
import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import com.pivotal.jvsd.fx.HoveredThresholdNode;
/**
* A named series of data items
*/
public abstract class ByteBufferNumberSeries<X extends Number, Y extends Number> extends BasicSeries<X, Y> {
// -------------- PUBLIC PROPERTIES ----------------------------------------
// TODO remove
/** ObservableList of data items that make up this series */
private final ObjectProperty<ByteBuffer> bufferProperty = new SimpleObjectProperty<ByteBuffer>() {
protected void invalidated() {
buffer = get();
}
};
// here for speed over getBuffer();
protected ByteBuffer buffer;
public final ByteBuffer getBuffer() {
return bufferProperty.get();
}
public final void setBuffer(ByteBuffer value) {
bufferProperty.setValue(value);
}
public final ObjectProperty<ByteBuffer> bufferProperty() {
return bufferProperty;
}
// // TODO remove
// /** ObservableList of data items that make up this series */
// private final ObjectProperty<ObservableList<Data<X, Y>>> data = new ObjectPropertyBase<ObservableList<Data<X, Y>>>() {
// @Override
// protected void invalidated() {
// // TODO update buffer, minX, maxX, etc.
// }
//
// @Override
// public Object getBean() {
// return ByteBufferNumberSeries.this;
// }
//
// @Override
// public String getName() {
// return "data";
// }
// };
//
// /*
// * (non-Javadoc)
// *
// * @see com.pivotal.javafx.scene.chart.Series#getData()
// */
// // TODO remove
// @Override
// public final ObservableList<Data<X, Y>> getData() {
// return data.getValue();
// // System.out.println("getData:");
// // return ;
// }
//
// /*
// * (non-Javadoc)
// *
// * @see
// * com.pivotal.javafx.scene.chart.Series#setData(javafx.collections.ObservableList
// * )
// */
// // TODO remove
// @Override
// public final void setData(ObservableList<Data<X, Y>> value) {
// data.setValue(value);
// }
//
// /*
// * (non-Javadoc)
// *
// * @see com.pivotal.javafx.scene.chart.Series#dataProperty()
// */
// // TODO remove
// @Override
// public final ObjectProperty<ObservableList<Data<X, Y>>> dataProperty() {
// return data;
// }
// -------------- CONSTRUCTORS ----------------------------------------------
/**
* Construct a empty series
*/
public ByteBufferNumberSeries() {
super();
buffer = null;
}
/**
* Constructs a Series and populates it with the given {@link ObservableList}
* data.
*
* @param data
* ObservableList of MultiAxisChart.Data
*/
public ByteBufferNumberSeries(ByteBuffer buffer) {
super();
setBuffer(buffer);
//setData(FXCollections.observableArrayList(new Data(0, 1000000), new Data(10000000000000l, -100000)));
setData(FXCollections.observableArrayList(new ThresholdDataCollection(0, getDataSize(), 1000)));
}
/**
* Constructs a named Series and populates it with the given
* {@link ObservableList} data.
*
* @param name
* a name for the series
* @param data
* ObservableList of MultiAxisChart.Data
*/
public ByteBufferNumberSeries(String name, ByteBuffer buffer) {
this(buffer);
setName(name);
}
@Override
public Collection<Data<X, Y>> getVisibleData() {
final Axis<X> xAxis = getChart().getXAxis();
final double width = xAxis.getWidth();
final int end = getDataSize() - 1;
final double min = getX(0);
final double max = getX(end);
final double left = xAxis.getValueForDisplay(0).doubleValue();
final double right = xAxis.getValueForDisplay(width).doubleValue();
// find file position for first visible point
int first;
if (left <= min) {
first = 0;
} else if (left >= max) {
first = end;
} else {
// time series should be linear on x, so guess file position and scan
first = (int) (((left - min) / (max - min)) * end);
if (getX(first) < left) {
first = findFirst(first + 1, left) - 1;
} else {
first = findLast(first - 1, left);
}
}
// find file position for last visible point
int last;
if (right <= min) {
last = 0;
} else if (right >= max) {
last = end;
} else {
// time series should be linear on x, so guess file position and scan
last = (int) (((right - min) / (max - min)) * end);
if (getX(last) < right) {
last = findFirst(last + 1, right);
} else {
last = findLast(last - 1, right) + 1;
}
}
// fit threshold to series width
int threshold = (int) width;
if (left < min || right > max) {
// there is space on either/both ends so adjust the threshold
threshold = Math.max((int) (xAxis.getDisplayPosition(convertX(Math.min(right, max))) - xAxis.getDisplayPosition(convertX(Math.max(left, min)))), 3);
}
// using 2 times the width makes it look a little smoother. Consider dynamic bucket size algorithm.
// you can still seem some points blink in and out on zoom and pan
// maybe used fixed points for bucket boundaries, first/last buckets will shrink by first/last data position
final Collection<Data<X, Y>> v = new ThresholdDataCollection(first, last - first + 1, threshold);
return v;
}
protected Data<X, Y> getData(final int index) {
Data<X, Y> data = createData(index);
data.setNode(createSymbol(data));
return data;
}
private Node createSymbol(Data<X, Y> data) {
return new HoveredThresholdNode(NumberFormat.getNumberInstance().format(data.getYValue()));
}
// TODO Make these Abstract
@Override
public abstract int getDataSize();
protected abstract Data<X,Y> createData(final int index);
protected abstract double getX(final int index);
protected abstract X convertX(double x);
protected abstract double getY(final int index);
protected abstract Y convertY(double y);
protected int findFirst(final int start, final double left) {
// TODO improve over scan? since likely linear chances are we hit on first try.
for (int i = start; i < getDataSize(); i++) {
if (getX(i) > left) {
// System.out.println("f: " + (i - start));
return i;
}
}
return -1;
}
protected int findLast(final int start, final double right) {
// TODO improve over scan? since likely linear chances are we hit on first try.
for (int i = start; i >= 0; i--) {
if (getX(i) < right) {
// System.out.println("l: " + (start - i));
return i;
}
}
return getDataSize() - 1;
}
protected class ThresholdDataCollection extends AbstractCollection<Data<X, Y>> {
final int offset;
final int length;
final int threshold;
final int size;
public ThresholdDataCollection(final int offset, final int length, final int threshold) {
this.offset = offset;
this.length = length;
this.threshold = threshold;
this.size = Math.min(length, (int) threshold);
System.out.println(MessageFormat.format("ThresholdDataCollection: offset={0}, length={1}, threshold={2}, end={3}", offset, length, threshold, offset + length));
}
@Override
public Iterator<Data<X, Y>> iterator() {
if (length > threshold) {
return createDownsampleVisibleIterator();
} else {
return createVisibleIterator();
}
}
@Override
public int size() {
return size;
}
protected Iterator<Data<X, Y>> createVisibleIterator() {
return new VisibleIterator();
}
protected Iterator<Data<X, Y>> createDownsampleVisibleIterator() {
return new DownsampleVisibleIterator();
}
protected abstract class AbstractDataIterator implements Iterator<Data<X, Y>> {
int index = offset;
Data<X, Y> next = null;
protected abstract void advance();
protected Data<X, Y> consume() {
final Data<X, Y> data = next;
next = null;
return data;
}
@Override
public boolean hasNext() {
advance();
return (null != next);
}
@Override
public Data<X, Y> next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return consume();
}
}
protected class VisibleIterator extends AbstractDataIterator {
@Override
protected void advance() {
if (null != next) {
return;
}
if (index >= (offset + length)) {
return;
}
next = getData(index);
index++;
}
}
protected class DownsampleVisibleIterator extends AbstractDataIterator {
// Bucket size. Leave room for start and end data points
final double bucketSize = (double) (length - 2) / (threshold - 2);
int bucket = 0;
int a = index; // Initially a is the first point in the triangle
public DownsampleVisibleIterator() {
super();
System.out.println(MessageFormat.format("DownsampleVisibleIterator: bucketSize={0}", bucketSize));
}
@Override
protected void advance() {
// TODO when zooming out bound to visible width not chart width
if (null != next) {
return;
}
if (index >= (offset + length)) {
return;
}
// final long start = System.nanoTime();
// try {
if (offset == index) {
// Always add the first point
next = getData(index);
index++;
return;
}
if (bucket < threshold - 2) {
// final long start2 = System.nanoTime();
// Calculate point average for next bucket (containing c)
double pointCX = 0;
double pointCY = 0;
int pointCStart = (int) Math.floor((bucket + 1) * bucketSize) + offset + 1;
int pointCEnd = (int) Math.floor((bucket + 2) * bucketSize) + offset + 1;
pointCEnd = Math.min(pointCEnd, (offset + length));
final int pointCSize = pointCEnd - pointCStart;
for (; pointCStart < pointCEnd; pointCStart++) {
pointCX += getX(pointCStart);
pointCY += getY(pointCStart);
}
pointCX /= pointCSize;
pointCY /= pointCSize;
// System.out.println("time2: " + (System.nanoTime() - start2));
// Point a
// TODO cache?
final double pointAX = getX(a);
final double pointAY = getY(a);
// Get the range for bucket b
int pointBStart = (int) Math.floor((bucket + 0) * bucketSize) + offset + 1;
final int pointBEnd = (int) Math.floor((bucket + 1) * bucketSize) + offset + 1;
double maxArea = -1;
for (; pointBStart < pointBEnd; pointBStart++) {
index++;
// Calculate triangle area over three buckets
final double area = Math.abs((pointAX - pointCX) * (getY(pointBStart) - pointAY) - (pointAX - getX(pointBStart))
* (pointCY - pointAY)) * 0.5;
if (area > maxArea) {
maxArea = area;
a = pointBStart; // Next a is this b
}
}
// Pick this point from the bucket
next = getData(a);
bucket++;
return;
}
// Always add last
assert ((offset + length - 1) == index);
next = getData(index);
index++;
// } finally {
// System.out.println(MessageFormat.format("DownsampleVisibleIterator: bucket={0}, time={1}ms",
// bucket, ((double) System.nanoTime() - start) / 1000000));
// }
}
}
}
}