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 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] [--html] [--png [--width N] [--height N]]
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.4.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.4.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] [--html] [--png [--width N] [--height N]]
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 { analyzedSvg, saveGpx, savePng, toHtmlAnalyzedFiles } from "gpx-stabilizer";
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(["out", "tz", "rate", "cache-dir", "organize", "width", "height"]);
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
- "usage: gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ]" +
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: g.tracks, // one <trk> per recording session, named after its own original video file
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 = g.tracks.reduce((s, t) => s + t.segments.reduce((s2, seg) => s2 + seg.length, 0), 0);
210
- const nseg = g.tracks.reduce((s, t) => s + t.segments.length, 0);
211
- written.push(`${g.name}.gpx (${npts} pts, ${g.tracks.length} trk, ${nseg} seg)`);
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, same
219
- // "core" defaults gpx-stabilizer's own CLI uses with no --mode — so drop reasons / hdop overlays
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
  }