polscm32 1 年之前
父節點
當前提交
5d5f54eb3b

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -307,6 +307,7 @@
 		BD7DA9A92AE06E9200601B20 /* BolusCalculatorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */; };
 		BD7DA9AC2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */; };
 		BDB3C1192C03DD1000CEEAA1 /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */; };
+		BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB899872C564509006F3298 /* ForeCastChart.swift */; };
 		BDBAACFA2C2D439700370AAE /* OverrideData.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBAACF92C2D439700370AAE /* OverrideData.swift */; };
 		BDC2EA452C3043B000E5BBD0 /* OverrideStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */; };
 		BDC2EA472C3045AD00E5BBD0 /* Override.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC2EA462C3045AD00E5BBD0 /* Override.swift */; };
@@ -907,6 +908,7 @@
 		BD7DA9A82AE06E9200601B20 /* BolusCalculatorStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorStateModel.swift; sourceTree = "<group>"; };
 		BD7DA9AB2AE06EB900601B20 /* BolusCalculatorConfigRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusCalculatorConfigRootView.swift; sourceTree = "<group>"; };
 		BDB3C1182C03DD1000CEEAA1 /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = "<group>"; };
+		BDB899872C564509006F3298 /* ForeCastChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForeCastChart.swift; sourceTree = "<group>"; };
 		BDBAACF92C2D439700370AAE /* OverrideData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideData.swift; sourceTree = "<group>"; };
 		BDC2EA442C3043B000E5BBD0 /* OverrideStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OverrideStorage.swift; sourceTree = "<group>"; };
 		BDC2EA462C3045AD00E5BBD0 /* Override.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Override.swift; sourceTree = "<group>"; };
@@ -2231,6 +2233,7 @@
 			children = (
 				BDFD16592AE40438007F0DDA /* BolusRootView.swift */,
 				58237D9D2BCF0A6B00A47A79 /* PopupView.swift */,
+				BDB899872C564509006F3298 /* ForeCastChart.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -3107,6 +3110,7 @@
 				CE7CA3532A064973004BE681 /* tempPresetIntent.swift in Sources */,
 				D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */,
 				38E98A3025F52FF700C0CED0 /* Config.swift in Sources */,
+				BDB899882C564509006F3298 /* ForeCastChart.swift in Sources */,
 				110AEDE32C5193D200615CC9 /* BolusIntent.swift in Sources */,
 				DDD1631A2C4C695E00CD525A /* EditOverrideForm.swift in Sources */,
 				CE1856F72ADC4869007E39C7 /* CarbPresetIntentRequest.swift in Sources */,

+ 50 - 3
FreeAPS/Sources/Modules/Bolus/BolusStateModel.swift

