skiz-parser 4.2.3 → 5.0.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/dist/index.d.ts +105 -0
- package/dist/index.js +268 -0
- package/package.json +18 -13
- package/index.d.ts +0 -119
- package/index.js +0 -299
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export interface SkizBatteryUsage {
|
|
2
|
+
level: number;
|
|
3
|
+
status: string;
|
|
4
|
+
timestamp: Date;
|
|
5
|
+
}
|
|
6
|
+
export interface SkizRelativeAltitude {
|
|
7
|
+
pressure: number;
|
|
8
|
+
relativeAltitude: number;
|
|
9
|
+
timestamp: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface SkizTrackEvent {
|
|
12
|
+
end: Date;
|
|
13
|
+
start: Date;
|
|
14
|
+
type: string;
|
|
15
|
+
}
|
|
16
|
+
export interface SkizTrackNode {
|
|
17
|
+
altitude: number;
|
|
18
|
+
hAccuracy: number;
|
|
19
|
+
heading: number;
|
|
20
|
+
latitude: number;
|
|
21
|
+
longitude: number;
|
|
22
|
+
timestamp: Date;
|
|
23
|
+
vAccuracy: number;
|
|
24
|
+
velocity: number;
|
|
25
|
+
}
|
|
26
|
+
export interface SkizTrackSegmentMetrics {
|
|
27
|
+
distance: number;
|
|
28
|
+
finishAltitude: number;
|
|
29
|
+
maxAltitude: number;
|
|
30
|
+
maxSlope: number;
|
|
31
|
+
maxSpeed: number;
|
|
32
|
+
minAltitude: number;
|
|
33
|
+
slope: number;
|
|
34
|
+
speed: number;
|
|
35
|
+
startAltitude: number;
|
|
36
|
+
time: number;
|
|
37
|
+
vertical: number;
|
|
38
|
+
}
|
|
39
|
+
export interface SkizTrackSegment {
|
|
40
|
+
category: string;
|
|
41
|
+
comment: string;
|
|
42
|
+
endTime: Date;
|
|
43
|
+
link: string;
|
|
44
|
+
metrics: SkizTrackSegmentMetrics;
|
|
45
|
+
name: string;
|
|
46
|
+
number: number;
|
|
47
|
+
startTime: Date;
|
|
48
|
+
type: string;
|
|
49
|
+
uuid: string;
|
|
50
|
+
}
|
|
51
|
+
export interface SkizTrackMetrics {
|
|
52
|
+
ascentDistance: number;
|
|
53
|
+
ascents: number;
|
|
54
|
+
averageAscentSpeed: number;
|
|
55
|
+
averageDescentSpeed: number;
|
|
56
|
+
averageSpeed: number;
|
|
57
|
+
descentDistance: number;
|
|
58
|
+
descents: number;
|
|
59
|
+
distance: number;
|
|
60
|
+
duration: number;
|
|
61
|
+
finishAltitude: number;
|
|
62
|
+
laps: number;
|
|
63
|
+
maxAltitude: number;
|
|
64
|
+
maxAscentSpeed: number;
|
|
65
|
+
maxAscentSteepness: number;
|
|
66
|
+
maxDescentSpeed: number;
|
|
67
|
+
maxDescentSteepness: number;
|
|
68
|
+
maxSpeed: number;
|
|
69
|
+
maxVerticalAscentSpeed: number;
|
|
70
|
+
maxVerticalDescentSpeed: number;
|
|
71
|
+
minAltitude: number;
|
|
72
|
+
movingAverageAscentSpeed: number;
|
|
73
|
+
movingAverageDescentSpeed: number;
|
|
74
|
+
movingAverageSpeed: number;
|
|
75
|
+
profileDistance: number;
|
|
76
|
+
startAltitude: number;
|
|
77
|
+
totalAscent: number;
|
|
78
|
+
totalDescent: number;
|
|
79
|
+
}
|
|
80
|
+
export interface SkizTrack {
|
|
81
|
+
activity: string;
|
|
82
|
+
batteryUsage: SkizBatteryUsage[];
|
|
83
|
+
conditions: string;
|
|
84
|
+
description: string;
|
|
85
|
+
duration: number;
|
|
86
|
+
finish: Date;
|
|
87
|
+
hidden: boolean;
|
|
88
|
+
includeInSeason: boolean;
|
|
89
|
+
name: string;
|
|
90
|
+
parseObjectId: string;
|
|
91
|
+
platform: string;
|
|
92
|
+
rating: number;
|
|
93
|
+
relativeAltitude: SkizRelativeAltitude[];
|
|
94
|
+
start: Date;
|
|
95
|
+
syncIdentifier: string;
|
|
96
|
+
syncVersion: number;
|
|
97
|
+
trackEvents: SkizTrackEvent[];
|
|
98
|
+
trackMetrics: SkizTrackMetrics;
|
|
99
|
+
trackNodes: SkizTrackNode[];
|
|
100
|
+
trackSegments: SkizTrackSegment[];
|
|
101
|
+
tz: string;
|
|
102
|
+
version: string;
|
|
103
|
+
weather: string;
|
|
104
|
+
}
|
|
105
|
+
export declare const parseSkizFile: (contents: ArrayBuffer | Buffer) => Promise<SkizTrack>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { parse as CSVParser } from 'csv-parse';
|
|
2
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
3
|
+
import { fromBuffer } from 'yauzl';
|
|
4
|
+
const xmlParserOptions = {
|
|
5
|
+
attributeNamePrefix: '',
|
|
6
|
+
ignoreAttributes: false,
|
|
7
|
+
};
|
|
8
|
+
const convertReadStreamToBuffer = (readStream) => {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
readStream
|
|
12
|
+
.on('error', reject)
|
|
13
|
+
.on('data', (chunk) => {
|
|
14
|
+
chunks.push(chunk);
|
|
15
|
+
})
|
|
16
|
+
.once('end', () => {
|
|
17
|
+
resolve(Buffer.concat(chunks));
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const openReadStream = (zipFile, entry) => {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
zipFile.openReadStream(entry, (err, readStream) => {
|
|
24
|
+
err ? reject(err) : resolve(readStream);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const parseBatteryCsvFile = (readStream) => {
|
|
29
|
+
const parser = CSVParser();
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const batteryUsage = [];
|
|
32
|
+
parser
|
|
33
|
+
.on('error', reject)
|
|
34
|
+
.on('readable', () => {
|
|
35
|
+
let record;
|
|
36
|
+
while ((record = parser.read()) !== null) {
|
|
37
|
+
batteryUsage.push({
|
|
38
|
+
timestamp: new Date(record[0]),
|
|
39
|
+
status: record[1],
|
|
40
|
+
level: parseFloat(record[2]),
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.once('end', () => {
|
|
45
|
+
resolve(batteryUsage);
|
|
46
|
+
});
|
|
47
|
+
readStream.pipe(parser);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const parseNodeCsvFile = (readStream) => {
|
|
51
|
+
const parser = CSVParser();
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
const trackNodes = [];
|
|
54
|
+
parser
|
|
55
|
+
.on('error', reject)
|
|
56
|
+
.on('readable', () => {
|
|
57
|
+
let record;
|
|
58
|
+
while ((record = parser.read()) !== null) {
|
|
59
|
+
trackNodes.push({
|
|
60
|
+
timestamp: new Date(parseFloat(record[0]) * 1000.0),
|
|
61
|
+
latitude: parseFloat(record[1]),
|
|
62
|
+
longitude: parseFloat(record[2]),
|
|
63
|
+
altitude: parseFloat(record[3]),
|
|
64
|
+
heading: parseFloat(record[4]),
|
|
65
|
+
velocity: parseFloat(record[5]),
|
|
66
|
+
hAccuracy: parseFloat(record[6]),
|
|
67
|
+
vAccuracy: parseFloat(record[7]),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.once('end', () => {
|
|
72
|
+
resolve(trackNodes);
|
|
73
|
+
});
|
|
74
|
+
readStream.pipe(parser);
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
const parseRelativeAltitudeSensorCsvFile = (readStream) => {
|
|
78
|
+
const parser = CSVParser();
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
const relativeAltitude = [];
|
|
81
|
+
readStream.pipe(parser);
|
|
82
|
+
parser
|
|
83
|
+
.on('error', reject)
|
|
84
|
+
.on('readable', () => {
|
|
85
|
+
let record;
|
|
86
|
+
while ((record = parser.read()) !== null) {
|
|
87
|
+
relativeAltitude.push({
|
|
88
|
+
timestamp: new Date(record[0]),
|
|
89
|
+
pressure: parseFloat(record[1]),
|
|
90
|
+
relativeAltitude: parseFloat(record[2]),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
.once('end', () => {
|
|
95
|
+
resolve(relativeAltitude);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
const parseSegmentCsvFile = (readStream) => {
|
|
100
|
+
const parser = CSVParser({ fromLine: 2 });
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
const trackSegments = [];
|
|
103
|
+
parser
|
|
104
|
+
.on('error', reject)
|
|
105
|
+
.on('readable', () => {
|
|
106
|
+
let record;
|
|
107
|
+
while ((record = parser.read()) !== null) {
|
|
108
|
+
trackSegments.push({
|
|
109
|
+
startTime: new Date(parseFloat(record[0]) * 1000),
|
|
110
|
+
endTime: new Date(parseFloat(record[1]) * 1000),
|
|
111
|
+
number: parseInt(record[4], 10),
|
|
112
|
+
name: record[5],
|
|
113
|
+
comment: record[6],
|
|
114
|
+
type: record[7],
|
|
115
|
+
category: record[8],
|
|
116
|
+
link: record[9],
|
|
117
|
+
uuid: record[10],
|
|
118
|
+
metrics: {
|
|
119
|
+
time: parseFloat(record[11]),
|
|
120
|
+
speed: parseFloat(record[12]),
|
|
121
|
+
distance: parseFloat(record[13]),
|
|
122
|
+
vertical: parseFloat(record[14]),
|
|
123
|
+
maxSpeed: parseFloat(record[15]),
|
|
124
|
+
slope: parseFloat(record[16]),
|
|
125
|
+
maxSlope: parseFloat(record[17]),
|
|
126
|
+
minAltitude: parseFloat(record[18]),
|
|
127
|
+
maxAltitude: parseFloat(record[19]),
|
|
128
|
+
startAltitude: parseFloat(record[20]),
|
|
129
|
+
finishAltitude: parseFloat(record[21]),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
.once('end', () => {
|
|
135
|
+
resolve(trackSegments);
|
|
136
|
+
});
|
|
137
|
+
readStream.pipe(parser);
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const parseTrackXmlFile = async (readStream) => {
|
|
141
|
+
const buffer = await convertReadStreamToBuffer(readStream);
|
|
142
|
+
const parser = new XMLParser(xmlParserOptions);
|
|
143
|
+
const parsed = parser.parse(buffer.toString('utf-8'));
|
|
144
|
+
const track = parsed.track;
|
|
145
|
+
const metrics = track.metrics;
|
|
146
|
+
return {
|
|
147
|
+
rating: parseInt(track.rating, 10),
|
|
148
|
+
start: new Date(Date.parse(track.start)),
|
|
149
|
+
finish: new Date(Date.parse(track.finish)),
|
|
150
|
+
description: track.description,
|
|
151
|
+
version: track.version,
|
|
152
|
+
parseObjectId: track.parseObjectId,
|
|
153
|
+
tz: track.tz,
|
|
154
|
+
includeInSeason: track.includeinseason === 'true',
|
|
155
|
+
syncIdentifier: track.syncIdentifier,
|
|
156
|
+
conditions: track.conditions,
|
|
157
|
+
platform: track.platform,
|
|
158
|
+
weather: track.weather,
|
|
159
|
+
activity: track.activity,
|
|
160
|
+
hidden: track.hidden === 'true',
|
|
161
|
+
duration: parseFloat(track.duration),
|
|
162
|
+
name: track.name,
|
|
163
|
+
syncVersion: parseInt(track.syncVersion, 10),
|
|
164
|
+
trackMetrics: {
|
|
165
|
+
maxSpeed: metrics.maxspeed,
|
|
166
|
+
maxDescentSpeed: metrics.maxdescentspeed,
|
|
167
|
+
maxAscentSpeed: metrics.maxascentspeed,
|
|
168
|
+
maxDescentSteepness: metrics.maxdescentsteepness,
|
|
169
|
+
maxAscentSteepness: metrics.maxascentsteepness,
|
|
170
|
+
maxVerticalDescentSpeed: metrics.maxverticaldescentspeed,
|
|
171
|
+
maxVerticalAscentSpeed: metrics.maxverticalascentspeed,
|
|
172
|
+
totalAscent: metrics.totalascent,
|
|
173
|
+
totalDescent: metrics.totaldescent,
|
|
174
|
+
maxAltitude: metrics.maxaltitude,
|
|
175
|
+
minAltitude: metrics.minaltitude,
|
|
176
|
+
distance: metrics.distance,
|
|
177
|
+
profileDistance: metrics.profiledistance,
|
|
178
|
+
descentDistance: metrics.descentdistance,
|
|
179
|
+
ascentDistance: metrics.ascentdistance,
|
|
180
|
+
averageSpeed: metrics.averagespeed,
|
|
181
|
+
averageDescentSpeed: metrics.averagedescentspeed,
|
|
182
|
+
averageAscentSpeed: metrics.averageascentspeed,
|
|
183
|
+
movingAverageSpeed: metrics.movingaveragespeed,
|
|
184
|
+
movingAverageDescentSpeed: metrics.movingaveragedescentspeed,
|
|
185
|
+
movingAverageAscentSpeed: metrics.movingaverageascentspeed,
|
|
186
|
+
duration: metrics.duration,
|
|
187
|
+
startAltitude: metrics.startaltitude,
|
|
188
|
+
finishAltitude: metrics.finishaltitude,
|
|
189
|
+
ascents: metrics.ascents,
|
|
190
|
+
descents: metrics.descents,
|
|
191
|
+
laps: metrics.laps,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
const parseEventsXmlFile = async (readStream) => {
|
|
196
|
+
const buffer = await convertReadStreamToBuffer(readStream);
|
|
197
|
+
const parser = new XMLParser(xmlParserOptions);
|
|
198
|
+
const parsed = parser.parse(buffer.toString('utf-8'));
|
|
199
|
+
const events = parsed.events.event || [];
|
|
200
|
+
const trackEvents = events.map((event) => ({
|
|
201
|
+
start: new Date(Date.parse(event.start)),
|
|
202
|
+
end: new Date(Date.parse(event.end)),
|
|
203
|
+
type: event.type,
|
|
204
|
+
}));
|
|
205
|
+
return trackEvents;
|
|
206
|
+
};
|
|
207
|
+
export const parseSkizFile = (contents) => {
|
|
208
|
+
const buffer = contents instanceof ArrayBuffer ? Buffer.from(contents) : contents;
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
fromBuffer(buffer, { lazyEntries: true }, (err, zipFile) => {
|
|
211
|
+
if (err) {
|
|
212
|
+
return reject(err);
|
|
213
|
+
}
|
|
214
|
+
let skizTrackXml;
|
|
215
|
+
let batteryUsage;
|
|
216
|
+
let relativeAltitude;
|
|
217
|
+
let trackEvents;
|
|
218
|
+
let trackNodes;
|
|
219
|
+
let trackSegments;
|
|
220
|
+
zipFile.readEntry();
|
|
221
|
+
zipFile
|
|
222
|
+
.on('error', reject)
|
|
223
|
+
.on('entry', async (entry) => {
|
|
224
|
+
if (![
|
|
225
|
+
'Battery.csv',
|
|
226
|
+
'Events.xml',
|
|
227
|
+
'Nodes.csv',
|
|
228
|
+
'RelativeAltitudeSensor.csv',
|
|
229
|
+
'Segment.csv',
|
|
230
|
+
'Track.xml',
|
|
231
|
+
].includes(entry.fileName)) {
|
|
232
|
+
return zipFile.readEntry();
|
|
233
|
+
}
|
|
234
|
+
const readStream = await openReadStream(zipFile, entry);
|
|
235
|
+
if (entry.fileName === 'Battery.csv') {
|
|
236
|
+
batteryUsage = await parseBatteryCsvFile(readStream);
|
|
237
|
+
}
|
|
238
|
+
else if (entry.fileName === 'Events.xml') {
|
|
239
|
+
trackEvents = await parseEventsXmlFile(readStream);
|
|
240
|
+
}
|
|
241
|
+
else if (entry.fileName === 'Nodes.csv') {
|
|
242
|
+
trackNodes = await parseNodeCsvFile(readStream);
|
|
243
|
+
}
|
|
244
|
+
else if (entry.fileName === 'RelativeAltitudeSensor.csv') {
|
|
245
|
+
relativeAltitude =
|
|
246
|
+
await parseRelativeAltitudeSensorCsvFile(readStream);
|
|
247
|
+
}
|
|
248
|
+
else if (entry.fileName === 'Segment.csv') {
|
|
249
|
+
trackSegments = await parseSegmentCsvFile(readStream);
|
|
250
|
+
}
|
|
251
|
+
else if (entry.fileName === 'Track.xml') {
|
|
252
|
+
skizTrackXml = await parseTrackXmlFile(readStream);
|
|
253
|
+
}
|
|
254
|
+
zipFile.readEntry();
|
|
255
|
+
})
|
|
256
|
+
.once('end', () => {
|
|
257
|
+
resolve({
|
|
258
|
+
...skizTrackXml,
|
|
259
|
+
batteryUsage,
|
|
260
|
+
relativeAltitude,
|
|
261
|
+
trackEvents,
|
|
262
|
+
trackNodes,
|
|
263
|
+
trackSegments,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
};
|
package/package.json
CHANGED
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skiz-parser",
|
|
3
|
+
"version": "5.0.0",
|
|
3
4
|
"description": "Parse a .skiz file exported from Ski Tracks",
|
|
4
|
-
"version": "4.2.3",
|
|
5
5
|
"author": "Kirk Eaton <contact@kirkeaton.ca>",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/kirkeaton/skiz-parser/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"csv-parse": "5.6.0",
|
|
11
|
-
"fast-xml-parser": "4.5.
|
|
11
|
+
"fast-xml-parser": "4.5.1",
|
|
12
12
|
"yauzl": "3.2.0"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
|
-
"@kirkeaton/prettier-config": "1.0.
|
|
16
|
-
"@kirkeaton/semantic-release-config": "1.0.
|
|
17
|
-
"@
|
|
18
|
-
"
|
|
15
|
+
"@kirkeaton/prettier-config": "1.0.3",
|
|
16
|
+
"@kirkeaton/semantic-release-config": "1.0.2",
|
|
17
|
+
"@kirkeaton/tsconfig": "1.0.0",
|
|
18
|
+
"@types/node": "22.10.2",
|
|
19
|
+
"@types/yauzl": "2.10.3",
|
|
20
|
+
"prettier": "3.4.2",
|
|
19
21
|
"semantic-release": "24.2.0",
|
|
20
|
-
"
|
|
22
|
+
"tsx": "4.19.2",
|
|
23
|
+
"typescript": "5.7.2"
|
|
21
24
|
},
|
|
22
25
|
"engines": {
|
|
23
26
|
"node": ">=18"
|
|
24
27
|
},
|
|
25
28
|
"exports": {
|
|
26
|
-
"types": "./index.d.ts",
|
|
27
|
-
"default": "./index.js"
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"default": "./dist/index.js"
|
|
28
31
|
},
|
|
29
32
|
"files": [
|
|
30
|
-
"
|
|
31
|
-
"index.d.ts"
|
|
33
|
+
"dist"
|
|
32
34
|
],
|
|
33
35
|
"homepage": "https://github.com/kirkeaton/skiz-parser#readme",
|
|
34
36
|
"keywords": [
|
|
@@ -38,13 +40,16 @@
|
|
|
38
40
|
"skiz"
|
|
39
41
|
],
|
|
40
42
|
"license": "BSD-3-Clause",
|
|
41
|
-
"packageManager": "pnpm@9.
|
|
43
|
+
"packageManager": "pnpm@9.15.1",
|
|
42
44
|
"repository": {
|
|
43
45
|
"type": "git",
|
|
44
46
|
"url": "git+https://github.com/kirkeaton/skiz-parser.git"
|
|
45
47
|
},
|
|
46
48
|
"scripts": {
|
|
47
|
-
"
|
|
49
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
50
|
+
"clean": "rm -rf dist",
|
|
51
|
+
"prepublishOnly": "pnpm build",
|
|
52
|
+
"test": "tsx --test"
|
|
48
53
|
},
|
|
49
54
|
"type": "module"
|
|
50
55
|
}
|
package/index.d.ts
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
export interface SkizBatteryUsage {
|
|
2
|
-
level: number;
|
|
3
|
-
status: string;
|
|
4
|
-
timestamp: Date;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export interface SkizRelativeAltitude {
|
|
8
|
-
pressure: number;
|
|
9
|
-
relativeAltitude: number;
|
|
10
|
-
timestamp: Date;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface SkizTrackEvent {
|
|
14
|
-
end: Date;
|
|
15
|
-
start: Date;
|
|
16
|
-
type: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface SkizTrackNode {
|
|
20
|
-
altitude: number;
|
|
21
|
-
hAccuracy: number;
|
|
22
|
-
heading: number;
|
|
23
|
-
latitude: number;
|
|
24
|
-
longitude: number;
|
|
25
|
-
timestamp: Date;
|
|
26
|
-
vAccuracy: number;
|
|
27
|
-
velocity: number;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface SkizTrackSegmentMetrics {
|
|
31
|
-
distance: number;
|
|
32
|
-
finishAltitude: number;
|
|
33
|
-
maxAltitude: number;
|
|
34
|
-
maxSlope: number;
|
|
35
|
-
maxSpeed: number;
|
|
36
|
-
minAltitude: number;
|
|
37
|
-
slope: number;
|
|
38
|
-
speed: number;
|
|
39
|
-
startAltitude: number;
|
|
40
|
-
time: number;
|
|
41
|
-
vertical: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SkizTrackSegment {
|
|
45
|
-
category: string;
|
|
46
|
-
comment: string;
|
|
47
|
-
endTime: Date;
|
|
48
|
-
link: string;
|
|
49
|
-
metrics: SkizTrackSegmentMetrics;
|
|
50
|
-
name: string;
|
|
51
|
-
number: number;
|
|
52
|
-
startTime: Date;
|
|
53
|
-
type: string;
|
|
54
|
-
uuid: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface SkizTrackMetrics {
|
|
58
|
-
ascentDistance: number;
|
|
59
|
-
ascents: number;
|
|
60
|
-
averageAscentSpeed: number;
|
|
61
|
-
averageDescentSpeed: number;
|
|
62
|
-
averageSpeed: number;
|
|
63
|
-
descentDistance: number;
|
|
64
|
-
descents: number;
|
|
65
|
-
distance: number;
|
|
66
|
-
duration: number;
|
|
67
|
-
finishAltitude: number;
|
|
68
|
-
laps: number;
|
|
69
|
-
maxAltitude: number;
|
|
70
|
-
maxAscentSpeed: number;
|
|
71
|
-
maxAscentSteepness: number;
|
|
72
|
-
maxDescentSpeed: number;
|
|
73
|
-
maxDescentSteepness: number;
|
|
74
|
-
maxSpeed: number;
|
|
75
|
-
maxVerticalAscentSpeed: number;
|
|
76
|
-
maxVerticalDescentSpeed: number;
|
|
77
|
-
minAltitude: number;
|
|
78
|
-
movingAverageAscentSpeed: number;
|
|
79
|
-
movingAverageDescentSpeed: number;
|
|
80
|
-
movingAverageSpeed: number;
|
|
81
|
-
profileDistance: number;
|
|
82
|
-
startAltitude: number;
|
|
83
|
-
totalAscent: number;
|
|
84
|
-
totalDescent: number;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface SkizTrack {
|
|
88
|
-
activity: string;
|
|
89
|
-
batteryUsage: SkizBatteryUsage[];
|
|
90
|
-
conditions: string;
|
|
91
|
-
description: string;
|
|
92
|
-
duration: number;
|
|
93
|
-
finish: Date;
|
|
94
|
-
hidden: boolean;
|
|
95
|
-
includeInSeason: boolean;
|
|
96
|
-
name: string;
|
|
97
|
-
parseObjectId: string;
|
|
98
|
-
platform: string;
|
|
99
|
-
rating: number;
|
|
100
|
-
relativeAltitude: SkizRelativeAltitude[];
|
|
101
|
-
start: Date;
|
|
102
|
-
syncIdentifier: string;
|
|
103
|
-
syncVersion: number;
|
|
104
|
-
trackEvents: SkizTrackEvent[];
|
|
105
|
-
trackMetrics: SkizTrackMetrics;
|
|
106
|
-
trackNodes: SkizTrackNode[];
|
|
107
|
-
trackSegments: SkizTrackSegment[];
|
|
108
|
-
tz: string;
|
|
109
|
-
version: string;
|
|
110
|
-
weather: string;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export function parseSkizFile(
|
|
114
|
-
contents: ArrayBuffer | Buffer
|
|
115
|
-
): Promise<SkizTrack>;
|
|
116
|
-
export function parseSkizFile(
|
|
117
|
-
contents: ArrayBuffer | Buffer,
|
|
118
|
-
callback: (err: Error | null, result: SkizTrack) => void
|
|
119
|
-
): void;
|
package/index.js
DELETED
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import { parse as CSVParser } from 'csv-parse';
|
|
2
|
-
import { XMLParser } from 'fast-xml-parser';
|
|
3
|
-
import yauzl from 'yauzl';
|
|
4
|
-
|
|
5
|
-
const xmlParserOptions = {
|
|
6
|
-
attributeNamePrefix: '',
|
|
7
|
-
ignoreAttributes: false,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
const convertReadStreamToBuffer = (readStream) => {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
12
|
-
const chunks = [];
|
|
13
|
-
|
|
14
|
-
readStream
|
|
15
|
-
.on('error', reject)
|
|
16
|
-
.on('data', (chunk) => {
|
|
17
|
-
chunks.push(chunk);
|
|
18
|
-
})
|
|
19
|
-
.once('end', () => {
|
|
20
|
-
resolve(Buffer.concat(chunks));
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const openReadStream = (zipFile, entry) => {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
zipFile.openReadStream(entry, (err, readStream) => {
|
|
28
|
-
err ? reject(err) : resolve(readStream);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const parseBatteryCsvFile = (readStream) => {
|
|
34
|
-
const parser = CSVParser();
|
|
35
|
-
|
|
36
|
-
return new Promise((resolve, reject) => {
|
|
37
|
-
const batteryUsage = [];
|
|
38
|
-
|
|
39
|
-
parser
|
|
40
|
-
.on('error', reject)
|
|
41
|
-
.on('readable', () => {
|
|
42
|
-
let record;
|
|
43
|
-
|
|
44
|
-
while ((record = parser.read()) !== null) {
|
|
45
|
-
batteryUsage.push({
|
|
46
|
-
timestamp: new Date(record[0]),
|
|
47
|
-
status: record[1],
|
|
48
|
-
level: parseFloat(record[2]),
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
.once('end', () => {
|
|
53
|
-
resolve({ batteryUsage });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
readStream.pipe(parser);
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const parseNodeCsvFile = (readStream) => {
|
|
61
|
-
const parser = CSVParser();
|
|
62
|
-
|
|
63
|
-
return new Promise((resolve, reject) => {
|
|
64
|
-
const trackNodes = [];
|
|
65
|
-
|
|
66
|
-
parser
|
|
67
|
-
.on('error', reject)
|
|
68
|
-
.on('readable', () => {
|
|
69
|
-
let record;
|
|
70
|
-
|
|
71
|
-
while ((record = parser.read()) !== null) {
|
|
72
|
-
trackNodes.push({
|
|
73
|
-
timestamp: new Date(parseFloat(record[0]) * 1000.0),
|
|
74
|
-
latitude: parseFloat(record[1]),
|
|
75
|
-
longitude: parseFloat(record[2]),
|
|
76
|
-
altitude: parseFloat(record[3]),
|
|
77
|
-
heading: parseFloat(record[4]),
|
|
78
|
-
velocity: parseFloat(record[5]),
|
|
79
|
-
hAccuracy: parseFloat(record[6]),
|
|
80
|
-
vAccuracy: parseFloat(record[7]),
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
})
|
|
84
|
-
.once('end', () => {
|
|
85
|
-
resolve({ trackNodes });
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
readStream.pipe(parser);
|
|
89
|
-
});
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
const parseRelativeAltitudeSensorCsvFile = (readStream) => {
|
|
93
|
-
const parser = CSVParser();
|
|
94
|
-
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
const relativeAltitude = [];
|
|
97
|
-
|
|
98
|
-
readStream.pipe(parser);
|
|
99
|
-
|
|
100
|
-
parser
|
|
101
|
-
.on('error', reject)
|
|
102
|
-
.on('readable', (data) => {
|
|
103
|
-
let record;
|
|
104
|
-
|
|
105
|
-
while ((record = parser.read()) !== null) {
|
|
106
|
-
relativeAltitude.push({
|
|
107
|
-
timestamp: new Date(record[0]),
|
|
108
|
-
pressure: parseFloat(record[1]),
|
|
109
|
-
relativeAltitude: parseFloat(record[2]),
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
})
|
|
113
|
-
.once('end', () => {
|
|
114
|
-
resolve({ relativeAltitude });
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const parseSegmentCsvFile = (readStream) => {
|
|
120
|
-
const parser = CSVParser({ fromLine: 2 });
|
|
121
|
-
|
|
122
|
-
return new Promise((resolve, reject) => {
|
|
123
|
-
const trackSegments = [];
|
|
124
|
-
|
|
125
|
-
parser
|
|
126
|
-
.on('error', reject)
|
|
127
|
-
.on('readable', () => {
|
|
128
|
-
let record;
|
|
129
|
-
|
|
130
|
-
while ((record = parser.read()) !== null) {
|
|
131
|
-
trackSegments.push({
|
|
132
|
-
startTime: new Date(parseFloat(record[0]) * 1000),
|
|
133
|
-
endTime: new Date(parseFloat(record[1]) * 1000),
|
|
134
|
-
number: parseInt(record[4], 10),
|
|
135
|
-
name: record[5],
|
|
136
|
-
comment: record[6],
|
|
137
|
-
type: record[7],
|
|
138
|
-
category: record[8],
|
|
139
|
-
link: record[9],
|
|
140
|
-
uuid: record[10],
|
|
141
|
-
metrics: {
|
|
142
|
-
time: parseFloat(record[11]),
|
|
143
|
-
speed: parseFloat(record[12]),
|
|
144
|
-
distance: parseFloat(record[13]),
|
|
145
|
-
vertical: parseFloat(record[14]),
|
|
146
|
-
maxSpeed: parseFloat(record[15]),
|
|
147
|
-
slope: parseFloat(record[16]),
|
|
148
|
-
maxSlope: parseFloat(record[17]),
|
|
149
|
-
minAltitude: parseFloat(record[18]),
|
|
150
|
-
maxAltitude: parseFloat(record[19]),
|
|
151
|
-
startAltitude: parseFloat(record[20]),
|
|
152
|
-
finishAltitude: parseFloat(record[21]),
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
})
|
|
157
|
-
.once('end', () => {
|
|
158
|
-
resolve({ trackSegments });
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
readStream.pipe(parser);
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const parseTrackXmlFile = async (readStream) => {
|
|
166
|
-
const buffer = await convertReadStreamToBuffer(readStream);
|
|
167
|
-
const parser = new XMLParser(xmlParserOptions);
|
|
168
|
-
const parsed = parser.parse(buffer.toString('utf-8'));
|
|
169
|
-
|
|
170
|
-
const track = parsed.track;
|
|
171
|
-
const metrics = track.metrics;
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
rating: parseInt(track.rating, 10),
|
|
175
|
-
start: new Date(Date.parse(track.start)),
|
|
176
|
-
finish: new Date(Date.parse(track.finish)),
|
|
177
|
-
description: track.description,
|
|
178
|
-
version: track.version,
|
|
179
|
-
parseObjectId: track.parseObjectId,
|
|
180
|
-
tz: track.tz,
|
|
181
|
-
includeInSeason: track.includeinseason === 'true',
|
|
182
|
-
syncIdentifier: track.syncIdentifier,
|
|
183
|
-
conditions: track.conditions,
|
|
184
|
-
platform: track.platform,
|
|
185
|
-
weather: track.weather,
|
|
186
|
-
activity: track.activity,
|
|
187
|
-
hidden: track.hidden === 'true',
|
|
188
|
-
duration: parseFloat(track.duration),
|
|
189
|
-
name: track.name,
|
|
190
|
-
syncVersion: parseInt(track.syncVersion, 10),
|
|
191
|
-
trackMetrics: {
|
|
192
|
-
maxSpeed: metrics.maxspeed,
|
|
193
|
-
maxDescentSpeed: metrics.maxdescentspeed,
|
|
194
|
-
maxAscentSpeed: metrics.maxascentspeed,
|
|
195
|
-
maxDescentSteepness: metrics.maxdescentsteepness,
|
|
196
|
-
maxAscentSteepness: metrics.maxascentsteepness,
|
|
197
|
-
maxVerticalDescentSpeed: metrics.maxverticaldescentspeed,
|
|
198
|
-
maxVerticalAscentSpeed: metrics.maxverticalascentspeed,
|
|
199
|
-
totalAscent: metrics.totalascent,
|
|
200
|
-
totalDescent: metrics.totaldescent,
|
|
201
|
-
maxAltitude: metrics.maxaltitude,
|
|
202
|
-
minAltitude: metrics.minaltitude,
|
|
203
|
-
distance: metrics.distance,
|
|
204
|
-
profileDistance: metrics.profiledistance,
|
|
205
|
-
descentDistance: metrics.descentdistance,
|
|
206
|
-
ascentDistance: metrics.ascentdistance,
|
|
207
|
-
averageSpeed: metrics.averagespeed,
|
|
208
|
-
averageDescentSpeed: metrics.averagedescentspeed,
|
|
209
|
-
averageAscentSpeed: metrics.averageascentspeed,
|
|
210
|
-
movingAverageSpeed: metrics.movingaveragespeed,
|
|
211
|
-
movingAverageDescentSpeed: metrics.movingaveragedescentspeed,
|
|
212
|
-
movingAverageAscentSpeed: metrics.movingaverageascentspeed,
|
|
213
|
-
duration: metrics.duration,
|
|
214
|
-
startAltitude: metrics.startaltitude,
|
|
215
|
-
finishAltitude: metrics.finishaltitude,
|
|
216
|
-
ascents: metrics.ascents,
|
|
217
|
-
descents: metrics.descents,
|
|
218
|
-
laps: metrics.laps,
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const parseEventsXmlFile = async (readStream) => {
|
|
224
|
-
const buffer = await convertReadStreamToBuffer(readStream);
|
|
225
|
-
const parser = new XMLParser(xmlParserOptions);
|
|
226
|
-
const parsed = parser.parse(buffer.toString('utf-8'));
|
|
227
|
-
|
|
228
|
-
const events = parsed.events.event || [];
|
|
229
|
-
const trackEvents = events.map((event) => ({
|
|
230
|
-
start: new Date(Date.parse(event.start)),
|
|
231
|
-
end: new Date(Date.parse(event.end)),
|
|
232
|
-
type: event.type,
|
|
233
|
-
}));
|
|
234
|
-
|
|
235
|
-
return { trackEvents };
|
|
236
|
-
};
|
|
237
|
-
|
|
238
|
-
export const parseSkizFile = (contents, callback) => {
|
|
239
|
-
const parse = (resolve, reject) => {
|
|
240
|
-
if (contents instanceof ArrayBuffer) {
|
|
241
|
-
contents = Buffer.from(contents);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
yauzl.fromBuffer(contents, { lazyEntries: true }, (err, zipFile) => {
|
|
245
|
-
if (err) {
|
|
246
|
-
return reject(err);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
let data = {};
|
|
250
|
-
|
|
251
|
-
zipFile.readEntry();
|
|
252
|
-
|
|
253
|
-
zipFile
|
|
254
|
-
.on('error', reject)
|
|
255
|
-
.on('entry', async (entry) => {
|
|
256
|
-
if (
|
|
257
|
-
![
|
|
258
|
-
'Battery.csv',
|
|
259
|
-
'Events.xml',
|
|
260
|
-
'Nodes.csv',
|
|
261
|
-
'RelativeAltitudeSensor.csv',
|
|
262
|
-
'Segment.csv',
|
|
263
|
-
'Track.xml',
|
|
264
|
-
].includes(entry.fileName)
|
|
265
|
-
) {
|
|
266
|
-
return zipFile.readEntry();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const readStream = await openReadStream(zipFile, entry);
|
|
270
|
-
|
|
271
|
-
let result;
|
|
272
|
-
if (entry.fileName === 'Battery.csv') {
|
|
273
|
-
result = await parseBatteryCsvFile(readStream);
|
|
274
|
-
} else if (entry.fileName === 'Events.xml') {
|
|
275
|
-
result = await parseEventsXmlFile(readStream);
|
|
276
|
-
} else if (entry.fileName === 'Nodes.csv') {
|
|
277
|
-
result = await parseNodeCsvFile(readStream);
|
|
278
|
-
} else if (entry.fileName === 'RelativeAltitudeSensor.csv') {
|
|
279
|
-
result = await parseRelativeAltitudeSensorCsvFile(readStream);
|
|
280
|
-
} else if (entry.fileName === 'Segment.csv') {
|
|
281
|
-
result = await parseSegmentCsvFile(readStream);
|
|
282
|
-
} else if (entry.fileName === 'Track.xml') {
|
|
283
|
-
result = await parseTrackXmlFile(readStream);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
data = { ...data, ...result };
|
|
287
|
-
|
|
288
|
-
zipFile.readEntry();
|
|
289
|
-
})
|
|
290
|
-
.once('end', () => {
|
|
291
|
-
resolve(data);
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
return typeof callback === 'function'
|
|
297
|
-
? parse(callback.bind(null, null), callback)
|
|
298
|
-
: new Promise(parse);
|
|
299
|
-
};
|