package info.nightscout.androidaps.watchfaces; import android.content.Context; import android.graphics.Color; import android.graphics.DashPathEffect; import android.preference.PreferenceManager; import android.text.format.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import java.util.TimeZone; import info.nightscout.androidaps.data.BasalWatchData; import info.nightscout.androidaps.data.BgWatchData; import info.nightscout.androidaps.data.BolusWatchData; import info.nightscout.androidaps.data.TempWatchData; import lecho.lib.hellocharts.model.Axis; import lecho.lib.hellocharts.model.AxisValue; import lecho.lib.hellocharts.model.Line; import lecho.lib.hellocharts.model.LineChartData; import lecho.lib.hellocharts.model.PointValue; import lecho.lib.hellocharts.model.Viewport; /** * Created by emmablack on 11/15/14. */ public class BgGraphBuilder { public static final double MAX_PREDICTION__TIME_RATIO = (3d / 5); private long predictionEndTime; private List<BgWatchData> predictionsList; private ArrayList<BolusWatchData> bolusWatchDataList; private ArrayList<BasalWatchData> basalWatchDataList; public List<TempWatchData> tempWatchDataList; private int timespan; public long end_time; public long start_time; public double fuzzyTimeDenom = (1000 * 60 * 1); public Context context; public double highMark; public double lowMark; public List<BgWatchData> bgDataList = new ArrayList<BgWatchData>(); public int pointSize; public int highColor; public int lowColor; public int midColor; public int gridColour; public int basalCenterColor; public int basalBackgroundColor; private int bolusInvalidColor; public boolean singleLine = false; private List<PointValue> inRangeValues = new ArrayList<PointValue>(); private List<PointValue> highValues = new ArrayList<PointValue>(); private List<PointValue> lowValues = new ArrayList<PointValue>(); public Viewport viewport; //used for low resolution screen. public BgGraphBuilder(Context context, List<BgWatchData> aBgList, List<BgWatchData> predictionsList, List<TempWatchData> tempWatchDataList, ArrayList<BasalWatchData> basalWatchDataList, ArrayList<BolusWatchData> bolusWatchDataList, int aPointSize, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int timespan) { this.start_time = System.currentTimeMillis() - (1000 * 60 * 60 * timespan); //timespan hours ago this.bgDataList = aBgList; this.predictionsList = predictionsList; this.context = context; this.highMark = aBgList.get(aBgList.size() - 1).high; this.lowMark = aBgList.get(aBgList.size() - 1).low; this.pointSize = aPointSize; this.singleLine = false; this.midColor = aMidColor; this.lowColor = aMidColor; this.highColor = aMidColor; this.timespan = timespan; this.tempWatchDataList = tempWatchDataList; this.basalWatchDataList = basalWatchDataList; this.bolusWatchDataList = (bolusWatchDataList!=null)?bolusWatchDataList:new ArrayList<BolusWatchData>(); this.gridColour = gridColour; this.basalCenterColor = basalCenterColor; this.basalBackgroundColor = basalBackgroundColor; this.bolusInvalidColor = bolusInvalidColor; this.end_time = System.currentTimeMillis() + (1000 * 60 * 6 * timespan); //Now plus 30 minutes padding (for 5 hours. Less if less.) this.predictionEndTime = getPredictionEndTime(); this.end_time = (predictionEndTime>end_time)?predictionEndTime:end_time; } public BgGraphBuilder(Context context, List<BgWatchData> aBgList, List<BgWatchData> predictionsList, List<TempWatchData> tempWatchDataList, ArrayList<BasalWatchData> basalWatchDataList, ArrayList<BolusWatchData> bolusWatchDataList, int aPointSize, int aHighColor, int aLowColor, int aMidColor, int gridColour, int basalBackgroundColor, int basalCenterColor, int bolusInvalidColor, int timespan) { this.start_time = System.currentTimeMillis() - (1000 * 60 * 60 * timespan); //timespan hours ago this.bgDataList = aBgList; this.predictionsList = predictionsList; this.context = context; this.highMark = aBgList.get(aBgList.size() - 1).high; this.lowMark = aBgList.get(aBgList.size() - 1).low; this.pointSize = aPointSize; this.highColor = aHighColor; this.lowColor = aLowColor; this.midColor = aMidColor; this.timespan = timespan; this.tempWatchDataList = tempWatchDataList; this.basalWatchDataList = basalWatchDataList; this.bolusWatchDataList = (bolusWatchDataList!=null)?bolusWatchDataList:new ArrayList<BolusWatchData>(); this.gridColour = gridColour; this.basalCenterColor = basalCenterColor; this.basalBackgroundColor = basalBackgroundColor; this.bolusInvalidColor = bolusInvalidColor; this.end_time = System.currentTimeMillis() + (1000 * 60 * 6 * timespan); //Now plus 30 minutes padding (for 5 hours. Less if less.) this.predictionEndTime = getPredictionEndTime(); this.end_time = (predictionEndTime>end_time)?predictionEndTime:end_time; } public LineChartData lineData() { LineChartData lineData = new LineChartData(defaultLines()); lineData.setAxisYLeft(yAxis()); lineData.setAxisXBottom(xAxis()); return lineData; } public List<Line> defaultLines() { addBgReadingValues(); List<Line> lines = new ArrayList<Line>(); lines.add(highLine()); lines.add(lowLine()); lines.add(inRangeValuesLine()); lines.add(lowValuesLine()); lines.add(highValuesLine()); double minChart = lowMark; double maxChart = highMark; for ( BgWatchData bgd:bgDataList) { if(bgd.sgv > maxChart){ maxChart = bgd.sgv; } if(bgd.sgv < minChart){ minChart = bgd.sgv; } } double maxBasal = 0.1; for (BasalWatchData bwd: basalWatchDataList) { if(bwd.amount > maxBasal){ maxBasal = bwd.amount; } } double maxTemp = maxBasal; for (TempWatchData twd: tempWatchDataList) { if(twd.amount > maxTemp){ maxTemp = twd.amount; } } double factor = (maxChart-minChart)/maxTemp; // in case basal is the highest, don't paint it totally at the top. factor = Math.min(factor, ((maxChart-minChart)/maxBasal)*(2/3d)); boolean highlight = PreferenceManager .getDefaultSharedPreferences(context) .getBoolean("highlight_basals", false); for (TempWatchData twd: tempWatchDataList) { if(twd.endTime > start_time) { lines.add(tempValuesLine(twd, (float) minChart, factor, false, highlight?(pointSize+1):pointSize)); if(highlight){ lines.add(tempValuesLine(twd, (float) minChart, factor, true, 1)); } } } lines.add(basalLine((float) minChart, factor, highlight)); lines.add(bolusLine((float) minChart)); lines.add(bolusInvalidLine((float) minChart)); lines.add(smbLine((float) minChart)); lines.add(predictionLine()); return lines; } private Line basalLine(float offset, double factor, boolean highlight) { List<PointValue> pointValues = new ArrayList<PointValue>(); for (BasalWatchData bwd: basalWatchDataList) { if(bwd.endTime > start_time) { long begin = (long) Math.max(start_time, bwd.startTime); pointValues.add(new PointValue(fuzz(begin), offset + (float) (factor * bwd.amount))); pointValues.add(new PointValue(fuzz(bwd.endTime), offset + (float) (factor * bwd.amount))); } } Line basalLine = new Line(pointValues); basalLine.setHasPoints(false); basalLine.setColor(basalCenterColor); basalLine.setPathEffect(new DashPathEffect(new float[]{4f, 3f}, 4f)); basalLine.setStrokeWidth(highlight?2:1); return basalLine; } private Line bolusLine(float offset) { List<PointValue> pointValues = new ArrayList<PointValue>(); for (BolusWatchData bwd: bolusWatchDataList) { if(bwd.date > start_time && bwd.date <= end_time && !bwd.isSMB && bwd.isValid && bwd.bolus > 0) { pointValues.add(new PointValue(fuzz(bwd.date), (float) offset-2)); } } Line line = new Line(pointValues); line.setColor(basalCenterColor); line.setHasLines(false); line.setPointRadius(pointSize*2); line.setHasPoints(true); return line; } private Line smbLine(float offset) { List<PointValue> pointValues = new ArrayList<PointValue>(); for (BolusWatchData bwd: bolusWatchDataList) { if(bwd.date > start_time && bwd.date <= end_time && bwd.isSMB && bwd.isValid && bwd.bolus > 0) { pointValues.add(new PointValue(fuzz(bwd.date), (float) offset-2)); } } Line line = new Line(pointValues); line.setColor(basalCenterColor); line.setHasLines(false); line.setPointRadius(pointSize); line.setHasPoints(true); return line; } private Line bolusInvalidLine(float offset) { List<PointValue> pointValues = new ArrayList<PointValue>(); for (BolusWatchData bwd: bolusWatchDataList) { if(bwd.date > start_time && bwd.date <= end_time && !(bwd.isValid && (bwd.bolus > 0 || bwd.carbs > 0))) { pointValues.add(new PointValue(fuzz(bwd.date), (float) offset-2)); } } Line line = new Line(pointValues); line.setColor(bolusInvalidColor); line.setHasLines(false); line.setPointRadius(pointSize); line.setHasPoints(true); return line; } private Line predictionLine() { List<PointValue> pointValues = new ArrayList<PointValue>(); long endTime = getPredictionEndTime(); for (BgWatchData bwd: predictionsList) { if(bwd.timestamp <= endTime) { pointValues.add(new PointValue(fuzz(bwd.timestamp), (float) bwd.sgv)); } } Line line = new Line(pointValues); line.setColor(Color.MAGENTA); line.setHasLines(false); int size = pointSize/2; size = (size>0)?size:1; line.setPointRadius(size); line.setHasPoints(true); return line; } public Line highValuesLine() { Line highValuesLine = new Line(highValues); highValuesLine.setColor(highColor); highValuesLine.setHasLines(false); highValuesLine.setPointRadius(pointSize); highValuesLine.setHasPoints(true); return highValuesLine; } public Line lowValuesLine() { Line lowValuesLine = new Line(lowValues); lowValuesLine.setColor(lowColor); lowValuesLine.setHasLines(false); lowValuesLine.setPointRadius(pointSize); lowValuesLine.setHasPoints(true); return lowValuesLine; } public Line inRangeValuesLine() { Line inRangeValuesLine = new Line(inRangeValues); inRangeValuesLine.setColor(midColor); if(singleLine) { inRangeValuesLine.setHasLines(true); inRangeValuesLine.setHasPoints(false); inRangeValuesLine.setStrokeWidth(pointSize); } else { inRangeValuesLine.setPointRadius(pointSize); inRangeValuesLine.setHasPoints(true); inRangeValuesLine.setHasLines(false); } return inRangeValuesLine; } public Line tempValuesLine(TempWatchData twd, float offset, double factor, boolean isHighlightLine, int strokeWidth) { List<PointValue> lineValues = new ArrayList<PointValue>(); long begin = (long) Math.max(start_time, twd.startTime); lineValues.add(new PointValue(fuzz(begin), offset + (float) (factor * twd.startBasal))); lineValues.add(new PointValue(fuzz(begin), offset + (float) (factor * twd.amount))); lineValues.add(new PointValue(fuzz(twd.endTime), offset + (float) (factor * twd.amount))); lineValues.add(new PointValue(fuzz(twd.endTime), offset + (float) (factor * twd.endBasal))); Line valueLine = new Line(lineValues); valueLine.setHasPoints(false); if (isHighlightLine){ valueLine.setColor(basalCenterColor); valueLine.setStrokeWidth(1); }else { valueLine.setColor(basalBackgroundColor); valueLine.setStrokeWidth(strokeWidth); } return valueLine; } private void addBgReadingValues() { if(singleLine) { for (BgWatchData bgReading : bgDataList) { if(bgReading.timestamp > start_time) { if (bgReading.sgv >= 400) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) 400)); } else if (bgReading.sgv >= highMark) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= lowMark) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= 40) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= 11) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) 40)); } } } } else { for (BgWatchData bgReading : bgDataList) { if (bgReading.timestamp > start_time) { if (bgReading.sgv >= 400) { highValues.add(new PointValue(fuzz(bgReading.timestamp), (float) 400)); } else if (bgReading.sgv >= highMark) { highValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= lowMark) { inRangeValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= 40) { lowValues.add(new PointValue(fuzz(bgReading.timestamp), (float) bgReading.sgv)); } else if (bgReading.sgv >= 11) { lowValues.add(new PointValue(fuzz(bgReading.timestamp), (float) 40)); } } } } } public Line highLine() { List<PointValue> highLineValues = new ArrayList<PointValue>(); highLineValues.add(new PointValue(fuzz(start_time), (float) highMark)); highLineValues.add(new PointValue(fuzz(end_time), (float) highMark)); Line highLine = new Line(highLineValues); highLine.setHasPoints(false); highLine.setStrokeWidth(1); highLine.setColor(highColor); return highLine; } public Line lowLine() { List<PointValue> lowLineValues = new ArrayList<PointValue>(); lowLineValues.add(new PointValue(fuzz(start_time), (float) lowMark)); lowLineValues.add(new PointValue(fuzz(end_time), (float) lowMark)); Line lowLine = new Line(lowLineValues); lowLine.setHasPoints(false); lowLine.setColor(lowColor); lowLine.setStrokeWidth(1); return lowLine; } /////////AXIS RELATED////////////// public Axis yAxis() { Axis yAxis = new Axis(); yAxis.setAutoGenerated(true); List<AxisValue> axisValues = new ArrayList<AxisValue>(); yAxis.setValues(axisValues); yAxis.setHasLines(false); yAxis.setLineColor(gridColour); return yAxis; } public Axis xAxis() { final boolean is24 = DateFormat.is24HourFormat(context); SimpleDateFormat timeFormat = new SimpleDateFormat(is24? "HH" : "h a"); timeFormat.setTimeZone(TimeZone.getDefault()); long timeNow = System.currentTimeMillis(); Axis xAxis = new Axis(); xAxis.setAutoGenerated(false); List<AxisValue> xAxisValues = new ArrayList<AxisValue>(); //get the time-tick at the full hour after start_time GregorianCalendar startGC = new GregorianCalendar(); startGC.setTimeInMillis(start_time); startGC.set(Calendar.MILLISECOND, 0); startGC.set(Calendar.SECOND, 0); startGC.set(Calendar.MINUTE, 0); startGC.add(Calendar.HOUR, 1); long start_hour = startGC.getTimeInMillis(); //Display current time on the graph SimpleDateFormat longTimeFormat = new SimpleDateFormat(is24? "HH:mm" : "h:mm a"); xAxisValues.add(new AxisValue(fuzz(timeNow)).setLabel((longTimeFormat.format(timeNow)))); long hourTick = start_hour; // add all full hours within the timeframe while (hourTick < end_time){ if(Math.abs(hourTick - timeNow) > (1000 * 60 * 8 * timespan)){ xAxisValues.add(new AxisValue(fuzz(hourTick)).setLabel(timeFormat.format(hourTick))); } else { //don't print hour label if too close to now to avoid overlaps xAxisValues.add(new AxisValue(fuzz(hourTick))); } //increment by one hour hourTick += 60*60*1000; } xAxis.setValues(xAxisValues); xAxis.setTextSize(10); xAxis.setHasLines(true); xAxis.setLineColor(gridColour); xAxis.setTextColor(gridColour); return xAxis; } public long getPredictionEndTime() { long maxPredictionDate = System.currentTimeMillis(); for (BgWatchData prediction : predictionsList) { if (maxPredictionDate < prediction.timestamp) { maxPredictionDate = prediction.timestamp; } } return (long) Math.min(maxPredictionDate, System.currentTimeMillis() + MAX_PREDICTION__TIME_RATIO *timespan*1000*60*60); } public float fuzz(long value) { return (float) Math.round(value / fuzzyTimeDenom); } }