package com.eveningoutpost.dexdrip.Models;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.util.Log;
import com.activeandroid.Model;
import com.activeandroid.annotation.Column;
import com.activeandroid.annotation.Table;
import com.activeandroid.query.Select;
import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.CalSubrecord;
import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.EGVRecord;
import com.eveningoutpost.dexdrip.ImportedLibraries.dexcom.records.SensorRecord;
import com.eveningoutpost.dexdrip.Sensor;
import com.eveningoutpost.dexdrip.Services.DexShareCollectionService;
import com.eveningoutpost.dexdrip.UtilityModels.BgSendQueue;
import com.eveningoutpost.dexdrip.UtilityModels.Constants;
import com.eveningoutpost.dexdrip.UtilityModels.Notifications;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.Expose;
import com.google.gson.internal.bind.DateTypeAdapter;
import java.text.DecimalFormat;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Table(name = "BgReadings", id = BaseColumns._ID)
public class BgReading extends Model {
private final static String TAG = BgReading.class.getSimpleName();
//TODO: Have these as adjustable settings!!
public final static double BESTOFFSET = (60000 * 0); // Assume readings are about x minutes off from actual!
@Column(name = "sensor", index = true)
public Sensor sensor;
@Column(name = "calibration", index = true)
public Calibration calibration;
@Expose
@Column(name = "timestamp", index = true)
public long timestamp;
@Expose
@Column(name = "time_since_sensor_started")
public double time_since_sensor_started;
@Expose
@Column(name = "raw_data")
public double raw_data;
@Expose
@Column(name = "filtered_data")
public double filtered_data;
@Expose
@Column(name = "age_adjusted_raw_value")
public double age_adjusted_raw_value;
@Expose
@Column(name = "calibration_flag")
public boolean calibration_flag;
@Expose
@Column(name = "calculated_value")
public double calculated_value;
@Expose
@Column(name = "calculated_value_slope")
public double calculated_value_slope;
@Expose
@Column(name = "a")
public double a;
@Expose
@Column(name = "b")
public double b;
@Expose
@Column(name = "c")
public double c;
@Expose
@Column(name = "ra")
public double ra;
@Expose
@Column(name = "rb")
public double rb;
@Expose
@Column(name = "rc")
public double rc;
@Expose
@Column(name = "uuid", index = true)
public String uuid;
@Expose
@Column(name = "calibration_uuid")
public String calibration_uuid;
@Expose
@Column(name = "sensor_uuid", index = true)
public String sensor_uuid;
@Column(name = "snyced")
public boolean synced;
@Column(name = "raw_calculated")
public double raw_calculated;
@Column(name = "hide_slope")
public boolean hide_slope;
@Column(name = "noise")
public String noise;
public double calculated_value_mmol() {
return mmolConvert(calculated_value);
}
public double mmolConvert(double mgdl) {
return mgdl * Constants.MGDL_TO_MMOLL;
}
public String displayValue(Context context) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String unit = prefs.getString("units", "mgdl");
DecimalFormat df = new DecimalFormat("#");
df.setMaximumFractionDigits(0);
if (calculated_value >= 400) {
return "HIGH";
} else if (calculated_value >= 40) {
if(unit.compareTo("mgdl") == 0) {
df.setMaximumFractionDigits(0);
return df.format(calculated_value);
} else {
df.setMaximumFractionDigits(1);
return df.format(calculated_value_mmol());
}
} else {
return "LOW";
}
}
public static double activeSlope() {
BgReading bgReading = BgReading.lastNoSenssor();
double slope = (2 * bgReading.a * (new Date().getTime() + BESTOFFSET)) + bgReading.b;
Log.w(TAG, "ESTIMATE SLOPE" + slope);
return slope;
}
public static double activePrediction() {
BgReading bgReading = BgReading.lastNoSenssor();
if (bgReading != null) {
double currentTime = new Date().getTime();
if (currentTime >= bgReading.timestamp + (60000 * 7)) { currentTime = bgReading.timestamp + (60000 * 7); }
double time = currentTime + BESTOFFSET;
return ((bgReading.a * time * time) + (bgReading.b * time) + bgReading.c);
}
return 0;
}
//*******CLASS METHODS***********//
public static void create(EGVRecord[] egvRecords, long addativeOffset, Context context) {
for(EGVRecord egvRecord : egvRecords) { BgReading.create(egvRecord, addativeOffset, context); }
}
public static void create(SensorRecord[] sensorRecords, long addativeOffset, Context context) {
for(SensorRecord sensorRecord : sensorRecords) { BgReading.create(sensorRecord, addativeOffset, context); }
}
public static void create(SensorRecord sensorRecord, long addativeOffset, Context context) {
Log.w(TAG, "gonna make some sensor records: " + sensorRecord.getUnfiltered());
if(BgReading.is_new(sensorRecord, addativeOffset)) {
BgReading bgReading = new BgReading();
Sensor sensor = Sensor.currentSensor();
Calibration calibration = Calibration.getForTimestamp(sensorRecord.getSystemTime().getTime() + addativeOffset);
if(sensor != null && calibration != null) {
bgReading.sensor = sensor;
bgReading.sensor_uuid = sensor.uuid;
bgReading.calibration = calibration;
bgReading.calibration_uuid = calibration.uuid;
bgReading.raw_data = (sensorRecord.getUnfiltered() / 1000);
bgReading.filtered_data = (sensorRecord.getFiltered() / 1000);
bgReading.timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset;
if(bgReading.timestamp > new Date().getTime()) { return; }
bgReading.uuid = UUID.randomUUID().toString();
bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;
bgReading.synced = false;
bgReading.calculateAgeAdjustedRawValue();
bgReading.save();
}
}
}
public static void create(EGVRecord egvRecord, long addativeOffset, Context context) {
BgReading bgReading = BgReading.getForTimestamp(egvRecord.getSystemTime().getTime() + addativeOffset);
Log.w(TAG, "Looking for BG reading to tag this thing to: " + egvRecord.getBGValue());
if(bgReading != null) {
bgReading.calculated_value = egvRecord.getBGValue();
if (egvRecord.getBGValue() <= 13) {
Calibration calibration = bgReading.calibration;
double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp)/(1000 * 60 * 10)));
double calSlope = (calibration.first_scale / firstAdjSlope)*1000;
double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope)*-1;
bgReading.raw_calculated = (((calSlope * bgReading.raw_data) + calIntercept) - 5);
bgReading.noise = egvRecord.noiseValue();
}
Log.w(TAG, "NEW VALUE CALCULATED AT: " + bgReading.calculated_value);
bgReading.calculated_value_slope = bgReading.slopefromName(egvRecord.getTrend().friendlyTrendName());
if(egvRecord.getTrend().friendlyTrendName().compareTo("NOT_COMPUTABLE") == 0 || egvRecord.getTrend().friendlyTrendName().compareTo("OUT_OF_RANGE") == 0) {
bgReading.hide_slope = true;
}
bgReading.save();
bgReading.find_new_curve();
bgReading.find_new_raw_curve();
Notifications.notificationSetter(context);
BgSendQueue.addToQueue(bgReading, "create", context);
}
}
public static BgReading getForTimestamp(double timestamp) {
Sensor sensor = Sensor.currentSensor();
if(sensor != null) {
BgReading bgReading = new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("timestamp <= ?", (timestamp + (60*1000))) // 1 minute padding (should never be that far off, but why not)
.where("calculated_value = 0")
.where("raw_calculated = 0")
.orderBy("timestamp desc")
.executeSingle();
if(bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3*60*1000)) { //cool, so was it actually within 4 minutes of that bg reading?
Log.w(TAG, "Found a BG timestamp match");
return bgReading;
}
}
Log.w(TAG, "No luck finding a BG timestamp match");
return null;
}
public static boolean is_new(SensorRecord sensorRecord, long addativeOffset) {
double timestamp = sensorRecord.getSystemTime().getTime() + addativeOffset;
Sensor sensor = Sensor.currentSensor();
if(sensor != null) {
BgReading bgReading = new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("timestamp <= ?", (timestamp + (60*1000))) // 1 minute padding (should never be that far off, but why not)
.orderBy("timestamp desc")
.executeSingle();
if(bgReading != null && Math.abs(bgReading.timestamp - timestamp) < (3*60*1000)) { //cool, so was it actually within 4 minutes of that bg reading?
Log.w(TAG, "Old Reading");
return false;
}
}
Log.w(TAG, "New Reading");
return true;
}
public static BgReading create(double raw_data, Context context, Long timestamp) {
BgReading bgReading = new BgReading();
Sensor sensor = Sensor.currentSensor();
if (sensor != null) {
Calibration calibration = Calibration.last();
if (calibration == null) {
bgReading.sensor = sensor;
bgReading.sensor_uuid = sensor.uuid;
bgReading.raw_data = (raw_data / 1000);
bgReading.filtered_data = (raw_data / 1000);
bgReading.timestamp = timestamp;
bgReading.uuid = UUID.randomUUID().toString();
bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;
bgReading.synced = false;
bgReading.calibration_flag = false;
bgReading.calculateAgeAdjustedRawValue();
bgReading.save();
bgReading.perform_calculations();
} else {
bgReading.sensor = sensor;
bgReading.sensor_uuid = sensor.uuid;
bgReading.calibration = calibration;
bgReading.calibration_uuid = calibration.uuid;
bgReading.raw_data = (raw_data/1000);
bgReading.filtered_data = (raw_data/1000);
bgReading.timestamp = timestamp;
bgReading.uuid = UUID.randomUUID().toString();
bgReading.time_since_sensor_started = bgReading.timestamp - sensor.started_at;
bgReading.synced = false;
bgReading.calculateAgeAdjustedRawValue();
if(calibration.check_in) {
double firstAdjSlope = calibration.first_slope + (calibration.first_decay * (Math.ceil(new Date().getTime() - calibration.timestamp)/(1000 * 60 * 10)));
double calSlope = (calibration.first_scale / firstAdjSlope)*1000;
double calIntercept = ((calibration.first_scale * calibration.first_intercept) / firstAdjSlope)*-1;
bgReading.calculated_value = (((calSlope * bgReading.raw_data) + calIntercept) - 5);
} else {
BgReading lastBgReading = BgReading.last();
if (lastBgReading != null && lastBgReading.calibration != null) {
if (lastBgReading.calibration_flag == true && ((lastBgReading.timestamp + (60000 * 20)) > bgReading.timestamp) && ((lastBgReading.calibration.timestamp + (60000 * 20)) > bgReading.timestamp)) {
lastBgReading.calibration.rawValueOverride(BgReading.weightedAverageRaw(lastBgReading.timestamp, bgReading.timestamp, lastBgReading.calibration.timestamp, lastBgReading.age_adjusted_raw_value, bgReading.age_adjusted_raw_value), context);
}
}
bgReading.calculated_value = ((calibration.slope * bgReading.age_adjusted_raw_value) + calibration.intercept);
}
bgReading.calculated_value = Math.min(400, Math.max(40, bgReading.calculated_value));
Log.w(TAG, "NEW VALUE CALCULATED AT: " + bgReading.calculated_value);
bgReading.save();
bgReading.perform_calculations();
Notifications.notificationSetter(context);
BgSendQueue.addToQueue(bgReading, "create", context);
}
}
Log.w("BG GSON: ",bgReading.toS());
return bgReading;
}
public static String slopeArrow() {
double slope = (float) (BgReading.activeSlope() * 60000);
return slopeArrow(slope);
}
public static String slopeArrow(double slope) {
String arrow;
if (slope <= (-3.5)) {
arrow = "\u21ca";
} else if (slope <= (-2)) {
arrow = "\u2193";
} else if (slope <= (-1)) {
arrow = "\u2198";
} else if (slope <= (1)) {
arrow = "\u2192";
} else if (slope <= (2)) {
arrow = "\u2197";
} else if (slope <= (3.5)) {
arrow = "\u2191";
} else {
arrow = "\u21c8";
}
return arrow;
}
public String slopeName() {
double slope_by_minute = calculated_value_slope * 60000;
String arrow = "NONE";
if (slope_by_minute <= (-3.5)) {
arrow = "DoubleDown";
} else if (slope_by_minute <= (-2)) {
arrow = "SingleDown";
} else if (slope_by_minute <= (-1)) {
arrow = "FortyFiveDown";
} else if (slope_by_minute <= (1)) {
arrow = "Flat";
} else if (slope_by_minute <= (2)) {
arrow = "FortyFiveUp";
} else if (slope_by_minute <= (3.5)) {
arrow = "SingleUp";
} else if (slope_by_minute <= (40)) {
arrow = "DoubleUp";
}
if(hide_slope) {
arrow = "9";
}
return arrow;
}
public double slopefromName(String slope_name) {
double slope_by_minute = 0;
if (slope_name.compareTo("DoubleDown") == 0) {
slope_by_minute = -3.5;
} else if (slope_name.compareTo("SingleDown") == 0) {
slope_by_minute = -2;
} else if (slope_name.compareTo("FortyFiveDown") == 0) {
slope_by_minute = -1;
} else if (slope_name.compareTo("Flat") == 0) {
slope_by_minute = 0;
} else if (slope_name.compareTo("FortyFiveUp") == 0) {
slope_by_minute = 2;
} else if (slope_name.compareTo("SingleUp") == 0) {
slope_by_minute = 2;
} else if (slope_name.compareTo("DoubleUp") == 0) {
slope_by_minute = 4;
} else if (slope_name.compareTo("NOT_COMPUTABLE") == 0 || slope_name.compareTo("OUT_OF_RANGE") == 0) {
slope_by_minute = 0;
}
return slope_by_minute /60000;
}
public static BgReading last() {
Sensor sensor = Sensor.currentSensor();
if (sensor != null) {
return new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("calculated_value != 0")
.where("raw_data != 0")
.orderBy("timestamp desc")
.executeSingle();
}
return null;
}
public static List<BgReading> latest_by_size(int number) {
Sensor sensor = Sensor.currentSensor();
return new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("raw_data != 0")
.orderBy("timestamp desc")
.limit(number)
.execute();
}
public static BgReading lastNoSenssor() {
return new Select()
.from(BgReading.class)
.where("calculated_value != 0")
.where("raw_data != 0")
.orderBy("timestamp desc")
.executeSingle();
}
public static List<BgReading> latest(int number) {
Sensor sensor = Sensor.currentSensor();
if (sensor == null) { return null; }
return new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("calculated_value != 0")
.where("raw_data != 0")
.orderBy("timestamp desc")
.limit(number)
.execute();
}
public static List<BgReading> latestUnCalculated(int number) {
Sensor sensor = Sensor.currentSensor();
if (sensor == null) { return null; }
return new Select()
.from(BgReading.class)
.where("Sensor = ? ", sensor.getId())
.where("raw_data != 0")
.orderBy("timestamp desc")
.limit(number)
.execute();
}
public static List<BgReading> latestForGraph(int number, double startTime) {
DecimalFormat df = new DecimalFormat("#");
df.setMaximumFractionDigits(1);
return new Select()
.from(BgReading.class)
.where("timestamp >= " + df.format(startTime))
.where("calculated_value != 0")
.where("raw_data != 0")
.orderBy("timestamp desc")
.limit(number)
.execute();
}
public static List<BgReading> last30Minutes() {
double timestamp = (new Date().getTime()) - (60000 * 30);
return new Select()
.from(BgReading.class)
.where("timestamp >= " + timestamp)
.where("calculated_value != 0")
.where("raw_data != 0")
.orderBy("timestamp desc")
.execute();
}
public static double estimated_bg(double timestamp) {
timestamp = timestamp + BESTOFFSET;
BgReading latest = BgReading.last();
if (latest == null) {
return 0;
} else {
return (latest.a * timestamp * timestamp) + (latest.b * timestamp) + latest.c;
}
}
public static double estimated_raw_bg(double timestamp) {
timestamp = timestamp + BESTOFFSET;
double estimate;
BgReading latest = BgReading.last();
if (latest == null) {
Log.w(TAG, "No data yet, assume perfect!");
estimate = 160;
} else {
estimate = (latest.ra * timestamp * timestamp) + (latest.rb * timestamp) + latest.rc;
}
Log.w(TAG, "ESTIMATE RAW BG" + estimate);
return estimate;
}
//*******INSTANCE METHODS***********//
public void perform_calculations() {
find_new_curve();
find_new_raw_curve();
find_slope();
}
public void find_slope() {
List<BgReading> last_2 = BgReading.latest(2);
if (last_2.size() == 2) {
BgReading second_latest = last_2.get(1);
double y1 = calculated_value;
double x1 = timestamp;
double y2 = second_latest.calculated_value;
double x2 = second_latest.timestamp;
if(y1 == y2) {
calculated_value_slope = 0;
} else {
calculated_value_slope = (y2 - y1)/(x2 - x1);
}
save();
} else if (last_2.size() == 1) {
calculated_value_slope = 0;
save();
} else {
Log.w(TAG, "NO BG? COULDNT FIND SLOPE!");
}
}
public void find_new_curve() {
List<BgReading> last_3 = BgReading.latest(3);
if (last_3.size() == 3) {
BgReading second_latest = last_3.get(1);
BgReading third_latest = last_3.get(2);
double y3 = calculated_value;
double x3 = timestamp;
double y2 = second_latest.calculated_value;
double x2 = second_latest.timestamp;
double y1 = third_latest.calculated_value;
double x1 = third_latest.timestamp;
a = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2));
b = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)));
c = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)));
Log.w(TAG, "BG PARABOLIC RATES: "+a+"x^2 + "+b+"x + "+c);
save();
} else if (last_3.size() == 2) {
Log.w(TAG, "Not enough data to calculate parabolic rates - assume Linear");
BgReading latest = last_3.get(0);
BgReading second_latest = last_3.get(1);
double y2 = latest.calculated_value;
double x2 = timestamp;
double y1 = second_latest.calculated_value;
double x1 = second_latest.timestamp;
if(y1 == y2) {
b = 0;
} else {
b = (y2 - y1)/(x2 - x1);
}
a = 0;
c = -1 * ((latest.b * x1) - y1);
Log.w(TAG, ""+latest.a+"x^2 + "+latest.b+"x + "+latest.c);
save();
} else {
Log.w(TAG, "Not enough data to calculate parabolic rates - assume static data");
a = 0;
b = 0;
c = calculated_value;
Log.w(TAG, ""+a+"x^2 + "+b+"x + "+c);
save();
}
}
public void calculateAgeAdjustedRawValue(){
double adjust_for = (86400000 * 1.9) - time_since_sensor_started;
if (adjust_for > 0) {
age_adjusted_raw_value = (((.45) * (adjust_for / (86400000 * 1.9))) * raw_data) + raw_data;
Log.w("RAW VALUE ADJUSTMENT: ", "FROM:" + raw_data + " TO: " + age_adjusted_raw_value);
} else {
age_adjusted_raw_value = raw_data;
}
}
public void find_new_raw_curve() {
List<BgReading> last_3 = BgReading.latest(3);
if (last_3.size() == 3) {
BgReading second_latest = last_3.get(1);
BgReading third_latest = last_3.get(2);
double y3 = age_adjusted_raw_value;
double x3 = timestamp;
double y2 = second_latest.age_adjusted_raw_value;
double x2 = second_latest.timestamp;
double y1 = third_latest.age_adjusted_raw_value;
double x1 = third_latest.timestamp;
ra = y1/((x1-x2)*(x1-x3))+y2/((x2-x1)*(x2-x3))+y3/((x3-x1)*(x3-x2));
rb = (-y1*(x2+x3)/((x1-x2)*(x1-x3))-y2*(x1+x3)/((x2-x1)*(x2-x3))-y3*(x1+x2)/((x3-x1)*(x3-x2)));
rc = (y1*x2*x3/((x1-x2)*(x1-x3))+y2*x1*x3/((x2-x1)*(x2-x3))+y3*x1*x2/((x3-x1)*(x3-x2)));
Log.w(TAG, "RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc);
save();
} else if (last_3.size() == 2) {
BgReading latest = last_3.get(0);
BgReading second_latest = last_3.get(1);
double y2 = latest.age_adjusted_raw_value;
double x2 = timestamp;
double y1 = second_latest.age_adjusted_raw_value;
double x1 = second_latest.timestamp;
if(y1 == y2) {
rb = 0;
} else {
rb = (y2 - y1)/(x2 - x1);
}
ra = 0;
rc = -1 * ((latest.rb * x1) - y1);
Log.w(TAG, "Not enough data to calculate parabolic rates - assume Linear data");
Log.w(TAG, "RAW PARABOLIC RATES: "+ra+"x^2 + "+rb+"x + "+rc);
save();
} else {
Log.w(TAG, "Not enough data to calculate parabolic rates - assume static data");
BgReading latest_entry = BgReading.lastNoSenssor();
ra = 0;
rb = 0;
rc = latest_entry.age_adjusted_raw_value;
save();
}
}
public static double weightedAverageRaw(double timeA, double timeB, double calibrationTime, double rawA, double rawB) {
double relativeSlope = (rawB - rawA)/(timeB - timeA);
double relativeIntercept = rawA - (relativeSlope * timeA);
return ((relativeSlope * calibrationTime) + relativeIntercept);
}
public String toS() {
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapter(Date.class, new DateTypeAdapter())
.serializeSpecialFloatingPointValues()
.create();
return gson.toJson(this);
}
public String noiseValue() {
if(noise == null || noise.compareTo("") == 0) {
return "1";
} else {
return String.valueOf(noise);
}
}
}