Files
OpenTracks/src/main/java/de/dennisguse/opentracks/chart/ChartView.java
T
2023-06-16 00:00:31 +02:00

1024 lines
34 KiB
Java

/*
* 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 de.dennisguse.opentracks.chart;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
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.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewParent;
import android.widget.Scroller;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import java.text.NumberFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import de.dennisguse.opentracks.R;
import de.dennisguse.opentracks.data.models.Marker;
import de.dennisguse.opentracks.settings.UnitSystem;
import de.dennisguse.opentracks.stats.ExtremityMonitor;
import de.dennisguse.opentracks.ui.markers.MarkerDetailActivity;
import de.dennisguse.opentracks.ui.markers.MarkerUtils;
import de.dennisguse.opentracks.ui.util.ThemeUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
* Visualization of the chart.
* Provides support for zooming (via pinch), scrolling, flinging, and selecting shown markers (single touch).
*
* @author Sandor Dornbush
* @author Leif Hendrik Wilden
*/
public class ChartView extends View {
static final int Y_AXIS_INTERVALS = 5;
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();
private static final int BORDER = 8;
private static final int SPACER = 4;
private static final int Y_AXIS_OFFSET = 16;
//TODO Determine from actual size of the used drawable
private static final float MARKER_X_ANCHOR = 13f / 48f;
static {
X_FRACTION_FORMAT.setMaximumFractionDigits(1);
X_FRACTION_FORMAT.setMinimumFractionDigits(1);
}
private final List<ChartValueSeries> seriesList = new LinkedList<>();
private final ChartValueSeries speedSeries;
private final ChartValueSeries paceSeries;
private final LinkedList<ChartPoint> chartPoints = new LinkedList<>();
private final List<Marker> markers = new LinkedList<>();
private final ExtremityMonitor xExtremityMonitor = new ExtremityMonitor();
private final int backgroundColor;
private final Paint axisPaint;
private final Paint xAxisMarkerPaint;
private final Paint gridPaint;
private final Paint markerPaint;
private final Drawable pointer;
private final Drawable markerPin;
private final int markerWidth;
private final int markerHeight;
private final Scroller scroller;
private double maxX = 1.0;
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 = false;
private UnitSystem unitSystem = UnitSystem.defaultUnitSystem();
private boolean reportSpeed = true;
private boolean showPointer = false;
private final GestureDetectorCompat detectorScrollFlingTab = new GestureDetectorCompat(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDown(MotionEvent e) {
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) > 0) {
int availableToScroll = effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, (int) distanceX));
}
}
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
fling((int) -velocityX);
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Check if the y event is within markerHeight of the marker center
if (Math.abs(event.getY() - topBorder - spacer - markerHeight / 2f) < markerHeight) {
int minDistance = Integer.MAX_VALUE;
Marker nearestMarker = null;
synchronized (markers) {
for (Marker marker : markers) {
int distance = Math.abs(getX(getMarkerXValue(marker)) - (int) event.getX() - getScrollX());
if (distance < minDistance) {
minDistance = distance;
nearestMarker = marker;
}
}
}
if (nearestMarker != null && minDistance < markerWidth) {
Intent intent = IntentUtils.newIntent(getContext(), MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, nearestMarker.getId());
getContext().startActivity(intent);
return true;
}
}
return false;
}
});
private final ScaleGestureDetector detectorZoom = new ScaleGestureDetector(getContext(), new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
if (scaleFactor >= 1.1f) {
zoomIn();
return true;
} else if (scaleFactor <= 0.9) {
zoomOut();
return true;
}
return false;
}
});
public ChartView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
int fontSizeSmall = ThemeUtils.getFontSizeSmallInPx(context);
int fontSizeMedium = ThemeUtils.getFontSizeMediumInPx(context);
seriesList.add(new ChartValueSeries(context,
Integer.MIN_VALUE,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000},
R.string.description_altitude_metric,
R.string.description_altitude_imperial,
R.string.description_altitude_imperial,
R.color.chart_altitude_fill,
R.color.chart_altitude_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getAltitude();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
speedSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 5, 10, 20, 50, 100},
R.string.description_speed_metric,
R.string.description_speed_imperial,
R.string.description_speed_nautical,
R.color.chart_speed_fill,
R.color.chart_speed_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getSpeed();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return reportSpeed;
}
};
seriesList.add(speedSeries);
paceSeries = new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{1, 2, 5, 10, 15, 20, 30, 60, 120},
R.string.description_pace_metric,
R.string.description_pace_imperial,
R.string.description_pace_nautical,
R.color.chart_pace_fill,
R.color.chart_pace_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPace();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return !reportSpeed;
}
};
seriesList.add(paceSeries);
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{25, 50},
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.string.description_sensor_heart_rate,
R.color.chart_heart_rate_fill,
R.color.chart_heart_rate_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getHeartRate();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
Integer.MAX_VALUE,
new int[]{5, 10, 25, 50},
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.string.description_sensor_cadence,
R.color.chart_cadence_fill,
R.color.chart_cadence_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getCadence();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
seriesList.add(new ChartValueSeries(context,
0,
1000,
new int[]{5, 50, 100, 200},
R.string.description_sensor_power,
R.string.description_sensor_power,
R.string.description_sensor_power,
R.color.chart_power_fill,
R.color.chart_power_border,
fontSizeSmall,
fontSizeMedium) {
@Override
protected Double extractDataFromChartPoint(@NonNull ChartPoint chartPoint) {
return chartPoint.getPower();
}
@Override
protected boolean drawIfChartPointHasNoData() {
return false;
}
});
backgroundColor = ThemeUtils.getBackgroundColor(context);
axisPaint = new Paint();
axisPaint.setStyle(Style.FILL_AND_STROKE);
axisPaint.setColor(ThemeUtils.getTextColorPrimary(context));
axisPaint.setAntiAlias(true);
axisPaint.setTextSize(fontSizeSmall);
xAxisMarkerPaint = new Paint(axisPaint);
xAxisMarkerPaint.setTextAlign(Align.CENTER);
gridPaint = new Paint();
gridPaint.setStyle(Style.STROKE);
gridPaint.setColor(ThemeUtils.getTextColorSecondary(context));
gridPaint.setAntiAlias(false);
gridPaint.setPathEffect(new DashPathEffect(new float[]{3, 2}, 0));
markerPaint = new Paint();
markerPaint.setStyle(Style.STROKE);
markerPaint.setAntiAlias(false);
pointer = ContextCompat.getDrawable(context, R.drawable.ic_logo_color_24dp);
pointer.setBounds(0, 0, pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
markerPin = MarkerUtils.getDefaultPhoto(context);
markerWidth = markerPin.getIntrinsicWidth();
markerHeight = markerPin.getIntrinsicHeight();
markerPin.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
setFocusable(true);
setClickable(true);
updateDimensions();
// either speedSeries or paceSeries should be enabled.
speedSeries.setEnabled(reportSpeed);
paceSeries.setEnabled(!reportSpeed);
}
@Override
public boolean canScrollHorizontally(int direction) {
return true;
}
public void setChartByDistance(boolean chartByDistance) {
this.chartByDistance = chartByDistance;
}
public UnitSystem getUnitSystem() {
return unitSystem;
}
public void setUnitSystem(UnitSystem value) {
unitSystem = value;
}
public boolean getReportSpeed() {
return reportSpeed;
}
/**
* Sets report speed.
*
* @param value report speed (true) or pace (false)
*/
public void setReportSpeed(boolean value) {
reportSpeed = value;
}
public boolean applyReportSpeed() {
if (reportSpeed) {
if (!speedSeries.isEnabled()) {
speedSeries.setEnabled(true);
paceSeries.setEnabled(false);
return true;
}
} else {
if (!paceSeries.isEnabled()) {
speedSeries.setEnabled(false);
paceSeries.setEnabled(true);
return true;
}
}
return false;
}
public void setShowPointer(boolean value) {
showPointer = value;
}
public void addChartPoints(List<ChartPoint> dataPoints) {
synchronized (chartPoints) {
chartPoints.addAll(dataPoints);
for (ChartPoint dataPoint : dataPoints) {
xExtremityMonitor.update(dataPoint.getTimeOrDistance());
for (ChartValueSeries i : seriesList) {
i.update(dataPoint);
}
}
updateDimensions();
updateSeries();
}
}
/**
* Clears all data.
*/
public void reset() {
synchronized (chartPoints) {
chartPoints.clear();
xExtremityMonitor.reset();
zoomLevel = 1;
updateDimensions();
}
}
/**
* Resets scroll.
* To be called on the UI thread.
*/
public void resetScroll() {
scrollTo(0, 0);
}
public void addMarker(Marker marker) {
synchronized (markers) {
markers.add(marker);
}
}
public void clearMarker() {
synchronized (markers) {
markers.clear();
}
}
private boolean canZoomIn() {
return zoomLevel < MAX_ZOOM_LEVEL;
}
private boolean canZoomOut() {
return zoomLevel > MIN_ZOOM_LEVEL;
}
private void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
updateSeries();
invalidate();
}
}
private void zoomOut() {
if (canZoomOut()) {
zoomLevel--;
scroller.abortAnimation();
int scrollX = getScrollX();
int maxWidth = effectiveWidth * (zoomLevel - 1);
if (scrollX > maxWidth) {
scrollX = maxWidth;
scrollTo(scrollX, 0);
}
updateSeries();
invalidate();
}
}
/**
* Initiates flinging.
*
* @param velocityX velocity of fling in pixels per second
*/
private void fling(int velocityX) {
int maxWidth = effectiveWidth * (zoomLevel - 1);
scroller.fling(getScrollX(), 0, velocityX, 0, 0, maxWidth, 0, 0);
invalidate();
}
/**
* Handle parent's view disallow touch event.
*
* @param disallow Does disallow parent touch event?
*/
private void requestDisallowInterceptTouchEventInParent(boolean disallow) {
ViewParent parent = getParent();
if (parent != null) {
parent.requestDisallowInterceptTouchEvent(disallow);
}
}
/**
* Scrolls the view horizontally by a given amount.
*
* @param deltaX the number of pixels to scroll
*/
private 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) {
boolean isZoom = detectorZoom.onTouchEvent(event);
boolean isScrollTab = detectorScrollFlingTab.onTouchEvent(event);
// ChartView handles zoom gestures (more than one pointer) and all gestures when zoomed itself
requestDisallowInterceptTouchEventInParent(event.getPointerCount() != 1 || zoomLevel != MIN_ZOOM_LEVEL);
return isZoom || isScrollTab;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateEffectiveDimensionsIfChanged(View.MeasureSpec.getSize(widthMeasureSpec), View.MeasureSpec.getSize(heightMeasureSpec));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
synchronized (chartPoints) {
canvas.save();
canvas.drawColor(backgroundColor);
canvas.save();
clipToGraphArea(canvas);
drawDataSeries(canvas);
drawMarker(canvas);
drawGrid(canvas);
canvas.restore();
drawSeriesTitles(canvas);
drawXAxis(canvas);
drawYAxis(canvas);
canvas.restore();
if (showPointer) {
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) {
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData()) {
chartValueSeries.drawPath(canvas);
}
}
}
private void drawMarker(Canvas canvas) {
synchronized (markers) {
for (Marker marker : markers) {
double xValue = getMarkerXValue(marker);
if (xValue > maxX) {
continue;
}
canvas.save();
float x = getX(getMarkerXValue(marker));
canvas.drawLine(x, topBorder + spacer + markerHeight / 2, x, topBorder + effectiveHeight, markerPaint);
canvas.translate(x - (markerWidth * MARKER_X_ANCHOR), topBorder + spacer);
markerPin.draw(canvas);
canvas.restore();
}
}
}
/**
* Draws the grid.
*
* @param canvas the canvas
*/
private void drawGrid(Canvas canvas) {
// X axis grid
List<Double> xAxisMarkerPositions = getXAxisMarkerPositions(getXAxisInterval());
for (double position : xAxisMarkerPositions) {
int x = getX(position);
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 series titles.
*
* @param canvas the canvas
*/
private void drawSeriesTitles(Canvas canvas) {
int[] titleDimensions = getTitleDimensions();
int lines = titleDimensions[0];
int lineHeight = titleDimensions[1];
int count = 0;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
count++;
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Paint paint = chartValueSeries.getTitlePaint();
int x = (int) (0.5 * width) + getScrollX();
int y = topBorder - spacer - (lines - count) * (lineHeight + spacer);
canvas.drawText(title, x, y, paint);
}
}
}
/**
* Gets the title dimensions.
* Returns an array of 2 integers, first element is the number of lines and the second element is the line height.
*/
private int[] getTitleDimensions() {
int lines = 0;
int lineHeight = 0;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
lines++;
String title = getContext().getString(chartValueSeries.getTitleId(unitSystem));
Rect rect = getRect(chartValueSeries.getTitlePaint(), title);
if (rect.height() > lineHeight) {
lineHeight = rect.height();
}
}
}
return new int[]{lines, lineHeight};
}
/**
* 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);
int yOffset = rect.height() / 2;
canvas.drawText(label, x + effectiveWidth + spacer, y + yOffset, axisPaint);
double interval = getXAxisInterval();
NumberFormat numberFormat = interval < 1 ? X_FRACTION_FORMAT : X_NUMBER_FORMAT;
for (double markerPosition : getXAxisMarkerPositions(interval)) {
drawXAxisMarker(canvas, markerPosition, numberFormat, spacer + yOffset);
}
}
private String getXAxisLabel() {
Context context = getContext();
if (chartByDistance) {
return switch (unitSystem) {
case METRIC -> context.getString(R.string.unit_kilometer);
case IMPERIAL_FEET, IMPERIAL_METER -> context.getString(R.string.unit_mile);
case NAUTICAL_IMPERIAL -> context.getString(R.string.unit_nautical_mile);
};
} else {
return context.getString(R.string.description_time);
}
}
/**
* Draws a x axis marker.
*
* @param canvas canvas
* @param value value
* @param numberFormat the number format
* @param spacing the spacing between x axis and marker
*/
private void drawXAxisMarker(Canvas canvas, double value, NumberFormat numberFormat, int spacing) {
String marker = chartByDistance ? numberFormat.format(value) : StringUtils.formatElapsedTime((Duration.ofMillis((long) value)));
Rect rect = getRect(xAxisMarkerPaint, marker);
canvas.drawText(marker, getX(value), topBorder + effectiveHeight + spacing + rect.height(), xAxisMarkerPaint);
}
private double getXAxisInterval() {
double interval = maxX / zoomLevel / 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;
}
private List<Double> getXAxisMarkerPositions(double interval) {
List<Double> markers = new ArrayList<>();
markers.add(0d);
for (int i = 1; i * interval < maxX; i++) {
markers.add(i * interval);
}
if (markers.size() < 2) {
markers.add(maxX);
}
return markers;
}
/**
* Draws the y axis.
*
* @param canvas the canvas
*/
private void drawYAxis(Canvas canvas) {
int x = getScrollX() + leftBorder;
int y = topBorder;
canvas.drawLine(x, y, x, y + effectiveHeight, axisPaint);
//TODO
int markerXPosition = x - spacer;
for (int i = 0; i < seriesList.size(); i++) {
int index = seriesList.size() - 1 - i;
ChartValueSeries chartValueSeries = seriesList.get(index);
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
markerXPosition -= drawYAxisMarkers(chartValueSeries, canvas, markerXPosition) + spacer;
}
}
}
/**
* 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) + (rect.height() / 2);
canvas.drawText(marker, xPosition, yPosition, paint);
return paint.measureText(marker);
}
private void drawPointer(Canvas canvas) {
if (chartPoints.isEmpty()) {
return;
}
ChartPoint last = chartPoints.getLast();
ChartValueSeries firstChartValueSeries = null;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() && chartValueSeries.isChartPointValid(last)) {
firstChartValueSeries = chartValueSeries;
break;
}
}
if (firstChartValueSeries != null && chartPoints.size() > 0) {
int dx = getX(maxX) - pointer.getIntrinsicWidth() / 2;
double value = firstChartValueSeries.extractDataFromChartPoint(last);
int dy = getY(firstChartValueSeries, value) - pointer.getIntrinsicHeight();
canvas.translate(dx, dy);
pointer.draw(canvas);
}
}
/**
* The path needs to be updated any time after the data or the dimensions change.
*/
private void updateSeries() {
synchronized (chartPoints) {
seriesList.stream().forEach(this::updateSerie);
}
}
private void updateSerie(ChartValueSeries series) {
final int yCorner = topBorder + effectiveHeight;
final Path path = series.getPath();
boolean drawFirstPoint = false;
path.rewind();
Integer finalX = null;
for (ChartPoint point : chartPoints) {
if (!series.isChartPointValid(point)) {
continue;
}
double value = series.extractDataFromChartPoint(point);
int x = getX(point.getTimeOrDistance());
int y = getY(series, value);
// start from lower left corner
if (!drawFirstPoint) {
path.moveTo(x, yCorner);
drawFirstPoint = true;
}
// draw graph
path.lineTo(x, y);
finalX = x;
}
// last point: move to lower right
if (finalX != null) {
path.lineTo(finalX, yCorner);
}
// back to lower left corner
path.close();
}
/**
* Updates the chart dimensions.
*/
private void updateDimensions() {
maxX = xExtremityMonitor.hasData() ? xExtremityMonitor.getMax() : 1.0;
for (ChartValueSeries chartValueSeries : seriesList) {
chartValueSeries.updateDimension();
}
float density = getResources().getDisplayMetrics().density;
spacer = (int) (density * SPACER);
yAxisOffset = (int) (density * Y_AXIS_OFFSET);
int markerLength = 0;
for (ChartValueSeries chartValueSeries : seriesList) {
if (chartValueSeries.isEnabled() && chartValueSeries.hasData() || allowIfEmpty(chartValueSeries)) {
Rect rect = getRect(chartValueSeries.getMarkerPaint(), chartValueSeries.getLargestMarker());
markerLength += rect.width() + spacer;
}
}
leftBorder = (int) (density * BORDER + markerLength);
int[] titleDimensions = getTitleDimensions();
topBorder = (int) (density * BORDER + titleDimensions[0] * (titleDimensions[1] + spacer));
Rect xAxisLabelRect = getRect(axisPaint, getXAxisLabel());
// border + x axis marker + spacer + .5 x axis label
bottomBorder = (int) (density * BORDER + getRect(xAxisMarkerPaint, "1").height() + spacer + (xAxisLabelRect.height() / 2));
rightBorder = (int) (density * BORDER + xAxisLabelRect.width() + spacer);
updateEffectiveDimensions();
}
/**
* 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();
updateSeries();
}
}
/**
* 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);
}
private double getMarkerXValue(Marker marker) {
if (chartByDistance) {
return marker.getLength().toKM_Miles(unitSystem);
} else {
return marker.getDuration().toMillis();
}
}
/**
* 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 true if the index is allowed when the chartData is empty.
*/
private boolean allowIfEmpty(ChartValueSeries chartValueSeries) {
if (!chartPoints.isEmpty()) {
return false;
}
return chartValueSeries.drawIfChartPointHasNoData();
}
}