/*
* Copyright 2016 Cel Skeggs
*
* This file is part of the CCRE, the Common Chicken Runtime Engine.
*
* The CCRE is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* The CCRE 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 Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the CCRE. If not, see <http://www.gnu.org/licenses/>.
*/
package ccre.timeline;
import java.awt.Color;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.WeakHashMap;
import ccre.drivers.ByteFiddling;
import ccre.recording.Recorder;
import ccre.recording.Replayer;
import ccre.recording.Replayer.ReplaySample;
import ccre.time.Time;
/**
* A timeline channel.
*
* @author skeggsc
*/
public class TimelineChannel {
private final Replayer.ReplayChannel rpc;
private static final float TICKS_PER_SECOND = Time.MICROSECONDS_PER_SECOND / 10f;
private final long zero_stamp;
private final List<String> options;
private final WeakHashMap<byte[], String> outCache = new WeakHashMap<>();
private float minFloat, maxFloat;
/**
* Creates a new TimelineChannel from a decoded channel.
*
* @param rpc the decoded channel.
* @param zero_stamp the timestamp for time zero.
*/
public TimelineChannel(Replayer.ReplayChannel rpc, long zero_stamp) {
this.rpc = rpc;
this.zero_stamp = zero_stamp;
options = new ArrayList<>();
if (rpc.type == Recorder.RawType.DISCRETE) {
byte[] last = null;
for (Iterator<ReplaySample> iterator = rpc.samples.iterator(); iterator.hasNext();) {
Replayer.ReplaySample rs = iterator.next();
if (rs.data != null && last != null && Arrays.equals(rs.data, last)) {
iterator.remove();
}
last = rs.data;
}
for (Replayer.ReplaySample rs : rpc.samples) {
String s = new String(rs.data);
if (!options.contains(s)) {
options.add(s);
}
}
} else if (rpc.type == Recorder.RawType.FLOAT) {
minFloat = Float.POSITIVE_INFINITY;
maxFloat = Float.NEGATIVE_INFINITY;
for (Replayer.ReplaySample rs : rpc.samples) {
float f = Float.intBitsToFloat((int) rs.value);
minFloat = Math.min(minFloat, f);
maxFloat = Math.max(maxFloat, f);
}
System.out.println("MAX AND MIN: " + minFloat + "-" + maxFloat + " for " + rpc.name);
}
}
/**
* Returns the number of samples in the channel.
*
* @return the number of samples.
*/
public int count() {
return rpc.samples.size();
}
/**
* Determines the absolute time, in seconds, for a certain sample.
*
* @param i the sample index.
* @return the time, in seconds, based on the earliest sample from any
* channel.
*/
public float timeFor(int i) {
return (rpc.samples.get(i).timestamp - zero_stamp) / TICKS_PER_SECOND;
}
/**
* Determines the graph position that should be displayed for a certain
* sample.
*
* @param i the sample index.
* @return the graph position (y axis of the point) from -1.0 to +1.0
*/
public float valueFor(int i) {
switch (rpc.type) {
case BOOLEAN:
return rpc.samples.get(i).value != 0 ? 1 : -1;
case EVENT:
case OUTPUT_STREAM:
return 0;
case FLOAT:
float value = Float.intBitsToFloat((int) rpc.samples.get(i).value);
return 2 * (value - minFloat) / (maxFloat - minFloat) - 1;
case DISCRETE:
byte[] key = rpc.samples.get(i).data;
if (!outCache.containsKey(key)) {
outCache.put(key, new String(key));
}
return options.indexOf(outCache.get(key)) * 2f / (options.size() - 1) - 1;
default:
return -1; // TODO
}
}
/**
* Determines the absolute time of the earliest sample, or zero if there are
* no samples.
*
* @return the beginning of the channel, in seconds.
*/
public float beginAt() {
return rpc.samples.isEmpty() ? 0 : (rpc.samples.get(0).timestamp - zero_stamp) / TICKS_PER_SECOND;
}
/**
* Determines the absolute time of the last sample, or zero if there are no
* samples.
*
* @return the end of the channel, in seconds.
*/
public float endAt() {
return rpc.samples.isEmpty() ? 0 : (rpc.samples.get(rpc.samples.size() - 1).timestamp - zero_stamp) / TICKS_PER_SECOND;
}
/**
* Determines if this is a channel containing float data.
*
* @return true if a float channel, otherwise false.
*/
public boolean isFloat() {
return rpc.type == Recorder.RawType.FLOAT;
}
/**
* Provides the human-readable (sort of) name for the channel.
*
* @return the name.
*/
public String name() {
return rpc.name;
}
/**
* Determines the string to display next to a particular sample.
*
* @param i the sample index.
* @return the text to display.
*/
public String stringFor(int i) {
switch (rpc.type) {
case BOOLEAN:
return Boolean.toString(rpc.samples.get(i).value != 0);
case FLOAT:
return Float.toString(Float.intBitsToFloat((int) rpc.samples.get(i).value));
case EVENT:
if (i < rpc.samples.size() - 1) {
long time_delta = rpc.samples.get(i + 1).timestamp - rpc.samples.get(i).timestamp;
return TimelinePanel.toTimeString(time_delta);
}
return "";
case OUTPUT_STREAM:
case DISCRETE:
byte[] bytes = rpc.samples.get(i).data;
if (!outCache.containsKey(bytes)) {
outCache.put(bytes, Charset.forName("UTF-8").decode(ByteBuffer.wrap(bytes)) + " :" + ByteFiddling.toHex(bytes, 0, bytes.length));
}
return outCache.get(bytes);
default:
return "???"; // TODO
}
}
/**
* Determines the color to use to display the specified sample.
*
* @param i the sample index.
* @return the color to display.
*/
public Color colorFor(int i) {
switch (rpc.type) {
case BOOLEAN:
return rpc.samples.get(i).value != 0 ? Color.GREEN : Color.RED;
case FLOAT:
float f = Float.intBitsToFloat((int) rpc.samples.get(i).value);
if (f < 0) {
return Renderer.blend(Color.RED, Color.BLACK, f + 1.0f);
} else {
return Renderer.blend(Color.BLACK, Color.GREEN, f);
}
case DISCRETE:
byte[] key = rpc.samples.get(i).data;
if (!outCache.containsKey(key)) {
outCache.put(key, new String(key));
}
return Renderer.nthColor(options.indexOf(outCache.get(key)));
case EVENT:
case OUTPUT_STREAM:
default:
return Color.BLACK;
}
}
/**
* Determines if the channel should have a connecting line between
* subsequent events to display the color.
*
* This will be true if the channel is a boolean channel or a discrete
* channel.
*
* @return true to draw a connecting line, false otherwise.
*/
public boolean hasContinuationChannel() {
return rpc.type == Recorder.RawType.BOOLEAN || rpc.type == Recorder.RawType.DISCRETE;
}
}