Explorar o código

Attempt to fix rate and time offsets WIP

Deniz Cengiz hai 1 ano
pai
achega
1dc5eb8ec0

+ 70 - 132
Trio/Sources/Modules/Onboarding/OnboardingStateModel.swift

@@ -31,12 +31,14 @@ extension Onboarding {
         var carbRatioItems: [CarbRatioEditor.Item] = []
         var initialCarbRatioItems: [CarbRatioEditor.Item] = []
         let carbRatioTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+            .sorted { $0 < $1 }
         let carbRatioRateValues = stride(from: 30.0, to: 501.0, by: 1.0).map { ($0.decimal ?? .zero) / 10 }
 
         // Basal Profile related
         var initialBasalProfileItems: [BasalProfileEditor.Item] = []
         var basalProfileItems: [BasalProfileEditor.Item] = []
         let basalProfileTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+            .sorted { $0 < $1 }
         var basalProfileRateValues: [Decimal] {
             switch pumpModel {
             case .dana,
@@ -51,7 +53,7 @@ extension Onboarding {
         // ISF related
         var isfItems: [ISFEditor.Item] = []
         var initialISFItems: [ISFEditor.Item] = []
-        let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        let isfTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }.sorted { $0 < $1 }
         var isfRateValues: [Decimal] {
             var values = stride(from: 9, to: 540.01, by: 1.0).map { Decimal($0) }
 
@@ -65,7 +67,8 @@ extension Onboarding {
         // Target related
         var targetItems: [TargetsEditor.Item] = []
         var initialTargetItems: [TargetsEditor.Item] = []
-        let targetTimeValues = stride(from: 0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+        let targetTimeValues = stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 }
+            .sorted { $0 < $1 }
 
         var targetRateValues: [Decimal] {
             let glucoseSetting = PickerSetting(value: 0, step: 1, min: 72, max: 180, type: .glucose)
@@ -162,18 +165,16 @@ extension Onboarding {
                     time: targetTimeValues[$0.timeIndex],
                     value: Double(targetRateValues[$0.lowIndex])
                 )
-            }
+            }.sorted { $0.time < $1.time }
         }
 
         func updateTargets(from therapyItems: [TherapySettingItem]) {
             targetItems = therapyItems.map { item in
                 let timeIndex = targetTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRate = targetRateValues.enumerated().min(by: {
-                    abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
-                })?.offset ?? 0
+                let closestTargetIndex = targetRateValues.firstIndex(of: Decimal(item.value)) ?? 0
 
-                return TargetsEditor.Item(lowIndex: closestRate, highIndex: closestRate, timeIndex: timeIndex)
-            }
+                return TargetsEditor.Item(lowIndex: closestTargetIndex, highIndex: closestTargetIndex, timeIndex: timeIndex)
+            }.sorted { $0.timeIndex < $1.timeIndex }
         }
 
         func getBasalTherapyItems(from basalRates: [BasalProfileEditor.Item]) -> [TherapySettingItem] {
@@ -183,18 +184,16 @@ extension Onboarding {
                     time: basalProfileTimeValues[$0.timeIndex],
                     value: Double(basalProfileRateValues[$0.rateIndex])
                 )
-            }
+            }.sorted { $0.time < $1.time }
         }
 
         func updateBasalRates(from therapyItems: [TherapySettingItem]) {
             basalProfileItems = therapyItems.map { item in
                 let timeIndex = basalProfileTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRate = basalProfileRateValues.enumerated().min(by: {
-                    abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
-                })?.offset ?? 0
+                let closestRateIndex = basalProfileRateValues.firstIndex(of: Decimal(item.value)) ?? 0
 
-                return BasalProfileEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
-            }
+                return BasalProfileEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
+            }.sorted { $0.timeIndex < $1.timeIndex }
         }
 
         func getCarbRatioTherapyItems(from carbRatios: [CarbRatioEditor.Item]) -> [TherapySettingItem] {
@@ -204,18 +203,16 @@ extension Onboarding {
                     time: carbRatioTimeValues[$0.timeIndex],
                     value: Double(carbRatioRateValues[$0.rateIndex])
                 )
-            }
+            }.sorted { $0.time < $1.time }
         }
 
         func updateCarbRatios(from therapyItems: [TherapySettingItem]) {
             carbRatioItems = therapyItems.map { item in
                 let timeIndex = carbRatioTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRate = carbRatioRateValues.enumerated().min(by: {
-                    abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
-                })?.offset ?? 0
+                let closestRateIndex = carbRatioRateValues.firstIndex(of: Decimal(item.value)) ?? 0
 
-                return CarbRatioEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
-            }
+                return CarbRatioEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
+            }.sorted { $0.timeIndex < $1.timeIndex }
         }
 
         func getSensitivityTherapyItems(from sensitivities: [ISFEditor.Item]) -> [TherapySettingItem] {
@@ -225,18 +222,16 @@ extension Onboarding {
                     time: isfTimeValues[$0.timeIndex],
                     value: Double(isfRateValues[$0.rateIndex])
                 )
-            }
+            }.sorted { $0.time < $1.time }
         }
 
         func updateSensitivies(from therapyItems: [TherapySettingItem]) {
             isfItems = therapyItems.map { item in
                 let timeIndex = isfTimeValues.firstIndex(where: { $0 == item.time }) ?? 0
-                let closestRate = isfRateValues.enumerated().min(by: {
-                    abs(Double($0.element) - item.value) < abs(Double($1.element) - item.value)
-                })?.offset ?? 0
+                let closestRateIndex = isfRateValues.firstIndex(of: Decimal(item.value)) ?? 0
 
-                return ISFEditor.Item(rateIndex: closestRate, timeIndex: timeIndex)
-            }
+                return ISFEditor.Item(rateIndex: closestRateIndex, timeIndex: timeIndex)
+            }.sorted { $0.timeIndex < $1.timeIndex }
         }
 
         // TODO: add update handler for all therapy items to automatically fill in time gaps and ensure schedule always starts at 00:00 and ends at 23:30