@@ -17,6 +17,9 @@ extension Bolus {
         @Injected() var glucoseStorage: GlucoseStorage!
         @Injected() var determinationStorage: DeterminationStorage!
 
+        @Published var lowGlucose: Decimal = 4 / 0.0555
+        @Published var highGlucose: Decimal = 10 / 0.0555
+
         @Published var predictions: Predictions?
         @Published var amount: Decimal = 0
         @Published var insulinRecommended: Decimal = 0
@@ -93,6 +96,7 @@ extension Bolus {
         @Published var showInfo: Bool = false
         @Published var glucoseFromPersistence: [GlucoseStored] = []
         @Published var determination: [OrefDetermination] = []
+        @Published var preprocessedData: [(id: UUID, forecast: Forecast, forecastValue: ForecastValue)] = []
 
         let now = Date.now
 
@@ -125,6 +129,9 @@ extension Bolus {
             sweetMealFactor = settings.settings.sweetMealFactor
             displayPresets = settings.settings.displayPresets
 
+            lowGlucose = settingsManager.settings.low
+            highGlucose = settingsManager.settings.high
+
             maxCarbs = settings.settings.maxCarbs
             skipBolus = settingsManager.settings.skipBolusScreenAfterCarbs
             useFPUconversion = settingsManager.settings.useFPUconversion
@@ -577,10 +584,10 @@ extension Bolus.StateModel {
         let results = await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: backgroundContext,
-            predicate: NSPredicate.predicateFor30MinAgo,
+            predicate: NSPredicate.predicateForFourHoursAgo,
             key: "date",
             ascending: false,
-            fetchLimit: 3
+            fetchLimit: 48
         )
 
         return await backgroundContext.perform {
@@ -596,7 +603,7 @@ extension Bolus.StateModel {
             glucoseFromPersistence = glucoseObjects
 
             let lastGlucose = glucoseFromPersistence.first?.glucose ?? 0
-            let thirdLastGlucose = glucoseFromPersistence.last?.glucose ?? 0
+            let thirdLastGlucose = glucoseFromPersistence.dropFirst(2).first?.glucose ?? 0
             let delta = Decimal(lastGlucose) - Decimal(thirdLastGlucose)
 
             currentBG = Decimal(lastGlucose)
@@ -615,6 +622,7 @@ extension Bolus.StateModel {
                 predicate: NSPredicate.enactedDetermination
             )
             await updateDeterminationsArray(with: ids)
+            await updateForecastData()
         }
     }
 
@@ -646,3 +654,42 @@ extension Bolus.StateModel {
         }
     }
 }
+
+extension Bolus.StateModel {
+    func preprocessForecastData() async
+        -> [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])]
+    {
+        guard let id = determination.first?.objectID else {
+            return []
+        }
+
+        // Get forecast and forecast values
+        let forecastIDs = await determinationStorage.getForecastIDs(for: id, in: context)
+        var result: [(id: UUID, forecastID: NSManagedObjectID, forecastValueIDs: [NSManagedObjectID])] = []
+
+        for forecastID in forecastIDs {
+            // Get the forecast value IDs for the given forecast ID
+            let forecastValueIDs = await determinationStorage.getForecastValueIDs(for: forecastID, in: context)
+            let uuid = UUID()
+            result.append((id: uuid, forecastID: forecastID, forecastValueIDs: forecastValueIDs))
+        }
+
+        return result
+    }
+
+    @MainActor func updateForecastData() async {
+        let forecastData = await preprocessForecastData()
+
+        preprocessedData = forecastData.reduce(into: []) { result, data in
+            guard let forecast = try? context.existingObject(with: data.forecastID) as? Forecast else {
+                return
+            }
+
+            for forecastValueID in data.forecastValueIDs {
+                if let forecastValue = try? context.existingObject(with: forecastValueID) as? ForecastValue {
+                    result.append((id: data.id, forecast: forecast, forecastValue: forecastValue))
+                }
+            }
+        }
+    }
+}

+ 5 - 0
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -435,6 +435,11 @@ extension Bolus {
                                 }
                             }
                         }.listRowBackground(Color.chart)
