/******************************************************************************* * Copyright 2013-2015 alladin-IT GmbH * * 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 at.alladin.rmbt.android.test; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.List; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Cap; import android.graphics.Paint.Join; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.PointF; import android.os.Bundle; import at.alladin.rmbt.android.graphview.GraphService; import at.alladin.rmbt.android.graphview.GraphView; public class SmoothGraph implements GraphService { public final static int FLAG_NONE = 0; public final static int FLAG_ALIGN_RIGHT = 1; public final static int FLAG_ALIGN_LEFT = 2; /** * some functions don't calculate the current point but previous ones (like the centered moving avarage). * this flag let the smooth graph take the position of the current added node to draw the current calcutaled avarage (instead of a previous time) */ public final static int FLAG_USE_CURRENT_NODE_TIME = 4; public static final String OPTION_STARTTIME = "startTime"; public static final String OPTION_FIRSTPOINT = "firstPoint"; public static final String OPTION_VALUELIST = "valueList"; /** * available smoothing function * @author lb * */ public enum SmoothingFunction { /** * equal number of data on each side<br> * function for centered moving avarage with n amount of data for element with the index x:<br> * f(x, n) = 1/n * (element(x-n/2) + element(x-n/2+1) + ... + element (x-n/2+n))<br> */ CENTERED_MOVING_AVARAGE, /** * previous n number of data<br> * function for simple moving avarage with n amount of data for element with the index x:<br> * f(x, n) = 1/n * (element(x-n) + element(x-n+1) + ... + element(x))<br> */ SIMPLE_MOVING_AVARAGE; public static double smooth(final SmoothingFunction smoothingFunction, final int element, final List<ValueEntry> valueList, final int dataAmount) { if (valueList == null || valueList.size() < 1) { return 0d; } int startingIndex = 0; int realDataAmount = SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount); switch (smoothingFunction) { case CENTERED_MOVING_AVARAGE: startingIndex = element - dataAmount/2; break; case SIMPLE_MOVING_AVARAGE: startingIndex = element - dataAmount; break; default: startingIndex = 0; } double sum = 0d; if (startingIndex < 0) { final int underflow = Math.abs(startingIndex); sum += underflow * valueList.get(0).value; realDataAmount -= underflow; startingIndex = 0; } if (startingIndex >= valueList.size()) { return 0d; } if ((startingIndex + realDataAmount) > valueList.size()) { final int overlfow = Math.abs(valueList.size() - (startingIndex + realDataAmount)); sum += overlfow * valueList.get(valueList.size()-1).value; realDataAmount -= overlfow; } for (int i = startingIndex; i < (startingIndex + realDataAmount); i++) { sum += valueList.get(i).getValue(); // System.out.print("[i:" + i + ", sum: " + sum + "]"); } return (sum / (double)SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount)); } public static int getDataAmountNeeded(SmoothingFunction smoothingFunction, int dataAmount) { switch (smoothingFunction) { case CENTERED_MOVING_AVARAGE: return dataAmount; case SIMPLE_MOVING_AVARAGE: return dataAmount; default: return 0; } } } private class ValueEntry { protected final double value; protected final double time; protected int flag; public ValueEntry(final double value, final double time, final int flag) { this.value = value; this.time = time; this.flag = flag; } public double getValue() { return value; } public double getTime() { return time; } public int getFlag() { return flag; } } private final float height; private final float width; private final List<ValueEntry> valueList = new ArrayList<ValueEntry>(); private final Path pathStroke; private final Path pathFill; private final Paint paintStroke; private final Paint paintFill; private PointF firstPoint; private SmoothingFunction smoothingFunction = SmoothingFunction.CENTERED_MOVING_AVARAGE; private int dataAmount = 0; private boolean matchHorizontally = false; private long startTime = -1; private long maxTimeNs = 0; /** * * @param graphView * @param color * @param dataAmount number of data to use for smoothing function * @return */ public static SmoothGraph addGraph(final GraphView graphView, final int color, final int dataAmount, final SmoothingFunction smoothingFunction) { return SmoothGraph.addGraph(graphView, color, dataAmount, smoothingFunction, true); } /** * * @param graphView * @param color * @param dataAmount number of data to use for smoothing function * @param matchHorizontally * @return */ public static SmoothGraph addGraph(final GraphView graphView, final int color, final int dataAmount, final SmoothingFunction smoothingFunction, final boolean matchHorizontally) { final SmoothGraph graph = new SmoothGraph(color, graphView.getGraphWidth(), graphView.getGraphHeight(), graphView.getGraphStrokeWidth()); graph.setMatchHorizontally(matchHorizontally); graph.setSmoothingFunction(smoothingFunction); graph.setDataAmount(dataAmount); graphView.addGraph(graph); return graph; } /** * * @param graphView * @param dataAmount * @param smoothingFunction * @param matchHorizontally * @param graphData * @return */ @SuppressWarnings("unchecked") public static SmoothGraph addGraph(final GraphView graphView,final int dataAmount, final SmoothingFunction smoothingFunction, final boolean matchHorizontally, final GraphData graphData) { final SmoothGraph graph = SmoothGraph.addGraph(graphView, dataAmount, smoothingFunction, matchHorizontally, graphData.getPathStroke(), graphData.getPathFill(), graphData.getPaintStroke(), graphData.getPaintFill()); graph.valueList.addAll((Collection<? extends ValueEntry>) graphData.getOptions().getSerializable(OPTION_VALUELIST)); if (graphData.getOptions().getParcelable(OPTION_FIRSTPOINT) != null) { graph.firstPoint = new PointF(); graph.firstPoint.set((PointF) graphData.getOptions().getParcelable(OPTION_FIRSTPOINT)); } graph.startTime = graphData.getOptions().getLong(OPTION_STARTTIME); return graph; } /** * * @param graphView * @param color * @param dataAmount * @param smoothingFunction * @param matchHorizontally * @param pathStroke * @param pathFill * @param paintStroke * @param paintFill * @return */ public static SmoothGraph addGraph(final GraphView graphView,final int dataAmount, final SmoothingFunction smoothingFunction, final boolean matchHorizontally, final Path pathStroke, final Path pathFill, final Paint paintStroke, final Paint paintFill) { final SmoothGraph graph = new SmoothGraph(graphView.getGraphWidth(), graphView.getGraphHeight(), pathStroke, pathFill, paintStroke, paintFill); graph.setMatchHorizontally(matchHorizontally); graph.setSmoothingFunction(smoothingFunction); graph.setDataAmount(dataAmount); graphView.addGraph(graph); return graph; } public SmoothingFunction getSmoothingFunction() { return smoothingFunction; } public void setSmoothingFunction(SmoothingFunction smoothingFunction) { this.smoothingFunction = smoothingFunction; } public int getDataAmount() { return dataAmount; } public void setDataAmount(int dataAmount) { this.dataAmount = dataAmount; } private SmoothGraph(final int color, final float width, final float height, final float strokeWidth) { this.height = height; this.width = width; paintStroke = new Paint(); paintStroke.setColor(color); paintStroke.setAlpha(204); // 80% paintStroke.setStyle(Style.STROKE); paintStroke.setStrokeWidth(strokeWidth); paintStroke.setStrokeCap(Cap.ROUND); paintStroke.setStrokeJoin(Join.ROUND); paintStroke.setAntiAlias(true); paintFill = new Paint(); paintFill.setColor(color); paintFill.setAlpha(51); // 20% paintFill.setStyle(Style.FILL); paintFill.setAntiAlias(true); pathStroke = new Path(); pathFill = new Path(); } private SmoothGraph(final float width, final float height, final Path pathStroke, final Path pathFill, final Paint paintStroke, final Paint paintFill) { this.height = height; this.width = width; this.paintFill = new Paint(paintFill); this.paintStroke = new Paint(paintStroke); this.pathFill = new Path(pathFill); this.pathStroke = new Path(pathStroke); } /* * (non-Javadoc) * @see at.alladin.rmbt.android.test.Graph#addValue(double) */ public void addValue(double value) { addValue(value, FLAG_NONE); } public void addValue(double value, int flag) { final long relTime; if (startTime == -1) { startTime = System.nanoTime(); relTime = 0; } else { relTime = System.nanoTime() - startTime; } if (relTime >= maxTimeNs) return; final double time = (double)relTime / (double)maxTimeNs; addValue(value, time, flag); } /* * (non-Javadoc) * @see at.alladin.rmbt.android.graphview.GraphService#addValue(double, double) */ public void addValue(double value, double time) { addValue(value, time, FLAG_NONE); } /* * (non-Javadoc) * @see at.alladin.rmbt.android.graphview.GraphService#addValue(double, double, int) */ public void addValue(double value, double time, int flag) { if (value < 0d) { value = 0d; } else if (value > 1d) { value = 1d; } if (time < 0d) { time = 0d; } else if (time > 1d) { time = 1d; } valueList.add(new ValueEntry(value, time, flag)); if (valueList.size() >= SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount)) { final int index; final int timeIndex; switch (smoothingFunction) { case CENTERED_MOVING_AVARAGE: index = valueList.size() - dataAmount / 2 - 1; break; case SIMPLE_MOVING_AVARAGE: index = valueList.size() - 1; break; default: index = dataAmount; break; } if ((flag & FLAG_USE_CURRENT_NODE_TIME) == FLAG_USE_CURRENT_NODE_TIME) { timeIndex = valueList.size() - 1; } else { timeIndex = index; } if (firstPoint == null) { for (int i = 0; i < index; i++) { value = SmoothingFunction.smooth(smoothingFunction, i, valueList, dataAmount); time = valueList.get(i).getTime(); final float x = getXCoord(time, valueList.get(i).getFlag()); final float y = (float) (height * (1 - value)); if (firstPoint == null) { pathStroke.moveTo(x, y); firstPoint = new PointF(x, y); } else { pathStroke.lineTo(x, y); pathFill.rewind(); pathFill.addPath(pathStroke); pathFill.lineTo(x, height); pathFill.lineTo(firstPoint.x, height); } } } value = SmoothingFunction.smooth(smoothingFunction, index, valueList, dataAmount); time = valueList.get(timeIndex).getTime(); final float x = getXCoord(time, flag); final float y = (float) (height * (1 - value)); pathStroke.lineTo(x, y); pathFill.rewind(); pathFill.addPath(pathStroke); pathFill.lineTo(x, height); pathFill.lineTo(firstPoint.x, height); } } protected float getXCoord(final double time, final int flag) { if ((flag & FLAG_ALIGN_LEFT) == FLAG_ALIGN_LEFT) { return 0f; } else if ((flag & FLAG_ALIGN_RIGHT) == FLAG_ALIGN_RIGHT) { return width; } else { return (float) (width * time); } } public void draw(final Canvas canvas) { if (valueList.size() == 1 && isMatchHorizontally()) { pathStroke.lineTo(width, firstPoint.y); pathFill.rewind(); pathFill.addPath(pathStroke); pathFill.lineTo(width, height); pathFill.lineTo(0, height); } canvas.drawPath(pathStroke, paintStroke); canvas.drawPath(pathFill, paintFill); } public void reset() { firstPoint = null; valueList.clear(); pathStroke.rewind(); pathFill.rewind(); startTime = -1; } public boolean hasBeenStarted() { return true; } public void clearGraphDontResetTime() { firstPoint = null; valueList.clear(); pathStroke.rewind(); pathFill.rewind(); } public boolean isMatchHorizontally() { return matchHorizontally; } public void setMatchHorizontally(boolean matchHorizontally) { this.matchHorizontally = matchHorizontally; } /** * * @param alpha */ public void setPaintAlpha(int alpha) { paintStroke.setAlpha(alpha); } /** * * @return */ public int getPaintAlpha() { return paintStroke.getAlpha(); } /** * * @param alpha */ public void setFillAlpha(int alpha) { paintFill.setAlpha(alpha); } /** * * @return */ public int getFillAlpha() { return paintFill.getAlpha(); } @Override public void setMaxTime(long maxTimeNs) { this.maxTimeNs = maxTimeNs; } @Override public Path getPathStroke() { return pathStroke; } @Override public Path getPathFill() { return pathFill; } @Override public Paint getPaintStroke() { return paintStroke; } @Override public Paint getPaintFill() { return paintFill; } /* * (non-Javadoc) * @see at.alladin.rmbt.android.graphview.GraphService#getGraphData() */ @Override public GraphData getGraphData() { final Bundle options = new Bundle(); options.putLong(OPTION_STARTTIME, startTime); options.putSerializable(OPTION_VALUELIST, (Serializable) valueList); options.putParcelable(OPTION_FIRSTPOINT, firstPoint); return new GraphData(pathStroke, pathFill, paintStroke, paintFill, options); } }