wangzhibo
4 天以前 ae6f40460dcd56af6c5f60ba52c883854c3bac55
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
 
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ALTITUDE_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CADENCE_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.DISTANCE_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STEPS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_AVG;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_MAX;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STRIDE_MIN;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS_PER_SECOND;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SPM;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STEPS;
 
import android.content.Context;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.io.File;
import java.util.List;
 
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.util.Accumulator;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
 
public class BangleJSWorkoutParser implements ActivitySummaryParser {
    private static final Logger LOG = LoggerFactory.getLogger(BangleJSWorkoutParser.class);
 
    private final Context mContext;
 
    public BangleJSWorkoutParser(final Context context) {
        this.mContext = context;
    }
 
    @Override
    public BaseActivitySummary parseBinaryData(final BaseActivitySummary summary, final boolean forDetails) {
        if (!forDetails) {
            // Re-parsing the csv is too slow for summary
            return summary;
        }
 
        if (summary.getRawDetailsPath() == null) {
            return summary;
        }
 
        final File inputFile = FileUtils.tryFixPath(new File(summary.getRawDetailsPath()));
        if (inputFile == null) {
            return summary;
        }
 
        final List<BangleJSActivityPoint> points = BangleJSActivityPoint.fromCsv(inputFile);
        if (points == null) {
            return summary;
        }
 
        summary.setSummaryData(dataFromPoints(points).toString());
        return summary;
    }
 
    public static ActivitySummaryData dataFromPoints(final List<BangleJSActivityPoint> points) {
        final Accumulator accHeartRate = new Accumulator();
        final Accumulator accSpeed = new Accumulator();
        final Accumulator accAltitude = new Accumulator();
        final Accumulator accStride = new Accumulator();
        double totalDistance = 0;
        int totalSteps = 0;
        long totalTime = 0;
        long totalActiveTime = 0;
        boolean hasGps = false;
 
        final ActivityUser activityUser = new ActivityUser();
 
        BangleJSActivityPoint previousPoint = null;
        for (final BangleJSActivityPoint p : points) {
            if (p.getHeartRate() > 0) {
                accHeartRate.add(p.getHeartRate());
            }
            final long timeDiff = previousPoint != null ? p.getTime() - previousPoint.getTime() : 0;
            double distanceDiff;
            // FIXME: GPS data can be missing for some entries which is handled here.
            //  Should use more complex logic to be more accurate. Use interpolation.
            //  Should distances be done via the GPX file we generate instead?
            if (previousPoint != null && previousPoint.getLocation() != null && p.getLocation() != null) {
                distanceDiff = p.getLocation().getDistance(previousPoint.getLocation());
                hasGps = true;
            } else {
                distanceDiff = p.getSteps() * activityUser.getStepLengthCm() * 0.01d;
            }
            if (p.getSteps() > 0) {
                accStride.add(distanceDiff / p.getSteps());
            }
            totalTime += timeDiff;
            totalDistance += distanceDiff;
            if (distanceDiff != 0) {
                totalActiveTime += timeDiff;
            }
            if (timeDiff > 0) {
                accSpeed.add(distanceDiff / (timeDiff / 1000d));
            }
 
            totalSteps += p.getSteps();
 
            previousPoint = p;
        }
 
        final ActivitySummaryData summaryData = new ActivitySummaryData();
        if (totalDistance != 0) {
            summaryData.add(DISTANCE_METERS, (float) totalDistance, UNIT_METERS);
        }
        if (totalActiveTime > 0) {
            summaryData.add(ACTIVE_SECONDS, Math.round(totalActiveTime / 1000d), UNIT_SECONDS);
        }
 
        if (totalSteps != 0) {
            summaryData.add(STEPS, totalSteps, UNIT_STEPS);
        }
        if (accHeartRate.getCount() > 0) {
            summaryData.add(HR_AVG, accHeartRate.getAverage(), UNIT_BPM);
            summaryData.add(HR_MAX, (int) accHeartRate.getMax(), UNIT_BPM);
            summaryData.add(HR_MIN, (int) accHeartRate.getMin(), UNIT_BPM);
        }
        if (accStride.getCount() > 0) {
            summaryData.add(STRIDE_AVG, accStride.getAverage(), UNIT_METERS);
            summaryData.add(STRIDE_MAX, accStride.getMax(), UNIT_METERS);
            summaryData.add(STRIDE_MIN, accStride.getMin(), UNIT_METERS);
        }
        if (accSpeed.getCount() > 0) {
            summaryData.add(SPEED_AVG, accSpeed.getAverage(), UNIT_METERS_PER_SECOND);
            summaryData.add(SPEED_MAX, accSpeed.getMax(), UNIT_METERS_PER_SECOND);
            summaryData.add(SPEED_MIN, accSpeed.getMin(), UNIT_METERS_PER_SECOND);
        }
        if (accAltitude.getCount() != 0) {
            summaryData.add(ALTITUDE_MAX, accAltitude.getMax(), UNIT_METERS);
            summaryData.add(ALTITUDE_MIN, accAltitude.getMin(), UNIT_METERS);
            summaryData.add(ALTITUDE_AVG, accAltitude.getAverage(), UNIT_METERS);
        }
 
        if (totalTime > 0) {
            // FIXME: Should cadence be steps/min or half that? https://www.polar.com/blog/what-is-running-cadence/
            //  The Bangle.js App Loader has Cadence = (steps/min)/2,  https://github.com/espruino/BangleApps/blob/master/apps/recorder/interface.html#L103,
            //  as discussed here: https://github.com/espruino/BangleApps/pull/3068#issuecomment-1790293879 .
            summaryData.add(CADENCE_AVG, 0.5 * 60 * totalSteps / (totalTime / 1000d), UNIT_SPM);
        }
 
        // TODO: Implement hrZones by doing calculations on Gadgetbridge side or make Bangle.js report
        //  this (Karvonen method implemented to a degree in watch app "Run+")?
 
        // TODO: Does Bangle.js report laps in recorder logs?
 
        summaryData.add(INTERNAL_HAS_GPS, String.valueOf(hasGps));
 
        return summaryData;
    }
}