|
|
|
import 'dart:math' as math;
|
|
|
|
|
|
|
|
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
|
|
|
|
import 'package:doublefeel_flutter/core/services/health_sleep_calculator.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
|
|
|
|
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
|
|
...
|
...
|
@@ -8,16 +9,6 @@ import 'package:doublefeel_flutter/data/models/health/hrv/hrv_statistics_data.da |
|
|
|
import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart';
|
|
|
|
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
|
|
|
|
|
|
|
|
const _sleepTypeInBed = 0;
|
|
|
|
const _sleepTypeAsleep = 1;
|
|
|
|
const _sleepTypeAsleepUnspecified = 1;
|
|
|
|
const _sleepTypeAwake = 2;
|
|
|
|
const _sleepTypeAsleepCore = 3;
|
|
|
|
const _sleepTypeAsleepDeep = 4;
|
|
|
|
const _sleepTypeAsleepRem = 5;
|
|
|
|
const _sleepGoalMinutes = 8 * 60.0;
|
|
|
|
const _sleepContinuityToleranceSeconds = 1;
|
|
|
|
|
|
|
|
class LocalHealthDataConvert {
|
|
|
|
const LocalHealthDataConvert._();
|
|
|
|
|
|
...
|
...
|
@@ -50,64 +41,16 @@ class LocalHealthDataConvert { |
|
|
|
DateTime day,
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
) {
|
|
|
|
final dayStart = unixSeconds(day);
|
|
|
|
final dayEnd = unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final queryStart = unixSeconds(day.subtract(const Duration(days: 1)));
|
|
|
|
final queryEnd = unixSeconds(day.add(const Duration(days: 2)));
|
|
|
|
final candidates = sleepIntervals
|
|
|
|
.where((e) => e.endTime > queryStart && e.startTime < queryEnd)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) {
|
|
|
|
final startCompare = a.startTime.compareTo(b.startTime);
|
|
|
|
if (startCompare != 0) return startCompare;
|
|
|
|
return a.endTime.compareTo(b.endTime);
|
|
|
|
});
|
|
|
|
if (candidates.isEmpty) return const <HealthKitRawDataPoint>[];
|
|
|
|
|
|
|
|
final groups = <List<HealthKitRawDataPoint>>[];
|
|
|
|
for (final interval in candidates) {
|
|
|
|
if (groups.isEmpty) {
|
|
|
|
groups.add([interval]);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
final latestGroup = groups.last;
|
|
|
|
final latestEnd = latestGroup.map((e) => e.endTime).reduce(math.max);
|
|
|
|
if (interval.startTime <= latestEnd + _sleepContinuityToleranceSeconds) {
|
|
|
|
latestGroup.add(interval);
|
|
|
|
} else {
|
|
|
|
groups.add([interval]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (final group in groups) {
|
|
|
|
final groupEnd = group.map((e) => e.endTime).reduce(math.max);
|
|
|
|
if (groupEnd > dayStart && groupEnd <= dayEnd) {
|
|
|
|
return group;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return const <HealthKitRawDataPoint>[];
|
|
|
|
return HealthSleepCalculator.completeSleepIntervalsForDay(
|
|
|
|
day,
|
|
|
|
sleepIntervals,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static HealthKitRawDataPoint? mergeContinuousSleepIntervals(
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
) {
|
|
|
|
if (sleepIntervals.isEmpty) return null;
|
|
|
|
final sorted = [...sleepIntervals]..sort((a, b) {
|
|
|
|
final startCompare = a.startTime.compareTo(b.startTime);
|
|
|
|
if (startCompare != 0) return startCompare;
|
|
|
|
return a.endTime.compareTo(b.endTime);
|
|
|
|
});
|
|
|
|
var start = sorted.first.startTime;
|
|
|
|
var end = sorted.first.endTime;
|
|
|
|
for (final interval in sorted.skip(1)) {
|
|
|
|
if (interval.startTime > end) break;
|
|
|
|
end = math.max(end, interval.endTime);
|
|
|
|
}
|
|
|
|
return HealthKitRawDataPoint(
|
|
|
|
dataType: sorted.first.dataType,
|
|
|
|
startTime: start,
|
|
|
|
endTime: end,
|
|
|
|
);
|
|
|
|
return HealthSleepCalculator.mergeContinuousSleepIntervals(sleepIntervals);
|
|
|
|
}
|
|
|
|
|
|
|
|
static SleepStatisticsData sleepStatistics({
|
|
...
|
...
|
@@ -119,24 +62,24 @@ class LocalHealthDataConvert { |
|
|
|
}) {
|
|
|
|
final daily = [
|
|
|
|
for (final day in days)
|
|
|
|
_sleepDaySummary(
|
|
|
|
day,
|
|
|
|
sleepIntervals,
|
|
|
|
heartRate,
|
|
|
|
HealthSleepCalculator.calculateDay(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final validSleep = daily.where((e) => e.durationSeconds > 0).toList();
|
|
|
|
final previousDaily = [
|
|
|
|
for (final day in previousDays)
|
|
|
|
_sleepDaySummary(
|
|
|
|
day,
|
|
|
|
sleepIntervals,
|
|
|
|
heartRate,
|
|
|
|
HealthSleepCalculator.calculateDay(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final previousValidSleep =
|
|
|
|
previousDaily.where((e) => e.durationSeconds > 0).toList();
|
|
|
|
final allSleepHr = validSleep.expand((e) => e.sleepHr).toList();
|
|
|
|
final allSleepHr = validSleep
|
|
|
|
.expand((e) => _sleepHeartRate(e.sleepIntervals, heartRate))
|
|
|
|
.toList();
|
|
|
|
final shouldIncludeHeartRate = dateRangeType == 3;
|
|
|
|
|
|
|
|
return SleepStatisticsData(
|
|
...
|
...
|
@@ -147,7 +90,7 @@ class LocalHealthDataConvert { |
|
|
|
validSleep.map((e) => e.score).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
avgSleepEvaluate: _averageOrNull(
|
|
|
|
validSleep.map((e) => e.evaluate).whereType<num>().toList(),
|
|
|
|
validSleep.map((e) => e.state?.value).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
qoqAvgSleepDuration: _averageOrNull(
|
|
|
|
previousValidSleep.map((e) => e.durationSeconds).toList(),
|
|
...
|
...
|
@@ -156,7 +99,7 @@ class LocalHealthDataConvert { |
|
|
|
previousValidSleep.map((e) => e.score).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
qoqSleepEvaluate: _averageOrNull(
|
|
|
|
previousValidSleep.map((e) => e.evaluate).whereType<num>().toList(),
|
|
|
|
previousValidSleep.map((e) => e.state?.value).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
sleepTrendList: [
|
|
|
|
for (final item in daily)
|
|
...
|
...
|
@@ -164,7 +107,7 @@ class LocalHealthDataConvert { |
|
|
|
timeKey: dateKey(item.day),
|
|
|
|
totalTime: item.durationSeconds,
|
|
|
|
score: item.score,
|
|
|
|
sleepEvaluate: item.evaluate,
|
|
|
|
sleepEvaluate: item.state?.value,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
asleepTimeTrendList: [
|
|
...
|
...
|
@@ -200,7 +143,97 @@ class LocalHealthDataConvert { |
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
sleepTargetDuration: (_sleepGoalMinutes * 60).round(),
|
|
|
|
sleepTargetDuration: healthSleepGoalMinutes * 60,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static SleepStatisticsData sleepStatisticsFromResults({
|
|
|
|
required int dateRangeType,
|
|
|
|
required List<DateTime> days,
|
|
|
|
required List<DateTime> previousDays,
|
|
|
|
required List<HealthRawSleepResult> sleepResults,
|
|
|
|
required List<HealthKitRawDataPoint> heartRate,
|
|
|
|
}) {
|
|
|
|
final daily = [
|
|
|
|
for (final day in days) _sleepResultForDay(day, sleepResults),
|
|
|
|
];
|
|
|
|
final validSleep = daily.whereType<HealthRawSleepResult>().toList();
|
|
|
|
final previousDaily = [
|
|
|
|
for (final day in previousDays) _sleepResultForDay(day, sleepResults),
|
|
|
|
];
|
|
|
|
final previousValidSleep =
|
|
|
|
previousDaily.whereType<HealthRawSleepResult>().toList();
|
|
|
|
final shouldIncludeHeartRate = dateRangeType == 3;
|
|
|
|
final allSleepHr = shouldIncludeHeartRate
|
|
|
|
? _sleepResultHeartRate(validSleep, heartRate)
|
|
|
|
: const <HealthKitRawDataPoint>[];
|
|
|
|
|
|
|
|
return SleepStatisticsData(
|
|
|
|
avgSleepDuration: _averageOrNull(
|
|
|
|
validSleep.map((e) => e.sleepMinutes * 60).toList(),
|
|
|
|
),
|
|
|
|
avgSleepScore: _averageOrNull(
|
|
|
|
validSleep.map((e) => e.sleepScore).toList(),
|
|
|
|
),
|
|
|
|
avgSleepEvaluate: _averageOrNull(
|
|
|
|
validSleep.map((e) => e.sleepStateValue).whereType<num>().toList(),
|
|
|
|
),
|
|
|
|
qoqAvgSleepDuration: _averageOrNull(
|
|
|
|
previousValidSleep.map((e) => e.sleepMinutes * 60).toList(),
|
|
|
|
),
|
|
|
|
qoqSleepScore: _averageOrNull(
|
|
|
|
previousValidSleep.map((e) => e.sleepScore).toList(),
|
|
|
|
),
|
|
|
|
qoqSleepEvaluate: _averageOrNull(
|
|
|
|
previousValidSleep
|
|
|
|
.map((e) => e.sleepStateValue)
|
|
|
|
.whereType<num>()
|
|
|
|
.toList(),
|
|
|
|
),
|
|
|
|
sleepTrendList: [
|
|
|
|
for (var index = 0; index < days.length; index++)
|
|
|
|
SleepTrendList(
|
|
|
|
timeKey: dateKey(days[index]),
|
|
|
|
totalTime: daily[index]?.sleepMinutes == null
|
|
|
|
? 0
|
|
|
|
: daily[index]!.sleepMinutes * 60,
|
|
|
|
score: daily[index]?.sleepScore,
|
|
|
|
sleepEvaluate: daily[index]?.sleepStateValue,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
asleepTimeTrendList: [
|
|
|
|
for (var index = 0; index < days.length; index++)
|
|
|
|
AsleepTimeTrendList(
|
|
|
|
timeKey: dateKey(days[index]),
|
|
|
|
asleepTime: daily[index]?.startDate,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
bestSleepInfo: _bestSleepResultInfo(validSleep, best: true),
|
|
|
|
worstSleepInfo: _worstSleepResultInfo(validSleep),
|
|
|
|
earliestSleepInfo: _earliestSleepResultInfo(validSleep),
|
|
|
|
latestSleepInfo: _latestSleepResultInfo(validSleep),
|
|
|
|
hrList: shouldIncludeHeartRate
|
|
|
|
? [
|
|
|
|
for (final point in allSleepHr)
|
|
|
|
SleepHrItem(time: point.endTime, value: point.value),
|
|
|
|
]
|
|
|
|
: null,
|
|
|
|
avgHr: shouldIncludeHeartRate
|
|
|
|
? _averageOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
maxHr: shouldIncludeHeartRate
|
|
|
|
? _maxOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
minHr: shouldIncludeHeartRate
|
|
|
|
? _minOrNull(
|
|
|
|
allSleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)
|
|
|
|
: null,
|
|
|
|
sleepTargetDuration: healthSleepGoalMinutes * 60,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -372,7 +405,7 @@ class LocalHealthDataConvert { |
|
|
|
? null
|
|
|
|
: latestWithGoal!.exerciseTimeGoal!.round(),
|
|
|
|
updateTime: latestWithGoal?.endTime,
|
|
|
|
sleepTargetDuration: (60 * _sleepGoalMinutes).round(),
|
|
|
|
sleepTargetDuration: 60 * healthSleepGoalMinutes,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
...
|
...
|
@@ -385,10 +418,9 @@ class LocalHealthDataConvert { |
|
|
|
required List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
}) {
|
|
|
|
final activitySummary = _activityDaySummary(day, activity);
|
|
|
|
final sleepSummary = _sleepDaySummary(
|
|
|
|
day,
|
|
|
|
sleepIntervals,
|
|
|
|
sleepingHeartRate,
|
|
|
|
final sleepSummary = HealthSleepCalculator.calculateDay(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: sleepIntervals,
|
|
|
|
);
|
|
|
|
final dayStart = unixSeconds(day);
|
|
|
|
final dayEnd = unixSeconds(day.add(const Duration(days: 1)));
|
|
...
|
...
|
@@ -410,7 +442,10 @@ class LocalHealthDataConvert { |
|
|
|
lastRestingHrValue:
|
|
|
|
dayRestingHr.isEmpty ? null : dayRestingHr.last.value?.round(),
|
|
|
|
hrAvg: _averageOrNull(
|
|
|
|
sleepSummary.sleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
_sleepHeartRate(sleepSummary.sleepIntervals, sleepingHeartRate)
|
|
|
|
.map((e) => e.value)
|
|
|
|
.whereType<num>()
|
|
|
|
.toList(),
|
|
|
|
)?.round(),
|
|
|
|
move: activitySummary.move.round(),
|
|
|
|
exercise: activitySummary.exerciseSeconds.round(),
|
|
...
|
...
|
@@ -418,7 +453,52 @@ class LocalHealthDataConvert { |
|
|
|
steps: activitySummary.steps.round(),
|
|
|
|
sleepDuration: sleepSummary.durationSeconds.round(),
|
|
|
|
sleepScore: sleepSummary.score?.toDouble(),
|
|
|
|
sleepState: sleepSummary.evaluate?.round(),
|
|
|
|
sleepState: sleepSummary.state?.value,
|
|
|
|
sleepTimeList: sleepTimeList,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static V2HealthData v2HealthDataFromSleepResult({
|
|
|
|
required DateTime day,
|
|
|
|
required List<HealthRawHrvStressPoint> hrvPoints,
|
|
|
|
required List<HealthKitRawActivityDataPoint> activity,
|
|
|
|
required List<HealthKitRawDataPoint> sleepingHeartRate,
|
|
|
|
required List<HealthKitRawDataPoint> restingHeartRate,
|
|
|
|
required HealthRawSleepResult? sleepResult,
|
|
|
|
}) {
|
|
|
|
final activitySummary = _activityDaySummary(day, activity);
|
|
|
|
final dayStart = unixSeconds(day);
|
|
|
|
final dayEnd = unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final sleepTimeList = sleepResult == null
|
|
|
|
? <V2SleepTimeRange>[]
|
|
|
|
: [
|
|
|
|
V2SleepTimeRange(
|
|
|
|
fromTime: sleepResult.startDate,
|
|
|
|
toTime: sleepResult.date,
|
|
|
|
),
|
|
|
|
];
|
|
|
|
final sleepHr = sleepResult == null
|
|
|
|
? const <HealthKitRawDataPoint>[]
|
|
|
|
: _sleepResultHeartRate([sleepResult], sleepingHeartRate);
|
|
|
|
final dayRestingHr = restingHeartRate
|
|
|
|
.where((e) => e.endTime >= dayStart && e.endTime < dayEnd)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.endTime.compareTo(b.endTime));
|
|
|
|
|
|
|
|
return V2HealthData(
|
|
|
|
hrvAvg: _averageOrNull(hrvPoints.map((e) => e.result).toList())?.round(),
|
|
|
|
lastRestingHrValue:
|
|
|
|
dayRestingHr.isEmpty ? null : dayRestingHr.last.value?.round(),
|
|
|
|
hrAvg: _averageOrNull(
|
|
|
|
sleepHr.map((e) => e.value).whereType<num>().toList(),
|
|
|
|
)?.round(),
|
|
|
|
move: activitySummary.move.round(),
|
|
|
|
exercise: activitySummary.exerciseSeconds.round(),
|
|
|
|
stand: activitySummary.standHours.round(),
|
|
|
|
steps: activitySummary.steps.round(),
|
|
|
|
sleepDuration: sleepResult == null ? 0 : sleepResult.sleepMinutes * 60,
|
|
|
|
sleepScore: sleepResult?.sleepScore.toDouble(),
|
|
|
|
sleepState: sleepResult?.sleepStateValue,
|
|
|
|
sleepTimeList: sleepTimeList,
|
|
|
|
);
|
|
|
|
}
|
|
...
|
...
|
@@ -479,43 +559,6 @@ class LocalHealthDataConvert { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static _SleepDaySummary _sleepDaySummary(
|
|
|
|
DateTime day,
|
|
|
|
List<HealthKitRawDataPoint> sleepIntervals,
|
|
|
|
List<HealthKitRawDataPoint> heartRate,
|
|
|
|
) {
|
|
|
|
final intervals = completeSleepIntervalsForDay(day, sleepIntervals);
|
|
|
|
final mergedInterval = mergeContinuousSleepIntervals(intervals);
|
|
|
|
final windowStart = mergedInterval?.startTime ?? unixSeconds(day);
|
|
|
|
final windowEnd = mergedInterval?.endTime ??
|
|
|
|
unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final summary = _sleepSummary(intervals, windowStart, windowEnd);
|
|
|
|
final score = _sleepScore(summary);
|
|
|
|
final sleepHr = heartRate
|
|
|
|
.where((point) => intervals.any((interval) =>
|
|
|
|
point.endTime > interval.startTime &&
|
|
|
|
point.endTime <= interval.endTime))
|
|
|
|
.toList();
|
|
|
|
final nullableScore = score == 0 ? null : score;
|
|
|
|
final sleepEvaluate = _sleepEvaluate(score);
|
|
|
|
final sleepDuration = nullableScore == null || sleepEvaluate == null
|
|
|
|
? 0
|
|
|
|
: summary.totalAsleepMinutes.round() * 60;
|
|
|
|
|
|
|
|
return _SleepDaySummary(
|
|
|
|
day: day,
|
|
|
|
sleepIntervals: intervals,
|
|
|
|
mergeSleepTimeRange: mergedInterval,
|
|
|
|
durationSeconds: sleepDuration.round(),
|
|
|
|
asleepTime: intervals.isEmpty
|
|
|
|
? null
|
|
|
|
: intervals.map((e) => e.startTime).reduce(math.min),
|
|
|
|
score: nullableScore,
|
|
|
|
evaluate: sleepEvaluate,
|
|
|
|
sleepHr: sleepHr,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static _ActivityDaySummary _activityDaySummary(
|
|
|
|
DateTime day,
|
|
|
|
List<HealthKitRawActivityDataPoint> activity,
|
|
...
|
...
|
@@ -653,12 +696,15 @@ class LocalHealthDataConvert { |
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
static BestSleepInfo? _bestSleepInfo(List<_SleepDaySummary> daily) {
|
|
|
|
final item = daily.where((e) => e.score != null).fold<_SleepDaySummary?>(
|
|
|
|
null,
|
|
|
|
(best, item) =>
|
|
|
|
best == null || item.score! > best.score! ? item : best,
|
|
|
|
);
|
|
|
|
static BestSleepInfo? _bestSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final item =
|
|
|
|
daily.where((e) => e.score != null).fold<HealthSleepDayCalculation?>(
|
|
|
|
null,
|
|
|
|
(best, item) =>
|
|
|
|
best == null || item.score! > best.score! ? item : best,
|
|
|
|
);
|
|
|
|
return item == null
|
|
|
|
? null
|
|
|
|
: BestSleepInfo(
|
|
...
|
...
|
@@ -668,12 +714,15 @@ class LocalHealthDataConvert { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static WorstSleepInfo? _worstSleepInfo(List<_SleepDaySummary> daily) {
|
|
|
|
final item = daily.where((e) => e.score != null).fold<_SleepDaySummary?>(
|
|
|
|
null,
|
|
|
|
(worst, item) =>
|
|
|
|
worst == null || item.score! < worst.score! ? item : worst,
|
|
|
|
);
|
|
|
|
static WorstSleepInfo? _worstSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final item =
|
|
|
|
daily.where((e) => e.score != null).fold<HealthSleepDayCalculation?>(
|
|
|
|
null,
|
|
|
|
(worst, item) =>
|
|
|
|
worst == null || item.score! < worst.score! ? item : worst,
|
|
|
|
);
|
|
|
|
return item == null
|
|
|
|
? null
|
|
|
|
: WorstSleepInfo(
|
|
...
|
...
|
@@ -683,7 +732,9 @@ class LocalHealthDataConvert { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static EarliestSleepInfo? _earliestSleepInfo(List<_SleepDaySummary> daily) {
|
|
|
|
static EarliestSleepInfo? _earliestSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
..sort((a, b) => a.asleepTime!.compareTo(b.asleepTime!));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
...
|
...
|
@@ -695,132 +746,81 @@ class LocalHealthDataConvert { |
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static LatestSleepInfo? _latestSleepInfo(List<_SleepDaySummary> daily) {
|
|
|
|
final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
..sort((a, b) => b.asleepTime!.compareTo(a.asleepTime!));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
return item == null
|
|
|
|
? null
|
|
|
|
: LatestSleepInfo(
|
|
|
|
timeKey: dateKey(item.day),
|
|
|
|
asleepTime: item.asleepTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static _SleepSummary _sleepSummary(
|
|
|
|
List<HealthKitRawDataPoint> intervals,
|
|
|
|
int windowStart,
|
|
|
|
int windowEnd,
|
|
|
|
) {
|
|
|
|
var inBedMinutes = 0.0;
|
|
|
|
var asleepMinutes = 0.0;
|
|
|
|
var awakeMinutes = 0.0;
|
|
|
|
var coreMinutes = 0.0;
|
|
|
|
var deepMinutes = 0.0;
|
|
|
|
var remMinutes = 0.0;
|
|
|
|
var wakeCount = 0;
|
|
|
|
var earliestStart = windowEnd;
|
|
|
|
var latestEnd = windowStart;
|
|
|
|
|
|
|
|
for (final interval in intervals) {
|
|
|
|
final clippedStart = math.max(interval.startTime, windowStart);
|
|
|
|
final clippedEnd = math.min(interval.endTime, windowEnd);
|
|
|
|
final minutes = math.max(0, clippedEnd - clippedStart) / 60.0;
|
|
|
|
if (minutes <= 0) continue;
|
|
|
|
earliestStart = math.min(earliestStart, clippedStart);
|
|
|
|
latestEnd = math.max(latestEnd, clippedEnd);
|
|
|
|
switch (interval.dataType) {
|
|
|
|
case _sleepTypeInBed:
|
|
|
|
inBedMinutes += minutes;
|
|
|
|
break;
|
|
|
|
case _sleepTypeAwake:
|
|
|
|
awakeMinutes += minutes;
|
|
|
|
wakeCount += 1;
|
|
|
|
break;
|
|
|
|
case _sleepTypeAsleepCore:
|
|
|
|
coreMinutes += minutes;
|
|
|
|
asleepMinutes += minutes;
|
|
|
|
break;
|
|
|
|
case _sleepTypeAsleepDeep:
|
|
|
|
deepMinutes += minutes;
|
|
|
|
asleepMinutes += minutes;
|
|
|
|
break;
|
|
|
|
case _sleepTypeAsleepRem:
|
|
|
|
remMinutes += minutes;
|
|
|
|
asleepMinutes += minutes;
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (_isAsleepSleepType(interval.dataType)) {
|
|
|
|
asleepMinutes += minutes;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (inBedMinutes <= 0 && latestEnd > earliestStart) {
|
|
|
|
inBedMinutes = (latestEnd - earliestStart) / 60.0;
|
|
|
|
}
|
|
|
|
if (inBedMinutes <= 0) {
|
|
|
|
inBedMinutes = asleepMinutes + awakeMinutes;
|
|
|
|
}
|
|
|
|
|
|
|
|
return _SleepSummary(
|
|
|
|
timeInBedMinutes: inBedMinutes,
|
|
|
|
totalAsleepMinutes: asleepMinutes,
|
|
|
|
awakeMinutes: awakeMinutes,
|
|
|
|
coreMinutes: coreMinutes,
|
|
|
|
deepMinutes: deepMinutes,
|
|
|
|
remMinutes: remMinutes,
|
|
|
|
wakeCount: wakeCount,
|
|
|
|
sleepGoalMinutes: _sleepGoalMinutes,
|
|
|
|
static BestSleepInfo? _bestSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults, {
|
|
|
|
required bool best,
|
|
|
|
}) {
|
|
|
|
final item = sleepResults.fold<HealthRawSleepResult?>(
|
|
|
|
null,
|
|
|
|
(selected, item) {
|
|
|
|
if (selected == null) return item;
|
|
|
|
if (best && item.sleepScore > selected.sleepScore) return item;
|
|
|
|
if (!best && item.sleepScore < selected.sleepScore) return item;
|
|
|
|
return selected;
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if (item == null) return null;
|
|
|
|
return BestSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
totalTime: item.sleepMinutes * 60,
|
|
|
|
score: item.sleepScore,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _sleepScore(_SleepSummary sleep) {
|
|
|
|
if (sleep.timeInBedMinutes <= 0 || sleep.totalAsleepMinutes <= 0) return 0;
|
|
|
|
final durationRatio = math.min(
|
|
|
|
sleep.totalAsleepMinutes / sleep.sleepGoalMinutes,
|
|
|
|
1.0,
|
|
|
|
static WorstSleepInfo? _worstSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final item = sleepResults.fold<HealthRawSleepResult?>(
|
|
|
|
null,
|
|
|
|
(worst, item) =>
|
|
|
|
worst == null || item.sleepScore < worst.sleepScore ? item : worst,
|
|
|
|
);
|
|
|
|
final durationScore = durationRatio * 40;
|
|
|
|
final efficiency = sleep.totalAsleepMinutes / sleep.timeInBedMinutes;
|
|
|
|
final efficiencyScore = math.min(efficiency / 0.9, 1.0) * 25;
|
|
|
|
final deepRatio = sleep.deepMinutes / sleep.totalAsleepMinutes;
|
|
|
|
final deepScore = math.min(deepRatio / 0.18, 1.0) * 15;
|
|
|
|
final remRatio = sleep.remMinutes / sleep.totalAsleepMinutes;
|
|
|
|
final remScore = math.min(remRatio / 0.22, 1.0) * 10;
|
|
|
|
final awakePenalty = math.min(
|
|
|
|
sleep.wakeCount * 2 + sleep.awakeMinutes / 10,
|
|
|
|
10,
|
|
|
|
if (item == null) return null;
|
|
|
|
return WorstSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
totalTime: item.sleepMinutes * 60,
|
|
|
|
score: item.sleepScore,
|
|
|
|
);
|
|
|
|
final rawScore =
|
|
|
|
durationScore + efficiencyScore + deepScore + remScore - awakePenalty;
|
|
|
|
return _mappedSleepScore(rawScore);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int _mappedSleepScore(num rawScore) {
|
|
|
|
final clamped = rawScore.clamp(0, 100).toDouble();
|
|
|
|
final mapped = switch (clamped) {
|
|
|
|
< 64 => clamped / 64 * 60,
|
|
|
|
< 74 => 60 + (clamped - 64) / 10 * 25,
|
|
|
|
_ => math.min(math.max(86, 85 + (clamped - 74) / 26 * 15), 100),
|
|
|
|
};
|
|
|
|
return mapped.round().clamp(0, 100);
|
|
|
|
static EarliestSleepInfo? _earliestSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final sorted = [...sleepResults]
|
|
|
|
..sort((a, b) => a.startDate.compareTo(b.startDate));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
if (item == null) return null;
|
|
|
|
return EarliestSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
asleepTime: item.startDate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static num? _sleepEvaluate(int score) {
|
|
|
|
if (score <= 0) return null;
|
|
|
|
if (score >= 85) return 1;
|
|
|
|
if (score >= 60) return 2;
|
|
|
|
return 3;
|
|
|
|
static LatestSleepInfo? _latestSleepResultInfo(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final sorted = [...sleepResults]
|
|
|
|
..sort((a, b) => b.startDate.compareTo(a.startDate));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
if (item == null) return null;
|
|
|
|
return LatestSleepInfo(
|
|
|
|
timeKey: dateKey(dayOf(item.date)),
|
|
|
|
asleepTime: item.startDate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static bool _isAsleepSleepType(int dataType) {
|
|
|
|
return dataType == _sleepTypeAsleep ||
|
|
|
|
dataType == _sleepTypeAsleepUnspecified ||
|
|
|
|
dataType == _sleepTypeAsleepCore ||
|
|
|
|
dataType == _sleepTypeAsleepDeep ||
|
|
|
|
dataType == _sleepTypeAsleepRem;
|
|
|
|
static LatestSleepInfo? _latestSleepInfo(
|
|
|
|
List<HealthSleepDayCalculation> daily,
|
|
|
|
) {
|
|
|
|
final sorted = daily.where((e) => e.asleepTime != null).toList()
|
|
|
|
..sort((a, b) => b.asleepTime!.compareTo(a.asleepTime!));
|
|
|
|
final item = sorted.isEmpty ? null : sorted.first;
|
|
|
|
return item == null
|
|
|
|
? null
|
|
|
|
: LatestSleepInfo(
|
|
|
|
timeKey: dateKey(item.day),
|
|
|
|
asleepTime: item.asleepTime,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int? _modeState(Iterable<int> states) {
|
|
...
|
...
|
@@ -836,6 +836,40 @@ class LocalHealthDataConvert { |
|
|
|
return intervals.any((e) => time > e.startTime && time <= e.endTime);
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<HealthKitRawDataPoint> _sleepHeartRate(
|
|
|
|
List<HealthKitRawDataPoint> intervals,
|
|
|
|
List<HealthKitRawDataPoint> heartRate,
|
|
|
|
) {
|
|
|
|
return heartRate
|
|
|
|
.where((point) => intervals.any((interval) =>
|
|
|
|
point.endTime > interval.startTime &&
|
|
|
|
point.endTime <= interval.endTime))
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
static HealthRawSleepResult? _sleepResultForDay(
|
|
|
|
DateTime day,
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
) {
|
|
|
|
final start = unixSeconds(day);
|
|
|
|
final end = unixSeconds(day.add(const Duration(days: 1)));
|
|
|
|
final dayResults = sleepResults
|
|
|
|
.where((e) => e.date >= start && e.date < end)
|
|
|
|
.toList()
|
|
|
|
..sort((a, b) => a.date.compareTo(b.date));
|
|
|
|
return dayResults.isEmpty ? null : dayResults.last;
|
|
|
|
}
|
|
|
|
|
|
|
|
static List<HealthKitRawDataPoint> _sleepResultHeartRate(
|
|
|
|
List<HealthRawSleepResult> sleepResults,
|
|
|
|
List<HealthKitRawDataPoint> heartRate,
|
|
|
|
) {
|
|
|
|
return heartRate
|
|
|
|
.where((point) => sleepResults.any((result) =>
|
|
|
|
point.endTime > result.startDate && point.endTime <= result.date))
|
|
|
|
.toList();
|
|
|
|
}
|
|
|
|
|
|
|
|
static DateTime dateFromKey(int key) {
|
|
|
|
final year = key ~/ 10000;
|
|
|
|
final month = (key ~/ 100) % 100;
|
|
...
|
...
|
@@ -873,50 +907,6 @@ class LocalHealthDataConvert { |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SleepDaySummary {
|
|
|
|
const _SleepDaySummary({
|
|
|
|
required this.day,
|
|
|
|
required this.sleepIntervals,
|
|
|
|
required this.mergeSleepTimeRange,
|
|
|
|
required this.durationSeconds,
|
|
|
|
required this.asleepTime,
|
|
|
|
required this.score,
|
|
|
|
required this.evaluate,
|
|
|
|
required this.sleepHr,
|
|
|
|
});
|
|
|
|
|
|
|
|
final DateTime day;
|
|
|
|
final List<HealthKitRawDataPoint> sleepIntervals;
|
|
|
|
final HealthKitRawDataPoint? mergeSleepTimeRange;
|
|
|
|
final num durationSeconds;
|
|
|
|
final num? asleepTime;
|
|
|
|
final num? score;
|
|
|
|
final num? evaluate;
|
|
|
|
final List<HealthKitRawDataPoint> sleepHr;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _SleepSummary {
|
|
|
|
const _SleepSummary({
|
|
|
|
required this.timeInBedMinutes,
|
|
|
|
required this.totalAsleepMinutes,
|
|
|
|
required this.awakeMinutes,
|
|
|
|
required this.coreMinutes,
|
|
|
|
required this.deepMinutes,
|
|
|
|
required this.remMinutes,
|
|
|
|
required this.wakeCount,
|
|
|
|
required this.sleepGoalMinutes,
|
|
|
|
});
|
|
|
|
|
|
|
|
final double timeInBedMinutes;
|
|
|
|
final double totalAsleepMinutes;
|
|
|
|
final double awakeMinutes;
|
|
|
|
final double coreMinutes;
|
|
|
|
final double deepMinutes;
|
|
|
|
final double remMinutes;
|
|
|
|
final int wakeCount;
|
|
|
|
final double sleepGoalMinutes;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _ActivityDaySummary {
|
|
|
|
const _ActivityDaySummary({
|
|
|
|
required this.day,
|
...
|
...
|
|