gpx-from-gopro 0.4.0 → 0.5.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/gopro-cli.js +51 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gpx-from-gopro",
3
- "version": "0.4.0",
3
+ "version": "0.5.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.5.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,7 @@
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 { analyzedSvg, MODES, saveGpx, savePng, stabilizeTrack, toHtmlAnalyzedFiles } from "gpx-stabilizer";
38
45
  import { buildGroups, family, fileNumber } from "./group.js";
39
46
  import { cacheMovePlan, executeMove, findSidecars, planMove } from "./organize.js";
40
47
  import { readGoproSamples } from "./telemetry.js";
@@ -45,7 +52,12 @@ const LOCAL_TZ = -new Date().getTimezoneOffset() / 60; // hours, may be fraction
45
52
 
46
53
  // ---- args ----
47
54
  const argv = process.argv.slice(2);
48
- const WITH_VALUE = new Set(["out", "tz", "rate", "cache-dir", "organize", "width", "height"]);
55
+ const WITH_VALUE = new Set(["out", "tz", "rate", "cache-dir", "organize", "width", "height", "mode"]);
56
+ const KNOWN_BOOL = new Set(["no-cache", "html", "png", "yes"]);
57
+ const USAGE =
58
+ "usage: gpx-from-gopro <dir|file.mp4> [...] [--out DIR] [--tz HOURS] [--rate HZ]" +
59
+ " [--cache-dir DIR | --no-cache] [--organize DIR] [--yes] [--mode core|ski]" +
60
+ " [--html] [--png [--width N] [--height N]]";
49
61
  const inputs = [];
50
62
  const opts = {};
51
63
  for (let i = 0; i < argv.length; i++) {
@@ -53,15 +65,21 @@ for (let i = 0; i < argv.length; i++) {
53
65
  if (a.startsWith("--")) {
54
66
  const name = a.slice(2);
55
67
  if (WITH_VALUE.has(name)) opts[name] = argv[++i];
56
- else opts[name] = true;
68
+ else if (KNOWN_BOOL.has(name)) opts[name] = true;
69
+ else {
70
+ // an unrecognized flag must fail loudly -- silently accepting it into `opts` would look like
71
+ // it took effect while doing nothing.
72
+ console.error(`gpx-from-gopro: unknown option --${name}\n\n${USAGE}`);
73
+ process.exit(1);
74
+ }
57
75
  } else inputs.push(a);
58
76
  }
59
77
  if (inputs.length === 0) {
60
- 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]]",
64
- );
78
+ console.error(USAGE);
79
+ process.exit(1);
80
+ }
81
+ if (opts.mode != null && !MODES[opts.mode]) {
82
+ console.error(`gpx-from-gopro: unknown --mode "${opts.mode}" (use: ${Object.keys(MODES).join(", ")})\n\n${USAGE}`);
65
83
  process.exit(1);
66
84
  }
67
85
  const outExplicit = opts.out != null; // --organize only sweeps the .gpx along when this is false
@@ -196,8 +214,18 @@ const { groups, skipped: emptyGroups } = buildGroups(entries);
196
214
  for (const name of emptyGroups) console.error(` no real fix, skip group: ${name}`);
197
215
  const written = [];
198
216
  for (const g of groups) {
217
+ // --mode stabilizes each recording session independently (stabilizeTrack analyzes a session's own
218
+ // <trkseg>s as one continuous stream, same handling core itself gives a multi-segment track) --
219
+ // sessions themselves stay separate <trk>s, matching buildGroups' own "one <trk> per recording"
220
+ // design. Omitting --mode ships today's raw extraction unchanged.
221
+ const tracks = opts.mode
222
+ ? g.tracks.map((trk) => ({
223
+ name: trk.name,
224
+ segments: stabilizeTrack({ segments: trk.segments }, { mode: opts.mode }).segments,
225
+ }))
226
+ : g.tracks;
199
227
  const track = {
200
- tracks: g.tracks, // one <trk> per recording session, named after its own original video file
228
+ tracks, // one <trk> per recording session, named after its own original video file
201
229
  meta: {
202
230
  name: g.name,
203
231
  time: g.startMs != null ? new Date(g.startMs).toISOString() : null,
@@ -206,23 +234,28 @@ for (const g of groups) {
206
234
  };
207
235
  const path = join(outDir, `${g.name}.gpx`);
208
236
  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)`);
237
+ const npts = tracks.reduce((s, t) => s + t.segments.reduce((s2, seg) => s2 + seg.length, 0), 0);
238
+ const nseg = tracks.reduce((s, t) => s + t.segments.length, 0);
239
+ written.push(`${g.name}.gpx (${npts} pts, ${tracks.length} trk, ${nseg} seg)`);
212
240
  }
213
241
 
214
242
  console.log(`\ndone. processed=${ok} skipped=${skipped} failed=${failed}`);
215
243
  for (const w of written) console.log(` -> ${join(outDir, w)}`);
216
244
 
217
245
  // ---- --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.)
246
+ // (analyzedLayers/analyzedSvg run core's noise-removal pipeline over the flattened group, under the
247
+ // same --mode as the .gpx above (core's own default when --mode is omitted) — so drop reasons / hdop
248
+ // overlays are visible without a separate `gpx-stabilizer --html` pass on the merged .gpx.)
221
249
  if (opts.html || opts.png) {
250
+ // fed from the RAW groups (not the --mode-stabilized `tracks` written above) -- these views run
251
+ // their OWN analyze()/stabilize() pass internally, so raw points go in and `analyzeOpts` (below)
252
+ // carries the SAME --mode through, keeping the preview consistent with the .gpx without double
253
+ // -stabilizing anything.
222
254
  const tracks = groups.map((g) => ({
223
255
  name: g.name,
224
256
  points: g.tracks.flatMap((t) => t.segments).flat(),
225
257
  }));
258
+ const analyzeOpts = opts.mode ? { mode: opts.mode } : {};
226
259
  if (opts.html) {
227
260
  const htmlPath = join(outDir, "gopro-view.html");
228
261
  // one toHtmlAnalyzedFiles() call analyzes every group (potentially slow — full analyze() over
@@ -231,7 +264,7 @@ if (opts.html || opts.png) {
231
264
  if (phase === "start") console.log(` [${index + 1}/${total}] analyzing ${name}...`);
232
265
  else console.log(` [${index + 1}/${total}] ${name} done (${(ms / 1000).toFixed(1)}s)`);
233
266
  };
234
- writeFileSync(htmlPath, toHtmlAnalyzedFiles(tracks, { onProgress }));
267
+ writeFileSync(htmlPath, toHtmlAnalyzedFiles(tracks, { ...analyzeOpts, onProgress }));
235
268
  console.log(`html -> ${htmlPath}`);
236
269
  }
237
270
  if (opts.png) {
@@ -239,7 +272,7 @@ if (opts.html || opts.png) {
239
272
  const height = Number(opts.height ?? 720);
240
273
  for (const t of tracks) {
241
274
  const pngPath = join(outDir, `${t.name}.png`);
242
- await savePng(analyzedSvg(t.points, { width, height }), pngPath);
275
+ await savePng(analyzedSvg(t.points, { ...analyzeOpts, width, height }), pngPath);
243
276
  console.log(`png -> ${pngPath}`);
244
277
  }
245
278
  }