trackswap 0.0.26 → 0.2.0
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/README.md +124 -431
- package/dist/FIT/base.d.ts +61 -0
- package/dist/FIT/course-plan.d.ts +3 -0
- package/dist/FIT/crc.d.ts +1 -0
- package/dist/FIT/decoder.d.ts +33 -0
- package/dist/FIT/encoder.d.ts +45 -0
- package/dist/FIT/encoderCourse.d.ts +37 -0
- package/dist/FIT/encoderWorkout.d.ts +41 -0
- package/dist/FIT/health.d.ts +121 -0
- package/dist/FIT/index.d.ts +15 -0
- package/dist/FIT/mesg.d.ts +37 -0
- package/dist/FIT/middleware.d.ts +33 -0
- package/dist/FIT/plugins.d.ts +17 -0
- package/dist/FIT/processor.d.ts +34 -0
- package/dist/FIT/types/checksum_type.d.ts +4 -0
- package/dist/FIT/types/course_point.d.ts +56 -0
- package/dist/FIT/types/date_time_type.d.ts +3 -0
- package/dist/FIT/types/event.d.ts +49 -0
- package/dist/FIT/types/event_type.d.ts +18 -0
- package/dist/FIT/types/file.d.ts +23 -0
- package/dist/FIT/types/file_flags_type.d.ts +5 -0
- package/dist/FIT/types/index.d.ts +119 -0
- package/dist/FIT/types/local_date_time_type.d.ts +3 -0
- package/dist/FIT/types/mesg_count_type.d.ts +5 -0
- package/dist/FIT/types/mesg_num.d.ts +125 -0
- package/dist/FIT/types/message_index_type.d.ts +4558 -0
- package/dist/FIT/types/sport.d.ts +47 -0
- package/dist/FIT/types.d.ts +516 -0
- package/dist/FIT/util.d.ts +1 -0
- package/dist/GPX/base.d.ts +16 -0
- package/dist/GPX/converters.d.ts +62 -0
- package/dist/GPX/decoder.d.ts +43 -0
- package/dist/GPX/encoder.d.ts +45 -0
- package/dist/GPX/extensions/index.d.ts +14 -0
- package/dist/GPX/index.d.ts +13 -0
- package/dist/GPX/middleware.d.ts +18 -0
- package/dist/GPX/processor.d.ts +30 -0
- package/dist/GPX/types.d.ts +124 -0
- package/dist/GPX/util.d.ts +1 -0
- package/dist/TCX/base.d.ts +19 -0
- package/dist/TCX/converters.d.ts +52 -0
- package/dist/TCX/decoder.d.ts +33 -0
- package/dist/TCX/encoder.d.ts +46 -0
- package/dist/TCX/index.d.ts +7 -0
- package/dist/TCX/middleware.d.ts +25 -0
- package/dist/TCX/processor.d.ts +39 -0
- package/dist/TCX/types.d.ts +192 -0
- package/dist/TrackSwap.d.ts +16 -0
- package/dist/activity/base.d.ts +62 -0
- package/dist/activity/converters.d.ts +40 -0
- package/dist/activity/encoders.d.ts +36 -0
- package/dist/activity/index.d.ts +17 -0
- package/dist/activity/metrics/MetricsCalculator.d.ts +18 -0
- package/dist/activity/metrics/index.d.ts +1 -0
- package/dist/activity/processor.d.ts +19 -0
- package/dist/core/base.d.ts +73 -0
- package/dist/core/builtin-adapters.d.ts +52 -0
- package/dist/core/format-adapter.d.ts +23 -0
- package/dist/core/format-registry.d.ts +9 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/xml-utils.d.ts +17 -0
- package/dist/document.d.ts +57 -0
- package/dist/errors.d.ts +12 -0
- package/dist/fit-debug.d.ts +3 -0
- package/dist/index.cjs +29 -26
- package/dist/index.d.ts +9 -0
- package/dist/index.js +20 -17
- package/dist/types.d.ts +76 -0
- package/dist/util.d.ts +25 -0
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -1,455 +1,148 @@
|
|
|
1
|
-
# TrackSwap
|
|
1
|
+
# TrackSwap
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TrackSwap 是一个面向 Node.js 和 TypeScript 的运动、轨迹与健康数据工具包。它使用统一文档模型读取 FIT、GPX 和 TCX,并通过格式适配器隔离各协议的解析与编码实现。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 安装
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## 🚀 特性
|
|
12
|
-
|
|
13
|
-
- 🔄 **多格式支持**: GPX 1.1、FIT、TCX 格式
|
|
14
|
-
- 🎯 **统一转换**: 任意格式之间相互转换
|
|
15
|
-
- 🤖 **自动检测**: 智能识别文件格式
|
|
16
|
-
- 🧩 **扩展系统**: 灵活的扩展架构,支持自定义数据处理
|
|
17
|
-
- 📊 **智能聚合**: 默认启用运动数据聚合(心率、功率、速度等)
|
|
18
|
-
- 🛡️ **类型安全**: 完整的 TypeScript 支持
|
|
19
|
-
- ⚡ **高性能**: 优化的解析和编码算法
|
|
20
|
-
|
|
21
|
-
## 📦 安装
|
|
22
|
-
|
|
23
|
-
```bash
|
|
7
|
+
~~~bash
|
|
24
8
|
npm install trackswap
|
|
25
|
-
|
|
9
|
+
~~~
|
|
26
10
|
|
|
27
|
-
|
|
11
|
+
需要 Node.js 18 或更高版本。
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
## 核心模型
|
|
30
14
|
|
|
31
|
-
|
|
32
|
-
import
|
|
33
|
-
import fs from 'fs';
|
|
15
|
+
~~~ts
|
|
16
|
+
import TrackSwap from "trackswap";
|
|
34
17
|
|
|
35
18
|
const trackSwap = new TrackSwap();
|
|
36
19
|
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
const gpxData = await trackSwap.parseGPX(gpxBuffer);
|
|
40
|
-
|
|
41
|
-
// 解析 FIT 文件
|
|
42
|
-
const fitBuffer = fs.readFileSync('activity.fit');
|
|
43
|
-
const fitData = await trackSwap.parseFIT(fitBuffer);
|
|
44
|
-
|
|
45
|
-
// 解析 TCX 文件
|
|
46
|
-
const tcxBuffer = fs.readFileSync('workout.tcx');
|
|
47
|
-
const tcxData = await trackSwap.parseTCX(tcxBuffer);
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### 格式转换
|
|
51
|
-
|
|
52
|
-
```typescript
|
|
53
|
-
// 🔄 自动检测格式并转换
|
|
54
|
-
const gpxBuffer = await trackSwap.convertFile(anyBuffer, 'gpx');
|
|
55
|
-
const fitBuffer = await trackSwap.convertFile(anyBuffer, 'fit');
|
|
56
|
-
const tcxBuffer = await trackSwap.convertFile(anyBuffer, 'tcx');
|
|
57
|
-
|
|
58
|
-
// 🎯 指定源格式转换(提升性能)
|
|
59
|
-
const convertedBuffer = await trackSwap.convertFile(
|
|
60
|
-
sourceBuffer,
|
|
61
|
-
'fit', // 目标格式
|
|
62
|
-
'gpx' // 源格式(可选)
|
|
63
|
-
);
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### 自动解析
|
|
67
|
-
|
|
68
|
-
```typescript
|
|
69
|
-
// 🤖 自动检测格式并解析
|
|
70
|
-
const parsedData = await trackSwap.parseFile(buffer);
|
|
71
|
-
|
|
72
|
-
// 🏃♂️ 解析并转换为统一的运动数据格式
|
|
73
|
-
const sportData = await trackSwap.parseToActivity(buffer);
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
### 扩展系统
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
import { TrackSwap } from 'trackswap';
|
|
80
|
-
import { SlopeExtension } from 'trackswap/extensions';
|
|
81
|
-
|
|
82
|
-
// 🧩 使用扩展增强功能
|
|
83
|
-
const trackSwap = new TrackSwap()
|
|
84
|
-
.extend(new SlopeExtension()); // 添加坡度计算
|
|
85
|
-
|
|
86
|
-
// 📊 默认已启用基础指标聚合(心率、功率、速度、踏频等)
|
|
87
|
-
const result = await trackSwap.parseGPX(gpxBuffer);
|
|
88
|
-
// result 包含完整的聚合数据和坡度信息
|
|
89
|
-
|
|
90
|
-
// 🔗 链式添加多个扩展
|
|
91
|
-
const enhancedTrackSwap = new TrackSwap()
|
|
92
|
-
.extend(new SlopeExtension())
|
|
93
|
-
.extend(new CustomExtension());
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
## 📖 详细使用指南
|
|
97
|
-
|
|
98
|
-
### 扩展系统
|
|
99
|
-
|
|
100
|
-
详细的扩展开发和使用指南请参考:[扩展系统文档](EXTENSIONS.md)
|
|
101
|
-
|
|
102
|
-
### 1. 文件解析
|
|
103
|
-
|
|
104
|
-
#### GPX 文件处理
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
// 从 Buffer 解析
|
|
108
|
-
const gpxData = await trackSwap.parseGPX(buffer);
|
|
20
|
+
try {
|
|
21
|
+
const document = await trackSwap.decode(buffer);
|
|
109
22
|
|
|
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
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
23
|
+
console.log(document.format);
|
|
24
|
+
console.log(document.kinds);
|
|
25
|
+
console.log(document.facets.activity);
|
|
26
|
+
console.log(document.facets.health);
|
|
27
|
+
console.log(document.metadata);
|
|
28
|
+
} finally {
|
|
29
|
+
await trackSwap.dispose();
|
|
30
|
+
}
|
|
31
|
+
~~~
|
|
32
|
+
|
|
33
|
+
`decode()` 默认不会返回格式原生对象。只有调试或协议级处理确实需要时才启用:
|
|
34
|
+
|
|
35
|
+
~~~ts
|
|
36
|
+
const document = await trackSwap.decode(buffer, {
|
|
37
|
+
format: "fit",
|
|
38
|
+
includeNative: true,
|
|
39
|
+
fit: {
|
|
40
|
+
includeUnknownMessages: true,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
~~~
|
|
44
|
+
|
|
45
|
+
## 活动读取、编码与转换
|
|
46
|
+
|
|
47
|
+
~~~ts
|
|
48
|
+
const activity = await trackSwap.decodeActivity(input);
|
|
49
|
+
|
|
50
|
+
const gpx = await trackSwap.encodeActivity(activity, { format: "gpx" });
|
|
51
|
+
const fit = await trackSwap.encodeActivity(activity, { format: "fit" });
|
|
52
|
+
const tcx = await trackSwap.encodeActivity(activity, { format: "tcx" });
|
|
53
|
+
|
|
54
|
+
const converted = await trackSwap.transcode(input, {
|
|
55
|
+
sourceFormat: "gpx",
|
|
56
|
+
format: "tcx",
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const course = await trackSwap.encodeCourse(input);
|
|
60
|
+
~~~
|
|
61
|
+
|
|
62
|
+
## FIT 健康数据
|
|
63
|
+
|
|
64
|
+
FIT 文件只解码一次,活动与健康数据作为同一 `TrackDocument` 的 facet 返回。健康结构不直接绑定 Garmin Connect JSON,使用稳定的中间表示:
|
|
65
|
+
|
|
66
|
+
- `points`:心率、压力、呼吸率、血氧、Body Battery、HRV、温度等时间点;
|
|
67
|
+
- `intervals`:步数、距离、热量和活动累计量;
|
|
68
|
+
- `sessions`:睡眠、小睡和 Health Snapshot;
|
|
69
|
+
- `summaries`:日汇总、睡眠汇总及设备汇总;
|
|
70
|
+
- `diagnostics`:FIT SDK profile、无效值、未知消息和解析警告。
|
|
71
|
+
|
|
72
|
+
~~~ts
|
|
73
|
+
const document = await trackSwap.decode(fitBuffer, {
|
|
74
|
+
fit: {
|
|
75
|
+
includeDeveloperFields: true,
|
|
76
|
+
health: {
|
|
77
|
+
reconstructIntervals: true,
|
|
78
|
+
retainCumulativeValues: false,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
~~~
|
|
83
|
+
|
|
84
|
+
## 错误处理
|
|
85
|
+
|
|
86
|
+
所有公共操作使用 `TrackSwapError`,可通过稳定错误码分类:
|
|
87
|
+
|
|
88
|
+
~~~ts
|
|
89
|
+
import { TrackSwapError } from "trackswap";
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
await trackSwap.decode(input);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof TrackSwapError) {
|
|
95
|
+
console.error(error.code, error.format, error.cause);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
~~~
|
|
156
99
|
|
|
157
|
-
|
|
158
|
-
const result = await trackSwap.convertFile(sourceBuffer, 'fit', 'gpx');
|
|
100
|
+
错误码包括 `FORMAT_UNKNOWN`、`FORMAT_UNSUPPORTED`、`DECODE_FAILED`、`ACTIVITY_NOT_FOUND`、`ENCODE_FAILED`、`ADAPTER_CONFLICT` 和 `DISPOSED`。
|
|
159
101
|
|
|
160
|
-
|
|
161
|
-
// GPX ↔ FIT ↔ TCX (任意格式之间相互转换)
|
|
162
|
-
```
|
|
102
|
+
## 自定义格式适配器
|
|
163
103
|
|
|
164
|
-
|
|
104
|
+
格式扩展发生在一个明确边界上,不需要访问 TrackSwap 内部解码器:
|
|
165
105
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const gpxActivity = await trackSwap.convertGPXToActivity(gpxData);
|
|
169
|
-
const fitActivity = await trackSwap.convertFITToActivity(fitData);
|
|
170
|
-
const tcxActivity = await trackSwap.convertTCXToActivity(tcxData);
|
|
106
|
+
~~~ts
|
|
107
|
+
import TrackSwap, { type TrackFormatAdapter } from "trackswap";
|
|
171
108
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
109
|
+
const adapter: TrackFormatAdapter = {
|
|
110
|
+
format: "geojson",
|
|
111
|
+
matches: (input) => input.includes(Buffer.from('"FeatureCollection"')),
|
|
112
|
+
decode: async (input, options) => ({
|
|
113
|
+
kinds: ["route"],
|
|
114
|
+
facets: { activity: myDecode(input, options) },
|
|
115
|
+
native: {},
|
|
116
|
+
}),
|
|
117
|
+
encodeActivity: async (activity) => myEncode(activity),
|
|
118
|
+
};
|
|
177
119
|
|
|
178
|
-
|
|
120
|
+
const trackSwap = new TrackSwap({ adapters: [adapter] });
|
|
121
|
+
~~~
|
|
179
122
|
|
|
180
|
-
|
|
181
|
-
// 🤖 自动检测格式
|
|
182
|
-
const format = trackSwap.detectFormat(buffer);
|
|
183
|
-
console.log(`检测到格式: ${format}`); // 'gpx' | 'fit' | 'tcx' | 'unknown'
|
|
123
|
+
自定义适配器默认追加到 FIT、GPX、TCX 之后。每种格式只能注册一个适配器;重复注册会立即抛出 `ADAPTER_CONFLICT`。测试或完全定制运行时可设置 `includeBuiltInAdapters: false`。
|
|
184
124
|
|
|
185
|
-
|
|
186
|
-
const parsedData = await trackSwap.parseFile(buffer);
|
|
187
|
-
const parsedData = await trackSwap.parseFile(buffer, 'gpx'); // 指定格式
|
|
125
|
+
## 公共 API
|
|
188
126
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
127
|
+
| 方法 | 作用 |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `detect(input)` | 检测 FIT、GPX 或 TCX |
|
|
130
|
+
| `decode(input, options?)` | 返回统一 `TrackDocument` |
|
|
131
|
+
| `decodeActivity(input, options?)` | 读取并要求存在活动 facet |
|
|
132
|
+
| `encodeActivity(activity, options)` | 将统一活动编码为目标格式 |
|
|
133
|
+
| `transcode(input, options)` | 通过统一活动模型转换格式 |
|
|
134
|
+
| `encodeCourse(inputOrActivity, options?)` | 生成 FIT Course |
|
|
135
|
+
| `dispose()` | 释放当前实例已创建的解码器 |
|
|
193
136
|
|
|
194
|
-
##
|
|
137
|
+
## 设计原则
|
|
195
138
|
|
|
196
|
-
|
|
139
|
+
- façade 只负责编排,不包含协议逻辑;
|
|
140
|
+
- 格式适配器负责检测、原生解码与编码;
|
|
141
|
+
- 统一活动和健康模型是格式之间唯一的数据交换边界;
|
|
142
|
+
- 默认惰性创建解码器,未使用的格式没有初始化成本;
|
|
143
|
+
- 核心解析阶段 fail-fast,不返回部分损坏的文档;
|
|
144
|
+
- 原生协议对象默认不进入公共结果,避免业务层依赖实现细节。
|
|
197
145
|
|
|
198
|
-
|
|
146
|
+
## License
|
|
199
147
|
|
|
200
|
-
|
|
201
|
-
|------|------|------|--------|
|
|
202
|
-
| `parseGPX(buffer)` | 解析 GPX Buffer | `Buffer` | `Promise<GPX11Type \| undefined>` |
|
|
203
|
-
| `parseGPXString(xml)` | 解析 GPX 字符串 | `string` | `Promise<GPX11Type \| undefined>` |
|
|
204
|
-
| `parseFIT(buffer)` | 解析 FIT Buffer | `Buffer` | `Promise<FITFileType>` |
|
|
205
|
-
| `parseTCX(buffer)` | 解析 TCX Buffer | `Buffer` | `Promise<TCXFileType>` |
|
|
206
|
-
| `parseTCXString(xml)` | 解析 TCX 字符串 | `string` | `Promise<TCXFileType>` |
|
|
207
|
-
| `parseFile(buffer, type?)` | 统一解析方法 | `Buffer, string?` | `Promise<GPX11Type \| FITFileType \| TCXFileType>` |
|
|
208
|
-
| `parseToActivity(buffer, type?)` | 解析为统一格式 | `Buffer, string?` | `Promise<ActivityType>` |
|
|
209
|
-
|
|
210
|
-
#### 编码方法
|
|
211
|
-
|
|
212
|
-
| 方法 | 说明 | 参数 | 返回值 |
|
|
213
|
-
|------|------|------|--------|
|
|
214
|
-
| `encodeGPX(data)` | 编码 GPX 为 Buffer | `GPX11Type` | `Promise<Buffer>` |
|
|
215
|
-
| `encodeGPXString(data)` | 编码 GPX 为字符串 | `GPX11Type` | `string` |
|
|
216
|
-
| `encodeFIT(data)` | 编码 FIT 为 Buffer | `FITFileType` | `Promise<Buffer>` |
|
|
217
|
-
| `encodeFITCourse(data)` | 编码 FIT Course | `FITFileType` | `Promise<Buffer>` |
|
|
218
|
-
| `encodeTCX(data)` | 编码 TCX 为 Buffer | `TCXFileType` | `Promise<Buffer>` |
|
|
219
|
-
| `encodeTCXString(data)` | 编码 TCX 为字符串 | `TCXFileType` | `Promise<string>` |
|
|
220
|
-
|
|
221
|
-
#### 转换方法
|
|
222
|
-
|
|
223
|
-
| 方法 | 说明 | 参数 | 返回值 |
|
|
224
|
-
|------|------|------|--------|
|
|
225
|
-
| `convertFile(buffer, target, source?)` | 统一转换方法 | `Buffer, string, string?` | `Promise<Buffer>` |
|
|
226
|
-
| `convertGPXToActivity(data)` | GPX → Activity | `GPX11Type` | `Promise<ActivityType>` |
|
|
227
|
-
| `convertFITToActivity(data)` | FIT → Activity | `FITFileType` | `Promise<ActivityType>` |
|
|
228
|
-
| `convertTCXToActivity(data)` | TCX → Activity | `TCXFileType` | `Promise<ActivityType>` |
|
|
229
|
-
| `convertActivityToGPX(data)` | Activity → GPX | `ActivityType` | `Promise<GPX11Type>` |
|
|
230
|
-
| `convertActivityToFIT(data)` | Activity → FIT | `ActivityType` | `Promise<FITFileType>` |
|
|
231
|
-
| `convertActivityToTCX(data)` | Activity → TCX | `ActivityType` | `Promise<TCXFileType>` |
|
|
232
|
-
|
|
233
|
-
#### 工具方法
|
|
234
|
-
|
|
235
|
-
| 方法 | 说明 | 参数 | 返回值 |
|
|
236
|
-
|------|------|------|--------|
|
|
237
|
-
| `detectFormat(buffer)` | 检测文件格式 | `Buffer` | `'gpx' \| 'fit' \| 'tcx' \| 'unknown'` |
|
|
238
|
-
| `getActivityProcessor()` | 获取运动处理器 | - | `ActivityProcessor` |
|
|
239
|
-
| `destroy()` | 清理资源 | - | `Promise<void>` |
|
|
240
|
-
|
|
241
|
-
## 🌟 实际应用示例
|
|
242
|
-
|
|
243
|
-
### 批量格式转换
|
|
244
|
-
|
|
245
|
-
```typescript
|
|
246
|
-
import { TrackSwap } from 'trackswap';
|
|
247
|
-
import fs from 'fs';
|
|
248
|
-
import path from 'path';
|
|
249
|
-
|
|
250
|
-
async function batchConvert(inputDir: string, outputFormat: 'gpx' | 'fit' | 'tcx') {
|
|
251
|
-
const trackSwap = new TrackSwap();
|
|
252
|
-
const files = fs.readdirSync(inputDir);
|
|
253
|
-
|
|
254
|
-
for (const file of files) {
|
|
255
|
-
try {
|
|
256
|
-
const inputPath = path.join(inputDir, file);
|
|
257
|
-
const buffer = fs.readFileSync(inputPath);
|
|
258
|
-
|
|
259
|
-
// 🔄 自动检测并转换
|
|
260
|
-
const converted = await trackSwap.convertFile(buffer, outputFormat);
|
|
261
|
-
|
|
262
|
-
const outputPath = path.join(inputDir,
|
|
263
|
-
`${path.parse(file).name}.${outputFormat}`);
|
|
264
|
-
fs.writeFileSync(outputPath, converted);
|
|
265
|
-
|
|
266
|
-
console.log(`✅ 转换完成: ${file} → ${outputFormat}`);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
console.error(`❌ 转换失败: ${file}`, error);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
await trackSwap.destroy();
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// 使用示例
|
|
276
|
-
await batchConvert('./tracks', 'gpx');
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### 轨迹数据分析
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
async function analyzeTrack(buffer: Buffer) {
|
|
283
|
-
const trackSwap = new TrackSwap();
|
|
284
|
-
|
|
285
|
-
// 🔍 检测格式
|
|
286
|
-
const format = trackSwap.detectFormat(buffer);
|
|
287
|
-
console.log(`文件格式: ${format}`);
|
|
288
|
-
|
|
289
|
-
// 📊 转换为统一格式进行分析
|
|
290
|
-
const sportData = await trackSwap.parseToActivity(buffer);
|
|
291
|
-
|
|
292
|
-
// 分析数据
|
|
293
|
-
const analysis = {
|
|
294
|
-
totalDistance: sportData.summary?.totalDistance || 0,
|
|
295
|
-
totalTime: sportData.summary?.totalTime || 0,
|
|
296
|
-
avgSpeed: sportData.summary?.avgSpeed || 0,
|
|
297
|
-
maxSpeed: sportData.summary?.maxSpeed || 0,
|
|
298
|
-
trackPoints: sportData.tracks?.[0]?.segments?.[0]?.points?.length || 0
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
console.log('轨迹分析结果:', analysis);
|
|
302
|
-
|
|
303
|
-
await trackSwap.destroy();
|
|
304
|
-
return analysis;
|
|
305
|
-
}
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
### 多格式数据融合
|
|
309
|
-
|
|
310
|
-
```typescript
|
|
311
|
-
async function mergeMultiFormatTracks(files: { buffer: Buffer, name: string }[]) {
|
|
312
|
-
const trackSwap = new TrackSwap();
|
|
313
|
-
const allActivityData: ActivityType[] = [];
|
|
314
|
-
|
|
315
|
-
// 🔄 将所有格式转换为统一格式
|
|
316
|
-
for (const file of files) {
|
|
317
|
-
try {
|
|
318
|
-
const sportData = await trackSwap.parseToActivity(file.buffer);
|
|
319
|
-
sportData.metadata = { ...sportData.metadata, originalFile: file.name };
|
|
320
|
-
allActivityData.push(sportData);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
console.error(`解析失败: ${file.name}`, error);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// 融合数据逻辑
|
|
327
|
-
const mergedData = mergeActivityData(allActivityData);
|
|
328
|
-
|
|
329
|
-
// 输出为不同格式
|
|
330
|
-
const results = {
|
|
331
|
-
gpx: await trackSwap.convertActivityToGPX(mergedData),
|
|
332
|
-
fit: await trackSwap.convertActivityToFIT(mergedData),
|
|
333
|
-
tcx: await trackSwap.convertActivityToTCX(mergedData)
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
await trackSwap.destroy();
|
|
337
|
-
return results;
|
|
338
|
-
}
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
## 🏗️ 架构设计
|
|
342
|
-
|
|
343
|
-
TrackSwap 采用模块化架构,核心组件包括:
|
|
344
|
-
|
|
345
|
-
```
|
|
346
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
347
|
-
│ TrackSwap 架构图 │
|
|
348
|
-
├─────────────────────────────────────────────────────────────┤
|
|
349
|
-
│ │
|
|
350
|
-
│ 🔧 核心处理模块 │
|
|
351
|
-
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
352
|
-
│ │ • GPXDecoder/Encoder - GPX 文件处理 │ │
|
|
353
|
-
│ │ • FITDecoder/Encoder - FIT 文件处理 │ │
|
|
354
|
-
│ │ • TCXDecoder/Encoder - TCX 文件处理 │ │
|
|
355
|
-
│ │ • ActivityProcessor - 统一格式转换处理 │ │
|
|
356
|
-
│ └─────────────────────────────────────────────────────────┘ │
|
|
357
|
-
│ │
|
|
358
|
-
│ 🔄 转换引擎 │
|
|
359
|
-
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
360
|
-
│ │ • GPXToActivityConverter - GPX → ActivityType │ │
|
|
361
|
-
│ │ • FITToActivityConverter - FIT → ActivityType │ │
|
|
362
|
-
│ │ • TCXToActivityConverter - TCX → ActivityType │ │
|
|
363
|
-
│ │ • ActivityToGPXEncoder - ActivityType → GPX │ │
|
|
364
|
-
│ │ • ActivityToFITEncoder - ActivityType → FIT │ │
|
|
365
|
-
│ │ • ActivityToTCXEncoder - ActivityType → TCX │ │
|
|
366
|
-
│ └─────────────────────────────────────────────────────────┘ │
|
|
367
|
-
│ │
|
|
368
|
-
│ 🎯 统一接口 │
|
|
369
|
-
│ ┌─────────────────────────────────────────────────────────┐ │
|
|
370
|
-
│ │ • 自动格式检测 │ │
|
|
371
|
-
│ │ • 统一解析方法 │ │
|
|
372
|
-
│ │ • 统一转换方法 │ │
|
|
373
|
-
│ │ • 生命周期管理 │ │
|
|
374
|
-
│ └─────────────────────────────────────────────────────────┘ │
|
|
375
|
-
│ │
|
|
376
|
-
└─────────────────────────────────────────────────────────────┘
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
## 🔍 支持的格式
|
|
380
|
-
|
|
381
|
-
### GPX (GPS Exchange Format)
|
|
382
|
-
- ✅ GPX 1.1 标准
|
|
383
|
-
- ✅ 航路点 (Waypoints)
|
|
384
|
-
- ✅ 路线 (Routes)
|
|
385
|
-
- ✅ 轨迹 (Tracks)
|
|
386
|
-
- ✅ 元数据 (Metadata)
|
|
387
|
-
- ✅ 扩展属性
|
|
388
|
-
|
|
389
|
-
### FIT (Flexible and Interoperable Data Transfer)
|
|
390
|
-
- ✅ Activity 文件
|
|
391
|
-
- ✅ Course 文件
|
|
392
|
-
- ✅ 轨迹点数据
|
|
393
|
-
- ✅ 心率、速度、海拔等传感器数据
|
|
394
|
-
- ✅ Garmin 设备兼容
|
|
395
|
-
|
|
396
|
-
### TCX (Training Center XML)
|
|
397
|
-
- ✅ 训练数据
|
|
398
|
-
- ✅ 运动轨迹
|
|
399
|
-
- ✅ 心率区间
|
|
400
|
-
- ✅ 圈速数据
|
|
401
|
-
- ✅ Garmin Connect 兼容
|
|
402
|
-
|
|
403
|
-
## 🛠️ 开发指南
|
|
404
|
-
|
|
405
|
-
### 环境要求
|
|
406
|
-
|
|
407
|
-
- Node.js >= 16
|
|
408
|
-
- TypeScript >= 4.5
|
|
409
|
-
|
|
410
|
-
### 项目结构
|
|
411
|
-
|
|
412
|
-
```
|
|
413
|
-
src/
|
|
414
|
-
├── TrackSwap.ts # 主入口类
|
|
415
|
-
├── activity/ # 活动处理模块
|
|
416
|
-
│ ├── extensions.ts # 扩展系统核心
|
|
417
|
-
│ ├── slope-extension.ts # 坡度计算扩展
|
|
418
|
-
│ ├── processor.ts # 活动处理器
|
|
419
|
-
│ └── pipeline.ts # 数据处理管道
|
|
420
|
-
├── decoders/ # 解码器模块
|
|
421
|
-
│ ├── GPXDecoder.ts
|
|
422
|
-
│ ├── FITDecoder.ts
|
|
423
|
-
│ └── TCXDecoder.ts
|
|
424
|
-
├── encoders/ # 编码器模块
|
|
425
|
-
│ ├── GPXEncoder.ts
|
|
426
|
-
│ ├── FITEncoder.ts
|
|
427
|
-
│ └── TCXEncoder.ts
|
|
428
|
-
├── converters/ # 格式转换器
|
|
429
|
-
│ ├── GPXConverter.ts
|
|
430
|
-
│ ├── FITConverter.ts
|
|
431
|
-
│ └── TCXConverter.ts
|
|
432
|
-
└── types.ts # 公共类型定义
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
### 贡献指南
|
|
436
|
-
|
|
437
|
-
1. Fork 项目
|
|
438
|
-
2. 创建特性分支
|
|
439
|
-
3. 提交更改
|
|
440
|
-
4. 发起 Pull Request
|
|
441
|
-
|
|
442
|
-
## 📄 许可证
|
|
443
|
-
|
|
444
|
-
MIT License
|
|
445
|
-
|
|
446
|
-
## 🙏 致谢
|
|
447
|
-
|
|
448
|
-
感谢所有贡献者和开源社区的支持!
|
|
449
|
-
|
|
450
|
-
---
|
|
451
|
-
|
|
452
|
-
<div align="center">
|
|
453
|
-
<strong>TrackSwap - 让轨迹数据处理变得简单</strong><br>
|
|
454
|
-
🚀 高效 • 🔄 统一 • 🛡️ 安全 • 📈 可扩展
|
|
455
|
-
</div>
|
|
148
|
+
MIT
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { IPlugin } from "../core/base.js";
|
|
2
|
+
import { FITContext, FITDecoderMesgs, FITFileType } from "./types.js";
|
|
3
|
+
export interface IFITMessageConverter extends IPlugin<FITContext> {
|
|
4
|
+
supportedMessageTypes: string[];
|
|
5
|
+
convertMessages(messages: any[], context: FITContext): any;
|
|
6
|
+
supports(messageType: string): boolean;
|
|
7
|
+
priority?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface IFITStructurePlugin extends IPlugin<FITContext> {
|
|
10
|
+
structureData(messages: FITDecoderMesgs, context: FITContext): Partial<FITFileType>;
|
|
11
|
+
priority?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface IFITMiddlewarePlugin extends IPlugin<FITContext> {
|
|
14
|
+
priority?: number;
|
|
15
|
+
onParse?(buffer: Buffer, context: FITContext): Promise<Buffer> | Buffer;
|
|
16
|
+
onExtractMessages?(messages: FITDecoderMesgs, context: FITContext): Promise<FITDecoderMesgs> | FITDecoderMesgs;
|
|
17
|
+
onStructure?(result: Partial<FITFileType>, context: FITContext): Promise<Partial<FITFileType>> | Partial<FITFileType>;
|
|
18
|
+
onComplete?(result: FITFileType, context: FITContext): Promise<FITFileType> | FITFileType;
|
|
19
|
+
onError?(error: Error, context: FITContext): Promise<void> | void;
|
|
20
|
+
}
|
|
21
|
+
export declare abstract class BaseFITMessageConverter implements IFITMessageConverter {
|
|
22
|
+
abstract name: string;
|
|
23
|
+
abstract supportedMessageTypes: string[];
|
|
24
|
+
version: string;
|
|
25
|
+
priority: number;
|
|
26
|
+
abstract convertMessages(messages: any[], context: FITContext): any;
|
|
27
|
+
supports(messageType: string): boolean;
|
|
28
|
+
initialize(context: FITContext): Promise<void>;
|
|
29
|
+
destroy(context: FITContext): Promise<void>;
|
|
30
|
+
validate(context: FITContext): Promise<boolean>;
|
|
31
|
+
protected parseTimestamp(timestamp: string | undefined): Date | undefined;
|
|
32
|
+
protected calculateDuration(startTime?: string, endTime?: string): number;
|
|
33
|
+
protected filterMessagesByTimeRange(messages: any[], startTime: string, endTime: string, tolerance?: number): any[];
|
|
34
|
+
protected groupMessagesByLap(records: any[], laps: any[]): Array<{
|
|
35
|
+
lap: any;
|
|
36
|
+
records: any[];
|
|
37
|
+
}>;
|
|
38
|
+
}
|
|
39
|
+
export declare abstract class BaseFITStructurePlugin implements IFITStructurePlugin {
|
|
40
|
+
abstract name: string;
|
|
41
|
+
version: string;
|
|
42
|
+
priority: number;
|
|
43
|
+
abstract structureData(messages: FITDecoderMesgs, context: FITContext): Partial<FITFileType>;
|
|
44
|
+
initialize(context: FITContext): Promise<void>;
|
|
45
|
+
destroy(context: FITContext): Promise<void>;
|
|
46
|
+
validate(context: FITContext): Promise<boolean>;
|
|
47
|
+
}
|
|
48
|
+
export declare abstract class BaseFITMiddleware implements IFITMiddlewarePlugin {
|
|
49
|
+
abstract name: string;
|
|
50
|
+
version: string;
|
|
51
|
+
description: string;
|
|
52
|
+
priority: number;
|
|
53
|
+
initialize(context: FITContext): Promise<void>;
|
|
54
|
+
destroy(context: FITContext): Promise<void>;
|
|
55
|
+
validate(context: FITContext): Promise<boolean>;
|
|
56
|
+
onParse(buffer: Buffer, context: FITContext): Promise<Buffer>;
|
|
57
|
+
onExtractMessages(messages: FITDecoderMesgs, context: FITContext): Promise<FITDecoderMesgs>;
|
|
58
|
+
onStructure(result: Partial<FITFileType>, context: FITContext): Promise<Partial<FITFileType>>;
|
|
59
|
+
onComplete(result: FITFileType, context: FITContext): Promise<FITFileType>;
|
|
60
|
+
onError(error: Error, context: FITContext): Promise<void>;
|
|
61
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function crc(buffer: ArrayBuffer | Uint8Array, initial?: number): number;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { IFITMessageConverter, IFITStructurePlugin, IFITMiddlewarePlugin } from "./base.js";
|
|
2
|
+
import { FITFileType } from "./types.js";
|
|
3
|
+
export declare class FITDecoder {
|
|
4
|
+
private converterPlugins;
|
|
5
|
+
private registeredPlugins;
|
|
6
|
+
private structurePlugins;
|
|
7
|
+
private middlewarePlugins;
|
|
8
|
+
private processors;
|
|
9
|
+
private initialized;
|
|
10
|
+
private defaultPluginsRegistered;
|
|
11
|
+
constructor();
|
|
12
|
+
private initializePipeline;
|
|
13
|
+
private registerDefaultPlugins;
|
|
14
|
+
private addConverterForMessageType;
|
|
15
|
+
private addStructurePlugin;
|
|
16
|
+
registerMessageConverter(plugin: IFITMessageConverter): Promise<void>;
|
|
17
|
+
registerStructurePlugin(plugin: IFITStructurePlugin): Promise<void>;
|
|
18
|
+
registerMiddleware(plugin: IFITMiddlewarePlugin): Promise<void>;
|
|
19
|
+
getConverter(messageType: string): IFITMessageConverter | undefined;
|
|
20
|
+
getConverters(messageType: string): IFITMessageConverter[];
|
|
21
|
+
unregisterPlugin(pluginName: string): boolean;
|
|
22
|
+
initialize(): Promise<void>;
|
|
23
|
+
destroy(): Promise<void>;
|
|
24
|
+
private createContext;
|
|
25
|
+
private executeMiddlewareHook;
|
|
26
|
+
parseByBuffer(buffer: Buffer, options?: {
|
|
27
|
+
userData?: Record<string, any>;
|
|
28
|
+
}): Promise<FITFileType | undefined>;
|
|
29
|
+
}
|
|
30
|
+
declare const _default: {
|
|
31
|
+
FITDecoder: typeof FITDecoder;
|
|
32
|
+
};
|
|
33
|
+
export default _default;
|