diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e2bfb01 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,70 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run lint + run: npm run lint + + test: + name: Test + runs-on: ubuntu-latest + needs: lint + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run tests + run: npm test + + build: + name: Build + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build diff --git a/README.md b/README.md index 1f477f2..484629c 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,229 @@ -# **gpx-export** +

gpx-export

-Zero‑dependency GPX 1.1 generator with optional Garmin TrackPointExtension v2 support. -Works in Node.js, browsers, Capacitor, and any JS runtime. +

+ + npm version + + dependencies + types + CI status +

+ +

+ A lightweight, zero-dependency GPX 1.1 generator for Node.js, browsers, Capacitor, and other JavaScript runtimes. +

--- -## Install +## **What gpx-export is for** + +gpx-export converts GPS data into valid GPX 1.1 XML. + +It supports: +- `generateGpx(track, metadata?)` for tracks-only input +- `generateGpx(document, metadata?)` for full document input +- waypoints, routes, tracks, and metadata +- Garmin TrackPointExtension v2 (`gpxtpx`) for speed, heart rate, and cadence + +--- + +## **Why gpx-export** + +Many apps collect location points but still need a clean, portable output format for exports, backups, and interoperability with mapping tools. + +gpx-export focuses on that narrow problem: +- simple TypeScript-first API +- deterministic XML output order for identical inputs +- no external dependencies +- compatible across Node.js and browser-like runtimes + +--- + +## **Features** + +### **GPX 1.1 Output** +Generates valid GPX 1.1 XML with metadata, routes, tracks, and waypoints. + +### **Garmin TrackPointExtension Support** +Supports `gpxtpx` values on track points (`trkpt`) for: +- speed +- heart rate +- cadence -```sh +### **Zero Dependencies** +No runtime packages required. + +--- + +## **Installation** + +```bash npm install gpx-export ``` --- -## Usage +## **Quick Start** ```ts import { generateGpx } from 'gpx-export'; +const now = new Date(); + +const gpx = generateGpx( + { + name: 'Morning Ride', + points: [ + { + lat: 54.5741, + lon: -1.318, + time: now, + elevation: 32.4, + speed: 5.2, + }, + ], + }, + { + time: now, + }, +); + +console.log(gpx); +``` + +--- + +## **Full Document Example** + +```ts +import { generateGpx } from 'gpx-export'; + +const now = new Date('2026-01-01T12:00:00.000Z'); + const gpx = generateGpx({ - name: 'Morning Ride', - points: [ + metadata: { + name: 'My Export', + desc: 'Generated by gpx-export', + author: { name: 'Chris' }, + link: { href: 'https://github.com/cmyers/gpx-export' }, + time: now, + keywords: 'gps,gpx,export', + }, + waypoints: [ + { + lat: 54.5, + lon: -1.3, + name: 'Start', + time: now, + }, + ], + routes: [ { - lat: 54.5741, - lon: -1.3180, - time: new Date(), - elevation: 32.4, - speed: 5.2, // m/s + name: 'Route 1', + points: [ + { lat: 54.5, lon: -1.3, time: now }, + { lat: 54.6, lon: -1.2, time: now }, + ], + }, + ], + tracks: [ + { + name: 'Track 1', + segments: [ + { + points: [ + { + lat: 54.5, + lon: -1.3, + time: now, + elevation: 20.12, + extensions: { + speed: 3.45, + heartRate: 152, + cadence: 86, + }, + }, + ], + }, + ], }, ], }); - -console.log(gpx); ``` --- -## API +## **API** -### `generateGpx(track: GpxTrack, options?: GpxOptions): string` +### `generateGpx(track: GpxTrack, metadata?: GpxMetadata): string` +### `generateGpx(document: GpxDocument, metadata?: GpxMetadata): string` -Returns a GPX 1.1 XML document as a string. +Generates a GPX 1.1 document from either a `GpxTrack` or `GpxDocument`, with optional metadata that shallow-merges into `document.metadata`. --- -## Types +## **Supported Types (Summary)** ```ts +interface GpxPointExtensions { + speed?: number; // gpxtpx:speed (m/s) + heartRate?: number; // gpxtpx:hr (bpm) + cadence?: number; // gpxtpx:cad (rpm) + rawXml?: string; // trusted XML inside +} + interface GpxPoint { lat: number; lon: number; time: Date; - speed?: number; // m/s — gpxtpx:speed - elevation?: number; // metres — + speed?: number; // legacy alias for extensions.speed + elevation?: number; // + extensions?: GpxPointExtensions; } interface GpxTrack { name: string; - createdAt?: Date; // defaults to first point time - points: GpxPoint[]; -} - -interface GpxOptions { - creator?: string; // defaults to "gpx-export" + createdAt?: Date; + points?: GpxPoint[]; + segments?: { points: GpxPoint[] }[]; + cmt?: string; + desc?: string; + extensions?: GpxPointExtensions; } ``` +Why speed appears in two places: +- `GpxPoint.speed` exists for backward compatibility with older callers. +- `GpxPoint.extensions.speed` is the canonical field for new code. +- When both are provided, `extensions.speed` wins and only one GPX speed tag is emitted. + +All exported type definitions are available from the package root. + +--- + +## **Notes** + +- elevation is formatted to 2 decimal places +- speed is formatted to 4 decimal places +- the Garmin `gpxtpx` namespace is included when Garmin metrics are present on track points +- Garmin `gpxtpx` metric tags are emitted on track points (`trkpt`) only +- `rawXml` values are inserted as trusted XML and are not escaped +- element output order is deterministic for identical inputs + --- -## Notes +## **Testing** -- Adds Garmin `gpxtpx` namespace only when speed values are present. -- If `createdAt` is omitted, metadata time uses the first point time; if there are no points, it falls back to the current time. -- Elevation is formatted to 2 decimal places; speed is formatted to 4 decimal places. -- Saving/downloading the GPX is left to the caller. +Run tests: + +```bash +npm test +``` --- -## License +Pull requests are welcome. + +## **License** -MIT \ No newline at end of file +MIT diff --git a/docs/api.html b/docs/api.html index 76a6053..3c1140a 100644 --- a/docs/api.html +++ b/docs/api.html @@ -14,26 +14,29 @@

