npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@signalk/tracks-plugin

v3.6.2

Published

Plugin to keep track of tracks of vessels

Readme

@signalk/tracks-plugin

Signal K server plugin that accumulates vessel positions into tracks and implements the track API.

Positions are recorded to a SQLite file in the plugin's data directory, at a configured time resolution, so tracks survive a restart with no other plugin required. The plugin is enabled on install and needs no configuration to start recording.

Requires Signal K server >= 2.33.0 for the v2 Track API; older servers get the v1 routes only.

| Setting | Behaviour | | ------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | Track resolution | Minimum spacing between recorded positions. | | Days to keep other vessels' positions | Drops other vessels' positions older than this, even while a vessel stays in range. 0 keeps every position. |

The plugin's configuration page shows the current defaults, and is the authority on them.

The package also exports a client side TrackAccumulator class that manages the track for a single vessel, exposing the result as Observable<LatLngTuple[]>.

Where a track comes from

With a history provider installed — signalk-questdb, signalk-to-influxdb2 — a track is answered from both it and the plugin's own store: the provider is the finer record for as long as its retention reaches, and the store keeps the older track, within its own retention, and any period the provider missed.

Nothing needs configuring for this, and the plugin works fully with no provider installed. Its own store is a SQLite file in the plugin's data directory, so tracks survive a restart on their own. Base stations and aids to navigation are not recorded, since their position never changes. See docs/history-and-storage.md for what that means in practice.

The own vessel is kept indefinitely. Other vessels' positions are kept for the number of days set in the plugin settings, also for a vessel that never leaves receiver range — a harbour puts hundreds of AIS targets past a receiver in a day, and keeping every one of them forever is rarely what anybody wants. Space freed by removing positions is reused for new ones, which slows the file's growth, but the file never shrinks on its own: only an SQLite VACUUM returns that space to the file system.

Keeping tracks for years

The own vessel's track is kept here indefinitely, at the track resolution. For a finer track over years, and for tracks recorded before Signal K, a history provider is the better place: this plugin reads whatever the server's default provider holds for the own vessel and shows it as part of its track. Where several providers are installed, the default is chosen under Data → Preferences.

signalk-questdb is one such provider. From 2.3.0 it can keep the own track past its retention, and it imports a chartplotter's GPX export of any size.

Imported tracks

