Merge from head.

This commit is contained in:
Sandor Dornbush
2010-12-31 09:14:09 -08:00
parent b961356b7b
commit 868c1ef891
7 changed files with 365 additions and 236 deletions
@@ -30,13 +30,15 @@ limitations under the License.
android:orientation="vertical" android:orientation="vertical"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:padding="10dip"
android:layout_weight="0"> android:layout_weight="0">
<ZoomControls android:id="@+id/elevation_zoom" <ZoomControls android:id="@+id/elevation_zoom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" android:layout_gravity="center_horizontal"
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"/> android:layout_centerHorizontal="true"
android:layout_alignParentRight="true"/>
</RelativeLayout> </RelativeLayout>
<LinearLayout android:id="@+id/elevation_busypane" <LinearLayout android:id="@+id/elevation_busypane"
android:orientation="vertical" android:orientation="vertical"
+12 -9
View File
@@ -17,7 +17,6 @@ limitations under the License.
<resources> <resources>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="blue">#FF0000FF</color> <color name="blue">#FF0000FF</color>
<color name="blue_transparent">#400000FF</color>
<color name="darkgray">#FF444444</color> <color name="darkgray">#FF444444</color>
<color name="gold">#FFffd700</color> <color name="gold">#FFffd700</color>
<color name="gray">#FFAAAAAA</color> <color name="gray">#FFAAAAAA</color>
@@ -30,18 +29,22 @@ limitations under the License.
<!-- Chart colors by series. --> <!-- Chart colors by series. -->
<!-- Green and a transparent green --> <!-- Green and a transparent green -->
<color name="elevation_border">#FF00AA00</color> <color name="elevation_border">#FF339933</color>
<color name="elevation_fill">#8000AA00</color> <color name="elevation_fill">#40B3CFB3</color>
<!-- Purple and a transparent purple --> <!-- Blue and a transparent blue -->
<color name="heartrate_border">#FF401465</color> <color name="speed_border">#FF0066FF</color>
<color name="heartrate_fill">#40401465</color> <color name="speed_fill">#40C8E0FF</color>
<!-- Red and a transparent red -->
<color name="heartrate_border">#FFCC3300</color>
<color name="heartrate_fill">#40E88A70</color>
<!-- Pink and a transparent pink --> <!-- Pink and a transparent pink -->
<color name="cadence_border">#FFF128F3</color> <color name="cadence_border">#FFF128F3</color>
<color name="cadence_fill">#40F128F3</color> <color name="cadence_fill">#40F128F3</color>
<!-- Red and a transparent red --> <!-- Purple and a transparent purple -->
<color name="power_border">#FFFF0000</color> <color name="power_border">#FF401465</color>
<color name="power_fill">#40FF0000</color> <color name="power_fill">#40401465</color>
</resources> </resources>
@@ -252,7 +252,7 @@ public class ChartActivity extends Activity implements
setVolumeControlStream(volumeStream); setVolumeControlStream(volumeStream);
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_elevation); setContentView(R.layout.mytracks_charts);
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
cv = new ChartView(this); cv = new ChartView(this);
cv.setMode(this.mode); cv.setMode(this.mode);
@@ -38,16 +38,95 @@ public class ChartValueSeries {
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 int rounding; private final ZoomSettings zoomSettings;
private String title; private String title;
private double min = 0; private double min;
private double max = 1; private double max = 1.0;
private int effectiveMax = 0; private int effectiveMax;
private int absoluteMax = Integer.MIN_VALUE; private int effectiveMin;
private int effectiveMin = 0; private double spread;
private double spread = 0; private int interval;
private boolean enabled = true; private boolean enabled = true;
/**
* This class controls how effective min/max values of a {@link ChartValueSeries} are calculated.
*/
public static class ZoomSettings {
private int intervals;
private final int absoluteMin;
private final int absoluteMax;
private final int[] zoomLevels;
public ZoomSettings(int intervals, int[] zoomLevels) {
this.intervals = intervals;
this.absoluteMin = Integer.MAX_VALUE;
this.absoluteMax = Integer.MIN_VALUE;
this.zoomLevels = zoomLevels;
checkArgs();
}
public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) {
this.intervals = intervals;
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.zoomLevels = zoomLevels;
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.
* This function attempts to find the smallest zoom level that fits [min,max] after rounding
* it to the current zoom level.
*
* @param min the minimum value in the series
* @param max the maximum value in the series
* @return the calculated interval for the given range
*/
public int calculateInterval(double min, double max) {
min = Math.min(min, absoluteMin);
max = Math.max(max, absoluteMax);
for (int i = 0; i < zoomLevels.length; ++i) {
int zoomLevel = zoomLevels[i];
int roundedMin = (int)(min / zoomLevel) * zoomLevel;
if (roundedMin > min) {
roundedMin -= zoomLevel;
}
double interval = (max - roundedMin) / intervals;
if (zoomLevel >= interval) {
return zoomLevel;
}
}
return zoomLevels[zoomLevels.length - 1];
}
}
/** /**
* Constructs a new chart value series. * Constructs a new chart value series.
@@ -56,14 +135,12 @@ public class ChartValueSeries {
* @param formatString The format of the decimal format for this series * @param formatString The format of the decimal format for this series
* @param fill The paint for filling the chart * @param fill The paint for filling the chart
* @param stroke The paint for stroking the outside the chart, optional * @param stroke The paint for stroking the outside the chart, optional
* @param rounding The factor to round the values by * @param zoomSettings The settings related to zooming
*
* TODO: Get rid of Context and inject appropriate values instead.
*/ */
public ChartValueSeries(Context context, public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor,
String formatString, ZoomSettings zoomSettings, int titleId) {
int fillColor,
int strokeColor,
int rounding,
int titleId) {
this.format = new DecimalFormat(formatString); this.format = new DecimalFormat(formatString);
fillPaint = new Paint(); fillPaint = new Paint();
fillPaint.setStyle(Style.FILL); fillPaint.setStyle(Style.FILL);
@@ -81,7 +158,7 @@ public class ChartValueSeries {
strokePaint = null; strokePaint = null;
labelPaint = fillPaint; labelPaint = fillPaint;
} }
this.rounding = rounding; this.zoomSettings = zoomSettings;
this.title = context.getString(titleId); this.title = context.getString(titleId);
} }
@@ -113,20 +190,7 @@ public class ChartValueSeries {
* @return The interval between markers * @return The interval between markers
*/ */
public int getInterval() { public int getInterval() {
// Try to find 5 even looking intervals. return interval;
int interval = (int) (spread / 5);
int minInterval = rounding / 4;
if (interval < minInterval) {
// We won't be able to find 5 even intervals.
return minInterval;
} else if (interval < rounding) {
// The desired interval is less than the rounding value.
// We will have less than 5 intervals.
return rounding;
} else {
// Round the interval.
return (interval / rounding) * rounding;
}
} }
/** /**
@@ -141,14 +205,18 @@ public class ChartValueSeries {
min = monitor.getMin(); min = monitor.getMin();
max = monitor.getMax(); max = monitor.getMax();
} }
min = Math.min(min, zoomSettings.getAbsoluteMin());
max = Math.max(max, zoomSettings.getAbsoluteMax());
this.interval = zoomSettings.calculateInterval(min, max);
// Round it up. // Round it up.
effectiveMax = ((int) (max / rounding)) * rounding + rounding; effectiveMax = ((int) (max / interval)) * interval + interval;
// Round it down. // Round it down.
effectiveMin = ((int) (min / rounding)) * rounding; effectiveMin = ((int) (min / interval)) * interval;
if (min < 0) { if (min < 0) {
effectiveMin -= rounding; effectiveMin -= interval;
} }
spread = getMax() - effectiveMin; spread = effectiveMax - effectiveMin;
} }
/** /**
@@ -168,16 +236,10 @@ public class ChartValueSeries {
} }
/** /**
* @return The minimum of the rounded up max value and the effective max. * @return The rounded up maximum value
*/ */
public int getMax() { public int getMax() {
// Return the effective max if no absolute is set return effectiveMax;
// or the effective max is less than the absolute.
if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) {
return effectiveMax;
} else {
return absoluteMax;
}
} }
/** /**
@@ -205,9 +267,7 @@ public class ChartValueSeries {
* @return The paint for this series * @return The paint for this series
*/ */
Paint getPaint() { Paint getPaint() {
return (strokePaint == null) return strokePaint == null ? fillPaint : strokePaint;
? fillPaint
: strokePaint;
} }
public Paint getLabelPaint() { public Paint getLabelPaint() {
@@ -239,14 +299,6 @@ public class ChartValueSeries {
this.enabled = enabled; this.enabled = enabled;
} }
public int getAbsoluteMax() {
return absoluteMax;
}
public void setAbsoluteMax(int absoluteMax) {
this.absoluteMax = absoluteMax;
}
public boolean hasData() { public boolean hasData() {
return monitor.hasData(); return monitor.hasData();
} }
@@ -15,6 +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.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.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
@@ -25,9 +26,11 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.VelocityTracker; import android.view.VelocityTracker;
@@ -60,8 +63,10 @@ public class ChartView extends View {
*/ */
private int zoomLevel = 1; private int zoomLevel = 1;
private final int minZoomLevel = 1; private final int minZoomLevel = 1;
private int maxZoomLevel = 5; private int maxZoomLevel = 10;
private static final int MAX_INTERVALS = 5;
/* /*
* Borders, margins, dimensions (in pixels): * Borders, margins, dimensions (in pixels):
*/ */
@@ -80,7 +85,7 @@ public class ChartView extends View {
/** /**
* Unscaled bottom border of the chart. * Unscaled bottom border of the chart.
*/ */
private static final float BOTTOM_BORDER = 100; private static final float BOTTOM_BORDER = 40;
/** /**
* Device scaled bottom border of the chart. * Device scaled bottom border of the chart.
@@ -122,6 +127,7 @@ public class ChartView extends View {
private final Paint borderPaint = new Paint(); private final Paint borderPaint = new Paint();
private final Paint labelPaint = new Paint(); private final Paint labelPaint = new Paint();
private final Paint gridPaint = new Paint(); private final Paint gridPaint = new Paint();
private final Paint gridBarPaint = new Paint();
private final Paint clearPaint = new Paint(); private final Paint clearPaint = new Paint();
private final Drawable pointer; private final Drawable pointer;
private final Drawable statsMarker; private final Drawable statsMarker;
@@ -142,8 +148,6 @@ public class ChartView extends View {
private boolean metricUnits = true; private boolean metricUnits = true;
private boolean showPointer = false; private boolean showPointer = false;
private boolean yLabelMask[] = null;
/** Display chart versus distance or time */ /** Display chart versus distance or time */
public enum Mode { public enum Mode {
BY_DISTANCE, BY_TIME BY_DISTANCE, BY_TIME
@@ -168,6 +172,9 @@ public class ChartView extends View {
gridPaint.setColor(context.getResources().getColor(R.color.gray)); gridPaint.setColor(context.getResources().getColor(R.color.gray));
gridPaint.setAntiAlias(false); gridPaint.setAntiAlias(false);
gridBarPaint.set(gridPaint);
gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0));
clearPaint.setStyle(Style.FILL); clearPaint.setStyle(Style.FILL);
clearPaint.setColor(context.getResources().getColor(R.color.white)); clearPaint.setColor(context.getResources().getColor(R.color.white));
clearPaint.setAntiAlias(false); clearPaint.setAntiAlias(false);
@@ -193,43 +200,46 @@ public class ChartView extends View {
public void setUpChartValueSeries(Context context) { public void setUpChartValueSeries(Context context) {
series = new ChartValueSeries[NUM_SERIES]; series = new ChartValueSeries[NUM_SERIES];
// Create the value series // Create the value series.
series[ELEVATION_SERIES] = series[ELEVATION_SERIES] =
new ChartValueSeries(context, new ChartValueSeries(context,
"###,###", "###,###",
R.color.elevation_fill, R.color.elevation_fill,
R.color.elevation_border, R.color.elevation_border,
100, new ZoomSettings(MAX_INTERVALS,
new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}),
R.string.elevation); R.string.elevation);
series[SPEED_SERIES] = series[SPEED_SERIES] =
new ChartValueSeries(context, new ChartValueSeries(context,
"###,###.0", "###,###",
R.color.blue_transparent, R.color.speed_fill,
R.color.blue, R.color.speed_border,
5, new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {5, 10, 20, 50}),
R.string.speed); R.string.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,
5, new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}),
R.string.power); R.string.power);
series[POWER_SERIES].setAbsoluteMax(1500);
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,
5, new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {5, 10, 25, 50}),
R.string.cadence); R.string.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,
5, new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {25, 50}),
R.string.heart_rate); R.string.heart_rate);
} }
@@ -295,7 +305,7 @@ public class ChartView extends View {
data.add(theData); data.add(theData);
addDataPointInternal(theData); addDataPointInternal(theData);
updateDimensions(); updateDimensions();
setupPath(); setUpPath();
} }
private void addDataPointInternal(double[] theData) { private void addDataPointInternal(double[] theData) {
@@ -326,7 +336,7 @@ public class ChartView extends View {
addDataPointInternal(d); addDataPointInternal(d);
} }
updateDimensions(); updateDimensions();
setupPath(); setUpPath();
} }
/** /**
@@ -360,7 +370,7 @@ public class ChartView extends View {
public void zoomIn() { public void zoomIn() {
if (canZoomIn()) { if (canZoomIn()) {
zoomLevel++; zoomLevel++;
setupPath(); setUpPath();
invalidate(); invalidate();
} }
} }
@@ -377,7 +387,7 @@ public class ChartView extends View {
scrollX = effectiveWidth * (zoomLevel - 1); scrollX = effectiveWidth * (zoomLevel - 1);
scrollTo(scrollX, 0); scrollTo(scrollX, 0);
} }
setupPath(); setUpPath();
invalidate(); invalidate();
} }
} }
@@ -543,12 +553,12 @@ public class ChartView extends View {
@Override @Override
protected synchronized void onDraw(Canvas c) { protected synchronized void onDraw(Canvas c) {
if (w != c.getWidth() || h != c.getHeight()) { if (w != c.getWidth() || h != c.getHeight()) {
// Dimensions have changed (for example due to orientation change): // Dimensions have changed (for example due to orientation change).
w = c.getWidth(); w = c.getWidth();
h = c.getHeight(); h = c.getHeight();
effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER);
effectiveHeight = Math.max(0, h - topBorder - bottomBorder); effectiveHeight = Math.max(0, h - topBorder - bottomBorder);
setupPath(); setUpPath();
} }
c.save(); c.save();
c.drawColor(Color.WHITE); c.drawColor(Color.WHITE);
@@ -558,16 +568,21 @@ public class ChartView extends View {
c.restore(); c.restore();
return; return;
} }
c.save();
c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1,
w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1);
// Draw the data series drawGrid(c);
// Draw the data series.
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) { if (cvs.isEnabled() && cvs.hasData()) {
cvs.drawPath(c); cvs.drawPath(c);
} }
} }
drawGrid(c);
// Draw the waypoints.
// Draw the waypoints:
for (int i = 1; i < waypoints.size(); i++) { for (int i = 1; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i); final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) { if (waypoint.getLocation() == null) {
@@ -577,7 +592,7 @@ public class ChartView extends View {
final float x = getWaypointX(waypoint); final float x = getWaypointX(waypoint);
c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint); c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint);
c.translate(x - (markerWidth / 2), markerHeight); c.translate(x - markerWidth / 2, markerHeight);
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statsMarker.draw(c); statsMarker.draw(c);
} else { } else {
@@ -586,24 +601,27 @@ public class ChartView extends View {
c.restore(); c.restore();
} }
// Draw the axis and labels: c.restore();
// Draw the axis and labels.
drawXLabels(c); drawXLabels(c);
drawXAxis(c); drawXAxis(c);
drawSeriesTitles(c); drawSeriesTitles(c);
yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1];
c.translate(getScrollX(), 0); c.translate(getScrollX(), 0);
drawYAxis(c); drawYAxis(c);
final int spacer = 5;
int x = leftBorder - spacer;
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) { if (cvs.isEnabled() && cvs.hasData()) {
drawYLabels(cvs, c); x -= drawYLabels(cvs, c, x) + spacer;
} }
} }
c.restore(); c.restore();
if (showPointer && data.size() > 0) { if (showPointer && !data.isEmpty()) {
c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2,
(getY(series[0], data.get(data.size() - 1)[1]) getY(series[0], data.get(data.size() - 1)[1])
- pointer.getIntrinsicHeight() / 2 - 12)); - pointer.getIntrinsicHeight() / 2 - 12);
pointer.draw(c); pointer.draw(c);
} }
} }
@@ -618,8 +636,8 @@ public class ChartView extends View {
int j = 0; int j = 0;
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) { if (cvs.isEnabled() && cvs.hasData()) {
int y = (int) (w * zoomLevel * ((double) (++j) / sections)); int x = (int) (w * (double) ++j / sections) + getScrollX();
c.drawText(cvs.getTitle(), y, topBorder, cvs.getLabelPaint()); c.drawText(cvs.getTitle(), x, topBorder, cvs.getLabelPaint());
} }
} }
} }
@@ -628,23 +646,30 @@ public class ChartView extends View {
* Sets up the path that is used to draw the histogram in onDraw(). The path * Sets up the path that is used to draw the histogram in onDraw(). The path
* needs to be updated any time after the data or histogram dimensions change. * needs to be updated any time after the data or histogram dimensions change.
*/ */
private synchronized void setupPath() { private synchronized void setUpPath() {
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
cvs.getPath().reset(); cvs.getPath().reset();
} }
if (data.size() < 1) { if (data.isEmpty()) {
return; return;
} }
// All of the data points to the respective series. // All of the data points to the respective series.
for (int i = 0; i < data.size(); i++) { // TODO: Come up with a better sampling than Math.max(1, (maxZoomLevel - zoomLevel + 1) / 2);
int sampling = 1;
for (int i = 0; i < data.size(); i += sampling) {
double[] d = data.get(i); double[] d = data.get(i);
int min = Math.min(series.length, d.length - 1); int min = Math.min(series.length, d.length - 1);
for (int j = 0; j < min; ++j) {
for (int j = 0; j < min; j++) {
ChartValueSeries cvs = series[j]; ChartValueSeries cvs = series[j];
Path path = cvs.getPath(); Path path = cvs.getPath();
path.lineTo(getX(d[0]), getY(cvs, d[j + 1])); int x = getX(d[0]);
int y = getY(cvs, d[j + 1]);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
} }
} }
@@ -693,12 +718,15 @@ public class ChartView extends View {
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
cvs.updateDimension(); cvs.updateDimension();
} }
// TODO: This is totally broken. Make sure that we calculate based on measureText for each
// grid line, as the labels may vary across intervals.
int maxLength = 0; int maxLength = 0;
for (ChartValueSeries cvs : series) { for (ChartValueSeries cvs : series) {
maxLength = Math.max(maxLength, if (cvs.isEnabled() && cvs.hasData()) {
cvs.getMaxLabelLength()); maxLength += cvs.getMaxLabelLength();
}
} }
leftBorder = 4 + 7 * maxLength; leftBorder = 4 + 8 * maxLength;
effectiveWidth = w - leftBorder - RIGHT_BORDER; effectiveWidth = w - leftBorder - RIGHT_BORDER;
float density = getContext().getResources().getDisplayMetrics().density; float density = getContext().getResources().getDisplayMetrics().density;
bottomBorder = (int) (density * BOTTOM_BORDER); bottomBorder = (int) (density * BOTTOM_BORDER);
@@ -716,23 +744,20 @@ public class ChartView extends View {
} }
private void drawXLabels(Canvas c) { private void drawXLabels(Canvas c) {
int numLabels = 0;
double interval = (int) (maxX / zoomLevel / 4); double interval = (int) (maxX / zoomLevel / 4);
boolean shortFormat = false; boolean shortFormat = false;
if (interval < 1) { if (interval < 1) {
interval = .5; interval = .5;
shortFormat = true; shortFormat = true;
} else if (interval < 5) {
interval = 2;
} else if (interval < 10) {
interval = 5;
} else { } else {
if (interval < 5) { interval = (interval / 10) * 10;
interval = 2;
} else if (interval < 10) {
interval = 5;
} else {
interval = (interval / 10) * 10;
}
} }
drawXLabel(c, 0, shortFormat); drawXLabel(c, 0, shortFormat);
numLabels++; int numLabels = 1;
for (int i = 1; i * interval < maxX; i++) { for (int i = 1; i * interval < maxX; i++) {
drawXLabel(c, i * interval, shortFormat); drawXLabel(c, i * interval, shortFormat);
numLabels++; numLabels++;
@@ -742,26 +767,19 @@ public class ChartView extends View {
} }
} }
private void drawYLabels(ChartValueSeries cvs, Canvas c) { private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) {
int numLabels = 0;
drawYLabel(cvs, c, 0);
final int y0 = getY(cvs, 0);
numLabels++;
int interval = cvs.getInterval(); int interval = cvs.getInterval();
for (int i = 0; i * interval < cvs.getSpread(); i++) { float maxTextWidth = 0;
final int y = i * interval + cvs.getMin(); for (int i = 0; i < MAX_INTERVALS; ++i) {
final int yi = getY(cvs, y); maxTextWidth = Math.max(maxTextWidth, drawYLabel(cvs, c, x, i * interval + cvs.getMin()));
if (Math.abs(y0 - yi) > 15) {
drawYLabel(cvs, c, y);
numLabels++;
}
}
if (numLabels < 3) {
drawYLabel(cvs, c, cvs.getMax());
} }
return maxTextWidth;
} }
private void drawXLabel(Canvas c, double x, boolean shortFormat) { private void drawXLabel(Canvas c, double x, boolean shortFormat) {
if (x < 0) {
return;
}
String s = String s =
(mode == Mode.BY_DISTANCE) (mode == Mode.BY_DISTANCE)
? (shortFormat ? xShortFormat.format(x) : xFormat.format(x)) ? (shortFormat ? xShortFormat.format(x) : xFormat.format(x))
@@ -772,72 +790,55 @@ public class ChartView extends View {
labelPaint); labelPaint);
} }
private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) { private float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) {
int desiredY = int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight /
(int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread()); (cvs.getInterval() * MAX_INTERVALS));
desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1;
// Make sure we don't write one label on top of another. Paint p = new Paint(cvs.getLabelPaint());
int slot = desiredY / FONT_HEIGHT; p.setTextAlign(Align.RIGHT);
if (slot < 0) { String text = cvs.getFormat().format(y);
return; c.drawText(text, x, desiredY, p);
} return p.measureText(text);
if (yLabelMask[slot]) {
desiredY = ++slot * FONT_HEIGHT;
}
desiredY = topBorder + effectiveHeight + 5 - desiredY;
if (slot < yLabelMask.length) {
yLabelMask[slot] = true;
}
c.drawText(cvs.getFormat().format(y),
2, desiredY,
cvs.getLabelPaint());
} }
private void drawXAxis(Canvas canvas) { private void drawXAxis(Canvas canvas) {
float rightEdge = getX(maxX); float rightEdge = getX(maxX);
canvas.drawLine(leftBorder, effectiveHeight + topBorder, rightEdge, final int y = effectiveHeight + topBorder;
effectiveHeight + topBorder, borderPaint); canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint);
Context c = getContext(); Context c = getContext();
String s = String s = mode == Mode.BY_DISTANCE
(mode == Mode.BY_DISTANCE) ? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.mile))
? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.min);
: c.getString(R.string.mile)) : (c.getString(R.string.min)); canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint);
canvas.drawText(s,
rightEdge,
effectiveHeight + .2f * UNIT_BORDER + topBorder,
labelPaint);
} }
private void drawYAxis(Canvas canvas) { private void drawYAxis(Canvas canvas) {
canvas.drawRect(0, 0,
leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1,
clearPaint);
canvas.drawLine(leftBorder, UNIT_BORDER + topBorder, canvas.drawLine(leftBorder, UNIT_BORDER + topBorder,
leftBorder, effectiveHeight + topBorder, leftBorder, effectiveHeight + topBorder,
borderPaint); borderPaint);
canvas.drawRect(0, 0, for (int i = 1; i < MAX_INTERVALS; ++i) {
leftBorder - 1, effectiveHeight + topBorder + 1, int y = i * effectiveHeight / MAX_INTERVALS + topBorder;
clearPaint); canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint);
}
Context c = getContext(); Context c = getContext();
// TODO: This should really show units for all series. // TODO: This should really show units for all series.
String s = metricUnits String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet);
? c.getString(R.string.meter) canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint);
: c.getString(R.string.feet);
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f,
UNIT_BORDER * .8f + topBorder, labelPaint);
} }
private synchronized void drawGrid(Canvas c) { private synchronized void drawGrid(Canvas c) {
if (data.size() < 1) { if (data.isEmpty()) {
return; return;
} }
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled()) { float rightEdge = getX(maxX);
int interval = cvs.getInterval(); for (int i = 1; i < MAX_INTERVALS; ++i) {
for (int i = 1; i * interval < cvs.getSpread(); i++) { int y = i * effectiveHeight / MAX_INTERVALS + topBorder;
int y = getY(cvs, i * interval + cvs.getMin()); c.drawLine(leftBorder, y, rightEdge, y, gridBarPaint);
c.drawLine(getX(data.get(0)[0]), y,
getX(data.get(data.size() - 1)[0]), y,
cvs.getPaint());
}
}
} }
} }
@@ -16,9 +16,11 @@
package com.google.android.apps.mytracks.io; package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -266,62 +268,80 @@ public class TrackWriter {
private void writeLocations() { private void writeLocations() {
boolean wroteFirst = false; boolean wroteFirst = false;
boolean segmentOpen = false; boolean segmentOpen = false;
Location lastLoc = null, loc = null;
boolean isLastValid = false; boolean isLastValid = false;
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
if (locationsCursor == null || !locationsCursor.moveToFirst()) { class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Log.w(MyTracksConstants.TAG, "Unable to get any points to write"); Location currentLocation;
return; Location lastLocation;
}
@Override
do { public Location createLocation() {
if (loc == null) loc = new Location(""); if (currentLocation == null) {
providerUtils.fillLocation(locationsCursor, loc); currentLocation = new MyTracksLocation("");
boolean isValid = MyTracksUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(lastLoc);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(lastLoc);
} }
return currentLocation;
// Write the current point }
writer.writeLocation(loc);
} else { public void swapLocations() {
if (segmentOpen) { Location tmpLoc = lastLocation;
writer.writeCloseSegment(); lastLocation = currentLocation;
segmentOpen = false; currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.reset();
} }
} }
};
// Swap loc and lastLoc (so lastLoc is reused)
Location tmp = lastLoc; TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
lastLoc = loc; LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
loc = tmp; locationFactory);
if (loc != null) loc.reset(); try {
if (!it.hasNext()) {
isLastValid = isValid; Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
} while (locationsCursor.moveToNext()); return;
}
if (segmentOpen) { while (it.hasNext()) {
writer.writeCloseSegment(); Location loc = it.next();
segmentOpen = false;
} boolean isValid = MyTracksUtils.isValidLocation(loc);
if (wroteFirst) { boolean validSegment = isValid && isLastValid;
writer.writeEndTrack(lastLoc); if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(locationFactory.lastLocation);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(locationFactory.lastLocation);
}
// Write the current point
writer.writeLocation(loc);
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
}
} finally {
it.close();
} }
} }
} }
@@ -15,6 +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.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.graphics.Paint.Style; import android.graphics.Paint.Style;
@@ -32,12 +33,12 @@ public class ChartValueSeriesTest extends AndroidTestCase {
"###,###", "###,###",
R.color.elevation_fill, R.color.elevation_fill,
R.color.elevation_border, R.color.elevation_border,
100, new ZoomSettings(5, new int[] {100}),
R.string.elevation); R.string.elevation);
} }
public void testInitialConditions() { public void testInitialConditions() {
assertEquals(25, series.getInterval()); assertEquals(0, series.getInterval());
assertEquals(1, series.getMaxLabelLength()); assertEquals(1, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(0, series.getMax()); assertEquals(0, series.getMax());
@@ -57,7 +58,7 @@ public class ChartValueSeriesTest extends AndroidTestCase {
series.update(0); series.update(0);
series.update(10); series.update(10);
series.updateDimension(); series.updateDimension();
assertEquals(25, series.getInterval()); assertEquals(100, series.getInterval());
assertEquals(3, series.getMaxLabelLength()); assertEquals(3, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(100, series.getMax()); assertEquals(100, series.getMax());
@@ -68,7 +69,7 @@ public class ChartValueSeriesTest extends AndroidTestCase {
series.update(0); series.update(0);
series.update(901); series.update(901);
series.updateDimension(); series.updateDimension();
assertEquals(200, series.getInterval()); assertEquals(100, series.getInterval());
assertEquals(5, series.getMaxLabelLength()); assertEquals(5, series.getMaxLabelLength());
assertEquals(0, series.getMin()); assertEquals(0, series.getMin());
assertEquals(1000, series.getMax()); assertEquals(1000, series.getMax());
@@ -79,10 +80,60 @@ public class ChartValueSeriesTest extends AndroidTestCase {
series.update(500); series.update(500);
series.update(1401); series.update(1401);
series.updateDimension(); series.updateDimension();
assertEquals(200, series.getInterval()); assertEquals(100, series.getInterval());
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()); assertEquals(1000.0, series.getSpread());
} }
public void testZoomSettings_invalidArgs() {
try {
new ZoomSettings(0, new int[] {10, 50, 100});
fail("Expected IllegalArgumentException");
} catch (IllegalArgumentException e) {
// OK.
}
try {
new ZoomSettings(1, null);
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() {
ZoomSettings settings = new ZoomSettings(5, new int[] {10, 50, 100});
assertEquals(50, settings.calculateInterval(5, 55));
assertEquals(10, settings.calculateInterval(10, 60));
assertEquals(50, settings.calculateInterval(7, 250));
assertEquals(100, settings.calculateInterval(7, 257));
assertEquals(100, settings.calculateInterval(11, 10000));
// A regression test.
settings = new ZoomSettings(5, new int[] {5, 10, 20});
assertEquals(10, settings.calculateInterval(-37.14, -11.89));
}
} }