/*
* Copyright (C) 2007 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 net.redgeek.android.eventrend.primitives;
import android.database.Cursor;
import android.util.Log;
import net.redgeek.android.eventrend.db.CategoryDbTable;
import net.redgeek.android.eventrend.db.EntryDbTable;
import net.redgeek.android.eventrend.db.EvenTrendDbAdapter;
import net.redgeek.android.eventrend.graph.TimeSeriesPainter;
import net.redgeek.android.eventrend.graph.plugins.TimeSeriesInterpolator;
import net.redgeek.android.eventrend.synthetic.Formula;
import net.redgeek.android.eventrend.synthetic.FormulaCache;
import net.redgeek.android.eventrend.util.DateUtil;
import net.redgeek.android.eventrend.util.Number;
import net.redgeek.android.eventrend.util.DateUtil.Period;
import net.redgeek.android.eventrend.util.Number.TrendState;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeSeriesCollector {
private ArrayList<TimeSeries> mSeries;
private long mAggregationMs;
private int mHistory = 20;
private float mSmoothing = 0.1f;
private float mSensitivity = 1.0f;
private Lock mLock;
private boolean mAutoAggregation;
private int mAutoAggregationOffset;
private DatapointCache mDatapointCache;
private FormulaCache mFormulaCache;
private ArrayList<TimeSeriesInterpolator> mInterpolators;
private EvenTrendDbAdapter mDbh;
private DateUtil mAutoAggSpan;
private Calendar mCal1;
private Calendar mCal2;
private long mCollectionStart;
private long mCollectionEnd;
private long mQueryStart;
private long mQueryEnd;
private TimeSeriesPainter mDefaultPainter;
public TimeSeriesCollector(EvenTrendDbAdapter dbh) {
initialize(dbh, null);
}
public TimeSeriesCollector(EvenTrendDbAdapter dbh, TimeSeriesPainter painter) {
initialize(dbh, painter);
}
public void initialize(EvenTrendDbAdapter dbh, TimeSeriesPainter painter) {
mDbh = dbh;
mSeries = new ArrayList<TimeSeries>();
mAutoAggSpan = new DateUtil();
mDatapointCache = new DatapointCache(mDbh);
mFormulaCache = new FormulaCache();
mAutoAggregation = false;
mAutoAggregationOffset = 0;
mCal1 = Calendar.getInstance();
mCal2 = Calendar.getInstance();
mDefaultPainter = painter;
mLock = new ReentrantLock();
}
@Override
public String toString() {
return mSeries.toString();
}
public EvenTrendDbAdapter getDbh() {
return mDbh;
}
public void updateTimeSeriesMetaLocking(boolean disableByDefault) {
waitForLock();
Cursor c = mDbh.fetchAllCategories();
c.moveToFirst();
for (int i = 0; i < c.getCount(); i++) {
CategoryDbTable.Row row = new CategoryDbTable.Row(c);
updateTimeSeriesMeta(row, disableByDefault);
c.moveToNext();
}
c.close();
// cycle through again, we may have had a series that was dependent on
// another series that was created later
for (int i = 0; i < mSeries.size(); i++) {
setDependents(mSeries.get(i));
setDependees(mSeries.get(i));
}
unlock();
}
private void updateTimeSeriesMeta(CategoryDbTable.Row row, boolean disable) {
TimeSeries ts = getSeriesByIdNonlocking(row.getId());
if (ts == null) {
if (mDefaultPainter == null) {
TimeSeriesPainter p = new TimeSeriesPainter.Default();
ts = new TimeSeries(row, mHistory, mSmoothing, p);
} else {
ts = new TimeSeries(row, mHistory, mSmoothing, mDefaultPainter);
}
mSeries.add(ts);
mDatapointCache.addCacheableCategory(row.getId(), mHistory);
}
ts.setDbRow(row);
setSeriesInterpolator(ts, row.getInterpolation());
if (row.getSynthetic() == true) {
Formula formula = mFormulaCache.getFormula(Long.valueOf(row.getId()));
if (formula == null)
formula = new Formula();
formula.setFormula(row.getFormula());
mFormulaCache.setFormula(row.getId(), formula);
}
if (disable)
ts.setEnabled(false);
setDependents(ts);
setDependees(ts);
}
public void updateTimeSeriesData(boolean flushCache) {
updateTimeSeriesData(mQueryStart, mQueryEnd, flushCache);
}
public void updateTimeSeriesData(long start, long end, boolean flushCache) {
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.isEnabled() == true) {
long catId = ts.getDbRow().getId();
updateTimeSeriesData(catId, start, end, flushCache);
}
}
unlock();
}
public void updateTimeSeriesData(long catId, boolean flushCache) {
waitForLock();
updateTimeSeriesData(catId, mQueryStart, mQueryEnd, flushCache);
unlock();
}
private void updateTimeSeriesData(long catId, long start, long end,
boolean flushCache) {
if (flushCache == true)
mDatapointCache.refresh(catId);
gatherSeries(start, end);
}
public void setSmoothing(float smoothing) {
mSmoothing = smoothing;
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
mSeries.get(i).recalcStatsAndBounds(mSmoothing, mHistory);
}
unlock();
}
public void setHistory(int history) {
mHistory = history;
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
mSeries.get(i).recalcStatsAndBounds(mSmoothing, mHistory);
}
unlock();
}
public void setSensitivity(float sensitivity) {
mSensitivity = sensitivity;
}
public void setInterpolators(ArrayList<TimeSeriesInterpolator> list) {
mInterpolators = list;
}
private void waitForLock() {
while (lock() == false) {
}
}
public boolean lock() {
try {
return mLock.tryLock(1000L, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
return false;
}
}
public void unlock() {
mLock.unlock();
}
public void setAutoAggregation(boolean b) {
mAutoAggregation = b;
}
public void setAutoAggregationOffset(int offset) {
mAutoAggregationOffset = offset;
}
public boolean getAutoAggregation() {
return mAutoAggregation;
}
public void setAggregationMs(long millis) {
mAggregationMs = millis;
}
public void setSeriesInterpolator(TimeSeries ts, String type) {
TimeSeriesInterpolator tsi = null;
if (mInterpolators == null)
return;
for (int i = 0; i < mInterpolators.size(); i++) {
tsi = mInterpolators.get(i);
if (type.equals(tsi.getName()))
break;
}
if (tsi != null) {
waitForLock();
ts.setInterpolator(tsi);
unlock();
}
return;
}
public void clearSeriesLocking() {
TimeSeries ts;
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
ts = mSeries.get(i);
if (ts != null) {
ts.clearSeries();
}
}
mSeries.clear();
unlock();
}
public boolean isSeriesEnabled(long catId) {
boolean b;
waitForLock();
TimeSeries ts = getSeriesByIdNonlocking(catId);
if (ts == null)
b = false;
else
b = ts.isEnabled();
unlock();
return b;
}
public void setSeriesEnabled(long catId, boolean b) {
waitForLock();
TimeSeries ts = getSeriesByIdNonlocking(catId);
if (ts != null)
ts.setEnabled(b);
unlock();
return;
}
public void toggleSeriesEnabled(long catId) {
waitForLock();
TimeSeries ts = getSeriesByIdNonlocking(catId);
if (ts.isEnabled())
ts.setEnabled(false);
else
ts.setEnabled(true);
unlock();
return;
}
public int numSeries() {
int i;
waitForLock();
i = mSeries.size();
unlock();
return i;
}
public TimeSeries getSeries(int i) {
TimeSeries ts = null;
try {
ts = mSeries.get(i);
} catch(IndexOutOfBoundsException e) {
ts = null;
}
return ts;
}
public CategoryDbTable.Row getSeriesMetaLocking(int i) {
CategoryDbTable.Row row = null;
waitForLock();
TimeSeries ts = getSeries(i);
row = new CategoryDbTable.Row(ts.getDbRow());
unlock();
return row;
}
public long getSeriesIdLocking(int i) {
long id = -1;
waitForLock();
TimeSeries ts = getSeries(i);
if (ts != null)
id = ts.getDbRow().getId();
unlock();
return id;
}
private TimeSeries getSeriesByIdNonlocking(long catId) {
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.getDbRow().getId() == catId)
return ts;
}
return null;
}
public TimeSeries getSeriesByIdLocking(long catId) {
waitForLock();
TimeSeries ts = getSeriesByIdNonlocking(catId);
unlock();
return ts;
}
public TimeSeries getSeriesByNameNonlocking(String name) {
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.getDbRow().getCategoryName().equals(name))
return ts;
}
return null;
}
public TimeSeries getSeriesByNameLocking(String name) {
waitForLock();
TimeSeries ts = getSeriesByNameNonlocking(name);
unlock();
return ts;
}
public ArrayList<TimeSeries> getAllSeries() {
return mSeries;
}
public ArrayList<TimeSeries> getAllEnabledSeries() {
ArrayList<TimeSeries> list = new ArrayList<TimeSeries>();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.isEnabled())
list.add(ts);
}
return list;
}
public Datapoint getVisibleFirstDatapointLocking() {
Datapoint first = null;
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.isEnabled() == true) {
Datapoint d = ts.getFirstVisible();
if (first == null)
first = d;
else if (d.mMillis < first.mMillis)
first = d;
}
}
unlock();
return first;
}
public Datapoint getVisibleLastDatapointLocking() {
Datapoint last = null;
waitForLock();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts != null && ts.isEnabled() == true) {
Datapoint d = ts.getLastVisible();
if (last == null)
last = d;
else if (d.mMillis > last.mMillis)
last = d;
}
}
unlock();
return last;
}
public void clearCache() {
mDatapointCache.clearCache();
clearSeriesLocking();
}
public synchronized void gatherLatestDatapointsLocking(long catId, int history) {
waitForLock();
mDatapointCache.populateLatest(catId, history);
TimeSeries ts = getSeriesByIdNonlocking(catId);
if (ts == null) {
unlock();
return;
}
if (ts.getDbRow().getSynthetic() == false) {
ts.clearSeries();
EntryDbTable.Row entry = mDbh.fetchLastCategoryEntry(catId);
if (entry != null) {
ArrayList<Datapoint> l = mDatapointCache.getLast(catId, history);
if (l == null || l.size() < 1
|| entry.getTimestamp() > l.get(0).mMillis
|| entry.getValue() != l.get(0).mValue.y) {
mDatapointCache.clearCache(catId);
mDatapointCache.populateLatest(catId, history);
l = mDatapointCache.getLast(catId, history);
}
l = aggregateDatapoints(l, ts.getDbRow().getType());
ts.setDatapoints(null, l, null, true);
}
}
unlock();
}
public synchronized void gatherSeriesLocking(long milliStart, long milliEnd) {
waitForLock();
gatherSeries(milliStart, milliEnd);
unlock();
}
public synchronized void gatherSeries(long milliStart, long milliEnd) {
ArrayList<Datapoint> pre, range, post;
boolean has_data;
long oldAggregationMs = mAggregationMs;
mQueryStart = milliStart;
mQueryEnd = milliEnd;
setCollectionTimes(milliStart, milliEnd);
for (int i = 0; i < mSeries.size(); i++) {
has_data = false;
TimeSeries ts = mSeries.get(i);
if (ts == null || ts.getDbRow().getSynthetic())
continue;
if (ts.isEnabled() == false) {
boolean skip = true;
for (int j = 0; j < ts.getDependees().size(); j++) {
if (ts.getDependees().get(j).isEnabled() == true) {
skip = false;
break;
}
}
if (skip == true)
continue;
}
mDatapointCache.populateRange(ts.getDbRow().getId(), mCollectionStart,
mCollectionEnd, mAggregationMs);
pre = mDatapointCache.getDataBefore(ts.getDbRow().getId(), mHistory,
mCollectionStart);
if (pre != null && pre.size() > 0)
has_data = true;
range = mDatapointCache.getDataInRange(ts.getDbRow().getId(),
mCollectionStart, mCollectionEnd);
if (range != null && range.size() > 0)
has_data = true;
post = mDatapointCache.getDataAfter(ts.getDbRow().getId(), 1,
mCollectionEnd);
if (post != null && range.size() > 0)
has_data = true;
if (has_data == true)
ts.setDatapoints(pre, range, post, true);
}
generateSynthetics();
ArrayList<TimeSeries> enabledSeries = getAllEnabledSeries();
for (int i = 0; i < enabledSeries.size(); i++) {
aggregateDatapoints(enabledSeries.get(i));
}
mAggregationMs = oldAggregationMs;
return;
}
public Datapoint getLastDatapoint(long catId) {
ArrayList<Datapoint> list = mDatapointCache.getLast(catId, 1);
if (list == null || list.size() < 1)
return null;
return list.get(0);
}
public synchronized void updateCategoryTrend(long catId) {
String trendStr = "trend_unknown";
float stdDev = 0.0f;
float lastTrend = 0.0f;
float newTrend = 0.0f;
gatherLatestDatapointsLocking(catId, mHistory);
TimeSeries ts = getSeriesByIdLocking(catId);
if (ts == null)
return;
if (ts.getDbRow().getSynthetic() == true)
return;
lastTrend = ts.getTrendStats().mTrendPrev;
newTrend = ts.getTrendStats().mTrend;
stdDev = ts.getValueStats().mStdDev;
TrendState state = Number.getTrendState(lastTrend, newTrend, ts.getDbRow()
.getGoal(), mSensitivity, stdDev);
trendStr = Number.mapTrendStateToString(state);
mDbh.updateCategoryTrend(catId, trendStr, newTrend);
if (ts.getDependees() != null && ts.getDependees().size() > 0) {
for (int i = 0; i < ts.getDependees().size(); i++) {
TimeSeries dependee = ts.getDependees().get(i);
for (int j = 0; j < dependee.getDependents().size(); j++) {
TimeSeries tmp = dependee.getDependents().get(j);
if (tmp != null)
gatherLatestDatapointsLocking(tmp.getDbRow().getId(), mHistory);
}
Formula formula = mFormulaCache.getFormula(dependee.getDbRow().getId());
ArrayList<Datapoint> calculated = formula.apply(dependee
.getDependents());
dependee.setDatapoints(null, calculated, null, true);
lastTrend = dependee.getTrendStats().mTrendPrev;
newTrend = dependee.getTrendStats().mTrend;
stdDev = dependee.getValueStats().mStdDev;
state = Number.getTrendState(lastTrend, newTrend, dependee.getDbRow()
.getGoal(), mSensitivity, stdDev);
trendStr = Number.mapTrendStateToString(state);
mDbh.updateCategoryTrend(dependee.getDbRow().getId(), trendStr,
newTrend);
}
}
}
private void generateSynthetics() {
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries synth = mSeries.get(i);
if (synth == null || synth.getDbRow().getSynthetic() == false
|| synth.isEnabled() == false)
continue;
generateSynthetic(synth);
}
}
private void generateSynthetic(TimeSeries synth) {
Formula formula = mFormulaCache.getFormula(synth.getDbRow().getId());
long ms;
long firstVisibleMs = Long.MAX_VALUE;
long lastVisibleMs = Long.MIN_VALUE;
for (int j = 0; j < synth.getDependents().size(); j++) {
TimeSeries ts = synth.getDependents().get(j);
List<Datapoint> range = ts.getVisible();
if (range != null) {
ms = range.get(0).mMillis;
if (ms < firstVisibleMs)
firstVisibleMs = ms;
ms = range.get(range.size() - 1).mMillis;
if (ms > lastVisibleMs)
lastVisibleMs = ms;
}
}
ArrayList<Datapoint> calculated = formula.apply(synth.getDependents());
ArrayList<Datapoint> pre = new ArrayList<Datapoint>();
ArrayList<Datapoint> visible = new ArrayList<Datapoint>();
ArrayList<Datapoint> post = new ArrayList<Datapoint>();
for (int j = 0; j < calculated.size(); j++) {
Datapoint d = calculated.get(j);
d.mCatId = synth.getDbRow().getId();
d.mSynthetic = true;
if (d.mMillis < firstVisibleMs)
pre.add(d);
else if (d.mMillis <= lastVisibleMs)
visible.add(d);
else
post.add(d);
}
pre = aggregateDatapoints(pre, synth.getDbRow().getType());
visible = aggregateDatapoints(visible, synth.getDbRow().getType());
post = aggregateDatapoints(post, synth.getDbRow().getType());
synth.setDatapoints(pre, visible, post, true);
}
private void aggregateDatapoints(TimeSeries ts) {
ArrayList<Datapoint> pre;
ArrayList<Datapoint> range;
ArrayList<Datapoint> post;
pre = aggregateDatapoints(ts.getVisiblePre(), ts.getDbRow().getType());
range = aggregateDatapoints(ts.getVisible(), ts.getDbRow().getType());
post = aggregateDatapoints(ts.getVisiblePost(), ts.getDbRow().getType());
ts.setDatapoints(pre, range, post, true);
return;
}
private ArrayList<Datapoint> aggregateDatapoints(List<Datapoint> list,
String type) {
Datapoint accumulator = null;
Datapoint d = null;
ArrayList<Datapoint> newList = new ArrayList<Datapoint>();
if (list == null)
return newList;
if (mAggregationMs == 0) {
newList.addAll(list);
return newList;
}
for (int i = 0; i < list.size(); i++) {
d = list.get(i);
if (i == 0) {
accumulator = new Datapoint(d);
continue;
}
if (inSameAggregationPeriod(accumulator, d) == false) {
newList.add(accumulator);
accumulator = new Datapoint(d);
accumulator.mNEntries = 1;
} else {
if (type.equals(CategoryDbTable.KEY_TYPE_SUM)) {
accumulator.mValue.y += d.mValue.y;
accumulator.mNEntries++;
} else if (type.equals(CategoryDbTable.KEY_TYPE_AVERAGE)) {
if (accumulator.mNEntries + d.mNEntries != 0) {
accumulator.mNEntries++;
float oldMean = accumulator.mValue.y;
accumulator.mValue.y += ((d.mValue.y - oldMean) / accumulator.mNEntries);
}
}
}
}
if (accumulator != null)
newList.add(accumulator);
return newList;
}
private void setCollectionTimes(long milliStart, long milliEnd) {
if (mAutoAggregation == true) {
mAutoAggSpan.setSpanOffset(mAutoAggregationOffset);
mAutoAggSpan.setSpan(milliStart, milliEnd);
mAggregationMs = DateUtil.mapPeriodToLong(mAutoAggSpan.getSpan());
}
// this adjustment is to make sure that the edges of the visible range
// doesn't
// span periods (for the purposes of calculations, not display --
// anything
// that's
// in the visible range array, but not actually on-screen due to
// aggregation
// back
// to the first datapoint in the period, will just be drawn off screen.
// Note
// that
// this will also cause the scaling to take into account these
// off-screen
// points,
// but that's okay, and perhaps desireable, since it's more likely to
// have
// no
// datapoints visible within the range, having only aggregated points on
// either
// side, when aggregating.
if (mAggregationMs != 0) {
Period p = DateUtil.mapLongToPeriod(mAggregationMs);
mCal1.setTimeInMillis(milliStart);
DateUtil.setToPeriodStart(mCal1, p);
milliStart = mCal1.getTimeInMillis();
mCal2.setTimeInMillis(milliEnd);
DateUtil.setToPeriodStart(mCal2, p);
int step = 1;
if (p == Period.QUARTER)
step = 3;
mCal2.add(DateUtil.mapLongToCal(mAggregationMs), step);
milliEnd = mCal2.getTimeInMillis();
}
mCollectionStart = milliStart;
mCollectionEnd = milliEnd;
return;
}
private boolean inSameAggregationPeriod(Datapoint d1, Datapoint d2) {
if (d1 == null || d2 == null)
return false;
mCal1.setTimeInMillis(d1.mMillis);
mCal2.setTimeInMillis(d2.mMillis);
return DateUtil.inSamePeriod(mCal1, mCal2, mAggregationMs);
}
private void setDependents(TimeSeries synth) {
if (synth.getDbRow().getSynthetic() == false)
return;
Formula formula = mFormulaCache.getFormula(synth.getDbRow().getId());
ArrayList<String> names = formula.getDependentNames();
if (names == null)
return;
synth.getDependents().clear();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries ts = mSeries.get(i);
if (ts == null || ts == synth)
continue;
if (names.contains(ts.getDbRow().getCategoryName()))
synth.addDependent(ts);
}
return;
}
private void setDependees(TimeSeries ts) {
ts.getDependees().clear();
for (int i = 0; i < mSeries.size(); i++) {
TimeSeries dependee = getSeries(i);
if (ts == null || dependee == null || ts == dependee)
continue;
if (dependee.getDbRow().getSynthetic() == true) {
Formula formula = mFormulaCache.getFormula(dependee.getDbRow().getId());
ArrayList<String> names = formula.getDependentNames();
if (names != null && names.contains(ts.getDbRow().getCategoryName()))
ts.addDependee(dependee);
}
}
return;
}
}