@@ -260,19 +255,6 @@ extension Onboarding.StateModel {
         return false
     }
 
-    func addCarbRatio() {
-        var time = 0
-        var rate = 0
-        if let last = carbRatioItems.last {
-            time = last.timeIndex + 1
-            rate = last.rateIndex
-        }
-
-        let newItem = CarbRatioEditor.Item(rateIndex: rate, timeIndex: time)
-
-        carbRatioItems.append(newItem)
-    }
-
     func saveCarbRatios() {
         guard carbRatiosHaveChanges else { return }
 
@@ -286,23 +268,21 @@ extension Onboarding.StateModel {
             return CarbRatioEntry(start: fotmatter.string(from: date), offset: minutes, ratio: rate)
         }
         let profile = CarbRatios(units: .grams, schedule: schedule)
-        saveCarbRatioProfile(profile)
+
+        fileStorage.save(profile, as: OpenAPS.Settings.carbRatios)
+
         initialCarbRatioItems = carbRatioItems.map { CarbRatioEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
     }
 
-//    func validate() {
-//        DispatchQueue.main.async {
-//            let uniq = Array(Set(self.items))
-//            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
-//            sorted.first?.timeIndex = 0
-//            if self.items != sorted {
-//                self.items = sorted
-//            }
-//        }
-//    }
-
-    func saveCarbRatioProfile(_ profile: CarbRatios) {
-        fileStorage.save(profile, as: OpenAPS.Settings.carbRatios)
+    func validateCarbRatios() {
+        DispatchQueue.main.async {
+            let uniq = Array(Set(self.carbRatioItems))
+            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+            sorted.first?.timeIndex = 0
+            if self.carbRatioItems != sorted {
+                self.carbRatioItems = sorted
+            }
+        }
     }
 }
 
@@ -313,21 +293,6 @@ extension Onboarding.StateModel {
         initialTargetItems != targetItems
     }
 
-    func addTarget() {
-        var time = 0
-        var low = 0
-        var high = 0
-        if let last = targetItems.last {
-            time = last.timeIndex + 1
-            low = last.lowIndex
-            high = low
-        }
-
-        let newItem = TargetsEditor.Item(lowIndex: low, highIndex: high, timeIndex: time)
-
-        targetItems.append(newItem)
-    }
-
     func saveTargets() {
         guard targetsHaveChanged else { return }
 
@@ -342,29 +307,22 @@ extension Onboarding.StateModel {
             return BGTargetEntry(low: low, high: high, start: formatter.string(from: date), offset: minutes)
         }
         let profile = BGTargets(units: .mgdL, userPreferredUnits: .mgdL, targets: targets)
-        saveTargets(profile)
+
+        fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
+
         initialTargetItems = targetItems
             .map { TargetsEditor.Item(lowIndex: $0.lowIndex, highIndex: $0.highIndex, timeIndex: $0.timeIndex) }
     }
 
-//    func validateTarget() {
-//        DispatchQueue.main.async {
-//            let uniq = Array(Set(self.items))
-//            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
-//                .map { item -> Item in
-//                    Item(lowIndex: item.lowIndex, highIndex: item.highIndex, timeIndex: item.timeIndex)
-//                }
-//            sorted.first?.timeIndex = 0
-//            self.items = sorted
-//
-//            if self.items.isEmpty {
-//                self.units = self.settingsManager.settings.units
-//            }
-//        }
-//    }
-
-    func saveTargets(_ profile: BGTargets) {
-        fileStorage.save(profile, as: OpenAPS.Settings.bgTargets)
+    func validateTarget() {
+        DispatchQueue.main.async {
+            let uniq = Array(Set(self.targetItems))
+            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+            sorted.first?.timeIndex = 0
+            if self.targetItems != sorted {
+                self.targetItems = sorted
+            }
+        }
     }
 }
 
@@ -375,19 +333,6 @@ extension Onboarding.StateModel {
         initialISFItems != isfItems
     }
 
-    func addISFValue() {
-        var time = 0
-        var rate = 0
-        if let last = isfItems.last {
-            time = last.timeIndex + 1
-            rate = last.rateIndex
-        }
-
-        let newItem = ISFEditor.Item(rateIndex: rate, timeIndex: time)
-
-        isfItems.append(newItem)
-    }
-
     func saveISFValues() {
         guard isfValuesHaveChanges else { return }
 
@@ -405,28 +350,21 @@ extension Onboarding.StateModel {
             userPreferredUnits: .mgdL,
             sensitivities: sensitivities
         )
-        saveISFProfile(profile)
+
+        fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+
         initialISFItems = isfItems.map { ISFEditor.Item(rateIndex: $0.rateIndex, timeIndex: $0.timeIndex) }
     }
 
-//    func validate() {
-//        DispatchQueue.main.async {
-//            DispatchQueue.main.async {
-//                let uniq = Array(Set(self.items))
-//                let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
-//                sorted.first?.timeIndex = 0
-//                if self.items != sorted {
-//                    self.items = sorted
-//                }
-//                if self.items.isEmpty {
-//                    self.units = self.settingsManager.settings.units
-//                }
-//            }
-//        }
-//    }
-
-    func saveISFProfile(_ profile: InsulinSensitivities) {
-        fileStorage.save(profile, as: OpenAPS.Settings.insulinSensitivities)
+    func validateISF() {
+        DispatchQueue.main.async {
+            let uniq = Array(Set(self.isfItems))
+            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+            sorted.first?.timeIndex = 0
+            if self.isfItems != sorted {
+                self.isfItems = sorted
+            }
+        }
     }
 }
 
@@ -447,19 +385,6 @@ extension Onboarding.StateModel {
         return false
     }
 
-    func addBasalRate() {
-        var time = 0
-        var rate = 20 // Default to 1.0 U/h (index 20 if basalProfileRateValues starts at 0.05 and increments by 0.05)
-
-        if let last = basalProfileItems.last {
-            time = last.timeIndex + 1
-            rate = last.rateIndex
-        }
-
-        let newItem = BasalProfileEditor.Item(rateIndex: rate, timeIndex: time)
-        basalProfileItems.append(newItem)
-    }
-
     func saveBasalProfile() {
         let profile = basalProfileItems.map { item -> BasalProfileEntry in
             let formatter = DateFormatter()
@@ -473,4 +398,17 @@ extension Onboarding.StateModel {
 
         fileStorage.save(profile, as: OpenAPS.Settings.basalProfile)
     }
+
+    func validateBasal() {
+        DispatchQueue.main.async {
+            let uniq = Array(Set(self.basalProfileItems))
+            let sorted = uniq.sorted { $0.timeIndex < $1.timeIndex }
+            if let first = sorted.first, first.timeIndex != 0 {
+                sorted[0].timeIndex = 0
+            }
+            if self.basalProfileItems != sorted {
+                self.basalProfileItems = sorted
+            }
+        }
+    }
 }

+ 3 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/BasalProfileStepView.swift

@@ -50,6 +50,7 @@ struct BasalProfileStepView: View {
                 TimeValueEditorView(
                     items: $therapyItems,
                     unit: String(localized: "U/hr"),
+                    timeOptions: state.basalProfileTimeValues,
                     valueOptions: state.basalProfileRateValues
                 )
 
@@ -80,8 +81,9 @@ struct BasalProfileStepView: View {
         }
         .onAppear {
             if state.basalProfileItems.isEmpty {
-                state.addBasalRate()
+                addBasalRate()
             }
+            state.validateBasal()
             therapyItems = state.getBasalTherapyItems(from: state.basalProfileItems)
         }.onChange(of: therapyItems) { _, newItems in
             state.updateBasalRates(from: newItems)

+ 13 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/CarbRatioStepView.swift

@@ -50,6 +50,7 @@ struct CarbRatioStepView: View {
                 TimeValueEditorView(
                     items: $therapyItems,
                     unit: String(localized: "g/U"),
+                    timeOptions: state.carbRatioTimeValues,
                     valueOptions: state.carbRatioRateValues
                 )
 
@@ -108,8 +109,9 @@ struct CarbRatioStepView: View {
         }
         .onAppear {
             if state.carbRatioItems.isEmpty {
-                state.addCarbRatio()
+                addCarbRatio()
             }
+            state.validateCarbRatios()
             therapyItems = state.getCarbRatioTherapyItems(from: state.carbRatioItems)
         }.onChange(of: therapyItems) { _, newItems in
             state.updateCarbRatios(from: newItems)
@@ -117,6 +119,16 @@ struct CarbRatioStepView: View {
         }
     }
 
+    // Add initial carb ratio
+    private func addCarbRatio() {
+        // Default to midnight (00:00) and 10 g/U
+        let timeIndex = state.carbRatioTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
+        let rateIndex = state.carbRatioRateValues.firstIndex { abs(Double($0) - 10.0) < 0.05 } ?? 10
+
+        let newItem = CarbRatioEditor.Item(rateIndex: rateIndex, timeIndex: timeIndex)
+        state.carbRatioItems.append(newItem)
+    }
+
     // Computed property to check if we can add more carb ratios
     private var canAddRatio: Bool {
         guard let lastItem = state.carbRatioItems.last else { return true }

+ 13 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/GlucoseTargetStepView.swift

@@ -59,14 +59,16 @@ struct GlucoseTargetStepView: View {
                 TimeValueEditorView(
                     items: $therapyItems,
                     unit: state.units.rawValue,
+                    timeOptions: state.targetTimeValues,
                     valueOptions: state.targetRateValues
                 )
             }
         }
         .onAppear {
             if state.targetItems.isEmpty {
-                state.addTarget()
+                addTarget()
             }
+            state.validateTarget()
             therapyItems = state.getTargetTherapyItems(from: state.targetItems)
         }.onChange(of: therapyItems) { _, newItems in
             state.updateTargets(from: newItems)
@@ -74,6 +76,16 @@ struct GlucoseTargetStepView: View {
         }
     }
 
+    // Add initial target
+    private func addTarget() {
+        // Default to midnight (00:00) and 1.0 U/h rate
+        let timeIndex = state.targetTimeValues.firstIndex { abs($0 - 0) < 1 } ?? 0
+        let targetIndex = state.targetRateValues.firstIndex { abs(Double($0) - 100) < 0.05 } ?? 100
+
+        let newItem = TargetsEditor.Item(lowIndex: targetIndex, highIndex: targetIndex, timeIndex: timeIndex)
+        state.targetItems.append(newItem)
+    }
+
     // Computed property to check if we can add more targets
     private var canAddTarget: Bool {
         guard let lastItem = state.targetItems.last else { return true }

+ 3 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/InsulinSensitivityStepView.swift

@@ -50,6 +50,7 @@ struct InsulinSensitivityStepView: View {
                 TimeValueEditorView(
                     items: $therapyItems,
                     unit: String(localized: "\(state.units.rawValue)/U"),
+                    timeOptions: state.isfTimeValues,
                     valueOptions: state.isfRateValues
                 )
 
@@ -120,8 +121,9 @@ struct InsulinSensitivityStepView: View {
         }
         .onAppear {
             if state.isfItems.isEmpty {
-                state.addISFValue()
+                addInitialISF()
             }
+            state.validateISF()
             therapyItems = state.getSensitivityTherapyItems(from: state.isfItems)
         }.onChange(of: therapyItems) { _, newItems in
             state.updateSensitivies(from: newItems)

+ 1 - 1
Trio/Sources/Modules/Onboarding/View/OnboardingSteps/OnboardingStepViews.swift

@@ -89,7 +89,7 @@ struct SettingItemView: View {
                 Image(icon)
                     .resizable()
                     .scaledToFit()
-                    .frame(width: 40, height: 40)
+                    .frame(width: 40, height: 24)
                     .colorMultiply(Color.green)
             } else {
                 Image(systemName: icon)

+ 22 - 17
Trio/Sources/Modules/Onboarding/View/TimeValueEditorView.swift

@@ -3,6 +3,7 @@ import SwiftUI
 struct TimeValueEditorView: View {
     @Binding var items: [TherapySettingItem]
     var unit: String
+    var timeOptions: [TimeInterval]
     var valueOptions: [Decimal]
 
     @State private var selectedItemID: UUID?
@@ -45,9 +46,11 @@ struct TimeValueEditorView: View {
 
                             HStack {
                                 Text("starts at").foregroundStyle(Color.secondary)
-
-                                let startDate = Date(timeIntervalSinceReferenceDate: item.time)
-                                Text(timeFormatter.string(from: startDate))
+                                let timeIndex = timeOptions.firstIndex { abs($0 - item.time) < 1 } ?? 0
+                                let time = timeOptions[timeIndex]
+                                let date = Date(timeIntervalSince1970: time)
+                                let timeString = timeFormatter.string(from: date)
+                                Text(timeString)
                                     .foregroundStyle(selectedItemID == item.id ? Color.accentColor : Color.primary)
                             }
                         }.contentShape(Rectangle())
@@ -57,6 +60,7 @@ struct TimeValueEditorView: View {
                     if selectedItemID == item.id {
                         TimeValuePickerRow(
                             item: $item,
+                            timeOptions: timeOptions,
                             valueOptions: valueOptions,
                             unit: unit
                         )
@@ -111,31 +115,31 @@ struct TherapySettingItem: Identifiable, Equatable {
 
 struct TimeValuePickerRow: View {
     @Binding var item: TherapySettingItem
+    var timeOptions: [TimeInterval]
     var valueOptions: [Decimal]
     var unit: String
 
     var body: some View {
         VStack(spacing: 8) {
             HStack {
-                Picker("Time", selection: Binding(
-                    get: { item.time },
-                    set: { item.time = $0 }
+                Picker("Value", selection: Binding(
+                    get: { item.value },
+                    set: { item.value = $0 }
                 )) {
-                    ForEach(0 ..< 48) { i in
-                        let seconds = Double(i * 30 * 60)
-                        Text(timeFormatter.string(from: Date(timeIntervalSinceReferenceDate: seconds)))
-                            .tag(seconds)
+                    ForEach(valueOptions, id: \.self) { value in
+                        Text("\(Double(value), specifier: "%.1f") \(unit)").tag(Double(value))
                     }
                 }
                 .frame(maxWidth: .infinity)
                 .clipped()
 
-                Picker("Value", selection: Binding(
-                    get: { item.value },
-                    set: { item.value = $0 }
+                Picker("Time", selection: Binding(
+                    get: { timeOptions.contains(item.time) ? item.time : timeOptions.first ?? 0 },
+                    set: { item.time = $0 }
                 )) {
-                    ForEach(valueOptions, id: \.self) { value in
-                        Text("\(Double(value), specifier: "%.1f") \(unit)").tag(Double(value))
+                    ForEach(timeOptions, id: \.self) { time in
+                        Text(timeFormatter.string(from: Date(timeIntervalSince1970: time)))
+                            .tag(time)
                     }
                 }
                 .frame(maxWidth: .infinity)
@@ -148,8 +152,8 @@ struct TimeValuePickerRow: View {
 
     private var timeFormatter: DateFormatter {
         let formatter = DateFormatter()
-        formatter.dateFormat = "HH:mm"
-        formatter.timeZone = TimeZone.current
+        formatter.timeZone = TimeZone(secondsFromGMT: 0)
+        formatter.timeStyle = .short
         return formatter
     }
 }
@@ -163,6 +167,7 @@ struct TimeValuePickerRow: View {
     TimeValueEditorView(
         items: $previewItems,
         unit: "U/h",
+        timeOptions: stride(from: 0.0, to: 1.days.timeInterval, by: 30.minutes.timeInterval).map { $0 },
         valueOptions: stride(from: 0.0, through: 10.0, by: 0.05).map { Decimal(round(100 * $0) / 100) }
     )
 }