File size: 10,487 Bytes
35527e2
 
 
f63ce21
35527e2
d831612
35527e2
 
 
 
 
 
f63ce21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35527e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b27fe22
35527e2
 
 
 
 
 
 
f63ce21
 
 
 
 
 
b27fe22
f63ce21
 
 
 
 
35527e2
 
 
b27fe22
35527e2
f63ce21
35527e2
 
 
 
 
 
 
 
 
 
 
 
 
 
bccd229
 
 
d831612
 
 
 
b27fe22
d831612
 
 
 
 
 
 
 
b27fe22
d831612
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0f92957
 
 
 
b27fe22
0f92957
 
 
 
 
35527e2
 
 
bccd229
35527e2
 
 
0f92957
d831612
 
 
35527e2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
import Papa from 'papaparse';
import type { Activity } from '@/types';
import { calculateTSS } from './tssCalculator';
import { validateCSV } from './csvValidator';

export function parseCSV(file: File, userFTP: number = 343, thresholdHR: number = 170, restingHR: number = 50): Promise<Activity[]> {
    return new Promise((resolve, reject) => {
        Papa.parse(file, {
            header: true,
            skipEmptyLines: true,
            complete: (results) => {
                try {
                    // Validate CSV structure first
                    const validation = validateCSV(results.data);

                    if (!validation.isValid) {
                        const errorMessage = [
                            'CSV Validation Failed:',
                            '',
                            ...validation.errors.map(err => `❌ ${err}`),
                            ...(validation.warnings.length > 0 ? ['', 'Warnings:', ...validation.warnings.map(warn => `⚠️ ${warn}`)] : []),
                        ].join('\n');
                        reject(new Error(errorMessage));
                        return;
                    }

                    // Log warnings to console if any
                    if (validation.warnings.length > 0) {
                        console.warn('CSV Validation Warnings:', validation.warnings);
                    }

                    const activities = results.data.map((row: any) => {
                        // Parse date - Garmin format: "2025-12-02 09:19:49"
                        const dateStr = row['Date'] || row['Activity Date'] || row['date'];
                        if (!dateStr || dateStr === '--') {
                            return null;
                        }
                        const date = new Date(dateStr);
                        if (isNaN(date.getTime())) {
                            return null;
                        }

                        // Parse distance - Garmin format: "8.34" (in km)
                        let distance: number | undefined;
                        const distanceStr = row['Distance'] || row['distance'];
                        if (distanceStr && distanceStr !== '--' && distanceStr !== '0.00') {
                            const parsed = parseFloat(distanceStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                distance = parsed;
                            }
                        }

                        // Parse duration - Garmin format: "00:48:51" (HH:MM:SS)
                        let duration: number | undefined;
                        let durationSeconds: number | undefined;
                        const durationStr = row['Time'] || row['Duration'] || row['Moving Time'] || row['Elapsed Time'];
                        if (durationStr && durationStr !== '--') {
                            if (durationStr.includes(':')) {
                                const parts = durationStr.split(':');
                                const hours = parseInt(parts[0]) || 0;
                                const minutes = parseInt(parts[1]) || 0;
                                const seconds = parseFloat(parts[2]) || 0;
                                duration = hours * 60 + minutes + seconds / 60;
                                durationSeconds = hours * 3600 + minutes * 60 + seconds;
                            } else {
                                const parsed = parseFloat(durationStr);
                                if (!isNaN(parsed)) {
                                    duration = parsed;
                                    durationSeconds = parsed * 60; // Assume minutes if no colon
                                }
                            }
                        }

                        // Parse TSS - Try to calculate from power data first
                        let trainingStressScore: number | undefined;

                        // First, check if TSS is already provided in CSV
                        const tssStr = row['Training Stress Score®'] || row['Training Stress Score'] || row['TSS'];
                        if (tssStr && tssStr !== '--' && tssStr !== '0.0' && tssStr !== '0') {
                            const parsed = parseFloat(tssStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                trainingStressScore = parsed;
                            }
                        }

                        // If no TSS provided, try to calculate from power data
                        if (!trainingStressScore && durationSeconds) {
                            // Parse Normalized Power (NP) - find column that starts with "Normalized Power"
                            let normalizedPower: number | undefined;
                            const npKey = Object.keys(row).find(key => key.startsWith('Normalized Power'));
                            if (npKey) {
                                const npStr = row[npKey];
                                if (npStr && npStr !== '--' && npStr !== '0.0' && npStr !== '0') {
                                    normalizedPower = parseFloat(npStr.replace(/,/g, ''));
                                    if (isNaN(normalizedPower) || normalizedPower <= 0) {
                                        normalizedPower = undefined;
                                    }
                                }
                            }

                            // Use FTP from CSV if available, otherwise use user-provided FTP
                            const ftpStr = row['FTP'] || row['Functional Threshold Power'];
                            const ftp = ftpStr && ftpStr !== '--' ? parseFloat(ftpStr.replace(/,/g, '')) : userFTP;

                            if (normalizedPower && normalizedPower > 0 && ftp && ftp > 0) {
                                const calculatedTSS = calculateTSS({
                                    durationSeconds,
                                    normalizedPower,
                                    ftp,
                                });
                                if (calculatedTSS !== undefined) {
                                    trainingStressScore = calculatedTSS;
                                }
                            }
                        }

                        // Get activity type
                        const activityType = row['Activity Type'] || row['Type'] || row['Sport'];

                        // Get activity title
                        const title = row['Title'] || row['Activity Name'] || row['Name'];

                        // Parse heart rate data
                        let averageHR: number | undefined;
                        const avgHRStr = row['Avg HR'] || row['Average HR'] || row['Average Heart Rate'];
                        if (avgHRStr && avgHRStr !== '--') {
                            const parsed = parseFloat(avgHRStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                averageHR = parsed;
                            }
                        }

                        let maxHR: number | undefined;
                        const maxHRStr = row['Max HR'] || row['Maximum HR'] || row['Max Heart Rate'];
                        if (maxHRStr && maxHRStr !== '--') {
                            const parsed = parseFloat(maxHRStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                maxHR = parsed;
                            }
                        }

                        // Parse total ascent
                        let totalAscent: number | undefined;
                        const ascentStr = row['Total Ascent'] || row['Ascent'] || row['Elevation Gain'];
                        if (ascentStr && ascentStr !== '--') {
                            const parsed = parseFloat(ascentStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                totalAscent = parsed;
                            }
                        }

                        // If no TSS calculated from power, try hrTSS from heart rate
                        if (!trainingStressScore && durationSeconds && averageHR) {
                            // hrTSS = duration_hours × HR_ratio² × 100
                            // HR_ratio = (avg_HR - resting_HR) / (threshold_HR - resting_HR)
                            if (thresholdHR > restingHR && averageHR > restingHR && averageHR < 220) {
                                const hrRatio = (averageHR - restingHR) / (thresholdHR - restingHR);
                                const durationHours = durationSeconds / 3600;
                                trainingStressScore = durationHours * Math.pow(hrRatio, 2) * 100;
                            }
                        }

                        // Parse calories
                        const caloriesStr = row['Calories'];
                        let calories: number | undefined;
                        if (caloriesStr && caloriesStr !== '--') {
                            const parsed = parseFloat(caloriesStr.replace(/,/g, ''));
                            if (!isNaN(parsed) && parsed > 0) {
                                calories = parsed;
                            }
                        }

                        const activity: Activity = {
                            date,
                            activityType,
                            title,
                            distance,
                            duration,
                            trainingStressScore,
                            calories,
                            averageHR,
                            maxHR,
                            totalAscent,
                        };

                        return activity;
                    }).filter((activity): activity is Activity => activity !== null);

                    // Sort by date
                    activities.sort((a, b) => a.date.getTime() - b.date.getTime());

                    resolve(activities);
                } catch (error) {
                    reject(new Error('Failed to parse CSV data'));
                }
            },
            error: (error) => {
                reject(new Error(`CSV parsing error: ${error.message}`));
            },
        });
    });
}