trackswap 0.0.1 → 0.0.2
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/dist/FIT/base.d.ts +91 -0
- package/dist/FIT/base.js +101 -0
- package/dist/FIT/crc.d.ts +1 -0
- package/dist/FIT/crc.js +16 -0
- package/dist/FIT/decoder.d.ts +83 -0
- package/dist/FIT/decoder.js +323 -0
- package/dist/FIT/encoder.d.ts +142 -0
- package/dist/FIT/encoder.js +445 -0
- package/dist/FIT/index.d.ts +12 -0
- package/dist/FIT/index.js +13 -0
- package/dist/FIT/mesg.d.ts +40 -0
- package/dist/FIT/mesg.js +208 -0
- package/dist/FIT/middleware.d.ts +48 -0
- package/dist/FIT/middleware.js +206 -0
- package/dist/FIT/plugins.d.ts +26 -0
- package/dist/FIT/plugins.js +104 -0
- package/dist/FIT/processor.d.ts +58 -0
- package/dist/FIT/processor.js +170 -0
- package/dist/FIT/types/course_point.d.ts +56 -0
- package/dist/FIT/types/course_point.js +58 -0
- package/dist/FIT/types/event.d.ts +49 -0
- package/dist/FIT/types/event.js +50 -0
- package/dist/FIT/types/event_type.d.ts +18 -0
- package/dist/FIT/types/event_type.js +19 -0
- package/dist/FIT/types/file.d.ts +23 -0
- package/dist/FIT/types/file.js +24 -0
- package/dist/FIT/types/index.d.ts +119 -0
- package/dist/FIT/types/index.js +297 -0
- package/dist/FIT/types/mesg_num.d.ts +105 -0
- package/dist/FIT/types/mesg_num.js +106 -0
- package/dist/FIT/types/sport.d.ts +47 -0
- package/dist/FIT/types/sport.js +48 -0
- package/dist/FIT/types.d.ts +231 -0
- package/dist/FIT/types.js +1 -0
- package/dist/FIT/util.d.ts +1 -0
- package/dist/FIT/util.js +7 -0
- package/dist/GPX/base.d.ts +43 -67
- package/dist/GPX/base.js +40 -67
- package/dist/GPX/converters.d.ts +12 -36
- package/dist/GPX/converters.js +11 -103
- package/dist/GPX/decoder.d.ts +5 -5
- package/dist/GPX/decoder.js +6 -0
- package/dist/GPX/encoder.d.ts +4 -0
- package/dist/GPX/encoder.js +3 -0
- package/dist/GPX/index.d.ts +1 -1
- package/dist/GPX/index.js +1 -1
- package/dist/GPX/middleware.d.ts +27 -0
- package/dist/GPX/middleware.js +93 -0
- package/dist/GPX/processor.d.ts +1 -1
- package/dist/GPX/processor.js +12 -11
- package/dist/GPX/types.d.ts +2 -3
- package/dist/TCX/base.d.ts +52 -0
- package/dist/TCX/base.js +62 -0
- package/dist/TCX/converters.d.ts +75 -0
- package/dist/TCX/converters.js +333 -0
- package/dist/TCX/decoder.d.ts +87 -0
- package/dist/TCX/decoder.js +350 -0
- package/dist/TCX/encoder.d.ts +150 -0
- package/dist/TCX/encoder.js +513 -0
- package/dist/TCX/index.d.ts +10 -0
- package/dist/TCX/index.js +9 -0
- package/dist/TCX/middleware.d.ts +27 -0
- package/dist/TCX/middleware.js +93 -0
- package/dist/TCX/processor.d.ts +48 -0
- package/dist/TCX/processor.js +197 -0
- package/dist/TCX/types.d.ts +203 -0
- package/dist/TCX/types.js +1 -0
- package/dist/core/base.d.ts +119 -0
- package/dist/core/base.js +138 -0
- package/dist/core/decoder.d.ts +66 -0
- package/dist/core/decoder.js +90 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +4 -0
- package/dist/core/processor.d.ts +69 -0
- package/dist/core/processor.js +265 -0
- package/dist/core/xml-utils.d.ts +41 -0
- package/dist/core/xml-utils.js +140 -0
- package/package.json +8 -5
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import utc from "dayjs/plugin/utc.js";
|
|
3
|
+
// 配置 dayjs UTC 插件
|
|
4
|
+
dayjs.extend(utc);
|
|
5
|
+
/**
|
|
6
|
+
* TCX 编码器 - 将 TCXFileType 数据编码为 TCX XML 格式
|
|
7
|
+
*/
|
|
8
|
+
export class TCXEncoder {
|
|
9
|
+
/**
|
|
10
|
+
* 构造函数
|
|
11
|
+
* @param options 编码器选项
|
|
12
|
+
*/
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.compact = options.compact ?? true;
|
|
15
|
+
}
|
|
16
|
+
// ==================== 公共接口 ====================
|
|
17
|
+
/**
|
|
18
|
+
* 编码 TCX 数据为 Buffer
|
|
19
|
+
*/
|
|
20
|
+
async encode(tcxData) {
|
|
21
|
+
const xmlContent = this.buildTCXXML(tcxData);
|
|
22
|
+
return Buffer.from(xmlContent, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 编码 TCX 数据为 XML 字符串
|
|
26
|
+
*/
|
|
27
|
+
encodeToString(tcxData) {
|
|
28
|
+
return this.buildTCXXML(tcxData);
|
|
29
|
+
}
|
|
30
|
+
// ==================== 核心构建方法 ====================
|
|
31
|
+
/**
|
|
32
|
+
* 构建完整的 TCX XML
|
|
33
|
+
*/
|
|
34
|
+
buildTCXXML(tcxData) {
|
|
35
|
+
const header = this.buildXMLHeader(tcxData);
|
|
36
|
+
const content = this.buildTCXContent(tcxData);
|
|
37
|
+
const footer = "</TrainingCenterDatabase>";
|
|
38
|
+
const xml = `${header}${content}\n${footer}`;
|
|
39
|
+
// 如果启用压缩模式,则压缩XML
|
|
40
|
+
return this.compact ? this.compressXML(xml) : xml;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 压缩XML字符串,去掉多余的换行和空格
|
|
44
|
+
*/
|
|
45
|
+
compressXML(xml) {
|
|
46
|
+
return (xml
|
|
47
|
+
// 去掉标签之间的换行和多余空格
|
|
48
|
+
.replace(/>\s+</g, "><")
|
|
49
|
+
// 去掉行首行尾的空白字符
|
|
50
|
+
.replace(/^\s+|\s+$/gm, "")
|
|
51
|
+
// 将多个连续空格替换为单个空格
|
|
52
|
+
.replace(/\s+/g, " ")
|
|
53
|
+
// 去掉整体的首尾空白
|
|
54
|
+
.trim());
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 构建 XML 头部
|
|
58
|
+
*/
|
|
59
|
+
buildXMLHeader(tcxData) {
|
|
60
|
+
const version = tcxData.version || "1.0";
|
|
61
|
+
const creator = tcxData.creator || "trackswap";
|
|
62
|
+
const xmlns = tcxData.xmlns ||
|
|
63
|
+
"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2";
|
|
64
|
+
const xmlnsXsi = tcxData["xmlns:xsi"] || "http://www.w3.org/2001/XMLSchema-instance";
|
|
65
|
+
const xsiSchemaLocation = tcxData["xsi:schemaLocation"] ||
|
|
66
|
+
"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www8.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd";
|
|
67
|
+
return [
|
|
68
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
69
|
+
`<TrainingCenterDatabase version="${this.escapeXMLAttribute(version)}"`,
|
|
70
|
+
` creator="${this.escapeXMLAttribute(creator)}"`,
|
|
71
|
+
` xmlns="${this.escapeXMLAttribute(xmlns)}"`,
|
|
72
|
+
` xmlns:xsi="${this.escapeXMLAttribute(xmlnsXsi)}"`,
|
|
73
|
+
` xsi:schemaLocation="${this.escapeXMLAttribute(xsiSchemaLocation)}">`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* 构建 TCX 内容
|
|
78
|
+
*/
|
|
79
|
+
buildTCXContent(tcxData) {
|
|
80
|
+
const parts = [];
|
|
81
|
+
// 按标准 TCX 顺序构建内容
|
|
82
|
+
if (tcxData.Folders) {
|
|
83
|
+
parts.push(this.buildFolders(tcxData.Folders));
|
|
84
|
+
}
|
|
85
|
+
if (tcxData.Activities) {
|
|
86
|
+
parts.push(this.buildActivities(tcxData.Activities));
|
|
87
|
+
}
|
|
88
|
+
if (tcxData.Workouts) {
|
|
89
|
+
parts.push(this.buildWorkouts(tcxData.Workouts));
|
|
90
|
+
}
|
|
91
|
+
if (tcxData.Courses) {
|
|
92
|
+
parts.push(this.buildCourses(tcxData.Courses));
|
|
93
|
+
}
|
|
94
|
+
if (tcxData.Author) {
|
|
95
|
+
parts.push(this.buildAuthor(tcxData.Author));
|
|
96
|
+
}
|
|
97
|
+
if (tcxData.Extensions) {
|
|
98
|
+
parts.push(this.buildExtensions(tcxData.Extensions));
|
|
99
|
+
}
|
|
100
|
+
return parts.join("\n");
|
|
101
|
+
}
|
|
102
|
+
// ==================== 结构元素构建 ====================
|
|
103
|
+
/**
|
|
104
|
+
* 构建 Folders 元素
|
|
105
|
+
*/
|
|
106
|
+
buildFolders(folders) {
|
|
107
|
+
const parts = ["<Folders>"];
|
|
108
|
+
if (folders.History) {
|
|
109
|
+
parts.push(this.buildHistory(folders.History));
|
|
110
|
+
}
|
|
111
|
+
if (folders.Workouts) {
|
|
112
|
+
parts.push(this.buildWorkouts(folders.Workouts));
|
|
113
|
+
}
|
|
114
|
+
if (folders.Courses) {
|
|
115
|
+
// TODO: 处理 Courses
|
|
116
|
+
console.warn("Courses in Folders 暂未实现");
|
|
117
|
+
}
|
|
118
|
+
parts.push("</Folders>");
|
|
119
|
+
return parts.join("\n ");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 构建 History 元素
|
|
123
|
+
*/
|
|
124
|
+
buildHistory(history) {
|
|
125
|
+
const parts = ["<History>"];
|
|
126
|
+
if (history.Running) {
|
|
127
|
+
parts.push(`<Running>${this.buildHistoryFolder(history.Running)}</Running>`);
|
|
128
|
+
}
|
|
129
|
+
if (history.Biking) {
|
|
130
|
+
parts.push(`<Biking>${this.buildHistoryFolder(history.Biking)}</Biking>`);
|
|
131
|
+
}
|
|
132
|
+
if (history.Other) {
|
|
133
|
+
parts.push(`<Other>${this.buildHistoryFolder(history.Other)}</Other>`);
|
|
134
|
+
}
|
|
135
|
+
if (history.MultiSport) {
|
|
136
|
+
parts.push(`<MultiSport>${this.buildHistoryFolder(history.MultiSport)}</MultiSport>`);
|
|
137
|
+
}
|
|
138
|
+
if (history.Extensions) {
|
|
139
|
+
parts.push(this.buildExtensions(history.Extensions));
|
|
140
|
+
}
|
|
141
|
+
parts.push("</History>");
|
|
142
|
+
return parts.join("\n ");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* 构建 HistoryFolder 元素
|
|
146
|
+
*/
|
|
147
|
+
buildHistoryFolder(folder) {
|
|
148
|
+
// TODO: 实现详细的 HistoryFolder 构建
|
|
149
|
+
return "";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 构建 Activities 元素
|
|
153
|
+
*/
|
|
154
|
+
buildActivities(activities) {
|
|
155
|
+
const parts = ["<Activities>"];
|
|
156
|
+
if (activities.Activity) {
|
|
157
|
+
activities.Activity.forEach((activity) => {
|
|
158
|
+
parts.push(this.buildActivity(activity));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (activities.MultiSportSession) {
|
|
162
|
+
activities.MultiSportSession.forEach((session) => {
|
|
163
|
+
parts.push(this.buildMultiSportSession(session));
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
parts.push("</Activities>");
|
|
167
|
+
return parts.join("\n ");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 构建单个 Activity 元素
|
|
171
|
+
*/
|
|
172
|
+
buildActivity(activity) {
|
|
173
|
+
const sport = activity.Sport || "Other";
|
|
174
|
+
const parts = [`<Activity Sport="${sport}">`];
|
|
175
|
+
// ID 是必需的
|
|
176
|
+
parts.push(`<Id>${this.formatTime(activity.Id)}</Id>`);
|
|
177
|
+
// Lap 是必需的
|
|
178
|
+
if (activity.Lap && activity.Lap.length > 0) {
|
|
179
|
+
activity.Lap.forEach((lap) => {
|
|
180
|
+
parts.push(this.buildActivityLap(lap));
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
// 可选元素
|
|
184
|
+
this.addOptionalElement(parts, "Notes", activity.Notes);
|
|
185
|
+
if (activity.Training) {
|
|
186
|
+
// TODO: 实现 Training 元素
|
|
187
|
+
console.warn("Activity Training 暂未实现");
|
|
188
|
+
}
|
|
189
|
+
if (activity.Creator) {
|
|
190
|
+
// TODO: 实现 Creator 元素
|
|
191
|
+
console.warn("Activity Creator 暂未实现");
|
|
192
|
+
}
|
|
193
|
+
if (activity.Extensions) {
|
|
194
|
+
parts.push(this.buildExtensions(activity.Extensions));
|
|
195
|
+
}
|
|
196
|
+
parts.push("</Activity>");
|
|
197
|
+
return parts.join("\n ");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 构建 ActivityLap 元素
|
|
201
|
+
*/
|
|
202
|
+
buildActivityLap(lap) {
|
|
203
|
+
const parts = ["<Lap>"];
|
|
204
|
+
// 时间相关
|
|
205
|
+
this.addOptionalNumericElement(parts, "TotalTimeSeconds", lap.TotalTimeSeconds);
|
|
206
|
+
this.addOptionalNumericElement(parts, "DistanceMeters", lap.DistanceMeters);
|
|
207
|
+
this.addOptionalNumericElement(parts, "MaximumSpeed", lap.MaximumSpeed);
|
|
208
|
+
this.addOptionalNumericElement(parts, "Calories", lap.Calories);
|
|
209
|
+
// 心率
|
|
210
|
+
if (lap.AverageHeartRateBpm) {
|
|
211
|
+
parts.push(`<AverageHeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t"><Value>${lap.AverageHeartRateBpm}</Value></AverageHeartRateBpm>`);
|
|
212
|
+
}
|
|
213
|
+
if (lap.MaximumHeartRateBpm) {
|
|
214
|
+
parts.push(`<MaximumHeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t"><Value>${lap.MaximumHeartRateBpm}</Value></MaximumHeartRateBpm>`);
|
|
215
|
+
}
|
|
216
|
+
// 其他属性
|
|
217
|
+
this.addOptionalElement(parts, "Intensity", lap.Intensity);
|
|
218
|
+
if (lap.Cadence) {
|
|
219
|
+
parts.push(this.buildCadence(lap.Cadence));
|
|
220
|
+
}
|
|
221
|
+
this.addOptionalElement(parts, "TriggerMethod", lap.TriggerMethod);
|
|
222
|
+
// Track 数据
|
|
223
|
+
if (lap.Track) {
|
|
224
|
+
lap.Track.forEach((track) => {
|
|
225
|
+
parts.push(this.buildTrack(track));
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
this.addOptionalElement(parts, "Notes", lap.Notes);
|
|
229
|
+
if (lap.Extensions) {
|
|
230
|
+
parts.push(this.buildExtensions(lap.Extensions));
|
|
231
|
+
}
|
|
232
|
+
parts.push("</Lap>");
|
|
233
|
+
return parts.join("\n ");
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* 构建 Track 元素
|
|
237
|
+
*/
|
|
238
|
+
buildTrack(track) {
|
|
239
|
+
const parts = ["<Track>"];
|
|
240
|
+
if (track.Trackpoint) {
|
|
241
|
+
track.Trackpoint.forEach((trackpoint) => {
|
|
242
|
+
parts.push(this.buildTrackpoint(trackpoint));
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
parts.push("</Track>");
|
|
246
|
+
return parts.join("\n ");
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 构建 Trackpoint 元素
|
|
250
|
+
*/
|
|
251
|
+
buildTrackpoint(trackpoint) {
|
|
252
|
+
const parts = ["<Trackpoint>"];
|
|
253
|
+
// 时间(通常是必需的)
|
|
254
|
+
if (trackpoint.Time) {
|
|
255
|
+
parts.push(`<Time>${this.formatTime(trackpoint.Time)}</Time>`);
|
|
256
|
+
}
|
|
257
|
+
// 位置
|
|
258
|
+
if (trackpoint.Position) {
|
|
259
|
+
parts.push(this.buildPosition(trackpoint.Position));
|
|
260
|
+
}
|
|
261
|
+
// 数值属性
|
|
262
|
+
this.addOptionalNumericElement(parts, "AltitudeMeters", trackpoint.AltitudeMeters);
|
|
263
|
+
this.addOptionalNumericElement(parts, "DistanceMeters", trackpoint.DistanceMeters);
|
|
264
|
+
// 心率
|
|
265
|
+
if (trackpoint.HeartRateBpm) {
|
|
266
|
+
parts.push(`<HeartRateBpm xsi:type="HeartRateInBeatsPerMinute_t"><Value>${trackpoint.HeartRateBpm}</Value></HeartRateBpm>`);
|
|
267
|
+
}
|
|
268
|
+
// 踏频
|
|
269
|
+
if (trackpoint.Cadence) {
|
|
270
|
+
parts.push(this.buildCadence(trackpoint.Cadence));
|
|
271
|
+
}
|
|
272
|
+
// 传感器状态
|
|
273
|
+
this.addOptionalElement(parts, "SensorState", trackpoint.SensorState);
|
|
274
|
+
// 扩展
|
|
275
|
+
if (trackpoint.Extensions) {
|
|
276
|
+
parts.push(this.buildExtensions(trackpoint.Extensions));
|
|
277
|
+
}
|
|
278
|
+
parts.push("</Trackpoint>");
|
|
279
|
+
return parts.join("\n ");
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 构建 Position 元素
|
|
283
|
+
*/
|
|
284
|
+
buildPosition(position) {
|
|
285
|
+
const parts = ["<Position>"];
|
|
286
|
+
if (position.LatitudeDegrees) {
|
|
287
|
+
parts.push(`<LatitudeDegrees>${this.formatCoordinate(parseFloat(position.LatitudeDegrees))}</LatitudeDegrees>`);
|
|
288
|
+
}
|
|
289
|
+
if (position.LongitudeDegrees) {
|
|
290
|
+
parts.push(`<LongitudeDegrees>${this.formatCoordinate(parseFloat(position.LongitudeDegrees))}</LongitudeDegrees>`);
|
|
291
|
+
}
|
|
292
|
+
parts.push("</Position>");
|
|
293
|
+
return parts.join("");
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 构建 Cadence 元素
|
|
297
|
+
*/
|
|
298
|
+
buildCadence(cadence) {
|
|
299
|
+
return `<Cadence><Low>${cadence.Low}</Low><High>${cadence.High}</High></Cadence>`;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* 构建 MultiSportSession 元素
|
|
303
|
+
*/
|
|
304
|
+
buildMultiSportSession(session) {
|
|
305
|
+
const parts = ["<MultiSportSession>"];
|
|
306
|
+
if (session.Id) {
|
|
307
|
+
parts.push(`<Id>${this.formatTime(session.Id)}</Id>`);
|
|
308
|
+
}
|
|
309
|
+
if (session.FirstSport) {
|
|
310
|
+
parts.push(this.buildFirstSport(session.FirstSport));
|
|
311
|
+
}
|
|
312
|
+
if (session.NextSport) {
|
|
313
|
+
session.NextSport.forEach((nextSport) => {
|
|
314
|
+
parts.push(this.buildNextSport(nextSport));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
this.addOptionalElement(parts, "Notes", session.Notes);
|
|
318
|
+
parts.push("</MultiSportSession>");
|
|
319
|
+
return parts.join("\n ");
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 构建 FirstSport 元素
|
|
323
|
+
*/
|
|
324
|
+
buildFirstSport(firstSport) {
|
|
325
|
+
const parts = ["<FirstSport>"];
|
|
326
|
+
if (firstSport.Activity) {
|
|
327
|
+
parts.push(this.buildActivity(firstSport.Activity));
|
|
328
|
+
}
|
|
329
|
+
parts.push("</FirstSport>");
|
|
330
|
+
return parts.join("\n ");
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* 构建 NextSport 元素
|
|
334
|
+
*/
|
|
335
|
+
buildNextSport(nextSport) {
|
|
336
|
+
const parts = ["<NextSport>"];
|
|
337
|
+
if (nextSport.Transition) {
|
|
338
|
+
// Transition 使用 ActivityLap 结构
|
|
339
|
+
parts.push(`<Transition>${this.buildActivityLap(nextSport.Transition)}</Transition>`);
|
|
340
|
+
}
|
|
341
|
+
if (nextSport.Activity) {
|
|
342
|
+
parts.push(this.buildActivity(nextSport.Activity));
|
|
343
|
+
}
|
|
344
|
+
parts.push("</NextSport>");
|
|
345
|
+
return parts.join("\n ");
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* 构建 Workouts 元素
|
|
349
|
+
*/
|
|
350
|
+
buildWorkouts(workouts) {
|
|
351
|
+
// TODO: 实现 Workouts 构建
|
|
352
|
+
console.warn("Workouts 构建暂未实现");
|
|
353
|
+
return "<Workouts></Workouts>";
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* 构建 Courses 元素
|
|
357
|
+
*/
|
|
358
|
+
buildCourses(courses) {
|
|
359
|
+
// TODO: 实现 Courses 构建
|
|
360
|
+
console.warn("Courses 构建暂未实现");
|
|
361
|
+
return "<Courses></Courses>";
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* 构建 Author 元素
|
|
365
|
+
*/
|
|
366
|
+
buildAuthor(author) {
|
|
367
|
+
const parts = ["<Author>"];
|
|
368
|
+
this.addOptionalElement(parts, "Name", author.Name);
|
|
369
|
+
parts.push("</Author>");
|
|
370
|
+
return parts.join("");
|
|
371
|
+
}
|
|
372
|
+
// ==================== 扩展处理 ====================
|
|
373
|
+
/**
|
|
374
|
+
* 构建扩展元素
|
|
375
|
+
*/
|
|
376
|
+
buildExtensions(extensions) {
|
|
377
|
+
const content = this.buildExtensionContent(extensions);
|
|
378
|
+
return content ? `<Extensions>${content}</Extensions>` : "";
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* 递归构建扩展内容
|
|
382
|
+
*/
|
|
383
|
+
buildExtensionContent(extension) {
|
|
384
|
+
return Object.entries(extension)
|
|
385
|
+
.filter(([key]) => !key.startsWith("xmlns"))
|
|
386
|
+
.map(([key, value]) => {
|
|
387
|
+
if (value && typeof value === "object") {
|
|
388
|
+
const content = this.buildExtensionContent(value);
|
|
389
|
+
return `<${key}>${content}</${key}>`;
|
|
390
|
+
}
|
|
391
|
+
else if (value !== undefined && value !== null) {
|
|
392
|
+
return `<${key}>${this.escapeXML(String(value))}</${key}>`;
|
|
393
|
+
}
|
|
394
|
+
return "";
|
|
395
|
+
})
|
|
396
|
+
.filter((content) => content)
|
|
397
|
+
.join("");
|
|
398
|
+
}
|
|
399
|
+
// ==================== 辅助方法 ====================
|
|
400
|
+
/**
|
|
401
|
+
* 格式化时间
|
|
402
|
+
*/
|
|
403
|
+
formatTime(time) {
|
|
404
|
+
// 如果是字符串且已经是正确的ISO格式,直接使用
|
|
405
|
+
if (typeof time === "string") {
|
|
406
|
+
// 检查是否已经是标准的ISO 8601格式
|
|
407
|
+
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(time)) {
|
|
408
|
+
return time;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// 对于Date对象或其他格式的字符串,使用dayjs处理
|
|
412
|
+
return dayjs(time).utc().format("YYYY-MM-DDTHH:mm:ss[Z]");
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 添加可选元素
|
|
416
|
+
*/
|
|
417
|
+
addOptionalElement(parts, tagName, value, formatter) {
|
|
418
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
419
|
+
const formattedValue = formatter
|
|
420
|
+
? formatter(value)
|
|
421
|
+
: this.escapeXML(value);
|
|
422
|
+
parts.push(`<${tagName}>${formattedValue}</${tagName}>`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* 添加可选数值元素
|
|
427
|
+
*/
|
|
428
|
+
addOptionalNumericElement(parts, tagName, value) {
|
|
429
|
+
if (value !== undefined && value !== null && !isNaN(value)) {
|
|
430
|
+
parts.push(`<${tagName}>${value}</${tagName}>`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* 验证坐标有效性
|
|
435
|
+
*/
|
|
436
|
+
isValidCoordinate(lat, lon) {
|
|
437
|
+
return (lat !== undefined &&
|
|
438
|
+
lat !== null &&
|
|
439
|
+
lon !== undefined &&
|
|
440
|
+
lon !== null &&
|
|
441
|
+
!isNaN(lat) &&
|
|
442
|
+
!isNaN(lon));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* 格式化坐标
|
|
446
|
+
*/
|
|
447
|
+
formatCoordinate(coord) {
|
|
448
|
+
return this.roundTo(coord, 6).toString();
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* 数字四舍五入
|
|
452
|
+
*/
|
|
453
|
+
roundTo(num, precision) {
|
|
454
|
+
const factor = Math.pow(10, precision);
|
|
455
|
+
return Math.round(num * factor) / factor;
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* XML 转义
|
|
459
|
+
*/
|
|
460
|
+
escapeXML(text) {
|
|
461
|
+
return text
|
|
462
|
+
.replace(/&/g, "&")
|
|
463
|
+
.replace(/</g, "<")
|
|
464
|
+
.replace(/>/g, ">")
|
|
465
|
+
.replace(/"/g, """)
|
|
466
|
+
.replace(/'/g, "'");
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* XML 属性值转义(专门用于属性值的安全转义)
|
|
470
|
+
*/
|
|
471
|
+
escapeXMLAttribute(text) {
|
|
472
|
+
if (typeof text !== "string") {
|
|
473
|
+
return String(text || "");
|
|
474
|
+
}
|
|
475
|
+
return text
|
|
476
|
+
.replace(/&/g, "&")
|
|
477
|
+
.replace(/"/g, """)
|
|
478
|
+
.replace(/'/g, "'")
|
|
479
|
+
.replace(/</g, "<")
|
|
480
|
+
.replace(/>/g, ">")
|
|
481
|
+
.replace(/\r/g, " ")
|
|
482
|
+
.replace(/\n/g, " ")
|
|
483
|
+
.replace(/\t/g, "	");
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* 深度拷贝
|
|
487
|
+
*/
|
|
488
|
+
deepClone(obj) {
|
|
489
|
+
if (obj === null || typeof obj !== "object") {
|
|
490
|
+
return obj;
|
|
491
|
+
}
|
|
492
|
+
if (obj instanceof Date) {
|
|
493
|
+
return new Date(obj.getTime());
|
|
494
|
+
}
|
|
495
|
+
if (Array.isArray(obj)) {
|
|
496
|
+
return obj.map((item) => this.deepClone(item));
|
|
497
|
+
}
|
|
498
|
+
const cloned = {};
|
|
499
|
+
for (const key in obj) {
|
|
500
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
501
|
+
cloned[key] = this.deepClone(obj[key]);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return cloned;
|
|
505
|
+
}
|
|
506
|
+
// ==================== 向后兼容 ====================
|
|
507
|
+
/**
|
|
508
|
+
* @deprecated 使用 encode() 替代
|
|
509
|
+
*/
|
|
510
|
+
encoder(node) {
|
|
511
|
+
return this.encode(node);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { TCXDecoder } from "./decoder.js";
|
|
2
|
+
export { TCXFileType, TCXContext } from "./types.js";
|
|
3
|
+
export { AstGenerateProcessor, CompleteProcessor, ConvertProcessor, IPipelineProcessor, TokenizeProcessor, PipelineStage, } from "./processor.js";
|
|
4
|
+
export * from "./base.js";
|
|
5
|
+
export * from "./converters.js";
|
|
6
|
+
import { TCXDecoder } from "./decoder.js";
|
|
7
|
+
declare const _default: {
|
|
8
|
+
TCXDecoder: typeof TCXDecoder;
|
|
9
|
+
};
|
|
10
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// ============ 核心类和接口 ============
|
|
2
|
+
export { TCXDecoder } from "./decoder.js";
|
|
3
|
+
export { AstGenerateProcessor, CompleteProcessor, ConvertProcessor, TokenizeProcessor, PipelineStage, } from "./processor.js";
|
|
4
|
+
// ============ 基础架构 ============
|
|
5
|
+
export * from "./base.js";
|
|
6
|
+
// ============ 转换器 ============
|
|
7
|
+
export * from "./converters.js";
|
|
8
|
+
import { TCXDecoder } from "./decoder.js";
|
|
9
|
+
export default { TCXDecoder };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Token, TokenAST } from "../types.js";
|
|
2
|
+
import { BaseTCXMiddleware } from "./base.js";
|
|
3
|
+
import { TCXContext } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* 性能监控中间件
|
|
6
|
+
*/
|
|
7
|
+
export declare class PerformanceMiddleware extends BaseTCXMiddleware {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
onTokenize(tokens: Token[], context: TCXContext): Promise<Token[]>;
|
|
11
|
+
onAstGenerate(ast: TokenAST, context: TCXContext): Promise<TokenAST>;
|
|
12
|
+
onComplete(result: any, context: TCXContext): Promise<any>;
|
|
13
|
+
private countNodes;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 验证中间件
|
|
17
|
+
*/
|
|
18
|
+
export declare class ValidationMiddleware extends BaseTCXMiddleware {
|
|
19
|
+
name: string;
|
|
20
|
+
description: string;
|
|
21
|
+
onComplete(result: any, context: TCXContext): Promise<any>;
|
|
22
|
+
private validateGPX;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 注册所有默认中间件
|
|
26
|
+
*/
|
|
27
|
+
export declare function registerDefaultMiddlewares(decoder: any): Promise<void>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { BaseTCXMiddleware } from "./base.js";
|
|
2
|
+
// ============ 示例中间件 ============
|
|
3
|
+
/**
|
|
4
|
+
* 性能监控中间件
|
|
5
|
+
*/
|
|
6
|
+
export class PerformanceMiddleware extends BaseTCXMiddleware {
|
|
7
|
+
constructor() {
|
|
8
|
+
super(...arguments);
|
|
9
|
+
this.name = "performance-middleware";
|
|
10
|
+
this.description = "监控解析性能";
|
|
11
|
+
}
|
|
12
|
+
async onTokenize(tokens, context) {
|
|
13
|
+
console.log(`Token化完成,生成了 ${tokens.length} 个token`);
|
|
14
|
+
return tokens;
|
|
15
|
+
}
|
|
16
|
+
async onAstGenerate(ast, context) {
|
|
17
|
+
const nodeCount = this.countNodes(ast);
|
|
18
|
+
console.log(`AST生成完成,包含 ${nodeCount} 个节点`);
|
|
19
|
+
return ast;
|
|
20
|
+
}
|
|
21
|
+
async onComplete(result, context) {
|
|
22
|
+
const performance = context.metadata.get("performance");
|
|
23
|
+
console.log("解析性能统计:", performance);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
countNodes(ast) {
|
|
27
|
+
let count = 1;
|
|
28
|
+
if (ast.children) {
|
|
29
|
+
for (const child of ast.children) {
|
|
30
|
+
count += this.countNodes(child);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return count;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 验证中间件
|
|
38
|
+
*/
|
|
39
|
+
export class ValidationMiddleware extends BaseTCXMiddleware {
|
|
40
|
+
constructor() {
|
|
41
|
+
super(...arguments);
|
|
42
|
+
this.name = "validation-middleware";
|
|
43
|
+
this.description = "验证GPX数据的完整性";
|
|
44
|
+
}
|
|
45
|
+
async onComplete(result, context) {
|
|
46
|
+
const errors = this.validateGPX(result);
|
|
47
|
+
if (errors.length > 0) {
|
|
48
|
+
console.warn("GPX验证发现问题:", errors);
|
|
49
|
+
context.metadata.set("validation-errors", errors);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log("GPX验证通过");
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
validateGPX(gpx) {
|
|
57
|
+
const errors = [];
|
|
58
|
+
if (!gpx.version) {
|
|
59
|
+
errors.push("缺少GPX版本信息");
|
|
60
|
+
}
|
|
61
|
+
if (!gpx.creator) {
|
|
62
|
+
errors.push("缺少GPX创建者信息");
|
|
63
|
+
}
|
|
64
|
+
// 验证轨迹点
|
|
65
|
+
if (gpx.trk) {
|
|
66
|
+
for (const track of gpx.trk) {
|
|
67
|
+
if (track.trkseg) {
|
|
68
|
+
for (const segment of track.trkseg) {
|
|
69
|
+
if (segment.trkpt) {
|
|
70
|
+
for (const point of segment.trkpt) {
|
|
71
|
+
if (typeof point.lat !== "number" ||
|
|
72
|
+
typeof point.lon !== "number") {
|
|
73
|
+
errors.push("轨迹点坐标无效");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return errors;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ============ 默认转换器注册函数 ============
|
|
85
|
+
/**
|
|
86
|
+
* 注册所有默认中间件
|
|
87
|
+
*/
|
|
88
|
+
export async function registerDefaultMiddlewares(decoder) {
|
|
89
|
+
const middlewares = [new PerformanceMiddleware(), new ValidationMiddleware()];
|
|
90
|
+
for (const middleware of middlewares) {
|
|
91
|
+
await decoder.registerMiddleware(middleware);
|
|
92
|
+
}
|
|
93
|
+
}
|