From 868c1ef89166573e3c841de15b8996d379a3fcdf Mon Sep 17 00:00:00 2001 From: Sandor Dornbush Date: Fri, 31 Dec 2010 09:14:09 -0800 Subject: [PATCH] Merge from head. --- ...acks_elevation.xml => mytracks_charts.xml} | 4 +- MyTracks/res/values/colors.xml | 21 +- .../android/apps/mytracks/ChartActivity.java | 2 +- .../apps/mytracks/ChartValueSeries.java | 156 ++++++++---- .../android/apps/mytracks/ChartView.java | 235 +++++++++--------- .../android/apps/mytracks/io/TrackWriter.java | 122 +++++---- .../apps/mytracks/ChartValueSeriesTest.java | 61 ++++- 7 files changed, 365 insertions(+), 236 deletions(-) rename MyTracks/res/layout/{mytracks_elevation.xml => mytracks_charts.xml} (94%) diff --git a/MyTracks/res/layout/mytracks_elevation.xml b/MyTracks/res/layout/mytracks_charts.xml similarity index 94% rename from MyTracks/res/layout/mytracks_elevation.xml rename to MyTracks/res/layout/mytracks_charts.xml index 823ee8be1..e758d30bd 100644 --- a/MyTracks/res/layout/mytracks_elevation.xml +++ b/MyTracks/res/layout/mytracks_charts.xml @@ -30,13 +30,15 @@ limitations under the License. android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent" + android:padding="10dip" android:layout_weight="0"> + android:layout_centerHorizontal="true" + android:layout_alignParentRight="true"/> #FF000000 #FF0000FF - #400000FF #FF444444 #FFffd700 #FFAAAAAA @@ -30,18 +29,22 @@ limitations under the License. - #FF00AA00 - #8000AA00 + #FF339933 + #40B3CFB3 - - #FF401465 - #40401465 + + #FF0066FF + #40C8E0FF + + + #FFCC3300 + #40E88A70 #FFF128F3 #40F128F3 - - #FFFF0000 - #40FF0000 + + #FF401465 + #40401465 diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 121615121..25757e541 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -252,7 +252,7 @@ public class ChartActivity extends Activity implements setVolumeControlStream(volumeStream); requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.mytracks_elevation); + setContentView(R.layout.mytracks_charts); ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); cv = new ChartView(this); cv.setMode(this.mode); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index 31a196bee..57b88125d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -38,16 +38,95 @@ public class ChartValueSeries { private final Paint fillPaint; private final Paint strokePaint; private final Paint labelPaint; - private final int rounding; + private final ZoomSettings zoomSettings; private String title; - private double min = 0; - private double max = 1; - private int effectiveMax = 0; - private int absoluteMax = Integer.MIN_VALUE; - private int effectiveMin = 0; - private double spread = 0; + private double min; + private double max = 1.0; + private int effectiveMax; + private int effectiveMin; + private double spread; + private int interval; 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. @@ -56,14 +135,12 @@ public class ChartValueSeries { * @param formatString The format of the decimal format for this series * @param fill The paint for filling the chart * @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, - String formatString, - int fillColor, - int strokeColor, - int rounding, - int titleId) { + public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor, + ZoomSettings zoomSettings, int titleId) { this.format = new DecimalFormat(formatString); fillPaint = new Paint(); fillPaint.setStyle(Style.FILL); @@ -81,7 +158,7 @@ public class ChartValueSeries { strokePaint = null; labelPaint = fillPaint; } - this.rounding = rounding; + this.zoomSettings = zoomSettings; this.title = context.getString(titleId); } @@ -113,20 +190,7 @@ public class ChartValueSeries { * @return The interval between markers */ public int getInterval() { - // Try to find 5 even looking intervals. - 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; - } + return interval; } /** @@ -141,14 +205,18 @@ public class ChartValueSeries { 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 / rounding)) * rounding + rounding; + effectiveMax = ((int) (max / interval)) * interval + interval; // Round it down. - effectiveMin = ((int) (min / rounding)) * rounding; + effectiveMin = ((int) (min / interval)) * interval; 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() { - // Return the effective max if no absolute is set - // or the effective max is less than the absolute. - if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) { - return effectiveMax; - } else { - return absoluteMax; - } + return effectiveMax; } /** @@ -205,9 +267,7 @@ public class ChartValueSeries { * @return The paint for this series */ Paint getPaint() { - return (strokePaint == null) - ? fillPaint - : strokePaint; + return strokePaint == null ? fillPaint : strokePaint; } public Paint getLabelPaint() { @@ -239,14 +299,6 @@ public class ChartValueSeries { this.enabled = enabled; } - public int getAbsoluteMax() { - return absoluteMax; - } - - public void setAbsoluteMax(int absoluteMax) { - this.absoluteMax = absoluteMax; - } - public boolean hasData() { return monitor.hasData(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index f72a521fd..948b5439d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -15,6 +15,7 @@ */ 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.stats.ExtremityMonitor; import com.google.android.apps.mytracks.util.StringUtils; @@ -25,9 +26,11 @@ import android.content.Context; import android.content.Intent; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.DashPathEffect; import android.graphics.Paint; -import android.graphics.Paint.Style; import android.graphics.Path; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; import android.graphics.drawable.Drawable; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -60,8 +63,10 @@ public class ChartView extends View { */ private int zoomLevel = 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): */ @@ -80,7 +85,7 @@ public class ChartView extends View { /** * 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. @@ -122,6 +127,7 @@ public class ChartView extends View { private final Paint borderPaint = new Paint(); private final Paint labelPaint = new Paint(); private final Paint gridPaint = new Paint(); + private final Paint gridBarPaint = new Paint(); private final Paint clearPaint = new Paint(); private final Drawable pointer; private final Drawable statsMarker; @@ -142,8 +148,6 @@ public class ChartView extends View { private boolean metricUnits = true; private boolean showPointer = false; - private boolean yLabelMask[] = null; - /** Display chart versus distance or time */ public enum Mode { BY_DISTANCE, BY_TIME @@ -168,6 +172,9 @@ public class ChartView extends View { gridPaint.setColor(context.getResources().getColor(R.color.gray)); gridPaint.setAntiAlias(false); + gridBarPaint.set(gridPaint); + gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0)); + clearPaint.setStyle(Style.FILL); clearPaint.setColor(context.getResources().getColor(R.color.white)); clearPaint.setAntiAlias(false); @@ -193,43 +200,46 @@ public class ChartView extends View { public void setUpChartValueSeries(Context context) { series = new ChartValueSeries[NUM_SERIES]; - // Create the value series + // Create the value series. series[ELEVATION_SERIES] = new ChartValueSeries(context, "###,###", R.color.elevation_fill, R.color.elevation_border, - 100, + new ZoomSettings(MAX_INTERVALS, + new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}), R.string.elevation); series[SPEED_SERIES] = new ChartValueSeries(context, - "###,###.0", - R.color.blue_transparent, - R.color.blue, - 5, + "###,###", + R.color.speed_fill, + R.color.speed_border, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {5, 10, 20, 50}), R.string.speed); series[POWER_SERIES] = new ChartValueSeries(context, "###,###", R.color.power_fill, R.color.power_border, - 5, + new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}), R.string.power); - series[POWER_SERIES].setAbsoluteMax(1500); series[CADENCE_SERIES] = new ChartValueSeries(context, "###,###", R.color.cadence_fill, R.color.cadence_border, - 5, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {5, 10, 25, 50}), R.string.cadence); series[HEART_RATE_SERIES] = new ChartValueSeries(context, "###,###", R.color.heartrate_fill, R.color.heartrate_border, - 5, + new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE, + new int[] {25, 50}), R.string.heart_rate); } @@ -295,7 +305,7 @@ public class ChartView extends View { data.add(theData); addDataPointInternal(theData); updateDimensions(); - setupPath(); + setUpPath(); } private void addDataPointInternal(double[] theData) { @@ -326,7 +336,7 @@ public class ChartView extends View { addDataPointInternal(d); } updateDimensions(); - setupPath(); + setUpPath(); } /** @@ -360,7 +370,7 @@ public class ChartView extends View { public void zoomIn() { if (canZoomIn()) { zoomLevel++; - setupPath(); + setUpPath(); invalidate(); } } @@ -377,7 +387,7 @@ public class ChartView extends View { scrollX = effectiveWidth * (zoomLevel - 1); scrollTo(scrollX, 0); } - setupPath(); + setUpPath(); invalidate(); } } @@ -543,12 +553,12 @@ public class ChartView extends View { @Override protected synchronized void onDraw(Canvas c) { 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(); h = c.getHeight(); effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); effectiveHeight = Math.max(0, h - topBorder - bottomBorder); - setupPath(); + setUpPath(); } c.save(); c.drawColor(Color.WHITE); @@ -558,16 +568,21 @@ public class ChartView extends View { c.restore(); 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) { if (cvs.isEnabled() && cvs.hasData()) { cvs.drawPath(c); } } - drawGrid(c); - - // Draw the waypoints: + + // Draw the waypoints. for (int i = 1; i < waypoints.size(); i++) { final Waypoint waypoint = waypoints.get(i); if (waypoint.getLocation() == null) { @@ -577,7 +592,7 @@ public class ChartView extends View { final float x = getWaypointX(waypoint); 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) { statsMarker.draw(c); } else { @@ -586,24 +601,27 @@ public class ChartView extends View { c.restore(); } - // Draw the axis and labels: + c.restore(); + + // Draw the axis and labels. drawXLabels(c); drawXAxis(c); drawSeriesTitles(c); - - yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1]; + c.translate(getScrollX(), 0); drawYAxis(c); + final int spacer = 5; + int x = leftBorder - spacer; for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { - drawYLabels(cvs, c); + x -= drawYLabels(cvs, c, x) + spacer; } } c.restore(); - if (showPointer && data.size() > 0) { + if (showPointer && !data.isEmpty()) { c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2, - (getY(series[0], data.get(data.size() - 1)[1]) - - pointer.getIntrinsicHeight() / 2 - 12)); + getY(series[0], data.get(data.size() - 1)[1]) + - pointer.getIntrinsicHeight() / 2 - 12); pointer.draw(c); } } @@ -618,8 +636,8 @@ public class ChartView extends View { int j = 0; for (ChartValueSeries cvs : series) { if (cvs.isEnabled() && cvs.hasData()) { - int y = (int) (w * zoomLevel * ((double) (++j) / sections)); - c.drawText(cvs.getTitle(), y, topBorder, cvs.getLabelPaint()); + int x = (int) (w * (double) ++j / sections) + getScrollX(); + 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 * 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) { cvs.getPath().reset(); } - if (data.size() < 1) { + if (data.isEmpty()) { return; } // 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); 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]; 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) { 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; for (ChartValueSeries cvs : series) { - maxLength = Math.max(maxLength, - cvs.getMaxLabelLength()); + if (cvs.isEnabled() && cvs.hasData()) { + maxLength += cvs.getMaxLabelLength(); + } } - leftBorder = 4 + 7 * maxLength; + leftBorder = 4 + 8 * maxLength; effectiveWidth = w - leftBorder - RIGHT_BORDER; float density = getContext().getResources().getDisplayMetrics().density; bottomBorder = (int) (density * BOTTOM_BORDER); @@ -716,23 +744,20 @@ public class ChartView extends View { } private void drawXLabels(Canvas c) { - int numLabels = 0; double interval = (int) (maxX / zoomLevel / 4); boolean shortFormat = false; if (interval < 1) { interval = .5; shortFormat = true; + } else if (interval < 5) { + interval = 2; + } else if (interval < 10) { + interval = 5; } else { - if (interval < 5) { - interval = 2; - } else if (interval < 10) { - interval = 5; - } else { - interval = (interval / 10) * 10; - } + interval = (interval / 10) * 10; } drawXLabel(c, 0, shortFormat); - numLabels++; + int numLabels = 1; for (int i = 1; i * interval < maxX; i++) { drawXLabel(c, i * interval, shortFormat); numLabels++; @@ -742,26 +767,19 @@ public class ChartView extends View { } } - private void drawYLabels(ChartValueSeries cvs, Canvas c) { - int numLabels = 0; - drawYLabel(cvs, c, 0); - final int y0 = getY(cvs, 0); - numLabels++; + private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) { int interval = cvs.getInterval(); - for (int i = 0; i * interval < cvs.getSpread(); i++) { - final int y = i * interval + cvs.getMin(); - final int yi = getY(cvs, y); - if (Math.abs(y0 - yi) > 15) { - drawYLabel(cvs, c, y); - numLabels++; - } - } - if (numLabels < 3) { - drawYLabel(cvs, c, cvs.getMax()); + float maxTextWidth = 0; + for (int i = 0; i < MAX_INTERVALS; ++i) { + maxTextWidth = Math.max(maxTextWidth, drawYLabel(cvs, c, x, i * interval + cvs.getMin())); } + return maxTextWidth; } private void drawXLabel(Canvas c, double x, boolean shortFormat) { + if (x < 0) { + return; + } String s = (mode == Mode.BY_DISTANCE) ? (shortFormat ? xShortFormat.format(x) : xFormat.format(x)) @@ -772,72 +790,55 @@ public class ChartView extends View { labelPaint); } - private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) { - int desiredY = - (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread()); - - // Make sure we don't write one label on top of another. - int slot = desiredY / FONT_HEIGHT; - if (slot < 0) { - return; - } - 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 float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) { + int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight / + (cvs.getInterval() * MAX_INTERVALS)); + desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1; + Paint p = new Paint(cvs.getLabelPaint()); + p.setTextAlign(Align.RIGHT); + String text = cvs.getFormat().format(y); + c.drawText(text, x, desiredY, p); + return p.measureText(text); } private void drawXAxis(Canvas canvas) { float rightEdge = getX(maxX); - canvas.drawLine(leftBorder, effectiveHeight + topBorder, rightEdge, - effectiveHeight + topBorder, borderPaint); + final int y = effectiveHeight + topBorder; + canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint); Context c = getContext(); - String s = - (mode == Mode.BY_DISTANCE) - ? (metricUnits ? c.getString(R.string.kilometer) - : c.getString(R.string.mile)) : (c.getString(R.string.min)); - canvas.drawText(s, - rightEdge, - effectiveHeight + .2f * UNIT_BORDER + topBorder, - labelPaint); + String s = mode == Mode.BY_DISTANCE + ? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.mile)) + : c.getString(R.string.min); + canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint); } private void drawYAxis(Canvas canvas) { + canvas.drawRect(0, 0, + leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1, + clearPaint); canvas.drawLine(leftBorder, UNIT_BORDER + topBorder, leftBorder, effectiveHeight + topBorder, borderPaint); - canvas.drawRect(0, 0, - leftBorder - 1, effectiveHeight + topBorder + 1, - clearPaint); + for (int i = 1; i < MAX_INTERVALS; ++i) { + int y = i * effectiveHeight / MAX_INTERVALS + topBorder; + canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint); + } + Context c = getContext(); // TODO: This should really show units for all series. - String s = metricUnits - ? c.getString(R.string.meter) - : c.getString(R.string.feet); - canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, - UNIT_BORDER * .8f + topBorder, labelPaint); + String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet); + canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint); } - + private synchronized void drawGrid(Canvas c) { - if (data.size() < 1) { + if (data.isEmpty()) { return; } - for (ChartValueSeries cvs : series) { - if (cvs.isEnabled()) { - int interval = cvs.getInterval(); - for (int i = 1; i * interval < cvs.getSpread(); i++) { - int y = getY(cvs, i * interval + cvs.getMin()); - c.drawLine(getX(data.get(0)[0]), y, - getX(data.get(data.size() - 1)[0]), y, - cvs.getPaint()); - } - } + + float rightEdge = getX(maxX); + for (int i = 1; i < MAX_INTERVALS; ++i) { + int y = i * effectiveHeight / MAX_INTERVALS + topBorder; + c.drawLine(leftBorder, y, rightEdge, y, gridBarPaint); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java index cb49c4a51..2f9555023 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java @@ -16,9 +16,11 @@ package com.google.android.apps.mytracks.io; 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.Track; 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.MyTracksUtils; import com.google.android.maps.mytracks.R; @@ -266,62 +268,80 @@ public class TrackWriter { private void writeLocations() { boolean wroteFirst = false; boolean segmentOpen = false; - Location lastLoc = null, loc = null; boolean isLastValid = false; - Cursor locationsCursor = - providerUtils.getLocationsCursor(track.getId(), 0, -1, false); - if (locationsCursor == null || !locationsCursor.moveToFirst()) { - Log.w(MyTracksConstants.TAG, "Unable to get any points to write"); - return; - } - - do { - if (loc == null) loc = new Location(""); - providerUtils.fillLocation(locationsCursor, loc); - - 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); + class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory { + Location currentLocation; + Location lastLocation; + + @Override + public Location createLocation() { + if (currentLocation == null) { + currentLocation = new MyTracksLocation(""); } - - // Write the current point - writer.writeLocation(loc); - } else { - if (segmentOpen) { - writer.writeCloseSegment(); - segmentOpen = false; + return currentLocation; + } + + public void swapLocations() { + Location tmpLoc = lastLocation; + lastLocation = currentLocation; + currentLocation = tmpLoc; + if (currentLocation != null) { + currentLocation.reset(); } } - - // Swap loc and lastLoc (so lastLoc is reused) - Location tmp = lastLoc; - lastLoc = loc; - loc = tmp; - if (loc != null) loc.reset(); - - isLastValid = isValid; - } while (locationsCursor.moveToNext()); - - if (segmentOpen) { - writer.writeCloseSegment(); - segmentOpen = false; - } - if (wroteFirst) { - writer.writeEndTrack(lastLoc); + }; + + TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory(); + LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false, + locationFactory); + try { + if (!it.hasNext()) { + Log.w(MyTracksConstants.TAG, "Unable to get any points to write"); + return; + } + while (it.hasNext()) { + Location loc = it.next(); + + boolean isValid = MyTracksUtils.isValidLocation(loc); + boolean validSegment = isValid && isLastValid; + 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(); } } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java index e1ff590aa..491f181bc 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java @@ -15,6 +15,7 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.ChartValueSeries.ZoomSettings; import com.google.android.maps.mytracks.R; import android.graphics.Paint.Style; @@ -32,12 +33,12 @@ public class ChartValueSeriesTest extends AndroidTestCase { "###,###", R.color.elevation_fill, R.color.elevation_border, - 100, + new ZoomSettings(5, new int[] {100}), R.string.elevation); } public void testInitialConditions() { - assertEquals(25, series.getInterval()); + assertEquals(0, series.getInterval()); assertEquals(1, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(0, series.getMax()); @@ -57,7 +58,7 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(0); series.update(10); series.updateDimension(); - assertEquals(25, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(3, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(100, series.getMax()); @@ -68,7 +69,7 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(0); series.update(901); series.updateDimension(); - assertEquals(200, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(5, series.getMaxLabelLength()); assertEquals(0, series.getMin()); assertEquals(1000, series.getMax()); @@ -79,10 +80,60 @@ public class ChartValueSeriesTest extends AndroidTestCase { series.update(500); series.update(1401); series.updateDimension(); - assertEquals(200, series.getInterval()); + assertEquals(100, series.getInterval()); assertEquals(5, series.getMaxLabelLength()); assertEquals(500, series.getMin()); assertEquals(1500, series.getMax()); 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)); + } }