skiz-parser 4.2.4 → 5.0.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # skiz-parser
2
2
 
3
- > Parse a .skiz file exported from Ski Tracks
3
+ Parse a .skiz file exported from Ski Tracks
4
4
 
5
5
  ## Install
6
6
 
@@ -11,10 +11,10 @@ $ npm install skiz-parser
11
11
  ## Usage
12
12
 
13
13
  ```js
14
- import { promises as fsAsync } from 'fs';
14
+ import { readFile } from 'node:fs/promises';
15
15
  import { parseSkizFile } from 'skiz-parser';
16
16
 
17
- const contents = await fsAsync.readFile('./example.skiz');
17
+ const contents = await readFile('./example.skiz');
18
18
  const result = await parseSkizFile(contents);
19
19
 
20
20
  console.log(result);
@@ -23,20 +23,12 @@ console.log(result);
23
23
 
24
24
  ## API
25
25
 
26
- ### parseSkizFile(contents, callback?)
26
+ ### parseSkizFile(contents)
27
27
 
28
- Returns a `Promise<object>` with the parsed .skiz file.
29
-
30
- Optionally a callback function may be used instead.
28
+ Returns a `Promise<SkizTrack>` with the parsed .skiz file.
31
29
 
32
30
  #### contents
33
31
 
34
- Type: `Buffer`
32
+ Type: `ArrayBuffer | Buffer`
35
33
 
36
34
  Contents of a `.skiz` file.
37
-
38
- #### callback
39
-
40
- Type: `Function`
41
-
42
- Optional callback with error as the first argument and the parsed .skiz file as the second.
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "skiz-parser",
3
- "version": "4.2.4",
3
+ "version": "5.0.1",
4
4
  "description": "Parse a .skiz file exported from Ski Tracks",
5
5
  "author": "Kirk Eaton <contact@kirkeaton.ca>",
6
6
  "bugs": {
@@ -12,23 +12,25 @@
12
12
  "yauzl": "3.2.0"
13
13
  },
14
14
  "devDependencies": {
15
- "@kirkeaton/prettier-config": "1.0.2",
16
- "@kirkeaton/semantic-release-config": "1.0.1",
15
+ "@kirkeaton/prettier-config": "1.0.3",
16
+ "@kirkeaton/semantic-release-config": "1.0.2",
17
+ "@kirkeaton/tsconfig": "1.0.0",
17
18
  "@types/node": "22.10.2",
19
+ "@types/yauzl": "2.10.3",
18
20
  "prettier": "3.4.2",
19
21
  "semantic-release": "24.2.0",
20
- "tsd": "0.31.2"
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
- "index.js",
31
- "index.d.ts"
33
+ "dist"
32
34
  ],
33
35
  "homepage": "https://github.com/kirkeaton/skiz-parser#readme",
34
36
  "keywords": [
@@ -44,7 +46,10 @@
44
46
  "url": "git+https://github.com/kirkeaton/skiz-parser.git"
45
47
  },
46
48
  "scripts": {
47
- "test": "node --test && tsd"
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
- };