Merge "Adds a timeline data method to save speed samples." into studio-1.4-dev
diff --git a/chartlib/src/main/java/com/android/tools/chartlib/TimelineData.java b/chartlib/src/main/java/com/android/tools/chartlib/TimelineData.java
index fbcc676..1ab32de 100644
--- a/chartlib/src/main/java/com/android/tools/chartlib/TimelineData.java
+++ b/chartlib/src/main/java/com/android/tools/chartlib/TimelineData.java
@@ -15,10 +15,11 @@
  */
 package com.android.tools.chartlib;
 
+import com.android.annotations.Nullable;
 import com.android.annotations.VisibleForTesting;
 import com.android.annotations.concurrency.GuardedBy;
 
-import java.util.List;
+import java.util.*;
 
 /**
  * A group of streams of data sampled over time. This object is thread safe as it can be
@@ -58,13 +59,100 @@
     }
 
     public synchronized void add(long time, int type, float... values) {
+        add(new Sample((time - mStart) / 1000.0f, type, values));
+    }
+
+    /**
+     * Converts stream area values to stream samples. All streams have different starting values from last sample, different areas,
+     * and result in different sample shapes. The conversion may break into multiple samples time points to make the shapes' areas are
+     * correct. For example, every stream flow is a triangle when not stacked with each other; it need four time points for all streams,
+     * one triangle is split into four parts at every time point, each part's shape may be changed while the area size is the same.
+     *
+     * @param time The current time in seconds from the start timestamp.
+     * @param areas The streams' area sizes.
+     * @param lastSample The last recent sample, which may be null.
+     * @param type The timeline data type.
+     */
+    private static List<Sample> convertAreasToSamples(float time, int type, float[] areas, @Nullable Sample lastSample) {
+        int streamSize = areas.length;
+        // The starting time and value are from last sample, to be consecutive.
+        float startTime = lastSample != null ? lastSample.time : 0.0f;
+        float[] startValues = lastSample != null ? lastSample.values : new float[streamSize];
+        assert streamSize == startValues.length;
+
+        // Computes how long every stream's value is non-zero and the ending value at last.
+        float maxInterval = time - startTime;
+        if (maxInterval <= 0) {
+            return new ArrayList<Sample>();
+        }
+        float[] nonZeroIntervalsForStreams = new float[streamSize];
+        float[] endValuesForStreams = new float[streamSize];
+        for (int i = 0; i < streamSize; i++) {
+            if (startValues[i] * maxInterval / 2 < areas[i]) {
+                nonZeroIntervalsForStreams[i] = maxInterval;
+                endValuesForStreams[i] = areas[i] * 2 / maxInterval - startValues[i];
+            }
+            else if (areas[i] == 0) {
+                nonZeroIntervalsForStreams[i] = maxInterval;
+                endValuesForStreams[i] = 0;
+            }
+            else {
+                // startValues[i] should be non-zero to be greater than areas[i].
+                nonZeroIntervalsForStreams[i] = areas[i] * 2 / startValues[i];
+                endValuesForStreams[i] = 0;
+            }
+        }
+
+        // Sorts the intervals, every different interval should be a sample.
+        float[] ascendingIntervals = Arrays.copyOf(nonZeroIntervalsForStreams, streamSize);
+        Arrays.sort(ascendingIntervals);
+        List<Sample> sampleList = new ArrayList<Sample>();
+        for (float interval : ascendingIntervals) {
+            float[] sampleValues = new float[streamSize];
+            for (int j = 0; j < streamSize; j++) {
+                sampleValues[j] = startValues[j] - (startValues[j] - endValuesForStreams[j]) * interval / nonZeroIntervalsForStreams[j];
+                if (sampleValues[j] < 0) {
+                    sampleValues[j] = 0.0f;
+                }
+            }
+            sampleList.add(new Sample(interval + startTime, type, sampleValues));
+            if (interval == maxInterval) {
+                break;
+            }
+        }
+        if (ascendingIntervals[streamSize - 1] < maxInterval) {
+            // Adds the ending sample that all stream values are zero.
+            sampleList.add(new Sample(time, type, new float[streamSize]));
+        }
+        return sampleList;
+    }
+
+
+    /**
+     * Adds the stream values which are values converted from the areas values. The values depends on both last sample values and
+     * the current areas' sizes. It should be a synchronized method to let the last recent sample be accurate.
+     *
+     * @param timeMills The current time in mills.
+     * @param type Sample data type.
+     * @param areas Value multiple time area sizes for all streams.
+     */
+    public synchronized void addFromArea(long timeMills, int type, float... areas) {
+        float timeForStart = (timeMills - mStart) / 1000.0f;
+        Sample lastSample = mSamples.isEmpty() ? null : mSamples.get(mSamples.size() - 1);
+        for (Sample sample : convertAreasToSamples(timeForStart, type, areas, lastSample)) {
+            add(sample);
+        }
+    }
+
+    private void add(Sample sample) {
+        float[] values = sample.values;
         assert values.length == myStreams;
         float total = 0.0f;
         for (float value : values) {
             total += value;
         }
         mMaxTotal = Math.max(mMaxTotal, total);
-        mSamples.add(new Sample((time - mStart) / 1000.0f, type, values));
+        mSamples.add(sample);
     }
 
     public synchronized void clear() {
diff --git a/chartlib/src/test/java/com/android/tools/chartlib/TimelineDataTest.java b/chartlib/src/test/java/com/android/tools/chartlib/TimelineDataTest.java
index df61bf1..db83037 100644
--- a/chartlib/src/test/java/com/android/tools/chartlib/TimelineDataTest.java
+++ b/chartlib/src/test/java/com/android/tools/chartlib/TimelineDataTest.java
@@ -19,6 +19,8 @@
 
 public class TimelineDataTest extends TestCase {
 
+    private static final int TYPE_DATA = 0;
+
     private TimelineData mData;
 
     private long mCreationTime;
@@ -80,4 +82,81 @@
         mData.clear();
         assertEquals(0, mData.size());
     }
-}
\ No newline at end of file
+
+    public void testAddFromAreaForFirstSample() {
+        assertEquals(0, mData.size());
+        long start = mData.getStartTime();
+        mData.addFromArea(start + 1000, TYPE_DATA, 500.0f, 100.0f);
+        assertEquals(1, mData.size());
+        TimelineData.Sample sample = mData.get(0);
+        assertEquals(1.0f, sample.time);
+        assertEquals(1000.0f, sample.values[0]);
+        assertEquals(200.0f, sample.values[1]);
+    }
+
+    public void testAddFromAreaExpectSpeedNotGoingDown() {
+        assertEquals(0, mData.size());
+        long start = mData.getStartTime();
+        mData.addFromArea(start + 1000, TYPE_DATA, 500.0f, 100.0f);
+        mData.addFromArea(start + 1500, TYPE_DATA, 500.0f, 100.0f);
+        assertEquals(2, mData.size());
+        TimelineData.Sample sample = mData.get(1);
+        assertEquals(1.5f, sample.time);
+        assertEquals(1000.0f, sample.values[0]);
+        assertEquals(200.0f, sample.values[1]);
+    }
+
+    public void testAddFromAreaExpectFirstStreamSpeedGoingToZeroFirst() {
+        mData = new TimelineData(2, 4);
+        assertEquals(0, mData.size());
+        long start = mData.getStartTime();
+        mData.addFromArea(start + 1000, TYPE_DATA, 500.0f, 100.0f);
+        mData.addFromArea(start + 2000, TYPE_DATA, 250.0f, 75.0f);
+        assertEquals(4, mData.size());
+        TimelineData.Sample sample = mData.get(1);
+        assertEquals(1.5f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(66.66667f, sample.values[1]);
+        sample = mData.get(2);
+        assertEquals(1.75f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+        sample = mData.get(3);
+        assertEquals(2.0f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+    }
+
+    public void testAddFromAreaExpectSecondStreamSpeedGoingToZeroFirst() {
+        mData = new TimelineData(2, 4);
+        assertEquals(0, mData.size());
+        long start = mData.getStartTime();
+        mData.addFromArea(start + 1000, TYPE_DATA, 500.0f, 100.0f);
+        mData.addFromArea(start + 2000, TYPE_DATA, 400.0f, 50.0f);
+        assertEquals(4, mData.size());
+        TimelineData.Sample sample = mData.get(1);
+        assertEquals(1.5f, sample.time);
+        assertEquals(375.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+        sample = mData.get(2);
+        assertEquals(1.8f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+        sample = mData.get(3);
+        assertEquals(2.0f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+    }
+
+    public void testAddFromAreaWithZeroAreas() {
+        assertEquals(0, mData.size());
+        long start = mData.getStartTime();
+        mData.addFromArea(start + 1000, TYPE_DATA, 500.0f, 100.0f);
+        mData.addFromArea(start + 2000, TYPE_DATA, 0.0f, 0.0f);
+        assertEquals(2, mData.size());
+        TimelineData.Sample sample = mData.get(1);
+        assertEquals(2.0f, sample.time);
+        assertEquals(0.0f, sample.values[0]);
+        assertEquals(0.0f, sample.values[1]);
+    }
+}