API Reference

gpx-export exposes a focused, typed API for building GPX 1.1 documents. - The main export is generateGpx(track, options?). + The main export is generateGpx(...).

-

generateGpx(track, options?)

+

generateGpx(...)

- Creates GPX 1.1 XML with metadata, track name, segment, and points. - Garmin TrackPointExtension output is automatically added when speed - values are present. + Generates GPX 1.1 XML from either: + generateGpx(track: GpxTrack, metadata?: GpxMetadata) or + generateGpx(document: GpxDocument, metadata?: GpxMetadata).

import { generateGpx } from "gpx-export";
 
+const now = new Date("2026-01-01T12:00:00.000Z");
+
 const xml = generateGpx(
   {
-    name: "Morning Ride",
-    points: [
-      { lat: 54.57, lon: -1.31, time: new Date(), speed: 5.2 }
-    ]
+    name: "Session",
+    points: [{ lat: 54.57, lon: -1.31, time: now }]
   },
-  { creator: "my-app" }
+  {
+    name: "Morning Ride",
+    time: now
+  }
 );
 
@@ -42,45 +45,114 @@

Returns

  • string - GPX 1.1 XML document
  • -

    GpxPoint

    -

    - Defines each point in the generated track. -

    +

    GpxDocument

    +
    interface GpxDocument {
    +  metadata?: GpxMetadata;
    +  waypoints?: GpxWaypoint[];
    +  routes?: GpxRoute[];
    +  tracks?: GpxTrack[];
    +}
    +
    + +

    GpxMetadata

    +
    interface GpxMetadata {
    +  name?: string;
    +  desc?: string;
    +  author?: GpxAuthor;
    +  link?: GpxLink;
    +  time?: Date;
    +  keywords?: string;
    +  copyright?: string | GpxCopyright;
    +  bounds?: GpxBounds;
    +}
     
    +interface GpxAuthor {
    +  name: string;
    +  link?: GpxLink;
    +}
    +
    +interface GpxCopyright {
    +  author: string;
    +  year?: number;
    +  license?: string;
    +}
    +
    +interface GpxLink {
    +  href: string;
    +  text?: string;
    +  type?: string;
    +}
    +
    + +

    GpxPointExtensions

    +
    interface GpxPointExtensions {
    +  speed?: number;      // gpxtpx:speed (m/s)
    +  heartRate?: number;  // gpxtpx:hr (bpm)
    +  cadence?: number;    // gpxtpx:cad (rpm)
    +  rawXml?: string;     // trusted XML in <extensions>
    +}
    +
    + +

    GpxPoint

    interface GpxPoint {
       lat: number;
       lon: number;
       time: Date;
    -  speed?: number;     // m/s
    -  elevation?: number; // metres
    +  speed?: number;      // legacy alias for extensions.speed
    +  elevation?: number;  // metres
    +  extensions?: GpxPointExtensions;
     }
     
    -

    GpxTrack

    - Defines metadata and ordered points. + Why speed appears in both places: GpxPoint.speed is a legacy compatibility alias, + while GpxPoint.extensions.speed is the canonical field for new code. + If both are provided, extension speed takes precedence and only one GPX speed tag is emitted.

    +

    GpxTrack

    interface GpxTrack {
       name: string;
       createdAt?: Date;
    -  points: GpxPoint[];
    +  points?: GpxPoint[];
    +  segments?: { points: GpxPoint[] }[];
    +  cmt?: string;
    +  desc?: string;
    +  extensions?: GpxPointExtensions;
     }
     
    -

    GpxOptions

    -
    interface GpxOptions {
    -  creator?: string;
    +    

    GpxRoute and GpxWaypoint

    +
    interface GpxRoute {
    +  name?: string;
    +  cmt?: string;
    +  desc?: string;
    +  points: GpxRoutePoint[];
    +  extensions?: GpxPointExtensions;
    +}
    +
    +interface GpxWaypoint {
    +  lat: number;
    +  lon: number;
    +  name?: string;
    +  cmt?: string;
    +  desc?: string;
    +  elevation?: number;
    +  time?: Date;
    +  extensions?: GpxPointExtensions;
     }
     

    Behavior notes

    • Creator defaults to gpx-export.
    • -
    • Track name and creator are XML-escaped before output.
    • -
    • Metadata time defaults to first point time, then current time.
    • +
    • generateGpx(track, metadata?) wraps track input into a one-track document.
    • +
    • metadata shallow-merges onto document.metadata.
    • Elevation is formatted to 2 decimal places.
    • Speed is formatted to 4 decimal places as gpxtpx:speed.
    • +
    • Garmin gpxtpx namespace is emitted when Garmin metrics are present on track points.
    • +
    • Garmin metric tags are emitted under track-point <extensions> only.
    • +
    • rawXml extension values are inserted as trusted XML and are not escaped.