/*
* Copyright (c) 2013, Will Szumski
* Copyright (c) 2013, Doug Szumski
*
* This file is part of Cyclismo.
*
* Cyclismo is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cyclismo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cyclismo. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright 2008 Google Inc.
*
* 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 org.cowboycoders.cyclismo;
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.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
import com.google.common.annotations.VisibleForTesting;
import org.cowboycoders.cyclismo.content.Waypoint;
import org.cowboycoders.cyclismo.stats.ExtremityMonitor;
import org.cowboycoders.cyclismo.util.IntentUtils;
import org.cowboycoders.cyclismo.util.StringUtils;
import org.cowboycoders.cyclismo.util.UnitConversions;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Visualization of the chart.
*
* @author Sandor Dornbush
* @author Leif Hendrik Wilden
*/
public class AltitudeProfileView extends View {
public static final String TAG = AltitudeProfileView.class.getSimpleName();
public static final float SMALL_TEXT_SIZE = 12f;
public static final int Y_AXIS_INTERVALS = 5;
public static final int NUM_SERIES = 1;
public static final int ELEVATION_SERIES = 0;
private static final int TARGET_X_AXIS_INTERVALS = 4;
private static final int MIN_ZOOM_LEVEL = 1;
private static final int MAX_ZOOM_LEVEL = 10;
private static final NumberFormat X_NUMBER_FORMAT = NumberFormat.getIntegerInstance();
private static final NumberFormat X_FRACTION_FORMAT = NumberFormat.getNumberInstance();
static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1);
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private static final int BORDER = 8;
private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16;
private final ChartValueSeries[] series = new ChartValueSeries[NUM_SERIES];
private final ChartValueSeries[] seriesOverlay = new ChartValueSeries[NUM_SERIES];
private final ArrayList<double[]> chartData = new ArrayList<double[]>();
private final ArrayList<double[]> chartDataOverlay = new ArrayList<double[]>();
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final ExtremityMonitor xExtremityMonitorOverlay = new ExtremityMonitor();
private double maxX = 1.0;
private final Paint axisPaint;
private final Paint xAxisMarkerPaint;
private final Paint gridPaint;
private final Paint markerPaint;
private final Drawable pointer;
private final Drawable statisticsMarker;
private final Drawable waypointMarker;
private final int markerWidth;
private final int markerHeight;
private final Scroller scroller;
private VelocityTracker velocityTracker = null;
private float lastMotionEventX = -1;
private int zoomLevel = 1;
private int leftBorder = BORDER;
private int topBorder = BORDER;
private int bottomBorder = BORDER;
private int rightBorder = BORDER;
private int spacer = SPACER;
private int yAxisOffset = Y_AXIS_OFFSET;
private int width = 0;
private int height = 0;
private int effectiveWidth = 0;
private int effectiveHeight = 0;
private boolean chartByDistance = true;
private boolean metricUnits = true;
private ArrayList<double[]> dataPoints;
/**
* Constructor.
*
* @param context the context
*/
public AltitudeProfileView(Context context) {
super(context);
initSeries(series);
initSeries(seriesOverlay);
//FIXME: this a workaround for the paths becoming too large at high zoom levels
// see: http://code.google.com/p/osmdroid/issues/detail?id=454,
// http://stackoverflow.com/questions/15039829/drawing-paths-and-hardware-acceleration
this.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
for(ChartValueSeries c : seriesOverlay) {
c.setEnabled(false);
}
float scale = context.getResources().getDisplayMetrics().density;
axisPaint = new Paint();
axisPaint.setStyle(Style.STROKE);
axisPaint.setColor(context.getResources().getColor(R.color.black));
axisPaint.setAntiAlias(true);
axisPaint.setTextSize(SMALL_TEXT_SIZE * scale);
axisPaint.setStyle(Style.FILL_AND_STROKE);
xAxisMarkerPaint = new Paint(axisPaint);
xAxisMarkerPaint.setTextAlign(Align.CENTER);
xAxisMarkerPaint.setStyle(Style.FILL_AND_STROKE);
gridPaint = new Paint();
gridPaint.setStyle(Style.STROKE);
gridPaint.setColor(context.getResources().getColor(R.color.gray));
gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[] { 3, 2 }, 0));
markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE);
markerPaint.setColor(context.getResources().getColor(R.color.gray));
markerPaint.setAntiAlias(false);
pointer = context.getResources().getDrawable(R.drawable.location_marker);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
statisticsMarker = getResources().getDrawable(R.drawable.yellow_pushpin);
markerWidth = statisticsMarker.getIntrinsicWidth();
markerHeight = statisticsMarker.getIntrinsicHeight();
statisticsMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = getResources().getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
setFocusable(true);
setClickable(true);
updateDimensions();
}
private void initSeries(ChartValueSeries[] seriesIn) {
Context context = getContext();
seriesIn[ELEVATION_SERIES] = new ChartValueSeries(context,
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[] { 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000 },
R.string.description_elevation_metric,
R.string.description_elevation_imperial,
R.color.elevation_border,
R.color.elevation_border);
}
/**
* Sets the enabled value for a chart value series.
*
* @param index the chart value series index
*/
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
}
/**
* Sets the enabled value for a chart value series.
*
* @param index the chart value series index
*/
public void setOverlayChartValueSeriesEnabled(int index, boolean enabled) {
seriesOverlay[index].setEnabled(enabled);
}
/**
* Sets chart by distance. It is expected that after changing this value, data
* will be reloaded.
*
* @param value true for by distance, false for by time
*/
public void setChartByDistance(boolean value) {
chartByDistance = value;
}
/**
* Sets metric units.
*
* @param value true to use metric units
*/
public void setMetricUnits(boolean value) {
metricUnits = value;
}
/**
* Adds data points ( for current location marker)
*
* @param dataPoints an array of data points to be added
*/
public void addDataPoints(ArrayList<double[]> dataPoints) {
synchronized (chartData) {
chartData.addAll(dataPoints);
for (int i = 0; i < dataPoints.size(); i++) {
double[] dataPoint = dataPoints.get(i);
xExtremityMonitor.update(dataPoint[0]);
Log.d(TAG, "x axis val: " + dataPoint[0]);
for (int j = 0; j < series.length; j++) {
if (!Double.isNaN(dataPoint[j+1])) {
series[j].update(dataPoint[j + 1]);
Log.d(TAG, "elevation: " + dataPoint[j+1]);
}
}
}
updateDimensions();
//updatePaths(series, chartData);
}
}
public void addAltitudeData(ArrayList<double[]> dataPoints) {
this.dataPoints = dataPoints;
synchronized (chartDataOverlay) {
Log.v(TAG, "adding overlay");
chartDataOverlay.clear();
chartDataOverlay.addAll(dataPoints);
xExtremityMonitorOverlay.reset();
for (int i = 0; i < dataPoints.size(); i++) {
double[] dataPoint = dataPoints.get(i);
xExtremityMonitorOverlay.update(dataPoint[0]);
for (int j = 0; j < seriesOverlay.length; j++) {
if (!Double.isNaN(dataPoint[j + 1])) {
seriesOverlay[j].update(dataPoint[j + 1]);
}
}
}
updateDimensions();
updatePaths(seriesOverlay,chartDataOverlay);
}
}
/**
* Clears all data.
*/
public void reset() {
Log.d(TAG, "calling reset");
synchronized (chartData) {
chartData.clear();
xExtremityMonitor.reset();
zoomLevel = 1;
updateDimensions();
}
forceRedraw();
}
private void forceRedraw() {
// this is to make sure the course data is redrawn,
// there may be a better way
this.post(new Runnable() {
@Override
public void run() {
updateAllPaths();
invalidate();
}
});
}
/**
* Resets scroll. To be called on the UI thread.
*/
public void resetScroll() {
scrollTo(0, 0);
}
/**
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Returns true if can zoom in.
*/
public boolean canZoomIn() {
return zoomLevel < MAX_ZOOM_LEVEL;
}
/**
* Returns true if can zoom out.
*/
public boolean canZoomOut() {
return zoomLevel > MIN_ZOOM_LEVEL;
}
/**
* Zooms in one level.
*/
public void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
Log.d(TAG, "zoomLevel:" + zoomLevel);
updateAllPaths();
invalidate();
}
}
private void updateAllPaths() {
updatePaths(seriesOverlay,chartDataOverlay);
}
/**
* Zooms out one level.
*/
public void zoomOut() {
if (canZoomOut()) {
zoomLevel--;
scroller.abortAnimation();
int scrollX = getScrollX();
int maxWidth = effectiveWidth * (zoomLevel - 1);
if (scrollX > maxWidth) {
scrollX = maxWidth;
scrollTo(scrollX, 0);
}
updateAllPaths();
invalidate();
}
}
/**
* Initiates flinging.
*
* @param velocityX velocity of fling in pixels per second
*/
public void fling(int velocityX) {
int maxWidth = effectiveWidth * (zoomLevel - 1);
scroller.fling(getScrollX(), 0, velocityX, 0, 0, maxWidth, 0, 0);
invalidate();
}
/**
* Scrolls the view horizontally by a given amount.
*
* @param deltaX the number of pixels to scroll
*/
public void scrollBy(int deltaX) {
int scrollX = getScrollX() + deltaX;
if (scrollX < 0) {
scrollX = 0;
}
int maxWidth = effectiveWidth * (zoomLevel - 1);
if (scrollX > maxWidth) {
scrollX = maxWidth;
}
scrollTo(scrollX, 0);
}
/**
* Called by the parent to indicate that the mScrollX/Y values need to be
* updated. Triggers a redraw during flinging.
*/
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
int oldX = getScrollX();
int x = scroller.getCurrX();
scrollTo(x, 0);
if (oldX != x) {
onScrollChanged(x, 0, oldX, 0);
postInvalidate();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
float x = event.getX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// Stop the fling
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
lastMotionEventX = x;
break;
case MotionEvent.ACTION_MOVE:
if (lastMotionEventX == -1) {
break;
}
// Scroll to follow the motion event
int deltaX = (int) (lastMotionEventX - x);
lastMotionEventX = x;
if (deltaX < 0) {
if (getScrollX() > 0) {
scrollBy(deltaX);
}
} else if (deltaX > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX));
}
}
break;
case MotionEvent.ACTION_UP:
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
Waypoint waypoint = waypoints.get(i);
int distance = Math.abs(
getX(getWaypointXValue(waypoint)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestWaypoint = waypoint;
}
}
}
if (nearestWaypoint != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
VelocityTracker myVelocityTracker = velocityTracker;
myVelocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) myVelocityTracker.getXVelocity();
if (Math.abs(initialVelocity) > ViewConfiguration.getMinimumFlingVelocity()) {
fling(-initialVelocity);
}
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged(
MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
synchronized (chartDataOverlay) {
canvas.save();
canvas.drawColor(Color.WHITE);
canvas.save();
clipToGraphArea(canvas);
drawDataSeries(canvas, seriesOverlay);
drawWaypoints(canvas);
//drawGrid(canvas);
canvas.restore();
drawXAxis(canvas);
drawYAxis(canvas, seriesOverlay);
canvas.restore();
drawPointer(canvas);
}
}
/**
* Clips a canvas to the graph area.
*
* @param canvas the canvas
*/
private void clipToGraphArea(Canvas canvas) {
int x = getScrollX() + leftBorder;
int y = topBorder;
canvas.clipRect(x, y, x + effectiveWidth, y + effectiveHeight);
}
/**
* Draws the data series.
*
* @param canvas the canvas
*/
private void drawDataSeries(Canvas canvas, ChartValueSeries [] seriesIn) {
for (ChartValueSeries chartValueSeries : seriesIn) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
chartValueSeries.drawPath(canvas);
}
}
}
/**
* Draws the waypoints.
*
* @param canvas the canvas
*/
private void drawWaypoints(Canvas canvas) {
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
double xValue = getWaypointXValue(waypoint);
if (xValue > maxX) {
continue;
}
canvas.save();
float x = getX(getWaypointXValue(waypoint));
canvas.drawLine(
x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(
x - (float) (markerWidth * MapOverlay.WAYPOINT_X_ANCHOR), topBorder + spacer);
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statisticsMarker.draw(canvas);
} else {
waypointMarker.draw(canvas);
}
canvas.restore();
}
}
}
/**
* Draws the grid.
*
* @param canvas the canvas
*/
private void drawGrid(Canvas canvas) {
// X axis grid
ArrayList<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (int i = 0; i < xAxisMarkerPositions.size(); i++) {
int x = getX(xAxisMarkerPositions.get(i));
canvas.drawLine(x, topBorder, x, topBorder + effectiveHeight, gridPaint);
}
// Y axis grid
float rightEdge = getX(maxX);
for (int i = 0; i <= Y_AXIS_INTERVALS; i++) {
double percentage = (double) i / Y_AXIS_INTERVALS;
int range = effectiveHeight - 2 * yAxisOffset;
int y = topBorder + yAxisOffset + (int) (percentage * range);
canvas.drawLine(leftBorder, y, rightEdge, y, gridPaint);
}
}
/**
* Draws the x axis.
*
* @param canvas the canvas
*/
private void drawXAxis(Canvas canvas) {
int x = getScrollX() + leftBorder;
int y = topBorder + effectiveHeight;
canvas.drawLine(x, y, x + effectiveWidth, y, axisPaint);
String label = getXAxisLabel();
Rect rect = getRect(axisPaint, label);
canvas.drawText(label, x + effectiveWidth + spacer, y + ((int) rect.height() / 2), axisPaint);
double interval = getXAxisInterval();
ArrayList<Double> markerPositions = getXAxisMarkerPositions(interval);
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT;
for (int i = 0; i < markerPositions.size(); i++) {
drawXAxisMarker(canvas, markerPositions.get(i), numberFormat);
}
}
// returns the
private double getXMid() {
//based on marker positions, so that it shares the same logic
double interval = getXAxisInterval();
ArrayList<Double> markerPositions = getXAxisMarkerPositions(interval);
return markerPositions.get(markerPositions.size() /2 );
}
/**
* Gets the x axis label.
*/
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return metricUnits ? context.getString(R.string.unit_kilometer)
: context.getString(R.string.unit_mile);
} else {
return context.getString(R.string.description_time);
}
}
/**
* Draws a x axis marker.
*
* @param canvas
* @param value value
* @param numberFormat the number format
*/
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat) {
String marker = chartByDistance ? numberFormat.format(value)
: StringUtils.formatElapsedTime((long) value);
Rect rect = getRect(xAxisMarkerPaint, marker);
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacer + rect.height(),
xAxisMarkerPaint);
}
/**
* Gets the x axis interval.
*/
private double getXAxisInterval() {
double interval = (maxX / zoomLevel) / (double) TARGET_X_AXIS_INTERVALS;
if (interval < 1) {
interval = .5;
} else if (interval < 5) {
interval = 2;
} else if (interval < 10) {
interval = 5;
} else {
interval = (interval / 10) * 10;
}
return interval;
}
/**
* Gets the x axis marker positions.
*/
private ArrayList<Double> getXAxisMarkerPositions(double interval) {
ArrayList<Double> markers = new ArrayList<Double>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
}
// At least 2 markers
if (markers.size() < 2) {
markers.add(maxX);
}
return markers;
}
/**
* Draws the y axis.
*
* @param canvas the canvas
*/
private void drawYAxis(Canvas canvas, ChartValueSeries [] seriesIn) {
int x = getScrollX() + leftBorder;
int y = topBorder;
canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint);
int markerXPosition = x - spacer;
for (int i = 0; i < seriesIn.length; i++) {
int index = seriesIn.length - 1 - i;
ChartValueSeries chartValueSeries = seriesIn[index];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer;
}
}
}
private ChartValueSeries[] getCombinedSeries() {
ChartValueSeries [] seriesLocal = Arrays.copyOf(series, series.length);
for (int i = 0 ; i < seriesLocal.length ; i++) {
if (seriesOverlay[i].isEnabled()) {
seriesLocal[i] = seriesOverlay[i];
}
}
return seriesLocal;
}
/**
* Draws the y axis markers for a chart value series.
*
* @param chartValueSeries the chart value series
* @param canvas the canvas
* @param xPosition the right most x position
* @return the maximum marker width.
*/
private float drawYAxisMarkers(ChartValueSeries chartValueSeries, Canvas canvas, int xPosition) {
int interval = chartValueSeries.getInterval();
float maxMarkerWidth = 0;
for (int i = 0; i <= Y_AXIS_INTERVALS; i++) {
maxMarkerWidth = Math.max(maxMarkerWidth, drawYAxisMarker(chartValueSeries, canvas, xPosition,
i * interval + chartValueSeries.getMinMarkerValue()));
}
return maxMarkerWidth;
}
/**
* Draws a y axis marker.
*
* @param chartValueSeries the chart value series
* @param canvas the canvas
* @param xPosition the right most x position
* @param yValue the y value
* @return the marker width.
*/
private float drawYAxisMarker(
ChartValueSeries chartValueSeries, Canvas canvas, int xPosition, int yValue) {
String marker = chartValueSeries.formatMarker(yValue);
Paint paint = chartValueSeries.getMarkerPaint();
Rect rect = getRect(paint, marker);
int yPosition = getY(chartValueSeries, yValue) + (int) (rect.height() / 2);
canvas.drawText(marker, xPosition, yPosition, paint);
return paint.measureText(marker);
}
/**
* Draws the current pointer.
*
* @param canvas the canvas
*/
private void drawPointer(Canvas canvas) {
int index = -1;
for (int i = 0; i < seriesOverlay.length; i++) {
ChartValueSeries chartValueSeries = seriesOverlay[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
index = i;
break;
}
}
double currentXValue = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 0.0;
if (index != -1 && chartData.size() > 0) {
int dx = getX(currentXValue) - pointer.getIntrinsicWidth() / 2;
int dy = getY(seriesOverlay[index], chartData.get(chartData.size() - 1)[index + 1])
- pointer.getIntrinsicHeight();
Log.d(TAG, "y chartDat: " + chartData.get(chartData.size() - 1)[index + 1]);
Log.d(TAG, "drawing pointer, dx:" + dx);
Log.d(TAG, "drawing pointer, dy:" + dy);
Log.d(TAG, "chartData size: " + chartData.size());
canvas.translate(dx, dy);
pointer.draw(canvas);
}
}
/**
* Updates paths. The path needs to be updated any time after the data or the
* dimensions change.
*/
private void updatePaths(ChartValueSeries[] seriesIn, ArrayList<double[]> chartDataIn) {
synchronized (chartDataIn) {
for (ChartValueSeries chartValueSeries : seriesIn) {
chartValueSeries.getPath().reset();
}
drawPaths(seriesIn, chartDataIn);
closePaths(seriesIn,chartDataIn);
}
}
/**
* Draws all paths.
*/
private void drawPaths(ChartValueSeries [] seriesIn , ArrayList<double[]> chartDataIn) {
boolean[] hasMoved = new boolean[seriesIn.length];
for (int i = 0; i < chartDataIn.size(); i++) {
double[] dataPoint = chartDataIn.get(i);
for (int j = 0; j < seriesIn.length; j++) {
double value = dataPoint[j + 1];
if (Double.isNaN(value)) {
continue;
}
ChartValueSeries chartValueSeries = seriesIn[j];
Path path = chartValueSeries.getPath();
int x = getX(dataPoint[0]);
int y = getY(chartValueSeries, value);
//Log.d("TAG", "updatePath x:" + x);
//Log.d("TAG", "updatePath y:" + y);
if (!hasMoved[j]) {
hasMoved[j] = true;
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
}
}
/**
* Closes all paths.
*/
private void closePaths(ChartValueSeries [] seriesIn, ArrayList<double[]> chartDataIn) {
closePath(seriesIn,chartDataIn);
}
private void closePath(ChartValueSeries[] seriesIn, ArrayList<double[]> chartDataIN) {
for (int i = 0; i < seriesIn.length; i++) {
int first = getFirstPopulatedChartDataIndex(i,chartDataIN);
if (first != -1) {
Log.d(TAG, "closingPaths");
int xCorner = getX(chartDataIN.get(first)[0]);
int yCorner = topBorder + effectiveHeight;
Log.d(TAG, "closePath, x corner:" + chartDataIN.get(chartDataIN.size() - 1)[0]);
ChartValueSeries chartValueSeries = seriesIn[i];
Log.d(TAG, "closePath, y corner:" + getY(chartValueSeries, chartDataIN.get(first)[i + 1]));
Path path = chartValueSeries.getPath();
// Bottom right corner
path.lineTo(getX(chartDataIN.get(chartDataIN.size() - 1)[0]), yCorner);
// Bottom left corner
path.lineTo(xCorner, yCorner);
// Top right corner
path.lineTo(xCorner, getY(chartValueSeries, chartDataIN.get(first)[i + 1]));
}
}
}
/**
* Finds the index of the first data point containing data for a series.
* Returns -1 if no data point contains data for the series.
*
* @param seriesIndex the series's index
*/
private int getFirstPopulatedChartDataIndex(int seriesIndex, ArrayList<double[]> chartDataIn) {
for (int i = 0; i < chartDataIn.size(); i++) {
if (!Double.isNaN(chartDataIn.get(i)[seriesIndex + 1])) {
return i;
}
}
return -1;
}
/**
* Updates the chart dimensions.
*/
private void updateDimensions() {
ChartValueSeries [] seriesLocal = getCombinedSeries();
double maxXOverlay = xExtremityMonitorOverlay.hasData() ? xExtremityMonitorOverlay.getMax() : 1.0;
maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0;
maxX = maxXOverlay > maxX ? maxXOverlay : maxX;
for (ChartValueSeries chartValueSeries : seriesLocal) {
chartValueSeries.updateDimension();
}
float density = getContext().getResources().getDisplayMetrics().density;
spacer = (int) (density * SPACER);
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
int markerLength = getMarkerLength(seriesLocal);
leftBorder = (int) (density * BORDER + markerLength);
topBorder = (int) (density * BORDER + spacer);
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer);
rightBorder = (int) (density * BORDER + getRect(axisPaint, getXAxisLabel()).width() + spacer);
updateEffectiveDimensions();
}
private int getMarkerLength(ChartValueSeries[] seriesIn) {
float density = getContext().getResources().getDisplayMetrics().density;
spacer = (int) (density * SPACER);
int markerLength = 0;
for (int i = 0; i < seriesIn.length; i ++) {
ChartValueSeries chartValueSeries = seriesIn[i];
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker());
markerLength += rect.width() + spacer;
}
}
return markerLength;
}
/**
* Updates the effective dimensions.
*/
private void updateEffectiveDimensions() {
effectiveWidth = Math.max(0, width - leftBorder - rightBorder);
effectiveHeight = Math.max(0, height - topBorder - bottomBorder);
}
/**
* Updates the effective dimensions if changed.
*
* @param newWidth the new width
* @param newHeight the new height
*/
private void updateEffectiveDimensionsIfChanged(int newWidth, int newHeight) {
if (width != newWidth || height != newHeight) {
width = newWidth;
height = newHeight;
updateEffectiveDimensions();
updateAllPaths();
}
}
/**
* Gets the x position for a value.
*
* @param value the value
*/
private int getX(double value) {
if (value > maxX) {
value = maxX;
}
double percentage = value / maxX;
return leftBorder + (int) (percentage * effectiveWidth * zoomLevel);
}
/**
* Gets the y position for a value in a chart value series
*
* @param chartValueSeries the chart value series
* @param value the value
*/
private int getY(ChartValueSeries chartValueSeries, double value) {
int effectiveSpread = chartValueSeries.getInterval() * Y_AXIS_INTERVALS;
double percentage = (value - chartValueSeries.getMinMarkerValue()) / effectiveSpread;
int rangeHeight = effectiveHeight - 2 * yAxisOffset;
return topBorder + yAxisOffset + (int) ((1 - percentage) * rangeHeight);
}
/**
* Gets a waypoint's x value.
*
* @param waypoint the waypoint
*/
private double getWaypointXValue(Waypoint waypoint) {
if (chartByDistance) {
double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM;
return metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI;
} else {
return waypoint.getDuration();
}
}
/**
* Gets a paint's Rect for a string.
*
* @param paint the paint
* @param string the string
*/
private Rect getRect(Paint paint, String string) {
Rect rect = new Rect();
paint.getTextBounds(string, 0, string.length(), rect);
return rect;
}
/**
* Returns the status of metricUnits.
*
* @return the status of metricUnits
*/
@VisibleForTesting
public boolean isMetricUnits() {
return metricUnits;
}
}