blob: d90b1b8ff9f1f13f7ebe4923eed3dacdb11230bf [file] [log] [blame]
/*
* Copyright (C) 2014 The Android Open Source Project
*
* 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.
*/
package com.android.tools.chartlib;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.ActionListener;
import java.awt.event.HierarchyListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.util.HashMap;
import java.util.Map;
import javax.swing.Icon;
import gnu.trove.TIntObjectHashMap;
/**
* A component to display a TimelineData object. It locks the timeline object to prevent
* modifications to it while it's begin rendered, but objects of this class should not be accessed
* from different threads.
*/
public final class TimelineComponent extends AnimatedComponent
implements ActionListener, HierarchyListener {
private static final Color TEXT_COLOR = new Color(128, 128, 128);
private static final int LEFT_MARGIN = 120;
private static final int RIGHT_MARGIN = 200;
private static final int TOP_MARGIN = 10;
private static final int BOTTOM_MARGIN = 30;
private static final int FPS = 40;
/**
* The number of pixels a second in the timeline takes on the screen.
*/
private static final float X_SCALE = 20;
private final float mBufferTime;
@NonNull
private final TimelineData mData;
@NonNull
private final EventData mEvents;
private final float mInitialMax;
private final float mAbsoluteMax;
private final float mInitialMarkerSeparation;
private String[] mStreamNames;
private Color[] mStreamColors;
private Map<Integer, Style> mStyles;
private boolean mFirstFrame;
/**
* The current maximum range in y-axis units.
*/
private float mCurrentMax;
/**
* Marker separation in y-axis units.
*/
private float mMarkerSeparation;
/**
* The current alpha of markers at even positions. When there are not enough/too many markers,
* the markers at even positions are faded in/out respectively. This tracks the animated alpha
* of such markers.
*/
private float mEvenMarkersAlpha;
/**
* The current value in pixels where the x-axis is drawn.
*/
private int mBottom;
/**
* The current value in pixels where the right hand side y-axis is drawn.
*/
private int mRight;
/**
* The current scale from y-axis values to pixels.
*/
private float mYScale;
/**
* The current time value at the right edge of the timeline in seconds.
*/
private float mEndTime;
/**
* The current time value at the left edge of the timeline in seconds.
*/
private float mBeginTime;
/**
* How to render each event type.
*/
private TIntObjectHashMap<EventInfo> mEventsInfo;
/**
* The units of the y-axis values.
*/
private String mUnits;
/**
* The number of available local samples.
*/
private int mSize;
/**
* The times at which the samples occurred.
*/
private float[] mTimes;
/**
* The times at which the samples occurred.
*/
private int[] mTypes;
/**
* The render values of the samples depending on the layout mode, as in mValues[stream][sample]
*/
private final float[][] mValues;
/**
* The last values of the samples for each stream
*/
private final float[] mCurrent;
/**
* The number of events to render.
*/
private int mEventsSize;
/**
* The start time of each event.
*/
private float[] mEventStart;
/**
* The end time of each event, if NaN then the event did not end.
*/
private float[] mEventEnd;
/**
* The type of each event.
*/
private int[] mEventTypes;
/**
* The animated angle of an event in progress.
*/
private float mEventProgressStart;
/**
* The direction of the event animation.
*/
private float mEventProgressDir = 1.0f;
/**
* The current state for all in-progress events.
*/
private float mEventProgress;
/**
* Creates a timeline component that renders the given timeline data. It will animate the
* timeline data by showing the value at the current time on the right y-axis of the graph.
*
* @param data the data to be displayed.
* @param bufferTime the time, in seconds, to lag behind the given {@code data}.
* @param initialMax the initial maximum value for the y-axis.
* @param absoluteMax the absolute maximum value for the y-axis.
* @param initialMarkerSeparation the initial separations for the markers on the y-axis.
*/
public TimelineComponent(
@NonNull TimelineData data,
@NonNull EventData events,
float bufferTime,
float initialMax,
float absoluteMax,
float initialMarkerSeparation) {
super(FPS);
mData = data;
mEvents = events;
mBufferTime = bufferTime;
mInitialMax = initialMax;
mAbsoluteMax = absoluteMax;
mInitialMarkerSeparation = initialMarkerSeparation;
int streams = mData.getStreamCount();
addHierarchyListener(this);
mStreamNames = new String[streams];
mStreamColors = new Color[streams];
mValues = new float[streams][];
mCurrent = new float[streams];
mSize = 0;
for (int i = 0; i < streams; i++) {
mStreamNames[i] = "Stream " + i;
mStreamColors[i] = Color.BLACK;
}
mStyles = new HashMap<Integer, Style>();
mUnits = "";
mEventsInfo = new TIntObjectHashMap<EventInfo>();
setOpaque(true);
reset();
}
public void configureStream(int stream, String name, Color color) {
mStreamNames[stream] = name;
mStreamColors[stream] = color;
}
public void configureEvent(int type, int stream, Icon icon, Color color,
Color progress, boolean range) {
mEventsInfo.put(type, new EventInfo(type, stream, icon, color, progress, range));
}
public void configureType(int type, Style style) {
mStyles.put(type, style);
}
public void configureUnits(String units) {
mUnits = units;
}
public void reset() {
mCurrentMax = mInitialMax;
mMarkerSeparation = mInitialMarkerSeparation;
mEvenMarkersAlpha = 1.0f;
mFirstFrame = true;
}
@Override
protected void draw(Graphics2D g2d) {
Dimension dim = getSize();
mBottom = dim.height - BOTTOM_MARGIN;
mRight = dim.width - RIGHT_MARGIN;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setFont(DEFAULT_FONT);
g2d.setClip(0, 0, dim.width, dim.height);
g2d.setColor(getBackground());
g2d.fillRect(0, 0, dim.width, dim.height);
g2d.setClip(LEFT_MARGIN, TOP_MARGIN, mRight - LEFT_MARGIN, mBottom - TOP_MARGIN);
drawTimelineData(g2d);
drawEvents(g2d);
g2d.setClip(0, 0, dim.width, dim.height);
drawLabels(g2d);
drawTimeMarkers(g2d);
drawMarkers(g2d);
drawGuides(g2d);
mFirstFrame = false;
}
@Override
protected void debugDraw(Graphics2D g2d) {
int drawn = 0;
g2d.setFont(DEFAULT_FONT.deriveFont(5.0f));
for (int i = 0; i < mSize; ++i) {
if (mTimes[i] > mBeginTime && mTimes[i] < mEndTime) {
for (int j = 0; j < mValues.length; ++j) {
int x = (int) timeToX(mTimes[i]);
int y = (int) valueToY(mValues[j][i]);
g2d.setColor(new Color((17 * mTypes[i]) % 255, (121 * mTypes[i]) % 255,
(71 * mTypes[i]) % 255));
g2d.drawLine(x, y - 2, x, y + 2);
g2d.drawLine(x - 2, y, x + 2, y);
g2d.setColor(TEXT_COLOR);
}
drawn++;
}
}
addDebugInfo("Drawn samples: %d", drawn);
}
private void drawTimelineData(Graphics2D g2d) {
mYScale = (mBottom - TOP_MARGIN) / mCurrentMax;
if (mSize > 1) {
int from = 0;
// Optimize to not render too many samples since they get clipped.
while (from < mSize - 1 && mTimes[from + 1] < mBeginTime) {
from++;
}
int to = from;
while (to + 1 < mSize && mTimes[to] <= mEndTime) {
to++;
}
if (from == to) {
return;
}
int drawnSegments = 0;
for (int j = mValues.length - 1; j >= 0; j--) {
Path2D.Float path = new Path2D.Float();
path.moveTo(timeToX(mTimes[from]), valueToY(0.0f));
for (int i = from; i <= to; i++) {
float val = mValues[j][i];
path.lineTo(timeToX(mTimes[i]), valueToY(Math.min(val, mAbsoluteMax)));
}
path.lineTo(timeToX(mTimes[to]), valueToY(0.0f));
g2d.setColor(mStreamColors[j]);
g2d.fill(path);
if (!mStyles.isEmpty()) {
path = new Path2D.Float();
Stroke current = g2d.getStroke();
float step = 3.0f;
float x0 = timeToX(mTimes[from]);
float y0 = valueToY(mValues[j][from]);
g2d.setColor(mStreamColors[j].darker());
Stroke stroke = null;
float strokeScale = Float.NaN;
for (int i = from + 1; i <= to; i++) {
float x1 = timeToX(mTimes[i]);
float y1 = valueToY(mValues[j][i]);
Style style = mStyles.get(mTypes[i]);
if (style != null && style != Style.NONE) {
BasicStroke str = new BasicStroke(1.0f);
float scale = 0;
if (style == Style.DASHED) {
float distance = (float) Point2D.distance(x0, y0, x1, y1);
float delta = mTimes[i] * X_SCALE;
scale = distance / (x1 - x0);
str = new BasicStroke(1.0f, BasicStroke.CAP_ROUND,
BasicStroke.JOIN_ROUND, 0.0f, new float[]{step * scale},
(delta * scale) % (step * scale * 2));
}
if (scale != strokeScale) {
if (stroke != null) {
g2d.setStroke(stroke);
g2d.draw(path);
path.reset();
drawnSegments++;
}
strokeScale = scale;
stroke = str;
path.moveTo(x0, y0);
}
path.lineTo(x1, y1);
}
x0 = x1;
y0 = y1;
}
if (stroke != null) {
g2d.setStroke(stroke);
g2d.draw(path);
drawnSegments++;
}
g2d.setStroke(current);
}
}
addDebugInfo("Drawn segments: %d", drawnSegments);
}
addDebugInfo("Total samples: %d", mSize);
}
private float interpolate(int stream, int sample, float time) {
int prev = sample > 0 ? sample - 1 : 0;
int next = sample < mSize ? sample : mSize - 1;
float a = mValues[stream][prev];
float b = mValues[stream][next];
float delta = mTimes[next] - mTimes[prev];
float ratio = delta != 0 ? (time - mTimes[prev]) / delta : 1.0f;
return (b - a) * ratio + a;
}
private void drawEvents(Graphics2D g2d) {
if (mSize > 0) {
int drawnEvents = 0;
AffineTransform tx = g2d.getTransform();
Stroke stroke = g2d.getStroke();
int s = 0;
int e = 0;
while (e < mEventsSize) {
if (s < mSize && mTimes[s] < mEventStart[e]) {
s++;
} else if (Float.isNaN(mEventEnd[e])
|| mEventEnd[e] > mBeginTime && mEventEnd[e] > mTimes[0]) {
drawnEvents++;
EventInfo info = mEventsInfo.get(mEventTypes[e]);
float x = timeToX(mEventStart[e]);
float y = valueToY(interpolate(info.stream, s, mEventStart[e]));
AffineTransform dt = new AffineTransform(tx);
dt.translate(x, y);
g2d.setTransform(dt);
info.icon.paintIcon(this, g2d, -info.icon.getIconWidth() / 2,
-info.icon.getIconHeight() - 5);
g2d.setTransform(tx);
g2d.setStroke(
new BasicStroke(1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
Path2D.Float p = new Path2D.Float();
boolean closed = !Float.isNaN(mEventEnd[e]);
if (info.range) {
p.moveTo(x, mBottom);
p.lineTo(x, y);
float endTime = Float.isNaN(mEventEnd[e]) ? mEndTime : mEventEnd[e];
int i = s;
for (; i < mSize && mTimes[i] < endTime; i++) {
float val = mValues[info.stream][i];
p.lineTo(timeToX(mTimes[i]), valueToY(val));
}
p.lineTo(timeToX(endTime), valueToY(interpolate(info.stream, i, endTime)));
p.lineTo(timeToX(closed ? mEventEnd[e] : endTime), valueToY(0));
if (info.color != null) {
g2d.setColor(info.color);
g2d.fill(p);
}
g2d.setColor(info.progress);
g2d.draw(p);
} else {
p.moveTo(x, y - 2.0f);
p.lineTo(x, y + 2.0f);
g2d.setColor(info.progress);
g2d.draw(p);
}
if (!closed) {
g2d.setColor(info.progress);
// Draw in progress marker
float end = 360 * mEventProgress;
float start = mEventProgressStart;
if (mEventProgressDir < 0.0f) {
start += end;
end = 360 - end;
}
g2d.draw(new Arc2D.Float(
x + info.icon.getIconWidth() / 2 + 3,
y - info.icon.getIconHeight() - 3,
6, 6,
start, end, Arc2D.OPEN));
}
e++;
} else {
e++;
}
}
g2d.setStroke(stroke);
addDebugInfo("Drawn events: %d", drawnEvents);
}
}
private float valueToY(float val) {
return mBottom - val * mYScale;
}
private float timeToX(float time) {
return LEFT_MARGIN + (time - mBeginTime) * X_SCALE;
}
private void drawLabels(Graphics2D g2d) {
g2d.setFont(DEFAULT_FONT);
FontMetrics metrics = g2d.getFontMetrics();
for (int i = 0; i < mStreamNames.length && mSize > 0; i++) {
g2d.setColor(mStreamColors[i]);
int y = TOP_MARGIN + 15 + (mStreamNames.length - i - 1) * 20;
g2d.fillRect(mRight + 20, y, 15, 15);
g2d.setColor(TEXT_COLOR);
g2d.drawString(
String.format("%s [%.2f %s]", mStreamNames[i], mCurrent[i], mUnits),
mRight + 40,
y + 7 + metrics.getAscent() * .5f);
}
}
private void drawTimeMarkers(Graphics2D g2d) {
g2d.setFont(DEFAULT_FONT);
g2d.setColor(TEXT_COLOR);
FontMetrics metrics = g2d.getFontMetrics();
float offset = metrics.stringWidth("000") * 0.5f;
Path2D.Float lines = new Path2D.Float();
for (int sec = Math.max((int) Math.ceil(mBeginTime), 0); sec < mEndTime; sec++) {
float x = timeToX(sec);
boolean big = sec % 5 == 0;
if (big) {
String text = formatTime(sec);
g2d.drawString(text, x - metrics.stringWidth(text) + offset,
mBottom + metrics.getAscent() + 5);
}
lines.moveTo(x, mBottom);
lines.lineTo(x, mBottom + (big ? 5 : 2));
}
g2d.draw(lines);
}
@VisibleForTesting
static String formatTime(int seconds) {
int[] factors = {60, seconds};
String[] suffix = {"m", "h"};
String ret = seconds % 60 + "s";
int t = seconds / 60;
for (int i = 0; i < suffix.length && t > 0; i++) {
ret = t % factors[i] + suffix[i] + " " + ret;
t /= factors[i];
}
return ret;
}
private void drawMarkers(Graphics2D g2d) {
if (mYScale <= 0) {
return;
}
int markers = (int) (mCurrentMax / mMarkerSeparation);
float markerPosition = LEFT_MARGIN - 10;
for (int i = 0; i < markers + 1; i++) {
float markerValue = (i + 1) * mMarkerSeparation;
int y = (int) valueToY(markerValue);
// Too close to the top
if (mCurrentMax - markerValue < mMarkerSeparation * 0.5f) {
markerValue = mCurrentMax;
//noinspection AssignmentToForLoopParameter
i = markers;
y = TOP_MARGIN;
}
if (i < markers && i % 2 == 0 && mEvenMarkersAlpha < 1.0f) {
g2d.setColor(
new Color(TEXT_COLOR.getColorSpace(), TEXT_COLOR.getColorComponents(null),
mEvenMarkersAlpha));
} else {
g2d.setColor(TEXT_COLOR);
}
g2d.drawLine(LEFT_MARGIN - 2, y, LEFT_MARGIN, y);
FontMetrics metrics = getFontMetrics(DEFAULT_FONT);
String marker = String.format("%.2f %s", markerValue, mUnits);
g2d.drawString(marker, markerPosition - metrics.stringWidth(marker),
y + metrics.getAscent() * 0.5f);
}
}
private void drawGuides(Graphics2D g2d) {
g2d.setColor(TEXT_COLOR);
g2d.drawLine(LEFT_MARGIN - 10, mBottom, mRight + 10, mBottom);
if (mYScale > 0) {
g2d.drawLine(LEFT_MARGIN, mBottom, LEFT_MARGIN, TOP_MARGIN);
g2d.drawLine(mRight, mBottom, mRight, TOP_MARGIN);
}
}
@Override
protected void updateData() {
long start;
synchronized (mData) {
start = mData.getStartTime();
mSize = mData.size();
assert mData.getStreamCount() == mValues.length;
if (mTimes == null || mTimes.length < mSize) {
int alloc = Math.max(mSize, mTimes == null ? 64 : mTimes.length * 2);
mTimes = new float[alloc];
mTypes = new int[alloc];
for (int j = 0; j < mData.getStreamCount(); ++j) {
mValues[j] = new float[alloc];
}
}
for (int i = 0; i < mSize; ++i) {
TimelineData.Sample sample = mData.get(i);
mTimes[i] = sample.time;
mTypes[i] = sample.type;
float value = 0.0f;
for (int j = 0; j < mData.getStreamCount(); ++j) {
value += sample.values[j];
mValues[j][i] = value;
}
}
for (int j = 0; j < mData.getStreamCount(); ++j) {
mCurrent[j] = mSize > 0 ? mData.get(mSize - 1).values[j] : 0.0f;
}
// Calculate begin and end times in seconds.
mEndTime = mData.getEndTime() - mBufferTime;
mBeginTime = mEndTime - (mRight - LEFT_MARGIN) / X_SCALE;
// Animate the current maximum towards the real one.
float cappedMax = Math.min(mData.getMaxTotal(), mAbsoluteMax);
if (cappedMax > mCurrentMax) {
mCurrentMax = lerp(mCurrentMax, cappedMax, mFirstFrame ? 1.f : .95f);
}
// Animate the fade in/out of markers.
FontMetrics metrics = getFontMetrics(DEFAULT_FONT);
int ascent = metrics.getAscent();
float distance = mMarkerSeparation * mYScale;
float evenMarkersTarget = 1.0f;
if (distance < ascent * 2) { // Too many markers
if (mEvenMarkersAlpha < 0.1f) {
mMarkerSeparation *= 2;
mEvenMarkersAlpha = 1.0f;
} else {
evenMarkersTarget = 0.0f;
}
} else if (distance > ascent * 5) { // Not enough
if (mEvenMarkersAlpha > 0.9f) {
mMarkerSeparation /= 2;
mEvenMarkersAlpha = 0.0f;
}
}
mEvenMarkersAlpha = lerp(mEvenMarkersAlpha, evenMarkersTarget, 0.999f);
}
synchronized (mEvents) {
mEventsSize = mEvents.size();
if (mEventStart == null || mEventStart.length < mEventsSize) {
int alloc = Math.max(mEventsSize, mEventStart == null ? 64 : mEventStart.length * 2);
mEventStart = new float[alloc];
mEventEnd = new float[alloc];
mEventTypes = new int[alloc];
}
for (int i = 0; i < mEventsSize; i++) {
EventData.Event event = mEvents.get(i);
mEventStart[i] = (event.from - start) / 1000.0f;
mEventEnd[i] = event.to == -1 ? Float.NaN : (event.to - start) / 1000.0f;
mEventTypes[i] = event.type;
}
// Animate events in progress
if (mEventProgress > 0.95f) {
mEventProgressDir = -mEventProgressDir;
mEventProgress = 0.0f;
}
mEventProgressStart = (mEventProgressStart + mFrameLength * 200.0f) % 360.0f;
mEventProgress = lerp(mEventProgress, 1.0f, .99f);
}
}
public enum Style {
NONE,
SOLID,
DASHED
}
private static class EventInfo {
public final int type;
public final int stream;
public final Icon icon;
public final Color color;
public final Color progress;
public final boolean range;
private EventInfo(int type, int stream, Icon icon, Color color,
Color progress, boolean range) {
this.type = type;
this.stream = stream;
this.icon = icon;
this.color = color;
this.progress = progress;
this.range = range;
}
}
}