/* * 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 java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.TreeMap; import net.redgeek.android.eventrend.db.CategoryDbTable; import net.redgeek.android.eventrend.graph.GraphView; import net.redgeek.android.eventrend.graph.TimeSeriesPainter; import net.redgeek.android.eventrend.graph.plugins.TimeSeriesInterpolator; import net.redgeek.android.eventrend.synthetic.AST; import net.redgeek.android.eventrend.util.DateUtil; import net.redgeek.android.eventrend.util.Number; import android.graphics.Canvas; /** * A representation of series of Datapoints plottable on screen. Specific * per-category. * * @author barclay * */ public final class TimeSeries { // All of the following are ordered via x-values (time), and are references // to // datapoints in the DataCache. These are just the datapoints necessary // for graphing the time series on screen, not an exhaustive list. Multiple // datapoints are required for VisiblePre (if available), as they are needed // to calculate the trend line accurately and continue the graph line to the // left edge of the screen, and at least one datapoint is needed // in VisiblePost (if available) in order to connect the last on-screen // point // to something offscreen, in order to continue drawing the line to the edge // of // the graph. private ArrayList<Datapoint> mDatapoints; // a concatenation of the // following: private int mVisiblePreFirstIdx; private int mVisiblePreLastIdx; private int mVisibleFirstIdx; private int mVisibleLastIdx; private int mVisiblePostFirstIdx; private int mVisiblePostLastIdx; // Various stats used for bounding private float mVisibleMinY; private float mVisibleMaxY; private long mVisibleMinX; private long mVisibleMaxX; private long mTimestampLast; private int mNumEntries = 0; private boolean mEnabled; CategoryDbTable.Row mDbRow; private ArrayList<TimeSeries> mDependents; private ArrayList<TimeSeries> mDependees; // Drawing-related private float mTouchRadius = GraphView.POINT_TOUCH_RADIUS; private String mColorStr; private TimeSeriesPainter mPainter; // Various stats private Number.RunningStats mValueStats; private Number.Trend mValueTrend; private Number.RunningStats mTimestampStats; private Number.WindowedStdDev mStdDevWindow; // Interpolator private TimeSeriesInterpolator mInterpolator; public TimeSeries(CategoryDbTable.Row row, int history, float smoothing) { initialize(row, history, smoothing, null); } public TimeSeries(CategoryDbTable.Row row, int history, float smoothing, TimeSeriesPainter painter) { initialize(row, history, smoothing, painter); } private void initialize(CategoryDbTable.Row row, int history, float smoothing, TimeSeriesPainter painter) { mDbRow = row; mPainter = painter; if (painter == null) mPainter = new TimeSeriesPainter.Default(); mDatapoints = new ArrayList<Datapoint>(); mEnabled = false; setColor(row.getColor()); // set thusly so we can apply min()/max() operators indiscriminately resetMinMax(); resetIndices(); mValueStats = new Number.RunningStats(); mTimestampStats = new Number.RunningStats(); mValueTrend = new Number.Trend(smoothing); mStdDevWindow = new Number.WindowedStdDev(history); mDependents = new ArrayList<TimeSeries>(); mDependees = new ArrayList<TimeSeries>(); } public TimeSeries(TimeSeries series) { mDbRow = new CategoryDbTable.Row(series.mDbRow); mDatapoints = new ArrayList<Datapoint>(); mVisiblePreFirstIdx = series.mVisiblePreFirstIdx; mVisiblePreLastIdx = series.mVisiblePreLastIdx; mVisibleFirstIdx = series.mVisibleFirstIdx; mVisibleLastIdx = series.mVisibleLastIdx; mVisiblePostFirstIdx = series.mVisiblePostFirstIdx; mVisiblePostLastIdx = series.mVisiblePostLastIdx; copyDatapoints(series); mEnabled = series.mEnabled; mVisibleMinY = series.mVisibleMinY; mVisibleMaxY = series.mVisibleMaxY; mVisibleMinX = series.mVisibleMinX; mVisibleMaxX = series.mVisibleMaxX; mTimestampLast = series.mTimestampLast; mNumEntries = series.mNumEntries; mEnabled = series.mEnabled; mValueStats = new Number.RunningStats(series.mValueStats); mTimestampStats = new Number.RunningStats(series.mTimestampStats); mValueTrend = new Number.Trend(series.mValueTrend); mStdDevWindow = new Number.WindowedStdDev(series.mStdDevWindow); mDependents = new ArrayList<TimeSeries>(); for (int i = 0; i < mDependents.size(); i++) { mDependents.add(series.mDependents.get(i)); } mDependees = new ArrayList<TimeSeries>(); for (int i = 0; i < mDependees.size(); i++) { mDependees.add(series.mDependees.get(i)); } mPainter = series.mPainter; // this should be fine to share; it should be stateless mInterpolator = series.mInterpolator; } @Override public String toString() { return mDbRow.getCategoryName() + " (id: " + mDbRow.getId() + ")"; } public void clearSeries() { resetIndices(); mDatapoints.clear(); } public void setPointRadius(float f) { mPainter.setPointRadius(f); } public CategoryDbTable.Row getDbRow() { return mDbRow; } public void setDbRow(CategoryDbTable.Row row) { mDbRow = row; } public void setInterpolator(TimeSeriesInterpolator i) { mInterpolator = i; } public TimeSeriesInterpolator getInterpolator() { return mInterpolator; } public void setEnabled(boolean b) { mEnabled = b; } public boolean isEnabled() { return mEnabled; } public void addDependent(TimeSeries ts) { if (mDependents.contains(ts) != true) mDependents.add(ts); } public ArrayList<TimeSeries> getDependents() { return mDependents; } public void addDependee(TimeSeries ts) { if (mDependees.contains(ts) != true) mDependees.add(ts); } public ArrayList<TimeSeries> getDependees() { return mDependees; } public int getVisibleNumEntries() { return mNumEntries; } public float getVisibleValueMin() { return mVisibleMinY; } public float getVisibleValueMax() { return mVisibleMaxY; } public long getVisibleTimestampMin() { return mVisibleMinX; } public long getVisibleTimestampMax() { return mVisibleMaxX; } public Number.RunningStats getValueStats() { return mValueStats; } public Number.RunningStats getTimestampStats() { return mTimestampStats; } public Number.Trend getTrendStats() { return mValueTrend; } public int getVisiblePreFirstIdx() { return mVisiblePreFirstIdx; } public int getVisiblePreLastIdx() { return mVisiblePreLastIdx; } public int getVisibleFirstIdx() { return mVisibleFirstIdx; } public int getVisibleLastIdx() { return mVisibleLastIdx; } public int getVisiblePostFirstIdx() { return mVisiblePostFirstIdx; } public int getVisiblePostLastIdx() { return mVisiblePostLastIdx; } public String getColor() { return mColorStr; } public void recalcStatsAndBounds(float smoothing, int history) { mValueStats = new Number.RunningStats(); mTimestampStats = new Number.RunningStats(); mValueTrend = new Number.Trend(smoothing); mStdDevWindow = new Number.WindowedStdDev(history); calcStatsAndBounds(); } private void calcStatsAndBounds() { int firstNEntries = 0; resetMinMax(); if (mDatapoints == null) return; for (int i = 0; i < mDatapoints.size(); i++) { Datapoint d = mDatapoints.get(i); if (i >= mVisibleFirstIdx && i <= mVisibleLastIdx) { mVisibleMinY = Math.min(mVisibleMinY, d.mValue.y); mVisibleMaxY = Math.max(mVisibleMaxY, d.mValue.y); mVisibleMinX = Math.min(mVisibleMinX, d.mMillis); mVisibleMaxX = Math.max(mVisibleMaxX, d.mMillis); // we run stats on the y-values themselves ... mValueStats.update(d.mValue.y, d.mNEntries); mValueTrend.update(d.mValue.y); mStdDevWindow.update(d.mValue.y); d.mStdDev = mStdDevWindow.getStandardDev(); // but use the delta of the timestamps for x-stats: if (i > 0) { long delta = (d.mMillis - mTimestampLast); mTimestampStats.update(delta, d.mNEntries + firstNEntries); } mTimestampLast = (d.mMillis); firstNEntries = 0; if (i == 0) firstNEntries = d.mNEntries; // d.mTrend will be used for plotting the trend line, // so we don't want to change the x value, since that // should still be an absolute time d.mTrend.x = d.mValue.x; d.mTrend.y = mValueTrend.mTrend; mNumEntries += d.mNEntries; } else { mValueTrend.update(d.mValue.y); d.mTrend.x = d.mValue.x; d.mTrend.y = mValueTrend.mTrend; mStdDevWindow.update(d.mValue.y); d.mStdDev = mStdDevWindow.getStandardDev(); } } interpolateBoundsToOffscreen(); } public void setDatapoints(ArrayList<Datapoint> pre, ArrayList<Datapoint> range, ArrayList<Datapoint> post, boolean recalc) { clearSeries(); if (pre != null && pre.size() > 0) { mVisiblePreFirstIdx = 0; mVisiblePreLastIdx = pre.size() - 1; mDatapoints.addAll(pre); } if (range != null && range.size() > 0) { mVisibleFirstIdx = mDatapoints.size(); mVisibleLastIdx = mVisibleFirstIdx + range.size() - 1; mDatapoints.addAll(range); } if (post != null && post.size() > 0) { mVisiblePostFirstIdx = mDatapoints.size(); mVisiblePostLastIdx = mVisiblePostFirstIdx + post.size() - 1; mDatapoints.addAll(post); } if (recalc == true) calcStatsAndBounds(); return; } public Datapoint getLastPreVisible() { if (mVisiblePreLastIdx >= 0 && mVisiblePreLastIdx < mDatapoints.size()) return mDatapoints.get(mVisiblePreLastIdx); return null; } public Datapoint getFirstVisible() { if (mVisibleFirstIdx >= 0 && mVisibleFirstIdx < mDatapoints.size()) return mDatapoints.get(mVisibleFirstIdx); return null; } public Datapoint getLastVisible() { if (mVisibleLastIdx >= 0 && mVisibleLastIdx < mDatapoints.size()) return mDatapoints.get(mVisibleLastIdx); return null; } public Datapoint getFirstPostVisible() { if (mVisiblePostFirstIdx >= 0 && mVisiblePostFirstIdx < mDatapoints.size()) return mDatapoints.get(mVisiblePostFirstIdx); return null; } public Datapoint lookupVisibleDatapoint(Tuple press) { Datapoint d; if (isEnabled() == false) return null; if (mVisibleFirstIdx != Integer.MIN_VALUE && mVisibleLastIdx != Integer.MAX_VALUE) { for (int i = mVisibleFirstIdx; i <= mVisibleLastIdx; i++) { d = mDatapoints.get(i); if (press.x >= d.mValueScreen.x - mTouchRadius && press.x <= d.mValueScreen.x + mTouchRadius && press.y >= d.mValueScreen.y - mTouchRadius && press.y <= d.mValueScreen.y + mTouchRadius) { return d; } } } return null; } public Datapoint findNeighbor(long timestamp, boolean pre) { Datapoint d = null; if (mDatapoints == null || mDatapoints.size() < 1) return d; int min = 0; int max = mDatapoints.size() - 1; int mid = max / 2; d = mDatapoints.get(mid); while (d != null) { if (d.mMillis == timestamp) { return d; } else if (max < min) { if (pre == true) { if (d.mMillis > timestamp) { if (mid - 1 >= 0) d = mDatapoints.get(mid - 1); else d = null; } } else { if (d.mMillis < timestamp) { if (mid + 1 < mDatapoints.size()) d = mDatapoints.get(mid + 1); else d = null; } } return d; } else if (d.mMillis < timestamp) { min = mid + 1; } else if (d.mMillis > timestamp) { max = mid - 1; } mid = min + ((max - min) / 2); // Check to see if we were trying to run off the end, if so, just // return the first or last entry. if (mid >= mDatapoints.size() && pre == true) return d; if (mid < 0 && pre == false) return d; if (mid < 0 || mid > mDatapoints.size() - 1) break; d = mDatapoints.get(mid); } return null; } public ArrayList<Datapoint> getDatapoints() { return mDatapoints; } public List<Datapoint> getVisiblePre() { List<Datapoint> view = null; if (mVisiblePreFirstIdx != Integer.MIN_VALUE && mVisiblePreLastIdx != Integer.MAX_VALUE) { try { view = mDatapoints.subList(mVisiblePreFirstIdx, mVisiblePreLastIdx + 1); } catch(IndexOutOfBoundsException e) { // nothing } catch(IllegalArgumentException e) { // nothing } } return view; } public List<Datapoint> getVisible() { List<Datapoint> view = null; if (mVisibleFirstIdx != Integer.MIN_VALUE && mVisibleLastIdx != Integer.MAX_VALUE) { try { view = mDatapoints.subList(mVisibleFirstIdx, mVisibleLastIdx + 1); } catch(IndexOutOfBoundsException e) { // nothing } catch(IllegalArgumentException e) { // nothing } } return view; } public List<Datapoint> getVisiblePost() { List<Datapoint> view = null; if (mVisiblePostFirstIdx != Integer.MIN_VALUE && mVisiblePostLastIdx != Integer.MAX_VALUE) { try { view =mDatapoints.subList(mVisiblePostFirstIdx, mVisiblePostLastIdx + 1); } catch(IndexOutOfBoundsException e) { // nothing } catch(IllegalArgumentException e) { // nothing } } return view; } public Datapoint findPreNeighbor(long timestamp) { return findNeighbor(timestamp, true); } public Datapoint findPostNeighbor(long timestamp) { return findNeighbor(timestamp, false); } public Float interpolateScreenCoord(long timestamp) { Datapoint d1 = findPreNeighbor(timestamp - 1); Datapoint d2 = findPostNeighbor(timestamp); if (d1 == null || d2 == null) return null; return mInterpolator.interpolateY(d1.mValueScreen, d2.mValueScreen, timestamp); } public Float interpolateValue(long timestamp) { Datapoint d2 = findPostNeighbor(timestamp); // inclusive if (d2 == null) return null; if (d2.mMillis == timestamp) return new Float(d2.mValue.y); Datapoint d1 = findPreNeighbor(timestamp); // inclusive if (d1 == null) return null; return mInterpolator.interpolateY(d1.mValue, d2.mValue, timestamp); } public void floatOp(Float f, AST.Opcode op, boolean pre) { if (f == null || f.isNaN() || f.isInfinite()) return; Datapoint d; for (int i = 0; i < mDatapoints.size(); i++) { d = mDatapoints.get(i); if (pre == false) { if (op == AST.Opcode.PLUS) d.mValue.y += f; else if (op == AST.Opcode.MINUS) d.mValue.y -= f; else if (op == AST.Opcode.MULTIPLY) d.mValue.y *= f; else if (op == AST.Opcode.DIVIDE) { if (f != 0) d.mValue.y /= f; } } else { if (op == AST.Opcode.PLUS) d.mValue.y = f + d.mValue.y; else if (op == AST.Opcode.MINUS) d.mValue.y = f - d.mValue.y; else if (op == AST.Opcode.MULTIPLY) d.mValue.y = f * d.mValue.y; else if (op == AST.Opcode.DIVIDE) { if (d.mValue.y != 0) d.mValue.y = f / d.mValue.y; } } } } public void plusPre(Float f) { floatOp(f, AST.Opcode.PLUS, true); } public void minusPre(Float f) { floatOp(f, AST.Opcode.MINUS, true); } public void multiplyPre(Float f) { floatOp(f, AST.Opcode.MULTIPLY, true); } public void dividePre(Float f) { floatOp(f, AST.Opcode.DIVIDE, true); } public void plusPost(Float f) { floatOp(f, AST.Opcode.PLUS, false); } public void minusPost(Float f) { floatOp(f, AST.Opcode.MINUS, false); } public void multiplyPost(Float f) { floatOp(f, AST.Opcode.MULTIPLY, false); } public void dividePost(Float f) { floatOp(f, AST.Opcode.DIVIDE, false); } public void longOp(Long l, AST.Opcode op, boolean pre) { if (l == null) return; Datapoint d; for (int i = 0; i < mDatapoints.size(); i++) { d = mDatapoints.get(i); if (pre == false) { if (op == AST.Opcode.PLUS) d.mValue.y += l; else if (op == AST.Opcode.MINUS) d.mValue.y -= l; else if (op == AST.Opcode.MULTIPLY) d.mValue.y *= l; else if (op == AST.Opcode.DIVIDE) { if (l != 0) d.mValue.y /= l; } } else { if (op == AST.Opcode.PLUS) d.mValue.y = l + d.mValue.y; else if (op == AST.Opcode.MINUS) d.mValue.y = l - d.mValue.y; else if (op == AST.Opcode.MULTIPLY) d.mValue.y = l * d.mValue.y; else if (op == AST.Opcode.DIVIDE) { if (d.mValue.y != 0) d.mValue.y = l / d.mValue.y; } } } } public void plusPre(Long l) { longOp(l, AST.Opcode.PLUS, true); } public void minusPre(Long l) { longOp(l, AST.Opcode.MINUS, true); } public void multiplyPre(Long l) { longOp(l, AST.Opcode.MULTIPLY, true); } public void dividePre(Long l) { longOp(l, AST.Opcode.DIVIDE, true); } public void plusPost(Long l) { longOp(l, AST.Opcode.PLUS, false); } public void minusPost(Long l) { longOp(l, AST.Opcode.MINUS, false); } public void multiplyPost(Long l) { longOp(l, AST.Opcode.MULTIPLY, false); } public void dividePost(Long l) { longOp(l, AST.Opcode.DIVIDE, false); } public void previousValue() { Datapoint d1, d2; for (int i = mDatapoints.size() - 1; i > 0; i--) { d1 = mDatapoints.get(i - 1); d2 = mDatapoints.get(i); d2.mValue.y = d2.mValue.y - d1.mValue.y; } } public void previousTimestamp() { Datapoint d1, d2; for (int i = mDatapoints.size() - 1; i > 0; i--) { d1 = mDatapoints.get(i - 1); d2 = mDatapoints.get(i); d2.mValue.y = d2.mMillis - d1.mMillis; } if (mDatapoints.size() > 0) { d1 = mDatapoints.get(0); d1.mValue.y = 0; } } public void inPeriod(DateUtil.Period p) { Datapoint d; long ms = DateUtil.mapPeriodToLong(p); for (int i = 0; i < mDatapoints.size(); i++) { d = mDatapoints.get(i); if (ms != 0) d.mValue.y /= ms; } } public void asPeriod(DateUtil.Period p) { Datapoint d; long ms = DateUtil.mapPeriodToLong(p); for (int i = 0; i < mDatapoints.size(); i++) { d = mDatapoints.get(i); d.mValue.y *= ms; } } // We need to gather a list of all timestamps so we can interpolate from // ones series to the other, and vice versa, in order to make the operations // commutative public void timeseriesOp(TimeSeries ts, AST.Opcode op) { Float f1, f2; Datapoint d1; ArrayList<Datapoint> pre = new ArrayList<Datapoint>(); ArrayList<Datapoint> range = new ArrayList<Datapoint>(); ArrayList<Datapoint> post = new ArrayList<Datapoint>(); TreeMap<Long, Boolean> timestamps = new TreeMap<Long, Boolean>(); for (int i = 0; i < mDatapoints.size(); i++) { timestamps.put(new Long(mDatapoints.get(i).mMillis), true); } for (int i = 0; i < ts.mDatapoints.size(); i++) { timestamps.put(new Long(ts.mDatapoints.get(i).mMillis), true); } Iterator<Long> iterator = timestamps.keySet().iterator(); while (iterator.hasNext()) { Long ms = iterator.next(); f1 = interpolateValue(ms); f2 = ts.interpolateValue(ms); // We handle invalid interpolations slightly differing depending on // opcode. For example, for + and -, it could be justified that // adding // or subtracting to/from a datapoint that doesn't exist means that // the // missing value should be 0 (this assumption can certainly be // challenged, // but for most of the use cases for the application, I believe this // is // correct.) However, for * and /, should we attempt to return the // identity, or 0? It's unclear, so we bail on the calculation. if (op == AST.Opcode.PLUS || op == AST.Opcode.MINUS) { if (f1 == null) f1 = new Float(0.0f); if (f2 == null) f2 = new Float(0.0f); } if (f1 == null || f1.isNaN() || f1.isInfinite()) continue; if (f2 == null || f2.isNaN() || f2.isInfinite()) continue; d1 = new Datapoint(ms, f1, getDbRow().getId(), -1, 1); if (op == AST.Opcode.PLUS) d1.mValue.y += f2; else if (op == AST.Opcode.MINUS) d1.mValue.y -= f2; else if (op == AST.Opcode.MULTIPLY) d1.mValue.y *= f2; else if (op == AST.Opcode.DIVIDE) { if (f2 == 0) continue; else d1.mValue.y /= f2; } if (ms < getVisibleTimestampMin() && ms < ts.getVisibleTimestampMin()) { pre.add(d1); } else if (ms > getVisibleTimestampMax() && ms > ts.getVisibleTimestampMax()) { post.add(d1); } else { range.add(d1); } } setDatapoints(pre, range, post, true); } public void timeseriesPlus(TimeSeries ts) { timeseriesOp(ts, AST.Opcode.PLUS); } public void timeseriesMinus(TimeSeries ts) { timeseriesOp(ts, AST.Opcode.MINUS); } public void timeseriesMultiply(TimeSeries ts) { timeseriesOp(ts, AST.Opcode.MULTIPLY); } public void timeseriesDivide(TimeSeries ts) { timeseriesOp(ts, AST.Opcode.DIVIDE); } public void drawPath(Canvas canvas) { mPainter.drawPath(canvas, this); } public void drawTrend(Canvas canvas) { mPainter.drawTrend(canvas, this); } public void drawText(Canvas canvas, String s, float x, float y) { mPainter.drawText(canvas, s, x, y); } public void drawGoal(Canvas canvas, Tuple start, Tuple end) { mPainter.drawGoal(canvas, start, end); } public void drawMarker(Canvas canvas, Tuple start, Tuple end) { mPainter.drawMarker(canvas, start, end); } private void interpolateBoundsToOffscreen() { int nDatapoints; Datapoint d1 = null; Datapoint d2 = null; float iy = 0.0f; List<Datapoint> preVisible = getVisiblePre(); List<Datapoint> visible = getVisible(); List<Datapoint> postVisible = getVisiblePost(); nDatapoints = mDatapoints.size(); if (nDatapoints < 2) return; if (postVisible != null) { // datapoints after visible range to interpolate to d2 = postVisible.get(0); if (visible != null) { // also datapoints in visible range d1 = visible.get(visible.size() - 1); } else if (preVisible != null) { // no datapoints in visible range, grab last from before the // visible range d1 = preVisible.get(preVisible.size() - 1); } else { // no datapoints to connect it to return; } if (d2.mMillis > d1.mMillis) { iy = (d2.mValue.y - d1.mValue.y) / (d2.mMillis - d1.mMillis); if (iy > 0 && d2.mValue.y > mVisibleMaxY) { mVisibleMaxY = d2.mValue.y; } if (iy < 0 && d2.mValue.y < mVisibleMinY) { mVisibleMinY = d2.mValue.y; } } } if (preVisible != null) { // we have a datapoint before the beginning to interpolate d1 = preVisible.get(preVisible.size() - 1); if (visible != null) { // also datapoints in visible range d2 = visible.get(0); } else if (postVisible != null) { // no datapoints in visible range, grab first from beyond the // visible range d2 = postVisible.get(0); } else { // no datapoints to connect it to return; } if (d1.mMillis < d2.mMillis) { iy = (d2.mValue.y - d1.mValue.y) / (d2.mMillis - d1.mMillis); if (iy < 0 && d1.mValue.y > mVisibleMaxY) { mVisibleMaxY = d1.mValue.y; } if (iy > 0 && d1.mValue.y < mVisibleMinY) { mVisibleMinY = d1.mValue.y; } } } } private void copyDatapoints(TimeSeries source) { mDatapoints.clear(); if (source.mDatapoints == null) return; for (int i = 0; i < source.mDatapoints.size(); i++) { Datapoint d = new Datapoint(source.mDatapoints.get(i)); mDatapoints.add(d); } return; } private void setColor(String color) { mColorStr = color; mPainter.setColor(color); } private void resetIndices() { mVisiblePreFirstIdx = Integer.MIN_VALUE; mVisiblePreLastIdx = Integer.MIN_VALUE; mVisibleFirstIdx = Integer.MIN_VALUE; mVisibleLastIdx = Integer.MIN_VALUE; mVisiblePostFirstIdx = Integer.MIN_VALUE; mVisiblePostLastIdx = Integer.MIN_VALUE; } private void resetMinMax() { mVisibleMinY = Float.MAX_VALUE; mVisibleMaxY = Float.MIN_VALUE; mVisibleMinX = Long.MAX_VALUE; mVisibleMaxX = Long.MIN_VALUE; } }