merge heads

This commit is contained in:
Jimmy Shih
2012-05-24 09:29:16 -07:00
3 changed files with 203 additions and 258 deletions
@@ -13,9 +13,11 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * the License.
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -26,122 +28,118 @@ import android.graphics.Path;
import java.text.NumberFormat; import java.text.NumberFormat;
/** /**
* This class encapsulates meta data about one series of values for a chart. * This class encapsulates the meta data for one series of chart values.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class ChartValueSeries { public class ChartValueSeries {
private final ExtremityMonitor monitor = new ExtremityMonitor(); private static final float STROKE_WIDTH = 2f;
private final NumberFormat format;
private final Path path = new Path(); private final ExtremityMonitor extremityMonitor;
private final NumberFormat numberFormat;
private final Path path;
private final Paint fillPaint; private final Paint fillPaint;
private final Paint strokePaint; private final Paint strokePaint;
private final Paint labelPaint; private final Paint labelPaint;
private final ZoomSettings zoomSettings; private final YAxisDimension markingInterval;
private String title; private String title;
private double min;
private double max = 1.0;
private int effectiveMax;
private int effectiveMin;
private double spread;
private int interval;
private boolean enabled = true; private boolean enabled = true;
private int interval = 1;
private int effectiveMin = 0;
private int effectiveMax = 1;
/** /**
* This class controls how effective min/max values of a {@link ChartValueSeries} are calculated. * This class to calculates the y axis dimension, interval, effective min, and
* effective max.
*/ */
public static class ZoomSettings { public static class YAxisDimension {
private int intervals; private final int numberOfIntervals;
private final int absoluteMin; private final int absoluteMin;
private final int absoluteMax; private final int absoluteMax;
private final int[] zoomLevels; private final int[] intervalValues;
public ZoomSettings(int intervals, int[] zoomLevels) { /**
this.intervals = intervals; * Constructor.
this.absoluteMin = Integer.MAX_VALUE; *
this.absoluteMax = Integer.MIN_VALUE; * @param numberOfIntervals the number of intervals
this.zoomLevels = zoomLevels; * @param absoluteMin the absolute minimum value
checkArgs(); * @param absoluteMax the absolute maximum value
} * @param intervalValues the list of interval values
*/
public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) { public YAxisDimension(
this.intervals = intervals; int numberOfIntervals, int absoluteMin, int absoluteMax, int[] intervalValues) {
this.numberOfIntervals = numberOfIntervals;
this.absoluteMin = absoluteMin; this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax; this.absoluteMax = absoluteMax;
this.zoomLevels = zoomLevels; this.intervalValues = intervalValues;
checkArgs();
}
private void checkArgs() {
if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) {
throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels");
}
for (int i = 1; i < zoomLevels.length; ++i) {
if (zoomLevels[i] <= zoomLevels[i - 1]) {
throw new IllegalArgumentException("Expecting zoom levels in ascending order");
}
}
}
public int getIntervals() {
return intervals;
}
public int getAbsoluteMin() {
return absoluteMin;
}
public int getAbsoluteMax() {
return absoluteMax;
}
public int[] getZoomLevels() {
return zoomLevels;
} }
/** /**
* Calculates the interval between markings given the min and max values. * Gets the y axis dimension. Returns an array of int[3], containing interval, effectiveMin,
* This function attempts to find the smallest zoom level that fits [min,max] after rounding * and effectiveMax.
* it to the current zoom level. *
* * @param min the min value
* @param min the minimum value in the series * @param max the max value
* @param max the maximum value in the series
* @return the calculated interval for the given range
*/ */
public int calculateInterval(double min, double max) { public int[] getYAxisDimension(double min, double max) {
min = Math.min(min, absoluteMin); min = Math.max(min, absoluteMin);
max = Math.max(max, absoluteMax); max = Math.min(max, absoluteMax);
for (int i = 0; i < zoomLevels.length; ++i) { int interval = 0;
int zoomLevel = zoomLevels[i]; int effectiveMin = 0;
int roundedMin = (int)(min / zoomLevel) * zoomLevel; for (int i = 0; i < intervalValues.length; i++) {
if (roundedMin > min) { interval = intervalValues[i];
roundedMin -= zoomLevel; effectiveMin = getEffetiveMin(min, interval);
} double targetInterval = (max - effectiveMin) / numberOfIntervals;
double interval = (max - roundedMin) / intervals; if (interval >= targetInterval) {
if (zoomLevel >= interval) { break;
return zoomLevel;
} }
} }
return zoomLevels[zoomLevels.length - 1]; int effectiveMax = getEffectiveMax(max, interval);
return new int[] { interval, effectiveMin, effectiveMax };
}
/**
* Gets the effective min value.
*
* @param min the min value
* @param interval the interval
*/
private int getEffetiveMin(double min, int interval) {
int value = (int) (min / interval) * interval;
// value > min if min is negative
if (value > min) {
return value - interval;
}
return value;
}
/**
* Gets the effective max value
*
* @param max the max value
* @param interval the interval
*/
private int getEffectiveMax(double max, int interval) {
return ((int) (max / interval)) * interval + interval;
} }
} }
/** /**
* Constructs a new chart value series. * Constructs a new chart value series.
* *
* @param context The context for the chart * @param context the context
* @param fillColor The paint for filling the chart * @param fillColor the fill color
* @param strokeColor The paint for stroking the outside the chart, optional * @param strokeColor the stroke color
* @param zoomSettings The settings related to zooming * @param yAxisDimension the marking interval
* @param titleId The title ID * @param titleId the title id
*
* TODO: Get rid of Context and inject appropriate values instead.
*/ */
public ChartValueSeries( public ChartValueSeries(Context context, int fillColor, int strokeColor,
Context context, int fillColor, int strokeColor, ZoomSettings zoomSettings, int titleId) { YAxisDimension yAxisDimension, int titleId) {
this.format = NumberFormat.getIntegerInstance(); extremityMonitor = new ExtremityMonitor();
numberFormat = NumberFormat.getIntegerInstance();
path = new Path();
fillPaint = new Paint(); fillPaint = new Paint();
fillPaint.setStyle(Style.FILL); fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor)); fillPaint.setColor(context.getResources().getColor(fillColor));
@@ -151,155 +149,137 @@ public class ChartValueSeries {
strokePaint.setStyle(Style.STROKE); strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor)); strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true); strokePaint.setAntiAlias(true);
// Make a copy of the stroke paint with the default thickness. // Make a copy of the stroke paint with the default thickness
labelPaint = new Paint(strokePaint); labelPaint = new Paint(strokePaint);
strokePaint.setStrokeWidth(2f); strokePaint.setStrokeWidth(STROKE_WIDTH);
} else { } else {
strokePaint = null; strokePaint = null;
labelPaint = fillPaint; labelPaint = fillPaint;
} }
this.zoomSettings = zoomSettings; this.markingInterval = yAxisDimension;
this.title = context.getString(titleId); title = context.getString(titleId);
} }
/** /**
* Draws the path of the chart * Returns true if the series has data.
*/ */
public void drawPath(Canvas c) { public boolean hasData() {
c.drawPath(path, fillPaint); return extremityMonitor.hasData();
if (strokePaint != null) {
c.drawPath(path, strokePaint);
}
} }
/** /**
* Resets this series * Updates the series with a new value.
*
* @param value the new value
*/ */
public void reset() { public void update(double value) {
monitor.reset(); extremityMonitor.update(value);
} }
/** /**
* Updates this series with a new value * Gets the number format for the series.
*/ */
public void update(double d) { public NumberFormat getNumberFormat() {
monitor.update(d); return numberFormat;
} }
/** /**
* @return The interval between markers * Gets the path.
*/ */
public int getInterval() { public Path getPath() {
return interval;
}
/**
* Determines what the min and max of the chart will be.
* This will round down and up the min and max respectively.
*/
public void updateDimension() {
if (monitor.getMax() == Double.NEGATIVE_INFINITY) {
min = 0;
max = 1;
} else {
min = monitor.getMin();
max = monitor.getMax();
}
min = Math.min(min, zoomSettings.getAbsoluteMin());
max = Math.max(max, zoomSettings.getAbsoluteMax());
this.interval = zoomSettings.calculateInterval(min, max);
// Round it up.
effectiveMax = ((int) (max / interval)) * interval + interval;
// Round it down.
effectiveMin = ((int) (min / interval)) * interval;
if (min < 0) {
effectiveMin -= interval;
}
spread = effectiveMax - effectiveMin;
}
/**
* @return The length of the longest string from the series
*/
public int getMaxLabelLength() {
String minS = format.format(effectiveMin);
String maxS = format.format(getMax());
return Math.max(minS.length(), maxS.length());
}
/**
* @return The rounded down minimum value
*/
public int getMin() {
return effectiveMin;
}
/**
* @return The rounded up maximum value
*/
public int getMax() {
return effectiveMax;
}
/**
* @return The difference between the min and max values in the series
*/
public double getSpread() {
return spread;
}
/**
* @return The number format for this series
*/
NumberFormat getFormat() {
return format;
}
/**
* @return The path for this series
*/
Path getPath() {
return path; return path;
} }
/** /**
* @return The paint for this series * Gets the label paint.
*/ */
Paint getPaint() {
return strokePaint == null ? fillPaint : strokePaint;
}
public Paint getLabelPaint() { public Paint getLabelPaint() {
return labelPaint; return labelPaint;
} }
/** /**
* @return The title of the series * Gets the title.
*/ */
public String getTitle() { public String getTitle() {
return title; return title;
} }
/**
* Sets the title.
*
* @param title the title
*/
public void setTitle(String title) { public void setTitle(String title) {
this.title = title; this.title = title;
} }
/** /**
* @return is this series enabled * Returns true if the series is enabled.
*/ */
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return enabled;
} }
/** /**
* Sets the series enabled flag. * Sets the enabled value.
*
* @param enabled true to enable
*/ */
public void setEnabled(boolean enabled) { public void setEnabled(boolean enabled) {
this.enabled = enabled; this.enabled = enabled;
} }
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
if (strokePaint != null) {
canvas.drawPath(path, strokePaint);
}
}
public boolean hasData() { /**
return monitor.hasData(); * Updates the y axis dimension.
*/
public void updateDimension() {
boolean hasData = extremityMonitor.hasData();
double min = hasData ? extremityMonitor.getMin() : 0.0;
double max = hasData ? extremityMonitor.getMax() : 1.0;
int[] dimension = markingInterval.getYAxisDimension(min, max);
interval = dimension[0];
effectiveMin = dimension[1];
effectiveMax = dimension[2];
}
/**
* Gets the y axis interval value.
*/
public int getInterval() {
return interval;
}
/**
* Gets the minimum value.
*/
public int getMin() {
return effectiveMin;
}
/**
* Gets the maximum value.
*/
@VisibleForTesting
int getMax() {
return effectiveMax;
}
/**
* Gets the maximum label length.
*/
public int getMaxLabelLength() {
return Math.max(numberFormat.format(getMin()).length(), numberFormat.format(getMax()).length());
} }
} }
@@ -15,7 +15,7 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.apps.mytracks.ChartValueSeries.YAxisDimension;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.ExtremityMonitor; import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.android.apps.mytracks.util.IntentUtils; import com.google.android.apps.mytracks.util.IntentUtils;
@@ -208,7 +208,7 @@ public class ChartView extends View {
new ChartValueSeries(context, new ChartValueSeries(context,
R.color.elevation_fill, R.color.elevation_fill,
R.color.elevation_border, R.color.elevation_border,
new ZoomSettings(MAX_INTERVALS, new YAxisDimension(MAX_INTERVALS, Integer.MIN_VALUE, Integer.MAX_VALUE,
new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}), new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}),
R.string.stats_elevation); R.string.stats_elevation);
@@ -216,27 +216,27 @@ public class ChartView extends View {
new ChartValueSeries(context, new ChartValueSeries(context,
R.color.speed_fill, R.color.speed_fill,
R.color.speed_border, R.color.speed_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE,
new int[] {1, 5, 10, 20, 50}), new int[] {1, 5, 10, 20, 50}),
R.string.stats_speed); R.string.stats_speed);
series[POWER_SERIES] = series[POWER_SERIES] =
new ChartValueSeries(context, new ChartValueSeries(context,
R.color.power_fill, R.color.power_fill,
R.color.power_border, R.color.power_border,
new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), new YAxisDimension(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}),
R.string.sensor_state_power); R.string.sensor_state_power);
series[CADENCE_SERIES] = series[CADENCE_SERIES] =
new ChartValueSeries(context, new ChartValueSeries(context,
R.color.cadence_fill, R.color.cadence_fill,
R.color.cadence_border, R.color.cadence_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE,
new int[] {5, 10, 25, 50}), new int[] {5, 10, 25, 50}),
R.string.sensor_state_cadence); R.string.sensor_state_cadence);
series[HEART_RATE_SERIES] = series[HEART_RATE_SERIES] =
new ChartValueSeries(context, new ChartValueSeries(context,
R.color.heartrate_fill, R.color.heartrate_fill,
R.color.heartrate_border, R.color.heartrate_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, new YAxisDimension(MAX_INTERVALS, 0, Integer.MAX_VALUE,
new int[] {25, 50}), new int[] {25, 50}),
R.string.sensor_state_heart_rate); R.string.sensor_state_heart_rate);
} }
@@ -819,7 +819,7 @@ public class ChartView extends View {
desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1; desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1;
Paint p = new Paint(cvs.getLabelPaint()); Paint p = new Paint(cvs.getLabelPaint());
p.setTextAlign(Align.RIGHT); p.setTextAlign(Align.RIGHT);
String text = cvs.getFormat().format(y); String text = cvs.getNumberFormat().format(y);
c.drawText(text, x, desiredY, p); c.drawText(text, x, desiredY, p);
return p.measureText(text); return p.measureText(text);
} }
@@ -15,36 +15,33 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.apps.mytracks.ChartValueSeries.YAxisDimension;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.graphics.Paint.Style;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
/** /**
* Tests {@link ChartValueSeries}.
*
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class ChartValueSeriesTest extends AndroidTestCase { public class ChartValueSeriesTest extends AndroidTestCase {
private ChartValueSeries series; private ChartValueSeries series;
@Override @Override
protected void setUp() throws Exception { protected void setUp() throws Exception {
series = new ChartValueSeries(getContext(), YAxisDimension yAxisDimension = new YAxisDimension(
R.color.elevation_fill, 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 100 });
R.color.elevation_border, series = new ChartValueSeries(getContext(), R.color.elevation_fill, R.color.elevation_border,
new ZoomSettings(5, new int[] {100}), yAxisDimension, R.string.stats_elevation);
R.string.stats_elevation);
} }
public void testInitialConditions() { public void testInitialConditions() {
assertEquals(0, series.getInterval()); assertEquals(1, series.getInterval());
assertEquals(1, series.getMaxLabelLength()); assertEquals(1, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(0, series.getMax()); assertEquals(1, series.getMax());
assertEquals(0.0, series.getSpread()); assertEquals(getContext().getString(R.string.stats_elevation), series.getTitle());
assertEquals(Style.STROKE, series.getPaint().getStyle());
assertEquals(getContext().getString(R.string.stats_elevation),
series.getTitle());
assertTrue(series.isEnabled()); assertTrue(series.isEnabled());
} }
@@ -52,7 +49,7 @@ public class ChartValueSeriesTest extends AndroidTestCase {
series.setEnabled(false); series.setEnabled(false);
assertFalse(series.isEnabled()); assertFalse(series.isEnabled());
} }
public void testSmallUpdates() { public void testSmallUpdates() {
series.update(0); series.update(0);
series.update(10); series.update(10);
@@ -61,7 +58,6 @@ public class ChartValueSeriesTest extends AndroidTestCase {
assertEquals(3, series.getMaxLabelLength()); assertEquals(3, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(100, series.getMax()); assertEquals(100, series.getMax());
assertEquals(100.0, series.getSpread());
} }
public void testBigUpdates() { public void testBigUpdates() {
@@ -72,7 +68,6 @@ public class ChartValueSeriesTest extends AndroidTestCase {
assertEquals(5, series.getMaxLabelLength()); assertEquals(5, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(1000, series.getMax()); assertEquals(1000, series.getMax());
assertEquals(1000.0, series.getSpread());
} }
public void testNotZeroBasedUpdates() { public void testNotZeroBasedUpdates() {
@@ -83,56 +78,26 @@ public class ChartValueSeriesTest extends AndroidTestCase {
assertEquals(5, series.getMaxLabelLength()); assertEquals(5, series.getMaxLabelLength());
assertEquals(500, series.getMin()); assertEquals(500, series.getMin());
assertEquals(1500, series.getMax()); assertEquals(1500, series.getMax());
assertEquals(1000.0, series.getSpread());
} }
public void testZoomSettings_invalidArgs() { public void testYAxisDimension_minAligned() {
try { YAxisDimension yAxisDimension = new YAxisDimension(
new ZoomSettings(0, new int[] {10, 50, 100}); 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 10, 50, 100 });
fail("Expected IllegalArgumentException"); assertEquals(10, yAxisDimension.getYAxisDimension(0, 15)[0]);
} catch (IllegalArgumentException e) { assertEquals(10, yAxisDimension.getYAxisDimension(0, 50)[0]);
// OK. assertEquals(50, yAxisDimension.getYAxisDimension(0, 111)[0]);
} assertEquals(50, yAxisDimension.getYAxisDimension(0, 250)[0]);
try { assertEquals(100, yAxisDimension.getYAxisDimension(0, 251)[0]);
new ZoomSettings(1, null); assertEquals(100, yAxisDimension.getYAxisDimension(0, 10000)[0]);
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// OK.
}
try {
new ZoomSettings(1, new int[] {});
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// OK.
}
try {
new ZoomSettings(1, new int[] {1, 3, 2});
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// OK.
}
}
public void testZoomSettings_minAligned() {
ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100});
assertEquals(10, settings.calculateInterval(0, 15));
assertEquals(10, settings.calculateInterval(0, 50));
assertEquals(50, settings.calculateInterval(0, 111));
assertEquals(50, settings.calculateInterval(0, 250));
assertEquals(100, settings.calculateInterval(0, 251));
assertEquals(100, settings.calculateInterval(0, 10000));
} }
public void testZoomSettings_minNotAligned() { public void testYAxisDimension_minNotAligned() {
ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100}); YAxisDimension settings = new YAxisDimension(
assertEquals(50, settings.calculateInterval(5, 55)); 5, Integer.MIN_VALUE, Integer.MAX_VALUE, new int[] { 10, 50, 100 });
assertEquals(10, settings.calculateInterval(10, 60)); assertEquals(50, settings.getYAxisDimension(5, 55)[0]);
assertEquals(50, settings.calculateInterval(7, 250)); assertEquals(10, settings.getYAxisDimension(10, 60)[0]);
assertEquals(100, settings.calculateInterval(7, 257)); assertEquals(50, settings.getYAxisDimension(7, 250)[0]);
assertEquals(100, settings.calculateInterval(11, 10000)); assertEquals(100, settings.getYAxisDimension(7, 257)[0]);
assertEquals(100, settings.getYAxisDimension(11, 10000)[0]);
// A regression test.
settings = new ZoomSettings(5, new int[] {5, 10, 20});
assertEquals(10, settings.calculateInterval(-37.14, -11.89));
} }
} }