@pond-ts/fit 0.31.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.
@@ -0,0 +1,358 @@
1
+ /**
2
+ * Geospatial operators on a pond-ts `TimeSeries` — the geo layer of
3
+ * `@pond-ts/fit`. The thesis: a GPS track is already a time-keyed series of
4
+ * `number` columns, so the geospatial value lives in operators over those
5
+ * columns, not in a new storage primitive.
6
+ *
7
+ * Built strictly on pond's PUBLIC surface: two `number` columns for lat/lng,
8
+ * `column(name).toFloat64Array()` for zero-copy reads, and column reductions.
9
+ * Two places reach past what pond expresses directly today:
10
+ * - Scalar / plain-array results are computed over the raw typed arrays rather
11
+ * than attached back as derived columns — a deliberate choice (no row
12
+ * rebuild). Where a derived column IS wanted, pond's `withColumn` covers it.
13
+ * - Distance-axis aggregation: value-axis histograms (zones / distribution)
14
+ * are pond-native via `byColumn`, but per-interval *splits* (many metrics
15
+ * per distance bucket) and contiguous-run segmentation still walk the arrays
16
+ * — candidate pond primitives (a multi-metric value-axis aggregate; a
17
+ * `scan` / run-length family). See `splitsByDistance` / `segmentsInRange`.
18
+ */
19
+ import { TimeSeries } from 'pond-ts';
20
+ /**
21
+ * The canonical activity schema: time key + lat/lng, with every other per-sample
22
+ * channel optional (present iff the source recorded it). This is the single
23
+ * source the compute layer reads — the "consume the series directly" seam:
24
+ * power/cadence/temp live IN the series alongside ele/hr, not as orphaned
25
+ * parallel arrays.
26
+ */
27
+ export declare const TRACK_SCHEMA: readonly [{
28
+ readonly name: "time";
29
+ readonly kind: "time";
30
+ }, {
31
+ readonly name: "lat";
32
+ readonly kind: "number";
33
+ readonly required: false;
34
+ }, {
35
+ readonly name: "lng";
36
+ readonly kind: "number";
37
+ readonly required: false;
38
+ }, {
39
+ readonly name: "ele";
40
+ readonly kind: "number";
41
+ readonly required: false;
42
+ }, {
43
+ readonly name: "hr";
44
+ readonly kind: "number";
45
+ readonly required: false;
46
+ }, {
47
+ readonly name: "power";
48
+ readonly kind: "number";
49
+ readonly required: false;
50
+ }, {
51
+ readonly name: "cadence";
52
+ readonly kind: "number";
53
+ readonly required: false;
54
+ }, {
55
+ readonly name: "temp";
56
+ readonly kind: "number";
57
+ readonly required: false;
58
+ }, {
59
+ readonly name: "distance";
60
+ readonly kind: "number";
61
+ readonly required: false;
62
+ }];
63
+ export type TrackSeries = TimeSeries<typeof TRACK_SCHEMA>;
64
+ /**
65
+ * One sample as a tuple matching {@link TRACK_SCHEMA}. Every channel is optional
66
+ * — including lat/lng, absent for a GPS-less indoor activity (the series is then
67
+ * just time + the recorded channels). Absent values are `undefined`: the
68
+ * constructor accepts `undefined` for `required: false` columns and records it in
69
+ * the validity bitmap (lossless); as of pond 0.29 `RowForSchema` honors
70
+ * `required: false` so the row type admits it too. It rejects `null` and `NaN`.
71
+ */
72
+ export type TrackPoint = [
73
+ timeMs: number,
74
+ lat: number | undefined,
75
+ lng: number | undefined,
76
+ ele: number | undefined,
77
+ hr: number | undefined,
78
+ power: number | undefined,
79
+ cadence: number | undefined,
80
+ temp: number | undefined,
81
+ distance: number | undefined
82
+ ];
83
+ /** Build a pond track from parallel sample tuples (already time-sorted). */
84
+ export declare function buildTrack(name: string, points: ReadonlyArray<TrackPoint>): TrackSeries;
85
+ /**
86
+ * The raw typed-array columns of an activity — the zero-copy read path the whole
87
+ * compute layer reads from. `lat`/`lng` are present only for a GPS activity; the
88
+ * other channels are present iff the source recorded them (missing cells are
89
+ * NaN). Optional channels are absent from the object when the source had none.
90
+ */
91
+ export interface TrackColumns {
92
+ /** Positions — present (length n) for a GPS activity, EMPTY for a GPS-less one
93
+ * (the `cols.lat.length` hasTrack signal). */
94
+ lat: Float64Array;
95
+ lng: Float64Array;
96
+ ele: Float64Array;
97
+ /** Absolute sample times in seconds (key column, ms → s). */
98
+ timeSec: Float64Array;
99
+ hr?: Float64Array;
100
+ power?: Float64Array;
101
+ cadence?: Float64Array;
102
+ temp?: Float64Array;
103
+ /** Device-recorded cumulative distance (m); present iff the source carried it
104
+ * (a GPS-less ride's distance axis). */
105
+ distance?: Float64Array;
106
+ }
107
+ /**
108
+ * Read a track's columns as `Float64Array`s. lat/lng off the packed value
109
+ * columns (required — never missing); the time axis off the key column via
110
+ * `keyColumn().begin` (a `Float64Array` of ms-since-epoch). Optional channels
111
+ * are read validity-aware and included only when the column carried any value.
112
+ */
113
+ export declare function readColumns(track: TrackSeries): TrackColumns;
114
+ /** Great-circle distance between two WGS84 points, in metres. */
115
+ export declare function haversineMeters(lat1: number, lng1: number, lat2: number, lng2: number): number;
116
+ /** Per-step distances (step[0] = 0), in metres. */
117
+ export declare function stepDistances(lat: Float64Array, lng: Float64Array): Float64Array;
118
+ /** Running total of a per-step series (cumulative[0] = step[0]). */
119
+ export declare function cumulative(step: Float64Array): Float64Array;
120
+ /** Total track distance, in metres. */
121
+ export declare function totalDistanceMeters(lat: Float64Array, lng: Float64Array): number;
122
+ /**
123
+ * Cumulative elevation gain/loss with a hysteresis threshold to reject the
124
+ * metre-scale jitter in barometric/GPS elevation. We only commit a move once it
125
+ * exceeds `thresholdMeters` from the last committed reference — the standard way
126
+ * to keep noise from inflating "gain" (raw positive-diff sums overcount wildly).
127
+ * NaN samples (missing ele) are skipped.
128
+ */
129
+ export declare function elevationGainLoss(ele: Float64Array, thresholdMeters?: number): {
130
+ gainMeters: number;
131
+ lossMeters: number;
132
+ };
133
+ /**
134
+ * Moving time: sum of inter-sample intervals where instantaneous speed is at or
135
+ * above `speedThresholdMps` (default 0.5 m/s ≈ 1.8 km/h — below this you're
136
+ * stopped/drifting). This is what separates "moving time" from "elapsed time".
137
+ */
138
+ export declare function movingTimeSeconds(step: Float64Array, timeSec: Float64Array, speedThresholdMps?: number): number;
139
+ /** Bounding box as `[[minLat, minLng], [maxLat, maxLng]]`. */
140
+ export declare function boundsOf(lat: Float64Array, lng: Float64Array): [[number, number], [number, number]];
141
+ /** A contiguous window of an activity, in cumulative distance (metres). */
142
+ export interface Segment {
143
+ startMeters: number;
144
+ endMeters: number;
145
+ }
146
+ /** Cumulative distance (m) at each polyline vertex; `[0] = 0`. */
147
+ export declare function polylineCumulative(polyline: ReadonlyArray<[number, number]>): number[];
148
+ /** The `[lat,lng]` point at cumulative distance `meters` along the polyline,
149
+ * linearly interpolated between the bracketing vertices. Clamps to the ends;
150
+ * non-finite `meters` resolves to the start; null for an empty polyline.
151
+ * `cum` may be passed to avoid recomputation — it MUST correspond to
152
+ * `polyline` (same length/order); a mismatch yields silently wrong results. */
153
+ export declare function interpolateAtDistance(polyline: ReadonlyArray<[number, number]>, meters: number, cum?: number[]): [number, number] | null;
154
+ /**
155
+ * The sub-polyline covering `[startMeters, endMeters]` of the route, with the
156
+ * endpoints interpolated to the exact distances (so a highlight begins and ends
157
+ * where the segment does, not at the nearest vertex) plus every vertex strictly
158
+ * between. Range is clamped to the track and normalized (start ≤ end). Returns
159
+ * the two interpolated endpoints for a zero-length range, `[]` for an empty
160
+ * polyline.
161
+ *
162
+ * `opts.domainTotal` is the length of the ruler `start`/`end` are measured in
163
+ * when that differs from this polyline's own length — laps come in FIT
164
+ * odometer metres and splits in raw-track haversine metres, but the polyline is
165
+ * Douglas–Peucker-simplified and a fraction shorter, so feeding those metres in
166
+ * raw would drift the highlight (≈1 km late on a 180 km ride). Given
167
+ * `domainTotal`, the window is rescaled proportionally onto the polyline so the
168
+ * same FRACTION of the route is sliced. `opts.cum`, if passed, MUST correspond
169
+ * to `polyline`.
170
+ */
171
+ export declare function polylineSlice(polyline: ReadonlyArray<[number, number]>, startMeters: number, endMeters: number, opts?: {
172
+ domainTotal?: number;
173
+ cum?: number[];
174
+ }): Array<[number, number]>;
175
+ /**
176
+ * Bounding box via pond's column min/max reductions — the pond-native path,
177
+ * over the public column API.
178
+ */
179
+ export declare function boundsViaPond(track: TrackSeries): [[number, number], [number, number]];
180
+ /**
181
+ * Douglas–Peucker polyline simplification with a metre tolerance, for the map
182
+ * overview line. Returns the kept `[lat, lng]` points (endpoints always kept).
183
+ * Perpendicular distance is approximated in a local equirectangular projection
184
+ * (fine at track scale; we are not a projection engine — RFC §6 non-goals).
185
+ */
186
+ export declare function simplify(points: ReadonlyArray<[number, number]>, toleranceMeters: number): Array<[number, number]>;
187
+ /** A per-interval split (the per-km/per-mile row). */
188
+ export interface Split {
189
+ /** 1-based split index. */
190
+ index: number;
191
+ /** Distance covered in this split, metres (the last split may be short). */
192
+ distanceMeters: number;
193
+ /** Elapsed seconds within the split. */
194
+ durationSeconds: number;
195
+ /** Elevation gain within the split, metres. */
196
+ elevationGainMeters: number;
197
+ /** Elevation loss within the split, metres (same ±3 m hysteresis as gain,
198
+ * reference carried across split boundaries). */
199
+ elevationLossMeters: number;
200
+ /** Normalized power over the split, watts — present only with a power meter
201
+ * (caller passes the 30 s-rolled power; see `splitsByDistance`). */
202
+ normalizedWatts?: number | undefined;
203
+ /** Per-split channel aggregates (mean of finite samples; max of finite
204
+ * samples) — present only when the matching stream is supplied to
205
+ * `splitsByDistance` via `extras`. Speed is m/s. */
206
+ avgHeartrate?: number | undefined;
207
+ maxHeartrate?: number | undefined;
208
+ avgWatts?: number | undefined;
209
+ maxWatts?: number | undefined;
210
+ avgCadence?: number | undefined;
211
+ avgSpeedMps?: number | undefined;
212
+ maxSpeedMps?: number | undefined;
213
+ }
214
+ /** Optional per-sample channels for `splitsByDistance` to aggregate per split
215
+ * (avg = mean of finite samples, max = max finite). All aligned to `step`. */
216
+ export interface SplitExtras {
217
+ heartrate?: Float64Array | undefined;
218
+ watts?: Float64Array | undefined;
219
+ cadence?: Float64Array | undefined;
220
+ /** Derived instantaneous speed (m/s); index 0 is typically NaN. */
221
+ speed?: Float64Array | undefined;
222
+ }
223
+ /**
224
+ * Per-interval splits over the DISTANCE axis (per-km, per-mile). A split is a
225
+ * bucket over *cumulative distance* (a derived, monotonic, non-key column) that
226
+ * carries MANY metrics per bucket — time, elevation gain/loss, NP, and avg/max
227
+ * of arbitrary extra channels. `byColumn` bins a single reducer over a value
228
+ * column; this multi-metric-per-bucket shape isn't a single `byColumn` call, so
229
+ * it walks the arrays here. A multi-metric value-axis aggregate is a candidate
230
+ * pond primitive.
231
+ */
232
+ export declare function splitsByDistance(step: Float64Array, timeSec: Float64Array, ele: Float64Array, intervalMeters?: number,
233
+ /** Optional 30 s-rolled power (NP smoothing) aligned to the samples; when
234
+ * given, each split gets `normalizedWatts = (mean of rolled^4)^¼`. */
235
+ npRolled?: Float64Array,
236
+ /** Optional raw channels to aggregate per split (avg + max). See {@link SplitExtras}. */
237
+ extras?: SplitExtras): Split[];
238
+ /** A point on the elevation-vs-distance profile. */
239
+ export interface ProfilePoint {
240
+ distanceMeters: number;
241
+ elevationMeters: number;
242
+ }
243
+ /**
244
+ * The elevation-vs-distance profile, resampled onto an even distance grid
245
+ * (distance-domain, not time). We average elevation within each distance bucket.
246
+ * Used for the profile chart and as the downsample for drawing.
247
+ */
248
+ export declare function elevationProfile(cumDist: Float64Array, ele: Float64Array, bucketMeters?: number): ProfilePoint[];
249
+ /** A `(distance, value)` sample on a distance-domain channel profile. */
250
+ export interface ProfileSample {
251
+ distanceMeters: number;
252
+ /** Median of the raw samples in the bucket — robust to anomalies. */
253
+ value: number;
254
+ /** Outer band edge (low) — a low percentile, not the raw min. NaN if empty. */
255
+ bandLo: number;
256
+ /** Outer band edge (high) — a high percentile, not the raw max. */
257
+ bandHi: number;
258
+ /** Inner band edge (low) — the 25th percentile (the typical-range floor). */
259
+ innerLo: number;
260
+ /** Inner band edge (high) — the 75th percentile (the typical-range ceiling). */
261
+ innerHi: number;
262
+ }
263
+ /**
264
+ * Resample ANY per-sample channel (elevation, hr, power, cadence, speed, …)
265
+ * onto an even distance grid — the generalization of {@link elevationProfile}
266
+ * to any value column. Per bucket: `value` is the **median** (robust), and
267
+ * `bandLo`/`bandHi` are the central-90% **percentiles** (not raw min/max), so
268
+ * a single anomalous sample can't set the band or the chart scale. `NaN`
269
+ * (missing) samples are skipped; the last bucket carries forward so the line
270
+ * stays continuous. Distance-domain bucketing of a value array — like the
271
+ * `byColumn` histograms, but emitting a median + percentile band per bucket.
272
+ */
273
+ export declare function profileByDistance(cumDist: Float64Array, values: Float64Array, bucketMeters?: number): ProfileSample[];
274
+ /**
275
+ * Like {@link profileByDistance} but buckets ONLY the samples whose cumulative
276
+ * distance falls in `[startMeters, endMeters]`, at `bucketMeters` resolution.
277
+ * Returned `distanceMeters` are absolute (offset by `startMeters`), so the
278
+ * output drops straight into the same distance axis as the full-activity
279
+ * profile. This is what lets the chart reveal fine detail when zoomed to a
280
+ * locked split/lap — the bucket can be far finer than the whole-activity grid
281
+ * (e.g. ~10 m vs 100 m) without paying to re-bucket the entire track. Buckets
282
+ * here are aligned to `startMeters`, an independent grid from profileByDistance.
283
+ */
284
+ export declare function profileByDistanceWindow(cumDist: Float64Array, values: Float64Array, startMeters: number, endMeters: number, bucketMeters?: number): ProfileSample[];
285
+ /** Percentile spread of the raw samples around a point — the variance band. */
286
+ export interface Spread {
287
+ /** Outer envelope (p5..p95) — the full excursions. NaN if no samples. */
288
+ bandLo: number;
289
+ bandHi: number;
290
+ /** Inner band (p25..p75) — the dense typical range. */
291
+ innerLo: number;
292
+ innerHi: number;
293
+ }
294
+ /**
295
+ * Rolling percentile spread of a raw per-sample channel, evaluated at each
296
+ * `distance` over a FIXED ±`radiusMeters` window of the raw samples — NOT the
297
+ * chart's buckets. This is what makes the variance underlay zoom-stable: the
298
+ * band measures the same real span of churn whether the chart is bucketed at
299
+ * 100 m (whole ride) or 10 m (a locked split), so it blooms where effort was
300
+ * genuinely punchy and pinches where steady, identically at every zoom. The
301
+ * within-bucket percentiles in {@link profileByDistance} can't do this — their
302
+ * width scales with bucket duration.
303
+ *
304
+ * pond 0.30's `rollingByColumn('dist', { radius, at: distances }, …)` owns the
305
+ * whole thing: the raw samples are the rows, and `at` evaluates the ±radius
306
+ * window + the four percentiles at each chart-grid center directly (one record
307
+ * per center) — no interleave, no read-back bookkeeping. `cum` and `distances`
308
+ * must both ascend (rollingByColumn enforces it). The `{ at }` option (added in
309
+ * pond 0.30) evaluates the window at each grid center directly.
310
+ */
311
+ export declare function rollingSpread(cumDist: Float64Array, values: Float64Array, distances: number[], radiusMeters: number): Spread[];
312
+ /** Canonical distances (metres) for a run's best-efforts table: 400 m, ½ mi,
313
+ * 1 K, 1 mi, 2 mi, 5 K, 10 K, half, full. */
314
+ export declare const BEST_EFFORT_DISTANCES: number[];
315
+ /** A fastest-over-distance effort: the quickest time to cover `meters`. */
316
+ export interface DistanceEffort {
317
+ meters: number;
318
+ /** Fastest time over any window covering ≥ `meters`, seconds. */
319
+ seconds: number;
320
+ /** Mean HR over that fastest window, when an `hr` channel is supplied. */
321
+ avgHeartrate?: number;
322
+ /** Inclusive sample range of the fastest window — for focusing the chart/map
323
+ * on where the effort happened (maps to distance via `cumDist`). */
324
+ startIndex?: number;
325
+ endIndex?: number;
326
+ }
327
+ /**
328
+ * Best efforts over distance: for each target distance, the fastest time over
329
+ * any window covering at least that distance. The distance-axis analogue of the
330
+ * power curve — a two-pointer over cumulative distance keeps the window just ≥
331
+ * the target while minimising elapsed time, O(n) per distance. Times are clamped
332
+ * non-decreasing across the (ascending) distance list. Walks the arrays: it's a
333
+ * fastest-window search over a derived monotonic axis swept across many target
334
+ * distances — beyond pond's single-window `rolling` (a multi-window sweep is a
335
+ * candidate pond primitive). `hr` (optional) yields the mean heart rate over each
336
+ * fastest window.
337
+ * Precondition: `distances` must be ascending — the `> total` early-exit and the
338
+ * non-decreasing-time clamp both rely on it (`BEST_EFFORT_DISTANCES` is).
339
+ */
340
+ export declare function bestEffortsByDistance(cumDist: Float64Array, timeSec: Float64Array, distances?: number[], hr?: Float64Array): DistanceEffort[];
341
+ /**
342
+ * The contiguous track pieces where a per-sample channel falls within
343
+ * `[lo, hi]` (inclusive) — the selection driver behind zone / power-distribution
344
+ * highlighting. A value predicate over the series yields the scattered stretches
345
+ * of the ride that match (e.g. "every part in the Tempo HR band"). Non-finite
346
+ * samples are out of range (they break a run). `cumDist` and `values` are
347
+ * sample-aligned. Adjacent runs separated by ≤ `bridgeMeters` of out-of-range
348
+ * distance merge into one (default 0 = faithful, no merge) so a momentary
349
+ * excursion doesn't shatter the selection into hundreds of slivers. Each run
350
+ * spans [cum at its first in-range sample, cum at its last]; a lone sample is a
351
+ * zero-length segment at its point.
352
+ *
353
+ * Run-length encoding of a predicate over a value column. pond has no
354
+ * contiguous-run / RLE-by-predicate primitive yet — a `scan` / segmentation
355
+ * family is a candidate pond primitive; until then this walks the arrays.
356
+ */
357
+ export declare function segmentsInRange(cumDist: Float64Array, values: ArrayLike<number>, lo: number, hi: number, bridgeMeters?: number): Segment[];
358
+ //# sourceMappingURL=index.d.ts.map