@activitymerge/core 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +21 -2
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/dist/index.d.ts +515 -0
- package/dist/index.js +22385 -0
- package/package.json +30 -4
- package/schema/report.v1.json +87 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manuel Vogl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @activitymerge/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MIT. Local FIT merge for Garmin, Wahoo, COROS, Zwift, and Strava files. Callers pass bytes. The library does not read the filesystem and does not open a network connection.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm i @activitymerge/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`engineVersion` is `0.1.0`.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { merge } from '@activitymerge/core'
|
|
13
|
+
|
|
14
|
+
const result = await merge(
|
|
15
|
+
[{ name: 'a.fit', bytes: a }, { name: 'b.fit', bytes: b }],
|
|
16
|
+
{ gap: 'keep', order: 'start-time', deviceFrom: 0 },
|
|
17
|
+
)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`result.bytes` is the FIT file. `result.report` matches `schema/report.v1.json`. Coordinates are omitted unless `includeCoordinates` is true.
|
|
21
|
+
|
|
22
|
+
A checkout of the repository is the other way to build it: `npm ci` and `npm run build:packages`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
`@activitymerge/core` bundles the following third-party software in `dist/index.js`.
|
|
4
|
+
|
|
5
|
+
## fit-file-parser 6.2.3
|
|
6
|
+
|
|
7
|
+
https://github.com/jimmykane/fit-parser
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
The MIT License (MIT)
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2015 Pierre Jacquier
|
|
13
|
+
http://pierrejacquier.com | @pierremtb
|
|
14
|
+
|
|
15
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
16
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
17
|
+
in the Software without restriction, including without limitation the rights
|
|
18
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
19
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
20
|
+
furnished to do so, subject to the following conditions:
|
|
21
|
+
|
|
22
|
+
The above copyright notice and this permission notice shall be included in all
|
|
23
|
+
copies or substantial portions of the Software.
|
|
24
|
+
|
|
25
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
26
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
27
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
28
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
29
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
30
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
31
|
+
SOFTWARE.
|
|
32
|
+
|
|
33
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
/** Stable error codes. Exit class is decided by the CLI and MCP, not by the byte merge. */
|
|
2
|
+
export declare const ERROR_CODES: readonly [
|
|
3
|
+
"UNSUPPORTED_FORMAT",
|
|
4
|
+
"CORRUPT_FILE",
|
|
5
|
+
"OVERLAPPING_TIMERANGES",
|
|
6
|
+
"SPORT_MISMATCH",
|
|
7
|
+
"TOO_FEW_INPUTS",
|
|
8
|
+
"FILE_TOO_LARGE",
|
|
9
|
+
"DUPLICATE_INPUT",
|
|
10
|
+
"OUTPUT_EXISTS"
|
|
11
|
+
];
|
|
12
|
+
export type ErrorCode = (typeof ERROR_CODES)[number];
|
|
13
|
+
export declare class ActivityError extends Error {
|
|
14
|
+
readonly code: ErrorCode;
|
|
15
|
+
readonly hint: string;
|
|
16
|
+
readonly input?: number;
|
|
17
|
+
constructor(code: ErrorCode, message: string, hint: string, input?: number);
|
|
18
|
+
toJSON(): {
|
|
19
|
+
code: ErrorCode;
|
|
20
|
+
message: string;
|
|
21
|
+
hint: string;
|
|
22
|
+
input?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** 2 usage/input, 3 merge/data, 4 IO. */
|
|
26
|
+
export declare function exitFor(code: ErrorCode): 2 | 3 | 4;
|
|
27
|
+
declare const WARNING_CODES: readonly [
|
|
28
|
+
"overlap_trimmed",
|
|
29
|
+
"file_fully_overlapped",
|
|
30
|
+
"sport_mismatch",
|
|
31
|
+
"multiple_sessions",
|
|
32
|
+
"developer_fields_omitted",
|
|
33
|
+
"training_metrics_omitted",
|
|
34
|
+
"partial_calories",
|
|
35
|
+
"partial_ascent",
|
|
36
|
+
"partial_descent",
|
|
37
|
+
"missing_distance",
|
|
38
|
+
"laps_keep_original",
|
|
39
|
+
"time_not_monotonic"
|
|
40
|
+
];
|
|
41
|
+
export type WarningCode = (typeof WARNING_CODES)[number];
|
|
42
|
+
export interface MergeWarning {
|
|
43
|
+
code: WarningCode;
|
|
44
|
+
message: string;
|
|
45
|
+
fileName?: string;
|
|
46
|
+
count?: number;
|
|
47
|
+
}
|
|
48
|
+
/** One sample. Optional channels stay absent when that file did not record them. */
|
|
49
|
+
export interface TrackPoint {
|
|
50
|
+
timestamp: Date;
|
|
51
|
+
latitude?: number;
|
|
52
|
+
longitude?: number;
|
|
53
|
+
altitudeMeters?: number;
|
|
54
|
+
/** Cumulative meters from the start of the merged activity after continuity is applied. */
|
|
55
|
+
distanceMeters?: number;
|
|
56
|
+
speedMps?: number;
|
|
57
|
+
heartRate?: number;
|
|
58
|
+
power?: number;
|
|
59
|
+
/**
|
|
60
|
+
* FIT cadence as stored. Running and walking use half-cadence (strides/min),
|
|
61
|
+
* not steps/min. `fractionalCadence` is the fractional revolution, 0–1.
|
|
62
|
+
*/
|
|
63
|
+
cadence?: number;
|
|
64
|
+
fractionalCadence?: number;
|
|
65
|
+
temperatureCelsius?: number;
|
|
66
|
+
/** Raw FIT uint8, including the right-side bit. */
|
|
67
|
+
leftRightBalance?: number;
|
|
68
|
+
/** Raw FIT uint8. The profile scale is 2, so the stored byte is percent × 2. */
|
|
69
|
+
leftTorqueEffectiveness?: number;
|
|
70
|
+
rightTorqueEffectiveness?: number;
|
|
71
|
+
leftPedalSmoothness?: number;
|
|
72
|
+
rightPedalSmoothness?: number;
|
|
73
|
+
/** Raw FIT uint16. The profile scale is 100. */
|
|
74
|
+
stanceTimeBalance?: number;
|
|
75
|
+
/** Record fields we do not reinterpret. Multi-byte values are stored little-endian. */
|
|
76
|
+
extraFields?: RecordRawField[];
|
|
77
|
+
/** Developer-field bytes, stored little-endian when a field description gives the base type. */
|
|
78
|
+
developerFields?: DeveloperFieldSample[];
|
|
79
|
+
}
|
|
80
|
+
export interface RecordRawField {
|
|
81
|
+
fieldNumber: number;
|
|
82
|
+
baseType: number;
|
|
83
|
+
raw: number[];
|
|
84
|
+
}
|
|
85
|
+
export interface DeveloperDataId {
|
|
86
|
+
index: number;
|
|
87
|
+
manufacturerId?: number;
|
|
88
|
+
applicationVersion?: number;
|
|
89
|
+
applicationId?: number[];
|
|
90
|
+
}
|
|
91
|
+
export interface DeveloperFieldDescription {
|
|
92
|
+
developerDataIndex: number;
|
|
93
|
+
fieldNumber: number;
|
|
94
|
+
baseType: number;
|
|
95
|
+
name?: string;
|
|
96
|
+
units?: string;
|
|
97
|
+
scale?: number;
|
|
98
|
+
offset?: number;
|
|
99
|
+
}
|
|
100
|
+
export interface DeveloperFieldSample {
|
|
101
|
+
developerDataIndex: number;
|
|
102
|
+
fieldNumber: number;
|
|
103
|
+
raw: number[];
|
|
104
|
+
}
|
|
105
|
+
export interface TimerEvent {
|
|
106
|
+
timestamp: Date;
|
|
107
|
+
type: "start" | "stop" | "stop_all";
|
|
108
|
+
}
|
|
109
|
+
export interface TimeRange {
|
|
110
|
+
start: Date;
|
|
111
|
+
end: Date;
|
|
112
|
+
}
|
|
113
|
+
export interface ActivityLap {
|
|
114
|
+
startTime: Date;
|
|
115
|
+
endTime: Date;
|
|
116
|
+
elapsedSeconds: number;
|
|
117
|
+
timerSeconds: number;
|
|
118
|
+
distanceMeters: number;
|
|
119
|
+
calories?: number;
|
|
120
|
+
ascentMeters?: number;
|
|
121
|
+
descentMeters?: number;
|
|
122
|
+
avgHeartRate?: number;
|
|
123
|
+
maxHeartRate?: number;
|
|
124
|
+
avgPower?: number;
|
|
125
|
+
maxPower?: number;
|
|
126
|
+
avgCadence?: number;
|
|
127
|
+
maxCadence?: number;
|
|
128
|
+
avgSpeedMps?: number;
|
|
129
|
+
maxSpeedMps?: number;
|
|
130
|
+
startLatitude?: number;
|
|
131
|
+
startLongitude?: number;
|
|
132
|
+
endLatitude?: number;
|
|
133
|
+
endLongitude?: number;
|
|
134
|
+
sport?: string;
|
|
135
|
+
subSport?: string;
|
|
136
|
+
trigger?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface DeviceIdentity {
|
|
139
|
+
manufacturerId?: number;
|
|
140
|
+
manufacturerName?: string;
|
|
141
|
+
product?: number;
|
|
142
|
+
productName?: string;
|
|
143
|
+
serialNumber?: number;
|
|
144
|
+
softwareVersion?: number;
|
|
145
|
+
hardwareVersion?: number;
|
|
146
|
+
}
|
|
147
|
+
export interface ActivityFile {
|
|
148
|
+
sourceName?: string;
|
|
149
|
+
sport: string;
|
|
150
|
+
subSport: string;
|
|
151
|
+
records: TrackPoint[];
|
|
152
|
+
laps: ActivityLap[];
|
|
153
|
+
timerEvents: TimerEvent[];
|
|
154
|
+
device: DeviceIdentity;
|
|
155
|
+
sessionCount: number;
|
|
156
|
+
totalCalories?: number;
|
|
157
|
+
totalAscentMeters?: number;
|
|
158
|
+
totalDescentMeters?: number;
|
|
159
|
+
/** Sum of `session.total_distance`, meters. Absent when the file recorded none. */
|
|
160
|
+
sessionDistanceMeters?: number;
|
|
161
|
+
/** Sum of `session.total_timer_time`, seconds. */
|
|
162
|
+
sessionTimerSeconds?: number;
|
|
163
|
+
/** Highest `session.max_heart_rate`. */
|
|
164
|
+
sessionMaxHeartRate?: number;
|
|
165
|
+
hasDeveloperData: boolean;
|
|
166
|
+
hasTrainingMetrics: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* `local_timestamp - timestamp` from the FIT activity message, in milliseconds.
|
|
169
|
+
* Missing when the file has no local timestamp. Calendar days use this offset, not the browser zone.
|
|
170
|
+
*/
|
|
171
|
+
localOffsetMs?: number;
|
|
172
|
+
developerDataIds: DeveloperDataId[];
|
|
173
|
+
fieldDescriptions: DeveloperFieldDescription[];
|
|
174
|
+
}
|
|
175
|
+
export interface SessionTotals {
|
|
176
|
+
startTime: Date;
|
|
177
|
+
endTime: Date;
|
|
178
|
+
elapsedSeconds: number;
|
|
179
|
+
timerSeconds: number;
|
|
180
|
+
pauseSeconds: number;
|
|
181
|
+
distanceMeters: number;
|
|
182
|
+
distanceKnown: boolean;
|
|
183
|
+
calories?: number;
|
|
184
|
+
ascentMeters?: number;
|
|
185
|
+
descentMeters?: number;
|
|
186
|
+
avgHeartRate?: number;
|
|
187
|
+
maxHeartRate?: number;
|
|
188
|
+
avgPower?: number;
|
|
189
|
+
maxPower?: number;
|
|
190
|
+
/** Combined FIT cadence, including the fractional part. */
|
|
191
|
+
avgCadence?: number;
|
|
192
|
+
maxCadence?: number;
|
|
193
|
+
avgSpeedMps?: number;
|
|
194
|
+
maxSpeedMps?: number;
|
|
195
|
+
avgTemperatureCelsius?: number;
|
|
196
|
+
maxTemperatureCelsius?: number;
|
|
197
|
+
sport: string;
|
|
198
|
+
subSport: string;
|
|
199
|
+
}
|
|
200
|
+
export interface MergedActivity {
|
|
201
|
+
device: DeviceIdentity;
|
|
202
|
+
sport: string;
|
|
203
|
+
subSport: string;
|
|
204
|
+
records: TrackPoint[];
|
|
205
|
+
laps: ActivityLap[];
|
|
206
|
+
intervals: TimeRange[];
|
|
207
|
+
timerEvents: TimerEvent[];
|
|
208
|
+
session: SessionTotals;
|
|
209
|
+
warnings: MergeWarning[];
|
|
210
|
+
developerDataIds: DeveloperDataId[];
|
|
211
|
+
fieldDescriptions: DeveloperFieldDescription[];
|
|
212
|
+
}
|
|
213
|
+
export interface MergeOptions {
|
|
214
|
+
/**
|
|
215
|
+
* `start-time` sorts by the first record (default).
|
|
216
|
+
* `given` keeps the caller's order, which is what the web UI sends after a manual reorder.
|
|
217
|
+
*/
|
|
218
|
+
order?: "start-time" | "given";
|
|
219
|
+
/** Shift later files earlier so the clock gap disappears. Off by default: it changes timestamps. */
|
|
220
|
+
removeGap?: boolean;
|
|
221
|
+
/** Add the straight-line distance across the gap. Off by default. */
|
|
222
|
+
includeGapDistance?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* `trim-next` drops the later file’s overlapping points (default).
|
|
225
|
+
* `trim-previous` drops the earlier file’s tail instead.
|
|
226
|
+
*/
|
|
227
|
+
overlap?: "trim-next" | "trim-previous";
|
|
228
|
+
/** Index into the ordered inputs whose device and file id are written. Defaults to the first file that contributes records. */
|
|
229
|
+
deviceFrom?: number;
|
|
230
|
+
}
|
|
231
|
+
export interface MergeSummary {
|
|
232
|
+
startTime: string;
|
|
233
|
+
endTime: string;
|
|
234
|
+
elapsedSeconds: number;
|
|
235
|
+
timerSeconds: number;
|
|
236
|
+
pauseSeconds: number;
|
|
237
|
+
distanceMeters: number;
|
|
238
|
+
distanceKnown: boolean;
|
|
239
|
+
sport: string;
|
|
240
|
+
subSport: string;
|
|
241
|
+
recordCount: number;
|
|
242
|
+
lapCount: number;
|
|
243
|
+
deviceName?: string;
|
|
244
|
+
avgHeartRate?: number;
|
|
245
|
+
maxHeartRate?: number;
|
|
246
|
+
avgPower?: number;
|
|
247
|
+
maxPower?: number;
|
|
248
|
+
avgCadence?: number;
|
|
249
|
+
maxCadence?: number;
|
|
250
|
+
cadenceUnit?: "spm" | "rpm";
|
|
251
|
+
avgSpeedMps?: number;
|
|
252
|
+
maxSpeedMps?: number;
|
|
253
|
+
ascentMeters?: number;
|
|
254
|
+
avgTemperatureCelsius?: number;
|
|
255
|
+
maxTemperatureCelsius?: number;
|
|
256
|
+
warnings: MergeWarning[];
|
|
257
|
+
preview: TrackPreview;
|
|
258
|
+
/** Route, stats, and chart for the result card. Built from the source files, before overlap trimming. */
|
|
259
|
+
resultPreview: ResultPreview;
|
|
260
|
+
}
|
|
261
|
+
export interface ResultPreviewPart {
|
|
262
|
+
start: number;
|
|
263
|
+
end: number;
|
|
264
|
+
/** Device offset, milliseconds, for local clocks and calendar days. */
|
|
265
|
+
offsetMs: number;
|
|
266
|
+
distM?: number;
|
|
267
|
+
color: "trail" | "accent";
|
|
268
|
+
}
|
|
269
|
+
export interface ResultPreviewJoin {
|
|
270
|
+
/** Positive when the next part starts after this one ends. */
|
|
271
|
+
gapMs: number;
|
|
272
|
+
/** Positive when the next part starts before this one ends. */
|
|
273
|
+
overlapMs: number;
|
|
274
|
+
}
|
|
275
|
+
export interface ResultPreviewStats {
|
|
276
|
+
distM?: number;
|
|
277
|
+
elapsedS: number;
|
|
278
|
+
movingS?: number;
|
|
279
|
+
pauseS: number;
|
|
280
|
+
upM?: number;
|
|
281
|
+
downM?: number;
|
|
282
|
+
hrAvg?: number;
|
|
283
|
+
hrMax?: number;
|
|
284
|
+
powerAvg?: number;
|
|
285
|
+
}
|
|
286
|
+
export interface ResultPreviewSeries {
|
|
287
|
+
t: number[];
|
|
288
|
+
v: number[];
|
|
289
|
+
}
|
|
290
|
+
export interface ResultRoutePart {
|
|
291
|
+
d: string;
|
|
292
|
+
color: "trail" | "accent";
|
|
293
|
+
/** Coral outline for the section of this part that overlaps the previous one. */
|
|
294
|
+
overlapD?: string;
|
|
295
|
+
}
|
|
296
|
+
export interface ResultRouteGap {
|
|
297
|
+
/** Dashed segment. Absent when the join is shorter than 30 m. */
|
|
298
|
+
d?: string;
|
|
299
|
+
dot: [
|
|
300
|
+
number,
|
|
301
|
+
number
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
export interface ResultRoute {
|
|
305
|
+
viewBox: [
|
|
306
|
+
number,
|
|
307
|
+
number
|
|
308
|
+
];
|
|
309
|
+
parts: ResultRoutePart[];
|
|
310
|
+
gaps: ResultRouteGap[];
|
|
311
|
+
start: [
|
|
312
|
+
number,
|
|
313
|
+
number
|
|
314
|
+
];
|
|
315
|
+
finish: [
|
|
316
|
+
number,
|
|
317
|
+
number
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
export interface ResultPreviewWarning {
|
|
321
|
+
kind: "overlap";
|
|
322
|
+
separationMs: number;
|
|
323
|
+
/** 1-based index of the earlier part. */
|
|
324
|
+
prev: number;
|
|
325
|
+
/** 1-based index of the later part. */
|
|
326
|
+
next: number;
|
|
327
|
+
}
|
|
328
|
+
/** Sensor channels that actually had a sample. Power counts only above 0 W. */
|
|
329
|
+
export type KeptChannel = "hr" | "cadence" | "power" | "elevation" | "temperature";
|
|
330
|
+
export interface ResultPreview {
|
|
331
|
+
files: number;
|
|
332
|
+
parts: ResultPreviewPart[];
|
|
333
|
+
joins: ResultPreviewJoin[];
|
|
334
|
+
/** Moving-time seconds (join pauses removed) where the chart draws a gap marker. */
|
|
335
|
+
joinAt: number[];
|
|
336
|
+
route: ResultRoute | null;
|
|
337
|
+
stats: ResultPreviewStats;
|
|
338
|
+
series: {
|
|
339
|
+
ele?: ResultPreviewSeries;
|
|
340
|
+
hr?: ResultPreviewSeries;
|
|
341
|
+
pw?: ResultPreviewSeries;
|
|
342
|
+
};
|
|
343
|
+
/** Present channels, in display order. */
|
|
344
|
+
kept: KeptChannel[];
|
|
345
|
+
warning: ResultPreviewWarning | null;
|
|
346
|
+
}
|
|
347
|
+
export interface FileSummary {
|
|
348
|
+
sourceName?: string;
|
|
349
|
+
sport: string;
|
|
350
|
+
subSport: string;
|
|
351
|
+
startTime: string;
|
|
352
|
+
endTime: string;
|
|
353
|
+
/** FIT activity local-time offset. Absent when the file did not carry `local_timestamp`. */
|
|
354
|
+
localOffsetMs?: number;
|
|
355
|
+
elapsedSeconds: number;
|
|
356
|
+
recordCount: number;
|
|
357
|
+
lapCount: number;
|
|
358
|
+
distanceMeters?: number;
|
|
359
|
+
deviceName?: string;
|
|
360
|
+
avgHeartRate?: number;
|
|
361
|
+
maxHeartRate?: number;
|
|
362
|
+
avgPower?: number;
|
|
363
|
+
avgSpeedMps?: number;
|
|
364
|
+
timerSeconds: number;
|
|
365
|
+
ascentMeters?: number;
|
|
366
|
+
hasHeartRate: boolean;
|
|
367
|
+
hasPower: boolean;
|
|
368
|
+
hasCadence: boolean;
|
|
369
|
+
hasSpeed: boolean;
|
|
370
|
+
preview: TrackPreview;
|
|
371
|
+
}
|
|
372
|
+
/** Downsampled series for an offline map and chart. Coordinates are degrees. */
|
|
373
|
+
export interface TrackPreview {
|
|
374
|
+
hasGps: boolean;
|
|
375
|
+
path: Array<[
|
|
376
|
+
number,
|
|
377
|
+
number
|
|
378
|
+
]>;
|
|
379
|
+
heartRate: Array<[
|
|
380
|
+
number,
|
|
381
|
+
number
|
|
382
|
+
]>;
|
|
383
|
+
altitude: Array<[
|
|
384
|
+
number,
|
|
385
|
+
number
|
|
386
|
+
]>;
|
|
387
|
+
power: Array<[
|
|
388
|
+
number,
|
|
389
|
+
number
|
|
390
|
+
]>;
|
|
391
|
+
speed: Array<[
|
|
392
|
+
number,
|
|
393
|
+
number
|
|
394
|
+
]>;
|
|
395
|
+
}
|
|
396
|
+
export interface FitInput {
|
|
397
|
+
name?: string;
|
|
398
|
+
bytes: Uint8Array;
|
|
399
|
+
}
|
|
400
|
+
export interface MergeFitResult {
|
|
401
|
+
bytes: Uint8Array;
|
|
402
|
+
activity: MergedActivity;
|
|
403
|
+
summary: MergeSummary;
|
|
404
|
+
}
|
|
405
|
+
declare const ERROR_CODES$1: readonly [
|
|
406
|
+
"need_two_files",
|
|
407
|
+
"invalid_fit",
|
|
408
|
+
"no_records",
|
|
409
|
+
"empty_merge"
|
|
410
|
+
];
|
|
411
|
+
export type FitMergeErrorCode = (typeof ERROR_CODES$1)[number];
|
|
412
|
+
export declare class FitMergeError extends Error {
|
|
413
|
+
readonly code: FitMergeErrorCode;
|
|
414
|
+
readonly warnings: MergeWarning[];
|
|
415
|
+
constructor(code: FitMergeErrorCode, message: string, warnings?: MergeWarning[]);
|
|
416
|
+
}
|
|
417
|
+
export declare function readActivity(bytes: Uint8Array, sourceName?: string): Promise<ActivityFile>;
|
|
418
|
+
export declare function summarizeActivity(file: ActivityFile): FileSummary;
|
|
419
|
+
export declare function mergeFitFiles(inputs: readonly FitInput[], options?: MergeOptions): Promise<MergeFitResult>;
|
|
420
|
+
export interface SampleFitFile {
|
|
421
|
+
name: string;
|
|
422
|
+
bytes: Uint8Array;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Two short synthetic rides with a one-minute pause.
|
|
426
|
+
* Six fixes each (twelve in total) so the result preview treats them as a GPS track.
|
|
427
|
+
* Built in memory so the page never fetches a file.
|
|
428
|
+
*/
|
|
429
|
+
export declare function buildSampleFits(start?: Date): SampleFitFile[];
|
|
430
|
+
export type GapHandling = "keep" | "close" | "distance";
|
|
431
|
+
export interface ReportInput {
|
|
432
|
+
index: number;
|
|
433
|
+
name?: string;
|
|
434
|
+
sport: string;
|
|
435
|
+
start: string;
|
|
436
|
+
end: string;
|
|
437
|
+
durationS: number;
|
|
438
|
+
distanceM: number;
|
|
439
|
+
records: number;
|
|
440
|
+
laps: number;
|
|
441
|
+
sensors: string[];
|
|
442
|
+
coordinates?: Array<{
|
|
443
|
+
latitude: number;
|
|
444
|
+
longitude: number;
|
|
445
|
+
}>;
|
|
446
|
+
}
|
|
447
|
+
export interface ActivityReport {
|
|
448
|
+
engineVersion: string;
|
|
449
|
+
inputs: ReportInput[];
|
|
450
|
+
output: {
|
|
451
|
+
format: "fit";
|
|
452
|
+
records: number;
|
|
453
|
+
laps: number;
|
|
454
|
+
durationS: number;
|
|
455
|
+
distanceM: number;
|
|
456
|
+
bytes: number;
|
|
457
|
+
};
|
|
458
|
+
gaps: Array<{
|
|
459
|
+
afterInput: number;
|
|
460
|
+
seconds: number;
|
|
461
|
+
handling: GapHandling;
|
|
462
|
+
}>;
|
|
463
|
+
warnings: Array<{
|
|
464
|
+
code: string;
|
|
465
|
+
message: string;
|
|
466
|
+
}>;
|
|
467
|
+
}
|
|
468
|
+
export interface InspectResult {
|
|
469
|
+
engineVersion: string;
|
|
470
|
+
input: ReportInput;
|
|
471
|
+
}
|
|
472
|
+
export declare function inspect(bytes: Uint8Array, name?: string, includeCoordinates?: boolean): Promise<InspectResult>;
|
|
473
|
+
export declare function validate(bytes: Uint8Array, name?: string): Promise<{
|
|
474
|
+
ok: true;
|
|
475
|
+
engineVersion: string;
|
|
476
|
+
records: number;
|
|
477
|
+
}>;
|
|
478
|
+
/** Spec limits. The browser tool keeps its own 50 MiB check and is not routed through this. */
|
|
479
|
+
export declare const MAX_FILE_BYTES = 50000000;
|
|
480
|
+
export declare const MAX_TOTAL_BYTES = 200000000;
|
|
481
|
+
export declare const MAX_FILES = 20;
|
|
482
|
+
export declare function assertMergeInputs(inputs: readonly {
|
|
483
|
+
bytes: Uint8Array;
|
|
484
|
+
}[]): void;
|
|
485
|
+
export interface FitPart {
|
|
486
|
+
name?: string;
|
|
487
|
+
bytes: Uint8Array;
|
|
488
|
+
}
|
|
489
|
+
export interface CoreMergeOptions {
|
|
490
|
+
/**
|
|
491
|
+
* `keep` leaves the pause (the browser default).
|
|
492
|
+
* `close` shifts later timestamps. `distance` adds straight-line distance.
|
|
493
|
+
*/
|
|
494
|
+
gap?: GapHandling;
|
|
495
|
+
/** `given` is what the web worker sends after the page has ordered the files. */
|
|
496
|
+
order?: "start-time" | "given";
|
|
497
|
+
overlap?: "trim-next" | "trim-previous";
|
|
498
|
+
/** The worker always sends 0. Omit only when calling the library default. */
|
|
499
|
+
deviceFrom?: number;
|
|
500
|
+
includeCoordinates?: boolean;
|
|
501
|
+
}
|
|
502
|
+
export interface CoreMergeResult {
|
|
503
|
+
bytes: Uint8Array;
|
|
504
|
+
summary: Awaited<ReturnType<typeof mergeFitFiles>>["summary"];
|
|
505
|
+
report: ActivityReport;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Same FIT bytes as `mergeFitFiles` for the same options.
|
|
509
|
+
* `gap=close` forces distance-across-gap off, matching the web worker.
|
|
510
|
+
*/
|
|
511
|
+
export declare function merge(inputs: readonly FitPart[], options?: CoreMergeOptions): Promise<CoreMergeResult>;
|
|
512
|
+
/** Matches packages/core/package.json. The report field engineVersion uses this string. */
|
|
513
|
+
export declare const engineVersion = "0.1.0";
|
|
514
|
+
|
|
515
|
+
export {};
|