import { Temporal } from '@js-temporal/polyfill'; import { Context, Path } from './deltas'; /** * Track API — recorded vessel positions over time. * * A track is what a vessel *did*: a recorded, time-ordered series of positions, * queried by time window and optionally by area. That makes it different from * `resources/routes`, which is what someone *intends*, authored and named, and * from the History API, which answers questions about arbitrary paths rather * than about where vessels have been. * * Discussed in https://github.com/SignalK/signalk-server/issues/2504 * * @category Track API */ /** * Bounding box as `[west, south, east, north]` — GeoJSON coordinate order, * matching the Resources API. * * A box whose west edge is numerically greater than its east edge crosses the * antimeridian and is read the short way round, so `[175, -10, -175, 10]` is a * box near Fiji rather than a band across the rest of the globe. * * @category Track API */ export type TrackBoundingBox = [number, number, number, number]; /** @category Track API */ export interface TracksRequest { /** * Contexts to return tracks for. Defaults to the own vessel when neither * `contexts` nor a bounding box is given. * * A bare id is qualified with `vessels.`; other prefixes are accepted so that * aircraft — SAR aircraft appear in AIS — can be queried too. */ contexts?: Context[]; /** Start of the window. */ from?: Temporal.Instant; /** End of the window. Defaults to now when `from` or `duration` is given. */ to?: Temporal.Instant; /** Window ending at `to`, as an alternative to giving `from`. */ duration?: Temporal.Duration; /** * Only return tracks that pass through this box within the window. * * Intersection, not containment, and not "where the vessel is now": a vessel * that crossed the box an hour ago and has since left still matches. * * What comes back of a matching track is set by {@link TracksRequest.clip}. */ bbox?: TrackBoundingBox; /** * Return only the parts of each track inside `bbox`. * * A client showing a view gets the points it can draw, not every point of * every vessel that once crossed it. Each stretch inside the box becomes its * own segment, so a track that leaves and re-enters is not joined across the * outside. Each segment keeps the recorded point just outside the box at * either crossing, so the line reaches the edge of the view without * interpolated points. A box crossing the antimeridian clips the same way. * Clipping comes before `resolution`, `maxPoints` and simplification, so * those budgets are spent on the part that is returned. * * Resolved by the server before a provider sees it: true when `bbox` is * given unless the client sent `clip=false`, which returns matching tracks * whole. Absent when there is no `bbox`, since there is nothing to clip to. */ clip?: boolean; /** * Minimum spacing between returned points. * * Normalised to hours and below before it reaches a provider, so * `total({ unit: 'milliseconds' })` is always safe on it — * `Temporal.Duration` refuses that for day-and-larger units without a * reference date, since their length is timezone-dependent. Windows here are * absolute, so `P1D` means 24h and arrives as `PT24H`. Years and months are * rejected: a month is 744h from January and 672h from February, so it does * not describe a spacing. So is anything below a millisecond, which is the * finest granularity a timestamp carries. */ resolution?: Temporal.Duration; /** * Upper bound on the number of points returned per track. * * A budget rather than a fidelity contract, for clients that must bound * transfer and rendering cost regardless of how convoluted a track is. */ maxPoints?: number; /** * Simplify the geometry, dropping points that do not change the line's shape * beyond `epsilon`. * * With a bounding box and no explicit `epsilon`, an implementation should * choose a tolerance suited to the size of the box. */ simplify?: boolean; /** Douglas-Peucker tolerance in metres. Implies `simplify`. */ epsilon?: number; /** Include the recording time of each point as `properties.coordTimes`. */ times?: boolean; /** * Signal K paths to return alongside each position, as arrays nested to * match `coordinates` in the same way `coordTimes` is. * * A client colouring a track by speed, or deriving a route from a recorded * passage, otherwise has to query the History API for those paths and join * the two responses by timestamp — the row-alignment problem `coordTimes` * exists to remove, reintroduced one level down. * * Optional for a provider: one that cannot co-record other paths returns the * geometry alone and omits them from * {@link TrackProperties.appliedProperties}, so a client can tell what it * actually received rather than inferring it from absent data. * * Values are matched to the *nearest* sample of that path, not to one sharing * the position's timestamp. Paths arrive from different talkers on their own * cadences — position and speed over ground typically a couple of hundred * milliseconds apart — so an equality join returns null for every point while * `appliedProperties` still claims success. A provider that buckets should * match within a tolerance of its bucket width; a value that genuinely has no * nearby sample is null. */ properties?: Path[]; /** Include geometry. Defaults to true; set false to return metadata only. */ geometry?: boolean; } /** * Per-track metadata. * * The time range, point count and resolution describe what was *returned* * rather than what was asked for, so a client can always tell whether it * received a simplified track and ask again for more detail. * * @category Track API */ export interface TrackProperties { /** * Identifies this track within the provider that holds it. * * Stable for as long as the track exists. A provider mints it and receives * it back unchanged in `getTrack` and `deleteTrack`; the server prefixes its * registry name to form the `providerId:trackId` a client addresses, so a * provider never has to know what it is registered as. * * A recorded track may derive it from the context; an imported one has * nothing to derive it from and gets an opaque id. * * Optional because not every provider identifies tracks as objects. One * serving a time-and-context query out of history has no handle to mint and * implements neither `getTrack` nor `deleteTrack`; requiring an id there * would mean inventing one that addresses nothing. */ id?: string; /** * The Signal K context this track belongs to, when it has one. * * Absent for an imported track that names no vessel — a GPX a friend sent, * with no MMSI in it. Such a track is still a track: a recorded series of * positions, which is what this API serves. What it is not is a claim about * a vessel in the data model, so it carries no context rather than an * invented one. */ context?: Context; /** * Whether this is the own vessel's track. * * Absent along with `context`: a track that names no vessel cannot be the * own vessel's, and saying `false` would imply the question was asked of a * vessel that exists. */ isSelf?: boolean; /** * Which registered provider answered for this track. * * Set by the server from the registry, so a provider neither has to name * itself nor can name itself wrongly. Every registered provider is queried * and their features concatenated, so a vessel recorded by two of them * yields two features, and this is what tells them apart. */ providerId?: string; /** * Name of the vessel, aircraft or other context, when known. * * Not the name of the track — see `name` for that. A track recorded from * position data has no name of its own; an imported one usually does. */ contextName?: string; /** * The track's own name, when it has one. * * A GPX `` survives an import here. Recorded tracks have no name * until someone gives them one, which is why this is separate from * `contextName` rather than overloading it. */ name?: string; /** * Time of the first returned point. * * Absent for a track whose points carry no times — an imported GPX without * `