A client can store a track of its own, a GPX from a friend or a passage logged by another device, with POST /signalk/v2/api/tracks (Signal K server with #3038). The body is a GeoJSON Feature, the shape a v2 query returns; the plugin answers with an id such as tracks:imported:0f0f2a1e-…, and GET or DELETE /signalk/v2/api/tracks/<id> fetch or delete it. Storing needs write access; deleting needs administrator rights, or write access on a server with security turned off.

An imported track is kept apart from the recording. It is listed by v2 queries like any other track, filtered by vessel, time window and area, but as a feature of its own even when it names a vessel the plugin also records. It is never merged with a history provider's data, never removed by the retention settings, and never returned by the v1 routes, which answer where vessels are now. Its own name and any other properties it was posted with come back as posted. It carries imported, the time it was stored, which a recorded track does not, so a client drawing a vessel's recording can leave the import out of it. Imports stored by a version before that time was kept are dated to the first start of a version that keeps it.

A timed track of the own vessel is the exception: it is the boat's own passage, logged before the plugin was installed or by another plotter, so it is written into the own vessel's recording instead of being kept as an import, and the answer is the recording's id, tracks:recorded:<context>. By default it only fills what the recording is missing: its points are used where the vessel has no position, from this plugin or a history provider, for at least five minutes, and left out elsewhere, so nothing recorded is overwritten and two sources are never interleaved minute by minute; while a history provider cannot be read, such an import is refused with a 503 rather than drawn over what it holds. Asked to replace instead, it takes the place of what was recorded over the same time: the positions this plugin stored there go to the recycle bin first, one entry per segment, and a history provider's data over that time is hidden while the entry is there. Restoring the entry brings both back. An untimed track of the own vessel stays an import, since it cannot be placed in time.

A track with neither times (coordTimes) nor a vessel (context) is kept too. No time window or vessel query matches it, but GET /plugins/tracks/imports lists it with every other import, and GET /signalk/v2/api/tracks/<id> returns it.

Importing a GPX file

The Tracks webapp imports a GPX file from the Import form, and a client can do the same with POST /plugins/tracks/imports, the file as the request body with Content-Type: application/gpx+xml. Each track in the file becomes an imported track as above. Ticking Tracks of my own vessel (?self=true) files them under the own vessel, which writes a timed one into the recording; otherwise a track names a vessel only if the file says which, as the plugin's own GPX export does. Ticking Replace what my vessel recorded over the same time as well (onOverlap=replace; the default is keep) replaces the recording rather than filling it.

The file can be at most 25 MiB. A larger export, such as years of a chartplotter's tracks, is better imported into a history provider; see Keeping tracks for years.

In a timed track, points without a time are skipped, and the answer says how many (skippedPoints), as it does for the points left out where the recording already has positions (overlappingPoints). A track posted through the Track API's POST /signalk/v2/api/tracks is always kept beside the recording this way, since that API has no such choice yet. The route needs write access on a Signal K server that lets a plugin say so, and administrator rights on an older one.

The webapp lists every imported track, however old and whether or not it has times, next to the last 30 days of recorded ones, so a passage imported from years ago can be found there. It can be deleted there on a Signal K server that deletes tracks by id.

GET /plugins/tracks/imports returns every imported track, and no recorded ones, each with imported, the time it was stored.

The Tracks webapp exports an imported track as GPX from its row, as it was imported. A client can download the same file from GET /plugins/tracks/imports/<id>/track.gpx, with the id less its tracks: prefix: under the track's own name, and without times if it had none.

Deleting tracks

DELETE /signalk/v2/api/tracks/<id> deletes a track, and with from, to or both only the part recorded between them (on a Signal K server that accepts them; an older one refuses the request rather than deleting the whole track). Deleting needs administrator rights, or write access on a server with security turned off. The Tracks webapp offers both from each row.

A recorded track has the id tracks:recorded:<vessel context>. Deleting it whole removes what was recorded up to now; the recording goes on, and what it records next is kept. With a history provider installed the deleted span is hidden in what the provider returns too, since the History API has no way to delete or hide data and the next query would otherwise fill it back in.

Nothing deleted is gone at once. It goes to a recycle bin, listed in the Tracks webapp, where it can be restored or deleted for good, until it is purged after the number of days set in the plugin settings (180 by default). The retention settings above do not reach into the bin, but they apply again to what is restored: an AIS vessel's positions restored after they have aged past the retention are dropped at the next prune. Once a recorded span is purged its points are gone, but the span stays hidden in the provider's data for good.

Glitch filtering

A receiver occasionally reports a position far from the vessel — a bad almanac, a multipath reflection, a unit resetting. On a live map it flickers past. In a stored track it is permanent: one spike stretches the bounding box across an ocean and draws a line over the chart every time the track is rendered.

Positions implying a speed above Discard positions implying a speed above this (knots) since the previous accepted fix are discarded, defaulting to 100 knots. That is well above any real vessel, and glitches usually miss it by orders of magnitude rather than by a little. The test is speed rather than distance, so a track resuming after a long gap — a passage with the plugin stopped, or an AIS target reappearing — is not filtered. Set it to 0 to record everything.

Positions a history provider supplies are not filtered here — they are that provider's record, and it is the one that decides what to keep.

Pausing while not under way

A boat on a mooring for the winter emits a position every second and travels nowhere. Setting Pause recording while navigation.state is one of stops those months costing any rows.

On by default for moored, not-under-way and aground. It needs navigation.state to be set — by signalk-autostate or by hand — and a vessel that reports no state is always recorded, so an install with no state source behaves as though this were off. Clear the list to record regardless of state.

Two limits are deliberate. anchored is offered but rarely wanted: an anchor alarm watches exactly the track a vessel makes while swinging on its rode, so pausing there would break it. And AIS targets are never paused — their navigational status comes from the transponder and is often stale, so a vessel under way still reporting moored would otherwise vanish from the track.

While paused, the plugin says so in its status on the server dashboard.

Resolution by state

The own vessel can be recorded at a different spacing for each navigation.state, finer or coarser than the track resolution. Out of the box only anchored is set, to one second, so the swing around the anchor shows.

Detail finer than the track resolution is kept in full for a day. While a stay at anchor goes on, its older part is thinned only to the spacing an anchor alarm reads the whole stay at, one second per day of the stay, so it keeps roughly 86,400 points plus the last day. That holds until the spacing reaches the track resolution, after 60 days at the default minute; from then on the older part is kept at the track resolution, never coarser. A day after the stay ends, it is thinned back to the track resolution.

Other vessels have a resolution of their own, the track resolution unless set. Their navigation.state is not used: it is whatever their transponder was last set to, and often stale. Detail recorded for them finer than the track resolution is thinned the same way.

Position sources

navigation.position often arrives from several sources at once — an internal GPS, an AIS transponder, a plotter echoing its own fix. Signal K decides which one wins through source priority, and the stream this plugin records from is already filtered by it, so normally only the winning source is stored.

When no priority rule matches the path, though, every source comes through. Their fixes are metres apart, so the track zigzags between receivers instead of following the boat. If that is happening, the plugin says so in its status on the server dashboard, naming the sources it has seen. The fix is to set a source priority for navigation.position in the server settings.

Usage:

Retrieve track for an individual vessel:

/signalk/v1/api/vessels/<vesselId>/track

<vesselId> may be self or a fully qualified context such as urn:mrn:imo:mmsi:123456789.

Retrieve the own vessel's track:

/signalk/v1/api/self/track


Narrow a track to a time window:

/signalk/v1/api/self/track?from=2026-08-09T06:00:00Z&to=2026-08-09T12:00:00Z

/signalk/v1/api/self/track?duration=6h

from and to are ISO-8601 timestamps; to defaults to now. duration is a window ending now. A window ending at now includes its most recent point; one ending earlier is half-open, so consecutive windows tile without returning the shared point twice.

timespan and timespanOffset are also accepted for Freeboard-SK compatibility, where timespan=23h&timespanOffset=1 means "23 hours ending an hour ago". They are not part of the proposed track API and are expected to be superseded by from/to.

A parameter these routes do not read is rejected with 400 rather than ignored, so a typo such as ?duratoin=6h fails rather than quietly returning the whole retained track. bbox and radius are not accepted here: they select which vessels to return and so belong to /tracks below. To ask whether one vessel's track passed through a box, use the v2 Track API, which matches any position in the window (being designed in SignalK/signalk-server#2504).


Reduce the number of points returned:

/signalk/v1/api/self/track?duration=24h&resolution=5m

resolution is the minimum spacing between returned points. Durations accept a bare number of seconds or an s/m/h/d suffix. The first and last points are always kept, so thinning never shortens the track.


Retrieve the time each position was recorded:

/signalk/v1/api/self/track?duration=6h&times

{
  "type": "MultiLineString",
  "coordinates": [
    [
      [24.9, 60.1],
      [25.0, 60.2]
    ]
  ],
  "times": [["2026-08-14T09:00:00.000Z", "2026-08-14T09:01:00.000Z"]],
  "context": "vessels.urn:mrn:imo:mmsi:123456789",
  "isSelf": true,
  "name": "Own Ship"
}

times adds a times array positionally aligned with coordinates: times[i][j] is when coordinates[i][j] was recorded, as ISO-8601 UTC. It is opt-in because the response grows by roughly a third and clients that only draw the geometry have no use for it. Accepts true/1/yes and false/0/no; a valueless ?times reads as true.

context is the fully qualified context the track belongs to, and isSelf says whether it is the own vessel. Asking for self resolves the alias, so the response tells you which vessel self actually is.

name is a display label, the way a chart plotter shows one: Own Ship, or AIS <shipname> falling back to AIS <mmsi> and finally the raw context. It is for putting in a list — the v2 Track API's contextName carries the undecorated vessel name instead, and is absent for a vessel that has not sent one.


Retrieve tracks for all vessels:

/signalk/v1/api/tracks

Every vessel the plugin holds a track for. Add ?radius= to narrow it to vessels near your own.

Each entry carries isSelf, so the own vessel can be told from an AIS target without string-matching the context against the server's self-identity, and name for display.

?times works here too, adding a times array to every vessel's entry. Note that asking for times also segments each track on the gap threshold, so coordinates and times line up; without times each vessel keeps its single unsegmented line.


Retrieve tracks for all vessels within a given radius (in meters) from your vessel position:

/signalk/v1/api/tracks?radius=50000

Distance from the own vessel's current position, matched against each track's last position — "which vessels are near me now". The v2 Track API asks a different question and matches any position in the window.


Retrieve tracks for all vessels within a bounded area:

/signalk/v1/api/tracks?bbox=130,-35,139,-33

Bounded area is defined as west, south, east, north — GeoJSON coordinate order, the same as the coordinates this endpoint returns, the Resources API, and the v2 Track API.

A box crossing the antimeridian is expressed with west greater than east, for example bbox=175,-10,-175,10.

Changed: this parameter was lat1, lon1, lat2, lon2 — latitude first — up to and including 2.0.2. A box in the old order is still four valid numbers, so it will not be rejected; it will simply describe a different area. Freeboard-SK is unaffected, as it filters by radius rather than bbox.


Development

npm ci
npm run build      # vite library build -> dist/
npm test           # vitest
npm run test:e2e   # against a real signalk-server; see below
npm run typecheck  # tsc --noEmit
npm run lint       # eslint
npm run format     # prettier --write

npm run test:e2e packs the plugin, installs it into a throwaway config directory, boots a real Signal K server against it and feeds positions as deltas — so it covers plugin loading, route mounting and the delta path, none of which the unit suite can. It needs a server to boot: a built checkout, by default at ~/dev/xxx_signalk-server, or an installed package's node_modules/signalk-server, named with SIGNALK_SERVER_DIR. A second tier installs a real history provider (signalk-questdb) into that server and exercises the reconciliation through it; it skips itself if no QuestDB is reachable at QUESTDB_URL, unless QUESTDB_REQUIRED is set. CI runs both tiers against the oldest supported signalk-server release and the latest, with QuestDB as a service container.

The package is ESM only and targets Node >= 22.5.0, the release that added node:sqlite. ESM alone would only need 20.19, the first release in which the Signal K server's require()-based plugin loader can load an ES module, but recording to SQLite raises the floor. The server itself requires Node >= 22, so this rules out nothing that could have run the plugin anyway.