+
+                        Section {
+                            ForeCastChart(state: state, units: $state.units)
+                                .padding(.vertical)
+                        }.listRowBackground(Color.chart)
                     }
                 }
                 .safeAreaInset(edge: .bottom, spacing: 0) {

+ 107 - 0
FreeAPS/Sources/Modules/Bolus/View/ForeCastChart.swift

@@ -0,0 +1,107 @@
+import Charts
+import CoreData
+import Foundation
+import SwiftUI
+
+struct ForeCastChart: View {
+    @StateObject var state: Bolus.StateModel
+    @Environment(\.colorScheme) var colorScheme
+    @Binding var units: GlucoseUnits
+
+    @State private var startMarker = Date(timeIntervalSinceNow: -4 * 60 * 60)
+    @State private var endMarker = Date(timeIntervalSinceNow: 3 * 60 * 60)
+
+    private var conversionFactor: Decimal {
+        units == .mmolL ? 0.0555 : 1
+    }
+
+    var body: some View {
+        forecastChart
+    }
+
+    private var forecastChart: some View {
+        Chart {
+            drawGlucose()
+            drawCurrentTimeMarker()
+            drawForecasts()
+        }
+        .chartXAxis { forecastChartXAxis }
+        .chartXScale(domain: startMarker ... endMarker)
+        .chartYAxis { forecastChartYAxis }
+    }
+
+    private func drawGlucose() -> some ChartContent {
+        ForEach(state.glucoseFromPersistence) { item in
+            if item.glucose > Int(state.highGlucose) {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                )
+                .foregroundStyle(Color.orange.gradient)
+                .symbolSize(20)
+            } else if item.glucose < Int(state.lowGlucose) {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                )
+                .foregroundStyle(Color.red.gradient)
+                .symbolSize(20)
+            } else {
+                PointMark(
+                    x: .value("Time", item.date ?? Date(), unit: .second),
+                    y: .value("Value", Decimal(item.glucose) * conversionFactor)
+                )
+                .foregroundStyle(Color.green.gradient)
+                .symbolSize(20)
+            }
+        }
+    }
+
+    private func drawForecasts() -> some ChartContent {
+        ForEach(state.preprocessedData, id: \.id) { tuple in
+            let forecastValue = tuple.forecastValue
+            let forecast = tuple.forecast
+            let valueAsDecimal = Decimal(forecastValue.value)
+            let displayValue = units == .mmolL ? valueAsDecimal.asMmolL : valueAsDecimal
+
+            LineMark(
+                x: .value("Time", timeForIndex(forecastValue.index)),
+                y: .value("Value", displayValue)
+            )
+            .foregroundStyle(by: .value("Predictions", forecast.type ?? ""))
+        }
+    }
+
+    private func timeForIndex(_ index: Int32) -> Date {
+        let currentTime = Date()
+        let timeInterval = TimeInterval(index * 300)
+        return currentTime.addingTimeInterval(timeInterval)
+    }
+
+    private func drawCurrentTimeMarker() -> some ChartContent {
+        RuleMark(
+            x: .value(
+                "",
+                Date(timeIntervalSince1970: TimeInterval(NSDate().timeIntervalSince1970)),
+                unit: .second
+            )
+        ).lineStyle(.init(lineWidth: 2, dash: [3])).foregroundStyle(Color(.systemGray2))
+    }
+
+    private var forecastChartXAxis: some AxisContent {
+        AxisMarks(values: .stride(by: .hour, count: 2)) { _ in
+            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            AxisValueLabel(format: .dateTime.hour(.defaultDigits(amPM: .narrow)), anchor: .top)
+                .font(.footnote)
+                .foregroundStyle(Color.primary)
+        }
+    }
+
+    private var forecastChartYAxis: some AxisContent {
+        AxisMarks(position: .trailing) { _ in
+            AxisGridLine(stroke: .init(lineWidth: 0.5, dash: [2, 3]))
+            AxisTick(length: 3, stroke: .init(lineWidth: 3)).foregroundStyle(Color.secondary)
+            AxisValueLabel().font(.footnote).foregroundStyle(Color.primary)
+        }
+    }
+}

+ 1 - 4
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -229,10 +229,7 @@ extension Home {
         private func registerHandlers() {
             coreDataObserver?.registerHandler(for: "OrefDetermination") { [weak self] in
                 guard let self = self else { return }
-                Task {
-                    self.setupDeterminationsArray()
-                    await self.updateForecastData()
-                }
+                self.setupDeterminationsArray()
             }
 
             coreDataObserver?.registerHandler(for: "GlucoseStored") { [weak self] in

+ 9 - 0
Model/Helper/NSPredicates.swift

@@ -26,6 +26,10 @@ extension Date {
         Calendar.current.date(byAdding: .hour, value: -2, to: Date())!
     }
 
+    static var fourHoursAgo: Date {
+        Calendar.current.date(byAdding: .hour, value: -4, to: Date())!
+    }
+
     static var sixHoursAgo: Date {
         Calendar.current.date(byAdding: .hour, value: -6, to: Date())!
     }
@@ -78,6 +82,11 @@ extension NSPredicate {
         return NSPredicate(format: "date >= %@", date as NSDate)
     }
 
+    static var predicateForFourHoursAgo: NSPredicate {
+        let date = Date.fourHoursAgo
+        return NSPredicate(format: "date >= %@", date as NSDate)
+    }
+
     static var predicateForSixHoursAgo: NSPredicate {
         let date = Date.sixHoursAgo
         return NSPredicate(format: "date >= %@", date as NSDate)