gpx-from-gopro 0.4.0 → 0.6.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 +5 -2
- package/package.json +2 -2
- package/src/gopro-cli.js +67 -16
package/README.md
CHANGED
|
@@ -20,7 +20,8 @@ No install needed:
|
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
22
|
npx gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ] [--cache-dir DIR | --no-cache]
|
|
23
|
-
[--organize DIR] [--yes] [--
|
|
23
|
+
[--organize DIR] [--yes] [--mode core|ski]
|
|
24
|
+
[--html] [--png [--width N] [--height N]]
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
Once installed (`npm install [-g] gpx-from-gopro`), drop the `npx` prefix and just run
|
|
@@ -31,7 +32,9 @@ family) + local date, and writes one merged `<YYYYMMDD>-<family>.gpx` per group
|
|
|
31
32
|
split into one `<trkseg>` per recording session (keyed on the filename file-number, with a time-gap
|
|
32
33
|
split for restarts/dropouts). A per-file extraction cache (keyed by size+mtime+rate) lets a killed
|
|
33
34
|
run resume without re-extracting. `--rate HZ` downsamples from the native ~18 Hz; `--tz HOURS`
|
|
34
|
-
overrides the longitude-guessed local date.
|
|
35
|
+
overrides the longitude-guessed local date. `--mode core|ski` runs each session through
|
|
36
|
+
[`gpx-stabilizer`](https://www.npmjs.com/package/gpx-stabilizer)'s `stabilizeTrack` before writing
|
|
37
|
+
(omit for the raw extraction).
|
|
35
38
|
|
|
36
39
|
### `--organize DIR` — reorganize the source videos to match the GPX
|
|
37
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gpx-from-gopro",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Extract a GoPro video's GPS telemetry as GPX or render-agnostic TrackPoints (points + meta + timezone + UTC anchor).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"gopro",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"directory": "packages/gopro"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"gpx-stabilizer": "^0.
|
|
35
|
+
"gpx-stabilizer": "^0.6.0",
|
|
36
36
|
"egm96-universal": "^1.1.1",
|
|
37
37
|
"gopro-telemetry": "^1.2.11",
|
|
38
38
|
"gpmf-extract": "^0.3.3",
|
package/src/gopro-cli.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// gpx-from-gopro — extract GoPro GPS into merged GPX, one file per camera per local date.
|
|
3
3
|
// gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ] [--cache-dir DIR | --no-cache]
|
|
4
|
-
// [--organize DIR] [--yes] [--
|
|
4
|
+
// [--organize DIR] [--yes] [--mode core|ski]
|
|
5
|
+
// [--html] [--png [--width N] [--height N]]
|
|
6
|
+
//
|
|
7
|
+
// - --mode core|ski: runs the merged points through gpx-stabilizer's own `stabilizeTrack` (per
|
|
8
|
+
// recording session — see the `--out` loop below) before writing, instead of shipping today's
|
|
9
|
+
// default raw extraction. --html/--png (below) render under the SAME mode when both are given,
|
|
10
|
+
// so the preview always matches what actually got written. Omitting --mode leaves every output
|
|
11
|
+
// exactly as before (raw, unstabilized) — this is purely additive.
|
|
5
12
|
//
|
|
6
13
|
// - --html / --png: alongside the merged .gpx (never instead of it), also render each group's merged
|
|
7
14
|
// track through the SAME analyzed view core's own CLI uses (clean track + drop markers) — an eval
|
|
@@ -34,7 +41,14 @@
|
|
|
34
41
|
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
35
42
|
import { basename, join } from "node:path";
|
|
36
43
|
import { createInterface } from "node:readline/promises";
|
|
37
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
analyzedSvg,
|
|
46
|
+
MODES,
|
|
47
|
+
saveGpx,
|
|
48
|
+
savePng,
|
|
49
|
+
stabilizeTrack,
|
|
50
|
+
toHtmlAnalyzedFiles,
|
|
51
|
+
} from "gpx-stabilizer";
|
|
38
52
|
import { buildGroups, family, fileNumber } from "./group.js";
|
|
39
53
|
import { cacheMovePlan, executeMove, findSidecars, planMove } from "./organize.js";
|
|
40
54
|
import { readGoproSamples } from "./telemetry.js";
|
|
@@ -45,7 +59,21 @@ const LOCAL_TZ = -new Date().getTimezoneOffset() / 60; // hours, may be fraction
|
|
|
45
59
|
|
|
46
60
|
// ---- args ----
|
|
47
61
|
const argv = process.argv.slice(2);
|
|
48
|
-
const WITH_VALUE = new Set([
|
|
62
|
+
const WITH_VALUE = new Set([
|
|
63
|
+
"out",
|
|
64
|
+
"tz",
|
|
65
|
+
"rate",
|
|
66
|
+
"cache-dir",
|
|
67
|
+
"organize",
|
|
68
|
+
"width",
|
|
69
|
+
"height",
|
|
70
|
+
"mode",
|
|
71
|
+
]);
|
|
72
|
+
const KNOWN_BOOL = new Set(["no-cache", "html", "png", "yes"]);
|
|
73
|
+
const USAGE =
|
|
74
|
+
"usage: gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ]" +
|
|
75
|
+
" [--cache-dir DIR | --no-cache] [--organize DIR] [--yes] [--mode core|ski]" +
|
|
76
|
+
" [--html] [--png [--width N] [--height N]]";
|
|
49
77
|
const inputs = [];
|
|
50
78
|
const opts = {};
|
|
51
79
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -53,14 +81,22 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
53
81
|
if (a.startsWith("--")) {
|
|
54
82
|
const name = a.slice(2);
|
|
55
83
|
if (WITH_VALUE.has(name)) opts[name] = argv[++i];
|
|
56
|
-
else opts[name] = true;
|
|
84
|
+
else if (KNOWN_BOOL.has(name)) opts[name] = true;
|
|
85
|
+
else {
|
|
86
|
+
// an unrecognized flag must fail loudly -- silently accepting it into `opts` would look like
|
|
87
|
+
// it took effect while doing nothing.
|
|
88
|
+
console.error(`gpx-from-gopro: unknown option --${name}\n\n${USAGE}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
57
91
|
} else inputs.push(a);
|
|
58
92
|
}
|
|
59
93
|
if (inputs.length === 0) {
|
|
94
|
+
console.error(USAGE);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
if (opts.mode != null && !MODES[opts.mode]) {
|
|
60
98
|
console.error(
|
|
61
|
-
|
|
62
|
-
" [--cache-dir DIR | --no-cache] [--organize DIR] [--yes]" +
|
|
63
|
-
" [--html] [--png [--width N] [--height N]]",
|
|
99
|
+
`gpx-from-gopro: unknown --mode "${opts.mode}" (use: ${Object.keys(MODES).join(", ")})\n\n${USAGE}`,
|
|
64
100
|
);
|
|
65
101
|
process.exit(1);
|
|
66
102
|
}
|
|
@@ -196,8 +232,18 @@ const { groups, skipped: emptyGroups } = buildGroups(entries);
|
|
|
196
232
|
for (const name of emptyGroups) console.error(` no real fix, skip group: ${name}`);
|
|
197
233
|
const written = [];
|
|
198
234
|
for (const g of groups) {
|
|
235
|
+
// --mode stabilizes each recording session independently (stabilizeTrack analyzes a session's own
|
|
236
|
+
// <trkseg>s as one continuous stream, same handling core itself gives a multi-segment track) --
|
|
237
|
+
// sessions themselves stay separate <trk>s, matching buildGroups' own "one <trk> per recording"
|
|
238
|
+
// design. Omitting --mode ships today's raw extraction unchanged.
|
|
239
|
+
const tracks = opts.mode
|
|
240
|
+
? g.tracks.map((trk) => ({
|
|
241
|
+
name: trk.name,
|
|
242
|
+
segments: stabilizeTrack({ segments: trk.segments }, { mode: opts.mode }).segments,
|
|
243
|
+
}))
|
|
244
|
+
: g.tracks;
|
|
199
245
|
const track = {
|
|
200
|
-
tracks
|
|
246
|
+
tracks, // one <trk> per recording session, named after its own original video file
|
|
201
247
|
meta: {
|
|
202
248
|
name: g.name,
|
|
203
249
|
time: g.startMs != null ? new Date(g.startMs).toISOString() : null,
|
|
@@ -206,23 +252,28 @@ for (const g of groups) {
|
|
|
206
252
|
};
|
|
207
253
|
const path = join(outDir, `${g.name}.gpx`);
|
|
208
254
|
saveGpx(track, path, { creator: "gpx-from-gopro" });
|
|
209
|
-
const npts =
|
|
210
|
-
const nseg =
|
|
211
|
-
written.push(`${g.name}.gpx (${npts} pts, ${
|
|
255
|
+
const npts = tracks.reduce((s, t) => s + t.segments.reduce((s2, seg) => s2 + seg.length, 0), 0);
|
|
256
|
+
const nseg = tracks.reduce((s, t) => s + t.segments.length, 0);
|
|
257
|
+
written.push(`${g.name}.gpx (${npts} pts, ${tracks.length} trk, ${nseg} seg)`);
|
|
212
258
|
}
|
|
213
259
|
|
|
214
260
|
console.log(`\ndone. processed=${ok} skipped=${skipped} failed=${failed}`);
|
|
215
261
|
for (const w of written) console.log(` -> ${join(outDir, w)}`);
|
|
216
262
|
|
|
217
263
|
// ---- --html / --png: eval visualization of each group's merged track, additive to the .gpx above ----
|
|
218
|
-
// (analyzedLayers/analyzedSvg run core's noise-removal pipeline over the flattened group,
|
|
219
|
-
//
|
|
220
|
-
// are visible without a separate `gpx-stabilizer --html` pass on the merged .gpx.)
|
|
264
|
+
// (analyzedLayers/analyzedSvg run core's noise-removal pipeline over the flattened group, under the
|
|
265
|
+
// same --mode as the .gpx above (core's own default when --mode is omitted) — so drop reasons / hdop
|
|
266
|
+
// overlays are visible without a separate `gpx-stabilizer --html` pass on the merged .gpx.)
|
|
221
267
|
if (opts.html || opts.png) {
|
|
268
|
+
// fed from the RAW groups (not the --mode-stabilized `tracks` written above) -- these views run
|
|
269
|
+
// their OWN analyze()/stabilize() pass internally, so raw points go in and `analyzeOpts` (below)
|
|
270
|
+
// carries the SAME --mode through, keeping the preview consistent with the .gpx without double
|
|
271
|
+
// -stabilizing anything.
|
|
222
272
|
const tracks = groups.map((g) => ({
|
|
223
273
|
name: g.name,
|
|
224
274
|
points: g.tracks.flatMap((t) => t.segments).flat(),
|
|
225
275
|
}));
|
|
276
|
+
const analyzeOpts = opts.mode ? { mode: opts.mode } : {};
|
|
226
277
|
if (opts.html) {
|
|
227
278
|
const htmlPath = join(outDir, "gopro-view.html");
|
|
228
279
|
// one toHtmlAnalyzedFiles() call analyzes every group (potentially slow — full analyze() over
|
|
@@ -231,7 +282,7 @@ if (opts.html || opts.png) {
|
|
|
231
282
|
if (phase === "start") console.log(` [${index + 1}/${total}] analyzing ${name}...`);
|
|
232
283
|
else console.log(` [${index + 1}/${total}] ${name} done (${(ms / 1000).toFixed(1)}s)`);
|
|
233
284
|
};
|
|
234
|
-
writeFileSync(htmlPath, toHtmlAnalyzedFiles(tracks, { onProgress }));
|
|
285
|
+
writeFileSync(htmlPath, toHtmlAnalyzedFiles(tracks, { ...analyzeOpts, onProgress }));
|
|
235
286
|
console.log(`html -> ${htmlPath}`);
|
|
236
287
|
}
|
|
237
288
|
if (opts.png) {
|
|
@@ -239,7 +290,7 @@ if (opts.html || opts.png) {
|
|
|
239
290
|
const height = Number(opts.height ?? 720);
|
|
240
291
|
for (const t of tracks) {
|
|
241
292
|
const pngPath = join(outDir, `${t.name}.png`);
|
|
242
|
-
await savePng(analyzedSvg(t.points, { width, height }), pngPath);
|
|
293
|
+
await savePng(analyzedSvg(t.points, { ...analyzeOpts, width, height }), pngPath);
|
|
243
294
|
console.log(`png -> ${pngPath}`);
|
|
244
295
|
}
|
|
245
296
|
}
|